Thursday, 31 October 2013

Transmitting Manchester encoded data over a single wire using PIC 16F1825 - VirtualWire for PIC

As we started to put together a receiver for our Manchester encoded transmitter, something started to really niggle. It wasn't a bit niggle, but a niggle all the same. See, we'd started to write our receiver using interrupts - to make sure that it was receiving and processing the data in a timely manner. But we also knew that because the transmitter was using a software-based delay, it had the possibility of a slight drift, especially when sending long streams of data.

It was probably nothing. It probably wouldn't make any difference at all. But since this was also an exercise in understanding how the single wire interface worked, not just a task to get a job done (after all, we'd already successfully implemented the VirtualWire over RF for our toy guns project, using an Arduino and a library of existing code) we thought that perhaps it was time to do it properly.

So here's a single wire transmitter, using Manchester encoding, and Timer1 interrupts.
It's calling a single function every time the interrupt fires, which toggles the data pin high or low as soon as possible after the interrupt has fired. So even if there is a tiny delay between the interrupt firing and the pin changing, there's no chance of drift - the timer1 interrupt ensures that however long the delay is, the next pin change will happen in a timely manner; effectively we've removed any possible cumulative error when sending multiple bytes.

#include <system.h>
// ##################################
// using a 32 Mhz internal oscillator
// on a PIC16F1825
#pragma DATA _CONFIG1, 0x0804
#pragma DATA _CONFIG2, 0x1DFF
#pragma CLOCK_FREQ 32000000
// ##################################

#define button portc.2
#define testing porta.5

#define dataPin porta.1
#define ledRed portc.5
#define ledGreen portc.4
#define ledBlue portc.3

char c;
char xorsum;
char dataBuffer[6];
char bitsSent=0;
bit bitFlag=0;
char byteToSend=0;
char bytesSent=0;
char byteIndex=0;
bit sendNextByte=0;
bit pinState;
bit sending=0;

// program initialise
void initialise(){
   
     osccon=0b11110000;            // oscillator=32mhz
     delay_ms(50);                  // wait for internal voltages to settle
     
     intcon=0b11000000;            // global peripheral interrupts enabled
     apfcon0=0b01000000;            // alternative tx/rx for pickit2/uart
     wpua=0xFF;                        // any inputs on portA have weak pull-ups
     wpuc=0xFF;                        // any inputs on portC have weak pull-ups
     trisa=0x00;                        // portA is all outputs
     trisc=0b00000100;            // RC.2 is an input (push button)
     ansela=0x00;                  // turn off analogue pins on portA
     anselc=0x00;                  // turn off analogue pins on portC
     porta=0x00;                        // set all output pins on portA off
     portc=0x00;                        // turn off all output pins on portC

     option_reg=0x00;
     option_reg.7=0;                  // weak pull-ups enabled on any input pins

     // set up timer1
     t1con=0b00001000;            // set bit zero of t1con to start the timer
     pie1.TMR1IE=1;                  // enable the timer1 interrupt

     // enable the global and peripheral interrupts
     intcon.GIE=1;
     intcon.PEIE=1;
}

// set timer1 value so it fires every X-uS
void preloadTimer1(){
     // at 32Mhz, fosc/4=8million
     // our radio modules are rated *up to* 4khz
     // so let's stick to about half that for now
     // 8m/2000 = 4000
     // so we want our timer1 to only count up to 4000
     // timer1 is a 16-bit timer (0-65535) so we need to
     // preload it with 65535-4000 = 61535 (0xF05F)
     tmr1h=0xF0;
     tmr1l=0x5F;
}

void startTimer(){
     // clear any left over interrupt flag
     pir1.TMR1IF=0;
     // set bit zero to start the timer
     t1con.0=1;
}

void stopTimer(){
     // clear bit zero to stop the timer
     t1con.0=0;
}

void endTransmission(){
     // first, kill the timer
     stopTimer();
   
     // now wait a timer-width before pulling the
     // data line low (in case we've only just pulled
     // it high)
     delay_100us(5);
   
     // pull the data pin low (end of transmission)
     dataPin=0;

     // turn off any transmission leds
     ledBlue=0;          

     // kill the sending flag
     delay_ms(5);
     sending=0;    
}

void sendBit(){

     if(bitFlag==0){
           // work out which way the bit should be sent
           // (start low for bit value 1, start high for bit value zero)
         
           // for MSB first, use this
           if(byteToSend & 0b10000000){pinState=1;}else{pinState=0;}
         
           // for LSB first, use this
           // (MSB first makes it easier to "see" the data on a logic analyser)
           //pinState=byteToSend & 1;
         
           // set the pin according to the bit value
           dataPin=pinState;
           bitFlag=1;
         
     }else{
   
           // it doesn't matter which way the pin is, just toggle it
           // the other way
           pinState=!pinState;
           dataPin=pinState;
         
           // keep track of the number of bits sent
           bitsSent++;
           if(bitsSent>=8){
                 sendNextByte=1;
           }else{
                 // nudge up the value in the byteToSend buffer
                 // for MSB first, shift left
                 byteToSend=byteToSend<<1;
               
                 // for LSB first, shift right
                 // byteToSend=byteToSend>>1;
               
                 sendNextByte=0;
           }
         
           bitFlag=0;
     }
   
     if(sendNextByte==1){
           byteIndex++;
           if(byteIndex>=6){
                 // we've sent three bytes (6 inc preamble + crc)
                 // so end transmission
                 endTransmission();
           }else{
                 // prepare the next byte for transmission
                 byteToSend=dataBuffer[byteIndex];
                 bitsSent=0;
                 bitFlag=0;
           }
           sendNextByte=0;
     }
}

