// main.c #include #include #include "pico/stdlib.h" #include "hardware/clocks.h" #include "hardware/dma.h" #include "hardware/irq.h" #include "hardware/pio.h" #include "video_dma.pio.h" // --------------------------- // User-adjustable pins // --------------------------- #define VIDEO_PIO pio0 #define DATA_SM 0 #define SYNC_SM 1 #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 WORDS_PER_LINE (VIDEO_WIDTH / 32) #define SYNC_INTERVAL_S 0.000002f // 2 us per sync-SM instruction #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. #ifndef CLOCKS_PER_BIT #define CLOCKS_PER_BIT 6 #endif // --------------------------- // Framebuffer // One bit per pixel, 32 pixels per word // --------------------------- static uint32_t framebuffer[VIDEO_HEIGHT][WORDS_PER_LINE]; // 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; // --------------------------- // Optional test pattern // Replace this with your own drawing code. // --------------------------- static void fill_test_pattern(void) { memset(framebuffer, 0x00, sizeof(framebuffer)); for (uint y = 0; y < VIDEO_HEIGHT; ++y) { for (uint x = 0; x < VIDEO_WIDTH; ++x) { bool on = false; // Simple visible pattern: // border + checker if (x < 20 || x >= VIDEO_WIDTH - 20 || y < 20 || y >= VIDEO_HEIGHT - 20) { on = true; } else if (((x >> 4) ^ (y >> 4)) & 1) { on = true; } if (on) { uint word = x >> 5; uint bit = 31 - (x & 31); // MSB-first framebuffer[y][word] |= (1u << bit); } } } } // --------------------------- // PIO init helpers // --------------------------- static void init_cvdata_program(PIO pio, uint sm, uint offset, float clkdiv, uint data_pin) { pio_sm_config c = cvdata_program_get_default_config(offset); sm_config_set_set_pins(&c, data_pin, 1); sm_config_set_out_pins(&c, data_pin, 1); sm_config_set_clkdiv(&c, clkdiv); // Shift left, autopull every 32 bits. // If the image is mirrored, change this to shift-right and/or change bit packing. sm_config_set_out_shift(&c, false, true, 32); pio_gpio_init(pio, data_pin); pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, true); pio_sm_init(pio, sm, offset, &c); // Preload X = VIDEO_WIDTH - 1 pio_sm_put_blocking(pio, sm, VIDEO_WIDTH - 1); pio_sm_exec(pio, sm, pio_encode_pull(false, false)); pio_sm_exec(pio, sm, pio_encode_mov(pio_x, pio_osr)); 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); sm_config_set_sideset_pins(&c, sync_pin); sm_config_set_clkdiv(&c, clkdiv); pio_gpio_init(pio, sync_pin); pio_sm_set_consecutive_pindirs(pio, sm, sync_pin, 1, true); pio_sm_init(pio, sm, offset, &c); // 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)); } // --------------------------- // DMA setup // --------------------------- static void init_video_dma(PIO pio, uint data_sm) { video_dma_chan = dma_claim_unused_channel(true); dma_channel_config cfg = dma_channel_get_default_config(video_dma_chan); channel_config_set_transfer_data_size(&cfg, DMA_SIZE_32); channel_config_set_read_increment(&cfg, true); channel_config_set_write_increment(&cfg, false); // Pace the DMA from the data SM TX FIFO DREQ channel_config_set_dreq(&cfg, pio_get_dreq(pio, data_sm, true)); dma_channel_configure( video_dma_chan, &cfg, &pio->txf[data_sm], // write address NULL, // read address set per line 0, // transfer count set per line false // don't start yet ); } // --------------------------- // 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); 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; } } // --------------------------- // 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. // --------------------------- 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(); } } } // --------------------------- // Video start // --------------------------- static void video_init(void) { if ((VIDEO_WIDTH % 32) != 0) { panic("VIDEO_WIDTH must be a multiple of 32"); } uint sync_offset = pio_add_program(VIDEO_PIO, &cvsync_program); uint data_offset; // sync SM: 1 instruction every 0.5 us float sys_hz = (float)clock_get_hz(clk_sys); 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_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 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); } int main(void) { stdio_init_all(); fill_test_pattern(); // Replace with your own framebuffer writer video_init(); while (true) { // Your application code goes here. // Some other code can freely draw into framebuffer[]. tight_loop_contents(); } }