NiMH battery maintenance trickle charger (Part3)
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 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:
- 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%).
- 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)!
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:
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!).
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 Sept. 17, 2012, 11 p.m.