HomeRaspberry PiPower no matter what! Waveshare UPS hat

Power no matter what! Waveshare UPS hat

Mission-critical projects, need this!

A long time ago, I covered a UPS extension board for RaspberryPi from 52pi. It was a great option at the time to keep your favourite board from shutting down abruptly on power loss. Unfortunately, the unexpected shutdown was still on the cards as the board had no way of letting the Pi know it is about to run out of power. That’s all about to change thanks to Waveshare’s UPS HAT(Amazon, AliExpress, ThePiHut).

Power loss protection with Waveshare UPS HAT

Waveshare UPS HAT for Raspberry Pi 4
Waveshare UPS HAT for Raspberry Pi 4

A recent power cut at work, which lasted almost an hour, reminded me that despite living in a country where power cuts rarely happen, it’s good to have your buttocks covered. After all, pulling the plug on a Raspberry Pi board can result in data loss and what’s worse, data corruption.

I think ThePiHut find out about that, as a day later I received a tempting email listing various Raspberry Pi items in stock, including the Waveshare UPS HAT. Who could resist the price of £21 for a 18650 powered board with interesting features?

The board screws up to the bottom of Raspberry Pi 4, 3B+/3 and supplies 5V with up to 5A peak current to keep the board operational even at the most demanding loads, with USB peripherals at work. Unlike the old 52pi counterpart, apart from 2 pogo pins delivering the power, Waveshare UPS HAT uses 2 extra pogo pins to communicate the UPS data via I2C. This design doesn’t obstruct the 40-pin header so you can slap extra HATs on your Raspberry Pi board.

The board requires 2 x 18650 batteries which output 8.4V to keep everything powered. To keep your board protected Waveshare UPS HAT is equipped with overcharge/discharge protection, over current protection, short circuit protection, and reverse protection – in case you decide to change the batteries without paying attention to the orientation. As a bonus, you also have an extra USB-A power to charge or power up another device. Waveshare UPS HAT comes with a dedicated 8.4V/2A charger and uses a DC jack to deliver the power. Once installed, you can ditch the Raspberry Pi power supply.

UPS but how long?

It’s near impossible to answer this question upfront, as it depends on a couple of conditions. First, you have to take the batteries capacity. 18650 comes in various flavours and Pi’s runtime will depend on the mAh stored.

Secondly, the power use of the board will change based on the number of peripherals connected and the amount of power Raspberry Pi needs to perform the computational operations. The board can draw as little as 300mAh for Raspberry Pi 3B in idle state to 3 times as much for the latest board during power-intensive benchmarks.

You should expect around 2-4h based on your load and the cells included. The good news is that your Raspberry Pi will be aware of how much juice Waveshare UPS HAT has, and can perform a shutdown safely when the power levels get critical. I fitted mine with 1200mAh cells and run a CPU intensive benchmark to see how long would the board last. With the current draw oscillating between 0.6-0.8mAh, the board shut the system down (I have it set at 25%) after exactly 2h.

The batteries were charged up again 2:20min later.

In use

Attach the Waveshare UPS HAT according to instructions, and power everything up. To receive the power usage data from the HAT, you have to enable the I2C interface in sudo raspi-config. Then try out the default Waveshare’s script to read the data from the board:

sudo apt-get install p7zip
wget https://www.waveshare.com/w/upload/4/4a/UPS_HAT_B.7z
7zr x UPS_HAT_B.7z -r -o./
cd UPS_HAT_B
python3 INA219.py

The script works on the latest version of RaspberryPi OS (Bullseye) but if you come across any issues like:

ModuleNotFoundError: No module named 'smbus'

Install supporting libraries:

sudo apt-get update
sudo apt-get install python3-smbus python3-dev i2c-tools

The installed code will give you access to the following metrics:

  • Load Voltage
  • Current (charge and discharge rate)
  • Power Usage
  • Battery level in %
  • PSU Voltage (optional)
  • Shunt Voltage (optional)
Waveshare UPS HAT for Raspberry Pi 4
Waveshare UPS HAT for Raspberry Pi 4

The sample code doesn’t include the safe shutdown routine, so you have to implement it yourself. I added a quick and dirty IF statement that shuts down my Raspberry Pi board at 30% power. Why 30%? It’s not healthy to discharge the cells, and I’m more concerned about safe shut-down than keeping the board running without an external source of power. After all, if the power supply is down, chances are, my entire automation has no power as well, leaving me only with the battery-powered ZigBee devices.

