Wednesday 24 August 2016

Sending/receiving IR data via PWM on an Arduino

Many years ago we did some low-level data exchange using radio modules and Manchester encoding. In a recent project, for real-life, work work, we were tasked with sending simple packets of data via IR. Naturally an IR 38khz receiver was tried and worked quite well.

Every now and again, the received data sort-of burped, so we sent each byte along with it's compliment value (so a value, say of 0xF0 would always be followed by a value of 0x0F or b11110000 would be followed by b00001111). The idea being that for every pair of 8-bit values, the XOR sum would always be exactly 0xFF.

We found that if we sent a single byte value, followed by it's compliment, and repeated this "packet" of data twice in quick succession, even if one packet failed validation, the second would always "get through".

Everything was great - for one-way communication.
Once we decided we wanted two-way comms, things got a bit trickier. But only because we're working to really tight margins when it comes to space; we simply don't have enough room to mount a 38khz IR receiver and a 3mm IR LED in two different devices.

 Because of space constraints, we just didn't have the room to use both an IR receiver and LED together in each device.

With space being tight, we figured that we could use an IR reflective sensor to give us our IR receiver and LED in a single, tiny, 1206-sized package. Some IR reflective sensors are just a 3mm LED and a 3mm photo-transistor in a single package


But what we were after was one of those really tiny ones, with the same kind of pinout and independently controlled LED/phototransistor combination, but in a not-much-bigger-than-a-1206 sized package.


The idea with these is that you activate the IR LED and then look for a reflected signal (when the photo-transistor receives IR light, it can be used to pull an input low, for example). But there's nothing to say we can't use two of these, facing each other, and have one device "listening" while the other is "talking". Instead of reflected IR, we'd just capture IR light sent directly from the other device. Genius!

The only thing is our photo-transistor doesn't respond to a 38khz carrier, like the larger, single, IR sensors (which is useful if you're firing IR light across a room, to a TV and need to filter out extraneous IR light from the sun and fluorescent lights). Our photo-transistor will simply conduct as soon as it sees and IR light, from any source. But given that we'll be transmitting data in a controlled environment (and not across the room) we either have to generate our own 38khz carrier wave (and decode it on the receive end) or simply forget all about it...
Guess which approach we took?

So doing away with the 38khz carrier, we simply have a receiver that pulls an input pin low when it can see IR light. We decided to try simple PWM to send data into the sensor.

