/*
 * Copyright(C) Paul und Scherer (mct.de/mct.net)
 *
 * This example demonstrates how to...
 *
 *  ... build a TV scope using the ADC+DAC+SSP.
 *
 * Note: Compile with "-O1" for correct timing!
 */

#include <stdio.h>
#include <conio.h>
#include <string.h>
#include <sys/arm7tdmi.h>
#include <target.h>
#include "tv_f8x9.h"

/*
 * CVBS signal levels
 */
#define SYNC	  0				// sync
#define BLACK	200				// black

#define TX	 24				// # of text    cols
#define TY	 24				//              rows
#define FY	  9				//      font    rows
#define GX	 20				//      graphic cols (*16)
#define GY	160				//              rows
#define PX	(GX*16)				//      pixel   cols
#define SP	(PX*64)				//      sample  points

#define WINDOW	(line >= 102 && line < 102+GY)	// inside graphic window

extern char _bdata;				// data start
#define FIQ_VEC	(*((long *)&_bdata+15))		// FIQ vector

static volatile int si = -1;			// sample index

static unsigned char tbuf[TY][TX];		// buffer for text
static short	     gbuf[GY][GX];		//            graphic
static char	     sbuf[SP];			//            sample
static int sw	 =  1;				// show window
static int sg	 =  1;				// show grid
static int tbase = 64;				// timebase
static int level =  1;				// trigger level
static int slow	 =  1;				//         mode
static int high	 =  1;				//         edge
static int shift;				// sample x pos

/*
 * ADC0 interrupt service (FIQ)
 *
 * Store ADC result in the sample buffer. While
 * sampling is active, the sample index is >= 0
 * and the ADC interrupt enabled. When sampling
 * done, the ADC interrupt is disabled, and the
 * sample index set to -1, which indicates that
 * the ADC is idle.
 */
static void __attribute__((interrupt))		// handle as ISR!
adc0_isr(void)
{
	sbuf[si++] = (Intern_ad0dr>>9)&0x7f;	// store (clears int flag)
	Intern_vicvectaddr = 0;			// reset VIC priority logic

	if (si >= SP) si = -1, Intern_vicintenclr = 0x40000;
}

/*
 * Delay loop
 */
static void
wait(int n)
{
	while (n--) ;
}

/*
 * Timer0 interrupt service
 *
 * The CVBS requires an extremely exact timing.
 * Varying interrupt latencies become visible -
 * as line jitter.
 *
 * If the main program "manages" to enter sleep
 * mode BEFORE an interrupt occurs, this can be
 * avoided (when interrupted in sleep mode, the
 * latency is constant, so the screen lines are
 * exactly vertically aligned).
 *
 * In addition, when the ADC FIQ is active, the
 * timing is completely "spoiled". The only way
 * to keep the video signal from being severely
 * distorted, is to hold it at the black level.
 * So, the screen is blanked during sampling.
 */
static void __attribute__((interrupt))		// handle as ISR!
t0_isr(void)
{
	static int line;			// line counter
	static int ty, fy, gy;			// row counters (text/font/graphic)
	static int hb;				// hold blanking

	int i;

	Intern_dacr = SYNC<<6;			// start of sync
	Intern_t0ir = 1;			// clear int flag
	Intern_vicvectaddr = 0;			// reset VIC priority logic

	/*
	 * Reset the counters and the hold
	 * flag when 312-lines field done.
	 */
	if (line > 311) line = fy = ty = gy = hb = 0;

	if (++line < 4) return;			// vertical   sync pulse
	wait(40);				// horizontal sync pulse
	Intern_dacr = BLACK<<6;			// black level reference

	/*
	 * A positive sample index indicates that
	 * sampling is active. The screen must go
	 * black immediately to avoid a distorted
	 * display. The hold flag is set to blank
	 * the remaining lines of this field.
	 */
	if (si >= 0) hb = 1;

	if (hb					// blank screen
	 || line <  56				// top and
	 || line > 271) return;			// bottom border

	/*
	 * The SSP speed and data size differ for text and graphic
	 * (text is slower, 8 bits, and graphic is fast, 16 bits).
	 *
	 * When the current screen line lies within the boundaries
	 * of the graphic window, AND if the "show window" flag is
	 * set, write SSP values for graphic, else those for text.
	 */
	Intern_sspcr0 = WINDOW && sw? 0x21f	// /3, SSI, 16 bits
				    : 0x417;	// /5, SSI,  8 bits

	wait(125);				// left border

	/*
	 * Write pixels.
	 *
	 * Text line pixels are obtained from the font array. The indices
	 * for this array are computed from the characters in the current
	 * text row and the current font row. The graphic line pixels are
	 * taken from the graphic buffer directly.
	 *
	 * Writing to the SSP when its FIFO is "not full", keeps the FIFO
	 * filled so that output at MOSI1 is back-to-back, resulting in a
	 * continuous bitstream.
	 *
	 * Graphic is written, when inside the graphic window, unless the
	 * "show window" flag is false, making the hidden underlying text
	 * to be displayed.
	 */
	if (WINDOW
	 && sw) for (i = 0; i < GX;) {
		while (!(Intern_sspsr&2)) ;
		Intern_sspdr = gbuf[gy][i++];
	} else	for (i = 0; i < TX;) {
		while (!(Intern_sspsr&2)) ;
		Intern_sspdr = f8x9[tbuf[ty][i++]*FY+fy];
	}

	while (!(Intern_sspsr&2)) ;		// right
	Intern_sspdr = 0;			//  border
	if (WINDOW) gy++;			// next graphic row
	if (++fy >= FY) fy = 0, ty++;		// first/next font/text row
}

/*
 * Write s @ position x, y.
 */
static void
putsxy(unsigned x, unsigned y, unsigned char *s)
{
	if (y >= TY) return;
	for (; x < TX && *s; s++) if (*s >= ' ' && *s < 0xd0) tbuf[y][x++] = *s-' ';
}

/*
 * Screen decoration
 *
 * Print settings. Draw grid, if
 * grid is non-zero, else remove
 * grid.
 */
static void
deco(int grid)
{
	static char buf[10];

	int x, y;

	/*
	 * Print settings.
	 *
	 * X-scale calibration:
	 *
	 * The ADC is clocked with
	 * f =60MHZ/14 =4.28MHz and uses 11 clocks/conv.
	 * T =1/f      =0.23us, *11 =2.57us. The graphic
	 * window has 320 pixels/row, and 32 pixels/div.
	 *
	 * With 2.57us/pixel follows: 82us/div.
	 *
	 * Although this is an odd value for a scope, it
	 * is exact (enough), and represents the fastest
	 * possible sampling rate.
	 */
	sprintf(buf, "%4d", 82*tbase), putsxy(12, 1, buf);
	sprintf(buf, "%d" ,    level), putsxy(15, 2, buf);
	putsxy(15, 3, high? "+": "-");
	putsxy(12, 4, slow? "SLOW": "FAST");

	/*
	 * Draw/remove grid.
	 */
	for (y = 0; y < GY; y++) for (x = 0; x < GX; x++) {
		gbuf[y][x] = 0;
		if (grid) {
			if ((!(x&0x1) && !(y&0x3)) ||
			   (x == GX/2 && !(y&0x1))) gbuf[y][x] |= 0x8000;
			if ( !(y&0xf))		    gbuf[y][x] |= 0x8888;
			if (y == GY-GY/10)	    gbuf[y][x] |= 0xaaaa;
		}

		/*
		 * Always draw window borders and the trigger
		 * position (highlighted area in left border).
		 */
		if (!y
		  || y == GY-1)	gbuf[y][x] |= 0xffff;
		if ( x == GX-1)	gbuf[y][x] |= 1;
		if (!x)		gbuf[y][x] |= 9-y/16 == level? 0xc000
							     : 0x8000;
	}
}

/*
 * Update screen.
 */
static void
update(void)
{
	int i, x;
	unsigned m = 0x8000;

	/*
	 * Check and correct settings.
	 */
	if (shift <	0) shift =     0; else
	if (shift > SP-PX) shift = SP-PX;
	if (tbase <	1) tbase =     1; else
	if (tbase >    64) tbase =    64;
	if (level <	1) level =     1; else
	if (level >	8) level =     8;
	deco(sg);

	/*
	 * Select the data to be displayed
	 * depending on the current shift.
	 */
	i = shift;

	/*
	 * Set corresponding bits in the graphic buffer.
	 * Depending on the current timebase, skip each
	 * second, fourth, eighth, ... sample point.
	 */
	for (x = 0; x < GX;) {
		gbuf[GY-sbuf[i]-GY/10][x] |= m;
		if ((i += tbase) >= SP) break;
		if (!(m >>= 1)) m = 0x8000, x++;
	}
}

/*
 * Record sample.
 *
 * To get correct, time-related sample data,
 * recording puts the ADC in interrupt mode.
 * By using FIQ, the ADC interrupt is always
 * serviced, even from within the video ISR.
 * Resulting interferences would be visible,
 * so the screen is blanked.
 *
 * While waiting for trigger the input level
 * is continuously compared with the trigger
 * level, polling the ADC and entering sleep
 * mode in turn, causing the trigger loop to
 * be ruled by a 64us raster.
 *
 * If the trigger mode is "fast", sleep mode
 * is omitted and the screen blanked, giving
 * the trigger loop a lot more time - at the
 * cost of a black screen.
 */
static void
sample(void)
{
	long l;
	int t = 128*level-64;			// trigger level

	if (!slow) si = 0;			// blank screen in fast mode

	/*
	 * For positive (negative) trigger edges wait until the signal level falls
	 * below (rises above) the trigger level minus (plus) an hysteresis value.
	 * After that wait for the trigger level to be crossed.
	 */
	putsxy(0, 23, "waiting for trigger...");
	if (high) {
		while ((l = Intern_ad0dr) >= 0 || ((l>>6)&0x3ff) > t-60) Intern_pcon = slow;
		while ((l = Intern_ad0dr) >= 0 || ((l>>6)&0x3ff) < t   ) Intern_pcon = slow;
	} else {
		while ((l = Intern_ad0dr) >= 0 || ((l>>6)&0x3ff) < t+60) Intern_pcon = slow;
		while ((l = Intern_ad0dr) >= 0 || ((l>>6)&0x3ff) > t   ) Intern_pcon = slow;
	}

	si = 0;					// start of sample, blank screen
	Intern_vicintenable = 0x40000;		// enable ADC0 int
	while (si >= 0) ;			// wait for sampling done
	memset(tbuf+TY-1, 0, TX);		// remove message
	update();				// show new sample
}

/*
 * Usage
 */
static void
usage(void)
{
	puts("\n"
	     "LPC2138 TV - SCOPE\n"
	     "==================\n"
	     "Space : sample\n"
	     " </>  : shift\n"
	     " -/+  : fine shift\n"
	     " x/X  : -/+ timebase\n"
	     " t/T  : -/+ trigger\n"
	     " e    : select edge\n"
	     " m    :        mode\n"
	     " g    : grid off/on\n"
	     " d    : default settings\n"
	);
}

