Started with font rendering

This commit is contained in:
2026-04-01 17:39:05 +02:00
parent 8cce172c95
commit 435a96d203
6 changed files with 633 additions and 207 deletions

293
main.c
View File

@@ -1,231 +1,116 @@
// 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 "pico/multicore.h"
#include "video_dma.pio.h"
#include "cvideo.h"
// ---------------------------
// User-adjustable pins
// ---------------------------
#define VIDEO_PIO pio0
#define DATA_SM 0
#define SYNC_SM 1
#include "font.xbm"
#define DATA_PIN 2 // pixel / luma bit pin
#define SYNC_PIN 3 // sync pin
static video_framebuffer_t fb0, fb1;
// ---------------------------
// 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 START_X 35
#define END_X (VIDEO_WIDTH-40)
#define START_Y 20
#define END_Y (VIDEO_HEIGHT-40)
#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 CHARS_PER_LINE 32
#define CHAR_LINES 20
// 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
static char text_mode_buffer[20][32];
// ---------------------------
// Framebuffer
// One bit per pixel, 32 pixels per word
// ---------------------------
static uint32_t framebuffer[VIDEO_HEIGHT][WORDS_PER_LINE];
static void clear(video_framebuffer_t fb){
memset(fb, 0, sizeof(video_framebuffer_t));
}
// 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;
static void set_bit(video_framebuffer_t fb, uint x, uint y, bool value) {
if(x>=VIDEO_WIDTH || y>=VIDEO_HEIGHT)
return;
uint index_x = x / 32;
uint pos_x = x % 32;
// DMA channel that feeds the PIO TX FIFO
static int video_dma_chan;
uint flag = value << (31-pos_x);
if(value)
fb[y][index_x] |= flag;
else
fb[y][index_x] &= ~flag;
}
// ---------------------------
// Optional test pattern
// Replace this with your own drawing code.
// ---------------------------
#define BORDER 40
static void fill_test_pattern(void) {
memset(framebuffer, 0x00, sizeof(framebuffer));
#define CHAR_WIDTH font_width / 16
#define CHAR_HEIGHT font_height / 16
for (uint y = 0; y < VIDEO_HEIGHT; ++y) {
for (uint x = 0; x < VIDEO_WIDTH; ++x) {
bool on = false;
void draw_character(video_framebuffer_t fb, unsigned int x, unsigned int y, unsigned int scale, char character) {
uint8_t row = character / 16;
uint8_t column = character % 16;
uint32_t font_x = column * 10;
uint32_t font_y = row * 12;
// Simple visible pattern:
// border + checker
if (x < BORDER || x >= VIDEO_WIDTH - BORDER || y < BORDER || y >= VIDEO_HEIGHT - BORDER) {
on = true;
} else if (((x >> 4) ^ (y >> 4)) & 1) {
on = true;
for (int j = font_y; j < font_y + CHAR_HEIGHT; j++) {
for (int i = font_x; i < font_x + CHAR_WIDTH; i++) {
// XBM images pad rows with zeros
uint32_t array_position;
if (font_width %8 != 0) {
array_position = (j * (font_width + (8 - font_width % 8))) + i;
}
else {
array_position = (j * font_width) + i;
}
uint32_t array_index = array_position / 8;
// XBM image, low bits are first
uint32_t byte_position = array_position % 8;
if (on) {
uint word = x >> 5;
uint bit = 31 - (x & 31); // MSB-first
framebuffer[y][word] |= (1u << bit);
uint8_t value = font_bits[array_index] >> byte_position & 1 ;
for (int sx = 0; sx < scale; sx++) {
for (int sy = 0; sy < scale; sy++) {
set_bit(fb, x + ((i - font_x) * scale) + sx, y + ((j- font_y) * scale) + sy, !value);
}
}
}
}
}
// ---------------------------
// 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:
// The sync SM pushes one token into its RX FIFO during each line's back porch.
// 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) {
if (!pio_sm_is_rx_fifo_empty(VIDEO_PIO, SYNC_SM) && !dma_channel_is_busy(video_dma_chan)) {
(void)pio_sm_get(VIDEO_PIO, SYNC_SM);
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 sync-SM RX FIFO notifications to CPU IRQ 0.
pio_set_irq0_source_enabled(VIDEO_PIO, pis_sm1_rx_fifo_not_empty, 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;
// Prime the FIFO so the very first active line does not depend on IRQ latency.
start_dma_for_current_line();
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();
void core1_entry() {
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);
while (true) {
// Your application code goes here.
// Some other code can freely draw into framebuffer[].
gpio_put(25, 1);
sleep_ms(500);
gpio_put(25, 0);
sleep_ms(500);
}
}
void framebuffer_switched_cb(){
}
void main() {
stdio_init_all();
multicore_launch_core1(core1_entry);
clear(fb0);
clear(fb1);
video_init(fb0);
video_set_framebuffer_switched_cb(framebuffer_switched_cb);
int fbnum = 1;
video_framebuffer_ptr_t fbs[] = {fb0, fb1};
int i = 0;
while (true) {
clear(fbs[fbnum]);
for(int y=0; y<20; y++){
for(int x=0; x<32; x++){
draw_character(fbs[fbnum], START_X+x*(CHAR_WIDTH+1)*2, START_Y+y*(CHAR_HEIGHT+1)*2, 2, '0'+(i+x+y)%10);
}
}
video_set_framebuffer(fbs[fbnum]);
fbnum = (fbnum+1)%2;
i = (i+1)%10;
sleep_ms(500);
tight_loop_contents();
}
}