The basic approach is that whenever a device sees a high-to-low transition on the input pin, we reset a timer/counter. This is because a high-to-low input signal means the IR led has just gone from low-to-high (i.e. just turned on). Then, when the input goes low-to-high, we know that the LED has just turned off (since we're using pull-up resistors on the input and using the photo-transistor to pull to ground when it sees IR light).

Following a low-to-high input, we look at the width of the pulse (in milliseconds). A simple look-up goes like this:

1-3 ms = bit value zero
6-10 ms = bit value one
15-20 ms = start/end of message marker

any other duration, ignore.

Whenever we get a "wide" pulse, we've either just started, or just ended, a message. Irrespective of which, look at the previously received bits of data and parse them. At a "start" pulse, we'd expect their to be no previous data, so we can skip parsing. After an "end" pulse, we should have a load of bits of data to parse. After a wide pulse, we reset the binary bit counter and set the incoming message buffer to blank again.

It's simple.
It's crude.
It works surprisingly well.

The only thing is, we want to make sure we're not parsing gibberish data. Which means we need some kind of checksum, to validate all the previously received data. We figured that the easiest method would be to send data in 3-byte packets, with the fourth byte acting as the checksum.

On receiving data, we'd recreate the checksum value from the first three bytes and compare it to the fourth. If the fourth byte and the checksum byte match, we accept the data and decide what to do based on the three-byte message.

The send routine uses simple delay routines to send bursts of IR light


int ir_pin = 2;
long ir_val;

void sendIRValue(){

     // start of message marker
     digitalWrite(ir_pin,HIGH);
     delay(16);
     digitalWrite(ir_pin,LOW);
     //delayMicroseconds(200);
     delay(1);
     
     for(int i=0; i<32; i++){
          int j=ir_val & 0x01;

          digitalWrite(ir_pin,HIGH);
          if(j==0){
               delay(2);
          }else{
               delay(8);
          }
          digitalWrite(ir_pin,LOW);
          //delayMicroseconds(200);
          delay(1);

          ir_val = ir_val >> 1;
     }

     // end of message marker
     digitalWrite(ir_pin,HIGH);
     delay(16);
     digitalWrite(ir_pin,LOW);
     
}

void setup() {
     // put your setup code here, to run once:
     Serial.begin(9600);

     // light the LED for a couple of seconds just so
     // we can see if it's working
     pinMode(ir_pin,OUTPUT);
     digitalWrite(ir_pin,HIGH);
     delay(2000);
     digitalWrite(ir_pin,LOW);
}

void loop() {
     // put your main code here, to run repeatedly:

     long k = random(0,256);
     long j = random(0,256);
     long     i = random(0,256);
     long h = k ^ j;
     h = h ^ i;

     Serial.print(F("sending values - k:"));
     Serial.print(k,HEX);
     Serial.print(F(" j:"));
     Serial.print(j,HEX);
     Serial.print(F(" i:"));
     Serial.print(i,HEX);     
     Serial.print(F(" checksum:"));
     Serial.print(h,HEX);
     
     k = k << 24;     
     j = j << 16;     
     i = i << 8;

     k = k | j;
     k = k | i;
     k = k | h;
     ir_val = k;

     Serial.print(F(" sent:"));
     Serial.print(ir_val,HEX);
     
     Serial.println();
     
     
     sendIRValue();

     delay(2000);
     
}


The receive routine uses interrupts to detect when the IR photo-transistor goes either low-to-high or high-to-low


int ir_in = 2;
int led_pin = 13;

long int_vcc;
long min_vcc;
long mil_ir_start;
long mil_ir_end;
long mil_ir;

long ir_val;     // we'll just make this a 32-bit value
int ir_bit_count;


// IR high and IR low are back-to-front in the receiver.
// If we're sending IR, the sensor will be low (its an open drain collector that
// pulls an input LOW when it can see IR light) So IRLow relates to the LED being lit

void IRLow(){
     // this fires on a high-to-low transition
     // whenever the line is pulled low it's because we're receving IR light
     // so reset the timer/counter
     mil_ir_start = millis();     
     digitalWrite(led_pin,HIGH);
}

void IRHigh(){
     // whenever the line floats high, its because we've just turned off the IR light
     // that is sending data to the receiver, so measure the width of the last pulse
     // and do something wisth the data if necessary
     mil_ir_end = millis();
     mil_ir = mil_ir_end - mil_ir_start;
     digitalWrite(led_pin,LOW);
     
     if(mil_ir < 11){
          Serial.print(mil_ir);
          Serial.print(F("."));
     }

     // decide what to do with the pulse width
     if(mil_ir >=1 && mil_ir <=4){
          // treat this as a zero
          ir_val = ir_val << 1;
          ir_bit_count++;
          
     }else if(mil_ir >=6 && mil_ir <=12){
          // treat this as a one
          ir_val = ir_val << 1;
          ir_val = ir_val|1;          
          ir_bit_count++;
          
     }else if(mil_ir >=14 && mil_ir <=20){
          // this is a start/end message marker
          // if we've received a message, validate it and parse
          Serial.println();
          if(ir_val != 0){ parseMessage(); }
          
          // now reset everything ready for the next blast
          ir_bit_count = 0;
          ir_val = 0;
     
     }

     
}

void parseMessage(){
     // a message can be up to three bytes long
     // we'll do simple XOR checksum on the fourth byte
     // and squash them all together

     int a = ir_val >> 24;
     int b = ir_val >> 16;
     int c = ir_val >> 8;
     int d = ir_val & 255;

     a = a & 0xFF;
     b = b & 0xFF;
     c = c & 0xFF;
     d = d & 0xFF;

     int k = a ^ b;
     k = k ^ c;
     if(k==d){
          // checksum success
          Serial.print(F("Received: "));
          Serial.print(ir_val,HEX);
          Serial.println();
          
     }else{
          // checksum fail
          Serial.print(F("checksum fail a:"));
          Serial.print(a,HEX);
          Serial.print(F(" b:"));
          Serial.print(b,HEX);
          Serial.print(F(" c:"));
          Serial.print(c,HEX);
          Serial.print(F(" d:"));
          Serial.print(d,HEX);
          Serial.println(F(" "));
          
     }
     
}

void IRChange(){
     int b = digitalRead(ir_in);
     if(b==HIGH){ IRHigh(); } else { IRLow();}
}

void setup() {
     // put your setup code here, to run once:
     Serial.begin(9600);

     pinMode(led_pin,OUTPUT);
     pinMode(ir_in,INPUT_PULLUP);

     // create an interrupt on pin 2 (IR receiver)
     attachInterrupt(digitalPinToInterrupt(ir_in), IRChange, CHANGE);
     
}

void loop() {
     // put your main code here, to run repeatedly:
     
     delay(1000);
     
}


We also added a bit of debugging to ensure that we were getting accurate data. Whenever we see a single pulse of IR light, we output the duration of the pulse. This makes it easy to debug.


8.2.8.8.8.7.7.2.2.2.8.2.9.8.8.2.7.8.7.8.8.2.2.9.2.8.7.1.7.1.1.7.
Received: BE2EF969

8.2.2.7.7.1.1.1.7.1.1.1.1.7.8.8.8.7.7.7.7.9.7.1.7.1.7.1.
checksum fail a:9 88 7F EA

8.9.7.7.1.1.1.8.1.7.2.2.2.2.2.8.2.3.2.8.2.1.2.7.7.2.7.1.1.1.1.7.
Received: F14111A1



-- interrupted here ---
2.7.8.1.7.7.
checksum fail a:0 0 0 1B




3.8.8.2.2.8.3.2.8.2.8.2.8.9.2.8.2.2.8.2.2.2.3.8.8.8.8.2.9.3.3.3.
Received: 64AD21E8

2.8.2.8.2.9.8.8.2.2.8.9.2.8.8.8.2.9.7.8.2.2.8.2.2.2.2.8.2.2.8.2.
Received: 57377212

If we take the first line and look at the length of the pulses, we can see that 8ms = 1 and 2ms = 0. So our pattern becomes

1011 1110 0010 1110 1111 1001 0110 1001

A quick binary-to-hex conversion shows us that

1011 = B
1110 = E
0010 = 2
1110 = E
1111 = F
1001 = 9
0110 = 6
1001 = 9

And that's exactly what appeared in our serial output.
So we've got some data. So we take the first byte 0xBE and XOR it with the second byte 0x2E. Then we take the result and XOR that with the third byte 0xF9 and the result.... 0x69,

And that's what our fourth byte value is. So we know we've got a valid message. If any single bit value of the message were incorrectly received, the XOR sum of the fourth byte would be different, and we'd know to throw that message away.

To prove this, we interrupted the IR signal as it was trying to send data.
At this point (see debug output above) as soon as we received a "long" pulse to indicate the end of a message, the XOR checksum failed (because the XOR sum of the bits received did not match the value of the final byte in the message).

As we're sending 32 bits with a maximum delay of 8ms, the longest time we'd spend sending a message is 256ms (actually it'd be 288ms because we have a 16ms long pulse at the start and at the end of each message). So about a third of a second to blast data across.
Often it's much less (since a zero bit value takes only 2ms to send).

So it's slow.
And crude.
But also very robust.
At least in our, specific, controlled environment.

Which means it'll do for now!


1 comment: