AT Tiny Techniques in a Macro IR Remote

by btidey in Circuits > Electronics

819 Views, 3 Favorites, 0 Comments

AT Tiny Techniques in a Macro IR Remote

ATTinyIR1.jpg

This instructable describes some techniques used in an ATTiny project.

The project was to produce a simple macro remote control with a few buttons each of which can issue a series of IR remote controls to a variety of devices. The purpose was to allow controlling several devices with single button presses rather than have to remember and handle several different remote controllers. An example is to turn on a television, cable box and AV box.

To accomplish with an ATTiny required some methods which are described in some detail. These methods have been used in the past but this instructable goes into their implementation in a bit of detail.

The construction and operation of the full remote control are also covered. The functionality of this included

  • 10 macro commands each containing a sequence of up to 20 device codes
  • Supports up to 32 different device codes
  • Supports NEC, rc5, 5c6 type encoding
  • variable delay between each code in a sequence
  • Macros and device codes stored in EEPROM
  • uart port for configuration and programming device codes and macros and for testing
  • Battery operated with low quiescent current for extended battery life

The particular techniques covered in detail are

  • Combined IR modulated code and UART handling with single timer interrupt
  • Multi button handling with limited I/O
  • Optimising quiescent current for extended battery life.
  • ATTiny pin allocation and fuse values

Complete code for this is accessible at https://github.com/roberttidey/ATTinyIR

IR Modulation and UART Handling

This is accomplished in a library TinyIrUart

The ATTiny has two timers. In an Arduino context Timer1 is used for internal timing functions like delay functions and miilis / micros timing. Timer0 is free for the user.

Here Timer0 is set up assuming for use with an 8MHz system clock with no prescaling. It is initialised with no prescale but is used in a phase accurate PWM mode. This means the comparator A controls the top value of the timer and triggers a reversal of the count direction. So the timer counts up from 0 to Comparator A then counts down to 0 where the cycle repeats. The overall cycle frequency is then 8MHz / (2 * Comparator A). This is arranged to be as close to 38.4KHz as possible. This is a good value for IR modulation and also convenient for uart handling.

Comparator B is then used to control the PWM output (which can appear on PB1 GPIO). By setting this to half the value of Comparator A then the output sets at half way on the up count and resets at half way on the down count and results in a square wave of 38.4KHz. The output of comparator B only appears on the PB1 pin if its direction is set output and this is exploited to simplify the handling.

In this phase correct PWM mode a timer overflow occurs once per cycle ( 26uSec)

So the simplified set up of TIMER0 via its registers looks like

TCCR0B = 0;	// Stop Counter
OCR0A = ocr;
OCR0B = OCR0A >> 2;
TCCR0A = 0x31;	//OCR0B set/clear, PWM PHASE CORRECT
TIMSK &= ~(1<<TOIE0); //Disable Timer0 Overflow Interrupt
TCCR0B = 9; //Start counter TOP = OCR0A
TIMSK |= (1<<TOIE0); //Enable Timer0 Overflow Interrupt

To utilise this to produce IR codes the TIMER0 overflow interrupt routines counts down a series of interrupts to control the length of a modulated burst or the length of a space. So, for example, a countdown of 22 produces a burst of 22 x 26 = 572 uSec which is a pulse width needed for NEC protocols. To keep the interrupt handling as short as possible a desired code is pre-computed into a series of countdown control bytes. The MSB of the byte determines whether modulation is on or off and the remaining 7 bits is the countdown. This allows for a maximum length of about 3.3mSec which is sufficient for the data bits used in protocols. Header pulses can be longer and can be handled by just using several countdowns with the same polarity.

The modulation on /off is achieved by just reversing the direction of the PB1 pin. When it is output then the modulation appears. When it is input then a pull down resistor gives a 0 output. The pull down resistor is used anyway as the bias component for a IR LED driver.

UART handling

The ATTiny does not have native UART hardware but the 38.4KHz frequency of the TIMER0 interrupt provides a convenient method of implementing the UART functions in software.

