mirror of
https://github.com/markqvist/RNode_Firmware.git
synced 2026-04-27 22:35:36 +00:00
Board definition (BOARD_TWATCH_ULT = 0x45) for LilyGo T-Watch Ultra with verified pin mapping from LilyGoLib hardware docs. Working subsystems: - SX1262 LoRa radio: online at 868 MHz, tested with rnsd/Reticulum - AXP2101 PMU: all power rails configured, battery monitoring, charging - MIA-M10Q GPS: UART at 38400 baud, TinyGPSPlus NMEA parsing - PCF85063A RTC: time read/write, GPS sync infrastructure - XL9555 GPIO expander: I2C driver, LoRa antenna switch - BLE: initialized, KISS protocol responsive - Deep sleep: button wake (PMU IRQ GPIO 7), timer wake for beacon - Beacon sleep cycle: periodic wake for GPS beacon TX in standalone mode New files: - VISION.md: R-Watch product vision document - XL9555.h: minimal I2C GPIO expander driver - CO5300.h: QSPI AMOLED display driver (not yet functional) Display driver (CO5300.h) is written but disabled (HAS_DISPLAY=false). QSPI init succeeds but pixel writes don't reach the display controller. Suspected XL9555/BHI260AP GPIO expander pin mapping issue under investigation.
332 lines
9.9 KiB
C
332 lines
9.9 KiB
C
// CO5300 QSPI AMOLED Display Driver for T-Watch Ultra
|
|
// 410x502 pixels, 16-bit RGB565, QSPI interface
|
|
// Based on LilyGoLib display implementation (MIT license)
|
|
|
|
#ifndef CO5300_H
|
|
#define CO5300_H
|
|
|
|
#if BOARD_MODEL == BOARD_TWATCH_ULT
|
|
|
|
#include <Arduino.h>
|
|
#include "driver/spi_master.h"
|
|
#include "driver/gpio.h"
|
|
|
|
#define CO5300_CMD_SLPIN 0x10
|
|
#define CO5300_CMD_SLPOUT 0x11
|
|
#define CO5300_CMD_CASET 0x2A
|
|
#define CO5300_CMD_RASET 0x2B
|
|
#define CO5300_CMD_RAMWR 0x2C
|
|
#define CO5300_CMD_MADCTL 0x36
|
|
#define CO5300_CMD_BRIGHTNESS 0x51
|
|
|
|
#define CO5300_WIDTH 410
|
|
#define CO5300_HEIGHT 502
|
|
#define CO5300_OFFSET_X 22
|
|
#define CO5300_OFFSET_Y 0
|
|
|
|
#define CO5300_SPI_HOST SPI3_HOST
|
|
#define CO5300_SPI_FREQ_MHZ 45
|
|
#define CO5300_SEND_BUF_SIZE 16384
|
|
|
|
// Init command table entry
|
|
typedef struct {
|
|
uint8_t cmd;
|
|
uint8_t data[20];
|
|
uint8_t len; // bit 7 = delay 120ms after command
|
|
} co5300_cmd_t;
|
|
|
|
static const co5300_cmd_t co5300_init_cmds[] = {
|
|
{0xFE, {0x00}, 0x01},
|
|
{0xC4, {0x80}, 0x01},
|
|
{0x3A, {0x55}, 0x01}, // RGB565
|
|
{0x35, {0x00}, 0x01}, // Tearing effect on
|
|
{0x53, {0x20}, 0x01}, // Brightness control enable
|
|
{0x63, {0xFF}, 0x01},
|
|
{0x2A, {0x00, 0x16, 0x01, 0xAF}, 0x04}, // Column: 22 to 431
|
|
{0x2B, {0x00, 0x00, 0x01, 0xF5}, 0x04}, // Row: 0 to 501
|
|
{0x11, {0}, 0x80}, // Sleep out + delay
|
|
{0x29, {0}, 0x80}, // Display on + delay
|
|
{0x51, {0x00}, 0x01}, // Brightness = 0
|
|
};
|
|
#define CO5300_INIT_CMD_COUNT (sizeof(co5300_init_cmds) / sizeof(co5300_init_cmds[0]))
|
|
|
|
static spi_device_handle_t co5300_spi = NULL;
|
|
static bool co5300_ready = false;
|
|
static uint8_t co5300_brightness = 0;
|
|
|
|
// Send a command with optional data bytes via QSPI
|
|
static void co5300_write_cmd(uint8_t cmd, uint8_t *data, uint32_t len) {
|
|
digitalWrite(DISP_CS, LOW);
|
|
spi_transaction_t t = {};
|
|
t.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
|
|
t.cmd = 0x02; // QSPI write command
|
|
t.addr = cmd << 8; // Display command in address field
|
|
if (len > 0 && data) {
|
|
t.tx_buffer = data;
|
|
t.length = 8 * len;
|
|
}
|
|
spi_device_polling_transmit(co5300_spi, &t);
|
|
digitalWrite(DISP_CS, HIGH);
|
|
}
|
|
|
|
// Set the pixel address window
|
|
static void co5300_set_window(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
|
x1 += CO5300_OFFSET_X;
|
|
x2 += CO5300_OFFSET_X;
|
|
y1 += CO5300_OFFSET_Y;
|
|
y2 += CO5300_OFFSET_Y;
|
|
|
|
uint8_t caset[] = {highByte(x1), lowByte(x1), highByte(x2), lowByte(x2)};
|
|
uint8_t raset[] = {highByte(y1), lowByte(y1), highByte(y2), lowByte(y2)};
|
|
co5300_write_cmd(CO5300_CMD_CASET, caset, 4);
|
|
co5300_write_cmd(CO5300_CMD_RASET, raset, 4);
|
|
co5300_write_cmd(CO5300_CMD_RAMWR, NULL, 0);
|
|
}
|
|
|
|
// Push pixel data to the display (RGB565, big-endian)
|
|
void co5300_push_pixels(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *pixels) {
|
|
if (!co5300_ready) return;
|
|
|
|
spi_device_acquire_bus(co5300_spi, portMAX_DELAY);
|
|
co5300_set_window(x, y, x + w - 1, y + h - 1);
|
|
|
|
uint32_t total = w * h;
|
|
uint16_t *p = pixels;
|
|
bool first = true;
|
|
|
|
digitalWrite(DISP_CS, LOW);
|
|
while (total > 0) {
|
|
uint32_t chunk = (total > CO5300_SEND_BUF_SIZE) ? CO5300_SEND_BUF_SIZE : total;
|
|
spi_transaction_ext_t t = {};
|
|
if (first) {
|
|
t.base.flags = SPI_TRANS_MODE_QIO;
|
|
t.base.cmd = 0x32;
|
|
t.base.addr = 0x002C00;
|
|
first = false;
|
|
} else {
|
|
t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
|
|
SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
|
|
t.command_bits = 0;
|
|
t.address_bits = 0;
|
|
t.dummy_bits = 0;
|
|
}
|
|
t.base.tx_buffer = p;
|
|
t.base.length = chunk * 16;
|
|
spi_device_polling_transmit(co5300_spi, (spi_transaction_t *)&t);
|
|
p += chunk;
|
|
total -= chunk;
|
|
}
|
|
digitalWrite(DISP_CS, HIGH);
|
|
|
|
spi_device_release_bus(co5300_spi);
|
|
}
|
|
|
|
// Fill a rectangle with a solid colour
|
|
void co5300_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
|
|
if (!co5300_ready) return;
|
|
|
|
// Polling SPI doesn't need DMA memory — use PSRAM-capable heap
|
|
uint32_t total = w * h;
|
|
uint32_t buf_size = (total > CO5300_SEND_BUF_SIZE) ? CO5300_SEND_BUF_SIZE : total;
|
|
uint16_t *buf = (uint16_t *)heap_caps_malloc(buf_size * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if (!buf) {
|
|
// Fallback to internal RAM
|
|
buf = (uint16_t *)malloc(buf_size * 2);
|
|
if (!buf) return;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < buf_size; i++) buf[i] = color;
|
|
|
|
spi_device_acquire_bus(co5300_spi, portMAX_DELAY);
|
|
co5300_set_window(x, y, x + w - 1, y + h - 1);
|
|
|
|
uint32_t remaining = total;
|
|
bool first = true;
|
|
digitalWrite(DISP_CS, LOW);
|
|
while (remaining > 0) {
|
|
uint32_t chunk = (remaining > buf_size) ? buf_size : remaining;
|
|
spi_transaction_ext_t t = {};
|
|
if (first) {
|
|
t.base.flags = SPI_TRANS_MODE_QIO;
|
|
t.base.cmd = 0x32;
|
|
t.base.addr = 0x002C00;
|
|
first = false;
|
|
} else {
|
|
t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
|
|
SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
|
|
t.command_bits = 0;
|
|
t.address_bits = 0;
|
|
t.dummy_bits = 0;
|
|
}
|
|
t.base.tx_buffer = buf;
|
|
t.base.length = chunk * 16;
|
|
spi_device_polling_transmit(co5300_spi, (spi_transaction_t *)&t);
|
|
remaining -= chunk;
|
|
}
|
|
digitalWrite(DISP_CS, HIGH);
|
|
spi_device_release_bus(co5300_spi);
|
|
|
|
heap_caps_free(buf);
|
|
}
|
|
|
|
// Clear the entire display to black
|
|
void co5300_clear() {
|
|
co5300_fill_rect(0, 0, CO5300_WIDTH, CO5300_HEIGHT, 0x0000);
|
|
}
|
|
|
|
void co5300_set_brightness(uint8_t level) {
|
|
if (!co5300_ready) return;
|
|
co5300_brightness = level;
|
|
co5300_write_cmd(CO5300_CMD_BRIGHTNESS, &level, 1);
|
|
}
|
|
|
|
void co5300_sleep() {
|
|
if (!co5300_ready) return;
|
|
co5300_write_cmd(CO5300_CMD_SLPIN, NULL, 0);
|
|
}
|
|
|
|
void co5300_wakeup() {
|
|
if (!co5300_ready) return;
|
|
co5300_write_cmd(CO5300_CMD_SLPOUT, NULL, 0);
|
|
delay(120);
|
|
}
|
|
|
|
bool co5300_init() {
|
|
pinMode(DISP_CS, OUTPUT);
|
|
digitalWrite(DISP_CS, HIGH);
|
|
|
|
// Hardware reset on GPIO 37 (direct pin, not XL9555 expander)
|
|
pinMode(DISP_RST, OUTPUT);
|
|
digitalWrite(DISP_RST, HIGH);
|
|
delay(200);
|
|
digitalWrite(DISP_RST, LOW);
|
|
delay(300);
|
|
digitalWrite(DISP_RST, HIGH);
|
|
delay(200);
|
|
|
|
// Configure QSPI bus
|
|
spi_bus_config_t bus_cfg = {};
|
|
bus_cfg.data0_io_num = DISP_D0;
|
|
bus_cfg.data1_io_num = DISP_D1;
|
|
bus_cfg.sclk_io_num = DISP_SCK;
|
|
bus_cfg.data2_io_num = DISP_D2;
|
|
bus_cfg.data3_io_num = DISP_D3;
|
|
bus_cfg.data4_io_num = -1;
|
|
bus_cfg.data5_io_num = -1;
|
|
bus_cfg.data6_io_num = -1;
|
|
bus_cfg.data7_io_num = -1;
|
|
bus_cfg.max_transfer_sz = 0x40000 + 8;
|
|
bus_cfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS;
|
|
|
|
spi_device_interface_config_t dev_cfg = {};
|
|
dev_cfg.command_bits = 8;
|
|
dev_cfg.address_bits = 24;
|
|
dev_cfg.mode = SPI_MODE0;
|
|
dev_cfg.clock_speed_hz = CO5300_SPI_FREQ_MHZ * 1000 * 1000;
|
|
dev_cfg.spics_io_num = -1; // CS managed manually
|
|
dev_cfg.flags = SPI_DEVICE_HALFDUPLEX;
|
|
dev_cfg.queue_size = 17;
|
|
|
|
if (spi_bus_initialize(CO5300_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO) != ESP_OK) {
|
|
return false;
|
|
}
|
|
if (spi_bus_add_device(CO5300_SPI_HOST, &dev_cfg, &co5300_spi) != ESP_OK) {
|
|
return false;
|
|
}
|
|
|
|
// Send init sequence (twice, per LilyGoLib pattern)
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
for (uint32_t i = 0; i < CO5300_INIT_CMD_COUNT; i++) {
|
|
co5300_write_cmd(co5300_init_cmds[i].cmd,
|
|
(uint8_t *)co5300_init_cmds[i].data,
|
|
co5300_init_cmds[i].len & 0x1F);
|
|
if (co5300_init_cmds[i].len & 0x80) {
|
|
delay(120);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set rotation to portrait (default for watch)
|
|
uint8_t madctl = 0x00; // RGB order, no mirror/swap
|
|
co5300_write_cmd(CO5300_CMD_MADCTL, &madctl, 1);
|
|
|
|
co5300_ready = true;
|
|
|
|
// Clear screen to black
|
|
co5300_clear();
|
|
|
|
// Set initial brightness
|
|
co5300_set_brightness(128);
|
|
|
|
return true;
|
|
}
|
|
|
|
void co5300_end() {
|
|
if (co5300_spi) {
|
|
spi_bus_remove_device(co5300_spi);
|
|
spi_bus_free(CO5300_SPI_HOST);
|
|
co5300_spi = NULL;
|
|
}
|
|
co5300_ready = false;
|
|
}
|
|
|
|
// ---- Simple text rendering using Adafruit GFX fonts ----
|
|
// Only available when HAS_DISPLAY is true (Display.h includes Adafruit_GFX.h)
|
|
|
|
#if HAS_DISPLAY
|
|
|
|
// Draw a single character using Adafruit GFX font at a given position
|
|
// Returns the advance width in pixels
|
|
static uint16_t co5300_draw_char(uint16_t *fb, uint16_t fb_w, uint16_t fb_h,
|
|
int16_t cx, int16_t cy, char c,
|
|
uint16_t fg, const GFXfont *font) {
|
|
if (c < font->first || c > font->last) return 0;
|
|
GFXglyph *glyph = &font->glyph[c - font->first];
|
|
uint8_t *bitmap = font->bitmap;
|
|
uint16_t bo = glyph->bitmapOffset;
|
|
uint8_t gw = glyph->width, gh = glyph->height;
|
|
int8_t xo = glyph->xOffset, yo = glyph->yOffset;
|
|
uint8_t bits = 0, bit = 0;
|
|
|
|
for (int16_t yy = 0; yy < gh; yy++) {
|
|
for (int16_t xx = 0; xx < gw; xx++) {
|
|
if (!(bit++ & 7)) bits = bitmap[bo++];
|
|
if (bits & 0x80) {
|
|
int16_t px = cx + xo + xx;
|
|
int16_t py = cy + yo + yy;
|
|
if (px >= 0 && px < fb_w && py >= 0 && py < fb_h) {
|
|
fb[py * fb_w + px] = fg;
|
|
}
|
|
}
|
|
bits <<= 1;
|
|
}
|
|
}
|
|
return glyph->xAdvance;
|
|
}
|
|
|
|
// Draw a string using Adafruit GFX font, returns total width
|
|
uint16_t co5300_draw_string(uint16_t *fb, uint16_t fb_w, uint16_t fb_h,
|
|
int16_t x, int16_t y, const char *str,
|
|
uint16_t fg, const GFXfont *font) {
|
|
int16_t cx = x;
|
|
while (*str) {
|
|
cx += co5300_draw_char(fb, fb_w, fb_h, cx, y, *str, fg, font);
|
|
str++;
|
|
}
|
|
return cx - x;
|
|
}
|
|
|
|
// RGB565 colour helpers
|
|
#define CO5300_BLACK 0x0000
|
|
#define CO5300_WHITE 0xFFFF
|
|
#define CO5300_RED 0xF800
|
|
#define CO5300_GREEN 0x07E0
|
|
#define CO5300_BLUE 0x001F
|
|
#define CO5300_CYAN 0x07FF
|
|
#define CO5300_YELLOW 0xFFE0
|
|
#define CO5300_GREY 0x7BEF
|
|
|
|
#endif // HAS_DISPLAY
|
|
|
|
#endif // BOARD_MODEL == BOARD_TWATCH_ULT
|
|
#endif // CO5300_H
|