VGA Monitor Output from an Arduino

One of the coolest things about programming on a platform for the first time is making something graphical or somehow otherwise tangible to the eye.  Terminal apps generally aren't very exciting because they're just text, but making an application with its own window & graphics is much more exciting for the beginner.  If it's a microcontroller, making that LED blink is a first big step toward future fun.  However, there is a lot more to do when it comes to making graphical output from microcontrollers -- output on 7-segment displays, an LED matrix, LCD touchscreens, or the pinnacle & mainstay of computer displays -- the monitor.

Having taken a class in FPGA design and becoming fascinated with video output without needing an operating system, I decided to look into what it'd take to use the Arduino to produce VGA output.  It turns out the hardware requirements can be somewhat hard to come by these days:
  • A nice, old, tolerant CRT monitor -- my LCD monitors won't support this
  • A breadboard
  • Several resistors of various values; pick your own after reading below

Once you have the prerequisite hardware, you need to acquire some esoteric knowledge about the way Atmels and many other microcontrollers work.  This definitely isn't beginner-level stuff; it took a little while to pore over the Atmega328 datasheet to figure out just what settings are required to make the analog* signals required to drive a VGA monitor.  Instead of making you repeat all this research, I'll just give it to you.  I've attempted to make explanations in the comments, but if anything is unclear, please refer to the datasheet.

The schematic



Note the voltage dividers put in place to change the output of the Arduino from 5 volts down to the maximum 0.7 volts allowed by VGA hardware.  Arduino pins 13 & 12 (sync) go out to one 220-ohm resistor each, and 7, 4, & 2 (the RGB pins) each go to their own voltage divider with R1 = 1200 ohms and R2 = 180 ohms.  Some further notes:

  • Pin 9 on the D-SUB connector should really be wired to ground. However, my circuit managed to work without doing this, so it's not in the schematic.
  • I can't remember the rhyme or reason for putting a resistance on the sync lines of specifically 220 ohms.
  • The 1200 ohm and 180 ohm resistors are in place as a voltage divider, since the maximum voltage the RGB pins in a VGA monitor can take is 0.7V. These resistor choices lead to (180 / (1200 + 180)) * 5V ~= 0.65V. You can use any other combination as long as it leads to the correct voltage.

Arduino firmware

The firmware is designed to consolidate the Red, Green, and Blue values for each "pixel" into 3 consecutive bits.  While this prevents any interesting variations in color (i.e. only eight colors are possible), and isn't truly "analog", it helps speed up processing time because you can cram 2 "pixels" into the same byte.  The next pixel can be shown simply by shifting the register left by 4 bits, which takes fewer clock cycles than loading the value from the flash ROM.  (This kind of causes some pixels to be slightly narrower than others.)  Speaking of pixels and resolution, this code generates an image with a resolution of approximately 376x282 @ 60 Hz.  Here are a few places I got guidance from on my quest to write this firmware:

  • http://arcanebolt.net/ - My code is based off theirs, but heavily modified.
  • http://www.cs.unc.edu/Research/stc/FAQs/Video/GTF_V1R1.xls - A GTF spreadsheet you can use for finding out what frequency and timings to use given the desired VGA resolution and refresh rate you specify. If this spreadsheet doesn't work out for you, the one I used originally has been recovered and I can share it.  I don't know where it came from originally.
  • Lots of Google searches for "vga pinout" and "vga timing specification", etc.
#include <avr/interrupt.h>
#include <avr/pgmspace.h>     // for PROGMEM

// horizontal line counter (negative is during vertical blanking)
int hzCount = -10;
int vCount = 0;
int timingCount = -10;
int base = 0;

unsigned short int OCR1AMEM = 0;
unsigned int pixRow = 0;
prog_uchar* curPix = 0;

unsigned int displayInt;
int k;    // counter variable
char myChar; 


// ==============
// Pinout:
#define HSYNC 0b00100000;   // 13: HSYNC (Port B5)
#define VSYNC 0b00010000;   // 12: VSYNC (Port B4)

#define BLACK   0b00000000;   // Black                0x00
#define BLUE    0b01000100;   // 6:  Blue  (Port D6)  0x40, 0x04, 0x44
#define GREEN   0b00100010;   // 5:  Green (Port D5)  0x20, 0x02, 0x22
#define RED     0b00010001;   // 4:  Red   (Port D4)  0x10, 0x01
#define CYAN    0b01000100;   // Cyan                 0x60
#define YELLOW  0b00110011;   // Yellow               0x30
#define MAGENTA 0b01010101;   // Magenta              0x50
#define WHITE   0b01110111;   // White                0x70

// prog_uchar
// prog_uint32_t signMessage[] PROGMEM  = {0x04108000, 0x00000410, 0x00808000, 0x00000080}; // , 0x92009292, 0x92000092, 0x00009292, 0x00920029};
prog_uchar signMessage[] PROGMEM  = {/*this is where a whole lot of hex numbers go for your custom image*/};

//===

// Others go to GND
// =======

// hurry up & wait
#define NOM    asm("nop");
#define NOM5   asm("nop\nnop\nnop\nnop\nnop");
#define NOM10  asm("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop");
#define NOM20  asm("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop");

void hLine(void)
{
  curPix = signMessage + base;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++;PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++;PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  // pix 21-
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;
  PORTD = pgm_read_byte_near(curPix);
  curPix++; PORTD = PORTD << 4;

  // pix 40-

  NOM; NOM; NOM;
  PORTD = BLACK;
  delayMicroseconds(1);   // wait for back porch time - 1

  PORTB = PORTB ^ HSYNC;         // invert HSYNC - negative polarity
  NOM5; NOM5; NOM; NOM; NOM;
  delayMicroseconds(3);    // wait for "sync length" time - 1
  // 127?
  // NOM20; NOM20; NOM20; NOM20; NOM20; NOM20; NOM5;
  PORTB = PORTB ^ HSYNC;          // invert HSYNC again
  // NOM20; NOM20; NOM20; NOM20; NOM20; NOM20;
  // NOM20; NOM20; NOM20; NOM20; NOM20; NOM20; NOM10; NOM; NOM; NOM;
  // NOM; NOM; NOM; NOM;
  // NOM; NOM; NOM; NOM;

}

void hSyncSig(void)
{
  // PORTD = PORTD ^ PIN2;
  PORTB = PORTB ^ HSYNC;         // invert HSYNC - negative polarity
  NOM5; NOM5;
  delayMicroseconds(3);    // wait for "sync length" time - 1
  PORTB = PORTB ^ HSYNC;          // invert HSYNC again
}

ISR(TIMER1_COMPA_vect)
{
  // do hSyncSig throughout blank interval, hLine for pixel data
  if (timingCount < 0) {
    delayMicroseconds(14);
    NOM;
    // 14 + 1 NOM was good for 38 pix; what will 40 take?
    NOM5; NOM5;
    hSyncSig();
    // do vsync with positive polarity
    if (timingCount == -10) PORTB = PORTB ^ VSYNC;
    if (timingCount == -5) PORTB = PORTB ^ VSYNC;
  } else {
    hLine();
    vCount++;
  }
  // was 159, why?
  // Wait for front porch

  timingCount++;
  if (vCount == 4) {
    vCount = 0;
    hzCount++;
    base = hzCount * 19;
  }
  if (hzCount > 71) {
    hzCount = 0;
    timingCount = -10;
    base = 0;
  }
}

void setup(void)
{
  // pins
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);
  digitalWrite(4, LOW);  // start red low
  digitalWrite(5, LOW);  // start green low
  digitalWrite(6, LOW);  // start blue low
  digitalWrite(12, HIGH);  // start VSYNC high
  digitalWrite(13, LOW);   // start HSYNC low

  // external interrupt timer, see ATmega328 datasheet for more details
  // enables real-time change of the count-up register
  // temporarily disable interrupts
  cli();
  // EICRA: External Interrupt Control Register A
  // ISC11, ISC10 = 1: with both set, rising edge of INT1 causes interrupt
  // EICRA = 1 << ISC11 | 1 << ISC10;
  // EIMSK: External interrupt Mask Register
  // INT0 = 1: when enabled, Interrupt Request 1 is enabled
  // EIMSK = 1 << INT0;
  // PCICR: Pin Change Interrupt Control Register
  // PCIE2 = 1: when enabled, fires an interrupt when PCINT23:16 pins are changed
  // PCICR = 1 << PCIE2;
  // PCINT19 = Arduino pin 3

  // hsync timer, see ATmega328 datasheet for more details

  // TIMSK0: Timer/Counter1 Interrupt Mask Register
  // TOIE0 = 1: when set, enables the Timer/Counter1 Overflow interrupt
  // This enables everything already set EXCEPT for TOIE0
  TIMSK0 &= !(1 << TOIE0);
  // Timer/Counter Control Registers
  // Timer/Counter1 Control Register A
  // DEPRECATED: Ignore any functionality provided by this Register
  // WGM10 = 1; combined with WGM12 in TCCR1B, enables count to TOP
  TCCR1A = 0;
  // Timer/Counter1 Control Register B
  // WGM22 = 8: Set timer to CTC mode with TOP = OCR1A (WGM12 may also work?)
  // CS10 = 1: Do not prescale the clock
  TCCR1B = 1 << WGM12 | 1 << CS10;
  // Sets the TOP (highest) value the Counter will count to
  // F_ocr1a = 16MHz / (2 * prescale * OCR1A);
  // F = 1/192 MHz: usu. 0x5FF; F = 1/57 MHz; usu. 1C7 F = 1/82 MHz; usu. 28F; 1/56: usu. 1BF
  OCR1A = 0x01BF;
  // TIMSK1: Timer/Counter1 Interrupt Mask Register
  // OCIE1A = 2: when set, enables the Timer1 Output Compare A Match interrupt
  TIMSK1 = 1 << OCIE1A;
  // re-enable interrupts
  sei();
}

void loop(){
  //twiddle thumbs

}

Now this isn't the greatest firmware in the world.  One thing it gets hung up on is all the conditional statements in here.  To make this code run more efficiently, you should use smarter branching by assuming the code path that runs most frequently is the one that is going to run, then using "return"s instead of additional "if"s.

Make a picture, make a picture!

Finally, this code isn't very interesting unless you can display something cool (or at least easily, without having to think in hex to make your bitmap).  Below is a program written in Visual Basic .NET that will load a Windows 24-bit bitmap file and convert that to the special 3-bit format I use.  It'll output the hex that you can put directly into your firmware in the spot specified.  (Feel free to convert this code to the language of your choice.  I realize VB.NET isn't really open-source friendly, but it's an old project and this is what I wrote in back then.)

Imports System.IO
Imports System.Drawing.Imaging
Imports System.Text.RegularExpressions

Public Class Form1

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        ' ************************
        ' * 1. Variable Setup    *
        ' ************************
        Dim b As New Bitmap("C:\Users\Stephen\Pictures\arduino pic 1.bmp")
        Dim ms As New MemoryStream
        Dim stri As String = ""
        Dim hexNum As String
        Dim colors(63) As String
        Dim arduinoColors() = {&H0, &H1, &H2, &H4, &H6, &H5, &H3, &H7} ' KRGBCMYW
        ' now make every possible combination of 2 colors (black + ___ is already done! :)
        b.Save(ms, ImageFormat.Bmp)
        Dim abyt(ms.Length - 1) As Byte
        ms.Seek(0, SeekOrigin.Begin)
        ms.Read(abyt, 0, ms.Length)
        Dim mx As New MemoryStream(abyt)
        Me.BackgroundImage = Bitmap.FromStream(mx)
        ' go through and hex the whole bitmap
        For a = 0 To abyt.Length - 1
            hexNum = Hex(abyt(a))
            If hexNum.Length = 1 Then hexNum = "0" & hexNum
            stri = stri & ",0x" & hexNum
        Next
        ' look for the color palette
        hexNum = Regex.Match(stri, "0xFF,0xFF,0xFF,(0x00,0x00,0x00,.{105})").Groups(1).ToString
        'MsgBox(hexNum)
        Dim i As Integer = 0
        ' gather our colors
        For Each Color As Match In Regex.Matches(hexNum, "0x[0-9A-F][0-9A-F],0x[0-9A-F][0-9A-F],0x[0-9A-F][0-9A-F]")
            colors(i) = "C" & Color.ToString
            i += 1
        Next

        ' **************************************
        ' * 2. First-Pass Pixel Replacement    *
        ' **************************************
        ' delete bitmap headers & palette -- first pixel better not be white!
        ' If there are row bounds, use bottom two lines; if not, use top
        ' ---
        stri = Regex.Match(stri, "0xFF,0xFF,0xFF,0xFF,(?!0xFF)(.*)").Groups(1).ToString
        ' stri = Regex.Match(stri, "0xD4,0x2D,(.*)").Groups(1).ToString
        ' stri = Regex.Replace(stri, "0xD4,0x2D,", "")
        ' ---

        ' demarcate pixel bounds to faciliate replacement regexes
        stri = Regex.Replace(stri, "(0x[0-9A-F][0-9A-F],0x[0-9A-F][0-9A-F],0x[0-9A-F][0-9A-F])", "C$1")

        ' run the replacement of palette colors to Arduino colors
        For i = 0 To 7
            stri = Regex.Replace(stri, colors(i), "0x" & Hex(arduinoColors(i)))
        Next
        TextBox1.Text = stri

        ' ************************
        ' * 3. Image Rotation    *
        ' ************************
        Dim myBytes() As String = stri.Split(",")
        stri = ""
        For i = 0 To 71     ' # of pixel columns is hard-coded for now
            For j = 0 To 37 ' # of pixel rows is hard-coded for now
                Try
                    stri = stri & myBytes((j * 72) + i) & ", "
                Catch ex As Exception
                    stri = stri & "0xGG, "  ' something invalid is obvious, something valid keeps a picture
                End Try
            Next
        Next
        TextBox1.Text = stri

        ' *********************
        ' * 4. Pixel Merge    *
        ' *********************
        stri = Regex.Replace(stri, "(0x[0-9A-F]), 0x([0-9A-F]),", "$1$2,")
        TextBox1.Text = stri

    End Sub

End Class

Now I haven't done this in a really long time, but as I recall, you actually have to submit the picture sideways, rotated 90 degrees to the left of upright.  The very last row of pixels on the bottom needs to be your color palette; you can draw the image using whatever colors you want in Paint, but you have to define which color corresponds to black, red, green, blue, cyan, magenta, yellow, and white.  Just one pixel per color is enough; the rest in that row can be white.  Unfortunately, I don't have a sample bitmap at this time (I guess I haven't rescued it from the failed hard drive yet) but if I find it, I'll post it here.

Comments

Popular posts from this blog

Less Coding, More Prompt Engineering!

Start Azure Pipeline from another pipeline with ADO CLI & PowerShell

An Augmented Reality Experience to Complement a Vintage Pinball Machine