User | Revision | Line number | New contents of line |
robyounger |
0:fb93ebe5f84f
|
1
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
2
|
// Software generation of a grayscale composite TV signal //
|
robyounger |
0:fb93ebe5f84f
|
3
|
// Puts a 105x128 grayscale fractal zoom onscreen (slow!) //
|
robyounger |
0:fb93ebe5f84f
|
4
|
// //
|
robyounger |
0:fb93ebe5f84f
|
5
|
// Hacked together, (ab)uses the LPC1768 DAC output (p18) //
|
robyounger |
0:fb93ebe5f84f
|
6
|
// with some shifty looking timing sensitive code //
|
robyounger |
0:fb93ebe5f84f
|
7
|
// //
|
robyounger |
0:fb93ebe5f84f
|
8
|
// //
|
robyounger |
0:fb93ebe5f84f
|
9
|
// Rob Younger 26th Oct 2009, (tweaked 15th Nov 2009) //
|
robyounger |
0:fb93ebe5f84f
|
10
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
11
|
|
robyounger |
0:fb93ebe5f84f
|
12
|
// Generating video like it's 1982!
|
robyounger |
0:fb93ebe5f84f
|
13
|
|
robyounger |
0:fb93ebe5f84f
|
14
|
// Warning : this is *very* hacky code - just proof of concept!
|
robyounger |
0:fb93ebe5f84f
|
15
|
// This might blow up your mbed or your TV.
|
robyounger |
0:fb93ebe5f84f
|
16
|
// I claim no responsibility for anything :-)
|
robyounger |
0:fb93ebe5f84f
|
17
|
|
robyounger |
0:fb93ebe5f84f
|
18
|
// Start with a 180 Ohm resistor in series with the DAC output
|
robyounger |
0:fb93ebe5f84f
|
19
|
// before connecting to a composite AV input,
|
robyounger |
0:fb93ebe5f84f
|
20
|
// DAC is about 0-3.3v output, Composite in 1v p-p, with a 75 Ohm termination, so 180 Ohms is about right.
|
robyounger |
0:fb93ebe5f84f
|
21
|
|
robyounger |
0:fb93ebe5f84f
|
22
|
// but it also worked without any resistor for me! Start with a higher value if you aren't sure.
|
robyounger |
0:fb93ebe5f84f
|
23
|
// More likely to burn out your mbed or TV with low/no resistor - Use at your own risk!
|
robyounger |
0:fb93ebe5f84f
|
24
|
|
robyounger |
0:fb93ebe5f84f
|
25
|
|
robyounger |
0:fb93ebe5f84f
|
26
|
// HOW THIS WORKS:
|
robyounger |
0:fb93ebe5f84f
|
27
|
|
robyounger |
0:fb93ebe5f84f
|
28
|
// The DAC output is written as fast as possible to software generate a composite signal
|
robyounger |
0:fb93ebe5f84f
|
29
|
// dac.write_u16() seems to take about 0.5 us: I worked this timing out using a big loop of
|
robyounger |
0:fb93ebe5f84f
|
30
|
// dac.write_u16(0);
|
robyounger |
0:fb93ebe5f84f
|
31
|
// ....
|
robyounger |
0:fb93ebe5f84f
|
32
|
// dac.write_u16(0);
|
robyounger |
0:fb93ebe5f84f
|
33
|
// dac.write_u16(0xFFFF);
|
robyounger |
0:fb93ebe5f84f
|
34
|
// ....
|
robyounger |
0:fb93ebe5f84f
|
35
|
// dac.write_u16(0xFFFF);
|
robyounger |
0:fb93ebe5f84f
|
36
|
// Until I got a frequency I could measure on a multimeter.
|
robyounger |
0:fb93ebe5f84f
|
37
|
//
|
robyounger |
0:fb93ebe5f84f
|
38
|
// At full speed gives us about 1MHz max frequency -
|
robyounger |
0:fb93ebe5f84f
|
39
|
// I don't have an oscilloscope to see how well this actually works, probably totally out of spec!
|
robyounger |
0:fb93ebe5f84f
|
40
|
//
|
robyounger |
0:fb93ebe5f84f
|
41
|
// The software just runs loads of these to generate the composite signal as fast as possible!
|
robyounger |
0:fb93ebe5f84f
|
42
|
//
|
robyounger |
0:fb93ebe5f84f
|
43
|
// Since a TV output is generated continuously this would use 100% CPU time.
|
robyounger |
0:fb93ebe5f84f
|
44
|
//
|
robyounger |
0:fb93ebe5f84f
|
45
|
// Clever to-the-metal code would do things like use the horizontal and vertical blanking
|
robyounger |
0:fb93ebe5f84f
|
46
|
// intervals to do any required calculation. This isn't clever to-the-metal code!
|
robyounger |
0:fb93ebe5f84f
|
47
|
// Instead, I just don't draw the bottom few percent of the TV picture, and use this free time to run code.
|
robyounger |
0:fb93ebe5f84f
|
48
|
// This may well cause your TV to loose sync, but it works for me - I did say it was a hack!
|
robyounger |
0:fb93ebe5f84f
|
49
|
//
|
robyounger |
0:fb93ebe5f84f
|
50
|
// Driving the display takes 90%, main code gets 10% to play with at the end of each frame.
|
robyounger |
0:fb93ebe5f84f
|
51
|
// Tweak these percentages up and down, but loose too many lines and the tv is much more likely to
|
robyounger |
0:fb93ebe5f84f
|
52
|
// drop the signal, equally as you hit 100% CPU the frame calls might start to overlap and it all goes a bit wrong!
|
robyounger |
0:fb93ebe5f84f
|
53
|
//
|
robyounger |
0:fb93ebe5f84f
|
54
|
// This code actually starts with the end of previous frame signalling first, then all the setup, then the actual picture.
|
robyounger |
0:fb93ebe5f84f
|
55
|
//
|
robyounger |
0:fb93ebe5f84f
|
56
|
// 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)
|
robyounger |
0:fb93ebe5f84f
|
57
|
// This makes it easy to have a main routing that can operate normally, without you having to worry (too much) about the timing involved.
|
robyounger |
0:fb93ebe5f84f
|
58
|
// The picture elements of the signal are created by dumping a global frame buffer over to the DAC:
|
robyounger |
0:fb93ebe5f84f
|
59
|
// unsigned short int framebuffer[HEIGHT][WIDTH];
|
robyounger |
0:fb93ebe5f84f
|
60
|
// The values in this framebuffer are the actual composite signal, NOT just shades of gray!
|
robyounger |
0:fb93ebe5f84f
|
61
|
// In other words, only write values between 0x56DB (black) and 0xFFFF (bright white).
|
robyounger |
0:fb93ebe5f84f
|
62
|
// For this reason, it's important to initialize the buffer to all 0x56DB or above
|
robyounger |
0:fb93ebe5f84f
|
63
|
|
robyounger |
0:fb93ebe5f84f
|
64
|
// Yes - there's probably a much better way to do this - but you don't want to slow down the DAC writes at all.
|
robyounger |
0:fb93ebe5f84f
|
65
|
// 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...
|
robyounger |
0:fb93ebe5f84f
|
66
|
|
robyounger |
0:fb93ebe5f84f
|
67
|
// The frame buffer is 105 pixels wide - this is just because 105 dac writes take up the time required for a horizontal tv line.
|
robyounger |
0:fb93ebe5f84f
|
68
|
// height is more arbitrary, as we draw every scan line - but I double or quadruple scan to get squarish pixels!
|
robyounger |
0:fb93ebe5f84f
|
69
|
// Use a modulo value in the picture write line to repeat the picture for small framebuffers.
|
robyounger |
0:fb93ebe5f84f
|
70
|
|
robyounger |
0:fb93ebe5f84f
|
71
|
// This program has a couple of demo routines. One draws a fractal, and the other just writes random values to the framebuffer
|
robyounger |
0:fb93ebe5f84f
|
72
|
|
robyounger |
0:fb93ebe5f84f
|
73
|
// A future enhancement could be to have two small framebuffers 105x64 and do double buffering? Needs to all be in fast memory though.
|
robyounger |
0:fb93ebe5f84f
|
74
|
// The code could definitely do with some tuning as the sync delays are all a bit off...
|
robyounger |
0:fb93ebe5f84f
|
75
|
|
robyounger |
0:fb93ebe5f84f
|
76
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
77
|
|
robyounger |
0:fb93ebe5f84f
|
78
|
|
robyounger |
0:fb93ebe5f84f
|
79
|
#include "mbed.h"
|
robyounger |
0:fb93ebe5f84f
|
80
|
|
robyounger |
0:fb93ebe5f84f
|
81
|
//Framebuffer size
|
robyounger |
0:fb93ebe5f84f
|
82
|
#define WIDTH 105
|
robyounger |
0:fb93ebe5f84f
|
83
|
#define HEIGHT 128
|
robyounger |
0:fb93ebe5f84f
|
84
|
|
robyounger |
0:fb93ebe5f84f
|
85
|
//TV signal generation controlling:
|
robyounger |
0:fb93ebe5f84f
|
86
|
#define LINES 256 //Visible lines drawn to screen
|
robyounger |
0:fb93ebe5f84f
|
87
|
#define SCAN 2 //Number of scanlines per pixel (vertical)
|
robyounger |
0:fb93ebe5f84f
|
88
|
#define DRAWWIDTH 105 //Pixels per line
|
robyounger |
0:fb93ebe5f84f
|
89
|
|
robyounger |
0:fb93ebe5f84f
|
90
|
// 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.
|
robyounger |
0:fb93ebe5f84f
|
91
|
// SCAN: controls double scan (e.g. 128 pixels to 256 lines)
|
robyounger |
0:fb93ebe5f84f
|
92
|
// DRAWWIDTH: number of pixels to attempt to draw in a line (should be =< framebuffer WIDTH). Very timing critical - expect different values to break
|
robyounger |
0:fb93ebe5f84f
|
93
|
|
robyounger |
0:fb93ebe5f84f
|
94
|
// Composite signal values for DAC output. These should really be scaled for 1v peak-to-peak
|
robyounger |
0:fb93ebe5f84f
|
95
|
#define IRE_m40 0x0000 //0volts
|
robyounger |
0:fb93ebe5f84f
|
96
|
#define IRE_0 0x4920 //Baseline
|
robyounger |
0:fb93ebe5f84f
|
97
|
#define IRE_7p5 0x56DB //Black
|
robyounger |
0:fb93ebe5f84f
|
98
|
#define IRE_100 0xFFFF //White
|
robyounger |
0:fb93ebe5f84f
|
99
|
// DAC is 10bit, but i'm using write_u16 to write to the DAC.
|
robyounger |
0:fb93ebe5f84f
|
100
|
// IRE is a definition:
|
robyounger |
0:fb93ebe5f84f
|
101
|
// the levels are -40 (0volts), 0 (baseline below black), 7.5 (Black), 100 (White).
|
robyounger |
0:fb93ebe5f84f
|
102
|
// IRE -40 is 0v, 100 is 1v, so scale accordingly!
|
robyounger |
0:fb93ebe5f84f
|
103
|
|
robyounger |
0:fb93ebe5f84f
|
104
|
AnalogOut dac(p18); // Video out pin
|
robyounger |
0:fb93ebe5f84f
|
105
|
Ticker timer; // Timer for calling the frame
|
robyounger |
0:fb93ebe5f84f
|
106
|
|
robyounger |
0:fb93ebe5f84f
|
107
|
DigitalOut led1(LED1);//Some status lights...
|
robyounger |
0:fb93ebe5f84f
|
108
|
DigitalOut led2(LED2);
|
robyounger |
0:fb93ebe5f84f
|
109
|
DigitalOut led3(LED3);
|
robyounger |
0:fb93ebe5f84f
|
110
|
DigitalOut led4(LED4);
|
robyounger |
0:fb93ebe5f84f
|
111
|
|
robyounger |
0:fb93ebe5f84f
|
112
|
// Framebuffer actually has video signal levels in it - not just grayscale data
|
robyounger |
0:fb93ebe5f84f
|
113
|
// This means it must be initialised to at least all black IRE_7p5 before it's used.
|
robyounger |
0:fb93ebe5f84f
|
114
|
// zero values will likely kill the output and TV will loose sync.
|
robyounger |
0:fb93ebe5f84f
|
115
|
unsigned short int framebuffer[HEIGHT][WIDTH];
|
robyounger |
0:fb93ebe5f84f
|
116
|
|
robyounger |
0:fb93ebe5f84f
|
117
|
|
robyounger |
0:fb93ebe5f84f
|
118
|
|
robyounger |
0:fb93ebe5f84f
|
119
|
/////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
120
|
//Software composite signal generation (very timing specific)
|
robyounger |
0:fb93ebe5f84f
|
121
|
/////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
122
|
|
robyounger |
0:fb93ebe5f84f
|
123
|
void createframe() {
|
robyounger |
0:fb93ebe5f84f
|
124
|
|
robyounger |
0:fb93ebe5f84f
|
125
|
// Procedure to create a output frame to a tv - needs to run on a very regular sync (e.g. 50Hz or 60Hz)
|
robyounger |
0:fb93ebe5f84f
|
126
|
// Using the DAC to create this output, which seems to happily run at 2MHz update
|
robyounger |
0:fb93ebe5f84f
|
127
|
// dac.write_u16 seems to take almost spot on 0.5us, so I'm using multiples of this to create a signal.
|
robyounger |
0:fb93ebe5f84f
|
128
|
|
robyounger |
0:fb93ebe5f84f
|
129
|
// 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!
|
robyounger |
0:fb93ebe5f84f
|
130
|
|
robyounger |
0:fb93ebe5f84f
|
131
|
// Someone with an oscilloscope can tweak this to get the delays more up to standard!
|
robyounger |
0:fb93ebe5f84f
|
132
|
|
robyounger |
0:fb93ebe5f84f
|
133
|
// TV signal specs
|
robyounger |
0:fb93ebe5f84f
|
134
|
|
robyounger |
0:fb93ebe5f84f
|
135
|
// For 50Hz PAL, each line takes up 64us, and there are 625 lines, but split into two fields of about 312 lines.
|
robyounger |
0:fb93ebe5f84f
|
136
|
// I'm treating both fields exactly the same, so we have a 312(ish) lines at 50Hz.
|
robyounger |
0:fb93ebe5f84f
|
137
|
// NTSC is actually very similar but with slightly different timings/counts. (525 lines at 60Hz).
|
robyounger |
0:fb93ebe5f84f
|
138
|
|
robyounger |
0:fb93ebe5f84f
|
139
|
// Some info found through google:
|
robyounger |
0:fb93ebe5f84f
|
140
|
|
robyounger |
0:fb93ebe5f84f
|
141
|
//525line (NTSC) - required timing in us for a line
|
robyounger |
0:fb93ebe5f84f
|
142
|
//NAME LENGTH LEVEL
|
robyounger |
0:fb93ebe5f84f
|
143
|
//Front porch 1.5 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
144
|
//Sync Tip 4.7 IRE_m40
|
robyounger |
0:fb93ebe5f84f
|
145
|
//Breezeway 0.6 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
146
|
//Color Burst 2.5 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
147
|
//Back Porch 1.6 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
148
|
//Active Video 52.6 IRE_7p5 - IRE100
|
robyounger |
0:fb93ebe5f84f
|
149
|
|
robyounger |
0:fb93ebe5f84f
|
150
|
//Total line time = 63.5us ( * half of 525 lines * 60Hz)
|
robyounger |
0:fb93ebe5f84f
|
151
|
|
robyounger |
0:fb93ebe5f84f
|
152
|
//625line (PAL) - required timing in us for a line
|
robyounger |
0:fb93ebe5f84f
|
153
|
//NAME LENGTH LEVEL
|
robyounger |
0:fb93ebe5f84f
|
154
|
//Front porch 1.65 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
155
|
//Sync Tip 4.7 IRE_m40
|
robyounger |
0:fb93ebe5f84f
|
156
|
//Breezeway 0.9 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
157
|
//Color Burst 2.25 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
158
|
//Back Porch 2.55 IRE_0
|
robyounger |
0:fb93ebe5f84f
|
159
|
//Active Video 51.95 IRE_7p5 - IRE100
|
robyounger |
0:fb93ebe5f84f
|
160
|
|
robyounger |
0:fb93ebe5f84f
|
161
|
//Total line time = 64us ( * half of 625 lines * 50Hz)
|
robyounger |
0:fb93ebe5f84f
|
162
|
|
robyounger |
0:fb93ebe5f84f
|
163
|
// There actually seem to be a lot of variations on this, but they all seem roughly the same.
|
robyounger |
0:fb93ebe5f84f
|
164
|
|
robyounger |
0:fb93ebe5f84f
|
165
|
// Colour needs a precision ~4MHz carrier signal applied over the 'color burst' and active video
|
robyounger |
0:fb93ebe5f84f
|
166
|
// with precise phase and amplitude control (sounds like a lot of work!)
|
robyounger |
0:fb93ebe5f84f
|
167
|
|
robyounger |
0:fb93ebe5f84f
|
168
|
// So for colour, Use svideo, VGA, or use 3 of these signals to generate an RGB scart signal?
|
robyounger |
0:fb93ebe5f84f
|
169
|
|
robyounger |
0:fb93ebe5f84f
|
170
|
//The basic frame format is
|
robyounger |
0:fb93ebe5f84f
|
171
|
//1) few lines of special start pulses,
|
robyounger |
0:fb93ebe5f84f
|
172
|
//2) some off screen lines, which had things like teletext/close captions
|
robyounger |
0:fb93ebe5f84f
|
173
|
//3) the tv picture bit you see,
|
robyounger |
0:fb93ebe5f84f
|
174
|
//4) some special pulses to say end of screen, go back to the top.
|
robyounger |
0:fb93ebe5f84f
|
175
|
// Then straight back to 1 for the next frame.
|
robyounger |
0:fb93ebe5f84f
|
176
|
|
robyounger |
0:fb93ebe5f84f
|
177
|
// To get the timing right - I do this:
|
robyounger |
0:fb93ebe5f84f
|
178
|
//4) some special pulses to say end of screen, go back to the top.
|
robyounger |
0:fb93ebe5f84f
|
179
|
//1) few lines of special start pulses,
|
robyounger |
0:fb93ebe5f84f
|
180
|
//2) some off screen lines, which had things like teletext/close captions
|
robyounger |
0:fb93ebe5f84f
|
181
|
//3) the tv picture bit you see,
|
robyounger |
0:fb93ebe5f84f
|
182
|
|
robyounger |
0:fb93ebe5f84f
|
183
|
// You can get away dropping the last few lines of 3)
|
robyounger |
0:fb93ebe5f84f
|
184
|
// I use this to drop back to the main program to run as normal.
|
robyounger |
0:fb93ebe5f84f
|
185
|
|
robyounger |
0:fb93ebe5f84f
|
186
|
// Ideally you'd use the few cycles between each line to do stuff, but that's going to be hard to get timing right.
|
robyounger |
0:fb93ebe5f84f
|
187
|
|
robyounger |
0:fb93ebe5f84f
|
188
|
|
robyounger |
0:fb93ebe5f84f
|
189
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
190
|
//Start of Frame
|
robyounger |
0:fb93ebe5f84f
|
191
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
192
|
|
robyounger |
0:fb93ebe5f84f
|
193
|
//Each dac.write is ~0.5us, so multiply up to create the timings.
|
robyounger |
0:fb93ebe5f84f
|
194
|
|
robyounger |
0:fb93ebe5f84f
|
195
|
// (This is a mix of PAL and NTSC - hack as appropriate)
|
robyounger |
0:fb93ebe5f84f
|
196
|
|
robyounger |
0:fb93ebe5f84f
|
197
|
// There are 21 lines per field in a vertical blanking period
|
robyounger |
0:fb93ebe5f84f
|
198
|
// the last 4 lines of a field indicate are just before flyback
|
robyounger |
0:fb93ebe5f84f
|
199
|
// then there are 5 blank lines for flyback itself...
|
robyounger |
0:fb93ebe5f84f
|
200
|
|
robyounger |
0:fb93ebe5f84f
|
201
|
//END OF A FRAME + FLYBACK + START OF NEW FRAME signalling (9 lines)
|
robyounger |
0:fb93ebe5f84f
|
202
|
for (int i = 0; i < 6; i++) { //6 equalizing pulses (time = 6 half lines)
|
robyounger |
0:fb93ebe5f84f
|
203
|
dac.write_u16(IRE_m40); //2.4us
|
robyounger |
0:fb93ebe5f84f
|
204
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
205
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
206
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
207
|
// dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
208
|
dac.write_u16(IRE_0); //29.4us
|
robyounger |
0:fb93ebe5f84f
|
209
|
wait_us(28);
|
robyounger |
0:fb93ebe5f84f
|
210
|
}
|
robyounger |
0:fb93ebe5f84f
|
211
|
for (int i = 0; i < 6; i++) {// 6 serrated vertical pulses (time = 6 half lines)
|
robyounger |
0:fb93ebe5f84f
|
212
|
dac.write_u16(IRE_0); //2.4us
|
robyounger |
0:fb93ebe5f84f
|
213
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
214
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
215
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
216
|
// dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
217
|
dac.write_u16(IRE_m40); //29.4us
|
robyounger |
0:fb93ebe5f84f
|
218
|
wait_us(28);
|
robyounger |
0:fb93ebe5f84f
|
219
|
}
|
robyounger |
0:fb93ebe5f84f
|
220
|
for (int i = 0; i < 6; i++) { // 6 equalizing pulses (time = 6 half lines)
|
robyounger |
0:fb93ebe5f84f
|
221
|
dac.write_u16(IRE_m40); //2.4us
|
robyounger |
0:fb93ebe5f84f
|
222
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
223
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
224
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
225
|
// dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
226
|
dac.write_u16(IRE_0); //29.4us
|
robyounger |
0:fb93ebe5f84f
|
227
|
wait_us(28);
|
robyounger |
0:fb93ebe5f84f
|
228
|
}
|
robyounger |
0:fb93ebe5f84f
|
229
|
|
robyounger |
0:fb93ebe5f84f
|
230
|
|
robyounger |
0:fb93ebe5f84f
|
231
|
// The lines just above the top of the picture used for setup/teletext/closed captions etc.
|
robyounger |
0:fb93ebe5f84f
|
232
|
// about 17 lines for PAL, 12 for NTSC?
|
robyounger |
0:fb93ebe5f84f
|
233
|
for (int i = 0; i < 17; i++) {
|
robyounger |
0:fb93ebe5f84f
|
234
|
//10.9us (NTSC) or 12.5us (PAL) for horizontal blanking interval
|
robyounger |
0:fb93ebe5f84f
|
235
|
dac.write_u16(IRE_0); //Front porch 1.6us
|
robyounger |
0:fb93ebe5f84f
|
236
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
237
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
238
|
dac.write_u16(IRE_m40); //Sync Tip 4.7us
|
robyounger |
0:fb93ebe5f84f
|
239
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
240
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
241
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
242
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
243
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
244
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
245
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
246
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
247
|
// dac.write_u16(IRE_m40); //extra for PAL timing
|
robyounger |
0:fb93ebe5f84f
|
248
|
dac.write_u16(IRE_0); //Breezeway 0.5us
|
robyounger |
0:fb93ebe5f84f
|
249
|
dac.write_u16(IRE_0); //ColorBurst 2.5us
|
robyounger |
0:fb93ebe5f84f
|
250
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
251
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
252
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
253
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
254
|
dac.write_u16(IRE_m40); //Back Porch 1.6us (2.55us in PAL)
|
robyounger |
0:fb93ebe5f84f
|
255
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
256
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
257
|
dac.write_u16(IRE_m40); //extra for PAL timing
|
robyounger |
0:fb93ebe5f84f
|
258
|
dac.write_u16(IRE_m40); //extra for PAL timing
|
robyounger |
0:fb93ebe5f84f
|
259
|
|
robyounger |
0:fb93ebe5f84f
|
260
|
// for (int j = 0; j < DRAWWIDTH; j++) {
|
robyounger |
0:fb93ebe5f84f
|
261
|
// dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
262
|
// } //next pixel
|
robyounger |
0:fb93ebe5f84f
|
263
|
|
robyounger |
0:fb93ebe5f84f
|
264
|
|
robyounger |
0:fb93ebe5f84f
|
265
|
//Then that video signal for 52.6us (52 for PAL)
|
robyounger |
0:fb93ebe5f84f
|
266
|
dac.write_u16(IRE_0); //Video signal for 52.6us
|
robyounger |
0:fb93ebe5f84f
|
267
|
wait_us(51); // replaces another 104 dac.write_u16(IRE_0)
|
robyounger |
0:fb93ebe5f84f
|
268
|
}
|
robyounger |
0:fb93ebe5f84f
|
269
|
|
robyounger |
0:fb93ebe5f84f
|
270
|
|
robyounger |
0:fb93ebe5f84f
|
271
|
//Draw the actual visible lines on screen: exactly same header as previous, but followed by real video data.
|
robyounger |
0:fb93ebe5f84f
|
272
|
// intentionally dropping the last few lines to throw some time to main()
|
robyounger |
0:fb93ebe5f84f
|
273
|
// otherwise this loop would use 100% of CPU.
|
robyounger |
0:fb93ebe5f84f
|
274
|
for (int i = 0; i < LINES; i++) {
|
robyounger |
0:fb93ebe5f84f
|
275
|
//10.9us (NTSC) or 12.5us (PAL) for horizontal blanking interval
|
robyounger |
0:fb93ebe5f84f
|
276
|
dac.write_u16(IRE_0); //Front porch 1.6us
|
robyounger |
0:fb93ebe5f84f
|
277
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
278
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
279
|
dac.write_u16(IRE_m40); //Sync Tip 4.7us
|
robyounger |
0:fb93ebe5f84f
|
280
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
281
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
282
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
283
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
284
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
285
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
286
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
287
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
288
|
// dac.write_u16(IRE_m40); //extra for PAL timing
|
robyounger |
0:fb93ebe5f84f
|
289
|
dac.write_u16(IRE_0); //Breezeway 0.5us
|
robyounger |
0:fb93ebe5f84f
|
290
|
dac.write_u16(IRE_0); //ColorBurst 2.5us
|
robyounger |
0:fb93ebe5f84f
|
291
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
292
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
293
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
294
|
dac.write_u16(IRE_0);
|
robyounger |
0:fb93ebe5f84f
|
295
|
dac.write_u16(IRE_m40); //Back Porch 1.6us (2.55us in PAL)
|
robyounger |
0:fb93ebe5f84f
|
296
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
297
|
dac.write_u16(IRE_m40);
|
robyounger |
0:fb93ebe5f84f
|
298
|
dac.write_u16(IRE_m40); //extra for PAL timing
|
robyounger |
0:fb93ebe5f84f
|
299
|
dac.write_u16(IRE_m40); //extra for PAL timing
|
robyounger |
0:fb93ebe5f84f
|
300
|
|
robyounger |
0:fb93ebe5f84f
|
301
|
//Then that video signal for 52.6us (52 for PAL):
|
robyounger |
0:fb93ebe5f84f
|
302
|
|
robyounger |
0:fb93ebe5f84f
|
303
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
304
|
//Write out the video data
|
robyounger |
0:fb93ebe5f84f
|
305
|
//(very timing sensitive as must last ~53us, no more or less)
|
robyounger |
0:fb93ebe5f84f
|
306
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
307
|
|
robyounger |
0:fb93ebe5f84f
|
308
|
// Examples:
|
robyounger |
0:fb93ebe5f84f
|
309
|
|
robyounger |
0:fb93ebe5f84f
|
310
|
//1) draw random shade per line
|
robyounger |
0:fb93ebe5f84f
|
311
|
// dac.write_u16(rand() % 40000 + IRE_7p5); //Video signal for 52.6us
|
robyounger |
0:fb93ebe5f84f
|
312
|
// wait_us(52);
|
robyounger |
0:fb93ebe5f84f
|
313
|
|
robyounger |
0:fb93ebe5f84f
|
314
|
//2) draw black
|
robyounger |
0:fb93ebe5f84f
|
315
|
// dac.write_u16(IRE_7p5);
|
robyounger |
0:fb93ebe5f84f
|
316
|
// wait_us(51);
|
robyounger |
0:fb93ebe5f84f
|
317
|
|
robyounger |
0:fb93ebe5f84f
|
318
|
//3) draw white
|
robyounger |
0:fb93ebe5f84f
|
319
|
// dac.write_u16(IRE_100);
|
robyounger |
0:fb93ebe5f84f
|
320
|
// wait_us(51);
|
robyounger |
0:fb93ebe5f84f
|
321
|
|
robyounger |
0:fb93ebe5f84f
|
322
|
//4) draw framebuffer
|
robyounger |
0:fb93ebe5f84f
|
323
|
|
robyounger |
0:fb93ebe5f84f
|
324
|
// Code here is very timing critical.
|
robyounger |
0:fb93ebe5f84f
|
325
|
|
robyounger |
0:fb93ebe5f84f
|
326
|
// loop count is instruction dependent, if you add some code here, it will need be a different width
|
robyounger |
0:fb93ebe5f84f
|
327
|
// We have ~52.5us and a a dac write takes 0.5us so 104/105px seems correct.
|
robyounger |
0:fb93ebe5f84f
|
328
|
// Trial+error shows ~100-110 pixels to be OKish on a particular old TV.
|
robyounger |
0:fb93ebe5f84f
|
329
|
|
robyounger |
0:fb93ebe5f84f
|
330
|
int k =(i/ SCAN ); //double scan the framebuffer, particularly convenient for 128 vertical px but 256 line resolution..
|
robyounger |
0:fb93ebe5f84f
|
331
|
|
robyounger |
0:fb93ebe5f84f
|
332
|
// The modulo is only needed if screen output size is bigger than framebuffer (wrapping occurs).
|
robyounger |
0:fb93ebe5f84f
|
333
|
// Stick to powers of 2 for modulo wrapping (or likely too slow).
|
robyounger |
0:fb93ebe5f84f
|
334
|
for (int j = 0; j < DRAWWIDTH; j++) {
|
robyounger |
0:fb93ebe5f84f
|
335
|
dac.write_u16( framebuffer[k%128][j%128] ); //modulo used to wrap framebuffer. Keep to power of 2 =< framebuffer sizes.
|
robyounger |
0:fb93ebe5f84f
|
336
|
} //next pixel
|
robyounger |
0:fb93ebe5f84f
|
337
|
|
robyounger |
0:fb93ebe5f84f
|
338
|
} //next line loop
|
robyounger |
0:fb93ebe5f84f
|
339
|
|
robyounger |
0:fb93ebe5f84f
|
340
|
//Default back to black when we don't bother drawing the last few lines of a frame!
|
robyounger |
0:fb93ebe5f84f
|
341
|
dac.write_u16(IRE_7p5);
|
robyounger |
0:fb93ebe5f84f
|
342
|
} //End of createframe routine
|
robyounger |
0:fb93ebe5f84f
|
343
|
|
robyounger |
0:fb93ebe5f84f
|
344
|
|
robyounger |
0:fb93ebe5f84f
|
345
|
|
robyounger |
0:fb93ebe5f84f
|
346
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
347
|
// randomfill the framebuffer //
|
robyounger |
0:fb93ebe5f84f
|
348
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
349
|
void randomfill () {
|
robyounger |
0:fb93ebe5f84f
|
350
|
for (int j = 0; j < HEIGHT; j++) {
|
robyounger |
0:fb93ebe5f84f
|
351
|
for (int i = 0; i < WIDTH; i++) {
|
robyounger |
0:fb93ebe5f84f
|
352
|
framebuffer[j][i] = rand();
|
robyounger |
0:fb93ebe5f84f
|
353
|
if (framebuffer[j][i] < IRE_7p5 ) {
|
robyounger |
0:fb93ebe5f84f
|
354
|
framebuffer[j][i] = IRE_7p5;
|
robyounger |
0:fb93ebe5f84f
|
355
|
}
|
robyounger |
0:fb93ebe5f84f
|
356
|
}
|
robyounger |
0:fb93ebe5f84f
|
357
|
}
|
robyounger |
0:fb93ebe5f84f
|
358
|
}
|
robyounger |
0:fb93ebe5f84f
|
359
|
|
robyounger |
0:fb93ebe5f84f
|
360
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
361
|
// blank the framebuffer //
|
robyounger |
0:fb93ebe5f84f
|
362
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
363
|
void blankfill () {
|
robyounger |
0:fb93ebe5f84f
|
364
|
for (int j = 0; j < HEIGHT; j++) {
|
robyounger |
0:fb93ebe5f84f
|
365
|
for (int i = 0; i < WIDTH; i++) {
|
robyounger |
0:fb93ebe5f84f
|
366
|
framebuffer[j][i] = IRE_7p5;
|
robyounger |
0:fb93ebe5f84f
|
367
|
//framebuffer[j][i] = IRE_100;
|
robyounger |
0:fb93ebe5f84f
|
368
|
}
|
robyounger |
0:fb93ebe5f84f
|
369
|
}
|
robyounger |
0:fb93ebe5f84f
|
370
|
}
|
robyounger |
0:fb93ebe5f84f
|
371
|
|
robyounger |
0:fb93ebe5f84f
|
372
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
373
|
// zooming mandelbot fractal in the framebuffer //
|
robyounger |
0:fb93ebe5f84f
|
374
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
375
|
|
robyounger |
0:fb93ebe5f84f
|
376
|
void mandelbrot () {
|
robyounger |
0:fb93ebe5f84f
|
377
|
//Mandelbrot escape time algorithm (doubles+iteration=slow)
|
robyounger |
0:fb93ebe5f84f
|
378
|
//Taken from wikipedia pseudocode,
|
robyounger |
0:fb93ebe5f84f
|
379
|
//tweaked by using the speeded up version that google found on geocities
|
robyounger |
0:fb93ebe5f84f
|
380
|
//(oops - Geocities has shut down in the 3 weeks since I wrote this! first time I've used it in years!)
|
robyounger |
0:fb93ebe5f84f
|
381
|
//http://www.geocities.com/CapeCanaveral/5003/Mandel.txt
|
robyounger |
0:fb93ebe5f84f
|
382
|
//then put in a loop to zoom in on a intersting co-ords point i saw elsewhere...
|
robyounger |
0:fb93ebe5f84f
|
383
|
double zoom;
|
robyounger |
0:fb93ebe5f84f
|
384
|
for (int z = 0; z < 200; z++) {//2^50 is quite a lot of zoom - thats why you need precision!
|
robyounger |
0:fb93ebe5f84f
|
385
|
zoom= pow((double)1.2,z);
|
robyounger |
0:fb93ebe5f84f
|
386
|
|
robyounger |
0:fb93ebe5f84f
|
387
|
led1=0;
|
robyounger |
0:fb93ebe5f84f
|
388
|
led2=0;
|
robyounger |
0:fb93ebe5f84f
|
389
|
led3=0;
|
robyounger |
0:fb93ebe5f84f
|
390
|
led4=0;
|
robyounger |
0:fb93ebe5f84f
|
391
|
|
robyounger |
0:fb93ebe5f84f
|
392
|
double x,y;
|
robyounger |
0:fb93ebe5f84f
|
393
|
double x0,y0;
|
robyounger |
0:fb93ebe5f84f
|
394
|
// double xtemp;
|
robyounger |
0:fb93ebe5f84f
|
395
|
double xsq;
|
robyounger |
0:fb93ebe5f84f
|
396
|
double ysq;
|
robyounger |
0:fb93ebe5f84f
|
397
|
|
robyounger |
0:fb93ebe5f84f
|
398
|
unsigned short int iteration = 0;
|
robyounger |
0:fb93ebe5f84f
|
399
|
unsigned short int max_iteration = (z*2)+20; //arbitrary scaling so there are more interation allowed as you zoom
|
robyounger |
0:fb93ebe5f84f
|
400
|
for (int j = 0; j < HEIGHT; j++) {
|
robyounger |
0:fb93ebe5f84f
|
401
|
//little status hack as as drawing fractals (particularly with doubles on only 10% of a cpu is slow!)
|
robyounger |
0:fb93ebe5f84f
|
402
|
if (j== (( HEIGHT /4)-1)) {
|
robyounger |
0:fb93ebe5f84f
|
403
|
led1=1;
|
robyounger |
0:fb93ebe5f84f
|
404
|
} else if (j==(( HEIGHT /2)-1)) {
|
robyounger |
0:fb93ebe5f84f
|
405
|
led2=1;
|
robyounger |
0:fb93ebe5f84f
|
406
|
} else if (j==(3*( HEIGHT /4)-1)) {
|
robyounger |
0:fb93ebe5f84f
|
407
|
led3=1;
|
robyounger |
0:fb93ebe5f84f
|
408
|
} else if (j==( HEIGHT -1)) {
|
robyounger |
0:fb93ebe5f84f
|
409
|
led4=1;
|
robyounger |
0:fb93ebe5f84f
|
410
|
}
|
robyounger |
0:fb93ebe5f84f
|
411
|
//end of little status hack
|
robyounger |
0:fb93ebe5f84f
|
412
|
|
robyounger |
0:fb93ebe5f84f
|
413
|
|
robyounger |
0:fb93ebe5f84f
|
414
|
for (int i = 0; i < WIDTH; i++) {
|
robyounger |
0:fb93ebe5f84f
|
415
|
// x0=(((float) i) -32.0)/32.0;//redefine 0to63 as -1to+1 mandelbrot window
|
robyounger |
0:fb93ebe5f84f
|
416
|
// y0=(((float) j) -32.0)/32.0;//redefine 0to63 as -1to+1 mandelbrot window
|
robyounger |
0:fb93ebe5f84f
|
417
|
//-1.865725138512217656771 moves center point to something interesting
|
robyounger |
0:fb93ebe5f84f
|
418
|
x0=((((double) i) - ( WIDTH /2)) /zoom)-1.865725138512217656771;//redefine 0to63 as -1to+1 mandelbrot window
|
robyounger |
0:fb93ebe5f84f
|
419
|
y0=(((double) j) - ( HEIGHT /2)) /zoom;//redefine 0to63 as -1to+1 mandelbrot window
|
robyounger |
0:fb93ebe5f84f
|
420
|
iteration = 0;
|
robyounger |
0:fb93ebe5f84f
|
421
|
|
robyounger |
0:fb93ebe5f84f
|
422
|
//Standard version of mandelbrot loop based on wikipedia pseudocode
|
robyounger |
0:fb93ebe5f84f
|
423
|
// x=0;
|
robyounger |
0:fb93ebe5f84f
|
424
|
// y=0;
|
robyounger |
0:fb93ebe5f84f
|
425
|
// while ( ((x*x + y*y) <= (2*2)) && (iteration < max_iteration) ) {
|
robyounger |
0:fb93ebe5f84f
|
426
|
// xtemp = x*x - y*y + x0;
|
robyounger |
0:fb93ebe5f84f
|
427
|
// y = 2*x*y + y0;
|
robyounger |
0:fb93ebe5f84f
|
428
|
// x = xtemp;
|
robyounger |
0:fb93ebe5f84f
|
429
|
// iteration++;
|
robyounger |
0:fb93ebe5f84f
|
430
|
// }
|
robyounger |
0:fb93ebe5f84f
|
431
|
|
robyounger |
0:fb93ebe5f84f
|
432
|
//Speedy version of main mandelbrot loop (algorithm from geocities page)
|
robyounger |
0:fb93ebe5f84f
|
433
|
x=x0+x0*x0-y0*y0;
|
robyounger |
0:fb93ebe5f84f
|
434
|
y=y0+x0*y0+x0*y0;
|
robyounger |
0:fb93ebe5f84f
|
435
|
for (iteration=0;iteration<max_iteration && (ysq=y*y)+(xsq=x*x)<4;iteration++,y=y0+x*y+x*y,x=x0-ysq+xsq) ;
|
robyounger |
0:fb93ebe5f84f
|
436
|
|
robyounger |
0:fb93ebe5f84f
|
437
|
//Iteration count determines color (clamp max iteration to zero, and normalize for black to white)
|
robyounger |
0:fb93ebe5f84f
|
438
|
framebuffer[j][i] = (( iteration == max_iteration ) ? (IRE_7p5) : (IRE_7p5 + ((iteration%20)*2000)) );
|
robyounger |
0:fb93ebe5f84f
|
439
|
}
|
robyounger |
0:fb93ebe5f84f
|
440
|
}
|
robyounger |
0:fb93ebe5f84f
|
441
|
}//zoom loop
|
robyounger |
0:fb93ebe5f84f
|
442
|
}
|
robyounger |
0:fb93ebe5f84f
|
443
|
|
robyounger |
0:fb93ebe5f84f
|
444
|
|
robyounger |
0:fb93ebe5f84f
|
445
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
446
|
// main() showing use framebuffer //
|
robyounger |
0:fb93ebe5f84f
|
447
|
// Puts a grayscale pic in it //
|
robyounger |
0:fb93ebe5f84f
|
448
|
////////////////////////////////////////////////////////////
|
robyounger |
0:fb93ebe5f84f
|
449
|
|
robyounger |
0:fb93ebe5f84f
|
450
|
int main() {
|
robyounger |
0:fb93ebe5f84f
|
451
|
|
robyounger |
0:fb93ebe5f84f
|
452
|
blankfill(); //set framebuffer to blank values
|
robyounger |
0:fb93ebe5f84f
|
453
|
|
robyounger |
0:fb93ebe5f84f
|
454
|
timer.attach_us(&createframe,20000);//attach the display (at 50Hz)
|
robyounger |
0:fb93ebe5f84f
|
455
|
|
robyounger |
0:fb93ebe5f84f
|
456
|
// int attached=0; //attach frame
|
robyounger |
0:fb93ebe5f84f
|
457
|
// //If you had a lot of setup in a main game loop, you could do something like this:
|
robyounger |
0:fb93ebe5f84f
|
458
|
// if (attached==0) {
|
robyounger |
0:fb93ebe5f84f
|
459
|
// timer.attach_us(&createframe,20000);
|
robyounger |
0:fb93ebe5f84f
|
460
|
// attached=1;
|
robyounger |
0:fb93ebe5f84f
|
461
|
// }
|
robyounger |
0:fb93ebe5f84f
|
462
|
|
robyounger |
0:fb93ebe5f84f
|
463
|
//Program loop
|
robyounger |
0:fb93ebe5f84f
|
464
|
while (1) {
|
robyounger |
0:fb93ebe5f84f
|
465
|
// Add you own demo code here. Expect it to get regularly interupted by the screen draw call!
|
robyounger |
0:fb93ebe5f84f
|
466
|
// very simple code can run at full fps.
|
robyounger |
0:fb93ebe5f84f
|
467
|
//Example - change HEIGHT to 64 and SCAN to 4 and use randomfill instead of mandelbrot...
|
robyounger |
0:fb93ebe5f84f
|
468
|
|
robyounger |
0:fb93ebe5f84f
|
469
|
//randomfill(); //random pixel fill
|
robyounger |
0:fb93ebe5f84f
|
470
|
mandelbrot(); //mandelbrot procedure is a 200 loop zoom so takes ages - and each scene redraw takes a few seconds!
|
robyounger |
0:fb93ebe5f84f
|
471
|
} //while
|
robyounger |
0:fb93ebe5f84f
|
472
|
|
robyounger |
0:fb93ebe5f84f
|
473
|
} //main
|
robyounger |
0:fb93ebe5f84f
|
474
|
|
robyounger |
0:fb93ebe5f84f
|
475
|
|