libs/kaptan-stm32f7: add I2C1 controller

This commit is contained in:
Onur Özkan committed 2026-07-26 18:03:39 +03:00
1 parent ba66850403
commit b985874240
4 files changed
+391 -2

No files matched your search

+16
View File
@@ -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
+361
View File
@@ -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);
}
}
+4 -2
View File
@@ -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};
+10
View File
@@ -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;