Click for the full flickr experience!

Arduino TV

After seeing someone on the Arduino forums use the microcontroller to do PAL TV output, I wanted to give it a shot. The technique is fairly simple and uses as little as two digital IO ports. The code has been totally rewritten and adapted to the NTSC standard. This project never went very far because lacking an oscilloscope and experience in ASM, it became clear that I would be unable to achieve perfect timing which is necessary for video output. Furthermore, the ATMega168 lacks the necessary RAM space to provide a decent frame buffer. I have still learned much from this project which appeared on the Make blog. Would I have had more resources to spend on this project I would have liked to use ASM and timer interrupts for better synchronizing and to expand it with gray levels using more IO pins.

Bibliography

Circuit

http://instruct1.cit.cornell.edu/courses/ee476/video/VideoDAC.png

Source code

You can also download a plain text copy.

/**
 *         NTSC, revision 4
 * -------------------------------------
 * Created Sun Aug 26 14:59:29 EDT 2007
 *
 * (cleft) 2007 by Matthieu Lalonde
 * Some rights reserved under a Creative Commons By-Sa 2.5
 *  http://creativecommons.org/licenses/by-nc-sa/2.5/ca/    
 * 
 * Notes:
 *      You can still use pins 10 to 13 as digital outputs (however, no pwn or input ! ).
 *          This is done through the use of "_SIGNAL | (PORTB & B00111100)"
 * 
 * Known Issues:
 *      If you put the resolution higher than ~21x16, the uC seems to crash
 *      Sometimes the uC acts as if the mode (output) had not been set properly.
 *          (Re-uploading seemed to have fixed the problem when I've seen it.)
 *      Going into signal mode sometimes seems to crash the uC (same behavior as the first issue).
 * 
 * Circuit:
 *      Use this circuit: http://www.eyetap.org/ece385/vinfo_da00.png
 *          Ideal values for the resistors are 300ohm and 900ohm.
 * 
 * Bibliography & Reference:
 *      Documentation: http://www.eyetap.org/ece385/lab5.htm
 *      Thanks to Binarymillenium for his video (http://vimeo.com/288344) explaining common sync problems.
 *      Thanks to Benoit Rousseau for the original PAL code & research
 *      Thanks to MAKE who featured this project on the blog and pushed me to continue
 *      Thanks to Ikaruga for his help with testing
 * 
 * Associated forum thread: 
 *      http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1188261175
 *
**/
#include "WProgram.h"

/**
 * Constants definition
**/
#define NOP PORTB = PORTB /* For some reason, I can seem to access NOP properly! This does the job */

// Debug levels
#define _debugLevelDisable      0
#define _debugLevelGeneric      1
#define _debugLevelSignal       10
#define _debugLevelSimul        20

/**
 * Notes about _debugLevelSignal:
 * 
 * Using this mode you can go through each voltage levels (Sync, black, gray, white).
 * This is useful for testing the circuit as you can make sure the voltages are right by using a multimeter
**/

// Select either of the above debug levels (_debugLevelDisable to disable debugging)
#define _debugMode              _debugLevelGeneric

// Pins definition
#if _debugMode > _debugLevelDisable
#define _pinLED                 B00010000
#endif
#define _pinSync                8
#define _pinVideo               9

// Defines signal levels for the 2 bit DAC
        /* Arduin pin mapping:   nn111100
                                 aa321098   */
#define _SYNC                   B00000000   /* 0.00v */
#define _BLACK                  B00000001   /* 0.33v */
#define _GRAY                   B00000010   /* 0.67v */
#define _WHITE                  B00000011   /* 1.00v */

// Defines TV Modes
#define _NTSC                   1
#define _PAL                    2

// Select a TV mode (PAL isn't implemented, yet)
#define _tvMode                 _NTSC

#if _tvMode == _NTSC
#define _tvNbrLines             262 /* Includes the last 20 lines for the vertical sync! */
#define _tvVSyncNbrLines        20  /* These 20 lines... */
#define _tvRefreshRate          60  /* Hertz */

/**
 * Note about timings. I believe they are all rounded as they are converter to ints!
 */
#define _ntscLineLength         63.3 /* Could be 63.625 */

#define _ntscDelayHSyncStart    4.7
#define _ntscDelayBackPorch     5.9
#define _ntscDelayFrontPorch    1.4
#define _ntscDelayPerLine       51.5
#define _ntscDelayVSync         50 /* Normally this would be 58.8, but it's apparently too long */
#endif

#if _tvMode == _PAL
    /* Nada */
