From 408e75854c0aaeda72cf12720188395a91268c76 Mon Sep 17 00:00:00 2001 From: maksyuki Date: Fri, 23 Oct 2026 08:04:58 +0800 Subject: [PATCH] feat(spi): implement SPI-P4 sub-stage A display acceptance Add the frozen display path on top of the committed SPI-P3 integration: the project-owned ST7789VI four-wire adapter in app/board (held-CS command/data chains, D/C at clean boundaries with readback, bounded workspaces), the TINY_SPI_P4_ACCEPTANCE ci_smoke image (discovery, panel init ordering, color bars, gradient, odd rectangles, rotation variant, abort/recover/release/reacquire case, measurements), the pin-independent panel oracle rtl/tiny/dv/tiny_st7789_panel.sv, the golden geometry/pixel checker scripts/spi_p4.py, pytest contracts tests/test_spi_p4.py and the st7789vi host tests. The runbook is docs/ip/spi-p4.md. Validation: Verilator acceptance passes on both Tiny profiles at SIM_TEST_PASS Tiny cycles=43019427 (build/ics55-tiny-2026-10-23-01-37-106e35060a8e/, build/ihp130-tiny-2026-10-23-01-38-616e0604657d/) and Icarus behavioral passes on both profiles with the documented QUICK delay/geometry variant at cycles=2710932 (build/ics55-tiny-2026-10-23-01-34-106e35060a8e/, build/ihp130-tiny-2026-10-23-01-39-616e0604657d/); the independent golden checker reports zero problems on all four logs (8/8 windows, pixel counts and FNV-1a checksums, violations=0); full Pytest 2102 passed, 0 failed, 1 skipped; C format/policy/host and RTL format gates clean. Sub-stage B (PSRAM-source display, sequential capture/verify/SD save, SRAM-vs-PSRAM performance) remains gated on R2-P8/R2-P9 human acceptance. SPI-P5 remains deferred; phase_accepted stays false. Recorded in docs/ip/spi-verification.md. --- Makefile | 19 +- app/apps/ci_smoke/tiny.mk | 9 + app/apps/ci_smoke/tiny_spi_p4.c | 222 +++++++++++++++ app/board/include/retrosoc/board/st7789vi.h | 43 +++ app/board/src/st7789vi.c | 284 ++++++++++++++++++++ app/board/src/st7789vi_math.c | 66 +++++ docs/ip/spi-p4.md | 43 +++ docs/ip/spi-verification.md | 44 +++ rtl/tiny/dv/retrosoc_tiny_tb.sv | 7 + rtl/tiny/dv/tiny_st7789_panel.sv | 171 ++++++++++++ rtl/tiny/filelist/tb.fl | 1 + scripts/run_c_tests.py | 3 + scripts/spi_p4.py | 185 +++++++++++++ tests/c/test_runtime.c | 48 ++++ tests/test_spi_p4.py | 115 ++++++++ 15 files changed, 1258 insertions(+), 2 deletions(-) create mode 100644 app/apps/ci_smoke/tiny_spi_p4.c create mode 100644 app/board/include/retrosoc/board/st7789vi.h create mode 100644 app/board/src/st7789vi.c create mode 100644 app/board/src/st7789vi_math.c create mode 100644 docs/ip/spi-p4.md create mode 100644 rtl/tiny/dv/tiny_st7789_panel.sv create mode 100644 scripts/spi_p4.py create mode 100644 tests/test_spi_p4.py diff --git a/Makefile b/Makefile index a917df3c..93d79f42 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,7 @@ TINY_P9_QUICK ?= NO TINY_P9_WORKLOAD ?= QVGA_RGB565 TINY_P10_ACCEPTANCE ?= NO TINY_SPI_P3_ACCEPTANCE ?= NO +TINY_SPI_P4_ACCEPTANCE ?= NO COREMARK_MODE ?= quick NPU_P5_ACCEPTANCE ?= NO NPU_P6_ACCEPTANCE ?= NO @@ -155,6 +156,9 @@ endif ifeq ($(TINY_SPI_P3_ACCEPTANCE),YES) CONFIG_KEY_VARS += TINY_SPI_P3_ACCEPTANCE endif +ifeq ($(TINY_SPI_P4_ACCEPTANCE),YES) +CONFIG_KEY_VARS += TINY_SPI_P4_ACCEPTANCE +endif VARIANT_ID := $(strip $(shell $(VCS_SHELL_PYTHON) $(ROOT_PATH)/scripts/config_key.py \ --lock $(LOCK_FILE) --profile $(PROFILE_NAME) --timestamp $(BUILD_TIMESTAMP) \ $(foreach var,$(CONFIG_KEY_VARS),--value $(var)=$($(var))) | tail -n 1)) @@ -321,14 +325,20 @@ $(call validate_value,TINY_P9_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,TINY_P9_QUICK,$(VALID_BOOL)) $(call validate_value,TINY_P10_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,TINY_SPI_P3_ACCEPTANCE,$(VALID_BOOL)) +$(call validate_value,TINY_SPI_P4_ACCEPTANCE,$(VALID_BOOL)) ifeq ($(TINY_P10_ACCEPTANCE),YES) ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE)/$(TINY_P8_ACCEPTANCE)/$(TINY_P9_ACCEPTANCE)/$(TINY_P9_QUICK),TINY/ci_smoke/YES/NO/NO/NO/NO) $(error TINY_P10_ACCEPTANCE requires Tiny ci_smoke with CSR and no P7/P8/P9 acceptance image) endif endif ifeq ($(TINY_SPI_P3_ACCEPTANCE),YES) -ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE)/$(TINY_P8_ACCEPTANCE)/$(TINY_P9_ACCEPTANCE)/$(TINY_P10_ACCEPTANCE),TINY/ci_smoke/YES/NO/NO/NO/NO) -$(error TINY_SPI_P3_ACCEPTANCE requires Tiny ci_smoke with CSR and no R2 acceptance image) +ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE)/$(TINY_P8_ACCEPTANCE)/$(TINY_P9_ACCEPTANCE)/$(TINY_P10_ACCEPTANCE)/$(TINY_SPI_P4_ACCEPTANCE),TINY/ci_smoke/YES/NO/NO/NO/NO/NO) +$(error TINY_SPI_P3_ACCEPTANCE requires Tiny ci_smoke with CSR and no other acceptance image) +endif +endif +ifeq ($(TINY_SPI_P4_ACCEPTANCE),YES) +ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE)/$(TINY_P8_ACCEPTANCE)/$(TINY_P9_ACCEPTANCE)/$(TINY_P10_ACCEPTANCE)/$(TINY_SPI_P3_ACCEPTANCE),TINY/ci_smoke/YES/NO/NO/NO/NO/NO) +$(error TINY_SPI_P4_ACCEPTANCE requires Tiny ci_smoke with CSR and no other acceptance image) endif endif $(call validate_value,TINY_P9_WORKLOAD,QVGA_RGB565 VGA_YUV422 RECOVERY) @@ -561,6 +571,11 @@ spi-p3-lint spi-p3-rtl spi-p3-report: manifest $(PYTHON) $(ROOT_PATH)/scripts/spi_p3.py $(patsubst spi-p3-%,%,$@) \ --variant-root $(VARIANT_ROOT) --pdk $(PDK) +.PHONY: spi-p4-lint spi-p4-rtl spi-p4-report +spi-p4-lint spi-p4-rtl spi-p4-report: manifest + $(PYTHON) $(ROOT_PATH)/scripts/spi_p4.py $(patsubst spi-p4-%,%,$@) \ + --variant-root $(VARIANT_ROOT) --pdk $(PDK) + .PHONY: crypto-p0-constants crypto-p0-rtl crypto-p0-baseline crypto-p0-report crypto-p0-constants: manifest $(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py constants --variant-root $(VARIANT_ROOT) diff --git a/app/apps/ci_smoke/tiny.mk b/app/apps/ci_smoke/tiny.mk index 3d754568..5124efae 100644 --- a/app/apps/ci_smoke/tiny.mk +++ b/app/apps/ci_smoke/tiny.mk @@ -25,4 +25,13 @@ APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p10.c endif ifeq ($(TINY_SPI_P3_ACCEPTANCE),YES) APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_spi_p3.c +endif +ifeq ($(TINY_SPI_P4_ACCEPTANCE),YES) +APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_spi_p4.c +APP_SRCS += $(ROOT_PATH)/app/board/src/st7789vi.c +APP_SRCS += $(ROOT_PATH)/app/board/src/st7789vi_math.c +APP_INC_DIRS += $(ROOT_PATH)/app/board/include +ifeq ($(SIMU),IVERILOG) +APP_CFLAGS += -DRS_ST7789VI_QUICK +endif endif \ No newline at end of file diff --git a/app/apps/ci_smoke/tiny_spi_p4.c b/app/apps/ci_smoke/tiny_spi_p4.c new file mode 100644 index 00000000..eccebea4 --- /dev/null +++ b/app/apps/ci_smoke/tiny_spi_p4.c @@ -0,0 +1,222 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define RS_P4_TIMEOUT UINT32_C(2000000) +#define RS_P4_WORKSPACE_LEN UINT32_C(16) + +#ifdef RS_ST7789VI_QUICK +#define RS_P4_FULL_X1 UINT16_C(39) +#define RS_P4_FULL_Y1 UINT16_C(23) +#else +#define RS_P4_FULL_X1 UINT16_C(319) +#define RS_P4_FULL_Y1 UINT16_C(239) +#endif + +static uint32_t rs_p4_workspace[RS_P4_WORKSPACE_LEN]; + +static const rs_gpio_config_t rs_p4_alt0 = { + .mode = RS_GPIO_MODE_ALT0, + .pull = RS_GPIO_PULL_NONE, + .trigger = RS_GPIO_TRIGGER_NONE, + .output_high = false, +}; +static const rs_gpio_config_t rs_p4_alt1 = { + .mode = RS_GPIO_MODE_ALT1, + .pull = RS_GPIO_PULL_NONE, + .trigger = RS_GPIO_TRIGGER_NONE, + .output_high = false, +}; +static const rs_gpio_config_t rs_p4_dc_out = { + .mode = RS_GPIO_MODE_OUTPUT, + .pull = RS_GPIO_PULL_NONE, + .trigger = RS_GPIO_TRIGGER_NONE, + .output_high = true, +}; + +static rs_status_t rs_p4_setup_display_pins(void) { + if ((rs_gpio_configure(27U, &rs_p4_alt0) != RS_OK) || + (rs_gpio_configure(28U, &rs_p4_alt1) != RS_OK) || + (rs_gpio_configure(31U, &rs_p4_alt1) != RS_OK) || + (rs_gpio_configure(30U, &rs_p4_dc_out) != RS_OK)) { + return RS_EIO; + } + return RS_OK; +} + +static void rs_p4_store_fault(uintptr_t mcause, uintptr_t stack_pointer) { + volatile uint32_t *const frame = (volatile uint32_t *)stack_pointer; + uintptr_t mtval; + rs_spi_status_t status; + __asm__ volatile("csrr %0, mtval" : "=r"(mtval)); + if (rs_spi_get_status(&status) != RS_OK) { + status.fault = 0U; + status.pins_ready = 0U; + status.status = 0U; + } + printf("SPI_P4_TRAP pins=%08x fault=%08x mcause=%lx mepc=%lx mtval=%lx status=%08x\n", + (unsigned long)status.pins_ready, (unsigned long)status.fault, (unsigned long)mcause, + (unsigned long)frame[11], (unsigned long)mtval, (unsigned long)status.status); + for (;;) { + } +} + +static void rs_p4_counters(uint32_t *cycles, uint32_t *instructions) { + __asm__ volatile("csrr %0, cycle" : "=r"(*cycles)); + __asm__ volatile("csrr %0, instret" : "=r"(*instructions)); +} + +static rs_status_t rs_p4_abort_case(rs_timeout_t timeout) { + const rs_st7789vi_window_t window = {0U, 0U, RS_P4_FULL_X1, RS_P4_FULL_Y1}; + uint32_t pushed = 0U; + uint32_t index; + rs_status_t status; + + status = rs_st7789vi_begin_pixels(&window, timeout); + if (status != RS_OK) { + return status; + } + while (pushed < 257U) { + for (index = 0U; index < RS_P4_WORKSPACE_LEN; index++) { + rs_p4_workspace[index] = UINT32_C(0x07E007E0); + } + status = rs_spi_transfer_cpu(rs_p4_workspace, NULL, RS_P4_WORKSPACE_LEN, timeout); + if (status != RS_OK) { + return status; + } + pushed += RS_P4_WORKSPACE_LEN; + } + status = rs_spi_abort(timeout); + if (status != RS_OK) { + return status; + } + status = rs_spi_recover(timeout); + if (status != RS_OK) { + return status; + } + status = rs_spi_release(timeout); + if (status != RS_OK) { + return status; + } + status = rs_p4_setup_display_pins(); + if (status != RS_OK) { + return status; + } + status = rs_spi_acquire(timeout); + if (status != RS_OK) { + return status; + } + return rs_st7789vi_fill_window(&(rs_st7789vi_window_t){100U, 100U, 116U, 108U}, + RS_ST7789VI_STAGE_FINAL, rs_p4_workspace, RS_P4_WORKSPACE_LEN, + timeout); +} + +int main(void) { + rs_spi_config_t config = { + .clock_hz = UINT32_C(24000000), + .divider = 1U, + .cs_setup = 1U, + .cs_hold = 1U, + .cs_inactive = 1U, + .cpol = false, + .cpha = false, + .lsb_first = false, + .miso_enable = false, + .display_dc = true, + }; + const rs_st7789vi_window_t full = {0U, 0U, RS_P4_FULL_X1, RS_P4_FULL_Y1}; + const rs_st7789vi_window_t odd_a = {13U, 17U, 45U, 29U}; + const rs_st7789vi_window_t odd_b = {319U, 0U, 319U, 239U}; + const rs_st7789vi_window_t odd_c = {200U, 239U, 260U, 239U}; + const rs_st7789vi_window_t rotated = {5U, 7U, 45U, 29U}; + rs_spi_status_t status; + uint32_t ip_id = 0U, version = 0U, capability = 0U; + uint32_t cycles_begin; + uint32_t instructions_begin; + uint32_t cycles_end; + uint32_t instructions_end; + uint8_t code = 0U; + + (void)rs_irq_register_exception(7U, rs_p4_store_fault); + (void)rs_uart_init(24000000U, 921600U); + + if ((rs_spi_probe(&ip_id, &version, &capability) != RS_OK) || (ip_id != RS_SPI_ABI_ID_VALUE) || + (version != RS_SPI_ABI_VERSION_VALUE) || + ((capability & UINT32_C(0x43)) != UINT32_C(0x43)) || + (rs_p4_setup_display_pins() != RS_OK) || (rs_spi_configure(&config) != RS_OK) || + (rs_spi_acquire(RS_P4_TIMEOUT) != RS_OK) || (rs_spi_get_status(&status) != RS_OK) || + ((status.status & (UINT32_C(1) << RS_SPI_ABI_STATUS_GPIO_READY)) == 0U)) { + code = 1U; + } + if (rs_spi_get_status(&status) == RS_OK) { + printf("SPI_P4_READY status=%08x pins=%08x fault=%08x\n", (unsigned long)status.status, + (unsigned long)status.pins_ready, (unsigned long)status.fault); + } + if ((code == 0U) && (rs_st7789vi_init(RS_ST7789VI_ROTATION_NORMAL) != RS_OK)) { + code = 2U; + } + + rs_p4_counters(&cycles_begin, &instructions_begin); + if ((code == 0U) && (rs_st7789vi_fill_window(&full, RS_ST7789VI_STAGE_BARS, rs_p4_workspace, + RS_P4_WORKSPACE_LEN, RS_P4_TIMEOUT) != RS_OK)) { + code = 3U; + } + rs_p4_counters(&cycles_end, &instructions_end); + printf("SPI_P4_STAGE id=1 pixels=%lu cycles=%lu instructions=%lu\n", + (unsigned long)(((uint32_t)RS_P4_FULL_X1 + 1U) * ((uint32_t)RS_P4_FULL_Y1 + 1U)), + (unsigned long)(cycles_end - cycles_begin), + (unsigned long)(instructions_end - instructions_begin)); + + rs_p4_counters(&cycles_begin, &instructions_begin); + if ((code == 0U) && (rs_st7789vi_fill_window(&full, RS_ST7789VI_STAGE_GRADIENT, rs_p4_workspace, + RS_P4_WORKSPACE_LEN, RS_P4_TIMEOUT) != RS_OK)) { + code = 4U; + } + rs_p4_counters(&cycles_end, &instructions_end); + printf("SPI_P4_STAGE id=2 pixels=%lu cycles=%lu instructions=%lu\n", + (unsigned long)(((uint32_t)RS_P4_FULL_X1 + 1U) * ((uint32_t)RS_P4_FULL_Y1 + 1U)), + (unsigned long)(cycles_end - cycles_begin), + (unsigned long)(instructions_end - instructions_begin)); + + if ((code == 0U) && ((rs_st7789vi_fill_window(&odd_a, RS_ST7789VI_STAGE_ODD_A, rs_p4_workspace, + RS_P4_WORKSPACE_LEN, RS_P4_TIMEOUT) != RS_OK) || + (rs_st7789vi_fill_window(&odd_b, RS_ST7789VI_STAGE_ODD_B, rs_p4_workspace, + RS_P4_WORKSPACE_LEN, RS_P4_TIMEOUT) != RS_OK) || + (rs_st7789vi_fill_window(&odd_c, RS_ST7789VI_STAGE_ODD_C, rs_p4_workspace, + RS_P4_WORKSPACE_LEN, RS_P4_TIMEOUT) != RS_OK))) { + code = 5U; + } + if ((code == 0U) && + ((rs_st7789vi_set_rotation(RS_ST7789VI_ROTATION_VARIANT) != RS_OK) || + (rs_st7789vi_fill_window(&rotated, RS_ST7789VI_STAGE_ROTATION, rs_p4_workspace, + RS_P4_WORKSPACE_LEN, RS_P4_TIMEOUT) != RS_OK) || + (rs_st7789vi_set_rotation(RS_ST7789VI_ROTATION_NORMAL) != RS_OK))) { + code = 6U; + } + if ((code == 0U) && (rs_p4_abort_case(RS_P4_TIMEOUT) != RS_OK)) { + code = 7U; + } + if ((code == 0U) && (rs_spi_release(RS_P4_TIMEOUT) != RS_OK)) { + code = 8U; + } + if ((code == 0U) && (rs_spi_get_status(&status) == RS_OK)) { + printf("SPI_P4_MEASURE sck_hz=%lu payload_bytes=%lu transport_words=%lu tx_stall=%lu\n", + (unsigned long)(config.clock_hz / (2U * config.divider)), + (unsigned long)status.payload_bytes, (unsigned long)status.transport_words, + (unsigned long)status.levels); + } + rs_sysctrl_write_test_status(code == 0U, code); + (void)rs_uart_init(24000000U, 921600U); + printf("SPI_P4_RESULT id=%08x version=%08x capability=%08x code=%u\n", ip_id, version, + capability, code); + for (;;) { + } +} diff --git a/app/board/include/retrosoc/board/st7789vi.h b/app/board/include/retrosoc/board/st7789vi.h new file mode 100644 index 00000000..5f40cd4d --- /dev/null +++ b/app/board/include/retrosoc/board/st7789vi.h @@ -0,0 +1,43 @@ +#ifndef RETROSOC_BOARD_ST7789VI_H +#define RETROSOC_BOARD_ST7789VI_H + +#include + +#include + +#define RS_ST7789VI_WIDTH UINT16_C(320) +#define RS_ST7789VI_HEIGHT UINT16_C(240) + +/* Deterministic acceptance stages; the same geometry is recomputed by the + * host checker, so every value here is part of the source-bound contract. */ +#define RS_ST7789VI_STAGE_BARS UINT32_C(1) +#define RS_ST7789VI_STAGE_GRADIENT UINT32_C(2) +#define RS_ST7789VI_STAGE_ODD_A UINT32_C(3) +#define RS_ST7789VI_STAGE_ODD_B UINT32_C(4) +#define RS_ST7789VI_STAGE_ODD_C UINT32_C(5) +#define RS_ST7789VI_STAGE_ROTATION UINT32_C(6) +#define RS_ST7789VI_STAGE_FINAL UINT32_C(7) +#define RS_ST7789VI_STAGE_ABORT UINT32_C(8) + +#define RS_ST7789VI_ROTATION_NORMAL UINT8_C(0x00) +#define RS_ST7789VI_ROTATION_VARIANT UINT8_C(0x60) + +typedef struct { + uint16_t x0; + uint16_t y0; + uint16_t x1; + uint16_t y1; +} rs_st7789vi_window_t; + +uint16_t rs_st7789vi_pattern(uint32_t stage, uint32_t row, uint32_t col); +rs_status_t rs_st7789vi_window_cells(const rs_st7789vi_window_t *window, uint32_t *pixels, + uint32_t *bytes, uint32_t *words); + +rs_status_t rs_st7789vi_init(uint8_t rotation); +rs_status_t rs_st7789vi_set_rotation(uint8_t madctl); +rs_status_t rs_st7789vi_begin_pixels(const rs_st7789vi_window_t *window, rs_timeout_t timeout); +rs_status_t rs_st7789vi_fill_window(const rs_st7789vi_window_t *window, uint32_t stage, + uint32_t *workspace, uint32_t workspace_words, + rs_timeout_t timeout); + +#endif diff --git a/app/board/src/st7789vi.c b/app/board/src/st7789vi.c new file mode 100644 index 00000000..c14cea83 --- /dev/null +++ b/app/board/src/st7789vi.c @@ -0,0 +1,284 @@ +#include + +#include +#include +#include +#include + +#define RS_ST7789VI_PIN_DC UINT32_C(30) + +#ifdef RS_ST7789VI_QUICK +#define RS_ST7789VI_DELAY_RESET_MS UINT32_C(1) +#define RS_ST7789VI_DELAY_SLPOUT_MS UINT32_C(1) +#define RS_ST7789VI_DELAY_DISPON_MS UINT32_C(1) +#else +#define RS_ST7789VI_DELAY_RESET_MS UINT32_C(120) +#define RS_ST7789VI_DELAY_SLPOUT_MS UINT32_C(120) +#define RS_ST7789VI_DELAY_DISPON_MS UINT32_C(20) +#endif + +#define RS_ST7789VI_CMD_SWRESET UINT8_C(0x01) +#define RS_ST7789VI_CMD_SLPOUT UINT8_C(0x11) +#define RS_ST7789VI_CMD_NORON UINT8_C(0x13) +#define RS_ST7789VI_CMD_INVON UINT8_C(0x21) +#define RS_ST7789VI_CMD_DISPON UINT8_C(0x29) +#define RS_ST7789VI_CMD_CASET UINT8_C(0x2A) +#define RS_ST7789VI_CMD_RASET UINT8_C(0x2B) +#define RS_ST7789VI_CMD_RAMWR UINT8_C(0x2C) +#define RS_ST7789VI_CMD_MADCTL UINT8_C(0x36) +#define RS_ST7789VI_CMD_COLMOD UINT8_C(0x3A) + +static rs_status_t rs_st7789vi_wait_clean(rs_timeout_t timeout) { + rs_spi_status_t status; + + while (timeout-- != 0U) { + if (rs_spi_get_status(&status) != RS_OK) { + return RS_EIO; + } + if ((status.status & ((UINT32_C(1) << RS_SPI_ABI_STATUS_SEGMENT_CLEAN) | + (UINT32_C(1) << RS_SPI_ABI_STATUS_TRANSPORT_IDLE))) == + ((UINT32_C(1) << RS_SPI_ABI_STATUS_SEGMENT_CLEAN) | + (UINT32_C(1) << RS_SPI_ABI_STATUS_TRANSPORT_IDLE))) { + return RS_OK; + } + } + return RS_ETIMEOUT; +} + +static rs_status_t rs_st7789vi_set_dc(bool data) { + bool level = false; + if (rs_gpio_write(RS_ST7789VI_PIN_DC, data) != RS_OK) { + return RS_EIO; + } + if (rs_gpio_read(RS_ST7789VI_PIN_DC, &level) != RS_OK) { + return RS_EIO; + } + return (level == data) ? RS_OK : RS_EIO; +} + +static rs_status_t rs_st7789vi_push_bytes(const uint8_t *bytes, uint32_t count, bool hold_cs, + rs_timeout_t timeout) { + const rs_spi_segment_t segment = { + .frames = count, + .direction = RS_SPI_DIRECTION_TX_ONLY, + .frame16 = false, + .hold_cs = hold_cs, + .dummy = UINT16_C(0xFFFF), + }; + uint32_t word = 0U; + uint32_t index; + rs_status_t status; + + if ((bytes == NULL) || (count == 0U)) { + return RS_EINVAL; + } + status = rs_spi_arm(&segment); + if (status != RS_OK) { + return status; + } + status = rs_spi_start(timeout); + if (status != RS_OK) { + return status; + } + for (index = 0U; index < count; index += 4U) { + uint32_t lane; + word = 0U; + for (lane = 0U; lane < 4U; lane++) { + uint32_t value = 0U; + if ((index + lane) < count) { + value = bytes[index + lane]; + } + word |= value << (lane * 8U); + } + status = rs_spi_transfer_cpu(&word, NULL, 1U, timeout); + if (status != RS_OK) { + return status; + } + } + (void)hold_cs; + return rs_st7789vi_wait_clean(timeout); +} + +static rs_status_t rs_st7789vi_command(uint8_t command, const uint8_t *params, uint32_t param_count, + rs_timeout_t timeout) { + rs_status_t status; + + status = rs_st7789vi_set_dc(false); + if (status != RS_OK) { + return status; + } + status = rs_st7789vi_push_bytes(&command, 1U, param_count != 0U, timeout); + if (status != RS_OK) { + return status; + } + if (param_count == 0U) { + return rs_st7789vi_wait_clean(timeout); + } + status = rs_st7789vi_set_dc(true); + if (status != RS_OK) { + return status; + } + return rs_st7789vi_push_bytes(params, param_count, false, timeout); +} + +static rs_status_t rs_st7789vi_delay_ms(uint32_t ms) { + uint64_t start; + uint64_t now; + uint64_t limit; + + if (rs_clint_get_time(&start) != RS_OK) { + return RS_EIO; + } + limit = start + (uint64_t)ms * UINT64_C(1000); + for (;;) { + if (rs_clint_get_time(&now) != RS_OK) { + return RS_EIO; + } + if (now >= limit) { + return RS_OK; + } + } +} + +rs_status_t rs_st7789vi_init(uint8_t rotation) { + const uint8_t colmod = UINT8_C(0x05); + const rs_timeout_t timeout = UINT32_C(1000000); + rs_status_t status; + + status = rs_st7789vi_command(RS_ST7789VI_CMD_SWRESET, NULL, 0U, timeout); + if (status == RS_OK) { + status = rs_st7789vi_delay_ms(RS_ST7789VI_DELAY_RESET_MS); + } + if (status == RS_OK) { + status = rs_st7789vi_command(RS_ST7789VI_CMD_SLPOUT, NULL, 0U, timeout); + } + if (status == RS_OK) { + status = rs_st7789vi_delay_ms(RS_ST7789VI_DELAY_SLPOUT_MS); + } + if (status == RS_OK) { + status = rs_st7789vi_command(RS_ST7789VI_CMD_COLMOD, &colmod, 1U, timeout); + } + if (status == RS_OK) { + status = rs_st7789vi_set_rotation(rotation); + } + if (status == RS_OK) { + status = rs_st7789vi_command(RS_ST7789VI_CMD_INVON, NULL, 0U, timeout); + } + if (status == RS_OK) { + status = rs_st7789vi_command(RS_ST7789VI_CMD_NORON, NULL, 0U, timeout); + } + if (status == RS_OK) { + status = rs_st7789vi_command(RS_ST7789VI_CMD_DISPON, NULL, 0U, timeout); + } + if (status == RS_OK) { + status = rs_st7789vi_delay_ms(RS_ST7789VI_DELAY_DISPON_MS); + } + return status; +} + +rs_status_t rs_st7789vi_set_rotation(uint8_t madctl) { + return rs_st7789vi_command(RS_ST7789VI_CMD_MADCTL, &madctl, 1U, UINT32_C(1000000)); +} + +rs_status_t rs_st7789vi_begin_pixels(const rs_st7789vi_window_t *window, rs_timeout_t timeout) { + uint32_t pixels; + uint32_t bytes; + uint32_t words; + rs_status_t status; + uint8_t address[4]; + rs_spi_segment_t data_segment = { + .frames = 0U, + .direction = RS_SPI_DIRECTION_TX_ONLY, + .frame16 = true, + .hold_cs = false, + .dummy = UINT16_C(0xFFFF), + }; + + status = rs_st7789vi_window_cells(window, &pixels, &bytes, &words); + if (status != RS_OK) { + return status; + } + address[0] = (uint8_t)(window->x0 >> 8U); + address[1] = (uint8_t)window->x0; + address[2] = (uint8_t)(window->x1 >> 8U); + address[3] = (uint8_t)window->x1; + status = rs_st7789vi_command(RS_ST7789VI_CMD_CASET, address, 4U, timeout); + if (status != RS_OK) { + return status; + } + address[0] = (uint8_t)(window->y0 >> 8U); + address[1] = (uint8_t)window->y0; + address[2] = (uint8_t)(window->y1 >> 8U); + address[3] = (uint8_t)window->y1; + status = rs_st7789vi_command(RS_ST7789VI_CMD_RASET, address, 4U, timeout); + if (status != RS_OK) { + return status; + } + status = rs_st7789vi_set_dc(false); + if (status != RS_OK) { + return status; + } + status = rs_st7789vi_push_bytes(&(uint8_t){RS_ST7789VI_CMD_RAMWR}, 1U, true, timeout); + if (status != RS_OK) { + return status; + } + status = rs_st7789vi_set_dc(true); + if (status != RS_OK) { + return status; + } + data_segment.frames = pixels; + status = rs_spi_arm(&data_segment); + if (status != RS_OK) { + return status; + } + return rs_spi_start(timeout); +} + +rs_status_t rs_st7789vi_fill_window(const rs_st7789vi_window_t *window, uint32_t stage, + uint32_t *workspace, uint32_t workspace_words, + rs_timeout_t timeout) { + uint32_t pixels; + uint32_t bytes; + uint32_t words; + uint32_t produced = 0U; + rs_status_t status; + uint32_t width; + + status = rs_st7789vi_window_cells(window, &pixels, &bytes, &words); + if ((status != RS_OK) || (workspace == NULL) || (workspace_words == 0U)) { + return RS_EINVAL; + } + status = rs_st7789vi_begin_pixels(window, timeout); + if (status != RS_OK) { + return status; + } + width = (uint32_t)window->x1 - (uint32_t)window->x0 + 1U; + while (produced < words) { + uint32_t chunk = words - produced; + uint32_t index; + uint32_t first_pixel; + if (chunk > workspace_words) { + chunk = workspace_words; + } + first_pixel = produced * 2U; + for (index = 0U; index < chunk; index++) { + uint32_t pair = first_pixel + (index * 2U); + uint32_t row = window->y0 + (pair / width); + uint32_t col = window->x0 + (pair % width); + uint32_t lo = rs_st7789vi_pattern(stage, row, col); + uint32_t hi = 0U; + if ((pair + 1U) < pixels) { + uint32_t nrow = window->y0 + ((pair + 1U) / width); + uint32_t ncol = window->x0 + ((pair + 1U) % width); + hi = rs_st7789vi_pattern(stage, nrow, ncol); + } + workspace[index] = lo | (hi << 16U); + } + status = rs_spi_transfer_cpu(workspace, NULL, chunk, timeout); + if (status != RS_OK) { + return status; + } + produced += chunk; + } + return rs_st7789vi_wait_clean(timeout); +} diff --git a/app/board/src/st7789vi_math.c b/app/board/src/st7789vi_math.c new file mode 100644 index 00000000..fcfe3796 --- /dev/null +++ b/app/board/src/st7789vi_math.c @@ -0,0 +1,66 @@ +#include + +#include + +static const uint16_t rs_st7789vi_bars[8] = { + UINT16_C(0xFFFF), UINT16_C(0xFFE0), UINT16_C(0x07FF), UINT16_C(0x07E0), + UINT16_C(0xF81F), UINT16_C(0xF800), UINT16_C(0x001F), UINT16_C(0x0000), +}; + +static uint32_t rs_st7789vi_clamp(uint32_t value, uint32_t limit) { + return (value > limit) ? limit : value; +} + +uint16_t rs_st7789vi_pattern(uint32_t stage, uint32_t row, uint32_t col) { + uint32_t red5; + uint32_t green6; + uint32_t blue5; + + switch (stage) { + case RS_ST7789VI_STAGE_BARS: + return rs_st7789vi_bars[rs_st7789vi_clamp(col / 40U, 7U)]; + case RS_ST7789VI_STAGE_GRADIENT: + case RS_ST7789VI_STAGE_ABORT: + red5 = rs_st7789vi_clamp(col >> 3U, 31U); + green6 = rs_st7789vi_clamp(row >> 2U, 63U); + blue5 = rs_st7789vi_clamp((row + col) >> 4U, 31U); + return (uint16_t)((red5 << 11U) | (green6 << 5U) | blue5); + case RS_ST7789VI_STAGE_ODD_A: + return ((row + col) & 1U) != 0U ? UINT16_C(0xFFFF) : UINT16_C(0x0000); + case RS_ST7789VI_STAGE_ODD_B: + return UINT16_C(0x07E0); + case RS_ST7789VI_STAGE_ODD_C: + return UINT16_C(0xFFE0); + case RS_ST7789VI_STAGE_ROTATION: + red5 = col & 31U; + green6 = row & 63U; + blue5 = (row ^ col) & 31U; + return (uint16_t)((red5 << 11U) | (green6 << 5U) | blue5); + case RS_ST7789VI_STAGE_FINAL: + return UINT16_C(0xFFFF); + default: + return UINT16_C(0x0000); + } +} + +rs_status_t rs_st7789vi_window_cells(const rs_st7789vi_window_t *window, uint32_t *pixels, + uint32_t *bytes, uint32_t *words) { + uint32_t width; + uint32_t height; + uint32_t count; + + if ((window == NULL) || (pixels == NULL) || (bytes == NULL) || (words == NULL)) { + return RS_EINVAL; + } + if ((window->x1 < window->x0) || (window->y1 < window->y0) || + (window->x1 >= RS_ST7789VI_WIDTH) || (window->y1 >= RS_ST7789VI_HEIGHT)) { + return RS_EINVAL; + } + width = (uint32_t)window->x1 - (uint32_t)window->x0 + 1U; + height = (uint32_t)window->y1 - (uint32_t)window->y0 + 1U; + count = width * height; + *pixels = count; + *bytes = count * 2U; + *words = (count + 1U) / 2U; + return RS_OK; +} diff --git a/docs/ip/spi-p4.md b/docs/ip/spi-p4.md new file mode 100644 index 00000000..6ea5ac53 --- /dev/null +++ b/docs/ip/spi-p4.md @@ -0,0 +1,43 @@ +# SPI-P4 Display and Snapshot Application Qualification + +SPI-P4 covers the frozen display path on top of the committed SPI-P3 +integration: a project-owned ST7789VI-style four-wire adapter in +`app/board/`, an independent pin-level panel oracle in the Tiny testbench, +and the `TINY_SPI_P4_ACCEPTANCE` image covering the display-pattern stages. +Camera/PSRAM-source display and the sequential SD snapshot evidence are +sub-stage B; their frozen cases additionally require R2-P8/R2-P9 human +acceptance, which is pending at preflight time. + +## Interface contract + +- Panel wiring: SCK=GPIO27 ALT0, MOSI=GPIO28 ALT1, CS_N=GPIO31 ALT1, + D/C=GPIO30 as an ordinary push-pull software output; no MISO. CS is + retained across each command/data boundary through held-CS segments; D/C + changes only at clean boundaries with a GPIO readback before the next START. +- Commands are 8-bit MSB-first frames with D/C low; parameters and RGB565 + pixels are byte streams with D/C high; pixels occupy two ordinary data + bytes, big-endian. The reference geometry is 320x240 landscape with zero + address offsets for full-screen windows. +- Init order (sub-stage A constants for the selected panel): SWRESET, 120 ms, + SLPOUT, 120 ms, COLMOD=0x05, MADCTL=rotation, INVON, NORON, DISPON, 20 ms. + +## Stages and evidence + +The acceptance image (`app/apps/ci_smoke/tiny_spi_p4.c`) runs: discovery, +GPIO/SPI configuration, acquire with GPIO_READY, panel init, stage 1 color +bars and stage 2 gradient (full 320x240), stage 3-5 odd rectangles, stage 6 +rotation variant (MADCTL=0x60), an abort/recover case (truncated full-frame +then a final clean window), release, and TEST_STATUS before the UART summary. +The testbench panel model (`rtl/tiny/dv/tiny_st7789_panel.sv`, active with +`+tiny_spi_p4`) decodes the pins independently and emits SPI_P4_CMD, +SPI_P4_WINDOW (geometry, pixel count, FNV-1a, abort flag), SPI_P4_VIOLATION +and a terminal SPI_P4_PANEL summary. `scripts/spi_p4.py check --log` recomputes +the golden geometry/pixel checksums and rejects any drift; verdicts remain +TEST_STATUS/`SIM_TEST_PASS` with no forbidden markers. + +Focused checks: `make CONFIG= APP=ci_smoke TINY_SPI_P4_ACCEPTANCE=YES spi-p4-lint spi-p4-rtl spi-p4-report`. +Acceptance: `make CONFIG= APP=ci_smoke TINY_SPI_P4_ACCEPTANCE=YES TINY_SIM_ARGS=+tiny_spi_p4 firmware sim`. + +Pre-final timing stays observational under the 2026-10-07 policy; SPI-P5 +remains the final physical qualification phase. `phase_accepted` is false +until human review. diff --git a/docs/ip/spi-verification.md b/docs/ip/spi-verification.md index 006df976..682d1b23 100644 --- a/docs/ip/spi-verification.md +++ b/docs/ip/spi-verification.md @@ -112,6 +112,50 @@ SPI request allocation, the full post-fix Pytest campaign and product regressions are unrun. Product instances retain V2.0/default-disabled SPI. +## SPI-P4 sub-stage A checkpoint (2026-10-23) + +The approved staged preflight (display-first, camera/PSRAM cases deferred to +sub-stage B pending R2-P8/R2-P9 acceptance) is implemented on the committed +SPI-P3 base. New content: the project-owned ST7789VI four-wire adapter +(`app/board/st7789vi.c`, pure pattern/window math in `st7789vi_math.c`), the +`TINY_SPI_P4_ACCEPTANCE` image (`app/apps/ci_smoke/tiny_spi_p4.c`), the +pin-independent panel oracle (`rtl/tiny/dv/tiny_st7789_panel.sv`), the +golden checker and focused contracts (`scripts/spi_p4.py`, +`tests/test_spi_p4.py`) and host tests (`test_st7789vi_patterns` in +`tests/c/test_runtime.c`). The [runbook](spi-p4.md) records the contract. + +Three defects found by the bring-up and fixed with evidence: (1) the +committed integration wired the SPI `gpio_ready_i` to the GPIO APB bridge's +transaction-level ready, which drops on every GPIO access and faulted any +mid-session D/C change required by the frozen contract; it now uses +GPIO-clock/reset-level availability (`retrosoc_tiny.sv`). (2) The adapter +issued `END` after non-held segments, which only HELD permits. (3) HAL +lifecycle gaps: `rs_spi_acquire` now also waits `BUS_IDLE` before returning a +usable session, `rs_spi_recover` waits wire-state Disabled (RECOVER retains +the session), and `rs_spi_release` skips the DISABLE step when the session is +already Disabled. SPI-P3 acceptance remains passing after the HAL changes +(`SIM_TEST_PASS Tiny cycles=1037899`). + +Sub-stage A acceptance, all with the independent panel oracle and golden +geometry/pixel/FNV-1a checks passing (`spi_p4.py check`, zero problems, +violations=0, 8/8 windows, 21 commands): Verilator passes on both profiles at +`SIM_TEST_PASS Tiny cycles=43019427` +(`build/ics55-tiny-2026-10-23-01-37-106e35060a8e/`, +`build/ihp130-tiny-2026-10-23-01-38-616e0604657d/`); Icarus behavioral passes +on both profiles with the QUICK delay/geometry variant at +`SIM_TEST_PASS Tiny cycles=2710932` +(`build/ics55-tiny-2026-10-23-01-34-106e35060a8e/`, +`build/ihp130-tiny-2026-10-23-01-39-616e0604657d/`). The image covers +discovery/configuration, panel init ordering (SLPOUT/DISPON checked), +full-screen color bars and gradient, odd rectangles, a rotation variant, an +abort/recover/release/reacquire case with a clean final window, and +cycle/payload measurements. Full Pytest: 2102 passed, 1 skipped (optional +`pypdf`), 0 failed in 4442.19 s. + +Sub-stage B (V21/V22: PSRAM-source display, sequential capture/verify/SD, +SRAM-vs-PSRAM performance) remains gated on R2-P8/R2-P9 human acceptance. +SPI-P5 stays deferred; `phase_accepted` stays false. + ## SPI-P3 implementation checkpoint (2026-10-21) The current authorized worktree integrates SPI-P2 into Tiny with APB4 diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv index f54a4227..a4a5ff79 100644 --- a/rtl/tiny/dv/retrosoc_tiny_tb.sv +++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv @@ -41,6 +41,13 @@ module retrosoc_tiny_tb; .vsync_o(s_dvp_vsync), .dat_o (s_dvp_data) ); + tiny_st7789_panel u_st7789_panel ( + .sck_i (s_gpio_27), + .csn_i (s_gpio_31), + .mosi_i (s_gpio_28), + .dc_i (s_gpio_30), + .test_done_i(u_dut.s_test_done) + ); assign s_gpio_12 = s_p9_camera_population ? s_dvp_data[0] : 1'bz; assign s_gpio_13 = s_p9_camera_population ? s_dvp_data[1] : 1'bz; assign s_gpio_14 = s_p9_camera_population ? s_dvp_data[2] : 1'bz; diff --git a/rtl/tiny/dv/tiny_st7789_panel.sv b/rtl/tiny/dv/tiny_st7789_panel.sv new file mode 100644 index 00000000..b95babea --- /dev/null +++ b/rtl/tiny/dv/tiny_st7789_panel.sv @@ -0,0 +1,171 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 + +// Passive ST7789VI four-wire SPI panel observer for the Tiny SPI-P4 flow. +// The model never drives the display pads and stays fully dormant unless +// +tiny_spi_p4 is set. It decodes the mode-0 MSB-first byte stream, tracks +// command/parameter chains and RAMWR pixel windows, and emits the records +// consumed by scripts/spi_p4.py; plain numeric fields are hexadecimal. +module tiny_st7789_panel ( + input logic sck_i, + input logic csn_i, + input logic mosi_i, + input logic dc_i, + input logic test_done_i +); + bit s_enabled; + bit s_cmd_active; + bit s_pixel_mode; + bit s_seen_slpout; + bit s_seen_dispon; + bit s_panel_done; + logic [ 7:0] s_shift; + logic [ 7:0] s_cmd_code; + logic [ 7:0] s_madctl; + logic [15:0] s_param_shift; + logic [31:0] s_win_fnv; + logic [31:0] s_panel_fnv; + integer s_bit_count; + integer s_param_count; + integer s_win_bytes; + integer s_win_x0; + integer s_win_y0; + integer s_win_x1; + integer s_win_y1; + integer s_commands; + integer s_windows; + integer s_pixels; + integer s_violations; + + task automatic record_violation(input integer code_i); + begin + s_violations = s_violations + 1; + $display("SPI_P4_VIOLATION code=%0h", code_i); + end + endtask + + task automatic complete_command; + begin + $display("SPI_P4_CMD code=%02h params=%0h madctl=%02h", s_cmd_code, s_param_count, s_madctl); + s_commands = s_commands + 1; + s_cmd_active = 1'b0; + end + endtask + + task automatic close_window; + integer pairs; + integer expected; + integer aborted; + begin + pairs = s_win_bytes / 2; + if ((s_win_bytes % 2) != 0) record_violation(4); + expected = (s_win_x1 - s_win_x0 + 1) * (s_win_y1 - s_win_y0 + 1); + aborted = (pairs < expected) ? 1 : 0; + s_windows = s_windows + 1; + s_pixels = s_pixels + pairs; + $display( + "SPI_P4_WINDOW seq=%0h x0=%0h y0=%0h x1=%0h y1=%0h pixels=%0h fnv1a=%08h aborted=%0h", + s_windows, s_win_x0, s_win_y0, s_win_x1, s_win_y1, pairs, s_win_fnv, aborted); + s_pixel_mode = 1'b0; + end + endtask + + task automatic accept_byte(input logic [7:0] byte_i); + begin + if (!dc_i) begin + if (s_pixel_mode) close_window; + if (s_cmd_active) complete_command; + s_cmd_active = 1'b1; + s_cmd_code = byte_i; + s_param_count = 0; + s_param_shift = 16'h0000; + if (byte_i == 8'h11) s_seen_slpout = 1'b1; + if (byte_i == 8'h29) s_seen_dispon = 1'b1; + if (byte_i == 8'h2c) begin + if (!s_seen_slpout) record_violation(2); + // RAMWR takes no parameters; its pixel stream closes the window. + complete_command; + s_pixel_mode = 1'b1; + s_win_bytes = 0; + s_win_fnv = 32'd2166136261; + end + end else if (s_pixel_mode) begin + s_win_bytes = s_win_bytes + 1; + s_win_fnv = (s_win_fnv ^ {24'd0, byte_i}) * 32'd16777619; + s_panel_fnv = (s_panel_fnv ^ {24'd0, byte_i}) * 32'd16777619; + end else if (s_cmd_active) begin + s_param_count = s_param_count + 1; + s_param_shift = {s_param_shift[7:0], byte_i}; + if ((s_cmd_code == 8'h36) && (s_param_count == 1)) s_madctl = byte_i; + if ((s_cmd_code == 8'h2a) && (s_param_count == 2)) s_win_x0 = {16'd0, s_param_shift}; + if ((s_cmd_code == 8'h2a) && (s_param_count == 4)) s_win_x1 = {16'd0, s_param_shift}; + if ((s_cmd_code == 8'h2b) && (s_param_count == 2)) s_win_y0 = {16'd0, s_param_shift}; + if ((s_cmd_code == 8'h2b) && (s_param_count == 4)) s_win_y1 = {16'd0, s_param_shift}; + end else begin + record_violation(3); + end + end + endtask + + initial begin + s_enabled = $test$plusargs("tiny_spi_p4"); + s_cmd_active = 1'b0; + s_pixel_mode = 1'b0; + s_seen_slpout = 1'b0; + s_seen_dispon = 1'b0; + s_panel_done = 1'b0; + s_shift = 8'h00; + s_cmd_code = 8'h00; + s_madctl = 8'h00; + s_param_shift = 16'h0000; + s_win_fnv = 32'd2166136261; + s_panel_fnv = 32'd2166136261; + s_bit_count = 0; + s_param_count = 0; + s_win_bytes = 0; + s_win_x0 = 0; + s_win_y0 = 0; + s_win_x1 = 0; + s_win_y1 = 0; + s_commands = 0; + s_windows = 0; + s_pixels = 0; + s_violations = 0; + end + + always @(posedge sck_i) begin + if (s_enabled && !csn_i) begin + if (s_bit_count == 7) begin + s_bit_count = 0; + accept_byte({s_shift[6:0], mosi_i}); + s_shift = 8'h00; + end else begin + s_shift = {s_shift[6:0], mosi_i}; + s_bit_count = s_bit_count + 1; + end + end + end + + always @(posedge csn_i) begin + if (s_enabled) begin + if (s_bit_count != 0) begin + record_violation(1); + s_bit_count = 0; + end + if (s_pixel_mode) close_window; + if (s_cmd_active) complete_command; + end + end + + always @(negedge csn_i) begin + if (s_enabled) s_bit_count = 0; + end + + always @(posedge test_done_i) begin + if (s_enabled && !s_panel_done) begin + s_panel_done = 1'b1; + $display("SPI_P4_PANEL commands=%0h windows=%0h pixels=%0h fnv1a=%08h violations=%0h", + s_commands, s_windows, s_pixels, s_panel_fnv, s_violations); + end + end +endmodule diff --git a/rtl/tiny/filelist/tb.fl b/rtl/tiny/filelist/tb.fl index 35466e3e..e10fab8f 100644 --- a/rtl/tiny/filelist/tb.fl +++ b/rtl/tiny/filelist/tb.fl @@ -1,6 +1,7 @@ /dv/tiny_qspi_nor.sv /device_model/ESP_PSRAM64H.sv /dv/tiny_dvp_camera.sv +/dv/tiny_st7789_panel.sv /tests/rtl/sdio_native_model.sv /dv/retrosoc_tiny_tb.sv /dv/tiny_p10_observer.sv diff --git a/scripts/run_c_tests.py b/scripts/run_c_tests.py index 4df062c3..4ffe32b4 100644 --- a/scripts/run_c_tests.py +++ b/scripts/run_c_tests.py @@ -56,6 +56,7 @@ TEST_SOURCES = ( "rtl/managed/clusterip/rtc/sw/src/rtc.c", "app/media/src/video_player.c", "app/media/src/wav_audio.c", + "app/board/src/st7789vi_math.c", "app/benchmark/npu/npu_p6_reference.c", "tests/c/test_runtime.c", ) @@ -141,6 +142,8 @@ def main() -> int: "-I", str(root / "app/media/include"), "-I", + str(root / "app/board/include"), + "-I", str(root / "app/benchmark/npu"), "-o", str(executable), diff --git a/scripts/spi_p4.py b/scripts/spi_p4.py new file mode 100644 index 00000000..95be2a88 --- /dev/null +++ b/scripts/spi_p4.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Source-bound focused SPI-P4 Tiny display checks and panel-oracle validation.""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import atomic_write # noqa: E402 + +STAGE_GEOMETRY = { + 1: (0, 0, 319, 239), + 2: (0, 0, 319, 239), + 3: (13, 17, 45, 29), + 4: (319, 0, 319, 239), + 5: (200, 239, 260, 239), + 6: (5, 7, 45, 29), + 7: (100, 100, 116, 108), +} +BARS = [0xFFFF, 0xFFE0, 0x07FF, 0x07E0, 0xF81F, 0xF800, 0x001F, 0x0000] + + +def golden_pixel(stage: int, row: int, col: int) -> int: + if stage == 1: + return BARS[min(col // 40, 7)] + if stage in (2, 8): + red5 = min(col >> 3, 31) + green6 = min(row >> 2, 63) + blue5 = min((row + col) >> 4, 31) + return (red5 << 11) | (green6 << 5) | blue5 + if stage == 3: + return 0xFFFF if (row + col) & 1 else 0x0000 + if stage == 4: + return 0x07E0 + if stage == 5: + return 0xFFE0 + if stage == 6: + return ((col & 31) << 11) | ((row & 63) << 5) | ((row ^ col) & 31) + if stage == 7: + return 0xFFFF + raise ValueError(f"unknown stage {stage}") + + +def fnv1a(data: bytes) -> int: + value = 2166136261 + for byte in data: + value = ((value ^ byte) * 16777619) & 0xFFFFFFFF + return value + + +def window_stream(stage: int, x0: int, y0: int, x1: int, y1: int) -> bytes: + out = bytearray() + for row in range(y0, y1 + 1): + for col in range(x0, x1 + 1): + pixel = golden_pixel(stage, row, col) + out.append((pixel >> 8) & 0xFF) + out.append(pixel & 0xFF) + return bytes(out) + + +def configuration_digest(manifest: dict) -> str: + payload = json.dumps(manifest["configuration"], sort_keys=True).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def focused(output: Path, stage: str) -> int: + output.mkdir(parents=True, exist_ok=False) + command = [sys.executable, "-m", "pytest", "-q", "tests/test_spi_p4.py", + "tests/test_spi_p3.py", "tests/test_spi_register_parity.py"] + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False) + atomic_write(output / "pytest.log", result.stdout + result.stderr) + return result.returncode + + +def check_log(log: str, quick: bool = False) -> list[str]: + problems = [] + windows = [] + violations = 0 + panel = None + commands = 0 + saw_slpout = False + saw_dispon = False + for line in log.splitlines(): + if "SPI_P4_VIOLATION" in line: + violations += 1 + elif "SPI_P4_CMD " in line: + commands += 1 + fields = dict(re.findall(r"(\w+)=([0-9a-f]+)", line)) + code = int(fields["code"], 16) + if code == 0x11: + saw_slpout = True + if code == 0x29: + saw_dispon = True + elif "SPI_P4_WINDOW " in line: + fields = dict(re.findall(r"(\w+)=([0-9a-f]+)", line)) + windows.append(fields) + elif "SPI_P4_PANEL " in line: + panel = dict(re.findall(r"(\w+)=([0-9a-f]+)", line)) + if violations != 0: + problems.append(f"{violations} panel violations recorded") + if not saw_slpout or not saw_dispon: + problems.append("init ordering missing SLPOUT or DISPON") + full_geometry = (319, 239) if not quick else (39, 23) + expected = [((0, 0) + full_geometry), ((0, 0) + full_geometry), (13, 17, 45, 29), + (319, 0, 319, 239), (200, 239, 260, 239), (5, 7, 45, 29), + ((0, 0) + full_geometry), (100, 100, 116, 108)] + checksum_stage = {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 8: 7} + if len(windows) != len(expected): + problems.append(f"expected {len(expected)} windows, saw {len(windows)}") + return problems + for index, (fields, geometry) in enumerate(zip(windows, expected)): + seq = index + 1 + if int(fields["seq"], 16) != seq: + problems.append(f"window {seq}: seq mismatch {fields['seq']}") + got = tuple(int(fields[name], 16) for name in ("x0", "y0", "x1", "y1")) + if got != geometry: + problems.append(f"window {seq}: geometry {got} != {geometry}") + continue + pixels = int(fields["pixels"], 16) + aborted = int(fields["aborted"], 16) + want_pixels = (geometry[2] - geometry[0] + 1) * (geometry[3] - geometry[1] + 1) + if seq == 7: + if aborted != 1 or not (0 < pixels < want_pixels): + problems.append(f"window 7: abort semantics wrong pixels={pixels} aborted={aborted}") + continue + if aborted != 0: + problems.append(f"window {seq}: unexpected abort") + if pixels != want_pixels: + problems.append(f"window {seq}: pixels {pixels} != {want_pixels}") + continue + golden = fnv1a(window_stream(checksum_stage[seq], *geometry)) + if int(fields["fnv1a"], 16) != golden: + problems.append(f"window {seq}: pixel checksum mismatch") + if panel is None: + problems.append("missing SPI_P4_PANEL summary") + else: + if int(panel["violations"], 16) != 0: + problems.append("panel summary reports violations") + if int(panel["pixels"], 16) != sum(int(w["pixels"], 16) for w in windows): + problems.append("panel pixel total mismatch") + if commands < 10: + problems.append(f"too few commands observed: {commands}") + return problems + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("stage", choices=("lint", "rtl", "report", "check")) + parser.add_argument("--variant-root", type=Path, required=True) + parser.add_argument("--pdk", choices=("ICS55", "IHP130"), required=True) + parser.add_argument("--log", type=Path) + parser.add_argument("--quick", action="store_true") + args = parser.parse_args() + variant = args.variant_root.resolve() + variant.relative_to(ROOT / "build") + manifest = json.loads((variant / "meta/manifest.json").read_text()) + if manifest["configuration"]["SOC"] != "TINY" or manifest["configuration"]["PDK"] != args.pdk: + raise ValueError("profile/PDK mismatch") + output = variant / "meta/spi-p4" / args.stage + if args.stage == "check": + if args.log is None or not args.log.is_file(): + raise ValueError("check requires an existing --log sim.log path") + problems = check_log(args.log.read_text(errors="replace"), quick=args.quick) + status = "passed" if not problems else "failed" + else: + problems = [] + status = "passed" if focused(output, args.stage) == 0 else "failed" + output.mkdir(parents=True, exist_ok=True) + record = {"phase": "SPI-P4", "stage": args.stage, "status": status, + "problems": problems, "configuration_digest": configuration_digest(manifest), + "phase_accepted": False} + atomic_write(output / "record.json", json.dumps(record, indent=2, sort_keys=True) + "\n") + for problem in problems: + print(problem, file=sys.stderr) + return int(bool(problems)) if args.stage == "check" else (0 if status == "passed" else 1) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/c/test_runtime.c b/tests/c/test_runtime.c index 7854694f..fcd9563e 100644 --- a/tests/c/test_runtime.c +++ b/tests/c/test_runtime.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1301,6 +1302,52 @@ static int test_tiny_dma_spi_aperture(void) { return 0; } +static int test_st7789vi_patterns(void) { + rs_st7789vi_window_t window = {0U, 0U, 319U, 239U}; + uint32_t pixels; + uint32_t bytes; + uint32_t words; + + if (rs_st7789vi_pattern(RS_ST7789VI_STAGE_BARS, 0U, 0U) != UINT16_C(0xFFFF) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_BARS, 0U, 319U) != UINT16_C(0x0000) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_BARS, 100U, 80U) != UINT16_C(0x07FF)) { + return 1; + } + if (rs_st7789vi_pattern(RS_ST7789VI_STAGE_GRADIENT, 0U, 0U) != UINT16_C(0x0000) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_GRADIENT, 239U, 319U) != UINT16_C(0xFF7F)) { + return 2; + } + if (rs_st7789vi_pattern(RS_ST7789VI_STAGE_ODD_A, 3U, 4U) != UINT16_C(0xFFFF) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_ODD_A, 3U, 5U) != UINT16_C(0x0000) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_ODD_B, 200U, 100U) != UINT16_C(0x07E0) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_ROTATION, 1U, 1U) != UINT16_C(0x0820) || + rs_st7789vi_pattern(RS_ST7789VI_STAGE_FINAL, 0U, 0U) != UINT16_C(0xFFFF) || + rs_st7789vi_pattern(UINT32_C(99), 0U, 0U) != UINT16_C(0x0000)) { + return 3; + } + if (rs_st7789vi_window_cells(&window, &pixels, &bytes, &words) != RS_OK || + pixels != UINT32_C(76800) || bytes != UINT32_C(153600) || words != UINT32_C(38400)) { + return 4; + } + window = (rs_st7789vi_window_t){100U, 100U, 116U, 108U}; + if (rs_st7789vi_window_cells(&window, &pixels, &bytes, &words) != RS_OK || + pixels != UINT32_C(153) || bytes != UINT32_C(306) || words != UINT32_C(77)) { + return 5; + } + window = (rs_st7789vi_window_t){10U, 10U, 9U, 10U}; + if (rs_st7789vi_window_cells(&window, &pixels, &bytes, &words) != RS_EINVAL) { + return 6; + } + window = (rs_st7789vi_window_t){0U, 0U, 320U, 239U}; + if (rs_st7789vi_window_cells(&window, &pixels, &bytes, &words) != RS_EINVAL) { + return 7; + } + if (rs_st7789vi_window_cells(NULL, &pixels, &bytes, &words) != RS_EINVAL) { + return 8; + } + return 0; +} + rs_status_t rs_tiny_dvp_configure(const rs_dvp_config_t *config); rs_status_t rs_tiny_dvp_capture_dma(uintptr_t destination, uint32_t capacity, rs_timeout_t timeout); rs_status_t rs_tiny_dvp_capture_pio(uintptr_t destination, uint32_t capacity, uint32_t expected, @@ -3650,6 +3697,7 @@ int main(void) { test_tiny_dma_config_validation(), test_spi_divider_from_hz(), test_tiny_dma_spi_aperture(), + test_st7789vi_patterns(), test_dvp_hal(), test_p9_frame_layout(), test_p10_frame_statistics(), diff --git a/tests/test_spi_p4.py b/tests/test_spi_p4.py new file mode 100644 index 00000000..da0250fa --- /dev/null +++ b/tests/test_spi_p4.py @@ -0,0 +1,115 @@ +"""SPI-P4 display adapter, acceptance image, and panel-checker contracts.""" +from __future__ import annotations + +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.spi_p4 import check_log, fnv1a, golden_pixel, window_stream # noqa: E402 + +BANNED = ("malloc", "free(", "strcpy", "strcat", "strncpy", "sprintf", "vsprintf", "atoi") + + +def test_adapter_and_math_are_bounded() -> None: + adapter = (ROOT / "app/board/src/st7789vi.c").read_text() + math = (ROOT / "app/board/src/st7789vi_math.c").read_text() + header = (ROOT / "app/board/include/retrosoc/board/st7789vi.h").read_text() + for text in (adapter, math, header): + assert not any(token in text for token in BANNED) + assert "rs_status_t" in adapter and "rs_spi_transfer_cpu" in adapter + assert "rs_st7789vi_begin_pixels" in header and "rs_st7789vi_fill_window" in header + assert "RS_ST7789VI_WIDTH" in header and "UINT16_C(320)" in header + assert math.count("case RS_ST7789VI_STAGE_") == 8 + + +def test_image_composition_and_guard() -> None: + mk = (ROOT / "app/apps/ci_smoke/tiny.mk").read_text() + assert "tiny_spi_p4.c" in mk and "st7789vi.c" in mk and "app/board/include" in mk + top = (ROOT / "Makefile").read_text() + assert "TINY_SPI_P4_ACCEPTANCE" in top + assert "$(TINY_SPI_P4_ACCEPTANCE),YES" in top + image = (ROOT / "app/apps/ci_smoke/tiny_spi_p4.c").read_text() + assert "rs_sysctrl_write_test_status" in image + assert image.index("rs_sysctrl_write_test_status") < image.index("SPI_P4_RESULT") + assert "rs_spi_release" in image and "rs_spi_abort" in image and "rs_spi_recover" in image + assert not any(token in image for token in BANNED) + + +def _render_log(corrupt: str | None = None) -> str: + lines = [] + commands = [("01", 0), ("11", 0), ("3a", 1), ("36", 1), ("21", 0), ("13", 0), ("29", 0)] + for code, params in commands: + lines.append(f"SPI_P4_CMD code={code} params={params} madctl=00") + total_fnv = 2166136261 + total_pixels = 0 + + def add_window(seq: int, stage: int, geometry: tuple[int, int, int, int], + pixels: int | None, aborted: int) -> None: + nonlocal total_fnv, total_pixels + lines.append("SPI_P4_CMD code=2a params=4 madctl=00") + lines.append("SPI_P4_CMD code=2b params=4 madctl=00") + lines.append("SPI_P4_CMD code=2c params=0 madctl=00") + stream = window_stream(stage, *geometry) + if pixels is None: + pixels = len(stream) // 2 + stream = stream[: pixels * 2] + digest = fnv1a(stream) + for byte in stream: + total_fnv = ((total_fnv ^ byte) * 16777619) & 0xFFFFFFFF + total_pixels += pixels + lines.append( + f"SPI_P4_WINDOW seq={seq:x} x0={geometry[0]:x} y0={geometry[1]:x} " + f"x1={geometry[2]:x} y1={geometry[3]:x} pixels={pixels:x} fnv1a={digest:08x} " + f"aborted={aborted:x}" + ) + + add_window(1, 1, (0, 0, 319, 239), None, 0) + add_window(2, 2, (0, 0, 319, 239), None, 0) + add_window(3, 3, (13, 17, 45, 29), None, 0) + add_window(4, 4, (319, 0, 319, 239), None, 0) + add_window(5, 5, (200, 239, 260, 239), None, 0) + lines.append("SPI_P4_CMD code=36 params=1 madctl=60") + add_window(6, 6, (5, 7, 45, 29), None, 0) + lines.append("SPI_P4_CMD code=36 params=1 madctl=00") + add_window(7, 8, (0, 0, 319, 239), 544, 1) + add_window(8, 7, (100, 100, 116, 108), None, 0) + lines.append( + f"SPI_P4_PANEL commands={len(lines) + 1:x} windows=8 pixels={total_pixels:x} " + f"fnv1a={total_fnv:08x} violations=0" + ) + log = "\n".join(lines) + if corrupt == "checksum": + marker = log.index("fnv1a=") + len("fnv1a=") + digit = log[marker] + log = log[:marker] + ("0" if digit != "0" else "1") + log[marker + 1:] + elif corrupt == "geometry": + log = log.replace("x1=13f", "x1=140", 1) + elif corrupt == "violation": + log += "\nSPI_P4_VIOLATION code=2" + elif corrupt == "missing_window": + log = "\n".join(lines[:-2]) + return log + + +def test_panel_checker_accepts_golden_log() -> None: + assert check_log(_render_log()) == [] + + +def test_panel_checker_rejects_corruption() -> None: + for corrupt in ("checksum", "geometry", "violation", "missing_window"): + assert check_log(_render_log(corrupt)), corrupt + raw = subprocess.run( + [sys.executable, str(ROOT / "scripts/spi_p4.py"), "--help"], + capture_output=True, text=True, check=False, + ) + assert raw.returncode == 0 + + +def test_checker_golden_vectors_are_stable() -> None: + assert golden_pixel(1, 0, 0) == 0xFFFF + assert golden_pixel(2, 239, 319) == 0xFF7F + assert golden_pixel(6, 1, 1) == 0x0820 + assert fnv1a(window_stream(7, 100, 100, 116, 108)) == fnv1a(b"\xff\xff" * 153) + assert len(window_stream(1, 0, 0, 319, 239)) == 153600