Embedded

NiMH battery maintenance trickle charger (Part3)

Introduction

In this section I'll be discussing the software for the timer in the trickle charger. The whole source code is available in a zip file here if you want to download it. Its free for you to use, as described in the licence at the top of the file. I won't have all the code in this article, just the "meat". If you want to see every byte and bit of it, then download the zip file .

Timing

Timing things accurately on a PIC 12F608/9 is a little problematical because the processor doesn't support any kind of interrupts other than using the watchdog timer to reset or wake up from sleep the device, but the watchdog timer isn't particularly accurate (to the extent that Microchip are very vague about what frequency the timer clocks at!). It also takes the watchdog some time to do the reset (~18mS). The only other options are:

  1. Count instructions very carefully and use NOPs to make sure alternate execution paths take the same amount of time to to execute, and so lock the timing to the CPU clock (which is fairly accurate, at better than 1%).
  2. Look at the TMR0 constantly until it overflows and then go off and do stuff. Provided you come back to check TMR0 before it gets chance to overflow, you can do stuff without worrying excessivly about execution time and yet still stay "in sync" with the timer.

Option (1) is a pain to do if you want to do accurate timing for short intervals, although it is immensly satisfying to do when it works out (and may be your only option if your generating waveforms close to the CPU clock frequency). For this project it would have been OK to make a small piece of code that looped for 30mS or so (to allow switch debounce) and ignore the few tens of microseconds difference in the excecution path, because the timing is not critical. However its not a terribly elegant solution to someone who worries far too much about such things. Needless to say, for this project I chose option (2)!

Using TMR0

The timer can be set to be clocked from the instruction clock (which with the internal oscillator is 1MHz±1%) either directly or through a prescaler. For this application I need the timer to overflow about a 100 times a second (so we can check the button and debounce it) which I will then subdivide down to ½ second intervals for flashing the LED, and further down to hours to count out the hours.

The TMR0 unit overflows on the 256th clock, but has a prescaler which when assigned to the timer can divide the clock by 2, 4, 8, 16, 32, 64, 128 or 256 by setting the PS<0:2> bits of the OPTION register. The resulting "frequencies" of TMR0 overflow can be calculated as:

PS<0:2> Rate Frequency
000 2 1953.125 Hz
001 4 976.563 Hz
010 8 488.281 Hz
011 16 244.141 Hz
100 32 122.070 Hz
101 64 61.035 Hz
110 128 30.518 Hz
111 256 15.259 Hz

A rate of 32 seems to fit the bill nicely. I generally prefer to set the static values of the TRIS and OPTION registers as #defines at the top of the program, so here are the various declarations and variable definitions:

#define NHOURS 10
;~WKPU, TMR0 clocked by prescaler /32
#define OPTIONVAL B'10010100'
;
;
; GPIO bits.
;
#define BUTTON   3
#define LED      2
#define HICHARGE 0
;
;
; Calculate the value to put in the GPIO TRIS.
#define TRISVAL 0XFF & ~(1<<LED | 1<<HICHARGE)
;
;
; Bit definitions for flags in the "FLAGS" byte
#define FLG_HI_CHARGE    0
#define FLG_FLASH_LED    1
#define FLG_LED_OFF      2
#define FLG_BUTTON_DOWN  3
; VARIABLES
                CBLOCK 0x07
FLAGS
DEBOUNCE_COUNT
TICK_COUNT
SEMISECONDS
MINUTES
HOURS
                ENDC

The variables I will describe later, but I will explain the FLAGS variable and the related #defines above it. Generally, when programming a PIC it is easier to check a bit in a variable than test the value of a variable, so when you need to keep track of things that have happened, or the status of something I prefer to create a byte or 2 of flags and define which bits mean what with a #define. (I could have just as easily used an EQU statement, I guess I have programmed C for too long!).

Initialisation

The initialisation of the PIC goes like this:

;
                ORG 0x0000

RESET
                MOVWF   OSCCAL
                CLRF    STATUS
                CLRWDT
                MOVLW   TRISVAL         ; led/hicharge out, others in
                TRIS    GPIO
                MOVLW   OPTIONVAL
                OPTION
                CLRF    TMR0
                MOVLW   1<<LED          ; Switch LED on, transistor off
                MOVWF   GPIO

