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,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, ®06, 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, ®04, 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, ®03, 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, ®0b, 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;
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user