-
This Is The End (I Hope).
03/05/2019 at 21:45 • 0 commentsI assembled two Hat versions and two 18650 versions. All PCBs are the latest revisions. All of the boards are functional, but I haven't bothered to take extensive parametrics. One of the 18650 boards is in service to my seismometer. The other 18650 board will be a spare. One of the Hat versions will find use in support of the various Raspberry Pi boards performing various functions around the house. The other has been shipped to a friend in The Netherlands that has been an invaluable consultant and sounding board for these kinds of things.
Discovered A Design Flaw:
I confronted one of my design assumptions today when swapping out an older UPS version for this version to service my seismometer. When I upgraded my seismometer I did not connect the UPS back into the system properly. Consequently, the backup battery drained to unsafe levels. When I transferred the battery, which had now drained to a voltage around 1V, into the U1LiUPSRPi-18650 version the UPS failed to supply power to the Raspberry Pi.
The problem was that I assumed that the battery would almost always have a potential greater than the POR threshold of the PIC16F18323. In this particular case it did not, so the PIC kept the LTC4040 shutdown. Without the LTC4040 activated the output voltage of the UPS did not increase to the point where the PIC could operate. I removed the battery and performed a recovery operation on it (10% charge current rating until the battery voltage exceeded 2.5V), then reinserting the battery when the voltage exceeded 3.2V. At this point the UPS worked properly again and proceeded to charge the battery to full.
If this was a commercial product I would be iterating the design at least once more. The UPS should function whenever a valid supply voltage is available at its inputs. But since it is primarily for my own use...it's good enough, as long as I'm aware of its shortcomings. BTW: I think the flaw could be corrected with another low current Schottky diode from the input to the PIC VDD pin.
LiFePO4 Disappointment:
I purchased four PKCELL 14500 LiFePO4 batteries from Amazon to see how well they perform compared to the 14500 Li-Ion cells. Just comparing W-hr the LiFePO4 are inferior:
W-hr = 600mAh x 3.2V = 1.9 W-hr for LiFePO4
W-hr = 650mAh x 3.7V = 2.4 W-hr for Li-Ion.
It also appears that the LiFe cells have a relatively high internal resistance and high leakage current. I measured the current that the UPS drains from the battery terminal as 45µA, so it should be weeks between recharge events. But the log is reporting that the battery need recharging every day:
2019-03-06 08:07:50,872 INFO *** UPS Monitor is active ***
Battery Type is LiFePO4
Charge Termination Voltage = 3.62019-03-06 14:25:14,430 INFO Battery is charging. VBAT = 3.46
2019-03-06 14:25:34,453 INFO Battery charging terminated. VBAT = 3.63
2019-03-07 14:31:23,550 INFO Battery is charging. VBAT = 3.63
2019-03-07 14:31:33,563 INFO Battery charging terminated. VBAT = 3.63
2019-03-08 14:34:55,227 INFO Battery is charging. VBAT = 3.60
2019-03-08 14:35:05,277 INFO Battery charging terminated. VBAT = 3.63
2019-03-09 14:35:14,757 INFO Battery is charging. VBAT = 3.47
2019-03-09 14:35:34,780 INFO Battery charging terminated. VBAT = 3.63
2019-03-10 15:33:21,720 INFO Battery is charging. VBAT = 3.47
2019-03-10 15:33:31,732 INFO Battery charging terminated. VBAT = 3.63The recharge event is usually less than 1 minute. I had to change the update time for the UPS monitor program to 10 seconds in order to catch the event. I don't know what effect that will have on battery longevity. I do know that the LiIon 14500 cell did not require a recharge for at least three weeks.
If I find anything else that's noteworthy I'll post it here.
-
U1LiUPSRPi Python Code
03/04/2019 at 19:17 • 3 commentsI assembled the latest PCBs for the Hat and 18650 yesterday. Both are performing as designed. Time to finalize the code. The logging information contains the battery chemistry and the charger termination target voltage when the ups monitor program begins. The log also records power failures, power recovery, fault events, charging events, and shutdowns.
#!/usr/bin/python3 #------------------------------------------------------------------------------- # Name: ups_u1liupsrpi.py # Purpose: # # Author: Bud Bennett # # Created: 31-01-2019 # Copyright: (c) Bud Bennett 2017 # Licence: <your licence> #------------------------------------------------------------------------------- ''' This program monitors the status of the UPS via the i2c interface and manages the Raspberry Pi shutdown when the battery is exhausted. Normally: This programs powers off the Raspberry Pi when UPS indicates that PWRGOOD = 0 (power has failed) and the battery voltage is less than 3.0V (for LiIon) or 2.75V (for LiFePO4). Otherwise: The UPS will assert BATLOW = 1, wait 20 seconds and then disconnect power when it determines that the battery has dropped below 2.75V (for LiIon) or 2.5V (for LiFePO4). If the UPS battery voltage (VBAT) is greater than 3.3V, the status is checked every minute. If VBAT < 3.3V then the checking interval is shortened to 1 second. Normally: If the Pi sets SHUTDN = 1 then the UPS asserts BATLOW = 1 as confirmation and immediately begins a 20 second timer to terminate the power. Otherwise: When the UPS asserts BATLOW = 1, the UPS will wait 5 seconds for the Raspberry Pi to acknowledge by setting the SHUTDN bit. If the Pi has not set the SHUTDN bit within the 5 second period then the UPS begins the 20 second timer irregardless. So the Pi must initiate it's shutdown sequence immediately after receiving the BATLOW signal from the UPS and sending/confirming the SHUTDN acknowledgement. ''' try: from smbus import SMBus except ImportError as e: print("Python module smbus not found, install it first") # Install the i2c interface using the following instructions: # sudo apt-get install i2c-tools # sudo apt-get install python-smbus # sudo raspi-config , goto Interfacing Options, enable i2c # sudo reboot # to make the i2c interface active # now check to see if the USB i2c device is present at address 36 # sudo i2cdetect -y 1 sys.exit(0) import time, os from datetime import datetime import logging import logging.handlers import traceback import sys # instantiate smbus interface i2c = SMBus(1) # set interface to i2c-1 (newer RPi's) #--logger definitions # save daily logs for 7 days LOG_FILENAME = "/var/log/bud_logs/ups_mon.log" LOG_LEVEL = logging.INFO # Could be e.g. "TRACE", "ERROR", "" or "WARNING" logger = logging.getLogger(__name__) logger.setLevel(LOG_LEVEL) handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=10000, backupCount=4) # save 4 logs formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) class MyLogger(): ''' A class that can be used to capture stdout and sterr to put it in the log ''' def __init__(self, level, logger): '''Needs a logger and a logger level.''' self.logger = logger self.level = level def write(self, message): # Only log if there is a message (not just a new line) if message.rstrip() != "": self.logger.log(self.level, message.rstrip()) def flush(self): pass # do nothing -- just to handle the attribute for now # Global flag to print more information DEBUG = False # If this script is installed as a Daemon by systemd, set this flag to True: DAEMON = True # when run as daemon, pipe all console information to the log file # --Replace stdout and stderr with logging to file so we can run it as a daemon # and still see what is going on if DAEMON : sys.stdout = MyLogger(logging.INFO, logger) sys.stderr = MyLogger(logging.ERROR, logger) def read_ups_data(address=0x36, cmd=0x00): global DAEMON, DEBUG if (DEBUG) and (not DAEMON) : print('Sending command {0:02x}'.format(cmd)) # logger.trace("Sending command {0:02x}".format(command)) try: word = i2c.read_word_data(address, cmd) bus_fail = 0 except Exception as e: # except all exceptions write_log("ERROR", "read_ups: I2C exception") bus_fail = 1 if bus_fail == 1: VBAT = 5.0 BATLOW = 0 SHUTDN = 0 F2 = 1 F1 = 0 F0 = 0 CHRG = 0 FAULT = 0 PWRGOOD = 1 else: byte1 = 255 & word byte2 = word >> 8 PWRGOOD = byte1 & 0x01 BATLOW = (byte1 & 0x02) >> 1 SHUTDN = (byte1 & 0x04) >> 2 F0 = (byte1 & 0x08) >> 3 F1 = (byte1 & 0x10) >> 4 F2 = (byte1 & 0x20) >> 5 CHRG = (byte1 & 0x40) >> 6 FAULT = (byte1 & 0x80) >> 7 if byte2 != 0: VBAT = 4.08 * byte2/255 else: VBAT = 5.0 write_log("ERROR", "ADC value error") if (F2): bat_type = "LiIon" else: bat_type = "LiFePO4" if (F2): # charger voltages for LiIon: if (not F1 and not F0): chrg_v = 3.95 elif(not F1 and F0): chrg_v = 4.00 elif(F1 and not F0): chrg_v = 4.05 else: chrg_v = 4.10 else: # charger voltages for LiFePO4: if (not F1 and not F0): chrg_v = 3.45 elif(not F1 and F0): chrg_v = 3.50 elif(F1 and not F0): chrg_v = 3.55 else: chrg_v = 3.60 if (DEBUG) and (not DAEMON): print('PWRGOOD = {0}'.format(int(PWRGOOD))) print('BATLOW = {0}'.format(int(BATLOW))) print('SHUTDN = {0}'.format(int(SHUTDN))) print('BAT Type = {0}'.format(bat_type)) print('Charging Termination Voltage = {0}'.format(chrg_v)) if (CHRG): print("Battery is charging.") if (FAULT): print("Charger is Faulted") print('VBAT = {0:.2f}\n'.format(VBAT)) return PWRGOOD, BATLOW, SHUTDN, bat_type, chrg_v, CHRG, FAULT, VBAT, bus_fail def write_log(level="INFO", msg=""): global DEBUG, DAEMON if (level is "ERROR"): if (DEBUG) and (not DAEMON) : print("ERROR: " + msg + "\n") logger.error(msg) else: if (DEBUG) and (not DAEMON) : print(msg + "\n") logger.info(msg) def main(): global DEBUG, DAEMON ups_address = 0x36 command = 0x00 # initialize command code pgood_flag = 1 CHRG_old = 0 FAULT_old = 0 time_now = datetime.now() PWRGOOD, BATLOW, SHUTDN, bat_type, chrg_v, CHRG, FAULT, VBAT, bus_fail = read_ups_data(ups_address, command) write_log("INFO", "*** UPS Monitor is active ***\n\tBattery Type is {0}\n\tCharge Termination Voltage = {1}\n".format(bat_type, chrg_v)) if (CHRG): write_log("INFO","Battery is Charging. VBAT = {0:.2f}".format(VBAT)) if (FAULT and CHRG): write_log("INFO","Charger is temporarily Faulted due to temperature outside limits.") elif(FAULT and not CHRG): write_log("ERROR","Charger is permanently faulted. Check Battery") CHRG_old = CHRG FAULT_old = FAULT try: while True: # read the UPS PWRGOOD, BATLOW, SHUTDN, bat_type, chrg_v, CHRG, FAULT, VBAT, bus_fail = read_ups_data(ups_address, command) # de-clutter log -- only send powerfail info once if (not PWRGOOD and pgood_flag): time_now = datetime.now() pgood_flag = 0 write_log("INFO", "Power failed: PWRGOOD = {0} VBAT = {1:.2f}".format(PWRGOOD, VBAT)) VBAT_old = VBAT elif(PWRGOOD and not pgood_flag): time_now = datetime.now() pgood_flag = 1 write_log("INFO", "Power restored: PWRGOOD = {0} VBAT = {1:.2f}".format(PWRGOOD, VBAT)) if (not PWRGOOD and VBAT < VBAT_old - 0.05): logger.info("VBAT = {0:.2f}".format(VBAT)) VBAT_old = VBAT # if the UPS has set BATLOW or VBAT < 3.0V, then send SHUTDN command to initiate UPS shutdown if ((BATLOW or ((bat_type is "LiIon" and VBAT < 3.0) or (bat_type is "LiFePO4" and VBAT < 2.8))) and not PWRGOOD): write_log("INFO", "Sending shutdown command to UPS: PWRGOOD = {0} VBAT = {1:.2f}".format(PWRGOOD, VBAT)) command = 0x04 PWRGOOD, BATLOW, SHUTDN, bat_type, chrg_v, CHRG, FAULT, VBAT, bus_fail = read_ups_data(ups_address, command) # confirm that the UPS received the shutdown command and then shutdown the Pi if (SHUTDN and command == 0x04): write_log("INFO", "UPS confirmed, shutting down now!") os.system("sudo shutdown -h now") # temporarily disable while debugging while True: time.sleep(10) if (CHRG != CHRG_old): if (CHRG): write_log("INFO", "Battery is charging. VBAT = {0:.2f}".format(VBAT)) elif(PWRGOOD): write_log("INFO", "Battery charging terminated. VBAT = {0:.2f}".format(VBAT)) CHRG_old = CHRG if (FAULT != FAULT_old): if (FAULT and CHRG): write_log("INFO", "Charger is temporarily faulted due to temperature outside limits.") elif(FAULT and not CHRG): write_log("INFO", "Charger is permanently faulted. Check Battery") else: write_log("INFO", "Fault has been reset.") FAULT_old = FAULT # check UPS status at 1 minute intervals until battery voltage drops below 3.2V, then # decrease interval to 1 second. if ((bat_type is "LiIon" and (VBAT < 3.3)) or (bat_type is "LiFePO4" and VBAT < 3.0)): time.sleep(1) else: time.sleep(60) # use 10 seconds during testing, 60 when running normally except KeyboardInterrupt: if (DEBUG) and (not DAEMON): print ("\nCtrl-C Terminating") except Exception as e: sys.stderr.write("Got exception: %s" % e) if (DEBUG) and (not DAEMON): print(traceback.format_exc()) logger.error(str(traceback.format_exc())) os._exit(1) if __name__ == '__main__': main()
systemd is used to manage the program as a daemon. It requires the ups.service file to be located in /lib/systemd/system. The service is started after the network is available to get the system time and the system is idle -- very late in the boot process. Otherwise the monitor program will report the incorrect time in the logs (as shown later).
# This service installs a python script that communicates with the UPS hardware. # It also provides logging to a file # the ups.service file is located in /lib/systemd/system/ # To test, use sudo systemctl start|stop|status ups # To install during the boot process, use: sudo systemctl enable ups # If this file gets changed, use: sudo systemctl daemon-reload # If the Python script is changed, use : sudo systemctl restart ups [Unit] Description=UPS Service Requires=basic.target After=network.target [Service] Type=idle User=pi ExecStart=/usr/bin/python3 /home/pi/programs/ups/ups_u1liupsrpi.py Restart=on-failure RestartSec=10 TimeoutSec=10 # The number of times the service is restarted within a time period can be set # If that condition is met, the RPi can be rebooted # WARNING: # Only use these options with a working system! #StartLimitBurst=4 #StartLimitInterval=180s # actions can be none|reboot|reboot-force|reboot-immidiate #StartLimitAction=reboot # The following are defined the /etc/systemd/system.conf file and are # global for all services # #DefaultTimeoutStartSec=90s #DefaultTimeoutStopSec=90s # # They can also be set on a per process here: # if they are not defined here, they fall back to the system.conf values #TimeoutStartSec=2s #TimeoutStopSec=2s [Install] WantedBy=multi-user.target
This is what the ups monitor reported for the 18650 version supplying power to a raspberry pi zero W:
2019-03-03 18:20:48,568 INFO *** UPS Monitor is active ***
Battery Type is LiIon
Charge Termination Voltage = 4.12019-03-03 18:20:48,571 INFO Battery is Charging. VBAT = 4.08
2019-03-03 21:45:32,586 INFO Power failed: PWRGOOD = 0 VBAT = 4.08
2019-03-03 21:46:32,639 INFO Power restored: PWRGOOD = 1 VBAT = 4.08
2019-03-03 21:46:32,642 INFO Battery is charging. VBAT = 4.08
2019-03-03 21:54:33,048 INFO Battery charging terminated. VBAT = 4.08
2019-03-03 22:28:34,618 INFO Power failed: PWRGOOD = 0 VBAT = 4.08
2019-03-03 23:44:37,630 INFO VBAT = 4.02
2019-03-04 00:27:39,611 INFO VBAT = 3.95
2019-03-04 01:20:42,628 INFO VBAT = 3.89
2019-03-04 02:18:45,992 INFO VBAT = 3.82
2019-03-04 03:11:49,067 INFO VBAT = 3.76
2019-03-04 04:06:52,238 INFO VBAT = 3.70
2019-03-04 05:11:55,970 INFO VBAT = 3.63
2019-03-04 06:42:01,158 INFO VBAT = 3.57
2019-03-04 07:39:03,514 INFO VBAT = 3.50
2019-03-04 08:22:05,248 INFO VBAT = 3.44
2019-03-04 09:07:07,203 INFO VBAT = 3.38
2019-03-04 09:22:07,787 INFO VBAT = 3.31
2019-03-04 09:33:33,994 INFO VBAT = 3.25
2019-03-04 09:43:41,436 INFO VBAT = 3.18
2019-03-04 09:53:57,893 INFO VBAT = 3.12
2019-03-04 10:03:05,188 INFO VBAT = 3.06
2019-03-04 10:09:46,139 INFO VBAT = 2.99
2019-03-04 10:09:46,142 INFO Sending shutdown command to UPS: PWRGOOD = 0 VBAT = 2.99
2019-03-04 10:09:46,147 INFO UPS confirmed, shutting down now!
2019-03-04 10:10:18,085 INFO *** UPS Monitor is active ***
Battery Type is LiIon
Charge Termination Voltage = 4.12019-03-04 10:10:18,092 INFO Battery is Charging. VBAT = 3.44
Note that the 18650 ups supplied power to the idling Zero W for almost 12 hours!! Note that the first couple of log entries after boot up have an incorrect time stamp. That is because the service started before the clock was set to the correct time. Setting the service unit to start after the network has started and the system is idle should prevent incorrect times in the log. (Does anybody have a better idea?)
-
The 18650 Version.
02/19/2019 at 16:37 • 0 commentsThe latest schematic for this version is shown in the details section of this project.
Now that the Hat version is pretty much performing to specifications, I'm ready to expand the UPS to an 18650 battery form factor. The only schematic differences between the two versions is the increased charging current to 1A and the removal of the LED indicators. Typical 18650 Li-Ion batteries have a capacity around 1.5-2.5Ah, so the UPS should charge the battery at about 1/2 C rate. More than this would overload a typical AC adapter rated for 2.5A @5V.
Almost all of the components are mounted on the top side of the PCB, which is the same width as the battery holder. The reset push button, 10K NTC thermistor, JST connector for the I2C interface, and the output voltage terminal block are also mounted on the bottom side of the PCB. I expect this unit to be mounted with the battery holder facing up, and servicing a headless Raspberry Pi in a closet somewhere, so the LED indicators were removed.
New PCBs for both versions were ordered from JLCPCB in 1 oz copper. I expect to receive them in a bit more than one week.
-
First Pass Preliminary Test Results
02/01/2019 at 05:24 • 0 commentsThese PCBs are technically the second pass, but I never assembled the first pass PCBs because they did not have the proper cutout to fit the case for the Raspberry Pi.
Executive Summary:
The performance, as far as tested, meets the design objectives. There is one functional failure -- the RESET function doesn't put the part to sleep when power is not applied -- the fix is easy but will require another PCB turn.
Functional Testing:
- When the battery was inserted, or the RESET button was pressed, the circuit immediately went into backup mode by providing output power via the battery. I was expecting the part to enter sleep mode waiting for power to be applied. Pressing the RESET button caused the circuit the enter backup mode until it was released, then the output voltage would fall for a second but then recover back to backup mode. The cause of this phenomenon was that R15 was connected to GND, so when RESET was pressed and the PIC outputs all went to high impedance the LTC4040 was enabled instead of disabled. The fix is to connect R15 to either BAT or the PIC's VDD supply. I cut R15's trace to GND and rewired it to BAT and this problem went away. This fix will require another PCB pass.
- The primary ability of automatic switchover to battery power appears to work well. I've been testing this function on a Raspberry Pi Zero W for a couple of days without a glitch.
- All of the PIC code appears to work well. The I2C interface is transmitting all of the new status bits and the Raspberry Pi can easily decode them without error. The PIC properly controls the on/off state of the LTC4040 and responds when the ACPR line is pulled low by the LTC4040. It also properly detects ACPR when in sleep mode. The PIC code doesn't appear to require any changes for functionality.
- The battery charger is functioning properly. At first the charger current was very low because the PROG pin was not connected. A bit of re-soldering fixed it. The DIP switches alter the charge termination voltage to correct levels. The Charge LED and Fault (cold) LED work as designed and the status is passed to the RPi via I2C.
Parametric Testing (room temp, Vin = 5.2V):
- Battery current when shutdown: 17.8µA @ 4.1V, 10µA @ 3.5V.
- Battery current when AC is present but not charging: 47.5µA.
- Output voltage: 4.79V with 4Ω load. No change with lighter load.
- Battery Charge Current: 640-670mA (target = 600mA)
- Battery termination and recharge voltages:
(This chart shows the wider spread reflected in the data sheet for when F2=0 and F1 = 1)
- ADC voltage error: +40-50mV (about 1.25%, which is acceptable)
- Booster efficiency with 4Ω load: 90% for VBAT = 4.1V, 77% for VBAT = 3.0V
- Input current limit on AC adapter: TBD.
- Fault thresholds on NTC pin: Cold = 75%, Hot = 34.6% (slightly low, but in spec).
Testing to date shows performance meeting specs, except for battery charging current, which is too high. The low currents from the battery indicate that it will be a long time between recharge events so expect long battery life.
-
Coding for the PIC
12/11/2018 at 02:11 • 0 commentsI updated the assembler code for the PIC. Here's the latest flow chart:
The flowchart also shows how the F0-F2, CHRG_ and FAULT_ pins are driven to zero when the PIC is running off the battery voltage but change to inputs with weak pull-ups when running off the 5V UPS output rail.
BATCHECK:
Here's the flow chart for the subroutine to check the battery voltage:
FVR is the Fixed Voltage Reference (set to 4.096V). The FVR and the ADC are normally shutdown when not needed to reduce power. The normal duty cycle is one sample per second. The rolling average is 8 samples.
After a bit of real-time debugging I will post the latest code to the files section of this project. I will also keep this log updated with the latest info regarding the PIC code.
-
Design Considerations
12/07/2018 at 10:33 • 0 commentsThis is a distillation of the thought process involved with putting this UPS together. There are always compromises to be made. I will go through the trade-offs involved with this design -- section by section. It's a bit long-winded, but I found it is best to put it into words before you spend money on parts/services. There was a saying in my engineering design circles, "If you can't explain how it works, you don't know."
Basic LTC4040 application
I haven't deviated much from what Analog Devices recommends for the LTC4040. Refer to the LTC4040 data sheet for particulars. I have included a couple of band-aids to help alleviate potential problems base upon my experience with the LTC4041 -- a supercap back up manager.
There are multiple ceramic capacitors at the load and also at the battery. This is normal SMPS procedure to place a smaller ceramic capacitor along with a larger ceramic capacitor at the input/output of the switching regulator. The smaller cap has a higher resonance frequency and tends to quell the HF stuff. The larger cap provides a lower Z but has a lower resonance frequency. The combination of the two capacitors covers a larger range and tends to prevent problems with ringing in response to switch transients. There is also a large tantalum capacitor, C4, which is a tank capacitor to hold up the output when the input power fails.
The input switch
The LTC4040 data sheet block diagram shows two input switches required to disconnect the input from the load. The basic application of the LTC4040 shows an NMOS FET connected with its source at the input and the drain connected to the load. This doesn't work for this application because the body diode of the FET will keep power applied to the load even if its gate is grounded. This application can get away with a single FET switch to disconnect the input from the load because the input source nearly always (always) becomes a high impedance when the AC power fails. In this case, M1 holds the input voltage at a diode drop below the output voltage because the FET drain-source is reversed. When IGATE is pulled to GND the FET turns off and allows the load voltage to drop to zero.
Note: if the input power source fails by shorting its output to GND then this approach would not work. In my experience all 5V AC adapters become high Z when the AC power fails.
The TSM038 is a logic level FET switch with 4mΩ Rdson, 30V BVDSS and can handle ± 20V from gate to source, which is necessary because the IGATE voltage can exceed 8V from gate to source. It costs about $0.60 in low quantities.
Safety First
I had originally intended that the battery charger parameters be programmed by a 3 position dip-switch. I got rid of the dip-switch when I upgraded the PIC controller to 14 pins, thinking that all the charger parameters could be programmed by the Raspberry Pi via the I2C interface. But this idea quickly fell by the wayside simply because it would expose the charger parameters to being hacked, which is a safety issue. So the dip-switches came back (albeit in SMD form) and the PIC is simply informing the Raspberry Pi about the switch settings and has no control over them. The user must set the dip switch positions when he/she installs the battery into the UPS.
I also implemented the thermistor sensor to prevent charging outside a safe temperature range. The sensor must be attached to the battery, not to the PCB, in order to be effective. I figure you can just tape it to the battery. Having the thermistor attached to the battery will prevent dangerous situations that could cause the battery to overheat and catch fire. At cold temperatures the battery electrolyte freezes so it is not a good idea to charge it under those conditions. The thermistor is a typical 10k NTC type with a B25/50 = 3950, which matches the desired temperature characteristic called out in the LTC4040 data sheet.
Additionally, the CHRG_ and FAULT_ status from the LTC4040 are monitored by the PIC and provided over the I2C interface. The Raspberry Pi might be able to communicate with the user if a fault condition is detected.
The PIC
With all of the added monitoring to perform, the PIC needed more pins. I knew that Microchip offered the PIC16F18323, a pin-count upgrade to the PIC16F313 that I had used previously, in a TSSOP-14 package that is approximately the same area as the SOIC-8 package used by the 8-pin version.
The PIC gets its power from the battery when the LTC4040 is shut down. When AC power is available the PIC gets power from the UPS output (~5VDC). A dual Schottky diode, D1, routes the highest voltage to the PIC power pin. When operating from the battery the PIC only draws a couple µA because it is in sleep mode. When active, the PIC can draw several hundred µA so it's best to get it off the battery -- to maximize the time between charging cycles and increase battery longevity.
In an effort to reduce component count the PIC uses its weak pull-ups instead of adding discrete resistors. The downside is that those weak pull-ups aren't so weak -- 200µA. When the UPS is powered up the weak pull-ups are engaged on FAULT_, CHRG_, F0-F2. When the LCT4040 is shutdown, and the PIC is running off the battery, weak pull-ups are disabled and those pins are driven to GND by logic outputs to keep indeterminate currents from flowing in the PIC input circuits when pins float.
AC presence detection, ACPR, is performed by the PIC when the LTC4040 is shut down, and by the LTC4040 when it is active. The PIC uses a resistor divider feeding a Schmitt Trigger input to get a rough idea of when power is available at the UPS input. The Schmitt Trigger is good for rejecting noise, and works well when the PIC is powered by the battery, but it is problematic when the PIC is operating from 5V. So whenever the PIC is getting power from the UPS output the ACPR input type is TTL, which has a VIHmin = 2.0V. If the LTC4040 PFI comparator detects that the AC adapter voltage has dropped below its threshold it pulls ACPR to GND and so informs the PIC that the power had failed. R22 reduces the current draw on the open-drain PFI_ output.
The PIC is now using an external pin, RC2, to monitor the battery voltage with an 8-bit ADC. I't pretty straightforward and there is a low frequency RC low-pass filter to reduce noise. The PIC has a 10-bit ADC, but it is a pain to implement the last two bits when all you have is an 8-bit µP. The resolution of the battery voltage is 16mV, which should be good enough.
R15 and R13 are probably not really required, but better safe than sorry. R13 just keeps the PIC from being reset until the pushbutton, S1, is pressed. There is an internal pull-up on the MCLR_ pin, but I don't know if it is always present. R15 is pure paranoia. As the PIC is booting up it floats all of its pins until they are assigned. It's better to have BSTOFF and CHGOFF set to VDD [2019-01-31 Edit. It was previously set to GND.] than float.
[2019-01-31 Edit: The first pass parts had problems because R15 was connected to GND rather than the VDD of the PIC. When the battery was inserted the PIC required some time to set the PWROFF pin high. This allowed the LTC4040 to apply backup power and force the output to 4.8V. Since this is identical to a failed power condition, the PIC just went with it and allowed the LTC4040 to continue its backup mission. If the RESET button was pushed it acually forced the LTC4040 into backup mode because the PIC reset condition allowed PWROFF to be low. If R15 is connected to BAT then the PWROFF pin is taken high and the LTC4040 is inhibited from applying backup power when the RESET button is pushed. After the RESET button is released the PIC continues to keep it high until it senses AC power is available. That is the correct functioning.]
The PCB Layout:
I've learned a few things about SMPS (Switched Mode Power Supply) design from the last few projects. There are a few things that you have to get right...the rest is small stuff. The things that you have to get right is the grounding and the switching nodes. Here's a view of the current layout around the inductor and the LTC4040:
Note that the trace from the SW pins to the inductor is as short as possible. This node is switching 5V p-p at 1.2MHz with up to 8A! Note also that the distance from C5 and C6 to the VSYS pins is very short and wide -- keeping the parasitic inductance/resistance to a minimum. The exposed pad of the LTC4040 is completely covered with vias to the ground plane on the bottom of the PCB -- same for the GND returns for C5, C6 and C10, C11. This will keep the ringing to a minimum. The rest of the layout almost doesn't matter if you get this part right.
The pads on L1 are really large and quite close together. I've found that if you make oversized pads like this it allows for many sizes of inductors to be soldered to the PCB. I believe that almost any 7mm x 7mm or 6mm x 6mm inductor size will fit these pads, but it will need to have a saturation current and thermal rating of at least 8A.
Kelvin Connections:
It's important to use Kelvin connections when monitoring current flow through a resistor.
Other Layout Stuff:
Resistor dividers that are used to set comparator trip voltages or output boost voltage or SCAP charge voltage must be accurate. Therefore they are located fairly close to the LTC4040. Components that are not critical are move farther away.