During garbage collection in my room, I found a Mitsubishi CGA display manufactured in 1986. I tested it with the 286 PC it came with and it turned out to be in working condition. CGA has many properties that make it perfect for experimentation with microcontrollers:
- displays are dirt cheap (if not free) and rarely used for their original purposes
- the connector is DB-9 thus cheap and easy to solder
- all signals are TTL (0-5V digital)
- clocks are in the range of cheap microcontrollers: HSYNC is 15,75 kHz, VSYNC is 60 Hz
- despite the above, 640 by 200 pixels can be drawn in 16 colors
Of course, life is never perfect, so there's one catch: it's not that well documented, there are not as many forum or blog posts and tutorials about CGA as with VGA or composite video. The pinout and the frequencies can be found on almost every ontopic web search result for the right keywords, the Wikipedia page has a pretty decent summary including colors, but most of the article deals with the PC-side hardware (video card), not the display nor the connection between them.
One of the most helpful document I found was a comment posted on a NES development forum, which revealed two important pieces of information: the pixel clock frequency (4 x NTSC (14.318 MHz) or 2 x NTSC) and the full timing table. I was not sure whether 14.318 MHz referred to NTSC or 4 x NTSC so I checked another helpful Wikipedia page and found that the NTSC M color subcarrier frequency is 3.579545 MHz, and multiplying it by four gives the 4 x NTSC frequency, also noted in the table. The full timing table is the following (in case the original post becomes unavailable):
0 visible-period A right-overscan B right-blanking C sync D left-blanking E left-overscan F
Horizontal:
A = 80 (640) B = 89 (712) C = 94 (752)
D = 102 (816) E = 109 (872) F = 114 (912)
Vertical:
A = 200 B = 223 C = 225
D = 228 E = 239 F = 261
Multiplying the numbers in parentheses gives the exact length of each period, which makes it possible to write a simple sketch for an Arduino to display something simple. I chose to test with three horizontal displaying the flag of Hungary using a 66 row high light red (12), a 68 row high white (15) and a 66 row high light green (10) stripe. For the sake of simplicity, I connected the high intensity pin (6) to constant 5 volts, so the Arduino had 5 wires connected to it using the following scheme.
- pin 1 and 2 (ground) were connected to the Arduino ground
- pin 3 (red) was connected to Arduino digital pin 10 (bit 2 of PORT B)
- pin 4 (green) was connected to Arduino digital pin 11 (bit 3 of PORT B)
- pin 5 (blue) was connected to Arduino digital pin 12 (bit 4 of PORT B)
- pin 6 (intensity) was connected to Arduino power pin 5V
- pin 7 (reserved) was left floating
- pin 8 (horizontal sync) was connected to Arduino digital pin 8 (bit 0 of PORT B)
- pin 9 (vertical sync) was connected to Arduino digital pin 9 (bit 1 of PORT B)
Those who used Arduino digitalWrite exclusively, might not know what PORT B is – if you're not one of them, you can skip this paragraph. The AVR microcontroller used in the Arduino has its I/O pins grouped into 8-bit registers that are mapped into the memory, thus accessible via certain variables, for example assigning an 8-bit value to PORTA writes the bits given to digital pins 0 to 7 in one quick step. In most cases, there's no need to get into this, but in timing-critical cases as this, there's significant advantage in accessing the hardware directly – see for yourself in Bill Grundmann's thorough blog post. You can read more about this direct access on the official Arduino page about port registers.
As you can see, I arranged all five pins to be connected to the low five bits of PORT B, which means that I can modify all their values in a single instruction. At the beginning of my sketch, I used #defines to provide constants named in a meaningful way.
#define HSYNC 1
#define VSYNC 2
#define RED 4
#define GREEN 8
#define BLUE 16
#define WHITE (RED | GREEN | BLUE)
#define BLACK 0
#define COLOR WHITE
#define ROWS 261
In the setup function, the sketch initializes the output ports and sets all output pins to low.
void setup() {
DDRB |= HSYNC | VSYNC | COLOR;
PORTB &= ~(HSYNC | VSYNC | COLOR);
}
There are also two global variables used to track the color of the current stripe (rgb) and the number of the current row (row).
int row = ROWS;
byte rgb = BLACK;
In the loop function, the sketch draws a single row. It begins with the left blanking and overscan area that takes around 6.7 μs, then sets the R-G-B pins to the color of the current stripe. The width of the visible area is 640 pixels that take approximately 44.69 μs, after that, all R-G-B pins are reset to low.
delayMicroseconds(6);
PORTB |= rgb;
delayMicroseconds(44);
PORTB &= ~COLOR;
The right overscan and blanking take around 7.8 μs, after that, HSYNC needs to be pulled high for approximately 4.47 μs.
delayMicroseconds(8);
PORTB |= HSYNC; // HSYNC HIGH
delayMicroseconds(4);
PORTB &= ~HSYNC; // HSYNC LOW
At the end of the row, the row-level logic increments the row counter and uses a switch statement to handle certain rows specially. The first three cases cover the flag generation: after the 66th row, the color changes to white, at the 134th it does again to green, and at the bottom of the screen, color gets turned off. Between the 225th and the 228th row, VSYNC is set to high, and after the last row, the row counter gets reset to zero.
switch (row++) {
case 66:
rgb = WHITE;
break;
case 134:
rgb = GREEN;
break;
case 200:
rgb = BLACK;
break;
case 225:
PORTB |= VSYNC; // VSYNC HIGH
break;
case 228:
PORTB &= ~VSYNC; // VSYNC LOW
break;
case ROWS:
rgb = RED;
row = 0;
break;
}
The actual delay values seen in the snippets above are the results of rounding and testing as the Arduino libraries hook certain interrupts making it difficult to predict the actual execution time of the code. Because of this, I measured the horizontal sync frequency with my multimeter and adjusted the values so that the HSYNC frequency is around 15.65 kHz (instead of 15.75 kHz). It almost worked for the first time – I forgot to put #define WHITE into parentheses causing the negate operator (~) to behave in an unexpected way. After fixing that, it worked perfectly for a proof of concept, as it can be seen on the photo below.
The weird edges are caused by the improper timing, so the next step will be to use plain AVR C/C++ code to avoid Arduino overhead allowing finer control over the timing. As the RAM of the Atmega168 is far too small for a framebuffer, I have plans to create a character map in the Flash (PROGMEM) and create a library that would allow to display any text or simple graphics. I hope you enjoyed this post, hold on till the next part, or better grab a CGA display and start experimenting!