// ======================================================================
// IR receiver and LCD controller with USB interface.
//
// Read IR mark/space patterns from a TSOP1738 on the ICP input pin.
// Mark and space periods are measured in units of 12Mhz/1024, which is
// 85.33 us. Up to 35 periods are stored in a buffer, where they can
// be read via a USB request. The buffer layout and USB requests are
// compatible with the "IgorPlug-USB" device, which is supported by LIRC
// through the "igorplugusb" driver. This implementation should also
// work with that driver. If IgorPlug-USB compatibility is not required,
// the resolution could be improved by reducing SCALING to 9, and you
// could also get rid of the additional endpoint (USBTINY_ENDPOINT).
//
// There are some IR protocols with very long packets length, such as
// the "NEC" protocol (http://www.xs4all.nl/~sbp/knowledge/ir/nec.htm).
// This protocol uses 67 mark/space periods, which is too much for the
// limited amount of RAM in the ATtiny2313. Simply truncating the
// packet to the amount of available buffer space is not an option,
// because the significant bits are at the end of the packet. Instead,
// a buffer of 35 bytes (IR_MAX) is used, of which the last 16 bytes
// (IR_WRAP) are used as a circular buffer. Because both the address
// (16 periods) and the command code (16 periods) are sent twice (normal
// and inverted), the wrapping will result in a buffer with both the
// device address and the inverted command code. Other IR protocols are
// typically shorter than 35 periods.
//
// 16-bit Timer1 is initialized in normal mode with a clock of 12MHz/8.
// Both the positive and negative edges of the IR input signal on the
// ICP input pin trigger an input capture interrupt, because the edge
// selection is toggled on every interrupt. Output compare register A
// is used for a timeout to detect the end of an IR transmission.
// The timeout value is updated on every edge. When at least 4 edges
// were detected when a timeout occurs, the packet count ir.count is
// incremented and the ir.length byte is set to the packed length. This
// byte is checked when a read request is received via USB to see if a
// packet is available
//
// The two interrupt handlers in this file run for more than 28 cycles,
// so they need to reenable interrupts to allow USB interrupts to be
// handled in time. To avoid nested IR interrupts, the input capture and
// output compare interrupts are disabled before the global interrupt
// flag is set. In the input capture interrupt handler, the interrupts
// are disabled for a maximum of 26, 27 and 21 cycles, and in the output
// compare interrupt handler for 21, 23 and 21 cycles. Unfortunately,
// it was not possible to meet the 28 cycle maximum for the input
// capture interrupt handler without replacing the compiler generated
// prolog and epilog code by inline assembly, which makes the code a
// little fragile.
//
// The IR receiver is disabled during an IR buffer read, so that a
// packet that is being tranmitted to the host, is not overwritten by
// the next packet. When a transfer is in progress, 'inpos' has a value
// other than 0xff. When the buffer is read completely, the IR receiver
// is reenabled again, but when the next IR packet has already started,
// this packed is discarded by setting 'edges' to 0xff.
//
// There are two USB request codes to control the LCD, one to write
// instructions, and one to write data to the display. OUT transfers
// are used to transfer up to 255 bytes for a single request.
//
// Stack space calculations for gcc-3.4.3 and gcc-4.1.0:
//
// 3.4.3   4.1.0
// -----   -----
//   0       0       main
//   0       0           lcd_init (inlined)
//   2       2           usb_init
//   0       0           ir_init (inlined)
//   6       5           usb_poll
//   0       0               usb_receive (inlined)
//   2       2                   usb_setup
//   5       5                   usb_out
//   4       4                       lcd_write
//   2       2                           lcd_read4
//   2       2                           lcd_write4
//   0       0               usb_transmit (inlined)
//   2       4                   usb_int
//   2       2                   crc
//
// Maximum stack space is 6+5+4+2=17 for gcc-3.4.3 and 5+5+4+2=16 for
// gcc-4.1.0. Total stack usage:
// - USB interrupt:			11 bytes
// - IR input capture interrupt:	11 bytes
// - IR output compare interrupt: 	 7 bytes
// - main():				17 bytes
// The two IR interrupt handler cannot be active simultaneously, so the
// maximum stack space is 17+11+11=39 bytes.
//
// NOTE: The above cycle and stack size figures were obtained by
//       inspecting the code generated by gcc-3.4.3 and gcc-4.1.0.
//       Other compiler versions may generate different code, leading
//       to different figures. Likewise, the replacement prolog and
//       epilog code may have to be adapted when a different compiler
//       version allocates different registers.
//
// Copyright (C) 2006 Dick Streefland
//
// This is free software, licensed under the terms of the GNU General
// Public License as published by the Free Software Foundation.
// ======================================================================

