Device migrations, drivers and fixes (#571)
Device migrations: - cyd-4848S040c - guition-jc1060p470ciwy - guition-jc2432w328c - guition-jc3248w535c (known issue with display and touch, Shadowtrance will look into it) - guition-jc8048w550c - heltec-wifi-lora-32-v3 - lilygo-tdeck-max (excluding graphics) - lilygo-tdisplay - lilygo-tdisplay-s3 - lilygo-tdongle-s3 - lilygo-tlora-pager Driver migrations: - Implemented haptic driver interface in kernel - AXS15231b - BQ25896 - BQ27220 - CST328 - CST6xx - DRV2605 - JD9165 - Custom LilyGO driver for T-Lora Pager - SSD1306 - ST7701 - ST7735 - SY6970 - TCA8418 Fixes/improvements: - Boot app: support for multiple power devices, improved UI - lvgl_devices and related code: fixes for mapping - Support for arrays in dts parser
This commit is contained in:
committed by
GitHub
parent
3b5a401594
commit
d896657bf9
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
# BQ25896 Power Management IC
|
||||
|
||||
[Product page](https://www.ti.com/product/BQ25896)
|
||||
[Data sheet](https://www.ti.com/lit/gpn/bq25896)
|
||||
[Refence implementation](https://github.com/lewisxhe/XPowersLib/blob/73b92a6641b72c0aca2be989207689cb05da9788/src/PowersBQ25896.tpp)
|
||||
@@ -1,15 +0,0 @@
|
||||
#include "Bq25896.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "BQ25896";
|
||||
|
||||
void Bq25896::powerOff() {
|
||||
LOG_I(TAG, "Power off");
|
||||
bitOn(0x09, BIT(5));
|
||||
}
|
||||
|
||||
void Bq25896::powerOn() {
|
||||
LOG_I(TAG, "Power on");
|
||||
bitOff(0x09, BIT(5));
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
constexpr auto BQ25896_ADDRESS = 0x6b;
|
||||
|
||||
class Bq25896 final : public tt::hal::i2c::I2cDevice {
|
||||
|
||||
public:
|
||||
|
||||
explicit Bq25896(::Device* controller) : I2cDevice(controller, BQ25896_ADDRESS) {
|
||||
powerOn();
|
||||
}
|
||||
|
||||
std::string getName() const override { return "BQ25896"; }
|
||||
|
||||
std::string getDescription() const override { return "I2C 1 cell 3A buck battery charger with power path and PSEL"; }
|
||||
|
||||
void powerOff();
|
||||
|
||||
void powerOn();
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
# BQ27220
|
||||
|
||||
Power management: Single-Cell CEDV Fuel Gauge
|
||||
|
||||
[Datasheet](https://www.ti.com/lit/gpn/bq27220)
|
||||
[User Guide](https://www.ti.com/lit/pdf/sluubd4)
|
||||
@@ -1,370 +0,0 @@
|
||||
#include "Bq27220.h"
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include "esp_sleep.h"
|
||||
|
||||
constexpr auto* TAG = "BQ27220";
|
||||
|
||||
#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a)))
|
||||
|
||||
static uint8_t highByte(const uint16_t word) { return (word >> 8) & 0xFF; }
|
||||
static uint8_t lowByte(const uint16_t word) { return word & 0xFF; }
|
||||
static constexpr void swapEndianess(uint16_t &word) { word = (lowByte(word) << 8) | highByte(word); }
|
||||
|
||||
namespace registers {
|
||||
static const uint16_t SUBCMD_CTRL_STATUS = 0x0000U;
|
||||
static const uint16_t SUBCMD_DEVICE_NUMBER = 0x0001U;
|
||||
static const uint16_t SUBCMD_FW_VERSION = 0x0002U;
|
||||
static const uint16_t SUBCMD_HW_VERSION = 0x0003U;
|
||||
static const uint16_t SUBCMD_BOARD_OFFSET = 0x0009U;
|
||||
static const uint16_t SUBCMD_CC_OFFSET = 0x000AU;
|
||||
static const uint16_t SUBCMD_CC_OFFSET_SAVE = 0x000BU;
|
||||
static const uint16_t SUBCMD_OCV_CMD = 0x000CU;
|
||||
static const uint16_t SUBCMD_BAT_INSERT = 0x000DU;
|
||||
static const uint16_t SUBCMD_BAT_REMOVE = 0x000EU;
|
||||
static const uint16_t SUBCMD_SET_SNOOZE = 0x0013U;
|
||||
static const uint16_t SUBCMD_CLEAR_SNOOZE = 0x0014U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_1 = 0x0015U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_2 = 0x0016U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_3 = 0x0017U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_4 = 0x0018U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_5 = 0x0019U;
|
||||
static const uint16_t SUBCMD_SET_PROFILE_6 = 0x001AU;
|
||||
static const uint16_t SUBCMD_CAL_TOGGLE = 0x002DU;
|
||||
static const uint16_t SUBCMD_SEALED = 0x0030U;
|
||||
static const uint16_t SUBCMD_RESET = 0x0041U;
|
||||
static const uint16_t SUBCMD_EXIT_CAL = 0x0080U;
|
||||
static const uint16_t SUBCMD_ENTER_CAL = 0x0081U;
|
||||
static const uint16_t SUBCMD_ENTER_CFG_UPDATE = 0x0090U;
|
||||
static const uint16_t SUBCMD_EXIT_CFG_UPDATE_REINIT = 0x0091U;
|
||||
static const uint16_t SUBCMD_EXIT_CFG_UPDATE = 0x0092U;
|
||||
static const uint16_t SUBCMD_RETURN_TO_ROM = 0x0F00U;
|
||||
|
||||
static const uint8_t CMD_CONTROL = 0x00U;
|
||||
static const uint8_t CMD_AT_RATE = 0x02U;
|
||||
static const uint8_t CMD_AT_RATE_TIME_TO_EMPTY = 0x04U;
|
||||
static const uint8_t CMD_TEMPERATURE = 0x06U;
|
||||
static const uint8_t CMD_VOLTAGE = 0x08U;
|
||||
static const uint8_t CMD_BATTERY_STATUS = 0x0AU;
|
||||
static const uint8_t CMD_CURRENT = 0x0CU;
|
||||
static const uint8_t CMD_REMAINING_CAPACITY = 0x10U;
|
||||
static const uint8_t CMD_FULL_CHARGE_CAPACITY = 0x12U;
|
||||
static const uint8_t CMD_AVG_CURRENT = 0x14U;
|
||||
static const uint8_t CMD_TIME_TO_EMPTY = 0x16U;
|
||||
static const uint8_t CMD_TIME_TO_FULL = 0x18U;
|
||||
static const uint8_t CMD_STANDBY_CURRENT = 0x1AU;
|
||||
static const uint8_t CMD_STANDBY_TIME_TO_EMPTY = 0x1CU;
|
||||
static const uint8_t CMD_MAX_LOAD_CURRENT = 0x1EU;
|
||||
static const uint8_t CMD_MAX_LOAD_TIME_TO_EMPTY = 0x20U;
|
||||
static const uint8_t CMD_RAW_COULOMB_COUNT = 0x22U;
|
||||
static const uint8_t CMD_AVG_POWER = 0x24U;
|
||||
static const uint8_t CMD_INTERNAL_TEMPERATURE = 0x28U;
|
||||
static const uint8_t CMD_CYCLE_COUNT = 0x2AU;
|
||||
static const uint8_t CMD_STATE_OF_CHARGE = 0x2CU;
|
||||
static const uint8_t CMD_STATE_OF_HEALTH = 0x2EU;
|
||||
static const uint8_t CMD_CHARGE_VOLTAGE = 0x30U;
|
||||
static const uint8_t CMD_CHARGE_CURRENT = 0x32U;
|
||||
static const uint8_t CMD_BTP_DISCHARGE_SET = 0x34U;
|
||||
static const uint8_t CMD_BTP_CHARGE_SET = 0x36U;
|
||||
static const uint8_t CMD_OPERATION_STATUS = 0x3AU;
|
||||
static const uint8_t CMD_DESIGN_CAPACITY = 0x3CU;
|
||||
static const uint8_t CMD_SELECT_SUBCLASS = 0x3EU;
|
||||
static const uint8_t CMD_MAC_DATA = 0x40U;
|
||||
static const uint8_t CMD_MAC_DATA_SUM = 0x60U;
|
||||
static const uint8_t CMD_MAC_DATA_LEN = 0x61U;
|
||||
static const uint8_t CMD_ANALOG_COUNT = 0x79U;
|
||||
static const uint8_t CMD_RAW_CURRENT = 0x7AU;
|
||||
static const uint8_t CMD_RAW_VOLTAGE = 0x7CU;
|
||||
static const uint8_t CMD_RAW_INTERNAL_TEMPERATURE = 0x7EU;
|
||||
static const uint8_t MAC_BUFFER_START = 0x40U;
|
||||
static const uint8_t MAC_BUFFER_END = 0x5FU;
|
||||
static const uint8_t MAC_DATA_SUM = 0x60U;
|
||||
static const uint8_t MAC_DATA_LEN = 0x61U;
|
||||
static const uint8_t ROM_START = 0x3EU;
|
||||
|
||||
static const uint16_t ROM_FULL_CHARGE_CAPACITY = 0x929DU;
|
||||
static const uint16_t ROM_DESIGN_CAPACITY = 0x929FU;
|
||||
static const uint16_t ROM_OPERATION_CONFIG_A = 0x9206U;
|
||||
static const uint16_t ROM_OPERATION_CONFIG_B = 0x9208U;
|
||||
|
||||
} // namespace registers
|
||||
|
||||
bool Bq27220::configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity) {
|
||||
return performConfigUpdate([this, designCapacity, fullChargeCapacity]() {
|
||||
// Set the design capacity
|
||||
if (!writeConfig16(registers::ROM_DESIGN_CAPACITY, designCapacity)) {
|
||||
LOG_E(TAG, "Failed to set design capacity!");
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
// Set full charge capacity
|
||||
if (!writeConfig16(registers::ROM_FULL_CHARGE_CAPACITY, fullChargeCapacity)) {
|
||||
LOG_E(TAG, "Failed to set full charge capacity!");
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool Bq27220::getVoltage(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_VOLTAGE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getCurrent(int16_t &value) {
|
||||
uint16_t u16 = 0;
|
||||
if (readRegister16(registers::CMD_CURRENT, u16)) {
|
||||
swapEndianess(u16);
|
||||
value = (int16_t)u16;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getBatteryStatus(BatteryStatus &batt_sta) {
|
||||
if (readRegister16(registers::CMD_BATTERY_STATUS, batt_sta.full)) {
|
||||
swapEndianess(batt_sta.full);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getOperationStatus(OperationStatus &oper_sta) {
|
||||
if (readRegister16(registers::CMD_OPERATION_STATUS, oper_sta.full)) {
|
||||
swapEndianess(oper_sta.full);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getTemperature(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_TEMPERATURE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getFullChargeCapacity(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_FULL_CHARGE_CAPACITY, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getDesignCapacity(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_DESIGN_CAPACITY, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getRemainingCapacity(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_REMAINING_CAPACITY, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getStateOfCharge(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_STATE_OF_CHARGE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getStateOfHealth(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_STATE_OF_HEALTH, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::getChargeVoltageMax(uint16_t &value) {
|
||||
if (readRegister16(registers::CMD_CHARGE_VOLTAGE, value)) {
|
||||
swapEndianess(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::unsealDevice() {
|
||||
uint8_t cmd1[] = {0x00, 0x14, 0x04};
|
||||
if (!write(cmd1, ARRAYSIZE(cmd1))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
uint8_t cmd2[] = {0x00, 0x72, 0x36};
|
||||
if (!write(cmd2, ARRAYSIZE(cmd2))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::unsealFullAccess()
|
||||
{
|
||||
uint8_t buffer[3];
|
||||
buffer[0] = 0x00;
|
||||
buffer[1] = lowByte((accessKey >> 24));
|
||||
buffer[2] = lowByte((accessKey >> 16));
|
||||
if (!write(buffer, ARRAYSIZE(buffer))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
buffer[1] = lowByte((accessKey >> 8));
|
||||
buffer[2] = lowByte((accessKey));
|
||||
if (!write(buffer, ARRAYSIZE(buffer))) {
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::exitSealMode() {
|
||||
return sendSubCommand(registers::SUBCMD_SEALED);
|
||||
}
|
||||
|
||||
bool Bq27220::sendSubCommand(uint16_t subCmd, bool waitConfirm)
|
||||
{
|
||||
uint8_t buffer[3];
|
||||
buffer[0] = 0x00;
|
||||
buffer[1] = lowByte(subCmd);
|
||||
buffer[2] = highByte(subCmd);
|
||||
if (!write(buffer, ARRAYSIZE(buffer))) {
|
||||
return false;
|
||||
}
|
||||
if (!waitConfirm) {
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
return true;
|
||||
}
|
||||
constexpr uint8_t statusReg = 0x00;
|
||||
int waitCount = 20;
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
while (waitCount--) {
|
||||
writeRead(&statusReg, 1, buffer, 2);
|
||||
uint16_t *value = reinterpret_cast<uint16_t *>(buffer);
|
||||
if (*value == 0xFFA5) {
|
||||
return true;
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
LOG_E(TAG, "Subcommand 0x%04X failed!", subCmd);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Bq27220::writeConfig16(uint16_t address, uint16_t value) {
|
||||
constexpr uint8_t fixedDataLength = 0x06;
|
||||
const uint8_t msbAccessValue = highByte(address);
|
||||
const uint8_t lsbAccessValue = lowByte(address);
|
||||
|
||||
// Write to access the MSB of Capacity
|
||||
writeRegister8(registers::ROM_START, msbAccessValue);
|
||||
|
||||
// Write to access the LSB of Capacity
|
||||
writeRegister8(registers::ROM_START + 1, lsbAccessValue);
|
||||
|
||||
// Write two Capacity bytes starting from 0x40
|
||||
uint8_t valueMsb = highByte(value);
|
||||
uint8_t valueLsb = lowByte(value);
|
||||
uint8_t configRaw[] = {valueMsb, valueLsb};
|
||||
writeRegister(registers::MAC_BUFFER_START, configRaw, 2);
|
||||
// Calculate new checksum
|
||||
uint8_t checksum = 0xFF - ((msbAccessValue + lsbAccessValue + valueMsb + valueLsb) & 0xFF);
|
||||
|
||||
// Write new checksum (0x60)
|
||||
writeRegister8(registers::MAC_DATA_SUM, checksum);
|
||||
|
||||
// Write the block length
|
||||
writeRegister8(registers::MAC_DATA_LEN, fixedDataLength);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::configPreamble(bool &isSealed) {
|
||||
int timeout = 0;
|
||||
OperationStatus status;
|
||||
|
||||
// Check access settings
|
||||
if(!getOperationStatus(status)) {
|
||||
LOG_E(TAG, "Cannot read initial operation status!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.reg.SEC == OperationStatusSecSealed) {
|
||||
isSealed = true;
|
||||
if (!unsealDevice()) {
|
||||
LOG_E(TAG, "Unsealing device failure!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status.reg.SEC != OperationStatusSecFull) {
|
||||
if (!unsealFullAccess()) {
|
||||
LOG_E(TAG, "Unsealing full access failure!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Send ENTER_CFG_UPDATE command (0x0090)
|
||||
if (!sendSubCommand(registers::SUBCMD_ENTER_CFG_UPDATE)) {
|
||||
LOG_E(TAG, "Config Update Subcommand failure!");
|
||||
}
|
||||
|
||||
// Confirm CFUPDATE mode by polling the OperationStatus() register until Bit 2 is set.
|
||||
bool isConfigUpdate = false;
|
||||
for (timeout = 30; timeout; --timeout) {
|
||||
getOperationStatus(status);
|
||||
if (status.reg.CFGUPDATE) {
|
||||
isConfigUpdate = true;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
if (!isConfigUpdate) {
|
||||
LOG_E(TAG, "Update Mode timeout, maybe the access key for full permissions is invalid!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bq27220::configEpilouge(const bool isSealed) {
|
||||
int timeout = 0;
|
||||
OperationStatus status;
|
||||
|
||||
// Exit CFUPDATE mode by sending the EXIT_CFG_UPDATE_REINIT (0x0091) or EXIT_CFG_UPDATE (0x0092) command
|
||||
sendSubCommand(registers::SUBCMD_EXIT_CFG_UPDATE_REINIT);
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
// Confirm that CFUPDATE mode has been exited by polling the OperationStatus() register until bit 2 is cleared
|
||||
for (timeout = 60; timeout; --timeout) {
|
||||
getOperationStatus(status);
|
||||
if (!status.reg.CFGUPDATE) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
if (timeout == 0) {
|
||||
LOG_E(TAG, "Timed out waiting to exit update mode.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the device was previously in SEALED state, return to SEALED mode by sending the Control(0x0030) subcommand
|
||||
if (isSealed) {
|
||||
LOG_D(TAG, "Restore Safe Mode!");
|
||||
exitSealMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
#define BQ27220_ADDRESS 0x55
|
||||
|
||||
class Bq27220 final : public tt::hal::i2c::I2cDevice {
|
||||
|
||||
uint32_t accessKey;
|
||||
|
||||
bool unsealDevice();
|
||||
bool unsealFullAccess();
|
||||
bool exitSealMode();
|
||||
bool sendSubCommand(uint16_t subCmd, bool waitConfirm = false);
|
||||
bool writeConfig16(uint16_t address, uint16_t value);
|
||||
bool configPreamble(bool &isSealed);
|
||||
bool configEpilouge(bool isSealed);
|
||||
|
||||
template<typename T>
|
||||
bool performConfigUpdate(T configUpdateFunc)
|
||||
{
|
||||
bool isSealed = false;
|
||||
|
||||
if (!configPreamble(isSealed)) {
|
||||
return false;
|
||||
}
|
||||
bool result = configUpdateFunc();
|
||||
configEpilouge(isSealed);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
// Register structures lifted from
|
||||
// https://github.com/Xinyuan-LilyGO/T-Deck-Pro/blob/master/lib/BQ27220/bq27220.h
|
||||
// Copyright (c) 2025 Liygo / Shenzhen Xinyuan Electronic Technology Co., Ltd
|
||||
|
||||
union BatteryStatus {
|
||||
struct
|
||||
{
|
||||
// Low byte, Low bit first
|
||||
uint16_t DSG : 1; /**< The device is in DISCHARGE */
|
||||
uint16_t SYSDWN : 1; /**< System down bit indicating the system should shut down */
|
||||
uint16_t TDA : 1; /**< Terminate Discharge Alarm */
|
||||
uint16_t BATTPRES : 1; /**< Battery Present detected */
|
||||
uint16_t AUTH_GD : 1; /**< Detect inserted battery */
|
||||
uint16_t OCVGD : 1; /**< Good OCV measurement taken */
|
||||
uint16_t TCA : 1; /**< Terminate Charge Alarm */
|
||||
uint16_t RSVD : 1; /**< Reserved */
|
||||
// High byte, Low bit first
|
||||
uint16_t CHGING : 1; /**< Charge inhibit */
|
||||
uint16_t FC : 1; /**< Full-charged is detected */
|
||||
uint16_t OTD : 1; /**< Overtemperature in discharge condition is detected */
|
||||
uint16_t OTC : 1; /**< Overtemperature in charge condition is detected */
|
||||
uint16_t SLEEP : 1; /**< Device is operating in SLEEP mode when set */
|
||||
uint16_t OCVFALL : 1; /**< Status bit indicating that the OCV reading failed due to current */
|
||||
uint16_t OCVCOMP : 1; /**< An OCV measurement update is complete */
|
||||
uint16_t FD : 1; /**< Full-discharge is detected */
|
||||
} reg;
|
||||
uint16_t full;
|
||||
};
|
||||
|
||||
enum OperationStatusSec {
|
||||
OperationStatusSecSealed = 0b11,
|
||||
OperationStatusSecUnsealed = 0b10,
|
||||
OperationStatusSecFull = 0b01,
|
||||
};
|
||||
|
||||
union OperationStatus {
|
||||
struct
|
||||
{
|
||||
// Low byte, Low bit first
|
||||
bool CALMD : 1; /**< Calibration mode enabled */
|
||||
uint8_t SEC : 2; /**< Current security access */
|
||||
bool EDV2 : 1; /**< EDV2 threshold exceeded */
|
||||
bool VDQ : 1; /**< Indicates if Current discharge cycle is NOT qualified or qualified for an FCC updated */
|
||||
bool INITCOMP : 1; /**< gauge initialization is complete */
|
||||
bool SMTH : 1; /**< RemainingCapacity is scaled by smooth engine */
|
||||
bool BTPINT : 1; /**< BTP threshold has been crossed */
|
||||
// High byte, Low bit first
|
||||
uint8_t RSVD1 : 2; /**< Reserved */
|
||||
bool CFGUPDATE : 1; /**< Gauge is in CONFIG UPDATE mode */
|
||||
uint8_t RSVD0 : 5; /**< Reserved */
|
||||
} reg;
|
||||
uint16_t full;
|
||||
};
|
||||
|
||||
std::string getName() const override { return "BQ27220"; }
|
||||
|
||||
std::string getDescription() const override { return "I2C-controlled CEDV battery fuel gauge"; }
|
||||
|
||||
explicit Bq27220(::Device* controller) : I2cDevice(controller, BQ27220_ADDRESS), accessKey(0xFFFFFFFF) {}
|
||||
|
||||
bool configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity);
|
||||
bool getVoltage(uint16_t &value);
|
||||
bool getCurrent(int16_t &value);
|
||||
bool getBatteryStatus(BatteryStatus &batt_sta);
|
||||
bool getOperationStatus(OperationStatus &oper_sta);
|
||||
bool getTemperature(uint16_t &value);
|
||||
bool getFullChargeCapacity(uint16_t &value);
|
||||
bool getDesignCapacity(uint16_t &value);
|
||||
bool getRemainingCapacity(uint16_t &value);
|
||||
bool getStateOfCharge(uint16_t &value);
|
||||
bool getStateOfHealth(uint16_t &value);
|
||||
bool getChargeVoltageMax(uint16_t &value);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
# DRV2605 Haptic Driver
|
||||
|
||||
[Datasheet](https://www.ti.com/product/DRV2605)
|
||||
[Reference implementation](https://github.com/lewisxhe/SensorLib/blob/master/src/SensorDRV2605.hpp)
|
||||
[Usage in T-Lora Pager code from LilyGO](https://github.com/Xinyuan-LilyGO/LilyGoLib/blob/6b534a28b0ec31313e4a7e89c5e8b7e4437e6fd1/src/LilyGo_LoRa_Pager.cpp#L956)
|
||||
@@ -1,96 +0,0 @@
|
||||
#include "Drv2605.h"
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "DRV2605";
|
||||
|
||||
Drv2605::Drv2605(::Device* controller, bool autoPlayStartupBuzz) : I2cDevice(controller, ADDRESS), autoPlayStartupBuzz(autoPlayStartupBuzz) {
|
||||
check(init(), "Initialize DRV2605");
|
||||
|
||||
if (autoPlayStartupBuzz) {
|
||||
setWaveFormForBuzz();
|
||||
startPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
bool Drv2605::init() {
|
||||
uint8_t status;
|
||||
if (!readRegister8(static_cast<uint8_t>(Register::Status), status)) {
|
||||
LOG_E(TAG, "Failed to read status");
|
||||
return false;
|
||||
}
|
||||
status >>= 5;
|
||||
|
||||
ChipId chip_id = static_cast<ChipId>(status);
|
||||
if (chip_id != ChipId::DRV2604 && chip_id != ChipId::DRV2604L && chip_id != ChipId::DRV2605 && chip_id != ChipId::DRV2605L) {
|
||||
LOG_E(TAG, "Unknown chip id %02X", static_cast<uint8_t>(chip_id));
|
||||
return false;
|
||||
}
|
||||
|
||||
writeRegister(Register::Mode, 0x00); // Get out of standby
|
||||
|
||||
writeRegister(Register::RealtimePlaybackInput, 0x00); // Disable
|
||||
|
||||
|
||||
setWaveFormForClick();
|
||||
|
||||
// ERM open loop
|
||||
|
||||
uint8_t feedback;
|
||||
if (!readRegister(Register::Feedback, feedback)) {
|
||||
LOG_E(TAG, "Failed to read feedback");
|
||||
return false;
|
||||
}
|
||||
|
||||
writeRegister(Register::Feedback, feedback & 0x7F); // N_ERM_LRA off
|
||||
|
||||
bitOnByIndex(static_cast<uint8_t>(Register::Control3), 5); // ERM_OPEN_LOOP on
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Drv2605::setWaveFormForBuzz() {
|
||||
writeRegister(Register::WaveSequence1, 1); // Strong click
|
||||
writeRegister(Register::WaveSequence2, 1); // Strong click
|
||||
writeRegister(Register::WaveSequence3, 1); // Strong click
|
||||
writeRegister(Register::WaveSequence4, 0); // End sequence
|
||||
|
||||
writeRegister(Register::OverdriveTimeOffset, 0); // No overdrive
|
||||
|
||||
writeRegister(Register::SustainTimeOffsetPostivie, 0);
|
||||
writeRegister(Register::SustainTimeOffsetNegative, 0);
|
||||
writeRegister(Register::BrakeTimeOffset, 0);
|
||||
|
||||
writeRegister(Register::AudioInputLevelMax, 0x64);
|
||||
}
|
||||
|
||||
void Drv2605::setWaveFormForClick() {
|
||||
writeRegister(Register::WaveSequence1, 1); // Strong click
|
||||
writeRegister(Register::WaveSequence2, 0); // End sequence
|
||||
|
||||
writeRegister(Register::OverdriveTimeOffset, 0); // No overdrive
|
||||
|
||||
writeRegister(Register::SustainTimeOffsetPostivie, 0);
|
||||
writeRegister(Register::SustainTimeOffsetNegative, 0);
|
||||
writeRegister(Register::BrakeTimeOffset, 0);
|
||||
|
||||
writeRegister(Register::AudioInputLevelMax, 0x64);
|
||||
}
|
||||
|
||||
void Drv2605::setWaveForm(uint8_t slot, uint8_t waveform) {
|
||||
writeRegister8(static_cast<uint8_t>(Register::WaveSequence1) + slot, waveform);
|
||||
}
|
||||
|
||||
void Drv2605::selectLibrary(uint8_t library) {
|
||||
writeRegister(Register::WaveLibrarySelect, library);
|
||||
}
|
||||
|
||||
void Drv2605::startPlayback() {
|
||||
writeRegister(Register::Go, 0x01);
|
||||
}
|
||||
|
||||
void Drv2605::stopPlayback() {
|
||||
writeRegister(Register::Go, 0x00);
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
class Drv2605 : public tt::hal::i2c::I2cDevice {
|
||||
|
||||
static constexpr auto ADDRESS = 0x5A;
|
||||
|
||||
bool autoPlayStartupBuzz;
|
||||
|
||||
// Chip IDs
|
||||
enum class ChipId {
|
||||
DRV2604 = 0x04, // Has RAM. Doesn't havew licensed ROM library.
|
||||
DRV2605 = 0x03, // Has licensed ROM library. Doesn't have RAM.
|
||||
DRV2604L = 0x06, // Low-voltage variant of the DRV2604.
|
||||
DRV2605L = 0x07 // Low-voltage variant of the DRV2605.
|
||||
};
|
||||
|
||||
enum class Register {
|
||||
Status = 0x00,
|
||||
Mode = 0x01,
|
||||
RealtimePlaybackInput = 0x02,
|
||||
WaveLibrarySelect = 0x03,
|
||||
WaveSequence1 = 0x04,
|
||||
WaveSequence2 = 0x05,
|
||||
WaveSequence3 = 0x06,
|
||||
WaveSequence4 = 0x07,
|
||||
WaveSequence5 = 0x08,
|
||||
WaveSequence6 = 0x09,
|
||||
WaveSequence7 = 0x0A,
|
||||
WaveSequence8 = 0x0B,
|
||||
Go = 0x0C,
|
||||
OverdriveTimeOffset = 0x0D,
|
||||
SustainTimeOffsetPostivie = 0x0E,
|
||||
SustainTimeOffsetNegative = 0x0F,
|
||||
BrakeTimeOffset = 0x10,
|
||||
AudioControl = 0x11,
|
||||
AudioInputLevelMin = 0x12,
|
||||
AudioInputLevelMax = 0x13,
|
||||
AudioOutputLevelMin = 0x14,
|
||||
AudioOutputLevelMax = 0x15,
|
||||
RatedVoltage = 0x16,
|
||||
OverdriveClampVoltage = 0x17,
|
||||
AutoCalibrationCompensation = 0x18,
|
||||
AutoCalibrationBackEmf = 0x19,
|
||||
Feedback = 0x1A,
|
||||
Control1 = 0x1B,
|
||||
Control2 = 0x1C,
|
||||
Control3 = 0x1D,
|
||||
Control4 = 0x1E,
|
||||
Vbat = 0x21,
|
||||
LraResonancePeriod = 0x22,
|
||||
};
|
||||
|
||||
bool writeRegister(Register reg, const uint8_t value) const {
|
||||
return writeRegister8(static_cast<uint8_t>(reg), value);
|
||||
}
|
||||
|
||||
bool readRegister(Register reg, uint8_t& value) const {
|
||||
return readRegister8(static_cast<uint8_t>(reg), value);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
explicit Drv2605(::Device* controller, bool autoPlayStartupBuzz = true);
|
||||
|
||||
std::string getName() const final { return "DRV2605"; }
|
||||
std::string getDescription() const final { return "Haptic driver for ERM/LRA with waveform library & auto-resonance tracking"; }
|
||||
|
||||
bool init();
|
||||
|
||||
void setWaveFormForBuzz();
|
||||
void setWaveFormForClick();
|
||||
|
||||
/**
|
||||
* @param slot a value from 0 to 7
|
||||
* @param waveform
|
||||
*/
|
||||
void setWaveForm(uint8_t slot, uint8_t waveform);
|
||||
void selectLibrary(uint8_t library);
|
||||
void startPlayback();
|
||||
void stopPlayback();
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility driver EspLcdCompat esp_lcd lvgl
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
# SSD1306
|
||||
|
||||
A very custom ESP32 LVGL driver for SSD1306 displays.
|
||||
@@ -1,216 +0,0 @@
|
||||
#include "Ssd1306Display.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
#include <esp_lcd_panel_commands.h>
|
||||
#include <esp_lcd_panel_dev.h>
|
||||
#include <esp_lcd_panel_ssd1306.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <driver/i2c.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
constexpr auto* TAG = "Ssd1306Display";
|
||||
|
||||
// SSD1306 commands
|
||||
#define SSD1306_CMD_SET_CLOCK 0xD5
|
||||
#define SSD1306_CMD_SET_CHARGE_PUMP 0x8D
|
||||
#define SSD1306_CMD_SET_SEGMENT_REMAP 0xA0
|
||||
#define SSD1306_CMD_SET_COM_SCAN_DIR 0xC0
|
||||
#define SSD1306_CMD_SET_COM_PIN_CFG 0xDA
|
||||
#define SSD1306_CMD_SET_CONTRAST 0x81
|
||||
#define SSD1306_CMD_SET_PRECHARGE 0xD9
|
||||
#define SSD1306_CMD_SET_VCOMH_DESELECT 0xDB
|
||||
#define SSD1306_CMD_DISPLAY_NORMAL 0xA6
|
||||
#define SSD1306_CMD_DISPLAY_INVERT 0xA7
|
||||
#define SSD1306_CMD_DISPLAY_OFF 0xAE
|
||||
#define SSD1306_CMD_DISPLAY_ON 0xAF
|
||||
#define SSD1306_CMD_SET_MEMORY_MODE 0x20
|
||||
#define SSD1306_CMD_SET_COLUMN_RANGE 0x21
|
||||
#define SSD1306_CMD_SET_PAGE_RANGE 0x22
|
||||
#define SSD1306_CMD_SET_MULTIPLEX 0xA8
|
||||
#define SSD1306_CMD_SET_OFFSET 0xD3
|
||||
#define SSD1306_CMD_STOP_SCROLL 0x2E
|
||||
#define SSD1306_CMD_SET_SCAN_DIRECTION_REVERSED 0xC8
|
||||
|
||||
// Helper to send I2C commands directly
|
||||
static bool ssd1306_i2c_send_cmd(i2c_port_t port, uint8_t addr, uint8_t cmd) {
|
||||
uint8_t data[2] = {0x00, cmd}; // 0x00 = command mode
|
||||
esp_err_t ret = i2c_master_write_to_device(port, addr, data, sizeof(data), pdMS_TO_TICKS(1000));
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to send command 0x%02X: %d", cmd, (int)ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Ssd1306Display::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) {
|
||||
const esp_lcd_panel_io_i2c_config_t io_config = {
|
||||
.dev_addr = configuration->deviceAddress,
|
||||
.on_color_trans_done = nullptr,
|
||||
.user_ctx = nullptr,
|
||||
.control_phase_bytes = 1,
|
||||
.dc_bit_offset = 6,
|
||||
.lcd_cmd_bits = 0,
|
||||
.lcd_param_bits = 0,
|
||||
.flags = {
|
||||
.dc_low_on_data = false,
|
||||
.disable_control_phase = false,
|
||||
},
|
||||
.scl_speed_hz = 0
|
||||
};
|
||||
|
||||
if (esp_lcd_new_panel_io_i2c(static_cast<esp_lcd_i2c_bus_handle_t>(configuration->port), &io_config, &ioHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create IO handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Ssd1306Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) {
|
||||
// Manual hardware reset with proper timing for Heltec V3
|
||||
if (configuration->resetPin != GPIO_NUM_NC) {
|
||||
gpio_config_t reset_gpio_config = {
|
||||
.pin_bit_mask = 1ULL << configuration->resetPin,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&reset_gpio_config);
|
||||
|
||||
gpio_set_level(configuration->resetPin, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
gpio_set_level(configuration->resetPin, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
// Create ESP-IDF panel (but don't call init - we'll do custom init)
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = GPIO_NUM_NC, // Already handled above
|
||||
.color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
|
||||
.bits_per_pixel = 1, // Must be 1 for monochrome
|
||||
.flags = {
|
||||
.reset_active_high = false,
|
||||
},
|
||||
.vendor_config = nullptr,
|
||||
};
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
esp_lcd_panel_ssd1306_config_t ssd1306_config = {
|
||||
.height = static_cast<uint8_t>(configuration->verticalResolution),
|
||||
};
|
||||
panel_config.vendor_config = &ssd1306_config;
|
||||
#endif
|
||||
|
||||
if (esp_lcd_new_panel_ssd1306(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't call esp_lcd_panel_init() - it doesn't configure correctly for Heltec V3!
|
||||
// Instead, send our custom initialization sequence directly via I2C
|
||||
|
||||
auto port = configuration->port;
|
||||
auto addr = configuration->deviceAddress;
|
||||
|
||||
LOG_I(TAG, "Sending Heltec V3 custom init sequence");
|
||||
|
||||
// Display off while configuring
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_DISPLAY_OFF);
|
||||
|
||||
// Set oscillator frequency (MUST come early in sequence)
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_CLOCK);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0xF0); // ~96 Hz
|
||||
|
||||
// Set multiplex ratio
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_MULTIPLEX);
|
||||
ssd1306_i2c_send_cmd(port, addr, configuration->verticalResolution - 1);
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_OFFSET);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x00);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x40);
|
||||
|
||||
// Enable charge pump (required for Heltec V3 - must be before memory mode)
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_CHARGE_PUMP);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x14); // Enable
|
||||
|
||||
// Horizontal addressing mode
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_MEMORY_MODE);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x00); // Horizontal addressing
|
||||
|
||||
// Segment remap (0xA1 for Heltec V3 orientation)
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_SEGMENT_REMAP | 0x01);
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_SCAN_DIRECTION_REVERSED );
|
||||
|
||||
// COM pin configuration
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_COM_PIN_CFG);
|
||||
if (configuration->verticalResolution == 64) {
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x12); // Alternative COM pin config for 64-row displays
|
||||
} else {
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x02); // Sequential COM pin config for 32-row displays
|
||||
}
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_CONTRAST);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0xCF);
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_PRECHARGE);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0xF1);
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_VCOMH_DESELECT);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x40);
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_COLUMN_RANGE);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0);
|
||||
ssd1306_i2c_send_cmd(port, addr, configuration->horizontalResolution - 1);
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_SET_PAGE_RANGE);
|
||||
ssd1306_i2c_send_cmd(port, addr, 0);
|
||||
if (configuration->verticalResolution == 64)
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x7);
|
||||
else if (configuration->verticalResolution == 32)
|
||||
ssd1306_i2c_send_cmd(port, addr, 0x3);
|
||||
else
|
||||
check(false, "Not supported");
|
||||
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_DISPLAY_INVERT);
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_STOP_SCROLL);
|
||||
ssd1306_i2c_send_cmd(port, addr, SSD1306_CMD_DISPLAY_ON);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // Let display stabilize
|
||||
|
||||
LOG_I(TAG, "Heltec V3 display initialized successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
lvgl_port_display_cfg_t Ssd1306Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) {
|
||||
return {
|
||||
.io_handle = ioHandle,
|
||||
.panel_handle = panelHandle,
|
||||
.control_handle = nullptr,
|
||||
.buffer_size = configuration->bufferSize,
|
||||
.double_buffer = false,
|
||||
.trans_size = 0,
|
||||
.hres = configuration->horizontalResolution,
|
||||
.vres = configuration->verticalResolution,
|
||||
.monochrome = true, // ESP-LVGL-port handles the conversion!
|
||||
.rotation = {
|
||||
.swap_xy = false,
|
||||
.mirror_x = false,
|
||||
.mirror_y = false,
|
||||
},
|
||||
.color_format = LV_COLOR_FORMAT_RGB565, // Use RGB565, monochrome flag makes it work!
|
||||
.flags = {
|
||||
.buff_dma = false,
|
||||
.buff_spiram = false,
|
||||
.sw_rotate = false,
|
||||
.swap_bytes = false,
|
||||
.full_refresh = true,
|
||||
.direct_mode = false
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <EspLcdDisplay.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_types.h>
|
||||
|
||||
class Ssd1306Display final : public EspLcdDisplay {
|
||||
|
||||
public:
|
||||
|
||||
class Configuration {
|
||||
|
||||
public:
|
||||
|
||||
Configuration(
|
||||
i2c_port_t port,
|
||||
uint8_t deviceAddress,
|
||||
gpio_num_t resetPin,
|
||||
unsigned int horizontalResolution, // Typically 128
|
||||
unsigned int verticalResolution, // 32 or 64
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
|
||||
bool invertColor = false
|
||||
) : port(port),
|
||||
deviceAddress(deviceAddress),
|
||||
resetPin(resetPin),
|
||||
horizontalResolution(horizontalResolution),
|
||||
verticalResolution(verticalResolution),
|
||||
invertColor(invertColor),
|
||||
touch(std::move(touch))
|
||||
{}
|
||||
|
||||
i2c_port_t port;
|
||||
uint8_t deviceAddress;
|
||||
gpio_num_t resetPin = GPIO_NUM_NC;
|
||||
unsigned int horizontalResolution;
|
||||
unsigned int verticalResolution;
|
||||
bool invertColor = false;
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||
uint32_t bufferSize = 0; // Size in pixel count. 0 means default (full screen / 8)
|
||||
int gapX = 0; // Column offset
|
||||
int gapY = 0; // Not used for SSD1306
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Configuration> configuration;
|
||||
|
||||
bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
|
||||
|
||||
bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override;
|
||||
|
||||
lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override;
|
||||
|
||||
public:
|
||||
|
||||
explicit Ssd1306Display(std::unique_ptr<Configuration> inConfiguration) :
|
||||
configuration(std::move(inConfiguration))
|
||||
{
|
||||
assert(configuration != nullptr);
|
||||
if (configuration->bufferSize == 0) {
|
||||
// For monochrome displays, ESP-LVGL-PORT expects full pixel count
|
||||
// It handles the monochrome conversion internally
|
||||
configuration->bufferSize = configuration->horizontalResolution * configuration->verticalResolution;
|
||||
}
|
||||
}
|
||||
|
||||
std::string getName() const override { return "SSD1306"; }
|
||||
|
||||
std::string getDescription() const override { return "SSD1306 monochrome OLED display with ESP-LVGL-PORT monochrome support"; }
|
||||
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override { return configuration->touch; }
|
||||
|
||||
void setBacklightDuty(uint8_t backlightDuty) override {
|
||||
// SSD1306 does not have backlight control
|
||||
}
|
||||
|
||||
bool supportsBacklightDuty() const override { return false; }
|
||||
|
||||
void setGammaCurve(uint8_t index) override {
|
||||
// SSD1306 does not support gamma curves
|
||||
}
|
||||
|
||||
uint8_t getGammaCurveCount() const override { return 0; }
|
||||
};
|
||||
|
||||
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility driver EspLcdCompat esp_lcd_st7735
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
# ST7735
|
||||
|
||||
A basic ESP32 LVGL driver for ST7735 displays.
|
||||
@@ -1,170 +0,0 @@
|
||||
#include "St7735Display.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_lcd_panel_commands.h>
|
||||
#include <esp_lcd_panel_dev.h>
|
||||
#include <esp_lcd_st7735.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
|
||||
constexpr auto* TAG = "ST7735";
|
||||
|
||||
bool St7735Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
|
||||
LOG_I(TAG, "Starting");
|
||||
|
||||
const esp_lcd_panel_io_spi_config_t panel_io_config = {
|
||||
.cs_gpio_num = configuration->csPin,
|
||||
.dc_gpio_num = configuration->dcPin,
|
||||
.spi_mode = 0,
|
||||
.pclk_hz = configuration->pixelClockFrequency,
|
||||
.trans_queue_depth = configuration->transactionQueueDepth,
|
||||
.on_color_trans_done = nullptr,
|
||||
.user_ctx = nullptr,
|
||||
.lcd_cmd_bits = 8,
|
||||
.lcd_param_bits = 8,
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.flags = {
|
||||
.dc_high_on_cmd = 0,
|
||||
.dc_low_on_data = 0,
|
||||
.dc_low_on_param = 0,
|
||||
.octal_mode = 0,
|
||||
.quad_mode = 0,
|
||||
.sio_mode = 1,
|
||||
.lsb_first = 0,
|
||||
.cs_high_active = 0
|
||||
}
|
||||
};
|
||||
|
||||
if (esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &outHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7735Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) {
|
||||
|
||||
const esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = configuration->resetPin,
|
||||
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
|
||||
.bits_per_pixel = 16,
|
||||
.flags = {
|
||||
.reset_active_high = false
|
||||
},
|
||||
.vendor_config = nullptr
|
||||
};
|
||||
|
||||
if (esp_lcd_new_panel_st7735(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to reset panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to init panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel to invert");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Warning: it looks like LVGL rotation is broken when "gap" is set and the screen is moved to a non-default orientation
|
||||
int gap_x = configuration->swapXY ? configuration->gapY : configuration->gapX;
|
||||
int gap_y = configuration->swapXY ? configuration->gapX : configuration->gapY;
|
||||
if (esp_lcd_panel_set_gap(panelHandle, gap_x, gap_y) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel gap");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to swap XY ");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel to mirror");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to turn display on");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
lvgl_port_display_cfg_t St7735Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) {
|
||||
return lvgl_port_display_cfg_t {
|
||||
.io_handle = ioHandle,
|
||||
.panel_handle = panelHandle,
|
||||
.control_handle = nullptr,
|
||||
.buffer_size = configuration->bufferSize,
|
||||
.double_buffer = false,
|
||||
.trans_size = 0,
|
||||
.hres = configuration->horizontalResolution,
|
||||
.vres = configuration->verticalResolution,
|
||||
.monochrome = false,
|
||||
.rotation = {
|
||||
.swap_xy = configuration->swapXY,
|
||||
.mirror_x = configuration->mirrorX,
|
||||
.mirror_y = configuration->mirrorY,
|
||||
},
|
||||
.color_format = LV_COLOR_FORMAT_RGB565,
|
||||
.flags = {
|
||||
.buff_dma = true,
|
||||
.buff_spiram = false,
|
||||
.sw_rotate = false,
|
||||
.swap_bytes = true,
|
||||
.full_refresh = false,
|
||||
.direct_mode = false
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Note:
|
||||
* The datasheet implies this should work, but it doesn't:
|
||||
* https://www.digikey.com/htmldatasheets/production/1640716/0/0/1/ILI9341-Datasheet.pdf
|
||||
*
|
||||
* This repo claims it only has 1 curve:
|
||||
* https://github.com/brucemack/hello-ili9341
|
||||
*
|
||||
* I'm leaving it in as I'm not sure if it's just my hardware that's problematic.
|
||||
*/
|
||||
void St7735Display::setGammaCurve(uint8_t index) {
|
||||
uint8_t gamma_curve;
|
||||
switch (index) {
|
||||
case 0:
|
||||
gamma_curve = 0x01;
|
||||
break;
|
||||
case 1:
|
||||
gamma_curve = 0x04;
|
||||
break;
|
||||
case 2:
|
||||
gamma_curve = 0x02;
|
||||
break;
|
||||
case 3:
|
||||
gamma_curve = 0x08;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const uint8_t param[] = {
|
||||
gamma_curve
|
||||
};
|
||||
|
||||
auto io_handle = getIoHandle();
|
||||
assert(io_handle != nullptr);
|
||||
if (esp_lcd_panel_io_tx_param(io_handle, LCD_CMD_GAMSET, param, 1) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set gamma");
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <EspLcdDisplay.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_types.h>
|
||||
#include <functional>
|
||||
#include <lvgl.h>
|
||||
|
||||
class St7735Display final : public EspLcdDisplay {
|
||||
|
||||
public:
|
||||
|
||||
class Configuration {
|
||||
|
||||
public:
|
||||
|
||||
Configuration(
|
||||
spi_host_device_t spiHostDevice,
|
||||
gpio_num_t csPin,
|
||||
gpio_num_t dcPin,
|
||||
gpio_num_t resetPin,
|
||||
unsigned int horizontalResolution,
|
||||
unsigned int verticalResolution,
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch,
|
||||
bool swapXY = false,
|
||||
bool mirrorX = false,
|
||||
bool mirrorY = false,
|
||||
bool invertColor = false,
|
||||
uint32_t bufferSize = 0, // Size in pixel count. 0 means default, which is 1/10 of the screen size
|
||||
int gapX = 0,
|
||||
int gapY = 0
|
||||
) : spiHostDevice(spiHostDevice),
|
||||
csPin(csPin),
|
||||
dcPin(dcPin),
|
||||
resetPin(resetPin),
|
||||
horizontalResolution(horizontalResolution),
|
||||
verticalResolution(verticalResolution),
|
||||
gapX(gapX),
|
||||
gapY(gapY),
|
||||
swapXY(swapXY),
|
||||
mirrorX(mirrorX),
|
||||
mirrorY(mirrorY),
|
||||
invertColor(invertColor),
|
||||
bufferSize(bufferSize),
|
||||
touch(std::move(touch))
|
||||
{
|
||||
if (this->bufferSize == 0) {
|
||||
this->bufferSize = horizontalResolution * verticalResolution / 10;
|
||||
}
|
||||
}
|
||||
|
||||
spi_host_device_t spiHostDevice;
|
||||
gpio_num_t csPin;
|
||||
gpio_num_t dcPin;
|
||||
gpio_num_t resetPin = GPIO_NUM_NC;
|
||||
unsigned int pixelClockFrequency = 27'000'000; // Hertz
|
||||
size_t transactionQueueDepth = 10;
|
||||
unsigned int horizontalResolution;
|
||||
unsigned int verticalResolution;
|
||||
int gapX;
|
||||
int gapY;
|
||||
bool swapXY = false;
|
||||
bool mirrorX = false;
|
||||
bool mirrorY = false;
|
||||
bool invertColor = false;
|
||||
uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||
std::function<void(uint8_t)> _Nullable backlightDutyFunction = nullptr;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Configuration> configuration;
|
||||
|
||||
bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
|
||||
|
||||
bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override;
|
||||
|
||||
lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override;
|
||||
|
||||
public:
|
||||
|
||||
explicit St7735Display(std::unique_ptr<Configuration> inConfiguration) :
|
||||
configuration(std::move(inConfiguration)
|
||||
) {}
|
||||
|
||||
std::string getName() const override { return "ST7735"; }
|
||||
|
||||
std::string getDescription() const override { return "ST7735 display"; }
|
||||
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override { return configuration->touch; }
|
||||
|
||||
void setBacklightDuty(uint8_t backlightDuty) override {
|
||||
if (configuration->backlightDutyFunction != nullptr) {
|
||||
configuration->backlightDutyFunction(backlightDuty);
|
||||
}
|
||||
}
|
||||
|
||||
bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; }
|
||||
|
||||
void setGammaCurve(uint8_t index) override;
|
||||
uint8_t getGammaCurveCount() const override { return 4; };
|
||||
};
|
||||
|
||||
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility EspLcdCompat esp_lcd_st7796 driver
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
# ST7796
|
||||
|
||||
A basic ESP32 LVGL driver for ST7796 displays.
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
#include "St7796Display.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_lcd_panel_dev.h>
|
||||
#include <esp_lcd_st7796.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
|
||||
constexpr auto* TAG = "ST7796";
|
||||
|
||||
bool St7796Display::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) {
|
||||
const esp_lcd_panel_io_spi_config_t panel_io_config = {
|
||||
.cs_gpio_num = configuration->csPin,
|
||||
.dc_gpio_num = configuration->dcPin,
|
||||
.spi_mode = 0,
|
||||
.pclk_hz = configuration->pixelClockFrequency,
|
||||
.trans_queue_depth = configuration->transactionQueueDepth,
|
||||
.on_color_trans_done = nullptr,
|
||||
.user_ctx = nullptr,
|
||||
.lcd_cmd_bits = 8,
|
||||
.lcd_param_bits = 8,
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.flags = {
|
||||
.dc_high_on_cmd = 0,
|
||||
.dc_low_on_data = 0,
|
||||
.dc_low_on_param = 0,
|
||||
.octal_mode = 0,
|
||||
.quad_mode = 0,
|
||||
.sio_mode = 0,
|
||||
.lsb_first = 0,
|
||||
.cs_high_active = 0
|
||||
}
|
||||
};
|
||||
|
||||
return esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &ioHandle) == ESP_OK;
|
||||
}
|
||||
|
||||
bool St7796Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) {
|
||||
static const st7796_lcd_init_cmd_t lcd_init_cmds[] = {
|
||||
{0x01, (uint8_t[]) {0x00}, 0, 120},
|
||||
{0x11, (uint8_t[]) {0x00}, 0, 120},
|
||||
{0xF0, (uint8_t[]) {0xC3}, 1, 0},
|
||||
{0xF0, (uint8_t[]) {0xC3}, 1, 0},
|
||||
{0xF0, (uint8_t[]) {0x96}, 1, 0},
|
||||
{0x36, (uint8_t[]) {0x58}, 1, 0},
|
||||
{0x3A, (uint8_t[]) {0x55}, 1, 0},
|
||||
{0xB4, (uint8_t[]) {0x01}, 1, 0},
|
||||
{0xB6, (uint8_t[]) {0x80, 0x02, 0x3B}, 3, 0},
|
||||
{0xE8, (uint8_t[]) {0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0},
|
||||
{0xC1, (uint8_t[]) {0x06}, 1, 0},
|
||||
{0xC2, (uint8_t[]) {0xA7}, 1, 0},
|
||||
{0xC5, (uint8_t[]) {0x18}, 1, 0},
|
||||
{0xE0, (uint8_t[]) {0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B}, 15, 0},
|
||||
{0xE1, (uint8_t[]) {0xE0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B}, 15, 120},
|
||||
{0xF0, (uint8_t[]) {0x3C}, 1, 0},
|
||||
{0xF0, (uint8_t[]) {0x69}, 1, 0},
|
||||
{0x21, (uint8_t[]) {0x00}, 1, 0},
|
||||
{0x29, (uint8_t[]) {0x00}, 1, 0},
|
||||
};
|
||||
|
||||
st7796_vendor_config_t vendor_config = {
|
||||
// Uncomment these lines if use custom initialization commands
|
||||
.init_cmds = lcd_init_cmds,
|
||||
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t),
|
||||
};
|
||||
|
||||
const esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = configuration->resetPin, // Set to -1 if not use
|
||||
.color_space = LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
|
||||
.bits_per_pixel = 16,
|
||||
.vendor_config = &vendor_config
|
||||
};
|
||||
|
||||
if (esp_lcd_new_panel_st7796(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to reset panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to init panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel to invert");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to swap XY ");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel to mirror");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_set_gap(panelHandle, configuration->gapX, configuration->gapY) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel gap");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to turn display on");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
lvgl_port_display_cfg_t St7796Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) {
|
||||
return {
|
||||
.io_handle = ioHandle,
|
||||
.panel_handle = panelHandle,
|
||||
.control_handle = nullptr,
|
||||
.buffer_size = configuration->bufferSize,
|
||||
.double_buffer = false,
|
||||
.trans_size = 0,
|
||||
.hres = configuration->horizontalResolution,
|
||||
.vres = configuration->verticalResolution,
|
||||
.monochrome = false,
|
||||
.rotation = {
|
||||
.swap_xy = configuration->swapXY,
|
||||
.mirror_x = configuration->mirrorX,
|
||||
.mirror_y = configuration->mirrorY,
|
||||
},
|
||||
.color_format = LV_COLOR_FORMAT_NATIVE,
|
||||
.flags = {
|
||||
.buff_dma = true,
|
||||
.buff_spiram = false,
|
||||
.sw_rotate = false,
|
||||
.swap_bytes = true,
|
||||
.full_refresh = false,
|
||||
.direct_mode = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void St7796Display::setGammaCurve(uint8_t index) {
|
||||
uint8_t gamma_curve;
|
||||
switch (index) {
|
||||
case 0:
|
||||
gamma_curve = 0x01;
|
||||
break;
|
||||
case 1:
|
||||
gamma_curve = 0x04;
|
||||
break;
|
||||
case 2:
|
||||
gamma_curve = 0x02;
|
||||
break;
|
||||
case 3:
|
||||
gamma_curve = 0x08;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t param[] = {
|
||||
gamma_curve
|
||||
};
|
||||
|
||||
/*if (esp_lcd_panel_io_tx_param(ioHandle , LCD_CMD_GAMSET, param, 1) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set gamma");
|
||||
}*/
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <driver/spi_common.h>
|
||||
|
||||
#include <EspLcdDisplay.h>
|
||||
#include <functional>
|
||||
|
||||
class St7796Display final : public EspLcdDisplay {
|
||||
|
||||
public:
|
||||
|
||||
class Configuration {
|
||||
|
||||
public:
|
||||
|
||||
Configuration(
|
||||
spi_host_device_t spiHostDevice,
|
||||
gpio_num_t csPin,
|
||||
gpio_num_t dcPin,
|
||||
unsigned int horizontalResolution,
|
||||
unsigned int verticalResolution,
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch,
|
||||
bool swapXY = false,
|
||||
bool mirrorX = false,
|
||||
bool mirrorY = false,
|
||||
bool invertColor = false,
|
||||
unsigned int gapX = 0,
|
||||
unsigned int gapY = 0,
|
||||
uint32_t bufferSize = 0 // Size in pixel count. 0 means default, which is 1/10 of the screen size
|
||||
) : spiHostDevice(spiHostDevice),
|
||||
csPin(csPin),
|
||||
dcPin(dcPin),
|
||||
horizontalResolution(horizontalResolution),
|
||||
verticalResolution(verticalResolution),
|
||||
swapXY(swapXY),
|
||||
mirrorX(mirrorX),
|
||||
mirrorY(mirrorY),
|
||||
invertColor(invertColor),
|
||||
gapX(gapX),
|
||||
gapY(gapY),
|
||||
bufferSize(bufferSize),
|
||||
touch(std::move(touch)) {
|
||||
if (this->bufferSize == 0) {
|
||||
this->bufferSize = horizontalResolution * verticalResolution / 10;
|
||||
}
|
||||
}
|
||||
|
||||
spi_host_device_t spiHostDevice;
|
||||
gpio_num_t csPin;
|
||||
gpio_num_t dcPin;
|
||||
gpio_num_t resetPin = GPIO_NUM_NC;
|
||||
unsigned int pixelClockFrequency = 80'000'000; // Hertz
|
||||
size_t transactionQueueDepth = 2;
|
||||
unsigned int horizontalResolution;
|
||||
unsigned int verticalResolution;
|
||||
bool swapXY = false;
|
||||
bool mirrorX = false;
|
||||
bool mirrorY = false;
|
||||
bool invertColor = false;
|
||||
unsigned int gapX = 0;
|
||||
unsigned int gapY = 0;
|
||||
uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||
std::function<void(uint8_t)> _Nullable backlightDutyFunction = nullptr;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Configuration> configuration;
|
||||
|
||||
bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
|
||||
|
||||
bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override;
|
||||
|
||||
lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override;
|
||||
|
||||
public:
|
||||
|
||||
explicit St7796Display(std::unique_ptr<Configuration> inConfiguration) :
|
||||
configuration(std::move(inConfiguration)
|
||||
) {
|
||||
assert(configuration != nullptr);
|
||||
}
|
||||
|
||||
std::string getName() const override { return "ST7796"; }
|
||||
|
||||
std::string getDescription() const override { return "ST7796 display"; }
|
||||
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override { return configuration->touch; }
|
||||
|
||||
void setBacklightDuty(uint8_t backlightDuty) override {
|
||||
if (configuration->backlightDutyFunction != nullptr) {
|
||||
configuration->backlightDutyFunction(backlightDuty);
|
||||
}
|
||||
}
|
||||
|
||||
void setGammaCurve(uint8_t index) override;
|
||||
|
||||
uint8_t getGammaCurveCount() const override { return 4; };
|
||||
|
||||
bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; }
|
||||
};
|
||||
|
||||
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||
@@ -1,26 +0,0 @@
|
||||
Software License Agreement (BSD License)
|
||||
|
||||
Copyright (c) 2019 Limor Fried (Adafruit Industries)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holders nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
Copyright 2023 Anthony DiGirolamo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,5 +0,0 @@
|
||||
# TCA8418 I2C Controlled Keypad Scan IC With Integrated ESD Protection
|
||||
|
||||
[Datasheet](https://www.ti.com/lit/ds/symlink/tca8418.pdf?ts=1751500237439)
|
||||
[Original implementation](https://github.com/AnthonyDiGirolamo/i2c-thumb-keyboard/tree/master) by Anthony DiGirolamo
|
||||
[Adafruit TCA8418](https://github.com/adafruit/Adafruit_TCA8418)
|
||||
@@ -1,227 +0,0 @@
|
||||
#include "Tca8418.h"
|
||||
|
||||
namespace registers {
|
||||
static const uint8_t CFG = 0x01U;
|
||||
static const uint8_t KP_GPIO1 = 0x1DU;
|
||||
static const uint8_t KP_GPIO2 = 0x1EU;
|
||||
static const uint8_t KP_GPIO3 = 0x1FU;
|
||||
|
||||
static const uint8_t KEY_EVENT_A = 0x04U;
|
||||
static const uint8_t KEY_EVENT_B = 0x05U;
|
||||
static const uint8_t KEY_EVENT_C = 0x06U;
|
||||
static const uint8_t KEY_EVENT_D = 0x07U;
|
||||
static const uint8_t KEY_EVENT_E = 0x08U;
|
||||
static const uint8_t KEY_EVENT_F = 0x09U;
|
||||
static const uint8_t KEY_EVENT_G = 0x0AU;
|
||||
static const uint8_t KEY_EVENT_H = 0x0BU;
|
||||
static const uint8_t KEY_EVENT_I = 0x0CU;
|
||||
static const uint8_t KEY_EVENT_J = 0x0DU;
|
||||
} // namespace registers
|
||||
|
||||
|
||||
/** From https://github.com/adafruit/Adafruit_TCA8418/blob/main/Adafruit_TCA8418.cpp */
|
||||
bool Tca8418::initMatrix(uint8_t rows, uint8_t columns) {
|
||||
if ((rows > 8) || (columns > 10))
|
||||
return false;
|
||||
|
||||
if ((rows != 0) && (columns != 0)) {
|
||||
// Configure the keypad matrix.
|
||||
uint8_t mask = 0x00;
|
||||
for (int r = 0; r < rows; r++) {
|
||||
mask <<= 1;
|
||||
mask |= 1;
|
||||
}
|
||||
writeRegister(registers::KP_GPIO1, &mask, 1);
|
||||
|
||||
mask = 0x00;
|
||||
for (int c = 0; c < columns && c < 8; c++) {
|
||||
mask <<= 1;
|
||||
mask |= 1;
|
||||
}
|
||||
writeRegister(registers::KP_GPIO2, &mask, 1);
|
||||
|
||||
if (columns > 8) {
|
||||
if (columns == 9)
|
||||
mask = 0x01;
|
||||
else
|
||||
mask = 0x03;
|
||||
writeRegister(registers::KP_GPIO3, &mask, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
void Tca8418::init(uint8_t numrows, uint8_t numcols) {
|
||||
/*
|
||||
* | ADDRESS | REGISTER NAME | REGISTER DESCRIPTION | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 |
|
||||
* |---------+---------------+----------------------+------+------+------+------+------+------+------+------|
|
||||
* | 0x1D | KP_GPIO1 | Keypad/GPIO Select 1 | ROW7 | ROW6 | ROW5 | ROW4 | ROW3 | ROW2 | ROW1 | ROW0 |
|
||||
* | 0x1E | KP_GPIO2 | Keypad/GPIO Select 2 | COL7 | COL6 | COL5 | COL4 | COL3 | COL2 | COL1 | COL0 |
|
||||
* | 0x1F | KP_GPIO3 | Keypad/GPIO Select 3 | N/A | N/A | N/A | N/A | N/A | N/A | COL9 | COL8 |
|
||||
*/
|
||||
|
||||
num_rows = numrows;
|
||||
num_cols = numcols;
|
||||
|
||||
initMatrix(num_rows, num_cols);
|
||||
|
||||
/*
|
||||
* BIT: NAME
|
||||
*
|
||||
* 7: AI
|
||||
* Auto-increment for read and write operations; See below table for more information
|
||||
* 0 = disabled
|
||||
* 1 = enabled
|
||||
*
|
||||
* 6: GPI_E_CFG
|
||||
* GPI event mode configuration
|
||||
* 0 = GPI events are tracked when keypad is locked
|
||||
* 1 = GPI events are not tracked when keypad is locked
|
||||
*
|
||||
* 5: OVR_FLOW_M
|
||||
* Overflow mode
|
||||
* 0 = disabled; Overflow data is lost
|
||||
* 1 = enabled; Overflow data shifts with last event pushing first event out
|
||||
*
|
||||
* 4: INT_CFG
|
||||
* Interrupt configuration
|
||||
* 0 = processor interrupt remains asserted (or low) if host tries to clear interrupt while there is
|
||||
* still a pending key press, key release or GPI interrupt
|
||||
* 1 = processor interrupt is deasserted for 50 μs and reassert with pending interrupts
|
||||
*
|
||||
* 3: OVR_FLOW_IEN
|
||||
* Overflow interrupt enable
|
||||
* 0 = disabled; INT is not asserted if the FIFO overflows
|
||||
* 1 = enabled; INT becomes asserted if the FIFO overflows
|
||||
*
|
||||
* 2: K_LCK_IEN
|
||||
* Keypad lock interrupt enable
|
||||
* 0 = disabled; INT is not asserted after a correct unlock key sequence
|
||||
* 1 = enabled; INT becomes asserted after a correct unlock key sequence
|
||||
*
|
||||
* 1: GPI_IEN
|
||||
* GPI interrupt enable to host processor
|
||||
* 0 = disabled; INT is not asserted for a change on a GPI
|
||||
* 1 = enabled; INT becomes asserted for a change on a GPI
|
||||
*
|
||||
* 0: KE_IEN
|
||||
* Key events interrupt enable to host processor
|
||||
* 0 = disabled; INT is not asserted when a key event occurs
|
||||
* 1 = enabled; INT becomes asserted when a key event occurs
|
||||
*/
|
||||
|
||||
// 10111001 xB9 -- fifo overflow enabled
|
||||
// 10011001 x99 -- fifo overflow disabled
|
||||
writeRegister8(registers::CFG, 0x99);
|
||||
|
||||
clear_released_list();
|
||||
clear_pressed_list();
|
||||
}
|
||||
|
||||
bool Tca8418::update() {
|
||||
last_update_micros = this_update_micros;
|
||||
uint8_t key_down, key_event, key_row, key_col;
|
||||
|
||||
key_event = get_key_event();
|
||||
// TODO: read gpio R7/R6 status? 0x14 bits 7&6
|
||||
// read(0x14, &new_keycode)
|
||||
|
||||
// TODO: use tick function to get an update delta time
|
||||
this_update_micros = 0;
|
||||
delta_micros = this_update_micros - last_update_micros;
|
||||
|
||||
if (key_event > 0) {
|
||||
key_down = (key_event & 0x80);
|
||||
uint16_t buffer = key_event;
|
||||
buffer &= 0x7F;
|
||||
buffer--;
|
||||
key_row = buffer / 10;
|
||||
key_col = buffer % 10;
|
||||
|
||||
// always clear the released list
|
||||
clear_released_list();
|
||||
|
||||
if (key_down) {
|
||||
add_pressed_key(key_row, key_col);
|
||||
// TODO reject ghosts (assume multiple key presses with the same hold time are ghosts.)
|
||||
|
||||
} else {
|
||||
add_released_key(key_row, key_col);
|
||||
remove_pressed_key(key_row, key_col);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Increment hold times for pressed keys
|
||||
for (int i = 0; i < pressed_key_count; i++) {
|
||||
pressed_list[i].hold_time += delta_micros;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void Tca8418::add_pressed_key(uint8_t row, uint8_t col) {
|
||||
if (pressed_key_count >= KEY_EVENT_LIST_SIZE)
|
||||
return;
|
||||
|
||||
pressed_list[pressed_key_count].row = row;
|
||||
pressed_list[pressed_key_count].col = col;
|
||||
pressed_list[pressed_key_count].hold_time = 0;
|
||||
pressed_key_count++;
|
||||
}
|
||||
|
||||
void Tca8418::add_released_key(uint8_t row, uint8_t col) {
|
||||
if (released_key_count >= KEY_EVENT_LIST_SIZE)
|
||||
return;
|
||||
|
||||
released_key_count++;
|
||||
released_list[0].row = row;
|
||||
released_list[0].col = col;
|
||||
}
|
||||
|
||||
void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) {
|
||||
if (pressed_key_count == 0)
|
||||
return;
|
||||
|
||||
// delete the pressed key
|
||||
for (int i = 0; i < pressed_key_count; i++) {
|
||||
if (pressed_list[i].row == row &&
|
||||
pressed_list[i].col == col) {
|
||||
// shift remaining keys left one index
|
||||
for (int j = i; i < pressed_key_count; j++) {
|
||||
if (j == KEY_EVENT_LIST_SIZE - 1)
|
||||
break;
|
||||
pressed_list[j].row = pressed_list[j + 1].row;
|
||||
pressed_list[j].col = pressed_list[j + 1].col;
|
||||
pressed_list[j].hold_time = pressed_list[j + 1].hold_time;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
pressed_key_count--;
|
||||
}
|
||||
|
||||
void Tca8418::clear_pressed_list() {
|
||||
for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) {
|
||||
pressed_list[i].row = 255;
|
||||
pressed_list[i].col = 255;
|
||||
}
|
||||
pressed_key_count = 0;
|
||||
}
|
||||
|
||||
void Tca8418::clear_released_list() {
|
||||
for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) {
|
||||
released_list[i].row = 255;
|
||||
released_list[i].col = 255;
|
||||
}
|
||||
released_key_count = 0;
|
||||
}
|
||||
|
||||
uint8_t Tca8418::get_key_event() {
|
||||
uint8_t new_keycode = 0;
|
||||
|
||||
readRegister8(registers::KEY_EVENT_A, new_keycode);
|
||||
return new_keycode;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
constexpr auto TCA8418_ADDRESS = 0x34U;
|
||||
constexpr auto KEY_EVENT_LIST_SIZE = 10;
|
||||
|
||||
/**
|
||||
* See https://www.ti.com/lit/ds/symlink/tca8418.pdf
|
||||
*/
|
||||
class Tca8418 final : public tt::hal::i2c::I2cDevice {
|
||||
|
||||
uint8_t tca8418_address;
|
||||
uint32_t last_update_micros;
|
||||
uint32_t this_update_micros;
|
||||
|
||||
uint8_t new_pressed_keys_count;
|
||||
|
||||
void clear_released_list();
|
||||
void clear_pressed_list();
|
||||
void add_pressed_key(uint8_t row, uint8_t col);
|
||||
void add_released_key(uint8_t row, uint8_t col);
|
||||
void remove_pressed_key(uint8_t row, uint8_t col);
|
||||
void write(uint8_t register_address, uint8_t data);
|
||||
bool read(uint8_t register_address, uint8_t* data);
|
||||
|
||||
bool initMatrix(uint8_t rows, uint8_t columns);
|
||||
|
||||
public:
|
||||
|
||||
struct PressedKey {
|
||||
uint8_t row;
|
||||
uint8_t col;
|
||||
uint32_t hold_time;
|
||||
};
|
||||
|
||||
struct ReleasedKey {
|
||||
uint8_t row;
|
||||
uint8_t col;
|
||||
};
|
||||
|
||||
std::string getName() const final { return "TCA8418"; }
|
||||
|
||||
std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; }
|
||||
|
||||
explicit Tca8418(::Device* controller) : I2cDevice(controller, TCA8418_ADDRESS) {
|
||||
delta_micros = 0;
|
||||
last_update_micros = 0;
|
||||
this_update_micros = 0;
|
||||
}
|
||||
|
||||
~Tca8418() {}
|
||||
|
||||
uint8_t num_rows;
|
||||
uint8_t num_cols;
|
||||
|
||||
uint32_t delta_micros;
|
||||
|
||||
std::array<PressedKey, KEY_EVENT_LIST_SIZE> pressed_list;
|
||||
std::array<ReleasedKey, KEY_EVENT_LIST_SIZE> released_list;
|
||||
uint8_t pressed_key_count;
|
||||
uint8_t released_key_count;
|
||||
|
||||
void init(uint8_t numrows, uint8_t numcols);
|
||||
bool update();
|
||||
uint8_t get_key_event();
|
||||
bool button_pressed(uint8_t row, uint8_t button_bit_position);
|
||||
bool button_released(uint8_t row, uint8_t button_bit_position);
|
||||
bool button_held(uint8_t row, uint8_t button_bit_position);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
modules.order
|
||||
Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
.vscode/
|
||||
.idea/
|
||||
build/
|
||||
cmake-build-debug-esp-idf/
|
||||
@@ -1,7 +0,0 @@
|
||||
file(GLOB_RECURSE SOURCES src/*.c)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCES}
|
||||
INCLUDE_DIRS include
|
||||
REQUIRES driver
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
menu "Example Configuration"
|
||||
|
||||
config I2C_MASTER_SCL
|
||||
int "SCL GPIO Num"
|
||||
default 6 if IDF_TARGET_ESP32C3
|
||||
default 19 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3
|
||||
help
|
||||
GPIO number for I2C Master clock line.
|
||||
|
||||
config I2C_MASTER_SDA
|
||||
int "SDA GPIO Num"
|
||||
default 5 if IDF_TARGET_ESP32C3
|
||||
default 18 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3
|
||||
help
|
||||
GPIO number for I2C Master data line.
|
||||
|
||||
endmenu
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Victor Hogeweij
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,8 +0,0 @@
|
||||
#
|
||||
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
|
||||
# project subdirectory.
|
||||
#
|
||||
|
||||
PROJECT_NAME := i2c-simple
|
||||
|
||||
include $(IDF_PATH)/make/project.mk
|
||||
@@ -1,49 +0,0 @@
|
||||
# I2C Simple Example
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
## Overview
|
||||
|
||||
This example demonstrates basic usage of I2C driver by reading and writing from a I2C connected sensor:
|
||||
|
||||
If you have a new I2C application to go (for example, read the temperature data from external sensor with I2C interface), try this as a basic template, then add your own code.
|
||||
|
||||
## How to use example
|
||||
|
||||
### Hardware Required
|
||||
|
||||
To run this example, you should have one ESP32, ESP32-S or ESP32-C based development board as well as a MPU9250. MPU9250 is a inertial measurement unit, which contains a accelerometer, gyroscope as well as a magnetometer, for more information about it, you can read the [PDF](https://invensense.tdk.com/wp-content/uploads/2015/02/PS-MPU-9250A-01-v1.1.pdf) of this sensor.
|
||||
|
||||
#### Pin Assignment:
|
||||
|
||||
**Note:** The following pin assignments are used by default, you can change these in the `menuconfig` .
|
||||
|
||||
| | SDA | SCL |
|
||||
| ---------------- | -------------- | -------------- |
|
||||
| ESP I2C Master | I2C_MASTER_SDA | I2C_MASTER_SCL |
|
||||
| MPU9250 Sensor | SDA | SCL |
|
||||
|
||||
|
||||
For the actual default value of `I2C_MASTER_SDA` and `I2C_MASTER_SCL` see `Example Configuration` in `menuconfig`.
|
||||
|
||||
**Note: ** There’s no need to add an external pull-up resistors for SDA/SCL pin, because the driver will enable the internal pull-up resistors.
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Enter `idf.py -p PORT flash monitor` to build, flash and monitor the project.
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Example Output
|
||||
|
||||
```bash
|
||||
I (328) i2c-simple-example: I2C initialized successfully
|
||||
I (338) i2c-simple-example: WHO_AM_I = 71
|
||||
I (338) i2c-simple-example: I2C unitialized successfully
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
|
||||
@@ -1,3 +0,0 @@
|
||||
#
|
||||
# Main Makefile. This is basically the same as a component makefile .
|
||||
#
|
||||
@@ -1,6 +0,0 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
set(EXTRA_COMPONENT_DIRS read)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(TCA9534_Examples)
|
||||
@@ -1,59 +0,0 @@
|
||||
#include "esp_log.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "TCA9534.h"
|
||||
|
||||
|
||||
#define I2C_MASTER_SCL_IO CONFIG_I2C_MASTER_SCL /*!< GPIO number used for I2C master clock */
|
||||
#define I2C_MASTER_SDA_IO CONFIG_I2C_MASTER_SDA /*!< GPIO number used for I2C master data */
|
||||
#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
|
||||
#define I2C_MASTER_FREQ_HZ 400000 /*!< I2C master clock frequency */
|
||||
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
|
||||
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
|
||||
|
||||
|
||||
/**
|
||||
* @brief i2c master initialization
|
||||
*/
|
||||
static esp_err_t i2c_master_init(i2c_config_t *conf) {
|
||||
int i2c_master_port = I2C_MASTER_NUM;
|
||||
|
||||
conf->mode = I2C_MODE_MASTER;
|
||||
conf->master.clk_speed = I2C_MASTER_FREQ_HZ;
|
||||
conf->sda_io_num = I2C_MASTER_SDA_IO;
|
||||
conf->scl_io_num = I2C_MASTER_SCL_IO;
|
||||
conf->sda_pullup_en = GPIO_PULLUP_ENABLE;
|
||||
conf->scl_pullup_en = GPIO_PULLUP_ENABLE;
|
||||
i2c_param_config(i2c_master_port, conf);
|
||||
|
||||
return i2c_driver_install(i2c_master_port, conf->mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
|
||||
}
|
||||
|
||||
|
||||
static const char *TAG = "TCA9534-Example";
|
||||
|
||||
void app_main(void) {
|
||||
TCA9534_IO_EXP IO_EXP1;
|
||||
esp_err_t status = i2c_master_init(&IO_EXP1.i2c_conf);
|
||||
if (status == ESP_OK) {
|
||||
ESP_LOGI(TAG, "I2C initialized successfully");
|
||||
IO_EXP1.I2C_ADDR = 0b0100000;
|
||||
IO_EXP1.i2c_master_port = I2C_MASTER_NUM;
|
||||
|
||||
set_tca9534_io_pin_direction(IO_EXP1, TCA9534_IO0, TCA9534_INPUT);
|
||||
set_tca9534_io_pin_direction(IO_EXP1, TCA9534_IO1, TCA9534_OUTPUT);
|
||||
|
||||
int pin_state = 0;
|
||||
while (1) {
|
||||
pin_state = get_io_pin_input_status(IO_EXP1, TCA9534_IO0);
|
||||
if (pin_state == -1) {
|
||||
ESP_LOGE(TAG, "Cannot get pin status from TCA9534");
|
||||
break;
|
||||
}
|
||||
set_tca9534_io_pin_output_state(IO_EXP1, TCA9534_IO1, pin_state);
|
||||
vTaskDelay(100 / portTICK_RATE_MS);
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(i2c_driver_delete(I2C_MASTER_NUM));
|
||||
ESP_LOGI(TAG, "I2C unitialized successfully");
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
#ifndef TCA9534_IDF_TCA9534_H
|
||||
#define TCA9534_IDF_TCA9534_H
|
||||
|
||||
#include <driver/i2c.h>
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define TCA9534_ERROR -1
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief TCA9534 IO Pins mapping
|
||||
*/
|
||||
typedef enum {
|
||||
TCA9534_IO0,
|
||||
TCA9534_IO1,
|
||||
TCA9534_IO2,
|
||||
TCA9534_IO3,
|
||||
TCA9534_IO4,
|
||||
TCA9534_IO5,
|
||||
TCA9534_IO6,
|
||||
TCA9534_IO7
|
||||
} TCA9534_PINS;
|
||||
|
||||
/**
|
||||
* @brief TCA9534 Port direction parameters
|
||||
*/
|
||||
typedef enum {
|
||||
TCA9534_OUTPUT,
|
||||
TCA9534_INPUT
|
||||
} TCA9534_PORT_DIRECTION;
|
||||
|
||||
/**
|
||||
* @brief TCA9534 initialization parameters
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t I2C_ADDR;
|
||||
int i2c_master_port;
|
||||
//Only when mode is set to interrupt, otherwise it won't be used..
|
||||
gpio_num_t interrupt_pin;
|
||||
TaskHandle_t* interrupt_task;
|
||||
} TCA9534_IO_EXP;
|
||||
|
||||
/**
|
||||
* @brief Setup interrupts using the builtin IO_EXP_INT pin of the tca9534 and interrupt handler+task
|
||||
* @param io_exp which contains the gpio pin where IO_EXP_INT is connected(interrupt_pin)
|
||||
* And optionally contains the task to run when interrupt triggered (interrupt_task) if not defined the
|
||||
* default handler will be used.
|
||||
*/
|
||||
void setup_tca9534_interrupt_handler(TCA9534_IO_EXP* io_exp);
|
||||
|
||||
/**
|
||||
* @brief Get the current input state of the specified input pin (1 or 0)
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
* @param io_pin The io expander pin to read the state from
|
||||
*
|
||||
* @return
|
||||
* - 0 Success! Pin is Low
|
||||
* - 1 Success! Pin is High
|
||||
* - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander
|
||||
*/
|
||||
int16_t get_io_pin_input_status(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin);
|
||||
|
||||
/**
|
||||
* @brief Get the current input state of all the io expander pins
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
*
|
||||
* @return
|
||||
* - 0x00 - 0xFF Success! Dump of input register, 1 bit is equal to 1 of the physical pins (Lower 8 bits of 16 bits result)
|
||||
* - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander
|
||||
*/
|
||||
int16_t get_tca9534_all_io_pin_input_status(TCA9534_IO_EXP* io_exp);
|
||||
|
||||
/**
|
||||
* @brief Get the current direction of all the io expander pins
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
*
|
||||
* @return
|
||||
* - 0x00 - 0xFF Success! Dump of configuration register, 1 bit is equal to 1 of the physical pins (Lower 8 bits of 16 bits result)
|
||||
* - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander
|
||||
*/
|
||||
int16_t get_all_io_pin_direction(TCA9534_IO_EXP* io_exp);
|
||||
|
||||
/**
|
||||
* @brief Get the current polarity inversion state of all the io expander pins
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
*
|
||||
* @return
|
||||
* - 0x00 - 0xFF Success! Dump of configuration register, 1 bit is equal to 1 of the physical pins (Lower 8 bits of 16 bits result)
|
||||
* - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander
|
||||
*/
|
||||
int16_t get_all_io_polarity_inversion(TCA9534_IO_EXP* io_exp);
|
||||
|
||||
/**
|
||||
* @brief Get the current direction of the specified io expander pin
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
* @param io_pin The io expander pin to read polarity inversion from
|
||||
*
|
||||
* @return
|
||||
* - 0 Success! Pin is Not inverted
|
||||
* - 1 Success! Pin is Inverted
|
||||
* - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander
|
||||
*/
|
||||
int16_t get_io_pin_polarity_inversion(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin);
|
||||
|
||||
/**
|
||||
* @brief Get the current direction of the specified physical pin (0 means OUTPUT or 1 means INPUT)
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
* @param io_pin The io expander pin to read the state from
|
||||
*
|
||||
* @return
|
||||
* - 0 Success! Pin is OUTPUT
|
||||
* - 1 Success! Pin is INPUT
|
||||
* - TCA9534_ERROR(-1) Error! Something went wrong in the process of reading the io expander
|
||||
*/
|
||||
int16_t get_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin);
|
||||
|
||||
/**
|
||||
* @brief Sets all physical pins of the io expander to a specified direction (INPUT or OUTPUT)
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
* @param properties The pin direction to be set (INPUT or OUTPUT)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK Success!
|
||||
* - ESP_ERR Error!
|
||||
*/
|
||||
esp_err_t set_all_tca9534_io_pins_direction(TCA9534_IO_EXP* io_exp, TCA9534_PORT_DIRECTION properties);
|
||||
|
||||
/**
|
||||
* @brief Set physical pin of the io expander to a specified direction (INPUT or OUTPUT)
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
* @param io_pin The io expander physical pin to be set
|
||||
* @param properties The pin direction to be set (INPUT or OUTPUT)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK Success!
|
||||
* - ESP_ERR Error!
|
||||
*/
|
||||
esp_err_t set_tca9534_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, TCA9534_PORT_DIRECTION properties);
|
||||
|
||||
/**
|
||||
* @brief Set physical pin of the io expander to an specified output state (HIGH(1) or LOW(0))
|
||||
*
|
||||
* @param io_exp The io expander instance to read from or write to
|
||||
* @param io_pin The io expander physical pin to be set
|
||||
* @param state The pin state to be set (1 or 0)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK Success!
|
||||
* - ESP_ERR Error!
|
||||
*
|
||||
* @note Pin output state can be inverted with the inversion register
|
||||
*/
|
||||
esp_err_t set_tca9534_io_pin_output_state(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, uint8_t state);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //TCA9534_IDF_TCA9534_H
|
||||
@@ -1,151 +0,0 @@
|
||||
#include "TCA9534.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "esp_log.h"
|
||||
#include <rom/gpio.h>
|
||||
|
||||
#define I2C_MASTER_TIMEOUT_MS 1000
|
||||
|
||||
#define TCA9534_LIB_TAG "TCA9534"
|
||||
#define TCA9534_IO_NUM 8
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief TCA9534 Internal configuration and pin registers
|
||||
*/
|
||||
typedef enum {
|
||||
TCA9534_REG_INPUT_PORT,
|
||||
TCA9534_REG_OUTPUT_PORT,
|
||||
TCA9534_REG_POLARITY_INVERSION,
|
||||
TCA9534_REG_CONFIGURATION
|
||||
} TCA9534_REGISTER;
|
||||
|
||||
/**
|
||||
* @brief Default TCA9534 interrupt task
|
||||
*/
|
||||
void TCA9534_default_interrupt_task(void * pvParameters){
|
||||
ESP_LOGW(TCA9534_LIB_TAG, "No interrupt task defined! Using standard TCA9523 interrupt task!");
|
||||
TCA9534_IO_EXP* io_exp = (TCA9534_IO_EXP*) pvParameters;
|
||||
uint32_t io_num;
|
||||
while(1){
|
||||
if(xTaskNotifyWait(0,0,&io_num,portTICK_PERIOD_MS) == pdTRUE) {
|
||||
uint8_t input_status = get_tca9534_all_io_pin_input_status(io_exp);
|
||||
printf("Current input status (pin : status):\n");
|
||||
for (uint8_t i = 0; i < TCA9534_IO_NUM; i++)
|
||||
printf("P%d : %d\n", i, (input_status & (1<<i)) == (1 << i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TCA9534 interrupt handler
|
||||
*/
|
||||
static void IRAM_ATTR TCA9534_interrupt_handler(void *args){
|
||||
TCA9534_IO_EXP* io_exp = (TCA9534_IO_EXP*) args;
|
||||
xTaskNotifyFromISR(*io_exp->interrupt_task, 0, eNoAction, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Setup TCA9534 interrupts
|
||||
*/
|
||||
void setup_tca9534_interrupt_handler(TCA9534_IO_EXP* io_exp){
|
||||
if(io_exp->interrupt_task == NULL){
|
||||
xTaskCreate(
|
||||
TCA9534_default_interrupt_task, /* Function that implements the task. */
|
||||
"NAME", /* Text name for the task. */
|
||||
2048, /* Stack size in words, not bytes. */
|
||||
( void * ) io_exp, /* Parameter passed into the task. */
|
||||
10,/* Priority at which the task is created. */
|
||||
io_exp->interrupt_task); /* Used to pass out the created task's handle. */
|
||||
|
||||
}
|
||||
|
||||
gpio_pad_select_gpio(GPIO_NUM_26);
|
||||
gpio_set_direction(GPIO_NUM_26,GPIO_MODE_INPUT);
|
||||
gpio_intr_enable(GPIO_NUM_26);
|
||||
|
||||
gpio_set_intr_type(io_exp->interrupt_pin, GPIO_INTR_NEGEDGE);
|
||||
gpio_install_isr_service(0);
|
||||
gpio_isr_handler_add(io_exp->interrupt_pin, TCA9534_interrupt_handler, (void *)io_exp);
|
||||
}
|
||||
|
||||
esp_err_t write_tca9534_reg(TCA9534_IO_EXP* io_exp, TCA9534_REGISTER cmd, uint8_t data) {
|
||||
uint8_t write_buffer[2] = {cmd, data};
|
||||
return i2c_master_write_to_device(io_exp->i2c_master_port, io_exp->I2C_ADDR, write_buffer,
|
||||
sizeof(write_buffer), I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
esp_err_t read_tca9534_reg(TCA9534_IO_EXP* io_exp, TCA9534_REGISTER cmd, uint8_t *read_buff) {
|
||||
uint8_t reg = cmd;
|
||||
return i2c_master_write_read_device(io_exp->i2c_master_port, io_exp->I2C_ADDR, ®,
|
||||
1, read_buff, 1, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
int16_t get_tca9534_all_io_pin_input_status(TCA9534_IO_EXP* io_exp) {
|
||||
uint8_t result = 0;
|
||||
esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_INPUT_PORT, &result);
|
||||
return (status == ESP_OK) ? result : TCA9534_ERROR;
|
||||
}
|
||||
|
||||
int16_t get_io_pin_input_status(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin) {
|
||||
int16_t result = get_tca9534_all_io_pin_input_status(io_exp);
|
||||
if (result != TCA9534_ERROR)
|
||||
result &= (1 << io_pin);
|
||||
return (result == (1<< io_pin));
|
||||
}
|
||||
|
||||
int16_t get_all_io_pin_direction(TCA9534_IO_EXP* io_exp) {
|
||||
uint8_t result;
|
||||
esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, &result);
|
||||
return (status == ESP_OK) ? result : TCA9534_ERROR;
|
||||
}
|
||||
|
||||
int16_t get_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin) {
|
||||
int16_t result = get_all_io_pin_direction(io_exp);
|
||||
if (result != TCA9534_ERROR)
|
||||
result &= (1 << io_pin);
|
||||
return (result == (1<< io_pin));
|
||||
}
|
||||
|
||||
int16_t get_all_io_polarity_inversion(TCA9534_IO_EXP* io_exp) {
|
||||
uint8_t result;
|
||||
esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_POLARITY_INVERSION, &result);
|
||||
return (status == ESP_OK) ? result : TCA9534_ERROR;
|
||||
}
|
||||
|
||||
int16_t get_io_pin_polarity_inversion(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin) {
|
||||
int16_t result = get_all_io_polarity_inversion(io_exp);
|
||||
if (result != TCA9534_ERROR)
|
||||
result &= (1 << io_pin);
|
||||
return (result == (1<< io_pin));
|
||||
}
|
||||
|
||||
esp_err_t set_all_tca9534_io_pins_direction(TCA9534_IO_EXP* io_exp, TCA9534_PORT_DIRECTION properties) {
|
||||
uint8_t dir = (properties == TCA9534_OUTPUT) ? 0x00 : 0xFF;
|
||||
esp_err_t status = write_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, dir);
|
||||
return status;
|
||||
}
|
||||
|
||||
esp_err_t set_tca9534_io_pin_direction(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, TCA9534_PORT_DIRECTION properties) {
|
||||
uint8_t port_status = 0;
|
||||
esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, &port_status);
|
||||
port_status = (properties != TCA9534_OUTPUT) ? (port_status | (1 << io_pin)) : (port_status & ~(1 << io_pin));
|
||||
|
||||
status |= write_tca9534_reg(io_exp, TCA9534_REG_CONFIGURATION, port_status);
|
||||
return status;
|
||||
}
|
||||
|
||||
esp_err_t set_tca9534_io_pin_output_state(TCA9534_IO_EXP* io_exp, TCA9534_PINS io_pin, uint8_t state) {
|
||||
uint8_t port_status = 0;
|
||||
esp_err_t status = read_tca9534_reg(io_exp, TCA9534_REG_OUTPUT_PORT, &port_status);
|
||||
port_status = (state != 0) ? (port_status | (1 << io_pin)) : (port_status & ~(1 << io_pin));
|
||||
|
||||
status |= write_tca9534_reg(io_exp, TCA9534_REG_OUTPUT_PORT, port_status);
|
||||
return status;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(axs15231b-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel platform-esp32 esp_lcd_axs15231b driver
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
description: AXS15231B touch controller (I2C side of the combined display+touch chip)
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "axs,axs15231b-touch"
|
||||
|
||||
bus: i2c
|
||||
|
||||
properties:
|
||||
x-max:
|
||||
type: int
|
||||
required: true
|
||||
description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution)
|
||||
y-max:
|
||||
type: int
|
||||
required: true
|
||||
description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution)
|
||||
swap-xy:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Swap the X and Y axes
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Reset GPIO pin
|
||||
pin-interrupt:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Interrupt GPIO pin
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
interrupt-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the interrupt pin is active high
|
||||
@@ -0,0 +1,75 @@
|
||||
description: >
|
||||
AXS15231B display panel (QSPI interface). Combined display+touch controller chip - see
|
||||
axs,axs15231b-touch for the touch side, which sits on a separate I2C bus and is modeled as an
|
||||
independent devicetree node.
|
||||
|
||||
compatible: "axs,axs15231b"
|
||||
|
||||
bus: spi
|
||||
|
||||
properties:
|
||||
horizontal-resolution:
|
||||
type: int
|
||||
required: true
|
||||
description: Horizontal resolution in pixels
|
||||
vertical-resolution:
|
||||
type: int
|
||||
required: true
|
||||
description: Vertical resolution in pixels
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis
|
||||
invert-color:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Invert the panel's color output
|
||||
bgr-order:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Use BGR element order instead of RGB
|
||||
pixel-clock-hz:
|
||||
type: int
|
||||
default: 40000000
|
||||
description: QSPI pixel clock frequency in Hz
|
||||
transaction-queue-depth:
|
||||
type: int
|
||||
default: 10
|
||||
description: Size of the internal SPI transaction queue
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Reset GPIO pin
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
pin-te:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Optional Tearing-Effect GPIO pin. When set, draw_bitmap waits (best-effort, up
|
||||
to 20ms) for a V-blank pulse on this pin before starting each transfer, to reduce visible
|
||||
tearing. Omit to skip TE sync entirely.
|
||||
init-sequence:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
description: >
|
||||
Custom vendor bring-up sequence, flattened into bytes as a run of
|
||||
[cmd, data-length, delay-ms, data-length bytes of data...] entries. Omit to use the
|
||||
AXS15231B component's own built-in default sequence.
|
||||
requires-full-frame:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >
|
||||
Whether this panel needs full-frame-only draws (DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME) -
|
||||
a sub-region draw_bitmap() call desyncs this chip's row auto-increment counter, since its
|
||||
QSPI command set has no row-address command.
|
||||
It's not certain that this is required for all driver implementations, so it's a config option for now.
|
||||
backlight:
|
||||
type: phandle
|
||||
default: "NULL"
|
||||
description: Optional reference to this display's backlight device
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module axs15231b_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axs15231b_display.h>
|
||||
|
||||
// The devicetree compiler derives the expected config typedef name from the compatible
|
||||
// string's suffix (e.g. "axs,axs15231b" -> axs15231b_config_dt), not from the node name or
|
||||
// driver name, so the tag here must match that exactly.
|
||||
DEFINE_DEVICETREE(axs15231b, struct Axs15231bDisplayConfig)
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axs15231b_touch.h>
|
||||
|
||||
// The devicetree compiler derives the expected config typedef name from the compatible
|
||||
// string's suffix (e.g. "axs,axs15231b-touch" -> axs15231b_touch_config_dt), not from the node
|
||||
// name or driver name, so the tag here must match that exactly.
|
||||
DEFINE_DEVICETREE(axs15231b_touch, struct Axs15231bTouchConfig)
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Axs15231bDisplayConfig {
|
||||
uint16_t horizontal_resolution;
|
||||
uint16_t vertical_resolution;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
bool invert_color;
|
||||
bool bgr_order;
|
||||
uint32_t pixel_clock_hz;
|
||||
uint32_t transaction_queue_depth;
|
||||
|
||||
// Reset pin. GPIO_PIN_SPEC_NONE means no reset line is wired up (matches the original
|
||||
// deprecated-HAL config for the boards using this chip so far).
|
||||
struct GpioPinSpec pin_reset;
|
||||
bool reset_active_high;
|
||||
|
||||
// Optional Tearing-Effect GPIO pin. When set (not GPIO_PIN_SPEC_NONE), draw_bitmap() waits
|
||||
// (best-effort, up to 20ms) for a V-blank pulse on this pin before starting each transfer, to
|
||||
// reduce visible tearing. GPIO_PIN_SPEC_NONE skips TE sync entirely.
|
||||
struct GpioPinSpec pin_te;
|
||||
|
||||
// Custom vendor init sequence, flattened as bytes: a run of
|
||||
// [cmd, data_len, delay_ms, data_len bytes of data...] entries. NULL/0 falls back to the
|
||||
// AXS15231B component's own built-in default sequence.
|
||||
const uint8_t* init_sequence;
|
||||
uint32_t init_sequence_length;
|
||||
|
||||
bool requires_full_frame;
|
||||
|
||||
// Optional reference to this display's backlight device, NULL if none.
|
||||
struct Device* backlight;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Axs15231bTouchConfig {
|
||||
// Devicetree address hint. Unused by the driver: the AXS15231B always sits at a fixed
|
||||
// I2C address (see ESP_LCD_TOUCH_IO_I2C_AXS15231B_ADDRESS in esp_lcd_axs15231b.h).
|
||||
uint8_t address;
|
||||
uint16_t x_max;
|
||||
uint16_t y_max;
|
||||
bool swap_xy;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
struct GpioPinSpec pin_reset;
|
||||
struct GpioPinSpec pin_interrupt;
|
||||
bool reset_active_high;
|
||||
bool interrupt_active_high;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,501 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axs15231b_display.h>
|
||||
#include <axs15231b_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/esp32_spi.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_lcd_axs15231b.h>
|
||||
#include <esp_lcd_io_spi.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "AXS15231B"
|
||||
#define GET_CONFIG(device) (static_cast<const Axs15231bDisplayConfig*>((device)->config))
|
||||
|
||||
struct Axs15231bDisplayInternal {
|
||||
esp_lcd_panel_io_handle_t io_handle;
|
||||
esp_lcd_panel_handle_t panel_handle;
|
||||
// Given from ISR context by on_color_trans_done() once a queued transfer physically
|
||||
// completes. draw_bitmap() blocks on this so it can honor DisplayApi's synchronous contract
|
||||
// (see lvgl_display.c: the caller reuses/overwrites the color buffer as soon as draw_bitmap
|
||||
// returns) - esp_lcd_panel_draw_bitmap() itself only queues the transfer and returns early.
|
||||
SemaphoreHandle_t draw_done_semaphore;
|
||||
// Non-null only when a TE (Tearing-Effect) pin is configured. Signaled by te_isr_handler()
|
||||
// on each rising edge, so draw_bitmap() can wait for the next V-blank before transferring.
|
||||
SemaphoreHandle_t te_semaphore;
|
||||
bool te_isr_installed;
|
||||
// Whether we're the one who called gpio_install_isr_service() - if so, we must be the one to
|
||||
// uninstall it, but only if no other pin on the system is still relying on it.
|
||||
bool te_isr_service_installed_by_us;
|
||||
axs15231b_lcd_init_cmd_t* parsed_init_cmds;
|
||||
};
|
||||
|
||||
static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(user_ctx);
|
||||
BaseType_t high_task_woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(internal->draw_done_semaphore, &high_task_woken);
|
||||
return high_task_woken == pdTRUE;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR te_isr_handler(void* arg) {
|
||||
auto* semaphore = static_cast<SemaphoreHandle_t>(arg);
|
||||
BaseType_t high_task_woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(semaphore, &high_task_woken);
|
||||
if (high_task_woken == pdTRUE) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
|
||||
static int pin_or_unused(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
|
||||
}
|
||||
|
||||
static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// Unpacks the devicetree's flat [cmd, data_len, delay_ms, data_len bytes...] encoding (produced
|
||||
// by the devicetree compiler's "array" property type - see init-sequence in
|
||||
// bindings/axs,axs15231b.yaml) into a heap-allocated axs15231b_lcd_init_cmd_t array.
|
||||
static bool parse_init_sequence(const uint8_t* bytes, uint32_t length, axs15231b_lcd_init_cmd_t** out_cmds, uint16_t* out_count) {
|
||||
uint32_t count = 0;
|
||||
for (uint32_t offset = 0; offset < length; count++) {
|
||||
if (offset + 3 > length) {
|
||||
LOG_E(TAG, "init-sequence truncated: entry header runs past the end of the array");
|
||||
return false;
|
||||
}
|
||||
offset += 3 + bytes[offset + 1];
|
||||
if (offset > length) {
|
||||
LOG_E(TAG, "init-sequence truncated: entry data runs past the end of the array");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto* cmds = static_cast<axs15231b_lcd_init_cmd_t*>(malloc(count * sizeof(axs15231b_lcd_init_cmd_t)));
|
||||
if (cmds == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t offset = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint8_t data_len = bytes[offset + 1];
|
||||
cmds[i] = {
|
||||
.cmd = bytes[offset],
|
||||
.data = data_len > 0 ? &bytes[offset + 3] : nullptr,
|
||||
.data_bytes = data_len,
|
||||
.delay_ms = bytes[offset + 2],
|
||||
};
|
||||
offset += 3 + data_len;
|
||||
}
|
||||
|
||||
*out_cmds = cmds;
|
||||
*out_count = (uint16_t)count;
|
||||
return true;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// Best-effort: a TE pin is a hardware refinement, not a requirement, so failures here are logged
|
||||
// and left for the caller to treat as non-fatal (matches the original deprecated-HAL driver,
|
||||
// which continued without TE sync if setup failed).
|
||||
static bool setup_te_sync(Axs15231bDisplayInternal* internal, gpio_num_t te_pin) {
|
||||
if (te_pin == GPIO_NUM_NC) {
|
||||
return true;
|
||||
}
|
||||
|
||||
internal->te_semaphore = xSemaphoreCreateBinary();
|
||||
if (internal->te_semaphore == nullptr) {
|
||||
LOG_E(TAG, "Failed to create TE sync semaphore");
|
||||
return false;
|
||||
}
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << te_pin,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_POSEDGE,
|
||||
};
|
||||
if (gpio_config(&io_conf) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to configure TE GPIO");
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t ret = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
|
||||
if (ret == ESP_OK) {
|
||||
internal->te_isr_service_installed_by_us = true;
|
||||
} else if (ret != ESP_ERR_INVALID_STATE) { // ESP_ERR_INVALID_STATE means it's already installed elsewhere
|
||||
LOG_E(TAG, "Failed to install GPIO ISR service");
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gpio_isr_handler_add(te_pin, te_isr_handler, internal->te_semaphore) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to add TE ISR handler");
|
||||
if (internal->te_isr_service_installed_by_us) {
|
||||
gpio_uninstall_isr_service();
|
||||
internal->te_isr_service_installed_by_us = false;
|
||||
}
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal->te_isr_installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void teardown_te_sync(Axs15231bDisplayInternal* internal, gpio_num_t te_pin) {
|
||||
if (internal->te_isr_installed) {
|
||||
gpio_isr_handler_remove(te_pin);
|
||||
gpio_intr_disable(te_pin);
|
||||
internal->te_isr_installed = false;
|
||||
}
|
||||
if (internal->te_isr_service_installed_by_us) {
|
||||
gpio_uninstall_isr_service();
|
||||
internal->te_isr_service_installed_by_us = false;
|
||||
}
|
||||
if (internal->te_semaphore != nullptr) {
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
|
||||
|
||||
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
struct GpioPinSpec cs_pin;
|
||||
if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to resolve CS pin");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(malloc(sizeof(Axs15231bDisplayInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
internal->te_semaphore = nullptr;
|
||||
internal->te_isr_installed = false;
|
||||
internal->te_isr_service_installed_by_us = false;
|
||||
internal->parsed_init_cmds = nullptr;
|
||||
|
||||
const axs15231b_lcd_init_cmd_t* init_cmds = nullptr;
|
||||
uint16_t init_cmds_size = 0;
|
||||
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
|
||||
if (!parse_init_sequence(config->init_sequence, config->init_sequence_length, &internal->parsed_init_cmds, &init_cmds_size)) {
|
||||
LOG_E(TAG, "Failed to parse init-sequence property");
|
||||
free(internal);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
init_cmds = internal->parsed_init_cmds;
|
||||
}
|
||||
|
||||
internal->draw_done_semaphore = xSemaphoreCreateBinary();
|
||||
if (internal->draw_done_semaphore == nullptr) {
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// AXS15231B is only ever driven over QSPI in this codebase (no plain-SPI/i80 board has shown
|
||||
// up yet), so quad_mode/lcd_cmd_bits/lcd_param_bits are fixed rather than devicetree knobs -
|
||||
// matches AXS15231B_PANEL_IO_QSPI_CONFIG() in esp_lcd_axs15231b.h. dc_gpio_num is unused in
|
||||
// QSPI mode (command/data framing goes over the command byte instead of a DC line).
|
||||
esp_lcd_panel_io_spi_config_t io_config = {
|
||||
.cs_gpio_num = pin_or_unused(cs_pin),
|
||||
.dc_gpio_num = -1,
|
||||
.spi_mode = 3,
|
||||
.pclk_hz = config->pixel_clock_hz,
|
||||
.trans_queue_depth = config->transaction_queue_depth,
|
||||
.on_color_trans_done = on_color_trans_done,
|
||||
.user_ctx = internal,
|
||||
.lcd_cmd_bits = 32,
|
||||
.lcd_param_bits = 8,
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.flags = {
|
||||
.dc_high_on_cmd = 0,
|
||||
.dc_low_on_data = 0,
|
||||
.dc_low_on_param = 0,
|
||||
.octal_mode = 0,
|
||||
.quad_mode = 1,
|
||||
.sio_mode = 0,
|
||||
.lsb_first = 0,
|
||||
.cs_high_active = 0,
|
||||
},
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret));
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
axs15231b_vendor_config_t vendor_config = {
|
||||
.init_cmds = init_cmds,
|
||||
.init_cmds_size = init_cmds_size,
|
||||
.flags = {
|
||||
.use_qspi_interface = 1,
|
||||
},
|
||||
};
|
||||
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = pin_or_unused(config->pin_reset),
|
||||
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
|
||||
.bits_per_pixel = 16,
|
||||
.flags = { .reset_active_high = config->reset_active_high },
|
||||
.vendor_config = &vendor_config,
|
||||
};
|
||||
|
||||
ret = esp_lcd_new_panel_axs15231b(internal->io_handle, &panel_config, &internal->panel_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Bring-up sequence. swap_xy is intentionally not called (and not exposed in DisplayApi below):
|
||||
// It doesn't work on this chip/panel combination.
|
||||
//
|
||||
// esp_lcd_axs15231b's disp_on_off callback is wired up backwards: its body branches on a
|
||||
// parameter it names "off" (true -> DISPOFF, false -> DISPON), but esp_lcd_panel_disp_on_off()
|
||||
// forwards its "on_off" argument straight through with no inversion - so passing true here
|
||||
// actually switches the panel OFF. Pass false to really turn it on (confirmed against the
|
||||
// deleted deprecated-HAL driver, which called this same function with false for the same
|
||||
// reason). See axs15231b_disp_on_off() below, which un-inverts this for DisplayApi callers.
|
||||
//
|
||||
// (note: all of this was tested on guition-jc3248w535c only)
|
||||
bool ok =
|
||||
esp_lcd_panel_reset(internal->panel_handle) == ESP_OK &&
|
||||
esp_lcd_panel_init(internal->panel_handle) == ESP_OK &&
|
||||
esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK &&
|
||||
esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK &&
|
||||
esp_lcd_panel_disp_on_off(internal->panel_handle, false) == ESP_OK;
|
||||
|
||||
if (!ok) {
|
||||
LOG_E(TAG, "Failed to bring up panel");
|
||||
esp_lcd_panel_del(internal->panel_handle);
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (!setup_te_sync(internal, pin_or_nc(config->pin_te))) {
|
||||
LOG_W(TAG, "TE sync setup failed, continuing without TE synchronization");
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
teardown_te_sync(internal, pin_or_nc(config->pin_te));
|
||||
|
||||
if (internal->panel_handle != nullptr) {
|
||||
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete panel");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->panel_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->io_handle != nullptr) {
|
||||
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete panel IO");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region DisplayApi
|
||||
|
||||
static error_t axs15231b_reset(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_init(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
// Best-effort wait for the next V-blank pulse before transferring, to reduce visible tearing.
|
||||
// Non-fatal if it times out (matches the original deprecated-HAL driver) - draw_bitmap()
|
||||
// still has to happen even if TE sync missed its window.
|
||||
if (internal->te_semaphore != nullptr) {
|
||||
xSemaphoreTake(internal->te_semaphore, 0); // drain any already-pending signal
|
||||
xSemaphoreTake(internal->te_semaphore, pdMS_TO_TICKS(20));
|
||||
}
|
||||
|
||||
// Drain any stale signal left over from a prior non-draw transaction (bring-up commands like
|
||||
// reset/init also complete through on_color_trans_done), so the take() below can only be
|
||||
// satisfied by this draw's own transfer completing.
|
||||
xSemaphoreTake(internal->draw_done_semaphore, 0);
|
||||
|
||||
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_mirror(Device* device, bool x_axis, bool y_axis) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static bool axs15231b_get_mirror_x(Device* device) {
|
||||
return GET_CONFIG(device)->mirror_x;
|
||||
}
|
||||
|
||||
static bool axs15231b_get_mirror_y(Device* device) {
|
||||
return GET_CONFIG(device)->mirror_y;
|
||||
}
|
||||
|
||||
// swap_xy/set_gap/disp_sleep are not exposed: swap_xy is confirmed non-functional on this
|
||||
// chip/panel combination on real hardware (see start()'s comment), and the AXS15231B component
|
||||
// doesn't implement set_gap or disp_sleep at all.
|
||||
|
||||
static error_t axs15231b_invert_color(Device* device, bool invert_color_data) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_disp_on_off(Device* device, bool on_off) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
// Inverted: see the comment on the disp_on_off call in start() above.
|
||||
return esp_lcd_panel_disp_on_off(internal->panel_handle, !on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// _SWAPPED (not plain RGB565): the panel expects each 16-bit pixel high-byte-first over the QSPI
|
||||
// bus, but this CPU is little-endian - the original deprecated-HAL driver did the equivalent
|
||||
// byte-swap by hand in its custom flush callback (lv_draw_sw_rgb565_swap()); the generic
|
||||
// lvgl-module bridge does it for us once we report this format (see lvgl_display_map_color_format()).
|
||||
static enum DisplayColorFormat axs15231b_get_color_format(Device*) {
|
||||
return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED;
|
||||
}
|
||||
|
||||
static uint16_t axs15231b_get_resolution_x(Device* device) {
|
||||
return GET_CONFIG(device)->horizontal_resolution;
|
||||
}
|
||||
|
||||
static uint16_t axs15231b_get_resolution_y(Device* device) {
|
||||
return GET_CONFIG(device)->vertical_resolution;
|
||||
}
|
||||
|
||||
static void axs15231b_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
|
||||
*out_buffer = nullptr;
|
||||
}
|
||||
|
||||
static uint8_t axs15231b_get_frame_buffer_count(Device*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static error_t axs15231b_get_backlight(Device* device, Device** backlight) {
|
||||
auto* configured_backlight = GET_CONFIG(device)->backlight;
|
||||
if (configured_backlight == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*backlight = configured_backlight;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// REQUIRES_FULL_FRAME is the only capability that varies per device instance (via the
|
||||
// requires-full-frame devicetree property, see axs15231b_display.h) - a sub-region draw_bitmap()
|
||||
// call desyncs this chip's row auto-increment counter, since its QSPI command set has no
|
||||
// row-address command, but that's been confirmed needed on some boards' wiring/panel combination
|
||||
// and not assumed true for every AXS15231B board (see start()'s notes on what's been tested where).
|
||||
// Every other bit stays fixed for the driver, mirrored here from axs15231b_display_api.capabilities.
|
||||
static bool axs15231b_has_capability(Device* device, uint32_t capability) {
|
||||
uint32_t static_capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
|
||||
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT;
|
||||
if (GET_CONFIG(device)->requires_full_frame) {
|
||||
static_capabilities |= DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME;
|
||||
}
|
||||
return (static_capabilities & capability) == capability;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static const DisplayApi axs15231b_display_api = {
|
||||
// Mirrors axs15231b_has_capability()'s static_capabilities for callers that read this field
|
||||
// directly instead of going through display_has_capability()/has_capability(). Excludes
|
||||
// REQUIRES_FULL_FRAME - see axs15231b_has_capability() above, which is the source of truth.
|
||||
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
|
||||
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT,
|
||||
.reset = axs15231b_reset,
|
||||
.init = axs15231b_init,
|
||||
.draw_bitmap = axs15231b_draw_bitmap,
|
||||
.mirror = axs15231b_mirror,
|
||||
.swap_xy = nullptr,
|
||||
.get_swap_xy = nullptr,
|
||||
.get_mirror_x = axs15231b_get_mirror_x,
|
||||
.get_mirror_y = axs15231b_get_mirror_y,
|
||||
.set_gap = nullptr,
|
||||
.invert_color = axs15231b_invert_color,
|
||||
.disp_on_off = axs15231b_disp_on_off,
|
||||
.disp_sleep = nullptr,
|
||||
.get_color_format = axs15231b_get_color_format,
|
||||
.get_resolution_x = axs15231b_get_resolution_x,
|
||||
.get_resolution_y = axs15231b_get_resolution_y,
|
||||
.get_frame_buffer = axs15231b_get_frame_buffer,
|
||||
.get_frame_buffer_count = axs15231b_get_frame_buffer_count,
|
||||
.get_backlight = axs15231b_get_backlight,
|
||||
.has_capability = axs15231b_has_capability,
|
||||
};
|
||||
|
||||
Driver axs15231b_display_driver = {
|
||||
.name = "axs15231b_display",
|
||||
.compatible = (const char*[]) { "axs,axs15231b", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &axs15231b_display_api,
|
||||
.device_type = &DISPLAY_TYPE,
|
||||
.owner = &axs15231b_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axs15231b_touch.h>
|
||||
#include <axs15231b_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/esp32_i2c.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_lcd_axs15231b.h>
|
||||
#include <esp_lcd_io_i2c.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_touch.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "AXS15231BTouch"
|
||||
#define GET_CONFIG(device) (static_cast<const Axs15231bTouchConfig*>((device)->config))
|
||||
|
||||
struct Axs15231bTouchInternal {
|
||||
esp_lcd_panel_io_handle_t io_handle;
|
||||
esp_lcd_touch_handle_t touch_handle;
|
||||
};
|
||||
|
||||
static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// AXS15231B always sits at a fixed I2C address (ESP_LCD_TOUCH_IO_I2C_AXS15231B_ADDRESS), unlike
|
||||
// GT911's strapping-dependent address, so no bus probing is needed here.
|
||||
static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) {
|
||||
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_AXS15231B_CONFIG();
|
||||
|
||||
auto* parent_driver = device_get_driver(parent);
|
||||
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c")) {
|
||||
auto port = static_cast<const Esp32I2cConfig*>(parent->config)->port;
|
||||
return esp_lcd_new_panel_io_i2c_v1(port, &io_config, out_handle);
|
||||
}
|
||||
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) {
|
||||
auto bus = esp32_i2c_master_get_bus_handle(parent);
|
||||
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(parent);
|
||||
return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, out_handle);
|
||||
}
|
||||
|
||||
LOG_E(TAG, "Unsupported I2C driver");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(malloc(sizeof(Axs15231bTouchInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
esp_lcd_touch_config_t touch_config = {
|
||||
.x_max = config->x_max,
|
||||
.y_max = config->y_max,
|
||||
.rst_gpio_num = pin_or_nc(config->pin_reset),
|
||||
.int_gpio_num = pin_or_nc(config->pin_interrupt),
|
||||
.levels = {
|
||||
.reset = config->reset_active_high ? 1u : 0u,
|
||||
.interrupt = config->interrupt_active_high ? 1u : 0u,
|
||||
},
|
||||
.flags = {
|
||||
.swap_xy = config->swap_xy ? 1u : 0u,
|
||||
.mirror_x = config->mirror_x ? 1u : 0u,
|
||||
.mirror_y = config->mirror_y ? 1u : 0u,
|
||||
},
|
||||
.process_coordinates = nullptr,
|
||||
.interrupt_callback = nullptr,
|
||||
.user_data = nullptr,
|
||||
.driver_data = nullptr,
|
||||
};
|
||||
|
||||
ret = esp_lcd_touch_new_i2c_axs15231b(internal->io_handle, &touch_config, &internal->touch_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create touch handle: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
|
||||
// esp_lcd_touch_del() only releases the touch-side resources; the panel IO handle is owned
|
||||
// separately and needs its own deletion.
|
||||
if (internal->touch_handle != nullptr) {
|
||||
if (esp_lcd_touch_del(internal->touch_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete touch handle");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->touch_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->io_handle != nullptr) {
|
||||
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete panel IO handle");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region PointerApi
|
||||
|
||||
static error_t axs15231b_touch_enter_sleep(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_enter_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_exit_sleep(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_exit_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_read_data(Device* device, TickType_t timeout) {
|
||||
(void)timeout; // esp_lcd_touch_read_data() has no timeout parameter
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_read_data(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static bool axs15231b_touch_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_coordinates(internal->touch_handle, x, y, strength, point_count, max_point_count);
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_set_swap_xy(Device* device, bool swap) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_set_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_get_swap_xy(Device* device, bool* swap) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_set_mirror_x(Device* device, bool mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_set_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_get_mirror_x(Device* device, bool* mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_set_mirror_y(Device* device, bool mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_set_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_get_mirror_y(Device* device, bool* mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static const PointerApi axs15231b_touch_pointer_api = {
|
||||
.enter_sleep = axs15231b_touch_enter_sleep,
|
||||
.exit_sleep = axs15231b_touch_exit_sleep,
|
||||
.read_data = axs15231b_touch_read_data,
|
||||
.get_touched_points = axs15231b_touch_get_touched_points,
|
||||
.set_swap_xy = axs15231b_touch_set_swap_xy,
|
||||
.get_swap_xy = axs15231b_touch_get_swap_xy,
|
||||
.set_mirror_x = axs15231b_touch_set_mirror_x,
|
||||
.get_mirror_x = axs15231b_touch_get_mirror_x,
|
||||
.set_mirror_y = axs15231b_touch_set_mirror_y,
|
||||
.get_mirror_y = axs15231b_touch_get_mirror_y,
|
||||
};
|
||||
|
||||
Driver axs15231b_touch_driver = {
|
||||
.name = "axs15231b_touch",
|
||||
.compatible = (const char*[]) { "axs,axs15231b-touch", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &axs15231b_touch_pointer_api,
|
||||
.device_type = &POINTER_TYPE,
|
||||
.owner = &axs15231b_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver axs15231b_display_driver;
|
||||
extern Driver axs15231b_touch_driver;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&axs15231b_display_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&axs15231b_touch_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&axs15231b_touch_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&axs15231b_display_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module axs15231b_module = {
|
||||
.name = "axs15231b",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(bq25896-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
description: >
|
||||
TI BQ25896 1-cell 3A buck battery charger with power path and PSEL. Creates a "power-supply"
|
||||
child device (see drivers/bq25896.h) exposing ship-mode power-off only - matches the
|
||||
deprecated HAL's Bq25896, which never read charging status/metrics from this chip (that came
|
||||
from a separate fuel gauge, if present - see the bq27220 driver).
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "ti,bq25896"
|
||||
|
||||
bus: i2c
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/bq25896.h>
|
||||
|
||||
DEFINE_DEVICETREE(bq25896, struct Bq25896Config)
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module bq25896_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct Bq25896Config {
|
||||
// Devicetree address hint (0x6B).
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,175 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/bq25896.h>
|
||||
#include <bq25896_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#define TAG "BQ25896"
|
||||
#define GET_CONFIG(device) (static_cast<const Bq25896Config*>((device)->config))
|
||||
// The power-supply child's config pointer isn't set; it reads its settings from its parent
|
||||
// (the bq25896 device) instead - same convention as the sy6970/bq27220 drivers.
|
||||
#define GET_PARENT_CONFIG(device) (static_cast<const Bq25896Config*>(device_get_parent(device)->config))
|
||||
#define GET_I2C_BUS(device) (device_get_parent(device_get_parent(device)))
|
||||
|
||||
static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
|
||||
|
||||
// REG09 bit5: BATFET_DIS. 0 = BATFET on (normal operation), 1 = BATFET off (ship mode).
|
||||
// Register offset is shared with the SY6970 (see that driver's own comment).
|
||||
static constexpr uint8_t REG_09 = 0x09;
|
||||
static constexpr uint8_t REG09_BATFET_DIS = 1u << 5;
|
||||
|
||||
extern "C" {
|
||||
|
||||
// region PowerSupplyApi (power-supply child device)
|
||||
|
||||
// Matches the deprecated HAL's Bq25896: no charging status/metrics were ever read from this
|
||||
// chip (those came from a separate fuel gauge, if present - see the bq27220 driver), so the
|
||||
// only capability exposed here is ship-mode power-off.
|
||||
static bool supports_property(Device*, PowerSupplyProperty) { return false; }
|
||||
static error_t get_property(Device*, PowerSupplyProperty, PowerSupplyPropertyValue*) { return ERROR_NOT_SUPPORTED; }
|
||||
static bool supports_charge_control(Device*) { return false; }
|
||||
static bool is_allowed_to_charge(Device*) { return false; }
|
||||
static error_t set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; }
|
||||
static bool supports_quick_charge(Device*) { return false; }
|
||||
static bool is_quick_charge_enabled(Device*) { return false; }
|
||||
static error_t set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; }
|
||||
|
||||
static bool supports_power_off(Device*) { return true; }
|
||||
|
||||
static error_t power_off(Device* device) {
|
||||
auto* i2c = GET_I2C_BUS(device);
|
||||
const auto* config = GET_PARENT_CONFIG(device);
|
||||
LOG_I(TAG, "Power off (BATFET_DIS)");
|
||||
return i2c_controller_register8_set_bits(i2c, config->address, REG_09, REG09_BATFET_DIS, I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
static constexpr PowerSupplyApi BQ25896_POWER_SUPPLY_API = {
|
||||
.supports_property = supports_property,
|
||||
.get_property = get_property,
|
||||
.supports_charge_control = supports_charge_control,
|
||||
.is_allowed_to_charge = is_allowed_to_charge,
|
||||
.set_allowed_to_charge = set_allowed_to_charge,
|
||||
.supports_quick_charge = supports_quick_charge,
|
||||
.is_quick_charge_enabled = is_quick_charge_enabled,
|
||||
.set_quick_charge_enabled = set_quick_charge_enabled,
|
||||
.supports_power_off = supports_power_off,
|
||||
.power_off = power_off,
|
||||
};
|
||||
|
||||
// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, but
|
||||
// never matched against a devicetree node: bq25896_driver wires it up directly by pointer.
|
||||
Driver bq25896_power_supply_driver = {
|
||||
.name = "bq25896-power-supply",
|
||||
.compatible = (const char*[]) { "bq25896-power-supply", nullptr },
|
||||
.start_device = nullptr,
|
||||
.stop_device = nullptr,
|
||||
.api = &BQ25896_POWER_SUPPLY_API,
|
||||
.device_type = &POWER_SUPPLY_TYPE,
|
||||
.owner = &bq25896_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle (bq25896 device itself)
|
||||
|
||||
struct Bq25896Internal {
|
||||
Device* power_supply_device = nullptr;
|
||||
};
|
||||
|
||||
static error_t create_power_supply_child(Device* parent, Device*& out_child) {
|
||||
auto* child = new (std::nothrow) Device { .address = 0, .name = "bq25896-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr };
|
||||
if (child == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = device_construct(child);
|
||||
if (error != ERROR_NONE) {
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_parent(child, parent);
|
||||
device_set_driver(child, &bq25896_power_supply_driver);
|
||||
|
||||
error = device_add(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
error = device_start(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_remove(child);
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
out_child = child;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static void destroy_power_supply_child(Device* child) {
|
||||
check(device_stop(child) == ERROR_NONE);
|
||||
check(device_remove(child) == ERROR_NONE);
|
||||
check(device_destruct(child) == ERROR_NONE);
|
||||
delete child;
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
// Matches the deprecated HAL's Bq25896::powerOn(): ensure the BATFET is on (normal
|
||||
// operation), in case a previous run left it in ship mode.
|
||||
LOG_I(TAG, "Power on");
|
||||
i2c_controller_register8_reset_bits(parent, config->address, REG_09, REG09_BATFET_DIS, I2C_TIMEOUT);
|
||||
|
||||
auto* internal = new (std::nothrow) Bq25896Internal();
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = create_power_supply_child(device, internal->power_supply_device);
|
||||
if (error != ERROR_NONE) {
|
||||
delete internal;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Bq25896Internal*>(device_get_driver_data(device));
|
||||
destroy_power_supply_child(internal->power_supply_device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver bq25896_driver = {
|
||||
.name = "bq25896",
|
||||
.compatible = (const char*[]) { "ti,bq25896", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = nullptr,
|
||||
.device_type = nullptr,
|
||||
.owner = &bq25896_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver bq25896_driver;
|
||||
extern Driver bq25896_power_supply_driver;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&bq25896_power_supply_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&bq25896_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&bq25896_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&bq25896_power_supply_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module bq25896_module = {
|
||||
.name = "bq25896",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(bq27220-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
description: >
|
||||
Texas Instruments BQ27220 CEDV battery fuel gauge. Creates a "power-supply" child device
|
||||
(see drivers/bq27220.h) exposing battery current, voltage, charge-level and charging status
|
||||
(derived from the BatteryStatus register's DSG/discharge flag); charge control and power-off
|
||||
are not available from this chip alone (they come from a separate charger, if present - see
|
||||
the sy6970 driver).
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "ti,bq27220"
|
||||
|
||||
bus: i2c
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/bq27220.h>
|
||||
|
||||
DEFINE_DEVICETREE(bq27220, struct Bq27220Config)
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module bq27220_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct Bq27220Config {
|
||||
// Devicetree address hint (0x55 on the LilyGO T-Deck Max).
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,214 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/bq27220.h>
|
||||
#include <bq27220_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#define TAG "BQ27220"
|
||||
#define GET_CONFIG(device) (static_cast<const Bq27220Config*>((device)->config))
|
||||
// The power-supply child's config pointer isn't set; it reads its settings from its parent
|
||||
// (the bq27220 device) instead - same convention as the sy6970 driver.
|
||||
#define GET_PARENT_CONFIG(device) (static_cast<const Bq27220Config*>(device_get_parent(device)->config))
|
||||
#define GET_I2C_BUS(device) (device_get_parent(device_get_parent(device)))
|
||||
|
||||
static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
|
||||
|
||||
// BQ27220 standard command registers (16-bit, little-endian on the wire).
|
||||
static constexpr uint8_t BQ27220_REG_VOLTAGE = 0x08;
|
||||
static constexpr uint8_t BQ27220_REG_BATTERY_STATUS = 0x0A;
|
||||
static constexpr uint8_t BQ27220_REG_CURRENT = 0x0C;
|
||||
static constexpr uint8_t BQ27220_REG_STATE_OF_CHARGE = 0x2C;
|
||||
// BatteryStatus bit 0: DSG (device is in DISCHARGE). Mirrors the deprecated HAL's
|
||||
// TpagerPower/TdeckmaxPower fallback: "is charging" = not discharging.
|
||||
static constexpr uint16_t BQ27220_BATTERY_STATUS_DSG = 1u << 0;
|
||||
|
||||
extern "C" {
|
||||
|
||||
// region PowerSupplyApi (power-supply child device)
|
||||
|
||||
static bool supports_property(Device*, PowerSupplyProperty property) {
|
||||
switch (property) {
|
||||
case POWER_SUPPLY_PROP_CURRENT:
|
||||
case POWER_SUPPLY_PROP_VOLTAGE:
|
||||
case POWER_SUPPLY_PROP_CAPACITY:
|
||||
case POWER_SUPPLY_PROP_IS_CHARGING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static error_t get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) {
|
||||
auto* i2c = GET_I2C_BUS(device);
|
||||
const auto* config = GET_PARENT_CONFIG(device);
|
||||
|
||||
switch (property) {
|
||||
case POWER_SUPPLY_PROP_VOLTAGE: {
|
||||
uint16_t voltage_mv = 0;
|
||||
if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_VOLTAGE, &voltage_mv, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
out_value->int_value = voltage_mv;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
case POWER_SUPPLY_PROP_CURRENT: {
|
||||
uint16_t raw = 0;
|
||||
if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_CURRENT, &raw, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
out_value->int_value = static_cast<int16_t>(raw);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
case POWER_SUPPLY_PROP_CAPACITY: {
|
||||
uint16_t soc = 0;
|
||||
if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_STATE_OF_CHARGE, &soc, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
out_value->int_value = soc;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
case POWER_SUPPLY_PROP_IS_CHARGING: {
|
||||
uint16_t status = 0;
|
||||
if (i2c_controller_register16le_get(i2c, config->address, BQ27220_REG_BATTERY_STATUS, &status, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
out_value->int_value = (status & BQ27220_BATTERY_STATUS_DSG) == 0 ? 1 : 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
static bool supports_charge_control(Device*) { return false; }
|
||||
static bool is_allowed_to_charge(Device*) { return false; }
|
||||
static error_t set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; }
|
||||
static bool supports_quick_charge(Device*) { return false; }
|
||||
static bool is_quick_charge_enabled(Device*) { return false; }
|
||||
static error_t set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; }
|
||||
static bool supports_power_off(Device*) { return false; }
|
||||
static error_t power_off(Device*) { return ERROR_NOT_SUPPORTED; }
|
||||
|
||||
static constexpr PowerSupplyApi BQ27220_POWER_SUPPLY_API = {
|
||||
.supports_property = supports_property,
|
||||
.get_property = get_property,
|
||||
.supports_charge_control = supports_charge_control,
|
||||
.is_allowed_to_charge = is_allowed_to_charge,
|
||||
.set_allowed_to_charge = set_allowed_to_charge,
|
||||
.supports_quick_charge = supports_quick_charge,
|
||||
.is_quick_charge_enabled = is_quick_charge_enabled,
|
||||
.set_quick_charge_enabled = set_quick_charge_enabled,
|
||||
.supports_power_off = supports_power_off,
|
||||
.power_off = power_off,
|
||||
};
|
||||
|
||||
// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal, but
|
||||
// never matched against a devicetree node: bq27220_driver wires it up directly by pointer.
|
||||
Driver bq27220_power_supply_driver = {
|
||||
.name = "bq27220-power-supply",
|
||||
.compatible = (const char*[]) { "bq27220-power-supply", nullptr },
|
||||
.start_device = nullptr,
|
||||
.stop_device = nullptr,
|
||||
.api = &BQ27220_POWER_SUPPLY_API,
|
||||
.device_type = &POWER_SUPPLY_TYPE,
|
||||
.owner = &bq27220_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle (bq27220 device itself)
|
||||
|
||||
struct Bq27220Internal {
|
||||
Device* power_supply_device = nullptr;
|
||||
};
|
||||
|
||||
static error_t create_power_supply_child(Device* parent, Device*& out_child) {
|
||||
auto* child = new (std::nothrow) Device { .address = 0, .name = "bq27220-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr };
|
||||
if (child == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = device_construct(child);
|
||||
if (error != ERROR_NONE) {
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_parent(child, parent);
|
||||
device_set_driver(child, &bq27220_power_supply_driver);
|
||||
|
||||
error = device_add(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
error = device_start(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_remove(child);
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
out_child = child;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static void destroy_power_supply_child(Device* child) {
|
||||
check(device_stop(child) == ERROR_NONE);
|
||||
check(device_remove(child) == ERROR_NONE);
|
||||
check(device_destruct(child) == ERROR_NONE);
|
||||
delete child;
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
auto* internal = new (std::nothrow) Bq27220Internal();
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = create_power_supply_child(device, internal->power_supply_device);
|
||||
if (error != ERROR_NONE) {
|
||||
delete internal;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Bq27220Internal*>(device_get_driver_data(device));
|
||||
destroy_power_supply_child(internal->power_supply_device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver bq27220_driver = {
|
||||
.name = "bq27220",
|
||||
.compatible = (const char*[]) { "ti,bq27220", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = nullptr,
|
||||
.device_type = nullptr,
|
||||
.owner = &bq27220_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver bq27220_driver;
|
||||
extern Driver bq27220_power_supply_driver;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&bq27220_power_supply_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&bq27220_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&bq27220_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&bq27220_power_supply_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module bq27220_module = {
|
||||
.name = "bq27220",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(cst328-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
description: >
|
||||
Hynitron CST328 capacitive touch controller (register-compatible with the CST226SE, per
|
||||
LilyGO's SensorLib TouchClassCST226). Supports up to 5 simultaneous touch points and
|
||||
self-reports its panel resolution during bring-up.
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "hynitron,cst328"
|
||||
|
||||
bus: i2c
|
||||
|
||||
properties:
|
||||
swap-xy:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Swap the X and Y axes
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis (against the chip's own self-reported resolution)
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis (against the chip's own self-reported resolution)
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Optional reset GPIO pin. Falls back to a software reset command if not set.
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/cst328.h>
|
||||
|
||||
DEFINE_DEVICETREE(cst328, struct Cst328Config)
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module cst328_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Cst328Config {
|
||||
// Devicetree address hint (0x1A on the LilyGO T-Deck Pro).
|
||||
uint8_t address;
|
||||
bool swap_xy;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
struct GpioPinSpec pin_reset;
|
||||
bool reset_active_high;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,293 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/cst328.h>
|
||||
#include <cst328_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
#define TAG "CST328"
|
||||
#define GET_CONFIG(device) (static_cast<const Cst328Config*>((device)->config))
|
||||
|
||||
static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
|
||||
|
||||
// The CST328/CST226SE protocol addresses registers via a 2-byte [class, sub-register] prefix
|
||||
// (not a plain 1-byte register index), sent as a raw write, optionally followed by a read of the
|
||||
// response - ported from LilyGO's SensorLib TouchClassCST226.cpp (touch/TouchClassCST226.cpp),
|
||||
// the vendor driver actually used for the T-Deck Pro's CST328.
|
||||
static constexpr uint8_t CST328_MAX_POINTS = 5;
|
||||
static constexpr uint8_t STATUS_BUFFER_SIZE = 28;
|
||||
|
||||
struct Cst328Internal {
|
||||
int16_t res_x;
|
||||
int16_t res_y;
|
||||
bool swap_xy;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
uint8_t point_count;
|
||||
int16_t x[CST328_MAX_POINTS];
|
||||
int16_t y[CST328_MAX_POINTS];
|
||||
uint8_t strength[CST328_MAX_POINTS];
|
||||
};
|
||||
|
||||
static bool write2(Device* i2c, uint8_t address, uint8_t b0, uint8_t b1) {
|
||||
uint8_t cmd[2] = { b0, b1 };
|
||||
return i2c_controller_write(i2c, address, cmd, sizeof(cmd), I2C_TIMEOUT) == ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool query(Device* i2c, uint8_t address, uint8_t b0, uint8_t b1, uint8_t* out, size_t out_len) {
|
||||
uint8_t cmd[2] = { b0, b1 };
|
||||
return i2c_controller_write_read(i2c, address, cmd, sizeof(cmd), out, out_len, I2C_TIMEOUT) == ERROR_NONE;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// Some boards wire a dedicated reset GPIO (e.g. the T-Deck Pro's GPIO45); others rely on the
|
||||
// chip's own software reset command instead.
|
||||
static void perform_reset(Device* i2c, uint8_t address, const Cst328Config* config) {
|
||||
if (config->pin_reset.gpio_controller != nullptr) {
|
||||
auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO);
|
||||
if (rst != nullptr) {
|
||||
gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT);
|
||||
gpio_descriptor_set_level(rst, config->reset_active_high);
|
||||
delay_millis(100);
|
||||
gpio_descriptor_set_level(rst, !config->reset_active_high);
|
||||
gpio_descriptor_release(rst);
|
||||
delay_millis(100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
write2(i2c, address, 0xD1, 0x0E);
|
||||
delay_millis(20);
|
||||
}
|
||||
|
||||
// Enters command mode, reads the firmware checkcode and panel resolution, verifies the firmware
|
||||
// is genuinely present, then exits command mode. Ported from TouchClassCST226::initImpl().
|
||||
static bool identify_and_configure(Device* i2c, uint8_t address, Cst328Internal* internal) {
|
||||
if (!write2(i2c, address, 0xD1, 0x01)) {
|
||||
return false;
|
||||
}
|
||||
delay_millis(10);
|
||||
|
||||
uint8_t buffer[8];
|
||||
if (!query(i2c, address, 0xD1, 0xFC, buffer, 4)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t checkcode = (static_cast<uint32_t>(buffer[3]) << 24) | (static_cast<uint32_t>(buffer[2]) << 16) |
|
||||
(static_cast<uint32_t>(buffer[1]) << 8) | buffer[0];
|
||||
|
||||
if (!query(i2c, address, 0xD1, 0xF8, buffer, 4)) {
|
||||
return false;
|
||||
}
|
||||
internal->res_x = static_cast<int16_t>((buffer[1] << 8) | buffer[0]);
|
||||
internal->res_y = static_cast<int16_t>((buffer[3] << 8) | buffer[2]);
|
||||
|
||||
if (!query(i2c, address, 0xD2, 0x08, buffer, 8)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t fw_version = (static_cast<uint32_t>(buffer[3]) << 24) | (static_cast<uint32_t>(buffer[2]) << 16) |
|
||||
(static_cast<uint32_t>(buffer[1]) << 8) | buffer[0];
|
||||
|
||||
if (fw_version == 0xA5A5A5A5) {
|
||||
LOG_E(TAG, "Chip has no firmware");
|
||||
return false;
|
||||
}
|
||||
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
|
||||
LOG_E(TAG, "Firmware info read error");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "CST328 identified: %dx%d, firmware 0x%08lX", internal->res_x, internal->res_y, (unsigned long)fw_version);
|
||||
return write2(i2c, address, 0xD1, 0x09); // exit command mode
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* internal = static_cast<Cst328Internal*>(calloc(1, sizeof(Cst328Internal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
internal->swap_xy = config->swap_xy;
|
||||
internal->mirror_x = config->mirror_x;
|
||||
internal->mirror_y = config->mirror_y;
|
||||
|
||||
perform_reset(parent, config->address, config);
|
||||
|
||||
if (!identify_and_configure(parent, config->address, internal)) {
|
||||
// Non-fatal: matches the tolerant bring-up pattern used by other touch drivers in this
|
||||
// codebase - a transient bus hiccup on the first transaction after boot shouldn't take
|
||||
// the whole kernel down. The device just won't report touches until it responds.
|
||||
LOG_W(TAG, "CST328 identity not confirmed (continuing)");
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Cst328Internal*>(device_get_driver_data(device));
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region PointerApi
|
||||
|
||||
static error_t cst328_enter_sleep(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
return write2(parent, config->address, 0xD1, 0x05) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t cst328_exit_sleep(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
perform_reset(parent, config->address, config);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst328_read_data(Device* device, TickType_t /*timeout*/) {
|
||||
auto* internal = static_cast<Cst328Internal*>(device_get_driver_data(device));
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* parent = device_get_parent(device);
|
||||
|
||||
uint8_t buffer[STATUS_BUFFER_SIZE];
|
||||
uint8_t reg = 0x00;
|
||||
if (i2c_controller_write_read(parent, config->address, ®, 1, buffer, sizeof(buffer), I2C_TIMEOUT) != ERROR_NONE) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Home-button gesture frame (T-Deck Pro doesn't have one wired, but the vendor protocol
|
||||
// reports it): no touch point data.
|
||||
bool is_home_gesture = buffer[0] == 0x83 && buffer[1] == 0x17 && buffer[5] == 0x80;
|
||||
if (is_home_gesture || buffer[6] != 0xAB || buffer[0] == 0xAB || buffer[5] == 0x80) {
|
||||
internal->point_count = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
uint8_t point = buffer[5] & 0x7F;
|
||||
if (point > CST328_MAX_POINTS || point == 0) {
|
||||
write2(parent, config->address, 0x00, 0xAB); // acknowledge/clear
|
||||
internal->point_count = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// A lone point whose status nibble isn't 0x06 is a spurious single-touch frame.
|
||||
uint8_t point1_status = buffer[0] & 0x0F;
|
||||
if (point == 1 && point1_status != 0x06) {
|
||||
point = 0;
|
||||
}
|
||||
|
||||
uint8_t index = 0;
|
||||
for (uint8_t i = 0; i < point; i++) {
|
||||
int16_t raw_x = static_cast<int16_t>((buffer[index + 1] << 4) | ((buffer[index + 3] >> 4) & 0x0F));
|
||||
int16_t raw_y = static_cast<int16_t>((buffer[index + 2] << 4) | (buffer[index + 3] & 0x0F));
|
||||
uint8_t pressure = buffer[index + 4];
|
||||
|
||||
if (internal->swap_xy) {
|
||||
std::swap(raw_x, raw_y);
|
||||
}
|
||||
if (internal->mirror_x && internal->res_x > 0) {
|
||||
raw_x = static_cast<int16_t>(internal->res_x - 1 - raw_x);
|
||||
}
|
||||
if (internal->mirror_y && internal->res_y > 0) {
|
||||
raw_y = static_cast<int16_t>(internal->res_y - 1 - raw_y);
|
||||
}
|
||||
|
||||
internal->x[i] = raw_x;
|
||||
internal->y[i] = raw_y;
|
||||
internal->strength[i] = pressure;
|
||||
|
||||
// First point's slot is 7 bytes wide (id/status + x-hi + y-hi + xy-lo + pressure + 2
|
||||
// reserved); subsequent points are 5 bytes wide.
|
||||
index = static_cast<uint8_t>((i == 0) ? (index + 7) : (index + 5));
|
||||
}
|
||||
internal->point_count = point;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool cst328_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
|
||||
auto* internal = static_cast<Cst328Internal*>(device_get_driver_data(device));
|
||||
uint8_t count = internal->point_count < max_point_count ? internal->point_count : max_point_count;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
x[i] = static_cast<uint16_t>(internal->x[i]);
|
||||
y[i] = static_cast<uint16_t>(internal->y[i]);
|
||||
if (strength != nullptr) {
|
||||
strength[i] = internal->strength[i];
|
||||
}
|
||||
}
|
||||
*point_count = count;
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
static error_t cst328_set_swap_xy(Device* device, bool swap) {
|
||||
static_cast<Cst328Internal*>(device_get_driver_data(device))->swap_xy = swap;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst328_get_swap_xy(Device* device, bool* swap) {
|
||||
*swap = static_cast<Cst328Internal*>(device_get_driver_data(device))->swap_xy;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst328_set_mirror_x(Device* device, bool mirror) {
|
||||
static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_x = mirror;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst328_get_mirror_x(Device* device, bool* mirror) {
|
||||
*mirror = static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_x;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst328_set_mirror_y(Device* device, bool mirror) {
|
||||
static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_y = mirror;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst328_get_mirror_y(Device* device, bool* mirror) {
|
||||
*mirror = static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_y;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static const PointerApi cst328_pointer_api = {
|
||||
.enter_sleep = cst328_enter_sleep,
|
||||
.exit_sleep = cst328_exit_sleep,
|
||||
.read_data = cst328_read_data,
|
||||
.get_touched_points = cst328_get_touched_points,
|
||||
.set_swap_xy = cst328_set_swap_xy,
|
||||
.get_swap_xy = cst328_get_swap_xy,
|
||||
.set_mirror_x = cst328_set_mirror_x,
|
||||
.get_mirror_x = cst328_get_mirror_x,
|
||||
.set_mirror_y = cst328_set_mirror_y,
|
||||
.get_mirror_y = cst328_get_mirror_y,
|
||||
};
|
||||
|
||||
Driver cst328_driver = {
|
||||
.name = "cst328",
|
||||
.compatible = (const char*[]) { "hynitron,cst328", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &cst328_pointer_api,
|
||||
.device_type = &POINTER_TYPE,
|
||||
.owner = &cst328_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver cst328_driver;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&cst328_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&cst328_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module cst328_module = {
|
||||
.name = "cst328",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(cst66xx-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
description: >
|
||||
Hynitron CST66xx capacitive touch controller. Vendor docs for the LilyGO T-Deck Max label
|
||||
this chip "CST328", but the vendor HynTouch driver probes cst66xx first and that is what
|
||||
answers on that board. This is a minimal, single-touch port of the vendor's
|
||||
hyn_cst66xx.c protocol.
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "hynitron,cst66xx"
|
||||
|
||||
bus: i2c
|
||||
|
||||
properties:
|
||||
x-max:
|
||||
type: int
|
||||
required: true
|
||||
description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution)
|
||||
y-max:
|
||||
type: int
|
||||
required: true
|
||||
description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution)
|
||||
swap-xy:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Swap the X and Y axes
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis
|
||||
pin-reset:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Reset GPIO pin. The chip only re-initializes into normal reporting mode after a clean reset pulse.
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/cst66xx.h>
|
||||
|
||||
DEFINE_DEVICETREE(cst66xx, struct Cst66xxConfig)
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module cst66xx_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Cst66xxConfig {
|
||||
// Devicetree address hint (0x1A on the LilyGO T-Deck Max).
|
||||
uint8_t address;
|
||||
uint16_t x_max;
|
||||
uint16_t y_max;
|
||||
bool swap_xy;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
struct GpioPinSpec pin_reset;
|
||||
bool reset_active_high;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,266 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/cst66xx.h>
|
||||
#include <cst66xx_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/gpio_descriptor.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
#define TAG "CST66xx"
|
||||
#define GET_CONFIG(device) (static_cast<const Cst66xxConfig*>((device)->config))
|
||||
|
||||
static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20);
|
||||
|
||||
// The protocol uses 4-byte register addresses, ported from the vendor's lib/HynTouch/src/hyn_cst66xx.c
|
||||
// (see Devices/lilygo-tdeck-max's former Source/devices/Cst66xxTouch.cpp for the pre-migration history).
|
||||
|
||||
// Normal-mode setup sequence (cst66xx_set_workmode, NOMAL_MODE).
|
||||
static constexpr uint8_t CMD_DISABLE_LP_I2C[4] = { 0xD0, 0x00, 0x04, 0x00 };
|
||||
static constexpr uint8_t CMD_NORMAL_A[4] = { 0xD0, 0x00, 0x00, 0x00 };
|
||||
static constexpr uint8_t CMD_NORMAL_B[4] = { 0xD0, 0x00, 0x0C, 0x00 };
|
||||
static constexpr uint8_t CMD_NORMAL_C[4] = { 0xD0, 0x00, 0x01, 0x00 };
|
||||
// Read-point register: write these 4 bytes, then read the touch frame.
|
||||
static constexpr uint8_t REG_READ_POINT[4] = { 0xD0, 0x07, 0x00, 0x00 };
|
||||
// Acknowledge/clear after each read.
|
||||
static constexpr uint8_t CMD_ACK[4] = { 0xD0, 0x00, 0x02, 0xAB };
|
||||
// Identity/config register (cst66xx_updata_tpinfo): read 50 bytes; buf[2]==0xCA && buf[3]==0xCA confirms a CST66xx.
|
||||
static constexpr uint8_t REG_INFO[4] = { 0xD0, 0x03, 0x00, 0x00 };
|
||||
|
||||
struct Cst66xxInternal {
|
||||
int16_t last_x;
|
||||
int16_t last_y;
|
||||
bool has_point;
|
||||
bool swap_xy;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
};
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
static void set_normal_mode(Device* i2c, uint8_t address) {
|
||||
i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT);
|
||||
delay_millis(1);
|
||||
i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT);
|
||||
i2c_controller_write(i2c, address, CMD_NORMAL_A, sizeof(CMD_NORMAL_A), I2C_TIMEOUT);
|
||||
i2c_controller_write(i2c, address, CMD_NORMAL_B, sizeof(CMD_NORMAL_B), I2C_TIMEOUT);
|
||||
i2c_controller_write(i2c, address, CMD_NORMAL_C, sizeof(CMD_NORMAL_C), I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
static void perform_hardware_reset(const Cst66xxConfig* config) {
|
||||
if (config->pin_reset.gpio_controller == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO);
|
||||
if (rst == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT);
|
||||
gpio_descriptor_set_level(rst, config->reset_active_high);
|
||||
delay_millis(10);
|
||||
gpio_descriptor_set_level(rst, !config->reset_active_high);
|
||||
gpio_descriptor_release(rst);
|
||||
// The reset pulse exits boot mode; give the controller time to re-init.
|
||||
delay_millis(80);
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* internal = static_cast<Cst66xxInternal*>(malloc(sizeof(Cst66xxInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
internal->last_x = 0;
|
||||
internal->last_y = 0;
|
||||
internal->has_point = false;
|
||||
internal->swap_xy = config->swap_xy;
|
||||
internal->mirror_x = config->mirror_x;
|
||||
internal->mirror_y = config->mirror_y;
|
||||
|
||||
perform_hardware_reset(config);
|
||||
|
||||
// Put the controller into normal reporting mode and verify the identity. The first read
|
||||
// after reset can miss, so retry like the vendor's cst66xx_updata_tpinfo (read 0xD0030000,
|
||||
// expect buf[2]==buf[3]==0xCA).
|
||||
bool confirmed = false;
|
||||
for (int attempt = 0; attempt < 5 && !confirmed; ++attempt) {
|
||||
set_normal_mode(parent, config->address);
|
||||
uint8_t info[50] = {};
|
||||
if (i2c_controller_write(parent, config->address, REG_INFO, sizeof(REG_INFO), I2C_TIMEOUT) == ERROR_NONE &&
|
||||
i2c_controller_read(parent, config->address, info, sizeof(info), I2C_TIMEOUT) == ERROR_NONE &&
|
||||
info[2] == 0xCA && info[3] == 0xCA) {
|
||||
confirmed = true;
|
||||
} else {
|
||||
delay_millis(10);
|
||||
}
|
||||
}
|
||||
if (confirmed) {
|
||||
LOG_I(TAG, "CST66xx initialised");
|
||||
} else {
|
||||
LOG_W(TAG, "CST66xx identity not confirmed (continuing)");
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Cst66xxInternal*>(device_get_driver_data(device));
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region PointerApi
|
||||
|
||||
static error_t cst66xx_read_data(Device* device, TickType_t /*timeout*/) {
|
||||
auto* internal = static_cast<Cst66xxInternal*>(device_get_driver_data(device));
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* parent = device_get_parent(device);
|
||||
|
||||
// CST66xx frame (cst66xx_report): write the read-point register, then read 9 bytes.
|
||||
// buf[2]=report type (0xFF=position/key), buf[3] low nibble = finger count, high nibble =
|
||||
// key count. The first 5-byte slot (buf[4..8]) is a key slot when key count > 0, otherwise
|
||||
// the first finger; further fingers, if any, follow at buf[index+...].
|
||||
uint8_t buf[9] = {};
|
||||
error_t err = i2c_controller_write(parent, config->address, REG_READ_POINT, sizeof(REG_READ_POINT), I2C_TIMEOUT);
|
||||
if (err == ERROR_NONE) {
|
||||
err = i2c_controller_read(parent, config->address, buf, sizeof(buf), I2C_TIMEOUT);
|
||||
}
|
||||
if (err != ERROR_NONE) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Acknowledge/clear the frame after every read.
|
||||
i2c_controller_write(parent, config->address, CMD_ACK, sizeof(CMD_ACK), I2C_TIMEOUT);
|
||||
|
||||
const uint8_t reportType = buf[2];
|
||||
const uint8_t fingerCount = buf[3] & 0x0F;
|
||||
const uint8_t keyCount = (buf[3] & 0xF0) >> 4;
|
||||
|
||||
// The controller also reports the three bezel touch-keys (heart/speech-bubble/paper-airplane)
|
||||
// mutually exclusively with finger coordinates in this same frame shape. Kernel drivers don't
|
||||
// have a home for UI-navigation policy (see the CST66xx driver's module README), so key frames
|
||||
// are just treated as "no touch" here.
|
||||
if ((reportType == 0xFF && keyCount > 0) || reportType != 0xFF || fingerCount < 1) {
|
||||
internal->has_point = false;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// First finger's slot follows any key slots.
|
||||
const uint8_t index = keyCount * 5;
|
||||
const uint8_t event = buf[index + 8] >> 4; // 0 = up
|
||||
if (event == 0) {
|
||||
internal->has_point = false;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
int16_t rawX = static_cast<int16_t>(buf[index + 4] | (static_cast<uint16_t>(buf[index + 7] & 0x0F) << 8));
|
||||
int16_t rawY = static_cast<int16_t>(buf[index + 5] | (static_cast<uint16_t>(buf[index + 7] & 0xF0) << 4));
|
||||
|
||||
if (internal->swap_xy) {
|
||||
std::swap(rawX, rawY);
|
||||
}
|
||||
if (internal->mirror_x) {
|
||||
rawX = static_cast<int16_t>(config->x_max - 1 - rawX);
|
||||
}
|
||||
if (internal->mirror_y) {
|
||||
rawY = static_cast<int16_t>(config->y_max - 1 - rawY);
|
||||
}
|
||||
|
||||
internal->last_x = rawX;
|
||||
internal->last_y = rawY;
|
||||
internal->has_point = true;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool cst66xx_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
|
||||
auto* internal = static_cast<Cst66xxInternal*>(device_get_driver_data(device));
|
||||
if (!internal->has_point || max_point_count < 1) {
|
||||
*point_count = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
x[0] = static_cast<uint16_t>(internal->last_x);
|
||||
y[0] = static_cast<uint16_t>(internal->last_y);
|
||||
if (strength != nullptr) {
|
||||
strength[0] = 0;
|
||||
}
|
||||
*point_count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static error_t cst66xx_set_swap_xy(Device* device, bool swap) {
|
||||
static_cast<Cst66xxInternal*>(device_get_driver_data(device))->swap_xy = swap;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst66xx_get_swap_xy(Device* device, bool* swap) {
|
||||
*swap = static_cast<Cst66xxInternal*>(device_get_driver_data(device))->swap_xy;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst66xx_set_mirror_x(Device* device, bool mirror) {
|
||||
static_cast<Cst66xxInternal*>(device_get_driver_data(device))->mirror_x = mirror;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst66xx_get_mirror_x(Device* device, bool* mirror) {
|
||||
*mirror = static_cast<Cst66xxInternal*>(device_get_driver_data(device))->mirror_x;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst66xx_set_mirror_y(Device* device, bool mirror) {
|
||||
static_cast<Cst66xxInternal*>(device_get_driver_data(device))->mirror_y = mirror;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t cst66xx_get_mirror_y(Device* device, bool* mirror) {
|
||||
*mirror = static_cast<Cst66xxInternal*>(device_get_driver_data(device))->mirror_y;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// enter_sleep/exit_sleep are not exposed: the ported protocol has no equivalent commands.
|
||||
|
||||
// endregion
|
||||
|
||||
static const PointerApi cst66xx_pointer_api = {
|
||||
.enter_sleep = nullptr,
|
||||
.exit_sleep = nullptr,
|
||||
.read_data = cst66xx_read_data,
|
||||
.get_touched_points = cst66xx_get_touched_points,
|
||||
.set_swap_xy = cst66xx_set_swap_xy,
|
||||
.get_swap_xy = cst66xx_get_swap_xy,
|
||||
.set_mirror_x = cst66xx_set_mirror_x,
|
||||
.get_mirror_x = cst66xx_get_mirror_x,
|
||||
.set_mirror_y = cst66xx_set_mirror_y,
|
||||
.get_mirror_y = cst66xx_get_mirror_y,
|
||||
};
|
||||
|
||||
Driver cst66xx_driver = {
|
||||
.name = "cst66xx",
|
||||
.compatible = (const char*[]) { "hynitron,cst66xx", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &cst66xx_pointer_api,
|
||||
.device_type = &POINTER_TYPE,
|
||||
.owner = &cst66xx_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver cst66xx_driver;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&cst66xx_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&cst66xx_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module cst66xx_module = {
|
||||
.name = "cst66xx",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(drv2605-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
description: >
|
||||
Texas Instruments DRV2605/DRV2604 (and low-voltage L-variant) haptic motor driver for ERM/LRA
|
||||
actuators, with an onboard waveform effect library. Brought up in open-loop ERM mode (matches
|
||||
the deprecated HAL's Drv2605 - closed-loop/LRA configuration isn't ported). Exposes a generic
|
||||
"haptic" device (see tactility/drivers/haptic.h) for playing further effects at runtime.
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "ti,drv2605"
|
||||
|
||||
bus: i2c
|
||||
|
||||
properties:
|
||||
autoplay:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Play startup-waveform once at start-up (e.g. a startup "buzz" to confirm the motor works)
|
||||
startup-waveform:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
description: >
|
||||
Effect sequence (waveform library indices, 0 ends the sequence) played once at start-up
|
||||
when autoplay is set. Up to 8 bytes. Omit for the default 3x strong-click "buzz" ({1,1,1}).
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/drv2605.h>
|
||||
|
||||
DEFINE_DEVICETREE(drv2605, struct Drv2605Config)
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct Drv2605Config {
|
||||
// Devicetree address hint (0x5A).
|
||||
uint8_t address;
|
||||
bool autoplay;
|
||||
// WaveSequence1.. effect slots to play once at start-up when autoplay is set. NULL/0-length
|
||||
// falls back to a 3x strong-click "buzz" ({1,1,1}).
|
||||
const uint8_t* startup_waveform;
|
||||
uint32_t startup_waveform_length;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module drv2605_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/drv2605.h>
|
||||
#include <drv2605_module.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/haptic.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#define TAG "DRV2605"
|
||||
#define GET_CONFIG(device) (static_cast<const Drv2605Config*>((device)->config))
|
||||
|
||||
static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
|
||||
|
||||
// DRV260x register map (ported from the deprecated HAL's Drv2605.h/.cpp).
|
||||
static constexpr uint8_t REG_STATUS = 0x00;
|
||||
static constexpr uint8_t REG_MODE = 0x01;
|
||||
static constexpr uint8_t REG_REALTIME_PLAYBACK_INPUT = 0x02;
|
||||
static constexpr uint8_t REG_WAVE_LIBRARY_SELECT = 0x03;
|
||||
static constexpr uint8_t REG_WAVE_SEQUENCE_1 = 0x04; // WaveSequence1..8 = 0x04..0x0B
|
||||
static constexpr uint8_t REG_GO = 0x0C;
|
||||
static constexpr uint8_t REG_OVERDRIVE_TIME_OFFSET = 0x0D;
|
||||
static constexpr uint8_t REG_SUSTAIN_TIME_OFFSET_POSITIVE = 0x0E;
|
||||
static constexpr uint8_t REG_SUSTAIN_TIME_OFFSET_NEGATIVE = 0x0F;
|
||||
static constexpr uint8_t REG_BRAKE_TIME_OFFSET = 0x10;
|
||||
static constexpr uint8_t REG_AUDIO_INPUT_LEVEL_MAX = 0x13;
|
||||
static constexpr uint8_t REG_FEEDBACK = 0x1A;
|
||||
static constexpr uint8_t REG_CONTROL3 = 0x1D;
|
||||
|
||||
static constexpr uint8_t FEEDBACK_N_ERM_LRA = 1u << 7;
|
||||
static constexpr uint8_t CONTROL3_ERM_OPEN_LOOP = 1u << 5;
|
||||
|
||||
// 3x strong-click "buzz", matching the deprecated HAL's Drv2605::setWaveFormForBuzz().
|
||||
static constexpr uint8_t DEFAULT_STARTUP_WAVEFORM[] = { 1, 1, 1, 0 };
|
||||
|
||||
extern "C" {
|
||||
|
||||
// region HapticApi
|
||||
|
||||
static error_t set_waveform(Device* device, uint8_t slot, uint8_t waveform) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* i2c = device_get_parent(device);
|
||||
return i2c_controller_register8_set(i2c, config->address, static_cast<uint8_t>(REG_WAVE_SEQUENCE_1 + slot), waveform, I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
static error_t select_library(Device* device, uint8_t library) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* i2c = device_get_parent(device);
|
||||
return i2c_controller_register8_set(i2c, config->address, REG_WAVE_LIBRARY_SELECT, library, I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
static error_t start_playback(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* i2c = device_get_parent(device);
|
||||
return i2c_controller_register8_set(i2c, config->address, REG_GO, 0x01, I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
static error_t stop_playback(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* i2c = device_get_parent(device);
|
||||
return i2c_controller_register8_set(i2c, config->address, REG_GO, 0x00, I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
static constexpr HapticApi DRV2605_HAPTIC_API = {
|
||||
.set_waveform = set_waveform,
|
||||
.select_library = select_library,
|
||||
.start_playback = start_playback,
|
||||
.stop_playback = stop_playback,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
static error_t write_waveform_sequence(Device* i2c, uint8_t address, const uint8_t* waveform, uint32_t length) {
|
||||
for (uint32_t i = 0; i < length && i < 8; i++) {
|
||||
error_t error = i2c_controller_register8_set(i2c, address, static_cast<uint8_t>(REG_WAVE_SEQUENCE_1 + i), waveform[i], I2C_TIMEOUT);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* i2c = device_get_parent(device);
|
||||
check(device_get_type(i2c) == &I2C_CONTROLLER_TYPE);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
uint8_t status = 0;
|
||||
if (i2c_controller_register8_get(i2c, config->address, REG_STATUS, &status, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to read status");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Bits[7:5]: 3=DRV2605, 4=DRV2604, 6=DRV2604L, 7=DRV2605L.
|
||||
uint8_t chip_id = status >> 5;
|
||||
if (chip_id != 3 && chip_id != 4 && chip_id != 6 && chip_id != 7) {
|
||||
LOG_E(TAG, "Unknown chip id %02X", chip_id);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
i2c_controller_register8_set(i2c, config->address, REG_MODE, 0x00, I2C_TIMEOUT); // Exit standby
|
||||
i2c_controller_register8_set(i2c, config->address, REG_REALTIME_PLAYBACK_INPUT, 0x00, I2C_TIMEOUT); // Disable
|
||||
|
||||
// Baseline single strong-click sequence (matches Drv2605::setWaveFormForClick()).
|
||||
write_waveform_sequence(i2c, config->address, (const uint8_t[]) { 1, 0 }, 2);
|
||||
i2c_controller_register8_set(i2c, config->address, REG_OVERDRIVE_TIME_OFFSET, 0x00, I2C_TIMEOUT);
|
||||
i2c_controller_register8_set(i2c, config->address, REG_SUSTAIN_TIME_OFFSET_POSITIVE, 0x00, I2C_TIMEOUT);
|
||||
i2c_controller_register8_set(i2c, config->address, REG_SUSTAIN_TIME_OFFSET_NEGATIVE, 0x00, I2C_TIMEOUT);
|
||||
i2c_controller_register8_set(i2c, config->address, REG_BRAKE_TIME_OFFSET, 0x00, I2C_TIMEOUT);
|
||||
i2c_controller_register8_set(i2c, config->address, REG_AUDIO_INPUT_LEVEL_MAX, 0x64, I2C_TIMEOUT);
|
||||
|
||||
// ERM open loop.
|
||||
i2c_controller_register8_reset_bits(i2c, config->address, REG_FEEDBACK, FEEDBACK_N_ERM_LRA, I2C_TIMEOUT);
|
||||
i2c_controller_register8_set_bits(i2c, config->address, REG_CONTROL3, CONTROL3_ERM_OPEN_LOOP, I2C_TIMEOUT);
|
||||
|
||||
if (config->autoplay) {
|
||||
const uint8_t* waveform = config->startup_waveform;
|
||||
uint32_t length = config->startup_waveform_length;
|
||||
if (waveform == nullptr || length == 0) {
|
||||
waveform = DEFAULT_STARTUP_WAVEFORM;
|
||||
length = sizeof(DEFAULT_STARTUP_WAVEFORM);
|
||||
}
|
||||
write_waveform_sequence(i2c, config->address, waveform, length);
|
||||
i2c_controller_register8_set(i2c, config->address, REG_GO, 0x01, I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device*) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
Driver drv2605_driver = {
|
||||
.name = "drv2605",
|
||||
.compatible = (const char*[]) { "ti,drv2605", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &DRV2605_HAPTIC_API,
|
||||
.device_type = &HAPTIC_TYPE,
|
||||
.owner = &drv2605_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver drv2605_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&drv2605_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&drv2605_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module drv2605_module = {
|
||||
.name = "drv2605",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(jd9165-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel platform-esp32 esp_lcd_jd9165 esp_lcd driver esp_hw_support
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
description: >
|
||||
JDI JD9165 MIPI-DSI display panel, driven over ESP-IDF's esp_lcd_jd9165 component (DPI/DBI
|
||||
interface, ESP32-P4 and other SOC_MIPI_DSI_SUPPORTED targets only). Owns the MIPI DSI PHY LDO
|
||||
channel and DSI bus directly, so unlike SPI/RGB panels it has no parent bus controller device.
|
||||
|
||||
compatible: "jdi,jd9165"
|
||||
|
||||
properties:
|
||||
horizontal-resolution:
|
||||
type: int
|
||||
required: true
|
||||
description: Horizontal resolution in pixels
|
||||
vertical-resolution:
|
||||
type: int
|
||||
required: true
|
||||
description: Vertical resolution in pixels
|
||||
bits-per-pixel:
|
||||
type: int
|
||||
default: 16
|
||||
description: Color depth in bits per pixel (16, 18 or 24)
|
||||
bgr-order:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Use BGR element order instead of RGB
|
||||
invert-color:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Invert the panel's color output
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Reset GPIO pin. Falls back to a software reset (sent over the DBI command
|
||||
interface) if not set.
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
ldo-channel:
|
||||
type: int
|
||||
required: true
|
||||
description: LDO channel index powering the MIPI DSI PHY (chan_id in esp_ldo_channel_config_t)
|
||||
ldo-voltage-mv:
|
||||
type: int
|
||||
required: true
|
||||
description: Voltage to supply to the MIPI DSI PHY LDO channel, in mV
|
||||
dsi-bus-id:
|
||||
type: int
|
||||
default: 0
|
||||
description: Which DSI controller to use, index from 0
|
||||
num-data-lanes:
|
||||
type: int
|
||||
default: 2
|
||||
description: Number of MIPI DSI data lanes. 0 falls back to the maximum available.
|
||||
lane-bit-rate-mbps:
|
||||
type: int
|
||||
required: true
|
||||
description: MIPI DSI lane bit rate, in Mbps
|
||||
dpi-clock-freq-mhz:
|
||||
type: int
|
||||
required: true
|
||||
description: MIPI DPI clock frequency, in MHz
|
||||
hsync-pulse-width:
|
||||
type: int
|
||||
required: true
|
||||
description: Horizontal sync width, in PCLK periods
|
||||
hsync-back-porch:
|
||||
type: int
|
||||
required: true
|
||||
description: Number of PCLK periods between hsync and the start of line active data
|
||||
hsync-front-porch:
|
||||
type: int
|
||||
required: true
|
||||
description: Number of PCLK periods between the end of active data and the next hsync
|
||||
vsync-pulse-width:
|
||||
type: int
|
||||
required: true
|
||||
description: Vertical sync width, in lines
|
||||
vsync-back-porch:
|
||||
type: int
|
||||
required: true
|
||||
description: Number of invalid lines between vsync and the start of the frame
|
||||
vsync-front-porch:
|
||||
type: int
|
||||
required: true
|
||||
description: Number of invalid lines between the end of the frame and the next vsync
|
||||
num-fbs:
|
||||
type: int
|
||||
default: 1
|
||||
description: Number of screen-sized frame buffers to allocate (0 or 1 = single-buffered)
|
||||
use-dma2d:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Use DMA2D to copy user buffers into the frame buffer when necessary (only
|
||||
meaningful on SOC_DMA2D_SUPPORTED targets - must be false otherwise)
|
||||
disable-lp:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Disable MIPI DSI low-power mode
|
||||
allow-tearing:
|
||||
type: boolean
|
||||
default: false
|
||||
description: By default, when draw_bitmap's color_data is one of the panel's own frame
|
||||
buffers (i.e. LVGL is bound directly onto them), draw_bitmap waits for a full scan-out to
|
||||
complete before returning so the caller can't start overwriting a buffer still being
|
||||
displayed. Set this to trade away that tear-free guarantee for lower latency (e.g. when
|
||||
other tasks occasionally block timing for long enough that waiting causes visible stalls).
|
||||
init-sequence:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
description: >
|
||||
Custom vendor bring-up sequence, flattened into bytes as a run of
|
||||
[cmd, data-length, delay-ms, data-length bytes of data...] entries, e.g.
|
||||
`init-sequence = [0x11 0 120 0x29 0 20];`.
|
||||
Omit to use the JD9165 component's own built-in default sequence.
|
||||
backlight:
|
||||
type: phandle
|
||||
default: "NULL"
|
||||
description: Optional reference to this display's backlight device
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/jd9165.h>
|
||||
|
||||
// The devicetree compiler derives the expected config typedef name from the compatible
|
||||
// string's suffix (e.g. "jdi,jd9165" -> jd9165_config_dt), not from the node name or driver
|
||||
// name, so the tag here must match that exactly.
|
||||
DEFINE_DEVICETREE(jd9165, struct Jd9165Config)
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Jd9165Config {
|
||||
uint16_t horizontal_resolution;
|
||||
uint16_t vertical_resolution;
|
||||
uint8_t bits_per_pixel;
|
||||
bool bgr_order;
|
||||
bool invert_color;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
|
||||
// Reset pin for the panel. GPIO_PIN_SPEC_NONE falls back to a software reset sent over the
|
||||
// DBI command interface (see esp_lcd_jd9165's panel_jd9165_reset()).
|
||||
struct GpioPinSpec pin_reset;
|
||||
bool reset_active_high;
|
||||
|
||||
// LDO channel powering the MIPI DSI PHY - the PHY has no power of its own until this is
|
||||
// acquired, so it must happen before the DSI bus is created. Both fields are int32_t (not
|
||||
// uint32_t) to exactly match esp_ldo_channel_config_t's plain `int` fields - assigning a
|
||||
// uint32_t into that struct's brace-init would be a narrowing conversion (-Werror=narrowing).
|
||||
int32_t ldo_channel;
|
||||
int32_t ldo_voltage_mv;
|
||||
|
||||
uint8_t dsi_bus_id;
|
||||
// Number of MIPI DSI data lanes. 0 falls back to the maximum available.
|
||||
uint8_t num_data_lanes;
|
||||
uint32_t lane_bit_rate_mbps;
|
||||
|
||||
uint32_t dpi_clock_freq_mhz;
|
||||
uint32_t hsync_pulse_width;
|
||||
uint32_t hsync_back_porch;
|
||||
uint32_t hsync_front_porch;
|
||||
uint32_t vsync_pulse_width;
|
||||
uint32_t vsync_back_porch;
|
||||
uint32_t vsync_front_porch;
|
||||
// Number of screen-sized frame buffers the driver allocates (0 or 1 = single-buffered).
|
||||
uint8_t num_fbs;
|
||||
bool use_dma2d;
|
||||
bool disable_lp;
|
||||
|
||||
// See the 'allow-tearing' binding property.
|
||||
bool allow_tearing;
|
||||
|
||||
// Custom vendor init sequence, flattened as bytes: a run of
|
||||
// [cmd, data_len, delay_ms, data_len bytes of data...] entries. NULL/0 falls back to the
|
||||
// JD9165 component's own built-in default sequence (see vendor_specific_init_default in
|
||||
// esp_lcd_jd9165.c) - just a sleep-out + display-on, not guaranteed to match any particular
|
||||
// panel's actual bring-up requirements.
|
||||
const uint8_t* init_sequence;
|
||||
uint32_t init_sequence_length;
|
||||
|
||||
// Optional reference to this display's backlight device, NULL if none.
|
||||
struct Device* backlight;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user