For the TX function it is fairly straightforward . If there is a byte to transmit from a character string then the transmit part of the overflow interrupt works on it. A software counter TXDiv in the interrupt gives the basic bit period. 32 gives 1200 baud, 16 gives 2400 baud, 8 gives 4800 baud, and 4 would give 9600 baud. I recommend using 2400 baud as this gives more tolerance on the RX part. Every time the TXDiv goes to zero then it steps through the byte one bit at a time controlled by TXState and sets the GPIO pin accordingly. To keep operations as quick as possible in an interrupt routine then direct register output is used rather than digitalWrite and the mask values for the pin used are pre-computed.

if(TXByteCount) {
	if(TXDiv) {
		TXDiv--;
	} else {
		if(TXState == 0) {
			// Start bit
			PORTB &= TXMaskI;
		} else if(TXState < (TXCOUNT - 1)) {
			//Data Bit
			if(TXByte & 1) {
				PORTB |= TXMask;
			} else {
				PORTB &= TXMaskI; 
			}
			TXByte >>= 1;
		} else {
			//Stop bit
			PORTB |= TXMask;
			if(TXState == TXCOUNT) {
				TXByteCount--;
				TXBytes++;
				TXByte = TXBytes[0];
				TXState = -1;
			}
		}
		TXState++;
		TXDiv = rxtxBaud - 1;
	}
}

The RX function has to get its timing based on when a start pulse edge is received. To achieve this an edge trigger interrupt routine is used. This is only used to detect the first edge from an quiescent state. The interrupt routine does 2 things. It sets a state variable to signal that a character is being received and it sets the initial phase of a RXTicks counter so that the RX handling in the TIMER0 overflow routine can sample bits close to the mid bit time throughout the receive byte. When the RXTicks counter signals its mid bit time in the timer overflow routine then the GPIO pin is read and shifted into a receive byte. When a whole byte is received then it is stored and the RX logic set back to look for the next start bit. Like the TX handling the reading of the GPIO is done with direct register access and pre-computed masks. The overall divider chosen for the UART determines how tolerant the UART is to inaccuracies in the clocking in ensuring the rx bit sampling is OK. So with the recommended 2400 bit divider (16) then the central bit will start off accurate to within 6% of the first centre bit. Even if the clock frequency is +/- 4% then the sampling will still be within the right bit after 9 bit periods. 4800 baud is pretty safe as well, but 9600 baud with a divider of 4 can mean the initial sample could be 25% out and the clock will need to be better than 2% accurate.

ISR(PCINT0_vect) {
	if(RXState == 0) {
		//start bit, set sample at half bit intervals
		RXTicks = (rxtxBaud >> 1) - 1;
		RXState++;
	}
}

// in the timer interrupt
if(RXState) {
	RXTicks++;
	if(RXTicks == rxtxBaud) {
		if(RXState == 10) {
			RXBuffer[RXBufferHead] = RXByte;
			RXBufferHead = (RXBufferHead + 1) & (RXBUFFER_MASK); 
			RXState = 0;
		} else {
			RXByte >>= 1;
			if((PINB & RXMask) != 0) {
				RXByte |= 0x80;
			}
			RXTicks = 0;
			RXState++;
		}
	}
}

RX characters are stored in a small circular queue to allow the foreground software handling of received characters to be less time critical.

The rest of the library itself gives basic access to initialisation, and uart transmit and receive functions. It also provides functions to pre-compute and initiate the ir protocol timing sequences for nec, rc5 and rc6 protocols from the basic address, command values.

Multi-button Handling

The ATTiny has85 only has 6 possible GPIO pins and with pins used for UART TX and RX and IR diode driving then little is left to handle button handling.

The technique used here is to employ the ADC to read the voltage from a resistor ladder with buttons grounding taps along the way. The basic technique has been described in various places before. What I do here is to use a ladder of very standard resistor values chosen to give well placed steps of voltage output for 10 buttons. This is shown in the schematic in the description of the build.

The differences are greater than 78 (0-1023). This means that one can also easily distinguish between presses using just 8 bits from the ADC conversion. This allows faster reading and also minimises code and memory by keeping all variables as bytes.

I use register access to set up and read the ADC rather than analogRead to keep the code as fast and small as possible.

The basic set up is

ADCSRA = 0x04;  // DISABLED | 16 prescale 
ADCSRB = 0x00;
ADMUX = 0x21; // // ADC1 ADLAR Vcc Reference

A routine to do a single 8 bit measurement is then

