New dma layout

This commit is contained in:
2026-04-01 15:06:39 +02:00
parent a894efd799
commit 8cce172c95
2 changed files with 98 additions and 118 deletions

101
main.c
View File

@@ -20,20 +20,16 @@
#define DATA_PIN 2 // pixel / luma bit pin #define DATA_PIN 2 // pixel / luma bit pin
#define SYNC_PIN 3 // sync 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 // 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_WIDTH 768
#define VIDEO_HEIGHT 240 #define VIDEO_HEIGHT 576
#define WORDS_PER_LINE (VIDEO_WIDTH / 32) #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 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. // Match the .pio program's published constant.
// pioasm will emit CLOCKS_PER_BIT in the generated header. // pioasm will emit CLOCKS_PER_BIT in the generated header.
@@ -47,8 +43,11 @@
// --------------------------- // ---------------------------
static uint32_t framebuffer[VIDEO_HEIGHT][WORDS_PER_LINE]; static uint32_t framebuffer[VIDEO_HEIGHT][WORDS_PER_LINE];
// Current active scanline within the progressive frame. // Field/line tracking for line-based DMA.
static volatile uint line_in_frame = 0; // 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 // DMA channel that feeds the PIO TX FIFO
static int video_dma_chan; static int video_dma_chan;
@@ -57,6 +56,7 @@ static int video_dma_chan;
// Optional test pattern // Optional test pattern
// Replace this with your own drawing code. // Replace this with your own drawing code.
// --------------------------- // ---------------------------
#define BORDER 40
static void fill_test_pattern(void) { static void fill_test_pattern(void) {
memset(framebuffer, 0x00, sizeof(framebuffer)); memset(framebuffer, 0x00, sizeof(framebuffer));
@@ -66,7 +66,7 @@ static void fill_test_pattern(void) {
// Simple visible pattern: // Simple visible pattern:
// border + checker // 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; on = true;
} else if (((x >> 4) ^ (y >> 4)) & 1) { } else if (((x >> 4) ^ (y >> 4)) & 1) {
on = true; 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)); 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) { 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); 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); pio_sm_init(pio, sm, offset, &c);
// Preload OSR = active_lines_per_frame - 1 // Preload OSR = lines_per_field - 1
pio_sm_put_blocking(pio, sm, VIDEO_HEIGHT - 1); pio_sm_put_blocking(pio, sm, LINES_PER_FIELD - 1);
pio_sm_exec(pio, sm, pio_encode_pull(false, false)); 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 // Start DMA for one scanline
// --------------------------- // ---------------------------
static inline void start_dma_for_current_line(void) { 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); dma_channel_set_trans_count(video_dma_chan, WORDS_PER_LINE, true);
line_in_frame++; line_in_field++;
if (line_in_frame >= VIDEO_HEIGHT) { if (line_in_field >= LINES_PER_FIELD) {
line_in_frame = 0; line_in_field = 0;
even_field = !even_field;
} }
} }
// --------------------------- // ---------------------------
// PIO IRQ handler: // PIO IRQ handler:
// fires once per active scanline when cvsync executes `irq set LINE_IRQ`. // The sync SM pushes one token into its RX FIFO during each line's back porch.
// We clear the PIO IRQ and arm one DMA transfer for the next active line. // 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) { static void pio0_irq0_handler(void) {
if (pio_interrupt_get(VIDEO_PIO, 1)) { if (!pio_sm_is_rx_fifo_empty(VIDEO_PIO, SYNC_SM) && !dma_channel_is_busy(video_dma_chan)) {
pio_interrupt_clear(VIDEO_PIO, 1); (void)pio_sm_get(VIDEO_PIO, SYNC_SM);
start_dma_for_current_line();
// 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();
}
} }
} }
@@ -198,41 +188,30 @@ static void video_init(void) {
} }
uint sync_offset = pio_add_program(VIDEO_PIO, &cvsync_program); 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 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; 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_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); init_video_dma(VIDEO_PIO, DATA_SM);
#endif
// Route PIO internal IRQ 1 to CPU IRQ 0. IRQ 0 stays reserved for the // Route sync-SM RX FIFO notifications to CPU IRQ 0.
// data SM's wait instruction inside the PIO block. pio_set_irq0_source_enabled(VIDEO_PIO, pis_sm1_rx_fifo_not_empty, true);
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_exclusive_handler(PIO0_IRQ_0, pio0_irq0_handler);
irq_set_enabled(PIO0_IRQ_0, true); irq_set_enabled(PIO0_IRQ_0, true);
// Start with the first active line of the frame. // Start with the first line of the even field
line_in_frame = 0; 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(); start_dma_for_current_line();
#endif
pio_sm_set_enabled(VIDEO_PIO, DATA_SM, true); pio_sm_set_enabled(VIDEO_PIO, DATA_SM, true);
pio_sm_set_enabled(VIDEO_PIO, SYNC_SM, true); pio_sm_set_enabled(VIDEO_PIO, SYNC_SM, true);

View File

@@ -4,53 +4,79 @@
; - SM1: sync timing + line start IRQ ; - SM1: sync timing + line start IRQ
; - SM0: pixel output during active video ; - SM0: pixel output during active video
; ;
; NTSC-compatible progressive timing using the same coarse 2us timing grid as ; Based on the same timing structure as alanpreed/pico-composite-video:
; the original PAL implementation: ; 4us hsync, 6us back porch, 52us active video, 2us front porch,
; - 64us line period ; 288 active lines per field, interlaced fields.
; - 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 DATA_DELAY 4
.define LINE_IRQ 0 .define LINE_IRQ 0
.define DMA_IRQ 1
.define PUBLIC CLOCKS_PER_BIT DATA_DELAY + 2 .define PUBLIC CLOCKS_PER_BIT DATA_DELAY + 2
.program cvsync .program cvsync
.side_set 1 .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 .wrap_target
frame_start: vsync_start:
; 3 lines of broad vertical sync. ; First set of short pulses:
set x, 2 side 1 ; 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: set_even_counter:
nop side 0 [14] set x, 4 side 1 [14]
vsync_short_pulse:
nop side 0 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] 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. ; 288 active video lines
set x, 18 side 1 mov x, osr side 0 [1]
blank_lines: hsync_video:
irq set DMA_IRQ side 0 [1] in null, 32 side 1 ; 2us back porch, prepare CPU token
nop side 1 [14] push noblock side 1 [1] ; 4us more back porch, notify CPU early
jmp x-- blank_lines side 1 [14] irq set LINE_IRQ side 1 [13] ; 52us active video
jmp !x vsync_start side 1 [12] ; 2us front porch (+ tail of active)
; 240 active video lines. jmp x-- hsync_video side 0 [1] ; 4us hsync pulse
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 .wrap
@@ -73,28 +99,3 @@ data_out:
out pins, 1 [DATA_DELAY] out pins, 1 [DATA_DELAY]
jmp y-- data_out jmp y-- data_out
.wrap .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