1+ #include <stdio.h>
2+ #include "pico/stdlib.h"
3+ #include "hardware/i2c.h"
4+ #include <math.h>
5+
6+ // --- Shunt resistor selection ----------------------------------------------
7+ // The shunt converts current into the small voltage the INA228 measures.
8+ // A bigger shunt gives better resolution at low current but drops more voltage
9+ // (and clips sooner) at high current. Pick one to match what you're measuring.
10+ // SHUNT_BOARD_15M : 15 mOhm, the breakout's stock part. Good for ~100s of mA.
11+ // SHUNT_SLEEP_1R : 1 Ohm precision part. Good for sleep currents (uA..few mA).
12+ #define SHUNT_BOARD_15M 0
13+ #define SHUNT_SLEEP_1R 1
14+
15+ #define SHUNT_SELECT SHUNT_BOARD_15M
16+
17+ #if SHUNT_SELECT == SHUNT_SLEEP_1R
18+ #define R_SHUNT 1.0
19+ // Optimised for sleep measurement (<5 mA). The DUT's ~150 mA active draw will
20+ // clip with this calibration -- intended; this build targets sleep only.
21+ #define MAX_EXPECTED_CURRENT 0.005
22+ #else // SHUNT_BOARD_15M
23+ #define R_SHUNT 0.015
24+ #define MAX_EXPECTED_CURRENT 0.2
25+ #endif
26+
27+ #define I2C_ADDR 0x40
28+
29+ // Shunt-voltage ADC input range:
30+ // 0 = +/-163.84 mV full-scale
31+ // 1 = +/-40.96 mV full-scale (~4x finer resolution, for small shunt voltages)
32+ // Defaults track the shunt: the stock 15 mOhm part may carry 100s of mA, so the
33+ // wider range avoids clipping; the 1 Ohm sleep part sees only mV, so the narrow
34+ // range gives much better resolution. Changing this updates VSHUNT_FACTOR and
35+ // SHUNT_CAL below, which MUST stay consistent with each other.
36+ #if SHUNT_SELECT == SHUNT_SLEEP_1R
37+ #define ADCRANGE 1
38+ #else // SHUNT_BOARD_15M
39+ #define ADCRANGE 0
40+ #endif
41+
42+ // --- Averaging --------------------------------------------------------------
43+ // The INA228 can average multiple conversions in hardware before updating the
44+ // registers (ADC_CONFIG AVG field, bits 2:0). More averaging = quieter readings
45+ // but slower updates.
46+ //
47+ // Default tracks the shunt/use case:
48+ // Sleep build -> heavy averaging: steady low currents benefit from a quiet
49+ // reading, and updates can afford to be slow.
50+ // Stock build -> light averaging: a long averaging window smears abrupt
51+ // load changes (e.g. a powman sleep/wake step) across the
52+ // window and produces garbage on the transition, so keep it
53+ // short to track changes and stay responsive.
54+ // Allowed values: 1, 4, 16, 64, 128, 256, 512, 1024 (samples averaged).
55+ #ifndef AVG_SAMPLES
56+ #if SHUNT_SELECT == SHUNT_SLEEP_1R
57+ #define AVG_SAMPLES 128
58+ #else // SHUNT_BOARD_15M
59+ #define AVG_SAMPLES 16
60+ #endif
61+ #endif
62+
63+ #if AVG_SAMPLES == 1
64+ #define AVG_FIELD 0
65+ #elif AVG_SAMPLES == 4
66+ #define AVG_FIELD 1
67+ #elif AVG_SAMPLES == 16
68+ #define AVG_FIELD 2
69+ #elif AVG_SAMPLES == 64
70+ #define AVG_FIELD 3
71+ #elif AVG_SAMPLES == 128
72+ #define AVG_FIELD 4
73+ #elif AVG_SAMPLES == 256
74+ #define AVG_FIELD 5
75+ #elif AVG_SAMPLES == 512
76+ #define AVG_FIELD 6
77+ #elif AVG_SAMPLES == 1024
78+ #define AVG_FIELD 7
79+ #else
80+ #error "AVG_SAMPLES must be one of: 1, 4, 16, 64, 128, 256, 512, 1024"
81+ #endif
82+
83+ // --- Conversion time --------------------------------------------------------
84+ // Time the ADC spends on each individual conversion (before averaging), set
85+ // for bus, shunt and temperature alike here. Longer = quieter samples but
86+ // slower. Total time per reading is roughly CONV_TIME x AVG_SAMPLES, so raising
87+ // both together can make updates very slow -- watch the combined figure.
88+ // Allowed values in microseconds: 50, 84, 150, 280, 540, 1052, 2074, 4120.
89+ #define CONV_TIME_US 1052
90+
91+ #if CONV_TIME_US == 50
92+ #define CT_FIELD 0
93+ #elif CONV_TIME_US == 84
94+ #define CT_FIELD 1
95+ #elif CONV_TIME_US == 150
96+ #define CT_FIELD 2
97+ #elif CONV_TIME_US == 280
98+ #define CT_FIELD 3
99+ #elif CONV_TIME_US == 540
100+ #define CT_FIELD 4
101+ #elif CONV_TIME_US == 1052
102+ #define CT_FIELD 5
103+ #elif CONV_TIME_US == 2074
104+ #define CT_FIELD 6
105+ #elif CONV_TIME_US == 4120
106+ #define CT_FIELD 7
107+ #else
108+ #error "CONV_TIME_US must be one of: 50, 84, 150, 280, 540, 1052, 2074, 4120"
109+ #endif
110+
111+ // ADC_CONFIG register value:
112+ // bits 15:12 MODE = 0xF (continuous bus, shunt and temperature)
113+ // bits 11:9 VBUSCT = conversion time for bus voltage
114+ // bits 8:6 VSHCT = conversion time for shunt voltage
115+ // bits 5:3 VTCT = conversion time for temperature
116+ // bits 2:0 AVG = samples averaged
117+ #define ADC_CONFIG_VALUE ( (0xFu << 12) \
118+ | ((CT_FIELD & 0x7) << 9) \
119+ | ((CT_FIELD & 0x7) << 6) \
120+ | ((CT_FIELD & 0x7) << 3) \
121+ | (AVG_FIELD & 0x7) )
122+
123+ // ina228 registers (see datasheet)
124+ #define CONFIG_REG 0x00
125+ #define ADC_CONFIG_REG 0x01
126+ #define SHUNT_CAL_REG 0x02
127+ #define VSHUNT_REG 0x04
128+ #define VBUS_REG 0x05
129+ #define DIETEMP_REG 0x06
130+ #define CURRENT_REG 0x07
131+ #define POWER_REG 0x08
132+ #define ENERGY_REG 0x09
133+ #define CHARGE_REG 0x0A
134+
135+ // Conversion factors (see datasheet)
136+ // VSHUNT_FACTOR: volts per LSB. 312.5 nV at ADCRANGE=0, 4x smaller at ADCRANGE=1.
137+ #if ADCRANGE == 1
138+ const double VSHUNT_FACTOR = 78.125 * 1e-9 ; // V per LSB (ADCRANGE = 1)
139+ #else
140+ const double VSHUNT_FACTOR = 312.5 * 1e-9 ; // V per LSB (ADCRANGE = 0)
141+ #endif
142+ const double VBUS_FACTOR = 195.3125 * 1e-6 ; // V per LSB
143+ const double DIETEMP_FACTOR = 7.8125 * 1e-3 ; // degC per LSB
144+ const double CURRENT_FACTOR = MAX_EXPECTED_CURRENT / (double )(1u << 19 ); // A per LSB
145+ const double POWER_FACTOR = 3.2 * (MAX_EXPECTED_CURRENT / (double )(1u << 19 )); // W per LSB
146+ const double ENERGY_FACTOR = 16.0 * 3.2 * (MAX_EXPECTED_CURRENT / (double )(1u << 19 )); // J per LSB
147+ const double CHARGE_FACTOR = MAX_EXPECTED_CURRENT / (double )(1u << 19 ); // C per LSB
148+
149+ // SHUNT_CAL = 13107.2e6 * CURRENT_LSB * R_SHUNT, then multiplied by 4 if ADCRANGE = 1.
150+ #if ADCRANGE == 1
151+ const uint16_t SHUNT_CAL = (uint16_t )(4.0 * 13107.2e6 * (MAX_EXPECTED_CURRENT / (double )(1u << 19 )) * R_SHUNT );
152+ #else
153+ const uint16_t SHUNT_CAL = (uint16_t )(13107.2e6 * (MAX_EXPECTED_CURRENT / (double )(1u << 19 )) * R_SHUNT );
154+ #endif
155+
156+ float vshunt , vbus , dietemp , current , power , energy , charge ;
157+
158+ // --- Byte-combining helpers --------------------------------------------------
159+ // Type-safe replacements for the old COALESCE macros. Each returns an unsigned
160+ // value; signedness is handled separately by the sign-extension helpers below.
161+
162+ // 16-bit register (e.g. DIETEMP)
163+ static inline uint16_t bytes_to_u16 (const uint8_t * b ) {
164+ return ((uint16_t )b [0 ] << 8 ) | b [1 ];
165+ }
166+
167+ // 24-bit register holding a 20-bit value in the upper bits; lower 4 are reserved
168+ // and read as zero (VSHUNT, VBUS, CURRENT).
169+ static inline uint32_t bytes_to_u20 (const uint8_t * b ) {
170+ return ((uint32_t )b [0 ] << 12 ) | ((uint32_t )b [1 ] << 4 ) | ((uint32_t )b [2 ] >> 4 );
171+ }
172+
173+ // 24-bit register, all bits significant (POWER).
174+ static inline uint32_t bytes_to_u24 (const uint8_t * b ) {
175+ return ((uint32_t )b [0 ] << 16 ) | ((uint32_t )b [1 ] << 8 ) | b [2 ];
176+ }
177+
178+ // 40-bit accumulation register (ENERGY, CHARGE).
179+ static inline uint64_t bytes_to_u40 (const uint8_t * b ) {
180+ return ((uint64_t )b [0 ] << 32 ) | ((uint64_t )b [1 ] << 24 ) |
181+ ((uint64_t )b [2 ] << 16 ) | ((uint64_t )b [3 ] << 8 ) | (uint64_t )b [4 ];
182+ }
183+
184+ // --- Sign-extension helpers --------------------------------------------------
185+ // VSHUNT, CURRENT (20-bit) and CHARGE (40-bit) are two's complement.
186+
187+ static inline int32_t sign_extend_20 (uint32_t v ) {
188+ return (v & 0x80000u ) ? (int32_t )(v | 0xFFF00000u ) : (int32_t )v ;
189+ }
190+
191+ static inline int16_t sign_extend_16 (uint16_t v ) {
192+ return (int16_t )v ; // already the right width
193+ }
194+
195+ static inline int64_t sign_extend_40 (uint64_t v ) {
196+ return (v & ((uint64_t )1 << 39 )) ? (int64_t )(v | 0xFFFFFF0000000000ull ) : (int64_t )v ;
197+ }
198+
199+ // --- Register access ---------------------------------------------------------
200+
201+ static void ina228_read_reg (uint8_t reg , uint8_t * buf , size_t len ) {
202+ i2c_write_blocking (i2c_default , I2C_ADDR , & reg , 1 , true);
203+ i2c_read_blocking (i2c_default , I2C_ADDR , buf , len , false);
204+ }
205+
206+ static void ina228_write_reg16 (uint8_t reg , uint16_t value ) {
207+ uint8_t buf [3 ] = { reg , (uint8_t )(value >> 8 ), (uint8_t )(value & 0xFF ) };
208+ i2c_write_blocking (i2c_default , I2C_ADDR , buf , 3 , false);
209+ }
210+
211+ static void ina228_init (void ) {
212+ // Set the shunt-voltage ADC range (CONFIG register bit 4 = ADCRANGE).
213+ #if ADCRANGE == 1
214+ ina228_write_reg16 (CONFIG_REG , 0x0010 );
215+ #else
216+ ina228_write_reg16 (CONFIG_REG , 0x0000 );
217+ #endif
218+
219+ // Program the shunt calibration so CURRENT/POWER read correctly.
220+ ina228_write_reg16 (SHUNT_CAL_REG , SHUNT_CAL );
221+
222+ // ADC config: continuous mode + averaging (see ADC_CONFIG_VALUE above).
223+ // Continuous mode is needed for the ENERGY/CHARGE accumulators.
224+ ina228_write_reg16 (ADC_CONFIG_REG , ADC_CONFIG_VALUE );
225+ }
226+
227+ static void ina228_read (float * vshunt , float * vbus , float * dietemp ,
228+ float * current , float * power , float * energy , float * charge ) {
229+ uint8_t buf [5 ];
230+
231+ ina228_read_reg (VSHUNT_REG , buf , 3 );
232+ * vshunt = sign_extend_20 (bytes_to_u20 (buf )) * VSHUNT_FACTOR ;
233+
234+ ina228_read_reg (VBUS_REG , buf , 3 );
235+ * vbus = bytes_to_u20 (buf ) * VBUS_FACTOR ; // unsigned
236+
237+ ina228_read_reg (DIETEMP_REG , buf , 2 );
238+ * dietemp = sign_extend_16 (bytes_to_u16 (buf )) * DIETEMP_FACTOR ;
239+
240+ ina228_read_reg (CURRENT_REG , buf , 3 );
241+ * current = sign_extend_20 (bytes_to_u20 (buf )) * CURRENT_FACTOR ;
242+
243+ ina228_read_reg (POWER_REG , buf , 3 );
244+ * power = bytes_to_u24 (buf ) * POWER_FACTOR ; // unsigned
245+
246+ ina228_read_reg (ENERGY_REG , buf , 5 );
247+ * energy = bytes_to_u40 (buf ) * ENERGY_FACTOR ; // unsigned
248+
249+ ina228_read_reg (CHARGE_REG , buf , 5 );
250+ * charge = sign_extend_40 (bytes_to_u40 (buf )) * CHARGE_FACTOR ; // signed
251+ }
252+
253+ // Cross-check the reading for self-consistency. The INA228 derives POWER
254+ // internally from its own current and bus-voltage measurement, so for a valid
255+ // sample the separately-read CURRENT and VBUS should reconstruct POWER:
256+ // P ~= Vbus * I
257+ // When the current register rails (e.g. the shunt voltage briefly leaves range
258+ // during an abrupt powman sleep/wake step), CURRENT and POWER stop agreeing.
259+ // That contradiction is a reliable "this sample is bogus" flag.
260+ // Returns true if the sample looks trustworthy.
261+ static bool ina228_reading_valid (float vbus , float current , float power ) {
262+ float expected_power = vbus * current ; // Watts
263+ float diff = fabsf (expected_power - power );
264+ // Allow a fixed floor (covers near-zero noise) plus a generous 25% of the
265+ // larger magnitude (covers timing skew between the register reads).
266+ float tol = 1e-3f + 0.25f * fmaxf (fabsf (expected_power ), fabsf (power ));
267+ return diff <= tol ;
268+ }
269+
270+ int main () {
271+ stdio_init_all ();
272+
273+ // I2C initialisation
274+ i2c_init (i2c_default , 400 * 1000 );
275+
276+ // GPIO initialisation
277+ gpio_set_function (PICO_DEFAULT_I2C_SDA_PIN , GPIO_FUNC_I2C );
278+ gpio_set_function (PICO_DEFAULT_I2C_SCL_PIN , GPIO_FUNC_I2C );
279+ gpio_pull_up (PICO_DEFAULT_I2C_SDA_PIN );
280+ gpio_pull_up (PICO_DEFAULT_I2C_SCL_PIN );
281+
282+ // Initialise ina228
283+ ina228_init ();
284+
285+ while (true) {
286+ ina228_read (& vshunt , & vbus , & dietemp , & current , & power , & energy , & charge );
287+ #if 0
288+ printf ("INA228 Measurements:\nVSHUNT: %f V\nVBUS: %f V\nDIETEMP: %f degC\n"
289+ "CURRENT: %f A\nPOWER: %f W\nENERGY: %f J\nCHARGE: %f C\n-----------------\n" ,
290+ vshunt , vbus , dietemp , current , power , energy , charge );
291+ #else
292+ if (!ina228_reading_valid (vbus , current , power )) {
293+ // Current and power disagree -- the sample railed (often on an
294+ // abrupt powman transition, or because the current is below what
295+ // this shunt can resolve). Don't print a misleading number.
296+ printf ("current: invalid voltage: %.3f V power: invalid\n" , vbus );
297+ } else
298+ #if SHUNT_SELECT == SHUNT_SLEEP_1R
299+ // Sleep build: currents are small, print in microamps.
300+ printf ("current: %.1f uA voltage: %.3f V power: %.3f mW\n" ,
301+ current * 1e6 , vbus , power * 1000 );
302+ #else
303+ // Stock build: print in milliamps.
304+ printf ("current: %.2f mA voltage: %.3f V power: %.2f mW\n" ,
305+ current * 1e3 , vbus , power * 1000 );
306+ #endif
307+ #endif
308+ sleep_ms (1000 );
309+ }
310+ }
0 commit comments