#endif

#define _tvPixelWidth           21  /* This _cannot_ be higher than the number of lines! */
#define _tvPixelHeight          16

#define _serialBauteRate        115200

/**
 * Prototypes definition
**/
void generateVSync(void);
void writeBufferLine(unsigned char position);
void writeTVLines(void);
void fillFrameBufferWith(byte _level);
void clearFrameBuffer(void);
void fillWhiteFrameBuffer(void);
void fillGrayFrameBuffer(void);
void fillPatternFrameBuffer(void);
void fillRandomFrameBuffer(void);
void setPixel(byte x, byte y, byte val);
void loadSprite(byte* graphic);

/**
 * Variable definition
**/
char    serialBuffer;                                   // Serial buffer
byte    frameBuffer[_tvPixelWidth][_tvPixelHeight];     // Video frame buffer
byte    linesPerPixel   = _tvNbrLines / _tvPixelHeight; // Contains the number of lines per pixels

// Defines a static sprite (this could come from EEPROM also but it's a bad idea to load into the ram)
/*const static byte graphic[_tvPixelWidth][_tvPixelHeight] = {
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_BLACK, _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE },
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_BLACK, _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE },
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_BLACK, _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE },
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_BLACK, _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE },
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_BLACK, _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE },
{_WHITE, _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY },
{_BLACK, _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE,  _BLACK,  _WHITE },
{_GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY,  _GRAY }};
*/
void setup(void) {
    //cli();    /* Disabled in wiring.c */
    
    // Enables port. As of now Arduino pins 8 to 13 are all inputs!
    DDRB    = B00111111;
    PORTB   = _SYNC;        // Turn all pins off
    
#ifdef _pinLED
    if (_pinLED < 8) digitalWrite(_pinLED, HIGH);
    else PORTB  = (_pinLED & B00111100);
#endif
    
    
#if _debugMode > _debugLevelDisable 
    fillRandomFrameBuffer();
#else
    clearFrameBuffer();
#endif
    
    Serial.begin(_serialBauteRate);
    Serial.print("Mode: ");
}

//unsigned int previousMillis, interval;
//interval = 1000/_tvRefreshRate;

void loop(void) {
#if _debugMode < _debugLevelSignal
    // Write the tv lines
    //if (millis() - previousMillis > interval) {
    //  previousMillis = millis();
        writeTVLines();
    //}
#endif
    
    // Check for commands
    if (Serial.available())
    {
        serialBuffer    = Serial.read();
        Serial.println(serialBuffer);
        
        switch (serialBuffer)
        {
#if _debugMode < _debugLevelSignal
            case 'w' :
            fillWhiteFrameBuffer();     // Fill the display with white
            break;
            case 'g' :
            fillGrayFrameBuffer();      // Fill the display with gray
            break;
            case 'p' :
            fillPatternFrameBuffer();   // Fill the display with a calculated patter
            break;
            case 'c' :
            clearFrameBuffer();         // Clear the display (all black)
            break;
            case 'r' :
            fillRandomFrameBuffer();    // Load a sprite to the display
            break;
            case 's' :
            //loadSprite();             // Load a sprite to the display
            break;
#else
        /**
            This section is used for sinal debug mode only!
            Use a multimeter to check the tv signal voltages
        **/
            case '1' :
            PORTB   = _SYNC | (PORTB & B00111100);
            break;
            case '2' :
            PORTB   = _BLACK | (PORTB & B00111100);
            break;
            case '3' :
            PORTB   = _GRAY | (PORTB & B00111100);
            break;
            case '4' :
            PORTB   = _WHITE | (PORTB & B00111100);
            break;
#endif      
            /**
             * Display usage
            **/
            default :
#if _debugMode < _debugLevelSignal
            Serial.println("c: clear screen | p: fill with pattern 1 | s: fill with sprite | w: fill with white | g: fill with gray");
#else
            Serial.println("Debug Levels >> 1: _SYNC | 2: _BLACK | 3: _GRAY | 4: _WHITE");
#endif
            break;
        }
        
        Serial.print("Mode: ");
    }
}

#if _tvMode == _NTSC
/**
 *  Begin the NTSC Specific block
 * *******************************
 *
 *  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 ***
**/

