mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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.
This commit is contained in:
15 files changed
+1258
-2
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,222 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <retrosoc/arch/riscv/system_base.h>
|
||||
#include <retrosoc/board/st7789vi.h>
|
||||
#include <retrosoc/core/irq.h>
|
||||
#include <retrosoc/hal/gpio.h>
|
||||
#include <retrosoc/hal/spi.h>
|
||||
#include <retrosoc/hal/sysctrl.h>
|
||||
#include <retrosoc/hal/uart.h>
|
||||
#include <retrosoc/lib/printf.h>
|
||||
|
||||
#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 (;;) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef RETROSOC_BOARD_ST7789VI_H
|
||||
#define RETROSOC_BOARD_ST7789VI_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <retrosoc/core/status.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,284 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#include <retrosoc/board/st7789vi.h>
|
||||
#include <retrosoc/hal/clint.h>
|
||||
#include <retrosoc/hal/gpio.h>
|
||||
#include <retrosoc/hal/spi.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#include <retrosoc/board/st7789vi.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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=<profile> APP=ci_smoke TINY_SPI_P4_ACCEPTANCE=YES spi-p4-lint spi-p4-rtl spi-p4-report`.
|
||||
Acceptance: `make CONFIG=<profile> 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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),
|
||||
|
||||
@@ -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())
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <retrosoc/hal/npu.h>
|
||||
#include <retrosoc/hal/psram.h>
|
||||
#include <retrosoc/hal/xpi_psram.h>
|
||||
#include <retrosoc/board/st7789vi.h>
|
||||
#include <retrosoc/hal/resource.h>
|
||||
#include <retrosoc/hal/sdram.h>
|
||||
#include <retrosoc/hal/sdio.h>
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user