From 52d4d286c66d4e465b588c0072c30af172d2b821 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sun, 30 Aug 2026 19:27:26 +0200 Subject: [PATCH] stuff --- CMakeLists.txt | 1 + core1.c | 255 +++++++++++++++++++++++++++++++++++++++++-------- core1.h | 28 ++++++ core_comm.c | 37 +++++-- core_comm.h | 9 +- main.c | 124 +++++++++++------------- text_mode.h | 6 +- z80_bus.pio | 61 ++++++++++-- 8 files changed, 391 insertions(+), 130 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 715542e..38d9fec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ target_link_libraries(picopal hardware_pio hardware_timer hardware_clocks + pico_atomic pico_multicore ) diff --git a/core1.c b/core1.c index 803b806..dc061aa 100644 --- a/core1.c +++ b/core1.c @@ -1,12 +1,17 @@ +#include "core1.h" #include #include +#include #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> 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*)®isters_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 +#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(); \ No newline at end of file diff --git a/core_comm.c b/core_comm.c index f2b87ad..9d5117a 100644 --- a/core_comm.c +++ b/core_comm.c @@ -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; } diff --git a/core_comm.h b/core_comm.h index 72388b1..3d1dbd2 100644 --- a/core_comm.h +++ b/core_comm.h @@ -3,15 +3,16 @@ #include #include -// 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); diff --git a/main.c b/main.c index b1ac6af..67f328f 100644 --- a/main.c +++ b/main.c @@ -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; } diff --git a/text_mode.h b/text_mode.h index f7c157a..8a6d0bc 100644 --- a/text_mode.h +++ b/text_mode.h @@ -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. diff --git a/z80_bus.pio b/z80_bus.pio index 68a694a..160d993 100644 --- a/z80_bus.pio +++ b/z80_bus.pio @@ -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