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
- Arduino Pong
- http://www.stanford.edu/class/ee281/handouts/lab4.pdf
- http://vimeo.com/288344
- Interfacing with an NTSC TV forum thread
- Vidéo avec Arduino, afficher sur moniteur forum thread (in French)
- Arduino TV post in the Makezine blog featuring my NTSC code.
Circuit
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))); }

