Uses the RP2040's Programmable IO to create a PAL colour video signal. It consists of the PIO program, some "driver" and tools code, a test program and a program for creating colour Look-up Tables (LUTs).
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
rppico-pio-pal/analogVideoDriver.c

551 lines
20 KiB

#include "analogVideoDriver.h"
#include "PAL_LUT_64_4b.h"
#include "build/PAL3.pio.h"
#define PAL_X_RES 230
#define PAL_BACK_PORCH_AFTER_CB 11
static const uint defLineSkipPre = 2, defLineSkipPost = 6;
static const uint defColSkipPre = 11, defColSkipPost = 12;
const uint hResPAL = PAL_X_RES, vResPAL = 610, vLinesTotalPAL = 625;
// these constants are used to control the vertical blank period.
// They are the line numbers at which the vBlank signal is switched
// on or off, respectively. While vSyncBegin are the line numbers of
// the first vSync lines, vSyncEnd are the line numbers of the first
// active video lines.
const uint vSyncBegin[4] = {309, 621, 310, 622}, vSyncEnd[4] = {5, 317, 4, 318};
const uint firstImageLine[2] = {23, 335};
const uint lastImageLine[2] = {308, 620};
const uint visibleLinesPerHalfImage = 285;
// here we store the line numbers between which pixel data is requested
// from the application. Requesting data starts one line before the first
// line's data is actually sent to the PIO module so the application has
// sufficient time for rendering. So, activeAreaBegin is vSyncEnd + lineSkipPre - 1.
// The last call will be made in line activeAreaEnd, which is vSyncBegin - lineSkipPost - 2.
// However, the last DMA transfer of pixel data is started in line activeAreaEnd + 1
uint activeAreaBegin[4], activeAreaEnd[4];
const uint black = 0x44444444;
// colour burst for even and odd lines. Each line shifts the colour carrier by
// -Pi/2 (since there are 283.75 carrier cycles per line).
// Assume 0° phase as the colour burst starts after 24.923 PAL carrier cycles
// (rotate one nibble right if that doesn't work out)
const uint colourBurst[4] = {0x64222466, 0x64222466, 0x24666422, 0x24666422};
// the number of colour bursts
const uint nColourBursts = pio_PAL_DAC_colourBursts;
// the number of carrier clock cycles of the back porch remaining after
// colour burst
const uint nBackPorchCCCycles = PAL_BACK_PORCH_AFTER_CB;
// each line should last 9062.589 processor clock cycles - that makes 64 us.
// 283.75 periods of the correct PAL colour carrier are 9062.5374 processor clock cycles
// As half cycles are a bit tricky the number of 3 cycle samples is varied each line
// which keeps the generated carrier frequency quite close to the correct one.
// SYNCHRONISATION SYMBOLS FOR V-BLANK
// Basically, each sync symbol lasts 4531 clock cycles. In order to compensate
// for the divergence of the generated colour carrier from the target frequency
// there are two versions of each symbol which are used to extend every second
// line by one clock cycle.
// OSR shifts left, so the value to be put into X first needs to be in the
// high 16 bits. Reload threshold must be > 16!
// Equalization pulse
// a sync pulse of 2.35 us followed by black for 29.65 us
// delaying by 16 clock cycles 21 times yields 2.37 us (should be okay);
// two instructions already delay 2*16 cycles, and the following loop delays at least
// 16, so this loop only needs to run 18 times. This is implicitly done to overlap
// the IRQ delay so 0 is passed here.
// 335 cycles are always delayed (2.36577 us)
// The overall symbol lasts 4531 clock cycles, 4196 are left to fill.
// There are 17 implicit cycles, so the delay loop needs to run 4179 times.
const uint shortSyncSymbol[2] = {0x00001053, 0x00001054};
// serration pulse
// a long sync pulse followed by 4.7 us (+- 200ns) of black (666 cycles).
// For the long pulse, 242 16 cycle delays yield 27.344 us, leaving 4.656 us.
// As the sync level has an implicit delay of 21 * 16 cycles, only 221 are left.
// This gives leaves 660 cycles to delay, 17 are implicit, the loop runs 643 times.
const uint longSyncSymbol[2] = {0x00dd0282, 0x00dd0283};
// a half video line, starting with a 4.7 us sync pulse, of which 335 cycles are
// already delayed, so we need to run the 16 cycle loop another 21 times.
// That leaves 3860 - 17 = 3843 cycles of black
const uint halfLine[2] = {0x00150f02, 0x00150f03};
// a full video line with a 4.7 us sync pulse, leaving 8391 - 17 = 8374
// cycles to delay
const uint fullLine[2] = {0x001520b5, 0x001520b6};
// a full line, but starting with an equalization pulse (which is implicit),
// leaving 8727 - 17 = 8710 cycles to delay
const uint eqFullLine[2] = {0x00002205, 0x00002206};
// video buffer, also contains the colour burst
uint videoBuffer[4][PAL_X_RES + pio_PAL_DAC_colourBursts + PAL_BACK_PORCH_AFTER_CB + 1];
// this buffer is used to hold valid data for overscan area (part of the
// visible image which isn't shown by TV). Could we simply use videoBuffer instead?
uint overscanBuffer[4][PAL_X_RES + pio_PAL_DAC_colourBursts + PAL_BACK_PORCH_AFTER_CB + 1];
// pointer to the user areas of the video buffer (i.e., excluding colour burst,
// back porch and possible overscan)
uint *userBuffer[4];
// number of uint32s each videoBuffer array contains
const uint nVideoBufferWords = hResPAL + nColourBursts + nBackPorchCCCycles + 1;
// line overscan, i.e., number of leading pixels which aren't shown by TV
uint videoBufferSkipWords = 0;
// the possible training sequences per line during vertical blank
// ordering: field 1 -> 2, 2 -> 3, 3-> 4, 4 -> 1 (synchronization pattern repeats after four fields)
const uint trainingSequence[4][17] = {
{fullLine[1], shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0], shortSyncSymbol[0],
longSyncSymbol[0], longSyncSymbol[1], longSyncSymbol[0], longSyncSymbol[0], longSyncSymbol[0],
shortSyncSymbol[1], shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[0], eqFullLine[1], 0},
{fullLine[1], halfLine[0], shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[0],
longSyncSymbol[1], longSyncSymbol[0], longSyncSymbol[0], longSyncSymbol[0], longSyncSymbol[1],
shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0]},
{shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0], shortSyncSymbol[0],
longSyncSymbol[0], longSyncSymbol[0], longSyncSymbol[1], longSyncSymbol[0], longSyncSymbol[0],
shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0], shortSyncSymbol[0], eqFullLine[0], fullLine[1], 0},
{halfLine[0], shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[0],
longSyncSymbol[1], longSyncSymbol[0], longSyncSymbol[0], longSyncSymbol[0], longSyncSymbol[1],
shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[0], shortSyncSymbol[1], shortSyncSymbol[0], fullLine[0]}
};
const uint trainingSequenceLength[4] = {16, 17, 16, 17};
// structure holding all internal data
AnalogVideo av;
// some variables for storing driver status, mainly used for debugging.
uint avStatus = AV_STATUS_STOPPED;
uint palLine = 0, videoLine = 0, activeLine = 0, halfImage = 0, field = 0, nActiveLines = 0, nActiveCols = 0;
// masks to generate an index into the colour LUT
const uint rMask = (1 << R_BITS) - 1;
const uint gMask = (1 << G_BITS) - 1;
const uint bMask = (1 << B_BITS) - 1;
// for debug and testing
uint testMode;
// the interrupt handler which keeps track of the display state and moves data to the PIO
void palIrqHandler(void);
// for debug only
void initVideoBuffersForTesting();
// init function, needs to be called first
void analogVideoInit(PIO pio, uint gpioBase, uint vBlankPin) {
testMode = 0;
palLine = vLinesTotalPAL;
halfImage = 3;
videoLine = 0;
activeLine = 0;
field = 4;
// store the colour burst sample counter. Could be left out here
// since it's done in the interrupt handler for every line.
// Legacy code.
videoBuffer[0][0] = 118;
videoBuffer[1][0] = 118;
videoBuffer[2][0] = 118;
videoBuffer[3][0] = 118;
overscanBuffer[0][0] = 118;
overscanBuffer[1][0] = 118;
overscanBuffer[2][0] = 118;
overscanBuffer[3][0] = 118;
uint i;
// initialize colour burst data, this never changes
for(i = 1; i <= nColourBursts; ++i) {
videoBuffer[0][i] = colourBurst[0];
videoBuffer[1][i] = colourBurst[1];
videoBuffer[2][i] = colourBurst[2];
videoBuffer[3][i] = colourBurst[3];
overscanBuffer[0][i] = colourBurst[0];
overscanBuffer[1][i] = colourBurst[1];
overscanBuffer[2][i] = colourBurst[2];
overscanBuffer[3][i] = colourBurst[3];
}
// initialize back porch colour data (must be black),
// never changes as well
for(; i <= nColourBursts + nBackPorchCCCycles; ++i) {
videoBuffer[0][i] = 0x44444444;
videoBuffer[1][i] = 0x44444444;
videoBuffer[2][i] = 0x44444444;
videoBuffer[3][i] = 0x44444444;
overscanBuffer[0][i] = 0x44444444;
overscanBuffer[1][i] = 0x44444444;
overscanBuffer[2][i] = 0x44444444;
overscanBuffer[3][i] = 0x44444444;
}
// initialize pixels to black
for(; i < nVideoBufferWords; ++i) {
overscanBuffer[0][i] = 0x44444444;
overscanBuffer[1][i] = 0x44444444;
overscanBuffer[2][i] = 0x44444444;
overscanBuffer[3][i] = 0x44444444;
}
// initialize overscan to default values
if(setLineSkip(defLineSkipPre, defLineSkipPost) != AV_ERR_OK)
printf("Failed to set line skip: %u %u\n", defLineSkipPre, defLineSkipPost);
// number of pixels to skip at the beginning and end of each line
setColSkip(defColSkipPre, defColSkipPost);
printf("active screen area: %u x %u\n", nActiveCols, nActiveLines);
// initialize callback functions
av.cbHBlank = NULL;
av.cbVBlank = NULL;
av.cbDbg = NULL;
// copy pin numbers
av.dacBase = gpioBase;
av.vBlankPin = vBlankPin;
// set up PIO.
av.pioInstance = pio;
av.pioSM = 0;
// load PIO program and prepare everything
av.pioProgOff = pio_add_program(av.pioInstance, &pio_PAL_DAC_program);
palProgramInit(av.pioInstance, av.pioSM, av.pioProgOff, gpioBase, vBlankPin);
// get us an DMA unused channel. If there's none the function will panic
// which is what we want here I guess.
av.dmaChannelId = dma_claim_unused_channel(true);
// now create the DMA configuration.
av.dmaChannelConf = dma_channel_get_default_config(av.dmaChannelId);
// we need 32 bit transfers
channel_config_set_transfer_data_size(&av.dmaChannelConf, DMA_SIZE_32);
// read address needs to be incremented as we're reading from a buffer
channel_config_set_read_increment(&av.dmaChannelConf, true);
// write address must be constant when writing to PIO FIFO
channel_config_set_write_increment(&av.dmaChannelConf, false);
// we need to set the proper data request source (PIO can do that!)
channel_config_set_dreq(&av.dmaChannelConf, pio_get_dreq(av.pioInstance, av.pioSM, true));
// configure the DMA channel; write address can be set up statically, write count varies
dma_channel_configure(
av.dmaChannelId,
&av.dmaChannelConf,
&av.pioInstance->txf[0], // write address (TX FIFO of selected PIO)
NULL, // read address is set in ISR
0, // data count is set there as well
false // don't start yet
);
irq_set_exclusive_handler(pio_get_index(av.pioInstance) ? PIO0_IRQ_1 : PIO0_IRQ_0, palIrqHandler);
irq_set_enabled(pio_get_index(av.pioInstance) ? PIO0_IRQ_1 : PIO0_IRQ_0, true);
irq_set_priority(pio_get_index(av.pioInstance) ? PIO0_IRQ_1 : PIO0_IRQ_0, PICO_HIGHEST_IRQ_PRIORITY);
pio_set_irq0_source_enabled(av.pioInstance, PIO_INTR_SM0_LSB, true);
// configure the vBlank pin which is used to tell the PIO program what to do
gpio_init(av.vBlankPin);
gpio_set_dir(av.vBlankPin, GPIO_OUT);
// now just set status flag
avStatus = AV_STATUS_INITIALIZED;
}
void analogVideoGetResolution(xy *res) {
res->x = nActiveCols;
res->y = nActiveLines;
}
uint getColourCount() {
return N_COLOUR_ENTRIES;
}
// used to set the active video area as most TVs will not display some lines
// at the beginning and end of each frame. The registered HBlank callback function
// is not called for these lines and "old" data is transmitted to avoid having
// to update buffers in the ISR.
uint setLineSkip(uint skipPre, uint skipPost) {
if((skipPre + skipPost) < ((vResPAL >> 1) - 1)) {
activeAreaBegin[0] = firstImageLine[0] + skipPre;
activeAreaBegin[1] = firstImageLine[1] + skipPre;
activeAreaBegin[2] = firstImageLine[0] + skipPre - 1;
activeAreaBegin[3] = firstImageLine[1] + skipPre + 1;
activeAreaEnd[0] = lastImageLine[0] - skipPost;
activeAreaEnd[1] = lastImageLine[1] - skipPost;
activeAreaEnd[2] = lastImageLine[0] - skipPost - 1;
activeAreaEnd[3] = lastImageLine[1] - skipPost + 1;
nActiveLines = (visibleLinesPerHalfImage - (skipPre + skipPost)) << 1;
return AV_ERR_OK;
}
return AV_ERR_INVALID_PARAMETERS;
}
uint setColSkip(uint skipPre, uint skipPost) {
if((skipPre + skipPost) < (hResPAL - 1)) {
videoBufferSkipWords = skipPre;
userBuffer[0] = videoBuffer[0] + videoBufferSkipWords + nColourBursts + nBackPorchCCCycles + 1;
userBuffer[1] = videoBuffer[1] + videoBufferSkipWords + nColourBursts + nBackPorchCCCycles + 1;
userBuffer[2] = videoBuffer[2] + videoBufferSkipWords + nColourBursts + nBackPorchCCCycles + 1;
userBuffer[3] = videoBuffer[3] + videoBufferSkipWords + nColourBursts + nBackPorchCCCycles + 1;
nActiveCols = hResPAL - (skipPre + skipPost);
return AV_ERR_OK;
}
return AV_ERR_INVALID_PARAMETERS;
}
// two functions to render pixel data into the video buffer
void putPixelRGB(uint *buffer, uint r, uint g, uint b) {
uint index = ((((r & rMask) << G_BITS) | (g & gMask)) << B_BITS) | (b & bMask);
putPixel(buffer, index);
}
void putPixel(uint *buffer, uint idx) {
idx &= PAL_LUT_MASK;
#if WORDS_PER_COLOUR > 1
for(uint i = 0; i < WORDS_PER_COLOUR; ++i)
buffer[i] = palLut[activeLine & 3][idx * WORDS_PER_COLOUR + i];
#else
*buffer = palLut[activeLine & 3][idx];
#endif
}
// function to actually start displaying video
uint analogVideoStart(void (*cbHBlank)(uint, uint*), void (*cbVBlank)(void)) {
if(avStatus & AV_STATUS_INITIALIZED) {
av.cbHBlank = cbHBlank;
av.cbVBlank = cbVBlank;
avStatus = AV_STATUS_ACTIVE;
// we start with frame blank, this needs to be set so the IRQ handler
// can select the proper next state.
halfImage = 3;
field = 4;
// normal operation
if(!testMode) {
palLine = vSyncBegin[halfImage] - 1;
gpio_put(av.vBlankPin, 0);
}
// drain the FIFO
pio_sm_drain_tx_fifo(av.pioInstance, av.pioSM);
// start the PIO program
pio_sm_set_enabled(av.pioInstance, av.pioSM, true);
} else {
return AV_ERR_STATUS;
}
return AV_ERR_OK;
}
// this can be used to stop video output. It should be possible to restart it,
// but I haven't tested that so far.
uint analogVideoStop() {
if(avStatus & AV_STATUS_ACTIVE) {
// clear all internal flags and stuff
avStatus = AV_STATUS_INITIALIZED;
// stop DMA operation
dma_channel_abort(av.dmaChannelId);
// first disable the state machine
pio_sm_set_enabled(av.pioInstance, av.pioSM, false);
// the reset it so it can be started again
pio_sm_restart(av.pioInstance, av.pioSM);
}
return AV_ERR_OK;
}
// some debug functions.
uint analogVideoStatus() {
return avStatus;
}
void setDebugIsrCallback(void (*cbDbg)(uint, uint)) {
av.cbDbg = cbDbg;
}
uint dbgGetProgOffset() {
return av.pioProgOff;
}
uint dbgGetDMAState() {
return dma_channel_hw_addr(av.dmaChannelId)->ctrl_trig;
}
void printDebugInfo() {
printf("PAL line: %u status: %x\n", palLine, avStatus);
printf("PIO PC: %u \n", pio_sm_get_pc(pio0, 0) - av.pioProgOff);
printf("DMA status: %x\tDREQ: %u\tTCR: %u\n", dma_channel_hw_addr(av.dmaChannelId)->ctrl_trig, dma_debug_hw->ch[av.dmaChannelId].ctrdeq, dma_debug_hw->ch[av.dmaChannelId].tcr);
}
uint enterTestMode(uint what) {
switch(what) {
case 0:
// just initialize pixel buffers
initVideoBuffersForTesting();
return AV_ERR_OK;
case AV_STATE_VBLANK:
palLine = vSyncBegin[3];
gpio_put(av.vBlankPin, 0);
// here we also need to change wrap of the PIO program
pio_sm_set_wrap(av.pioInstance, av.pioSM, av.pioProgOff + pio_PAL_DAC_wrap_target, av.pioProgOff + pio_PAL_DAC_wrap);
initVideoBuffersForTesting();
testMode = AV_STATE_VBLANK;
return AV_ERR_OK;
case AV_STATE_OVERSCAN:
case AV_STATE_VIDEO:
palLine = vSyncBegin[3] - 1;
gpio_put(av.vBlankPin, 1);
initVideoBuffersForTesting();
testMode = AV_STATE_OVERSCAN | AV_STATE_VIDEO;
return AV_ERR_OK;
}
return AV_ERR_INVALID_PARAMETERS;
}
// does almost the same as analogVideoInit()
void initVideoBuffersForTesting() {
uint i;
for(i = 1; i <= nColourBursts; ++i) {
videoBuffer[0][i] = colourBurst[0];
videoBuffer[1][i] = colourBurst[1];
videoBuffer[2][i] = colourBurst[2];
videoBuffer[3][i] = colourBurst[3];
}
for(; i <= nColourBursts + nBackPorchCCCycles; ++i) {
videoBuffer[0][i] = 0x44444444;
videoBuffer[1][i] = 0x44444444;
videoBuffer[2][i] = 0x44444444;
videoBuffer[3][i] = 0x44444444;
}
// initialize pixels to red
for(; i < nVideoBufferWords; ++i) {
// LUT offsets: R = 48, G = 12, B = 3
// R + G = 60, R + B = 51, G + B = 15
videoBuffer[0][i] = palLut[0][12];
videoBuffer[1][i] = palLut[1][12];
videoBuffer[2][i] = palLut[2][12];
videoBuffer[3][i] = palLut[3][12];
}
}
// IRQ handler
void palIrqHandler(void) {
uint branch = 0;
// clear interrupt first
pio_interrupt_clear(av.pioInstance, 0);
// we can always do that.
if(!testMode)
++palLine;
// first check whether we have to set/unset the vBlank control pin. This is of
// utmost importance as the PIO will go to the wrong branch if it's set too late.
if(palLine == (vSyncBegin[halfImage] - 1))
gpio_put(av.vBlankPin, 0);
// palLine is the Y coordinate of the line which is about to be drawn,
// i.e., we're now sending data for line palLine to the PIO module.
if(avStatus & AV_STATUS_ACTIVE) {
// DMA control, this is the second most time-critical thing.
if((palLine >= activeAreaBegin[halfImage & 1]) && (palLine <= activeAreaEnd[halfImage & 1])) {
videoBuffer[activeLine & 3][0] = (activeLine & 1) ? 112 : 118;
// video data.
// set up DMA to transfer next line's pixel data and start transmission
dma_channel_set_read_addr(av.dmaChannelId, videoBuffer[activeLine & 3], false);
// set number of words to transfer and start the DMA operation
dma_channel_set_trans_count(av.dmaChannelId, nVideoBufferWords, true);
avStatus = (avStatus & ~AV_STATE_MASK) | AV_STATE_VIDEO;
branch = 1;
videoLine += 2;
++activeLine;
} else {
if((palLine > vSyncEnd[halfImage]) && (palLine < vSyncBegin[halfImage])) {
overscanBuffer[activeLine & 3][0] = (activeLine & 1) ? 112 : 118;
// we're in overscan, send the overscan buffer which contains a colour burst
// but only black pixels.
dma_channel_set_read_addr(av.dmaChannelId, overscanBuffer[activeLine & 3], false);
// set number of words to transfer and start the operation
dma_channel_set_trans_count(av.dmaChannelId, nVideoBufferWords, true);
avStatus = (avStatus & ~AV_STATE_MASK) | AV_STATE_OVERSCAN;
branch = 2;
++activeLine;
} else {
// otherwise, we're in the vertical synchronization period.
// Start DMA first, then see what else has to be done.
dma_channel_set_read_addr(av.dmaChannelId, trainingSequence[halfImage], false);
dma_channel_set_trans_count(av.dmaChannelId, trainingSequenceLength[halfImage], true);
// update counters
halfImage = (halfImage + 1) & 3;
++field;
palLine = vSyncEnd[halfImage];
activeLine = (field & 4) >> 1;
videoLine = halfImage & 1;
avStatus = (avStatus & ~AV_STATE_MASK) | AV_STATE_VBLANK;
branch = 4;
gpio_put(av.vBlankPin, 1);
}
}
avStatus |= (halfImage & 1) ? AV_STATE_ODD_HALF_IMAGE : AV_STATE_EVEN_HALF_IMAGE;
// notify application here; this is least time-critical
if((palLine >= (activeAreaBegin[halfImage & 1] - 1)) && (palLine < (activeAreaEnd[halfImage & 1] - 1)))
if(av.cbHBlank != NULL)
av.cbHBlank(videoLine, userBuffer[activeLine & 3]);
// we have to test against vSyncEnd as the branch handling vBlank already sets palLine
if(palLine == vSyncEnd[halfImage])
if(av.cbVBlank != NULL)
av.cbVBlank();
}
if(NULL != av.cbDbg)
av.cbDbg(palLine, branch);
}