/*
 * Copyright(C) Paul und Scherer (mct.de/mct.net)
 *
 * This example demonstrates how to...
 *
 *  ... build a TV terminal using the 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"

/*
 * Comment the following
 * line to skip the demo.
 */
#define DEMO

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

#define TX	40				// # of text cols
#define TY	24				//           rows
#define FY	 9				//      font rows

#define sleep(s)	delay((s)*15625)	// sleep s seconds

static unsigned char tbuf[TY+1][TX];		// text buffer
static int blank;				// blank screen
static int rv;					// reverse video
static int dot;					// dot-style
static int zoom;				// zoom
static int zb;					//  bottom half

/*
 * 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).
 */
static void __attribute__((interrupt))		// handle as ISR!
t0_isr(void)
{
	static int line;			// line counter
	static int ty, fy;			// row counters (text/font)
	static int sz, sb;			// sync zoom and 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, when 312-lines field is done. If
	 * zooming the bottom half of the screen, the row count
	 * for text starts in the middle of the text buffer.
	 *
	 * To avoid mid-field zoom/blanking, sync zoom/blanking
	 * with beginning of field.
	 */
	if (line > 311) line = fy = 0, ty = zb? TY/2: 0, sz = zoom, sb = blank;

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

	if (sb					// blank screen
	 || line <  56				// top and
	 || line > 271				// bottom border
	 || (sz && dot && line&1)) return;	// dot-style

	wait(86);				// left border

	/*
	 * Write pixels.
	 *
	 * The pixels for each line 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.
	 *
	 * Reverse video takes the complement of the pixel bits.
	 *
	 * 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.
	 */
	if (rv) for (i = 0; i < TX;) {
		while (!(Intern_sspsr&2)) ;
		Intern_sspdr = ~f8x9[tbuf[ty][i++]*FY+fy];
	} 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 (sz && line&1) return;		// zoom writes lines twice
	if (++fy >= FY) fy = 0, ty++;		// first/next font/text row
}

/*
 * Sleep n times 64us.
 * Return on keypress.
 */
static void
delay(long n)
{
	while (n--) {
		Intern_pcon = 1;
		if (kbhit()) {
			getch();
			return;
		}
	}
}

/*
 * Write s to terminal.
 *
 * Sleep n times 64us after
 * each char (except space).
 */
static void
terminal(unsigned char *s, int n)
{
	static int tx;				// current x pos

	int c;

	for (; *s; s++) {
		switch (c = *s) {
		case 'B'-'@':
			blank = !blank;
			continue;

		case 'C'-'@':
			memset(tbuf, 0, sizeof(tbuf));
			tx = 0;
			continue;

		case 'R'-'@':
			rv = !rv;
			continue;

		case 'Z'-'@':
			if (zoom != zb)	 zb = 1; else
			if (zoom) zoom = zb = 0; else
			zoom = 1;
			continue;

		case 'D'-'@':
			if (zoom) dot = !dot;
			continue;

		case '\b':
			if (tx) tbuf[TY-1][--tx] = 0;
			continue;

		case '\t':
			c = ' ';
		default:
			if (c < ' '
			 || c > 0xcf) continue;
			tbuf[TY-1][tx++] = c-' ';
			if (tx < TX) break;
		case '\r':
		case '\n':
			tx = 0;
			memmove(tbuf, tbuf+1, TX*TY);
			continue;
		}
		if (c > ' ') delay(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". Thera 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 pseudo graphics
 * on a "TV - terminal".
 *
 * 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.
 */
int
main(void)
{
	Intern_vicvectaddr0 = (long)t0_isr;	// set ISR addr
	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_sspcpsr	= 2;			// /2
	Intern_sspcr0	= 0x317;		// /4, SSI, 8 bits
	Intern_sspcr1	= 2;			// enable SSP
	Intern_pinsel1 |= 0x80080;		//        DAC, MOSI1 pins

	puts("\n"
	     "LPC2138 TV - TERMINAL\n"
	     "=====================\n"
	     " ^B : blank screen\n"
	     " ^C : clear screen\n"
	     " ^R : reverse video\n"
	     " ^Z : zoom\n"
	     " ^D : dot-style\n"
	     "     (only if zoom)\n\n"
	     "Enter text to display..."
	);

	ENABLE_INTERRUPTS;			// enable core ints

	while (1) {

	#ifdef DEMO
		#include "tv_demo.h"
	#else
		static char buf[2];

		if (kbhit()) *buf = getch(), terminal(buf, 0);

		Intern_pcon = 1;		// go to sleep...
	#endif
	}
}
