Extreme Lightweight IR Sender for NEC protocol

Extreme Lightweight IR Sender for NEC protocol

I use an IR sender to synchronise some clocks. It simply sends a timestamp over Infrared using the NEC protocol from a master clock which itself get the time from the internet.
.
.
NEC IR Protocol


.
.
Previously, I used an Esp8266 Arduino IR library for the sending task. However, since the task is very simple, it use only the NEC protocol, and does not really need the power and functionality of a full library, I decided to replace this part with my own development.

It is small enough at about 30 or so active code lines that I have not even written it as a library. It is implemented simply as a small, self contained function which can be copied into a sketch. There are no libraries to load and no direct timer configuration so you can choose any pin for the IR LED.

It exists in two versions now, one which requires a 16MHz processor or above. This has been tested with a Uno, ESP8266, Bluepill (STM32F103C8T6) and an ESP32. The other is a pure AVR (Uno, ATTiny85 etc.) development which can run at clocks speeds down to 1MHz which makes it suitable for battery operation. This version uses some special techniques to run at very low processor speeds such as direct port manipulation and an AVR built-in function for creating multiple NOPs for timing. Both are very accurate. They do, however, use blocking code (70ms per shot).

Muli MCU version (16MHz or better). It is presented as a function contained in an sample driver sketch. It has 3 mandatory parameters, the 4 byte NEC code block to send, and the IR carrier frequency. There are 2 additional parameters. One concerns the LED and permits it to negative logic, if required. The other allows the NEC code bit order to be mirrored such as version 3 of the Arduino IR library appears to promote.
.
.

// ===COPY UNTIL NEXT BREAK INTO YOUR SKETCH =====================================================================

void irSendNECblk( uint32_t code, uint8_t irLedPin, uint8_t freqKhz, bool inverted = false, bool invertedBitOrder = false ) {
	// sends an NEC IR code. No library required. Just copy this function into your sketch.
	// where:
	// code:  the code to send e.g. 0x00FDA857
	// irLedPin: the output pin e.g. 3
	// freqKhz: the carrier frequency in KHz
	// inverted: (default false). Default case : led wired low side so pin HIGH = on. otherwise pin LOW = on.
	// invertedBitOrder (default false). Default case: MSB as used in ver <= 2  of the Ken Shirriff IR library. Otherwise mirrored for V3+

	// sample call: irSendNECblk( 0x00FF1A9B, 3, 38 )  // pin 3, 38kHz carrier, pin HIGH puts led on, original (non mirrored) hex code format.
	// Note: blocks the loop() for approx 70 ms .
	// Author: 6v6gt 04.05.2021 https://forum.arduino.cc/t/extreme-lightweight-ir-sender-for-nec-protocol/858910

	uint16_t NecBurstUnit = ( freqKhz * 562L) / 1000L ; // IR carrier waves for 1 NEC mark or 1 NEC space( bit 0).
	uint8_t carrierPeriodUs = (int16_t)1000 / freqKhz ;
	uint8_t * codeSplit ;
	codeSplit = ( uint8_t* ) &code ;  // treat uint32_t as byte array ;

	auto xmit = [irLedPin,  inverted, carrierPeriodUs ]( bool isOn, uint16_t waves ) {
		// send carrier burst. on = IR mark, otherwise IR space (filler)
		uint32_t burstStartUs = micros() ;

		for ( uint16_t i = 0 ; i < waves ; i ++ ) {
			digitalWrite( irLedPin , isOn != inverted ? HIGH : LOW ) ;  // Xor. carrier mark or filler
			delayMicroseconds( carrierPeriodUs / 3 ) ;  // 33% duty cycle
			digitalWrite( irLedPin, inverted ? HIGH : LOW ) ;   // carrier space
			while (  micros() - ( burstStartUs + ( i * carrierPeriodUs ) ) < carrierPeriodUs )  ;  // idle until end of wave(i)
		} ;
	} ;

	pinMode( irLedPin, OUTPUT ) ;
	xmit( true , NecBurstUnit * 16 ) ;   // header mark 9000 us
	xmit( false , NecBurstUnit * 8 ) ;   // header space 4500 us
	for ( uint8_t i = 0 ; i < 32 ; i ++ ) {  // 32 bits
		xmit( true , NecBurstUnit ) ; // NEC mark
		uint8_t codeByte = !invertedBitOrder ? 3 - i / 8 /*MSB*/ : i/8 /*LSB*/ ;
		uint8_t codeBit = !invertedBitOrder ? 7 - i % 8 /*MSB*/ : i % 8 /*LSB*/ ;
		xmit( false, bitRead( *(codeSplit + codeByte) , codeBit ) == 1   ? NecBurstUnit * 3 : NecBurstUnit ) ;  // NEC space(0) 562us or NEC space(1) ~1675us
	}
	xmit( true , NecBurstUnit ) ;  // terminator
} // end of irSendNECblk()