Safe shut-down at 30%
#!/usr/bin/python3

import smbus
import time
import sys
from subprocess import call
import requests 
import json


# Config Register (R/W)
_REG_CONFIG                 = 0x00
# SHUNT VOLTAGE REGISTER (R)
_REG_SHUNTVOLTAGE           = 0x01

# BUS VOLTAGE REGISTER (R)
_REG_BUSVOLTAGE             = 0x02

# POWER REGISTER (R)
_REG_POWER                  = 0x03

# CURRENT REGISTER (R)
_REG_CURRENT                = 0x04

# CALIBRATION REGISTER (R/W)
_REG_CALIBRATION            = 0x05

class BusVoltageRange:
    """Constants for ``bus_voltage_range``"""
    RANGE_16V               = 0x00      # set bus voltage range to 16V
    RANGE_32V               = 0x01      # set bus voltage range to 32V (default)

class Gain:
    """Constants for ``gain``"""
    DIV_1_40MV              = 0x00      # shunt prog. gain set to  1, 40 mV range
    DIV_2_80MV              = 0x01      # shunt prog. gain set to /2, 80 mV range
    DIV_4_160MV             = 0x02      # shunt prog. gain set to /4, 160 mV range
    DIV_8_320MV             = 0x03      # shunt prog. gain set to /8, 320 mV range

class ADCResolution:
    """Constants for ``bus_adc_resolution`` or ``shunt_adc_resolution``"""
    ADCRES_9BIT_1S          = 0x00      #  9bit,   1 sample,     84us
    ADCRES_10BIT_1S         = 0x01      # 10bit,   1 sample,    148us
    ADCRES_11BIT_1S         = 0x02      # 11 bit,  1 sample,    276us
    ADCRES_12BIT_1S         = 0x03      # 12 bit,  1 sample,    532us
    ADCRES_12BIT_2S         = 0x09      # 12 bit,  2 samples,  1.06ms
    ADCRES_12BIT_4S         = 0x0A      # 12 bit,  4 samples,  2.13ms
    ADCRES_12BIT_8S         = 0x0B      # 12bit,   8 samples,  4.26ms
    ADCRES_12BIT_16S        = 0x0C      # 12bit,  16 samples,  8.51ms
    ADCRES_12BIT_32S        = 0x0D      # 12bit,  32 samples, 17.02ms
    ADCRES_12BIT_64S        = 0x0E      # 12bit,  64 samples, 34.05ms
    ADCRES_12BIT_128S       = 0x0F      # 12bit, 128 samples, 68.10ms

class Mode:
    """Constants for ``mode``"""
    POWERDOW                = 0x00      # power down
    SVOLT_TRIGGERED         = 0x01      # shunt voltage triggered
    BVOLT_TRIGGERED         = 0x02      # bus voltage triggered
    SANDBVOLT_TRIGGERED     = 0x03      # shunt and bus voltage triggered
    ADCOFF                  = 0x04      # ADC off
    SVOLT_CONTINUOUS        = 0x05      # shunt voltage continuous
    BVOLT_CONTINUOUS        = 0x06      # bus voltage continuous
    SANDBVOLT_CONTINUOUS    = 0x07      # shunt and bus voltage continuous