Then we initialise all the variables. Note I usually define the flag state to be such that clearing all the flags leaves them in a constant initial state:

;
                CLRF    FLAGS
                MOVLW   .61
                MOVWF   TICK_COUNT
                MOVLW   .120
                MOVFW   SEMISECONDS
                MOVLW   .60
                MOVFW   MINUTES
                MOVLW   NHOURS
                MOVFW   HOURS

Tick count is initialised to 61 so that a half-second (61 / 120.070) is initally counted down (see later!). The 120 semi-seconds makes a minute, and 60 minutes make an hour; we have yet another counter for the hours. Doing it this way ensures that we don't need more than 8 bits for any one counter, which would just complicate the software.

Sychronising with the timer

This section is the bit that ensures that any unused processor time is "soaked up" and the loops run in a regular timing. As mentioned before, we need to detect when the counter "rolls over" from 0xFF to 0x00. The simplest way to do this is to wait (if necessary) for the top bit of the timer to be set, and then wait for it to clear. This takes just 4 instructions:

TMR0OVF
                BTFSS   TMR0, 7
                GOTO    $-1
                BTFSC   TMR0, 7
                GOTO    $-1

The symbol '$' is set to the address of the instruction, so doing 'GOTO $-1' simply tells the PIC to repeat the last instruction. We test the top bit of the timer, and skip the goto if it is - then we test for the opposite, and the instruction following the 2nd GOTO will be executed when the timer rolls over.

Debouncing the key

The first job we do is to check for a keypress, see that it has been pressed lomg enough, and if so act on the button press:

;               ;
                ; Timer 0 has gone from 0xFF->0x00, which it does 122.070 times
                ; a second.
                BTFSC   GPIO, BUTTON
                GOTO    RELEASED
                BTFSC   FLAGS, FLG_BUTTON_DOWN
                GOTO    TICKTOCK        ; WE ALREADY KNOW ITS PRESSED.

Here we have seen the first of our flags! FLG_BUTTON_DOWN is set when we know a button has been pressed. If we already know its pressed, then we want to ignore it and go to the next step (TICKTOCK - update time counters) because we will already have taken the button press action. If not we want to count the number of consecutive occasions the button has been detected as pressed, and if it exceeds an (arbitrary!) limit we deem the button pressed:

;               ;
                ; IF THE BUTTON BIT HAS BEEN 0 FOR 8 TICKS (~=66mS) THEN
                ; WE COUNT IT AS PRESSED.
                INCF    DEBOUNCE_COUNT, F
                MOVLW   .8              ; IF COUNT >=8, THEN SET BUTTON PRESSED
                SUBWF   DEBOUNCE_COUNT, W
                SKPC
                GOTO    TICKTOCK

Note that when debouncing a key, as a rule-of-thumb, a debounce interval of >100 mS becomes noticable and irritating in a situation where an immediate action is desired. Conversely, if I had set this to a high value, say 120 rather than 8, then the user would have to hold the button down for a second before the action happened, which might actually be a desirable option for a control in a position where it might be accidently activated.

Having decided we have a button press, we set the flag (so the action is not repeated on the next test of the button) and either switch on or off the high rate charging, depending on the current setting of the FLG_HI_CHARGE flag:

;               ; Button is definitely pressed, so switch on/off the high
                ; charge, deal with the LED etc.
                BSF     FLAGS, FLG_BUTTON_DOWN
                BTFSS   FLAGS, FLG_HI_CHARGE
                GOTO    START_CHARGE
                GOTO    STOP_CHARGE
                ;

The START_CHARGE and STOP_CHARGE sections are the main action routines, and are much later in the program, so we'll deal with them then.

Next we deal with the case where the button is not pressed - the action here is simple, we clear the button down flag (so the next press causes an action) and clear the debounce counter. If the button is bouncing, then this will mean the counter gets reset before it reaches the magic number (8) and therefore the button press will not be registered until the signal stabilises:

RELEASED
                CLRF    DEBOUNCE_COUNT
                BCF     FLAGS, FLG_BUTTON_DOWN

That concludes processing of the button!

Next we need to divide the "ticks" by another 61 to get 2 ticks a second, so we can flash the LED at about 1 Hz (i.e. on for 1/2 second, off for 1/2 second). To do this we count them on the counter "TICK_COUNT" which you will remember we initialised to 61 during reset. On each incoming tick we take 1 from the counter until it reaches zero, when we reset it to 61 and perform the next action:

