Tuesday, 11 February 2014

Receiving Manchester-encoded data over a single wire using Oshonsoft

Having built a single-wire transmitter, it's time to receive the encoded data.
We're running this code on a PIC16F883 on it's own internal oscillator at 8Mhz. The code includes error checking, but not correction- i.e. if a message received is junk, it's simply discarded. To increase stability, it may be preferable to run the mcu off a crystal, to ensure accurate timing:


Define CONFIG = 0x20c4
Define CONFIG2 = 0x3eff
Define CLOCK_FREQUENCY = 8
OSCCON = OSCCON Or 01110000b
AllDigital

declarations:
     Dim buttondown As Bit
     Symbol button = PORTB.0
     Symbol led_red = PORTC.4
     Symbol led_green = PORTC.2
     Symbol led_blue = PORTC.3
     Symbol data_pin = PORTB.5
     Symbol tx = PORTB.7
   
     ConfigPin PORTC = Output
     ConfigPin button = Input
     ConfigPin data_pin = Input
               
     Dim prev_pin_state As Bit
     Dim received_bit As Bit
     Dim last_received_bit As Bit
     Dim received_byte As Byte
     Dim bitflag As Bit
     Dim bits_received As Byte
     Dim bytes_received As Byte
     Dim preamble_count As Byte
   
     Dim receive_buffer(6) As Byte
     Dim state As Byte
     Dim k As Byte
         
init:
     PORTC = 0
     OPTION_REG.7 = 0 'enable pull ups on portb (RBPU)
   
     'start up serial output
     '(we'll use software serial for ease when running with
     'the PicKit2 programmer)
     ConfigPin tx = Output

     High led_blue
     WaitMs 255
     WaitMs 255
     Low led_blue

     Gosub setuptimer1
     Serout tx, 9600, "Ready to receive..."
         
     'start up the state machine
     state = 0
     'our inputs have pull-ups on them so the prev_pin_state
     'for the dataPin should be high be default
     prev_pin_state = 0
   
loop:

     Select Case state
   
           '---------------------------------------------
           'just idling to see if we've had any data yet
           Case 0
           '---------------------------------------------
           If data_pin = 0 And prev_pin_state = 1 Then
                 'hey up! something's pulled the data pin low,
                 'it could be the start of a preamble
                 state = 1
                 bitflag = 0
                 received_byte = 0
                 bits_received = 0
                 bytes_received = 0
                 last_received_bit = 0
                 preamble_count = 0
                 Gosub ledsoff
                 Gosub starttimer1
                           
           Else
                 'I suppose we could see if the user has pressed a button
                 'for testing
                 Gosub getinput
                       If buttondown = 1 Then
                       Gosub flashled
                 Endif
           Endif
           prev_pin_state = data_pin
   
           '----------------------------------------------------
           'something's twitched on the data pin line, so better
           'receive some data in
           Case 1
           '----------------------------------------------------
           'this is handled with the timer1 interrupt handler

           '--------------------------------------------
           'end of transmission or data receive time out
           Case 3
           '--------------------------------------------
           'validate the output
           k = 0
           k = k Xor receive_buffer(0)
           k = k Xor receive_buffer(1)
           k = k Xor receive_buffer(2)
           If k = receive_buffer(3) Then
                 'cool, checksums add up
                 Serout tx, 9600, receive_buffer(0)
                 Serout tx, 9600, receive_buffer(1)
                 Serout tx, 9600, receive_buffer(2)
                 Serout tx, 9600, receive_buffer(3)
           Else
                 High led_red
           Endif
         
           state = 4
               
           Case 4
           prev_pin_state = 0
           state = 0

     EndSelect
Goto loop
End

getinput:
     buttondown = 0
     If button = 0 Then
           'debounce the button input
           WaitMs 10
           If button = 0 Then
                 While button = 0
                       'do nothing
                 Wend
                 buttondown = 1
           Endif
     Endif
Return

ledsoff:
     Low led_red
     Low led_green
     Low led_blue
Return

flashled:
     Gosub ledsoff
     WaitMs 50
     High led_red
     WaitMs 200

     Gosub ledsoff
     WaitMs 50
     High led_green
     WaitMs 200
   
     Gosub ledsoff
     WaitMs 50
     High led_blue
     WaitMs 200

     Gosub ledsoff
