Basic pattern gen

This commit is contained in:
2026-04-01 12:27:10 +02:00
commit 4a4a4a6692
7 changed files with 494 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

18
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -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
}

34
.vscode/launch.json vendored Normal file
View File

@@ -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"
]
}
]
}

49
CMakeLists.txt Normal file
View File

@@ -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)

231
main.c Normal file
View File

@@ -0,0 +1,231 @@
// main.c
#include <stdio.h>
#include <string.h>
#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();
}
}

62
pico_sdk_import.cmake Normal file
View File

@@ -0,0 +1,62 @@
# This is a copy of <PICO_SDK_PATH>/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})

99
video_dma.pio Normal file
View File

@@ -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