TICKTOCK
                ; So we count the ticks and when we have 61 we'll have
                ; waited a half second.
                DECFSZ  TICK_COUNT, F
                GOTO    TMR0OVF
                MOVLW   .61
                MOVWF   TICK_COUNT

At this point in the program, we get here twice a second, so we check if we're supposed to be flashing the LED, and if we are we do so:

;               ;
                ; Flash LED?
                ;
                BTFSS   FLAGS, FLG_FLASH_LED
                GOTO    NOFLASH
                ;
                BTFSS   FLAGS, FLG_LED_OFF
                GOTO    LED_IS_ON
                ;
                ; LED IS OFF, SO SWITCH IT ON
                ;
                BSF     GPIO, LED
                BCF     FLAGS, FLG_LED_OFF
                GOTO    NOFLASH
LED_IS_ON
                ;
                ; LED IS ON, SO SWITCH IT OFF
                ;
                BCF     GPIO, LED
                BSF     FLAGS, FLG_LED_OFF
                ;

All very straightforward!

Next we update the time counter for the charger. This is only necessary if we are actually in high charge mode, so we test the FLG_HI_CHARGE flag:

NOFLASH
                ; WE GET HERE TWICE A SECOND.
                ;
                BTFSS   FLAGS, FLG_HI_CHARGE
                GOTO    TMR0OVF

If we are in high charge mode, then we need to count down 10 hours. I could have had one HUGE counter to count down the 72,000 semi-seconds in 10 hours, but by splitting it into semiseconds, minutes and hours we make things a bit easier by having 3 eight bit counters rather than one 24 bit counter:

;               ;
                DECFSZ  SEMISECONDS, F
                GOTO    TMR0OVF
                MOVLW   .120
                MOVWF   SEMISECONDS
                ;
                ; WE GET HERE ONCE A MINUTE.
                DECFSZ  MINUTES, F
                GOTO    TMR0OVF
                MOVLW   .60
                MOVWF   MINUTES
                ;
                ; WE GET HERE ONCE AN HOUR...
                DECFSZ  HOURS, F
                GOTO    TMR0OVF

If we skip over the last GOTO, that means the 10 hours is up, and we can stop the charging process. Remember we also come here if the button is pressed while charging is in progress:

;               ;
                ; TIMERS RUN OUT, THEREFORE WE SWITCH OFF THE HI CHARGE
STOP_CHARGE
                BCF     FLAGS, FLG_HI_CHARGE
                BCF     FLAGS, FLG_FLASH_LED
                BCF     FLAGS, FLG_LED_OFF
                BCF     GPIO,  HICHARGE
                NOP
                NOP
                BSF     GPIO,  LED
                GOTO    TMR0OVF

Stopping the charge process just entails clearing a few flags, and switching the transistor drive OFF and the LED ON (its on steadily during trickle charge). The two NOPs between the two bit instructions is to allow the change to propagate from the output latch to the pins, as recommended in the data sheet for PIC when you update the same I/O Port consecutively.

Finally, the last bit of the program is where we start the charge:

START_CHARGE
                MOVLW   .120
                MOVWF   SEMISECONDS
                MOVLW   .60
                MOVWF   MINUTES
                MOVLW   NHOURS
                MOVWF   HOURS
                BSF     FLAGS, FLG_HI_CHARGE
                BSF     FLAGS, FLG_FLASH_LED
                BCF     FLAGS, FLG_LED_OFF
                BSF     GPIO,  HICHARGE
                NOP
                NOP
                BSF     GPIO,  LED
                GOTO    TICKTOCK

                END

This consists of initialising the tine counters, setting the appropriate flags, and setting the transistor drive and LED to ON. Note setting FLG_FLASH_LED here will cause the code to flash the LED to activate.

Thats pretty much it! If you want to use this code, the download the zip file because there are a few extra bits and more comments, and details of the software licence, along with a pre-assembled HEX file of the code which might also be useful if you just want to use the 10 hour timer.

If you make use of this code or the hardware design, please tell me and I'll give you a mention and put a link in to your project if its on the web.

Version 1 published 18 Sep 2012, midnight