Skip to content

Commit 74e724b

Browse files
Louis31423142peterharperuklurch
authored
Added ina228 example (#705)
* Added ina228 i2c example * Update example a bit Reduce the output. Come up with a more realistic example monitoring the memory usage of another device. Add USB logging. Now matches ina260 example. * Update readme * Apply suggestion from @lurch Co-authored-by: Andrew Scheller <lurch@durge.org> * Make ina228 work * Reduce average samples Check for invalid results. --------- Co-authored-by: Peter Harper <peter.harper@raspberrypi.com> Co-authored-by: Peter Harper <77111776+peterharperuk@users.noreply.github.com> Co-authored-by: Andrew Scheller <lurch@durge.org>
1 parent 3cbc84b commit 74e724b

7 files changed

Lines changed: 370 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ App|Description
152152
---|---
153153
[bus_scan](i2c/bus_scan) | Scan the I2C bus for devices and display results.
154154
[bmp280_i2c](i2c/bmp280_i2c) | Read and convert temperature and pressure data from a BMP280 sensor, attached to an I2C bus.
155+
[ina260_i2c](i2c/ina260_i2c) | Monitor power usage of another Pico device ina260 sensor, via I2C.
156+
[ina228_i2c](i2c/ina228_i2c) | Monitor power usage of another Pico device ina228 sensor, via I2C.
155157
[ina219_i2c](i2c/ina219_i2c) | Monitor power usage of another Pico device using an ina219 sensor, via I2C.
156158
[ina237_i2c](i2c/ina237_i2c) | Monitor power usage of another Pico device using an ina237 sensor, via I2C.
157159
[ina260_i2c](i2c/ina260_i2c) | Monitor power usage of another Pico device using an ina260 sensor, via I2C.

i2c/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ if (TARGET hardware_i2c)
1313
add_subdirectory_exclude_platforms(ht16k33_i2c)
1414
add_subdirectory_exclude_platforms(slave_mem_i2c)
1515
add_subdirectory_exclude_platforms(ina260_i2c)
16+
add_subdirectory_exclude_platforms(ina228_i2c)
1617
add_subdirectory_exclude_platforms(ina219_i2c)
1718
add_subdirectory_exclude_platforms(ina237_i2c)
1819
else()

i2c/ina228_i2c/CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
add_executable(ina228_i2c ina228_i2c.c)
2+
3+
# pull in common dependencies and additional i2c hardware support
4+
target_link_libraries(ina228_i2c
5+
pico_stdlib
6+
hardware_i2c
7+
)
8+
# Wait at most 3s for stdio_usb to be ready
9+
target_compile_definitions(ina228_i2c PRIVATE
10+
PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=3000
11+
)
12+
13+
# create map/bin/hex file etc.
14+
pico_add_extra_outputs(ina228_i2c)
15+
16+
# add url via pico_set_program_url
17+
example_auto_set_url(ina228_i2c)
18+
19+
pico_enable_stdio_usb(ina228_i2c 1)
20+
pico_enable_stdio_uart(ina228_i2c 1)

i2c/ina228_i2c/README.adoc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
= Reading an INA228 power sensor via I2C
2+
3+
This example code shows how to interface the Raspberry Pi Pico with the INA228 current/voltage/power/temperature/energy/charge sensor. The sensor features a 20-bit ADC and supports up to 85V DC, making it a great choice for high precision measurements. It is capable of high-side and low-side measurements (which can be configured by soldering the jumper on the back of the board), but this example focuses on low-side measurements.
4+
5+
The sensor breakout board has an integrated 0.015 ohm, high precision shunt resistor, which is used for measuring voltage drop, which also gives current since the resistor value is known. This current, combined with the load voltage, allows load power and energy to be calculated. This example measures the power consumption of an LED.
6+
7+
[TIP]
8+
======
9+
The INA228 is highly configurable. Find the datasheet online (https://www.ti.com/lit/ds/symlink/ina228.pdf) to explore all of its capabilities beyond the simple example given here.
10+
======
11+
12+
== Wiring information
13+
14+
[[ina228_i2c_wiring]]
15+
[pdfwidth=75%]
16+
.Wiring Diagram for INA228 sensor via I2C.
17+
image::ina228_i2c.png[]
18+
19+
== List of Files
20+
21+
CMakeLists.txt:: CMake file to incorporate the example into the examples build tree.
22+
ina228_i2c.c:: The example code.
23+
24+
== Bill of Materials
25+
26+
.A list of materials required for the example
27+
[[ina228_i2c-bom-table]]
28+
[cols=3]
29+
|===
30+
| *Item* | *Quantity* | Details
31+
| Breadboard | 1 | generic part
32+
| Raspberry Pi Pico or Pico 2 (any model) | 1 | https://www.raspberrypi.com/products/raspberry-pi-pico/
33+
| INA228-based breakout board | 1 | https://www.adafruit.com/product/5832
34+
| M/M Jumper wires | 8 | generic part
35+
| LED | 1 | generic part
36+
| 330 ohm resistor | 1 | generic part
37+
|===

i2c/ina228_i2c/ina228_i2c.c

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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+
}

i2c/ina228_i2c/ina228_i2c.fzz

563 KB
Binary file not shown.

i2c/ina228_i2c/ina228_i2c.png

314 KB
Loading

0 commit comments

Comments
 (0)