#include <avr/io.h>
#include <avr/interrupt.h>
#include "usb.h"

// ----------------------------------------------------------------------
// IR receiver definitions
// ----------------------------------------------------------------------

#define	TIMEOUT		10500	// IR transmission timeout in us
#define	IR_MAX		35	// maximum number of IR data bytes
#define	IR_WRAP		16	// wrap area at end (for 67 byte NEC protocol)
#define	SCALING		10	// 9: 42.67 us period, 10: 85.33 us period
#define	LED		5	// PORTD bit number for LED
#define	PULLUP		3	// PORTD bit number for D- pullup

enum
{
	// Generic requests
	USBTINY_ECHO,		// echo test
	// IgorPlug-USB requests
	IGORPLUG_CLEAR,		// clear IR data
	IGORPLUG_READ,		// read IR data (wValue: offset)
	// LCD requests
	LCD_INSTR = 20,		// write instructions to LCD (via OUT)
	LCD_DATA,		// write data to LCD (via OUT)
};

static	byte_t	inpos = 0xff;	// read position for usb_in(), or 0xff
static	byte_t	edges;		// incremented for each edge
static	struct			// IgorPlug-USB compatible data layout
{
	byte_t	length;		// length of data[]
	byte_t	count;		// incremented for each IR packet
	byte_t	offset;		// not used
	byte_t	data[IR_MAX];	// mark/space periods
}		ir;

// ----------------------------------------------------------------------
// LCD definitions
// ----------------------------------------------------------------------

#define	LCD_PRESENT	1	// set to 0 to remove LCD support

#define	DDR		DDRB
#define	PORT		PORTB
#define	PIN		PINB

#define	D4		0
#define	RS		4
#define	RW		5
#define	E		6

#define	MASK_D4		(0xf << D4)
#define	MASK_RS		(1 << RS)
#define	MASK_RW		(1 << RW)
#define	MASK_E		(1 << E)

// ----------------------------------------------------------------------
// Handler for timer1 input capture interrupt: edge on IR input
// ----------------------------------------------------------------------
__attribute__((signal,naked))			// interrupts are DISABLED
extern	void	SIG_TIMER1_CAPT ( void )
{
	static	uint_t	prev;
	uint_t		stamp;
	byte_t		delta;
	byte_t		e;

	asm volatile(
		"push	r23\n"
		"in	r23,__SREG__\n"
		"push	r18\n"
		"push	r19\n"
		"push	r24\n"
		"push	r25\n"
	:: );
	stamp = ICR1;				// get time stamp
	TIMSK = 0;				// disable both IR interrupts
	TCCR1B ^= _BV(ICES1);			// toggle edge detector
	sei();					// allow USB interrupt
	PORTD |= _BV(LED);			// switch LED on
	asm volatile(
		"push	r20\n"
		"push	r21\n"
		"push	r30\n"
		"push	r31\n"
	:: );
	delta = (stamp - prev + (1 << (SCALING-3-1))) >> (SCALING-3);
	asm volatile("" : : "r"(delta) );	// calculate delta (gcc-4.1.0)
	prev  = stamp;
	OCR1A = stamp + 12L * TIMEOUT / 8;	// update timeout value
	cli();					// enter critical region
	e = edges;
	if	( e != 0xff )			// packet should not be ignored?
	{
		if	( e > IR_MAX )		// packet too long for buffer?
		{
			e -= IR_WRAP;		// wrap, don't truncate
		}
		if	( inpos == 0xff )	// update ir only when USB idle
		{
			ir.length = 0;		// discard previous packet
			if	( e > 0 )
			{
				ir.data[e - 1] = delta;
			}
		}
		edges = e + 1;
	}
	sei();					// allow USB interrupt
	asm volatile("nop");			// reduce latency to 1 cycle
	asm volatile(
		"pop	r31\n"
		"pop	r30\n"
		"pop	r21\n"
		"pop	r20\n"
	:: );
	cli();
	TIMSK = _BV(OCIE1A) | _BV(ICIE1);	// reenable IR interrupts
	asm volatile(
		"pop	r25\n"
		"pop	r24\n"
		"pop	r19\n"
		"pop	r18\n"
		"out	__SREG__,r23\n"
		"pop	r23\n"
		"reti\n"
	:: );
}

