Compare commits

...

9 Commits

9 changed files with 597 additions and 328 deletions

View File

@ -18,6 +18,9 @@ add_compile_options(-Wall
add_executable(ceda2vga
src/main.c
src/draw_demo.c
src/framebuffer.c
src/vga.c
)
pico_generate_pio_header(ceda2vga

170
src/draw_demo.c Normal file
View File

@ -0,0 +1,170 @@
#include "draw_demo.h"
#include "framebuffer.h"
#include "pico/platform.h"
#include <string.h>
static bool valid_coord(uint16_t x, uint16_t y)
{
if (x < 0 || x >= HPIXEL)
return false;
if (y < 0 || y >= VPIXEL)
return false;
return true;
}
// just because i did not need all the letters
// and didn't want to fill a proper array :-)
const Letter letters[] = {
{'A', 6, (Point[]){{0, 7}, {0, 0}, {7, 0}, {7, 7}, {7, 3}, {0, 3}}},
{'G', 6, (Point[]){{7, 0}, {0, 0}, {0, 7}, {7, 7}, {7, 3}, {3, 3}}},
{'L', 3, (Point[]){{0, 0}, {0, 7}, {7, 7}}},
{'M', 5, (Point[]){{0, 7}, {0, 0}, {3, 3}, {7, 0}, {7, 7}}},
{'O', 5, (Point[]){{0, 0}, {0, 7}, {7, 7}, {7, 0}, {0, 0}}},
{'P', 5, (Point[]){{0, 7}, {0, 0}, {7, 0}, {7, 3}, {0, 3}}},
{'R', 6, (Point[]){{0, 7}, {0, 0}, {7, 0}, {7, 3}, {0, 3}, {7, 7}}},
{'S', 6, (Point[]){{0, 7}, {7, 7}, {7, 3}, {0, 3}, {0, 0}, {7, 0}}},
};
void draw_path(uint16_t x, uint16_t y, Point points[], size_t len)
{
Point *old_point = &points[0];
for (int i = 1; i < len; ++i)
{
Point *current_point = &points[i];
draw_line(x + old_point->x, y + old_point->y, x + current_point->x, y + current_point->y,
true);
old_point = current_point;
}
}
void draw_letter(uint16_t x, uint16_t y, char c)
{
for (int i = 0; i < count_of(letters); ++i)
{
if (letters[i].letter == c)
{
draw_path(x, y, letters[i].points, letters[i].len);
break;
}
}
}
void draw_string(uint16_t x, uint16_t y, const char *s)
{
while (*s != 0)
{
draw_letter(x, y, *s);
s++;
x += 9;
}
}
void draw_demo(void)
{
// draw a little demo
// clear frame buffer
memset(&frame[0], 0, sizeof(frame));
// horizontal lines
draw_line(0, 0, 640 - 1, 0, true);
draw_line(0, 240 - 1, 640 - 1, 240 - 1, true);
draw_line(0, 480 - 1, 640 - 1, 480 - 1, true);
// vertical lines
draw_line(0, 0, 0, 480 - 1, true);
draw_line(320 - 1, 0, 320 - 1, 480 - 1, true);
draw_line(638, 0, 638, 480 - 1, true); // last pixel of a line can't be set, see set_pixel()
// cat eye
for (uint16_t x = 0; x < 640; x += 20)
{
draw_line(x, 0, 0, (480 - x * 480 / 640), true);
draw_line(640, x * 480 / 640, 640 - x, 480, true);
}
// string
draw_string(200, 200, "GLG PROGRAMS");
}
void draw_simple_line(uint16_t x, uint16_t y, uint16_t len, bool horizontal)
{
for (uint16_t i = 0; i < len; ++i)
{
if (horizontal)
set_pixel(x + i, y);
else
set_pixel(x, y + i);
}
}
void draw_line(uint16_t _x0, uint16_t _y0, uint16_t _x1, uint16_t _y1, bool fill)
{
const float x0 = _x0;
const float y0 = _y0;
const float x1 = _x1;
const float y1 = _y1;
if (x0 != x1)
{
const uint16_t x_begin = MIN(x0, x1);
const uint16_t x_end = MAX(x0, x1);
for (uint16_t x = x_begin; x < x_end; ++x)
{
const uint16_t y = (y1 - y0) * (x - x0) / (x1 - x0) + y0;
if (fill)
set_pixel(x, y);
else
clear_pixel(x, y);
}
}
else
{
const uint16_t y_begin = MIN(y0, y1);
const uint16_t y_end = MAX(y0, y1);
for (uint16_t y = y_begin; y < y_end; ++y)
{
if (fill)
set_pixel(x0, y);
else
clear_pixel(x0, y);
}
}
}
void set_pixel(uint16_t x, uint16_t y)
{
// ignore out of frame pixels
if (!valid_coord(x, y))
return;
// Since there is not much space in PIO program memory,
// it is not possible to set the last pixel of a line,
// because there is not enough program space to unset it
// in the hardware, and, if left set, will mess with
// the sync signals.
if (x == 639)
return;
// skip wasted blanking lines
// (see frame struct declaration)
y += VBBLANK + VSPULSE - 1;
// set bit
frame[y * 80 + x / 8] |= (1 << x % 8);
}
void clear_pixel(uint16_t x, uint16_t y)
{
if (!valid_coord(x, y))
return;
y += VBBLANK + VSPULSE - 1;
frame[y * 80 + x / 8] &= ~(1 << x % 8);
}

30
src/draw_demo.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef DRAW_DEMO_H
#define DRAW_DEMO_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
typedef struct Point
{
uint16_t x;
uint16_t y;
} Point;
typedef struct Letter
{
char letter;
size_t len;
Point *points;
} Letter;
void draw_path(uint16_t x, uint16_t y, Point points[], size_t len);
void draw_letter(uint16_t x, uint16_t y, char c);
void draw_string(uint16_t x, uint16_t y, const char *s);
void draw_demo(void);
void draw_simple_line(uint16_t x, uint16_t y, uint16_t len, bool horizontal);
void draw_line(uint16_t _x0, uint16_t _y0, uint16_t _x1, uint16_t _y1, bool fill);
void set_pixel(uint16_t x, uint16_t y);
void clear_pixel(uint16_t x, uint16_t y);
#endif // DRAW_DEMO_H

8
src/framebuffer.c Normal file
View File

@ -0,0 +1,8 @@
#include "framebuffer.h"
// Frame buffer.
// Due to limitations in program space of PIO interface,
// additional blank lines are needed and sent to the display,
// to fill the vertical blanking interval at the top of the display.
// (at 1BPP, this wastes about 1k)
uint8_t frame[HPIXEL * (VSPULSE + VBBLANK + VPIXEL) * BPP / 8] = {0};

16
src/framebuffer.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef FRAMEBUFFER_H
#define FRAMEBUFFER_H
#include <stdint.h>
#define HPIXEL 640 // number of pixels displayed horizontally
#define VPIXEL 480 // number of pixels displayed vertically
#define BPP 1 // bits per pixel
#define VSPULSE 3 // number of lines during the vertical sync pulse
#define VBBLANK 16 // number of lines of the initial back porch blanking period
#define VFBLANK 1 // number of lines of the final front porch blanking period
extern uint8_t frame[HPIXEL * (VSPULSE + VBBLANK + VPIXEL) * BPP / 8];
#endif

View File

@ -7,335 +7,39 @@
*
*/
#include "hardware/clocks.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "hardware/pll.h"
#include "hardware/structs/pio.h"
#include "pico/stdlib.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include "vga.pio.h"
#define HPIXEL 640 // number of pixels displayed horizontally
#define VPIXEL 480 // number of pixels displayed vertically
#define BPP 1 // bits per pixel
#define VSPULSE 3 // number of lines during the vertical sync pulse
#define VBBLANK 16 // number of lines of the initial back porch blanking period
#define VFBLANK 1 // number of lines of the final front porch blanking period
// Frame buffer.
// Due to limitations in program space of PIO interface,
// additional blank lines are needed and sent to the display,
// to fill the vertical blanking interval at top and bottom
// of the display
// (at 1BPP, this wastes about 1k)
uint8_t frame[HPIXEL * (VSPULSE + VBBLANK + VPIXEL) * BPP / 8] = {0};
static void vga_pixel_program_init(PIO pio, uint sm, uint offset)
{
// magic definition of the called function?
pio_sm_config config = vga_free_run_program_get_default_config(offset);
// destination pins for OUT instructions
sm_config_set_out_pins(&config, 22, 1);
sm_config_set_out_shift(&config, true, true, 0);
pio_gpio_init(pio, 22);
pio_sm_set_consecutive_pindirs(pio, sm, 22, 1, true);
sm_config_set_wrap(&config, offset + vga_free_run_wrap_target, offset + vga_free_run_wrap);
pio_sm_init(pio, sm, offset, &config);
pio_sm_set_clkdiv_int_frac(pio, sm, 1, 0);
pio_sm_set_enabled(pio, sm, true);
}
static void vga_hsync_program_init(PIO pio, uint sm, uint offset)
{
pio_sm_config config = vga_hsync_program_get_default_config(offset);
// destination pins for SET instructions
sm_config_set_set_pins(&config, 20, 1);
pio_gpio_init(pio, 20);
pio_sm_set_consecutive_pindirs(pio, sm, 20, 1, true);
sm_config_set_wrap(&config, offset + vga_hsync_wrap_target, offset + vga_hsync_wrap);
pio_sm_init(pio, sm, offset, &config);
pio_sm_set_clkdiv_int_frac(pio, sm, 1, 0);
pio->sm->shiftctrl = (1 << PIO_SM0_SHIFTCTRL_FJOIN_TX_LSB);
pio_sm_set_enabled(pio, sm, true);
}
static void vga_vsync_program_init(PIO pio, uint sm, uint offset)
{
pio_sm_config config = vga_vsync_program_get_default_config(offset);
// destination pins for SET instructions
sm_config_set_set_pins(&config, 21, 1);
pio_gpio_init(pio, 21);
pio_sm_set_consecutive_pindirs(pio, sm, 21, 1, true);
sm_config_set_wrap(&config, offset + vga_vsync_wrap_target, offset + vga_vsync_wrap);
pio_sm_init(pio, sm, offset, &config);
pio_sm_set_clkdiv_int_frac(pio, sm, 1, 0);
pio->sm->shiftctrl = (1 << PIO_SM0_SHIFTCTRL_FJOIN_TX_LSB);
pio_sm_set_enabled(pio, sm, true);
}
void setup_clocks(void)
{
// disable resuscitation clock
clocks_hw->resus.ctrl = 0;
// before changing PLL, switch sys and ref cleanly away from their aux sources
hw_clear_bits(&clocks_hw->clk[clk_sys].ctrl, CLOCKS_CLK_SYS_CTRL_SRC_BITS);
while (clocks_hw->clk[clk_sys].selected != 0x1)
tight_loop_contents();
hw_clear_bits(&clocks_hw->clk[clk_ref].ctrl, CLOCKS_CLK_REF_CTRL_SRC_BITS);
while (clocks_hw->clk[clk_ref].selected != 0x1)
tight_loop_contents();
// set PLL at 126 MHz, and wait for it to stabilize
pll_init(pll_sys, 1, 1512 * MHZ, 6, 2);
// re-configure sys_clk to use PLL
clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX,
CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 126 * MHZ, 126 * MHZ);
// re-configure CLK PERI = clk_sys
clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, 126 * MHZ, 126 * MHZ);
}
static bool dma_ready = false;
static int dma_channel;
/**
* @brief Represents a program running on the SM of a PIO.
*
*/
typedef struct PIORun
{
PIO pio; //< executing PIO
uint offset; //< PIO memory offset for program
uint sm; //< executing SM
} PIORun;
static PIORun *vga_pixel_ptr = NULL;
static unsigned long int frame_counter = 0;
void dma_handler(void)
{
// acknoweledge DMA
dma_hw->ints0 = 1 << dma_channel;
}
bool valid_coord(uint16_t x, uint16_t y)
{
if (x < 0 || x >= HPIXEL)
return false;
if (y < 0 || y >= VPIXEL)
return false;
return true;
}
void set_pixel(uint16_t x, uint16_t y)
{
// ignore out of frame pixels
if (!valid_coord(x, y))
return;
// Since there is not much space in PIO program memory,
// it is not possible to set the last pixel of a line,
// because there is not enough program space to unset it
// in the hardware, and, if left set, will mess with
// the sync signals.
if (x == 639)
return;
// skip wasted blanking lines
// (see frame struct declaration)
y += VBBLANK + VSPULSE - 1;
// set bit
frame[y * 80 + x / 8] |= (1 << x % 8);
}
void clear_pixel(uint16_t x, uint16_t y)
{
if (!valid_coord(x, y))
return;
y += VBBLANK + VSPULSE - 1;
frame[y * 80 + x / 8] &= ~(1 << x % 8);
}
void draw_simple_line(uint16_t x, uint16_t y, uint16_t len, bool horizontal)
{
for (uint16_t i = 0; i < len; ++i)
{
if (horizontal)
set_pixel(x + i, y);
else
set_pixel(x, y + i);
}
}
void new_frame_handler(void)
{
if (vga_pixel_ptr && dma_ready)
{
// Explicitly put the first uint32_t word in the FIFO,
// because, when PIO pulls data for the first time,
// and its FIFO is empty, the DMA controller takes "a while"
// to put the first word and keep up with the transfer,
// making the whole frame a bit offset to the right.
// Since front porch is very short, even the smallest delay,
// may disrupt the timing.
// Also note: despite the "blocking put", when this interrupt
// is serviced, PIO FIFO is always empty, so in practice
// this will never block.
pio_sm_put_blocking(vga_pixel_ptr->pio, vga_pixel_ptr->sm, 0);
// Start DMA (but skip the initial word, which has been put manually)
dma_channel_set_read_addr(dma_channel, &frame[4], true);
}
frame_counter++;
pio_interrupt_clear(pio0_hw, 1);
}
#include "draw_demo.h"
#include "framebuffer.h"
#include "vga.h"
int main()
{
setup_clocks();
stdio_init_all();
sleep_ms(1000);
// clear frame buffer
memset(&frame[0], 0, sizeof(frame));
// draw a cross
// horizontal lines
for (uint16_t x = 0; x < 640; ++x)
{
if ((x / 32) % 2 == 0)
set_pixel(x, 0);
if ((x / 32) % 2 == 1)
set_pixel(x, 2);
set_pixel(x, 239);
set_pixel(x, 479);
}
// vertical lines
for (uint16_t y = 0; y < 480; ++y)
{
set_pixel(0 + (y / 10) * 4, y);
set_pixel(0, y);
set_pixel(319, y);
// rightmost pixels can't be set, see description
set_pixel(638, y);
}
// try to find strangeness using stairs
for (uint16_t x = 0; x < 640; ++x)
{
set_pixel(x, (x / 32) * 4 + 120);
}
// draw "E" - nice because it's not symmetrical along X
draw_simple_line(100, 100, 8, true);
draw_simple_line(100, 100, 8, false);
draw_simple_line(100, 104, 8, true);
draw_simple_line(100, 108, 8, true);
draw_demo();
// Running programs on PIO
PIORun vga_hsync, vga_vsync, vga_pixel;
vga_hsync.pio = vga_vsync.pio = vga_pixel.pio = pio0;
// Prepare frame interrupt
// Frame interrupt is asserted on PIO0 interrupt 1 by SM1
pio_set_irq1_source_enabled(vga_vsync.pio, pis_interrupt1,
true); // allow SM1 of PIO to fire the interrupt
irq_set_exclusive_handler(PIO0_IRQ_1, new_frame_handler); // set handler for IRQ1 of PIO
irq_set_enabled(PIO0_IRQ_1, true); // enable interrupt
// VGA HSYNC program
printf("Starting VGA hsync machine... ");
if (!pio_can_add_program(vga_hsync.pio, &vga_hsync_program))
panic("cannot add program");
vga_hsync.offset = pio_add_program(vga_hsync.pio, &vga_hsync_program);
vga_hsync.sm = pio_claim_unused_sm(vga_hsync.pio, true);
vga_hsync_program_init(vga_hsync.pio, vga_hsync.sm, vga_hsync.offset);
// low pulse is X pixels long, and SM runs at 4x of pixel clock
pio_sm_put_blocking(vga_hsync.pio, vga_hsync.sm, (64 - 2));
pio_sm_put_blocking(vga_hsync.pio, vga_hsync.sm, (640 + 16 - 1)); // 16 is front porch
printf("OK\n");
// VGA VSYNC program
printf("Starting VGA vsync machine... ");
if (!pio_can_add_program(vga_vsync.pio, &vga_vsync_program))
panic("cannot add program");
vga_vsync.offset = pio_add_program(vga_vsync.pio, &vga_vsync_program);
vga_vsync.sm = pio_claim_unused_sm(vga_vsync.pio, true);
vga_vsync_program_init(vga_vsync.pio, vga_vsync.sm, vga_vsync.offset);
// low pulse assert lasts for 3 horizontal lines, then adjust by one
pio_sm_put_blocking(vga_vsync.pio, vga_vsync.sm, 3 - 1);
pio_sm_put_blocking(vga_vsync.pio, vga_vsync.sm, 500 - 3 - 1);
printf("OK\n");
// VGA pixel program
vga_pixel_ptr = &vga_pixel;
printf("Starting VGA pixel machine... ");
if (!pio_can_add_program(vga_pixel.pio, &vga_free_run_program))
panic("cannot add program");
vga_pixel.offset = pio_add_program(vga_pixel.pio, &vga_free_run_program);
vga_pixel.sm = pio_claim_unused_sm(vga_pixel.pio, false);
vga_pixel_program_init(vga_pixel.pio, vga_pixel.sm, vga_pixel.offset);
pio_sm_put_blocking(vga_pixel.pio, vga_pixel.sm, 640 - 1);
printf("OK\n");
printf("Setting up DMA... ");
dma_channel = dma_claim_unused_channel(true);
dma_channel_config dma_config = dma_channel_get_default_config(dma_channel);
channel_config_set_transfer_data_size(&dma_config, DMA_SIZE_32);
channel_config_set_read_increment(&dma_config, true);
channel_config_set_write_increment(&dma_config, false);
channel_config_set_dreq(&dma_config, DREQ_PIO0_TX2);
dma_channel_set_irq0_enabled(dma_channel, true);
irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
irq_set_enabled(DMA_IRQ_0, true);
// -1 because first word is manually injected
// before the first DMA request, in order to
// avoid any possible delay
dma_channel_configure(dma_channel, &dma_config, &pio0_hw->txf[2], &frame[0],
HPIXEL * (VSPULSE + VPIXEL + VBBLANK) / 32 - 1, true);
dma_ready = true;
printf("OK\n");
printf("Now running.\n");
vga_machines_init();
for (;;)
{
// fancy drawings
const uint16_t fc = frame_counter;
// moving sine
const uint16_t fc = vga_get_frame_counter();
const uint16_t x = fc % 640;
const uint16_t y = 239 + 100 * (abs(320 - x) / 640.0) * sin((fc % 640) / 12.738);
if ((fc / 640) % 2)
set_pixel(x, y);
else
clear_pixel(x, y);
sleep_ms(5);
const uint16_t y = 380 + 100 * (abs(320 - x) / 640.0) * sin((fc % 640) / 12.738);
set_pixel(x, y);
// 13 ms <=> 75 Hz
// Actually, there is a point in going in sync with vertical refresh,
// but let's just forget this for the demo.
sleep_ms(13);
clear_pixel(x, y);
}
}

