logo
background
 Home and Links
 Your PC and Security
 Server NAS
 Wargames
 Astronomy
 PhotoStory
 DVD making
 Raspberry Pi
 PIC projects
 Other projects
 Next >>

DIY RTC for the RaspberryPi

PIC RTC for Pi

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 clock chip with a battery back-up. There is one on almost every computer motherboard (usually it's 'part of the BIOS chip' and runs from a CR2032 'coin cell'), but not the Pi

Why do we need to build one ?

You don't = although the RaspberryPi has no RTC 'on-board', every time you turn it on it tries to connect to the Internet and get the time from a 'Time Server'. Even if you are running the Pi 'stand-alone', with no network connection, you can purchase a RTC 'add-on' board for less than $2.

Yes, that's £1.25 each (from Hong-Kong, via eBay) or buy two you pay only £2.10 (inc postage) and that's £1.05 each ! 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 our rip-off 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 think of costs less if sent by air-mail from China than does the cost of the postage alone within the UK. Whilst CPC (a UK supplier) is offering 'free postage' they must be loosing money on anything under £5 or so

Of course if you REALLY want to pay through the nose, just go buy at Maplins (they will offer you the GertDuino expansion board for £29.99 = which tells you more about the demise of the UK 'High Street' than the profitability of Maplins)

So why build your own ?

Because it's a challenge (and an excuse to do some PIC Programming) !

There really is no other reason = purchasing a 'real' Pi RTC is only slightly more expensive than anything in the 99p Store !

What chip ?

For a DIY Project, I mean 'doing it all yourself' (so no 'cheating' with a DS3231 / DS1307 chip !). Instead I will use a PIC chip (actually, the PIC16F54) so I get to do all my own programming !

OSC Clock frequency

32,768 Hz, of course, so the PIC16F54 CPU is clocked at OSC/4 i.e. 8196

Communicating with the Pi

The simplest method is to use a Serial Link - and since the data rate is going to be 300 baud (300 bps) that means any of the Pi i/o pins will 'do the 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 = 'send time to Pi', however there's the issue of 'how to start the clock at the correct time' (which implies the Pi must have some means of 'initialising' the time). Of course Pi i/o pins are 'valuable' and dedicating 2 pins to the clock is 'extravagant' when almost all of the time the PIC will be sending to the Pi, not receiving.

Instead I combine receive and transmit on a single pin as follows :-
 
At power on, the PIC designates the i/o pin as 'input' and waits for the Pi to 'tell it something'. If the Pi sends a time, the PIC resets it's own timer. When the Pi sends a 'go' command, after a suitable delay, the PIC will switch it's pin to 'output' mode and start sending the time.

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 feed it's serial transmission into a PC's serial port and use PuTTY (or some other terminal' comms utility) to show what the PIC is sending

What's the max baud rate ?

The CPU is clocked at OSC/4 = 8196. To 'send' a bit, the state of the bit has to be detected and then the i/o pin set. If the 'send' sequence is coded 'in line' (i.e. repeated 8 times to send 8 bits) there will be no 'loop' counting overhead. So, at best we might get the 'send' down to half a dozen instructions. However, the fewer the number of instructions per bit, the larger the effect one instruction more or less has on the bit rate timing.

The typical UART is clocked at 16x the baud rate and each bit is sampled multiple times in order to detect errors. Depending on how 'sensitive' the UART is to 'sample' errors, each 'bit transition' may need to be within 1/16th of the baud rate

If we take 16 instruction CLK's as the 'minimum' for one bit, this gives us a max speed of 8192/16 = 512 baud. This suggests that, with some level of error, we might be able to achieve 1200 baud.

1200 baud

The 'advantage' of transmitting at 1200 baud is that far fewer CLK cycles are used sending the data (i.e. waiting between bits) than at 300 baud (1200 baud is approx 7 CLK/bit, 300 is 27 CLK/bit). This leaves more CPU cycles for other activity (like, maintaining the time)

1200 corresponds to 6.83 CLKs. If coded 'in-line', including start and stop bits, this requires 68 instructions (at 1 CLK per instruction). The 'nominal' bit CLK sequence is 7 (start bit), 7,6,7,7,7,7,7,6, 7 (stop bit). If we start bit o/p at tick0, b1 at tick

The maximum Error is 2.5% (first bit), followed by -2.4% (2nd bit) after which the bit position errors gradually decrease (-1.17%, -.4%, .05%, .4%, .66%, -.76%).

Note that the subroutine will exit immediately after setting the i/o pin Hi for the 'stop' bit (i.e. without waiting for the final 7 CLK's). This works because RET is 2 CLK, CALL is 2 and main code will take at least 3 CLK's to place the next character into TxDATA.

To support 'burst' transmission i.e. sending multiple bytes one after the other, reg0 and the IND reg (Indirect Pointer) could be used = start with IND pointing at start of string, increment IND after each byte, exit when the pointer reaches 0 = the end (i.e. IND overflows the top of the register set)

NOTE that the code below sends all arbitrary 8 bits of the data 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)

Note that, because we are 'sensing' the state of bits using Bit Test (rather than Rotate through Carry), on exit the TxDATA byte is unchanged.

NB. if Rotate via Cy is used, to return with TxDATA unchanged, the carry bit just has to be set correctly before the start of the rotating
1200Txd: ; Data to be sent is in register TxDATThe 'serial port' line is assumed to be PORT_A, bit0. ; Clk counts given thus ()(), order (clr),(set) ; CLK counts are (start bit)7, 7,6,7,7,7,7,7,6, 7+(stop bit) ; (ie delay from start to b0 = 7, b0-b1 7, b1-b2 6 etc) ; TOTAL instruction count is 65 ; BCLR PORT_A,bit0 ;Set the Start bit Lo, Icount 1 ; Here at count (0)(0), change to b0 after 7, Icount 5 CALL DELAY4 ; delay 4 CLK's (4)(4) NOP ; fine adjust (5)(5) SKIP_Bclr 0,TxDATA ; skip if bit0 is also Lo (no change reqd), (7)(6) BSET PORT_A,bit0 ;send bit0 (it's a 1) ()(7) GOTO $+1 ; (2)(2) ; Here at count (2)(2), change to bit1 at (7), Icount 8 NOP ; (3)(3) SKIP_BSet 1,TxDATA ; skip if bit1 set (4)(5) GOTO b1CLR ; bit is clr, jump (6)() NOP ; adjust befor set ()(6) BSET PORT_A,bit0 ;Set the bit ()(7) GOTO $+3 ; jump to start of bit2 ()(2) b1CLR: BCLR PORT_A,bit0 ;Clear the bit (7)() GOTO $+1 ; jump to start of bit2 (2)() ; Here count is (2)(2), change to bit2 at (6), Icount 7 SKIP_BSet 2,TxDATA ; skip if bit1 set (3)(4) GOTO b2CLR ; clr, jump (5)() NOP ; adjust for set ()(5) BSET PORT_A,bit0 ;Set the bit ()(6) GOTO $+3 ; jump to start of bit3 ()(2) b2CLR: BCLR PORT_A,bit0 ;Clear the bit (6)() GOTO $+1 ; jump to start of bit3 (2)() ; Here count is (2)(2), change to bit3 at (7), Icount 8 NOP ; (3)(3) SKIP_BSet 3,TxDATA ; skip if bit1 set (4)(5) GOTO b3CLR ; bit is clr, jump (6)() NOP ; adjust befor set ()(6) BSET PORT_A,bit0 ;Set the bit ()(7) GOTO $+3 ; jump to start of bit2 ()(2) b3CLR: BCLR PORT_A,bit0 ;Clear the bit (7)() GOTO $+1 ; jump to start of bit4 (2)() ; Here count is (2)(2), change to bit4 at (7), Icount 8 NOP ; (3)(3) SKIP_BSet 3,TxDATA ; skip if bit1 set (4)(5) GOTO b3CLR ; bit is clr, jump (6)() NOP ; adjust before set ()(6) BSET PORT_A,bit0 ;Set the bit ()(7) GOTO $+3 ; jump to start of bit2 ()(2) b3CLR: BCLR PORT_A,bit0 ;Clear the bit (7)() GOTO $+1 ; jump to start of bit5 (2)() ; Here count is (2)(2), change to bit5 at (7), Icount 8 NOP ; (3)(3) SKIP_BSet 3,TxDATA ; skip if bit1 set (4)(5) GOTO b3CLR ; bit is clr, jump (6)() NOP ; adjust before set ()(6) BSET PORT_A,bit0 ;Set the bit ()(7) GOTO $+3 ; jump to start of bit2 ()(2) b3CLR: BCLR PORT_A,bit0 ;Clear the bit (7)() GOTO $+1 ; jump to start of bit5 (2)() ; Here count is (2)(2), change to bit6 at (7), Icount 8 NOP ; (3)(3) SKIP_BSet 3,TxDATA ; skip if bit1 set (4)(5) GOTO b3CLR ; bit is clr, jump (6)() NOP ; adjust before set ()(6) BSET PORT_A,bit0 ;Set the bit ()(7) GOTO $+3 ; jump to start of bit2 ()(2) b3CLR: BCLR PORT_A,bit0 ;Clear the bit (7)() GOTO $+1 ; jump to start of bit5 (2)() ; Here count is (2)(2), change to bit7 at (7), Icount 8 NOP ; (3)(3) SKIP_BSet 3,TxDATA ; skip if bit1 set (4)(5) GOTO b3CLR ; bit is clr, jump (6)() NOP ; adjust before set ()(6) BSET PORT_A,bit0 ;Set the bit ()(7) GOTO $+3 ; jump to start of bit2 ()(2) b3CLR: BCLR PORT_A,bit0 ;Clear the bit (7)() GOTO $+1 ; jump to start of bit5 (2)() ; Here count is (2)(2), change to Stopbit5 at (6), Icount 4 CALL DELAY4 ; delay 4 CLK's (4)(4) NOP ; (5)(5) BSET PORT_A,bit0 ;Set the bit (6)(6) ; Thats it, stop bit set, we can exit ;DELAY4 RETURN

The time of day 'time stamp'

The 'obvious' approach is to have our DIY RTC send it's time is exactly the same format as the 'standard' DS1307 chip (so you can plug in a real 1307 to debug your Pi code until you get the PIC running :-) )

The 1307 'time' is BCD encoded (so eg 59 seconds == 0x59) in the first 7 registers, seconds, minutes, hours, day, date, month, and year. By default, hours is 24 format. Register 0x3F contains the 'high digits' of the year (default is 0x20).

The basic accuracy is thus 'to within 1 second'

The 1307 runs at 32kHz and uses the "I2C" = a 2 wire serial interface (see here for I2C tutorial)

When using the I2C bus to communicate with the 1307, the Pi would 'send' the register number it wanted to 'read' and the 1307 would respond with that registers contents. However because our PIC is using a rather more basic serial line 'send only' approach, it will transmit all 7 bytes every time a second changes

Keeping time

Rather than store the time in a 32bit binary number - which would then require 'conversion' into BCD - our PIC will 'add up' the time directly into the 7 BCD registers

Time subroutine

Called on each 'tick', the seconds, minutes, hours, day, date, month, and year BCD bytes are incremented as appropriate

The 16F54 has no 'ADD (or SUBtract) value to Accumulator' (you can load a value to Acc and the 'ADD or SUB Acc from reg, placing the result back in Acc' ). This makes value comparisons a bit harder than you might think. To save time, the register values are checked before being incremented
Tick: INC seconds ; ASSUME we just need to inc the units LOAD 0x5A ; was 59 ? (0x5A after inc) SUB Acc,seconds,Acc ; Acc (0x5A) - seconds, result to Acc SKIP NZ ; skip if result non-zero (i.e. was not 59) GOTO MinsTick ; 59s, goto increment the miniutes (note, Acc=0) AND 0x0F ; < 59, but was units less than 9 ? SKIP Z ; skip if it was 9 RETURN ; not 9, so we can just exit (we inc'd the units on entry ; OK units was 9, inc the 10's LOAD 0x10 ADD Acc,seconds,Acc ;seconds + Acc to Acc AND 0xF0 ; zero the units COPY Acc,seconds ;save the new count RETURN MinsTick: COPY Acc,seconds ; clr seconds INC mins ; ASSUME we just need to inc the units LOAD 0x5A ; look for was 59 (i.e. 0x5A after inc) SUB mins,Acc,Acc ; mins-Acc (0x5A), result to Acc SKIP NZ ; skip if result non-zero (i.e. was not 59) GOTO HrTick ; 59m, goto increment the Hours (note, Acc=0) AND 0x0F ; < 59, but was units less than 9 ? SKIP Z ; skip if it was 9 RETURN ; not 9, so we can just exit (we inc'd the units on entry) ; OK units was 9, inc the 10's LOAD 0x10 ADD Acc,mins,Acc ;mins + Acc to Acc AND 0xF0 ; zero the units COPY Acc,mins ;save the new count RETURN HrsTick: ;59m 59s COPY Acc,mins ; zero the mins INC hours ; assume inc LOAD 0x24 ; was 23? (now 24) SUB hours,Acc,Acc ; hours was 23 ? SKIP NZ ;skip if not 23 GOTO DaysTick ; go to increment the days (note, Acc=0, so can be written straight back) ; Here hours are less than 23, if units <9 (now

Next subject :- PIC alarm controller

[top]