diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c07dbf5..54c91c1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,16 @@ "WebFetch(domain:docs.rs)", "WebFetch(domain:docs.espressif.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)" ] } } diff --git a/Cargo.lock b/Cargo.lock index 20e3649..ebe9629 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "az" version = "1.2.1" @@ -61,6 +72,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -484,7 +501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54786287c0a61ca0f78cb0c338a39427551d1be229103b4444591796c579e093" dependencies = [ "bitfield", - "bitflags", + "bitflags 2.11.0", "bytemuck", "cfg-if", "critical-section", @@ -668,6 +685,7 @@ dependencies = [ name = "esp32c6-display" version = "0.1.0" dependencies = [ + "axs5106l", "embedded-graphics", "embedded-hal 1.0.0", "embedded-hal-bus", @@ -676,6 +694,8 @@ dependencies = [ "esp-bootloader-esp-idf", "esp-hal", "esp-println", + "heapless 0.9.2", + "ina3221", "log", "mipidsi", ] @@ -832,6 +852,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "indexmap" version = "2.13.0" @@ -976,6 +1007,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ohms" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d55b4d2ed96afaf7449a0bbf831c84bcd32e0a35c1ac22edb931decdd855a7" + [[package]] name = "paste" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index 9820853..295fe19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ mipidsi = "0.10" # 2D graphics primitives and text rendering embedded-graphics = "0.8" +ina3221 = "0.4.5" +axs5106l = "0.1.0" +heapless = "0.9" [profile.release] codegen-units = 1 diff --git a/src/main.rs b/src/main.rs index d2d2635..b117fa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,206 @@ #![no_std] #![no_main] +use core::cell::RefCell; +use core::fmt::Write; +use embedded_hal::delay::DelayNs; + use esp_backtrace as _; esp_bootloader_esp_idf::esp_app_desc!(); use esp_hal::{ delay::Delay, gpio::{Level, Output, OutputConfig}, + i2c::master::{Config as I2cConfig, I2c}, main, - spi::master::{Config, Spi}, + spi::master::{Config as SpiConfig, Spi}, time::Rate, }; use esp_println::println; -mod jd9853; +use embedded_hal_bus::i2c::RefCellDevice; +mod jd9853; use jd9853::JD9853; use mipidsi::{interface::SpiInterface, Builder}; use embedded_graphics::{ - mono_font::{ascii::FONT_10X20, MonoTextStyle}, + mono_font::{ + ascii::{FONT_6X10, FONT_10X20}, + MonoTextStyle, + }, pixelcolor::Rgb565, prelude::*, - primitives::{PrimitiveStyleBuilder, Rectangle}, + primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, text::Text, }; -// TODO: verify pin assignments against your board schematic -// -// Typical wiring for a 1.47" JD9853 breakout on ESP32-C6: -// -// Display pin │ ESP32-C6 GPIO -// ─────────────┼─────────────── -// SCL / SCK │ GPIO6 -// SDA / MOSI │ GPIO7 -// CS │ GPIO10 -// DC │ GPIO4 -// RST │ GPIO5 -// BLK / BL │ GPIO22 (set high to enable backlight) -// +use ina3221::INA3221; +use axs5106l::{Axs5106l, Rotation}; +use heapless::String as HString; + +// --- Hardware constants ------------------------------------------------------ + const LCD_W: u16 = 172; 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>(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] fn main() -> ! { 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); - println!("ESP32-C6 display demo starting…"); + println!("Power monitor starting..."); - // --- SPI bus ----------------------------------------------------------- + let mut delay = Delay::new(); + + // --- SPI / display ------------------------------------------------------- let spi = Spi::new( peripherals.SPI2, - Config::default() + SpiConfig::default() .with_frequency(Rate::from_mhz(40)) .with_mode(esp_hal::spi::Mode::_0), ) @@ -61,50 +208,93 @@ fn main() -> ! { .with_sck(peripherals.GPIO1) .with_mosi(peripherals.GPIO2); - // Manual chip-select via GPIO - let cs = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default()); - - // Wrap bus + CS into an embedded-hal SpiDevice + let cs = Output::new(peripherals.GPIO14, Level::High, OutputConfig::default()); 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()); - - // Backlight — drive high to enable let _bl = Output::new(peripherals.GPIO23, Level::High, OutputConfig::default()); - // mipidsi 0.10 requires a small scratch buffer owned by the interface - let mut buf = [0u8; 512]; - let di = SpiInterface::new(spi_device, dc, &mut buf); + let mut spi_buf = [0u8; 512]; + let di = SpiInterface::new(spi_device, dc, &mut spi_buf); - // JD9853 has a 240-wide internal framebuffer; the 172-pixel panel starts at column 34. let mut display = Builder::new(JD9853, di) .reset_pin(rst) .display_size(LCD_W, LCD_H) .display_offset(34, 0) - .init(&mut Delay::new()) + .init(&mut delay) .expect("display init failed"); - // --- Drawing ----------------------------------------------------------- - display.clear(Rgb565::BLACK).unwrap(); + // --- I2C (shared between INA3221 and AXS5106L) --------------------------- + 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 - let bg_style = PrimitiveStyleBuilder::new() - .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(); + // --- INA3221 (channel index 0 = physical CH1, shunt = 50 mΩ) ------------ + let ina = INA3221::new(RefCellDevice::new(&i2c_bus), 0x40); - // White text - let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); - Text::new("ESP32-C6", Point::new(10, 28), text_style) - .draw(&mut display) - .unwrap(); + // --- AXS5106L touch controller ------------------------------------------- + let touch_rst = Output::new(peripherals.GPIO20, Level::High, OutputConfig::default()); + let mut touch = Axs5106l::new( + RefCellDevice::new(&i2c_bus), + 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); + } }