275
src/vga.c Normal file
View File

@ -0,0 +1,275 @@
#include "vga.h"
#include <stdio.h>
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "hardware/structs/pio.h"
#include "hardware/clocks.h"
#include "hardware/pll.h"
#include "framebuffer.h"
#include "vga.pio.h"
/**
* @brief Represents a program running on the SM of a PIO.
*
*/
typedef struct PIORun
{
PIO pio; //< executing PIO
uint offset; //< PIO memory offset for program
uint sm; //< executing SM
} PIORun;
/*
There are 3 SM running: horizontal and vertical sync, and pixels.
See vga.pio for details.
*/
static PIORun vga_hsync, vga_vsync, vga_pixel;
static bool dma_ready = false; //< current DMA acceleration status
static int dma_channel; //< DMA channel number, used to refill the vga_pixel FIFO
static unsigned long int frame_counter = 0; //< current frame
/**
* @brief Setup system clocks.
*
* Main system clock must run at 126MHz,
* which is 4 times pixel clock (31.5 MHz).
* This allows SM to output one pixel every 4 clock cycles,
* and to do some stuff in the mean time (eg. sending interrupts to sync with the other SM)
*
*/
static void setup_clocks(void)
{
// disable resuscitation clock
clocks_hw->resus.ctrl = 0;
// before changing PLL, switch sys and ref cleanly away from their aux sources
hw_clear_bits(&clocks_hw->clk[clk_sys].ctrl, CLOCKS_CLK_SYS_CTRL_SRC_BITS);
while (clocks_hw->clk[clk_sys].selected != 0x1)
tight_loop_contents();
hw_clear_bits(&clocks_hw->clk[clk_ref].ctrl, CLOCKS_CLK_REF_CTRL_SRC_BITS);
while (clocks_hw->clk[clk_ref].selected != 0x1)
tight_loop_contents();
// set PLL at 126 MHz, and wait for it to stabilize
pll_init(pll_sys, 1, 1512 * MHZ, 6, 2);
// re-configure sys_clk to use PLL
clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX,
CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 126 * MHZ, 126 * MHZ);
// re-configure CLK PERI = clk_sys
clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, 126 * MHZ, 126 * MHZ);
}
/**
* @brief Initializes the VGA pixel shift machine.
*
* @param pio PIO peripheral to use.
* @param sm SM to use.
* @param offset PIO memory offset for program.
*/
static void vga_pixel_program_init(PIO pio, uint sm, uint offset)
{
// config function is automatically declared by SDK scripts
// based on the names given to programs in .pio
pio_sm_config config = vga_pixel_program_get_default_config(offset);
// destination pins for OUT instructions
sm_config_set_out_pins(&config, 22, 1);
sm_config_set_out_shift(&config, true, true, 0);
pio_gpio_init(pio, 22);
pio_sm_set_consecutive_pindirs(pio, sm, 22, 1, true);
sm_config_set_wrap(&config, offset + vga_pixel_wrap_target, offset + vga_pixel_wrap);
pio_sm_init(pio, sm, offset, &config);
pio_sm_set_clkdiv_int_frac(pio, sm, 1, 0);
pio_sm_set_enabled(pio, sm, true);
}
/**
* @brief Initializes the VGA horizontal sync machine.
*
* @param pio PIO peripheral to use.
* @param sm SM to use.
* @param offset PIO memory offset for program.
*/
static void vga_hsync_program_init(PIO pio, uint sm, uint offset)
{
pio_sm_config config = vga_hsync_program_get_default_config(offset);
// destination pins for SET instructions
sm_config_set_set_pins(&config, 20, 1);
pio_gpio_init(pio, 20);
pio_sm_set_consecutive_pindirs(pio, sm, 20, 1, true);
sm_config_set_wrap(&config, offset + vga_hsync_wrap_target, offset + vga_hsync_wrap);
pio_sm_init(pio, sm, offset, &config);
pio_sm_set_clkdiv_int_frac(pio, sm, 1, 0);
pio->sm->shiftctrl = (1 << PIO_SM0_SHIFTCTRL_FJOIN_TX_LSB);
pio_sm_set_enabled(pio, sm, true);
}
/**
* @brief Initializes the VGA vertical sync machine.
*
* @param pio PIO peripheral to use.
* @param sm SM to use.
* @param offset PIO memory offset for program.
*/
static void vga_vsync_program_init(PIO pio, uint sm, uint offset)
{
pio_sm_config config = vga_vsync_program_get_default_config(offset);
// destination pins for SET instructions
sm_config_set_set_pins(&config, 21, 1);
pio_gpio_init(pio, 21);
pio_sm_set_consecutive_pindirs(pio, sm, 21, 1, true);
sm_config_set_wrap(&config, offset + vga_vsync_wrap_target, offset + vga_vsync_wrap);
pio_sm_init(pio, sm, offset, &config);
pio_sm_set_clkdiv_int_frac(pio, sm, 1, 0);
pio->sm->shiftctrl = (1 << PIO_SM0_SHIFTCTRL_FJOIN_TX_LSB);
pio_sm_set_enabled(pio, sm, true);
}
/**
* @brief DMA interrupt handler.
*/
static void dma_handler(void)
{
// acknoweledge DMA
dma_hw->ints0 = 1 << dma_channel;
}
/**
* @brief Vertical sync interrupt handler.
*
*/
static void new_frame_handler(void)
{
if (dma_ready)
{
// Explicitly put the first uint32_t word in the FIFO,
// because, when PIO pulls data for the first time,
// and its FIFO is empty, the DMA controller takes "a while"
// to put the first word and keep up with the transfer,
// making the whole frame a bit offset to the right.
// Since front porch is very short, even the smallest delay,
// may disrupt the timing.
// Also note: despite the "blocking put", when this interrupt
// is serviced, PIO FIFO is always empty, so in practice
// this will never block.
pio_sm_put_blocking(vga_pixel.pio, vga_pixel.sm, 0);
// Start DMA (but skip the initial word, which has been put manually)
dma_channel_set_read_addr(dma_channel, &frame[4], true);
}
frame_counter++;
pio_interrupt_clear(pio0_hw, 1);
}
void vga_machines_init(void)
{
// setup a proper system clock
setup_clocks();
// Running programs on PIO
vga_hsync.pio = vga_vsync.pio = vga_pixel.pio = pio0;
// Prepare frame interrupt
// Frame interrupt is asserted on PIO0 interrupt 1 by SM1
pio_set_irq1_source_enabled(vga_vsync.pio, pis_interrupt1,
true); // allow SM1 of PIO to fire the interrupt
irq_set_exclusive_handler(PIO0_IRQ_1, new_frame_handler); // set handler for IRQ1 of PIO
irq_set_enabled(PIO0_IRQ_1, true); // enable interrupt
// VGA HSYNC program
printf("Starting VGA hsync machine... ");
if (!pio_can_add_program(vga_hsync.pio, &vga_hsync_program))
panic("cannot add program");
vga_hsync.offset = pio_add_program(vga_hsync.pio, &vga_hsync_program);
vga_hsync.sm = pio_claim_unused_sm(vga_hsync.pio, true);
vga_hsync_program_init(vga_hsync.pio, vga_hsync.sm, vga_hsync.offset);
// push configuration words to horizontal sync machine FIFO
// - hsync pulse duration (in pixel), assert time (low)
// - hsync pulse duration (in pixel), idle time (high), minus back porch
pio_sm_put_blocking(vga_hsync.pio, vga_hsync.sm, (64 - 2));
pio_sm_put_blocking(vga_hsync.pio, vga_hsync.sm, (640 + 16 - 1)); // 16 is front porch
printf("OK\n");
// VGA VSYNC program
printf("Starting VGA vsync machine... ");
if (!pio_can_add_program(vga_vsync.pio, &vga_vsync_program))
panic("cannot add program");
vga_vsync.offset = pio_add_program(vga_vsync.pio, &vga_vsync_program);
vga_vsync.sm = pio_claim_unused_sm(vga_vsync.pio, true);
vga_vsync_program_init(vga_vsync.pio, vga_vsync.sm, vga_vsync.offset);
// push configuration words to vertical sync machine FIFO
// - vsync pulse duration (in lines), assert time (low)
// - vsync pulse duration (in lines), idle time (high)
pio_sm_put_blocking(vga_vsync.pio, vga_vsync.sm, 3 - 1);
pio_sm_put_blocking(vga_vsync.pio, vga_vsync.sm, 500 - 3 - 1);
printf("OK\n");
// VGA pixel program
printf("Starting VGA pixel machine... ");
if (!pio_can_add_program(vga_pixel.pio, &vga_pixel_program))
panic("cannot add program");
vga_pixel.offset = pio_add_program(vga_pixel.pio, &vga_pixel_program);
vga_pixel.sm = pio_claim_unused_sm(vga_pixel.pio, false);
vga_pixel_program_init(vga_pixel.pio, vga_pixel.sm, vga_pixel.offset);
// push configuration word to pixel machine FIFO
// - number of visible pixels in a line
pio_sm_put_blocking(vga_pixel.pio, vga_pixel.sm, 640 - 1);
printf("OK\n");
printf("Setting up DMA... ");
dma_channel = dma_claim_unused_channel(true);
dma_channel_config dma_config = dma_channel_get_default_config(dma_channel);
channel_config_set_transfer_data_size(&dma_config, DMA_SIZE_32);
channel_config_set_read_increment(&dma_config, true);
channel_config_set_write_increment(&dma_config, false);
// link Data Request signal of PIO SM2 TX FIFO to DMA
channel_config_set_dreq(&dma_config, DREQ_PIO0_TX2);
// setup transfer acknowledge interrupt
dma_channel_set_irq0_enabled(dma_channel, true);
irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
irq_set_enabled(DMA_IRQ_0, true);
// pull one word less, because first word is manually injected
// before the first DMA request, in order to avoid any possible delay
// (see new frame interrupt handler)
dma_channel_configure(dma_channel, &dma_config, &pio0_hw->txf[2], &frame[0],
HPIXEL * (VSPULSE + VPIXEL + VBBLANK) / 32 - 1, true);
dma_ready = true;
printf("OK\n");
printf("Now running.\n");
}
/**
* @brief Get current frame counter.
*
* @return Current frame counter.
*/
unsigned long int vga_get_frame_counter(void)
{
return frame_counter;
}