uint8_t getAnalog() {
	//start conversion
	ADCSRA = ADCSRA_STARTSINGLE;
	//wait till complete
	while((ADCSRA & (1 << ADIF)) == 0) {
	}
	//reset complete flag
	ADCSRA = ADCSRA_CLEAR;
	return ADCH;
}

All buttons released will give a value close to 255. A single button press can be decoded by checking the value against a threshold arrray and using successive readings with a tolerance to ensure that it is valid.

// holds button pressed number, 0xff means up, 0xfe means actioned but not yet released
uint8_t buttonDown = 0xff;
uint8_t buttonUp = 1;
//button ladder 47k,4k7,4k7,10k,10k,22k,22k,47k,100k,220k
#define NUMBER_BUTTONS 10
#define BUTTON_TOLERANCE 5
//button nominal adc 0,93,171,299,394,534,624,736,843,924,1023
// use nearest 8 bit equivalent
uint8_t buttonValue[NUMBER_BUTTONS] = {2,24,43,75,98,136,156,184,211,231};

void handleButtons() {
	uint8_t b;
	int16_t d;
	uint8_t adc = getAnalog();
	if(adc > 250) {
		buttonDown = 0xff;
	} else if(buttonDown != 0xfe) {
		for(b = 0; b < NUMBER_BUTTONS; b++) {
			d = adc - buttonValue[b];
			if(d < BUTTON_TOLERANCE && d > -BUTTON_TOLERANCE) {
				if(b == (buttonDown & 0xf)) {
					// 2 successive readings indicate same button
					executeButtonFUnction(b);
					//flag button processed
					buttonDown = 0xfe;
				} else {
					// 
					buttonDown = b;
				}
				break;
			} 
		}
	}
}

Optimising Quiescent Current

In a battery operated remote control it is important to keep quiescent current as small as possible. I had a target of about 1 year operation from a 300mAh rechargeable Li battery which implies quiescent current less than 40uA.

To achieve this requires a number of steps.

The first step is to ensure that any side current drains are removed. As I use Digistump type devices as a convenient ATTiny85 implementation this is important as there are 3 items which consume significant current even if the ATTiny is put into power down sleep. These are the quiescent current of the on board regulator, the power on LED, and the bias current for the USB interface. These contribute about 7mA by themselves. They can be eliminated by some hardware modifications and these are described in an instructable https://www.instructables.com/Reducing-Sleep-Curre...

The second step is to ensure that the idle state of GPIO pins is such that no bias currents are drawn. This can be done by either putting states into a output 0 condition or as inputs with the internal pull up disabled.

The third step is to use power down sleep on the ATTiny to minimise current consumption when the remote control is idle. The ATTiny draws very little current in this mode and conveniently retains the state of its IO and will resume operation for when it wakes up. To do this one must have a wake up strategy so that when a button is pressed then the ATTiny becomes active and can action the button press. The easiest way to achieve this is by using the watchdog timer on the ATTiny which is a low power counter which can run even with the ATTiny in power-down sleep. Normally it would be used to reset the CPU if something goes wrong but it can be configured to wake up the device and cause an interrupt. This is the mode used here. The watchdog timer interval can be set from 16mSec to 8 seconds in binary intervals. Using very short intervals can lead to more current draw as less time is spent in sleep. I chose an interval of 256mSec as this allows checking for a button press at a reasonable rate to pick up and activate quickly.

The basic operation is to have a sleep loop which activates every 256mSec and checks for any button being pressed using the getAnalog function. If this indicates a button is down then the sleep loop terminates and the button is actioned before returning back to the sleep loop. The active part of the sleep loop including button checking takes about 50 uSec so the ATTiny is in sleep for 99.98% of the time. I also allow long button presses to suspend the operation of the sleep loop so that other activities like configuration via the UART interface can take place.

The code consists of the watchdog wake interrupt handler and the 'simplified' sleep loop itself.

ISR(WDT_vect) {
	MCUSR = 0x00; // Clear WDRF in MCUSR/
	WDTCR |= (1<<WDCE) | (1<<WDE); // Write logical one to WDCE and WDE/
	WDTCR = 0x00; // Turn off WDT
}

