Rob Younger
/
fractal_demo
composite.cpp
- Committer:
- robyounger
- Date:
- 2009-11-15
- Revision:
- 0:fb93ebe5f84f
File content as of revision 0:fb93ebe5f84f:
//////////////////////////////////////////////////////////// // Software generation of a grayscale composite TV signal // // Puts a 105x128 grayscale fractal zoom onscreen (slow!) // // // // Hacked together, (ab)uses the LPC1768 DAC output (p18) // // with some shifty looking timing sensitive code // // // // // // Rob Younger 26th Oct 2009, (tweaked 15th Nov 2009) // //////////////////////////////////////////////////////////// // Generating video like it's 1982! // Warning : this is *very* hacky code - just proof of concept! // This might blow up your mbed or your TV. // I claim no responsibility for anything :-) // Start with a 180 Ohm resistor in series with the DAC output // before connecting to a composite AV input, // DAC is about 0-3.3v output, Composite in 1v p-p, with a 75 Ohm termination, so 180 Ohms is about right. // but it also worked without any resistor for me! Start with a higher value if you aren't sure. // More likely to burn out your mbed or TV with low/no resistor - Use at your own risk! // HOW THIS WORKS: // The DAC output is written as fast as possible to software generate a composite signal // dac.write_u16() seems to take about 0.5 us: I worked this timing out using a big loop of // dac.write_u16(0); // .... // dac.write_u16(0); // dac.write_u16(0xFFFF); // .... // dac.write_u16(0xFFFF); // Until I got a frequency I could measure on a multimeter. // // At full speed gives us about 1MHz max frequency - // I don't have an oscilloscope to see how well this actually works, probably totally out of spec! // // The software just runs loads of these to generate the composite signal as fast as possible! // // Since a TV output is generated continuously this would use 100% CPU time. // // Clever to-the-metal code would do things like use the horizontal and vertical blanking // intervals to do any required calculation. This isn't clever to-the-metal code! // Instead, I just don't draw the bottom few percent of the TV picture, and use this free time to run code. // This may well cause your TV to loose sync, but it works for me - I did say it was a hack! // // Driving the display takes 90%, main code gets 10% to play with at the end of each frame. // Tweak these percentages up and down, but loose too many lines and the tv is much more likely to // drop the signal, equally as you hit 100% CPU the frame calls might start to overlap and it all goes a bit wrong! // // This code actually starts with the end of previous frame signalling first, then all the setup, then the actual picture. // // It's coded up as a routine that draws a whole frame (field), which is called from main on a timer interrupt (at 50Hz for PAL) // This makes it easy to have a main routing that can operate normally, without you having to worry (too much) about the timing involved. // The picture elements of the signal are created by dumping a global frame buffer over to the DAC: // unsigned short int framebuffer[HEIGHT][WIDTH]; // The values in this framebuffer are the actual composite signal, NOT just shades of gray! // In other words, only write values between 0x56DB (black) and 0xFFFF (bright white). // For this reason, it's important to initialize the buffer to all 0x56DB or above // Yes - there's probably a much better way to do this - but you don't want to slow down the DAC writes at all. // Adding checks or shifting the value from a normal range might be to slow - over to the real programmers to work out how to do this... // The frame buffer is 105 pixels wide - this is just because 105 dac writes take up the time required for a horizontal tv line. // height is more arbitrary, as we draw every scan line - but I double or quadruple scan to get squarish pixels! // Use a modulo value in the picture write line to repeat the picture for small framebuffers. // This program has a couple of demo routines. One draws a fractal, and the other just writes random values to the framebuffer // A future enhancement could be to have two small framebuffers 105x64 and do double buffering? Needs to all be in fast memory though. // The code could definitely do with some tuning as the sync delays are all a bit off... ////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "mbed.h" //Framebuffer size #define WIDTH 105 #define HEIGHT 128 //TV signal generation controlling: #define LINES 256 //Visible lines drawn to screen #define SCAN 2 //Number of scanlines per pixel (vertical) #define DRAWWIDTH 105 //Pixels per line // LINES: theoretically up to 286 for PAL, 241 for NTSC). 285 seems to be about 100% CPU on PAL. Smaller values means I stop drawing the signal early. // SCAN: controls double scan (e.g. 128 pixels to 256 lines) // DRAWWIDTH: number of pixels to attempt to draw in a line (should be =< framebuffer WIDTH). Very timing critical - expect different values to break // Composite signal values for DAC output. These should really be scaled for 1v peak-to-peak #define IRE_m40 0x0000 //0volts #define IRE_0 0x4920 //Baseline #define IRE_7p5 0x56DB //Black #define IRE_100 0xFFFF //White // DAC is 10bit, but i'm using write_u16 to write to the DAC. // IRE is a definition: // the levels are -40 (0volts), 0 (baseline below black), 7.5 (Black), 100 (White). // IRE -40 is 0v, 100 is 1v, so scale accordingly! AnalogOut dac(p18); // Video out pin Ticker timer; // Timer for calling the frame DigitalOut led1(LED1);//Some status lights... DigitalOut led2(LED2); DigitalOut led3(LED3); DigitalOut led4(LED4); // Framebuffer actually has video signal levels in it - not just grayscale data // This means it must be initialised to at least all black IRE_7p5 before it's used. // zero values will likely kill the output and TV will loose sync. unsigned short int framebuffer[HEIGHT][WIDTH]; ///////////////////////////////////////////////////////////// //Software composite signal generation (very timing specific) ///////////////////////////////////////////////////////////// void createframe() { // Procedure to create a output frame to a tv - needs to run on a very regular sync (e.g. 50Hz or 60Hz) // Using the DAC to create this output, which seems to happily run at 2MHz update // dac.write_u16 seems to take almost spot on 0.5us, so I'm using multiples of this to create a signal. // Could maybe be done with timing precision through multiple digital outputs and a resistor ladder to create an external DAC, but this didn't need any external components! // Someone with an oscilloscope can tweak this to get the delays more up to standard! // TV signal specs // For 50Hz PAL, each line takes up 64us, and there are 625 lines, but split into two fields of about 312 lines. // I'm treating both fields exactly the same, so we have a 312(ish) lines at 50Hz. // NTSC is actually very similar but with slightly different timings/counts. (525 lines at 60Hz). // Some info found through google: //525line (NTSC) - required timing in us for a line //NAME LENGTH LEVEL //Front porch 1.5 IRE_0 //Sync Tip 4.7 IRE_m40 //Breezeway 0.6 IRE_0 //Color Burst 2.5 IRE_0 //Back Porch 1.6 IRE_0 //Active Video 52.6 IRE_7p5 - IRE100 //Total line time = 63.5us ( * half of 525 lines * 60Hz) //625line (PAL) - required timing in us for a line //NAME LENGTH LEVEL //Front porch 1.65 IRE_0 //Sync Tip 4.7 IRE_m40 //Breezeway 0.9 IRE_0 //Color Burst 2.25 IRE_0 //Back Porch 2.55 IRE_0 //Active Video 51.95 IRE_7p5 - IRE100 //Total line time = 64us ( * half of 625 lines * 50Hz) // There actually seem to be a lot of variations on this, but they all seem roughly the same. // Colour needs a precision ~4MHz carrier signal applied over the 'color burst' and active video // with precise phase and amplitude control (sounds like a lot of work!) // So for colour, Use svideo, VGA, or use 3 of these signals to generate an RGB scart signal? //The basic frame format is //1) few lines of special start pulses, //2) some off screen lines, which had things like teletext/close captions //3) the tv picture bit you see, //4) some special pulses to say end of screen, go back to the top. // Then straight back to 1 for the next frame. // To get the timing right - I do this: //4) some special pulses to say end of screen, go back to the top. //1) few lines of special start pulses, //2) some off screen lines, which had things like teletext/close captions //3) the tv picture bit you see, // You can get away dropping the last few lines of 3) // I use this to drop back to the main program to run as normal. // Ideally you'd use the few cycles between each line to do stuff, but that's going to be hard to get timing right. //////////////////////////////////////////////////////////// //Start of Frame //////////////////////////////////////////////////////////// //Each dac.write is ~0.5us, so multiply up to create the timings. // (This is a mix of PAL and NTSC - hack as appropriate) // There are 21 lines per field in a vertical blanking period // the last 4 lines of a field indicate are just before flyback // then there are 5 blank lines for flyback itself... //END OF A FRAME + FLYBACK + START OF NEW FRAME signalling (9 lines) for (int i = 0; i < 6; i++) { //6 equalizing pulses (time = 6 half lines) dac.write_u16(IRE_m40); //2.4us dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); // dac.write_u16(IRE_m40); dac.write_u16(IRE_0); //29.4us wait_us(28); } for (int i = 0; i < 6; i++) {// 6 serrated vertical pulses (time = 6 half lines) dac.write_u16(IRE_0); //2.4us dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_0); // dac.write_u16(IRE_0); dac.write_u16(IRE_m40); //29.4us wait_us(28); } for (int i = 0; i < 6; i++) { // 6 equalizing pulses (time = 6 half lines) dac.write_u16(IRE_m40); //2.4us dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); // dac.write_u16(IRE_m40); dac.write_u16(IRE_0); //29.4us wait_us(28); } // The lines just above the top of the picture used for setup/teletext/closed captions etc. // about 17 lines for PAL, 12 for NTSC? for (int i = 0; i < 17; i++) { //10.9us (NTSC) or 12.5us (PAL) for horizontal blanking interval dac.write_u16(IRE_0); //Front porch 1.6us dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_m40); //Sync Tip 4.7us dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); // dac.write_u16(IRE_m40); //extra for PAL timing dac.write_u16(IRE_0); //Breezeway 0.5us dac.write_u16(IRE_0); //ColorBurst 2.5us dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_m40); //Back Porch 1.6us (2.55us in PAL) dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); //extra for PAL timing dac.write_u16(IRE_m40); //extra for PAL timing // for (int j = 0; j < DRAWWIDTH; j++) { // dac.write_u16(IRE_0); // } //next pixel //Then that video signal for 52.6us (52 for PAL) dac.write_u16(IRE_0); //Video signal for 52.6us wait_us(51); // replaces another 104 dac.write_u16(IRE_0) } //Draw the actual visible lines on screen: exactly same header as previous, but followed by real video data. // intentionally dropping the last few lines to throw some time to main() // otherwise this loop would use 100% of CPU. for (int i = 0; i < LINES; i++) { //10.9us (NTSC) or 12.5us (PAL) for horizontal blanking interval dac.write_u16(IRE_0); //Front porch 1.6us dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_m40); //Sync Tip 4.7us dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); // dac.write_u16(IRE_m40); //extra for PAL timing dac.write_u16(IRE_0); //Breezeway 0.5us dac.write_u16(IRE_0); //ColorBurst 2.5us dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_0); dac.write_u16(IRE_m40); //Back Porch 1.6us (2.55us in PAL) dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); dac.write_u16(IRE_m40); //extra for PAL timing dac.write_u16(IRE_m40); //extra for PAL timing //Then that video signal for 52.6us (52 for PAL): //////////////////////////////////////////////////////////// //Write out the video data //(very timing sensitive as must last ~53us, no more or less) //////////////////////////////////////////////////////////// // Examples: //1) draw random shade per line // dac.write_u16(rand() % 40000 + IRE_7p5); //Video signal for 52.6us // wait_us(52); //2) draw black // dac.write_u16(IRE_7p5); // wait_us(51); //3) draw white // dac.write_u16(IRE_100); // wait_us(51); //4) draw framebuffer // Code here is very timing critical. // loop count is instruction dependent, if you add some code here, it will need be a different width // We have ~52.5us and a a dac write takes 0.5us so 104/105px seems correct. // Trial+error shows ~100-110 pixels to be OKish on a particular old TV. int k =(i/ SCAN ); //double scan the framebuffer, particularly convenient for 128 vertical px but 256 line resolution.. // The modulo is only needed if screen output size is bigger than framebuffer (wrapping occurs). // Stick to powers of 2 for modulo wrapping (or likely too slow). for (int j = 0; j < DRAWWIDTH; j++) { dac.write_u16( framebuffer[k%128][j%128] ); //modulo used to wrap framebuffer. Keep to power of 2 =< framebuffer sizes. } //next pixel } //next line loop //Default back to black when we don't bother drawing the last few lines of a frame! dac.write_u16(IRE_7p5); } //End of createframe routine //////////////////////////////////////////////////////////// // randomfill the framebuffer // //////////////////////////////////////////////////////////// void randomfill () { for (int j = 0; j < HEIGHT; j++) { for (int i = 0; i < WIDTH; i++) { framebuffer[j][i] = rand(); if (framebuffer[j][i] < IRE_7p5 ) { framebuffer[j][i] = IRE_7p5; } } } } //////////////////////////////////////////////////////////// // blank the framebuffer // //////////////////////////////////////////////////////////// void blankfill () { for (int j = 0; j < HEIGHT; j++) { for (int i = 0; i < WIDTH; i++) { framebuffer[j][i] = IRE_7p5; //framebuffer[j][i] = IRE_100; } } } //////////////////////////////////////////////////////////// // zooming mandelbot fractal in the framebuffer // //////////////////////////////////////////////////////////// void mandelbrot () { //Mandelbrot escape time algorithm (doubles+iteration=slow) //Taken from wikipedia pseudocode, //tweaked by using the speeded up version that google found on geocities //(oops - Geocities has shut down in the 3 weeks since I wrote this! first time I've used it in years!) //http://www.geocities.com/CapeCanaveral/5003/Mandel.txt //then put in a loop to zoom in on a intersting co-ords point i saw elsewhere... double zoom; for (int z = 0; z < 200; z++) {//2^50 is quite a lot of zoom - thats why you need precision! zoom= pow((double)1.2,z); led1=0; led2=0; led3=0; led4=0; double x,y; double x0,y0; // double xtemp; double xsq; double ysq; unsigned short int iteration = 0; unsigned short int max_iteration = (z*2)+20; //arbitrary scaling so there are more interation allowed as you zoom for (int j = 0; j < HEIGHT; j++) { //little status hack as as drawing fractals (particularly with doubles on only 10% of a cpu is slow!) if (j== (( HEIGHT /4)-1)) { led1=1; } else if (j==(( HEIGHT /2)-1)) { led2=1; } else if (j==(3*( HEIGHT /4)-1)) { led3=1; } else if (j==( HEIGHT -1)) { led4=1; } //end of little status hack for (int i = 0; i < WIDTH; i++) { // x0=(((float) i) -32.0)/32.0;//redefine 0to63 as -1to+1 mandelbrot window // y0=(((float) j) -32.0)/32.0;//redefine 0to63 as -1to+1 mandelbrot window //-1.865725138512217656771 moves center point to something interesting x0=((((double) i) - ( WIDTH /2)) /zoom)-1.865725138512217656771;//redefine 0to63 as -1to+1 mandelbrot window y0=(((double) j) - ( HEIGHT /2)) /zoom;//redefine 0to63 as -1to+1 mandelbrot window iteration = 0; //Standard version of mandelbrot loop based on wikipedia pseudocode // x=0; // y=0; // while ( ((x*x + y*y) <= (2*2)) && (iteration < max_iteration) ) { // xtemp = x*x - y*y + x0; // y = 2*x*y + y0; // x = xtemp; // iteration++; // } //Speedy version of main mandelbrot loop (algorithm from geocities page) x=x0+x0*x0-y0*y0; y=y0+x0*y0+x0*y0; for (iteration=0;iteration<max_iteration && (ysq=y*y)+(xsq=x*x)<4;iteration++,y=y0+x*y+x*y,x=x0-ysq+xsq) ; //Iteration count determines color (clamp max iteration to zero, and normalize for black to white) framebuffer[j][i] = (( iteration == max_iteration ) ? (IRE_7p5) : (IRE_7p5 + ((iteration%20)*2000)) ); } } }//zoom loop } //////////////////////////////////////////////////////////// // main() showing use framebuffer // // Puts a grayscale pic in it // //////////////////////////////////////////////////////////// int main() { blankfill(); //set framebuffer to blank values timer.attach_us(&createframe,20000);//attach the display (at 50Hz) // int attached=0; //attach frame // //If you had a lot of setup in a main game loop, you could do something like this: // if (attached==0) { // timer.attach_us(&createframe,20000); // attached=1; // } //Program loop while (1) { // Add you own demo code here. Expect it to get regularly interupted by the screen draw call! // very simple code can run at full fps. //Example - change HEIGHT to 64 and SCAN to 4 and use randomfill instead of mandelbrot... //randomfill(); //random pixel fill mandelbrot(); //mandelbrot procedure is a 200 loop zoom so takes ages - and each scene redraw takes a few seconds! } //while } //main