diff --git a/mgmt-embassy/.cargo/config.toml b/mgmt-embassy/.cargo/config.toml new file mode 100644 index 00000000..bbe53273 --- /dev/null +++ b/mgmt-embassy/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = "probe-rs run --chip STM32F072CBTx" + +[build] +target = "thumbv6m-none-eabi" + +[env] +DEFMT_LOG = "trace" diff --git a/mgmt-embassy/Cargo.toml b/mgmt-embassy/Cargo.toml new file mode 100644 index 00000000..421979f8 --- /dev/null +++ b/mgmt-embassy/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition = "2021" +name = "mgmt-embassy" +version = "0.1.0" +license = "MIT OR Apache-2.0" + +[dependencies] +cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } +cortex-m-rt = "0.7.0" + +defmt = "1.0.1" +defmt-rtt = "1.0.0" + +embassy-executor = { version = "0.8.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } +embassy-stm32 = { version = "0.3.0", features = ["defmt", "stm32f072cb", "memory-x", "exti", "chrono", "time-driver-any"] } +embassy-sync = { version = "0.7.0", features = ["defmt"] } +embassy-time = { version = "0.4.0", features = ["defmt"] } +heapless = "0.8" +embedded-hal = "1.0" + +panic-probe = { version = "1.0.0", features = ["print-defmt"] } +num_enum = { version = "0.7.4", default-features = false } +embedded-io-async = "0.6.1" + +[profile.release] +debug = 2 diff --git a/mgmt-embassy/build.rs b/mgmt-embassy/build.rs new file mode 100644 index 00000000..8cd32d7e --- /dev/null +++ b/mgmt-embassy/build.rs @@ -0,0 +1,5 @@ +fn main() { + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} diff --git a/mgmt-embassy/src/commands.rs b/mgmt-embassy/src/commands.rs new file mode 100644 index 00000000..14d0fb87 --- /dev/null +++ b/mgmt-embassy/src/commands.rs @@ -0,0 +1,30 @@ +use num_enum::TryFromPrimitive; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format, TryFromPrimitive)] +pub enum Command { + Version = 0, + WhoAreYou = 1, + HardReset = 2, + Reset = 3, + ResetUi = 4, + ResetNet = 5, + FlashUi = 6, + FlashNet = 7, + EnableLogs = 8, + EnableLogsUi = 9, + EnableLogsNet = 10, + DisableLogs = 11, + DisableLogsUi = 12, + DisableLogsNet = 13, + DefaultLogging = 14, + ToUi = 15, + ToNet = 16, + ToUsb = 17, +} + +pub const VERSION: &[u8] = b"v1.0.0\n"; +pub const HELLO_I_AM_A_HACTAR_DEVICE: &[u8] = b"HELLO, I AM A HACTAR DEVICE"; +pub const OK_ASCII: &[u8] = b"Ok\n"; +pub const OK_BYTE: u8 = 0x80; +pub const READY_BYTE: u8 = 0x81; diff --git a/mgmt-embassy/src/drivers.rs b/mgmt-embassy/src/drivers.rs new file mode 100644 index 00000000..c31647ca --- /dev/null +++ b/mgmt-embassy/src/drivers.rs @@ -0,0 +1,194 @@ +use embassy_stm32::{ + gpio::{Flex, Level, Output, Pull, Speed}, + Peri, +}; +use embassy_time::Delay; +use embedded_hal::delay::DelayNs; + +/// RGB LED controller +pub struct RgbLed { + r: Output<'static>, + g: Output<'static>, + b: Output<'static>, +} + +impl RgbLed { + pub fn new( + r: Peri<'static, impl embassy_stm32::gpio::Pin>, + g: Peri<'static, impl embassy_stm32::gpio::Pin>, + b: Peri<'static, impl embassy_stm32::gpio::Pin>, + ) -> Self { + Self { + r: Output::new(r, Level::High, Speed::Low), + g: Output::new(g, Level::High, Speed::Low), + b: Output::new(b, Level::High, Speed::Low), + } + } + + pub fn set_rgb(&mut self, r: bool, g: bool, b: bool) { + // LEDs are active low + self.r.set_level(if r { Level::Low } else { Level::High }); + self.g.set_level(if g { Level::Low } else { Level::High }); + self.b.set_level(if b { Level::Low } else { Level::High }); + } + + #[allow(dead_code)] + pub fn off(&mut self) { + self.set_rgb(false, false, false); + } + + #[allow(dead_code)] + pub fn set_red(&mut self) { + self.set_rgb(true, false, false); + } + + #[allow(dead_code)] + pub fn set_green(&mut self) { + self.set_rgb(false, true, false); + } + + #[allow(dead_code)] + pub fn set_blue(&mut self) { + self.set_rgb(false, false, true); + } + + #[allow(dead_code)] + pub fn toggle_red(&mut self) { + self.r.toggle(); + } + + #[allow(dead_code)] + pub fn toggle_green(&mut self) { + self.g.toggle(); + } + + #[allow(dead_code)] + pub fn toggle_blue(&mut self) { + self.b.toggle(); + } +} + +/// Control pins for UI chip +pub struct UiControl { + pub nrst: Flex<'static>, + pub boot0: Output<'static>, + pub boot1: Output<'static>, +} + +impl UiControl { + pub fn new( + nrst: Peri<'static, impl embassy_stm32::gpio::Pin>, + boot0: Peri<'static, impl embassy_stm32::gpio::Pin>, + boot1: Peri<'static, impl embassy_stm32::gpio::Pin>, + ) -> Self { + let mut nrst_flex = Flex::new(nrst); + nrst_flex.set_as_output(Speed::Low); + nrst_flex.set_high(); + + Self { + nrst: nrst_flex, + boot0: Output::new(boot0, Level::Low, Speed::Low), + boot1: Output::new(boot1, Level::High, Speed::Low), + } + } + + /// Put UI chip into bootloader mode (boot0=1, boot1=0) + pub fn bootloader_mode(&mut self) { + self.boot0.set_high(); + self.boot1.set_low(); + + // Power cycle + self.power_cycle(); + } + + /// Put UI chip into normal mode (boot0=0, boot1=1) + pub fn normal_mode(&mut self) { + self.boot0.set_low(); + self.boot1.set_high(); + + // Power cycle + self.power_cycle(); + } + + /// Hold UI chip in reset + pub fn hold_in_reset(&mut self) { + self.boot0.set_low(); + self.boot1.set_high(); + + self.nrst.set_as_output(Speed::Low); + self.nrst.set_low(); + } + + /// Power cycle the UI chip + pub fn power_cycle(&mut self) { + let mut delay = Delay; + // Set nrst as output + self.nrst.set_as_output(Speed::Low); + self.nrst.set_low(); + delay.delay_ms(10); + + self.nrst.set_high(); + delay.delay_ms(10); + + self.nrst.set_low(); + delay.delay_ms(10); + + // Switch to input mode with pull-up (matching C code behavior) + self.nrst.set_as_input(Pull::Up); + } +} + +/// Control pins for NET chip +pub struct NetControl { + pub nrst: Output<'static>, + pub boot: Output<'static>, +} + +impl NetControl { + pub fn new( + nrst: Peri<'static, impl embassy_stm32::gpio::Pin>, + boot: Peri<'static, impl embassy_stm32::gpio::Pin>, + ) -> Self { + Self { + nrst: Output::new(nrst, Level::High, Speed::Low), + boot: Output::new(boot, Level::High, Speed::Low), + } + } + + /// Power cycle the NET chip reset pin + fn power_cycle(&mut self, delay_ms: u64) { + let mut delay = Delay; + self.nrst.set_low(); + delay.delay_ms(delay_ms as u32); + self.nrst.set_high(); + } + + /// Put NET chip into bootloader mode + pub fn bootloader_mode(&mut self) { + self.power_cycle(10); + + // Bring boot low for ESP bootloader mode + self.boot.set_low(); + + // Power cycle + self.power_cycle(10); + } + + /// Put NET chip into normal mode + pub fn normal_mode(&mut self) { + self.boot.set_high(); + + // Power cycle + self.power_cycle(10); + } + + /// Hold NET chip in reset + pub fn hold_in_reset(&mut self) { + let mut delay = Delay; + self.boot.set_high(); + + // Reset and hold + self.nrst.set_low(); + delay.delay_ms(100); + } +} diff --git a/mgmt-embassy/src/main.rs b/mgmt-embassy/src/main.rs new file mode 100644 index 00000000..13e4deff --- /dev/null +++ b/mgmt-embassy/src/main.rs @@ -0,0 +1,51 @@ +#![no_std] +#![no_main] + +mod commands; +mod drivers; +mod state; + +use defmt::*; +use embassy_executor::Spawner; +use {defmt_rtt as _, panic_probe as _}; + +use state::{Interface, State}; + +pub const READ_BUFFER_SIZE: usize = 64; +pub const DMA_BUFFER_SIZE: usize = 1024; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut usb_rx_buf = [0u8; DMA_BUFFER_SIZE]; + let mut ui_rx_buf = [0u8; DMA_BUFFER_SIZE]; + let mut net_rx_buf = [0u8; DMA_BUFFER_SIZE]; + + let mut s = State::new(&mut usb_rx_buf, &mut ui_rx_buf, &mut net_rx_buf); + + info!("Starting main loop"); + + let mut buf = [0u8; 64]; + let mut led_timer = embassy_time::Instant::now(); + + // Main loop - handle UART routing and command parsing + loop { + // Blink LED A every second + if led_timer.elapsed().as_millis() >= 1000 { + s.led_a.toggle_green(); + led_timer = embassy_time::Instant::now(); + } + + // USB interface is special because it might be routed or read for commands + if s.routing.usb == Interface::Command { + s.handle_command(&mut buf).await; + } else { + s.route_data(Interface::Usb, &mut buf).await; + } + + // Read and route data from the other interfaces + s.route_data(Interface::Ui, &mut buf).await; + s.route_data(Interface::Net, &mut buf).await; + + embassy_time::Timer::after(embassy_time::Duration::from_micros(100)).await; + } +} diff --git a/mgmt-embassy/src/state.rs b/mgmt-embassy/src/state.rs new file mode 100644 index 00000000..46d07499 --- /dev/null +++ b/mgmt-embassy/src/state.rs @@ -0,0 +1,380 @@ +use defmt::info; +use embassy_stm32::{ + bind_interrupts, + mode::Async, + peripherals, usart, + usart::{Config, DataBits, Parity, RingBufferedUartRx, StopBits, Uart, UartTx}, +}; +use embedded_io_async::Read; + +use crate::commands::*; +use crate::drivers::{NetControl, RgbLed, UiControl}; + +bind_interrupts!(struct Irqs { + USART1 => usart::InterruptHandler; + USART2 => usart::InterruptHandler; + USART3_4 => usart::InterruptHandler; +}); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Interface { + Drop, + Usb, + Ui, + Net, + Command, +} + +/// Routing configuration for each UART +pub struct UartRouting { + pub usb: Interface, + pub ui: Interface, + pub net: Interface, +} + +impl Default for UartRouting { + // Default state: + // * Process commands from USB rx + // * Route NET and UI to USB tx + fn default() -> Self { + Self { + usb: Interface::Command, + ui: Interface::Usb, + net: Interface::Usb, + } + } +} + +pub struct State<'d> { + // LEDs + pub led_a: RgbLed, + pub led_b: RgbLed, + + // Control registers for the UI chips + pub ui_control: UiControl, + pub net_control: NetControl, + + // UART connections + pub usb_tx: UartTx<'static, Async>, + pub usb_rx: RingBufferedUartRx<'d>, + + pub ui_tx: UartTx<'static, Async>, + pub ui_rx: RingBufferedUartRx<'d>, + + pub net_tx: UartTx<'static, Async>, + pub net_rx: RingBufferedUartRx<'d>, + + // Routing + pub routing: UartRouting, +} + +impl<'d> State<'d> { + pub fn new( + usb_rx_buf: &'d mut [u8], + ui_rx_buf: &'d mut [u8], + net_rx_buf: &'d mut [u8], + ) -> Self { + let p = embassy_stm32::init(Default::default()); + + // LEDs + let led_a = RgbLed::new(p.PA4, p.PA6, p.PA7); + let led_b = RgbLed::new(p.PB0, p.PB6, p.PB15); + + // Control registers for the UI chips + let ui_control = UiControl::new(p.PB3, p.PA15, p.PB8); + let net_control = NetControl::new(p.PB4, p.PB5); + + // UART to USB + let mut config = Config::default(); + config.baudrate = 115200; + config.data_bits = DataBits::DataBits8; + config.stop_bits = StopBits::STOP1; + config.parity = Parity::ParityNone; + + let (usb_tx, usb_rx) = { + let (tx, rx) = Uart::new( + p.USART1, p.PA10, p.PA9, Irqs, p.DMA1_CH2, p.DMA1_CH3, config, + ) + .unwrap() + .split(); + + (tx, rx.into_ring_buffered(usb_rx_buf)) + }; + + // UART to UI + let (ui_tx, ui_rx) = { + let (tx, rx) = Uart::new(p.USART2, p.PA3, p.PA2, Irqs, p.DMA1_CH4, p.DMA1_CH5, config) + .unwrap() + .split(); + + (tx, rx.into_ring_buffered(ui_rx_buf)) + }; + + // UART to NET + let (net_tx, net_rx) = { + let (tx, rx) = Uart::new( + p.USART3, p.PB11, p.PB10, Irqs, p.DMA1_CH7, p.DMA1_CH6, config, + ) + .unwrap() + .split(); + + (tx, rx.into_ring_buffered(net_rx_buf)) + }; + + Self { + led_a, + led_b, + ui_control, + net_control, + usb_tx, + usb_rx, + ui_tx, + ui_rx, + net_tx, + net_rx, + routing: Default::default(), + } + } + + pub async fn route_data(&mut self, src: Interface, buf: &mut [u8]) { + let dst = match src { + Interface::Usb => self.routing.usb, + Interface::Ui => self.routing.ui, + Interface::Net => self.routing.net, + _ => unreachable!("Invalid source interface"), + }; + + // Command data is handled separately + if dst == Interface::Command { + return; + } + + let n = match src { + Interface::Usb => self.usb_rx.read(buf).await.unwrap(), + Interface::Ui => self.ui_rx.read(buf).await.unwrap(), + Interface::Net => self.net_rx.read(buf).await.unwrap(), + _ => unreachable!("Invalid source interface"), + }; + + // No data to process + if n == 0 { + return; + } + + let data = &buf[..n]; + + match dst { + Interface::Drop => {} + Interface::Usb => self.usb_tx.write(&data).await.unwrap(), + Interface::Ui => self.ui_tx.write(&data).await.unwrap(), + Interface::Net => self.net_tx.write(&data).await.unwrap(), + Interface::Command => unreachable!("Invalid destination interface"), + } + } + + pub async fn handle_command(&mut self, buf: &mut [u8]) { + let mut type_len = [0u8; 5]; + self.usb_rx.read_exact(&mut type_len).await.unwrap(); + + let Ok(command) = Command::try_from(type_len[0]) else { + defmt::warn!("Invalid command: {}", type_len[0]); + return; + }; + + let mut len_bytes = [0u8; 4]; + len_bytes.copy_from_slice(&type_len[1..]); + let len = u32::from_be_bytes(len_bytes) as usize; + + if len == 0 { + self.direct_command(command).await; + } else { + self.forwarding_command(command, len, buf).await; + } + } + + async fn direct_command(&mut self, command: Command) { + match command { + Command::Version => self.usb_tx.write(VERSION).await.unwrap(), + Command::WhoAreYou => self.usb_tx.write(HELLO_I_AM_A_HACTAR_DEVICE).await.unwrap(), + Command::HardReset => { + info!("Hard reset requested"); + + // Reset both chips + self.reset_ui().await; + self.reset_net().await; + + // Reset routing to defaults (Debug mode: logs enabled) + self.routing = UartRouting::default(); + + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::Reset => { + self.reset_ui().await; + self.reset_net().await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::ResetUi => { + self.reset_ui().await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::ResetNet => { + self.reset_net().await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::FlashUi => { + info!("Entering UI flash mode"); + + // Hold NET in reset + self.net_control.hold_in_reset(); + + // Configure routing: USB->UI, UI->USB, NET->None + self.routing.usb = Interface::Ui; + self.routing.ui = Interface::Usb; + self.routing.net = Interface::Drop; + + // Send OK byte + let _ = self.usb_tx.write(&[OK_BYTE]).await; + + // Reconfigure the USB interface + let config = { + let mut config = Config::default(); + config.baudrate = 115200; + config.data_bits = DataBits::DataBits9; + config.stop_bits = StopBits::STOP1; + config.parity = Parity::ParityEven; + config + }; + + self.usb_rx.set_config(&config).unwrap(); + self.usb_tx.set_config(&config).unwrap(); + + // Delay to allow UART reconfiguration to settle + embassy_time::Timer::after(embassy_time::Duration::from_millis(200)).await; + + // Put UI chip into bootloader mode + self.ui_control.bootloader_mode(); + + // Send Ready byte + let _ = self.usb_tx.write(&[READY_BYTE]).await; + + info!("UI flash mode active - bootloader ready"); + } + Command::FlashNet => { + info!("Entering NET flash mode"); + + // Hold UI in reset + self.ui_control.hold_in_reset(); + + // Configure routing: USB->NET, NET->USB, UI->None + self.routing.usb = Interface::Net; + self.routing.net = Interface::Usb; + self.routing.ui = Interface::Drop; + + // Send OK byte + let _ = self.usb_tx.write(&[OK_BYTE]).await; + + // Delay before entering bootloader mode + embassy_time::Timer::after(embassy_time::Duration::from_millis(200)).await; + + // Put NET chip into bootloader mode + self.net_control.bootloader_mode(); + + // Send Ready byte + let _ = self.usb_tx.write(&[READY_BYTE]).await; + info!("NET flash mode active - bootloader ready"); + } + Command::EnableLogs => { + self.enable_logs_ui(true).await; + self.enable_logs_net(true).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::EnableLogsUi => { + self.enable_logs_ui(true).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::EnableLogsNet => { + self.enable_logs_net(true).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::DisableLogs => { + self.enable_logs_ui(false).await; + self.enable_logs_net(false).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::DisableLogsUi => { + self.enable_logs_ui(false).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::DisableLogsNet => { + self.enable_logs_net(false).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + Command::DefaultLogging => { + self.enable_logs_ui(true).await; + self.enable_logs_net(true).await; + self.usb_tx.write(OK_ASCII).await.unwrap() + } + + // When these commands are sent with zero data, they are just a noop + Command::ToUsb => {} + Command::ToUi => {} + Command::ToNet => {} + } + } + + async fn forwarding_command(&mut self, command: Command, len: usize, buf: &mut [u8]) { + let mut remaining = len; + while remaining != 0 { + let curr_len = buf.len().min(remaining); + let curr = &mut buf[..curr_len]; + + self.usb_rx.read_exact(curr).await.unwrap(); + + match command { + Command::ToUsb => { + self.led_b.toggle_red(); + self.usb_tx.write(buf).await.unwrap(); + } + Command::ToUi => { + self.led_b.toggle_blue(); + self.ui_tx.write(buf).await.unwrap(); + } + Command::ToNet => { + self.led_b.toggle_green(); + self.net_tx.write(buf).await.unwrap(); + } + _ => unreachable!("Invalid forwarding command"), + } + + remaining -= curr_len + } + } + + pub async fn reset_ui(&mut self) { + info!("Resetting UI chip"); + self.ui_control.normal_mode(); + } + + pub async fn reset_net(&mut self) { + info!("Resetting NET chip"); + self.net_control.normal_mode(); + } + + pub async fn enable_logs_ui(&mut self, enabled: bool) { + info!("Enabling UI logs"); + self.routing.ui = if enabled { + Interface::Usb + } else { + Interface::Drop + }; + } + + pub async fn enable_logs_net(&mut self, enabled: bool) { + info!("Enabling NET logs"); + self.routing.net = if enabled { + Interface::Usb + } else { + Interface::Drop + }; + } +}