Tuesday, January 5, 2010

NTSC Timing

NTSC output from Arduino generally uses old-school timing, controlled by the number of clock cycles you use, with delay loops to stretch it out when needed.

At TV horizontal retrace speeds, there's not a lot of free time left over.  We don't always have to be perfect, since TVs can adjust their timing a certain amount to match your timing.  While you can get a stable synchronized picture using a range of timing values, the more accurate you are, straighter the lines, and the more consistent the shades.

At the pixel and line level, you need to be very precise.  The difference of a single clock cycle can change the width of a pixel.  If you change the timing of a horizontal retrace mid-screen even by a couple clocks, the TV will adjust its timing to match, but it may do so "slowly" over a few lines.  The result is a vertical skew and/or curve that occurs after the timing change.

We can take advantage of the way TVs adjust their timing to re-sync, by rendering a frame that uses stable lines at the top and bottom of the screen, with experimental lines in between.  Even if the timing of the test lines is bad, the TV can often resync itself, and you can get a stable image of your test lines to see what needs to be adjusted.

Below is code that does this.  It can be used for experimenting with different line-rendering code.  It also lets you interactively modify various timing variables through the Arduino Monitor.

While you can add delays and nops to get the timing of a line right, you also have to make sure you're not changing the timing of the few clock cycles that occur before the HSync.  Even changing the way a for-loop is initialized, between lines, can cause visual artifacts.  The key is to have completely consistent code or timing at all parts of the rendering of a line.  There are many pitfalls.  For example, the ATmega chips don't have a Div instruction, so using innocent-looking / or % in code ends up calling a function that includes a loop, and the timing of it will be different for different values.



Lines with different timing
Matched timing

/*
NTSC_timing

Based on Matthieu Lalonde's NTSC framebuffer project,
this project adds interactive adjustable timing.

It draws grey "color bars" at the top and bottom of the screen.
In between are several lines that can be used to test and time
custom line-rendering code.  The stable bars at the top and bottom
of the screen should let a TV re-sync, allowing you to see a stable
image of the custom lines, even when their timing is out of sync.

version: 0.9
- Current timing values "work on my tv" and may not be optimum.
- Some cycles could be saved by using bytes instead of words.

(c) 2010, Michael Devine.
Use as you wish.


-----------------
Serial commands:
12345678: Test the voltage of the 8 levels for a few seconds

asdfghj: increase, and
zxcvbnm: decrease various timing values.

*/

#include "WProgram.h"

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif

// This is a stripped-down inline modification of the "busy wait" in delayMicroSeconds
// to allow 4x greater precision
unsigned int qt;
#define DELAY_4CLOCKS(/*unsigned int*/ quadticks) { qt = quadticks; __asm__ __volatile__ ( "1: sbiw %0,1" "\n\t" /*2 cycles*/ "brne 1b" : "=w" (qt) : "0" (qt) /*2 cycles*/); }

#define NOP __asm__ ( "nop\n\t" )


// ----------------------------------------------------------------
// Consts and globals...

#define NUM_LINES              262 /* Includes the last 20 lines for the vertical sync! */
#define NUM_VSYNC_LINES        20  /* These 20 lines... */


// PORTB is used so these pins must be between 8 and 15.
// If changed, change the color defines.
#define PIN_SYNC                8
#define PIN_VIDEO               9
#define PIN_VIDEO_FINE          10

// Definition of signal levels for the AD 3-bit converter
// The third bit gives us greylevel detail.
// The odd ordering of bit significance is done so that the
// program is compatible with typical 2-bit hardware.
#define SYNC                   0x00    /* 0.00v */
#define SYNC_UNDEFINED         0x04
#define BLACK                  0x01    /* 0.33v */
#define LIGHTBLACK             0x05
#define DARKGREY               0x02    /* 0.67v */
#define GREY                   0x06
#define LIGHTGREY              0x03    /* 1.00v */
#define WHITE                  0x07

#define NUM_COLORS  8

// Sorted list of levels...
const static byte aColors[NUM_COLORS] =
{
  SYNC,
  SYNC_UNDEFINED,
  BLACK,
  LIGHTBLACK,
  DARKGREY,
  GREY,
  LIGHTGREY,
  WHITE
};


#define DELAY_VOLTAGE_TEST_MSEC  5000

// These timing values are tweaked for decent usage of screen real estate
// They should be in quarter microseconds.
unsigned int  nDelayHSyncStart    = 5;
unsigned int  nDelayBackPorch     = 35;
unsigned int  nDelayFrontPorch =   14;
unsigned int  nDelayVSync      =   104;
//
unsigned int  nDelayBarWidth    =   30;
unsigned int  nDelayTestLines    =   169;

#define _serialBauteRate        19200

char c; // Serial buffer

unsigned int nLineCount = 0;



// ----------------------------------------------------------------
// Code...

