Skip to content

Add AcknowledgeCheckFailedReason #2862

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 1 addition & 3 deletions esp-hal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Renamed variants of `CpuClock`, made the enum non-exhaustive (#2899)
- SPI: Fix naming violations for `Mode` enum variants (#2902)
- SPI: Fix naming violations for `Address` and `Command` enum variants (#2906)

- `ClockSource` enums are now `#[non_exhaustive]` (#2912)

- `gpio::{Input, Flex}::wakeup_enable` now returns an error instead of panicking. (#2916)

- Removed the `I` prefix from `DriveStrength` enum variants. (#2922)
- Removed the `Attenuation` prefix from `Attenuation` enum variants. (#2922)
- Renamed / changed some I2C error variants (#2844, #2862)

### Fixed

Expand Down
7 changes: 7 additions & 0 deletions esp-hal/MIGRATING-0.22.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ To avoid abbreviations and contractions (as per the esp-hal guidelines), some er
+ Error::ZeroLengthInvalid
```

The `AckCheckFailed` variant changed to `AcknowledgeCheckFailed(AcknowledgeCheckFailedReason)`

```diff
- Err(Error::AckCheckFailed)
+ Err(Error::AcknowledgeCheckFailed(reason))
```

## The crate prelude has been removed

The reexports that were previously part of the prelude are available through other paths:
Expand Down
84 changes: 75 additions & 9 deletions esp-hal/src/i2c/master/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub enum Error {
/// The transmission exceeded the FIFO size.
FifoExceeded,
/// The acknowledgment check failed.
AckCheckFailed,
AcknowledgeCheckFailed(AcknowledgeCheckFailedReason),
/// A timeout occurred during transmission.
Timeout,
/// The arbitration for the bus was lost.
Expand All @@ -106,13 +106,58 @@ pub enum Error {
ZeroLengthInvalid,
}

/// I2C no acknowledge error reason.
///
/// Consider this as a hint and make sure to always handle all cases.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this non_exhaustive, isn't this enum complete? What else is there for the device to not acknowledge?

(Using both Unknown and non_exhaustive seems wrong to me at a glance)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In future we might want to provide more information with the error (e.g. at which byte count the nack was detected) - adding such a variant would be a breaking change then. I imagine user-code will do the same when matching Unknown or _ so I don't think it's a big deal

I agree for now we wouldn't even need Address and Data for now, then

Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't such a change be breaking (as we'd add the info to Data)? Or how do you imagine such change to manifest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if we change the variant that would be breaking - but we could add another variant - but I agree we should just remove the currently unused variants for now

Copy link
Collaborator

Choose a reason for hiding this comment

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

if we change the variant that would be breaking - but we could add another variant - but I agree we should just remove the currently unused variants for now

Adding a variant can still be a breaking change, not at compile time but at runtime. Errors are tricky.

If you add a new variant that is a subset of an existing variant, this is a breaking change.

Imagine a user doing something like this. (Based on the documentation this is acceptable to do)

if err == AcknowledgeCheckFailedReason::Unknown {
    // do something because address might not have been acknowledged.
}

If you split Unknown into Unknown and Address, once the user pulls in the new version of esp-hal, this if statement no longer executes where they expected it to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes but that is true for pretty much every error-enum we have

checking an error like that is also bad for exhaustive enums

Copy link
Collaborator

Choose a reason for hiding this comment

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

My code example isn't fantastic but it's easy for code to semantically look like that.

Perhaps I'm being paranoid, I can't think of a nice solution besides completing the enum.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is precedent in rust std to introduce such breaking changes. We can't prevent users from explicitly matching against Unknown but we can document that we don't guarantee that we don't split error variants out of it, and it should be handled as the "nothing else applies" case.

That being said, we should be extra careful with these cases. We should probably add and handle Address and Data right now, and accept that they won't carry additional data, or include an empty struct in them if we know we might have something useful in them later.

pub enum AcknowledgeCheckFailedReason {
/// The device did not acknowledge its address. The device may be missing.
Address,

/// The device did not acknowledge the data. It may not be ready to process
/// requests at the moment.
Data,

/// Either the device did not acknowledge its address or the data, but it is
/// unknown which.
Unknown,
}

impl core::fmt::Display for AcknowledgeCheckFailedReason {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AcknowledgeCheckFailedReason::Address => write!(f, "Address"),
AcknowledgeCheckFailedReason::Data => write!(f, "Data"),
AcknowledgeCheckFailedReason::Unknown => write!(f, "Unknown"),
}
}
}

impl From<&AcknowledgeCheckFailedReason> for embedded_hal::i2c::NoAcknowledgeSource {
fn from(value: &AcknowledgeCheckFailedReason) -> Self {
match value {
AcknowledgeCheckFailedReason::Address => {
embedded_hal::i2c::NoAcknowledgeSource::Address
}
AcknowledgeCheckFailedReason::Data => embedded_hal::i2c::NoAcknowledgeSource::Data,
AcknowledgeCheckFailedReason::Unknown => {
embedded_hal::i2c::NoAcknowledgeSource::Unknown
}
}
}
}

impl core::error::Error for Error {}

impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::FifoExceeded => write!(f, "The transmission exceeded the FIFO size"),
Error::AckCheckFailed => write!(f, "The acknowledgment check failed"),
Error::AcknowledgeCheckFailed(reason) => {
write!(f, "The acknowledgment check failed. Reason: {}", reason)
}
Error::Timeout => write!(f, "A timeout occurred during transmission"),
Error::ArbitrationLost => write!(f, "The arbitration for the bus was lost"),
Error::ExecutionIncomplete => {
Expand Down Expand Up @@ -211,12 +256,12 @@ impl Operation<'_> {

impl embedded_hal::i2c::Error for Error {
fn kind(&self) -> embedded_hal::i2c::ErrorKind {
use embedded_hal::i2c::{ErrorKind, NoAcknowledgeSource};
use embedded_hal::i2c::ErrorKind;

match self {
Self::FifoExceeded => ErrorKind::Overrun,
Self::ArbitrationLost => ErrorKind::ArbitrationLoss,
Self::AckCheckFailed => ErrorKind::NoAcknowledge(NoAcknowledgeSource::Unknown),
Self::AcknowledgeCheckFailed(reason) => ErrorKind::NoAcknowledge(reason.into()),
_ => ErrorKind::Other,
}
}
Expand Down Expand Up @@ -633,7 +678,9 @@ impl<'a> I2cFuture<'a> {
}

if r.nack().bit_is_set() {
return Err(Error::AckCheckFailed);
return Err(Error::AcknowledgeCheckFailed(estimate_ack_failed_reason(
self.info.register_block(),
)));
}

#[cfg(not(esp32))]
Expand All @@ -646,7 +693,9 @@ impl<'a> I2cFuture<'a> {
.resp_rec()
.bit_is_clear()
{
return Err(Error::AckCheckFailed);
return Err(Error::AcknowledgeCheckFailed(
AcknowledgeCheckFailedReason::Data,
));
}

Ok(())
Expand Down Expand Up @@ -1655,7 +1704,7 @@ impl Driver<'_> {
let retval = if interrupts.time_out().bit_is_set() {
Err(Error::Timeout)
} else if interrupts.nack().bit_is_set() {
Err(Error::AckCheckFailed)
Err(Error::AcknowledgeCheckFailed(estimate_ack_failed_reason(self.register_block())))
} else if interrupts.arbitration_lost().bit_is_set() {
Err(Error::ArbitrationLost)
} else {
Expand All @@ -1666,11 +1715,11 @@ impl Driver<'_> {
let retval = if interrupts.time_out().bit_is_set() {
Err(Error::Timeout)
} else if interrupts.nack().bit_is_set() {
Err(Error::AckCheckFailed)
Err(Error::AcknowledgeCheckFailed(estimate_ack_failed_reason(self.register_block())))
} else if interrupts.arbitration_lost().bit_is_set() {
Err(Error::ArbitrationLost)
} else if interrupts.trans_complete().bit_is_set() && self.register_block().sr().read().resp_rec().bit_is_clear() {
Err(Error::AckCheckFailed)
Err(Error::AcknowledgeCheckFailed(AcknowledgeCheckFailedReason::Data))
} else {
Ok(())
};
Expand Down Expand Up @@ -2254,6 +2303,23 @@ fn write_fifo(register_block: &RegisterBlock, data: u8) {
}
}

// Estimate the reason for an acknowledge check failure on a best effort basis.
// When in doubt it's better to return `Unknown` than to return a wrong reason.
fn estimate_ack_failed_reason(_register_block: &RegisterBlock) -> AcknowledgeCheckFailedReason {
cfg_if::cfg_if! {
if #[cfg(any(esp32, esp32s2, esp32c2, esp32c3))] {
AcknowledgeCheckFailedReason::Unknown
} else {
// this is based on observations rather than documented behavior
if _register_block.fifo_st().read().txfifo_raddr().bits() <= 1 {
AcknowledgeCheckFailedReason::Address
} else {
AcknowledgeCheckFailedReason::Data
}
}
}
}

macro_rules! instance {
($inst:ident, $peri:ident, $scl:ident, $sda:ident, $interrupt:ident) => {
impl Instance for crate::peripherals::$inst {
Expand Down
26 changes: 21 additions & 5 deletions hil-test/tests/i2c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#![no_main]

use esp_hal::{
i2c::master::{Config, Error, I2c, Operation},
i2c::master::{AcknowledgeCheckFailedReason, Config, Error, I2c, Operation},
Async,
Blocking,
};
Expand Down Expand Up @@ -51,10 +51,26 @@ mod tests {

#[test]
fn empty_write_returns_ack_error_for_unknown_address(mut ctx: Context) {
assert_eq!(
ctx.i2c.write(NON_EXISTENT_ADDRESS, &[]),
Err(Error::AckCheckFailed)
);
// on some chips we can determine the ack-check-failed reason but not on all
// chips
cfg_if::cfg_if! {
if #[cfg(any(esp32,esp32s2,esp32c2,esp32c3))] {
assert_eq!(
ctx.i2c.write(NON_EXISTENT_ADDRESS, &[]),
Err(Error::AcknowledgeCheckFailed(
AcknowledgeCheckFailedReason::Unknown
))
);
} else {
assert_eq!(
ctx.i2c.write(NON_EXISTENT_ADDRESS, &[]),
Err(Error::AcknowledgeCheckFailed(
AcknowledgeCheckFailedReason::Address
))
);
}
}

assert_eq!(ctx.i2c.write(DUT_ADDRESS, &[]), Ok(()));
}

Expand Down
Loading