Skip to content

Instantly share code, notes, and snippets.

@sloppycoder
Last active August 3, 2025 14:28
Show Gist options
  • Select an option

  • Save sloppycoder/4b4500200b8655af15376cdadf9acd56 to your computer and use it in GitHub Desktop.

Select an option

Save sloppycoder/4b4500200b8655af15376cdadf9acd56 to your computer and use it in GitHub Desktop.
Raspberry PI Compute Module 4 with Wavesahre CM4-POE-UPS board setup
#! /usr/bin/python3
# sudo apt install -y python3-smbus
import smbus
import time
# 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
"batt" [readonly] 226L, 8856B
#! /usr/bin/python3
import smbus
import time
# 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_16V_5A()
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_16V_5A(self):
"""Configures to INA219 to be able to measure up to 16V and 5A of current. Counter
overflow occurs at 16A.
..note :: These calculations assume a 0.01 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 = 16V (Assumes 16V, can also be set to 32V)
# VSHUNT_MAX = 0.08 (Assumes Gain 2, 80mV, can also be 0.32, 0.16, 0.04)
# RSHUNT = 0.01 (Resistor value in ohms)
# 1. Determine max possible current
# MaxPossible_I = VSHUNT_MAX / RSHUNT
# MaxPossible_I = 8.0A
# 2. Determine max expected current
# MaxExpected_I = 5.0A
# 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit)
# MinimumLSB = MaxExpected_I/32767
# MinimumLSB = 0.0001529 (61uA per bit)
# MaximumLSB = MaxExpected_I/4096
# MaximumLSB = 0,0012207 (488uA per bit)
# 4. Choose an LSB between the min and max values
# (Preferrably a roundish number close to MinLSB)
# CurrentLSB = 0.00016 (uA per bit)
self._current_lsb = 0.1524 # Current LSB = 100uA per bit
# 5. Compute the calibration register
# Cal = trunc (0.04096 / (Current_LSB * RSHUNT))
# Cal = 13434 (0x347a)
self._cal_value = 26868
# 6. Calculate the power LSB
# PowerLSB = 20 * CurrentLSB
# PowerLSB = 0.002 (2mW per bit)
self._power_lsb = 0.003048 # 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_16V
self.gain = Gain.DIV_2_80MV
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__':
import os
# Create an INA219 instance.
ina219 = INA219(i2c_bus=10,addr=0x43)
low = 0
if 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 - 3)/1.2*100
if(p > 100):p = 100
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: {:6.3f} A".format(current/1000))
print("Power: {:6.3f} W".format(power))
print("Percent: {:3.1f}%".format(p))
if(bus_voltage < 3.15) and (current > 50):
low += 1
if(low >= 30):
print("System shutdown now")
os.system("sudo poweroff")
else:
print("Voltage Low,please charge in time,otherwise it will shut down in {:2d} s".format(60-2*low))
else:
low = 0
print("")
console=serial0,115200 console=tty1 root=PARTUUID=ea8bf869-02 rootfstype=ext4 fsck.repair=yes rcgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1 rootwait
# /boot/config.txt
dtparam=i2c_arm=on
arm_64bit=1
disable_overscan=1
[cm4]
otg_mode=1
[pi4]
arm_boost=1
[all]
# enable hw RTC and FAN controller for CM4-POE-UPS-BASE board
dtparam=i2c_vc=on
dtoverlay=i2c-rtc,pcf85063a,i2c_csi_dsi
# not working on bookworm, perhaps controller chip damanaged
#dtoverlay=i2c-fan,emc2301,i2c_csi_dsi,minpwm=25,maxpwm=255,midtemp=40000,maxtemp=75000,addr=0x2f
dtoverlay=disable-bt
#!/bin/bash
#
# Jeff Geerling's super-rudimentary fan controller script for the CM4.
#
# Use:
# 1. Download this script to a path like `/opt/cm4-fan.sh`
# 2. Run it with: `nohup bash /opt/cm4-fan.sh`
# 3. Profit!
#
# You should wrap it in a systemd unit file if you want it to persist and come
# up after reboot, too.
# Explicitly set $PATH so i2c tools are found.
PATH=$PATH:/usr/sbin
# Temperature set point (Celcius).
TEMP_THRESHOLD=70
TEMP_COMPARE=$(($TEMP_THRESHOLD * 1000))
# Minimum fan speed (0-255).
MIN_FAN_SPEED=100
MIN_FAN_SPEED_HEX=$(printf "0x%02x" "$MIN_FAN_SPEED")
# Maximum fan speed (0-255).
MAX_FAN_SPEED=255
MAX_FAN_SPEED_HEX=$(printf "0x%02x" "$MAX_FAN_SPEED")
# TODO: The script should also check that `i2cget` is present, and that the
# fan is visible on the bus.
# Start a loop.
while true; do
# Get the current temperature.
CURRENT_TEMP=$(cat /sys/class/thermal/thermal_zone0/temp)
if [ $CURRENT_TEMP -ge $TEMP_COMPARE ]; then
# If current temperature is more than desired, set fan to maximum speed.
i2cset -y 10 0x2f 0x30 $MAX_FAN_SPEED_HEX
echo $CURRENT_TEMP $MAX_FAN_SPEED_HEX
else
# If current temperature is good, set fan to minimum speed.
i2cset -y 10 0x2f 0x30 $MIN_FAN_SPEED_HEX
#echo $CURRENT_TEMP $MIN_FAN_SPEED_HEX
fi
sleep 30
done
# /etc/modules
i2c-dev
# /etc/ssh/sshd_config.d/disable_root_login.conf
ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no
PermitRootLogin no
#PermitRootLogin prohibit-password

Tests

tested on Raspian bullseyes 64bit release date May 3rd 2023 with kernel 6.1.21-v8+ #1642 SMP PREEMPT Mon Apr 3 17:24:16 BST 2023 aarch64

i2c for FAN and RTC


# probe i2c bus
# address with UU means address claimed by kernel driver 
# thus inaccessible to userspace utilties, including 
# i2cget/i2cset

/usr/sbin/i2cdetect -y 10

0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- 0c -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 2f
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- 43 -- -- -- -- -- -- -- -- -- -- -- --
50: -- UU -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

# test RTC

sudo hwclock --test

hwclock from util-linux 2.36.1
System Time: 1686027513.627552
Trying to open: /dev/rtc0
Using the rtc interface to the clock.
Assuming hardware clock is kept in UTC time.
Waiting for clock tick...
ioctl(4, RTC_UIE_ON, 0): Invalid argument
Waiting in loop for time from /dev/rtc0 to change
...got clock tick

# fan speed control
/usr/sbin/i2cget -y 10 0x2f 0x30
/usr/sbin/i2cget -y 10 0x2f 0x30 0xff 

# get temperature reading
vcgencmd measure_temp



as for 2025-08-03, fan is not on not working after bookworm, perhaps chip is damaged. /sys/class/hwmon/hwmon3/pwm1 shows positive number when system is underload but fan is not on :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment