Introduction

This project was heavily inspired by (and using small bits of) the original TVout library for the Arduino, and aims to provide a simple firmware for generating color video output using AVR microcontrollers, controlled via a serial interface.

Visit the GitHub repository for the latest version: https://github.com/dj-pixus/avrgbi

It can be built and flashed using the official Arduino IDE.

For bug reports and suggestions, please use GitHub's issue tracker.

Currently only the ATMega4809 is officially supported and tested. Older chips like the ATMega328p and ATMega168 are not supported due to hardware limitations.

Note about clock frequency

Note: For some cursed reason the Arduino IDE is configured for a clock speed of 16 MHz for the ATMega4809, even though it can do up to 20 MHz. We hope they will fix it in the future, but for now you will need to change some config for this firmware to work properly.
The board definition file can be found at /home/you/.arduinoXX/packages/arduino/hardware/megaavr/X.X.X/boards.txt on Linux and in a similar location under C:\Users\you\AppData\Local\ArduinoXX on Windows. If you find a line saying something like nona4809.bootloader.OSCCFG=0x01, change the value to 0x02 in order to set the fuses in the hardware correctly at upload, and change the value of nona4809.build.f_cpu to 20000000L for the software to know about it. If the image is stretched to the right or there are sync issues, it's most probably due to these settings being left on default. You may need to restart the Arduino IDE for the changes to take effect.

Also make sure the legacy register emulation is disabled in the Tools menu, otherwise you will get errors.

More note: Many modern televisions and monitors tend to be very finicky about signal timing. If your board uses the internal PLL oscillator (as e.g. the Arduino Nano Every does) instead of a quartz, the picture might get fluffy or completely borked due to the massive jitter of the clock. In that case you might want to use an external 20 MHz crystal oscillator to make it stable. For this to work, you will need to uncomment the appropriate line in the setup() function in avrgbi.ino

Supported modes

Video modes are defined in the form of header files in the modes folder. This means that the firmware has to be built with a specific mode selected at the top of avrgbi-videogen.h, and it can NOT be changed without re-flashing.

Currently the following modes are available:

Please note that especially in PAL mode the aspect ratio will be stretched. This is due to the AVR clock speed and memory size being quite incompatible with the weird numbers used in the video signal specs.

How on earth does it work

The synchronisation signal is generated by the builtin timer/counter of the ATMega chip, which also controls the timing of the video output by interrupts. This way the rest of the program can safely run in the blanking intervals without interfering with the signal timing.

The picture data is stored in the memory as a series of bytes, where each byte stores the color values for 2 pixels, as shown by this very professional figure below:

The reason for 4-bit color (instead of the more reasonable 8-bit on this architecture) is the small amount of memory and the fact that most AVR boards don't have any output ports with all 8 bits wired out.

Using the RGB connection described in the next section, you will get an RGBI (almost CGA) color space. You can imagine this as if all three color channels had 2 bits, except the lower bits are tied together into that one I (intensity?) bit. This gives us a total of 16 possible colors. In monochrome configuration you will get 16 shades of grey (or whatever color your screen is).

After a cold boot, the screen will show the garbage that happens to be in memory, mostly determined by manufacturing imperfections, cosmic radiation, and the number of femboy foxes in your area. This is normal behavior, and you can get rid of it by filling the screen with a color as described in the protocol specification.

If you want to know more about the way this whole thing is implemented, go study the code. ;)

Connecting the hardware

Below is a table about what I/O ports are used for what:

RGBISyncMode switchClock in (optional)
ATMega4809PD3PD2PD1PD0PB0PB1PA0
Arduino Nano EveryA0A1A2A3D9D10D2

The device can be connected either to a television with a EuroScart or composite input using PAL or NTSC timing, or to a VGA monitor with a 15-pin (or maybe only 14-pin?) D-Sub input using standard VESA timing (which is not yet implemented in the firmware but I show you the physical layer anyway).

Scart RGB connectionVGA (D-Sub) connection (not supported yet)Composite (monochrome) connection

Sync signal

In the case of Scart, our composite sync signal goes to pin 19, where normally composite video goes. Please note that the sync signal in a Scart connector has separate input and output pins with crossover wiring in the cable. This diagram shows a female connector that generously offers the signal on its output pin. If you decide to cut a cable with a male plug (no clue why would anyone do that), make sure to use pin 20 instead to fire right into the TV's input.

For the Scart connection, the signal level has to be reduced to around 0.3 volts. This is done by putting a 1.2k resistor in series, forming a voltage divider with the 75 ohms of the TV's input.

VGA uses TTL levels for the sync signal, so we can connect it directly to the Arduino's output. We could use separate pins for the H and V sync signals here, but as more or less every monitor supports composite sync, no need to make things more complicated.

RGB signals

The weird way of wiring the color signals is because the Arduino outputs a 4-bit digital signal that we need to mix down to 3 analog channels somehow. Using the shown resistor values, each channel's own color bit weights roughly twice as much as the I bit, making a kinda-sorta linear palette of colors, while also reducing the voltage to a kinda-sorta correct level. Kinda-sorta. Having both the color bit and the I bit switched to 5 volts, the two resistors go parallel and become around 468 ohms, forming a nice voltage divider with the receiving device and reducing the voltage to around 0.69 V ( ͡° ͜ʖ ͡°), which is a comfortable maximum according to the specs. You can tweak the color palette by tweaking the resistor values if you wish.

Further magic

Pin 16 of the Scart connector is the so-called blanking signal that tells the TV when to switch to RGB input. This is like a 1-bit alpha channel and is usually used by CRT boxes to overlay teletext and other things on top of a composite video source (the TV programme for example). The voltage threshold can vary, but it should be somewhere between 1 and 3 volts when enabled. As we always want pure RGB input, we can tie it up to 5 V through 220 ohms to set it to a friendly 1.27 V level. If your Arduino has a 3.3 V pin, you can use that too, changing the resistor value accordingly. Please note that in many non-computerized TV sets this will override the builtin OSD too, causing it not to display at all. The saturation setting might also not have any effect in this mode for obvious reasons. This is normal behavior and you didn't break your TV (yet).

Pin 8 is a switching signal that tells the TV that we want to use the Scart input. Connecting it is only required if your TV doesn't offer an option to switch to it manually. Using this pin you can say to the TV something like "could u pls switch to this input right here thx". Some compassionate tubes will even turn on from stand-by, just for you. Please note that as our circuit only has 5 volts of juice, this signal will also make some TVs switch to 16:9 aspect ratio. If your TV requires this signal to be present and also supports 16:9 mode, you will need to steal 9-12 volts from somewhere else to make it look right.

VGA monitors don't need this much pleading, they just accept RGB input by nature, and come to life automagically when the sync signal is present. Monitors are really kind-hearted objects. Love and respect your monitor.

There are two kinds of TVs in this world

The picture above shows two possible sets of colors, and which one you will get depends on the way your TV handles signal levels. As the traditional composite video standard says, a TV should calibrate its blanking level based on the signal level during the "back porch", which happens in each line and is the time interval between the horizontal sync pulse and the left edge of the actual picture. (This was invented this way to make the system immune to DC offset.) Although this should still be below the black level of the picture, some TV sets just ignore this fact and interpret brightness from this level up. (Although you can usually correct it using the brightness control on the tv.) By default, these devices will display the palette shown on the left. Some other TVs on the other hand have the proper offset on the input, and on those the low signals get buried, giving a palette similar to the one on the right. As in our simple circuit the blanking level and the black level are the same thing, we have to live with this inconsistency for now.

Composite mode

Because of the cursed way color is encoded in composite video, AVRGBI only supports monochrome output when using this connection. In this case we simply use a resistor network to convert the digital signals into the appropriate voltage levels for the analog video signal to produce a 16-color grayscale output. Note that as the composite video signal has some play even between the blanking level and the darkest visible black, a quick and dirty hack here is using a smaller resistor for the sync signal to offset things a little. Otherwise the second darkest color would never be visible on some devices, no matter how you adjust the brightness and contrast settings.

The rest is for converting the 4-bit value to an analog voltage level where R is the MSB and I is the LSB. The resistor values shown on the diagram are theoretical in the sense that you probably won't find them at any store on the planet, but you can compile them from multiple pieces, or use potentiometers. But even if you just use values that are "close enough", the worse thing that can happen is some nonlinearity in brightness.

Of course you don't necessarily need to use a yellow rca plug, it also works just fine through any other color RCA Scart connection, using pin 19 or 20 as mentioned earlier.

Serial protocol specification

Commands can be sent to the device using the serial interface at 115200 Baud, with no handshake signals, 8 data bits, no parity bit, and one stop bit. A command is made up of a one-byte command identifier followed by some parameters. A command is executed after receiving the required number of bytes with no time limit.

Commands

0x00 - C - X - YSet pixel at X and Y coordinates to color C.
0x01 - C - X0 - Y0 - X1 - Y1Draw a filled rectangle of color C with X0, Y0, X1 and Y1 specifying two opposite corners. Depending on the specified parameters, optimized functions are called for single horizontal or vertical lines, or filling a screen-wide area.
0x02 - XBYTE - Y - WBYTE - H - ...Display a rectangular bitmap described by the data following this command, starting at row Y. Due to the way pixel data is stored in memory, the horizontal offset must be even, and is going to be XBYTE * 2. The bitmap is drawn in horizontal lines with a width of WBYTE * 2 and a height of H. The command returns after XBYTE * H bytes of data has been received.

Colors

When using the C parameter, the lower 4 bits of the byte specify the 4-bit color value:

Hex valueColorHex valueColorHex valueColorHex valueColor
X0Black X1Dark grey X2Dark blue X3Blue
X4Dark green X5Light green X6Teal X7Cyan
X8Dark Red X9Light red XAPurple XBMagenta
XCDark yellow XDYellow XESilver XFWhite

The upper 4 bits of the byte specify how the new color value is applied to the selected area of pixels:

Commands with color mode bits outside the range described above are ignored.

Debug mode

To enter debug mode, reset or power cycle the device while the Mode switch pin is pulled to ground. When debug mode is active, the following things will happen: