Expand description
Use the SERCOM peripheral for I2C communications
Configuring an I2C peripheral occurs in three steps. First, you must create
a set of Pads
for use by the peripheral. Next, you assemble pieces into
a Config
struct. After configuring the peripheral, you then enable
it, yielding a functional I2c
struct. Transactions are performed using
traits from embedded-hal (v1.0 and blocking traits from
v0.2).
§Pads
A Sercom
uses two Pin
s as peripheral Pad
s, but only certain
Pin
combinations are acceptable. All Pin
s must be mapped to the same
Sercom
, and SDA is always Pad0
, while SCL is always Pad1
(see
the datasheet). SAMx5x chips have an additional constraint that Pin
s
must be in the same IoSet
. This HAL makes it impossible to use invalid
Pin
/Pad
combinations, and the Pads
struct is responsible for
enforcing these constraints.
A Pads
type takes three type parameters. The first type specifies the
Sercom
, SDA
and SCL
represent the SDA and SCL pads respectively. A
Pad
is just a Pin
configured in the correct PinMode
that
implements IsPad
. The bsp_pins!
macro can be used
to define convenient type aliases for Pad
types.
use atsamd_hal::gpio::{PA08, PA09, AlternateC};
use atsamd_hal::sercom::{Sercom0, i2c};
use atsamd_hal::typelevel::NoneT;
type Sda = Pin<PA08, AlternateC>;
type Scl = Pin<PA09, AlternateC>;
type Pads = i2c::Pads<Sercom0, Sda, Scl>;
Alternatively, you can use the PadsFromIds
alias to define a set of
Pads
in terms of PinId
s instead of Pin
s. This is useful when you
don’t have Pin
aliases pre-defined.
use atsamd_hal::gpio::{PA08, PA09};
use atsamd_hal::sercom::{Sercom0, i2c};
type Pads = i2c::PadsFromIds<Sercom0, PA08, PA09>;
Instances of Pads
are created using the new
method.
On SAMD21 and SAMx5x chips, new
method automatically convert
each pin to the correct PinMode
. But for SAMD11 chips, users must
manually convert each pin before calling the builder methods. This is a
consequence of inherent ambiguities in the SAMD11 SERCOM pad definitions.
Specifically, the same PinId
can correspond to two different PadNum
s
for the same Sercom
.
use atsamd_hal::pac::Peripherals;
use atsamd_hal::gpio::Pins;
use atsamd_hal::sercom::{Sercom0, i2c};
let mut peripherals = Peripherals::take().unwrap();
let pins = Pins::new(peripherals.PORT);
let pads = i2c::Pads::<Sercom0>::new(pins.pa08, pins.pa09);
§Config
Next, create a Config
struct, which represents the I2C peripheral in its
disabled state. A Config
is specified with one type parameters, the
Pads
type.
Upon creation, the Config
takes ownership of both the Pads
struct
and the PAC Sercom
struct. It takes a reference to the PM, so that it
can enable the APB clock, and it takes a frequency to indicate the GCLK
configuration. Users are responsible for correctly configuring the GCLK.
use atsamd_hal::gpio::{PA08, PA09};
use atsamd_hal::sercom::{Sercom0, i2c};
type Pads = i2c::PadsFromIds<Sercom0, PA08, PA09>;
type Config = i2c::Config<Pads>;
let pm = peripherals.PM;
let sercom = peripherals.SERCOM0;
// Configure GCLK for 10 MHz
let freq = 10.mhz();
let config = i2c::Config::new(&pm, sercom, pads, freq);
The Config
struct can configure the peripheral in one of two ways:
- A set of methods is provided to use in a builder pattern: for example
baud
,run_in_standby
, etc. These methods takeself
and returnSelf
. - A set of methods is provided to use as setters: for example
set_baud
,set_run_in_standby
, etc. These methods take&mut self
and return nothing.
In any case, the peripheral setup ends with a call to enable
, which
consumes the Config
and returns an enabled I2c
peripheral.
let i2c = i2c::Config::new(&pm, sercom, pads, freq)
.baud(1.mhz())
.enable();
Alternatively,
let i2c = i2c::Config::new(&mclk, sercom, pads, freq);
i2c.set_baud(1.mhz());
let i2c = i2c.enable();
§Reading the current configuration
It is possible to read the current configuration by using the getter methods
provided: for example get_baud
,
get_run_in_standby
, etc.
§I2c
I2c
structs can only be created from a Config
. They have one type
parameter, representing the underlying Config
.
Only the I2c
struct can actually perform transactions. To do so, use the
embedded_hal::i2c::I2c
trait.
use embedded_hal::i2c::I2c;
i2c.write(0x54, 0x0fe).unwrap();
§Reading the current configuration
The AsRef<Config<P>>
trait is implemented for I2c<Config<P>>
. This means
you can use the get_
methods implemented for Config
, since they take an
&self
argument.
// Assume i2c is a I2c<C<P>>
let baud = i2c.as_ref().get_baud();
§Reconfiguring
The reconfigure
method gives out an &mut Config
reference, which can
then use the set_*
methods.
use atsamd_hal::sercom::i2c::I2c;
// Assume config is a valid Duplex I2C Config struct
let i2c = config.enable();
// Send/receive data...
// Reconfigure I2C peripheral
i2c.reconfigure(|c| c.set_run_in_standby(false));
// Disable I2C peripheral
let config = i2c.disable();
§Non-supported features
- Slave mode is not supported at this time.
- High-speed mode is not supported.
- 4-wire mode is not supported.
- 32-bit extension mode is not supported (SAMx5x). If you need to transfer
slices, consider using the DMA methods instead
dma
.
§Using I2C with DMA dma
This HAL includes support for DMA-enabled I2C transfers. Use
I2c::with_dma_channel
to attach a DMA channel to the I2c
struct. A
DMA-enabled I2c
implements the blocking
embedded_hal::i2c::I2c
trait, which can be used
to perform I2C transfers which are fast, continuous and low jitter, even if
they are preemped by a higher priority interrupt.
use atsamd_hal::dmac::channel::{AnyChannel, Ready};
use atsand_hal::sercom::i2c::{I2c, AnyConfig, Error};
use atsamd_hal::embedded_hal::i2c::I2c;
fn i2c_write_with_dma<A: AnyConfig, C: AnyChannel<Status = Ready>>(i2c: I2c<A>, channel: C, bytes: &[u8]) -> Result<(), Error>{
// Attach a DMA channel
let i2c = i2c.with_dma_channel(channel);
i2c.write(0x54, bytes)?;
}
§Limitations of using DMA with I2C
-
The I2C peripheral only supports continuous DMA read/writes of up to 255 bytes. Trying to read/write with a transfer of 256 bytes or more will result in a panic. This also applies to using
I2c::transaction
with adjacent write/read operations of the same type; the total number of bytes across all adjacent operations must not exceed 256. If you need continuous transfers of 256 bytes or more, use the non-DMAI2c
implementations. -
When using
I2c::transaction
orI2c::write_read
, theembedded_hal::i2c::I2c
specification mandates that a REPEATED START (instead of a STOP+START) is sent between transactions of a different type (read/write). Unfortunately, in DMA mode, the hardware is only capable of sending STOP+START. If you absolutely need repeated starts, the only workaround is to use the I2C without DMA. -
Using
I2c::transaction
consumes significantly more memory than the other methods provided byembedded_hal::i2c::I2c
(at least 256 bytes extra). -
When using
I2c::transaction
, up to 17 adjacent operations of the same type can be continuously handled by DMA without CPU intervention. If you need more than 17 adjacent operations of the same type, the transfer will reverted to using the byte-by-byte (non-DMA) implementation.
All these limitations also apply to I2C transfers in async mode when using DMA. They do not apply to I2C transfers in async mode when not using DMA.
§async
operation async
An I2c
can be used for async
operations. Configuring an I2c
in
async mode is relatively simple:
- Bind the corresponding
SERCOM
interrupt source to the SPIInterruptHandler
(refer to the module-levelasync_hal
documentation for more information). - Turn a previously configured
I2c
into anI2cFuture
by callingI2c::into_future
- Optionally, add a DMA channel by using
I2cFuture::with_dma_channel
. The API is exactly the same whether a DMA channel is used or not. - Use the provided async methods for reading or writing to the I2C
peripheral.
I2cFuture
implementsembedded_hal_async::i2c::I2c
.
I2cFuture
implements AsRef<I2c>
and AsMut<I2c>
so that it can be
reconfigured using the regular I2c
methods.
§Considerations when using async
I2c
with DMA async
dma
- An
I2c
struct must be turned into anI2cFuture
by callingI2c::into_future
before callingwith_dma_channel
. The DMA channel itself must also be configured in async mode by usingDmaController::into_future
. If a DMA channel is added to theI2c
struct before it is turned into anI2cFuture
, it will not be able to use DMA in async mode.
// This will work
let i2c = i2c.into_future().with_dma_channel(channel);
// This won't
let i2c = i2c.with_dma_channel(channel).into_future();
§Safety considerations
In async
mode, an I2C+DMA transfer does not require 'static
source and
destination buffers. This, in theory, makes its use unsafe
. However it is
marked as safe for better ergonomics, and to enable the implementation of
the embedded_hal_async::i2c::I2c
trait.
This means that, as an user, you must ensure that the Future
s
returned by the embedded_hal_async::i2c::I2c
methods may never be
forgotten through forget
or by wrapping them with a ManuallyDrop
.
The returned futures implement Drop
and will automatically stop any
ongoing transfers; this guarantees that the memory occupied by the
now-dropped buffers may not be corrupted by running transfers.
This means that using functions like futures::select_biased
to implement
timeouts is safe; transfers will be safely cancelled if the timeout expires.
This also means that should you forget
this Future
after its first
poll
call, the transfer will keep running, ruining the now-reclaimed
memory, as well as the rest of your day.
await
ing is fine: theFuture
will run to completion.- Dropping an incomplete transfer is also fine. Dropping can happen, for example, if the transfer doesn’t complete before a timeout expires.
- Dropping an incomplete transfer without running its destructor is unsound and will trigger undefined behavior.
async fn always_ready() {}
let mut buffer = [0x00; 10];
// This is completely safe
i2c.read(&mut buffer).await?;
// This is also safe: we launch a transfer, which is then immediately cancelled
futures::select_biased! {
_ = i2c.read(&mut buffer)?,
_ = always_ready(),
}
// This, while contrived, is also safe.
{
use core::future::Future;
let future = i2c.read(&mut buffer);
futures::pin_mut!(future);
// Assume ctx is a `core::task::Context` given out by the executor.
// The future is polled, therefore starting the transfer
future.as_mut().poll(ctx);
// Future is dropped here - transfer is cancelled.
}
// DANGER: This is an example of undefined behavior
{
use core::future::Future;
use core::ops::DerefMut;
let future = core::mem::ManuallyDrop::new(i2c.read(&mut buffer));
futures::pin_mut!(future);
// To actually make this example compile, we would need to wrap the returned
// future from `i2c.read()` in a newtype that implements Future, because we
// can't actually call as_mut() without being able to name the type we want
// to deref to.
let future_ref: &mut SomeNewTypeFuture = &mut future.as_mut();
future.as_mut().poll(ctx);
// Future is NOT dropped here - transfer is not cancelled, resulting un UB.
}
As you can see, unsoundness is relatively hard to come by - however, caution should still be exercised.
Structs§
- Config
- A configurable, disabled I2C peripheral
- Flags
- Interrupt bitflags for I2C transactions
- I2c
- Abstraction over a I2C peripheral, allowing to perform I2C transactions.
- I2cFuture
async
version ofI2c
.- Interrupt
Handler - Interrupt handler for async I2C operarions
- Pads
- Container for a set of SERCOM
Pad
s - Status
- Status flags for I2C transactions
Enums§
- BusState
- Type representing the current bus state
- Error
- Errors available for I2C transactions
- Inactive
Timeout - Inactive timeout configuration
Traits§
- AnyConfig
- Type class for all possible
Config
types - PadSet
- Type-level function to recover the
Pad
types from a generic set ofPads
Type Aliases§
- Config
Sercom - Type alias to recover the specific
Sercom
type from an implementation ofAnyConfig
- I2cFuture
Dma - Convenience type for a
I2cFuture
in DMA mode. - Pads
From Ids - Define a set of
Pads
usingPinId
s instead ofPin
s - Specific
Config - Type alias to recover the specific
Config
type from an implementation ofAnyConfig
- Word
- Word size for an I2C message