Current measurement with ina3221

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-03-13 07:12:29 +01:00
parent 2301274850
commit 1556a2a2fe
4 changed files with 292 additions and 53 deletions
+10 -1
View File
@@ -8,7 +8,16 @@
"WebFetch(domain:docs.rs)", "WebFetch(domain:docs.rs)",
"WebFetch(domain:docs.espressif.com)", "WebFetch(domain:docs.espressif.com)",
"WebFetch(domain:esp32.implrust.com)", "WebFetch(domain:esp32.implrust.com)",
"WebSearch" "WebSearch",
"Bash(cargo search:*)",
"Bash(find /home/esp/.cargo/registry/src -path */esp-hal-1.0.0/src/i2c/master.rs)",
"Bash(cargo add:*)",
"Bash(find /home/esp/.cargo/registry/src -path */ohms-*/src/*.rs)",
"Bash(find /home/esp/.cargo/registry/src -path */embedded-hal-bus-*/src/i2c*)",
"Bash(find /home/esp/.cargo/registry/src -path */axs5106l*/src/*.rs)",
"Bash(grep -n \"pub fn read_touch_data\\\\|TouchData\\\\|Coordinates\\\\|finger\\\\|x\\\\|y\\\\|count\" /home/esp/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axs5106l-0.1.0/src/*.rs)",
"Bash(grep -n \"pub fn read_touch_data\\\\|pub fn read_raw\" /home/esp/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axs5106l-0.1.0/src/*.rs)",
"Bash(grep -n \"pub fn micro_volts\\\\|pub fn milli_volts\\\\|pub fn from_micro\\\\|micro_volts\\\\|milli_volts\\\\|-> i32\\\\|-> f32\\\\|-> u32\" /home/esp/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ohms-0.2.0/src/*.rs)"
] ]
} }
} }
Generated
+38 -1
View File
@@ -20,6 +20,17 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axs5106l"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "412c958bcde165622bed3b9d5839efff5def86bd6c8ce5c4c415f714885e3549"
dependencies = [
"embedded-hal 1.0.0",
"heapless 0.9.2",
"nb 1.1.0",
]
[[package]] [[package]]
name = "az" name = "az"
version = "1.2.1" version = "1.2.1"
@@ -61,6 +72,12 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.0" version = "2.11.0"
@@ -484,7 +501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54786287c0a61ca0f78cb0c338a39427551d1be229103b4444591796c579e093" checksum = "54786287c0a61ca0f78cb0c338a39427551d1be229103b4444591796c579e093"
dependencies = [ dependencies = [
"bitfield", "bitfield",
"bitflags", "bitflags 2.11.0",
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"critical-section", "critical-section",
@@ -668,6 +685,7 @@ dependencies = [
name = "esp32c6-display" name = "esp32c6-display"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axs5106l",
"embedded-graphics", "embedded-graphics",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-hal-bus", "embedded-hal-bus",
@@ -676,6 +694,8 @@ dependencies = [
"esp-bootloader-esp-idf", "esp-bootloader-esp-idf",
"esp-hal", "esp-hal",
"esp-println", "esp-println",
"heapless 0.9.2",
"ina3221",
"log", "log",
"mipidsi", "mipidsi",
] ]
@@ -832,6 +852,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "ina3221"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7553b95bc4d5b72e4a1e4d945364d83361eac2a5e1d88321cfb3d9281e01d937"
dependencies = [
"bitflags 1.3.2",
"embedded-hal 1.0.0",
"ohms",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@@ -976,6 +1007,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ohms"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d55b4d2ed96afaf7449a0bbf831c84bcd32e0a35c1ac22edb931decdd855a7"
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
+3
View File
@@ -28,6 +28,9 @@ mipidsi = "0.10"
# 2D graphics primitives and text rendering # 2D graphics primitives and text rendering
embedded-graphics = "0.8" embedded-graphics = "0.8"
ina3221 = "0.4.5"
axs5106l = "0.1.0"
heapless = "0.9"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
+239 -49
View File
@@ -1,59 +1,206 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
use core::cell::RefCell;
use core::fmt::Write;
use embedded_hal::delay::DelayNs;
use esp_backtrace as _; use esp_backtrace as _;
esp_bootloader_esp_idf::esp_app_desc!(); esp_bootloader_esp_idf::esp_app_desc!();
use esp_hal::{ use esp_hal::{
delay::Delay, delay::Delay,
gpio::{Level, Output, OutputConfig}, gpio::{Level, Output, OutputConfig},
i2c::master::{Config as I2cConfig, I2c},
main, main,
spi::master::{Config, Spi}, spi::master::{Config as SpiConfig, Spi},
time::Rate, time::Rate,
}; };
use esp_println::println; use esp_println::println;
mod jd9853; use embedded_hal_bus::i2c::RefCellDevice;
mod jd9853;
use jd9853::JD9853; use jd9853::JD9853;
use mipidsi::{interface::SpiInterface, Builder}; use mipidsi::{interface::SpiInterface, Builder};
use embedded_graphics::{ use embedded_graphics::{
mono_font::{ascii::FONT_10X20, MonoTextStyle}, mono_font::{
ascii::{FONT_6X10, FONT_10X20},
MonoTextStyle,
},
pixelcolor::Rgb565, pixelcolor::Rgb565,
prelude::*, prelude::*,
primitives::{PrimitiveStyleBuilder, Rectangle}, primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle},
text::Text, text::Text,
}; };
// TODO: verify pin assignments against your board schematic use ina3221::INA3221;
// use axs5106l::{Axs5106l, Rotation};
// Typical wiring for a 1.47" JD9853 breakout on ESP32-C6: use heapless::String as HString;
//
// Display pin │ ESP32-C6 GPIO // --- Hardware constants ------------------------------------------------------
// ─────────────┼───────────────
// SCL / SCK │ GPIO6
// SDA / MOSI │ GPIO7
// CS │ GPIO10
// DC │ GPIO4
// RST │ GPIO5
// BLK / BL │ GPIO22 (set high to enable backlight)
//
const LCD_W: u16 = 172; const LCD_W: u16 = 172;
const LCD_H: u16 = 320; const LCD_H: u16 = 320;
// Display pin │ ESP32-C6 GPIO
// ─────────────┼───────────────
// SCL / SCK │ GPIO1
// SDA / MOSI │ GPIO2
// CS │ GPIO14
// DC │ GPIO15
// RST │ GPIO22
// BLK / BL │ GPIO23 (set high to enable backlight)
//
// I2C SDA │ GPIO18
// I2C SCL │ GPIO19
// Touch RST │ GPIO20
//
// INA3221 I2C address: 0x40 (A0 = GND)
// Shunt resistor: 50 mΩ
/// Shunt resistor in milliohms.
const SHUNT_MOHMS: i32 = 50;
// --- App types ---------------------------------------------------------------
#[derive(Clone, Copy, PartialEq)]
enum AppState {
Idle,
Measuring,
}
#[derive(Clone, Copy, Default)]
struct Measurement {
bus_uv: i32, // bus voltage, µV
current_ua: i32, // channel current, µA (derived from shunt voltage)
power_uw: i32, // power, µW
}
// --- Formatting helpers ------------------------------------------------------
fn fmt_voltage(buf: &mut HString<24>, uv: i32) {
let sign = if uv < 0 { "-" } else { "" };
let uv = uv.unsigned_abs();
write!(buf, "{}{}.{:02} V", sign, uv / 1_000_000, (uv % 1_000_000) / 10_000).ok();
}
fn fmt_current(buf: &mut HString<24>, ua: i32) {
let sign = if ua < 0 { "-" } else { "" };
let ua = ua.unsigned_abs();
write!(buf, "{}{}.{} mA", sign, ua / 1000, (ua % 1000) / 100).ok();
}
fn fmt_power(buf: &mut HString<24>, uw: i32) {
let sign = if uw < 0 { "-" } else { "" };
let uw = uw.unsigned_abs();
write!(buf, "{}{}.{} mW", sign, uw / 1000, (uw % 1000) / 100).ok();
}
fn fmt_duration(buf: &mut HString<24>, secs: u32) {
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
write!(buf, "{:02}:{:02}:{:02}", h, m, s).ok();
}
// --- UI ----------------------------------------------------------------------
fn draw_ui<D: DrawTarget<Color = Rgb565>>(display: &mut D, state: AppState, m: &Measurement, elapsed_s: u32) {
const BLUE: Rgb565 = Rgb565::new(0, 0, 20);
const GREEN: Rgb565 = Rgb565::new(0, 40, 0);
const GREY: Rgb565 = Rgb565::new(10, 20, 10);
display.clear(Rgb565::BLACK).ok();
// Header bar
Rectangle::new(Point::new(0, 0), Size::new(LCD_W as u32, 42))
.into_styled(PrimitiveStyleBuilder::new().fill_color(BLUE).build())
.draw(display)
.ok();
Text::new(
"POWER MONITOR",
Point::new(21, 29),
MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE),
)
.draw(display)
.ok();
// Status line
let (status_str, status_color) = if state == AppState::Measuring {
("* MEASURING", GREEN)
} else {
(" STOPPED ", GREY)
};
Text::new(status_str, Point::new(8, 68), MonoTextStyle::new(&FONT_10X20, status_color))
.draw(display)
.ok();
// Divider
Line::new(Point::new(0, 78), Point::new(LCD_W as i32 - 1, 78))
.into_styled(PrimitiveStyle::with_stroke(GREY, 1))
.draw(display)
.ok();
// Labels
let label = MonoTextStyle::new(&FONT_6X10, GREY);
Text::new("Voltage", Point::new(8, 100), label).draw(display).ok();
Text::new("Current", Point::new(8, 160), label).draw(display).ok();
Text::new("Power", Point::new(8, 220), label).draw(display).ok();
// Values
let value = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let mut buf: HString<24> = HString::new();
fmt_voltage(&mut buf, m.bus_uv);
Text::new(&buf, Point::new(8, 128), value).draw(display).ok();
buf.clear();
fmt_current(&mut buf, m.current_ua);
Text::new(&buf, Point::new(8, 188), value).draw(display).ok();
buf.clear();
fmt_power(&mut buf, m.power_uw);
Text::new(&buf, Point::new(8, 248), value).draw(display).ok();
// Footer
Line::new(Point::new(0, 263), Point::new(LCD_W as i32 - 1, 263))
.into_styled(PrimitiveStyle::with_stroke(GREY, 1))
.draw(display)
.ok();
let mut buf: HString<24> = HString::new();
write!(buf, "Runtime: ").ok();
fmt_duration(&mut buf, elapsed_s);
Text::new(&buf, Point::new(5, 278), MonoTextStyle::new(&FONT_6X10, GREY))
.draw(display)
.ok();
Text::new(
"tap to start / stop",
Point::new(5, 293),
MonoTextStyle::new(&FONT_6X10, GREY),
)
.draw(display)
.ok();
}
// --- Entry point -------------------------------------------------------------
#[main] #[main]
fn main() -> ! { fn main() -> ! {
let peripherals = esp_hal::init(esp_hal::Config::default()); let peripherals = esp_hal::init(esp_hal::Config::default());
// 72 KB heap for mipidsi internal buffers, alloc, etc.
esp_alloc::heap_allocator!(size: 72 * 1024); esp_alloc::heap_allocator!(size: 72 * 1024);
println!("ESP32-C6 display demo starting"); println!("Power monitor starting...");
// --- SPI bus ----------------------------------------------------------- let mut delay = Delay::new();
// --- SPI / display -------------------------------------------------------
let spi = Spi::new( let spi = Spi::new(
peripherals.SPI2, peripherals.SPI2,
Config::default() SpiConfig::default()
.with_frequency(Rate::from_mhz(40)) .with_frequency(Rate::from_mhz(40))
.with_mode(esp_hal::spi::Mode::_0), .with_mode(esp_hal::spi::Mode::_0),
) )
@@ -61,50 +208,93 @@ fn main() -> ! {
.with_sck(peripherals.GPIO1) .with_sck(peripherals.GPIO1)
.with_mosi(peripherals.GPIO2); .with_mosi(peripherals.GPIO2);
// Manual chip-select via GPIO
let cs = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default()); let cs = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default());
// Wrap bus + CS into an embedded-hal SpiDevice
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new(spi, cs, Delay::new()).unwrap(); let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new(spi, cs, Delay::new()).unwrap();
// --- Display -----------------------------------------------------------
let dc = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default()); let dc = Output::new(peripherals.GPIO15, Level::Low, OutputConfig::default());
let rst = Output::new(peripherals.GPIO22, Level::High, OutputConfig::default()); let rst = Output::new(peripherals.GPIO22, Level::High, OutputConfig::default());
// Backlight — drive high to enable
let _bl = Output::new(peripherals.GPIO23, Level::High, OutputConfig::default()); let _bl = Output::new(peripherals.GPIO23, Level::High, OutputConfig::default());
// mipidsi 0.10 requires a small scratch buffer owned by the interface let mut spi_buf = [0u8; 512];
let mut buf = [0u8; 512]; let di = SpiInterface::new(spi_device, dc, &mut spi_buf);
let di = SpiInterface::new(spi_device, dc, &mut buf);
// JD9853 has a 240-wide internal framebuffer; the 172-pixel panel starts at column 34.
let mut display = Builder::new(JD9853, di) let mut display = Builder::new(JD9853, di)
.reset_pin(rst) .reset_pin(rst)
.display_size(LCD_W, LCD_H) .display_size(LCD_W, LCD_H)
.display_offset(34, 0) .display_offset(34, 0)
.init(&mut Delay::new()) .init(&mut delay)
.expect("display init failed"); .expect("display init failed");
// --- Drawing ----------------------------------------------------------- // --- I2C (shared between INA3221 and AXS5106L) ---------------------------
display.clear(Rgb565::BLACK).unwrap(); let i2c = I2c::new(peripherals.I2C0, I2cConfig::default())
.expect("I2C init failed")
.with_sda(peripherals.GPIO18)
.with_scl(peripherals.GPIO19);
let i2c_bus = RefCell::new(i2c);
// Blue rectangle as background banner // --- INA3221 (channel index 0 = physical CH1, shunt = 50 mΩ) ------------
let bg_style = PrimitiveStyleBuilder::new() let ina = INA3221::new(RefCellDevice::new(&i2c_bus), 0x40);
.fill_color(Rgb565::new(0, 0, 20))
.build();
Rectangle::new(Point::new(0, 0), Size::new(LCD_W as u32, 40))
.into_styled(bg_style)
.draw(&mut display)
.unwrap();
// White text // --- AXS5106L touch controller -------------------------------------------
let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); let touch_rst = Output::new(peripherals.GPIO20, Level::High, OutputConfig::default());
Text::new("ESP32-C6", Point::new(10, 28), text_style) let mut touch = Axs5106l::new(
.draw(&mut display) RefCellDevice::new(&i2c_bus),
.unwrap(); touch_rst,
LCD_W,
LCD_H,
Rotation::Rotate0,
);
touch.init(&mut delay).expect("touch init failed");
println!("Display ready."); // --- App state -----------------------------------------------------------
let mut state = AppState::Idle;
let mut measurement = Measurement::default();
let mut was_touched = false;
let mut tick: u32 = 0;
let mut runtime_loops: u32 = 0; // increments every 10 ms while measuring
loop {} draw_ui(&mut display, state, &measurement, 0);
loop {
// Touch: toggle measuring on the leading edge of a tap
if let Ok(td) = touch.read_raw_touch_data() {
let touched = td.has_touches();
if touched && !was_touched {
if state == AppState::Idle {
state = AppState::Measuring;
tick = 50; // trigger first sensor read immediately
runtime_loops = 0;
} else {
state = AppState::Idle;
}
draw_ui(&mut display, state, &measurement, runtime_loops / 100);
}
was_touched = touched;
}
// Read INA3221 every 500 ms while measuring (10 ms loop × 50 = 500 ms)
if state == AppState::Measuring {
runtime_loops = runtime_loops.saturating_add(1);
tick += 1;
if tick >= 50 {
tick = 0;
match (ina.get_shunt_voltage(0), ina.get_bus_voltage(0)) {
(Ok(shunt_v), Ok(bus_v)) => {
let bus_uv = bus_v.micro_volts();
let shunt_uv = shunt_v.micro_volts();
// I (µA) = V_shunt (µV) × 1000 / R_shunt (mΩ)
let current_ua = shunt_uv * 1000 / SHUNT_MOHMS;
// P (µW) = V_bus (µV) × I (µA) / 1 000 000
let power_uw =
((bus_uv as i64 * current_ua as i64) / 1_000_000) as i32;
measurement = Measurement { bus_uv, current_ua, power_uw };
draw_ui(&mut display, state, &measurement, runtime_loops / 100);
}
_ => println!("INA3221 read error"),
}
}
}
delay.delay_ms(10u32);
}
} }