Serial to parallel conversion is needed when your 'main controller', such as a Raspberry Pi, has a limited number of i/o pins, and you need to drive an 8 bit parallel loading device (such as a cheap 2x16 line display). Using a PIC means you can program it to do some 'clever things' (like, show some 'splash screen' text on the display at power-on, convert incoming ascii BCD to binary, 'strobe' and wait for the display or add 'pre-fix' bits/codes etc).
PIC serial to parallel converter
When building a project using the Raspberry Pi, you will almost certainly run into the Pi I/O pin limit. This can be a real restriction, especially when you need to deliver (or receive) 8 bit parallel data.
Using the Pi serial command terminal
The Pi has a serial 'command terminal' which is very easy to use if you want to transmit data (especially from scripts). It's real power comes when debugging (just feed the TxD to a PC serial input and you can monitor exactly what's happening, which makes it really easy to 'get going' = unlike I2C etc. where you may end up being forced to use an oscilloscope to 'snoop' what's actually being sent)
To use the Pi TxD pin to send 8 bit parallel data, all we need is a chip to 'pick up' the serial data (which could be sent as a pair of ascii BCD characters to ease debugging) and 'convert' this to parallel (and generate a 'load strobe' for the destination device, such as a parallel input display)
We can, in fact, use multiple PIC 'serial to parallel converters' on the same serial line if each PIC is programed to look for a unique 'pre-fix' code** before picking up the data
** or 'slave address', which is what the 'I2C' protocol uses. It is, in fact, quite possible to program the PIC to act as an I2C slave device (and use the i2C Pi pins instead of the serial port TxD)
PIC serial input
The 'higher end' PIC's have a serial UART 'built in' - but I want to use my stock of ancient (sub-50p) 16F54/57/59 series chips :-)
This means 'doing it the hard way' = specifically writing software for the PIC that 'detects' each individual serial TxD bit, assembles the byte and interprets it as a Hex BCD character (or 'slave address', or whatever)
Serial 'receive' using software is a similar problem to serial 'transmit', or, in fact, rather harder if we want to 'sample' the serial input more than once per bit (to eliminate 'jitter' etc).
Data rates
The goal is, of course, to support the Pi 'default' command terminal 9600 'baud' = 9600 bits per second. Using a simple R-C clock 4MHz OSC, the PIC achieves a 1MHz CPU instruction rate. This is 1,000,000/9600 = 104 instructions 'per bit' which is more than enough to 'sample' the 'mid position' of every incoming bit multiple times, with enough instructions left over to both decide the 'state' of the bit and save the result before needing to sample the next.
The devil is, as usual, in the details. Specifically, we need to :-
1) Wait for the 'start bit' 2) Wait until we are 'near the middle of' the first data bit. Then, for each of 8 data bits :- 3a)'sample' each bit multiple times) 3b) decide if it is a 0 or a 1 3c) Save that bit as part of the result. 3d) Wait until 'near the middle of' the next data bit .. and so on until we have all 8 data bits.
The main problem is in waiting for the middle of the next data bit. Plainly the PIC has to count CPU CLK cycles - but the exact R-C CLK frequency depends on the actual Resistor / Capacitor values (as well as the PIC device OSC) 'spread'. So 'counting' 104 to the 'middle' of a bit is going to be 'a bit hit and miss' :-) (any error will accumulate from the start to the end of the byte sequence).
The 'obvious' solution is to 'calibrate' the PIC CLK against a 'known standard' i.e. have the Pi transmit a 'known' character that can be 'measured' by the PIC. Since we need to time 'bits' the obvious code is 0x55 ('01010101'), which is the uppercase character 'U'. So, when the Pi 'script' is launched, it will transmit an upper case 'U' (ascii code 0x55h = '01010101') which will allow the PIC to 'time' the individual bits (serial transmission starts with a '0' or 'start bit', then sends the 8 data bits (LSB first) ending with a 'stop bit' (= '1' ))
PIC R-C CLK calibration code
After being Reset, the PIC waits for the Pi to send it one or more upper case 'U' (ascii code 0x55h = '01010101') characters so it can 'calibrate' it's R-C CLK. This means looking for the 'start bit' (Serial in = Lo) then counting to the next Hi (which will be the LSB (bit 0) of the 01 01 01 01 sequence (serial RS232 characters are transmitted LSB first).
The max. R-C CLK is 4MHz, and may well be less. 4MHz gives a CPU CLK of 1MHz, so at 9600 baud each bit corresponds to 104 CPU CLKs. Assuming the Timer/Counter is et to count bit times in CPU CLK's, the maximum count we can cope with is 255. This corresponds to a minimum R-C OSC of 9600*255*4 = 1.6MHz. So, setting the R-C OSC to at least 2 MHz will be fine
Note that the Timer/Counter is used for compatibility with higher end PIC devices (that support Interrupts). Instead of using TMR0, you could just count the 'wait loop' cycles (using an INC register instruction).
;
; The Timer/Counter can be used to check how many CLK cycles have elapsed whilst waiting for the next bit (we expect 'about 104')
; To set the TMR0 'pre-scaler' to 'none' register OPTION b3 is set to '1'. To have it count CPU CLK's (rather than Timer Clk in pin), register OPTION b5 is set to 0.
; (the other Option bits can be ignored, but are set to '1' at power-on)
LOAD Acc, 0x0F ;b5 0, b3 1, others don't care
COPY Acc,rOPTION ;set the counter to count each CPU clk
;
; we want to find the average count using only 8 bit registers
; There is no 'add reg to reg' instruction, so we DO THE SUMS IN PAIRS, saving the first to a Reg, then adding the second from the ACC
; RotateRight (from Cy) after the add will correctly sum 2 values up to 255 each
;
; It's slightly faster to use rINDF / rFSR (indirect reg. / pointer) addressing
;
; Start by pointing at 4 reg partial result stack
LOAD Acc,rBase ;(the other 3 registers are rBase1, rBase2, rBase3)
COPY Acc,rFSR
;
; PORT B, Bit0 will be the serial input
; 'BTest and skip on Z' is used to test for a 0 (start bit)
; If Z (start bit) is not found, the next instruction, a 'jump back and test again', is executed
SkipZ, PORTB,b0 ;test and skip if start bit found
JMP $-1 ;no start bit, keep waiting
; OK we have the start bit, which we can time, along with the following '1' bit (LSB of 'U')
; set Acc=count adjustment (number of CLK ticks we have 'seen' of this bit)
LOAD Acc,0x06 ;(skip 2, this load 1, the call 2, and load TMR 1)
Call doPair ;do a pair of bits, average of these in rBase, on return Acc=count adjust for next call
Call doPair ;do a pair of bits, average of these in rBase1
Call doPair ;do a pair of bits, average of these in rBase2
Call doPair ;do a pair of bits, average of these in rBase3
;
; OK, now ignore the rest of the serial input and calc. the average
; (here it's faster to use direct reg addressing)
COPY rBase3,Acc ;get r3
ADD Acc,rBase2 ;add into r2
RotR rBase2 ; part sum to r2
; done last pair, now the first
COPY rBase1,Acc ;get r1
ADD Acc,rBase ;add into r0
RotR rBase ; part sum to r0
; finally, sum the two partials
COPY rBase1,Acc ;get r2
ADD Acc,rBase ;add into r0
RotR rBase ; Final average is now in r0
;
; Now continue (jump or drop through) into the mainline code ....
;
...
...
; Subroutone to time two sucessive bits (and calc their average), starting with a 0 bit, then a 1 (which ends on another 0)
doPair:
; Called to do a pair (rest of a 0 followed by a whole 1 bit), with Acc=count adjust
COPY Acc,CTR0 ; restart the counter
SkipNZ, PORTB,b0 ;time the 0 (skip when 1 found)
JMP $-1 ;still 0, else keep waiting
COPY TMR0,Acc ;got 1, get the count
COPY Acc,rINDF ;save to reg
LOAD Acc,0x06 ;next count adjust (skip 2, copy x2, this Load 1, count load 1)
COPY Acc,CTR0 ; restart the count
SkipZ, PORTB,b0 ;time the 1 (skip on 0)
JMP $-1 ;still 1, else keep waiting
COPY TMR0,Acc ;got 0, get the count
ADD Acc,rINDF ;add to reg
RotR rINDF ; average the reg
INC rINDF ; aim at next reg
Return 0x0B ;exit with Acc=new timer adjust (2 skip, 1 copy, 1 add, 1 Rot, 1 Inc, 2 Ret, 2 Call, 1 load TMR = 11)
;
One problem is that 'by default' the Pi uses it's Serial port as a 'debug terminal'. This means it sends out lots of 'status' data during it's power-on sequence, which (if the PIC is enabled at the same time as the Pi) would 'misslead' the PIC bit count. The 'obvious' way to address this is to wire the PIC 'Reset' line to one of the Pi's GPIO lines - and have the Pi 'reset' the PIC just before transmitting the 'U'
Once the PIC has the 'average' bit count, it can start to look for data characters (by waiting for a 'strat bit' then 'sampling' the serial input at each 'half bit' position).
Bit Sampling code
Whilst 'Decrement register' loop count could be used to 'time' the mid-bit sample position (and woould work fine, since we can assume that any higher end PIC used as a serial to parrallel converter will have it's Interrupts turned off duing the 'recieve' cycle), using the timer/counter avoids the need to keep adjusting the loop count when the code is modifyed
;
; Wait for the start bit (Lo)
CALL startBit ; To eliminate glitches, this checks multiple times
; on Return, we are half way into the start bit ... so just do 8 bits
CALL doBit ;0 (lsb) - each new bit is shifted into LSB of rTemp
CALL doBit ;1
CALL doBit ;2
CALL doBit ;3
CALL doBit ;4
CALL doBit ;5
CALL doBit ;6
CALL doBit ;7 (msb)
; OK got the byte, can ignore serial in for now
; Subroutine to wait for the next start bit
; uses TMR0 to exit in the middle of the start bit
startBit:
ROTR rBase,Acc ;get half the bit time to Acc
COPY Acc,rTemp
INV rTemp,Acc ;Timer can't count down, so we will invert & count up
;
; start the loop - on exit, serial in is Lo and timer counted out
sbwait:
SkipZ, PORTB,b0 ;test and skip if start bit found
JMP sbwait ;no start bit, keep waiting
; count is in Acc, start counter now
COPY Acc,TMR0
; keep checking until TMR counts out
swait1:
SkipZ, PORTB,b0 ;eliminate glitch (skip if start bit still Lo)
JMP sbwait ;lost the start bit, go back to wait
SkipZ, TMR0,b7 ;TMR is counting up to 0, so start by waiting for it to exceed 0x80
JMP swait1
swait0:
SkipZ, PORTB,b0 ;eliminate glitch (skip if start bit still Lo)
JMP sbwait ;lost the start bit, go back to wait
SkipZ, TMR0,b7 ;TMR count is above 0x80, wait for it to roll over
JMP swait0
RETURN ;Done
; Subroutine to shift new bit into rTemp
doBit:
INV rBase,Acc ;get the bit count (as a count up)
ADD rOffset,Acc ; add the offset (fiddle factor, allows for all this code)
COPY Acc,TMR0 ; start counting - on time out we have reached 'near the bit middle'
dbwait1:
SkipNZ, TMR0,b7 ;wait for TMR count to reach 0x80
JMP dbwait1
dbwait0:
SkipZ, TMR0,b7 ;TMR count is above 0x80, wait for it to roll over
JMP dbwait0
; OK, now in middle of bit, do some samples, decide bit, exit
Next page :- 5x8 character set