Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bf01151
stm32/i2c_v1: Remove redundant timing config abstractions for DutyCyc…
HybridChild Jul 27, 2025
399ec8d
stm32/i2c: Rename FrameOptions enum to OperationFraming
HybridChild Jul 27, 2025
b690a03
stm32/i2c: v1 and v2 now has separate definitions for the State struct
HybridChild Jul 27, 2025
593fb96
stm32/i2c: Add temporary trait for version-specific initialization du…
HybridChild Jul 27, 2025
3925489
stm32/i2c_v1: Rename function parameters for consistent naming
HybridChild Jul 28, 2025
c531af4
Revert "stm32/i2c: Add temporary trait for version-specific initializ…
HybridChild Aug 7, 2025
b88c519
stm32/i2c_v1: Add MultiMaster (Slave) mode implementation
HybridChild Aug 10, 2025
6570036
stm32/i2c_v1: Add defmt trace messages
HybridChild Aug 10, 2025
d6d5439
stm32/i2c_v1: Fix bugs with slave address initialization and missing …
HybridChild Aug 10, 2025
46c9592
stm32/i2c_v1: Fix bugs in slave read and write logic
HybridChild Aug 11, 2025
4f7febc
stm32/i2c_v1: Better handling of slave read and write overflow
HybridChild Aug 11, 2025
fd71580
stm32/i2c_v1: Clean up slave implementation
HybridChild Aug 11, 2025
0349686
stm32/i2c_v1: Add handling of zero-length read
HybridChild Aug 12, 2025
0ab366e
stm32/i2c_v1: Add async slave implementation
HybridChild Aug 13, 2025
a52497b
stm32/i2c_v1: Add handling of excess Write and Read from Master
HybridChild Aug 22, 2025
8111bbc
stm32/i2c_v1: Clean up Async MultiMaster implementation
HybridChild Aug 22, 2025
e630e1a
stm32/i2c_v1: Update defmt logging for MultiMaster
HybridChild Aug 22, 2025
572a40b
stm32/i2c_v1: Add async and blocking example code
HybridChild Aug 23, 2025
fbefd6c
Revert "stm32/i2c: v1 and v2 now has separate definitions for the Sta…
HybridChild Aug 23, 2025
109c2cb
stm32/i2c: Add core::fmt::Debug for enums
HybridChild Aug 23, 2025
5d3a1dd
stm32/i2c: Remove rustdoc example for assign_operation_framing function
HybridChild Aug 23, 2025
a51c032
stm32: Add entry in CHANGELOG.md file
HybridChild Aug 23, 2025
97d5de6
stm32: Run cargo fmt
HybridChild Aug 23, 2025
944ac0b
Run cargo fmt for examples
HybridChild Aug 23, 2025
524db5a
Fix formatting in examples
HybridChild Aug 23, 2025
3c8d078
Fix formatting in examples
HybridChild Aug 23, 2025
f4965c3
Merge branch 'main' into feature/stm32-i2c-v1-slave-mode
xoviat Oct 30, 2025
6543876
rustfmt and update
xoviat Oct 30, 2025
ae72627
reduce diff with misc. reversions
xoviat Oct 30, 2025
f85d709
Merge branch 'main' into i2c
xoviat Nov 1, 2025
f8f7820
Merge branch 'main' into i2c
xoviat Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions embassy-stm32/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- feat: timer: add ability to set master mode
- fix: sdmmc: don't wait for DBCKEND flag on sdmmc_v2 devices as it never fires (Fixes #4723)
- fix: usart: fix race condition in ringbuffered usart
- feat: Add I2C MultiMaster (Slave) support for I2C v1
- feat: stm32/fdcan: add ability to control automatic recovery from bus off ([#4821](https://github.com/embassy-rs/embassy/pull/4821))
- low-power: update rtc api to allow reconfig
- adc: consolidate ringbuffer
Expand Down
8 changes: 4 additions & 4 deletions embassy-stm32/src/i2c/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::gpio::{AfType, OutputType, Speed};
use crate::time::Hertz;

#[repr(u8)]
#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
/// Bits of the I2C OA2 register to mask out.
pub enum AddrMask {
Expand Down Expand Up @@ -60,7 +60,7 @@ impl Address {
}
}

#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
/// The second Own Address register.
pub struct OA2 {
Expand All @@ -70,7 +70,7 @@ pub struct OA2 {
pub mask: AddrMask,
}

#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
/// The Own Address(es) of the I2C peripheral.
pub enum OwnAddresses {
Expand All @@ -88,7 +88,7 @@ pub enum OwnAddresses {
}

/// Slave Configuration
#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct SlaveAddrConfig {
/// Target Address(es)
Expand Down
102 changes: 66 additions & 36 deletions embassy-stm32/src/i2c/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ impl<'d, M: Mode> I2c<'d, M, Master> {
sda,
},
};

this.enable_and_init(config);

this
Expand Down Expand Up @@ -437,15 +438,15 @@ impl<'d, IM: MasterMode> embedded_hal_async::i2c::I2c for I2c<'d, Async, IM> {

/// Frame type in I2C transaction.
///
/// This tells each method what kind of framing to use, to generate a (repeated) start condition (ST
/// This tells each method what kind of frame to use, to generate a (repeated) start condition (ST
/// or SR), and/or a stop condition (SP). For read operations, this also controls whether to send an
/// ACK or NACK after the last byte received.
///
/// For write operations, the following options are identical because they differ only in the (N)ACK
/// treatment relevant for read operations:
///
/// - `FirstFrame` and `FirstAndNextFrame`
/// - `NextFrame` and `LastFrameNoStop`
/// - `FirstFrame` and `FirstAndNextFrame` behave identically for writes
/// - `NextFrame` and `LastFrameNoStop` behave identically for writes
///
/// Abbreviations used below:
///
Expand Down Expand Up @@ -474,23 +475,26 @@ enum FrameOptions {

#[allow(dead_code)]
impl FrameOptions {
/// Sends start or repeated start condition before transfer.
/// Returns true if a start or repeated start condition should be generated before this operation.
fn send_start(self) -> bool {
match self {
Self::FirstAndLastFrame | Self::FirstFrame | Self::FirstAndNextFrame => true,
Self::NextFrame | Self::LastFrame | Self::LastFrameNoStop => false,
}
}

/// Sends stop condition after transfer.
/// Returns true if a stop condition should be generated after this operation.
fn send_stop(self) -> bool {
match self {
Self::FirstAndLastFrame | Self::LastFrame => true,
Self::FirstFrame | Self::FirstAndNextFrame | Self::NextFrame | Self::LastFrameNoStop => false,
}
}

/// Sends NACK after last byte received, indicating end of read operation.
/// Returns true if NACK should be sent after the last byte received in a read operation.
///
/// This signals the end of a read sequence and releases the bus for the master's
/// next transmission (or stop condition).
fn send_nack(self) -> bool {
match self {
Self::FirstAndLastFrame | Self::FirstFrame | Self::LastFrame | Self::LastFrameNoStop => true,
Expand All @@ -499,24 +503,44 @@ impl FrameOptions {
}
}

/// Iterates over operations in transaction.
/// Analyzes I2C transaction operations and assigns appropriate frame to each.
///
/// This function processes a sequence of I2C operations and determines the correct
/// frame configuration for each operation to ensure proper I2C protocol compliance.
/// It handles the complex logic of:
///
/// - Generating start conditions for the first operation of each type (read/write)
/// - Generating stop conditions for the final operation in the entire transaction
/// - Managing ACK/NACK behavior for read operations, including merging consecutive reads
/// - Ensuring proper bus handoff between different operation types
///
/// **Transaction Contract Compliance:**
/// The frame assignments ensure compliance with the embedded-hal I2C transaction contract,
/// where consecutive operations of the same type are logically merged while maintaining
/// proper protocol boundaries.
///
/// Returns necessary frame options for each operation to uphold the [transaction contract] and have
/// the right start/stop/(N)ACK conditions on the wire.
/// **Error Handling:**
/// Returns an error if any read operation has an empty buffer, as this would create
/// an invalid I2C transaction that could halt mid-execution.
///
/// # Arguments
/// * `operations` - Mutable slice of I2C operations from embedded-hal
///
/// # Returns
/// An iterator over (operation, frame) pairs, or an error if the transaction is invalid
///
/// [transaction contract]: embedded_hal_1::i2c::I2c::transaction
#[allow(dead_code)]
fn operation_frames<'a, 'b: 'a>(
operations: &'a mut [embedded_hal_1::i2c::Operation<'b>],
) -> Result<impl IntoIterator<Item = (&'a mut embedded_hal_1::i2c::Operation<'b>, FrameOptions)>, Error> {
use embedded_hal_1::i2c::Operation::{Read, Write};

// Check empty read buffer before starting transaction. Otherwise, we would risk halting with an
// error in the middle of the transaction.
// Validate that no read operations have empty buffers before starting the transaction.
// Empty read operations would risk halting with an error mid-transaction.
//
// In principle, we could allow empty read frames within consecutive read operations, as long as
// at least one byte remains in the final (merged) read operation, but that makes the logic more
// complicated and error-prone.
// Note: We could theoretically allow empty read operations within consecutive read
// sequences as long as the final merged read has at least one byte, but this would
// complicate the logic significantly and create error-prone edge cases.
if operations.iter().any(|op| match op {
Read(read) => read.is_empty(),
Write(_) => false,
Expand All @@ -525,46 +549,52 @@ fn operation_frames<'a, 'b: 'a>(
}

let mut operations = operations.iter_mut().peekable();

let mut next_first_frame = true;
let mut next_first_operation = true;

Ok(iter::from_fn(move || {
let op = operations.next()?;
let current_op = operations.next()?;

// Is `op` first frame of its type?
let first_frame = next_first_frame;
// Determine if this is the first operation of its type (read or write)
let is_first_of_type = next_first_operation;
let next_op = operations.peek();

// Get appropriate frame options as combination of the following properties:
// Compute the appropriate frame based on three key properties:
//
// - For each first operation of its type, generate a (repeated) start condition.
// - For the last operation overall in the entire transaction, generate a stop condition.
// - For read operations, check the next operation: if it is also a read operation, we merge
// these and send ACK for all bytes in the current operation; send NACK only for the final
// read operation's last byte (before write or end of entire transaction) to indicate last
// byte read and release the bus for transmission of the bus master's next byte (or stop).
// 1. **Start Condition**: Generate (repeated) start for first operation of each type
// 2. **Stop Condition**: Generate stop for the final operation in the entire transaction
// 3. **ACK/NACK for Reads**: For read operations, send ACK if more reads follow in the
// sequence, or NACK for the final read in a sequence (before write or transaction end)
//
// We check the third property unconditionally, i.e. even for write opeartions. This is okay
// because the resulting frame options are identical for write operations.
let frame = match (first_frame, next_op) {
// The third property is checked for all operations since the resulting frame
// configurations are identical for write operations regardless of ACK/NACK treatment.
let frame = match (is_first_of_type, next_op) {
// First operation of type, and it's also the final operation overall
(true, None) => FrameOptions::FirstAndLastFrame,
// First operation of type, next operation is also a read (continue read sequence)
(true, Some(Read(_))) => FrameOptions::FirstAndNextFrame,
// First operation of type, next operation is write (end current sequence)
(true, Some(Write(_))) => FrameOptions::FirstFrame,
//

// Continuation operation, and it's the final operation overall
(false, None) => FrameOptions::LastFrame,
// Continuation operation, next operation is also a read (continue read sequence)
(false, Some(Read(_))) => FrameOptions::NextFrame,
// Continuation operation, next operation is write (end current sequence, no stop)
(false, Some(Write(_))) => FrameOptions::LastFrameNoStop,
};

// Pre-calculate if `next_op` is the first operation of its type. We do this here and not at
// the beginning of the loop because we hand out `op` as iterator value and cannot access it
// anymore in the next iteration.
next_first_frame = match (&op, next_op) {
// Pre-calculate whether the next operation will be the first of its type.
// This is done here because we consume `current_op` as the iterator value
// and cannot access it in the next iteration.
next_first_operation = match (&current_op, next_op) {
// No next operation
(_, None) => false,
// Operation type changes: next will be first of its type
(Read(_), Some(Write(_))) | (Write(_), Some(Read(_))) => true,
// Operation type continues: next will not be first of its type
(Read(_), Some(Read(_))) | (Write(_), Some(Write(_))) => false,
};

Some((op, frame))
Some((current_op, frame))
}))
}
Loading