Implemented LilyGO T-Lora Pager (#295)

This commit is contained in:
flip
2025-07-23 20:45:57 +00:00
committed by GitHub
parent ab4cf79a47
commit 00b62a2831
36 changed files with 2013 additions and 9 deletions
+4 -4
View File
@@ -8,10 +8,10 @@
* https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads
*/
namespace registers {
static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl
static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control
static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status
static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status
static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl
static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control
static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status
static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status
} // namespace registers
bool Bq24295::readChargeTermination(uint8_t& out) const {
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility
)
+6
View File
@@ -0,0 +1,6 @@
# 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)
+370
View File
@@ -0,0 +1,370 @@
#include "Bq27220.h"
#include <Tactility/Log.h>
#include "esp_sleep.h"
#define 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)) {
TT_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)) {
TT_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(Bq27220::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);
}
TT_LOG_E(TAG, "Subcommand x%X 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)) {
TT_LOG_E(TAG, "Cannot read initial operation status!");
return false;
}
if (status.reg.SEC == OperationStatusSecSealed) {
isSealed = true;
if (!unsealDevice()) {
TT_LOG_E(TAG, "Unsealing device failure!");
return false;
}
}
if (status.reg.SEC != OperationStatusSecFull) {
if (!unsealFullAccess()) {
TT_LOG_E(TAG, "Unsealing full access failure!");
return false;
}
}
// Send ENTER_CFG_UPDATE command (0x0090)
if (!sendSubCommand(registers::SUBCMD_ENTER_CFG_UPDATE)) {
TT_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) {
TT_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) {
TT_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) {
TT_LOG_D(TAG, "Restore Safe Mode!");
exitSealMode();
}
return true;
}
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <Tactility/hal/i2c/I2cDevice.h>
#define BQ27220_ADDRESS 0x55
class Bq27220 final : public tt::hal::i2c::I2cDevice {
private:
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(const 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 final { return "BQ27220"; }
std::string getDescription() const final { return "I2C-controlled CEDV battery fuel gauge"; }
explicit Bq27220(i2c_port_t port) : I2cDevice(port, 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);
};
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd_st7796 driver
)
+3
View File
@@ -0,0 +1,3 @@
# ST7796
A basic ESP32 LVGL driver for ST7796 displays.
+210
View File
@@ -0,0 +1,210 @@
#include "St7796Display.h"
#include <Tactility/Log.h>
#include <esp_lcd_panel_commands.h>
#include <esp_lcd_panel_dev.h>
#include <esp_lcd_st7796.h>
#include <esp_lvgl_port.h>
#define TAG "st7796"
bool St7796Display::start() {
TT_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 = 0,
.lsb_first = 0,
.cs_high_active = 0
}
};
if (esp_lcd_new_panel_io_spi(configuration->spiBusHandle, &panel_io_config, &ioHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to create panel");
return false;
}
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
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
.color_space = ESP_LCD_COLOR_SPACE_RGB,
#else
.color_space = LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
#endif
.bits_per_pixel = 16,
.vendor_config = &vendor_config
};
/*
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = configuration->resetPin,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = 16,
.flags = {
.reset_active_high = false
},
.vendor_config = nullptr
};
*/
if (esp_lcd_new_panel_st7796(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to create panel");
return false;
}
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to reset panel");
return false;
}
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to init panel");
return false;
}
if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set panel to invert");
return false;
}
if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) {
TT_LOG_E(TAG, "Failed to swap XY ");
return false;
}
if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set panel to mirror");
return false;
}
if (esp_lcd_panel_set_gap(panelHandle, configuration->gapX, configuration->gapY) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set panel gap");
return false;
}
if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) {
TT_LOG_E(TAG, "Failed to turn display on");
return false;
}
uint32_t buffer_size;
if (configuration->bufferSize == 0) {
buffer_size = configuration->horizontalResolution * configuration->verticalResolution / 10;
} else {
buffer_size = configuration->bufferSize;
}
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = ioHandle,
.panel_handle = panelHandle,
.control_handle = nullptr,
.buffer_size = buffer_size,
.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}
};
displayHandle = lvgl_port_add_disp(&disp_cfg);
TT_LOG_I(TAG, "Finished");
return displayHandle != nullptr;
}
bool St7796Display::stop() {
assert(displayHandle != nullptr);
lvgl_port_remove_disp(displayHandle);
if (esp_lcd_panel_del(panelHandle) != ESP_OK) {
return false;
}
if (esp_lcd_panel_io_del(ioHandle) != ESP_OK) {
return false;
}
displayHandle = nullptr;
return true;
}
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) {
TT_LOG_E(TAG, "Failed to set gamma");
}*/
}
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#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 St7796Display final : public tt::hal::display::DisplayDevice {
public:
class Configuration {
public:
Configuration(
esp_lcd_spi_bus_handle_t spi_bus_handle,
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
) : spiBusHandle(spi_bus_handle),
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)) {}
esp_lcd_spi_bus_handle_t spiBusHandle;
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;
esp_lcd_panel_io_handle_t ioHandle = nullptr;
esp_lcd_panel_handle_t panelHandle = nullptr;
lv_display_t* displayHandle = nullptr;
public:
explicit St7796Display(std::unique_ptr<Configuration> inConfiguration) : configuration(std::move(inConfiguration)) {
assert(configuration != nullptr);
}
std::string getName() const final { return "ST7796"; }
std::string getDescription() const final { return "ST7796 display"; }
bool start() final;
bool stop() final;
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable createTouch() final { return configuration->touch; }
void setBacklightDuty(uint8_t backlightDuty) final {
if (configuration->backlightDutyFunction != nullptr) {
configuration->backlightDutyFunction(backlightDuty);
}
}
void setGammaCurve(uint8_t index) final;
uint8_t getGammaCurveCount() const final { return 4; };
bool supportsBacklightDuty() const final { return configuration->backlightDutyFunction != nullptr; }
lv_display_t* _Nullable getLvglDisplay() const final { return displayHandle; }
};
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility
)
+18
View File
@@ -0,0 +1,18 @@
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.
+4
View File
@@ -0,0 +1,4 @@
# 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
+202
View File
@@ -0,0 +1,202 @@
#include "Tca8418.h"
#include <Tactility/Log.h>
#define TAG "tca8418"
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
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;
// everything enabled in key scan mode
uint8_t enabled_rows = 0x3F;
uint16_t enabled_cols = 0x3FF;
writeRegister8(registers::KP_GPIO1, enabled_rows);
writeRegister8(registers::KP_GPIO2, (uint8_t)(0xFF & enabled_cols));
writeRegister8(registers::KP_GPIO3, (uint8_t)(0x03 & (enabled_cols >> 8)));
/*
* 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_code, 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_code = key_event & 0x7F;
key_down = (key_event & 0x80) >> 7;
key_row = key_code / num_cols;
key_col = key_code % num_cols;
// 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;
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <array>
#include <Tactility/hal/i2c/I2cDevice.h>
#define TCA8418_ADDRESS 0x34U
#define KEY_EVENT_LIST_SIZE 10
class Tca8418 final : public tt::hal::i2c::I2cDevice {
private:
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);
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(i2c_port_t port) : I2cDevice(port, 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);
};