What's a RTC ?
A 'Real Time Clock' is simply a 'seconds counter' that 'keeps the time' even when the main power is removed - in other words, it's a computer clock chip with a battery back-up. There is one on almost every computer motherboard (on a PC, it's 'part of the BIOS chip' and runs from a CR2032 'coin cell') - but there is none on the Raspberry Pi
Why do we need to build one ?
You don't - you can buy single function Raspberry Pi plug-in 'RTC module'** for less than $2, however many of my applications for the 16F54 require 'time keeping', so building a RTC is a good 'first project' to learn how to use the PIC - and an excuse to do some real PIC programming !
The best way to learn is to actually program the PIC16F54 the 'do something real'. Not only do you gain an in depth understanding of the instruction set, but you also get to know how the 'debug' tools work, especially the 'emulation' utilities
I will use the finished project as a 'stand-alone' RTC for the Raspberry Pi
**Although the RaspberryPi has no RTC 'on-board', every time you turn it on it the Pi will try to get the correct time from an Internet NTP 'Time Server'. It's only when you need to run the Pi in a totally 'stand-alone' mode (with no network connection at all), that you need a RTC 'add-on' board - and these cost less than $2, i.e. about £1.25 each (from Hong-Kong, via eBay) - or buy two and you pay only £2.10 (inc postage) and that's only £1.05 each !
Rant mode on ... this is less than the cost of UK postage from a UK manufacturer (who will expect you to pay 5x the price for the same functionality, for example CPC offer a range of Pi RTC modules starting with the CISECO at £5.99 all the way up to the GertDuino expansion board with RTC at £22.20 !).
The cost of the rip-off UK postal 'service' (aka the 'Royal Mail') is just one more nail in the coffin of UK suppliers, since almost any small electronic component you care to mention will cost less sent by air-mail from China than the cost of the postage alone, when sent within the UK. At the time of writing, both Element14 and CPC (UK suppliers) are offering 'free postage', however they must be loosing money on every order under about £10
Of course if you REALLY want to pay through the nose, just go to a retail store such as Maplins = they will offer you the Gertduino expansion board for £29.99 (which tells you more about the cost of running a UK High Street store than it does about the profitability of Maplins).
Why use the PIC16F54 ?
The PIC16F54 costs less than 50p each, so it's cheap enough to 'break' whilst learning 'how to' use it :-) Also, with space for only 512 instructions, you really must learn how to program with minimal instruction counts i.e. it forces you to do some real 'hands on' programming !
If it turns out you really can't get all you want into 512 instructions, you can always 'move up' to the 16F57 with it's 2k instruction space, without having to 're-learn' everything
Anyone thinking of doing this 'in earnest' should look to one of the other chips. Those with a low-power 'idle' and 'interrupt' capability make it a LOT easier to 'keep the time', and some PIC's even have a 'built in' RTC !
OSC Clock frequency
32,768 Hz, of course, so the PIC16F54 CPU is clocked at OSC/4 i.e. CLK = 8196 Hz
NB. The cheapest way to get the 32,768 Hz OSC 'crystal' (actually, a 'resonator') may well be to dismantle an old digital watch or clock mechanism
The 16F54 can run at 32kHz from a supply of 2v. A simple diode based 'battery switch over' from 'main power' to a 3v 'button cell' should thus work fine (although low Vfwd 'Schottky' diodes will be needed for a 'passive' switchover)
After 'main power' is removed, the 16F54 must minimise it's power usage to avoid draining the battery. First, all the i/o pins can be switched to 'input' mode (if the Pi power is 'off' we don't need to transmit anything to anyone). Next, we can look at using 'sleep mode'
During 'sleep', the 32kHz OSC is disabled, so we have a problem 'keeping the time'. However if the WDT (Watch Dog Timer) can be 'calibrated' it should be possible to maintain an 'acceptable' time (ideally we want 'within 1 second', however if we are just 'displaying a digital clock', 'within 1 minute' might do)
Calibrating the WDT to support Sleep mode
Sleep stops the OSC and waits for the WDT (Watch Dog Timer) with it's own clock (nominal period of 18mS = 55.5 Hz) to 'expire'. The WDT can be 'activated' whilst the CPU (and TMR timer/counter) is still running (at 8196 Hz). When the WDT 'times out' it generates a 'soft' Reset leaving the register contents intact (for sure PORTC and PORTD data registers are 'unchanged' over a WDT reset).
This means it's possible to use TMR0 to 'measure' the WDT time-out to quite a high degree of accuracy = and if the WDT period is known then it is possible to 'Sleep' waiting for the WDT - and then adjust the RTC count on 'wake-up'
Two bits in the status reg allows us to detect a 'soft reset' and determine if this is WDT from Sleep or WDT 'calibration' (during normal operation). The WDT can be used with the 'divide by 2n' pre-scaler (max divide 128 = 2.3 s) in which case TMR0 (the 8bit timer/counter) will have to run direct from the 8196 CPU CLK (TMR0 count 0-255 is approx .03s. So a maximum WDT time-out of 2.3s will be approx 76 'full' (0-255) counts of TMR0.
To measure the WDT 'time out' period :-
Clear the 'full counts' reg
Clear the WDT/pre-scaler and Set the pre-scaler to 1/128 and WDT use
Clear the TMR
Read TMR - if less than 128, goto Loop0:
INC the full count
Read TMR - if greater than 127, goto Loop1:
;At the 'power-on reset' (location 0, assuming we have set max. location to NOP),
; we must 'detect' the 'soft WDT reset' and save the current TMR count
; WDT time (in CLK's) is then 256*'full counts'
; + residual TMR, -128 (we counted first 'full' after only 128)
; - the few CLK's used to do the WDT detect
; + the OSC start-up delay after Sleep
; We can now 'go to sleep' knowing how much to adjust the Time by on a wake up
Note - to detect the TMR0 127/128 transition just ROTL TMR0,Acc - which puts b7 of TMR into Cy (and the rotated TMR0 into Acc, thus avoiding upsetting the TMR0 count) and then 'skip on Carry/noCarry'
Communicating with the Pi / PC
The simplest method is to use a Serial Link - and since it's plainly going to be hard to get a 32,768Hz PIC to communicate at anything much faster than 300 baud (300 bps), that means any of the Pi i/o pins can be assigned to this job (there is no point 'wasting' the Pi's UART function on such a slow data rate)
In theory we only need to use 1 pin as the RTC will always be 'sending the time to the Pi'. However there's the issue of 'start up' and need to allow the Pi to adjust the RTC for 'leap seconds' etc. This means the Pi must have some means of transmitting to the RTC. All i/o pins (on both the PIC and Pi) are 'valuable' and dedicating 2 pins to serial i/o is 'extravagant' when 'initialisation' only occurs 'once' (after which the PIC will only ever be transmitting, not receiving).
So I've combined the serial transmit and receive onto a single pin, which works as follows :-
The PIC has to monitor the Pi (and thus it's own) 'main power'. When the Pi is 'off' (and the RTC running from battery), the PIC stops wasting power transmitting the date and time to the Pi and instead switches the serial i/o pin to 'receive' mode.
When Pi (and thus RTC) main power is restored, the PIC waits for the Pi to 'tell it something'. When (if) the Pi sends a date and time, the PIC 'resets' it's own timer. When the Pi sends a 'go' command, after a suitable delay, the PIC will switch the i/o pin to 'transmit' mode and starts sending the date and time every second.
Of course, having the PIC just sit there 'for ever' waiting makes testing quite hard :-) So I soon ended up allocating a second PIC pin to 'serial input' (so I could send test strings from a PC)
Sending the Date and Time
Instead of 'sending the time on demand', it's far simpler to 'transmit on every tick' and let the Pi/PC decode what to do with it (typically, ignore it)
A 'send on demand' approach is complicated by the possibility of a 'time tick' occurring in the middle of a 'send' packet. To avoid missing a 'tick' we either have to delay the send until just after one tick so we know another can't occur for 1s) or write some clever code that 'notes' the TMR0 count prior to a 'transmit' and checks again afterwards for a 'missing' tick (indicated by the TMR 'rolling over')
Time and Date format
One issue is that the Raspberry Pi, being a unix system, 'operates' in Unix Time, a 32 bit number that is a running count (in seconds) starting from 00:00:00 UTC, 1 January 1970.
In most systems, a 'signed' 32 bit is used, which permits 'negative' times (= dates before 1970). However this means that Unix time will 'overflow' in 2038 (and the max. 'negative' time corresponds to some day in 1902). Whilst 86400 seconds make 1 exact day, human time is based on Solar days i.e. the rotation of the Earth which is slowing down gradually (this is discussed extensively in the Wikipedia ref. above). Of course there is a further 'slippage' due to variation of the Earths orbit (1 year), so human time is occasionally adjusted by an extra 'leap second' usually** added at the end of a year ! As a result, converting from the UNIX 'seconds count' time into 'now' (within 1s) is quite hard :-)
** Accounting for end of year leap seconds is actually quite a serious issue, especially as the GPS system implements the adjustment at arbitrary times (i.e. not the end of year). The GPS count to UTC time difference has to cope with at least 16 'leap seconds' and, even today, some GPS receivers still have problems converting from the GPS 'count' into the human readable date/time (despite the fact that the GPS satellites transmit the required adjustment factor)
To avoid all the UNIX Time issues, I'm just going to keep the date and time in a set of registers as simple BCD numbers and just 'count up' at 1 second per tick (and rely on the Pi to send updates often enough to avoid getting too far 'out of sync' with real human time 'leap seconds' etc)
SO, to facilitate testing, and avoid Unix Time problems, my RTC will transmit the time twice a second (i.e. at half second intervals) as the ascii string "YYYY mm dd dd:mm:ss<lf>"
<lf> is 'line feed', the unix 'end of line'. Yes, I know Windows uses 'cr+lf', but every Windows 'Terminal emulation' utility will cope with <lf> just fine.
Effect of transmission delays
The slower the baud rate, the longer it takes to send (or receive) the date/time. To send "YYYY mm dd hh:mm:ss<lf>" = 20 ascii characters, at 8 bit per character with one start and one stop bit (so 10 bits per character) that's 210 bits. At 300 baud (300 bits/sec) that's about 2/3 secs so it's only just possible to send the current time 'once per second' !
Worse, if it takes 2/3 of a second for the Pi to send an 'adjustment' time to the PIC, the PIC time will be at least 2/3rds 'out' compared to the Pi (NTP). If the PIC then takes another 2/3 seconds to return the time, the Pi will 'see' a PIC time that is always 'slow' by 4/3rds second (i.e. >1 s) and (if you are not careful) could get into an infinite loop trying to correct it !
To 'start' with an accurate time, clever operating systems will 'estimate' the transmission delays (even on receiving NTP via the web) and 'offset' the received values. If we are clever, we can program the Pi to do something similar with our PIC RTC (i.e. check what the PIC sends back immediately after being 'adjusted' and thus work out the offset)
On the other hand, on a 300 baud system you could just program the PIC to 'add' 1 second to the 'initialisation' time sent from the Pi :-)
Why use a specific baud rate ?
In theory the PIC could just send data at the max. rate it can manage and leave it to the Pi to sort out the 'receive'. However, it's plainly going to be easier to test the PIC if we can send it's transmissions into a PC's serial port (and use PuTTY or some other 'terminal emulation' utility to show what date/time the PIC is sending)
Note that since we are not using any form of 'flow control' (RTS/CTS) getting 'in sync' also takes time !
When receiving the serial data stream, to be sure we are not seeing the 'middle' of a time 'packet', we have to wait for 8 successive "1's" = since we are sending 7 bit ascii characters in 8 bit bytes, the max valid "1" sequence is 7 (char = 0x7F) - so when an 8th "1" is seen the serial line must be in 'idle' mode (sending 'stop' bits) and thus we have found the 'inter-packet gap'.
If the PIC is only sending 'once a second', then to receive the current time could take more than 1s (whilst waiting for inter-packet gap) ! If we want the time to be 'right' to within 1 second, the PIC has to transmit the time at least twice a second (with an inter-packet gap ('stop bit' sequence) = all "1's") of at least 1 byte time)
At 32kHz OSC, whats the PIC16F54 max baud rate ?
If the byte 'send' sequence is coded 'in line' (i.e. repeated 8 times to send 8 bits) there will be no 'loop' counting overhead, so the absolute maximum send speed would be 1 instruction per bit (with longer times between bytes). However the CPU clock speed, OSC/4 = 8196, is not a recognised baud rate :-)
Standard 'signalling bit rates' (baud) rates are:- 110, 150, 300, 1200, 2400, 4800, 9600, 19200 ...
A 'real' UART is clocked at 16x the baud rate and each bit is sampled multiple times in order to detect errors. No doubt something similar will be done when programming the Pi 'serial receive' i/o pin. Depending on how 'sensitive' the Pi code is to 'sample' errors, each 'bit transition' may need to be within 1/16th (6.25%) of the baud rate. On the other hand, the pin could simply be sampled in the middle of the nominal baud rate 'time slot' and whatever is found accepted as that bit (which is what the PIC will do when receiving from the Pi) = in which case, the bit would still be received correctly so long as the accumulative timing error never exceeded 50% of the baud bit time
So we start by assuming 16 instruction CLK's per 'bit', which gives us a max speed of 8192/16 = 512 baud (this is where the 'we can support 300 baud' assumption came from). However 512 is not so far from 1200 which, should be achievable if we allow some level of error.
Coding for 1200 baud
The main advantage of using 1200 (rather than 300) baud is the reduced 'latency' = the 'round trip' time from an adjustment being sent to PIC until the time/date is received by the Pi. At 1200, this will be less than 1/2 second, so it will no longer be necessary to adjust the PIC time (by adding +1s to the Pi time).
Further, at 1200 we can send the complete 20 character = 210 bit time packet at least twice a second. Finally, with fewer CLK cycles 'used up' sending the date/time (1200 baud consumes approx 7 CLK/bit, whilst 300 consumes 27 CLK/bit) we have more CLK cycles to perform the actual date & time count !
At 1200 baud, 1 bit corresponds to 6.83 CLKs. If each byte send is coded 'in-line', including start and stop bits, this requires 68 instructions (at 1 CLK per instruction). To get the overall byte timing 'correct', we have to 'slip' the occasional CLK. The 'ideal' bit time sequence (in CPU CLK's) is 7(start bit), 7,6,7,7,7,7,7,6, 7(stop bit) = 68 CLK's.
The maximum Error is 2.5% (the first bit after start, b0 is 'too long'), followed by -2.4% (on the following bit, b1, which is 'too short') after which the bit position errors gradually decrease. Counting from the 'correct' transition point, they are -1.17%, -.4%, .05%, .4%, .66%, -.76%. This is well within the allowed UART error (6.25%) and will work just fine for the entire packet ASSUMING the receiver "re-sync's" on each next byte 'start bit' correctly (if it doesn't, the error will accumulate from one byte to the next)
It might seem that 6 CLK's to 'send a bit' is plenty, however if we code 'send a bit' as a subroutine the CALL 'costs' 2 CLK's, the RETURN another 2 (and destroys the contents of the Acc) leaving only 2 instructions to actually 'get the (next) bit from the register and use it to set the i/o pin' - which is virtually impossible (it can only be done by using an entire 8 bit i/o PORT as a shift register which allows 1 CLK bit transmission).
Although 'sending in 6' with a subroutine can't be done, 'sending in 7' can, but only just ! After allowing 2 CLK CALL, 2 RETURN we have 3 CLK to send the bit - and it takes too long to actually discover the (next) bit state (the 'test and skip on bit' alone 'costs' 2 CLK's). So the approach below is based on 'copying' the bit 'sight unseen' to the i/o pin using 'Rotate via Cy'.
Possible alternative approaches.
First, it might be possible to use the bits to control a 'computed jump' (by using it to change the PCL contents) thus selecting one of two 'subroutines' (one that sets the i/o pin, the other that clears it), however 'return' then becomes problematic ... (it's easy enough to 'jump back' to a fixed location, but then we have to implement some sort of 'loop control')
Second is a simpler but rather more restrictive approach. The date/time packet to be sent contains a small sub-set of all possible ascii codes - specifically 0-9, space, ':', and <lf> i.e. each byte is one of only 13 possibilities. We could thus code 13 separate 'send a byte' subroutines, each of which would be 'hand optimised' (to minimise timing errors) for the bit sequence of one specific ascii character.
Such an approach might allow 2400 (or even 4800) baud to be supported with 'tolerable' timing errors. The 'problem' is, of course, that we would then have to find a way to receive data (from the Pi / PC) at that speed (serial Tx and Rx speeds are typically identical) !
The 1200 baud (bps) code
The code below sends 8 bits per byte. If we are only ever sending ASCII character codes, then we can reduce the data 'byte' to 7 bits (however you would then have to make sure the PC / Pi was set to receive 7 bit data)
Extra code has been added to the start of the subroutine to cope with the BCD data held a register stack 'pointed to' by INDF at 2 BCD values per byte. To make the 'counting' code simpler, rather than 'count up to 60s, 60m, 24 Hrs, X days (per month), 12 months per year' etc. the 'count' chain is implemented in a 'count down X (where X = 60,60,24,28,29,30,31 etc) to 0'
Actual value is thus "start value (top count) - current count" (not forgetting that seconds are actually '0-59', whilst months are '1-12')
; byteSend subroutine. ; The data to be sent is in register TxDATA, destructive read out. One tempR (temp register) is used ; The 'serial port' line is assumed to be PORT_A, bit0 (the other 3 bits are unchanged by the Tx routine) ; CLK counts are (start bit)7, 7,6,7,7,7,7,7,6, 7+(stop bit) ; (the 7 CLK bits are sent using a subroutine, the 6 CLK's are coded in line) ; sendComp: ; enter here BCD is counting down from 99 etc. Acc contains the top count, FSR points at the reg SUB INDF,Acc,Acc ;reg-Acc to Acc COMP Acc, TxSAVE ; 2's complement the Acc (macro) to the TxSAVE reg JMP doLowNib ; jump to do bottom nibble doTopNib: ; re-enter here to do the top nibble NIB_swap TxSAVE ; swap the nibbles INC FSR ; point at next BCD pair byte doLowNib: LOAD Acc,0xF ; set the mask AND TxSAVE,Acc,Acc ; mask off top IORLW 0x30 ; add ascii top for data char (0x3n) byteSend: ; enter here with data to send in Acc COPY Acc,TxDATA BCLR PORTA,b0 ;issue the Start bit (0) ROTR PORTA,Acc ;get PORT A state (1 CLK) = note top bits of PORTA are read as 0, however b7 Acc will actually be state of Cy on CALL) COPY Acc,tempR ; save PORT state (2 CLK) CALL bitSend ;send bit0 (2 + 5 = 7, on return 2 CLK's have elapsed) CALL bitSend ;send bit1 (2 + 5 = 7, on return 2 CLK's have elapsed) ; only 4 more CLK's until bit 2, so we can't CALL for it ROTR TxDATA ; get Tx bit2 to Cy (3 CLK) ROTL tempR,Acc ; get PORT (Cy=Tx=b0) (4 CLK) BSET tempR,b7 ; (5 CLK) [we need to ensure top bit of temp is set so ROTL sets Cy (and we can use it as the stop bit == see later)] COPY Acc,PORTA ; send b2 (after b1 = 6 CLK) JMP $+1 ; waste some time to get back in sync (2 CLK) CALL bitSend ; send b3 (after b2, 2+5 = 7) CALL bitSend ; send b4 (after b3, 2+5 = 7) CALL bitSend ; send b5 (after b4, 2+5 = 7) CALL bitSend ; send b6 (after b5, 2+5 = 7) CALL bitSend ; send b7 (after b6, 2+5 = 7) ; only 4 more CLK's until stop bit, however we know that the previous ROTL tempR,Acc has set Cy, ; so we can jump into the bitSend subroutine to Set the Stop bit and exit via it's RETURN JMP stopSend ; +2 CLK for the jump, +2 CLK in the stopSend ; bitSend: ; (+2 CLK) ROTR TxDATA ; get Tx bit 0 to Cy (+3 CLK) stopSend: ; jump in here to send the stop bit (Cy will have been set by previous ROTL tempR) ROTL tempR,Acc ; get PORT (Cy=Tx=b0) (+4 CLK) COPY Acc,PORTA ; set the i/o pin (+5 CLK) RETURN ; (+2 CLK)
Total instructions = 20
Note how the 'byteSend' subroutine exits via a jump to the bitSend subroutine . This saves a few instructions, however to send the 'stop' bit we have to guarantee Cy is set - which is why tempR b7 is 'set' (when we have a 'free' instruction slot during the first 6 CLK (bit1) Tx)Sending at 2400 baud (demo code)
As an exercise (and before using the 1 CLK 'shift register' method) I took a look at 2400 baud transmission. Since we already discovered (above) that the minimum 'bit send' subroutine is 7 CLK's, to support 2400 baud we would have to code the bit send 'in line'
8196/2400 = 3.415 CLK per bit. This is 'close enough' to 3.5 that we should be able to 'get away' with alternating 3 CLK / 4 CLK per bit sends, so that's 7 CLK's for a 'pair' of same bits (i.e. 00, or 11), 10 CLK's for '3 of a kind' and 13.6 for four of the same (17 for 5, 20.5 for 6), not forgetting that the 'start bit' (Lo, 0) also 'counts' as part of the timing.
Since Serial Tx is 'b0 first', we can 'jump to common code' to send the 'top nibble' for all but 2 of the ascii characters (not forgetting to exit with the send pin left in the Hi (1) state = sending stop bits)
The codes to be sent are :-
<lf> 0x0A 1, 0000 1010 ,0 (space) 0x20 1, 0010 0000 ,0 0-9 0x30 - 0x39 1, 0011 0000 ,0 1, 0011 0001 ,0 1, 0011 0010 ,0 1, 0011 0011 ,0 1, 0011 0100 ,0 1, 0011 0101 ,0 1, 0011 0110 ,0 1, 0011 0111 ,0 1, 0011 1000 ,0 1, 0011 1001 ,0 : 0x3A 1, 0011 1010 ,0 Transmission is from LSB ('Least Significant Bit' i.e. bit 0), so ',0' designates the 'start bit, from which the timing sequence starts, and '1,' designates the stop bit (at the end of the timing sequence)
To minimise the number of instructions required to support all 13 characters, we can reuse common bit sequences (if coded as subroutines), so long as we are careful with the 2 CLK CALL (and 2 CLK RETURN) delays (which means, in effect, that the i/o pin has to be switched just before a CALL, and just before a RETURN .. and then changed again almost immediately after the Return). Thus a 'pair of 0' will exit with '1' (and 'pair of 1' will exit with 0)
Counting from the start bit (CLK 0), the exact transitions (i.e. bit's output) should be on :-
b0 3.4 (4. 4 is used rather than 3 since it's assumed that 'start bit detect' takes a little longer)
b1 6.8 (7)
b2 10.24 (10)
b3 13.66 (14)
b4 17 (this allows a 'common' top nibble o/p routine)
b6 23.9 (24)
b7 (27.32 = b7 is always same as b6, so we can ignore this time )
stop 30.73 (31)
; The Tx i/o pin can be anything (we are using BSET and BCLR instructions), here we assume PORTA b1 ; The required ascii code is CALL'd directly, so no registers are used (remember Acc is always lost on the RETURN) doLF: ; call here to output <lf> 0x0A 1, 0000 1010 ,0 ; start+B0=0, so 1st transition is end b0, i.e. b1 Hi at CLK7 ; then b1 is Hi for 3 CLK, before b2 Lo at CLK10 ; b2 is Lo for 4 CLK until b3 Hi at CLK 14 ; b3 stays Hi for 3 CLK until b4 Lo at CLK 17 ; then nothing until the stop bit (at CLK 31) CALL do2zs: ;CALL for the 001 start sequence. On return, b1 was set on CLK7, 8&9 are the RETURN, get back on 10 BCLR PORTA,b1 ; b2 at CLK 10 ; next up is b3 (a 1) is not until CLK 14, then so we do a CALL CALL do100s: ; //////////////////////// to be completed ///////////////////// do100s: ; call here to do the 1,00 'slow' sequence NOP do100f: ; call here to do the 1,00 'fast' sequence BSET PORTA,b1 ; set the bit, wait JMP $+1 ;wait 2 BCLR PORTA,b1 ; ; doSpace: ; call here to o/p (space) 0x20 = 1, 0010 0000 ,0 ; After setting 0 (start bit), delay until b5 (CLK 21) then exit via the final 001 of the x3nibble subroutine BCLR PORTA,b1 ; start bit, CLK 0, now wait until CLK 21 LOAD Acc,5 ; load Acc 0 (CLK 1) spDelay: DECskipZ, Acc ; First DEC is at CLK 2, then at 5, 8, 11, 14. At CLK 17 exit the loop (next inst is at CLK 19) JMP spDelay ; (each loop is 3 CLK's in total) JMP $+1 ; here at CLK 19 & 20 BSET PORTA,b1 ; o/p bit 5 at CLK 21 JUMP b6nib ; jump to o/p b6 at CLK 24 and RETURN ; ; ; Top nibble subroutine, Jump for 1,1,0,0,1 end sequence (also CALL'd for 1,0,0,1, 0,0,1 and 0,1 or Jump for those as end) x3nibble: ;Jump here to o/p the 0x3 top nibble. Exit with stop bit (so 1,1,0,0,1(stop bit). ; it takes 2 CLK's to get here, so CALL has to be immediately after setting b3 at CLK 14 BSET PORTA,b1 ; this is 'CLK 17' of the sequence (transition to stop bit is on CLK 31) JMP $+1 ; next transition is b6 at CLK 24 i.e. 7 CLK's away JMP $+1 ; JMP +2 CLK ea. JMP $+1 b6nib: ;doSpace Jumps here to output b6 at CLK 24 and exit do2zs: ; CALL here for the 001 start sequence (if CALL is CLK1, '0' is set on 3, '1' is set on 10) BCLR PORTA,b1 ; this this b6 at CLK 24 JMP $+1 ; next transition is stop bit at CLK 31 after another 7 JMP $+1 ; JMP +2 CLK ea. JMP $+1 BSET PORTA,b1 ; exit with stop bit = 1 RETURN ;
1200 baud Serial receiver
The PIC will loop sensing the 'receive' i/o pin and looking for the start bit. As with transmit, the 'bit time' will need to be carefully controlled (each bit has to be 'sampled' at the correct 6-7 CLK's position, counting from the 'middle' of the start bit)
With no flow control (as already touched on above) we need to 'sync' with the start of a 'packet' (as well as start of a byte). Packet start is at least 8 x 'Hi' bits seen on the pin, i.e. Hi for > 56 CLK's
Note that the code below has one issue - it will 'loop for ever' waiting for the 'packet'. To prevent this on a 'real' system, the WDT (Watch Dog Timer) could be enabled to 'break-out' of the loop
As with the transmit code, there is no time to 'sense' the state of the bit = instead we have to use 'Rotate with Cy' to shift in the bit 'sight unseen'. This means the 'serial receive' has to be PORTA b0 or PORTB b0 (ROTR) or PORTB b7 (ROTL) .
The expected packet is 20 bytes - "YYYY MM dd dd:mm:ss<lf>" - all ascii codes. More to the point, the data consists of BCD digits, and this will be 'packed' 2 digits per byte into our date/time register set. Transmission will start with YYYY (the first YY can be 'assumed' = 20 and won't be stored), after which the data consists of 6 pairs of BCD ascii codes, separated by a space or ':' (which can be dropped). The main line code can also ignore the trailing <lf> code.
The 'nominal' byte timing is start bit)7, 7,6,7,7,7,7,7,6, 7+(stop bit) = 68 CLK's. Since we expect 7 bit ascii codes, the 'byte receive' subroutine can return after the 7th bit, thus dropping both the 8th bit and stop bit and allowing sufficient inter-byte CLK's to do the BCD packing
In fact, because we will only ever see BCD characters, we could exit the 'byte receive' after getting the first 4 bits ...
; Start by waiting for 56 'Hi's .. this means, if effect, resetting the count every time a Lo is seen ; Serial receive is 'PORTA b0' - PORTB bi0 or b7 would also 'work' ; ; Main line code (receiver uses both subroutine levels) ; ; Wait for 56 CLK '1' wait1: ;reset the '56 count', 5 CLK per loop, so actual count down from 11 LOAD 11 ; count is in Acc loop1: ; start looking for 56 x Hi SKIP nZ,PORTA,b0 ;skip if PORTA b0 is Hi (2 CLK) JMP wait1 ; found a Lo, reset the count DEC skipZ ; dec the count, skip if expired (1 CLK if not) JMP loop1 ; keep looking (2 CLK, total for loop 5, so '56 count' = 11) ; OK here we have found 56 Hi's, so we must be within the inter-packet gap, we can begin looking for a start bit ; first get and drop the first 2 YY BCD = "20" CALL start ; on return (2 CLK), we have reg RxByte .. and only 9 CLK left before next start bit JMP delay4 ; delay to avoid catching the end of b7 (i.e. must be in stop bit time) CALL start ; on return (2 CLK), we have reg RxByte .. and only 9 CLK left before next start bit LOAD 6 ; OK, it's 6 pairs COPY Acc,pairCount ; start the count JMP get6 ; left with only 2 delay this time, spend 2 CLK jumping to next instruction get6: ; OK, now get 6 pairs of digits, hi first, the Lo, then inter-pair space (or :, or trailing <lf>) CALL start ; get Hi BCD - on return (2 CLK), we have reg RxByte .. and only 9 CLK left before next start bit SWAP_nib RxByte,Acc ; get top BCD to low bits of Acc AND 0xF0 ; mask off the bottom bits COPY Acc,INDF ; save it CALL start ; get Lo BCD - on return (2 CLK), we have reg RxByte .. and only 9 CLK left before next start bit COPY RxByte,Acc ; get it to Acc AND 0x0F ; remove the top bits ADD Acc,INDF ; add low BCD to existing Hi CALL start ; get the inter-pair gap (space, : of <lf>) and ignore it DEC_SkipZ pairCount ;Dec the number of pairs (6), skip if zero JMP get6 ; go and do the rest ; here we have the entire packet, continue with rest of main code ... ; ; ; Get byte and Get bit subroutines (must be in first 256 program locations) ; Get a byte start: ; arrive looking for the start bit SKIP Z,PORTA,b0 ;skip if PORTA b0 is Lo (1/2 CLK) JMP start ; keep looking (2 CLK, so 3 CLK loop if bit not seen) ; Here we are 2 CLK after first seeing the start bit, which could have 'arrived' at anytime during the '3 CLK' wait loop ; so, 'on average' we are 3.5 CLK into the start bit, which is 'exactly right' (i.e. it's mid posn.), so 7 more until b0 sample CALL delay4 ; wait until end of start bit CALL getBit ;b0, sample at 3 CLK pos (i.e. 7 after seeing start bit), then 3 to get back here CALL getBit ;b1, sample at 3 CLK i.e. 6 after b0, 3 to get back NOP ; adjust +1 CLK (now at 4) CALL getBit ;b2 at 3 (= 7 after b1) NOP ; adjust +1 CLK CALL getBit ;b3 NOP ; adjust +1 CLK CALL getBit ;b4 NOP ; adjust +1 CLK CALL getBit ;b5 NOP ; adjust +1 CLK CALL getBit ;b6 JMP getBit7 ; exit via set of (dummy) bit 7, saves 2 CLK on b7 +7 on stop bit ; ; Get a bit (6 CLK, sample pin on first inst. after 2 CLK CALL . i.e. CLK posn 3) getBit: ;get a bit, taking 6 CLK's total (2 CLK for the CALL, 2 for the RETURN) ROTR PORTA ;Rotate receive (b0) into Cy getBit7: ;entry point for end of ascii char (b7), saves 1 CLK by skipping the actual bit sample ROTL RxByte ;Rotate the Cy bit into the receive register ;delay4 ;main line code uses a JMP delay4 to absorb 4 CLK RETURN ; don't forget Return dumps the Acc
Keeping the time
After having being sent a new date/time, at what point should we start counting ?? All we can assume is that the date/time was sent on an exact second boundary by the Pi. Since the whole 20 byte packet will have taken 20 x 68 = 1360 CLK cycles to be received, the PIC '1 second tick' should be started at 1360 CLK's 'in'.
8196 CLK's = 1 second. Since TMR0 count 256 = 1s, the pre-scaler will be set to 'divide by 32'. 1360 is thus a TMR0 count of 42.5 (I will use 43). The only complication is that we are going to detect '1 second' elapsed by looking for TMR0 b7 changing state from 0 to 1 (we can't guarantee to check the counter every 32 CLK's (so could miss 'count=0')
; Set pre-scaler, load TMR0 with 43 and 'start the clock' CLR WDT ;reqd before accessing OPTN LOAD Acc,B'xxxx0xxx ; load OPTION ; copy Acc to OPT latch, starts the clock LOAD Acc,0xAB ; count 43 means 43 after 0-1 transition (so 128+43 = 0xAB) COPY Acc,TMR0 ; to the counter ; OK that's it - when TMR0 b7 transitions from 0 to 1 we have 1 second elapsed ....
The PIC holds the date/time in a set of 6 BCD registers (the '20' in YYYY will be hard-coded). On each completed 'tick', the seconds (ssReg), minutes (mmReg), hours (hhReg), day (ddReg), month(mmReg), and year (yyReg) BCD bytes are incremented as appropriate. Note that the registers are assigned consecutively (so INDF addressing with INC/DEC FSR can be used to access them later)
; wait here for the next 0-1 transition, and when found, CALL tick ('inc the time') and then send it to the serial link. ; then wait for the 1-0 transition (and when found, re-send the time) ; waitT1: SKIP Bset,TMR0,b7 ; loop until b7 is Hi GOTO waitT1 CALL tick ; 0-1, transition - inc the time ; ; send the time LOAD Acc,timeBase ; start by pointing at the seconds reg COPY Acc,FSR LOAD Acc,0x99 ; set Acc to top count 99 for seconds CALL sendComp ; send low BCD CALL doTopNib ; send hi LOAD ACC, 0x3A ; load ':' COPY Acc,TxDATA CALL byteSend: ; miniutes LOAD Acc,0x99 ; set Acc to top count 99 for mins CALL sendComp ; send low BCD CALL doTopNib ; send hi LOAD ACC, 0x3A ; load ':' COPY Acc,TxDATA CALL byteSend: ; hours LOAD Acc,0x23 ; set Acc to top count 23 for hours CALL sendComp ; send lo CALL doTopNib ; send hi LOAD ACC, 0x20 ; load (space) COPY Acc,TxDATA CALL byteSend: ; send ; OK, now it's days and top count depends on month .... CALL daysInMon ; get the count to Acc CALL sendComp ; send lo CALL doTopNib ; send hi LOAD ACC, 0x20 ; load (space) COPY Acc,TxDATA CALL byteSend: ; send ; month LOAD Acc,0x11 CALL sendComp ; send the month lo CALL doTopNib ; send month hi LOAD ACC, 0x20 ; load (space) COPY Acc,TxDATA CALL byteSend: ; send ; year LOAD Acc,99 ; year counts from 99 CALL sendComp ; send yr lo CALL doTopNib ; send hi LOAD ACC, 0x30 ; load ascii '0' COPY Acc,TxDATA CALL byteSend: ; send LOAD ACC, 0x32 ; load ascii '2' COPY Acc,TxDATA CALL byteSend: ; send LOAD Acc,0A ; load LF COPY Acc,TxDATA CALL byteSend: waitT1: SKIP Bset,TMR0,b7 ; loop until b7 is Hi GOTO waitT1
Maintaining the actual time and date
We are holding everything as BCD 'digits', one per 'nibble' = 2 per byte. This means that the two digit seconds, miniutes, hours, days, months and years are each held in their own single register. When it comes to 'counting' however, things get complex - on a 'tick' we have to detect both '9' in the 'units' nibble as well as '59' in the seconds/minutes register, 23 in the hours register, one of 28,29,30 or 31 in the days register (depending on the month) and 11 in the months register.
The reason for extra complexity is that there is no 'Compare' instruction. Instead you have to do a Subtract - and 'SUBtract register and Accumulator' is ALWAYS 'SUBtract Acc from reg' (with result to reg or Acc) - so to do a 'compare' you load a value to Acc and then 'SUB Acc from reg, placing the result back in Acc'. The Z bit is set if the two values were equal, plus Cy & NibCy is set if no borrow occurs, i.e. if reg is > Acc
Needless to say, it's even more complex when we have to detect if a BCD digit has reached '9' (since that means selecting the top (or bottom) nibble only)
Is there an easier way to count ?
Rather than 'count up (from 0), and 'check for 9' (or 23 etc) it's a lot simpler to 'count down' (from 59, or 23 etc) and check the Z status. This has a big advantage when it comes to the days count, since we only have to work out 'how many days in this month' when 'changing month' (instead of every time we want to increment the days). The disadvantage is, of course, that transmitting the time becomes rather more complex :-)
The 'counts' are kept in a register 'stack'. When a count is 'fetched' to the Acc, the Z flag is set, so we can actually count down from 59 to 00 and 'spot' we have reached 00 when getting the byte reg. to the Acc on the 60th 'tick'.
This works fine for the seconds & minutes count (59 to 00) and hours (11 to 0), however days and months need to be counted 'down to 1' i.e. from 30 to 1 (or 12 to 1).
; The count ('tick') code should be included in the 'main line' sequence ; (it's only ever invoked from one place) ; ; Arrive here to "Count down", uses tempR temp register tick: LOAD Acc,timeBase ; point at the seconds reg COPY Acc,FSR DEC FSR ; the dec count subroutine starts with an INC FSR, so pre-dec to base-1 ; ; Dec the seconds register. If routine Returns with Acc=1, all is OK, if Acc=2 then it's 00 and needs to be reset CALL subDec ; do the seconds, will return with Acc=1 if 0 reached ADD Acc,PCL ; skip if underflow (i.e Acc = 2) RETURN ; exit, tick done ; LOAD Acc,0x59 ; seconds overflow, reset and tick the mins COPY Acc,INDF CALL subDec ; do the minutes (the sub will INC FSR) ADD Acc, PCL ; skip if underflow RETURN ; exit, tick done ; LOAD Acc,0x59 ; mins overflow reset to 59 COPY Acc,INDF CALL subDec ; do the hours ADD Acc, PCL ; skip if underflow RETURN ; exit, tick done ; LOAD Acc,0x23 ; hours overflow, reset 23 COPY Acc,INDF CALL subDec ; do the days ADD Acc, PCL ; skip if underflow RETURN ; exit, tick done ; ; OK the days overflowed, so we need to reset the days to the correct count for the NEXT month ; Months are counting down from 11. If current is 0, next will be 11 = Jan. ; if current is 11, then next is 10 = Feb. Current 10, next Mar and so on. ; Feb (current count 11) is going to be another exception (leap years) ; ; now we can either do some clever calculations or just CALL a look-up table ... CALL daysInMon ; yep. do it the easy way TEST Acc ; If Acc is Zero on return it's Feb SKIP Z ; skip if Feb GOTO doDays ; jump to reset day count to Acc contents LOAD Acc,27 ; assume not a leap yr. (count down is from 27 to 0, not 28 to 1) ; It's Feb, but only if Yr is divisible by 4, is it a leap year ; - we are counting down from 99 ('15 = count 84, years-99 = FB, 2's comp = 15, not div by 4 because that's an odd no. SKIP Bset,years,b0 ; could be div/4 if it's an even year = odd count GOTO doDays ; b0 was clr = even count = odd year, not a leap yr ; OK, now it depends on b1 plus odd/even number of 10's (i.e. b4) ; IF b1=b4 then it's a leap year (i.e. we have a '2's' yr + add 2's 10 or 0 Yr + 0 tens) SKIP Bset,years,b1 GOTO div4set SKIP Bset,years,b4 ; b1 set, if b4 also set, it's a leap yr GOTO doDays ; b4 was clr leapYr: LOAD Acc,28 ; leap yr, count down from 28 to 0, not 29 to 1 GOTO doDays div4set: ; b1 was clr, if b4 also, it's a leap yr SKIP Bset,years,b4 GOTO leapYr ; b4 clr, do leap yr, else drop through to doDays doDays: COPY Acc,INDF ; OK, we have the days start, save it (INDF is still pointing at days) CALL subDec ; dec the months count ADD Acc, PCL ; skip if underflow RETURN ; exit, tick done ; reset the months, inc the year LOAD Acc,0x11 ; months count from 11 to 0 (rather than 12 to 1) COPY Acc,INDF ' save JUMP subDec ; exit this subroutine via year adjust (ignore yr overflow) ; yep, this code has the 'millennium bug' = for yr. 2099 :-) ; ; Days per month data table daysInMon: LOAD Acc,11 ; months count is 11 to 0 SUB month,Acc,Acc ; months-Acc to Acc COMP Acc ; 2's comp the Acc INC Acc ; get the months count, it was 0-11 we now have 1-12 ADD PCL,Acc ; Add Acc (current count +1, so if current (was) 0, next month is Jan and so on) to PCL (i.e. jump into table) ; note that the 'day count' ends at 0, so must start at 'days in month -1' RETURN 0x30 ; Jan, 31 days, count 30-0 RETURN 0x00 ; Feb - set 0 as a flag RETURN 0x30 ; Mar RETURN 0x29 ; Apr RETURN 0x30 ; May RETURN 0x29 ; Jun RETURN 0x30 ; Jly RETURN 0x30 ; Aug RETURN 0x29 ; Spt RETURN 0x30 ; Oct RETURN 0x29 ; Nov RETURN 0x30 ; Dec ; ; subroutine to count down = if units reach 0, 10's are dec'd ; WARNING - calling code is going to ADD Acc,PCL so Returning 0 will lock us up ! subDec: INC FSR ; start by incrementing the FSR COPY INDF,Acc ; get the count SKIP NZ ;skip if non-Zero RETURN 2 ; exit = reached 00, we need to tell calling code to reset the count ; not 00, but have we reached x0 ? AND Acc,0x0F ; mask off the top nibble SKIP NZ ;skip if units non-Zero JUMP dec10s ; x0, dec the top nibble DEC Acc,INDF ; dec the count back to the register RETURN 1 ; exit, all OK dec10s: ; units 0, need to remove 1 from the 10's (which is currently non-zero) whilst setting the units back to 9 LOAD Acc,0xF9 ; this is '-1 top nibble' '+9 bottom nibble' ADD Acc,INDF ; ADD ACC to register, result is top nibble -1, bottom nibble = 9 RETURN 1 ;
The pages in this topic are :-
+ DIY RTC - (for a RaspberryPi) == Latest changes (modified 29th May 2018 14:37.)
Next page :- DIY RTC - (for a RaspberryPi)