diff --git a/.gitignore b/.gitignore index a8f5bff..fcfce2b 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,8 @@ dkms.conf build*/ doc/ Testing/ + +# Miscellaneous +*.log +*.tmp +.DS_Store diff --git a/doxygen/modules_diagram.dox.inc b/doxygen/modules_diagram.dox.inc index bcc3b3b..95ed334 100644 --- a/doxygen/modules_diagram.dox.inc +++ b/doxygen/modules_diagram.dox.inc @@ -13,7 +13,7 @@ digraph modules { Cond [label="Condition Variables", URL="\ref SystemSyncCond"]; WaitGroup [label="Wait Groups", URL="\ref SystemSyncWaitgroup"]; Atomic [label="Atomic Operations", URL="\ref SystemAtomic"]; - Data [label="Data", URL="\ref SystemData"]; + Data [label="Data", URL="\ref SystemData"]; DataRune [label="Runes", URL="\ref SystemDataRune"]; DataString [label="Strings", URL="\ref SystemDataString"]; DataScanner [label="Scanner", URL="\ref SystemDataScanner"]; @@ -53,8 +53,9 @@ digraph modules { FT6236 [label="FT6236", URL="\ref FT6236"]; STMPE610 [label="STMPE610", URL="\ref STMPE610"]; ILI9341 [label="ILI9341", URL="\ref ILI9341"]; + ST7701 [label="ST7701", URL="\ref ST7701"]; SDL [label="SDL", URL="\ref SDL"]; - Pixels [label="Pixels", URL="\ref Pixel"]; + Pixels [label="Pixels", URL="\ref Pixel"]; PixelDisplay [label="Display", URL="\ref PixelDisplay"]; PixelBitmap [label="Bitmap", URL="\ref PixelBitmap"]; Application [label="Application", URL="\ref Application"]; @@ -111,6 +112,7 @@ digraph modules { Sensor -> BME680; Display -> ILI9341; Display -> SDL; + Display -> ST7701; Pixels -> PixelDisplay; Pixels -> PixelBitmap; Application -> HID; diff --git a/include/picofuse/dev.h b/include/picofuse/dev.h index e3f579d..5bb9ace 100644 --- a/include/picofuse/dev.h +++ b/include/picofuse/dev.h @@ -35,4 +35,5 @@ #include "dev/ft6236.h" #include "dev/ili9341.h" #include "dev/sdl.h" +#include "dev/st7701.h" #include "dev/stmpe610.h" diff --git a/include/picofuse/dev/st7701.h b/include/picofuse/dev/st7701.h new file mode 100644 index 0000000..c31cfb4 --- /dev/null +++ b/include/picofuse/dev/st7701.h @@ -0,0 +1,74 @@ +/** + * @file st7701.h + * @brief ST7701 TFT LCD controller interface. + * @defgroup ST7701 ST7701 + * @ingroup Display + */ +#pragma once + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +/** + * @brief Optional ST7701 initialization options. + * @ingroup ST7701 + */ +typedef struct { + uint16_t rotation; +} dev_st7701_config_t; + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +/** @name Lifecycle + * @{ */ + +/** + * @brief Fill an ST7701 config struct with safe defaults. + * @ingroup ST7701 + * @param config Config structure to initialize. + */ +void dev_st7701_default_config(dev_st7701_config_t *config); + +/** + * @brief Initialize an ST7701-driven display over SPI. + * @ingroup ST7701 + * @param device SPI device handle. This panel has no D/CX GPIO - `device` + * must be a hw_spi_init() handle configured with `bits_per_word = 9`, + * command/data select packed as the 9th bit of every word (see + * hw_deviceio_xfr()). + * @param size Panel resolution. + * @param bl_pin Optional GPIO handle for the backlight. Pass `NULL` if + * the backlight isn't software-controlled. + * @param config Optional pointer to initialization options. Pass `NULL` + * to use default values. + * @return display handle, or `NULL` on failure + */ +pix_display_t *dev_st7701_init(hw_deviceio_t *device, pix_size_t size, + hw_gpio_t *bl_pin, + const dev_st7701_config_t *config); + +/** @} */ + +/////////////////////////////////////////////////////////////////////////////// +// METHODS + +/** @name Methods + * @{ */ + +/** + * @brief Set the backlight brightness. + * @ingroup ST7701 + * @param display Display handle from dev_st7701_init(). + * @param brightness Brightness from `0` (off) to `255` (full). + * @return `false` if `display` wasn't created by dev_st7701_init(), or has + * no backlight pin. + */ +bool dev_st7701_set_backlight(pix_display_t *display, uint8_t brightness); + +/** @} */ diff --git a/include/picofuse/hw/deviceio.h b/include/picofuse/hw/deviceio.h index 9a8c22b..89d2f5c 100644 --- a/include/picofuse/hw/deviceio.h +++ b/include/picofuse/hw/deviceio.h @@ -103,18 +103,30 @@ hw_deviceio_bus_t hw_deviceio_bus(const hw_deviceio_t *device); * @brief Perform a raw, bidirectional transfer. * @ingroup DeviceIO * @param device Device handle. - * @param data Buffer used for transmitted and received bytes. - * @param tx Number of bytes to transmit from `data`. - * @param rx Number of bytes to receive into `data + tx`. + * @param data Buffer used for transmitted and received words - `uint8_t*` + * unless noted otherwise below. + * @param tx Number of words to transmit from `data`. + * @param rx Number of words to receive into `data + tx * word_size`. * @param timeout_ms Timeout in milliseconds for the operation. Set to `0` * to use the backend's default transfer path. - * @return Number of bytes transferred, or `0` on failure. + * @return Number of words transferred, or `0` on failure. * * Supports write-only (`tx > 0, rx == 0`), read-only (`tx == 0, rx > 0`), * and write-then-read (`tx > 0, rx > 0`) transfers. The exact semantics of * a write-then-read - a repeated start on I2C, a continuous chip-select * assertion on SPI - are defined by whichever bus `device` is actually * bound to. + * + * A "word" here is 1 byte for every I2C device (no framing concept + * applies) and for an SPI device left at its default `hw_spi_config_t:: + * bits_per_word` (8) - `data` is `uint8_t*`, and `tx`/`rx` count bytes + * exactly as before. For an SPI device configured with `bits_per_word` + * above 8 (9-16, e.g. a display panel that packs a command/data select + * bit as the 9th bit of every word), a word is 2 bytes instead - `data` + * must then be `uint16_t*`, and `tx`/`rx` count 16-bit words, not bytes + * (so the underlying buffer needs `(tx + rx) * 2` bytes of storage, not + * `tx + rx`). Check whichever `hw_*_init()` built `device` for what its + * own word size is. */ size_t hw_deviceio_xfr(hw_deviceio_t *device, void *data, size_t tx, size_t rx, uint32_t timeout_ms); diff --git a/include/picofuse/hw/spi.h b/include/picofuse/hw/spi.h index c4ed7d7..d6c4704 100644 --- a/include/picofuse/hw/spi.h +++ b/include/picofuse/hw/spi.h @@ -56,7 +56,9 @@ typedef enum { typedef struct { bool cs_active_low; ///< True when chip-select is active low. hw_spi_mode_t mode; ///< SPI mode selection. - uint8_t bits_per_word; ///< SPI frame size in bits. + uint8_t bits_per_word; ///< SPI frame size in bits (4-16). Above 8 changes + ///< hw_deviceio_xfr()'s own word size for this + ///< device - see its doc. } hw_spi_config_t; /////////////////////////////////////////////////////////////////////////////// @@ -88,7 +90,8 @@ hw_deviceio_t *hw_spi_init_default(uint32_t baud_rate, * @param index SPI adapter index to use. * @param sck_pin GPIO handle for SCK. * @param tx_pin GPIO handle for MOSI. - * @param rx_pin GPIO handle for MISO. + * @param rx_pin Optional GPIO handle for MISO. Pass `NULL` for a + * write-only device with no MISO line (e.g. a 3-wire display panel). * @param cs_pin Optional GPIO handle for CS. Pass NULL to leave CS unmanaged. * @param baud_rate Desired SPI clock rate in Hz. * @param config Optional pointer to extended SPI configuration. Pass `NULL` diff --git a/src/picofuse/dev/CMakeLists.txt b/src/picofuse/dev/CMakeLists.txt index 5bc1759..bfc57c5 100644 --- a/src/picofuse/dev/CMakeLists.txt +++ b/src/picofuse/dev/CMakeLists.txt @@ -1,8 +1,14 @@ -# SDL support -find_package(PkgConfig QUIET) -if(PkgConfig_FOUND) - pkg_check_modules(SDL2 QUIET IMPORTED_TARGET sdl2) +# SDL support - host-only. pkg-config always searches the host's own +# library paths regardless of cross-compilation, so on a Pico build this +# would otherwise "find" and try to link against the host's native SDL2 +# (wrong ABI/arch entirely) - and SDL2 makes no sense on Pico anyway, with +# no windowing system to speak of. +if(NOT DEFINED PICO_BOARD) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(SDL2 QUIET IMPORTED_TARGET sdl2) + endif() endif() if(SDL2_FOUND) message(STATUS "Found SDL2: enabling SDL display backend") @@ -22,6 +28,7 @@ picofuse_library( stmpe610/stmpe610.c stmpe610/hid.c ${_sdl_src} + st7701/st7701.c ) target_include_directories(picofuse-dev PRIVATE ${CMAKE_SOURCE_DIR}/include) diff --git a/src/picofuse/dev/st7701/ST7701.pdf b/src/picofuse/dev/st7701/ST7701.pdf new file mode 100644 index 0000000..7886b32 Binary files /dev/null and b/src/picofuse/dev/st7701/ST7701.pdf differ diff --git a/src/picofuse/dev/st7701/st7701.c b/src/picofuse/dev/st7701/st7701.c new file mode 100644 index 0000000..4698c65 --- /dev/null +++ b/src/picofuse/dev/st7701/st7701.c @@ -0,0 +1,427 @@ +#include "../../pix/private.h" +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +// Command1 table - always available regardless of which Command2 bank (if +// any) is currently selected. +#define ST7701_SWRESET 0x01u +#define ST7701_SLPOUT 0x11u +#define ST7701_INVON 0x21u +#define ST7701_DISPON 0x29u +#define ST7701_MADCTL 0x36u +#define ST7701_COLMOD 0x3Au +#define ST7701_CND2BKXSEL \ + 0xFFu // Selects which Command2 bank (if any) is active + +// Command2 BK0 register addresses (only meaningful once BK0 is selected). +#define ST7701_BK0_PVGAMCTRL 0xB0u +#define ST7701_BK0_NVGAMCTRL 0xB1u +#define ST7701_BK0_LNESET 0xC0u +#define ST7701_BK0_PORCTRL 0xC1u +#define ST7701_BK0_INVSET 0xC2u +#define ST7701_BK0_RGBCTRL 0xC3u +#define ST7701_BK0_SDIR 0xC7u +#define ST7701_BK0_COLCTRL 0xCDu + +// Command2 BK1 register addresses (only meaningful once BK1 is selected). +#define ST7701_BK1_VHRS 0xB0u +#define ST7701_BK1_VCOMS 0xB1u +#define ST7701_BK1_VGHSS 0xB2u +#define ST7701_BK1_TESTCMD 0xB3u +#define ST7701_BK1_VGLS 0xB5u +#define ST7701_BK1_PWCTRL1 0xB7u +#define ST7701_BK1_PWCTRL2 0xB8u +#define ST7701_BK1_PDR1 0xC1u +#define ST7701_BK1_PDR2 0xC2u + +// CND2BKxSEL argument bytes - selects/deselects which Command2 bank is +// active. Fixed by the controller, not display-specific. +static const uint8_t _st7701_bkx_disable[] = {0x77, 0x01, 0x00, 0x00, 0x00}; +static const uint8_t _st7701_bk0_select[] = {0x77, 0x01, 0x00, 0x00, 0x10}; +static const uint8_t _st7701_bk1_select[] = {0x77, 0x01, 0x00, 0x00, 0x11}; +static const uint8_t _st7701_bk3_select[] = {0x77, 0x01, 0x00, 0x00, 0x13}; + +// Longest argument list any single _dev_st7701_command() call below needs. +#define ST7701_MAX_COMMAND_ARGS 16u + +// Ma +#define ST7701_LNESET_MAX_HEIGHT 1024u // Line[6:0] is 7 bits: (127+1)*8 + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// ST7701 context +typedef struct { + hw_deviceio_t *device; + hw_pwm_t *bl_pwm; // Optional - NULL if bl_pin wasn't given, or its PWM failed + // to init +} _dev_st7701_ctx_t; + +_Static_assert(sizeof(_dev_st7701_ctx_t) <= PIX_DISPLAY_CONTEXT_SIZE, + "_dev_st7701_ctx_t exceeds PIX_DISPLAY_CONTEXT_SIZE"); + +/////////////////////////////////////////////////////////////////////////////// +// OPS DECLARATIONS + +// Not yet implemented - see each body's own comment. +static pix_bitmap_t *_dev_st7701_lock(pix_display_t *display); +static void _dev_st7701_unlock(pix_display_t *display); +static bool _dev_st7701_poll(pix_display_t *display); +static void _dev_st7701_deinit(pix_display_t *display); + +static const pix_display_ops_t _dev_st7701_ops = { + .lock = _dev_st7701_lock, + .unlock = _dev_st7701_unlock, + .poll = _dev_st7701_poll, + .deinit = _dev_st7701_deinit, +}; + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +// This panel has no D/CX GPIO - command/data select is packed as the 9th +// bit of every SPI word instead (see dev_st7701_init()'s own doc), which +// is why `device` must be configured with bits_per_word=9 and this passes +// a uint16_t buffer to hw_deviceio_xfr() - see its own doc on word size. +// Sent as a single transfer (one CS assertion) covering the command byte +// and all of its arguments together. +static bool _dev_st7701_command(pix_display_t *display, uint8_t command, + const uint8_t *args, size_t len) { + if (len > ST7701_MAX_COMMAND_ARGS) { + return false; + } + _dev_st7701_ctx_t *ctx = _pix_display_context(display); + + uint16_t words[1 + ST7701_MAX_COMMAND_ARGS]; + words[0] = command; // D/CX = 0 (command) + for (size_t i = 0; i < len; i++) { + words[1 + i] = 0x0100u | args[i]; // D/CX = 1 (data) + } + + size_t total = 1 + len; + return hw_deviceio_xfr(ctx->device, words, total, 0, 0) == total; +} + +// Select which Command2 bank (if any) is active - subsequent +// _dev_st7701_command() register addresses are only meaningful relative +// to whichever bank (or none) is currently selected. See the enum bk0/ +// bk1/bk3 register maps in ST7701::command in Pimoroni's own driver - +// picofuse has no need to name every register, only the ones actually +// used below. +static bool _dev_st7701_bk0_enable(pix_display_t *display) { + return _dev_st7701_command(display, ST7701_CND2BKXSEL, _st7701_bk0_select, + sizeof(_st7701_bk0_select)); +} +static bool _dev_st7701_bk1_enable(pix_display_t *display) { + return _dev_st7701_command(display, ST7701_CND2BKXSEL, _st7701_bk1_select, + sizeof(_st7701_bk1_select)); +} +static bool _dev_st7701_bk3_enable(pix_display_t *display) { + return _dev_st7701_command(display, ST7701_CND2BKXSEL, _st7701_bk3_select, + sizeof(_st7701_bk3_select)); +} +static bool _dev_st7701_bkx_disable(pix_display_t *display) { + return _dev_st7701_command(display, ST7701_CND2BKXSEL, _st7701_bkx_disable, + sizeof(_st7701_bkx_disable)); +} + +// Quadratic ease, matching the perceptual-brightness approach already +// used for NeoPixel brightness (see hw/pico/led_neopixel.c's own +// _hw_led_neopixel_scale()) - smooth and monotonic across the full 0-255 +// range, unlike Pimoroni's own set_backlight() curve, which only reaches +// ~15% duty by brightness=254 and then jumps straight to 100% at 255. +static float _dev_st7701_backlight_duty_percent(uint8_t brightness) { + return ((float)brightness * (float)brightness) / (255.0f * 255.0f) * 100.0f; +} + +// Panel bring-up sequence, adapted from Pimoroni's own ST7701::common_init() +// - mostly gamma/voltage/"Forbidden Knowledge" tuning specific to Presto's +// own TL040WVS03CT15-H1263A glass, not documented in the ST7701 datasheet +// itself (LNESET is the one exception - see its own comment below). +static bool _dev_st7701_common_init(pix_display_t *display, pix_size_t size, + const dev_st7701_config_t *config) { + // Accumulated with short-circuiting `&&`, so a failed transfer both + // gets reported and stops any further commands from being issued - + // not just a discarded-result, always-true rubber stamp. + bool ok = _dev_st7701_command(display, ST7701_SWRESET, NULL, 0); + sys_sleep_ms(150); + + // NL = (Line[6:0]+1)*8 (ST7701 datasheet 12.3.2.6) - dev_st7701_init() + // already validated size.h is a positive multiple of 8 within range. + uint8_t line = (uint8_t)(size.h / 8 - 1); + ok = ok && _dev_st7701_bk0_enable(display); + ok = ok && _dev_st7701_command(display, ST7701_BK0_LNESET, + (const uint8_t[]){line, 0x00}, 2); + ok = ok && _dev_st7701_command(display, ST7701_BK0_PORCTRL, + (const uint8_t[]){0x0d, 0x02}, 2); + ok = ok && _dev_st7701_command(display, ST7701_BK0_INVSET, + (const uint8_t[]){0x31, 0x01}, 2); + ok = ok && _dev_st7701_command(display, ST7701_BK0_COLCTRL, + (const uint8_t[]){0x08}, 1); + ok = ok && + _dev_st7701_command(display, ST7701_BK0_PVGAMCTRL, + (const uint8_t[]){0x00, 0x11, 0x18, 0x0e, 0x11, 0x06, + 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, + 0x0f, 0xaa, 0x31, 0x18}, + 16); + ok = ok && + _dev_st7701_command(display, ST7701_BK0_NVGAMCTRL, + (const uint8_t[]){0x00, 0x11, 0x19, 0x0e, 0x12, 0x07, + 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, + 0x11, 0xa9, 0x32, 0x18}, + 16); + ok = ok && _dev_st7701_command(display, ST7701_BK0_RGBCTRL, + (const uint8_t[]){0x80, 0x2e, 0x0e}, 3); + + ok = ok && _dev_st7701_bk1_enable(display); + ok = ok && _dev_st7701_command(display, ST7701_BK1_VHRS, + (const uint8_t[]){0x60}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_VCOMS, + (const uint8_t[]){0x32}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_VGHSS, + (const uint8_t[]){0x07}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_TESTCMD, + (const uint8_t[]){0x80}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_VGLS, + (const uint8_t[]){0x49}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_PWCTRL1, + (const uint8_t[]){0x85}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_PWCTRL2, + (const uint8_t[]){0x21}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_PDR1, + (const uint8_t[]){0x78}, 1); + ok = ok && _dev_st7701_command(display, ST7701_BK1_PDR2, + (const uint8_t[]){0x78}, 1); + + // Undocumented registers required by this specific panel - still within + // BK1 (no bank switch since the block above). Present verbatim from the + // reference driver; the display doesn't work without them. + ok = ok && _dev_st7701_command(display, 0xE0, + (const uint8_t[]){0x00, 0x1b, 0x02}, 3); + ok = ok && + _dev_st7701_command(display, 0xE1, + (const uint8_t[]){0x08, 0xa0, 0x00, 0x00, 0x07, 0xa0, + 0x00, 0x00, 0x00, 0x44, 0x44}, + 11); + ok = ok && _dev_st7701_command(display, 0xE2, + (const uint8_t[]){0x11, 0x11, 0x44, 0x44, 0xed, + 0xa0, 0x00, 0x00, 0xec, 0xa0, + 0x00, 0x00}, + 12); + ok = ok && _dev_st7701_command(display, 0xE3, + (const uint8_t[]){0x00, 0x00, 0x11, 0x11}, 4); + ok = ok && + _dev_st7701_command(display, 0xE4, (const uint8_t[]){0x44, 0x44}, 2); + ok = ok && + _dev_st7701_command(display, 0xE5, + (const uint8_t[]){0x0a, 0xe9, 0xd8, 0xa0, 0x0c, 0xeb, + 0xd8, 0xa0, 0x0e, 0xed, 0xd8, 0xa0, + 0x10, 0xef, 0xd8, 0xa0}, + 16); + ok = ok && _dev_st7701_command(display, 0xE6, + (const uint8_t[]){0x00, 0x00, 0x11, 0x11}, 4); + ok = ok && + _dev_st7701_command(display, 0xE7, (const uint8_t[]){0x44, 0x44}, 2); + ok = ok && + _dev_st7701_command(display, 0xE8, + (const uint8_t[]){0x09, 0xe8, 0xd8, 0xa0, 0x0b, 0xea, + 0xd8, 0xa0, 0x0d, 0xec, 0xd8, 0xa0, + 0x0f, 0xee, 0xd8, 0xa0}, + 16); + ok = + ok && _dev_st7701_command( + display, 0xEB, + (const uint8_t[]){0x02, 0x00, 0xe4, 0xe4, 0x88, 0x00, 0x40}, 7); + ok = ok && + _dev_st7701_command(display, 0xEC, (const uint8_t[]){0x3c, 0x00}, 2); + ok = ok && + _dev_st7701_command(display, 0xED, + (const uint8_t[]){0xab, 0x89, 0x76, 0x54, 0x02, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, + 0x45, 0x67, 0x98, 0xba}, + 16); + ok = ok && + _dev_st7701_command(display, ST7701_MADCTL, (const uint8_t[]){0x00}, 1); + + ok = ok && _dev_st7701_bk3_enable(display); + ok = ok && _dev_st7701_command(display, 0xE5, (const uint8_t[]){0xe4}, 1); + + ok = ok && _dev_st7701_bkx_disable(display); + ok = ok && + _dev_st7701_command(display, ST7701_COLMOD, (const uint8_t[]){0x66}, 1); + + // Rotation - only 0/180 supported, matching the reference driver. + // Mirrors the reference's own set_rotation(), just inlined rather than + // exposed as its own public entry point yet. + uint8_t madctl = 0x00; // Mirror Y off, no RGB swap + uint8_t sdir = 0x00; // Mirror X off + if (config->rotation == 180) { + madctl = 0x10; + sdir = 0x04; + } + ok = ok && _dev_st7701_command(display, ST7701_MADCTL, &madctl, 1); + ok = ok && _dev_st7701_bk0_enable(display); + ok = ok && _dev_st7701_command(display, ST7701_BK0_SDIR, &sdir, 1); + ok = ok && _dev_st7701_bkx_disable(display); + + ok = ok && _dev_st7701_command(display, ST7701_INVON, NULL, 0); + sys_sleep_ms(1); + ok = ok && _dev_st7701_command(display, ST7701_SLPOUT, NULL, 0); + sys_sleep_ms(120); + ok = ok && _dev_st7701_command(display, ST7701_DISPON, NULL, 0); + sys_sleep_ms(50); + return ok; +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +/** @brief Fill an ST7701 config struct with safe defaults. + * @ingroup ST7701 + */ +void dev_st7701_default_config(dev_st7701_config_t *config) { + if (config == NULL) { + return; + } + config->rotation = 0; +} + +/** @brief Initialize an ST7701-driven display over SPI. + * @ingroup ST7701 + * @param device SPI device handle. This panel has no D/CX GPIO - `device` + * must be a hw_spi_init() handle configured with `bits_per_word = 9`, + * command/data select packed as the 9th bit of every word (see + * hw_deviceio_xfr()). + * @param size Panel resolution. `size.w` just needs to be nonzero - the + * current SPI-only bring-up sequence doesn't otherwise depend on it (see + * this file's own CONSTANTS comment). `size.h` must be a positive + * multiple of 8, up to 1024, and is used to compute the LNESET register + * directly. + * @param bl_pin Optional GPIO handle for the backlight. Pass `NULL` if + * the backlight isn't software-controlled. + * @param config Optional pointer to initialization options. Pass `NULL` + * to use default values. + * @return display handle, or `NULL` on failure + */ +pix_display_t *dev_st7701_init(hw_deviceio_t *device, pix_size_t size, + hw_gpio_t *bl_pin, + const dev_st7701_config_t *config) { + if (device == NULL || size.w == 0 || size.h == 0 || size.h % 8 != 0 || + size.h > ST7701_LNESET_MAX_HEIGHT) { + sys_debugf("st7701", "dev_st7701_init: invalid arguments"); + return NULL; + } + + dev_st7701_config_t settings; + if (config == NULL) { + dev_st7701_default_config(&settings); + } else { + settings = *config; + } + + if (settings.rotation != 0 && settings.rotation != 180) { + sys_debugf("st7701", "dev_st7701_init: unsupported rotation"); + return NULL; + } + + // Backlight is optional - wrap it as PWM + hw_pwm_t *bl_pwm = NULL; + if (bl_pin != NULL) { + hw_gpio_set_mode(bl_pin, hw_gpio_pwm); + // ~32.25kHz, matching Pimoroni's own reference (TOP=6200 against + // their 200MHz sys clock) rather than an arbitrary 1kHz - the AP3031 + // backlight driver's dimming behaviour at low duty may be frequency + // sensitive. + hw_pwm_config_t bl_config = { + .period_ns = 31000u, + .duty_percent = 0.0f, + .enabled = true, + }; + bl_pwm = hw_pwm_init(bl_pin, NULL, NULL, &bl_config); + if (bl_pwm == NULL) { + sys_debugf("st7701", "dev_st7701_init: failed to init backlight PWM"); + return NULL; + } + } + + // Allocate memory for the display + pix_display_t *display = + _pix_display_alloc(&_dev_st7701_ops, size, PIX_FMT_RGB565); + if (display == NULL) { + if (bl_pwm != NULL) { + hw_pwm_deinit(bl_pwm); + } + return NULL; + } + + // Create the driver context + _dev_st7701_ctx_t *ctx = _pix_display_context(display); + ctx->device = device; + ctx->bl_pwm = bl_pwm; + + // Initalize the hardware + if (!_dev_st7701_common_init(display, size, &settings)) { + sys_debugf("st7701", "dev_st7701_init: panel bring-up failed"); + if (bl_pwm != NULL) { + hw_pwm_deinit(bl_pwm); + } + _pix_display_free(display); + return NULL; + } + + // Switch on the screen (full brightness) + if (bl_pwm != NULL) { + sys_sleep_ms(50); // Let the panel settle before lighting it up + hw_pwm_set_duty_percent(bl_pwm, _dev_st7701_backlight_duty_percent(255)); + } + + // TODO: pixel data path (parallel RGB666 bus, PIO-driven scanout) - + // this panel has no SPI framebuffer path. Not yet implemented. + return display; +} + +/////////////////////////////////////////////////////////////////////////////// +// METHODS + +/** @brief Set the backlight brightness. + * @ingroup ST7701 + */ +bool dev_st7701_set_backlight(pix_display_t *display, uint8_t brightness) { + if (display == NULL || display->ops != &_dev_st7701_ops) { + return false; + } + _dev_st7701_ctx_t *ctx = _pix_display_context(display); + if (ctx->bl_pwm == NULL) { + return false; + } + return hw_pwm_set_duty_percent( + ctx->bl_pwm, _dev_st7701_backlight_duty_percent(brightness)); +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS - OPS + +/** Not yet implemented - see dev_st7701_init()'s own TODO. */ +static pix_bitmap_t *_dev_st7701_lock(pix_display_t *display) { + (void)display; + return NULL; +} + +/** Not yet implemented - see dev_st7701_init()'s own TODO. */ +static void _dev_st7701_unlock(pix_display_t *display) { (void)display; } + +/** Not yet implemented - see dev_st7701_init()'s own TODO. */ +static bool _dev_st7701_poll(pix_display_t *display) { + (void)display; + return false; +} + +static void _dev_st7701_deinit(pix_display_t *display) { + _dev_st7701_ctx_t *ctx = _pix_display_context(display); + if (ctx->bl_pwm != NULL) { + hw_pwm_deinit(ctx->bl_pwm); + } +} diff --git a/src/picofuse/hw/linux/spi.c b/src/picofuse/hw/linux/spi.c index ac8cf6a..21c550a 100644 --- a/src/picofuse/hw/linux/spi.c +++ b/src/picofuse/hw/linux/spi.c @@ -227,8 +227,12 @@ hw_deviceio_t *hw_spi_init_device(const char *device, uint32_t baud_rate, : (hw_spi_config_t){.cs_active_low = true, .mode = hw_spi_mode_0, .bits_per_word = 8}; + // This backend's own _hw_spi_ops_xfr() only handles 8-bit words (byte + // buffers, tx/rx counted in bytes) - see hw_deviceio_xfr()'s own doc on + // word size. Reject anything wider up front rather than silently + // under-transferring/misplacing data as if it worked. if (device == NULL || device[0] == '\0' || baud_rate == 0 || - settings.bits_per_word == 0) { + settings.bits_per_word == 0 || settings.bits_per_word > 8) { return NULL; } diff --git a/src/picofuse/hw/pico/spi.c b/src/picofuse/hw/pico/spi.c index cf37477..a6b4f5a 100644 --- a/src/picofuse/hw/pico/spi.c +++ b/src/picofuse/hw/pico/spi.c @@ -19,6 +19,7 @@ typedef struct hw_spi_ctx_t { hw_gpio_t *rx_pin; hw_gpio_t *cs_pin; // NULL if this device leaves CS unmanaged mutex_t lock; + uint8_t bits_per_word; // See hw_spi_config_t::bits_per_word bool cs_active_low; bool owns_pins; } hw_spi_ctx_t; @@ -67,32 +68,57 @@ static inline void _hw_spi_set_cs(const hw_spi_ctx_t *ctx, bool active) { /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS - OPS +// See hw_deviceio_xfr()'s own doc on word size - `data` is `uint8_t*` and +// tx/rx count bytes for the common bits_per_word <= 8 case; `uint16_t*` +// and tx/rx count 16-bit words once this device was configured with +// bits_per_word above 8, since the SDK's 8-bit spi_*_blocking() calls +// can't frame anything wider. static size_t _hw_spi_ops_xfr(hw_deviceio_t *device, void *data, size_t tx, size_t rx, uint32_t timeout_ms) { (void)timeout_ms; // the SDK's spi_*_blocking() calls have no timeout path hw_spi_ctx_t *ctx = _hw_deviceio_context(device); - if ((tx == 0 && rx == 0) || ((tx > 0 || rx > 0) && data == NULL)) { + if ((tx == 0 && rx == 0) || ((tx > 0 || rx > 0) && data == NULL) || + (rx > 0 && ctx->rx_pin == NULL)) { return 0; } - uint8_t *bytes = data; size_t transferred = 0; mutex_enter_blocking(&ctx->lock); _hw_spi_set_cs(ctx, true); - if (tx > 0 && rx > 0) { - if (spi_write_blocking(ctx->instance, bytes, tx) == (int)tx && - spi_read_blocking(ctx->instance, 0x00, bytes + tx, rx) == (int)rx) { - transferred = tx + rx; - } - } else if (tx > 0) { - if (spi_write_blocking(ctx->instance, bytes, tx) == (int)tx) { - transferred = tx; + if (ctx->bits_per_word > 8) { + uint16_t *words = data; + if (tx > 0 && rx > 0) { + if (spi_write16_blocking(ctx->instance, words, tx) == (int)tx && + spi_read16_blocking(ctx->instance, 0x0000, words + tx, rx) == + (int)rx) { + transferred = tx + rx; + } + } else if (tx > 0) { + if (spi_write16_blocking(ctx->instance, words, tx) == (int)tx) { + transferred = tx; + } + } else { + if (spi_read16_blocking(ctx->instance, 0x0000, words, rx) == (int)rx) { + transferred = rx; + } } } else { - if (spi_read_blocking(ctx->instance, 0x00, bytes, rx) == (int)rx) { - transferred = rx; + uint8_t *bytes = data; + if (tx > 0 && rx > 0) { + if (spi_write_blocking(ctx->instance, bytes, tx) == (int)tx && + spi_read_blocking(ctx->instance, 0x00, bytes + tx, rx) == (int)rx) { + transferred = tx + rx; + } + } else if (tx > 0) { + if (spi_write_blocking(ctx->instance, bytes, tx) == (int)tx) { + transferred = tx; + } + } else { + if (spi_read_blocking(ctx->instance, 0x00, bytes, rx) == (int)rx) { + transferred = rx; + } } } @@ -309,7 +335,7 @@ hw_deviceio_t *hw_spi_init(uint8_t index, hw_gpio_t *sck_pin, hw_gpio_t *tx_pin, uint32_t baud_rate, const hw_spi_config_t *config) { sys_debugf("hw", "spi_init: index=%u baud=%u", index, baud_rate); if (index >= hw_spi_count() || sck_pin == NULL || tx_pin == NULL || - rx_pin == NULL || baud_rate == 0) { + baud_rate == 0) { return NULL; } @@ -318,7 +344,7 @@ hw_deviceio_t *hw_spi_init(uint8_t index, hw_gpio_t *sck_pin, hw_gpio_t *tx_pin, : (hw_spi_config_t){.cs_active_low = true, .mode = hw_spi_mode_0, .bits_per_word = 8}; - if (settings.bits_per_word == 0) { + if (settings.bits_per_word < 4 || settings.bits_per_word > 16) { return NULL; } @@ -331,7 +357,9 @@ hw_deviceio_t *hw_spi_init(uint8_t index, hw_gpio_t *sck_pin, hw_gpio_t *tx_pin, spi_inst_t *instance = spi_get_instance(index); hw_gpio_set_mode(sck_pin, hw_gpio_spi); hw_gpio_set_mode(tx_pin, hw_gpio_spi); - hw_gpio_set_mode(rx_pin, hw_gpio_spi); + if (rx_pin != NULL) { + hw_gpio_set_mode(rx_pin, hw_gpio_spi); + } if (cs_pin != NULL) { hw_gpio_set_mode(cs_pin, hw_gpio_output); } @@ -354,6 +382,7 @@ hw_deviceio_t *hw_spi_init(uint8_t index, hw_gpio_t *sck_pin, hw_gpio_t *tx_pin, ctx->cs_pin = cs_pin; mutex_init(&ctx->lock); ctx->cs_active_low = settings.cs_active_low; + ctx->bits_per_word = settings.bits_per_word; ctx->owns_pins = false; _hw_spi_set_cs(ctx, false); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ec20c1b..2550cb3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -875,7 +875,16 @@ if(DEFINED PICO_BOARD) ) endif() -# hid_init/_deinit singleton lifecycle +# dev_st7701_* against Presto's own display - scaffold only for now, see +# dev_st7701/main.c. +if(DEFINED PICO_BOARD AND PICO_BOARD STREQUAL "presto") + picofuse_test(dev_st7701 + dev_st7701/main.c + LIBRARIES picofuse-hw picofuse-dev + ) +endif() + +# hid_init/_deinit singleton lifecycle picofuse_test(hid_001 hid_001/main.c LIBRARIES picofuse-hw picofuse-hid diff --git a/test/dev_st7701/main.c b/test/dev_st7701/main.c new file mode 100644 index 0000000..254b8cc --- /dev/null +++ b/test/dev_st7701/main.c @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +// Registered only for PICO_BOARD=presto (see test/CMakeLists.txt). These +// pins aren't exposed as board header macros the way touch/LED/I2C are - +// taken directly from the schematic (src/picofuse/dev/pimoroni/presto/ +// pico_presto_schematic.pdf, sheet 2 "RP2350B chip"): GPIO26/27 are +// SPI1's hardware SCLK/TX. GPIO28 is SPI1's hardware RX, but Presto wires +// it to LCD_SPI_CS instead and drives it as a plain GPIO output (matching +// Pimoroni's own driver, which bit-bangs CS rather than using the +// peripheral's hardware CS line) - hence passing it as cs_pin below, not +// as an rx_pin (this panel has no MISO line at all). +#define PRESTO_LCD_SPI_INDEX 1u +#define PRESTO_LCD_SPI_SCK_PIN 26u +#define PRESTO_LCD_SPI_DATA_PIN 27u +#define PRESTO_LCD_SPI_CS_PIN 28u +#define PRESTO_LCD_BACKLIGHT_PIN 45u +#define PRESTO_LCD_WIDTH 480u +#define PRESTO_LCD_HEIGHT 480u +#define PRESTO_LCD_SPI_BAUD_HZ 8000000u + +test_main_hw(0) { + hw_gpio_t *sck = hw_gpio_init(0, PRESTO_LCD_SPI_SCK_PIN, hw_gpio_none); + hw_gpio_t *tx = hw_gpio_init(0, PRESTO_LCD_SPI_DATA_PIN, hw_gpio_none); + hw_gpio_t *cs = hw_gpio_init(0, PRESTO_LCD_SPI_CS_PIN, hw_gpio_none); + hw_gpio_t *bl = hw_gpio_init(0, PRESTO_LCD_BACKLIGHT_PIN, hw_gpio_none); + test_assert(sck != NULL); + test_assert(tx != NULL); + test_assert(cs != NULL); + test_assert(bl != NULL); + + // No MISO line on this panel - rx_pin is NULL. bits_per_word=9 packs + // command/data select as the 9th bit of every word (see + // dev_st7701_init()'s own doc). + hw_spi_config_t spi_config = { + .cs_active_low = true, + .mode = hw_spi_mode_0, + .bits_per_word = 9, + }; + hw_deviceio_t *device = hw_spi_init(PRESTO_LCD_SPI_INDEX, sck, tx, NULL, cs, + PRESTO_LCD_SPI_BAUD_HZ, &spi_config); + test_assert(device != NULL); + + pix_size_t size = {.w = PRESTO_LCD_WIDTH, .h = PRESTO_LCD_HEIGHT}; + pix_display_t *display = dev_st7701_init(device, size, bl, NULL); + sys_printf("[dev_st7701] init: %s\n", display != NULL ? "ok" : "failed"); + test_assert(display != NULL); + + // Held, labelled steps rather than a fast continuous ramp - easier to + // correlate what's actually visible against a printed value. + static const uint8_t levels[] = {0, 32, 64, 96, 128, + 160, 192, 224, 255, 0}; + for (size_t i = 0; i < sizeof(levels) / sizeof(levels[0]); i++) { + sys_printf("[dev_st7701] backlight -> %u\n", levels[i]); + dev_st7701_set_backlight(display, levels[i]); + sys_sleep_ms(1500); + } + + pix_display_deinit(display); + hw_deviceio_deinit(device); + hw_gpio_deinit(bl); + hw_gpio_deinit(cs); + hw_gpio_deinit(tx); + hw_gpio_deinit(sck); +}