/*
 * How it works:
 *
 * A TV video signal ("Composite Video Blanking and Sync" =CVBS)
 * has the following (simplified) form (PAL norm, no color):
 *
 * 100% (white) ca. 1V       ___________________________
 *                          |                           |
 *                          |       PICTURE LINE        |
 *  30% (black)        _____|___________________________|     ..
 *   0% (sync )  |____|                                 |____|
 *
 *         5us ->|    |<-
 *        12us ->|          |<-
 *        64us ->|                                      |<-
 *
 * This represents one TV screen line. One full picture has 625
 * lines, written interlaced in two fields of 312.5 lines each.
 * A vertical sync signal (composed of lots of small pulses, in
 * the first few lines of a field) indicates the beginning of a
 * new field and defines its starting position on the screen.
 *
 * Here, a much simpler form is used: Non-interlaced mode (i.e.
 * the same field, written twice to the same position). To keep
 * timing as close as possible, 312 lines/field are used (which
 * results in a slightly higher vertical frequency). The signal
 * form for the vertical sync simplifies to a long low pulse in
 * the first three lines of each field.
 *
 * Three levels (at least) are required to generate a black and
 * white picture: "sync", "black" and something between "black"
 * and "white". There are tricky ways to generate such a signal
 * using the SSP for pixel output and a PWM signal for syncing.
 * Combining these signals via resistors then forms the CVBS.
 *
 * This example uses the DAC for the "sync" and "black" levels,
 * and the SSP for pixel output, reducing the required external
 * "hardware" to a single diode connected between the SSP MOSI1
 * and the Aout pin:
 *                         |\ |
 * SSP MOSI1 (=P0.19) _____| \|_____ DAC output (=Aout =P0.25)
 *                         | /|
 *                         |/ |
 *
 * The CVBS is created by a simple ISR, which gets called every
 * 64us. The DAC then sets only the levels for a BLACK line and
 * holds the first three lines of each field at the sync level.
 * The WHITE pixels come from the SSP MOSI1, simply pulling the
 * DAC output high.
 *
 * The video signal is used to display text and graphics on the
 * TV screen - turning it into a "TV - scope". On keypress, the
 * AD0.0 input is sampled, and the result shown in a window.
 *
 * Connect the DAC output (=Aout =P0.25) to a TV's video input,
 * either via cinch, or SCART (pin 20) and select "AV" as video
 * source. Then apply a (low-frequent) signal between 0V and 3V
 * to AD0.0 (=P0.27). Hit the space bar...
 */
int
main(void)
{
	FIQ_VEC		    = (long)adc0_isr;	// set ADC0 ISR addr
	Intern_vicvectaddr0 = (long)  t0_isr;	// set   t0 ISR addr
	Intern_vicintselect = 0x40000;		// select ADC0 FIQ
	Intern_vicintenable = 0x10;		// enable t0 int
	Intern_vicvectcntl0 = 0x24;		//  vector

	Intern_t0mr0  = _PCLK/1000000*64-1;	// set MR0 to 64us
	Intern_t0mcr |= 3;			// int on MR0, reset
	Intern_t0tcr  = 1;			// go...

	Intern_ad0cr = 0x210d01;		// enable AD0.0, BURST on

	Intern_sspcpsr  =			// /2
	Intern_sspcr1   = 2;			// enable SSP
	Intern_pinsel1 |= 0x480080;		//        AD0.0, DAC, MOSI1 pins

	putsxy(0,  0, "Y-SCALE      375 mV/div.");
	putsxy(0,  1, "X-SCALE     xxxx us/div.");
	putsxy(0,  2, "Trigger        x    div.");
	putsxy(0,  3, "Edge");
	putsxy(0,  4, "Mode");
	putsxy(0, 10, "       TV - SCOPE");
	putsxy(0, 12, "    Copyright(C) MCT");
	putsxy(0, 14, "www.mct.de   www.mct.net");
	deco(sg);
	usage();

	ENABLE_INTERRUPTS;			// enable core ints

	while (1) {
		int c;

		/*
		 * Command interface
		 */
		if (kbhit()) switch (sw = 1, c = getchar()) {
		default	: sw = 0;			 usage (); break; // usage
		case ' ':				 sample(); break; // sample
		case '<': shift += PX;			 update(); break; // shift  +320
		case '>': shift -= PX;			 update(); break; //        -320
		case '-': shift += PX/10;		 update(); break; // shift  + 32
		case '+': shift -= PX/10;		 update(); break; //        - 32
		case 'x': tbase /= 2;			 update(); break; // timebase /2
		case 'X': tbase *= 2;			 update(); break; //          *2
		case 't': level--;			 update(); break; // trigger  -1
		case 'T': level++;			 update(); break; //          +1
		case 'e': high = !high, level = 9-level; update(); break; // toggle edge
		case 'm': slow = !slow;			 update(); break; //        mode
		case 'g': sg = !sg;			 update(); break; //        grid
		case 'd': sg = high = level = 1			 ;	  // set
			  tbase = 64, shift = 0;	 update(); break; //  defaults
		}
		Intern_pcon = 1;		// go to sleep...
	}
}
