This commit is contained in:
2026-08-30 19:27:26 +02:00
parent 073a538794
commit 52d4d286c6
8 changed files with 391 additions and 130 deletions

View File

@@ -48,6 +48,7 @@ target_link_libraries(picopal
hardware_pio
hardware_timer
hardware_clocks
pico_atomic
pico_multicore
)

255
core1.c
View File

@@ -1,12 +1,17 @@
#include "core1.h"
#include <stdint.h>
#include <stdio.h>
#include <stdatomic.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "hardware/irq.h"
#include "hardware/pio.h"
#include "core_comm.h"
#include "z80_bus.pio.h"
#include "text_mode.h"
#include "cvideo.h"
#define Z80_PIO pio1
#define Z80_MONITOR_SM 0
@@ -20,25 +25,102 @@
#define Z80_CS_PIN 12
#define Z80_A0_PIN 13
#define Z80_A1_PIN 14
#define Z80_CLK_PIN 15
#define Z80_WAIT_PIN 16
#define Z80_RD_MASK (1u << Z80_RD_PIN)
#define Z80_CS_MASK (1u << Z80_CS_PIN)
#define Z80_ADDR_MASK 0x3u
#define Z80_DATA_MASK 0xffu
#define Z80_DIRTY_SHIFT 32u
#define Z80_DIRTY_MASK (UINT64_C(0x0f) << Z80_DIRTY_SHIFT)
// Bits 0..31 contain registers 0..3. Bits 32..35 are their dirty flags.
static _Atomic uint64_t z80_registers;
uint16_t cursor_x = 0;
uint16_t cursor_y = 0;
uint16_t pixel_x = 0;
uint16_t pixel_y = 0;
#define VID_CMD_SET_TEXT_MODE 0
#define VID_CMD_SET_PIXEL_MODE 1
#define VID_CMD_CLR 2
#define VID_CMD_TEXT_POS_X 3
#define VID_CMD_PIXEL_POS_X 4
#define VID_CMD_TEXT_POS_Y 5
#define VID_CMD_PIXEL_POS_Y 6
typedef enum VID_MODE{
VID_MODE_TEXT,
VID_MODE_PIXEL,
VID_MODE_TEXT_POS_X,
VID_MODE_TEXT_POS_Y,
VID_MODE_PIXEL_POS_X_1,
VID_MODE_PIXEL_POS_X_2,
VID_MODE_PIXEL_POS_Y_1,
VID_MODE_PIXEL_POS_Y_2,
} vid_mode_t;
vid_mode_t vid_mode = VID_MODE_TEXT;
// --------------------------------
// CORE 1
// --------------------------------
static void core1_puts(const char *s) {
static void core1_putc(char c){
video_command_t cmd;
switch((uint8_t)c){
case '\r':
cursor_x = 0;
break;
case '\n':
cursor_y = (cursor_y<CHAR_LINES-1) ? cursor_y+1 : cursor_y;
break;
default:
cmd.cmd = VIDEO_WRITE_TEXT,
cmd.data[0] = (uint16_t)c,
cmd.data[1] = cursor_x,
cmd.data[2] = cursor_y,
core1_to_core0_write_blocking(*((uint64_t*)&cmd));
cursor_x = (cursor_x<CHARS_PER_LINE-1) ? cursor_x+1 : cursor_x;
}
}
static void core1_puts(char * s){
while(*s){
core1_to_core0_write_blocking((uint32_t)*s);
core1_putc(*s);
s++;
}
}
static void z80_write_monitor_init(void) {
uint offset = pio_add_program(Z80_PIO, &z80_write_monitor_program);
static void z80_bus_irq_handler(void) {
while (!pio_sm_is_rx_fifo_empty(Z80_PIO, Z80_MONITOR_SM)) {
uint32_t bus_sample = pio_sm_get(Z80_PIO, Z80_MONITOR_SM);
uint8_t addr = (bus_sample >> Z80_A0_PIN) & Z80_ADDR_MASK;
uint32_t shift = (uint32_t)addr * 8u;
uint64_t registers = atomic_load_explicit(&z80_registers,
memory_order_relaxed);
if ((bus_sample & Z80_RD_MASK) != 0) {
uint8_t data = (bus_sample >> Z80_DATA_PIN) & Z80_DATA_MASK;
registers &= ~(UINT64_C(0xff) << shift);
registers |= (uint64_t)data << shift;
registers |= UINT64_C(1) << (Z80_DIRTY_SHIFT + addr);
atomic_store_explicit(&z80_registers, registers,
memory_order_release);
} else {
uint8_t data = (registers >> shift) & Z80_DATA_MASK;
pio_sm_put(Z80_PIO, Z80_MONITOR_SM, data);
}
}
}
static void z80_bus_init(void) {
uint offset = pio_add_program(Z80_PIO, &z80_bus_program);
// Keep the bus isolated while selecting B (Z80) -> A (Pico).
gpio_init(Z80_OE_PIN);
@@ -47,60 +129,149 @@ static void z80_write_monitor_init(void) {
gpio_init(Z80_DIR_PIN);
gpio_put(Z80_DIR_PIN, 0);
gpio_set_dir(Z80_DIR_PIN, GPIO_OUT);
gpio_init(Z80_WAIT_PIN);
gpio_put(Z80_WAIT_PIN, 1);
gpio_set_dir(Z80_WAIT_PIN, GPIO_OUT);
for (uint pin = Z80_DATA_PIN; pin <= Z80_A1_PIN; pin++) {
for (uint pin = Z80_OE_PIN; pin <= Z80_CLK_PIN; pin++) {
pio_gpio_init(Z80_PIO, pin);
}
pio_sm_set_consecutive_pindirs(Z80_PIO, Z80_MONITOR_SM,
Z80_DATA_PIN,
Z80_A1_PIN - Z80_DATA_PIN + 1,
false);
pio_gpio_init(Z80_PIO, Z80_WAIT_PIN);
pio_sm_config config = z80_write_monitor_program_get_default_config(offset);
pio_sm_config config = z80_bus_program_get_default_config(offset);
sm_config_set_in_pins(&config, Z80_OE_PIN);
sm_config_set_in_shift(&config, false, false, 32);
sm_config_set_out_pins(&config, Z80_DATA_PIN, Z80_DATA_WIDTH);
sm_config_set_out_shift(&config, true, false, 32);
sm_config_set_set_pins(&config, Z80_WAIT_PIN, 1);
sm_config_set_sideset_pins(&config, Z80_OE_PIN);
pio_sm_init(Z80_PIO, Z80_MONITOR_SM, offset, &config);
pio_sm_set_enabled(Z80_PIO, Z80_MONITOR_SM, true);
// Enable only after every Pico-side data pin has become an input.
gpio_put(Z80_OE_PIN, 0);
uint32_t control_mask = (1u << Z80_OE_PIN) |
(1u << Z80_DIR_PIN) |
(1u << Z80_WAIT_PIN);
uint32_t initial_levels = (1u << Z80_WAIT_PIN);
pio_sm_set_pins_with_mask(Z80_PIO, Z80_MONITOR_SM,
initial_levels, control_mask);
pio_sm_set_pindirs_with_mask(Z80_PIO, Z80_MONITOR_SM,
control_mask, control_mask);
pio_sm_set_consecutive_pindirs(Z80_PIO, Z80_MONITOR_SM,
Z80_DATA_PIN,
Z80_CLK_PIN - Z80_DATA_PIN + 1,
false);
// IRQ configuration is per-core. This function runs on core 1, so bus
// events preempt diagnostics on core 1 and never involve core 0.
pio_set_irq0_source_enabled(Z80_PIO,
pis_sm0_rx_fifo_not_empty,
true);
irq_set_exclusive_handler(PIO1_IRQ_0, z80_bus_irq_handler);
irq_set_priority(PIO1_IRQ_0, PICO_HIGHEST_IRQ_PRIORITY);
irq_set_enabled(PIO1_IRQ_0, true);
pio_sm_set_enabled(Z80_PIO, Z80_MONITOR_SM, true);
}
void core1_entry() {
z80_write_monitor_init();
core1_puts("core 1: Z80 write monitor started\r\n");
uint64_t write_count = 0;
uint8_t last_addr = 0;
uint8_t last_data = 0;
absolute_time_t next_report = make_timeout_time_ms(100);
z80_bus_init();
core1_puts("core 1: Z80 read/write bus started > ");
while (true) {
while (!pio_sm_is_rx_fifo_empty(Z80_PIO, Z80_MONITOR_SM)) {
uint32_t bus_sample = pio_sm_get(Z80_PIO, Z80_MONITOR_SM);
// Check if it is for us -> CS=1
if ((bus_sample & Z80_CS_MASK) != 0) {
uint8_t addr = (bus_sample >> Z80_A0_PIN) & Z80_ADDR_MASK;
if ((bus_sample & Z80_RD_MASK) != 0) {
// WR
last_addr = addr;
last_data = (bus_sample >> Z80_DATA_PIN) & Z80_DATA_MASK;
write_count += 1;
} else {
// RD
}
// Take a coherent snapshot and consume all four dirty flags. A
// concurrent later write sets its register's flag again.
uint64_t registers_i = atomic_fetch_and_explicit(&z80_registers, ~Z80_DIRTY_MASK, memory_order_acq_rel);
registers_t regs = *((registers_t*)&registers_i);
if((regs.flags & 0x01) == 0x01){
// Write to vid_cmd happened
switch(regs.vid_cmd){
case VID_CMD_SET_TEXT_MODE: {
video_command_t cmd = {
.cmd = VIDEO_COMMAND_MODE_SET,
.data[0] = VIDEO_MODE_TEXT,
};
core1_to_core0_write_blocking(*((uint64_t*)&cmd));
vid_mode = VID_MODE_TEXT;
} break;
case VID_CMD_SET_PIXEL_MODE: {
video_command_t cmd = {
.cmd = VIDEO_COMMAND_MODE_SET,
.data[0] = VIDEO_MODE_PIXEL,
};
core1_to_core0_write_blocking(*((uint64_t*)&cmd));
vid_mode = VID_MODE_PIXEL;
} break;
case VID_CMD_CLR: {
if(vid_mode == VIDEO_MODE_TEXT){
video_command_t cmd = {
.cmd = VIDEO_CLR_TEXT
};
core1_to_core0_write_blocking(*((uint64_t*)&cmd));
}else if(vid_mode == VIDEO_MODE_PIXEL){
video_command_t cmd = {
.cmd = VIDEO_CLR_PIXEL
};
core1_to_core0_write_blocking(*((uint64_t*)&cmd));
}
} break;
case VID_CMD_TEXT_POS_X: {
vid_mode = VID_MODE_TEXT_POS_X;
} break;
case VID_CMD_PIXEL_POS_X: {
vid_mode = VID_MODE_PIXEL_POS_X_1;
} break;
case VID_CMD_TEXT_POS_Y: {
vid_mode = VID_MODE_TEXT_POS_Y;
} break;
case VID_CMD_PIXEL_POS_Y: {
vid_mode = VID_MODE_PIXEL_POS_Y_1;
} break;
default:
}
}
if((regs.flags & 0x02) == 0x02){
// Write to vid_dat happened
switch(vid_mode){
case VID_MODE_TEXT_POS_X:
cursor_x = (regs.vid_data<CHARS_PER_LINE) ? regs.vid_data : cursor_x;
break;
case VID_MODE_TEXT_POS_Y:
cursor_y = (regs.vid_data<CHAR_LINES) ? regs.vid_data : cursor_y;
break;
if (time_reached(next_report)) {
char message[48];
snprintf(message, sizeof(message), "Z80 writes: %llu\r\n", (unsigned long long)write_count);
core1_puts(message);
snprintf(message, sizeof(message), "Last address: %02x\r\n", last_addr);
core1_puts(message);
snprintf(message, sizeof(message), "Last data: %02x\r\n", last_data);
core1_puts(message);
next_report = make_timeout_time_ms(100);
case VID_MODE_PIXEL_POS_X_1:
pixel_x = pixel_x&0xff00 | regs.vid_data;
vid_mode = VID_MODE_PIXEL_POS_X_2;
break;
case VID_MODE_PIXEL_POS_X_2:
pixel_x = pixel_x&0x00ff | regs.vid_data<<8;
vid_mode = VID_MODE_PIXEL_POS_X_1;
pixel_x = (pixel_x<VIDEO_WIDTH) ? pixel_x : 0;
break;
case VID_MODE_PIXEL_POS_Y_1:
pixel_y = pixel_y&0xff00 | regs.vid_data;
vid_mode = VID_MODE_PIXEL_POS_Y_2;
break;
case VID_MODE_PIXEL_POS_Y_2:
pixel_y = pixel_y&0x00ff | regs.vid_data<<8;
vid_mode = VID_MODE_PIXEL_POS_Y_1;
pixel_y = (pixel_y<VIDEO_HEIGHT) ? pixel_y : 0;
break;
case VID_MODE_PIXEL:{
video_command_t cmd = {
.cmd = VIDEO_WRITE_PIXEL,
.data[0] = regs.vid_data,
.data[1] = pixel_x,
.data[2] = pixel_y,
};
pixel_x = (pixel_x<VIDEO_WIDTH-8) ? pixel_x+8 : pixel_x;
core1_to_core0_write_blocking(*((uint64_t*)&cmd));
} break;
case VID_MODE_TEXT:
default:
core1_putc(regs.vid_data);
}
}
tight_loop_contents();

28
core1.h
View File

@@ -1,3 +1,31 @@
#pragma once
#include <stdint.h>
#include "pico/stdlib.h"
enum VIDEO_COMMAND{
VIDEO_COMMAND_MODE_SET, // data[0] VIDEO_MODE
VIDEO_WRITE_TEXT, // write data[0] to (data[1],data[2])
VIDEO_WRITE_PIXEL, // write 8 pixels (low byte of data[0]) to (data[1],daa[2])
VIDEO_SCROLL_TEXT, // Sroll text up
VIDEO_CLR_TEXT, // Clear screen
VIDEO_CLR_PIXEL, // Clear screen
};
enum VIDEO_MODE{
VIDEO_MODE_TEXT, // Text mode
VIDEO_MODE_PIXEL, // Pixel mode
};
typedef struct __packed{
uint16_t cmd;
uint16_t data[3];
} video_command_t; // fits inside uint64_t
typedef struct __packed{
uint8_t vid_cmd;
uint8_t vid_data;
uint8_t unused[2];
uint32_t flags;
} registers_t; // fits inside uint64_t
void core1_entry();

View File

@@ -3,18 +3,43 @@
#include "pico/multicore.h"
#include "pico/platform.h"
bool core1_to_core0_try_write(uint32_t data) {
bool core1_to_core0_try_write(uint64_t data) {
hard_assert(get_core_num() == 1);
return multicore_fifo_push_timeout_us(data, 0);
if (!multicore_fifo_push_timeout_us((uint32_t)data, 0)) {
return false;
}
// Once the low word is committed, complete the pair to preserve framing.
multicore_fifo_push_blocking((uint32_t)(data >> 32));
return true;
}
void core1_to_core0_write_blocking(uint32_t data) {
void core1_to_core0_write_blocking(uint64_t data) {
hard_assert(get_core_num() == 1);
multicore_fifo_push_blocking(data);
multicore_fifo_push_blocking((uint32_t)data);
multicore_fifo_push_blocking((uint32_t)(data >> 32));
}
bool core0_try_read_from_core1(uint32_t *data) {
bool core0_try_read_from_core1(uint64_t *data) {
static bool have_low_word;
static uint32_t low_word;
hard_assert(get_core_num() == 0);
hard_assert(data != NULL);
return multicore_fifo_pop_timeout_us(0, data);
if (!have_low_word) {
if (!multicore_fifo_pop_timeout_us(0, &low_word)) {
return false;
}
have_low_word = true;
}
uint32_t high_word;
if (!multicore_fifo_pop_timeout_us(0, &high_word)) {
return false;
}
*data = (uint64_t)low_word | ((uint64_t)high_word << 32);
have_low_word = false;
return true;
}

View File

@@ -3,15 +3,16 @@
#include <stdbool.h>
#include <stdint.h>
// The RP2350 multicore FIFO holds eight 32-bit messages in each direction.
// The RP2350 multicore FIFO is physically 32 bits wide. Each logical 64-bit
// message is transferred low word first, followed by its high word.
// These functions intentionally enforce the sending/receiving core roles.
// Try to send one word from core 1. Returns false instead of waiting when the
// FIFO is full.
bool core1_to_core0_try_write(uint32_t data);
bool core1_to_core0_try_write(uint64_t data);
// Send one word from core 1, waiting until core 0 has made FIFO space.
void core1_to_core0_write_blocking(uint32_t data);
void core1_to_core0_write_blocking(uint64_t data);
// Try to read one word on core 0. Returns false when no message is available.
bool core0_try_read_from_core1(uint32_t *data);
bool core0_try_read_from_core1(uint64_t *data);

124
main.c
View File

@@ -15,87 +15,49 @@
// --------------------------------
static video_framebuffer_t fb0, fb1;
static video_framebuffer_t pixel_mode_buffer;
static text_mode_buffer_t text_mode_buffer;
static volatile bool framebuffer_switched = true;
static bool text_mode = true;
static bool text_mode_dirty = true;
static uint console_row = 0;
static uint console_col = 0;
void framebuffer_switched_cb(){
framebuffer_switched = true;
}
static void text_mode_clear_buffer(void) {
static void text_mode_clear_buffer() {
memset(text_mode_buffer, 0, sizeof(text_mode_buffer));
console_row = 0;
console_col = 0;
}
static void text_mode_scroll_up(void) {
static void text_mode_scroll_up() {
memmove(&text_mode_buffer[0][0],
&text_mode_buffer[1][0],
(CHAR_LINES - 1) * CHARS_PER_LINE * sizeof(text_mode_buffer[0][0]));
memset(text_mode_buffer[CHAR_LINES - 1], 0, CHARS_PER_LINE * sizeof(text_mode_buffer[0][0]));
}
static void text_mode_newline(void) {
console_col = 0;
if (console_row + 1 >= CHAR_LINES) {
text_mode_scroll_up();
} else {
console_row++;
}
}
static void text_mode_put_char(char c) {
if (c == '\r') {
console_col = 0;
static void pixel_mode_write_byte(uint16_t x, uint16_t y, uint8_t pixels) {
if (x >= VIDEO_WIDTH || y >= VIDEO_HEIGHT) {
return;
}
if (c == '\n') {
text_mode_newline();
return;
}
for (uint bit_index = 0; bit_index < 8 && x + bit_index < VIDEO_WIDTH;
bit_index++) {
uint pixel_x = x + bit_index;
uint word = pixel_x >> 5;
uint bit = 31u - (pixel_x & 31u);
uint32_t mask = 1u << bit;
if (c == '\b' || c == 0x7f) {
if (console_col > 0) {
console_col--;
text_mode_buffer[console_row][console_col] = 0;
if ((pixels & (1u << (7u - bit_index))) != 0) {
pixel_mode_buffer[y][word] |= mask;
} else {
pixel_mode_buffer[y][word] &= ~mask;
}
return;
}
if ((unsigned char)c < 32 || (unsigned char)c > 126) {
return;
}
text_mode_buffer[console_row][console_col] = c;
console_col++;
if (console_col >= CHARS_PER_LINE) {
text_mode_newline();
}
}
static void text_mode_puts(const char *s) {
while (*s) {
text_mode_put_char(*s++);
}
}
static void poll_usb_console(void) {
int ch;
while ((ch = getchar_timeout_us(0)) != PICO_ERROR_TIMEOUT) {
text_mode_put_char((char)ch);
putchar(ch);
}
}
void main() {
stdio_init_all();
multicore_launch_core1(core1_entry);
init_font_cache();
@@ -104,33 +66,57 @@ void main() {
video_init(fb0);
text_mode_clear_buffer();
text_mode_puts("+------------------------------+\r\n");
text_mode_puts("| Pico-PAL Text Mode |\r\n");
text_mode_puts("+------------------------------+\r\n");
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);
memset(pixel_mode_buffer, 0, sizeof(pixel_mode_buffer));
int fbnum = 1;
video_framebuffer_ptr_t fbs[] = {fb0, fb1};
while (true) {
poll_usb_console();
uint32_t value;
while (core0_try_read_from_core1(&value)) {
char ch = (char)value;
text_mode_put_char((char)ch);
putchar(ch);
video_command_t cmd;
while (core0_try_read_from_core1((uint64_t*)&cmd)) {
switch(cmd.cmd){
case VIDEO_COMMAND_MODE_SET:{
switch(cmd.data[0]){
case VIDEO_MODE_TEXT:
text_mode = true;
break;
case VIDEO_MODE_PIXEL:
text_mode = false;
break;
default:
}
} break;
case VIDEO_WRITE_TEXT:{
char c = (char)cmd.data[0];
int posx = ((int)cmd.data[1] >= CHARS_PER_LINE) ? CHARS_PER_LINE-1 : (int)cmd.data[1];
int posy = ((int)cmd.data[2] >= CHAR_LINES) ? CHAR_LINES-1 : (int)cmd.data[2];
text_mode_buffer[posy][posx] = c;
} break;
case VIDEO_WRITE_PIXEL:{
pixel_mode_write_byte(cmd.data[1], cmd.data[2],
(uint8_t)cmd.data[0]);
} break;
case VIDEO_SCROLL_TEXT:{
text_mode_scroll_up();
} break;
case VIDEO_CLR_TEXT:{
text_mode_clear_buffer();
} break;
case VIDEO_CLR_PIXEL:{
memset(pixel_mode_buffer, 0, sizeof(pixel_mode_buffer));
} break;
default:
}
}
// Check if need to draw new frame
if(framebuffer_switched){
gpio_put(25, fbnum);
if(text_mode){
draw_text_mode(fbs[fbnum], text_mode_buffer);
} else {
memcpy(fbs[fbnum], pixel_mode_buffer,
sizeof(pixel_mode_buffer));
video_set_framebuffer(fbs[fbnum]);
}
fbnum = (fbnum+1)%2;
framebuffer_switched = false;
}

View File

@@ -4,17 +4,17 @@
// Text mode config
#define CHARS_PER_LINE 60
#define CHAR_LINES 24
#define CHAR_LINES 18
#define CHAR_SCALE 2
// Text origin in framebuffer pixels. Set both to 0 for the top-left corner.
#define TEXT_MARGIN_LEFT (30)
#define TEXT_MARGIN_TOP (42)
#define TEXT_MARGIN_TOP (40)
// Crop glyph pixels before scaling. Padding is split across both sides; for
// odd values, the extra pixel is removed from the right or bottom.
#define GLYPH_PADDING_X 1
#define GLYPH_PADDING_Y 4
#define GLYPH_PADDING_Y 0
// Set to 1 for a white background with black text, or 0 for the default
// black background with white text.

View File

@@ -1,10 +1,59 @@
.program z80_write_monitor
; Z80 register bus on PIO1.
;
; GP0 /OE, GP1 DIR, GP2..9 D0..D7, GP10 /IOREQ, GP11 /RD,
; GP12 CS (A7), GP13..14 A0..A1, GP15 CLK, GP16 /WAIT.
;
; side-set values for GP1:GP0:
; 0 = B->A enabled, 1 = B->A disabled
; 2 = A->B enabled, 3 = A->B disabled
.program z80_bus
.side_set 2 opt
.wrap_target
wait 0 gpio 10 ; /IOREQ asserted
wait 0 gpio 10
nop [1] ; allow synchronized bus signals to settle
mov isr, null
; IN base is GP0. Capture /OE, DIR, D0-D7, /IOREQ, /RD, CS,
; A0 and A1 in their corresponding GPIO bit positions.
in pins, 15
in pins, 15 ; snapshot GP0..GP14
; Decode /RD (bit 11) and CS (bit 12), preserving ISR for core 1.
mov osr, isr
out null, 11
out x, 1 ; X = /RD
out y, 1 ; Y = CS
jmp !y cycle_done
jmp !x read_cycle
write_cycle:
push block
wait 1 gpio 10 ; do not count this bus cycle again
jmp cycle_done
read_cycle:
set pins, 0 side 1 ; assert /WAIT, disable transceiver
push block ; send address request to core 1
pull block side 1 ; wait for response byte
; Prepare data and direction while the transceiver remains disabled.
mov x, osr side 1
mov osr, ~null side 3 ; select A->B
out pindirs, 8 side 3 ; GP2..GP9 outputs
mov osr, x side 3
out pins, 8 side 3 ; put response on A-side pins
; Keep /WAIT asserted through a real falling Z80 clock edge. PIO observes
; the edge through its synchronizer and releases /WAIT just afterwards,
; guaranteeing that the Z80 sampled it low and inserted a wait state.
wait 1 gpio 15 side 3
wait 0 gpio 15 side 3
set pins, 1 side 2 ; enable A->B and release /WAIT
wait 1 gpio 11 side 2 ; wait for /RD to deassert
nop side 3 ; disable before changing direction
mov osr, null side 3
out pindirs, 8 side 3 ; GP2..GP9 inputs
nop side 1 ; select B->A while disabled
nop side 0 ; enable B->A for write reception
cycle_done:
wait 1 gpio 10
.wrap