// ----------------------------------------------------------------------
// Handler for timer1 output compare A interrupt: IR transmission timeout
// ----------------------------------------------------------------------
__attribute__((signal))				// interrupts are DISABLED
extern	void	SIG_TIMER1_COMPA ( void )
{
	TIMSK = 0;				// disable both IR interrupts
	sei();					// allow USB interrupt
	PORTD &= ~_BV(LED);			// switch LED off
	cli();					// enter critical region
	if	(  edges >= 4			// at least two pulses (NEC)
		&& edges != 0xff		// packet should not be ignored
		&& inpos == 0xff		// update ir only when USB idle
		)
	{
		ir.count++;
		ir.length = edges - 1;		// new packet is complete
	}
	edges = 0;
	sei();					// allow USB interrupt
	TCCR1B &= ~_BV(ICES1);			// reset to negative edge
	cli();
	TIMSK = _BV(OCIE1A) | _BV(ICIE1);	// reenable IR interrupts
}

// ----------------------------------------------------------------------
// Initialize the IR receiver.
// ----------------------------------------------------------------------
static	void	ir_init ( void )
{
	DDRD |= _BV(LED);
	TCCR1B = _BV(ICNC1)	// noise canceler, trigger on negative edge
	       | _BV(CS11);	// clock source clk/8
	TIMSK  = _BV(OCIE1A)	// output compare 1A match interrupt enable
	       | _BV(ICIE1);	// input capture 1 interrupt enable
}

#if	LCD_PRESENT
// ----------------------------------------------------------------------
// Delay <count> times 100 microseconds.
// ----------------------------------------------------------------------
static	void	lcd_delay100u ( byte_t count )
{
	asm volatile(
		"0:	ldi	r25, 240\n"
		"1:	rjmp	2f\n"
		"2:	dec	r25\n"
		"	brne	1b\n"	// 240 * 5 cycles = 100us
		"	dec	r24\n"
		"	brne	0b\n"
	);
}

// ----------------------------------------------------------------------
// Write 4 bits to the LCD.
// ----------------------------------------------------------------------
static	void	lcd_write4 ( byte_t data )
{
	PORT = (PORT & ~ MASK_D4) | (data << D4);
	PORT |= MASK_E;			// E high period: > 230ns
	DDR  = MASK_RS | MASK_RW | MASK_E | MASK_D4; // Dx setup time: > 80ns
	PORT &= ~ MASK_E;		// hold time: > 10ns
	DDR  = MASK_RS | MASK_RW | MASK_E;
}

// ----------------------------------------------------------------------
// Read 4 bits from the LCD.
// ----------------------------------------------------------------------
static	byte_t	lcd_read4 ( void )
{
	byte_t	save;
	byte_t	data;

	save = PORT;
	PORT = MASK_RW;			// address setup time: > 40ns
	PORT |= MASK_E;			// E high period: > 230ns
	asm volatile("rjmp 0f\n0:");
	data = (PIN & MASK_D4) >> D4;	// data access time: > 160ns
	PORT = save;
	return data;
}

