Teletext signal generator with the Raspberry Pico
05-11-2023
What was Teletext?
Teletext was a standard devised to allow for data transfer on the vertical blanking interval of analogue TV transmissions . Originally, Teletext decoders had tiny buffers, only capable of storing the current page, and evolved to store at most a few pages at a time.
This meant that the standard worked by continuously broadcasting pages over frames, and all the TV had to do was wait until the requested page became available again. This worked at a sufficiently rapid rate that allowed for quick transitions from page to page.
Functioning principles
Data was transmitted using a single non-return to zero data line, operating between the range of 30% and 80% (where 0% is "black" and 100% is "white") of the nominal voltage of the analogue transmission line.
The system set its timing by analysing two clock run-in bursts at the start of each data row, which determined the length of each bit for the rest of the row. Following that, a framing code with a length of one byte is transmitted, allowing decoders to perform detection of Teletext signals.
The page was comprised of a number of rows, the first of which contained date information, page metadata, among other useful discerning qualities.
Data was encoded in Hamming (8,4), in order to allow for error detection and correction in limited cases.
Despite the specification's age, it still recommended to transmit at 6.9 MBit/s, or 444 times the line frequency for a given frame.
Forming pages on the Pico
The Pico is a surprisingly powerful 5 pound microcontroller. Armed with dual cortex CPUs running at 200 MHz, and a neat programmable input/output implementation. Touting 4 state machines and 32 words of instruction memory, it should be more than enough to pump out Teletext data at the required frequency without a hitch (and maybe some ringing).
Rather than bother one of the two main cores with such menial work, we can offload IO switching to the programmable IO controller, which has the added benefit of consistent timing, freeing us from having to ruining our transmissions by performing work on the core responsible for actually writing data out to the IO pins.
For example, the following code pulls data from the input shift register, looping while there is no more data. Once the transmission has been completed, it sets an interrupt, which is handled by the main core:
.program pin_ctrl
pull block
loop:
out pins, 1 [5]
jmp !OSRE loop
irq set 0
.wrap
The main core can then handle this interrupt and write data for the next row. That is, the sync pulse (pull total output voltage to 0) and the colour burst, if we so desired. Therefore, we can safely loop through and pass data onto our PIO by moving onwards to the next row during sync pulses to minimise disturbance, but with the main core running at a blistering 200 MHz, this is not rocket science. So, the interrupt handler would look something like this:
void __not_in_flash_func(dma_irh)() {
for (int i = 0; i < 3; i++) {
timing_buffer[i] = 0xfffff;
// or whatever data we wanted, for an arbitrary array size
// but it would be even easier in the real implementation,
// as we can just point the DMA read address to the next location
// in our page buffer. This is just a "naive" example
}
dma_channel_set_read_addr(pio_dma_chan, timing_buffer, true);
dma_hw->ints0 = (1u << pio_dma_chan);
}
Thankfully, the Pico is clever enough to autopull data from the buffer, which allows us to pass an array of wide characters (4 bytes), all of which are then happily ingested by the ISR.
Voltage control
Unfortunately, it's not as simple as 1s and 0s in the end. We need to use a resistor ladder, such as the one used by the composite video output implementation that inspired this project. Since I am fairly limited in space, I had to come up with a sad excuse for a resistor ladder that operated within the confines of the technical specifications, whilst still allowing me to use the resistors I owned:
Now, since the "magic values" we are looking for are 800 mV (80% of full white, 1V) and 300 mV (30%), this should do the trick with a fairly minimal error margin, which I might tweak as this inevitably crashes and burns in reality.
What's next?
I will build and test the ladder itself, and make sure the voltages are close enough to what I expect. With that done, I will write code for the second state machine, which will handle horizontal sync pulses, whilst developing logic on the main core to skip "visible" lines.
Huge thanks to Greg Chadwick for his excellent articles on Pico's PIO.