[Home] [Downloads] [Search] [Help/forum]

Gammon Software Solutions forum

See www.mushclient.com/spam for dealing with forum spam. Please read the MUSHclient FAQ!

[Folder]  Entire forum
-> [Folder]  Electronics
. -> [Folder]  Microprocessors
. . -> [Subject]  Arduino Uno output to VGA monitor
Home  |  Users  |  Search  |  FAQ
Username:
Register forum user name
Password:
Forgotten password?

Arduino Uno output to VGA monitor

Postings by administrators only.

[Refresh] Refresh page


Posted by Nick Gammon   Australia  (19,505 posts)  [Biography] bio   Forum Administrator
Date Sat 21 Apr 2012 04:12 AM (UTC)  quote  ]

Amended on Tue 26 Nov 2013 01:34 AM (UTC) by Nick Gammon

Message
This post shows how you can output VGA signals to a monitor, turning your Uno (or similar) processor into a "video card". Example:



The only extra hardware used here was 5 resistors, and a DB15 socket to plug the monitor cable into:



Watch live demo:




VGA Timing


To understand how it all works, we need to look at how VGA signal timing works.

There are 5 signals being sent to the monitor:


  • Vertical Synchronization (sync) (TTL)
  • Horizontal Synchronization (sync) (TTL)
  • Red analog data (0 to 0.7V)
  • Green analog data (0 to 0.7V)
  • Blue analog data (0 to 0.7V)


For timing reasons we don't have time to output separate red, green, and blue levels, so they are physically connected at the socket, giving white.

[EDIT] However see follow-up post showing how you can do colour.

Thus there are three important signals now:


  • Vertical sync pulses
  • Horizontal sync pulses
  • Pixel data


On the logic analyzer we can see an overall view:



The two lower signals are just blocks of white right now - we'll have to zoom in to see them better.

Overall structure


This diagram shows the various timings involved:



Assuming for discussion that we have a 640 x 480 resolution screen, the hardware needs a little extra time to move the electron beam (in the days when we used CRT monitors) from one side of the screen to the other, and from the bottom back to the top. These are the front and back "porches" (don't ask me why the name).

The screen refresh starts off with a vertical sync pulse, which tells the monitor to reset to the top of the screen. It has the "back porch" number of lines to get ready to start drawing, and to draw a blank area at the top. Then it draws the image, and has the "front porch" extra lines to draw the blank information at the bottom of the screen.

Meanwhile, for each line, there is a horizontal sync pulse which signals the start of that line, followed by another delay to give the beam time to get ready, then it draws the line, and has some extra "front porch" time over for the blank part at the end of the line.

Vertical Sync


Let's start with the vertical sync pulses. In fact, we'll show how all the timing data can be derived from three figures:


  • The screen refresh rate (eg. 60 Hz)
  • The screen resolution (eg. 640 x 480)


So what is the refresh rate? It's the rate at which the entire screen is redrawn. If you ever looked at Windows screen resolution you probably saw something like this:



That's the refresh rate: 60 Hz (60 times a second). This figure was probably originally chosen because it is the mains frequency in the USA, so that would minimize the artifact of mains hum bars appearing on the screen, in the days of CRT monitors.

The other figure of interest is the screen resolution, for example:



In our case we are going for the minimum we can (640 x 480) and seeing where those figures lead us.

Starting with the refresh rate, a 60 Hz refresh rate will require a "vertical sync" pulse 60 times a second, or a period of 1/60 (16.66 mS).

This screenshot shows that the vertical sync pulses are indeed (nearly) 16.66 mS apart:



Observed figures might differ from theory by about 1% due to the internal clock of the processor not being 100% accurate.

Generating vertical sync pulses


This code, in setup, generates the vertical sync pulses:


  // Timer 1 - vertical sync pulses
  pinMode (vSyncPin, OUTPUT); 
  Timer1::setMode (15, Timer1::PRESCALE_1024, Timer1::CLEAR_B_ON_COMPARE);
  OCR1A = 259;  // 16666 / 64 uS = 260 (less one)
  OCR1B = 0;    // 64 / 64 uS = 1 (less one)


The clock period is 62.5 nS (1/16000000). By applying a prescaler of 1024 the timer counts up once every 64 uS. So to get a period of 1/60 of a second (16666 uS) we need to count up to 260 (16666 / 64 = 260). Since the timer counts are zero-relative we set OCR1A to count up to 259. This sets the frequency of Timer 1.

Then we need to set the pulse width by putting the correct value in OCR1B. We want a pulse width of two lines. One line is 1 / 60 / 525, namely 31.7 uS. So two lines would be 63.4 uS. This is close enough to exactly one timer count (64 uS). So OCR1B is set to zero (being zero-relative, making it zero gives a count of 1).

Timer 1 is configured to "clear B on compare" which effectively means that it toggles the output pin (D10 on the Uno) so it is high for the duty cycle width (64 uS) and low the rest of the time. If you wanted the opposite sync pulse polarity, change CLEAR_B_ON_COMPARE to SET_B_ON_COMPARE.

We can see from the logic analyzer output that the vertical sync pulses are indeed happening at 60 Hz, and that the pulse width is 63.9 uS.

So far so good ...



The timer is also set up to generate an interrupt, which is used to tell the code that we are starting another vertical cycle, by setting the line count to zero:


ISR (TIMER1_OVF_vect)
  {
  vLine = 0;
  messageLine = 0;
  backPorchLinesToGo = verticalBackPorchLines;
  } // end of TIMER1_OVF_vect


Horizontal sync


Horizontal sync pulses tell the monitor when to start drawing each line.

To calculate the horizontal sync frequency we need to divide the overall frame rate (60 Hz) by the number of total lines (525 if you count the sync pulse itself, and the front and back porches).

In other words:


 (1/60) / 525 * 1e6 = 31.74 uS


We also need to know the sync pulse width. That is documented to be 96 pixels, so we need to know the width of one pixel. That would be the figure above, divided by 800 (being the total screen width including the sync pulse, and front and back porches).

Thus that is:


((1/60) / 525 * 1e9) / 800 = 39.68  nS
1 / (((1/60) / 525 * 1e6) / 800) = 25.2 MHz


So our pixel width is 39.68 nS and the pixel clock is 25.2 mHz.

The horizontal sync pulse width is 96 pixels, so we want a pulse of:


96 * 39.68 nS = 3.8 uS


Generating horizontal sync pulses


Now we are ready to set up the timer for the horizontal sync pulses:


  // Timer 2 - horizontal sync pulses
  pinMode (hSyncPin, OUTPUT); 
  Timer2::setMode (7, Timer2::PRESCALE_8, Timer2::CLEAR_B_ON_COMPARE);
  OCR2A = 63;   // 32 / 0.5 uS = 64 (less one)
  OCR2B = 7;    // 4 / 0.5 uS = 8 (less one)


The clock period is 62.5 nS (1/16000000). By applying a prescaler of 8 the timer counts up once every 0.5 uS. So to get a period of 32 uS we need to count up to 64 (32 / 0.5 = 64). Since the timer counts are zero-relative we set OCR2A to count up to 63. This sets the frequency of Timer 2.

Then we need to set the pulse width by putting the correct value in OCR2B. We want a pulse width of 96 pixels (3.8 uS). So OCR2B is set to 7 (being zero-relative, making it 7 gives a count of 8, which is 4 / 0.5).

Again, if you wanted the opposite sync pulse polarity, change CLEAR_B_ON_COMPARE to SET_B_ON_COMPARE.

We can see from the logic analyzer output that the horizontal sync pulses are indeed happening every 31.9 uS, and that the pulse width is 4 uS.



The timer is also set up to generate an interrupt, which has the sole purpose of waking the processor up from sleep, so it can draw each line with exactly the same delay after the pulse. If it wasn't asleep, there would be a variation of two to three clock cycles (since an interrupt cannot occur during a single instruction) and this gives very bad-looking "jitter" on the screen.


ISR (TIMER2_OVF_vect)
  {
  } // end of TIMER2_OVF_vect



Pixel data


Now things get tricky ...

Let's see how long we have to draw 640 pixels:


((1/60) / 525 * 1e9) / 800  * 640 = 25396.82 nS (25.39 uS)


It just can't be done on this processor. The clock period itself is only 62.5 nS, so there is no way we can output a pixel every 39.68 nS (that is: 25396.82 / 640 = 39.68).

The fastest way we can get bits "out the door" is using the SPI hardware. That can run at a maximum clock rate of twice the system clock, that is, one pixel every 125 nS. So we will have to settle for having the pixels 4 times as wide. A horizontal resolution therefore of 160 pixels. Since we will display 8-pixel characters, that gives us 20 characters per line.

We also don't have enough memory to even hold those pixels all at once. Even at a bit per pixel, that would be:


160 * 480 / 8 = 9600 bytes


Since the processor only has 2048 bytes of memory there is no way we can hold any sort of bitmap. So, we will have to decode text into pixels "on the fly" for every line. This itself takes time. Especially since to hold 256 characters as 8 bytes each (8 x 8 pixels) takes 2048 bytes. So we can't hold the font data in RAM either.

So, the fonts have to go into PROGMEM (program memory) and be accessed on the fly for every line, using "program memory read".

I am using the USART in SPI mode. This is double-buffered so hopefully should give us a slight edge in performance. Even doing that isn't perfect because of what I have read of as the "9th bit problem". That is, the USART sends out one additional "high" bit per byte. This looks very annoying if you are doing white-on-black because there is a white bar between each letter. To hide this, we invert the fonts to give black on white text. Then the extra bit just looks like a space between the letters.

The line drawing is done in doOneScanLine which is called directly after being woken from sleep. As mentioned above, the sleep is necessary to make sure that we start each scan line at exactly the same time after the sync pulse:


void loop() 
  {
  // sleep to ensure we start up in a predictable way
  sleep_mode ();
  doOneScanLine ();
 }  // end of loop


Inside doOneScanLine we check if we have done the back porch lines, and if not, exit. If it is time to draw something, we pre-load some pointers (to save time) and then execute this loop:


  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));


For each byte in the current "message" (text) line, we look up the relevant pixel (font) data in program memory, and stuff that into the USART "send" register.

The font data has been deliberately structured to save a multiply by 8. Initially I had each character together (so from one letter to the next was 8 bytes, since it takes 8 bytes per character). Unfortunately, multiplying by 8 (even if you do it via a shift left) just took too long. So instead, the font table was restructured so all the data per scan line was together, not all the data for one letter. Also the font is pre-inverted (to white on black) to save doing that at drawing time.

There aren't any delays or tests for "is SPI ready?" in that loop. Let's see why not. The code for that loop is:


  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));
 (1)    f04:	32 2f       	mov	r19, r18
 (1)    f06:	22 27       	eor	r18, r18
 (2)    f08:	0b c0       	rjmp	.+22     	; 0xf20 

----
 (2)    f0a:	ed 91       	ld	r30, X+
 (1)    f0c:	ff 27       	eor	r31, r31
 (1)    f0e:	e7 fd       	sbrc	r30, 7
 (1)    f10:	f0 95       	com	r31
 (1)    f12:	e2 0f       	add	r30, r18
 (1)    f14:	f3 1f       	adc	r31, r19
 (1)    f16:	e8 59       	subi	r30, 0x98	; 152
 (1)    f18:	ff 4f       	sbci	r31, 0xFF	; 255
 (3)    f1a:	e4 91       	lpm	r30, Z+
 (2)    f1c:	e0 93 c6 00 	sts	0x00C6, r30
 (1)    f20:	81 50       	subi	r24, 0x01	; 1
 (2)    f22:	98 f7       	brcc	.-26     	; 0xf0a 

----
17 cycles in loop = 1062.5 nS


Apart from the initial setup at the start of the loop (done once) there are 17 cycles in the loop. I added the cycle count for each instruction at the start of the line.

Now it takes 16 cycles to output 8 bits (since each bit takes 125 nS) so each byte is transferred in 16 cycles, and the loop above takes 17. So that gives us a 1-cycle gap between letters, which apparently we get anyway because of the way that SPI works. So the loop is exactly the right length.

One scan line being drawn:



Note that the thinnest pixel (arrowed) is 125 nS long. That's the shortest we can do.

Finished code


The finished code is remarkably short. This is what it looks like without any ability to accept incoming text changes:


/*
 VGA video generation
 
 Author:   Nick Gammon
 Date:     20th April 2012
 
 Example code without I2C communications.

 Connections:
 
 D1 : Pixel output (180 ohms in series) (connect to R, G, B tied together)   --> Pins 1, 2, 3 on DB15 socket
 D3 : Horizontal Sync (68 ohms in series) --> Pin 13 on DB15 socket
 D10 : Vertical Sync (68 ohms in series) --> Pin 14 on DB15 socket
 
 Gnd : --> Pins 5, 6, 7, 8, 10 on DB15 socket

*/

#include <TimerHelpers.h>
#include <avr/pgmspace.h>
#include "screenFont.h"
#include <avr/sleep.h>

#define BETA_ARDUINO ARDUINO < 100

const byte pixelPin = 1;     // <------- Pixel data
const byte hSyncPin = 3;     // <------- HSYNC
const byte MSPIM_SCK = 4;    // <-- we aren't using it directly
const byte vSyncPin = 10;    // <------- VSYNC

const int horizontalBytes = 20;  // 160 pixels wide
const int verticalPixels = 480;  // 480 pixels high

const byte i2cAddress = 42;

// Timer 1 - Vertical sync

// output    OC1B   pin 16  (D10) <------- VSYNC

//   Period: 16.64 mS (60 Hz)
//      1/60 * 1e6 = 16666.66 uS
//   Pulse for 64 uS  (2 x HSync width of 32 uS)
//    Sync pulse: 2 lines
//    Back porch: 33 lines
//    Active video: 480 lines
//    Front porch: 10 lines
//       Total: 525 lines

// Timer 2 - Horizontal sync

// output    OC2B   pin 5  (D3)   <------- HSYNC

//   Period: 32 uS (31.25 kHz)
//      (1/60) / 525 * 1e6 = 31.74 uS
//   Pulse for 4 uS (96 times 39.68 nS)
//    Sync pulse: 96 pixels
//    Back porch: 48 pixels
//    Active video: 640 pixels
//    Front porch: 16 pixels
//       Total: 800 pixels

// Pixel time =  ((1/60) / 525 * 1e9) / 800 = 39.68  nS
//  frequency =  1 / (((1/60) / 525 * 1e6) / 800) = 25.2 MHz

// However in practice, it is the SPI speed, namely a period of 125 nS
//     (that is 2 x system clock speed)
//   giving an 8 MHz pixel frequency. Thus the characters are about 3 times too wide.
// Thus we fit 160 of "our" pixels on the screen in what usually takes 3 x 160 = 480

const byte screenFontHeight = 8;
const byte screenFontWidth = 8;

const int verticalLines = verticalPixels / screenFontHeight / 2;  // double-height characters
const int horizontalPixels = horizontalBytes * screenFontWidth;

const byte verticalBackPorchLines = 35;  // includes sync pulse?
const int verticalFrontPorchLines = 525 - verticalBackPorchLines;

volatile int vLine;
volatile int messageLine;
volatile byte backPorchLinesToGo;

char message [verticalLines]  [horizontalBytes];

// ISR: Vsync pulse
ISR (TIMER1_OVF_vect)
  {
  vLine = 0;
  messageLine = 0;
  backPorchLinesToGo = verticalBackPorchLines;
  } // end of TIMER1_OVF_vect
  
// ISR: Hsync pulse ... this interrupt merely wakes us up
ISR (TIMER2_OVF_vect)
  {
  } // end of TIMER2_OVF_vect
    
void setup()
  {
  
  // initial message ... change to suit
  for (int i = 0; i < verticalLines; i++)
    sprintf (message [i], "Line %03i - hello!", i);
   
  // disable Timer 0
  TIMSK0 = 0;  // no interrupts on Timer 0
  OCR0A = 0;   // and turn it off
  OCR0B = 0;
  
  // Timer 1 - vertical sync pulses
  pinMode (vSyncPin, OUTPUT); 
  Timer1::setMode (15, Timer1::PRESCALE_1024, Timer1::CLEAR_B_ON_COMPARE);
  OCR1A = 259;  // 16666 / 64 uS = 260 (less one)
  OCR1B = 0;    // 64 / 64 uS = 1 (less one)
  TIFR1 = bit (TOV1);   // clear overflow flag
  TIMSK1 = bit (TOIE1);  // interrupt on overflow on timer 1

  // Timer 2 - horizontal sync pulses
  pinMode (hSyncPin, OUTPUT); 
  Timer2::setMode (7, Timer2::PRESCALE_8, Timer2::CLEAR_B_ON_COMPARE);
  OCR2A = 63;   // 32 / 0.5 uS = 64 (less one)
  OCR2B = 7;    // 4 / 0.5 uS = 8 (less one)
  TIFR2 = bit (TOV2);   // clear overflow flag
  TIMSK2 = bit (TOIE2);  // interrupt on overflow on timer 2
 
  // Set up USART in SPI mode (MSPIM)
  
  // baud rate must be zero before enabling the transmitter
  UBRR0 = 0;  // USART Baud Rate Register
  pinMode (MSPIM_SCK, OUTPUT);   // set XCK pin as output to enable master mode
  UCSR0B = 0; 
  UCSR0C = bit (UMSEL00) | bit (UMSEL01) | bit (UCPHA0) | bit (UCPOL0);  // Master SPI mode

  // prepare to sleep between horizontal sync pulses  
  set_sleep_mode (SLEEP_MODE_IDLE);  
 
}  // end of setup

// draw a single scan line
void doOneScanLine ()
  {
    
  // after vsync we do the back porch
  if (backPorchLinesToGo)
    {
    backPorchLinesToGo--;
    return;   
    }  // end still doing back porch
    
  // if all lines done, do the front porch
  if (vLine >= verticalPixels)
    return;
    
  // pre-load pointer for speed
  const register byte * linePtr = &screen_font [ (vLine >> 1) & 0x07 ] [0];
  register char * messagePtr =  & (message [messageLine] [0] );

  // how many pixels to send
  register byte i = horizontalBytes;

  // turn transmitter on 
  UCSR0B = bit (TXEN0);  // transmit enable (starts transmitting white)

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

  // wait till done    
  while (!(UCSR0A & bit(TXC0))) 
    {}
  
  // disable transmit
  UCSR0B = 0;   // drop back to black
  
  // this line done
  vLine++;  
  
  // every 16 pixels it is time to move to a new line in our text
  //  (because we double up the characters vertically)
  if ((vLine & 0xF) == 0)
    messageLine++;
    
  }  // end of doOneScanLine

void loop() 
  {
  // sleep to ensure we start up in a predictable way
  sleep_mode ();
  doOneScanLine ();
 }  // end of loop


This is the file "screenFont.h" which has the font patterns in it:



// bit patterns for the CP437 font, ROTATED, INVERTED

// To save rendering time, they are ordered by line (ie. line 1, line 2) and then 
//  indexed by character.

byte screen_font [8] [256] PROGMEM = {
// Row 1
 { 
  0xFF, 0xC0, 0xC0, 0xC9, 0xF7, 0xE3, 0xF7, 0xFF, 0x80, 0xFF, 0x80, 0xF8, 0xE1, 0xE0, 0xC0, 0xB3, 
  0xBF, 0xFE, 0xF3, 0xCC, 0xC0, 0xE0, 0xFF, 0xF3, 0xF3, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xE7, 0xC9, 0xC9, 0xE7, 0xFF, 0xE3, 0xCF, 0xF3, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 
  0xC1, 0xE7, 0xC3, 0xC3, 0xF1, 0x81, 0xE3, 0x81, 0xC3, 0xC3, 0xFF, 0xFF, 0xF3, 0xFF, 0xCF, 0xC3, 
  0xC1, 0xE7, 0x81, 0xE1, 0x83, 0x80, 0x80, 0xE1, 0x99, 0xC3, 0xF0, 0x8C, 0x87, 0x9C, 0x9C, 0xE3, 
  0x81, 0xC3, 0x81, 0xC3, 0x81, 0x99, 0x99, 0x9C, 0x9C, 0x99, 0x80, 0xC3, 0x9F, 0xC3, 0xF7, 0xFF, 
  0xE7, 0xFF, 0x8F, 0xFF, 0xF1, 0xFF, 0xE3, 0xFF, 0x8F, 0xE7, 0xF9, 0x8F, 0xC7, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xF3, 0x8F, 0xC4, 0xFF, 
  0xC3, 0xFF, 0xF1, 0xC0, 0x99, 0x8F, 0xE7, 0xFF, 0xC0, 0x99, 0x8F, 0x99, 0xC1, 0x8F, 0x9C, 0xE7, 
  0xF1, 0xFF, 0xE0, 0xC3, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x9E, 0x99, 0xF3, 0xE3, 0x99, 0x83, 0xF8, 
  0xF1, 0xE3, 0xFF, 0xFF, 0xFF, 0x81, 0xE1, 0xE3, 0xE7, 0xFF, 0xFF, 0x9E, 0x9E, 0xF3, 0xFF, 0xFF, 
  0xEE, 0xD5, 0x92, 0xF3, 0xF3, 0xF3, 0xE4, 0xFF, 0xFF, 0xE4, 0xE4, 0xFF, 0xE4, 0xE4, 0xF3, 0xFF, 
  0xF3, 0xF3, 0xFF, 0xF3, 0xFF, 0xF3, 0xF3, 0xE4, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xF3, 
  0xE4, 0xFF, 0xFF, 0xE4, 0xF3, 0xFF, 0xFF, 0xE4, 0xF3, 0xF3, 0xFF, 0x80, 0xFF, 0x87, 0xF8, 0x80, 
  0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0x81, 0xE3, 0xE3, 0xF1, 0xFF, 0xFC, 0xE3, 0xC3, 
  0xFF, 0xE7, 0xCF, 0xF3, 0xF8, 0xF3, 0xE7, 0xFF, 0xE3, 0xFF, 0xFF, 0xF8, 0xC3, 0xC7, 0xFF, 0xFF, 
  }, 
// Row 2
 { 
  0xFF, 0xBF, 0x80, 0x80, 0xE3, 0xC1, 0xF7, 0xFF, 0x80, 0xE1, 0x9E, 0xFC, 0xCC, 0xE6, 0xCE, 0xD2, 
  0x8F, 0xF8, 0xE1, 0xCC, 0x92, 0xCE, 0xFF, 0xE1, 0xE1, 0xF3, 0xF3, 0xE7, 0xFF, 0xED, 0xF3, 0x80, 
  0xFF, 0xC3, 0xC9, 0xC9, 0xC1, 0x9C, 0xC9, 0xCF, 0xE7, 0xE7, 0xCC, 0xE7, 0xFF, 0xFF, 0xFF, 0xF9, 
  0x9C, 0xC7, 0x99, 0x99, 0xE1, 0x9F, 0xCF, 0x99, 0x99, 0x99, 0xE7, 0xE7, 0xE7, 0xFF, 0xE7, 0x99, 
  0x9C, 0xC3, 0xCC, 0xCC, 0xC9, 0xCE, 0xCE, 0xCC, 0x99, 0xE7, 0xF9, 0xCC, 0xCF, 0x88, 0x8C, 0xC9, 
  0xCC, 0x99, 0xCC, 0x99, 0xA5, 0x99, 0x99, 0x9C, 0x9C, 0x99, 0x9C, 0xCF, 0xCF, 0xF3, 0xE3, 0xFF, 
  0xE7, 0xFF, 0xCF, 0xFF, 0xF9, 0xFF, 0xC9, 0xFF, 0xCF, 0xFF, 0xFF, 0xCF, 0xE7, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xF3, 0xE7, 0x91, 0xF7, 
  0x99, 0x99, 0xFF, 0x9E, 0xFF, 0xFF, 0xE7, 0xFF, 0x9E, 0xFF, 0xFF, 0xFF, 0x9C, 0xFF, 0xE3, 0xE7, 
  0xFF, 0xFF, 0xC9, 0x99, 0x99, 0x8F, 0x99, 0x8F, 0x99, 0xF3, 0xFF, 0xF3, 0xC9, 0x99, 0x99, 0xF2, 
  0xFF, 0xFF, 0xF1, 0xF1, 0x83, 0xFF, 0xC9, 0xC9, 0xFF, 0xFF, 0xFF, 0x9C, 0x9C, 0xF3, 0xE6, 0x99, 
  0xBB, 0xAA, 0xC4, 0xF3, 0xF3, 0xF3, 0xE4, 0xFF, 0xFF, 0xE4, 0xE4, 0xFF, 0xE4, 0xE4, 0xF3, 0xFF, 
  0xF3, 0xF3, 0xFF, 0xF3, 0xFF, 0xF3, 0xF3, 0xE4, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xF3, 
  0xE4, 0xFF, 0xFF, 0xE4, 0xF3, 0xFF, 0xFF, 0xE4, 0xF3, 0xF3, 0xFF, 0x80, 0xFF, 0x87, 0xF8, 0x80, 
  0xFF, 0xC3, 0x81, 0x80, 0x99, 0xFF, 0xCC, 0xC4, 0xE7, 0xC9, 0xC9, 0xE7, 0xFF, 0xF9, 0xCF, 0x99, 
  0x81, 0xE7, 0xE7, 0xE7, 0xF2, 0xF3, 0xE7, 0xC4, 0xC9, 0xFF, 0xFF, 0xF9, 0xC9, 0xF3, 0xFF, 0xFF, 
  }, 
// Row 3
 { 
  0xFF, 0xAD, 0x92, 0x80, 0xC1, 0xE3, 0xE3, 0xF3, 0x8C, 0xCC, 0xB3, 0xF8, 0xCC, 0xE0, 0xC0, 0xE1, 
  0x83, 0xE0, 0xC0, 0xCC, 0x92, 0xE3, 0xFF, 0xC0, 0xC0, 0xF3, 0xF9, 0xCF, 0x9F, 0xCC, 0xE1, 0x80, 
  0xFF, 0xC3, 0xC9, 0x80, 0x9F, 0x99, 0xE3, 0x9F, 0xCF, 0xF3, 0xE1, 0xE7, 0xFF, 0xFF, 0xFF, 0xF3, 
  0x98, 0xE7, 0xF9, 0xF9, 0xC9, 0x83, 0x9F, 0xF9, 0x99, 0x99, 0xE7, 0xE7, 0xCF, 0x81, 0xF3, 0xF9, 
  0x90, 0x99, 0xCC, 0x9F, 0xCC, 0xCB, 0xCB, 0x9F, 0x99, 0xE7, 0xF9, 0xC9, 0xCF, 0x80, 0x84, 0x9C, 
  0xCC, 0x99, 0xCC, 0x8F, 0xE7, 0x99, 0x99, 0x9C, 0xC9, 0x99, 0xB9, 0xCF, 0xE7, 0xF3, 0xC9, 0xFF, 
  0xF3, 0xC3, 0xCF, 0xC3, 0xF9, 0xC3, 0xCF, 0xC4, 0xC9, 0xC7, 0xF9, 0xCC, 0xE7, 0x99, 0x83, 0xC3, 
  0x91, 0xC4, 0x91, 0xC1, 0xC1, 0x99, 0x99, 0x9C, 0x9C, 0x99, 0x81, 0xE7, 0xF3, 0xE7, 0xFF, 0xE3, 
  0x9F, 0xFF, 0xC3, 0xE1, 0xC3, 0xC3, 0xC3, 0xC3, 0xE1, 0xC3, 0xC3, 0xC7, 0xE3, 0xC7, 0xC9, 0xFF, 
  0x81, 0xC0, 0x99, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0x99, 0xC0, 0xCD, 0xC3, 0x99, 0xF3, 
  0xC3, 0xC7, 0xFF, 0xFF, 0xFF, 0x99, 0xC9, 0xC9, 0xE7, 0xFF, 0xFF, 0x99, 0x99, 0xFF, 0xCC, 0xCC, 
  0xEE, 0xD5, 0x92, 0xF3, 0xF3, 0x83, 0xE4, 0xFF, 0x83, 0x84, 0xE4, 0x80, 0x84, 0xE4, 0x83, 0xFF, 
  0xF3, 0xF3, 0xFF, 0xF3, 0xFF, 0xF3, 0xF0, 0xE4, 0xE4, 0xE0, 0x84, 0x80, 0xE4, 0x80, 0x84, 0x80, 
  0xE4, 0x80, 0xFF, 0xE4, 0xF0, 0xF0, 0xFF, 0xE4, 0x80, 0xF3, 0xFF, 0x80, 0xFF, 0x87, 0xF8, 0x80, 
  0xC4, 0x99, 0x99, 0xC9, 0xCF, 0xC0, 0xCC, 0x91, 0xC3, 0x9C, 0x9C, 0xF3, 0xC0, 0xC0, 0x9F, 0x99, 
  0xFF, 0x81, 0xF3, 0xCF, 0xF2, 0xF3, 0xFF, 0x91, 0xC9, 0xFF, 0xFF, 0xF9, 0xC9, 0xE7, 0xE1, 0xFF, 
  }, 
// Row 4
 { 
  0xFF, 0xBF, 0x80, 0x80, 0x80, 0x80, 0xC1, 0xE1, 0x9E, 0xDE, 0xA1, 0xC1, 0xCC, 0xE7, 0xCE, 0x8C, 
  0x80, 0x80, 0xF3, 0xCC, 0xC2, 0xC9, 0xFF, 0xF3, 0xF3, 0xF3, 0x80, 0x80, 0x9F, 0x80, 0xC0, 0xC0, 
  0xFF, 0xE7, 0xFF, 0xC9, 0xC3, 0xF3, 0xC4, 0xFF, 0xCF, 0xF3, 0x80, 0x81, 0xFF, 0x81, 0xFF, 0xE7, 
  0x90, 0xE7, 0xE3, 0xE3, 0x99, 0xF9, 0x83, 0xF3, 0xC3, 0xC1, 0xFF, 0xFF, 0x9F, 0xFF, 0xF9, 0xF3, 
  0x90, 0x99, 0xC1, 0x9F, 0xCC, 0xC3, 0xC3, 0x9F, 0x81, 0xE7, 0xF9, 0xC3, 0xCF, 0x80, 0x90, 0x9C, 
  0xC1, 0x99, 0xC1, 0xC7, 0xE7, 0x99, 0x99, 0x94, 0xE3, 0xC3, 0xF3, 0xCF, 0xF3, 0xF3, 0x9C, 0xFF, 
  0xFF, 0xF9, 0xC1, 0x99, 0xC1, 0x99, 0x87, 0x99, 0xC4, 0xE7, 0xF9, 0xC9, 0xE7, 0x80, 0x99, 0x99, 
  0xCC, 0x99, 0xC4, 0x9F, 0xE7, 0x99, 0x99, 0x94, 0xC9, 0x99, 0xB3, 0x8F, 0xFF, 0xF1, 0xFF, 0xC9, 
  0x99, 0x99, 0x99, 0xFC, 0xF9, 0xF9, 0xF9, 0x9F, 0xCC, 0x99, 0x99, 0xE7, 0xF3, 0xE7, 0x9C, 0xC3, 
  0xCF, 0xF9, 0x80, 0xC3, 0xC3, 0xC3, 0x99, 0x99, 0x99, 0xCC, 0x99, 0x9F, 0x87, 0x81, 0x82, 0xE1, 
  0xF9, 0xE7, 0xC3, 0x99, 0x83, 0x89, 0xE0, 0xE3, 0xCF, 0x81, 0x81, 0x90, 0x92, 0xF3, 0x99, 0xE6, 
  0xBB, 0xAA, 0x88, 0xF3, 0xF3, 0xF3, 0xE4, 0xFF, 0xF3, 0xFC, 0xE4, 0xFC, 0xFC, 0xE4, 0xF3, 0xFF, 
  0xF3, 0xF3, 0xFF, 0xF3, 0xFF, 0xF3, 0xF3, 0xE4, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 
  0xE4, 0xFF, 0xFF, 0xE4, 0xF3, 0xF3, 0xFF, 0xE4, 0xF3, 0xF3, 0xFF, 0x80, 0xFF, 0x87, 0xF8, 0x80, 
  0x91, 0x83, 0x9F, 0xC9, 0xE7, 0x93, 0xCC, 0xF3, 0x99, 0x80, 0x9C, 0xC1, 0x92, 0x92, 0x83, 0x99, 
  0x81, 0xE7, 0xE7, 0xE7, 0xF3, 0xF3, 0x81, 0xFF, 0xE3, 0xF3, 0xFF, 0xF9, 0xC9, 0xCF, 0xE1, 0xFF, 
  }, 
// Row 5
 { 
  0xFF, 0xA1, 0x9E, 0xC1, 0xC1, 0x80, 0x80, 0xE1, 0x9E, 0xDE, 0xA1, 0x99, 0xE1, 0xE7, 0xCE, 0x8C, 
  0x83, 0xE0, 0xF3, 0xCC, 0xF2, 0xC9, 0xC0, 0xC0, 0xF3, 0xC0, 0xF9, 0xCF, 0x9F, 0xCC, 0x80, 0xE1, 
  0xFF, 0xE7, 0xFF, 0x80, 0xF9, 0xE7, 0x91, 0xFF, 0xCF, 0xF3, 0xE1, 0xE7, 0xFF, 0xFF, 0xFF, 0xCF, 
  0x84, 0xE7, 0xCF, 0xF9, 0x80, 0xF9, 0x99, 0xE7, 0x99, 0xF9, 0xFF, 0xFF, 0xCF, 0xFF, 0xF3, 0xE7, 
  0x90, 0x81, 0xCC, 0x9F, 0xCC, 0xCB, 0xCB, 0x98, 0x99, 0xE7, 0x99, 0xC9, 0xCE, 0x94, 0x98, 0x9C, 
  0xCF, 0x91, 0xC9, 0xF1, 0xE7, 0x99, 0x99, 0x80, 0xE3, 0xE7, 0xE6, 0xCF, 0xF9, 0xF3, 0xFF, 0xFF, 
  0xFF, 0xC1, 0xCC, 0x9F, 0x99, 0x81, 0xCF, 0x99, 0xCC, 0xE7, 0xF9, 0xC3, 0xE7, 0x80, 0x99, 0x99, 
  0xCC, 0x99, 0xCC, 0xC3, 0xE7, 0x99, 0x99, 0x80, 0xE3, 0x99, 0xE7, 0xE7, 0xF3, 0xE7, 0xFF, 0x9C, 
  0xC3, 0x99, 0x81, 0xE0, 0xC1, 0xC1, 0xC1, 0x9F, 0xC0, 0x81, 0x81, 0xE7, 0xF3, 0xE7, 0x80, 0x99, 
  0xC3, 0xC0, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xCC, 0x99, 0x9F, 0xCF, 0xE7, 0x9C, 0xF3, 
  0xC1, 0xE7, 0x99, 0x99, 0x99, 0x81, 0xFF, 0xFF, 0x9F, 0x9F, 0xF9, 0xE6, 0xE4, 0xF3, 0xCC, 0xCC, 
  0xEE, 0xD5, 0x92, 0xF3, 0x83, 0x83, 0x84, 0x80, 0x83, 0x84, 0xE4, 0x84, 0x80, 0x80, 0x83, 0x83, 
  0xF0, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF0, 0xE4, 0xE0, 0xE4, 0x80, 0x84, 0xE4, 0x80, 0x84, 0x80, 
  0x80, 0x80, 0x80, 0xE0, 0xF0, 0xF0, 0xE0, 0x80, 0x80, 0x83, 0xF0, 0x80, 0x80, 0x87, 0xF8, 0xFF, 
  0x9B, 0x99, 0x9F, 0xC9, 0xCF, 0x93, 0xCC, 0xF3, 0x99, 0x9C, 0xC9, 0x99, 0x92, 0x92, 0x9F, 0x99, 
  0xFF, 0xE7, 0xCF, 0xF3, 0xF3, 0xF3, 0xFF, 0xC4, 0xFF, 0xF3, 0xF3, 0x89, 0xC9, 0xC3, 0xE1, 0xFF, 
  }, 
// Row 6
 { 
  0xFF, 0xB3, 0x8C, 0xE3, 0xE3, 0xC1, 0xC1, 0xF3, 0x8C, 0xCC, 0xB3, 0x99, 0xF3, 0xC7, 0xCC, 0xE1, 
  0x8F, 0xF8, 0xC0, 0xFF, 0xF2, 0xE3, 0xC0, 0xE1, 0xF3, 0xE1, 0xF3, 0xE7, 0x80, 0xED, 0x80, 0xF3, 
  0xFF, 0xFF, 0xFF, 0xC9, 0x83, 0xCC, 0x99, 0xFF, 0xE7, 0xE7, 0xCC, 0xE7, 0xE7, 0xFF, 0xE7, 0x9F, 
  0x8C, 0xE7, 0x99, 0x99, 0xF9, 0x99, 0x99, 0xE7, 0x99, 0xF3, 0xE7, 0xE7, 0xE7, 0x81, 0xE7, 0xFF, 
  0x9F, 0x99, 0xCC, 0xCC, 0xC9, 0xCE, 0xCF, 0xCC, 0x99, 0xE7, 0x99, 0xCC, 0xCC, 0x9C, 0x9C, 0xC9, 
  0xCF, 0xC3, 0xCC, 0x99, 0xE7, 0x99, 0xC3, 0x88, 0xC9, 0xE7, 0xCC, 0xCF, 0xFC, 0xF3, 0xFF, 0xFF, 
  0xFF, 0x99, 0xCC, 0x99, 0x99, 0x9F, 0xCF, 0xC1, 0xCC, 0xE7, 0x99, 0xC9, 0xE7, 0x94, 0x99, 0x99, 
  0xC1, 0xC1, 0xCF, 0xF9, 0xE5, 0x99, 0xC3, 0x80, 0xC9, 0xC1, 0xCD, 0xE7, 0xF3, 0xE7, 0xFF, 0x9C, 
  0xF3, 0x99, 0x9F, 0xCC, 0x99, 0x99, 0x99, 0xC3, 0xCF, 0x9F, 0x9F, 0xE7, 0xF3, 0xE7, 0x9C, 0x81, 
  0xCF, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xC1, 0xE1, 0x99, 0xC0, 0x8C, 0x81, 0x98, 0xF3, 
  0x99, 0xE7, 0x99, 0x99, 0x99, 0x91, 0xC0, 0xC1, 0x99, 0x9F, 0xF9, 0xCC, 0xC8, 0xF3, 0xE6, 0x99, 
  0xBB, 0xAA, 0xC4, 0xF3, 0xF3, 0xF3, 0xE4, 0xE4, 0xF3, 0xE4, 0xE4, 0xE4, 0xFF, 0xFF, 0xFF, 0xF3, 
  0xFF, 0xFF, 0xF3, 0xF3, 0xFF, 0xF3, 0xF3, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xE4, 0xFF, 0xE4, 0xFF, 
  0xFF, 0xF3, 0xE4, 0xFF, 0xFF, 0xF3, 0xE4, 0xE4, 0xF3, 0xFF, 0xF3, 0x80, 0x80, 0x87, 0xF8, 0xFF, 
  0x91, 0x83, 0x9F, 0xC9, 0x99, 0x93, 0xC1, 0xF3, 0xC3, 0xC9, 0xC9, 0x99, 0xC0, 0xC0, 0xCF, 0x99, 
  0x81, 0xFF, 0xFF, 0xFF, 0xF3, 0x93, 0xE7, 0x91, 0xFF, 0xFF, 0xFF, 0xC9, 0xFF, 0xFF, 0xE1, 0xFF, 
  }, 
// Row 7
 { 
  0xFF, 0xBF, 0x80, 0xF7, 0xF7, 0xE3, 0xE3, 0xFF, 0x80, 0xE1, 0x9E, 0x99, 0xC0, 0x87, 0x8C, 0xD2, 
  0xBF, 0xFE, 0xE1, 0xCC, 0xF2, 0x99, 0xC0, 0xF3, 0xF3, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xE7, 0xFF, 0xC9, 0xE7, 0x9C, 0xC4, 0xFF, 0xF3, 0xCF, 0xFF, 0xFF, 0xE7, 0xFF, 0xE7, 0xBF, 
  0xC1, 0x81, 0x81, 0xC3, 0xF0, 0xC3, 0xC3, 0xE7, 0xC3, 0xC7, 0xE7, 0xE7, 0xF3, 0xFF, 0xCF, 0xE7, 
  0xC3, 0x99, 0x81, 0xE1, 0x83, 0x80, 0x87, 0xE0, 0x99, 0xC3, 0xC3, 0x8C, 0x80, 0x9C, 0x9C, 0xE3, 
  0x87, 0xF1, 0x8C, 0xC3, 0xC3, 0x81, 0xE7, 0x9C, 0x9C, 0xC3, 0x80, 0xC3, 0xFE, 0xC3, 0xFF, 0xFF, 
  0xFF, 0xC4, 0x91, 0xC3, 0xC4, 0xC3, 0x87, 0xF9, 0x8C, 0xC3, 0x99, 0x8C, 0xC3, 0x9C, 0x99, 0xC3, 
  0xCF, 0xF9, 0x87, 0x83, 0xF3, 0xC4, 0xE7, 0xC9, 0x9C, 0xF9, 0x81, 0xF1, 0xF3, 0x8F, 0xFF, 0x80, 
  0xF9, 0xC0, 0xC3, 0xE0, 0xC0, 0xC0, 0xC0, 0xF9, 0xE1, 0xC3, 0xC3, 0xC3, 0xE1, 0xC3, 0x9C, 0x99, 
  0x81, 0xC0, 0x98, 0xC3, 0xC3, 0xC3, 0xC0, 0xC0, 0xF9, 0xF3, 0xC3, 0xF3, 0x81, 0xE7, 0x9C, 0x93, 
  0xC0, 0xC3, 0xC3, 0xC0, 0x99, 0x99, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x99, 0x98, 0xF3, 0xFF, 0xFF, 
  0xEE, 0xD5, 0x92, 0xF3, 0xF3, 0xF3, 0xE4, 0xE4, 0xF3, 0xE4, 0xE4, 0xE4, 0xFF, 0xFF, 0xFF, 0xF3, 
  0xFF, 0xFF, 0xF3, 0xF3, 0xFF, 0xF3, 0xF3, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xE4, 0xFF, 0xE4, 0xFF, 
  0xFF, 0xF3, 0xE4, 0xFF, 0xFF, 0xF3, 0xE4, 0xE4, 0xF3, 0xFF, 0xF3, 0x80, 0x80, 0x87, 0xF8, 0xFF, 
  0xC4, 0x9F, 0x9F, 0xC9, 0x81, 0xC7, 0xCF, 0xF3, 0xE7, 0xE3, 0x88, 0xC3, 0xFF, 0xCF, 0xE3, 0x99, 
  0xFF, 0x81, 0x81, 0x81, 0xF3, 0x93, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0xFF, 
  }, 
// Row 8
 { 
  0xFF, 0xC0, 0xC0, 0xFF, 0xFF, 0xC1, 0xC1, 0xFF, 0x80, 0xFF, 0x80, 0xC3, 0xF3, 0x8F, 0x9F, 0xB3, 
  0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xC3, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0x87, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xF3, 0xFF, 0xE7, 0x9C, 0xC7, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xFE, 0xFF, 0xFF, 0xFF, 
  0xBB, 0xAA, 0x88, 0xF3, 0xF3, 0xF3, 0xE4, 0xE4, 0xF3, 0xE4, 0xE4, 0xE4, 0xFF, 0xFF, 0xFF, 0xF3, 
  0xFF, 0xFF, 0xF3, 0xF3, 0xFF, 0xF3, 0xF3, 0xE4, 0xFF, 0xE4, 0xFF, 0xE4, 0xE4, 0xFF, 0xE4, 0xFF, 
  0xFF, 0xF3, 0xE4, 0xFF, 0xFF, 0xF3, 0xE4, 0xE4, 0xF3, 0xFF, 0xF3, 0x80, 0x80, 0x87, 0xF8, 0xFF, 
  0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 
  0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 
  }, 

};  //  end of screen_font


To compile you also need the timer helpers library:

The TimerHelpers.h file can be downloaded from:

http://gammon.com.au/Arduino/TimerHelpers.zip

Communicating with the VGA sketch


The problem with this sketch is, it's very busy. Almost all its time is spent refreshing the screen. There is a horizontal sync pulse every 32 uS and then it spends 25 uS drawing the pixels. So, not much time over to do other stuff. (There's a bit, because during the vertical retrace we aren't drawing pixels).

So really, we need to have a second processor that actually does the useful work (eg. monitor the aquarium, do the burglar alarm, measure the temperature) and send the text to the VGA output sketch for displaying.

To achieve this, the only real way is to use I2C. A pin needed for SPI (pin 10) is in use by Timer 1 for Vsync. The serial hardware is in use by the output routines. So that leaves I2C.

In the "communicating" version I have set up a I2C request event, like this:


  // for incoming data to display from I2C
  Wire.begin (i2cAddress);
  Wire.onReceive (receiveEvent);


The slave address is currently 42, but you can change that to any free address (just make sure the sending end uses the same address).

When incoming I2C data arrives it is directed to a "state machine" that decodes the text and puts it into the "message" array (30 lines x 20 characters). To make it more useful you can send a few "control" characters, namely:


  • carriage-return (0x0D): returns cursor to start of current line
  • newline (0x0A): drops down a line and also goes to the start of the line
  • clear screen (0x0C): clear screen, return cursor to 1,1
  • ESC (0x1B) followed by:

    • 1 : clear screen, return cursor to 1,1
    • 2 : clear to end of current line
    • 3 : go to x,y ... next two bytes are X and then Y: one-relative



The newline character also scrolls the display, by moving everything up one line, if it occurs with the cursor on the 30th line.

Unfortunately incoming data can tend to make the display "glitch" as the interrupt stops the display drawing in the middle of a line. Attempts to correct that have not been a big success. If you turn off interrupts long enough to stop the glitching you then have the problem of incoming data being dropped. So I think it's better to have the occasional glitch, and see everything, than have important messages disappear.

To try to minimize this I increased the I2C clock rate in the sending "test" sketch, so that we transfer the message to the VGA output sketch as fast as possible.

Example sending sketch:


// Video text sender
// Author: Nick Gammon
// Date:    20th April 2012
// License: Released to public domain.

#include <Wire.h>

const byte i2cAddress = 42;

enum SEND_COMMANDS { CLRSCR = 1, CLREOL, GOTOXY, ESC = 27 };
int count = 0;

char clearScreen [] = { ESC, CLRSCR };
char clearToEndOfLine [] = { ESC, CLREOL };
char gotoXY [] = { ESC, GOTOXY };  // followed by x and y

void setup () 
{
  Wire.begin ();
  TWBR = 4;   // fast .. fast .. I2C : 16000000 / (16 + 2 * 4) = 666666 Hz
}  // end of setup

void sendString (const char * s)
  {
  Wire.beginTransmission (i2cAddress);
  Wire.write (s);
  Wire.endTransmission ();
  }  // end of sendString
 
char buf [20];
   
void loop () 
{
  sendString (clearScreen);
  sendString ("Hi there\n");
  sendString ("Everyone\n");
  
  delay (2000);
 
  // clear screen test
  sendString (clearScreen);
  sendString ("After CLRSCR\n");

  // display a counter  
  sprintf (buf, "%i\n", count++);
  
  sendString (buf);

  delay (2000);  
}  // end of loop


To save a lot of repetition the sending is done inside the sendString function, which does the begin and end transmission as well. Note that I2C has a buffer size of 32, so don't try to send more than 32 bytes in one send. (So, a line at a time would be a good compromise).

Code with communications stuff added



/*
 VGA video generation
 
 Author:   Nick Gammon
 Date:     20th April 2012
 Version:  1.2
 
 Version 1.0: initial release
 Version 1.1: code cleanups
 Version 1.2: more cleanups, added clear screen (0x0C), added scrolling
 

 Connections:
 
 D1 : Pixel output (470 ohms in series to each one of R, G, B)   --> Pins 1, 2, 3 on DB15 socket
 D3 : Horizontal Sync (68 ohms in series) --> Pin 13 on DB15 socket
 D10 : Vertical Sync (68 ohms in series) --> Pin 14 on DB15 socket
 
 Gnd : --> Pins 5, 6, 7, 8, 10 on DB15 socket

 PERMISSION TO DISTRIBUTE
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 and associated documentation files (the "Software"), to deal in the Software without restriction, 
 including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in 
 all copies or substantial portions of the Software.
 
 
 LIMITATION OF LIABILITY
 
 The software is provided "as is", without warranty of any kind, express or implied, 
 including but not limited to the warranties of merchantability, fitness for a particular 
 purpose and noninfringement. In no event shall the authors or copyright holders be liable 
 for any claim, damages or other liability, whether in an action of contract, 
 tort or otherwise, arising from, out of or in connection with the software 
 or the use or other dealings in the software. 

*/

#include <TimerHelpers.h>
#include <avr/pgmspace.h>
#include "screenFont.h"
#include <avr/sleep.h>
#include <Wire.h>

#define BETA_ARDUINO ARDUINO < 100

const byte pixelPin = 1;     // <------- Pixel data
const byte hSyncPin = 3;     // <------- HSYNC
const byte MSPIM_SCK = 4;    // <-- we aren't using it directly
const byte vSyncPin = 10;    // <------- VSYNC

const int horizontalBytes = 20;  // 160 pixels wide
const int verticalPixels = 480;  // 480 pixels high

const byte i2cAddress = 42;

// Timer 1 - Vertical sync

// output    OC1B   pin 16  (D10) <------- VSYNC

//   Period: 16.64 mS (60 Hz)
//      1/60 * 1e6 = 16666.66 uS
//   Pulse for 64 uS  (2 x HSync width of 32 uS)
//    Sync pulse: 2 lines
//    Back porch: 33 lines
//    Active video: 480 lines
//    Front porch: 10 lines
//       Total: 525 lines

// Timer 2 - Horizontal sync

// output    OC2B   pin 5  (D3)   <------- HSYNC

//   Period: 32 uS (31.25 kHz)
//      (1/60) / 525 * 1e6 = 31.74 uS
//   Pulse for 4 uS (96 times 39.68 nS)
//    Sync pulse: 96 pixels
//    Back porch: 48 pixels
//    Active video: 640 pixels
//    Front porch: 16 pixels
//       Total: 800 pixels

// Pixel time =  ((1/60) / 525 * 1e9) / 800 = 39.68  nS
//  frequency =  1 / (((1/60) / 525 * 1e6) / 800) = 25.2 MHz

// However in practice, it is the SPI speed, namely a period of 125 nS
//     (that is 2 x system clock speed)
//   giving an 8 MHz pixel frequency. Thus the characters are about 3 times too wide.
// Thus we fit 160 of "our" pixels on the screen in what usually takes 3 x 160 = 480

const byte screenFontHeight = 8;
const byte screenFontWidth = 8;

const int verticalLines = verticalPixels / screenFontHeight / 2;  // double-height characters
const int horizontalPixels = horizontalBytes * screenFontWidth;

const byte verticalBackPorchLines = 35;  // includes sync pulse?
const int verticalFrontPorchLines = 525 - verticalBackPorchLines;

volatile int vLine;
volatile int messageLine;
volatile byte backPorchLinesToGo;

enum SEND_COMMANDS { CLRSCR = 1, CLREOL, GOTOXY, ESC = 27 };
enum STATES { NORMAL, GOT_ESCAPE, GOT_GOTOXY, GOT_X };

char message [verticalLines]  [horizontalBytes];
byte column, line;
STATES state = NORMAL;
byte x, y;  // for gotoxy


// ISR: Vsync pulse
ISR (TIMER1_OVF_vect)
  {
  vLine = 0; 
  messageLine = 0;
  backPorchLinesToGo = verticalBackPorchLines;
  } // end of TIMER1_OVF_vect
  
// ISR: Hsync pulse ... this interrupt merely wakes us up
ISR (TIMER2_OVF_vect)
  {
  } // end of TIMER2_OVF_vect
    
// called by interrupt service routine when incoming data arrives

/*
Expected formats are:
   * ordinary text:           gets displayed
   * carriage-return (0x0D):  returns cursor to start of current line
   * newline (0x0A):          drops down a line and also goes to the start of the line
   * clear screen (0x0C):     clear screen, return cursor to 1,1
   * ESC (0x1B) followed by:
      * 1 : clear screen, return cursor to 1,1
      * 2 : clear to end of current line
      * 3 : go to x,y ... next two bytes are X and then Y: one-relative

  All writing wraps, eg. text wraps at end of line, then end of screen back to line 1, column 1.
  A gotoxy out of range is ignored.
*/

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
  {
    byte c;
#if BETA_ARDUINO    
    c = Wire.receive ();
#else
    c = Wire.read ();
#endif 
    
    // first check state ... see if we are expecting a command or an x/y position
    switch (state)
      {
      // normal is, well, normal unless we get an ESC character
      case NORMAL:
          switch (c)
            {
            case ESC: 
              state = GOT_ESCAPE; 
              break;
 
            // otherwise just display the character
            default:
               message [line] [column] = c;
               if (++column >= horizontalBytes)
                 {
                 column = 0;
                 line++;
                 } // end wrapped line
                 
              if (line < verticalLines)
                  break;         
            // if wrapped past end of buffer, fall through to do a newline which will scroll up
                    
            // newline starts a new line, and drops down to do a carriage-return as well
            case '\n': 
              // end end? scroll
              if (++line >= verticalLines)
                {
                // move line 2 to line 1 and so on ...
                memmove (& message [0] [0], & message [1] [0], sizeof message - horizontalBytes);
                // clear last line
                memset (&message [verticalLines - 1] [0], ' ', horizontalBytes);    
                // put cursor on last line
                line = verticalLines - 1;    // back to last line          
                }
            // fall through ...
            
            // carriage-return returns to start of line
            case '\r': 
              column = 0; 
              break;
            
            // clear screen
            case '\f': 
              memset (message, ' ', sizeof message);
              line = column = 0;
              break;

             }  // end of switch on incoming character
          
          break;  // end of NORMAL
          
        // got ESC previously
        case GOT_ESCAPE:
          switch (c)
            {
            // clear screen ... just do it
            case CLRSCR:
              memset (message, ' ', sizeof message);
              line = column = 0;
              state = NORMAL;
              break;
              
            // clear to end of line
            case CLREOL:
              memset (&message [line] [column], ' ', horizontalBytes - column);
              state = NORMAL;
              break;

            // gotoxy expects two more bytes (x and y)
            case GOTOXY:
              state = GOT_GOTOXY;
              break;

            // unexpected ... not recognized command
            default:
              state = NORMAL;
              break;
            } // end of switch on command type
          break;  // end of GOT_ESCAPE
        
        // we got x, now we want y
        case GOT_GOTOXY:
          x = c - 1;  // make zero-relative
          state = GOT_X;
          break;
          
        // we now have x and y, we can move the cursor
        case GOT_X:
          y = c - 1;  // make zero-relative
          
          // if possible that is
          if (x < horizontalBytes && y < verticalLines)
            {
            column = x;
            line = y; 
            }
          state = NORMAL;
          break;
          
        // unexpected ... not recognized state
        default:
          state = NORMAL;
          break;
      } // end of switch on state
      
  }  // end of while available
}  // end of receiveEvent

void setup()
  {
  
  // initial message ... change to suit
  for (int i = 0; i < verticalLines; i++)
    sprintf (message [i], "Line %03i - hello!", i);
   
  // disable Timer 0
  TIMSK0 = 0;  // no interrupts on Timer 0
  OCR0A = 0;   // and turn it off
  OCR0B = 0;
  
  // Timer 1 - vertical sync pulses
  pinMode (vSyncPin, OUTPUT); 
  Timer1::setMode (15, Timer1::PRESCALE_1024, Timer1::CLEAR_B_ON_COMPARE);
  OCR1A = 259;  // 16666 / 64 uS = 260 (less one)
  OCR1B = 0;    // 64 / 64 uS = 1 (less one)
  TIFR1 = bit (TOV1);   // clear overflow flag
  TIMSK1 = bit (TOIE1);  // interrupt on overflow on timer 1

  // Timer 2 - horizontal sync pulses
  pinMode (hSyncPin, OUTPUT); 
  Timer2::setMode (7, Timer2::PRESCALE_8, Timer2::CLEAR_B_ON_COMPARE);
  OCR2A = 63;   // 32 / 0.5 uS = 64 (less one)
  OCR2B = 7;    // 4 / 0.5 uS = 8 (less one)
  TIFR2 = bit (TOV2);   // clear overflow flag
  TIMSK2 = bit (TOIE2);  // interrupt on overflow on timer 2
 
  // Set up USART in SPI mode (MSPIM)
  
  // baud rate must be zero before enabling the transmitter
  UBRR0 = 0;  // USART Baud Rate Register
  pinMode (MSPIM_SCK, OUTPUT);   // set XCK pin as output to enable master mode
  UCSR0B = 0; 
  UCSR0C = bit (UMSEL00) | bit (UMSEL01) | bit (UCPHA0) | bit (UCPOL0);  // Master SPI mode

  // prepare to sleep between horizontal sync pulses  
  set_sleep_mode (SLEEP_MODE_IDLE);  

  // for incoming data to display from I2C
  Wire.begin (i2cAddress);
  Wire.onReceive (receiveEvent);
  
}  // end of setup

// draw a single scan line
void doOneScanLine ()
  {
    
  // after vsync we do the back porch
  if (backPorchLinesToGo)
    {
    backPorchLinesToGo--;
    return;   
    }  // end still doing back porch
    
  // if all lines done, do the front porch
  if (vLine >= verticalPixels)
    return;
    
  // pre-load pointer for speed
  const register byte * linePtr = &screen_font [ (vLine >> 1) & 0x07 ] [0];
  register char * messagePtr =  & (message [messageLine] [0] );

  // how many pixels to send
  register byte i = horizontalBytes;

  // turn transmitter on 
  UCSR0B = bit (TXEN0);  // transmit enable (starts transmitting white)

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

  // wait till done    
  while (!(UCSR0A & bit(TXC0))) 
    {}
  
  // disable transmit
  UCSR0B = 0;   // drop back to black

  // finished this line 
  vLine++;

  // every 16 pixels it is time to move to a new line in our text
  //  (because we double up the characters vertically)
  if ((vLine & 0xF) == 0)
    messageLine++;
    
  }  // end of doOneScanLine

void loop() 
  {
  // sleep to ensure we start up in a predictable way
  sleep_mode ();
  doOneScanLine ();
 }  // end of loop


Wiring


This shows how to connect up the DB15 connector (socket) that you plug your VGA cable into:



The 470 ohm resistors are there to convert the 5V output from the Uno into 0.7V for the analog pixel data. This is because there is a 75 ohm resistor inside the monitor, so the 470 ohm resistors form voltage dividers:


 75 / (75 + 470) * 5 = 0.688 V


688 mV is close enough to the required 700 mV.

I'm not sure about the 68 ohms resistors for the two TTL lines (sync lines) but I have seen them suggested on other web sites.

Of course, if you prefer green or blue output, just omit the wire to the appropriate pins. You can have red/green/blue or some combination (cyan/magenta/yellow/white).

The three wires on the left are for connecting up to another processor to feed this one with the data to be displayed.

Download


The VGA Output sketch above, plus the font file, and the timers helper file, are all in the .zip file here:

http://gammon.com.au/Arduino/VGA_output.zip

Where to go from here? Colour?


I've had suggestions to "improve" the sketch by adding colour. Hopefully I've explained why you can't simply do that, without sacrificing something. The output loop is already very tight, with only one clock cycle to spare.

Let's say, for example, that you wanted to send colour. First you would need to have 3 pixel pins (reg/green/blue), so that rules out using SPI. You would have to manually load up (bit bang) a register with all 3 colours. This would be a lot slower. Also you would need to keep the colour information somewhere.

And to do graphics you would need to keep the "bitmap" somewhere. Even to store the bits for 160 x 480 x 4 bit colour would require 160 * 480 * 4 bits = 307200 bits which is 38400 bytes. Well there simply isn't that much memory in the processor. You have 2048 bytes.


Summary of timings



Vertical

60 Hz frame rate
1/60 = 0.016667 Seconds period (16667 uS)

525 vertical scan lines (480 visible)

Therefore:  1 / 60 / 525 = 31.746 uS per line (13.5 KHz)

Vertical sync pulse: 2 lines (   64 uS)
Back porch:         33 lines ( 1047 uS)
Visible area:      480 lines (15238 uS)
Front porch:        10 lines (  317 uS)
---------------------------------------
TOTAL                         16666 uS per frame


Horizontal

31.746 uS per line (13.5 KHz)

800 pixels per line (640 visible)

Therefore: 1 / 60 / 525 / 800 = 39.68 nS per pixel (25.2 MHz)

Horizontal sync pulse: 96 pixels ( 3.81 uS)
Back porch:            48 pixels ( 1.90 uS)
Visible area:         640 pixels (25.40 uS)
Front porch:           16 pixels ( 0.63 uS)
-------------------------------------------
TOTAL                             31.74 uS per line


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (19,505 posts)  [Biography] bio   Forum Administrator
Date Reply #1 on Sun 22 Apr 2012 01:09 AM (UTC)  quote  ]

Amended on Tue 26 Nov 2013 01:38 AM (UTC) by Nick Gammon

Message
Colour output


Just as an experiment, I modified the above sketch to try to output colour. Here is the result:



Wiring for colour


For colour output we need to send a different signal to the R, G and B pins, like this:



Colour output sketch



/*
 VGA colour video generation
 
 Author:   Nick Gammon
 Date:     22nd April 2012
 Version:  1.0
 
 Version 1.0: initial release

 Connections:
 
 D3 : Horizontal Sync (68 ohms in series) --> Pin 13 on DB15 socket
 D4 : Red pixel output (470 ohms in series) --> Pin 1 on DB15 socket
 D5 : Green pixel output (470 ohms in series) --> Pin 2 on DB15 socket
 D6 : Blue pixel output (470 ohms in series) --> Pin 3 on DB15 socket
 D10 : Vertical Sync (68 ohms in series) --> Pin 14 on DB15 socket
 
 Gnd : --> Pins 5, 6, 7, 8, 10 on DB15 socket


 Note: As written, this sketch has 34 bytes of free SRAM memory.
 
 PERMISSION TO DISTRIBUTE
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 and associated documentation files (the "Software"), to deal in the Software without restriction, 
 including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in 
 all copies or substantial portions of the Software.
 
 
 LIMITATION OF LIABILITY
 
 The software is provided "as is", without warranty of any kind, express or implied, 
 including but not limited to the warranties of merchantability, fitness for a particular 
 purpose and noninfringement. In no event shall the authors or copyright holders be liable 
 for any claim, damages or other liability, whether in an action of contract, 
 tort or otherwise, arising from, out of or in connection with the software 
 or the use or other dealings in the software. 

*/

#include <TimerHelpers.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>

const byte hSyncPin = 3;     // <------- HSYNC

const byte redPin = 4;       // <------- Red pixel data
const byte greenPin = 5;     // <------- Green pixel data
const byte bluePin = 6;      // <------- Blue pixel data

const byte vSyncPin = 10;    // <------- VSYNC

const int horizontalBytes = 60;  // 480 pixels wide
const int verticalPixels = 480;  // 480 pixels high

// Timer 1 - Vertical sync

// output    OC1B   pin 16  (D10) <------- VSYNC

//   Period: 16.64 mS (60 Hz)
//      1/60 * 1e6 = 16666.66 uS
//   Pulse for 64 uS  (2 x HSync width of 32 uS)
//    Sync pulse: 2 lines
//    Back porch: 33 lines
//    Active video: 480 lines
//    Front porch: 10 lines
//       Total: 525 lines

// Timer 2 - Horizontal sync

// output    OC2B   pin 5  (D3)   <------- HSYNC

//   Period: 32 uS (31.25 kHz)
//      (1/60) / 525 * 1e6 = 31.74 uS
//   Pulse for 4 uS (96 times 39.68 nS)
//    Sync pulse: 96 pixels
//    Back porch: 48 pixels
//    Active video: 640 pixels
//    Front porch: 16 pixels
//       Total: 800 pixels

// Pixel time =  ((1/60) / 525 * 1e9) / 800 = 39.68  nS
//  frequency =  1 / (((1/60) / 525 * 1e6) / 800) = 25.2 MHz

// However in practice, it we can only pump out pixels at 375 nS each because it
//  takes 6 clock cycles to read one in from RAM and send it out the port.

const int verticalLines = verticalPixels / 16;  
const int horizontalPixels = horizontalBytes * 8;

const byte verticalBackPorchLines = 35;  // includes sync pulse?
const int verticalFrontPorchLines = 525 - verticalBackPorchLines;

volatile int vLine;
volatile int messageLine;
volatile byte backPorchLinesToGo;

#define nop asm volatile ("nop\n\t")

// bitmap - gets sent to PORTD
// For D4/D5/D6 bits need to be shifted left 4 bits
//  ie. 00BGR0000

char message [verticalLines]  [horizontalBytes];

// ISR: Vsync pulse
ISR (TIMER1_OVF_vect)
  {
  vLine = 0; 
  messageLine = 0;
  backPorchLinesToGo = verticalBackPorchLines;
  } // end of TIMER1_OVF_vect
  
// ISR: Hsync pulse ... this interrupt merely wakes us up
ISR (TIMER2_OVF_vect)
  {
  } // end of TIMER2_OVF_vect


void setup()
  {
  
  // initial bitmap ... change to suit
  for (int y = 0; y < verticalLines; y++)
    for (int x = 0; x < horizontalBytes; x++)
      message [y] [x] = (x + y) << 4;
   
  // disable Timer 0
  TIMSK0 = 0;  // no interrupts on Timer 0
  OCR0A = 0;   // and turn it off
  OCR0B = 0;
  
  // Timer 1 - vertical sync pulses
  pinMode (vSyncPin, OUTPUT); 
  Timer1::setMode (15, Timer1::PRESCALE_1024, Timer1::CLEAR_B_ON_COMPARE);
  OCR1A = 259;  // 16666 / 64 uS = 260 (less one)
  OCR1B = 0;    // 64 / 64 uS = 1 (less one)
  TIFR1 = bit (TOV1);   // clear overflow flag
  TIMSK1 = bit (TOIE1);  // interrupt on overflow on timer 1

  // Timer 2 - horizontal sync pulses
  pinMode (hSyncPin, OUTPUT); 
  Timer2::setMode (7, Timer2::PRESCALE_8, Timer2::CLEAR_B_ON_COMPARE);
  OCR2A = 63;   // 32 / 0.5 uS = 64 (less one)
  OCR2B = 7;    // 4 / 0.5 uS = 8 (less one)
  TIFR2 = bit (TOV2);   // clear overflow flag
  TIMSK2 = bit (TOIE2);  // interrupt on overflow on timer 2
 
  // prepare to sleep between horizontal sync pulses  
  set_sleep_mode (SLEEP_MODE_IDLE);  
  
  // pins for outputting the colour information
  pinMode (redPin, OUTPUT);
  pinMode (greenPin, OUTPUT);
  pinMode (bluePin, OUTPUT);
  
}  // end of setup

// draw a single scan line
void doOneScanLine ()
  {
    
  // after vsync we do the back porch
  if (backPorchLinesToGo)
    {
    backPorchLinesToGo--;
    return;   
    }  // end still doing back porch
    
  // if all lines done, do the front porch
  if (vLine >= verticalPixels)
    return;
    
  // pre-load pointer for speed
  register char * messagePtr =  & (message [messageLine] [0] );

  delayMicroseconds (1);
  
  // how many pixels to send
  register byte i = horizontalBytes;

  // blit pixel data to screen    
  while (i--)
    PORTD = * messagePtr++;

  // stretch final pixel
  nop; nop; nop;

  PORTD = 0;  // back to black
  // finished this line 
  vLine++;

  // every 16 pixels it is time to move to a new line in our text
  if ((vLine & 0xF) == 0)
    messageLine++;
    
  }  // end of doOneScanLine

void loop() 
  {
  // sleep to ensure we start up in a predictable way
  sleep_mode ();
  doOneScanLine ();
 }  // end of loop


Main output loop


The code to output the pixels is a simple loop:


while (i--)
    PORTD = * messagePtr++;


It relies on the pixel data pins all being on the same hardware port (PORTD in this case).

The generated assembler code is:


  while (i--)
    PORTD = * messagePtr++;
(2) 194:	89 91       	ld	r24, Y+
(1) 196:	8b b9       	out	0x0b, r24	; 11
(1) 198:	91 50       	subi	r25, 0x01	; 1
(2) 19a:	e0 f7       	brcc	.-8      	; 0x194 

-------
6 cycles in loop = 375 nS


Thus it takes 6 cycles (375 nS) per pixel, and the 60 pixels will take 22.5 uS which is inside the 25.4 uS allowed time for the visible frame.

Notes


It turned out we had time to send 60 pixels horizontally. The width of each pixel is entirely due to the time it takes to read the next one from memory and clock it out (6 clock cycles).

