{{{ #!html
}}} {{{ #!html }}} = Arduino TV = After seeing someone on the [http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1176757335/0 Arduino forums] use the microcontroller to do [http://en.wikipedia.org/wiki/PAL/ 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 [http://en.wikipedia.org/wiki/NTSC 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 [http://blog.makezine.com/archive/2007/08/arduino_tv.html 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 == * [http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1187659197 Arduino Pong] * [http://www.stanford.edu/class/ee281/handouts/lab4.pdf] * [http://vimeo.com/288344] * [http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1188261175/0 Interfacing with an NTSC TV] forum thread * [http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1176757335/0 Vidéo avec Arduino, afficher sur moniteur] forum thread (in French) * [http://blog.makezine.com/archive/2007/08/arduino_tv.html Arduino TV ] post in the Makezine blog featuring my NTSC code. == Circuit == [[Image(http://instruct1.cit.cornell.edu/courses/ee476/video/VideoDAC.png)]] == Source code == You can also download a [/basedoc/NTSC.pde plain text] copy. {{{ #!cpp /** * 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))); } }}}