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 );
  }
}

No comments:

Post a Comment