Vertically we draw 30 pixels (each one is drawn for 16 scan lines, taking a total of 480 pixels from the monitor's point of view). The bitmap alone takes 30 * 60 = 1800 bytes. That only leaves 248 bytes for the rest of the program, which (once you allow for the fact that around the first 100 are used by the processor for control registers) doesn't give much over.

The pixels are copied from the "message" array which is initialized into a nice checkerboard pattern in setup. You could change that (if you can find some free cycles) to animate graphics.

Be warned that there are about 34 bytes of SRAM free ... not a lot. If you need more memory you could compromise by making the pixels bigger, so not as much memory is needed to hold them.

Final pixel

After taking the above photo, I realized the final pixel was a bit narrow. This is because we change to black a bit too fast. The amended code above now has three "nop" (no operation) instructions added to stretch that final pixel a bit.

Logic analyzer output


This shows the three different colours changing in one scan line:



More colours


You could have more colours for the same amount of memory, and the same speed, by using 6 pins (2 for each colour) with a different value resistor for the second 3 pins. I haven't done the maths, but something like 1K resistors in series for the second set of pins would mean if they were active you would have half the voltage (eg. 0.35V) of video rather than 0.7V). Thus you could have "dull" and "bright" for each of R, G, B and of course all sorts of combinations inbetween.

The pins would need to be chosen from PORTD still, for example D0, D1, D2. Since we are already copying a single byte to PORTD we could simply have more "active" bits in that byte. That would give 64 colours (2^6) rather than 8 colours (2^3).

Example of 64 colours


This is an example of using more colours:



That was produced by this sketch:


/*
 VGA colour video generation
 
 Author:   Nick Gammon
 Date:     22nd April 2012
 Version:  1.0
 
 Version 1.0: initial release
 Version 1.1: Amended to output 64 colours

 Connections:
 
 D0 : Dull Red pixel output (1K resistor in series) --> Pin 1 on DB15 socket
 D1 : Dull Green pixel output (1K resistor in series) --> Pin 2 on DB15 socket
 D2 : Dull Blue pixel output (1K resistor in series) --> Pin 3 on DB15 socket
 D3 : Horizontal Sync (68 ohms in series) --> Pin 13 on DB15 socket
 D4 : Red pixel output (470 ohms in series) --> Pin 1 on DB15 socket (also)
 D5 : Green pixel output (470 ohms in series) --> Pin 2 on DB15 socket (also)
 D6 : Blue pixel output (470 ohms in series) --> Pin 3 on DB15 socket (also)
 D10 : Vertical Sync (68 ohms in series) --> Pin 14 on DB15 socket
 
 Gnd : --> Pins 5, 6, 7, 8, 10 on DB15 socket


 Note: As written, this sketch has 34 bytes of free SRAM memory.
 
 PERMISSION TO DISTRIBUTE
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 and associated documentation files (the "Software"), to deal in the Software without restriction, 
 including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in 
 all copies or substantial portions of the Software.
 
 
 LIMITATION OF LIABILITY
 
 The software is provided "as is", without warranty of any kind, express or implied, 
 including but not limited to the warranties of merchantability, fitness for a particular 
 purpose and noninfringement. In no event shall the authors or copyright holders be liable 
 for any claim, damages or other liability, whether in an action of contract, 
 tort or otherwise, arising from, out of or in connection with the software 
 or the use or other dealings in the software. 

*/

#include <TimerHelpers.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>

const byte hSyncPin = 3;     // <------- HSYNC

const byte redPin = 4;       // <------- Red pixel data
const byte greenPin = 5;     // <------- Green pixel data
const byte bluePin = 6;      // <------- Blue pixel data

const byte dullRedPin = 0;     // <------- Red pixel data 50% brightness
const byte dullGreenPin = 1;   // <------- Green pixel data 50% brightness
const byte dullBluePin = 2;    // <------- Blue pixel data 50% brightness

const byte vSyncPin = 10;    // <------- VSYNC

const int horizontalBytes = 60;  // 480 pixels wide
const int verticalPixels = 480;  // 480 pixels high

// Timer 1 - Vertical sync

// output    OC1B   pin 16  (D10) <------- VSYNC

//   Period: 16.64 mS (60 Hz)
//      1/60 * 1e6 = 16666.66 uS
//   Pulse for 64 uS  (2 x HSync width of 32 uS)
//    Sync pulse: 2 lines
//    Back porch: 33 lines
//    Active video: 480 lines
//    Front porch: 10 lines
//       Total: 525 lines

// Timer 2 - Horizontal sync

// output    OC2B   pin 5  (D3)   <------- HSYNC

//   Period: 32 uS (31.25 kHz)
//      (1/60) / 525 * 1e6 = 31.74 uS
//   Pulse for 4 uS (96 times 39.68 nS)
//    Sync pulse: 96 pixels
//    Back porch: 48 pixels
//    Active video: 640 pixels
//    Front porch: 16 pixels
//       Total: 800 pixels

// Pixel time =  ((1/60) / 525 * 1e9) / 800 = 39.68  nS
//  frequency =  1 / (((1/60) / 525 * 1e6) / 800) = 25.2 MHz

// However in practice, it we can only pump out pixels at 375 nS each because it
//  takes 6 clock cycles to read one in from RAM and send it out the port.

const int verticalLines = verticalPixels / 16;  
const int horizontalPixels = horizontalBytes * 8;

const byte verticalBackPorchLines = 35;  // includes sync pulse?
const int verticalFrontPorchLines = 525 - verticalBackPorchLines;

volatile int vLine;
volatile int messageLine;
volatile byte backPorchLinesToGo;

#define nop asm volatile ("nop\n\t")

// bitmap - gets sent to PORTD
// For D4/D5/D6 bits need to be shifted left 4 bits
//  ie. 00BGR0000

char message [verticalLines]  [horizontalBytes];

// ISR: Vsync pulse
ISR (TIMER1_OVF_vect)
  {
  vLine = 0; 
  messageLine = 0;
  backPorchLinesToGo = verticalBackPorchLines;
  } // end of TIMER1_OVF_vect
  
// ISR: Hsync pulse ... this interrupt merely wakes us up
ISR (TIMER2_OVF_vect)
  {
  } // end of TIMER2_OVF_vect


void setup()
  {
  byte count = 0;
  // initial bitmap ... change to suit
  for (int y = 0; y < verticalLines; y++)
    for (int x = 0; x < horizontalBytes; x++)
        {
        message [y] [x] = ((count << 1) & 0x70) | (count & 0x07);
        if (++count >= 64)
          count = 0;
        }
 
   
  // disable Timer 0
  TIMSK0 = 0;  // no interrupts on Timer 0
  OCR0A = 0;   // and turn it off
  OCR0B = 0;
  
  // Timer 1 - vertical sync pulses
  pinMode (vSyncPin, OUTPUT); 
  Timer1::setMode (15, Timer1::PRESCALE_1024, Timer1::CLEAR_B_ON_COMPARE);
  OCR1A = 259;  // 16666 / 64 uS = 260 (less one)
  OCR1B = 0;    // 64 / 64 uS = 1 (less one)
  TIFR1 = bit (TOV1);   // clear overflow flag
  TIMSK1 = bit (TOIE1);  // interrupt on overflow on timer 1

  // Timer 2 - horizontal sync pulses
  pinMode (hSyncPin, OUTPUT); 
  Timer2::setMode (7, Timer2::PRESCALE_8, Timer2::CLEAR_B_ON_COMPARE);
  OCR2A = 63;   // 32 / 0.5 uS = 64 (less one)
  OCR2B = 7;    // 4 / 0.5 uS = 8 (less one)
  TIFR2 = bit (TOV2);   // clear overflow flag
  TIMSK2 = bit (TOIE2);  // interrupt on overflow on timer 2
 
  // prepare to sleep between horizontal sync pulses  
  set_sleep_mode (SLEEP_MODE_IDLE);  
  
  // pins for outputting the colour information
  pinMode (redPin, OUTPUT);
  pinMode (greenPin, OUTPUT);
  pinMode (bluePin, OUTPUT);

  pinMode (dullRedPin, OUTPUT);
  pinMode (dullGreenPin, OUTPUT);
  pinMode (dullBluePin, OUTPUT);
  
}  // end of setup

// draw a single scan line
void doOneScanLine ()
  {
    
  // after vsync we do the back porch
  if (backPorchLinesToGo)
    {
    backPorchLinesToGo--;
    return;   
    }  // end still doing back porch
    
  // if all lines done, do the front porch
  if (vLine >= verticalPixels)
    return;
    
  // pre-load pointer for speed
  register char * messagePtr =  & (message [messageLine] [0] );

  delayMicroseconds (1);
  
  // how many pixels to send
  register byte i = horizontalBytes;

  // blit pixel data to screen    
  while (i--)
    PORTD = * messagePtr++;

  // stretch final pixel
  nop; nop; nop;
  
  PORTD = 0;  // back to black
  // finished this line 
  vLine++;

  // every 16 pixels it is time to move to a new line in our text
  if ((vLine & 0xF) == 0)
    messageLine++;
    
  }  // end of doOneScanLine

void loop() 
  {
  // loop to avoid overhead of function all
  while (true)
    {
    // sleep to ensure we start up in a predictable way
    sleep_mode ();
    doOneScanLine ();
    }  // end of while
 }  // end of loop


I connected the red/green/blue pins on the DB15 to Arduino pins D0/D1/D2 via 1K resistors.

Example of moving graphics





Code to produce:


/*
 VGA colour video generation - Sine wave generation
 
 Author:   Nick Gammon
 Date:     22nd April 2012
 Version:  1.0
 
 Version 1.0: initial release

 Connections:
 
 D3 : Horizontal Sync (68 ohms in series) --> Pin 13 on DB15 socket
 D4 : Red pixel output (470 ohms in series) --> Pin 1 on DB15 socket
 D5 : Green pixel output (470 ohms in series) --> Pin 2 on DB15 socket
 D6 : Blue pixel output (470 ohms in series) --> Pin 3 on DB15 socket
 D10 : Vertical Sync (68 ohms in series) --> Pin 14 on DB15 socket
 
 Gnd : --> Pins 5, 6, 7, 8, 10 on DB15 socket


 Note: As written, this sketch has 34 bytes of free SRAM memory.
 
 PERMISSION TO DISTRIBUTE
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 and associated documentation files (the "Software"), to deal in the Software without restriction, 
 including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in 
 all copies or substantial portions of the Software.
 
 
 LIMITATION OF LIABILITY
 
 The software is provided "as is", without warranty of any kind, express or implied, 
 including but not limited to the warranties of merchantability, fitness for a particular 
 purpose and noninfringement. In no event shall the authors or copyright holders be liable 
 for any claim, damages or other liability, whether in an action of contract, 
 tort or otherwise, arising from, out of or in connection with the software 
 or the use or other dealings in the software. 

*/

#include <TimerHelpers.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>

const byte hSyncPin = 3;     // <------- HSYNC

const byte redPin = 4;       // <------- Red pixel data
const byte greenPin = 5;     // <------- Green pixel data
const byte bluePin = 6;      // <------- Blue pixel data

const byte vSyncPin = 10;    // <------- VSYNC

const int horizontalBytes = 50;  // 480 pixels wide
const int verticalPixels = 480;  // 480 pixels high

// Timer 1 - Vertical sync

// output    OC1B   pin 16  (D10) <------- VSYNC

//   Period: 16.64 mS (60 Hz)
//      1/60 * 1e6 = 16666.66 uS
//   Pulse for 64 uS  (2 x HSync width of 32 uS)
//    Sync pulse: 2 lines
//    Back porch: 33 lines
//    Active video: 480 lines
//    Front porch: 10 lines
//       Total: 525 lines

// Timer 2 - Horizontal sync

// output    OC2B   pin 5  (D3)   <------- HSYNC

//   Period: 32 uS (31.25 kHz)
//      (1/60) / 525 * 1e6 = 31.74 uS
//   Pulse for 4 uS (96 times 39.68 nS)
//    Sync pulse: 96 pixels
//    Back porch: 48 pixels
//    Active video: 640 pixels
//    Front porch: 16 pixels
//       Total: 800 pixels

// Pixel time =  ((1/60) / 525 * 1e9) / 800 = 39.68  nS
//  frequency =  1 / (((1/60) / 525 * 1e6) / 800) = 25.2 MHz

// However in practice, it we can only pump out pixels at 375 nS each because it
//  takes 6 clock cycles to read one in from RAM and send it out the port.

const int verticalLines = verticalPixels / 16;  
const int horizontalPixels = horizontalBytes * 8;

const byte verticalBackPorchLines = 35;  // includes sync pulse?
const int verticalFrontPorchLines = 525 - verticalBackPorchLines;

volatile int vLine;
volatile int messageLine;
volatile int backPorchLinesToGo;
volatile byte newFrame;

#define nop asm volatile ("nop\n\t")

// bitmap - gets sent to PORTD
// For D4/D5/D6 bits need to be shifted left 4 bits
//  ie. 00BGR0000

char message [verticalLines]  [horizontalBytes];

// ISR: Vsync pulse
ISR (TIMER1_OVF_vect)
  {
  vLine = 0; 
  messageLine = 0;
  backPorchLinesToGo = verticalBackPorchLines;
  newFrame = true;
  } // end of TIMER1_OVF_vect
  
// ISR: Hsync pulse ... this interrupt merely wakes us up
ISR (TIMER2_OVF_vect)
  {
  backPorchLinesToGo--;    
  } // end of TIMER2_OVF_vect


void setup()
  {
  
  // initial bitmap ... change to suit
  for (int y = 0; y < verticalLines; y++)
    for (int x = 0; x < horizontalBytes; x++)
      message [y] [x] = (7) << 4;
   
  // disable Timer 0
  TIMSK0 = 0;  // no interrupts on Timer 0
  OCR0A = 0;   // and turn it off
  OCR0B = 0;
  
  // Timer 1 - vertical sync pulses
  pinMode (vSyncPin, OUTPUT); 
  Timer1::setMode (15, Timer1::PRESCALE_1024, Timer1::CLEAR_B_ON_COMPARE);
  OCR1A = 259;  // 16666 / 64 uS = 260 (less one)
  OCR1B = 0;    // 64 / 64 uS = 1 (less one)
  TIFR1 = bit (TOV1);   // clear overflow flag
  TIMSK1 = bit (TOIE1);  // interrupt on overflow on timer 1

  // Timer 2 - horizontal sync pulses
  pinMode (hSyncPin, OUTPUT); 
  Timer2::setMode (7, Timer2::PRESCALE_8, Timer2::CLEAR_B_ON_COMPARE);
  OCR2A = 63;   // 32 / 0.5 uS = 64 (less one)
  OCR2B = 7;    // 4 / 0.5 uS = 8 (less one)
  TIFR2 = bit (TOV2);   // clear overflow flag
  TIMSK2 = bit (TOIE2);  // interrupt on overflow on timer 2
 
  // prepare to sleep between horizontal sync pulses  
  set_sleep_mode (SLEEP_MODE_IDLE);  
  
  // pins for outputting the colour information
  pinMode (redPin, OUTPUT);
  pinMode (greenPin, OUTPUT);
  pinMode (bluePin, OUTPUT);
  
}  // end of setup

// draw a single scan line
boolean doOneScanLine ()
  {
    
  // after vsync we do the back porch
  if (backPorchLinesToGo > 0)
    {
    backPorchLinesToGo--;
    return false;   
    }  // end still doing back porch
    
  // if all lines done, do the front porch
  if (vLine == verticalPixels)
    return newFrame;
    
  // pre-load pointer for speed
  register char * messagePtr =  & (message [messageLine] [0] );

  delayMicroseconds (1);
  
  // how many pixels to send
  register byte i = horizontalBytes;

  // blit pixel data to screen    
  while (i--)
    PORTD = * messagePtr++;

  // stretch final pixel
  nop; nop; nop;
  
  PORTD = 0;  // back to black
  // finished this line 
  vLine++;

  // every 16 pixels it is time to move to a new line in our text
  if ((vLine & 0xF) == 0)
    messageLine++;
    
  return false;
  }  // end of doOneScanLine

float radians = 0;
const float pi = 3.1415926;
const float radiansIncrement = (pi / 2.0) / (horizontalBytes / 2);
byte x;
boolean Up = true;
byte colour = 0;
boolean Calc = true;

void advanceLine ()
  {
  if (Calc)
    {
    x = sin (radians) * horizontalBytes;
    if (Up)
      {
      radians += radiansIncrement;
      if (radians >= pi / 2)
        Up = false;
      }
    else
      {
      radians -= radiansIncrement;
      if (radians <= 0)
        {
        Up = true;
        radians = 0;
        colour++;
        }
      }
    Calc = false;
    }
  else
    {
    memmove (& message [0] [0], & message [1] [0], sizeof message - horizontalBytes);  
    memset (&message [verticalLines - 1] [0], (colour + 1) << 4, horizontalBytes);
    memset (&message [verticalLines - 1] [0], colour << 4, x);
    Calc = true;
    }

  newFrame = false;
  }
  
void loop() 
  {
  // loop to avoid overhead of function call
  while (true)
    {
    // sleep to ensure we start up in a predictable way
    sleep_mode ();
    if (doOneScanLine ())
      advanceLine ();
    }  // end of while
 }  // end of loop


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


37,079 views.

Postings by administrators only.

[Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

[Home]

Written by Nick Gammon - 5K

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( http://www.gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Web site powered by FutureQuest.Net]