diff --git a/Cargo.lock b/Cargo.lock index 964ef8ee15f0cdb740738ce0c2e076dc20a12b62..bd83b795d85525eb755fe217fd6f4d27a3e89386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,6 +524,30 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "i2c-bootloader" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embassy-executor", + "embassy-stm32", + "embassy-time", + "i2c2-slave", + "panic-probe", +] + +[[package]] +name = "i2c2-slave" +version = "0.1.0" +dependencies = [ + "defmt", + "embassy-stm32", + "embassy-sync", +] + [[package]] name = "ident_case" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 4be5d623afeb40e27fb1aeba74167bf8d0d522ec..dd1733205d895c1a51ddb2d722218f6253b91f5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["controller"] +members = ["controller", "i2c-bootloader", "i2c2-slave"] resolver = "2" [profile.release] diff --git a/i2c-bootloader/Cargo.toml b/i2c-bootloader/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..17ea1d7b7778cfa2f61620f83010e76863654166 --- /dev/null +++ b/i2c-bootloader/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "i2c-bootloader" +version = "0.1.0" +authors = ["Samuel Tardieu <sam@rfc1149.net>"] +edition = "2021" + +[dependencies] +cortex-m = { version = "0.7.7", features = ["critical-section-single-core", "inline-asm"] } +cortex-m-rt = "0.7.3" +defmt = "0.3.5" +defmt-rtt = "0.4.0" +embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "stm32f103c8", "unstable-pac", "time-driver-tim4", "memory-x"] } +embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } +i2c2-slave = { path = "../i2c2-slave" } +panic-probe = { version = "0.3.1", features = ["print-defmt"] } diff --git a/i2c-bootloader/src/main.rs b/i2c-bootloader/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..b209a13597152f898b4081f0d9fef87c62169ae6 --- /dev/null +++ b/i2c-bootloader/src/main.rs @@ -0,0 +1,62 @@ +#![no_std] +#![no_main] + +use defmt_rtt as _; +use embassy_executor::Spawner; +use embassy_stm32::{ + gpio::{Level, Output, Speed}, + interrupt::Priority, + peripherals::PB15, + time::mhz, + Config, +}; +use embassy_time::Timer; +use panic_probe as _; + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let mut config = Config::default(); + config.rcc.hse = Some(mhz(8)); + config.rcc.sys_ck = Some(mhz(72)); + config.rcc.pclk1 = Some(mhz(36)); + let p = embassy_stm32::init(config); + + defmt::info!("I²C bootloader starting"); + + spawner.spawn(blink(p.PB15)).unwrap(); + + let mut i2c2 = i2c2_slave::I2C2::new(p.I2C2, p.PB10, p.PB11, 0x57, Priority::P3); + loop { + let mut buffer = [0; 8]; + match i2c2.receive(&mut buffer).await { + Ok(n) => { + defmt::info!("Received command {:#x}, reversing", &buffer[..n]); + buffer[..n].reverse(); + i2c2.send(&buffer[..n]).unwrap(); + } + Err(i2c2_slave::Error::ExtraBytes(n)) => { + defmt::info!( + "Received {} extra bytes after command {:#x}, sending 0xDE 0xAD", + n, + buffer + ); + i2c2.send(&[0xDE, 0xAD]).unwrap(); + } + Err(e) => { + defmt::info!("Received another error: {:?}", defmt::Debug2Format(&e)); + } + } + } +} + +#[embassy_executor::task] +async fn blink(pin: PB15) { + let mut led = Output::new(pin, Level::High, Speed::Low); + loop { + for _ in 0..4 { + led.toggle(); + Timer::after_millis(100).await; + } + Timer::after_secs(1).await; + } +} diff --git a/i2c2-slave/Cargo.toml b/i2c2-slave/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..336c27dc1d9e15b9c353e13998cf5747cb543297 --- /dev/null +++ b/i2c2-slave/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "i2c2-slave" +version = "0.1.0" +authors = ["Samuel Tardieu <sam@rfc1149.net>"] +edition = "2021" + +[dependencies] +defmt = "0.3.5" +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "stm32f103c8", "unstable-pac", "time-driver-tim4"] } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } diff --git a/i2c2-slave/src/lib.rs b/i2c2-slave/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..6b51869b3f2079ea97aca51ac80a2f96f72df5b3 --- /dev/null +++ b/i2c2-slave/src/lib.rs @@ -0,0 +1,286 @@ +#![no_std] + +use embassy_stm32::{ + gpio::low_level::{AFType, Pin}, + i2c::{self, SclPin}, + interrupt, + interrupt::{InterruptExt, Priority}, + pac, + peripherals::{self, PB10, PB11}, +}; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel}; + +enum I2cRxEvent { + Byte(u8), + Stop, +} + +static RX_QUEUE: Channel<CriticalSectionRawMutex, I2cRxEvent, 9> = Channel::new(); +static TX_QUEUE: Channel<CriticalSectionRawMutex, u8, 8> = Channel::new(); +static mut STATE: State = State::Idle { after_write: false }; + +#[derive(Debug)] +pub enum Error { + ExtraBytes(usize), + FifoFull, +} + +pub type Result<T, E = Error> = core::result::Result<T, E>; + +#[derive(Clone, Copy, Debug)] +enum State { + Idle { after_write: bool }, + ReadInProgress { waiting_for_data: bool }, + WriteInProgress, +} + +impl State { + fn get_state() -> Self { + unsafe { STATE } + } + + fn set_state(state: Self) { + defmt::trace!("Setting new state to {:?}", defmt::Debug2Format(&state)); + unsafe { STATE = state } + } + + fn is_write(&self) -> bool { + matches!( + self, + Self::Idle { after_write: true } | Self::WriteInProgress + ) + } +} + +pub struct I2C2 { + _pb10: PB10, + _pb11: PB11, + _i2c2: peripherals::I2C2, +} + +impl I2C2 { + #[must_use] + pub fn new(i2c2: peripherals::I2C2, pb10: PB10, pb11: PB11, addr: u16, prio: Priority) -> Self { + // Set pins in AF mode + pb10.set_as_af(SclPin::af_num(&pb10), AFType::OutputOpenDrain); + pb11.set_as_af(i2c::SdaPin::af_num(&pb11), AFType::OutputOpenDrain); + // Turn I2C2 clock on + pac::RCC.apb1enr().modify(|w| w.set_i2c2en(true)); + // Reset I2C2 peripheral + pac::RCC.apb1rstr().modify(|w| w.set_i2c2rst(true)); + pac::RCC.apb1rstr().modify(|w| w.set_i2c2rst(false)); + pac::I2C2.cr1().write(|_| ()); + // Configure address + pac::I2C2.oar1().write(|w| w.set_add(addr << 1)); + // Enable event interrupt for I2C2 and 2MHz frequency + pac::I2C2.cr2().write(|w| { + w.set_itevten(true); + w.set_iterren(true); + w.set_freq(36); + }); + // Enable I2C2 + pac::I2C2.cr1().write(|w| { + w.set_pe(true); + }); + // Set ack generation + pac::I2C2.cr1().modify(|w| { + w.set_ack(true); + }); + interrupt::I2C2_EV.set_priority(prio); + interrupt::I2C2_ER.set_priority(prio); + unsafe { + interrupt::I2C2_EV.enable(); + interrupt::I2C2_ER.enable(); + } + Self { + _pb10: pb10, + _pb11: pb11, + _i2c2: i2c2, + } + } + + pub async fn receive(&mut self, buffer: &mut [u8]) -> Result<usize> { + for (i, b) in buffer.iter_mut().enumerate() { + match RX_QUEUE.receive().await { + I2cRxEvent::Byte(v) => *b = v, + I2cRxEvent::Stop => return Ok(i), + } + } + for extra in 0..usize::MAX { + if matches!(RX_QUEUE.receive().await, I2cRxEvent::Stop) { + return Err(Error::ExtraBytes(extra)); + } + } + unreachable!(); + } + + pub fn send(&mut self, data: &[u8]) -> Result<()> { + defmt::trace!("Enqueuing {:?}", data); + // Enqueue data to send next + for &b in data { + if TX_QUEUE.try_send(b).is_err() { + return Err(Error::FifoFull); + } + } + // If the controller is already waiting for data (and + // stretching the clock down), send it right away by + // trigerring an interrupt. + if matches!( + State::get_state(), + State::ReadInProgress { + waiting_for_data: true + } + ) { + pac::I2C2.cr2().modify(|w| { + w.set_itbufen(true); + }); + } + Ok(()) + } + + fn empty_tx_queue() { + while TX_QUEUE.try_receive().is_ok() {} + } +} + +#[interrupt] +unsafe fn I2C2_EV() { + let sr1 = pac::I2C2.sr1().read(); + defmt::info!( + "I2C2_EV, sr1 = {:#06x}, cr1 = {:#06x}, cr2 = {:#06x}", + sr1.0, + pac::I2C2.cr1().read().0, + pac::I2C2.cr2().read().0 + ); + if sr1.addr() { + defmt::trace!("I2C2_EV addr"); + // Addr event is cleared by reading sr2 + let sr2 = pac::I2C2.sr2().read(); + // If a write operation was in progress, send a simulated Stop to + // the RX queue. + let previous_state = State::get_state(); + if matches!(previous_state, State::WriteInProgress) + && RX_QUEUE.try_send(I2cRxEvent::Stop).is_err() + { + defmt::error!("Cannot send STOP event due to repeated start to the receive queue"); + } + let new_state = if sr2.tra() { + let outgoing = if !previous_state.is_write() { + // Another read transaction happened just before this one, + // send 0 to avoid locking the bus. + defmt::trace!("Read transaction without write, sending 0 to prevent bus locking"); + Some(0) + } else if let Ok(b) = TX_QUEUE.try_receive() { + // If bytes are ready to send already, send the first + // one and enable the buffer events to receive the + // next TXE. + defmt::trace!("Sending already ready byte {:#04x}", b); + Some(b) + } else { + // Otherwise, indicate that data is waiting. + defmt::trace!("No data ready, stretching clock"); + None + }; + outgoing + .clone() + .map(|b| pac::I2C2.dr().write(|w| w.set_dr(b))); + State::ReadInProgress { + waiting_for_data: outgoing.is_none(), + } + } else { + // In case of a write request, fill the transmit buffer + // and enable the buffer events to receive only the RXNE + // events, and not the TXE ones. Also, empty the receive + // buffer in case it already contains something. The + // outgoing buffer will later be cleared when the stop + // event is received. + pac::I2C2.dr().modify(|_| ()); + pac::I2C2.cr2().modify(|w| { + w.set_itbufen(true); + }); + defmt::trace!("Current state of CR1: {:#06x}", pac::I2C2.cr1().read().0); + defmt::trace!("Current state of CR2: {:#06x}", pac::I2C2.cr2().read().0); + // Empty a previously filled TX queue in case a read request + // arrives right after. + I2C2::empty_tx_queue(); + defmt::trace!("Waiting for RXNE or STOPF event"); + State::WriteInProgress + }; + State::set_state(new_state); + pac::I2C2.cr2().modify(|w| { + w.set_itbufen(!matches!( + new_state, + State::ReadInProgress { + waiting_for_data: true + } + )) + }); + } else if sr1.rxne() { + defmt::trace!("I2C2_EV RXNE"); + let b = pac::I2C2.dr().read().0 as u8; + defmt::trace!("Received byte {:#04x}", &b); + if RX_QUEUE.try_send(I2cRxEvent::Byte(b)).is_err() { + defmt::error!("I2C byte has overfilled the buffer"); + } + } else if sr1.txe() { + defmt::trace!("I2C2_EV TXE"); + if let Ok(b) = TX_QUEUE.try_receive() { + defmt::trace!("Sending byte {:#04x}", b); + pac::I2C2.dr().write(|w| w.set_dr(b)); + } else { + defmt::trace!("Nothing to send, storing 0 in DR to bus locking"); + pac::I2C2.dr().write(|w| w.set_dr(0)); + } + } else if sr1.stopf() { + defmt::trace!( + "I2C2_EV STOPF while state is {:?}", + defmt::Debug2Format(&State::get_state()) + ); + // Clear stopf by writing to cr1 + pac::I2C2.cr1().modify(|_w| ()); + // Opt out of the TXE/RXNE interrupt + pac::I2C2.cr2().modify(|w| { + w.set_itbufen(false); + }); + // Do a clearing sequence as recommended in the reference manual + // by reading and writing dr + pac::I2C2.dr().modify(|_w| ()); + // If receiving, indicate the end of data + let previous_state = State::get_state(); + if matches!(previous_state, State::WriteInProgress) { + if RX_QUEUE.try_send(I2cRxEvent::Stop).is_err() { + defmt::error!("Unable to send stop event to the buffer"); + } + State::set_state(State::Idle { after_write: true }); + } else { + State::set_state(State::Idle { after_write: false }); + } + } else { + defmt::error!("I2C2_EV interrupt without cause"); + } +} + +#[interrupt] +unsafe fn I2C2_ER() { + let sr1 = pac::I2C2.sr1().read(); + if sr1.af() { + defmt::trace!("I2C2_ER NACKF"); + pac::I2C2.sr1().modify(|w| w.set_af(false)); + // Nack are only used when transmitting, opt out of the RXNE interrupt + pac::I2C2.cr2().modify(|w| { + w.set_itbufen(false); + }); + // Flush unsend bytes + while let Ok(b) = TX_QUEUE.try_receive() { + defmt::trace!("Discarding {:#04x}", b); + } + State::set_state(State::Idle { after_write: false }); + } else { + defmt::error!( + "I2C2_ER sr1={:#06x} sr2={:#06x}", + sr1.0, + pac::I2C2.sr2().read().0 + ); + panic!(); + } +}