class INA219:
    def __init__(self, i2c_bus=1, addr=0x40):
        self.bus = smbus.SMBus(i2c_bus);
        self.addr = addr

        # Set chip to known config values to start
        self._cal_value = 0
        self._current_lsb = 0
        self._power_lsb = 0
        self.set_calibration_32V_2A()

    def read(self,address):
        data = self.bus.read_i2c_block_data(self.addr, address, 2)
        return ((data[0] * 256 ) + data[1])

    def write(self,address,data):
        temp = [0,0]
        temp[1] = data & 0xFF
        temp[0] =(data & 0xFF00) >> 8
        self.bus.write_i2c_block_data(self.addr,address,temp)

    def set_calibration_32V_2A(self):
        """Configures to INA219 to be able to measure up to 32V and 2A of current. Counter
           overflow occurs at 3.2A.
           ..note :: These calculations assume a 0.1 shunt ohm resistor is present
        """
        # By default we use a pretty huge range for the input voltage,
        # which probably isn't the most appropriate choice for system
        # that don't use a lot of power.  But all of the calculations
        # are shown below if you want to change the settings.  You will
        # also need to change any relevant register settings, such as
        # setting the VBUS_MAX to 16V instead of 32V, etc.

        # VBUS_MAX = 32V             (Assumes 32V, can also be set to 16V)
        # VSHUNT_MAX = 0.32          (Assumes Gain 8, 320mV, can also be 0.16, 0.08, 0.04)
        # RSHUNT = 0.1               (Resistor value in ohms)

        # 1. Determine max possible current
        # MaxPossible_I = VSHUNT_MAX / RSHUNT
        # MaxPossible_I = 3.2A

        # 2. Determine max expected current
        # MaxExpected_I = 2.0A

        # 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit)
        # MinimumLSB = MaxExpected_I/32767
        # MinimumLSB = 0.000061              (61uA per bit)
        # MaximumLSB = MaxExpected_I/4096
        # MaximumLSB = 0,000488              (488uA per bit)

        # 4. Choose an LSB between the min and max values
        #    (Preferrably a roundish number close to MinLSB)
        # CurrentLSB = 0.0001 (100uA per bit)
        self._current_lsb = .1  # Current LSB = 100uA per bit

        # 5. Compute the calibration register
        # Cal = trunc (0.04096 / (Current_LSB * RSHUNT))
        # Cal = 4096 (0x1000)

        self._cal_value = 4096

        # 6. Calculate the power LSB
        # PowerLSB = 20 * CurrentLSB
        # PowerLSB = 0.002 (2mW per bit)
        self._power_lsb = .002  # Power LSB = 2mW per bit

        # 7. Compute the maximum current and shunt voltage values before overflow
        #
        # Max_Current = Current_LSB * 32767
        # Max_Current = 3.2767A before overflow
        #
        # If Max_Current > Max_Possible_I then
        #    Max_Current_Before_Overflow = MaxPossible_I
        # Else
        #    Max_Current_Before_Overflow = Max_Current
        # End If
        #
        # Max_ShuntVoltage = Max_Current_Before_Overflow * RSHUNT
        # Max_ShuntVoltage = 0.32V
        #
        # If Max_ShuntVoltage >= VSHUNT_MAX
        #    Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX
        # Else
        #    Max_ShuntVoltage_Before_Overflow = Max_ShuntVoltage
        # End If

        # 8. Compute the Maximum Power
        # MaximumPower = Max_Current_Before_Overflow * VBUS_MAX
        # MaximumPower = 3.2 * 32V
        # MaximumPower = 102.4W

        # Set Calibration register to 'Cal' calculated above
        self.write(_REG_CALIBRATION,self._cal_value)

        # Set Config register to take into account the settings above
        self.bus_voltage_range = BusVoltageRange.RANGE_32V
        self.gain = Gain.DIV_8_320MV
        self.bus_adc_resolution = ADCResolution.ADCRES_12BIT_32S
        self.shunt_adc_resolution = ADCResolution.ADCRES_12BIT_32S
        self.mode = Mode.SANDBVOLT_CONTINUOUS
        self.config = self.bus_voltage_range << 13 | \
                      self.gain << 11 | \
                      self.bus_adc_resolution << 7 | \
                      self.shunt_adc_resolution << 3 | \
                      self.mode
        self.write(_REG_CONFIG,self.config)

    def getShuntVoltage_mV(self):
        self.write(_REG_CALIBRATION,self._cal_value)
        value = self.read(_REG_SHUNTVOLTAGE)
        if value > 32767:
            value -= 65535
        return value * 0.01

    def getBusVoltage_V(self):
        self.write(_REG_CALIBRATION,self._cal_value)
        self.read(_REG_BUSVOLTAGE)
        return (self.read(_REG_BUSVOLTAGE) >> 3) * 0.004

    def getCurrent_mA(self):
        value = self.read(_REG_CURRENT)
        if value > 32767:
            value -= 65535
        return value * self._current_lsb

    def getPower_W(self):
        self.write(_REG_CALIBRATION,self._cal_value)
        value = self.read(_REG_POWER)
        if value > 32767:
            value -= 65535
        return value * self._power_lsb
        
