Tab5 features, StackChan, fixes, drivers.... (#526)

This commit is contained in:
Shadowtrance
2026-05-29 07:31:25 +10:00
committed by GitHub
parent 5c78d55b04
commit a59fbf4ed5
140 changed files with 2500 additions and 835 deletions
+2 -4
View File
@@ -1,7 +1,5 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port driver
REQUIRES Tactility EspLcdCompat esp_lcd_touch_ft6336u driver
)
+26 -124
View File
@@ -1,134 +1,36 @@
#include "Ft6x36Touch.h"
#include <Ft6x36Touch.h>
#include <Tactility/Logger.h>
#include <esp_lcd_touch_ft6x36.h>
#include <esp_err.h>
#include <esp_lvgl_port.h>
static const auto LOGGER = tt::Logger("FT6x36");
void Ft6x36Touch::touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data) {
auto* touch = (Ft6x36Touch*)lv_indev_get_driver_data(indev);
touch->mutex.lock();
data->point = touch->lastPoint;
data->state = touch->lastState;
touch->mutex.unlock();
bool Ft6x36Touch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_FT6x36_CONFIG();
return esp_lcd_new_panel_io_i2c(configuration->port, &io_config, &outHandle) == ESP_OK;
}
Ft6x36Touch::Ft6x36Touch(std::unique_ptr<Configuration> inConfiguration) :
configuration(std::move(inConfiguration)) {
nativeTouch = std::make_shared<Ft6TouchDriver>(*this);
bool Ft6x36Touch::createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) {
return esp_lcd_touch_new_i2c_ft6x36(ioHandle, &configuration, &panelHandle) == ESP_OK;
}
Ft6x36Touch::~Ft6x36Touch() {
if (driverThread != nullptr && driverThread->getState() != tt::Thread::State::Stopped) {
interruptDriverThread = true;
driverThread->join();
}
}
void Ft6x36Touch::driverThreadMain() {
TPoint point = { .x = 0, .y = 0 };
TEvent event = TEvent::None;
while (!shouldInterruptDriverThread()) {
driver.processTouch();
driver.poll(&point, &event);
if (mutex.lock(100)) {
switch (event) {
case TEvent::TouchStart:
case TEvent::TouchMove:
case TEvent::DragStart:
case TEvent::DragMove:
case TEvent::DragEnd:
lastState = LV_INDEV_STATE_PR;
lastPoint.x = point.x;
lastPoint.y = point.y;
break;
case TEvent::TouchEnd:
lastState = LV_INDEV_STATE_REL;
lastPoint.x = point.x;
lastPoint.y = point.y;
break;
case TEvent::Tap:
case TEvent::None:
break;
}
mutex.unlock();
}
}
}
bool Ft6x36Touch::shouldInterruptDriverThread() const {
bool interrupt = false;
if (mutex.lock(50 / portTICK_PERIOD_MS)) {
interrupt = interruptDriverThread;
mutex.unlock();
}
return interrupt;
}
bool Ft6x36Touch::start() {
LOGGER.info("Start");
if (!driver.begin(FT6X36_DEFAULT_THRESHOLD, configuration->width, configuration->height)) {
LOGGER.error("driver.begin() failed");
return false;
}
mutex.lock();
interruptDriverThread = false;
driverThread = std::make_shared<tt::Thread>("ft6x36", 4096, [this] {
driverThreadMain();
return 0;
});
driverThread->start();
mutex.unlock();
return true;
}
bool Ft6x36Touch::stop() {
LOGGER.info("Stop");
mutex.lock();
interruptDriverThread = true;
mutex.unlock();
driverThread->join();
mutex.lock();
driverThread = nullptr;
mutex.unlock();
return false;
}
bool Ft6x36Touch::startLvgl(lv_display_t* display) {
if (deviceHandle != nullptr) {
return false;
}
deviceHandle = lv_indev_create();
lv_indev_set_type(deviceHandle, LV_INDEV_TYPE_POINTER);
lv_indev_set_driver_data(deviceHandle, this);
lv_indev_set_read_cb(deviceHandle, touchReadCallback);
return true;
}
bool Ft6x36Touch::stopLvgl() {
if (deviceHandle == nullptr) {
return false;
}
lv_indev_delete(deviceHandle);
deviceHandle = nullptr;
return true;
esp_lcd_touch_config_t Ft6x36Touch::createEspLcdTouchConfig() {
return {
.x_max = configuration->xMax,
.y_max = configuration->yMax,
.rst_gpio_num = configuration->pinReset,
.int_gpio_num = configuration->pinInterrupt,
.levels = {
.reset = configuration->pinResetLevel,
.interrupt = configuration->pinInterruptLevel,
},
.flags = {
.swap_xy = configuration->swapXy,
.mirror_x = configuration->mirrorX,
.mirror_y = configuration->mirrorY,
},
.process_coordinates = nullptr,
.interrupt_callback = nullptr,
.user_data = nullptr,
.driver_data = nullptr
};
}
+35 -58
View File
@@ -2,12 +2,11 @@
#include <Tactility/hal/touch/TouchDevice.h>
#include <Tactility/TactilityCore.h>
#include <Tactility/Thread.h>
#include <driver/i2c.h>
#include "ft6x36/FT6X36.h"
#include <EspLcdTouch.h>
class Ft6x36Touch final : public tt::hal::touch::TouchDevice {
class Ft6x36Touch final : public EspLcdTouch {
public:
@@ -16,78 +15,56 @@ public:
Configuration(
i2c_port_t port,
gpio_num_t pinInterrupt,
uint16_t width,
uint16_t height
uint16_t xMax,
uint16_t yMax,
bool swapXy = false,
bool mirrorX = false,
bool mirrorY = false,
gpio_num_t pinReset = GPIO_NUM_NC,
gpio_num_t pinInterrupt = GPIO_NUM_NC,
unsigned int pinResetLevel = 0,
unsigned int pinInterruptLevel = 0
) : port(port),
xMax(xMax),
yMax(yMax),
swapXy(swapXy),
mirrorX(mirrorX),
mirrorY(mirrorY),
pinReset(pinReset),
pinInterrupt(pinInterrupt),
width(width),
height(height)
pinResetLevel(pinResetLevel),
pinInterruptLevel(pinInterruptLevel)
{}
i2c_port_t port;
uint16_t xMax;
uint16_t yMax;
bool swapXy;
bool mirrorX;
bool mirrorY;
gpio_num_t pinReset;
gpio_num_t pinInterrupt;
uint16_t width;
uint16_t height;
};
unsigned int pinResetLevel;
unsigned int pinInterruptLevel;
};
private:
std::unique_ptr<Configuration> configuration;
lv_indev_t* deviceHandle = nullptr;
FT6X36 driver = FT6X36(configuration->port, configuration->pinInterrupt);
std::shared_ptr<tt::Thread> driverThread;
bool interruptDriverThread = false;
tt::Mutex mutex;
std::shared_ptr<tt::hal::touch::TouchDriver> nativeTouch;
lv_point_t lastPoint = { .x = 0, .y = 0 };
lv_indev_state_t lastState = LV_INDEV_STATE_RELEASED;
bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override;
bool shouldInterruptDriverThread() const;
bool createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) override;
void driverThreadMain();
static void touchReadCallback(lv_indev_t* indev, lv_indev_data_t* data);
esp_lcd_touch_config_t createEspLcdTouchConfig() override;
public:
explicit Ft6x36Touch(std::unique_ptr<Configuration> inConfiguration);
~Ft6x36Touch() override;
explicit Ft6x36Touch(std::unique_ptr<Configuration> inConfiguration) : configuration(std::move(inConfiguration)) {
assert(configuration != nullptr);
}
std::string getName() const override { return "FT6x36"; }
std::string getDescription() const override { return "FT6x36 I2C touch driver"; }
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 deviceHandle; }
class Ft6TouchDriver : public tt::hal::touch::TouchDriver {
public:
const Ft6x36Touch& parent;
Ft6TouchDriver(const Ft6x36Touch& parent) : parent(parent) {}
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* _Nullable strength, uint8_t* pointCount, uint8_t maxPointCount) {
auto lock = parent.mutex.asScopedLock();
lock.lock();
if (parent.lastState == LV_INDEV_STATE_PRESSED) {
*x = parent.lastPoint.x;
*y = parent.lastPoint.y;
*pointCount = 1;
return true;
} else {
*pointCount = 0;
return false;
}
}
};
bool supportsTouchDriver() override { return true; }
std::shared_ptr<tt::hal::touch::TouchDriver> _Nullable getTouchDriver() override { return nativeTouch; }
};
-378
View File
@@ -1,378 +0,0 @@
#include "FT6X36.h"
#include "freertos/FreeRTOS.h"
#define CONFIG_FT6X36_DEBUG false
FT6X36 *FT6X36::_instance = nullptr;
static const char *TAG = "i2c-touch";
//Handle indicating I2C is ready to read the touch
SemaphoreHandle_t TouchSemaphore = xSemaphoreCreateBinary();
FT6X36::FT6X36(i2c_port_t port, gpio_num_t interruptPin)
{
_instance = this;
_port = port;
_intPin = interruptPin;
}
// Destructor should detach interrupt to the pin
FT6X36::~FT6X36()
{
if (_intPin >= 0)
gpio_isr_handler_remove((gpio_num_t)_intPin);
}
bool FT6X36::begin(uint8_t threshold, uint16_t width, uint16_t height)
{
_touch_width = width;
_touch_height = height;
if (width == 0 || height ==0) {
ESP_LOGE(TAG,"begin(uint8_t threshold, uint16_t width, uint16_t height) did not receive the width / height so touch cannot be rotation aware");
}
uint8_t data_panel_id;
readRegister8(FT6X36_REG_PANEL_ID, &data_panel_id);
if (data_panel_id != FT6X36_VENDID) {
ESP_LOGE(TAG,"FT6X36_VENDID does not match. Received:0x%x Expected:0x%x\n",data_panel_id,FT6X36_VENDID);
return false;
}
ESP_LOGI(TAG, "\tDevice ID: 0x%02x", data_panel_id);
uint8_t chip_id;
readRegister8(FT6X36_REG_CHIPID, &chip_id);
if (chip_id != FT6206_CHIPID && chip_id != FT6236_CHIPID && chip_id != FT6336_CHIPID) {
ESP_LOGE(TAG,"FT6206_CHIPID does not match. Received:0x%x\n",chip_id);
return false;
}
ESP_LOGI(TAG, "\tFound touch controller with Chip ID: 0x%02x", chip_id);
if (_intPin >= 0)
{
// INT pin triggers the callback function on the Falling edge of the GPIO
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_NEGEDGE; // GPIO_INTR_NEGEDGE repeats always interrupt
io_conf.pin_bit_mask = 1ULL<<_intPin;
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_down_en = (gpio_pulldown_t) 0; // disable pull-down mode
io_conf.pull_up_en = (gpio_pullup_t) 1; // pull-up mode
gpio_config(&io_conf);
esp_err_t isr_service = gpio_install_isr_service(0);
printf("ISR trigger install response: 0x%x %s\n", isr_service, (isr_service==0)?"ESP_OK":"");
gpio_isr_handler_add((gpio_num_t)_intPin, isr, (void*) 1);
}
writeRegister8(FT6X36_REG_DEVICE_MODE, 0x00);
writeRegister8(FT6X36_REG_THRESHHOLD, threshold);
writeRegister8(FT6X36_REG_TOUCHRATE_ACTIVE, 0x0E);
return true;
}
void FT6X36::registerTouchHandler(void (*fn)(TPoint point, TEvent e))
{
_touchHandler = fn;
if (CONFIG_FT6X36_DEBUG) printf("Touch handler function registered\n");
}
uint8_t FT6X36::touched()
{
uint8_t data_buf;
esp_err_t ret = readRegister8(FT6X36_REG_NUM_TOUCHES, &data_buf);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error reading from device: %s", esp_err_to_name(ret));
}
if (data_buf > 2)
{
data_buf = 0;
}
return data_buf;
}
void FT6X36::loop()
{
processTouch();
}
void IRAM_ATTR FT6X36::isr(void* arg)
{
/* Un-block the interrupt processing task now */
xSemaphoreGive(TouchSemaphore);
//xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
void FT6X36::processTouch()
{
/* Task move to Block state to wait for interrupt event */
if (_intPin >= 0)
{
if (xSemaphoreTake(TouchSemaphore, portMAX_DELAY) == false) return;
}
readData();
uint8_t n = 0;
TRawEvent event = (TRawEvent)_touchEvent[n];
TPoint point{_touchX[n], _touchY[n]};
switch (event) {
case TRawEvent::PressDown:
_points[0] = point;
_dragMode = false;
// Note: Is in microseconds. Ref https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html
_touchStartTime = esp_timer_get_time()/1000;
fireEvent(point, TEvent::TouchStart);
break;
case TRawEvent::Contact:
// Dragging makes no sense IMHO. Since the X & Y are not getting updated while dragging
// Dragging && _points[0].aboutEqual(point) - Not used IDEA 2: && (lastEvent == 2)
if (!_dragMode &&
(abs(lastX-_touchX[n]) <= maxDeviation || abs(lastY-_touchY[n])<=maxDeviation) &&
esp_timer_get_time()/1000 - _touchStartTime > 300) {
_dragMode = true;
fireEvent(point, TEvent::DragStart);
#if defined(CONFIG_FT6X36_DEBUG_EVENTS) && CONFIG_FT6X36_DEBUG_EVENTS==1
printf("EV: DragStart\n");
#endif
} else if (_dragMode) {
fireEvent(point, TEvent::DragMove);
#if defined(CONFIG_FT6X36_DEBUG_EVENTS) && CONFIG_FT6X36_DEBUG_EVENTS==1
printf("EV: DragMove\n");
#endif
}
fireEvent(point, TEvent::TouchMove);
// For me the _touchStartTime shouold be set in both PressDown & Contact events, but after Drag detection
_touchStartTime = esp_timer_get_time()/1000;
break;
case TRawEvent::LiftUp:
_points[9] = point;
_touchEndTime = esp_timer_get_time()/1000;
//printf("TIMEDIFF: %lu End: %lu\n", _touchEndTime - _touchStartTime, _touchEndTime);
fireEvent(point, TEvent::TouchEnd);
if (_dragMode) {
fireEvent(point, TEvent::DragEnd);
#if defined(CONFIG_FT6X36_DEBUG_EVENTS) && CONFIG_FT6X36_DEBUG_EVENTS==1
printf("EV: DragEnd\n");
#endif
_dragMode = false;
}
if ( _touchEndTime - _touchStartTime <= 900) {
// Do not get why this: _points[0].aboutEqual(point) (Original library)
fireEvent(point, TEvent::Tap);
_points[0] = {0, 0};
_touchStartTime = 0;
#if defined(CONFIG_FT6X36_DEBUG_EVENTS) && CONFIG_FT6X36_DEBUG_EVENTS==1
printf("EV: Tap\n");
#endif
_dragMode = false;
}
break;
case TRawEvent::NoEvent:
#if defined(CONFIG_FT6X36_DEBUG_EVENTS) && CONFIG_FT6X36_DEBUG_EVENTS==1
printf("EV: NoEvent\n");
#endif
break;
}
// Store lastEvent
lastEvent = (int) event;
lastX = _touchX[0];
lastY = _touchY[0];
}
void FT6X36::poll(TPoint * point, TEvent * e)
{
readData();
// TPoint point{_touchX[0], _touchY[0]};
TRawEvent event = (TRawEvent)_touchEvent[0];
if (point != NULL)
{
point->x = _touchX[0];
point->y = _touchY[0];
}
if (e != NULL)
{
switch (event)
{
case TRawEvent::PressDown:
*e = TEvent::TouchStart;
break;
case TRawEvent::Contact:
*e = TEvent::TouchMove;
break;
case TRawEvent::LiftUp:
default:
*e = TEvent::TouchEnd;
break;
}
}
}
uint8_t FT6X36::read8(uint8_t regName) {
uint8_t buf;
readRegister8(regName, &buf);
return buf;
}
#define data_size 16 // Discarding last 2: 0x0E & 0x0F as not relevant
bool FT6X36::readData(void)
{
esp_err_t ret;
uint8_t data[data_size];
uint8_t touch_pnt_cnt; // Number of detected touch points
readRegister8(FT6X36_REG_NUM_TOUCHES, &touch_pnt_cnt);
// Read data
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (FT6X36_ADDR<<1), ACK_CHECK_EN);
i2c_master_write_byte(cmd, 0, ACK_CHECK_EN);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(_port, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) {
return ret;
}
cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (FT6X36_ADDR<<1)|1, ACK_CHECK_EN);
i2c_master_read(cmd, data, data_size, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(_port, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
if (CONFIG_FT6X36_DEBUG) {
//printf("REGISTERS:\n");
for (int16_t i = 0; i < data_size; i++)
{
printf("%x:%x ", i, data[i]);
}
printf("\n");
}
const uint8_t addrShift = 6;
// READ X, Y and Touch events (X 2)
for (uint8_t i = 0; i < 2; i++)
{
_touchX[i] = data[FT6X36_REG_P1_XH + i * addrShift] & 0x0F;
_touchX[i] <<= 8;
_touchX[i] |= data[FT6X36_REG_P1_XL + i * addrShift];
_touchY[i] = data[FT6X36_REG_P1_YH + i * addrShift] & 0x0F;
_touchY[i] <<= 8;
_touchY[i] |= data[FT6X36_REG_P1_YL + i * addrShift];
_touchEvent[i] = data[FT6X36_REG_P1_XH + i * addrShift] >> 6;
}
// Make _touchX[idx] and _touchY[idx] rotation aware
switch (_rotation)
{
case 1:
swap(_touchX[0], _touchY[0]);
swap(_touchX[1], _touchY[1]);
_touchY[0] = _touch_width - _touchY[0] -1;
_touchY[1] = _touch_width - _touchY[1] -1;
break;
case 2:
_touchX[0] = _touch_width - _touchX[0] - 1;
_touchX[1] = _touch_width - _touchX[1] - 1;
_touchY[0] = _touch_height - _touchY[0] - 1;
_touchY[1] = _touch_height - _touchY[1] - 1;
break;
case 3:
swap(_touchX[0], _touchY[0]);
swap(_touchX[1], _touchY[1]);
_touchX[0] = _touch_height - _touchX[0] - 1;
_touchX[1] = _touch_height - _touchX[1] - 1;
break;
}
if (CONFIG_FT6X36_DEBUG) {
printf("X0:%d Y0:%d EVENT:%d\n", _touchX[0], _touchY[0], _touchEvent[0]);
//printf("X1:%d Y1:%d EVENT:%d\n", _touchX[1], _touchY[1], _touchEvent[1]);
}
return true;
}
void FT6X36::writeRegister8(uint8_t reg, uint8_t value)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, FT6X36_ADDR << 1 | I2C_MASTER_WRITE, ACK_CHECK_EN);
i2c_master_write_byte(cmd, reg , ACK_CHECK_EN);
i2c_master_write_byte(cmd, value , ACK_CHECK_EN);
i2c_master_stop(cmd);
i2c_master_cmd_begin(_port, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
}
uint8_t FT6X36::readRegister8(uint8_t reg, uint8_t *data_buf)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, FT6X36_ADDR << 1 | I2C_MASTER_WRITE, ACK_CHECK_EN);
i2c_master_write_byte(cmd, reg, I2C_MASTER_ACK);
// Research: Why it's started a 2nd time here
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (FT6X36_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, data_buf, I2C_MASTER_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(_port, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
//FT6X36_REG_GESTURE_ID. Check if it can be read!
#if defined(FT6X36_DEBUG) && FT6X36_DEBUG==1
printf("REG 0x%x: 0x%x\n",reg,ret);
#endif
return ret;
}
void FT6X36::fireEvent(TPoint point, TEvent e)
{
if (_touchHandler)
_touchHandler(point, e);
}
void FT6X36::debugInfo()
{
printf(" TH_DIFF: %d CTRL: %d\n", read8(FT6X36_REG_FILTER_COEF), read8(FT6X36_REG_CTRL));
printf(" TIMEENTERMONITOR: %d PERIODACTIVE: %d\n", read8(FT6X36_REG_TIME_ENTER_MONITOR), read8(FT6X36_REG_TOUCHRATE_ACTIVE));
printf(" PERIODMONITOR: %d RADIAN_VALUE: %d\n", read8(FT6X36_REG_TOUCHRATE_MONITOR), read8(FT6X36_REG_RADIAN_VALUE));
printf(" OFFSET_LEFT_RIGHT: %d OFFSET_UP_DOWN: %d\n", read8(FT6X36_REG_OFFSET_LEFT_RIGHT), read8(FT6X36_REG_OFFSET_UP_DOWN));
printf("DISTANCE_LEFT_RIGHT: %d DISTANCE_UP_DOWN: %d\n", read8(FT6X36_REG_DISTANCE_LEFT_RIGHT), read8(FT6X36_REG_DISTANCE_UP_DOWN));
printf(" DISTANCE_ZOOM: %d CIPHER: %d\n", read8(FT6X36_REG_DISTANCE_ZOOM), read8(FT6X36_REG_CHIPID));
printf(" G_MODE: %d PWR_MODE: %d\n", read8(FT6X36_REG_INTERRUPT_MODE), read8(FT6X36_REG_POWER_MODE));
printf(" FIRMID: %d FOCALTECH_ID: %d STATE: %d\n", read8(FT6X36_REG_FIRMWARE_VERSION), read8(FT6X36_REG_PANEL_ID), read8(FT6X36_REG_STATE));
}
void FT6X36::setRotation(uint8_t rotation) {
_rotation = rotation;
}
void FT6X36::setTouchWidth(uint16_t width) {
printf("touch w:%d\n",width);
_touch_width = width;
}
void FT6X36::setTouchHeight(uint16_t height) {
printf("touch h:%d\n",height);
_touch_height = height;
}
-184
View File
@@ -1,184 +0,0 @@
#include <cstdio>
#include <cstdlib>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "driver/i2c.h"
#include "sdkconfig.h"
#include <esp_timer.h>
#ifndef ft6x36_h
#define ft6x36_h
// I2C Constants
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define ACK_CHECK_EN 0x1 /*!< I2C master will check ack from slave*/
#define ACK_CHECK_DIS 0x0 /*!< I2C master will not check ack from slave */
#define ACK_VAL 0x0 /*!< I2C ack value */
#define NACK_VAL 0x1 /*!< I2C nack value */
//SemaphoreHandle_t print_mux = NULL;
#define FT6X36_ADDR 0x38
#define FT6X36_REG_DEVICE_MODE 0x00
#define FT6X36_REG_GESTURE_ID 0x01
#define FT6X36_REG_NUM_TOUCHES 0x02
#define FT6X36_REG_P1_XH 0x03
#define FT6X36_REG_P1_XL 0x04
#define FT6X36_REG_P1_YH 0x05
#define FT6X36_REG_P1_YL 0x06
#define FT6X36_REG_P1_WEIGHT 0x07
#define FT6X36_REG_P1_MISC 0x08
#define FT6X36_REG_P2_XH 0x09
#define FT6X36_REG_P2_XL 0x0A
#define FT6X36_REG_P2_YH 0x0B
#define FT6X36_REG_P2_YL 0x0C
#define FT6X36_REG_P2_WEIGHT 0x0D
#define FT6X36_REG_P2_MISC 0x0E
#define FT6X36_REG_THRESHHOLD 0x80
#define FT6X36_REG_FILTER_COEF 0x85
#define FT6X36_REG_CTRL 0x86
#define FT6X36_REG_TIME_ENTER_MONITOR 0x87
#define FT6X36_REG_TOUCHRATE_ACTIVE 0x88
#define FT6X36_REG_TOUCHRATE_MONITOR 0x89 // value in ms
#define FT6X36_REG_RADIAN_VALUE 0x91
#define FT6X36_REG_OFFSET_LEFT_RIGHT 0x92
#define FT6X36_REG_OFFSET_UP_DOWN 0x93
#define FT6X36_REG_DISTANCE_LEFT_RIGHT 0x94
#define FT6X36_REG_DISTANCE_UP_DOWN 0x95
#define FT6X36_REG_DISTANCE_ZOOM 0x96
#define FT6X36_REG_LIB_VERSION_H 0xA1
#define FT6X36_REG_LIB_VERSION_L 0xA2
#define FT6X36_REG_CHIPID 0xA3
#define FT6X36_REG_INTERRUPT_MODE 0xA4
#define FT6X36_REG_POWER_MODE 0xA5
#define FT6X36_REG_FIRMWARE_VERSION 0xA6
#define FT6X36_REG_PANEL_ID 0xA8
#define FT6X36_REG_STATE 0xBC
#define FT6X36_PMODE_ACTIVE 0x00
#define FT6X36_PMODE_MONITOR 0x01
#define FT6X36_PMODE_STANDBY 0x02
#define FT6X36_PMODE_HIBERNATE 0x03
/* Possible values returned by FT6X36_GEST_ID_REG */
#define FT6X36_GEST_ID_NO_GESTURE 0x00
#define FT6X36_GEST_ID_MOVE_UP 0x10
#define FT6X36_GEST_ID_MOVE_RIGHT 0x14
#define FT6X36_GEST_ID_MOVE_DOWN 0x18
#define FT6X36_GEST_ID_MOVE_LEFT 0x1C
#define FT6X36_GEST_ID_ZOOM_IN 0x48
#define FT6X36_GEST_ID_ZOOM_OUT 0x49
#define FT6X36_VENDID 0x11
#define FT6206_CHIPID 0x06
#define FT6236_CHIPID 0x36
#define FT6336_CHIPID 0x64
#define FT6X36_DEFAULT_THRESHOLD 22
// From: https://github.com/lvgl/lv_port_esp32/blob/master/components/lvgl_esp32_drivers/lvgl_touch/ft6x36.h
#define FT6X36_MSB_MASK 0x0F
#define FT6X36_LSB_MASK 0xFF
enum class TRawEvent
{
PressDown,
LiftUp,
Contact,
NoEvent
};
enum class TEvent
{
None,
TouchStart,
TouchMove,
TouchEnd,
Tap,
DragStart,
DragMove,
DragEnd
};
struct TPoint
{
uint16_t x;
uint16_t y;
/**
* This is being used in the original library but I'm not using it in this implementation
*/
bool aboutEqual(const TPoint point)
{
return abs(x - point.x) <= 5 && abs(y - point.y) <= 5;
}
};
class FT6X36
{
static void IRAM_ATTR isr(void* arg);
public:
// TwoWire * wire will be replaced by ESP-IDF https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2c.html
FT6X36(i2c_port_t = I2C_NUM_0, gpio_num_t interruptPin = GPIO_NUM_NC);
~FT6X36();
bool begin(uint8_t threshold = FT6X36_DEFAULT_THRESHOLD, uint16_t width = 0, uint16_t height = 0);
void registerTouchHandler(void(*fn)(TPoint point, TEvent e));
uint8_t touched();
void loop();
void processTouch();
void debugInfo();
void poll(TPoint * point, TEvent * event);
// Helper functions to make the touch display aware
void setRotation(uint8_t rotation);
void setTouchWidth(uint16_t width);
void setTouchHeight(uint16_t height);
// Pending implementation. How much x->touch y↓touch is placed (In case is smaller than display)
void setXoffset(uint16_t x_offset);
void setYoffset(uint16_t y_offset);
// Smart template from EPD to swap x,y:
template <typename T> static inline void
swap(T& a, T& b)
{
T t = a;
a = b;
b = t;
}
void(*_touchHandler)(TPoint point, TEvent e) = nullptr;
bool readData(void);
private:
void writeRegister8(uint8_t reg, uint8_t val);
uint8_t readRegister8(uint8_t reg, uint8_t *data_buf);
void fireEvent(TPoint point, TEvent e);
uint8_t read8(uint8_t regName);
static FT6X36 * _instance;
i2c_port_t _port;
int8_t _intPin;
// Make touch rotation aware:
uint8_t _rotation = 0;
uint16_t _touch_width = 0;
uint16_t _touch_height = 0;
uint8_t _touches;
uint16_t _touchX[2], _touchY[2], _touchEvent[2];
TPoint _points[10];
uint8_t _pointIdx = 0;
unsigned long _touchStartTime = 0;
unsigned long _touchEndTime = 0;
uint8_t lastEvent = 3; // No event
uint16_t lastX = 0;
uint16_t lastY = 0;
bool _dragMode = false;
const uint8_t maxDeviation = 5;
};
#endif
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 strange_v
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-8
View File
@@ -1,8 +0,0 @@
This project is an adaption of the code at https://github.com/martinberlin/FT6X36-IDF which is an adaptation of https://github.com/strange-v/FT6X36
The original license is an MIT license and is included in this directory.
Changes:
- Remove Kconfig-based configuratio
- Removed I2C init code
- Allow for passing a different I2C port
+1
View File
@@ -14,6 +14,7 @@ std::shared_ptr<EspLcdConfiguration> St7789Display::createEspLcdConfiguration(co
.mirrorY = configuration.mirrorY,
.invertColor = configuration.invertColor,
.bufferSize = configuration.bufferSize,
.buffSpiram = configuration.buffSpiram,
.touch = configuration.touch,
.backlightDutyFunction = configuration.backlightDutyFunction,
.resetPin = configuration.resetPin,
+1
View File
@@ -25,6 +25,7 @@ public:
std::function<void(uint8_t)> _Nullable backlightDutyFunction;
gpio_num_t resetPin;
bool lvglSwapBytes;
bool buffSpiram = false;
lcd_rgb_element_order_t rgbElementOrder = LCD_RGB_ELEMENT_ORDER_RGB;
};
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(ina226-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel
)
+5
View File
@@ -0,0 +1,5 @@
# INA226
Texas Instruments INA226 High-Side or Low-Side Measurement, Bi-Directional Current and Power Monitor.
- Datasheet: https://www.ti.com/lit/ds/symlink/ina226.pdf
@@ -0,0 +1,12 @@
description: TI INA226 voltage/current monitor
include: ["i2c-device.yaml"]
compatible: "ti,ina226"
properties:
shunt-milliohms:
type: int
required: true
# shunt value should be a minimum of 1
description: Shunt resistor value in milliohms
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/ina226.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(ina226, struct Ina226Config)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdint.h>
#include <tactility/error.h>
struct Device;
#ifdef __cplusplus
extern "C" {
#endif
struct Ina226Config {
uint8_t address;
uint16_t shunt_milliohms;
};
/** Bus voltage in volts (1.25 mV/LSB) */
error_t ina226_read_bus_voltage(struct Device* device, float* volts);
/** Shunt current in amps (positive = charging, negative = discharging) */
error_t ina226_read_shunt_current(struct Device* device, float* amps);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module ina226_module;
#ifdef __cplusplus
}
#endif
+138
View File
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/ina226.h>
#include <ina226_module.h>
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#define TAG "INA226"
// ---------------------------------------------------------------------------
// Register map
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_CONFIG = 0x00; ///< RW - Configuration
static constexpr uint8_t REG_SHUNT_VOLT = 0x01; ///< R - Shunt voltage (2.5 µV/LSB, signed)
static constexpr uint8_t REG_BUS_VOLT = 0x02; ///< R - Bus voltage (1.25 mV/LSB)
static constexpr uint8_t REG_CURRENT = 0x04; ///< R - Current (CURRENT_LSB/LSB, signed; valid after calibration)
static constexpr uint8_t REG_CALIBRATION = 0x05; ///< RW - Calibration
static constexpr uint8_t REG_MANUFACTURER_ID = 0xFE;
static constexpr uint16_t EXPECTED_MFR_ID = 0x5449; // "TI"
// CONFIG: AVG=16 (010), VBUSCT=1100µs (100), VSHCT=1100µs (100), MODE=continuous shunt+bus (111)
static constexpr uint16_t CONFIG_VALUE = 0x4527;
static constexpr uint16_t CONFIG_SHUTDOWN = 0x4520; // Same as CONFIG_VALUE but MODE=000
static constexpr float MAX_CURRENT_A = 8.192f;
static constexpr float CURRENT_LSB = MAX_CURRENT_A / 32768.0f;
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50);
#define GET_CONFIG(device) (static_cast<const Ina226Config*>((device)->config))
// ---------------------------------------------------------------------------
// Driver lifecycle
// ---------------------------------------------------------------------------
static error_t start(Device* device) {
Device* i2c = device_get_parent(device);
if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) {
LOG_E(TAG, "Parent is not an I2C controller");
return ERROR_RESOURCE;
}
const uint8_t addr = GET_CONFIG(device)->address;
uint16_t mfr_id = 0;
error_t err = i2c_controller_register16be_get(i2c, addr, REG_MANUFACTURER_ID, &mfr_id, TIMEOUT);
if (err != ERROR_NONE) {
LOG_E(TAG, "Failed to read INA226 manufacturer ID");
return ERROR_RESOURCE;
}
if (mfr_id != EXPECTED_MFR_ID) {
LOG_E(TAG, "Wrong device detected (mfr_id=0x%04X, expected=0x%04X)", mfr_id, EXPECTED_MFR_ID);
return ERROR_RESOURCE;
}
if (i2c_controller_register16be_set(i2c, addr, REG_CONFIG, CONFIG_VALUE, TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to configure INA226");
return ERROR_RESOURCE;
}
const uint16_t shunt_milliohms = GET_CONFIG(device)->shunt_milliohms;
if (shunt_milliohms == 0) {
LOG_E(TAG, "Invalid shunt value: 0 mOhms");
return ERROR_INVALID_ARGUMENT;
}
const float shunt_ohms = shunt_milliohms / 1000.0f;
const float cal_f = 0.00512f / (CURRENT_LSB * shunt_ohms);
if (cal_f < 1.0f || cal_f > 65535.0f) {
LOG_E(TAG, "Calibration out of range (shunt=%u mOhms)", shunt_milliohms);
return ERROR_INVALID_ARGUMENT;
}
const uint16_t cal_value = static_cast<uint16_t>(cal_f);
if (i2c_controller_register16be_set(i2c, addr, REG_CALIBRATION, cal_value, TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to calibrate INA226");
return ERROR_RESOURCE;
}
LOG_I(TAG, "INA226 started (addr=0x%02X, shunt=%umΩ, cal=0x%04X)", addr, GET_CONFIG(device)->shunt_milliohms, cal_value);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* i2c = device_get_parent(device);
if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) {
LOG_E(TAG, "Parent is not an I2C controller");
return ERROR_RESOURCE;
}
auto addr = GET_CONFIG(device)->address;
// Put device into shutdown mode to save power
if (i2c_controller_register16be_set(i2c, addr, REG_CONFIG, CONFIG_SHUTDOWN, TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to shutdown INA226 (ignored)");
}
return ERROR_NONE;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
extern "C" {
error_t ina226_read_bus_voltage(Device* device, float* volts) {
if (device == nullptr) return ERROR_INVALID_ARGUMENT;
if (volts == nullptr) return ERROR_INVALID_ARGUMENT;
uint16_t raw = 0;
error_t err = i2c_controller_register16be_get(device_get_parent(device), GET_CONFIG(device)->address, REG_BUS_VOLT, &raw, TIMEOUT);
if (err != ERROR_NONE) return err;
*volts = static_cast<float>(raw) * 0.00125f;
return ERROR_NONE;
}
error_t ina226_read_shunt_current(Device* device, float* amps) {
if (device == nullptr) return ERROR_INVALID_ARGUMENT;
if (amps == nullptr) return ERROR_INVALID_ARGUMENT;
uint16_t raw = 0;
error_t err = i2c_controller_register16be_get(device_get_parent(device), GET_CONFIG(device)->address, REG_CURRENT, &raw, TIMEOUT);
if (err != ERROR_NONE) return err;
*amps = static_cast<float>(static_cast<int16_t>(raw)) * CURRENT_LSB;
return ERROR_NONE;
}
Driver ina226_driver = {
.name = "ina226",
.compatible = (const char*[]) { "ti,ina226", nullptr },
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = nullptr,
.owner = &ina226_module,
.internal = nullptr
};
} // extern "C"
+34
View File
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver ina226_driver;
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
* there is no guarantee that the previously constructed drivers can be destroyed */
check(driver_construct_add(&ina226_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
check(driver_remove_destruct(&ina226_driver) == ERROR_NONE);
return ERROR_NONE;
}
extern const ModuleSymbol ina226_module_symbols[];
Module ina226_module = {
.name = "ina226",
.start = start,
.stop = stop,
.symbols = ina226_module_symbols,
.internal = nullptr
};
} // extern "C"
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/ina226.h>
#include <tactility/module.h>
const struct ModuleSymbol ina226_module_symbols[] = {
DEFINE_MODULE_SYMBOL(ina226_read_bus_voltage),
DEFINE_MODULE_SYMBOL(ina226_read_shunt_current),
MODULE_SYMBOL_TERMINATOR
};
@@ -41,6 +41,8 @@ error_t m5pm1_is_charging(struct Device* device, bool* charging);
error_t m5pm1_set_charge_enable(struct Device* device, bool enable);
error_t m5pm1_set_boost_enable(struct Device* device, bool enable); ///< 5V BOOST / Grove power
error_t m5pm1_set_ldo_enable(struct Device* device, bool enable); ///< 3.3V LDO
/** PM1_G3: speaker amplifier enable (HIGH = on) */
error_t m5pm1_set_speaker_enable(struct Device* device, bool enable);
// ---------------------------------------------------------------------------
// Temperature (internal chip sensor)
+32
View File
@@ -49,6 +49,9 @@ static constexpr uint8_t ADC_CH_TEMP = 6;
// PM1_G2: LCD power enable on M5Stack StickS3
static constexpr uint8_t LCD_POWER_BIT = (1U << 2U);
// PM1_G3: Speaker amplifier enable on M5Stack StickS3
static constexpr uint8_t SPEAKER_AMP_BIT = (1U << 3U);
static constexpr uint8_t SPEAKER_AMP_FUNC_MASK = (0x3U << 6U); // GPIO3 function select bits
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50);
@@ -90,6 +93,13 @@ static error_t start(Device* device) {
LOG_W(TAG, "Failed to disable I2C sleep (non-fatal)");
}
// BOOST_EN → EXT_5V / Grove / Hat power rail always on
if (i2c_controller_register8_set_bits(i2c, addr, REG_PWR_CFG, PWR_CFG_BOOST_EN, TIMEOUT) == ERROR_NONE) {
LOG_I(TAG, "EXT_5V boost enabled");
} else {
LOG_W(TAG, "Failed to enable EXT_5V boost (non-fatal)");
}
// PM1_G2 → LCD power enable (L3B rail on StickS3)
// Sequence matches M5GFX: clear FUNC0 bit2, set MODE bit2 output, clear DRV bit2 push-pull, set OUT bit2 high
bool lcd_ok =
@@ -104,6 +114,18 @@ static error_t start(Device* device) {
LOG_E(TAG, "Failed to enable LCD power via PM1_G2");
}
// PM1_G3 → speaker amp EN, initially LOW (amp off until audio starts)
bool spk_ok =
i2c_controller_register8_reset_bits(i2c, addr, REG_GPIO_FUNC0, SPEAKER_AMP_FUNC_MASK, TIMEOUT) == ERROR_NONE &&
i2c_controller_register8_set_bits (i2c, addr, REG_GPIO_MODE, SPEAKER_AMP_BIT, TIMEOUT) == ERROR_NONE &&
i2c_controller_register8_reset_bits(i2c, addr, REG_GPIO_DRV, SPEAKER_AMP_BIT, TIMEOUT) == ERROR_NONE &&
i2c_controller_register8_reset_bits(i2c, addr, REG_GPIO_OUT, SPEAKER_AMP_BIT, TIMEOUT) == ERROR_NONE;
if (spk_ok) {
LOG_I(TAG, "Speaker amp pin configured");
} else {
LOG_W(TAG, "Failed to configure speaker amp pin");
}
return ERROR_NONE;
}
@@ -170,6 +192,16 @@ error_t m5pm1_set_ldo_enable(Device* device, bool enable) {
}
}
error_t m5pm1_set_speaker_enable(Device* device, bool enable) {
Device* i2c = device_get_parent(device);
const uint8_t addr = GET_CONFIG(device)->address;
if (enable) {
return i2c_controller_register8_set_bits(i2c, addr, REG_GPIO_OUT, SPEAKER_AMP_BIT, TIMEOUT);
} else {
return i2c_controller_register8_reset_bits(i2c, addr, REG_GPIO_OUT, SPEAKER_AMP_BIT, TIMEOUT);
}
}
error_t m5pm1_get_temperature(Device* device, uint16_t* decidegc) {
Device* i2c = device_get_parent(device);
uint8_t addr = GET_CONFIG(device)->address;
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(py32ioexpander-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel
)
@@ -0,0 +1,5 @@
description: M5Stack PY32 IO Expander (StackChan body module)
include: ["i2c-device.yaml"]
compatible: "m5stack,py32ioexpander"
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/py32ioexpander.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(py32ioexpander, struct Py32IoExpanderConfig)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/error.h>
struct Device;
#ifdef __cplusplus
extern "C" {
#endif
struct Py32IoExpanderConfig {
uint8_t address;
};
// ---------------------------------------------------------------------------
// GPIO (16 pins, index 015)
// ---------------------------------------------------------------------------
error_t py32_gpio_set_output(struct Device* device, uint8_t pin, bool value);
error_t py32_gpio_get_input(struct Device* device, uint8_t pin, bool* value);
// ---------------------------------------------------------------------------
// NeoPixel LED ring (up to 32 WS2812C LEDs)
// ---------------------------------------------------------------------------
error_t py32_led_set_count(struct Device* device, uint8_t count);
error_t py32_led_set_color(struct Device* device, uint8_t index, uint8_t r, uint8_t g, uint8_t b);
error_t py32_led_refresh(struct Device* device);
error_t py32_led_disable(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module py32ioexpander_module;
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver py32ioexpander_driver;
static error_t start() {
check(driver_construct_add(&py32ioexpander_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
check(driver_remove_destruct(&py32ioexpander_driver) == ERROR_NONE);
return ERROR_NONE;
}
Module py32ioexpander_module = {
.name = "py32ioexpander",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: Apache-2.0
// Reference: https://github.com/m5stack/StackChan/tree/main/firmware/main/hal/drivers/PY32IOExpander_Class
#include <drivers/py32ioexpander.h>
#include <py32ioexpander_module.h>
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#define TAG "PY32IOExpander"
// ---------------------------------------------------------------------------
// Register map
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_UID_L = 0x00; ///< R - Unique ID low byte
static constexpr uint8_t REG_VERSION = 0x02; ///< R - Firmware version
static constexpr uint8_t REG_GPIO_MODE_L = 0x03; ///< RW - GPIO direction [15:0] (1=output, 0=input), little-endian
static constexpr uint8_t REG_GPIO_OUT_L = 0x05; ///< RW - GPIO output level [15:0], little-endian
static constexpr uint8_t REG_GPIO_IN_L = 0x07; ///< R - GPIO input state [15:0], little-endian
static constexpr uint8_t REG_LED_CFG = 0x24; ///< RW - [6]=REFRESH [5:0]=LED_COUNT
static constexpr uint8_t REG_LED_DATA = 0x30; ///< RW - RGB565 data, 2 bytes per LED (max 32)
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50);
#define GET_CONFIG(device) (static_cast<const Py32IoExpanderConfig*>((device)->config))
// ---------------------------------------------------------------------------
// Driver lifecycle
// ---------------------------------------------------------------------------
static error_t start(Device* device) {
Device* i2c = device_get_parent(device);
if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) {
LOG_E(TAG, "Parent is not an I2C controller");
return ERROR_RESOURCE;
}
const uint8_t addr = GET_CONFIG(device)->address;
// PY32 may need a moment after power-on. Retry with increasing delays.
uint8_t version = 0;
bool online = false;
for (int attempt = 0; attempt < 5; attempt++) {
if (i2c_controller_register8_get(i2c, addr, REG_VERSION, &version, TIMEOUT) == ERROR_NONE &&
version != 0x00 && version != 0xFF) {
online = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(20 * (attempt + 1)));
}
if (!online) {
LOG_E(TAG, "PY32IOExpander not responding at 0x%02X — LED ring will not work", addr);
return ERROR_NONE; // non-fatal: don't crash the kernel
}
LOG_I(TAG, "PY32IOExpander online (addr=0x%02X, fw=0x%02X)", addr, version);
return ERROR_NONE;
}
static error_t stop(Device* device) {
return ERROR_NONE;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
extern "C" {
error_t py32_gpio_set_output(Device* device, uint8_t pin, bool value) {
if (pin > 15U) return ERROR_RESOURCE;
Device* i2c = device_get_parent(device);
const uint8_t addr = GET_CONFIG(device)->address;
// Ensure pin is in output mode
uint16_t mode = 0;
error_t err = i2c_controller_register16le_get(i2c, addr, REG_GPIO_MODE_L, &mode, TIMEOUT);
if (err != ERROR_NONE) return err;
mode |= static_cast<uint16_t>(1U << pin);
err = i2c_controller_register16le_set(i2c, addr, REG_GPIO_MODE_L, mode, TIMEOUT);
if (err != ERROR_NONE) return err;
// Set output level
uint16_t out = 0;
err = i2c_controller_register16le_get(i2c, addr, REG_GPIO_OUT_L, &out, TIMEOUT);
if (err != ERROR_NONE) return err;
if (value) {
out |= static_cast<uint16_t>(1U << pin);
} else {
out &= static_cast<uint16_t>(~(1U << pin));
}
return i2c_controller_register16le_set(i2c, addr, REG_GPIO_OUT_L, out, TIMEOUT);
}
error_t py32_gpio_get_input(Device* device, uint8_t pin, bool* value) {
if (pin > 15U) return ERROR_RESOURCE;
Device* i2c = device_get_parent(device);
uint16_t in = 0;
error_t err = i2c_controller_register16le_get(i2c, GET_CONFIG(device)->address, REG_GPIO_IN_L, &in, TIMEOUT);
if (err != ERROR_NONE) return err;
*value = (in >> pin) & 1U;
return ERROR_NONE;
}
error_t py32_led_set_count(Device* device, uint8_t count) {
if (count > 32U) return ERROR_RESOURCE;
Device* i2c = device_get_parent(device);
uint8_t cfg = count & 0x3FU;
return i2c_controller_register8_set(i2c, GET_CONFIG(device)->address, REG_LED_CFG, cfg, TIMEOUT);
}
error_t py32_led_set_color(Device* device, uint8_t index, uint8_t r, uint8_t g, uint8_t b) {
if (index > 32U) return ERROR_RESOURCE;
Device* i2c = device_get_parent(device);
// RGB565: [15:11]=R5 [10:5]=G6 [4:0]=B5, stored little-endian
uint16_t rgb565 = static_cast<uint16_t>(((r >> 3U) << 11U) | ((g >> 2U) << 5U) | (b >> 3U));
uint8_t buf[2] = {static_cast<uint8_t>(rgb565 & 0xFFU), static_cast<uint8_t>(rgb565 >> 8U)};
return i2c_controller_write_register(i2c, GET_CONFIG(device)->address, static_cast<uint8_t>(REG_LED_DATA + index * 2U), buf, 2, TIMEOUT);
}
error_t py32_led_refresh(Device* device) {
Device* i2c = device_get_parent(device);
const uint8_t addr = GET_CONFIG(device)->address;
// Read current LED_CFG (contains LED count), then set the REFRESH bit (6)
uint8_t cfg = 0;
error_t err = i2c_controller_register8_get(i2c, addr, REG_LED_CFG, &cfg, TIMEOUT);
if (err != ERROR_NONE) return err;
return i2c_controller_register8_set(i2c, addr, REG_LED_CFG, cfg | 0x40U, TIMEOUT);
}
error_t py32_led_disable(Device* device) {
// Write black to all 32 possible LED slots then refresh with count=32
// Using the maximum count ensures no stale color data is displayed.
static constexpr uint8_t BLACK[64] = {}; // 32 LEDs × 2 bytes, all zero
Device* i2c = device_get_parent(device);
uint8_t addr = GET_CONFIG(device)->address;
error_t err = i2c_controller_write_register(i2c, addr, REG_LED_DATA, BLACK, sizeof(BLACK), TIMEOUT);
if (err != ERROR_NONE) return err;
// count=32 | REFRESH
return i2c_controller_register8_set(i2c, addr, REG_LED_CFG, 0x60U, TIMEOUT);
}
Driver py32ioexpander_driver = {
.name = "py32ioexpander",
.compatible = (const char*[]) {"m5stack,py32ioexpander", nullptr},
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = nullptr,
.owner = &py32ioexpander_module,
.internal = nullptr
};
} // extern "C"