// === END OF COPY =============================================================================================



void setup() {

	Serial.begin( 9600 ) ;
	Serial.println( "irSendNECblk demo. Needs 16MHz clock or better" );
}

void loop() {

	uint32_t hexCode ;

	delay( 1000 ) ;
	hexCode = 0x00FDA857 ; // OK
	// hexCode = 0xEA15BF00 ; // NEC OK mirrored
	Serial.println( hexCode, HEX ) ;

	irSendNECblk( hexCode , 13, 38  ) ;

	Serial.println( "stopping" ) ;
	while (1) delay(1000) ;
}

.
.
.
The AVR version has a slightly different call signature. You give it the 4 byte NEC code block to send. However, you specify the IR pin by its port and pin number within the port. You must also pass a reference to the data direction register so it can set the pin as output. There is probably an easier way which someone might suggest. The carrier frequency is fixed at 38KHz. When used at very low clock frequences, say 1MHz or 2MHz, the time taken to service the millis() timer interrupt (140uS at 1MHz) can cause glitches so it is better to suspend interrupts at the currently commented out in the sketch. The optional parameters are the same as above.


// ===COPY UNTIL END MARKER INTO YOUR SKETCH =====================================================================

void irSendNECavr( uint32_t code, volatile uint8_t *port, volatile uint8_t *portDdr, uint8_t portPin, bool inverted = false, bool invertedBitOrder = false ) {
	// sends an NEC IR code. No library required. Just copy this function into your sketch.
	// where:
	// code:  the code to send e.g. 0x00FDA857
	// port: a refererence to the port for the led pin e.g. &PORTB  (note leading &)
	// portDdr: a refererence to the port's data direction register e.g. &DDRB (note leading &)
	// portPin: the port pin number in range 0 to 7 (NOT the Arduino digital pin number nor physical pin number)

	// inverted: (default false). Default case : led wired so pin HIGH = on. otherwise pin LOW = on.
	// invertedBitOrder (default false). Default case: MSB as used in ver <= 2  of the Ken Shirriff IR library. Otherwise mirrored for V3+

	// sample call: irSendNECblk( 0x00FF1A9B, &PORTB, &DDRB, 5 , false, false ) ; // last 2 parameters optional
	// digital pin 13 (portB port pin 5), 38kHz carrier, pin HIGH puts led on, original (non mirrored) hex code format.

	// Note: blocks the loop() for approx 70 ms during send operation. Fixed to 38kHz carrier.
	//         Suspends interrupts during IR_mark phases to minimise glitches from millis() timer. Not really required
	//         at 4MHz clock and above.
	//
	// Author: 6v6gt Ver 0_09 04.05.2021 https://forum.arduino.cc/t/extreme-lightweight-ir-sender-for-nec-protocol/858910


	uint16_t NecBurstUnit = ( 38 * 562L) / 1000L ; // IR carrier waves for 1 NEC mark or 1 NEC space( bit 0). Fixed at 38KHz
	uint8_t * codeSplit ;
	codeSplit = ( uint8_t* ) &code ;  // treat uint32_t as byte array ;

	//  tune this table if required to match the Mark and Space for the IR carrier in CPU ticks (33% duty cycle)
	// 	calculated as irPeriodCpuTicks = F_CPU / ( freqKhz * 1000L ) then deduction of overhead.
	//  Behaviour dependent on compiler optimisation.

#if F_CPU == 16000000L
	const uint16_t IR_MARKWAIT = 134; const uint16_t IR_SPACEWAIT = 275 ;
#elif F_CPU == 8000000L
	const uint16_t IR_MARKWAIT = 67 ; const uint16_t IR_SPACEWAIT = 130 ;
#elif F_CPU == 4000000L
	const uint16_t IR_MARKWAIT = 30 ; const uint16_t IR_SPACEWAIT = 60 ;
#elif F_CPU == 2000000L
	const uint16_t IR_MARKWAIT = 14; const uint16_t IR_SPACEWAIT = 24 ;
#elif F_CPU == 1000000L
	const uint16_t IR_MARKWAIT = 7; const uint16_t IR_SPACEWAIT = 7 ;
#else
#error "Unsupported clock rate. AVR with 1, 2, 4, 8 or 16MHz supported."
#endif

	struct Xmit {
		static void xmit( bool isOn, uint16_t waves, bool inverted, volatile uint8_t *port,  uint8_t portPin ) {
			// send one IR carrier wave
			bool markVal = !!isOn != !!inverted ; // XOR
			for ( uint16_t i = 0 ; i < waves ; i ++ ) {
				markVal ?  *port |= _BV(portPin) : *port &= ~_BV(portPin) ;
				__builtin_avr_delay_cycles( IR_MARKWAIT ) ;
				inverted ? *port |= _BV(portPin) : *port &= ~_BV(portPin) ;
				__builtin_avr_delay_cycles( IR_SPACEWAIT ) ;
			} ;
		};
	};

	*portDdr |= _BV(portPin) ;  // equivalent to pinMode( <digital pin>, OUTPUT ) ;

	// noInterrupts() ; // required for lower clock rates only
	Xmit::xmit( true , NecBurstUnit * 16 , inverted, port,  portPin ) ;   // header mark 9000 us
	Xmit::xmit( false , NecBurstUnit * 8 ,  inverted,  port,  portPin) ;   // header space 4500 us  // !!!!!!!!!
	for ( uint8_t i = 0 ; i < 32 ; i ++ ) {  // 32 bits
		Xmit::xmit( true , NecBurstUnit ,  inverted,  port,  portPin) ; // NEC mark
		uint8_t codeByte = !invertedBitOrder ? 3 - i / 8 /*MSB*/ : i/8 /*LSB*/ ;
		uint8_t codeBit = !invertedBitOrder ? 7 - i % 8 /*MSB*/ : i % 8 /*LSB*/ ;
		Xmit::xmit( false, bitRead( *(codeSplit + codeByte) , codeBit ) == 1   ? NecBurstUnit * 3 : NecBurstUnit ,  inverted,  port,  portPin) ;  // NEC space(0) 562us or NEC space(1) ~1675us
	}
	Xmit::xmit( true , NecBurstUnit , inverted,  port,  portPin ) ;  // terminator
	// interrupts() ; // required for lower clock rates only

} // end of irSendNECblk()

// === END OF COPY =============================================================================================


void setup() {

	Serial.begin( 9600 ) ;
	Serial.print("irSendNECavr() AVR slow clock version. F_CPU = ");
	Serial.println(F_CPU);
}

void loop() {

	uint32_t hexCode ;

	hexCode = 0x00FDA857 ; // OK
	// hexCode = 0xEA15BF00 ; // NEC OK mirrored

	Serial.println( hexCode, HEX ) ;

	// You may choose any pin. You identify it by Port and Pin within port.
	// You must also include the data direction register for that port.
	// The example below is for digital pin 3

	// digital pin 3 = port: PORTD, port pin 3 with matching data direction register.
	//  both inverted and mirrored bit order are false.
	//  Note that 3 here is pin number within the port, not necessarily the Arduino
	//    digital pin number
	irSendNECavr( hexCode , &PORTD, &DDRD, 3 , false, false ) ;

	Serial.println( "stopping" ) ;
	while (1) delay(1000) ;
}

.
.
I’ve previously published two other IR projects, one the counter part to this an NEC IR code receiver and also an IR learning remote control which could be interesting for some users: