markqvist___RNode_Firmware/CO5300.h
GlassOnTin 09d1f6409f Add R-Watch (T-Watch Ultra) board support with LoRa, GPS, PMU, RTC, and deep sleep
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.
2026-03-27 12:18:46 +00:00

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