How the code is designed
With a small project like this, there will be one 'main loop' supported by a number of 'stand alone' functions coded as subroutines.
Get tick count Subroutine(s)
Called many times, this waits for the Sense Hi, then for sense Lo, gets the count to Acc, and returns
NextTick:
TEST PORTA,bitN,skipNZ ; Bit Hi yet ?
GOTO $-1 ; No, keep looking
TEST PORTA,bitN,skipNZ ; Still Hi ?
GOTO NextTick: ; No, must be bounce, keep looking
; fall through to GetTick
GetTick:
TEST PORTA,bitN,skipZ ; Bit Lo yet ?
GOTO $-1 ; No, keep looking
COPY CTR0,Acc ; Get the count
COPY Acc,Count ; save to count reg
RETURN ; exit
The 'convert to ascii' code
It is tempting to make 'convert to ascii' a subroutine that in turn calls the 'send a character' subroutine. However the PIC16F54 only has a 2 level subroutine 'return stack' = and the TxD subroutine will need one of those levels for it's delay loop (see below).
However, whilst 'send a character' needs to be used between up to four times per value (3 numeric chars and a comma), the 'convert to ascii' code only happens 'once', so it can be included 'in-line' with the main code.
Whilst multiple methods exist to convert from binary to decimal (the 'shift and add 3' being one), the 'fastest' is to just to examine the value
;
; To minimise register use, we start by extracting the hundreds (if any) and transmit the value.
; The same register can then be used to extract the tens (and send that), and finally used a 3rd time to send the units.
; Each digit is sent as an ascii value (0x30-0x39), and transmission ends with a 'comma' (0x2C)
;
LOAD Acc,0x30 ; ascii code for 0
MOV Acc,TxDATA ; into transmit (i.e 0 the hundreds)
; Value in Count is 0-255. PIC ALU regards >127 as -ve, so we have to deal with top bit first
GOTO dealb7:
inc100:
LOAD Acc,-100 ;we are goig to add -100 to Count
inc101:
INC TxDATA ; inc the hundreds count
CLR Count,b7 ; clr bit 7 (it may already have been clear, if not we are subtracting 128)
ADD Acc,Count ; add Acc (which is -100 or 28), i.e. add back the residual from a 128 subtract
;
dealb7:
LOAD Acc,28 ; set Acc = 28 (0001 1100), residual of b7 (=128)-100
Skip Count,b7,Clr ; skip if count b7 is clear (i.e. Count is 0-127)
GOTO inc101 ; b7 is set, increment the hundreds, Clr b7 and add back 28
; OK here Count is <127 (but may still be >99), to find out, add 28 and test bit7
ADD Acc,Count,Acc ; Count+Acc (which had better be 28) to Acc
Skip Acc,b7,Clr ; skip if ADD to Acc didn't overflow
GOTO inc100 ; >99, increment 100 and deduct 100 from Count
; Arrive here 99 or less, so we have the 100's, send it
CALL TxD ; transmit the 100's
; OK now for the 10's (9 loops of -10)
LOAD Acc,0x30 ; ascii code for 0
MOVE Acc,TxDATA ; set val = 0
top10:
LOAD Acc,10 ;
SUB Acc,Count,Acc ; subtract 10 from Cont, result to Acc
; If CY clear, we overflowed
Skip CY ; skip if carry set i.e. still OK
GOTO Done10: ; OK <10, exit loop
MOV Acc,Count ; save the reduced count
INC TxDATA ; inc the 10's
GOTO top10: ; and loop back
Done10:
CALL TxD ; transmit the 10's
LOAD Acc,0x30 ; ascii code for 0
MOVE Acc,TxDATA ; set val = 0
MOVE Count,Acc ; get the residual digits
ADD Acc,TxDATA ; load digits for Tx
CALL TxD ; transmit the 1's
LOAD Acc,0x2C ; ASCII ,
MOVE Acc,TxDATA ; load to Tx
CALL TxD ; transmit the ,
; OK, thats all
The serial comms subroutine
The PIC16F54 has no 'interrupt' function. Whilst this might seem a big disadvantage, when it comes to coding the Serial Transmit subroutine, knowing that nothing can actually 'interrupt' our careful timing calculations is a big plus !
remember - the PIC16F54 is running from a click crystal of 32,768 Hz. One CPU CLK is 4 ticks, so 8192 CLKs is 1 second. At 300 baud, 1 bit is 27.306r CLKs.
The subroutine is 'called' to send 1 character (from a register) at a time. Two additional registers are used to control the timing - one to control the 'inter-bit' delays (below) and the other to count the bits sent (one start, 8 data, exit after half a stop bit)
We need to send at an 'average' of 27.31 instructions per bit. The receiver will sync on the 'start bit' leading edge, so after sending 2 bits at 27 clocks, we will send one at 28 (and then 2 at 27, one 28 and so on).
The 'bit error' sequence (in CLKs) will thus be as follows = +0.31 (start bit), +0.61 (b0), -0.08 (b1), +0.23 (b2), +0.53 (b3), -0.16 (b4), +0.15 (b5), +0.45 (b7), -0.24 (b7).
The maximum error is at b0 = +0.61 CLK, which should be well within the receiving UART tolerance (most UARTs sample at 16x baud rate ie. at 8192/(300*16) = 1.7 CLK intervals, so it will never 'see' a slip of 0.61 CLK)
Note that generating the required CLK count (delay loop) is a bit of a pain (NOP is 1 CLK, GOTO $+1 is 2 CLK, DEC Reg,skip on zero / GOTO $-1 is 3 CLK (is 1 for Dec& not skipping, 2 for goto), a 'dummy' Call SUB (aimed at this subs' own RETURN) is 4 CLKs)
NB. TMR0 is running on a 'divide (CPU CLK) by 64' pre-scaler so no good 'as is' for TxD bit timing (bit time is 27.3 CLK). In theory it would be possible to save the current TMR value, change the pre-scaler (to x1), use the TMR to control bit timings, then restore the pre-scaler (x64) and saved TMR value (with a suitable adjustment) afterward - however this risks upsetting the 'wheel tick' measurements (i.e. the source of the values we are sending :-) ). A quick bit of coding shows this will require more instructions than 'padding' the loop with some dummy CALL's etc.
; Called with character to send in reg=TxDATA (numeric value 0-9) to send is acsii 0x30 to 0x39 or a 'comma' = 0x2C)
; Uses 2 working registers TxLOOP (8 bit Tx loop) and TxBITD (bit delay pattern) and one extra subroutine level
; (note = serial links send LSB first)
;
; To send a bit (discover hi/lo, set the timing) 'costs' about 8 instructions ea., if coded 'in line'
; An in-line stack would take a few instructions under 80 to send all 10bits (start bit, 8data, 1stop)
; Since we only have 512 instructions available, I decided to write a 'loop'
; this 'costs' 6 to set up the loop, 18 to loop (of which 7 instructions are timing) for a total of 24
;
; The Loop counter is used to set the stop bit (Carry)
; Note = only bit set/clear, add/subtract and rotate effect the CY bit (INC and DEC do not)
TxD:
LOAD Acc,11011011 ;inter-bit +1 delay pattern (0 is +1 flag so 27,27,28,27,27,28,27,27, (Clr CYin) 28)
COPY Acc,TxBITD ;set the delay pattern
LOAD Acc,-8 ;bit count (we are going to add toward 0, via Carry)
; count will be F1000, F1001, F1010, F1011, F1100, F1101 , F1110, F1111, CY&Z (0)
COPY Acc,TxLOOP ;F1000 to counter
BCLR CY ;clear the CY bit
GOTO Tx3 ;send the first bit (note = 7 clk since entry so would be OK for back to back Tx)
Tx1:
TEST TxLOOP,skipNZ ; if count reached zero, exit, all done (2 clk if skipping)
Tx2: ;we are going to 'call' here in order to generate a 4 clk delay (Call is 2 clk, Return is 2 clk)
RETURN ; (delay by 4 clks each time Tx2 is called)
; OK, count up toward zero (through CY)
LOAD Acc,1 ; load 1 (1 clk)
ADD Acc,TxLOOP ; Add to loop count = we have to do ADD, not INC, because we want the CY set (1 clk)
SKIP CY ; Skip next if the ADD set CY = we reached the stop bit (1 clk/2 clk)
RRCY TxDATA ; rotate the data right through CY (unless we reached the stop bit) (1 clk / 0 clk)
Tx3: ;
; Send the CY bit - we are now 6 clk since Tx1 (loop) or 7 clk since TxD (subroutine entry point)
MOVE PORTB,Acc ;get current data PORTB (is all o/p, bit 7 is TxD) 1clk
BITCLR Acc,7 ; clear bit 7 (assume 0) 1clk
SKIP CY ; check if Cy set (bit =1) (1clk / 2clk)
BITSET Acc,7 ; yes it is, set the bit (1clk / 0 clk)
; (here is 11 clks after TxD entry, so we can exit 11 clks 'early' and still send back-to-back bytes)
MOVE Acc,PORTB ;initialise transmission of the bit (1clk)
;
; now 11 clk since Tx1, delay 12 (with 3x sub calls to Tx3 = Return)
CALL Tx2 ;4clk
CALL Tx2 ;4clk
CALL Tx2 ;4clk
; now 23 since Tx1, check the BitDelay pattern (27,27,28) for +1clk (so 23 +4 or +5 to get 27 or 28)
RRCY TxBITD ; rotate bit delay pattern via carry (note, start bit =CY clr, so TxBITD gets a 0 in MSB on first Rot) 1clk
SKIP CY ; skip if carry = no extra clock (1clk), or skip if extra (2clk)
GOTO Tx1 ; no extra, goto next bit (2 clk), so tot +4 = 1 rot, 1 no skip, 2 goto
GOTO Tx1 ; extra, goto next bit (2 clk), so tot +5 = 1 rot, 2 skip, 2 goto)
; done
; Called with character to send in reg=TxDATA (numeric value 0-9) to send is acsii 0x30 to 0x39 or a 'comma' = 0x2C)
; Uses 2 working registers TxLOOP (8 bit Tx loop) and TxBITD (bit delay pattern) and one extra subroutine level
; (note = serial links send LSB first)
;
; To send a bit (discover hi/lo, set the timing) 'costs' about 8 instructions ea., if coded 'in line'
; An in-line stack would take a few instructions under 80 to send all 10bits (start bit, 8data, 1stop)
; Since we only have 512 instructions available, I decided to write a 'loop'
; this 'costs' 6 to set up the loop, 18 to loop (of which 7 instructions are timing) for a total of 24
;
; The Loop counter is used to set the stop bit (Carry)
; Note = only bit set/clear, add/subtract and rotate effect the CY bit (INC and DEC do not)
TxD:
LOAD Acc,11011011 ;inter-bit +1 delay pattern (0 is +1 flag so 27,27,28,27,27,28,27,27, (Clr CYin) 28)
COPY Acc,TxBITD ;set the delay pattern
LOAD Acc,-8 ;bit count (we are going to add toward 0, via Carry)
; count will be F1000, F1001, F1010, F1011, F1100, F1101 , F1110, F1111, CY&Z (0)
COPY Acc,TxLOOP ;F1000 to counter
BCLR CY ;clear the CY bit
GOTO Tx3 ;send the first bit (note = 7 clk since entry so would be OK for back to back Tx)
Tx1:
TEST TxLOOP,skipNZ ; if count reached zero, exit, all done (2 clk if skipping)
Tx2: ;we are going to 'call' here in order to generate a 4 clk delay (Call is 2 clk, Return is 2 clk)
RETURN ; (delay by 4 clks each time Tx2 is called)
; OK, count up toward zero (through CY)
LOAD Acc,1 ; load 1 (1 clk)
ADD Acc,TxLOOP ; Add to loop count = we have to do ADD, not INC, because we want the CY set (1 clk)
SKIP CY ; Skip next if the ADD set CY = we reached the stop bit (1 clk/2 clk)
RRCY TxDATA ; rotate the data right through CY (unless we reached the stop bit) (1 clk / 0 clk)
Tx3: ;
; Send the CY bit - we are now 6 clk since Tx1 (loop) or 7 clk since TxD (subroutine entry point)
MOVE PORTB,Acc ;get current data PORTB (is all o/p, bit 7 is TxD) 1clk
BITCLR Acc,7 ; clear bit 7 (assume 0) 1clk
SKIP CY ; check if Cy set (bit =1) (1clk / 2clk)
BITSET Acc,7 ; yes it is, set the bit (1clk / 0 clk)
; (here is 11 clks after TxD entry, so we can exit 11 clks 'early' and still send back-to-back bytes)
MOVE Acc,PORTB ;initialise transmission of the bit (1clk)
;
; now 11 clk since Tx1, delay 12 (with 3x sub calls to Tx3 = Return)
CALL Tx2 ;4clk
CALL Tx2 ;4clk
CALL Tx2 ;4clk
; now 23 since Tx1, check the BitDelay pattern (27,27,28) for +1clk (so 23 +4 or +5 to get 27 or 28)
RRCY TxBITD ; rotate bit delay pattern via carry (note, start bit =CY clr, so TxBITD gets a 0 in MSB on first Rot) 1clk
SKIP CY ; skip if carry = no extra clock (1clk), or skip if extra (2clk)
GOTO Tx1 ; no extra, goto next bit (2 clk), so tot +4 = 1 rot, 1 no skip, 2 goto
GOTO Tx1 ; extra, goto next bit (2 clk), so tot +5 = 1 rot, 2 skip, 2 goto)
; done
NOTE that if the Count to be sent is 'FF' then (in theory) all we need to do is send the start bit and then we can exit (so long as we don't try to send another char within the byte time)
This observation can be extended to "we don't need to send fruther bits when all remaining bits = 1"
Because we are using 'Rotate through Carry' there is a trick we can pull to detect the end of byte transmission without needing to use a seperate bit counter.
Essentially it works by setting CY before the first shift, then clearing the CY prior to each future shift. After 8 shifts the reg will be Z (i.e. the 1 shifted in at the start will have been followed by all 0's which will have emptied the reg).
In this case, the BitDelay reg (which starts as 11011011) can be 'double used' ...
The same 'sort' of trick can do done with the Tx byte to set the 'stop' bit at the end of trsnamission