/**
 * Writes every line from what's inside the frameBuffer
 * 
 * Warning: You cannot take the beginning and end sync out of this functions or the sync will fail
 *          (This has been tested on a Mega8)
**/
void writeTVLines(void)
{
    
    unsigned char i, j, k;
    j = 0;
    k = 0;

    for (i=0;i<(_tvNbrLines - _tvVSyncNbrLines);i++) // Correction for the 20 lines of vertical sync
    {
        /* Begin Line Sync */

        // H Sync
        PORTB = _SYNC | (PORTB & B00111100);
        delayMicroseconds(_ntscDelayHSyncStart);
        
        // Back Porch
        PORTB = _BLACK | (PORTB & B00111100);
        delayMicroseconds(_ntscDelayBackPorch);
        
        writeBufferLine(j); // Visible scan (51.5µs)
        
        /* End Line Sync (Front Porch) */
        PORTB = _BLACK | (PORTB & B00111100);
        delayMicroseconds(_ntscDelayFrontPorch);
        
        if (i == (k + linesPerPixel))
        {
            k = i;
            j++;
        }
    }

    // Vertical Sync follows
    generateVSync();
}

/**
 * Writes each pixel of a line
**/
void writeBufferLine(unsigned char position)
{
    unsigned char ii = _tvPixelWidth;
    byte    lineDelay   = (_ntscDelayPerLine - _tvPixelWidth)  / _tvPixelWidth;
    
    while (ii != 0)
    {
        PORTB = frameBuffer[ii][position] | (PORTB & B00111100);
        // Micro-correction
        NOP;
        NOP;
        NOP;
        NOP;
        // Waste some time to make sure the line is sent during the proper timing
        delayMicroseconds(lineDelay);
        ii--;
    }
    /*
    for (ii = 0; ii < _tvPixelWidth; ii++)
    {
        PORTB = frameBuffer[ii][position] | (PORTB & B00111100);
        // Micro-correction
        NOP;
        NOP;
        NOP;
        NOP;
        // Waste some time to make sure the line is sent during the proper timing
        delayMicroseconds(lineDelay);
    }
    */
}

/**
 * Generate sync pulse for the virtual sync (lasts _tvVSyncNbrLines lines)
**/
void generateVSync(void)
{
    unsigned char ii    = _tvVSyncNbrLines;
    while(ii != 0)
    {
        // Begin V Sync
        PORTB = _BLACK | (PORTB & B00111100);
        delayMicroseconds(_ntscDelayHSyncStart);

        PORTB = _SYNC | (PORTB & B00111100);
        // Micro-correction
        NOP;
        NOP;
        
        delayMicroseconds(_ntscDelayVSync);
        
        ii--;
    }
}

/**
 * End of the NTSC Specific block
**/
#endif

#if _tvMode == _PAL
    /* Nada */
#endif

/**
 * Clears the frame buffer (if mode is true it will fill it with a chess board pattern)
**/
void fillFrameBufferWith(byte _level)
{
    unsigned char index, index2;

    for (index2 = 0; index2 < _tvPixelHeight; index2++)
    {
        for (index = 0; index < _tvPixelWidth; index++)
        {
            frameBuffer[index][index2] = _level;
        }
    }
}

/**
 * Fills the frame buffer with black
**/
void clearFrameBuffer(void)
{
    fillFrameBufferWith(_BLACK);
}

/**
 * Fills the frame buffer with white
**/ 
void fillWhiteFrameBuffer(void)
{
    fillFrameBufferWith(_WHITE);
}

/**
 * Fills the frame buffer with gray
**/ 
void fillGrayFrameBuffer(void)
{
    fillFrameBufferWith(_GRAY);
}

/**
 * Fills the frame buffer with a pattern
**/ 
void fillPatternFrameBuffer(void)
{
    unsigned char index, index2;

    for (index2 = 0; index2 < _tvPixelHeight; index2++)
    {
        for (index = 0; index < _tvPixelWidth; index++)
        {
            frameBuffer[index][index2] = (index + index2) % 3 + 1;
        }
    }
}

/**
 * Fills the frame buffer with random values
**/ 
void fillRandomFrameBuffer(void)
{
    unsigned char index, index2;

    for (index2 = 0; index2 < _tvPixelHeight; index2++)
    {
        for (index = 0; index < _tvPixelWidth; index++)
        {
            frameBuffer[index][index2] = rand()%3 + 1;
        }
    }
}

/**
 * Allows setting pixels independently
**/
void setPixel(byte x, byte y, byte val)
{
    frameBuffer[x][y]   = val;
}

/**
 * Copies a sprite to the frameBuffer
**/
void loadSprite(byte* graphic)
{
    if (memcpy(frameBuffer[0], graphic, (_tvPixelWidth * _tvPixelHeight)));
}