diff --git a/Cargo.lock b/Cargo.lock index 5850b5e..4655c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,8 +46,8 @@ dependencies = [ "ch32-metapac 0.0.6", "embassy-executor", "embassy-futures", - "embassy-sync 0.7.2", - "embassy-time 0.4.0", + "embassy-sync", + "embassy-time", "embassy-usb", "embedded-hal 1.0.0", "heapless", @@ -57,6 +57,7 @@ dependencies = [ "panic-halt", "qingke", "qingke-rt", + "static_cell", "thiserror", ] @@ -75,23 +76,24 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "ch32-hal" version = "0.1.0" -source = "git+https://github.com/ju6ge/ch32-hal?branch=feature%2Fi2c-slave-api#9b1df89f5e8de21c7daad0f22fa7186dcfcbecdf" +source = "git+https://github.com/ju6ge/ch32-hal?branch=feature%2Fi2c-slave-api#dd285961d7c4604caf1f9cf3c306d4b0929e36c6" dependencies = [ "bitmaps", "ch32-metapac 0.1.0", "critical-section", "embassy-futures", "embassy-hal-internal", - "embassy-sync 0.7.2", - "embassy-time 0.5.0", + "embassy-sync", + "embassy-time", "embassy-time-driver", "embassy-time-queue-utils", - "embassy-usb-driver 0.2.0", + "embassy-usb-driver", "embedded-can", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-hal-nb", + "embedded-storage", "futures", "nb 1.1.0", "proc-macro2", @@ -115,9 +117,9 @@ dependencies = [ [[package]] name = "ch32-metapac" version = "0.1.0" -source = "git+https://github.com/ch32-rs/ch32-metapac?rev=b1cbc7a98e43af3fd3170821654784e2c01cb26b#b1cbc7a98e43af3fd3170821654784e2c01cb26b" +source = "git+https://github.com/ch32-rs/ch32-metapac?rev=7cd6fb70d07de3a987c63d0d8f5547bb8261e403#7cd6fb70d07de3a987c63d0d8f5547bb8261e403" dependencies = [ - "riscv 0.11.1", + "riscv 0.12.1", "vcell", ] @@ -238,21 +240,7 @@ checksum = "b7b2739fbcf6cd206ae08779c7d709087b16577d255f2ea4a45bc4bbbf305b3f" dependencies = [ "embassy-futures", "embassy-net-driver", - "embassy-sync 0.7.2", -] - -[[package]] -name = "embassy-sync" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049" -dependencies = [ - "cfg-if", - "critical-section", - "embedded-io-async", - "futures-sink", - "futures-util", - "heapless", + "embassy-sync", ] [[package]] @@ -269,22 +257,6 @@ dependencies = [ "heapless", ] -[[package]] -name = "embassy-time" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f820157f198ada183ad62e0a66f554c610cdcd1a9f27d4b316358103ced7a1f8" -dependencies = [ - "cfg-if", - "critical-section", - "document-features", - "embassy-time-driver", - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "futures-util", -] - [[package]] name = "embassy-time" version = "0.5.0" @@ -295,6 +267,7 @@ dependencies = [ "critical-section", "document-features", "embassy-time-driver", + "embassy-time-queue-utils", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -322,28 +295,20 @@ dependencies = [ [[package]] name = "embassy-usb" -version = "0.3.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d0b882133fa684b9d4652351cd7aac5afe8a2c2bf4a7da59f442ff61087cda2" +checksum = "dc4462e48b19a4f401a11901bdd981aab80c6a826608016a0bdc73cbbab31954" dependencies = [ "embassy-futures", "embassy-net-driver-channel", - "embassy-sync 0.6.2", - "embassy-usb-driver 0.1.1", + "embassy-sync", + "embassy-usb-driver", + "embedded-io-async", "heapless", "ssmarshal", "usbd-hid", ] -[[package]] -name = "embassy-usb-driver" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340c5ce591ef58c6449e43f51d2c53efe1bf0bb6a40cbf80afa0d259c7d52c76" -dependencies = [ - "embedded-io-async", -] - [[package]] name = "embassy-usb-driver" version = "0.2.0" @@ -412,6 +377,12 @@ dependencies = [ "embedded-io", ] +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -522,6 +493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", + "portable-atomic", "stable_deref_trait", ] @@ -630,6 +602,9 @@ name = "portable-atomic" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +dependencies = [ + "critical-section", +] [[package]] name = "proc-macro-error" @@ -723,6 +698,19 @@ dependencies = [ "embedded-hal 1.0.0", ] +[[package]] +name = "riscv" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea8ff73d3720bdd0a97925f0bf79ad2744b6da8ff36be3840c48ac81191d7a7" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", + "paste", + "riscv-macros 0.1.0", + "riscv-pac", +] + [[package]] name = "riscv" version = "0.15.0" @@ -732,10 +720,21 @@ dependencies = [ "critical-section", "embedded-hal 1.0.0", "paste", - "riscv-macros", + "riscv-macros 0.3.0", "riscv-pac", ] +[[package]] +name = "riscv-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f265be5d634272320a7de94cea15c22a3bfdd4eb42eb43edc528415f066a1f25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "riscv-macros" version = "0.3.0" @@ -804,6 +803,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" +dependencies = [ + "portable-atomic", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/firmware/.cargo/config.toml b/firmware/.cargo/config.toml index c0b2201..3c5acc1 100644 --- a/firmware/.cargo/config.toml +++ b/firmware/.cargo/config.toml @@ -10,5 +10,7 @@ rustflags = [ # runner = "gdb -q -x openocd.gdb" # runner = "wlink -v flash" -runner = "wlink -v flash --enable-sdi-print --watch-serial --erase" + +runner = "wchisp flash" +#runner = "wlink -v flash --enable-sdi-print --watch-serial --erase" # runner = "wlink -v flash" diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index fa74169..42959ea 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -14,14 +14,20 @@ ch32-hal = { git = "https://github.com/ju6ge/ch32-hal", branch = "feature/i2c-sl ], default-features = false } embassy-executor = { version = "0.7.0", features = [ - "arch-riscv32", + # arch-riscv32's WFI-based sleep races with the USB interrupt wake signal, + # so tasks awaiting USB (e.g. wait_connection/write_packet) can hang + # forever even though the device enumerates fine. arch-spin busy-polls + # instead of sleeping, sidestepping the race. See CAN_Sensor/Cargo.toml + # for the same fix on the same ch32-hal/embassy-executor combo. + "arch-spin", "executor-thread", ] } -embassy-time = { version = "0.4.0" } -embassy-usb = { version = "0.3.0" } +embassy-time = { version = "0.5.0", features = ["generic-queue-8"] } +embassy-usb = { version = "0.5.1" } embassy-futures = { version = "0.1.0" } embassy-sync = { version = "0.7.2" } +static_cell = "2.1.1" # This is okay because we should automatically use whatever ch32-hal uses qingke-rt = "*" @@ -30,7 +36,7 @@ qingke = "*" panic-halt = "1.0" embedded-hal = "1.0.0" -heapless = "0.8.0" +heapless = { version = "0.8.0", features = ["portable-atomic-critical-section"] } micromath = { version = "2.1.0", features = ["num-traits"] } ch32-metapac = { version = "0.0.6", features = ["ch32v203c8t6"] } ina219 = "0.2.0" diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 3361c5d..99328bc 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -4,24 +4,53 @@ #![feature(impl_trait_in_assoc_type)] mod measurement; +mod selfcheck; use ch32_hal::i2c::mode::OperatingMode; use ch32_hal::mode::Mode; -use ch32_hal::println; -use ch32_hal::time::Hertz; +use ch32_hal::peripherals::USBD; +use core::fmt::Write as _; use embassy_executor::Spawner; use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}; +use embassy_sync::channel::Channel; use embassy_sync::lazy_lock::LazyLock; use embassy_sync::mutex::Mutex; use embassy_sync::rwlock::RwLock; -//use embassy_time::{Duration, Timer}; +use embassy_time::Timer; +use embassy_usb::class::cdc_acm::{CdcAcmClass, State}; +use embassy_usb::{Builder, UsbDevice}; use hal::bind_interrupts; -use hal::i2c::{Config, I2c, SlaveAddress, SlaveConfig}; +use hal::gpio::{Flex, Input, Level, Output, Pull}; +use hal::i2c::I2c; +use hal::spi::Spi; +use hal::usbd::Driver; use {ch32_hal as hal, panic_halt as _}; +macro_rules! mk_static { + ($t:ty,$val:expr) => {{ + static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); + #[deny(unused_attributes)] + let x = STATIC_CELL.uninit().write(($val)); + x + }}; +} + +pub(crate) static LOG_CH: Channel, 8> = Channel::new(); + +/// Formats like `std::println!`, but sends the line over USB CDC instead of stdout. +#[macro_export] +macro_rules! println { + ($($arg:tt)*) => {{ + let mut s: heapless::String<128> = heapless::String::new(); + let _ = core::write!(&mut s, $($arg)*); + let _ = $crate::LOG_CH.try_send(s); + }}; +} + bind_interrupts!(struct Irqs { I2C1_EV => ch32_hal::i2c::EventInterruptHandler; I2C1_ER => ch32_hal::i2c::ErrorInterruptHandler; + USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler; }); const MEASUREMENT_SAMPLES_CAPACITY: usize = 1024; @@ -38,36 +67,137 @@ async fn measurement_task<'m, M: Mode, O: OperatingMode>( } #[embassy_executor::main(entry = "qingke_rt::entry")] -async fn main(_spawner: Spawner) -> ! { - let p = hal::init(Default::default()); +async fn main(spawner: Spawner) { + let p = hal::init(hal::Config { + rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI, + ..Default::default() + }); - let config = Config::default(); - let mut i2c_slave = I2c::new_blocking(p.I2C1, p.PB6, p.PB7, Hertz::khz(100), config) - .into_slave(SlaveConfig { - address: SlaveAddress::SevenBit(0x55), - general_call: false, - }); + // D7 startup blink so we can visually confirm the board booted before it + // goes quiet waiting on the I2C bus. + let mut led = Output::new(p.PA8, Level::Low, Default::default()); + for _ in 0..5 { + led.toggle(); + Timer::after_millis(200).await; + } + led.set_low(); + + // USB CDC-ACM console: lets us `println!` self-test output over the USB + // cable instead of needing a WCH-Link SWD probe attached. + let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11); + + let mut usb_config = embassy_usb::Config::new(0xC0DE, 0xCAFE); + usb_config.manufacturer = Some("ch32-bms"); + usb_config.product = Some("BMS debug console"); + usb_config.serial_number = Some("12345678"); + usb_config.max_power = 100; + usb_config.max_packet_size_0 = 64; + // Windows compatibility requires these; CDC-ACM + usb_config.device_class = 0x02; + usb_config.device_sub_class = 0x02; + usb_config.device_protocol = 0x00; + usb_config.composite_with_iads = false; + + let mut builder = Builder::new( + driver, + usb_config, + mk_static!([u8; 256], [0; 256]), + mk_static!([u8; 256], [0; 256]), + &mut [], // no msos descriptors + mk_static!([u8; 64], [0; 64]), + ); + let class = mk_static!( + CdcAcmClass<'static, Driver<'static, USBD>>, + CdcAcmClass::new(&mut builder, mk_static!(State, State::new()), 64) + ); + let usb = mk_static!(UsbDevice>, builder.build()); + + spawner.spawn(usb_task(usb)).unwrap(); + spawner.spawn(usb_writer(class)).unwrap(); + + println!("BMS booted, D7 blink done, USB console up"); + + // Power-on self-check: exercises the other LEDs, the spare input pin, + // the INA219 current sensor, and the SPI flash, reporting over USB. + let mut led_d3 = Output::new(p.PB12, Level::Low, Default::default()); + let mut led_d4 = Output::new(p.PB13, Level::Low, Default::default()); + let mut led_d5 = Output::new(p.PB14, Level::Low, Default::default()); + let mut led_d6 = Output::new(p.PB15, Level::Low, Default::default()); + let input_pb2 = Input::new(p.PB2, Pull::None); + let mut spi = Spi::new_blocking::<0>(p.SPI1, p.PA5, p.PA7, p.PA6, Default::default()); + let mut flash_cs = Output::new(p.PA4, Level::High, Default::default()); + let mut i2c_scl = Flex::new(p.PB10); + let mut i2c_sda = Flex::new(p.PB9); loop { - match i2c_slave.listen_blocking() { - Ok(command) => { - match command { - ch32_hal::i2c::SlaveCommand::GeneralCall => { /* will not be triggered because we disabled it */ - } - ch32_hal::i2c::SlaveCommand::ReadCommand => { - // send empty response - let _ = i2c_slave.blocking_write_timeout(&[0x05, 0x01]); - } - ch32_hal::i2c::SlaveCommand::WriteCommand => { - let mut buf: [u8; 3] = [0x0, 0x0, 0x0]; - let _ = i2c_slave.blocking_read_timeout(&mut buf); - println!("received byte 0x{buf:x?}"); - } + selfcheck::run( + &mut led, + &mut led_d3, + &mut led_d4, + &mut led_d5, + &mut led_d6, + &input_pb2, + &mut spi, + &mut flash_cs, + &mut i2c_scl, + &mut i2c_sda, + ) + .await; + Timer::after_secs(5).await; + } + + // I2C slave listen loop disabled for now: it busy-waits with no `.await`, + // which would starve the USB tasks above on this single-threaded executor. + // let config = hal::i2c::Config::default(); + // let mut i2c_slave = I2c::new_blocking(p.I2C1, p.PB6, p.PB7, hal::time::Hertz::khz(100), config) + // .into_slave(hal::i2c::SlaveConfig { + // address: hal::i2c::SlaveAddress::SevenBit(0x55), + // general_call: false, + // }); + // + // loop { + // match i2c_slave.listen_blocking() { + // Ok(command) => { + // match command { + // ch32_hal::i2c::SlaveCommand::GeneralCall => { /* will not be triggered because we disabled it */ + // } + // ch32_hal::i2c::SlaveCommand::ReadCommand => { + // // send empty response + // let _ = i2c_slave.blocking_write_timeout(&[0x05, 0x01]); + // } + // ch32_hal::i2c::SlaveCommand::WriteCommand => { + // let mut buf: [u8; 3] = [0x0, 0x0, 0x0]; + // let _ = i2c_slave.blocking_read_timeout(&mut buf); + // println!("received byte 0x{buf:x?}"); + // } + // } + // } + // Err(err) => { + // println!("{:?}", err) + // } + // } + // } +} + +#[embassy_executor::task] +async fn usb_task(usb: &'static mut UsbDevice<'static, Driver<'static, USBD>>) { + usb.run().await; +} + +#[embassy_executor::task] +async fn usb_writer(class: &'static mut CdcAcmClass<'static, Driver<'static, USBD>>) { + loop { + class.wait_connection().await; + 'connected: loop { + let msg = LOG_CH.receive().await; + // write_packet() fails outright for any single write over the + // endpoint's max packet size (64 bytes here), so chunk it. + for chunk in msg.as_bytes().chunks(64) { + if class.write_packet(chunk).await.is_err() { + // Disconnected or endpoint disabled; wait for reconnection. + break 'connected; } } - Err(err) => { - println!("{:?}", err) - } } } } diff --git a/firmware/src/measurement.rs b/firmware/src/measurement.rs index cfc03fc..3343f7b 100644 --- a/firmware/src/measurement.rs +++ b/firmware/src/measurement.rs @@ -1,5 +1,4 @@ -use embassy_usb::msos::ConfigurationSubsetHeader; use lib_bms_protocol::BatteryState; use ina219::{address::Address, calibration::{IntCalibration, MicroAmpere, MicroWatt, UnCalibrated}, configuration::Configuration, errors::{ConfigurationReadError, InitializationError}, measurements::{CurrentRegister, Measurements, PowerRegister}, SyncIna219};