-
-
Notifications
You must be signed in to change notification settings - Fork 94
Add LilyGO T-Deck Max (e-paper) base board support #552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Crazypedia
wants to merge
2
commits into
TactilityProject:main
Choose a base branch
from
Crazypedia:tdeckmax-baseboard
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| file(GLOB_RECURSE SOURCE_FILES Source/*.c*) | ||
|
|
||
| idf_component_register( | ||
| SRCS ${SOURCE_FILES} | ||
| INCLUDE_DIRS "Source" | ||
| REQUIRES Tactility GDEQ031T10 TCA8418 driver | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| #include "devices/Display.h" | ||
| #include "devices/TdeckmaxKeyboard.h" | ||
|
|
||
| #include <driver/gpio.h> | ||
|
|
||
| #include <Tactility/hal/Configuration.h> | ||
| #include <tactility/check.h> | ||
| #include <tactility/delay.h> | ||
| #include <tactility/device.h> | ||
| #include <tactility/drivers/gpio_controller.h> | ||
|
|
||
| using namespace tt::hal; | ||
|
|
||
| // LoRa and SD card share the EPD's SPI bus but aren't wired up yet, so their | ||
| // chip-select lines are left floating. Deassert them before the EPD driver | ||
| // touches the bus, matching the vendor reference driver's own setup(), so a | ||
| // floating CS can't make either chip latch EPD command bytes meant for it. | ||
| constexpr auto LORA_PIN_CS = GPIO_NUM_3; | ||
| constexpr auto SD_PIN_CS = GPIO_NUM_48; | ||
|
|
||
| // 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() { | ||
| gpio_config_t config = { | ||
| .pin_bit_mask = (1ULL << LORA_PIN_CS) | (1ULL << SD_PIN_CS), | ||
| .mode = GPIO_MODE_OUTPUT, | ||
| .pull_up_en = GPIO_PULLUP_DISABLE, | ||
| .pull_down_en = GPIO_PULLDOWN_DISABLE, | ||
| .intr_type = GPIO_INTR_DISABLE, | ||
| }; | ||
| gpio_config(&config); | ||
| gpio_set_level(LORA_PIN_CS, 1); | ||
| gpio_set_level(SD_PIN_CS, 1); | ||
|
|
||
| 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)); | ||
|
|
||
| return devices; | ||
| } | ||
|
|
||
| extern const Configuration hardwareConfiguration = { | ||
| .initBoot = initBoot, | ||
| .createDevices = createDevices | ||
| }; | ||
225 changes: 225 additions & 0 deletions
225
Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| #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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| #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); | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
esp32_spi_deselect_all_cs()would be better for this. If that doesn't work, the above logic is ok.