void sleepTillButton () {
	while(getAnalog() > 252) {
		saveADCSRA = ADCSRA;
		ADCSRA = 0;
		power_all_disable();
		noInterrupts();
		sleep_bod_disable();
		WDTCR = 1<<WDIE | 1<<WDE | 1<<WDP2; // enable watchdog 256mS
		set_sleep_mode(SLEEP_MODE_PWR_DOWN);
		sleep_enable();
		interrupts();
		// go to sleep
		sleep_mode();
		//resume here after watchdog wakes up from sleep
		sleep_disable();
		power_all_enable();
		ADCSRA = saveADCSRA;
	}
}

Some extra items which can contribute to current saving are described in the fuse section.

ATTiny Pins and Fuses

This section describes choice of GPIO pins and also the role that ATTiny fuses can play.

The choice of pins can be quite important particularly with the use of Digistump boards as they have circuitry associated with the USB interface used for simple code uploading.

  • PB0 is fairly general purpose.
  • PB1 is the pin that is used to supply the output of Timer0 comparator B so in this case it must be used for the infra red diode driving.
  • PB2 is fairly general purpose and can be used as one ofthe ADC inputs (ADC1)
  • PB3 is used by the Digistump bootloader to interface to USB so although it can be used after boot for anything this must take into account the resistor and zener diode loading.
  • PB4 is like PB3 as it is used by the bootloader for USB interfacing.
  • PB5 can be used as GPIO or ADC providing the configuration fuse disables its use as a RST pin. However, its characterisitics are a bit different as its drive capability and input pull up arrangements means that it can draw a bit more current if used for this.

Given these I chose to allocate the pins as follows.

  • PB0 as UART TX output. It is set low during sleep to minimise any external current draw but then activated high when awake.
  • PB1 the IR modulated driver
  • PB2 as ADC1 reading the multi-button resistor ladder
  • PB3 unused
  • PB4 as UART RX input. The UART driving this will see the USB bias network but this will not cause any problems
  • PB5 unused

The ATTiny has a couple of configuration 'fuse bytes' which can control some functionality of operation and which can't be changed with normal in code programming. Three items are of interest here.

2 bits of the 'low fuse' byte control a delay when starting up or waking up from sleep before the CPU starts executing instructions. This is to allow the clocks to stabilise. During this delay period the chip seems to draw about 1 mA. The relevance here is that it can impact on the sleep loop. The loop is set up to wake up every 256mSec and then rapidly return to sleep. The delay then can become a significant factor in determining the overall quiescent current. By default in a Digistump the delay is set to a very conservative 64mSec so the contribution becomes very significant resulting in a quiescent current of about 500uA.

By reprogramming the fuse the delay can be minimised and for example can be set to 4mSec when the default 16MHz pll clock is used. This reduces the quiescent current down to 30uA which is under the target I had.

Current can be reduced still further by changing the default clock start up setting to the internal 8MHz clock rather than the pll. This reduces the current a little bit anyway but if the fuse controlling the PB5 function is set to enable RST then the clock start delay becomes very short reducing the current consumption further. However, if this is done then the Digistump USB bootloader will no longer function to allow easy software updates and the fuses would need to be restored back to16Mz pll each time an update was needed.

I have made the main software automatically adapt to whichever clock selection is used and at the moment I use the 16MHz clock method to allow easier updates.

Fuses can be edited using something like my programmer https://www.instructables.com/ATTiny-HV-Programmer/

Construction

ATTinyIRSchematic.jpg
ATTinyIR3.jpg

The construction is straightforward and will be largely determined by the choice of switches and the enclosure used.

I used small push button switches and a 3d printed enclosure which had recessed holes to allow the buttons to poke though a little. https://www.thingiverse.com/thing:4771961

The switches were mounted on through hole prototype board and they need to be soldered on fairly accurately so they line up with the holes in the enclosure. The resistors and wiring of the button ladder were mounted on the rear of the board as was the ir led driver using a MOSFET transistor. This keeps the front of the board clear of anything but the switches allowing it to be pushed flush against the enclosure.

The ATTiny was mounted by soldering onto two wires poking up from the button board and aligned to the micro USB hole in the enclosure.

A 3 pin connector allowed for recharging the battery. centre +, and 0v either side means the polarity does not mattter. I put a little power switch inline inside the case as it is good to turn the power off when connecting USB for program updates. Resin glue was used to secure the switch board to the case.

I made the UART connections via an internal flying 3 pin header socket which can be plugged firect into a USB serial board when configuration is needed.

Software. Operation and Configuration

Software

The software for this project is maintained at https://github.com/roberttidey/ATTinyIR

