deadbug
web log
abusing a trashed TV teletext superimposer smells like bad apple published on the 3rd of october of 2026
23 clicks
1 claps

origins

once, on a walk, i noticed a pretty PCB inside of a smashed CRT televisor. on it were convenient 2.54mm pin headers and two beautiful dual-inline-packaged chips. i didn't hesitate to bring it home. my computer told me that the two black rects are a teletext slicer, whose job is to "slice out" teletext from an analog video signal, and a video processor with a character generator. they both talk on the same "bus", and seem to be made for this exact use-case. the second chip is what got me really interested — it allows controlling everything through I²C. i was thinking of making a tiny embedded operating system for a while at that point, so a pluggable video card was a really adequate gift from the universe. it has multiple language sets, graphics characters, and can hold 128 pages of these characters (thanks to the RAM chip on the CB's flipside).

green circuit board with two pdip integrated circuits, a pin socket, and many other components
the victim

re

i got to work. i drew out the whole double-sided board in kicad. i then got the datasheets for both devices printed, and this way figured out what all of the pins are responsible for. the sheets were not easy to read. they had some small, annoying mistakes, and the german folks at siemens apparently didn't have anyone well-versed in english to check the documents. my lack of teletext knowledge didn't help either. i still don't know what some options in the registers do, since their descriptions are, to me, undecipherable, and my tests didn't reveal any changes.1
after lots of tinkering and breadboard issues, i got it to draw characters. a big issue was that the board had very weak pullups on the RGB lines (the processor outputs are open-collector, and only pull the signal low). because of that i would see very low-contrast images, or none at all. this was mitigated by adding parallel resistors around the existing pullups. i didn't want to modify the board in any "permanent" way — as a challenge, so i wrapped the new parts around the leads of the ones on the board. this was very finicky, and would constantly fail. only at the very end of the road, where i was trying to record a video of the finished project, i soldered them on the back. :/. they look fair, though.

pain

graphics mode is entered with one of the special control characters, which denote the color. then, images can be drawn with 2x3 "sixels". i extracted a few frames of bad apple into pictures with ffmpeg, and then wrote some awful software to convert them into sixel arrays.2 using the pages, i implemented a double-buffered frame drawer. it worked, unfortunately at horrible speeds. that's when i realized that a 100kHz I²C clock does not actually provide much bandwidth (duh). then began the crazy optimization fight. i installed a 1.5MB EEPROM on the breadboard, to store video frames. i kept trying to implement some sort of compression, but it was crazy work! after making some row-optimized frames, i would count the amount of bytes they would require to send, and keep the fattest ones stored in the 128 pages that the processor allows me to use, hoping to switch to them at the right time, and continue drawing more changes on them, leaving the old ones behind. this didn't work, probably due to timing issues — everything would blow up and fucking die if i switched to a page and drew to it too fast — but i don't remember. a later iteration of this was an approach where i would store frames with equal time gaps, sort of like a time-lapse of the entire video, and then switch to them when the time came. this was to try and circumvent the awful artifacts getting created on-screen. it didn't work either.
but what artifacts? i implemented an algorithm that would cut unnecessary data from the frames, based on the former frame. that sounds good, but all control characters take up space on the screen3: starting graphics (mosaic) mode takes one, and switching the foreground color can actually take two or more "cells" on the screen. some of this can be somewhat patched up with the use of the «hold mode» character, which makes it so these empty characters are replaced with a copy the last-used sixel, but from what i remember, this didn't work very well either. even when it did, it looked awful, since the sixels didn't match, and often resulted in long streaks of the same sixel.
reprogramming the EEPROM to test every tiniest change was driving me mad, and instead of implementing a tiny UART interface, i wrote a whole ass emulator. it was useful, but even with this, i eventually gave up and left this alone for a few months.
btw, i know all of this teletext stuff thanks to an awesome article, which i found while searching for some way to minimize the damage, thanks you!

a breadboard with many wires lying on the desk, connected to a circuit board and a SCART socket
spaghetti

grand return

some day, this huge monstrosity dusting up on the desk got too annoying — i didn't have any place to put it. i was bored, so i quickly drew a ciruit on a piece of laminate and etched it. my CD marker was running dry, so there were a few bad traces, but they were easy enough to fix. during testing, i noticed that my EEPROM reader was failing. turns out i commited an insane mistake on the circuit board. the EEPROM chip was supplied with 5V, oops. i cut the trace and installed a bodge wire to one of the 3.3V pins of the bluepill, and the chip thankfully survived — i didn't have more... i was actually very happy to find it on one of my trash PC motherboards.

