Skip to content

Conversation

astapleton
Copy link
Member

This extends the I2C driver to support target operation. It adds an I2cTarget type to represent the I2C operation as the target.

Target operation can be operated in automatic ACKing mode, letting the hardware manage clock stretching and ACKing automatically, or it can be operated in manual ACK control mode, which lets the user of the driver determine when to ACK the end of a transaction. Manual ACK control allows for clock stretching while data is being processed or retrieved before ACK'ing the final byte of a transfer and initiating either the end of a transfer or a repeat start to read the processed/retrieved data. This implementation does not currently allow for ACKing individual or a subset of bytes manually, but extending the interface to do so can build upon what is implemented so far.

This also adds role switching to the driver, enabling both controller and target implementation through choosing the appropriate initialization functions.

src/i2c.rs Outdated
Comment on lines 227 to 241
/// This encapsulates a transaction event that occurs when listening in Target
/// operation. A TargetWrite event indicates that the Controller wants to read
/// data from the Target (I2C read operation). A TargetRead event indicates
/// that the Controller wants to write data to the Target (I2C write
/// operation). A Stop event indicates that a Stop condition was received and
/// the transaction has been completed.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TargetEvent {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These definitions are pretty confusing. Wouldn't a TargetRead indicates that the controller wants to read data from the target?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. I actually had these the other way round, simply called TargetEvent::Read (controller wants to read, target must execute a write) and TargetEvent::Write (controller wants to write, target must execute a read), but I thought that was confusing, so I tried to disambiguate it by indicating that a TargetRead event is in response to controller write. Guess they're both a bit confusing, so I'll go back to the old iteration.

length <= u8::MAX as usize,
"I2C max transfer size per reload = {}",
u8::MAX
);
Copy link
Member Author

@astapleton astapleton Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these bounds checks in the controller start logic because I realized that the controller implementation does not use NBYTES reloads to write buffers longer than 255 bytes, but also doesn't bounds check on repeat starts or reloads, leading to wrap around bugs on the value written to the NBYTES field and transactions that were unexpectedly short.

Created this issue to track: #46

@astapleton
Copy link
Member Author

@ryan-summers mind taking another look at this? I believe I've addressed everything

@usbalbin usbalbin mentioned this pull request May 27, 2025
src/i2c.rs Outdated
self.inner.disable_target_event(event)
}

fn configure_target(&mut self, config: impl Into<TargetConfig>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why Into here?

Copy link
Member Author

@astapleton astapleton Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, in a previous revision I didn't include the bus frequency in the target config, so I had an implementation of u8 for Into<TargetConfig> so you could simply provide the address. Unfortunately, one needs to provide the frequency to set up timing parameters correctly, and providing a tuple was less ergonomic, so I just removed that implementation. And I will change to just use a TargetConfig here.

if let Some(secondary_address) = config.secondary_address {
i2c.oar2().write(|w| {
w.oa2().set(secondary_address);
if let Some(mask) = config.secondary_address_mask_bits {
Copy link
Member

@usbalbin usbalbin Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the user specifies an address but no mask?

(I have very limited experiance with i2c so keep that in mind :) )

Copy link
Member Author

@astapleton astapleton Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mask allows for the target to respond to a wider range of addresses (ie. an address of 0x20 and mask of 0x1F will let the target watch for transmissions on any address from 0x20 to 0x3F). This is an STM32 specific feature, not a standard feature of I2C, by the way. If no mask is specified, the only secondary address that the target will respond to is the one specified (0x20 in the example)

ControllerExpectedWrite,
/// Target operation only:
/// While waiting for a controller write event, a read event was received.
ControllerExpectedRead,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a (not well thought out) thought, would it make sense to split the errors into TargetError and ControllerError or similar? This so the user won't have to worry about the non relevant cases?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is TargetError a super set of ControllerError could ControllerError impl Into<TargetError>?

Copy link
Member Author

@astapleton astapleton Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My latest commit adds a TargetError enum and a TargetError variant for Error. Does that look better to you?

astapleton and others added 12 commits July 9, 2025 16:32
The example is currently only tested on  NUCLEO-STM32H503, and uses
pins that are not available on other H5 processors. This can be
enabled on other processors by choosing different pins for the
various evaluation boards.
- Deduplicate functions shared between AutoAck and ManualAck implementations for I2cTarget
- Rename TargetEvent variants
- Fix writing buffer larger than 255 bytes in ManualAck operation
- Improved documentation
Co-authored-by: Albin Hedman <[email protected]>
Copy link
Member

@usbalbin usbalbin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice as far as I can tell! :)

@astapleton astapleton merged commit 4b6cc4d into master Jul 10, 2025
33 checks passed
@astapleton astapleton deleted the as/i2c-target branch July 10, 2025 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants