Exercise write-enable, page programming, sector erase and NOR bit semantics
through the RP2040 SSI command path. Verify raw flash persistence across
QEMU invocations and IO_QSPI chip-select behavior.

Cover XIP accesses while the flash is busy, including direct HardFault
delivery, multicore lockout, and faults observed independently by core 0
and core 1.

Signed-off-by: gilles grimaud <[email protected]>
---
 tests/tcg/arm/system/meson.build              |  33 ++
 tests/tcg/arm/system/rp2040-core0-xip-fault.S | 193 ++++++++
 .../tcg/arm/system/rp2040-core0-xip-fault.ref |   2 +
 tests/tcg/arm/system/rp2040-core1-xip-fault.S | 331 +++++++++++++
 .../tcg/arm/system/rp2040-core1-xip-fault.ref |   3 +
 .../arm/system/rp2040-flash-busy-hardfault.S  | 179 ++++++++
 .../system/rp2040-flash-busy-hardfault.ref    |   2 +
 .../arm/system/rp2040-flash-busy-lockout.S    | 434 ++++++++++++++++++
 .../arm/system/rp2040-flash-busy-lockout.ref  |   4 +
 tests/tcg/arm/system/rp2040-flash-commands.S  | 405 ++++++++++++++++
 .../tcg/arm/system/rp2040-flash-commands.ref  |   6 +
 tests/tcg/arm/system/rp2040-flash-ioqspi.S    | 212 +++++++++
 tests/tcg/arm/system/rp2040-flash-ioqspi.ref  |   2 +
 .../system/rp2040-flash-persist-reader.ref    |   1 +
 tests/tcg/arm/system/rp2040-flash-persist.sh  |  34 ++
 15 files changed, 1841 insertions(+)
 create mode 100644 tests/tcg/arm/system/rp2040-core0-xip-fault.S
 create mode 100644 tests/tcg/arm/system/rp2040-core0-xip-fault.ref
 create mode 100644 tests/tcg/arm/system/rp2040-core1-xip-fault.S
 create mode 100644 tests/tcg/arm/system/rp2040-core1-xip-fault.ref
 create mode 100644 tests/tcg/arm/system/rp2040-flash-busy-hardfault.S
 create mode 100644 tests/tcg/arm/system/rp2040-flash-busy-hardfault.ref
 create mode 100644 tests/tcg/arm/system/rp2040-flash-busy-lockout.S
 create mode 100644 tests/tcg/arm/system/rp2040-flash-busy-lockout.ref
 create mode 100644 tests/tcg/arm/system/rp2040-flash-commands.S
 create mode 100644 tests/tcg/arm/system/rp2040-flash-commands.ref
 create mode 100644 tests/tcg/arm/system/rp2040-flash-ioqspi.S
 create mode 100644 tests/tcg/arm/system/rp2040-flash-ioqspi.ref
 create mode 100644 tests/tcg/arm/system/rp2040-flash-persist-reader.ref
 create mode 100755 tests/tcg/arm/system/rp2040-flash-persist.sh

diff --git a/tests/tcg/arm/system/meson.build b/tests/tcg/arm/system/meson.build
index 8e7286e390..c57bf5f0d0 100644
--- a/tests/tcg/arm/system/meson.build
+++ b/tests/tcg/arm/system/meson.build
@@ -24,6 +24,7 @@ rp2040_qemu_args = ['-M', 'raspi-pico', '-monitor', 'none',
 rp2040_uart_qemu_args = ['-M', 'raspi-pico,strict-uart-pins=off',
                          '-monitor', 'none', '-serial', 'stdio',
                          qemu_base_args]
+rp2040_flash_persist = find_program('rp2040-flash-persist.sh')
 
 # Multi arch tests
 multi_src = []
@@ -99,6 +100,38 @@ tests += {
     'qemu_args': rp2040_uart_qemu_args,
     'expected_output': 'rp2040-timer.ref',
   },
+  'rp2040-flash-commands.S': {
+    'cflags': rp2040_cflags,
+    'qemu_args': rp2040_uart_qemu_args,
+    'wrapper': [rp2040_flash_persist,
+                files('rp2040-flash-commands.ref'),
+                files('rp2040-flash-persist-reader.ref')],
+  },
+  'rp2040-flash-ioqspi.S': {
+    'cflags': rp2040_cflags,
+    'qemu_args': rp2040_uart_qemu_args,
+    'expected_output': 'rp2040-flash-ioqspi.ref',
+  },
+  'rp2040-flash-busy-hardfault.S': {
+    'cflags': rp2040_cflags,
+    'qemu_args': rp2040_uart_qemu_args,
+    'expected_output': 'rp2040-flash-busy-hardfault.ref',
+  },
+  'rp2040-flash-busy-lockout.S': {
+    'cflags': rp2040_cflags,
+    'qemu_args': rp2040_uart_qemu_args,
+    'expected_output': 'rp2040-flash-busy-lockout.ref',
+  },
+  'rp2040-core0-xip-fault.S': {
+    'cflags': rp2040_cflags,
+    'qemu_args': rp2040_uart_qemu_args,
+    'expected_output': 'rp2040-core0-xip-fault.ref',
+  },
+  'rp2040-core1-xip-fault.S': {
+    'cflags': rp2040_cflags,
+    'qemu_args': rp2040_uart_qemu_args,
+    'expected_output': 'rp2040-core1-xip-fault.ref',
+  },
   'semiconsole.c': {
     'cflags': cflags,
     'qemu_args': ['-serial', 'none', '-chardev', 'stdio,mux=on,id=stdio0',
diff --git a/tests/tcg/arm/system/rp2040-core0-xip-fault.S 
b/tests/tcg/arm/system/rp2040-core0-xip-fault.S
new file mode 100644
index 0000000000..675dd231d2
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-core0-xip-fault.S
@@ -0,0 +1,193 @@
+/*
+ * Raspberry Pi Pico core 0 XIP-busy instruction fault test.
+ *
+ * Copyright 2026 Gilles Grimaud
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+.syntax unified
+.cpu cortex-m0plus
+.thumb
+
+#define SRAM_END               0x20042000
+#define SRAM_BASE              0x20000000
+#define UART0_DR               0x40034000
+#define SCB_VTOR               0xe000ed08
+#define SSI_BASE               0x18000000
+#define SSI_CTRLR0             0x00
+#define SSI_SSIENR             0x08
+#define SSI_SER                0x10
+#define SSI_RXFLR              0x24
+#define SSI_DR0                0x60
+#define SSI_CTRLR0_SPI_TXRX    0x00070000
+#define SYS_EXIT               0x18
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026
+#define ADP_STOPPED_RUNTIME_ERROR    0x20023
+
+.section .vectors, "a", %progbits
+.global vector_table
+vector_table:
+    .word SRAM_END
+    .word reset_handler + 1
+    .word default_handler + 1
+    .word default_handler + 1
+
+.section .text, "ax", %progbits
+.thumb_func
+.global reset_handler
+reset_handler:
+    ldr r0, =start_message
+    bl xip_puts
+    ldr r0, =SRAM_BASE
+    ldr r1, =sram_start
+    ldr r2, =sram_end
+
+1:
+    cmp r1, r2
+    bhs 2f
+    ldr r3, [r1]
+    str r3, [r0]
+    adds r1, 4
+    adds r0, 4
+    b 1b
+
+2:
+    ldr r0, =SRAM_BASE
+    adds r0, 1
+    bx r0
+
+.thumb_func
+default_handler:
+    b default_handler
+
+.thumb_func
+xip_probe:
+    ldr r0, =xip_no_fault_message
+    bl xip_puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+3:
+    b 3b
+
+.thumb_func
+xip_puts:
+    ldr r2, =UART0_DR
+
+4:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 5f
+    str r1, [r2]
+    adds r0, 1
+    b 4b
+
+5:
+    bx lr
+
+.align 2
+sram_start:
+    ldr r0, =SRAM_BASE
+    ldr r1, =SRAM_END
+    str r1, [r0]
+    adr r1, sram_entry
+    adds r1, 1
+    str r1, [r0, 4]
+    movs r1, 0
+    str r1, [r0, 8]
+    adr r1, sram_hardfault
+    adds r1, 1
+    str r1, [r0, 12]
+    ldr r2, =SCB_VTOR
+    str r0, [r2]
+
+.align 2
+.thumb_func
+sram_entry:
+    ldr r4, =SSI_BASE
+    movs r0, 0
+    str r0, [r4, SSI_SSIENR]
+    ldr r0, =SSI_CTRLR0_SPI_TXRX
+    str r0, [r4, SSI_CTRLR0]
+    movs r0, 1
+    str r0, [r4, SSI_SSIENR]
+
+    str r0, [r4, SSI_SER]
+    movs r0, 0x06
+    bl sram_ssi_txrx
+    movs r0, 0
+    str r0, [r4, SSI_SER]
+
+    movs r0, 1
+    str r0, [r4, SSI_SER]
+    movs r0, 0x02
+    bl sram_ssi_txrx
+    movs r0, 0x00
+    bl sram_ssi_txrx
+    movs r0, 0x20
+    bl sram_ssi_txrx
+    movs r0, 0x00
+    bl sram_ssi_txrx
+    movs r0, 0x5a
+    bl sram_ssi_txrx
+    movs r0, 0
+    str r0, [r4, SSI_SER]
+
+    ldr r0, =xip_probe
+    adds r0, 1
+    bx r0
+
+.thumb_func
+sram_ssi_txrx:
+    ldr r2, =(SSI_BASE + SSI_DR0)
+    str r0, [r2]
+    ldr r3, =(SSI_BASE + SSI_RXFLR)
+
+6:
+    ldr r0, [r3]
+    cmp r0, 0
+    beq 6b
+    ldr r0, [r2]
+    bx lr
+
+.align 2
+.thumb_func
+sram_hardfault:
+    adr r0, sram_ok_message
+    bl sram_puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+7:
+    b 7b
+
+.thumb_func
+sram_puts:
+    ldr r2, =UART0_DR
+
+8:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 9f
+    str r1, [r2]
+    adds r0, 1
+    b 8b
+
+9:
+    bx lr
+
+.align 2
+sram_ok_message:
+    .asciz "PICO TCG CORE0 XIP HARDFAULT OK\n"
+
+.ltorg
+.align 2
+sram_end:
+
+.section .rodata, "a", %progbits
+start_message:
+    .asciz "PICO TCG CORE0 XIP START\n"
+xip_no_fault_message:
+    .asciz "PICO TCG CORE0 XIP FAIL: NO FAULT\n"
diff --git a/tests/tcg/arm/system/rp2040-core0-xip-fault.ref 
b/tests/tcg/arm/system/rp2040-core0-xip-fault.ref
new file mode 100644
index 0000000000..22232ee864
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-core0-xip-fault.ref
@@ -0,0 +1,2 @@
+PICO TCG CORE0 XIP START
+PICO TCG CORE0 XIP HARDFAULT OK
diff --git a/tests/tcg/arm/system/rp2040-core1-xip-fault.S 
b/tests/tcg/arm/system/rp2040-core1-xip-fault.S
new file mode 100644
index 0000000000..5f8a1217eb
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-core1-xip-fault.S
@@ -0,0 +1,331 @@
+/*
+ * Raspberry Pi Pico core 1 XIP-busy instruction fault test.
+ *
+ * Copyright 2026 Gilles Grimaud
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+.syntax unified
+.cpu cortex-m0plus
+.thumb
+
+#define SRAM_END               0x20042000
+#define SRAM_BASE              0x20000000
+#define CORE1_STACK            0x20041000
+#define UART0_DR               0x40034000
+#define SIO_BASE               0xd0000000
+#define SIO_CPUID              0x00
+#define SIO_FIFO_ST            0x50
+#define SIO_FIFO_WR            0x54
+#define SIO_FIFO_RD            0x58
+#define SIO_FIFO_ST_VLD        0x01
+#define SIO_FIFO_ST_RDY        0x02
+#define SCB_VTOR               0xe000ed08
+#define SSI_BASE               0x18000000
+#define SSI_CTRLR0             0x00
+#define SSI_SSIENR             0x08
+#define SSI_SER                0x10
+#define SSI_RXFLR              0x24
+#define SSI_DR0                0x60
+#define SSI_CTRLR0_SPI_TXRX    0x00070000
+#define CORE1_READY            0xc01e1001
+#define SYS_EXIT               0x18
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026
+#define ADP_STOPPED_RUNTIME_ERROR    0x20023
+
+.section .vectors, "a", %progbits
+.global vector_table
+vector_table:
+    .word SRAM_END
+    .word reset_handler + 1
+    .word default_handler + 1
+    .word default_handler + 1
+
+.section .text, "ax", %progbits
+.thumb_func
+.global reset_handler
+reset_handler:
+    ldr r0, =start_message
+    bl xip_puts
+    ldr r0, =SRAM_BASE
+    ldr r1, =sram_start
+    ldr r2, =sram_end
+
+1:
+    cmp r1, r2
+    bhs 2f
+    ldr r3, [r1]
+    str r3, [r0]
+    adds r1, 4
+    adds r0, 4
+    b 1b
+
+2:
+    ldr r0, =SRAM_BASE
+    adds r0, sram_core0_main - sram_start + 1
+    bx r0
+
+.thumb_func
+default_handler:
+    b default_handler
+
+.thumb_func
+xip_core1_loop:
+    ldr r0, =CORE1_READY
+    bl xip_fifo_push
+
+3:
+    b 3b
+
+.thumb_func
+xip_fifo_push:
+    ldr r1, =(SIO_BASE + SIO_FIFO_ST)
+
+4:
+    ldr r2, [r1]
+    movs r3, SIO_FIFO_ST_RDY
+    tst r2, r3
+    beq 4b
+    ldr r1, =(SIO_BASE + SIO_FIFO_WR)
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+xip_puts:
+    ldr r2, =UART0_DR
+
+5:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 6f
+    str r1, [r2]
+    adds r0, 1
+    b 5b
+
+6:
+    bx lr
+
+.align 2
+sram_start:
+    .word SRAM_END
+    .word SRAM_BASE + (sram_core0_main - sram_start) + 1
+    .word 0
+    .word SRAM_BASE + (sram_hardfault - sram_start) + 1
+
+.align 2
+.thumb_func
+sram_core0_main:
+    ldr r0, =SCB_VTOR
+    ldr r1, =SRAM_BASE
+    str r1, [r0]
+    bl sram_launch_core1
+    bl sram_fifo_pop
+    ldr r1, =CORE1_READY
+    cmp r0, r1
+    bne sram_fail_ready
+
+    adr r0, sram_ready_message
+    bl sram_puts
+    bl sram_start_flash_program
+
+7:
+    b 7b
+
+.thumb_func
+sram_launch_core1:
+    push {r4, r5, lr}
+    adr r4, sram_launch_sequence
+    movs r5, 6
+
+8:
+    ldr r0, [r4]
+    cmp r0, 0
+    bne 9f
+    bl sram_fifo_drain
+    sev
+    ldr r0, [r4]
+
+9:
+    bl sram_fifo_push
+    sev
+    bl sram_fifo_pop
+    ldr r1, [r4]
+    cmp r0, r1
+    bne sram_fail_launch
+    adds r4, 4
+    subs r5, 1
+    bne 8b
+    pop {r4, r5, pc}
+
+.thumb_func
+sram_fifo_push:
+    ldr r1, =(SIO_BASE + SIO_FIFO_ST)
+
+10:
+    ldr r2, [r1]
+    movs r3, SIO_FIFO_ST_RDY
+    tst r2, r3
+    beq 10b
+    ldr r1, =(SIO_BASE + SIO_FIFO_WR)
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+sram_fifo_pop:
+    ldr r1, =(SIO_BASE + SIO_FIFO_ST)
+
+11:
+    ldr r2, [r1]
+    movs r3, SIO_FIFO_ST_VLD
+    tst r2, r3
+    beq 11b
+    ldr r1, =(SIO_BASE + SIO_FIFO_RD)
+    ldr r0, [r1]
+    bx lr
+
+.thumb_func
+sram_fifo_drain:
+    push {r0, r1, r2, r3, lr}
+    ldr r1, =(SIO_BASE + SIO_FIFO_ST)
+
+12:
+    ldr r2, [r1]
+    movs r3, SIO_FIFO_ST_VLD
+    tst r2, r3
+    beq 13f
+    ldr r0, =(SIO_BASE + SIO_FIFO_RD)
+    ldr r0, [r0]
+    b 12b
+
+13:
+    pop {r0, r1, r2, r3, pc}
+
+.thumb_func
+sram_start_flash_program:
+    push {lr}
+    ldr r4, =SSI_BASE
+    movs r0, 0
+    str r0, [r4, SSI_SSIENR]
+    ldr r0, =SSI_CTRLR0_SPI_TXRX
+    str r0, [r4, SSI_CTRLR0]
+    movs r0, 1
+    str r0, [r4, SSI_SSIENR]
+
+    str r0, [r4, SSI_SER]
+    movs r0, 0x06
+    bl sram_ssi_txrx
+    movs r0, 0
+    str r0, [r4, SSI_SER]
+
+    movs r0, 1
+    str r0, [r4, SSI_SER]
+    movs r0, 0x02
+    bl sram_ssi_txrx
+    movs r0, 0x00
+    bl sram_ssi_txrx
+    movs r0, 0x30
+    bl sram_ssi_txrx
+    movs r0, 0x00
+    bl sram_ssi_txrx
+    movs r0, 0xa5
+    bl sram_ssi_txrx
+    movs r0, 0
+    str r0, [r4, SSI_SER]
+    pop {pc}
+
+.thumb_func
+sram_ssi_txrx:
+    ldr r2, =(SSI_BASE + SSI_DR0)
+    str r0, [r2]
+    ldr r3, =(SSI_BASE + SSI_RXFLR)
+
+14:
+    ldr r0, [r3]
+    cmp r0, 0
+    beq 14b
+    ldr r0, [r2]
+    bx lr
+
+.align 2
+.thumb_func
+sram_hardfault:
+    ldr r0, =SIO_BASE
+    ldr r0, [r0, SIO_CPUID]
+    cmp r0, 1
+    bne sram_fail_core
+    adr r0, sram_ok_message
+    bl sram_puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+15:
+    b 15b
+
+sram_fail_launch:
+    adr r0, sram_fail_launch_message
+    b sram_test_failed
+
+sram_fail_ready:
+    adr r0, sram_fail_ready_message
+    b sram_test_failed
+
+sram_fail_core:
+    adr r0, sram_fail_core_message
+
+sram_test_failed:
+    bl sram_puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+16:
+    b 16b
+
+.thumb_func
+sram_puts:
+    ldr r2, =UART0_DR
+
+17:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 18f
+    str r1, [r2]
+    adds r0, 1
+    b 17b
+
+18:
+    bx lr
+
+.align 2
+sram_launch_sequence:
+    .word 0
+    .word 0
+    .word 1
+    .word SRAM_BASE
+    .word CORE1_STACK
+    .word xip_core1_loop + 1
+
+.align 2
+sram_ready_message:
+    .asciz "PICO TCG CORE1 XIP READY\n"
+.align 2
+sram_ok_message:
+    .asciz "PICO TCG CORE1 XIP HARDFAULT OK\n"
+.align 2
+sram_fail_launch_message:
+    .asciz "PICO TCG CORE1 XIP FAIL: LAUNCH\n"
+.align 2
+sram_fail_ready_message:
+    .asciz "PICO TCG CORE1 XIP FAIL: READY\n"
+.align 2
+sram_fail_core_message:
+    .asciz "PICO TCG CORE1 XIP FAIL: CPUID\n"
+
+.ltorg
+.align 2
+sram_end:
+
+.section .rodata, "a", %progbits
+start_message:
+    .asciz "PICO TCG CORE1 XIP START\n"
diff --git a/tests/tcg/arm/system/rp2040-core1-xip-fault.ref 
b/tests/tcg/arm/system/rp2040-core1-xip-fault.ref
new file mode 100644
index 0000000000..4488763747
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-core1-xip-fault.ref
@@ -0,0 +1,3 @@
+PICO TCG CORE1 XIP START
+PICO TCG CORE1 XIP READY
+PICO TCG CORE1 XIP HARDFAULT OK
diff --git a/tests/tcg/arm/system/rp2040-flash-busy-hardfault.S 
b/tests/tcg/arm/system/rp2040-flash-busy-hardfault.S
new file mode 100644
index 0000000000..81bcafe260
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-busy-hardfault.S
@@ -0,0 +1,179 @@
+/*
+ * Raspberry Pi Pico XIP access while flash is busy test.
+ *
+ * Copyright 2026 Gilles Grimaud
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+.syntax unified
+.cpu cortex-m0plus
+.thumb
+
+#define SRAM_BASE               0x20000000
+#define SRAM_VTOR               0x20001000
+#define SRAM_END                0x20042000
+#define VTOR                    0xe000ed08
+#define UART0_DR                0x40034000
+#define SSI_SER                 0x18000010
+#define SSI_RXFLR               0x18000024
+#define SSI_DR0                 0x18000060
+#define XIP_TARGET              0x10001400
+#define SYS_EXIT                0x18
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026
+#define ADP_STOPPED_RUNTIME_ERROR    0x20023
+
+.section .vectors, "a", %progbits
+vector_table:
+    .word SRAM_END
+    .word reset_handler + 1
+    .word default_handler + 1
+    .word default_handler + 1
+
+.section .text, "ax", %progbits
+.thumb_func
+.global reset_handler
+reset_handler:
+    ldr r0, =SRAM_BASE
+    ldr r1, =ram_code_start
+    ldr r2, =ram_code_end
+
+copy_loop:
+    cmp r1, r2
+    bhs copy_done
+    ldr r3, [r1]
+    str r3, [r0]
+    adds r0, 4
+    adds r1, 4
+    b copy_loop
+
+copy_done:
+    ldr r0, =SRAM_BASE + 1
+    bx r0
+
+.thumb_func
+default_handler:
+    b default_handler
+
+.align 2
+ram_code_start:
+.thumb_func
+ram_main:
+    ldr r0, =SRAM_VTOR
+    ldr r1, =SRAM_END
+    str r1, [r0]
+    ldr r1, =SRAM_BASE + 1
+    str r1, [r0, 4]
+    movs r1, 0
+    str r1, [r0, 8]
+    adr r1, hardfault_handler
+    adds r1, 1
+    str r1, [r0, 12]
+    ldr r1, =VTOR
+    str r0, [r1]
+
+    adr r0, start_message
+    bl puts
+    bl write_enable
+    bl select_flash
+    movs r0, 0x02
+    bl ssi_byte
+    movs r0, 0x00
+    bl ssi_byte
+    movs r0, 0x14
+    bl ssi_byte
+    movs r0, 0x00
+    bl ssi_byte
+    movs r0, 'X'
+    bl ssi_byte
+    bl deselect_flash
+
+    ldr r1, =XIP_TARGET
+faulting_access:
+    ldrb r0, [r1]
+    adr r0, fail_message
+    b test_failed
+
+.thumb_func
+write_enable:
+    push {lr}
+    bl select_flash
+    movs r0, 0x06
+    bl ssi_byte
+    bl deselect_flash
+    pop {pc}
+
+.thumb_func
+select_flash:
+    ldr r1, =SSI_SER
+    movs r0, 1
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+deselect_flash:
+    ldr r1, =SSI_SER
+    movs r0, 0
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+ssi_byte:
+    ldr r2, =SSI_DR0
+    str r0, [r2]
+    ldr r3, =SSI_RXFLR
+
+1:
+    ldr r0, [r3]
+    cmp r0, 0
+    beq 1b
+    ldr r0, [r2]
+    bx lr
+
+.align 2
+.thumb_func
+hardfault_handler:
+    adr r0, ok_message
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+2:
+    b 2b
+
+test_failed:
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+3:
+    b 3b
+
+.thumb_func
+puts:
+    ldr r2, =UART0_DR
+
+4:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 5f
+    str r1, [r2]
+    adds r0, 1
+    b 4b
+
+5:
+    bx lr
+
+.align 2
+start_message:
+    .asciz "PICO TCG FLASH XIP BUSY START\n"
+.align 2
+ok_message:
+    .asciz "PICO TCG FLASH XIP BUSY HARDFAULT OK\n"
+.align 2
+fail_message:
+    .asciz "PICO TCG FLASH XIP BUSY FAIL: NO HARDFAULT\n"
+.align 2
+.ltorg
+ram_code_end:
diff --git a/tests/tcg/arm/system/rp2040-flash-busy-hardfault.ref 
b/tests/tcg/arm/system/rp2040-flash-busy-hardfault.ref
new file mode 100644
index 0000000000..ee43e3ae15
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-busy-hardfault.ref
@@ -0,0 +1,2 @@
+PICO TCG FLASH XIP BUSY START
+PICO TCG FLASH XIP BUSY HARDFAULT OK
diff --git a/tests/tcg/arm/system/rp2040-flash-busy-lockout.S 
b/tests/tcg/arm/system/rp2040-flash-busy-lockout.S
new file mode 100644
index 0000000000..a0397d7a1d
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-busy-lockout.S
@@ -0,0 +1,434 @@
+/*
+ * Raspberry Pi Pico multicore flash-busy lockout test.
+ *
+ * Copyright 2026 Gilles Grimaud
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+.syntax unified
+.cpu cortex-m0plus
+.thumb
+
+#define SRAM_END               0x20042000
+#define CORE0_SRAM_VTOR        0x20000000
+#define CORE0_SRAM_CODE        0x20000100
+#define PAGE_BUFFER            0x20000400
+#define CORE1_SRAM_VTOR        0x20001000
+#define CORE1_STACK            0x20041000
+#define UART0_DR               0x40034000
+#define SIO_BASE               0xd0000000
+#define SIO_FIFO_ST            0x50
+#define SIO_FIFO_WR            0x54
+#define SIO_FIFO_RD            0x58
+#define SIO_SPINLOCK0          0x100
+#define SIO_FIFO_ST_VLD        0x01
+#define SIO_FIFO_ST_RDY        0x02
+#define NVIC_ISER              0xe000e100
+#define SCB_VTOR               0xe000ed08
+#define SIO_IRQ_PROC1_MASK     0x00010000
+#define SSI_BASE               0x18000000
+#define SSI_SSIENR             0x08
+#define SSI_SER                0x10
+#define SSI_DR0                0x60
+#define FLASH_ADDRESS          0x10101000
+#define LOCK_REQUEST           0xfacefeed
+#define LOCK_ACK               0xface0001
+#define BAD_ACK                0xbad10003
+#define SYS_EXIT               0x18
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026
+#define ADP_STOPPED_RUNTIME_ERROR    0x20023
+
+.section .vectors, "a", %progbits
+.global vector_table
+vector_table:
+    .word SRAM_END
+    .word reset_handler + 1
+    .word default_handler + 1
+    .word default_handler + 1
+
+.section .text, "ax", %progbits
+.thumb_func
+.global reset_handler
+reset_handler:
+    ldr r0, =start_message
+    bl puts
+    bl copy_core1_sram
+    bl launch_core1
+
+    ldr r0, =launch_message
+    bl puts
+    bl claim_spinlock
+    ldr r0, =LOCK_REQUEST
+    bl fifo_push
+    sev
+    ldr r0, =LOCK_ACK
+    bl fifo_pop_expect
+
+    ldr r0, =locked_message
+    bl puts
+    bl copy_and_run_core0_sram
+    ldr r0, =fail_return_message
+    b test_failed
+
+.thumb_func
+launch_core1:
+    push {r4, r5, lr}
+    ldr r4, =launch_sequence
+    movs r5, 6
+
+1:
+    ldr r0, [r4]
+    cmp r0, 0
+    bne 2f
+    bl fifo_drain
+    sev
+    ldr r0, [r4]
+
+2:
+    bl fifo_push
+    sev
+    bl fifo_pop
+    ldr r1, [r4]
+    cmp r0, r1
+    bne fail_launch
+    adds r4, 4
+    subs r5, 1
+    bne 1b
+    pop {r4, r5, pc}
+
+.thumb_func
+copy_core1_sram:
+    ldr r0, =core1_sram_start
+    ldr r1, =CORE1_SRAM_VTOR
+    ldr r2, =core1_sram_end
+
+3:
+    cmp r0, r2
+    bhs 4f
+    ldr r3, [r0]
+    str r3, [r1]
+    adds r0, 4
+    adds r1, 4
+    b 3b
+
+4:
+    bx lr
+
+.thumb_func
+claim_spinlock:
+    ldr r0, =(SIO_BASE + SIO_SPINLOCK0)
+    ldr r1, [r0]
+    cmp r1, 0
+    beq fail_spinlock
+    bx lr
+
+.thumb_func
+copy_and_run_core0_sram:
+    ldr r0, =core0_sram_start
+    ldr r1, =CORE0_SRAM_CODE
+    ldr r2, =core0_sram_end
+
+5:
+    cmp r0, r2
+    bhs 6f
+    ldr r3, [r0]
+    str r3, [r1]
+    adds r0, 4
+    adds r1, 4
+    b 5b
+
+6:
+    ldr r0, =CORE0_SRAM_CODE
+    adds r0, 1
+    bx r0
+
+.thumb_func
+fifo_push:
+    push {r1, r2, r3, lr}
+    ldr r1, =SIO_BASE
+
+7:
+    ldr r2, [r1, SIO_FIFO_ST]
+    movs r3, SIO_FIFO_ST_RDY
+    tst r2, r3
+    beq 7b
+    str r0, [r1, SIO_FIFO_WR]
+    pop {r1, r2, r3, pc}
+
+.thumb_func
+fifo_pop:
+    push {r1, r2, r3, lr}
+    ldr r1, =SIO_BASE
+
+8:
+    ldr r2, [r1, SIO_FIFO_ST]
+    movs r3, SIO_FIFO_ST_VLD
+    tst r2, r3
+    beq 8b
+    ldr r0, [r1, SIO_FIFO_RD]
+    pop {r1, r2, r3, pc}
+
+.thumb_func
+fifo_drain:
+    push {r0, r1, r2, r3, lr}
+    ldr r1, =SIO_BASE
+
+9:
+    ldr r2, [r1, SIO_FIFO_ST]
+    movs r3, SIO_FIFO_ST_VLD
+    tst r2, r3
+    beq 10f
+    ldr r0, [r1, SIO_FIFO_RD]
+    b 9b
+
+10:
+    pop {r0, r1, r2, r3, pc}
+
+.thumb_func
+fifo_pop_expect:
+    push {r4, lr}
+    mov r4, r0
+    bl fifo_pop
+    cmp r0, r4
+    bne fail_fifo
+    pop {r4, pc}
+
+fail_launch:
+    ldr r0, =fail_launch_message
+    b test_failed
+
+fail_spinlock:
+    ldr r0, =fail_spinlock_message
+    b test_failed
+
+fail_fifo:
+    ldr r0, =fail_fifo_message
+    b test_failed
+
+test_failed:
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+11:
+    b 11b
+
+.thumb_func
+default_handler:
+    ldr r0, =fail_exception_message
+    b test_failed
+
+.thumb_func
+puts:
+    push {r1, r2, lr}
+    ldr r1, =UART0_DR
+
+12:
+    ldrb r2, [r0]
+    cmp r2, 0
+    beq 13f
+    str r2, [r1]
+    adds r0, 1
+    b 12b
+
+13:
+    pop {r1, r2, pc}
+
+.align 2
+core0_sram_start:
+    ldr r0, =CORE0_SRAM_VTOR
+    ldr r1, =SRAM_END
+    str r1, [r0]
+    movs r1, 0
+    str r1, [r0, 4]
+    ldr r1, =core0_sram_hardfault
+    ldr r2, =core0_sram_start
+    subs r1, r2
+    ldr r2, =CORE0_SRAM_CODE
+    adds r1, r1, r2
+    str r1, [r0, 12]
+    ldr r0, =SCB_VTOR
+    ldr r1, =CORE0_SRAM_VTOR
+    str r1, [r0]
+
+    ldr r0, =PAGE_BUFFER
+    movs r1, 0x5a
+    str r1, [r0]
+
+    ldr r4, =SSI_BASE
+    movs r0, 1
+    str r0, [r4, SSI_SSIENR]
+    str r0, [r4, SSI_SER]
+    movs r0, 0x06
+    str r0, [r4, SSI_DR0]
+    movs r0, 0
+    str r0, [r4, SSI_SER]
+
+    movs r0, 1
+    str r0, [r4, SSI_SER]
+    movs r0, 0x02
+    str r0, [r4, SSI_DR0]
+    movs r0, 0x10
+    str r0, [r4, SSI_DR0]
+    str r0, [r4, SSI_DR0]
+    movs r0, 0
+    str r0, [r4, SSI_DR0]
+    movs r0, 0x5a
+    str r0, [r4, SSI_DR0]
+    movs r0, 0
+    str r0, [r4, SSI_SER]
+
+    ldr r0, =FLASH_ADDRESS
+    ldrb r0, [r0]
+    ldr r0, =core0_sram_no_fault_message
+    ldr r1, =core0_sram_start
+    subs r0, r1
+    ldr r1, =CORE0_SRAM_CODE
+    adds r0, r1
+    bl core0_sram_puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+14:
+    b 14b
+
+.thumb_func
+core0_sram_hardfault:
+    ldr r0, =core0_sram_ok_message
+    ldr r1, =core0_sram_start
+    subs r0, r1
+    ldr r1, =CORE0_SRAM_CODE
+    adds r0, r1
+    bl core0_sram_puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+15:
+    b 15b
+
+.thumb_func
+core0_sram_puts:
+    ldr r1, =UART0_DR
+
+16:
+    ldrb r2, [r0]
+    cmp r2, 0
+    beq 17f
+    str r2, [r1]
+    adds r0, 1
+    b 16b
+
+17:
+    bx lr
+
+.align 2
+core0_sram_ok_message:
+    .asciz "PICO TCG FLASH BUSY LOCKOUT OK\n"
+core0_sram_no_fault_message:
+    .asciz "PICO TCG FLASH BUSY LOCKOUT FAIL: NO FAULT\n"
+
+.ltorg
+.align 2
+core0_sram_end:
+
+core1_sram_start:
+    .word CORE1_STACK
+    .word CORE1_SRAM_VTOR + (core1_sram_entry - core1_sram_start) + 1
+    .word 0
+    .word 0
+    .space 0x80 - 16
+    .word CORE1_SRAM_VTOR + (core1_sram_fifo_irq - core1_sram_start) + 1
+    .space 0x100 - 0x84
+
+.thumb_func
+core1_sram_entry:
+    ldr r0, =SCB_VTOR
+    ldr r1, =CORE1_SRAM_VTOR
+    str r1, [r0]
+    ldr r0, =NVIC_ISER
+    ldr r1, =SIO_IRQ_PROC1_MASK
+    str r1, [r0]
+    cpsie i
+
+18:
+    wfe
+    b 18b
+
+.thumb_func
+core1_sram_fifo_irq:
+    push {r0, r1, r2, r3, lr}
+    bl core1_sram_fifo_pop
+    ldr r1, =LOCK_REQUEST
+    cmp r0, r1
+    bne core1_sram_bad_request
+    ldr r0, =LOCK_ACK
+    bl core1_sram_fifo_push
+
+19:
+    wfe
+    b 19b
+
+core1_sram_bad_request:
+    ldr r0, =BAD_ACK
+    bl core1_sram_fifo_push
+
+20:
+    b 20b
+
+.thumb_func
+core1_sram_fifo_push:
+    ldr r1, =SIO_BASE
+
+21:
+    ldr r2, [r1, SIO_FIFO_ST]
+    movs r3, SIO_FIFO_ST_RDY
+    tst r2, r3
+    beq 21b
+    str r0, [r1, SIO_FIFO_WR]
+    bx lr
+
+.thumb_func
+core1_sram_fifo_pop:
+    ldr r1, =SIO_BASE
+
+22:
+    ldr r2, [r1, SIO_FIFO_ST]
+    movs r3, SIO_FIFO_ST_VLD
+    tst r2, r3
+    beq 22b
+    ldr r0, [r1, SIO_FIFO_RD]
+    bx lr
+
+.ltorg
+.align 2
+core1_sram_end:
+
+.section .rodata, "a", %progbits
+.align 2
+launch_sequence:
+    .word 0
+    .word 0
+    .word 1
+    .word CORE1_SRAM_VTOR
+    .word CORE1_STACK
+    .word CORE1_SRAM_VTOR + (core1_sram_entry - core1_sram_start) + 1
+
+start_message:
+    .asciz "PICO TCG FLASH BUSY START\n"
+launch_message:
+    .asciz "PICO TCG FLASH BUSY CORE1 SRAM OK\n"
+locked_message:
+    .asciz "PICO TCG FLASH BUSY LOCKED\n"
+fail_launch_message:
+    .asciz "PICO TCG FLASH BUSY FAIL: LAUNCH\n"
+fail_spinlock_message:
+    .asciz "PICO TCG FLASH BUSY FAIL: SPINLOCK\n"
+fail_fifo_message:
+    .asciz "PICO TCG FLASH BUSY FAIL: FIFO\n"
+fail_return_message:
+    .asciz "PICO TCG FLASH BUSY FAIL: RETURN\n"
+fail_exception_message:
+    .asciz "PICO TCG FLASH BUSY FAIL: EXCEPTION\n"
diff --git a/tests/tcg/arm/system/rp2040-flash-busy-lockout.ref 
b/tests/tcg/arm/system/rp2040-flash-busy-lockout.ref
new file mode 100644
index 0000000000..53b85daabd
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-busy-lockout.ref
@@ -0,0 +1,4 @@
+PICO TCG FLASH BUSY START
+PICO TCG FLASH BUSY CORE1 SRAM OK
+PICO TCG FLASH BUSY LOCKED
+PICO TCG FLASH BUSY LOCKOUT OK
diff --git a/tests/tcg/arm/system/rp2040-flash-commands.S 
b/tests/tcg/arm/system/rp2040-flash-commands.S
new file mode 100644
index 0000000000..a3440fe889
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-commands.S
@@ -0,0 +1,405 @@
+/*
+ * Raspberry Pi Pico flash command and NOR semantics test.
+ *
+ * Copyright 2026 Gilles Grimaud
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+.syntax unified
+.cpu cortex-m0plus
+.thumb
+
+#define SRAM_BASE               0x20000000
+#define SRAM_END                0x20042000
+#define UART0_DR                0x40034000
+#define SSI_SSIENR              0x18000008
+#define SSI_SER                 0x18000010
+#define SSI_RXFLR               0x18000024
+#define SSI_DR0                 0x18000060
+#define XIP_TARGET              0x10001000
+#define FLASH_TARGET_OFFSET     0x00001000
+#define FLASH_STATUS_WIP        0x01
+#define FLASH_STATUS_WEL        0x02
+#define SYS_EXIT                0x18
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026
+#define ADP_STOPPED_RUNTIME_ERROR    0x20023
+
+.section .vectors, "a", %progbits
+.global vector_table
+vector_table:
+    .word SRAM_END
+    .word reset_handler + 1
+    .word default_handler + 1
+    .word default_handler + 1
+
+.section .text, "ax", %progbits
+.thumb_func
+.global reset_handler
+reset_handler:
+    ldr r0, =SRAM_BASE
+    ldr r1, =ram_code_start
+    ldr r2, =ram_code_end
+
+copy_loop:
+    cmp r1, r2
+    bhs copy_done
+    ldr r3, [r1]
+    str r3, [r0]
+    adds r0, 4
+    adds r1, 4
+    b copy_loop
+
+copy_done:
+    ldr r0, =SRAM_BASE + 1
+    bx r0
+
+.thumb_func
+default_handler:
+    b default_handler
+
+.align 2
+ram_code_start:
+.thumb_func
+ram_main:
+    ldr r0, =SSI_SSIENR
+    movs r1, 0
+    str r1, [r0]
+    movs r1, 1
+    str r1, [r0]
+
+    ldr r0, =XIP_TARGET
+    ldr r1, =flash_message
+    movs r2, 5
+
+check_persisted_loop:
+    ldrb r3, [r0]
+    ldrb r4, [r1]
+    cmp r3, r4
+    bne run_command_tests
+    adds r0, 1
+    adds r1, 1
+    subs r2, 1
+    bne check_persisted_loop
+
+    ldr r0, =persist_ok_message
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+run_command_tests:
+    ldr r0, =start_message
+    bl puts
+
+    bl write_enable
+    bl expect_wel
+    ldr r0, =FLASH_TARGET_OFFSET
+    bl erase_sector
+    bl expect_busy_wel_clear
+    bl expect_target_erased
+
+    bl write_enable
+    bl expect_wel
+    ldr r0, =FLASH_TARGET_OFFSET
+    ldr r1, =flash_message
+    movs r2, 5
+    bl program_data
+    bl expect_busy_wel_clear
+    bl expect_flash_message
+    ldr r0, =program_ok_message
+    bl puts
+
+    bl write_enable
+    ldr r0, =FLASH_TARGET_OFFSET
+    bl erase_sector
+    bl expect_busy_wel_clear
+    bl expect_target_erased
+    ldr r0, =erase_ok_message
+    bl puts
+
+    bl write_enable
+    ldr r0, =FLASH_TARGET_OFFSET
+    ldr r1, =flash_message
+    movs r2, 5
+    bl program_data
+    bl expect_busy_wel_clear
+    bl expect_flash_message
+
+    ldr r0, =FLASH_TARGET_OFFSET + 8
+    ldr r1, =no_wel_value
+    movs r2, 1
+    bl program_data
+    bl read_status
+    movs r1, FLASH_STATUS_WIP | FLASH_STATUS_WEL
+    tst r0, r1
+    beq 8f
+    b fail_no_wel
+8:
+    ldr r1, =XIP_TARGET + 8
+    ldrb r0, [r1]
+    cmp r0, 0xff
+    beq 9f
+    b fail_no_wel
+9:
+    ldr r0, =no_wel_ok_message
+    bl puts
+
+    bl write_enable
+    ldr r0, =FLASH_TARGET_OFFSET + 16
+    ldr r1, =zero_value
+    movs r2, 1
+    bl program_data
+    bl expect_busy_wel_clear
+    bl write_enable
+    ldr r0, =FLASH_TARGET_OFFSET + 16
+    ldr r1, =erased_value
+    movs r2, 1
+    bl program_data
+    bl expect_busy_wel_clear
+    ldr r1, =XIP_TARGET + 16
+    ldrb r0, [r1]
+    cmp r0, 0
+    beq 10f
+    b fail_old_and_new
+10:
+    ldr r0, =old_and_new_ok_message
+    bl puts
+
+    ldr r0, =ok_message
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+1:
+    b 1b
+
+.thumb_func
+write_enable:
+    push {lr}
+    bl select_flash
+    movs r0, 0x06
+    bl ssi_byte
+    bl deselect_flash
+    pop {pc}
+
+.thumb_func
+read_status:
+    push {r4, lr}
+    bl select_flash
+    movs r0, 0x05
+    bl ssi_byte
+    mov r4, r0
+    bl deselect_flash
+    mov r0, r4
+    pop {r4, pc}
+
+.thumb_func
+expect_wel:
+    push {lr}
+    bl read_status
+    movs r1, FLASH_STATUS_WEL
+    tst r0, r1
+    beq fail_wel
+    pop {pc}
+
+.thumb_func
+expect_busy_wel_clear:
+    push {lr}
+    bl read_status
+    movs r1, FLASH_STATUS_WIP
+    tst r0, r1
+    beq fail_status
+    movs r1, FLASH_STATUS_WEL
+    tst r0, r1
+    bne fail_status
+    pop {pc}
+
+.thumb_func
+erase_sector:
+    push {r4, lr}
+    mov r4, r0
+    bl select_flash
+    movs r0, 0x20
+    bl ssi_byte
+    lsrs r0, r4, 16
+    bl ssi_byte
+    lsrs r0, r4, 8
+    bl ssi_byte
+    mov r0, r4
+    bl ssi_byte
+    bl deselect_flash
+    pop {r4, pc}
+
+.thumb_func
+program_data:
+    push {r4, r5, r6, lr}
+    mov r4, r0
+    mov r5, r1
+    mov r6, r2
+    bl select_flash
+    movs r0, 0x02
+    bl ssi_byte
+    lsrs r0, r4, 16
+    bl ssi_byte
+    lsrs r0, r4, 8
+    bl ssi_byte
+    mov r0, r4
+    bl ssi_byte
+
+program_loop:
+    cmp r6, 0
+    beq program_done
+    ldrb r0, [r5]
+    bl ssi_byte
+    adds r5, 1
+    subs r6, 1
+    b program_loop
+
+program_done:
+    bl deselect_flash
+    pop {r4, r5, r6, pc}
+
+.thumb_func
+select_flash:
+    ldr r1, =SSI_SER
+    movs r0, 1
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+deselect_flash:
+    ldr r1, =SSI_SER
+    movs r0, 0
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+ssi_byte:
+    ldr r2, =SSI_DR0
+    str r0, [r2]
+    ldr r3, =SSI_RXFLR
+
+2:
+    ldr r0, [r3]
+    cmp r0, 0
+    beq 2b
+    ldr r0, [r2]
+    bx lr
+
+.thumb_func
+expect_target_erased:
+    ldr r1, =XIP_TARGET
+    movs r2, 20
+
+3:
+    ldrb r0, [r1]
+    cmp r0, 0xff
+    bne fail_erase
+    adds r1, 1
+    subs r2, 1
+    bne 3b
+    bx lr
+
+.thumb_func
+expect_flash_message:
+    ldr r0, =XIP_TARGET
+    ldr r1, =flash_message
+    movs r2, 5
+
+4:
+    ldrb r3, [r0]
+    ldrb r4, [r1]
+    cmp r3, r4
+    bne fail_program
+    adds r0, 1
+    adds r1, 1
+    subs r2, 1
+    bne 4b
+    bx lr
+
+fail_wel:
+    ldr r0, =fail_wel_message
+    b test_failed
+
+fail_status:
+    ldr r0, =fail_status_message
+    b test_failed
+
+fail_program:
+    ldr r0, =fail_program_message
+    b test_failed
+
+fail_erase:
+    ldr r0, =fail_erase_message
+    b test_failed
+
+fail_no_wel:
+    ldr r0, =fail_no_wel_message
+    b test_failed
+
+fail_old_and_new:
+    ldr r0, =fail_old_and_new_message
+
+test_failed:
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+5:
+    b 5b
+
+.thumb_func
+puts:
+    ldr r2, =UART0_DR
+
+6:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 7f
+    str r1, [r2]
+    adds r0, 1
+    b 6b
+
+7:
+    bx lr
+
+.align 2
+flash_message:
+    .ascii "FLASH"
+no_wel_value:
+    .byte 0xa5
+zero_value:
+    .byte 0x00
+erased_value:
+    .byte 0xff
+start_message:
+    .asciz "PICO TCG FLASH COMMANDS START\n"
+program_ok_message:
+    .asciz "PICO TCG FLASH PROGRAM OK\n"
+erase_ok_message:
+    .asciz "PICO TCG FLASH ERASE OK\n"
+no_wel_ok_message:
+    .asciz "PICO TCG FLASH NO WEL OK\n"
+old_and_new_ok_message:
+    .asciz "PICO TCG FLASH OLD AND NEW OK\n"
+ok_message:
+    .asciz "PICO TCG FLASH COMMANDS OK\n"
+persist_ok_message:
+    .asciz "PICO TCG FLASH PERSIST OK\n"
+fail_wel_message:
+    .asciz "PICO TCG FLASH COMMANDS FAIL: WEL\n"
+fail_status_message:
+    .asciz "PICO TCG FLASH COMMANDS FAIL: STATUS\n"
+fail_program_message:
+    .asciz "PICO TCG FLASH COMMANDS FAIL: PROGRAM\n"
+fail_erase_message:
+    .asciz "PICO TCG FLASH COMMANDS FAIL: ERASE\n"
+fail_no_wel_message:
+    .asciz "PICO TCG FLASH COMMANDS FAIL: NO WEL\n"
+fail_old_and_new_message:
+    .asciz "PICO TCG FLASH COMMANDS FAIL: OLD AND NEW\n"
+.align 2
+.ltorg
+ram_code_end:
diff --git a/tests/tcg/arm/system/rp2040-flash-commands.ref 
b/tests/tcg/arm/system/rp2040-flash-commands.ref
new file mode 100644
index 0000000000..ac90dcb404
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-commands.ref
@@ -0,0 +1,6 @@
+PICO TCG FLASH COMMANDS START
+PICO TCG FLASH PROGRAM OK
+PICO TCG FLASH ERASE OK
+PICO TCG FLASH NO WEL OK
+PICO TCG FLASH OLD AND NEW OK
+PICO TCG FLASH COMMANDS OK
diff --git a/tests/tcg/arm/system/rp2040-flash-ioqspi.S 
b/tests/tcg/arm/system/rp2040-flash-ioqspi.S
new file mode 100644
index 0000000000..081c643529
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-ioqspi.S
@@ -0,0 +1,212 @@
+/*
+ * Raspberry Pi Pico IO_QSPI chip-select flash command test.
+ *
+ * Copyright 2026 Gilles Grimaud
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+.syntax unified
+.cpu cortex-m0plus
+.thumb
+
+#define SRAM_BASE               0x20000000
+#define SRAM_END                0x20042000
+#define UART0_DR                0x40034000
+#define SSI_CTRLR0              0x18000000
+#define SSI_SSIENR              0x18000008
+#define SSI_RXFLR               0x18000024
+#define SSI_DR0                 0x18000060
+#define IO_QSPI_SS_CTRL         0x4001800c
+#define XIP_TARGET              0x10001200
+#define FLASH_TARGET_OFFSET     0x00001200
+#define SYS_EXIT                0x18
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026
+#define ADP_STOPPED_RUNTIME_ERROR    0x20023
+
+.section .vectors, "a", %progbits
+vector_table:
+    .word SRAM_END
+    .word reset_handler + 1
+    .word default_handler + 1
+    .word default_handler + 1
+
+.section .text, "ax", %progbits
+.thumb_func
+.global reset_handler
+reset_handler:
+    ldr r0, =SRAM_BASE
+    ldr r1, =ram_code_start
+    ldr r2, =ram_code_end
+
+copy_loop:
+    cmp r1, r2
+    bhs copy_done
+    ldr r3, [r1]
+    str r3, [r0]
+    adds r0, 4
+    adds r1, 4
+    b copy_loop
+
+copy_done:
+    ldr r0, =SRAM_BASE + 1
+    bx r0
+
+.thumb_func
+default_handler:
+    b default_handler
+
+.align 2
+ram_code_start:
+.thumb_func
+ram_main:
+    ldr r0, =start_message
+    bl puts
+    ldr r1, =SSI_SSIENR
+    movs r0, 0
+    str r0, [r1]
+    ldr r1, =SSI_CTRLR0
+    ldr r0, =0x00070000
+    str r0, [r1]
+    ldr r1, =SSI_SSIENR
+    movs r0, 1
+    str r0, [r1]
+
+    bl write_enable
+    bl cs_low
+    movs r0, 0x20
+    bl ssi_byte
+    movs r0, 0x00
+    bl ssi_byte
+    movs r0, 0x12
+    bl ssi_byte
+    movs r0, 0x00
+    bl ssi_byte
+    bl cs_high
+    bl read_status
+
+    bl write_enable
+    bl cs_low
+    movs r0, 0x02
+    bl ssi_byte
+    movs r0, 0x00
+    bl ssi_byte
+    movs r0, 0x12
+    bl ssi_byte
+    movs r0, 0x00
+    bl ssi_byte
+    movs r0, 'Q'
+    bl ssi_byte
+    movs r0, 'S'
+    bl ssi_byte
+    movs r0, 'P'
+    bl ssi_byte
+    movs r0, 'I'
+    bl ssi_byte
+    bl cs_high
+    bl read_status
+
+    ldr r1, =XIP_TARGET
+    ldrb r0, [r1]
+    cmp r0, 'Q'
+    bne test_failed
+    ldrb r0, [r1, 1]
+    cmp r0, 'S'
+    bne test_failed
+    ldrb r0, [r1, 2]
+    cmp r0, 'P'
+    bne test_failed
+    ldrb r0, [r1, 3]
+    cmp r0, 'I'
+    bne test_failed
+
+    ldr r0, =ok_message
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_APPLICATION_EXIT
+    bkpt 0xab
+
+1:
+    b 1b
+
+.thumb_func
+write_enable:
+    push {lr}
+    bl cs_low
+    movs r0, 0x06
+    bl ssi_byte
+    bl cs_high
+    pop {pc}
+
+.thumb_func
+read_status:
+    push {lr}
+    bl cs_low
+    movs r0, 0x05
+    bl ssi_byte
+    bl cs_high
+    pop {pc}
+
+.thumb_func
+ssi_byte:
+    ldr r2, =SSI_DR0
+    str r0, [r2]
+    ldr r3, =SSI_RXFLR
+
+2:
+    ldr r0, [r3]
+    cmp r0, 0
+    beq 2b
+    ldr r0, [r2]
+    bx lr
+
+.thumb_func
+cs_low:
+    ldr r1, =IO_QSPI_SS_CTRL
+    movs r0, 2
+    lsls r0, r0, 8
+    str r0, [r1]
+    bx lr
+
+.thumb_func
+cs_high:
+    ldr r1, =IO_QSPI_SS_CTRL
+    movs r0, 3
+    lsls r0, r0, 8
+    str r0, [r1]
+    bx lr
+
+test_failed:
+    ldr r0, =fail_message
+    bl puts
+    movs r0, SYS_EXIT
+    ldr r1, =ADP_STOPPED_RUNTIME_ERROR
+    bkpt 0xab
+
+3:
+    b 3b
+
+.thumb_func
+puts:
+    ldr r2, =UART0_DR
+
+4:
+    ldrb r1, [r0]
+    cmp r1, 0
+    beq 5f
+    str r1, [r2]
+    adds r0, 1
+    b 4b
+
+5:
+    bx lr
+
+.align 2
+start_message:
+    .asciz "PICO TCG IO_QSPI FLASH START\n"
+ok_message:
+    .asciz "PICO TCG IO_QSPI FLASH OK\n"
+fail_message:
+    .asciz "PICO TCG IO_QSPI FLASH FAIL\n"
+.align 2
+.ltorg
+ram_code_end:
diff --git a/tests/tcg/arm/system/rp2040-flash-ioqspi.ref 
b/tests/tcg/arm/system/rp2040-flash-ioqspi.ref
new file mode 100644
index 0000000000..8261199083
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-ioqspi.ref
@@ -0,0 +1,2 @@
+PICO TCG IO_QSPI FLASH START
+PICO TCG IO_QSPI FLASH OK
diff --git a/tests/tcg/arm/system/rp2040-flash-persist-reader.ref 
b/tests/tcg/arm/system/rp2040-flash-persist-reader.ref
new file mode 100644
index 0000000000..0ac066b0cf
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-persist-reader.ref
@@ -0,0 +1 @@
+PICO TCG FLASH PERSIST OK
diff --git a/tests/tcg/arm/system/rp2040-flash-persist.sh 
b/tests/tcg/arm/system/rp2040-flash-persist.sh
new file mode 100755
index 0000000000..7119ed68de
--- /dev/null
+++ b/tests/tcg/arm/system/rp2040-flash-persist.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+
+expected=$1
+persist_expected=$2
+shift 2
+
+qemu=$1
+guest=
+for arg do
+    guest=$arg
+done
+
+tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/rp2040-flash-persist.XXXXXX")
+trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
+
+"$@" > "$tmpdir/commands.out"
+diff -u "$expected" "$tmpdir/commands.out"
+
+dd if=/dev/zero bs=512 count=1 2>/dev/null |
+    tr '\000' '\377' > "$tmpdir/flash.bin"
+
+"$qemu" -display none -monitor none \
+    -M "raspi-pico,strict-uart-pins=off,flash-file=$tmpdir/flash.bin" \
+    -serial null -semihosting-config enable=on,target=native \
+    -kernel "$guest"
+
+"$qemu" -display none -monitor none \
+    -M "raspi-pico,strict-uart-pins=off,flash-file=$tmpdir/flash.bin" \
+    -serial stdio -semihosting-config enable=on,target=native \
+    > "$tmpdir/persist.out"
+diff -u "$persist_expected" "$tmpdir/persist.out"
-- 
2.55.0


Reply via email to