void setup(void)
{
  // Disable timer 0 overflow interrupt, so it doesn't screw up precise timing.
  cbi(TIMSK0, TOIE0);

  pinMode( PIN_SYNC, OUTPUT );
  pinMode( PIN_VIDEO, OUTPUT );
#ifdef PIN_VIDEO_FINE
  pinMode( PIN_VIDEO_FINE, OUTPUT );
#endif

  Serial.begin(_serialBauteRate);
  Serial.print("Command? ");
}

void loop(void) {
  // Draw the screen
  renderScreen();

  // Check for commands
  if (Serial.available())
  {
    c = Serial.read();
    Serial.println(c);

    switch (c)
    {     
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      {
        /*
         * Using this mode you can go through each voltage levels (Sync, black, gray, white, etc).
         * This is useful for testing the circuit as you can make sure the voltages are right by using a multimeter
         **/
       
        // Set the voltage level
        PORTB = aColors[c - '1'];
       
        int nDelayLoops;
        unsigned int nQuadTicks = 1000*4;  // Delay about a millisecond on 16MHz CPU, each time through the loop
        for (nDelayLoops = 0;  nDelayLoops < DELAY_VOLTAGE_TEST_MSEC;  ++nDelayLoops)
        {
          DELAY_4CLOCKS( nQuadTicks );
        }
      }
      break;

       
    // Timing adjustment commands...         
    case 'a':
      nDelayHSyncStart += 2;  // no break
    case 'z':
      nDelayHSyncStart --;
      Serial.print("nDelayHSyncStart: ");
      Serial.println( nDelayHSyncStart );
      break;

    case 's':
      nDelayBackPorch += 2;  // no break
    case 'x':
      nDelayBackPorch --;
      Serial.print("nDelayBackPorch: ");
      Serial.println( nDelayBackPorch );
      break;
     
    case 'd':
      nDelayFrontPorch += 2;  // no break
    case 'c':
      nDelayFrontPorch --;
      Serial.print("nDelayFrontPorch: ");
      Serial.println( nDelayFrontPorch );
      break;
     
    case 'f':
      nDelayVSync += 2;  // no break
    case 'v':
      nDelayVSync --;
      Serial.print("nDelayVSync: ");
      Serial.println( nDelayVSync );
      break;
     
    case 'g':
      nDelayBarWidth += 2;  // no break
    case 'b':
      nDelayBarWidth --;
      Serial.print("nDelayBarWidth: ");
      Serial.println( nDelayBarWidth );
      break;
     
    case 'h':
      nDelayTestLines += 2;  // no break
    case 'n':
      nDelayTestLines --;
      Serial.print("nDelayTestLines: ");
      Serial.println( nDelayTestLines );
      break;

     
    // Display usage
    default :
      Serial.println( "12345678: Test voltage levels.  asdfgh increase, zxcvbn decrease timing values" );
      break;
    }
  }
}

/**
 * NTSC Signal definition:
 * -- Horizontal sync (hsync) pulse: Start each scanline with 0.3V,
 * then 0V for 4.7us (microseconds), and then back to 0.3V. This tells the TV to start drawing a new scanline
 *
 * -- The "Back Porch": A transition region of 0.3V for 5.9us between the
 * hsync pulse and the visible region, off the left edge of the TV
 *
 * -- Visible scan region: This is the part you actually see.
 * 0.3V shows up as black, 1V as white, everything in between is greyscale. The visible region lasts for 51.5us
 *
 * -- The "Front Porch": A transition region of 0.3V for 1.4us before
 * the hsync pulse of the next line, off the right edge of the TV
 *
 * -- Vertical sync (vsync) pulse: Lines 243-262 of each frame (off the bottom of the TV)
 * start with 0.3V for 4.7us, and the rest is 0V. This tells the TV to prepare for a new frame.
 * Think of it as just 0V with an inverted sync pulse
 *
 *** http://www.eyetap.org/ece385/lab5.htm ***
 **/


