From 8cce172c95963752306713b6caf8a67ced27e2f2 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Wed, 1 Apr 2026 15:06:39 +0200 Subject: [PATCH] New dma layout --- main.c | 101 ++++++++++++++++++-------------------------- video_dma.pio | 115 +++++++++++++++++++++++++------------------------- 2 files changed, 98 insertions(+), 118 deletions(-) diff --git a/main.c b/main.c index cecfb30..f5da604 100644 --- a/main.c +++ b/main.c @@ -20,20 +20,16 @@ #define DATA_PIN 2 // pixel / luma bit pin #define SYNC_PIN 3 // sync pin -// Set to 1 to bypass DMA/framebuffer and generate fixed black/white bars. -#define VIDEO_DIAGNOSTIC_BARS 0 - // --------------------------- // Video format // --------------------------- -// NTSC-compatible 240p timing using the same 64us line structure as the -// original PAL code, but with 262 lines/frame at about 59.6Hz. #define VIDEO_WIDTH 768 -#define VIDEO_HEIGHT 240 +#define VIDEO_HEIGHT 576 #define WORDS_PER_LINE (VIDEO_WIDTH / 32) +#define LINES_PER_FIELD (VIDEO_HEIGHT / 2) #define SYNC_INTERVAL_S 0.000002f // 2 us per sync-SM instruction -#define ACTIVE_VIDEO_S 0.000050f // 52 us active video per line +#define ACTIVE_VIDEO_S 0.000052f // 52 us active video per line // Match the .pio program's published constant. // pioasm will emit CLOCKS_PER_BIT in the generated header. @@ -47,8 +43,11 @@ // --------------------------- static uint32_t framebuffer[VIDEO_HEIGHT][WORDS_PER_LINE]; -// Current active scanline within the progressive frame. -static volatile uint line_in_frame = 0; +// Field/line tracking for line-based DMA. +// This is intentionally kept in software to match the sync SM's 288 active +// lines per field. +static volatile bool even_field = true; +static volatile uint line_in_field = 0; // DMA channel that feeds the PIO TX FIFO static int video_dma_chan; @@ -57,6 +56,7 @@ static int video_dma_chan; // Optional test pattern // Replace this with your own drawing code. // --------------------------- +#define BORDER 40 static void fill_test_pattern(void) { memset(framebuffer, 0x00, sizeof(framebuffer)); @@ -66,7 +66,7 @@ static void fill_test_pattern(void) { // Simple visible pattern: // border + checker - if (x < 20 || x >= VIDEO_WIDTH - 20 || y < 20 || y >= VIDEO_HEIGHT - 20) { + if (x < BORDER || x >= VIDEO_WIDTH - BORDER || y < BORDER || y >= VIDEO_HEIGHT - BORDER) { on = true; } else if (((x >> 4) ^ (y >> 4)) & 1) { on = true; @@ -107,18 +107,6 @@ static void init_cvdata_program(PIO pio, uint sm, uint offset, float clkdiv, uin pio_sm_exec(pio, sm, pio_encode_out(pio_null, 32)); } -static void init_cvdata_diag_program(PIO pio, uint sm, uint offset, float clkdiv, uint data_pin) { - pio_sm_config c = cvdata_diag_program_get_default_config(offset); - - sm_config_set_set_pins(&c, data_pin, 1); - sm_config_set_clkdiv(&c, clkdiv); - - pio_gpio_init(pio, data_pin); - pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, true); - - pio_sm_init(pio, sm, offset, &c); -} - static void init_cvsync_program(PIO pio, uint sm, uint offset, float clkdiv, uint sync_pin) { pio_sm_config c = cvsync_program_get_default_config(offset); @@ -130,9 +118,12 @@ static void init_cvsync_program(PIO pio, uint sm, uint offset, float clkdiv, uin pio_sm_init(pio, sm, offset, &c); - // Preload OSR = active_lines_per_frame - 1 - pio_sm_put_blocking(pio, sm, VIDEO_HEIGHT - 1); + // Preload OSR = lines_per_field - 1 + pio_sm_put_blocking(pio, sm, LINES_PER_FIELD - 1); pio_sm_exec(pio, sm, pio_encode_pull(false, false)); + + // Start with Y = 0, meaning "even field" in this program. + pio_sm_exec(pio, sm, pio_encode_set(pio_y, 0)); } // --------------------------- @@ -163,29 +154,28 @@ static void init_video_dma(PIO pio, uint data_sm) { // Start DMA for one scanline // --------------------------- static inline void start_dma_for_current_line(void) { - dma_channel_set_read_addr(video_dma_chan, framebuffer[line_in_frame], false); + uint src_line = even_field ? (line_in_field * 2u) : (line_in_field * 2u + 1u); + + dma_channel_set_read_addr(video_dma_chan, framebuffer[src_line], false); dma_channel_set_trans_count(video_dma_chan, WORDS_PER_LINE, true); - line_in_frame++; - if (line_in_frame >= VIDEO_HEIGHT) { - line_in_frame = 0; + line_in_field++; + if (line_in_field >= LINES_PER_FIELD) { + line_in_field = 0; + even_field = !even_field; } } // --------------------------- // PIO IRQ handler: -// fires once per active scanline when cvsync executes `irq set LINE_IRQ`. -// We clear the PIO IRQ and arm one DMA transfer for the next active line. +// The sync SM pushes one token into its RX FIFO during each line's back porch. +// As soon as the previous line DMA has finished, consume one token and arm the +// next scanline so the data FIFO is primed before visible pixels start. // --------------------------- static void pio0_irq0_handler(void) { - if (pio_interrupt_get(VIDEO_PIO, 1)) { - pio_interrupt_clear(VIDEO_PIO, 1); - - // A new line request arrives during hsync/back porch so the DMA can - // prime the data SM before visible pixels begin. - if (!dma_channel_is_busy(video_dma_chan)) { - start_dma_for_current_line(); - } + if (!pio_sm_is_rx_fifo_empty(VIDEO_PIO, SYNC_SM) && !dma_channel_is_busy(video_dma_chan)) { + (void)pio_sm_get(VIDEO_PIO, SYNC_SM); + start_dma_for_current_line(); } } @@ -198,41 +188,30 @@ static void video_init(void) { } uint sync_offset = pio_add_program(VIDEO_PIO, &cvsync_program); - uint data_offset; + uint data_offset = pio_add_program(VIDEO_PIO, &cvdata_program); - // sync SM: 1 instruction every 0.5 us + // Same timing idea as the original repo: + // sync SM: 1 instruction every 2 us + // data SM: enough PIO cycles to emit VIDEO_WIDTH bits in 52 us float sys_hz = (float)clock_get_hz(clk_sys); + float data_clkdiv = (sys_hz / ((float)VIDEO_WIDTH / ACTIVE_VIDEO_S)) / (float)CLOCKS_PER_BIT; float sync_clkdiv = sys_hz * SYNC_INTERVAL_S; - init_cvsync_program(VIDEO_PIO, SYNC_SM, sync_offset, sync_clkdiv, SYNC_PIN); - -#if VIDEO_DIAGNOSTIC_BARS - // 0.5 us per instruction gives a visible bar pattern over the 52 us active period. - float diag_clkdiv = sys_hz * 0.0000005f; - data_offset = pio_add_program(VIDEO_PIO, &cvdata_diag_program); - init_cvdata_diag_program(VIDEO_PIO, DATA_SM, data_offset, diag_clkdiv, DATA_PIN); -#else - // data SM: enough PIO cycles to emit VIDEO_WIDTH bits in 52 us - float data_clkdiv = (sys_hz / ((float)VIDEO_WIDTH / ACTIVE_VIDEO_S)) / (float)CLOCKS_PER_BIT; - data_offset = pio_add_program(VIDEO_PIO, &cvdata_program); init_cvdata_program(VIDEO_PIO, DATA_SM, data_offset, data_clkdiv, DATA_PIN); + init_cvsync_program(VIDEO_PIO, SYNC_SM, sync_offset, sync_clkdiv, SYNC_PIN); init_video_dma(VIDEO_PIO, DATA_SM); -#endif - // Route PIO internal IRQ 1 to CPU IRQ 0. IRQ 0 stays reserved for the - // data SM's wait instruction inside the PIO block. - pio_interrupt_clear(VIDEO_PIO, 0); - pio_interrupt_clear(VIDEO_PIO, 1); - pio_set_irq0_source_enabled(VIDEO_PIO, pis_interrupt1, true); + // Route sync-SM RX FIFO notifications to CPU IRQ 0. + pio_set_irq0_source_enabled(VIDEO_PIO, pis_sm1_rx_fifo_not_empty, true); irq_set_exclusive_handler(PIO0_IRQ_0, pio0_irq0_handler); irq_set_enabled(PIO0_IRQ_0, true); - // Start with the first active line of the frame. - line_in_frame = 0; + // Start with the first line of the even field + even_field = true; + line_in_field = 0; -#if !VIDEO_DIAGNOSTIC_BARS + // Prime the FIFO so the very first active line does not depend on IRQ latency. start_dma_for_current_line(); -#endif pio_sm_set_enabled(VIDEO_PIO, DATA_SM, true); pio_sm_set_enabled(VIDEO_PIO, SYNC_SM, true); diff --git a/video_dma.pio b/video_dma.pio index bfe473d..a92f610 100644 --- a/video_dma.pio +++ b/video_dma.pio @@ -4,53 +4,79 @@ ; - SM1: sync timing + line start IRQ ; - SM0: pixel output during active video ; -; NTSC-compatible progressive timing using the same coarse 2us timing grid as -; the original PAL implementation: -; - 64us line period -; - 4us hsync -; - 6us back porch -; - 52us active video + front porch tail -; - 240 active lines, 262 total lines per frame (~59.64Hz) +; Based on the same timing structure as alanpreed/pico-composite-video: +; 4us hsync, 6us back porch, 52us active video, 2us front porch, +; 288 active lines per field, interlaced fields. .define DATA_DELAY 4 .define LINE_IRQ 0 -.define DMA_IRQ 1 .define PUBLIC CLOCKS_PER_BIT DATA_DELAY + 2 .program cvsync .side_set 1 -; OSR = active_lines_per_frame - 1 +; OSR = lines_per_field - 1 +; Y = field flag: 0 = even field, !0 = odd field .wrap_target -frame_start: - ; 3 lines of broad vertical sync. - set x, 2 side 1 +vsync_start: + ; First set of short pulses: + ; even field: 6 + ; odd field : 5 + jmp !y set_even_counter side 0 + set x, 3 side 1 + jmp vsync_short_pulse side 1 [13] -vsync_lines: - nop side 0 [14] +set_even_counter: + set x, 4 side 1 [14] + +vsync_short_pulse: nop side 0 + jmp x-- vsync_short_pulse [14] side 1 + + ; Long sync pulses: always 5 + set x, 4 side 0 [13] + +vsync_long_start: + jmp !x vsync_long_end [1] side 1 + jmp x-- vsync_long_start [13] side 0 +vsync_long_end: + + ; Second set of short pulses: + ; even field: 5 + ; odd field : 4 + jmp !y set_even_counter_2 side 0 + set x, 2 side 1 + jmp flip_field_flag side 1 [12] + +set_even_counter_2: + set x, 3 side 1 [13] + +flip_field_flag: + mov y, !y side 1 + +vsync_short_pulse_2: + nop side 0 + jmp x-- vsync_short_pulse_2 [14] side 1 + + ; 17 blank lines + set x, 16 side 0 [1] + +hsync_blank_start: nop side 1 [14] - jmp x-- vsync_lines side 1 + jmp !x hsync_blank_end side 1 [14] + jmp x-- hsync_blank_start side 0 [1] +hsync_blank_end: - ; 19 blank lines before active video. - set x, 18 side 1 + ; 288 active video lines + mov x, osr side 0 [1] -blank_lines: - irq set DMA_IRQ side 0 [1] - nop side 1 [14] - jmp x-- blank_lines side 1 [14] - - ; 240 active video lines. - mov x, osr side 1 - -active_lines: - irq set DMA_IRQ side 0 [1] ; 4us hsync, ask CPU to arm DMA - nop side 1 [2] ; 6us back porch - irq set LINE_IRQ side 1 [13] ; 28us visible video - jmp x-- active_lines side 1 [12] - - jmp frame_start side 1 [12] +hsync_video: + in null, 32 side 1 ; 2us back porch, prepare CPU token + push noblock side 1 [1] ; 4us more back porch, notify CPU early + irq set LINE_IRQ side 1 [13] ; 52us active video + jmp !x vsync_start side 1 [12] ; 2us front porch (+ tail of active) + jmp x-- hsync_video side 0 [1] ; 4us hsync pulse .wrap @@ -73,28 +99,3 @@ data_out: out pins, 1 [DATA_DELAY] jmp y-- data_out .wrap - - -.program cvdata_diag - -; Diagnostic program: -; waits for each active line, then emits wide black/white bars without DMA. -; This isolates the analog output path from the framebuffer/DMA path. - -.wrap_target - wait 1 irq LINE_IRQ - irq clear LINE_IRQ - - set x, 3 - -diag_bars: - set pins, 1 [15] - nop [15] - nop [15] - set pins, 0 [15] - nop [15] - nop [15] - jmp x-- diag_bars [15] - - set pins, 0 -.wrap