It is compiled and uploaded in an Arduino environment with Digistump support added.

The TinyIrUart library should be placed in your libraries folder. The other libraries used for sleep, watchdog etc are standard.

Operation

The device after starting up enters its sleep loop and will stay there until a button is pressed. A normal button press will wake it up and issue the macro sequence of ir codes associated with that button press before returning to sleep. Note that on first use before any configuration is done then nothing happens as all the macro sequences are empty.

A long press (> 5 seconds on a button) tells the device to suspend sleep operation for about 90 seconds. This allows the uart interface to become operational and accept configuration commands. One of these commands can suspend sleep indefinitely until another command is issued and allows configuration to be done without fear of sleep resuming.

If the device is powered on with one of the buttons depressed then this will reset any clock tweaks done back to their default values. It does not affect any of the ret of the configuration.

Configuration

Configuration is done by issuing simple serial commands over the Uart interface. Commands consist of a single lower case letter followed by a number of numeric parameters separated by coomas. The command must be terminated in either a CRLF or a LF

- Save a code cCode,Type,Addr, Val1,Val2,Val3
	- Code=code number (0-31)
	- Type = 0(NEC),1(rc5),2(rc6),3(rc6Extended)
	- Addr = IR Address
	- Val = 1,2 or 3 data values
- Save a macro sequence mMacro,Sequence...
	- Macro = macro number (0-9)
	- Sequence is code number(0-31) + 32*delayTick(0-7) (Tick is 96mSec)
- Adjust Timing oCmd,Val
	- Cmd 0 is read OSCVAL (Basic 8MHz clock), Ticks (to divide to 38.4KHz)
	- Cmd 1 is adjust OSCVAL (Val=0 decrement, Val=1 increment) 
	- Cmd 2 is adjust Ticks (Val=0 decrement, Val=1 increment) 
- Read Macros and code r
	- Dumps all codes and Macros to serial port
- Set sleep Mode sMode
	- Mode 0(start sleeping), 1(temporary awake), 2(permanent awake)
	- use this again to set back to sleeping after configuration finished
- Test transmit a code tType,Address,Val1,Val2,Val3
	- same parameter definitions as c Command
- Execute Macro xMacro
	- Send macro sequence as previously defined
- Send Code zCode
	- Send Code as previously defined

So the basic method of configuration is to enter a number of c commands to define individual IR codes for the devices to be controlled. Each has an identifying code number to be used in macro defintions and can be a different protocol. It then normally has a device identity or address and a command / data value. Next one issues a series of m commands to define the sequence to be associated with each button. Each number in the sequence is a single value consisting of the code number + 32 * delay value. The delay value controls the delay before the corresponding code is emitted. It can take values of 0-7 and the delay is in 96msec units. So a delay of 0 gives no delay and 7 gives a delay of 670mSec.

The adjust timing command (o) can be used to tweak the clock timing to get the modulation clock as close as possible to 38.4KHz, which is optimal for the UART operation. This should not be necessary. One would need access to a logic analyser or scope to do this with the basic method to capture a ir transmitted pulse sequence and measure its frequency. Two tweaks are available. OSCVAL is used by the ATTiny to tweak the basic internal clock oscillator. To avoid large changes which might make the uart non functional the command can only increment or decrement the value. The same applies to the Ticks value which is the basic divider value used to prescale down from 8MHz to 38.4KHz. One should not normally need to change this as the OSCVAL adjustment is just as good a method.

Getting IR codes

- Often the codes for a remote can be found on the web
- A utility rxirEx.py is also provided in the docs folder which can get codes
	 for nec and rc6 based remotes
- This can be run on a Raspberry Pi with an IR receiver connected to a GPIO pin (24 default)
- IR polarity is defined near top of rxirEx.py
	 (default is high is IR active, using an inverting buffer to amplify output of receiver)
- Prepare a text file like the example (philipstv-top) which contains
	 a list of the buttons to capture
- Run the program, enter device name (e.g. philipstv, subset of buttons (e.g. top), protocol
	 (e.g. rc6) and retry (e.g. n)
- It will prompt for each button to be pressed in turn
- Results are placed in device.ircodes (e.g. philipstv.ircodes)
- Each button has the hex and binary data received and the address and command values
	 are at the end of the line
- These last two values are what is needed to program the ATTinyIR device