// ----------------------------------------------------------------------
// Write a byte to the LCD.
// ----------------------------------------------------------------------
static	void	lcd_write ( byte_t b )
{
	byte_t	h;

	do
	{
		h = lcd_read4();
		(void) lcd_read4();
	} while	( h & 0x08 );
	lcd_write4( b >> 4 );
	lcd_write4( b & 15 );
}

// ----------------------------------------------------------------------
// Initialize the LCD.
// ----------------------------------------------------------------------
static	void	lcd_init ( void )
{
	DDR = MASK_RS | MASK_RW | MASK_E;
	PORT = 0;
	//lcd_delay100u( 150 );	-- skipped because of RESET delay
	lcd_write4( 3 );
	lcd_delay100u( 41 );
	lcd_write4( 3 );
	lcd_delay100u( 1 );
	lcd_write( 0x32 );	// switch to 4-bit mode
	lcd_write( 0x28 );	// function set: 4-bit mode, two lines
	lcd_write( 0x01 );	// clear display
	lcd_write( 0x06 );	// entry mode: increment, no shift
	lcd_write( 0x0c );	// display control: on, no cursor
}
#endif	/* LCD_PRESENT */

// ----------------------------------------------------------------------
// Handle a non-standard SETUP packet.
// ----------------------------------------------------------------------
extern	byte_t	usb_setup ( byte_t data[8] )
{
	byte_t	req;
	byte_t	r;

	r = 0;

	// Generic requests
	req = data[1];
	if	( req == USBTINY_ECHO )
	{
		r = 8;
	}

	// IgorPlug-USB requests
	if	( req == IGORPLUG_CLEAR )
	{
		ir.length = 0;
		inpos = 0xff;
	}
	if	( req == IGORPLUG_READ )
	{
		cli();
		if	( ir.length > 0 )
		{
			inpos = data[4];
			r = 0xff;	// call usb_in() to get the data
		}
		else
		{
			data[0] = 0;
			r = 1;
		}
		sei();
	}

	// LCD requests
	if	( req == LCD_INSTR )
	{
		PORT &= ~ MASK_RS;	// data will be received by usb_out()
	}
	if	( req == LCD_DATA )
	{
		PORT |= MASK_RS;	// data will be received by usb_out()
	}

	return r;
}

// ----------------------------------------------------------------------
// Handle an IN packet.
// ----------------------------------------------------------------------
extern	byte_t	usb_in ( byte_t* data, byte_t len )
{
	byte_t	n;
	byte_t	max;

	max = ir.length + 3;
	n = 0;
	while	( n < len )
	{
		if	( inpos >= max )	// end of packet?
		{
			cli();
			if	( edges )	// next packet already started?
			{
				edges = 0xff;	// ignore remainder of packet
			}
			inpos = 0xff;		// reenable receiver
			sei();
			break;
		}
		data[n++] = (& ir.length)[inpos++];
	}
	return n;
}

// ----------------------------------------------------------------------
// Handle an OUT packet.
// ----------------------------------------------------------------------
extern	void	usb_out ( byte_t* data, byte_t len )
{
#if	LCD_PRESENT
	while	( len )
	{
		lcd_write( *data++ );
		len--;
	}
#endif	/* LCD_PRESENT */
}

// ----------------------------------------------------------------------
// Main
// ----------------------------------------------------------------------
__attribute__((naked))		// suppress redundant SP initialization
extern	int	main ( void )
{
	PORTD |= _BV(PULLUP);
	DDRD  |= _BV(PULLUP);	// enable pullup on D-
#if	LCD_PRESENT
	lcd_init();
#endif	/* LCD_PRESENT */
	usb_init();
	ir_init();
	for	( ;; )
	{
		usb_poll();
	}
	return 0;
}