Return

setuptimer1:
     T1CON = 00001000b 'set bit zero to start the timer

     INTCON.GIE = 1
     INTCON.PEIE = 1

     PIE1.TMR1IE = 1
     PIR1.TMR1IF = 0
     T1CON.TMR1ON = 0
Return

preloadtimer1:
     'at 8mhz, fosc/4=2Mhz (2,000,000)
     'our radio modules are rated up to 4khz
     'so let's use about half that, just to be safe
     'so 2m/2k = 1,000
     'we want our counter to go up to 1,000 then
     'send another pulse
     '65535-2000 = 63535 (0xF82F)
     '65535-1000 = 64535 (0xFC17)
     TMR1H = 0xf8
     TMR1L = 0x2f
Return

starttimer1:
     'in case the timer is already running, stop it
     T1CON.TMR1ON = 0
     'reset the timer1 interrupt flag
     PIR1.TMR1IF = 0
     'preload with the correct value
     Gosub preloadtimer1
     'start the timer
     T1CON.TMR1ON = 1
Return

stoptimer1:
     'stop timer1
     T1CON.TMR1ON = 0
     'reset the timer1 interrupt flag
     PIR1.TMR1IF = 0
Return

getbit:
     If bitflag = 0 Then
           'this is fine
           prev_pin_state = data_pin
           bitflag = 1
     Else
           'in the second half of any bit, there
           'should be a transition on the data line
           '(if there's not, we're out of sync)
           received_bit = 0
         
           If prev_pin_state <> data_pin Then
                 'this is awesome. We've had a transition
                 'on the second half of the bit, just as we expect
                 If data_pin = 0 Then
                       'this is a one bit
                       received_bit = 1
                 Endif
           Else
                 'something has gone weird
                 'maybe we're just out of sync?
                 High led_red
           Endif
         
           prev_pin_state = data_pin
           bitflag = 0
         
           'now decode the bit that's just come in
           If preamble_count < 32 Then
                 '--------------------------------------------------
                 'we're still waiting for 1010101 etc followed by 11
                 '--------------------------------------------------
                 If last_received_bit = 1 And received_bit = 1 And preamble_count > 8 Then
                       'here's our 11 at the end of the preamble!
                       preamble_count = 99
                       received_byte = 0
                       bits_received = 0
                       bytes_received = 0
                       High led_blue
                 Else
                       'we're looking for a transition from 01 or 10
                       If last_received_bit <> received_bit Then
                             preamble_count = preamble_count + 1
                       Else
                             'we've had two bits of the same value but not
                             'preceded by 010101 etc so reset the preamble count
                             preamble_count = 0
                       Endif
                 Endif
           Else
                 '-------------------------------------------------------
                 'we're into the meaty stuff now, actually receiving data
                 '-------------------------------------------------------
                 If received_bit = 1 Then
                       'write this bit to the received byte value
                       received_byte = received_byte Or 0x01
                 Endif
               
                 bits_received = bits_received + 1
                 If bits_received >= 8 Then
                       'put the received_byte into the bytebuffer
                       receive_buffer(bytes_received) = received_byte
                       bytes_received = bytes_received + 1
                       received_byte = 0
                       bits_received = 0
                     
                       If bytes_received >= 4 Then
                             Gosub stoptimer1
                             state = 3
                       Endif
                 Else
                       'prepare for the next incoming bit
                       'shunt the received_byte value along
                       '(whether we received a one or a zero)
                       received_byte = ShiftLeft(received_byte, 1)
                 Endif
           Endif
         
           last_received_bit = received_bit
         
     Endif
Return

On Interrupt
     Save System

     If PIR1.TMR1IF = 1 Then
           'clear the interrupt flag for timer1
           PIR1.TMR1IF = 0
           Gosub preloadtimer1
           Gosub getbit
     Endif
Resume

We've tested the receiver a few times and get consistently good results.
However, the RC internal oscillator in a PIC is accurate to about 1% but can drift over time and with changes in temperature. If you're likely to have the sender and receiver in two different places (we're planning on using these routines to send data via RF link between two controllers in different rooms in the house) it might be worthwhile adding a precision cut crystal, just to ensure exact timing between the two mcus.