blob: 04cd044f60b5e6d776b24e7c4c58626190f35177 [file] [log] [blame]
.. _seed-0117:
=============
0117: I3C
=============
.. seed::
:number: 117
:name: I3C
:status: Accepted
:proposal_date: 2023-10-30
:authors: Jack Chen
:cl: 178350
:facilitator: Alexei Frolov
-------
Summary
-------
A new peripheral protocol, I3C (pronounced eye-three-see) was introduced to
electronic world and it has been widely accepted by SoC and sensor
manufacturers. This seed is to propose a new front-end library pw_i3c, to help
communicate with devices on I3C bus.
----------
Motivation
----------
Though widely used, I²C peripheral bus has several significant shortages, for
example the low bus speed, extra physical line to carry interrupt from each
device on the I²C bus, etc. To cope with higher requirements and to address
shortages of I²C, MIPI proposed a new fast, low-power and two-wire peripheral
protocol, I3C.
I3C could be regarded as improved I²C. But in the meantime, it is a new protocol
which is different to I²C in both hardware and software. Some important
differences include:
1. I3C SDA uses open-drain mode when necessary for legacy I²C compatibility,
but switches to push-pull outputs whenever possible.
2. I3C SCL runs in only in pull-pull mode and can only be driven by I3C
initiator.
3. I3C devices supports dynamic address assignment (DAA) and has higher address
requirements.
4. I3C needs bus initialization and DAA before it is ready to use.
5. I3C has a standardized command set, called common command codes (CCC).
6. I3C supports In-Band interrupt and hot-join.
7. I3C device is identified by a 48-bit Provisioned ID (Manufacturer ID + Part
ID + Instance ID).
In conclusion, it is worth providing an independent library pw_i3c to help
initialize I3C initiator and communicate with I3C devices on the bus.
--------------------------
Proposal (Detailed design)
--------------------------
Since I3C is very similar to I²C, following proposal is to create a standalone
library pw_i3c, which shares a similar structure as pw_i2c.
device type
-----------
The ``DeviceType`` is used to help differentiate legacy I²C devices and I3C
devices on one I3C bus.
.. code-block:: c++
enum class DeviceType : bool {
kI2c,
kI3c,
};
Address
-------
The ``Address`` is a helper class to represent addresses which are used by
``pw_i3c`` APIs. In I3C protocol, addresses are differentiated by I²C (static)
address and I3C (active or dynamic) address. The reason why device type is
embedded into address is that transactions for I²C and I3C devices are
different. So ``Initiator`` could tell the device type just by the provided
``Address`` object.
Apart from creating and checking ``Address`` at compile time, a helper
constructor to create and check ``Address`` at runtime is created, with
following reasons:
1. A new active/dynamic address could be assigned to I3C devices at run time.
2. New devices could hot-join the bus at run time.
.. code-block:: c++
class Address {
public:
static constexpr uint8_t kHotJoinAddress = 0x02;
static constexpr uint8_t kBroadcastAddress = 0x7E;
static constexpr uint8_t kStaticMaxAddress = 0x7F;
// For I3C, the active (dynamic) address restriction is dynamic (depending
// on devices on the bus, details can be found in "MIPI I3C Basic
// Specification Version 1.1.1" chapter 5.1.2.2.5). But to simplify the
// design, the strictest rule is applied. And there will be 108 addresses
// free for dynamic address chosen.
static constexpr uint8_t kDynamicMinAddress = 0x08;
static constexpr uint8_t kDynamicMaxAddress = 0x7D;
// Helper constructor to ensure the address fits in the address space at
// compile time, skipping the construction time assert.
template <DeviceType kDeviceType, uint8_t kAddress>
static constexpr Address Create() {
static_assert(!IsOutOfRange(kDeviceType, kAddress));
static_assert((DeviceType::kI2c == kDeviceType) ||
!SingleBitErrorDetection(kAddress));
return Address{kDeviceType, kAddress};
}
// Helper constructor to create the address at run time.
// Returns std::nullopt if provided type and address does not fit address
// rules.
static constexpr std::optional<Address> Create(
DeviceType device_type, uint8_t address) {
if (IsOutOfRange(device_type, address)) {
return std::nullopt;
}
if (DeviceType::kI3c == device_type && SingleBitErrorDetection(address)) {
return std::nullopt;
}
return Address{type, address};
}
// Return the type of address.
constexpr DeviceType GetType() const;
// Return the address.
constexpr uint8_t GetAddress() const;
private:
static constexpr uint8_t kAddressMask = 0x7F;
static constexpr int kTypeShift = 7;
static constexpr uint8_t kTypeMask = 0x01;
constexpr Address(DeviceType type, uint8_t address)
: packed_address_{Pack(type, address)} {}
uint8_t packed_address_;
};
Ccc
---
Common Command Codes are categorized into broadcast(Command Codes from 0x00 to
0x7F) and direct(Command Codes from 0x80 to 0xFE). The rational behind it is
broadcast CCC can only be write and is executed in one transaction, but direct
CCC can be both write and read, and is executed in two transactions. We can
eliminate extra CCC type check in initiator CCC API.
.. code-block:: c++
enum class CccBroadcast : uint8_t {
kEnc = 0x00,
kDisec = 0x01,
kEntdas0 = 0x02,
...
kSetxtime = 0x28,
kSetaasa = 0x29,
}
enum class CccDirect : uint8_t {
kEnc = 0x80,
kDisec = 0x81,
kEntas0 = 0x82,
...
kSetxtime = 0x98,
kGetxtime = 0x99,
};
inline constexpr uint8_t kCccDirectBit = 7;
enum class CccAction : bool {
kWrite,
kRead,
};
Initiator
---------
.. inclusive-language: disable
Similar as ``pw::i2c``, ``Initiator`` is the common, base driver interface for
initiating thread-safe transactions with devices on an I3C bus. Other
documentation may call this style of interface an “master”, “central”, or
“controller”.
.. inclusive-language: enable
The main difference by comparison with I²C, is I3C initiator needs a bus
initialization and dynamic address assignment (DAA) step, before it is fully
functional. And after first bus initialization, I3C initiator should be able to
do bus re-initialization anytime when the bus is free. However, different
backend implementations may deal with this part differently. For example, Linux
does not expose I3C bus to userspace, which means users cannot control bus
initialization. NXP Mcuxpresso SDK exposes I3C as a pure library to users, so it
is users' responsibility to initialize the bus and perform DAA to get a usable
initiator. Zephyr provides more functions than Linux regarding I3C, which
makes I3C usage in Zephyr looks more likely to NXP Mcuxpresso SDK.
Considering the complexity of different backend implementations of I3C bus, it
is better to have an "Initiator Maker" to take care of making an I3C
``Initiator``. And this is not considered in the first version of ``pw_i3c``
design.
.. code-block:: c++
// PID is the unique identifier to an I3C device.
struct Pid {
uint16_t manuf_id = 0;
uint16_t part_id = 0;
uint16_t instance_id = 0;
friend constexpr bool operator==(Pid const& lhs, Pid const& rhs) {
return (lhs.manuf_id == rhs.manuf_id) && (lhs.part_id == rhs.part_id) &&
(lhs.instance_id == rhs.instance_id);
}
friend constexpr bool operator!=(Pid const& lhs, Pid const& rhs) {
return (lhs.manuf_id != rhs.manuf_id) || (lhs.part_id != rhs.part_id) ||
(lhs.instance_id != rhs.instance_id);
}
// Concat manuf_id, part_id and instance_id to a pid in 64-bit.
constexpr uint64_t AsUint64(Pid const pid) {
return (static_cast<uint64_t>(pid.manuf_id) << 33) |
(static_cast<uint64_t>(pid.part_id) << 16) |
(static_cast<uint64_t>(pid.instance_id) << 12);
}
// Split a 64-bit pid into manuf_id, part_id and instance_id (struct Pid).
static constexpr Pid FromUint64(const uint64_t pid) {
return Pid{
.manuf_id = ExtractBits<uint16_t, 47, 33>(pid),
.part_id = ExtractBits<uint16_t, 31, 16>(pid),
.instance_id = ExtractBits<uint16_t, 15, 12>(pid)};
}
};
// I3C supports in-band interrupt (IBI), but generalize the name to cover
// traditional interrupts.
// For IBI, the argument can be used to store data transferred.
// If the handler is queued in a workqueue, the data could be of any length.
// If the handler is executed inside CPU ISR directly, the data should only
// be mandatory data byte.
using InterruptHandler = ::pw::Function<void(ByteSpan)>;
class Initiator {
public:
virtual ~Initiator() = default;
// Pid is the unique identifier to an I3C device and it should be known to
// users through datasheets, like static addresses to I²C or I3C devices.
// But active (dynamic) address to an I3C device is changeable during
// run-time. Users could use this API to retrieve active address through
// PID.
//
// There are other information which users may be interested to know about
// an I3C device, like Bus Characteristics Register (BCR) and Device
// Characteristic Register(s) (DCR). But they can be read through direct
// read CCC API (ReadDirectCcc).
//
// Returns:
// Dynamic Address - Success.
// NOT_FOUND - Provided pid does not match with any active i3c device.
Result<Address> RetrieveDynamicAddressByPid(Pid pid);
// Perform a broadcast CCC transaction.
// ccc_id: the broadcast CCC ID.
// buffer: payload to broadcast.
//
// Returns:
// OK - Success.
// INVALID_ARGUMENT - provided ccc_id is not supported.
// UNAVAILABLE - NACK condition occurred, meaning there are no active I3C
// devices on the bus.
// Other status codes as defined by backend.
Status WriteBroadcastCcc(CccBroadcast ccc_id, ConstByteSpan buffer);
Status WriteBroadcastCcc(CccBroadcast ccc_id,
const void* buffer,
size_t size_bytes);
// Perform a direct write CCC transaction.
// ccc_id: the direct CCC ID.
// device_address: the address which the CCC targets for.
// buffer: payload to write.
//
// Returns:
// OK - Success.
// INVALID_ARGUMENT - provided ccc_id is not supported, or device_address is
// for I3C devices.
// UNAVAILABLE - NACK condition occurred, meaning there is no active I3C
// device with the provided device_address or it is busy now.
// Other status codes as defined by backend.
Status WriteDirectCcc(CccDirect ccc_id,
Address device_address,
ConstByteSpan buffer);
Status WriteDirectCcc(CccDirect ccc_id,
Address device_address,
const void* buffer,
size_t size_bytes);
// Perform a direct read CCC transaction.
// ccc_id: the direct CCC ID.
// device_address: the address which the CCC targets for.
// buffer: payload to read.
//
// Returns:
// OK - Success.
// INVALID_ARGUMENT - provided ccc_id is not supported, or device_address is
// for I3C devices.
// UNAVAILABLE - NACK condition occurred, meaning there is no active I3C
// device with the provided device_address or it is busy now.
// Other status codes as defined by backend.
Status ReadDirectCcc(CccDirect ccc_id,
Address device_address,
ByteSpan buffer);
Status ReadDirectCcc(CccDirect ccc_id,
Address device_address,
void* buffer,
size_t size_bytes);
// Write bytes and read bytes (this is normally executed in two independent
// transactions).
//
// Timeout is no longer needed in I3C transactions because only I3C
// initiator drives the clock in push-pull mode, and devices on the bus
// cannot stretch the clock.
//
// Returns:
// Ok - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
// Other status codes as defined by backend.
Status WriteReadFor(Address device_address,
ConstByteSpan tx_buffer,
ByteSpan rx_buffer);
Status WriteReadFor(I3cResponder device,
const void* tx_buffer,
size_t tx_size_bytes,
void* rx_buffer,
size_t rx_size_bytes);
// Write bytes.
//
// Returns:
// OK - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
// Other status codes as defined by backend.
Status WriteFor(Address device_address, ConstByteSpan tx_buffer);
Status WriteFor(Address device_address,
const void* tx_buffer,
size_t tx_size_bytes);
// Read bytes.
//
// Returns:
// OK - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
// Other status codes as defined by backend.
Status ReadFor(Address device_address, ByteSpan rx_buffer);
Status ReadFor(Address device_address,
void* rx_buffer,
size_t rx_size_bytes);
// Probes the device for an ACK after only writing the address.
// This is done by attempting to read a single byte from the specified
// device.
//
// Returns:
// OK - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
Status ProbeDeviceFor(Address device_address);
// Sets a (IBI) handler to execute when an interrupt is triggered from a
// device with the provided address. Handler for one address should be
// registered only once, unless it is cleared. Registration twice with
// same address should fail.
//
// Note that hot-join handler could be registered with this function since
// hot-join is sent through IBI.
//
// This handler is finally executed by I3C initiator, which means it may
// include any valid I3C actions (write and read). When I3C write/read
// happens, the interrupt handler is more like an I3C transaction than a
// traditional interrupt. Different I3C initiators may execute the handler
// in different ways. Some may queue the work on a workqueue and some may
// execute the handler directly inside IBI IRQ. Users should be aware of the
// backend algorithm and when execution happens in IBI IRQ, they should just
// read the mandatory data byte out through the handler and perform other
// actions in a different thread.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Returns:
// OK - The interrupt handler was configured.
// INVALID_ARGUMENT - The handler is empty, or the handler is for IBI but
// the address is not for an I3C device.
// Other status codes as defined by the backend.
Status SetInterruptHandler(Address device_address,
InterruptHandler&& handler);
// Clears the interrupt handler.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Returns:
// OK - The interrupt handler was cleared
// INVALID_ARGUMENT - the handler is for IBI but the address is not for an
// I3C device.
// Other status codes as defined by the backend.
Status ClearInterruptHandler(Address device_address);
// Enables interrupts which will trigger the interrupt handler.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Preconditions:
// A handler has been set using `SetInterruptHandler()`.
//
// Returns:
// OK - The interrupt handler was enabled.
// INVALID_ARGUMENT - the handler is for IBI but the address is not for an
// I3C device.
// Other status codes as defined by the backend.
Status EnableInterruptHandler(Address device_address);
// Disables interrupts which will trigger the interrupt handler.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Preconditions:
// A handler has been set using `SetInterruptHandler()`.
//
// Returns:
// OK - The interrupt handler was disabled.
// INVALID_ARGUMENT - the handler is for IBI but the address is not for an
// I3C device.
// Other status codes as defined by the backend.
Status DisableInterruptHandler(Address device_address);
private:
virtual Result<Address> DoRetrieveDynamicAddressByPid(Pid pid) = 0;
virtual Status DoTransferCcc(CccAction read_or_write,
uint8_t ccc_id,
std::optional<Address> device_address,
ByteSpan buffer) = 0;
virtual Status DoWriteReadFor(Address device_address,
ConstByteSpan tx_buffer,
ByteSpan rx_buffer) = 0;
virtual Status DoSetInterruptHandler(Address address,
InterruptHandler&& handler) = 0;
virtual Status DoEnableInterruptHandler(Address address, bool enable) = 0;
};
Device
------
Same as ``pw::i2c::Device``, a ``Device`` class is used in ``pw::i3c`` to
write/read arbitrary chunks of data over a bus to a specific device, or perform
other I3C operations, e.g. direct CCC.
Though PID is the unique identifier for I3C devices, considering backward
compatibility with I²C devices, this object also wraps the Initiator API with
an active ``Address``. Application should initiate or be notified the
``Address`` change and update the ``Address`` in ``Device`` object.
.. code-block:: c++
class Device {
public:
// It is users' responsibility to get and pass the active (dynamic) address
// when creating a ``Device``. If the dynamic address of an I3C device is
// unknown, users could get it through initiator:
// pw::i3c::Initiator::RetrieveDynamicAddressByPid();
constexpr Device(Initiator& initiator, Address device_address, Pid pid)
: initiator_(initiator),
device_address_(device_address),
pid_(pid) {}
// For I2C devices connected to I3C bus, pid_ is default-initialized to be
// std::nullopt.
constexpr Device(Initiator& initiator, Address device_address)
: initiator_(initiator),
device_address_(device_address) {}
Device(const Device&) = delete;
Device(Device&&) = default;
~Device() = default;
// Perform a direct write CCC transaction.
// ccc_id: the direct CCC ID.
// buffer: payload to write.
//
// Returns:
// OK - Success.
// INVALID_ARGUMENT - provided ccc_id is not supported, or device_address_
// is not for I3C devices.
// UNAVAILABLE - NACK condition occurred, meaning there is no active I3C
// device with the provided device_address or it is busy now.
// Other status codes as defined by backend.
Status WriteDirectCcc(CccDirect ccc_id, ConstByteSpan buffer);
Status WriteDirectCcc(CccDirect ccc_id,
const void* buffer,
size_t size_bytes);
// Perform a direct read CCC transaction.
// ccc_id: the direct CCC ID.
// buffer: payload to read.
//
// Returns:
// OK - Success.
// INVALID_ARGUMENT - provided ccc_id is not supported, or device_address_
// is not for I3C devices.
// UNAVAILABLE - NACK condition occurred, meaning there is no active I3C
// device with the provided device_address or it is busy now.
// Other status codes as defined by backend.
Status ReadDirectCcc(CccDirect ccc_id, ByteSpan buffer);
Status ReadDirectCcc(CccDirect ccc_id,
const void* buffer,
size_t size_bytes);
// Write bytes and read bytes (this is normally executed in two independent
// transactions).
//
// Timeout is no longer needed in I3C transactions because only I3C
// initiator drives the clock in push-pull mode, and devices on the bus
// cannot stretch the clock.
//
// Returns:
// OK - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
// Other status codes as defined by backend.
Status WriteReadFor(ConstByteSpan tx_buffer, ByteSpan rx_buffer);
Status WriteReadFor(const void* tx_buffer,
size_t tx_size_bytes,
void* rx_buffer,
size_t rx_size_bytes);
// Write bytes.
//
// Returns:
// OK - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
// Other status codes as defined by backend.
Status WriteFor(ConstByteSpan tx_buffer);
Status WriteFor(const void* tx_buffer, size_t tx_size_bytes);
// Read bytes.
//
// Returns:
// Ok - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
// Other status codes as defined by backend.
Status ReadFor(ByteSpan rx_buffer);
Status ReadFor(void* rx_buffer, size_t rx_size_bytes);
// Probes the device for an ACK after only writing the address.
// This is done by attempting to read a single byte from this device.
//
// Returns:
// Ok - Success.
// UNAVAILABLE - NACK condition occurred, meaning the addressed device did
// not respond or was unable to process the request.
Status ProbeFor();
// Sets a (IBI) handler to execute when an interrupt is triggered from a
// device with the provided address. Handler for one address should be
// registered only once, unless it is cleared. Registration twice with
// same address should fail.
//
// This handler is finally executed by I3C initiator, which means it may
// include any valid I3C actions (write and read). When I3C write/read
// happens, the interrupt handler is more like an I3C transaction than a
// traditional interrupt. Different I3C initiators may execute the handler
// in different ways. Some may queue the work on a workqueue and some may
// execute the handler directly inside IBI IRQ. Users should be aware of the
// backend algorithm and when execution happens in IBI IRQ, they should just
// read the mandatory data byte out through the handler and perform other
// actions in a different thread.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
// Do not register hot-join handler for I3C devices as hot-join handler is
// initiator specific, not for a single device.
//
// Returns:
// OK - The interrupt handler was configured.
// INVALID_ARGUMENT - The handler is empty, or the handler is for IBI but
// the device is an I3C device.
// Other status codes as defined by the backend.
Status SetInterruptHandler(InterruptHandler&& handler);
// Clears the interrupt handler.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Returns:
// OK - The interrupt handler was cleared
// INVALID_ARGUMENT - the handler is for IBI but the device is an I3C device.
// Other status codes as defined by the backend.
Status ClearInterruptHandler();
// Enables interrupts which will trigger the interrupt handler.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Preconditions:
// A handler has been set using `SetInterruptHandler()`.
//
// Returns:
// OK - The interrupt handler was enabled.
// INVALID_ARGUMENT - the handler is for IBI but the device is an I3C device.
// Other status codes as defined by the backend.
Status EnableInterruptHandler();
// Disables interrupts which will trigger the interrupt handler.
//
// Warning:
// This method is not thread-safe and cannot be used in interrupt handlers.
//
// Preconditions:
// A handler has been set using `SetInterruptHandler()`.
//
// Returns:
// OK - The interrupt handler was disabled.
// INVALID_ARGUMENT - the handler is for IBI but the device is an I3C device.
// Other status codes as defined by the backend.
Status DisableInterruptHandler();
// Update device address during run-time actively.
//
// Warning:
// This function is dedicatedly to I3C devices.
void UpdateAddressActively(Address new_address);
// Update device address during run-time Passively.
// initiator_ will be responsible for retrieving the dynamic address and
// substitute the device_address_.
//
// Warning:
// This function is dedicatedly to I3C devices.
//
// Returns:
// OK - Success.
// UNIMPLEMENTED - pid is empty (I²C device).
// NOT_FOUND - initiator_ fails to retrieve dynamic address based on pid_.
Status UpdateAddressPassively();
private:
Initiator& initiator_;
Address device_address_;
std::optional<Pid> pid_;
};
------------
Alternatives
------------
Since I3C is similar to I²C and pw_i3c is similar to pw_i2c, instead of creating
a standalone library, an alternative solution is to combine pw_i3c and pw_i2c,
and providing a single library pw_ixc, or other suitable names. And in this
comprehensive library, I3C-related features could be designed to be optional.
On one hand, this solution could reuse a lot of code and simplify some work in
user level, if users want to abstract usage of I²C and I3C in application.
On the other hand, it also brings churn to existing projects using pw_i2c, and
ambiguity and confusion in the long run (I²C is mature, but I3C is new and
actively improving).
--------------
Open Questions
--------------
1. As mentioned in the description of I3C ``Initiator`` class, the creation of a
fully functional ``Initiator`` would be handled by a different class.
2. Because there are two types of ``Address`` in I3C, the static and dynamic,
the ``pw::i3c::Address`` is not compatible with ``pw::i2c::Address``. So when
users try to create an ``Address`` for an I²C device, they need to carefully
choose the correct class depending on which bus the device is connected to.
This class may cause bigger concern if ``Address`` is needed to be shared
through interface in application code.
But the problem is resolvable by templating ``Address`` in caller code.
Also, we can have a helper function in ``pw::i3c::Address``, which consumes a
``pw::i2c::Addres``s and create a ``pw::i3c::Address``. This helper will be
added and discussed further in a following patch.
3. ``DeviceType`` is embedded into ``Address``. So ``pw::i3c::Initiator`` could
tell the device type (I²C or I3C) based on provided ``Address``. But this is
not necessary because Initiator has performed DAA during initialization so it
should know which addresses have been assigned to I3C devices.
In this case, the only advantage of this design is to help applying address
restriction during creating ``Address`` object. Should address restriction be
taken care of by HAL? Though fewer, I²C has reserved addresses, too, but they
are not checked in ``pw::i2c::Address``.
4. ``RegisterDevice`` for I3C is the same as I²C, in protocol. To read/write
from a register, the ``Initiator`` sends an active address on the bus. Once
acknowledged, it will send the register address followed by data.
So the ideal design is to abstract ``RegisterDevice`` across I²C and I3C, or
maybe even other peripheral buses (e.g. SPI and DSI). However, the underlying
register operation functions are different. It is better to be handled in a
separate SEED.