libs/kaptan-stm32f7: add I2C1 controller
This commit is contained in:
1 parent
ba66850403
commit
b985874240
4 files changed
+391
-2
No files matched your search
@@ -48,6 +48,22 @@ pub fn configure_spi1_pins_pa5_pa6_pa7(rcc: &stm32f722::RCC, port: &stm32f722::G
|
||||
});
|
||||
}
|
||||
|
||||
/// Configures PB8/PB9 for I²C1 alternate function 4.
|
||||
pub fn configure_i2c1_pins_pb8_pb9(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) {
|
||||
rcc.ahb1enr().modify(|_, w| w.gpioben().set_bit());
|
||||
port.moder()
|
||||
.modify(|_, w| w.moder8().alternate().moder9().alternate());
|
||||
// SAFETY: AF4 is the documented I2C1 alternate-function selector for PB8/PB9.
|
||||
port.afrh()
|
||||
.modify(|_, w| unsafe { w.afrh8().bits(4).afrh9().bits(4) });
|
||||
port.otyper()
|
||||
.modify(|_, w| w.ot8().open_drain().ot9().open_drain());
|
||||
port.ospeedr()
|
||||
.modify(|_, w| w.ospeedr8().high_speed().ospeedr9().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, w| w.pupdr8().floating().pupdr9().floating());
|
||||
}
|
||||
|
||||
/// Configures PA4 as the input pin for an EXTI4 route.
|
||||
///
|
||||
/// A board must select this explicitly rather than inheriting the pin mode
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
//! STM32F722 I²C1 interrupt-driven controller.
|
||||
|
||||
use kaptan_i2c::{I2cBus, I2cCompletion};
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const MAX_BYTES: usize = 255;
|
||||
const CR1_INTERRUPTS: u32 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 4) | (1 << 5) | (1 << 7);
|
||||
const ISR_TXIS: u32 = 1 << 1;
|
||||
const ISR_RXNE: u32 = 1 << 2;
|
||||
const ISR_NACKF: u32 = 1 << 4;
|
||||
const ISR_STOPF: u32 = 1 << 5;
|
||||
const ISR_BERR: u32 = 1 << 8;
|
||||
const ISR_ARLO: u32 = 1 << 9;
|
||||
const ISR_OVR: u32 = 1 << 10;
|
||||
const ISR_ERRORS: u32 = ISR_BERR | ISR_ARLO | ISR_OVR;
|
||||
const ICR_NACKCF: u32 = 1 << 4;
|
||||
const ICR_STOPCF: u32 = 1 << 5;
|
||||
const ICR_ERRORS: u32 = (1 << 8) | (1 << 9) | (1 << 10);
|
||||
const ICR_TERMINAL: u32 = ICR_NACKCF | ICR_STOPCF | ICR_ERRORS;
|
||||
const ICR_FAULTS: u32 = ICR_NACKCF | ICR_ERRORS;
|
||||
const CR2_STOP: u32 = 1 << 14;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum I2c1Error {
|
||||
InvalidLength,
|
||||
InvalidAddress,
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Board-selected I²C1 electrical timing.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct I2c1Config {
|
||||
timing: u32,
|
||||
}
|
||||
|
||||
impl I2c1Config {
|
||||
/// Creates a configuration from an STM32 `I2C_TIMINGR` register value.
|
||||
pub const fn from_timing_register(timing: u32) -> Self {
|
||||
Self { timing }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Direction {
|
||||
Write,
|
||||
Read,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum TransferState {
|
||||
Idle,
|
||||
Active,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
fn read_index_after_event(direction: Direction, flags: u32, index: usize, length: usize) -> usize {
|
||||
if matches!(direction, Direction::Read) && flags & ISR_RXNE != 0 && index < length {
|
||||
index + 1
|
||||
} else {
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_after_stop(index: usize, length: usize) -> I2cCompletion {
|
||||
if index == length {
|
||||
I2cCompletion::Complete
|
||||
} else {
|
||||
I2cCompletion::TransferFault
|
||||
}
|
||||
}
|
||||
|
||||
impl TransferState {
|
||||
fn accepts_start(self, bus_busy: bool) -> bool {
|
||||
self == Self::Idle && !bus_busy
|
||||
}
|
||||
|
||||
fn after_stop(self) -> Self {
|
||||
if self == Self::Stopping {
|
||||
Self::Idle
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn after_driver_completion(self) -> Self {
|
||||
if self == Self::Active {
|
||||
Self::Idle
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns I²C1 while one interrupt-driven transaction is active.
|
||||
pub struct I2c1 {
|
||||
peripheral: stm32f722::I2C1,
|
||||
buffer: [u8; MAX_BYTES],
|
||||
length: usize,
|
||||
index: usize,
|
||||
direction: Direction,
|
||||
state: TransferState,
|
||||
}
|
||||
|
||||
impl I2c1 {
|
||||
/// Configures I²C1 using board-selected pin electrical timing.
|
||||
pub fn new(rcc: &stm32f722::RCC, peripheral: stm32f722::I2C1, config: I2c1Config) -> Self {
|
||||
rcc.apb1rstr().modify(|_, w| w.i2c1rst().set_bit());
|
||||
rcc.apb1rstr().modify(|_, w| w.i2c1rst().clear_bit());
|
||||
rcc.apb1enr().modify(|_, w| w.i2c1en().set_bit());
|
||||
// SAFETY: zero is the documented reset configuration for CR1.
|
||||
peripheral.cr1().write(|w| unsafe { w.bits(0) });
|
||||
// SAFETY: `timing` is a board-selected I2C_TIMINGR bit pattern.
|
||||
peripheral
|
||||
.timingr()
|
||||
.write(|w| unsafe { w.bits(config.timing) });
|
||||
// SAFETY: these are only documented CR1 interrupt-enable and PE bits.
|
||||
peripheral
|
||||
.cr1()
|
||||
.write(|w| unsafe { w.bits(CR1_INTERRUPTS) });
|
||||
Self {
|
||||
peripheral,
|
||||
buffer: [0; MAX_BYTES],
|
||||
length: 0,
|
||||
index: 0,
|
||||
direction: Direction::Write,
|
||||
state: TransferState::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the event IRQ; returns a terminal result only after STOPF.
|
||||
pub fn acknowledge_event_interrupt(&mut self) -> I2cCompletion {
|
||||
let flags = self.peripheral.isr().read().bits();
|
||||
if self.state != TransferState::Active {
|
||||
self.clear_inactive_event_flags(flags);
|
||||
return I2cCompletion::NotPending;
|
||||
}
|
||||
if flags & ISR_ERRORS != 0 {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
return I2cCompletion::TransferFault;
|
||||
}
|
||||
if flags & ISR_NACKF != 0 {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
return I2cCompletion::TransferFault;
|
||||
}
|
||||
// With AUTOEND a final RXNE and STOPF may be visible in one ISR
|
||||
// entry. Drain RXDR before retiring the transfer: otherwise RXNE
|
||||
// remains asserted after software advances its state, retriggering
|
||||
// I2C1_EV indefinitely at priority 11.
|
||||
if matches!(self.direction, Direction::Write)
|
||||
&& flags & ISR_TXIS != 0
|
||||
&& self.index < self.length
|
||||
{
|
||||
// SAFETY: the value is an eight-bit TXDR payload.
|
||||
self.peripheral
|
||||
.txdr()
|
||||
.write(|w| unsafe { w.bits(u32::from(self.buffer[self.index])) });
|
||||
self.index += 1;
|
||||
}
|
||||
let next_read_index =
|
||||
read_index_after_event(self.direction, flags, self.index, self.length);
|
||||
if next_read_index != self.index {
|
||||
self.buffer[self.index] = self.peripheral.rxdr().read().bits() as u8;
|
||||
self.index = next_read_index;
|
||||
}
|
||||
if flags & ISR_STOPF != 0 {
|
||||
self.acknowledge_stop();
|
||||
return completion_after_stop(self.index, self.length);
|
||||
}
|
||||
I2cCompletion::NotPending
|
||||
}
|
||||
|
||||
pub fn acknowledge_error_interrupt(&mut self) -> I2cCompletion {
|
||||
let flags = self.peripheral.isr().read().bits();
|
||||
if flags & ISR_ERRORS == 0 {
|
||||
return I2cCompletion::NotPending;
|
||||
}
|
||||
if self.state == TransferState::Active {
|
||||
self.abort();
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
I2cCompletion::TransferFault
|
||||
} else {
|
||||
// A STOP may have already retired the software transaction before
|
||||
// the error IRQ is serviced. Its flags still must be acknowledged
|
||||
// or this high-priority IRQ will continuously retrigger.
|
||||
self.clear_inactive_event_flags(flags);
|
||||
I2cCompletion::NotPending
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&mut self, address: u8, length: usize, direction: Direction) -> Result<(), I2c1Error> {
|
||||
if length == 0 || length > MAX_BYTES {
|
||||
return Err(I2c1Error::InvalidLength);
|
||||
}
|
||||
if address > 0x7f {
|
||||
return Err(I2c1Error::InvalidAddress);
|
||||
}
|
||||
if !self.state.accepts_start(self.bus_is_busy()) {
|
||||
return Err(I2c1Error::Busy);
|
||||
}
|
||||
// SAFETY: ICR_TERMINAL contains only documented write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_TERMINAL) });
|
||||
self.length = length;
|
||||
self.index = 0;
|
||||
self.direction = direction;
|
||||
self.state = TransferState::Active;
|
||||
let direction_bit = if matches!(direction, Direction::Read) {
|
||||
1 << 10
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let cr2 = (u32::from(address) << 1)
|
||||
| direction_bit
|
||||
| ((length as u32) << 16)
|
||||
| (1 << 25)
|
||||
| (1 << 13);
|
||||
// SAFETY: `cr2` contains a validated 7-bit address and documented transfer fields.
|
||||
self.peripheral.cr2().write(|w| unsafe { w.bits(cr2) });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn abort(&mut self) {
|
||||
// Keep software ownership closed until STOPF is observed. A requested
|
||||
// STOP may be delayed indefinitely by a held bus, so this state keeps
|
||||
// a new START from being issued before the terminal STOPF arrives.
|
||||
self.state = TransferState::Stopping;
|
||||
// SAFETY: this only requests STOP while preserving the current CR2 fields.
|
||||
self.peripheral
|
||||
.cr2()
|
||||
.modify(|read, write| unsafe { write.bits(read.bits() | CR2_STOP) });
|
||||
// SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_FAULTS) });
|
||||
}
|
||||
|
||||
fn bus_is_busy(&self) -> bool {
|
||||
self.peripheral.isr().read().bits() & (1 << 15) != 0
|
||||
}
|
||||
|
||||
fn acknowledge_stop(&mut self) {
|
||||
// SAFETY: ICR_STOPCF is the documented STOP write-one-to-clear bit.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_STOPCF) });
|
||||
self.state = self.state.after_stop();
|
||||
}
|
||||
|
||||
fn acknowledge_stop_if_present(&mut self, flags: u32) {
|
||||
if flags & ISR_STOPF != 0 {
|
||||
self.acknowledge_stop();
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_inactive_event_flags(&mut self, flags: u32) {
|
||||
// An abort may request STOP after the driver has already released the
|
||||
// transaction. Drain that terminal IRQ instead of leaving I2C1_EV
|
||||
// pending forever. RXNE is normally impossible here, but draining it
|
||||
// keeps a malformed terminal transfer from becoming an IRQ storm.
|
||||
if flags & ISR_RXNE != 0 {
|
||||
let _ = self.peripheral.rxdr().read().bits();
|
||||
}
|
||||
if flags & (ISR_NACKF | ISR_ERRORS) != 0 {
|
||||
// SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags.
|
||||
self.peripheral
|
||||
.icr()
|
||||
.write(|w| unsafe { w.bits(ICR_FAULTS) });
|
||||
}
|
||||
self.acknowledge_stop_if_present(flags);
|
||||
if flags & ISR_TXIS != 0 {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl I2cBus for I2c1 {
|
||||
type StartError = I2c1Error;
|
||||
fn start_write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::StartError> {
|
||||
if write.is_empty() || write.len() > MAX_BYTES {
|
||||
return Err(I2c1Error::InvalidLength);
|
||||
}
|
||||
if !self.state.accepts_start(self.bus_is_busy()) {
|
||||
return Err(I2c1Error::Busy);
|
||||
}
|
||||
// Populate controller-owned storage before `start` makes the
|
||||
// transaction visible to the higher-priority I²C event IRQ.
|
||||
self.buffer[..write.len()].copy_from_slice(write);
|
||||
self.start(address, write.len(), Direction::Write)?;
|
||||
Ok(())
|
||||
}
|
||||
fn start_read(&mut self, address: u8, length: usize) -> Result<(), Self::StartError> {
|
||||
self.start(address, length, Direction::Read)
|
||||
}
|
||||
fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) {
|
||||
if completion == I2cCompletion::Complete
|
||||
&& matches!(self.direction, Direction::Read)
|
||||
&& let Some(read) = read
|
||||
{
|
||||
debug_assert_eq!(read.len(), self.length);
|
||||
read.copy_from_slice(&self.buffer[..self.length]);
|
||||
}
|
||||
self.state = self.state.after_driver_completion();
|
||||
self.length = 0;
|
||||
self.index = 0;
|
||||
}
|
||||
|
||||
fn cancel_transaction(&mut self) {
|
||||
if self.state == TransferState::Active {
|
||||
self.abort();
|
||||
}
|
||||
self.length = 0;
|
||||
self.index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Direction, I2cCompletion, ICR_ERRORS, ICR_FAULTS, ICR_NACKCF, ICR_STOPCF, ISR_RXNE,
|
||||
ISR_STOPF, TransferState, completion_after_stop, read_index_after_event,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn rejects_starts_while_a_transaction_or_recovery_is_pending() {
|
||||
assert!(TransferState::Idle.accepts_start(false));
|
||||
assert!(!TransferState::Idle.accepts_start(true));
|
||||
assert!(!TransferState::Active.accepts_start(false));
|
||||
assert!(!TransferState::Stopping.accepts_start(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abort_stays_closed_until_stop_and_normal_completion_releases_ownership() {
|
||||
assert_eq!(TransferState::Stopping.after_stop(), TransferState::Idle);
|
||||
assert_eq!(TransferState::Active.after_stop(), TransferState::Active);
|
||||
assert_eq!(
|
||||
TransferState::Active.after_driver_completion(),
|
||||
TransferState::Idle
|
||||
);
|
||||
assert_eq!(
|
||||
TransferState::Stopping.after_driver_completion(),
|
||||
TransferState::Stopping
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fault_cleanup_does_not_acknowledge_stop_before_stop_is_observed() {
|
||||
assert_eq!(ICR_FAULTS, ICR_NACKCF | ICR_ERRORS);
|
||||
assert_eq!(ICR_FAULTS & ICR_STOPCF, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rxne_and_stopf_complete_a_final_read_in_one_event() {
|
||||
let index = read_index_after_event(Direction::Read, ISR_RXNE | ISR_STOPF, 1, 2);
|
||||
|
||||
assert_eq!(index, 2);
|
||||
assert_eq!(completion_after_stop(index, 2), I2cCompletion::Complete);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
mod clock;
|
||||
mod dma_buffer;
|
||||
mod gpio;
|
||||
mod i2c1;
|
||||
mod spi1;
|
||||
mod tim2;
|
||||
mod time;
|
||||
@@ -15,9 +16,10 @@ mod usart1_tx_dma2_stream7;
|
||||
pub use clock::{ClockProfileError, select_pll216_profile};
|
||||
pub use dma_buffer::DcacheDisabledDmaMemory;
|
||||
pub use gpio::{
|
||||
ActiveLowLedPd2, configure_exti4_pin_pa4, configure_spi1_pins_pa5_pa6_pa7,
|
||||
configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
ActiveLowLedPd2, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9,
|
||||
configure_spi1_pins_pa5_pa6_pa7, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
};
|
||||
pub use i2c1::{I2c1, I2c1Config, I2c1Error};
|
||||
pub use spi1::{Spi1, Spi1Error};
|
||||
pub use tim2::{Tim2Periodic, Tim2PeriodicConfig};
|
||||
pub use time::{DwtClockStartError, DwtMicrosecondClock};
|
||||
|
||||
@@ -51,6 +51,16 @@ impl DwtMicrosecondClock {
|
||||
self.advance_to(DWT::cycle_count())
|
||||
}
|
||||
|
||||
/// Creates another independent reader for the same already-running DWT
|
||||
/// counter. Each reader must be serviced often enough to observe wraps.
|
||||
pub const fn fork(&self) -> Self {
|
||||
Self {
|
||||
cycles_per_microsecond: self.cycles_per_microsecond,
|
||||
previous_cycles: self.previous_cycles,
|
||||
elapsed_cycles: self.elapsed_cycles,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_to(&mut self, current_cycles: u32) -> Timestamp {
|
||||
let elapsed = current_cycles.wrapping_sub(self.previous_cycles);
|
||||
self.previous_cycles = current_cycles;
|
||||
|
||||
Reference in new issue
Block a user