diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/launch.json b/.vscode/launch.json index 4843321..1276530 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,9 +22,6 @@ "adapter speed 5000" ], "svdFile": "${env:PICO_SDK_PATH}/src/rp2350/hardware_regs/rp2350.svd", - "showDevDebugOutput": "raw", - "runToEntryPoint": "main", - // Give restart the same functionality as runToMain "postRestartCommands": [ "break main", "continue" diff --git a/main.c b/main.c index deef338..cecfb30 100644 --- a/main.c +++ b/main.c @@ -20,16 +20,20 @@ #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 576 +#define VIDEO_HEIGHT 240 #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.000052f // 52 us active video per line +#define ACTIVE_VIDEO_S 0.000050f // 52 us active video per line // Match the .pio program's published constant. // pioasm will emit CLOCKS_PER_BIT in the generated header. @@ -43,11 +47,8 @@ // --------------------------- static uint32_t framebuffer[VIDEO_HEIGHT][WORDS_PER_LINE]; -// 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; +// Current active scanline within the progressive frame. +static volatile uint line_in_frame = 0; // DMA channel that feeds the PIO TX FIFO static int video_dma_chan; @@ -65,7 +66,7 @@ static void fill_test_pattern(void) { // Simple visible pattern: // border + checker - if (x < 8 || x >= VIDEO_WIDTH - 8 || y < 8 || y >= VIDEO_HEIGHT - 8) { + if (x < 20 || x >= VIDEO_WIDTH - 20 || y < 20 || y >= VIDEO_HEIGHT - 20) { on = true; } else if (((x >> 4) ^ (y >> 4)) & 1) { on = true; @@ -106,6 +107,18 @@ 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); @@ -117,12 +130,9 @@ static void init_cvsync_program(PIO pio, uint sm, uint offset, float clkdiv, uin pio_sm_init(pio, sm, offset, &c); - // Preload OSR = lines_per_field - 1 - pio_sm_put_blocking(pio, sm, LINES_PER_FIELD - 1); + // Preload OSR = active_lines_per_frame - 1 + pio_sm_put_blocking(pio, sm, VIDEO_HEIGHT - 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)); } // --------------------------- @@ -153,15 +163,12 @@ static void init_video_dma(PIO pio, uint data_sm) { // Start DMA for one scanline // --------------------------- static inline void start_dma_for_current_line(void) { - 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_read_addr(video_dma_chan, framebuffer[line_in_frame], false); dma_channel_set_trans_count(video_dma_chan, WORDS_PER_LINE, true); - line_in_field++; - if (line_in_field >= LINES_PER_FIELD) { - line_in_field = 0; - even_field = !even_field; + line_in_frame++; + if (line_in_frame >= VIDEO_HEIGHT) { + line_in_frame = 0; } } @@ -171,11 +178,11 @@ static inline void start_dma_for_current_line(void) { // We clear the PIO IRQ and arm one DMA transfer for the next active line. // --------------------------- static void pio0_irq0_handler(void) { - if (pio_interrupt_get(VIDEO_PIO, 0)) { - pio_interrupt_clear(VIDEO_PIO, 0); + if (pio_interrupt_get(VIDEO_PIO, 1)) { + pio_interrupt_clear(VIDEO_PIO, 1); - // Safety: a new line should only arrive after the previous line DMA is done. - // If it isn't, the clocks are wrong or something stalled badly. + // 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(); } @@ -191,27 +198,41 @@ static void video_init(void) { } uint sync_offset = pio_add_program(VIDEO_PIO, &cvsync_program); - uint data_offset = pio_add_program(VIDEO_PIO, &cvdata_program); + uint data_offset; - // 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 + // sync SM: 1 instruction every 0.5 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_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); - // Route PIO internal IRQ 0 to CPU IRQ 0 - pio_set_irq0_source_enabled(VIDEO_PIO, pis_interrupt0, true); +#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_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); irq_set_exclusive_handler(PIO0_IRQ_0, pio0_irq0_handler); irq_set_enabled(PIO0_IRQ_0, true); - // Start with the first line of the even field - even_field = true; - line_in_field = 0; + // Start with the first active line of the frame. + line_in_frame = 0; + +#if !VIDEO_DIAGNOSTIC_BARS + start_dma_for_current_line(); +#endif pio_sm_set_enabled(VIDEO_PIO, DATA_SM, true); pio_sm_set_enabled(VIDEO_PIO, SYNC_SM, true); @@ -228,4 +249,4 @@ int main(void) { // Some other code can freely draw into framebuffer[]. tight_loop_contents(); } -} \ No newline at end of file +} diff --git a/video_dma.pio b/video_dma.pio index 5f6d119..bfe473d 100644 --- a/video_dma.pio +++ b/video_dma.pio @@ -4,78 +4,53 @@ ; - SM1: sync timing + line start IRQ ; - SM0: pixel output during active video ; -; 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. +; 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) .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 = lines_per_field - 1 -; Y = field flag: 0 = even field, !0 = odd field +; OSR = active_lines_per_frame - 1 .wrap_target -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] - -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 +frame_start: + ; 3 lines of broad vertical sync. 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: +vsync_lines: + nop side 0 [14] 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 hsync_blank_end side 1 [14] - jmp x-- hsync_blank_start side 0 [1] -hsync_blank_end: + jmp x-- vsync_lines side 1 - ; 288 active video lines - mov x, osr side 0 [1] + ; 19 blank lines before active video. + set x, 18 side 1 -hsync_video: - nop side 1 [2] ; 6us back porch - 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 +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] .wrap @@ -92,8 +67,34 @@ hsync_video: set pins, 0 mov y, x wait 1 irq LINE_IRQ + irq clear LINE_IRQ data_out: out pins, 1 [DATA_DELAY] jmp y-- data_out -.wrap \ No newline at end of file +.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