if __name__=='__main__':

    # Create an INA219 instance.
    ina219 = INA219(addr=0x42)
    while True:
        bus_voltage = ina219.getBusVoltage_V()             # voltage on V- (load side)
        shunt_voltage = ina219.getShuntVoltage_mV() / 1000 # voltage between V+ and V- across the shunt
        current = ina219.getCurrent_mA()                   # current in mA
        power = ina219.getPower_W()                        # power in W
        p = (bus_voltage - 6)/2.4*100
        if(p > 100):p = 100
        #shutdown happens when battery is below 25% and the device is actively discharging
        if(p < 25 and current/1000 < -0.30):
            call("sudo shutdown --poweroff", shell=True) #safe poweroff at 30 percent
            sys.exit()        
        if(p < 0):p = 0

        # INA219 measure bus voltage on the load side. So PSU voltage = bus_voltage + shunt_voltage
        #print("PSU Voltage:   {:6.3f} V".format(bus_voltage + shunt_voltage))
        #print("Shunt Voltage: {:9.6f} V".format(shunt_voltage))
        print("Load Voltage:  {:6.3f} V".format(bus_voltage))
        print("Current:       {:9.6f} A".format(current/1000))
        print("Power:         {:6.3f} W".format(power))
        print("Percent :       {:3.1f}".format(p))
        print("Shutdown at 25%")
        print("")
        
        #if you want to send the data via REST here is how
        #data = {"load": round(bus_voltage, 2), "current": round(current/1000, 2), "power": round(power, 2), "battery": round(p, 2)}
        #data = json.dumps(data) 
        #URL = "http://home.local:1880/power"     
        #headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        #response = requests.post(URL, data, headers, auth=("user", "pass"))    
        
        time.sleep(30)


When triggered, it will schedule the shutdown, exit out the script and display a message:

Shutdown scheduled for Sat 2022-01-22 16:23:54 GMT, use 'shutdown -c' to cancel.

The system will terminate a minute later in a safe and planned manner.

It would be nice to see a simple battery indicator LED included on the PCB, but knowing that you can get the data from the board, you can always design your own indicator.

Final thoughts

Waveshare UPS HAT (Amazon, AliExpress, ThePiHut) is a nice improvement over the other UPS board I had. It keeps the server alive long enough to either survive a power cut or me accidentally blow up the main fuse or shut down safely when the power isn't restored in time. With dozen or so battery-operated ZigBee sensors, I have the incentive to log all that data even during the shutdown, and do my best to keep the system and collected data intact. After all, better safe than sorry, and £21 is a fair price to pay for that peace of mind. All I need now is a much better case! Let me know if you have any questions in this Reddit thread.

💳📈 - See the transparency note for details.

PayPal

Nothing says "Thank you" better than keeping my coffee jar topped up!

Patreon

Support me on Patreon and get an early access to tutorial files and videos.

image/svg+xml

Bitcoin (BTC)

Use this QR to keep me caffeinated with BTC: 1FwFqqh71mUTENcRe9q4s9AWFgoc8BA9ZU

M5Paper

Programable, ESP32 based awesome dev platform with 4.7 e-ink display by M5Stack

More HATs

client-image
client-image

Argon One M.2

Enclose Raspberry Pi 4 inside this great case with custom I/O, cooling and GPIO and M.2 SSD support

More cases on

client-image
client-image

Best Raspberry Pi Projects

How to use Raspberry PI as WOL (wake on lan) server

0
While you could wake up your PC from a mobile directly, having a dedicated server capable of doing so is the best solution. The reason is simple. You can hook up as many devices as you wish with a single endpoint. This is why Raspberry Pi is perfect for this.

Slow Internet Warning

0
From time to time my Internet grinds to a stop. Since Raspberry Pi 4 comes with a 1Gbps Ethernet, I decided to take advantage of it and create a reporting system in NodeRED that will monitor and report when the ISP is not keeping the contractual agreements. Works with Alexa, Google Home, Android and Windows 10.

How fast Raspberry Pi NAS is?

0
Let's see how fast Raspberry Pi NAS really is?

Argon18: Argon ONE SSD modification

0
Argon One case just got better - now you can boot it from USB without ruining the design thanks to Argon 18: Argon One SSD modification

HOW TO...

How to boot Raspberry Pi 5 from NVMe M.2 SSD

0
This is how you can boot your Raspberry PI 5 from NVMe in 10 min! Check out these easy instructions

It took me 2 months to boot CM4 from NVMe

0
Complete beginners guide to Compute Module 4 boot from NVMe.

Raspberry Pi Zero 2 W vs other Zero boards

0
It's time to test the Raspberry Pi Zero 2 W against other Raspberry Pi boards from Zero series: power, WiFi, temperature and core performance

C/C++ and MicroPython SDK for Raspberry Pi Pico on Windows

0
A guide to SDK toolchain for Raspberry Pi Pico and C/C++ , Micropython on Windows.

A comprehensive guide to Grafana & InfluxDB

0
How to use Grafana and InfluxDB on Raspberry Pi for IoT sensors in home automation