// Renders the screen line by line (including vsync)
void renderScreen(void)
{
  unsigned int i;

  nLineCount = 0;

  // Draw some bars
  for (i=0;i
  {
    renderBarsLine( );
  }
  // Draw some test lines
  for ( ;i
  {
    renderTestLine( );
  }
  // Remainder of screen is bars and then VSync
  for (  ;i
  {
    renderBarsLine( );
  }

  // Vertical Sync follows
  generateVSync();    // NUM_VSYNC_LINES lines for vertical sync


// Renders a line consisting of wide bars of colors
void renderBarsLine( )
{
  // Begin Line Sync

  // H Sync
  PORTB = SYNC;
  DELAY_4CLOCKS( nDelayHSyncStart );

  // Back Porch
  PORTB = BLACK;
  DELAY_4CLOCKS( nDelayBackPorch );

  // Draw some bars
  int i;
  for (i=2; i
  {
    PORTB = aColors[i];
    DELAY_4CLOCKS( nDelayBarWidth );
  }

  /* End Line Sync (Front Porch) */
  PORTB = BLACK;
  DELAY_4CLOCKS( nDelayFrontPorch );
}


void renderTestLine( )
{
  // Begin Line Sync

  // H Sync
  PORTB = SYNC;
  DELAY_4CLOCKS( nDelayHSyncStart );

  // Back Porch
  PORTB = BLACK;
  DELAY_4CLOCKS( nDelayBackPorch );


  // Draw sets of 4 pixel pairs, with varying pixel widths...
  byte nColor = aColors[(nLineCount & 3) + 3];  // Pick various shades of grey
  nLineCount++;
  byte nBlack = BLACK;

 
  // No delay...
  PORTB = nColor;
  PORTB = nBlack;
  PORTB = nColor;
  PORTB = nBlack;
  PORTB = nColor;
  PORTB = nBlack;
  PORTB = nColor;
  PORTB = nBlack;
 
  // Delays...
  PORTB = nColor;  NOP;
  PORTB = nBlack;  NOP;
  PORTB = nColor;  NOP;
  PORTB = nBlack;  NOP;
  PORTB = nColor;  NOP;
  PORTB = nBlack;  NOP;
  PORTB = nColor;  NOP;
  PORTB = nBlack;  NOP;

  PORTB = nColor;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;
  PORTB = nColor;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;
  PORTB = nColor;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;
  PORTB = nColor;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;

  PORTB = nColor;  NOP;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;  NOP;
  PORTB = nColor;  NOP;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;  NOP;
  PORTB = nColor;  NOP;  NOP;  NOP;
  PORTB = nBlack;  NOP;  NOP;  NOP;
  PORTB = nColor;  NOP;  NOP;  NOP;
  PORTB = nBlack;  // NOP;  NOP;  NOP;

 
  DELAY_4CLOCKS( nDelayTestLines );

  // Add a slight delay to perfectly match the timing of the color bars...
  NOP;
 
  /* End Line Sync (Front Porch) */
  PORTB = BLACK;
  DELAY_4CLOCKS( nDelayFrontPorch );
}

// Generate sync pulse for the virtual sync (lasts NUM_VSYNC_LINES lines)
void generateVSync(void)
{
  unsigned int ii;
  for (ii = 0; ii < NUM_VSYNC_LINES; ii++)
  {
    // Begin V Sync
    PORTB = BLACK;
    DELAY_4CLOCKS( nDelayHSyncStart );

    PORTB = SYNC; 
    DELAY_4CLOCKS( nDelayVSync );
  }
}

Sunday, January 3, 2010

Initial NTSC test code

Hooking up an Arduino board to a TV requires very little hardware (a few resistors and an RCA plug), and there is a ton of stuff you can do with it.  Elsewhere on the web you can find implementations of Pong (for PAL at least... not sure about NTSC), and other large-pixel "frame buffer"-based NTSC display implementations.

I started with the hardware and software found here, and have been modifying it.

To get this program to work with arduino-0017 and a Duemilanove with ATmega328, I made the following timing changes:

#define _ntscDelayHSyncStart 3
#define _ntscDelayBackPorch 1
#define _ntscDelayFrontPorch 1
#define _ntscDelayPerLine (2*21)
#define _ntscDelayVSync 25

As well, rather than editing wiring.c or turning off all interrupts (sending serial commands requires one), I turned off only the one, in setup( ):

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif

// Disable timer 0 overflow interrupt, for precise timing.
cbi(TIMSK0, TOIE0);

Friday, January 1, 2010

Project: Robot

This is the main project posting for the Robot project.  It will be updated as the project evolves.

See all Robot posts for information and latest source code.


Well okay there is no code yet, and there is no robot, and there is no design.  But I am waiting for a shipment of servos from Hong Kong!  I'm planning to build a 3 or 4-legged walker using 2 servos per leg, and then add some autonomous behaviors to it.

Hardware:
  • Arduino board
  • 6 Servos ($18; eBay)

Project: NTSC Sprite-based Video Game System

This is the main project posting for the NTSC Video Game project.  It will be updated as the project evolves.

See all NTSC posts for information and latest source code.

Overview: There are several projects out there that are variations of NTSC output code that use 2 or more output pins to generate NTSC voltages, and precise code timing to create a properly synchronized image.  Many render the screen as an array of large pixels, using the microcontroller's memory as a framebuffer.  The limited memory means low resolution, and large blocky pixels.  An alternative is to render smaller, repeating blocks at higher resolution: tiled sprites.

I expect I'll be able to render a background filled with perhaps 16x16 or smaller blocks.  There will probably be seams between the blocks.  After that, the ultimate goal is to have 1, 2, or 3 moving animated sprites over top (compositing of sprites is highly doubtful.  Foreground sprites would instead prevent the display of nearby background).


Hardware:
  • Arduino board
  • a few of resistors
  • RCA cable or connector
Cost: < $5 using spare parts

Arduino

The projects on this blog use an Arduino single-board microcontroller platform and development environment.

I'm using a Duemilanove board with an ATmega328 chip.

Cost: $30 (eBay)



You can connect this board into a computer with a USB cable, download the dev software, and begin working on projects immediately. You don't have to worry about soldering components, building a chip programmer/UART hardware, power supply, etc. You can program in C and assembly in the integrated development environment, and can write code and run it on the board from one place. Also, it's cheap and it's open source!