etched hand-drawn circuit board
etched circuit board

i sat down to write more software some days later. i came up with the genius idea to... just lower the resolution of video. i wrote a tiny static bandwidth analyzer. i called it that because it analyzes the bandwidth on my computer, instead of me downloading the software to the µC and analyzing the framerate with my eyes. i assumed that i would get something like 50kB from the 110kHz-clocked I²C device4 (sampling on one edge, minus the start, stop, ack stuff), and this may very well be wrong, so please correct me if it is. with this analyzer, i found a res that would look good, while still allowing me to play bad apple at 14fps. i reused the simplest compression algorithm that i made before — all it does it just slice the end of rows up to the point where something changed, relative to the former frame. this way, all the control characters stay in-line, "before" the video. the downside is that a single character change on the right side requires me to send the whole row.

audio/video

at some point i decided that i want to also play audio to complete the bad apple experience. i knew that storing it raw in the EEPROM was not an option. i was already using the heatshrink rust implementation to decrease the amount of space taken in ROM by images. i found a promising algorithm, called «Simple Embedded Audio Codec». unfortunately the implementation is not no-std, which is an interesting choice for an embedded algorithm. in the end, i ended up using the quite ok audio codec, which managed to compress a 20700Hz-sampled bad apple WAV into around 1.25MB, which fit snugly on the rest of the ROM.
making these two work in parallel with no glitches was a very fun challenge. i ended up using a 14Hz timer to switch a mutable static bool, which would allow the mainloop to send the sixel arrays at appropriate times. in the meantime, the mainloop would replenish audio and video buffers: reading from ROM, running the decoders, and storing the output.
audio is implemented using two timers. one is set up as a high-frequency PWM output on one of the stm32 pins. the other runs at 20700Hz, stealing samples from the decoder, and using their values to adjust the PWM duty cycle. this signal is later filtered using a low-pass filter. i soldered the trimmers and capacitors mid-air, since i wasn't very excited for a CB revision. the audio quality is surprisingly good.

components soldered mid-air to a circuit board
audio sub-circuit (jank)

heapless::Deque really came in clutch here. since i was dealing with static buffers, i had no idea how to synchronise the reader and the decoder. sometimes the audio decoder would read less bytes than i provided (which didn't happen with the heatshrink decoder, so i had it easy with that one), which caused byte-skipping and failure. with the deque, i could perform this loop:

  • pull bytes from W25Reader (W25 is the EEPROM) into self.inbuf, until the reader runs out, or the length is hit
  • update self.bufcap with the current amount of valid bytes
  • allocate a new buffer for samples
  • decompress &self.inbuf[..self.bufcap] into the new buffer
  • subtract the amount of bytes consumed from the current bufcap, and memmove the untouched region to the start of the array
  • push produced amount of bytes from the produce buffer into the deque

later, i just pop a sample in the interrupt handler, first-in-first-out. this is certainly not the most efficient way to do this, but i didn't want to modify the decoder code. i was seriously stuck and overwhelmed without the deque. thanks you, deque; thanks you, heapless. it's a beautiful day.5

NO ONE CARES

OK BAD APPLE NOW
everyone came here to see bad apple, so here's a bad apple:

i'm leaving all the code in a git repo, read at your own risk, not meant for consumption.

footnotes

  1. «Register 5: Display Control Normal Inside and Outside Box», «b7: Function: 0 = only for foreground colors outside, 1 = foreground and background colors outside; Comment: has priority over "picture outside"». the inside and outside are later explained to be «inside/outside a teletext box area». no. idea. what this does. i know that the device can grab sync from an existing composite signal (which is actually the default, i had to set "master" mode myself), and lay images over the top, which could have something to do with this, but i couldn't get it to work with what my PS2 outputs. ↩

  2. i need to say that while the code for all programs used is released in a repo, which i'll link later, it's ALL absolutely abysmal. the first script i tried writing actually contains a lookup table for all of the 60 possible sixels, because i didn't realize that it's a simple bit-masking job. ↩

  3. which is why i quickly gave up on the foreground color switching code. i tried to switch to a black foreground and white background when there were little black sixels on screen, but dealing with the extra characters was not simple, and this probably wouldn't bring much more performance anyway, not without the pages. ↩

  4. btw, this already was over the limit, as the fmax is 100kHz. it wouldn't make any additional artifacts so i went with it. in reality, i had to go a few kHz lower, since some sixels would later come out wrong ↩

  5. after writing this whole process out, i could probably implement something that would fit my needs now. it wouldn't be the same, as heapless uses some sort of double-array design. not sure what's up with that. ↩