7
src/vga.h Normal file
View File

@ -0,0 +1,7 @@
#ifndef VGA_H
#define VGA_H
void vga_machines_init(void);
unsigned long int vga_get_frame_counter(void);
#endif // VGA_H

View File

@ -1,60 +1,116 @@
.program vga_free_run
/*
- horizontal sync pulse generation
this machine commands the timing for the other machines too
- vertical sync pulse generation
counts the horizontal sync pulses and generated a vertical sync pulse accordingly
- pixel machine
horizontal sync machine also sends an interrupt when the back porch is finished,
thus triggering the pixel machine to start shifting out the pixels
*/
entrypoint_vga_free_run:
/*
Pixel Machine
Wait for the "start-of-visible-line" interrupt signal,
and output 640 pixels
*/
.program vga_pixel
entrypoint_vga_pixel:
// pull configuration into Y register
// Y = number of visible pixels in the line
pull
mov y, osr
.wrap_target
mov x, y
wait irq 7
// loop lasts 4 SM clocks, which corresponds to 1 VGA pixel
loop:
out pins, 1 [2]
jmp x-- loop
.wrap
/*
Horizontal Sync Machine
A line is composed by 840 pixels:
- 64 sync pulse
- 120 back porch
- 640 visible area
- 16 front porch
*/
.program vga_hsync
entrypoint_vga_hsync:
// pull configuration into ISR and OSR registers
// ISR = sync pulse duration - 2 (64 pixel)
// OSR = visible line + front porch - 1 (640 + 16 pixel)
// Note: configuration contains desired value -1/-2,
// because of how end-of-loop check is performed
// and how time is spent in overhead instructions
pull
mov isr, osr
pull
// once configured, no more data is pulled from the TX FIFO,
// so program wraps around here
.wrap_target
set pins, 0 [1]
irq set 0
mov x, isr
set pins, 0 [1] // start pulse low
irq set 0 // trigger "start-of-line" interrupt signal
mov x, isr // load register X with sync pulse duration
// +4 SM clocks elapsed (first VGA pixel)
// loop for the duration of the low pulse (remaining 63 pixels)
hsync_pulse:
jmp x-- hsync_pulse [3]
set pins, 1 [1]
set y, 27
set pins, 1 [1] // start pulse high (.5 pixel)
// hardcoded backporch duration: 120 pixel => 120 x 4 = 480 SM clocks
// but one pixel is already in the surronding logic, then only 476 clocks are cycled
// where 476 = (27 + 1) * (16 + 1)
set y, 27 // another .25 pixel
hsync_back_porch:
jmp y-- hsync_back_porch [16]
irq set 7
irq set 7 // trigger "start-of-visible-line" interrupt signal
// (.25 more pixel)
mov x, osr [3]
mov x, osr [3] // load register X with remaining line duration (visible line + front porch)
// also, just make this last 4 SM clocks (1 pixel)
hsync_idle:
jmp x-- hsync_idle [3]
.wrap
/*
Vertical Sync Machine
A frame is composed by 500 lines:
- 3 vertical sync pulse
- 16 back porch
- 480 visible lines
- 1 front porch
*/
.program vga_vsync
entrypoint_vga_vsync:
// Pull configuration into ISR and OSR registers
// ISR = sync pulse assert duration - 1 (3 lines)
// OSR = sync pulse idle duration - 1 (497 lines)
pull
mov isr, osr
pull
// once configured, no more data is pulled from the TX FIFO,
// so program wraps around here
.wrap_target
mov x, isr
mov x, isr // load register X with low pulse duration
vsync_pulse:
wait irq 0
set pins, 0
wait irq 0 // wait for "start-of-line" interrupt from hsync machine
set pins, 0 // start pulse low
jmp x-- vsync_pulse
// trigger "start-of-frame" interrupt
// (actually, 3 lines of the back porch are already passed)
irq set 1
mov x, osr
mov x, osr // load register X with high pulse duration
vsync_idle:
wait irq 0
set pins, 1