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:
Ken Van Hoeylandt
2026-07-18 15:01:42 +02:00
committed by GitHub
parent 3b5a401594
commit d896657bf9
290 changed files with 9113 additions and 6719 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility GDEQ031T10 TCA8418 BQ27220 driver
INCLUDE_DIRS "source"
REQUIRES Tactility GDEQ031T10 driver
)
@@ -1,100 +0,0 @@
#include "devices/Display.h"
#include "devices/TdeckmaxKeyboard.h"
#include "devices/TdeckmaxPower.h"
#include <Bq27220.h>
#include <Tactility/hal/Configuration.h>
#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/gpio_controller.h>
using namespace tt::hal;
// Reset lines routed through the XL9555 IO expander (P-numbers from the vendor
// lib/TDeckMaxBoard/src/TDeckMaxBoard.h). Both are active low.
constexpr auto XL9555_PIN_TOUCH_RST = 7; // P07
constexpr auto XL9555_PIN_KEY_RST = 9; // P11
// Release the touch and keyboard reset lines held by the XL9555. Without this
// the touch controller may stay in a half-powered state and the keyboard's
// TCA8418 is held in reset, so neither responds. Mirrors the vendor factory
// firmware's XL9555 init (examples/factory/factory.ino).
static void initIoExpander() {
auto* xl9555 = device_find_by_name("xl9555");
if (xl9555 == nullptr) {
// Boot anyway; display works without the expander, but touch/keyboard won't.
return;
}
auto* touch_rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_TOUCH_RST, GPIO_OWNER_GPIO);
auto* key_rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_KEY_RST, GPIO_OWNER_GPIO);
if (touch_rst != nullptr) {
gpio_descriptor_set_flags(touch_rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(touch_rst, false);
}
if (key_rst != nullptr) {
gpio_descriptor_set_flags(key_rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(key_rst, false);
}
delay_millis(20);
// Release both resets and give the controllers time to boot before they're probed.
if (touch_rst != nullptr) {
gpio_descriptor_set_level(touch_rst, true);
gpio_descriptor_release(touch_rst);
}
if (key_rst != nullptr) {
gpio_descriptor_set_level(key_rst, true);
gpio_descriptor_release(key_rst);
}
delay_millis(60);
}
static bool initBoot() {
// The LoRa and SD chip-selects share the EPD's SPI bus but aren't wired up
// yet. spi0's start() already drives every cs-gpio high during kernel init;
// re-assert deselection before the EPD driver first transacts, as a cheap
// guard against either chip latching stray EPD command bytes (same pattern
// as the esp32_sdspi mount path).
auto* spi0 = device_find_by_name("spi0");
check(spi0 != nullptr);
esp32_spi_deselect_all_cs(spi0);
initIoExpander();
return true;
}
static DeviceVector createDevices() {
auto* i2c = device_find_by_name("i2c0");
check(i2c != nullptr);
// SD card is not created here: it's an espressif,esp32-sdspi node in the
// devicetree (sdcard@1 under spi0), started by Hal::init's mountAll().
DeviceVector devices = {
createDisplay()
};
auto keypad = std::make_shared<Tca8418>(i2c);
devices.push_back(keypad);
devices.push_back(std::make_shared<TdeckmaxKeyboard>(keypad));
// Battery metrics from the BQ27220 fuel gauge (0x55); power-off via the
// SY6970 charger (0x6A). Enables the launcher's power-off button.
auto gauge = std::make_shared<Bq27220>(i2c);
devices.push_back(gauge);
devices.push_back(std::make_shared<TdeckmaxPower>(gauge, i2c));
return devices;
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -1,225 +0,0 @@
#include "Cst66xxTouch.h"
#include <tactility/log.h>
#include <Tactility/app/App.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <freertos/FreeRTOS.h>
// Touch reset is wired to XL9555 P07 (active low).
static constexpr uint32_t XL9555_PIN_TOUCH_RST = 7;
constexpr auto* TAG = "CST66xx";
static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20);
// The protocol uses 4-byte register addresses, taken from the vendor's
// lib/HynTouch/src/hyn_cst66xx.c.
// 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};
static void setNormalMode(::Device* i2c, uint8_t address) {
i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT);
vTaskDelay(pdMS_TO_TICKS(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);
}
bool Cst66xxTouch::start() {
// Reset the controller (XL9555 P07, active low). The chip only re-initialises
// into normal reporting mode after a clean reset pulse.
auto* xl9555 = device_find_by_name("xl9555");
if (xl9555 != nullptr) {
auto* rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_TOUCH_RST, GPIO_OWNER_GPIO);
if (rst != nullptr) {
gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(rst, false);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_descriptor_set_level(rst, true);
gpio_descriptor_release(rst);
}
}
// The reset pulse exits boot mode; give the controller time to re-init.
vTaskDelay(pdMS_TO_TICKS(80));
// 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).
auto* i2c = configuration.i2cController;
const uint8_t address = configuration.address;
bool confirmed = false;
for (int attempt = 0; attempt < 5 && !confirmed; ++attempt) {
setNormalMode(i2c, address);
uint8_t info[50] = {};
if (i2c_controller_write(i2c, address, REG_INFO, sizeof(REG_INFO), I2C_TIMEOUT) == ERROR_NONE &&
i2c_controller_read(i2c, address, info, sizeof(info), I2C_TIMEOUT) == ERROR_NONE &&
info[2] == 0xCA && info[3] == 0xCA) {
confirmed = true;
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
if (confirmed) {
LOG_I(TAG, "CST66xx initialised");
} else {
LOG_W(TAG, "CST66xx identity not confirmed (continuing)");
}
return true;
}
bool Cst66xxTouch::stop() {
return true;
}
bool Cst66xxTouch::readPoint(int16_t& x, int16_t& y) {
// 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(configuration.i2cController, configuration.address, REG_READ_POINT, sizeof(REG_READ_POINT), I2C_TIMEOUT);
if (err == ERROR_NONE) {
err = i2c_controller_read(configuration.i2cController, configuration.address, buf, sizeof(buf), I2C_TIMEOUT);
}
if (err != ERROR_NONE) {
return false;
}
// Acknowledge/clear the frame after every read.
i2c_controller_write(configuration.i2cController, configuration.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;
// Bezel touch-keys are reported in the first slot (buf[8]): low nibble = key
// id, high nibble = state (non-zero = pressed). The controller reports keys
// mutually exclusively with finger coordinates, so a key frame never carries
// a touch point.
if (reportType == 0xFF && keyCount > 0) {
const uint8_t keyByte = buf[8];
handleBezelKey(keyByte & 0x0F, (keyByte >> 4) != 0);
return false;
}
// No key in this frame: clear the latch so the next press fires once.
bezelKeyDown = false;
if (reportType != 0xFF || fingerCount < 1) {
return false;
}
// 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) {
return false;
}
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 (configuration.swapXy) {
std::swap(rawX, rawY);
}
if (configuration.mirrorX) {
rawX = static_cast<int16_t>(configuration.width - 1 - rawX);
}
if (configuration.mirrorY) {
rawY = static_cast<int16_t>(configuration.height - 1 - rawY);
}
x = rawX;
y = rawY;
return true;
}
// Bezel touch-key ids as reported by the CST66xx, left to right (confirmed on
// hardware). To re-map a button, change the action in the switch below — the
// mapping is also documented in NOTES.md.
static constexpr uint8_t BEZEL_KEY_HEART = 0; // left
static constexpr uint8_t BEZEL_KEY_SPEECH = 1; // centre
static constexpr uint8_t BEZEL_KEY_AIRPLANE = 2; // right
void Cst66xxTouch::handleBezelKey(uint8_t keyId, bool pressed) {
if (!pressed) {
bezelKeyDown = false;
return;
}
if (bezelKeyDown) {
return; // still held; the press edge was already handled
}
bezelKeyDown = true;
LOG_D(TAG, "bezel key %u", keyId);
// Bezel button -> navigation action. Edit these cases to customise:
switch (keyId) {
case BEZEL_KEY_HEART: // Back: stop the current app (no-op at the launcher root)
tt::app::stop();
break;
// Home (BEZEL_KEY_SPEECH) and Recents (BEZEL_KEY_AIRPLANE) are intentionally
// unbound: tt::app::start() always pushes a new instance onto the app
// stack, so repeated presses would keep growing it (Launcher/AppList
// instances never get cleaned up) and leak memory over time. The app
// stack has no API yet to collapse back to an existing instance instead
// of pushing a new one; wire these up once that exists.
default:
break;
}
}
void Cst66xxTouch::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
auto* self = static_cast<Cst66xxTouch*>(lv_indev_get_user_data(indev));
int16_t x;
int16_t y;
if (self->readPoint(x, y)) {
self->lastX = x;
self->lastY = y;
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
// Report the release at the last known position.
data->point.x = self->lastX;
data->point.y = self->lastY;
data->state = LV_INDEV_STATE_RELEASED;
}
}
bool Cst66xxTouch::startLvgl(lv_display_t* display) {
if (indev != nullptr) {
return true;
}
indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, &readCallback);
lv_indev_set_display(indev, display);
lv_indev_set_user_data(indev, this);
return true;
}
bool Cst66xxTouch::stopLvgl() {
if (indev == nullptr) {
return true;
}
lv_indev_delete(indev);
indev = nullptr;
return true;
}
@@ -1,54 +0,0 @@
#pragma once
#include <Tactility/hal/touch/TouchDevice.h>
#include <tactility/device.h>
// Capacitive touch for the LilyGO T-Deck Max's Hynitron CST66xx controller at
// I2C 0x1A. (The vendor docs label it "CST328", but the vendor HynTouch driver
// probes cst66xx first and that is what answers here.) This is a minimal,
// single-touch port of the vendor's hyn_cst66xx.c protocol for LVGL pointer input.
class Cst66xxTouch final : public tt::hal::touch::TouchDevice {
public:
struct Configuration {
::Device* i2cController;
uint8_t address;
uint16_t width;
uint16_t height;
bool swapXy;
bool mirrorX;
bool mirrorY;
};
explicit Cst66xxTouch(const Configuration& configuration) : configuration(configuration) {}
std::string getName() const override { return "CST66xx"; }
std::string getDescription() const override { return "CST66xx I2C capacitive touch"; }
bool start() override;
bool stop() override;
bool supportsLvgl() const override { return true; }
bool startLvgl(lv_display_t* display) override;
bool stopLvgl() override;
lv_indev_t* getLvglIndev() override { return indev; }
bool supportsTouchDriver() override { return false; }
std::shared_ptr<tt::hal::touch::TouchDriver> getTouchDriver() override { return nullptr; }
private:
Configuration configuration;
lv_indev_t* indev = nullptr;
int16_t lastX = 0;
int16_t lastY = 0;
// The CST66xx also reports the three bezel touch-keys (heart / speech-bubble /
// paper-airplane). bezelKeyDown latches a press so we act once on the rising
// edge rather than every poll while the key is held.
bool bezelKeyDown = false;
bool readPoint(int16_t& x, int16_t& y);
void handleBezelKey(uint8_t keyId, bool pressed);
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
};
@@ -1,57 +0,0 @@
#include "Display.h"
#include "Cst66xxTouch.h"
#include <Gdeq031t10Display.h>
#include <tactility/log.h>
#include <tactility/device.h>
constexpr auto* TAG = "TdeckmaxDisplay";
// Pins and I2C address from Xinyuan-LilyGO/T-Deck-MAX's
// lib/TDeckMaxBoard/src/TDeckMaxBoard.h and docs/pinmap.md.
constexpr auto EPD_SPI_HOST = SPI2_HOST;
constexpr auto EPD_PIN_CS = GPIO_NUM_34;
constexpr auto EPD_PIN_DC = GPIO_NUM_35;
constexpr auto EPD_PIN_RST = GPIO_NUM_9;
constexpr auto EPD_PIN_BUSY = GPIO_NUM_37;
constexpr uint16_t TOUCH_I2C_ADDRESS = 0x1A;
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto* i2c = device_find_by_name("i2c0");
if (i2c == nullptr) {
LOG_E(TAG, "i2c0 not found, booting without touch");
return nullptr;
}
// The touch reset line is released earlier via the XL9555 expander (see
// Configuration.cpp). Orientation flags are starting guesses; adjust after
// checking on hardware.
const Cst66xxTouch::Configuration configuration = {
.i2cController = i2c,
.address = TOUCH_I2C_ADDRESS,
.width = Gdeq031t10Display::WIDTH,
.height = Gdeq031t10Display::HEIGHT,
.swapXy = false,
.mirrorX = false,
.mirrorY = false,
};
return std::make_shared<Cst66xxTouch>(configuration);
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto configuration = std::make_unique<Gdeq031t10Display::Configuration>(
EPD_SPI_HOST,
EPD_PIN_CS,
EPD_PIN_DC,
EPD_PIN_RST,
EPD_PIN_BUSY,
createTouch(),
10'000'000,
// Default to a fast (~1s) refresh for the automatic ghost-clears so the
// full-screen flash they cause is as short as possible.
Gdeq031t10Display::RefreshMode::Fast
);
return std::make_shared<Gdeq031t10Display>(std::move(configuration));
}
@@ -1,249 +0,0 @@
#include "TdeckmaxKeyboard.h"
#include <tactility/log.h>
#include <driver/i2c.h>
#include <driver/gpio.h>
constexpr auto* TAG = "TdeckmaxKeyboard";
// Keyboard backlight (LED_PWM), GPIO42 on the T-Deck Max.
constexpr auto BACKLIGHT = GPIO_NUM_42;
constexpr auto KB_ROWS = 4;
constexpr auto KB_COLS = 10;
// Keymaps are written in the vendor's row/column orientation
// (Xinyuan-LilyGO/T-Deck-MAX examples/factory/peri_keypad.cpp). The TCA8418
// columns are wired in reverse, so the vendor reads keymap[row][9 - col]. We do
// the same reversal in processKeyboard(), which keeps these tables a direct,
// verifiable copy of the vendor layout.
//
// Modifier keys (ALT and SYM) are blanked here ('\0') and handled by position:
// ALT -> shift/uppercase, at vendor column 0 of row 2
// SYM -> symbol layer, at vendor column 8 of row 3
// Lowercase (base) layer
static constexpr char keymap_lc[KB_ROWS][KB_COLS] = {
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', LV_KEY_BACKSPACE},
{'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '$', LV_KEY_ENTER},
{'\0', '\0', '\0', '\0', '\0', LV_KEY_PREV, '0', ' ', '\0', LV_KEY_NEXT}
};
// Uppercase layer (ALT held or caps toggled)
static constexpr char keymap_uc[KB_ROWS][KB_COLS] = {
{'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'},
{'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', LV_KEY_BACKSPACE},
{'\0', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '$', LV_KEY_ENTER},
{'\0', '\0', '\0', '\0', '\0', LV_KEY_PREV, '0', ' ', '\0', LV_KEY_NEXT}
};
// Symbol layer (SYM held). The T-Deck Max silkscreen for the symbol layer is
// not documented in the vendor sources, so these are sensible defaults; adjust
// to match the printed keys once verified on hardware.
static constexpr char keymap_sy[KB_ROWS][KB_COLS] = {
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
{'@', '#', '+', '-', '*', '/', '(', ')', '_', LV_KEY_BACKSPACE},
{'\0', '!', '?', ';', ':', '\'', '"', ',', '.', LV_KEY_ENTER},
{'\0', '\0', '\0', '\0', '\0', LV_KEY_PREV, '0', ' ', '\0', LV_KEY_NEXT}
};
void TdeckmaxKeyboard::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
auto keyboard = static_cast<TdeckmaxKeyboard*>(lv_indev_get_user_data(indev));
// Emit the release edge for the previous key first: LVGL's keypad handling
// only delivers a key on a RELEASED->PRESSED transition, so two different
// keys reported PRESSED back-to-back would swallow the second one.
if (keyboard->lastKeyNeedsRelease) {
keyboard->lastKeyNeedsRelease = false;
data->key = keyboard->lastKey;
data->state = LV_INDEV_STATE_RELEASED;
data->continue_reading = uxQueueMessagesWaiting(keyboard->queue) > 0;
return;
}
char keypress = 0;
if (xQueueReceive(keyboard->queue, &keypress, 0) == pdPASS) {
keyboard->lastKey = static_cast<uint32_t>(keypress);
keyboard->lastKeyNeedsRelease = true;
data->key = keyboard->lastKey;
data->state = LV_INDEV_STATE_PRESSED;
// Drain the whole queue in this read cycle so a typing burst lands in
// one render pass (and thus a single e-paper refresh).
data->continue_reading = true;
} else {
data->key = 0;
data->state = LV_INDEV_STATE_RELEASED;
}
}
void TdeckmaxKeyboard::processKeyboard() {
bool anykey_pressed = false;
// Each update() pops one event from the TCA8418's FIFO. Drain it fully per
// poll (bounded, in case of a misbehaving bus) so a fast typing burst isn't
// throttled to one event per poll period. Handling each event as a discrete
// press/release edge also means a key held across another key's press (fast
// typing rollover) no longer re-sends its character.
for (int drained = 0; drained < 16 && keypad->update(); drained++) {
if (keypad->released_key_count > 0) {
// Release event: only modifier state cares about releases.
auto row = keypad->released_list[0].row;
auto vcol = (KB_COLS - 1) - keypad->released_list[0].col;
if ((row == 2) && (vcol == 0)) {
shiftPressed = false; // ALT key
}
if ((row == 3) && (vcol == 8)) {
symPressed = false; // SYM key
}
} else if (keypad->pressed_key_count > 0) {
// Press event: the key that caused it is the newest list entry.
auto row = keypad->pressed_list[keypad->pressed_key_count - 1].row;
auto vcol = (KB_COLS - 1) - keypad->pressed_list[keypad->pressed_key_count - 1].col;
anykey_pressed = true;
if ((row == 2) && (vcol == 0)) {
shiftPressed = true; // ALT key
} else if ((row == 3) && (vcol == 8)) {
symPressed = true; // SYM key
} else {
char chr;
if (symPressed) {
chr = keymap_sy[row][vcol];
} else if (shiftPressed || capToggle) {
chr = keymap_uc[row][vcol];
} else {
chr = keymap_lc[row][vcol];
}
// Non-blocking: this runs on the periodic input timer, so a full
// queue (reader stalled) must drop the keystroke rather than
// stall this callback and the timer's other periodic work.
if (chr != '\0' && xQueueSend(queue, &chr, 0) != pdPASS) {
LOG_W(TAG, "Keyboard queue full, dropping keystroke");
}
}
if ((symPressed && shiftPressed) && capToggleArmed) {
capToggle = !capToggle;
capToggleArmed = false;
}
}
if ((!symPressed && !shiftPressed) && !capToggleArmed) {
capToggleArmed = true;
}
}
if (anykey_pressed) {
makeBacklightImpulse();
}
}
bool TdeckmaxKeyboard::startLvgl(lv_display_t* display) {
backlightOkay = initBacklight(BACKLIGHT, 30000, LEDC_TIMER_0, LEDC_CHANNEL_1);
keypad->init(KB_ROWS, KB_COLS);
assert(inputTimer == nullptr);
inputTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, tt::kernel::millisToTicks(20), [this] {
processKeyboard();
});
assert(backlightImpulseTimer == nullptr);
backlightImpulseTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, tt::kernel::millisToTicks(50), [this] {
processBacklightImpulse();
});
kbHandle = lv_indev_create();
lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(kbHandle, &readCallback);
lv_indev_set_display(kbHandle, display);
lv_indev_set_user_data(kbHandle, this);
inputTimer->start();
backlightImpulseTimer->start();
return true;
}
bool TdeckmaxKeyboard::stopLvgl() {
assert(inputTimer);
inputTimer->stop();
inputTimer = nullptr;
assert(backlightImpulseTimer);
backlightImpulseTimer->stop();
backlightImpulseTimer = nullptr;
lv_indev_delete(kbHandle);
kbHandle = nullptr;
return true;
}
bool TdeckmaxKeyboard::isAttached() const {
return i2c_controller_has_device_at_address(keypad->getController(), keypad->getAddress(), 100) == ERROR_NONE;
}
bool TdeckmaxKeyboard::initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel) {
backlightPin = pin;
backlightTimer = timer;
backlightChannel = channel;
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = backlightTimer,
.freq_hz = frequencyHz,
.clk_cfg = LEDC_AUTO_CLK,
.deconfigure = false
};
if (ledc_timer_config(&ledc_timer) != ESP_OK) {
LOG_E(TAG, "Backlight timer config failed");
return false;
}
ledc_channel_config_t ledc_channel = {
.gpio_num = backlightPin,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = backlightChannel,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = backlightTimer,
.duty = 0,
.hpoint = 0,
.sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD,
.flags = {
.output_invert = 0
}
};
if (ledc_channel_config(&ledc_channel) != ESP_OK) {
LOG_E(TAG, "Backlight channel config failed");
return false;
}
return true;
}
bool TdeckmaxKeyboard::setBacklightDuty(uint8_t duty) {
if (!backlightOkay) {
LOG_E(TAG, "Backlight not ready");
return false;
}
return (ledc_set_duty(LEDC_LOW_SPEED_MODE, backlightChannel, duty) == ESP_OK) &&
(ledc_update_duty(LEDC_LOW_SPEED_MODE, backlightChannel) == ESP_OK);
}
void TdeckmaxKeyboard::makeBacklightImpulse() {
backlightImpulseDuty = 255;
setBacklightDuty(backlightImpulseDuty);
}
void TdeckmaxKeyboard::processBacklightImpulse() {
if (backlightImpulseDuty > 0) {
backlightImpulseDuty--;
setBacklightDuty(backlightImpulseDuty);
}
}
@@ -1,69 +0,0 @@
#pragma once
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include <Tactility/Timer.h>
#include <Tca8418.h>
#include <driver/gpio.h>
#include <driver/ledc.h>
#include <freertos/queue.h>
// QWERTY keyboard for the LilyGO T-Deck (Pro) Max.
//
// The keyboard is a TCA8418 4x10 matrix scanner on the shared I2C bus at 0x34.
// The keyboard reset line is wired through the XL9555 IO expander (P11), so it
// must be released before this device is started (see Configuration.cpp).
//
// Pins, address and keymap are taken from Xinyuan-LilyGO/T-Deck-MAX:
// examples/factory/peri_keypad.cpp and lib/TDeckMaxBoard/src/TDeckMaxBoard.h.
class TdeckmaxKeyboard final : public tt::hal::keyboard::KeyboardDevice {
lv_indev_t* kbHandle = nullptr;
gpio_num_t backlightPin = GPIO_NUM_NC;
ledc_timer_t backlightTimer;
ledc_channel_t backlightChannel;
bool backlightOkay = false;
int backlightImpulseDuty = 0;
QueueHandle_t queue = nullptr;
// LVGL only registers a key on a RELEASED->PRESSED edge, so readCallback
// alternates press/release per queued key; this tracks the pending release.
uint32_t lastKey = 0;
bool lastKeyNeedsRelease = false;
std::shared_ptr<Tca8418> keypad;
std::unique_ptr<tt::Timer> inputTimer;
std::unique_ptr<tt::Timer> backlightImpulseTimer;
bool shiftPressed = false;
bool symPressed = false;
bool capToggle = false;
bool capToggleArmed = true;
bool initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel);
void processKeyboard();
void processBacklightImpulse();
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
public:
explicit TdeckmaxKeyboard(const std::shared_ptr<Tca8418>& tca) : keypad(tca) {
queue = xQueueCreate(20, sizeof(char));
}
~TdeckmaxKeyboard() override {
vQueueDelete(queue);
}
std::string getName() const override { return "T-Deck Max Keyboard"; }
std::string getDescription() const override { return "T-Deck Max TCA8418 I2C matrix keyboard"; }
bool startLvgl(lv_display_t* display) override;
bool stopLvgl() override;
bool isAttached() const override;
lv_indev_t* getLvglIndev() override { return kbHandle; }
bool setBacklightDuty(uint8_t duty);
void makeBacklightImpulse();
};
@@ -1,139 +0,0 @@
#include "TdeckmaxPower.h"
#include <tactility/log.h>
#include <tactility/drivers/i2c_controller.h>
constexpr auto* TAG = "TdeckmaxPower";
// SY6970 charger (newer T-Deck Max revisions; older boards use BQ25896 @ 0x6B).
// Register map is BQ25896-compatible for everything used here (see NOTES.md).
static constexpr uint8_t SY6970_ADDRESS = 0x6A;
static constexpr uint8_t SY6970_REG_03 = 0x03; // bit4 CHG_CONFIG: charge enable
static constexpr uint8_t SY6970_REG_04 = 0x04; // bits[6:0] ICHG: charge current, 64 mA/LSB
static constexpr uint8_t SY6970_REG_06 = 0x06; // bits[7:2] VREG: charge voltage, 3840 mV + n*16 mV
static constexpr uint8_t SY6970_REG_07 = 0x07; // bits[5:4] WATCHDOG: I2C watchdog timer
static constexpr uint8_t SY6970_REG_09 = 0x09;
static constexpr uint8_t SY6970_REG_0B = 0x0B; // read-only status
static constexpr uint8_t SY6970_CHG_CONFIG = 1 << 4;
static constexpr uint8_t SY6970_BATFET_DIS = 1 << 5; // Force BATFET off = ship mode
static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
// Charge parameters from the vendor factory firmware (XPowersLib SY6970 init):
// watchdog off, 4288 mV target, 1024 mA fast-charge current. The I2C watchdog
// must be disabled first, otherwise the chip reverts custom register values to
// defaults after the watchdog expires. Input current limit (REG00) is left at
// the hardware PSEL/ILIM default. Non-fatal: charging still works on chip
// defaults if any write fails.
bool TdeckmaxPower::configureCharger() const {
if (i2c_controller_register8_reset_bits(i2c, SY6970_ADDRESS, SY6970_REG_07, 0x30, I2C_TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to disable SY6970 watchdog");
return false;
}
uint8_t reg06 = 0;
if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_06, &reg06, I2C_TIMEOUT) != ERROR_NONE ||
i2c_controller_register8_set(i2c, SY6970_ADDRESS, SY6970_REG_06, (reg06 & 0x03) | (28 << 2), I2C_TIMEOUT) != ERROR_NONE) { // 3840 + 28*16 = 4288 mV
LOG_E(TAG, "Failed to set SY6970 charge voltage");
return false;
}
uint8_t reg04 = 0;
if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_04, &reg04, I2C_TIMEOUT) != ERROR_NONE ||
i2c_controller_register8_set(i2c, SY6970_ADDRESS, SY6970_REG_04, (reg04 & 0x80) | 0x10, I2C_TIMEOUT) != ERROR_NONE) { // 16 * 64 = 1024 mA
LOG_E(TAG, "Failed to set SY6970 charge current");
return false;
}
LOG_I(TAG, "SY6970 configured: 4288 mV / 1024 mA, watchdog off");
return true;
}
bool TdeckmaxPower::isAllowedToCharge() const {
uint8_t reg03 = 0;
if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_03, &reg03, I2C_TIMEOUT) != ERROR_NONE) {
return false;
}
return (reg03 & SY6970_CHG_CONFIG) != 0;
}
void TdeckmaxPower::setAllowedToCharge(bool canCharge) {
error_t result;
if (canCharge) {
result = i2c_controller_register8_set_bits(i2c, SY6970_ADDRESS, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT);
} else {
result = i2c_controller_register8_reset_bits(i2c, SY6970_ADDRESS, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT);
}
if (result != ERROR_NONE) {
LOG_E(TAG, "Failed to set SY6970 charge enable");
}
}
bool TdeckmaxPower::supportsMetric(MetricType type) const {
switch (type) {
using enum MetricType;
case IsCharging:
case Current:
case BatteryVoltage:
case ChargeLevel:
return true;
default:
return false;
}
}
bool TdeckmaxPower::getMetric(MetricType type, MetricData& data) {
uint16_t u16 = 0;
int16_t s16 = 0;
switch (type) {
using enum MetricType;
case IsCharging: {
// REG0B CHRG_STAT bits[4:3]: 0 = not charging, 1 = pre-charge,
// 2 = fast charge, 3 = charge done. Only 1|2 count as charging —
// "done" must not (XPowersLib had this inverted once). Fall back to
// the fuel gauge's discharge flag if the charger doesn't answer.
uint8_t reg0b = 0;
if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_0B, &reg0b, I2C_TIMEOUT) == ERROR_NONE) {
uint8_t charge_status = (reg0b >> 3) & 0x03;
data.valueAsBool = charge_status == 1 || charge_status == 2;
return true;
}
Bq27220::BatteryStatus status;
if (gauge->getBatteryStatus(status)) {
data.valueAsBool = !status.reg.DSG;
return true;
}
return false;
}
case Current:
if (gauge->getCurrent(s16)) {
data.valueAsInt32 = s16;
return true;
}
return false;
case BatteryVoltage:
if (gauge->getVoltage(u16)) {
data.valueAsUint32 = u16;
return true;
}
return false;
case ChargeLevel:
if (gauge->getStateOfCharge(u16)) {
data.valueAsUint8 = u16;
return true;
}
return false;
default:
return false;
}
}
void TdeckmaxPower::powerOff() {
// Ship mode: force the charger's BATFET off (REG09 bit5). Mirrors the vendor
// XPowersLib PowersSY6970::shutdown(). Note: this only fully powers the board
// down when running on battery with USB unplugged.
LOG_I(TAG, "Power off (SY6970 BATFET_DIS)");
if (i2c_controller_register8_set_bits(i2c, SY6970_ADDRESS, SY6970_REG_09, SY6970_BATFET_DIS, pdMS_TO_TICKS(50)) != ERROR_NONE) {
LOG_E(TAG, "Failed to write SY6970 shutdown register");
}
}
@@ -1,42 +0,0 @@
#pragma once
#include "Tactility/hal/power/PowerDevice.h"
#include <Bq27220.h>
#include <tactility/device.h>
#include <memory>
using tt::hal::power::PowerDevice;
// Power device for the LilyGO T-Deck Max. Battery metrics come from the BQ27220
// fuel gauge (I2C 0x55); charging status, charge control and power-off (ship
// mode, REG09 BATFET_DIS) use the SY6970 charger. Newer T-Deck Max revisions
// ship the SY6970 (0x6A) instead of the older BQ25896 (0x6B); this board has
// the SY6970 (confirmed by I2C scan). The two are register-compatible for
// everything used here.
class TdeckmaxPower final : public PowerDevice {
std::shared_ptr<Bq27220> gauge;
::Device* i2c; // i2c0 controller, used to reach the SY6970 charger
bool configureCharger() const;
public:
TdeckmaxPower(const std::shared_ptr<Bq27220>& gauge, ::Device* i2c) : gauge(gauge), i2c(i2c) {
configureCharger();
}
std::string getName() const final { return "T-Deck Max Power"; }
std::string getDescription() const final { return "Battery metrics (BQ27220), SY6970 charging and power-off"; }
bool supportsMetric(MetricType type) const override;
bool getMetric(MetricType type, MetricData& data) override;
bool supportsChargeControl() const override { return true; }
bool isAllowedToCharge() const override;
void setAllowedToCharge(bool canCharge) override;
bool supportsPowerOff() const override { return true; }
void powerOff() override;
};
+4
View File
@@ -1,4 +1,8 @@
dependencies:
- Platforms/platform-esp32
- Drivers/xl9555-module
- Drivers/cst66xx-module
- Drivers/tca8418-module
- Drivers/sy6970-module
- Drivers/bq27220-module
dts: lilygo,tdeck-max.dts
@@ -7,7 +7,13 @@
#include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/xl9555.h>
#include <bindings/cst66xx.h>
#include <bindings/tca8418.h>
#include <bindings/sy6970.h>
#include <bindings/bq27220.h>
// Reference: https://github.com/Xinyuan-LilyGO/T-Deck-MAX
// lib/TDeckMaxBoard/src/TDeckMaxBoard.h, docs/pinmap.md
@@ -40,6 +46,88 @@
compatible = "xlsemi,xl9555";
reg = <0x20>;
};
// Vendor docs label this chip "CST328", but the vendor HynTouch driver probes
// cst66xx first and that is what answers on this board.
touch {
compatible = "hynitron,cst66xx";
reg = <0x1A>;
x-max = <240>;
y-max = <320>;
// Reset routed through the XL9555 IO expander (P07, active low).
pin-reset = <&xl9555 7 GPIO_FLAG_NONE>;
};
// TCA8418 4x10 matrix scanner. Keymaps are written in the vendor's row/column
// orientation (Xinyuan-LilyGO/T-Deck-MAX examples/factory/peri_keypad.cpp); the
// TCA8418 columns are wired in reverse of that order, hence reverse-columns.
// ALT (shift/uppercase) is row 2 col 0; SYM (symbol layer) is row 3 col 8.
keyboard {
compatible = "ti,tca8418";
reg = <0x34>;
rows = <4>;
columns = <10>;
reverse-columns;
keymap-lc = [
113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p
97 115 100 102 103 104 106 107 108 8 // a s d f g h j k l BACKSPACE
0 122 120 99 118 98 110 109 36 10 // z x c v b n m $ ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
keymap-uc = [
81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P
65 83 68 70 71 72 74 75 76 8 // A S D F G H J K L BACKSPACE
0 90 88 67 86 66 78 77 36 10 // Z X C V B N M $ ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
keymap-sy = [
49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0
64 35 43 45 42 47 40 41 95 8 // @ # + - * / ( ) _ BACKSPACE
0 33 63 59 58 39 34 44 46 10 // ! ? ; : ' " , . ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
shift-row = <2>;
shift-col = <0>;
sym-row = <3>;
sym-col = <8>;
// Reset routed through the XL9555 IO expander (P11, active low).
pin-reset = <&xl9555 9 GPIO_FLAG_NONE>;
};
// Battery metrics from the BQ27220 fuel gauge; power-off via the SY6970 charger.
// Newer T-Deck Max revisions ship the SY6970 (0x6A) instead of the older BQ25896
// (0x6B); this board has the SY6970 (confirmed by I2C scan). The two are
// register-compatible for everything used here.
sy6970 {
compatible = "silergy,sy6970";
reg = <0x6A>;
charge-voltage-mv = <4288>;
charge-current-ma = <1024>;
};
bq27220 {
compatible = "ti,bq27220";
reg = <0x55>;
};
};
// Keyboard backlight (LED_PWM), GPIO42. The keypress-triggered flash/decay effect
// from the pre-migration driver isn't reproduced here - kernel drivers don't have a
// home for that kind of UI policy (same reasoning as the touch driver's dropped bezel
// keys). Off by default; app code can drive brightness via the generic pwm-backlight
// API if desired.
keyboard_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 42 GPIO_FLAG_NONE>;
period-ns = <33333>;
ledc-timer = <0>;
ledc-channel = <0>;
};
keyboard_backlight {
compatible = "pwm-backlight";
status = "disabled";
pwm = <&keyboard_backlight_pwm>;
};
spi0 {
@@ -0,0 +1,35 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/esp32_spi.h>
using namespace tt::hal;
static bool initBoot() {
// The LoRa and SD chip-selects share the EPD's SPI bus but aren't wired up
// yet. spi0's start() already drives every cs-gpio high during kernel init;
// re-assert deselection before the EPD driver first transacts, as a cheap
// guard against either chip latching stray EPD command bytes (same pattern
// as the esp32_sdspi mount path).
auto* spi0 = device_find_by_name("spi0");
check(spi0 != nullptr);
esp32_spi_deselect_all_cs(spi0);
return true;
}
static DeviceVector createDevices() {
// Touch, keyboard and battery/charging are kernel drivers (cst66xx, tca8418, sy6970,
// bq27220 - see the .dts), started independently of this HAL layer. Only the EPD
// display remains on the deprecated HAL.
return DeviceVector {
createDisplay()
};
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -0,0 +1,29 @@
#include "Display.h"
#include <Gdeq031t10Display.h>
// Pins from Xinyuan-LilyGO/T-Deck-MAX's lib/TDeckMaxBoard/src/TDeckMaxBoard.h and docs/pinmap.md.
constexpr auto EPD_SPI_HOST = SPI2_HOST;
constexpr auto EPD_PIN_CS = GPIO_NUM_34;
constexpr auto EPD_PIN_DC = GPIO_NUM_35;
constexpr auto EPD_PIN_RST = GPIO_NUM_9;
constexpr auto EPD_PIN_BUSY = GPIO_NUM_37;
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
// Touch is a kernel driver (hynitron,cst66xx - see the .dts), discovered and fed to LVGL
// independently of this display, so no touch device is passed into the configuration here.
auto configuration = std::make_unique<Gdeq031t10Display::Configuration>(
EPD_SPI_HOST,
EPD_PIN_CS,
EPD_PIN_DC,
EPD_PIN_RST,
EPD_PIN_BUSY,
nullptr,
10'000'000,
// Default to a fast (~1s) refresh for the automatic ghost-clears so the
// full-screen flash they cause is as short as possible.
Gdeq031t10Display::RefreshMode::Fast
);
return std::make_shared<Gdeq031t10Display>(std::move(configuration));
}