commit 4a4a4a6692049bc901bc506fed000b05596379ae Author: Joppe Blondel Date: Wed Apr 1 12:27:10 2026 +0200 Basic pattern gen diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..e385a5e --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "${env:PICO_SDK_PATH}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/arm-none-eabi-gcc", + "cStandard": "gnu17", + "cppStandard": "gnu++14", + "intelliSenseMode": "linux-gcc-arm", + "configurationProvider" : "ms-vscode.cmake-tools" + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4843321 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Cortex Debug", + "cwd": "${workspaceFolder}", + "executable": "${command:cmake.launchTargetPath}", + "request": "launch", + "type": "cortex-debug", + "servertype": "openocd", + "gdbPath": "arm-none-eabi-gdb", + "device": "RP2350", + "targetProcessor": 0, + "configFiles": [ + "interface/cmsis-dap.cfg", + "target/rp2350.cfg" + ], + "openOCDLaunchCommands": [ + "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/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..74f123a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,49 @@ +# Generated Cmake Pico project file + +cmake_minimum_required(VERSION 3.13) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +set(PICO_BOARD pico2) + +# initalize pico_sdk from installed location +# (note this can come from environment, CMake cache etc) +set(PICO_SDK_PATH "/usr/share/pico-sdk") + +# Pull in Raspberry Pi Pico SDK (must be before project) +include(pico_sdk_import.cmake) + +project(picopal C CXX ASM) + +# Initialise the Raspberry Pi Pico SDK +pico_sdk_init() + +# Add executable. Default name is the project name, version 0.1 +add_executable(picopal) + +pico_generate_pio_header(${PROJECT_NAME} + ${CMAKE_CURRENT_LIST_DIR}/video_dma.pio +) + +target_sources(picopal PRIVATE main.c) + + +pico_set_program_name(picopal "picopal") +pico_set_program_version(picopal "0.1") + +pico_enable_stdio_uart(picopal 1) +pico_enable_stdio_usb(picopal 1) + +# Add the standard library to the build +target_link_libraries(picopal pico_stdlib) + +# Add any user requested libraries +target_link_libraries(picopal + hardware_dma + hardware_pio + hardware_timer + hardware_clocks + ) + +pico_add_extra_outputs(picopal) diff --git a/main.c b/main.c new file mode 100644 index 0000000..deef338 --- /dev/null +++ b/main.c @@ -0,0 +1,231 @@ +// 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 + +// --------------------------- +// Video format +// --------------------------- +#define VIDEO_WIDTH 768 +#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.000052f // 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]; + +// 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; + +// --------------------------- +// 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 < 8 || x >= VIDEO_WIDTH - 8 || y < 8 || y >= VIDEO_HEIGHT - 8) { + 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_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 = 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)); +} + +// --------------------------- +// 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) { + 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_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. +// --------------------------- +static void pio0_irq0_handler(void) { + if (pio_interrupt_get(VIDEO_PIO, 0)) { + pio_interrupt_clear(VIDEO_PIO, 0); + + // 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. + 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 = pio_add_program(VIDEO_PIO, &cvdata_program); + + // 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_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); + 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; + + 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(); + } +} \ No newline at end of file diff --git a/pico_sdk_import.cmake b/pico_sdk_import.cmake new file mode 100644 index 0000000..28efe9e --- /dev/null +++ b/pico_sdk_import.cmake @@ -0,0 +1,62 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/video_dma.pio b/video_dma.pio new file mode 100644 index 0000000..5f6d119 --- /dev/null +++ b/video_dma.pio @@ -0,0 +1,99 @@ +; video_dma.pio +; +; Composite video generator: +; - 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. + +.define DATA_DELAY 4 +.define LINE_IRQ 0 +.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 + +.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 + 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 hsync_blank_end side 1 [14] + jmp x-- hsync_blank_start side 0 [1] +hsync_blank_end: + + ; 288 active video lines + mov x, osr side 0 [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 +.wrap + + +.program cvdata + +; X = pixels_per_line - 1, preloaded once from C +; Each line: +; clear output +; reload Y from X +; wait for sync SM to raise LINE_IRQ +; shift out one bit per loop + +.wrap_target + set pins, 0 + mov y, x + wait 1 irq LINE_IRQ + +data_out: + out pins, 1 [DATA_DELAY] + jmp y-- data_out +.wrap \ No newline at end of file