// send some data
void sendData(unsigned char i1, unsigned char i2, unsigned char i3){

     sending=1;
   
     // send a manchester standard preamble
     // alternate 10101010 a few times
     // then end the preamble with 10101011
     // (two ones indicate end of preamble)
     dataBuffer[0]=0b10101010;
     dataBuffer[1]=0b10101011;
   
     // fill the data buffer with new data to be sent
     dataBuffer[2]=i1;
     dataBuffer[3]=i2;
     dataBuffer[4]=i3;
   
     // create an XOR sum and put this in the last byte
     c=0;
     c^=i1;
     c^=i2;
     c^=i3;
     dataBuffer[5]=c;
   
     // now start the pin wiggling
     dataPin=0;
     byteIndex=0;
     byteToSend=dataBuffer[0];
     bitsSent=0;
     bitFlag=0;          
     sendNextByte=0;
   
     // start timer1 and the interrupt will send the data
     preloadTimer1();
     startTimer();
   
     // this is a blocking function
     while(sending==1){}
}

// main program executes from here
void main(){

     initialise();
   
     // at rest, the data line is low
     dataPin=0;
   
     // this is the main code loop
     while(1){
           if(button==0){
                 // debounce
                 delay_ms(10);
                 if(button==0){
               
                       // light the led to show we've aknowledged user action
                       ledBlue=1;
                     
                       // wait for release
                       while(button==0){}
                     
                       // send some data
                       // (values chosen to be a mix of 1 and 0 in binary)
                       sendData(179,227,24);
                       ledBlue=0;
                     
                 }
           }
     }
}

void interrupt(){
     // every time timer1 fires, we send another part of a bit
     // (in Manchester encoding). Bit zero of pir1 is tmr1 overflow interrupt
     if(pir1.TMR1IF==1){
           // clear the timer1 interrupt flag
           pir1.TMR1IF=0;
           // preload timer1 to get the next interrupt to occur in a timely manner
           preloadTimer1();

           // send the appropriate bit-part
           sendBit();
     }
}

We changed over to the 16F1825 chip for this, just to aid debugging.
We've used these before and one handy register they have is the APFCON0 register, which allows you to map the UART TX and RX lines onto the same pins as used for programming the chip. This means that when running code, you can add in some serial debugging and have the PICKit2 programmer report back any data received, without having to move jumpers or wires all over the place.

Although we've no serial debugging in this example, our receiver will need lots of debugging, so we're sticking with these 16F1825 chips for now. Also, we can run them at 32Mhz, meaning we get a few extra clock cycles between the timer1 interrupts  firing (on an 8Mhz chip, we get a quarter of available clock cycles between interrupts firing to do any processing).

You can use pretty much any chip you like, just change the include and pragma statements at the head of the code to run on your own favourite chip (and maybe the TRIS registers to map the i/o to different pins if necessary).

Anyway, with the code in place, it was time to do some testing. We only recently discovered a really useful tool in the PICKit2 software. And it really is useful - it's a logic analyser. We've used one of these before, and it was invaluable for debugging our sd card initialisation routines about 12 months ago. But it's only recently we discovered that the PICKit2 has a logic analyser built right in! To use the analyser, we simply made our dataOut pin on the transmitter the same as one of the programming pins on the PICKit2.

Now we can simply start our transmitter running, put a test condition in to tell the PICKit2 when to start taking samples, and hit the button to actually send the data.


Here we're using the PICKit2 programmer to provide power to our two PICs (one is the transmitter, one will be the receiver).


One thing to remember is to turn on the voltage supply to the PIC before starting the analyser tool. Our clone programmer doesn't have a cancel button on it. Forget to actually turn the voltage on and the PIC never sends a signal to start the analyser running. This can cause the application to hang and require Task Manager to sort the mess out!

The other highlighted section of this screenshot is the trigger condition. We made our dataPin RA.1 which is also one of the programming pins. So we set up our logic tool to monitor "channel 2" (which we have connected to RA.1 on the transmitter) and we're going to start recording data on a falling edge.


With everything hooked up, it's time to set the logic probe running and hit the transmit button. Below is a composite screenshot of the results


The top part of the image is the data as displayed on the software logic probe. We changed our tranmitter to send the most significant bit (MSB) first, so that the data would be more easily "seen" on our logic probe. At first it just looks like a load of peaks and troughs - we can see we're getting some data through!

The lower part of the image is the same graph, only this time, we've highlighted the high-to-low transition than indicates a bit value of one (the remaining un-highlighted parts show the low-to-high transitions that indicate a bit value of zero).

Now we can actually see the data in binary form. If we decode this binary data back into it's decimal equivalents, we can read back the data, exactly as it was sent from the transmitter.

Now we've got a working single-wire transmitter that can send data using the Manchester encoding, using timer1 interrupts so there's no cumulative drift error. At least now, when it comes to debugging the receiver, we won't have any niggling doubts about whether any problems are with the code in the receiver, or actually  an inherent problem with the transmitter. As far as we can see, the transmitter is as "tight" as it can be, and the data should be transmitted consistently with no drift error. It's not going to make debugging the receiver any easier but at least it's one less thing to eliminate if/when it all starts to go horribly wrong!