Implement Tulip Creative Computer 4r11 (#608)

This commit is contained in:
NellowTCS
2026-08-08 06:52:34 -06:00
committed by GitHub
parent c6373b79e9
commit 564d8af64c
20 changed files with 798 additions and 9 deletions
@@ -0,0 +1,9 @@
description: TI PCM5101A stereo DAC (I2S input, no register interface, no GPIO enable)
compatible: "ti,pcm5101a"
properties:
i2s:
type: phandle
required: true
description: "I2S controller device that carries audio data"
@@ -14,6 +14,7 @@ extern "C" {
// typedef even though they all share the same underlying config layout.
DEFINE_DEVICETREE(max98357a, struct DummyI2sAmpConfig)
DEFINE_DEVICETREE(ns4168, struct DummyI2sAmpConfig)
DEFINE_DEVICETREE(pcm5101a, struct DummyI2sAmpConfig)
#ifdef __cplusplus
}
@@ -373,7 +373,7 @@ extern "C" {
Driver dummy_i2s_amp_driver = {
.name = "dummy_i2s_amp",
.compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", nullptr },
.compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", "ti,pcm5101a", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &API,
@@ -30,8 +30,39 @@ properties:
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin
description: |
Reset GPIO pin. Reset polarity is set with the GPIO descriptor's ACTIVE_HIGH or
ACTIVE_LOW flag (active-low is the GT911's native convention; boards that invert the
reset line, e.g. Tulip 4 R11, use ACTIVE_HIGH).
pin-interrupt:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Interrupt GPIO pin
reset-pulses:
type: int
default: 1
description: |
Number of reset pulses applied before the controller is initialized. Some panels
(e.g. Tulip 4 R11) require two toggle cycles before they respond on I2C.
x-offset:
type: int
default: 0
description: |
Signed X coordinate correction added to the raw controller output before scaling.
Aligns the sensor with the panel (e.g. -2 on Tulip 4 R11).
y-offset:
type: int
default: 0
description: |
Signed Y coordinate correction added to the raw controller output before scaling
(e.g. -12 on Tulip 4 R11).
x-scale:
type: int
default: 1000
description: |
X coordinate scale as per-mille (1000 = 1.0), applied after x-offset.
y-scale:
type: int
default: 1000
description: |
Y coordinate scale as per-mille (1000 = 1.0), applied after y-offset.
@@ -22,6 +22,11 @@ struct Gt911Config {
bool mirror_y;
struct GpioPinSpec pin_reset;
struct GpioPinSpec pin_interrupt;
uint8_t reset_pulses;
int16_t x_offset;
int16_t y_offset;
uint16_t x_scale;
uint16_t y_scale;
};
#ifdef __cplusplus
+76 -4
View File
@@ -7,6 +7,7 @@
#include <tactility/driver.h>
#include <tactility/drivers/esp32_i2c.h>
#include <tactility/drivers/esp32_i2c_master.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
@@ -16,6 +17,8 @@
#include <esp_lcd_panel_io.h>
#include <esp_lcd_touch.h>
#include <esp_lcd_touch_gt911.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstdlib>
@@ -31,6 +34,42 @@ static gpio_num_t pin_or_nc(const GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
}
// Reset polarity is carried by the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag, so the
// pin is always pulsed with logical levels here. Some boards also require several toggle cycles
// before the controller starts responding on I2C (e.g. Tulip 4 R11). The esp_lcd_touch_gt911
// driver only performs a single reset, so the pin is pulsed here first
static error_t reset_controller_pin(const GpioPinSpec& pin, uint8_t pulses) {
if (pulses == 0 || pin.gpio_controller == nullptr) {
return ERROR_NONE;
}
auto* descriptor = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, pin.flags | GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
if (descriptor == nullptr) {
LOG_E(TAG, "Failed to acquire reset pin");
return ERROR_RESOURCE;
}
for (uint8_t i = 0; i < pulses; ++i) {
bool last_pulse = i == pulses - 1;
error_t error = gpio_descriptor_set_level(descriptor, true);
if (error == ERROR_NONE) {
vTaskDelay(pdMS_TO_TICKS(11));
error = gpio_descriptor_set_level(descriptor, false);
}
if (error == ERROR_NONE) {
vTaskDelay(pdMS_TO_TICKS(last_pulse ? 1000 : 60));
}
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to pulse reset pin");
gpio_descriptor_release(descriptor);
return error;
}
}
gpio_descriptor_release(descriptor);
return ERROR_NONE;
}
// region Driver lifecycle
// GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not
@@ -62,6 +101,32 @@ static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out
return ESP_ERR_NOT_SUPPORTED;
}
// Applies the per-device affine calibration (offset + per-mille scale) in the esp_lcd_touch
// framework's own coordinate hook, so downstream consumers always receive panel-space coords.
// Called before mirror/swap in esp_lcd_touch_get_coordinates().
static void gt911_process_coordinates(
esp_lcd_touch_handle_t tp,
uint16_t* x,
uint16_t* y,
uint16_t* strength,
uint8_t* point_count,
uint8_t max_point_count
) {
(void)strength;
(void)max_point_count;
auto* config = static_cast<const Gt911Config*>(tp->config.user_data);
if (config == nullptr) {
return;
}
for (uint8_t i = 0; i < *point_count; i++) {
int32_t mapped_x = (static_cast<int32_t>(x[i]) + config->x_offset) * config->x_scale / 1000;
int32_t mapped_y = (static_cast<int32_t>(y[i]) + config->y_offset) * config->y_scale / 1000;
x[i] = static_cast<uint16_t>(mapped_x < 0 ? 0 : (mapped_x > config->x_max ? config->x_max : mapped_x));
y[i] = static_cast<uint16_t>(mapped_y < 0 ? 0 : (mapped_y > config->y_max ? config->y_max : mapped_y));
}
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
@@ -73,6 +138,12 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY;
}
error_t error = reset_controller_pin(config->pin_reset, config->reset_pulses);
if (error != ERROR_NONE) {
free(internal);
return error;
}
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
if (ret != ESP_OK) {
free(internal);
@@ -84,9 +155,10 @@ static error_t start(Device* device) {
.y_max = config->y_max,
.rst_gpio_num = pin_or_nc(config->pin_reset),
.int_gpio_num = pin_or_nc(config->pin_interrupt),
// GT911's reset and interrupt lines are both fixed active-low in hardware.
// Reset polarity comes from the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag; the
// interrupt line is fixed active-low in hardware.
.levels = {
.reset = 0u,
.reset = (config->pin_reset.flags & GPIO_FLAG_ACTIVE_LOW) != 0 ? 0u : 1u,
.interrupt = 0u,
},
.flags = {
@@ -94,9 +166,9 @@ static error_t start(Device* device) {
.mirror_x = config->mirror_x ? 1u : 0u,
.mirror_y = config->mirror_y ? 1u : 0u,
},
.process_coordinates = nullptr,
.process_coordinates = gt911_process_coordinates,
.interrupt_callback = nullptr,
.user_data = nullptr,
.user_data = (void*)config,
.driver_data = nullptr,
};
@@ -222,3 +222,11 @@ properties:
type: phandle
default: "NULL"
description: Optional reference to this display's backlight device
custom-pixel-format:
type: int
default: 0
description: |
Optional software pixel-format conversion applied to scan-out pixels, when the host renders
in a different depth than the panel (LVGL always renders RGB565).
0 = no conversion (RGB_DISPLAY_PIXEL_FORMAT_DEFAULT), 1 = RGB332
(RGB_DISPLAY_PIXEL_FORMAT_RGB332) for 8 data-line panels.
@@ -11,6 +11,18 @@ extern "C" {
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
/**
* Optional software pixel-format conversion applied to scan-out pixels. The panel itself always
* runs at bits_per_pixel; when the host renders in a different depth (LVGL always renders RGB565
* for this driver), the selected mapper converts each tile before it reaches esp_lcd.
*/
enum RgbDisplayPixelFormat {
/** No conversion: scan-out pixels must already match bits_per_pixel. */
RGB_DISPLAY_PIXEL_FORMAT_DEFAULT = 0,
/** Convert RGB565 to packed 8-bit RGB332 (3R, 3G, 2B), for 8 data-line panels. */
RGB_DISPLAY_PIXEL_FORMAT_RGB332 = 1,
};
struct RgbDisplayConfig {
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
@@ -84,6 +96,9 @@ struct RgbDisplayConfig {
// Optional reference to this display's backlight device, NULL if none.
struct Device* backlight;
// Optional software pixel-format conversion for scan-out (see enum RgbDisplayPixelFormat).
enum RgbDisplayPixelFormat pixel_format;
};
#ifdef __cplusplus
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/**
* Opaque per-instance state for a software pixel mapper, allocated by SoftwarePixelMapperCreateFn
* and released by SoftwarePixelMapperDestroyFn. The instance owns its conversion destination
* buffer: the driver passes this handle as map()'s dst argument.
*/
typedef void* SoftwarePixelMapperData;
/**
* Allocates a mapper instance, including its destination buffer, sized for a width x height
* source buffer.
* @param width source buffer width in pixels
* @param height source buffer height in pixels
* @return a non-null instance handle, or NULL on allocation failure
*/
typedef SoftwarePixelMapperData (*SoftwarePixelMapperCreateFn)(uint16_t width, uint16_t height);
/**
* Converts pixel_count pixels from the mapper's source format to its destination format.
* @param data instance handle from SoftwarePixelMapperCreateFn
* @param src RGB565 source pixels
* @param dst destination buffer, typically the instance's own buffer (i.e. the data handle)
* @param pixel_count number of pixels to convert
*/
typedef void (*SoftwarePixelMapperMapFn)(SoftwarePixelMapperData data, const uint16_t* src, uint8_t* dst, uint32_t pixel_count);
/**
* Releases a mapper instance created by SoftwarePixelMapperCreateFn, including its destination
* buffer.
* @param data instance handle to release
*/
typedef void (*SoftwarePixelMapperDestroyFn)(SoftwarePixelMapperData data);
/**
* A software pixel-format conversion, used to translate a driver's native source depth into a
* panel's scan-out depth when the two differ. Each mapper owns its own destination buffer, so it
* can size the allocation for its output format.
*/
struct SoftwarePixelMapper {
SoftwarePixelMapperCreateFn create;
SoftwarePixelMapperMapFn map;
SoftwarePixelMapperDestroyFn destroy;
};
/**
* RGB565 to packed 8-bit RGB332 (3 bits red, 3 bits green, 2 bits blue) mapper for panels wired
* with only 8 data lines.
*/
extern const struct SoftwarePixelMapper software_pixel_mapper_rgb332;
#ifdef __cplusplus
}
#endif
@@ -4,6 +4,7 @@
#include <drivers/rgb_display.h>
#include <rgb_display_module.h>
#include <drivers/software_pixel_mapper.h>
#include <tactility/delay.h>
#include <tactility/device.h>
@@ -40,6 +41,12 @@ struct RgbDisplayInternal {
// Signaled by on_frame_buf_complete once per real DMA scan-out of a whole frame. Only
// waited on in draw_bitmap() when color_data is one of frame_buffers - see the comment there for why.
SemaphoreHandle_t frame_complete_semaphore;
// Software pixel-format conversion active when custom-pixel-format != DEFAULT (see start()).
// LVGL always renders RGB565 for this driver, so draw_bitmap() converts each tile through the
// mapper before handing it to esp_lcd. The mapper owns its scratch buffer, held in
// pixel_mapper_data. Null when no conversion is active.
const struct SoftwarePixelMapper* pixel_mapper;
SoftwarePixelMapperData pixel_mapper_data;
};
// esp_lcd_rgb_panel's draw_bitmap() has a zero-copy path when color_data is one of the panel's
@@ -128,6 +135,8 @@ static error_t start(Device* device) {
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->pixel_mapper = nullptr;
internal->pixel_mapper_data = nullptr;
error_t reset_error = perform_hardware_reset(config);
if (reset_error != ERROR_NONE) {
@@ -231,6 +240,28 @@ static error_t start(Device* device) {
return error;
}
if (config->pixel_format != RGB_DISPLAY_PIXEL_FORMAT_DEFAULT) {
switch (config->pixel_format) {
case RGB_DISPLAY_PIXEL_FORMAT_RGB332:
internal->pixel_mapper = &software_pixel_mapper_rgb332;
break;
default:
LOG_E(TAG, "Unsupported pixel format %d", (int)config->pixel_format);
free(internal);
return ERROR_INVALID_ARGUMENT;
}
// The mapper sizes and allocates its own whole-frame destination buffer, which lands in
// PSRAM on boards that have it (see software_pixel_mapper.cpp).
internal->pixel_mapper_data = internal->pixel_mapper->create(config->horizontal_resolution, config->vertical_resolution);
if (internal->pixel_mapper_data == nullptr) {
LOG_E(TAG, "Failed to create pixel mapper");
esp_lcd_panel_del(internal->panel_handle);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
}
internal->frame_complete_semaphore = xSemaphoreCreateBinary();
if (internal->frame_complete_semaphore == nullptr) {
esp_lcd_panel_del(internal->panel_handle);
@@ -264,6 +295,9 @@ static error_t stop(Device* device) {
}
vSemaphoreDelete(internal->frame_complete_semaphore);
if (internal->pixel_mapper != nullptr) {
internal->pixel_mapper->destroy(internal->pixel_mapper_data);
}
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
@@ -299,15 +333,33 @@ static bool rgb_display_color_data_is_frame_buffer(const RgbDisplayInternal* int
return false;
}
// Converts a contiguous RGB565 region into the configured scan-out format, if any. The
// conversion is delegated to the pixel mapper selected in start(); the result is written into the
// mapper's scratch buffer so the zero-copy frame-buffer wait logic below can treat it like any
// other caller-owned buffer.
static error_t rgb_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<RgbDisplayInternal*>(device_get_driver_data(device));
bool wait_for_scanout = rgb_display_color_data_is_frame_buffer(internal, color_data);
const void* source_data = color_data;
if (internal->pixel_mapper != nullptr) {
uint32_t pixel_count = (uint32_t)(x_end - x_start) * (uint32_t)(y_end - y_start);
// The mapper's own destination buffer is its instance data, so it is passed as both the
// instance handle and the output buffer.
internal->pixel_mapper->map(
internal->pixel_mapper_data,
static_cast<const uint16_t*>(color_data),
static_cast<uint8_t*>(internal->pixel_mapper_data),
pixel_count
);
source_data = internal->pixel_mapper_data;
}
bool wait_for_scanout = rgb_display_color_data_is_frame_buffer(internal, source_data);
if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal
}
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, source_data) != ESP_OK) {
return ERROR_RESOURCE;
}
@@ -375,7 +427,12 @@ static void rgb_display_get_frame_buffer(Device* device, uint8_t index, void** o
static uint8_t rgb_display_get_frame_buffer_count(Device* device) {
auto* internal = static_cast<RgbDisplayInternal*>(device_get_driver_data(device));
return internal->frame_buffer_count;
// A converted-format panel's frame buffer runs at the native bits_per_pixel, which LVGL can't
// write directly (it always renders RGB565 for this driver). Exposing it would make
// lvgl_display.c bind LVGL straight onto it and corrupt the buffer; instead report 0 so LVGL
// renders into its own RGB565 buffers and flushes per-tile through the conversion in
// draw_bitmap().
return GET_CONFIG(device)->pixel_format != RGB_DISPLAY_PIXEL_FORMAT_DEFAULT ? 0 : internal->frame_buffer_count;
}
static error_t rgb_display_get_backlight(Device* device, Device** backlight) {
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
#include <soc/soc_caps.h>
#if SOC_LCD_RGB_SUPPORTED
#include <drivers/software_pixel_mapper.h>
#include <esp_heap_caps.h>
// RGB332 packs each pixel into one byte: 3 bits red, 3 bits green, 2 bits blue. The byte layout
// maps straight onto an 8-data-line panel's significant color inputs (R7..R5, G7..G5, B7..B6),
// so each RGB565 channel's top bits land on the corresponding MSB lines
static void rgb332_map(SoftwarePixelMapperData data, const uint16_t* src, uint8_t* dst, uint32_t pixel_count) {
(void)data;
for (uint32_t i = 0; i < pixel_count; i++) {
uint16_t px = src[i];
dst[i] = (uint8_t)(((px >> 13) & 0x07) << 5) | (((px >> 8) & 0x07) << 2) | ((px >> 3) & 0x03);
}
}
// The destination buffer must hold a whole frame (1 byte/pixel for RGB332). A 1024x600 panel
// needs ~600KB, which only fits in PSRAM on most boards, prefer SPIRAM and fall back to whatever
// internal RAM is available. The returned handle is this buffer, passed back as map()'s dst.
static SoftwarePixelMapperData rgb332_create(uint16_t width, uint16_t height) {
size_t buffer_size = (size_t)width * height;
void* buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (buffer == nullptr) {
buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_DEFAULT);
}
return buffer;
}
static void rgb332_destroy(SoftwarePixelMapperData data) {
heap_caps_free(data);
}
const struct SoftwarePixelMapper software_pixel_mapper_rgb332 = {
.create = rgb332_create,
.map = rgb332_map,
.destroy = rgb332_destroy,
};
#endif // SOC_LCD_RGB_SUPPORTED