pw_i2c#

pw_i2c contains interfaces and utility functions for using I2C.

Guides#

Configure and read an I2C register device#

The following code example is adapted from Kudzu. See the following files for real pw::i2c::RegisterDevice usage:

// WARNING: Don't rely on any values from this example. Consult your
// datasheet to determine what values make sense for your device.

#include <chrono>
#include <cstddef>
#include <cstdint>

#include "pw_bytes/bit.h"
#include "pw_i2c/address.h"
#include "pw_i2c/register_device.h"
#include "pw_log/log.h"
#include "pw_status/status.h"

using ::pw::Status;
using namespace std::chrono_literals;

// Search for `pi4ioe5v6416` in the Kudzu codebase to see real usage of
// pw::i2c::RegisterDevice
namespace pw::pi4ioe5v6416 {

namespace {

constexpr pw::i2c::Address kAddress = pw::i2c::Address::SevenBit<0x20>();
enum Register : uint32_t {
  InputPort0 = 0x0,
  ConfigPort0 = 0x6,
  PullUpDownEnablePort0 = 0x46,
  PullUpDownSelectionPort0 = 0x48,
};

}  // namespace

// This particular example instantiates `pw::i2c::RegisterDevice`
// as part of a higher-level general "device" interface.
// See ///lib/pi4ioe5v6416/public/pi4ioe5v6416/device.h in Kudzu.
Device::Device(pw::i2c::Initiator& initiator)
    : initiator_(initiator),
      register_device_(initiator,
                       kAddress,
                       endian::little,
                       pw::i2c::RegisterAddressSize::k1Byte) {}

Status Device::Enable() {
  // Set port 0 as inputs for buttons (1=input)
  device_.WriteRegister8(Register::ConfigPort0,
                         0xff,
                         pw::chrono::SystemClock::for_at_least(10ms));
  // Select pullup resistors for button input (1=pullup)
  device_.WriteRegister8(Register::PullUpDownSelectionPort0,
                         0xff,
                         pw::chrono::SystemClock::for_at_least(10ms));
  // Enable pullup/down resistors for button input (1=enable)
  device_.WriteRegister8(Register::PullUpDownEnablePort0,
                         0xff,
                         pw::chrono::SystemClock::for_at_least(10ms));
  return OkStatus();
}

pw::Result<uint8_t> Device::ReadPort0() {
  return device_.ReadRegister8(Register::InputPort0,
                               pw::chrono::SystemClock::for_at_least(10ms));
}

}  // namespace pw::pi4ioe5v6416

Reference#

Note

This reference is incomplete. See //pw_i2c/public/pw_i2c/ for the complete interface.

pw::i2c::Initiator#

class Initiator#

The common, base driver interface for initiating thread-safe transactions with devices on an I2C bus. Other documentation may call this style of interface an I2C “master”, “central”, or “controller”.

Initiator isn’t required to support 10-bit addressing. If only 7-bit addressing is supported, Initiator asserts when given an address that is out of 7-bit address range.

The implementer of this pure virtual interface is responsible for ensuring thread safety and enabling functionality such as initialization, configuration, enabling/disabling, unsticking SDA, and detecting device address registration collisions.

Note

Initiator uses internal synchronization, so it’s safe to initiate transactions from multiple threads. However, write+read transactions may not be atomic with multiple controllers on the bus. Furthermore, devices may require specific sequences of transactions, and application logic must provide the synchronization to execute these sequences correctly.

Subclassed by pw::i2c::LinuxInitiator

pw::i2c::Device#

The common interface for interfacing with generic I2C devices. This object contains pw::i2c::Address and wraps the pw::i2c::Initiator API. Common use case includes streaming arbitrary data (Read/Write). Only works with devices with a single device address.

Note

Device is intended to represent ownership of a specific responder. Individual transactions are atomic (as described under Initiator), but there is no synchronization for sequences of transactions. Therefore, shared access should be faciliated with higher level application abstractions. To help enforce this, the Device object is only movable and not copyable.

pw::i2c::RegisterDevice#

See Configure and read an I2C register device for example usage of pw::i2c::RegisterDevice.

class RegisterDevice : public Device#

The common interface for I2C register devices. Contains methods to help read and write the device’s registers.

Warning

This interface assumes that you know how to consult your device’s datasheet to determine correct address sizes, data sizes, endianness, etc.

Public Functions

inline constexpr RegisterDevice(
Initiator &initiator,
Address address,
endian register_address_order,
endian data_order,
RegisterAddressSize register_address_size,
)#

This constructor specifies the endianness of the register address and data separately. If your register address and data have the same endianness and you’d like to specify them both with a single argument, see the other pw::i2c::RegisterDevice constructor.

Parameters:
  • initiator[in] A pw::i2c::Initiator instance for the bus that the device is on.

  • address[in] The address of the I2C device.

  • register_address_order[in] The endianness of the register address.

  • data_order[in] The endianness of the data.

  • register_address_size[in] The size of the register address.

inline constexpr RegisterDevice(Initiator &initiator, Address address, endian order, RegisterAddressSize register_address_size)#

This constructor specifies the endianness of the register address and data with a single argument. If your register address and data have different endianness, use the other pw::i2c::RegisterDevice constructor.

Parameters:
  • initiator[in] A pw::i2c::Initiator instance for the bus that the device is on.

  • address[in] The address of the I2C device.

  • order[in] The endianness of both the register address and register data.

  • register_address_size[in] The size of the register address.

inline Status WriteRegisters(
uint32_t register_address,
ConstByteSpan register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout,
)#

Writes data to multiple contiguous registers starting at a specific register. This method is byte-addressable.

register_address and register_data use the endianness that was provided when this pw::i2c::RegisterDevice instance was constructed.

Parameters:
  • register_address[in] The register address to begin writing at.

  • register_data[in] The data to write. Endianness is taken into account if the data is 2 or 4 bytes.

  • buffer[in] A buffer for constructing the write data. The size of this buffer must be at least as large as the size of register_address plus the size of register_data.

  • timeout[in] The maximum duration to block waiting for both exclusive bus access and the completion of the I2C transaction.

Pre:

This method assumes that you’ve verified that your device supports bulk writes and that register_data is a correct size for your device.

Returns:

A pw::Status object with one of the following statuses:

  • OK - The bulk write was successful.

  • DEADLINE_EXCEEDED - Unable to acquire exclusive bus access and complete the transaction in time.

  • FAILED_PRECONDITION - The interface is not initialized or enabled.

  • INTERNAL - An issue occurred while building register_data.

  • INVALID_ARGUMENT - register_address is larger than the 10-bit address space.

  • OUT_OF_RANGE - The size of buffer is less than the size of register_address plus the size of register_data.

  • UNAVAILABLE - The device took too long to respond to the NACK.

inline Status WriteRegisters8(
uint32_t register_address,
span<const uint8_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout,
)#

Variant of pw::i2c::RegisterDevice::WriteRegisters() that requires register_data to be exactly 8 bits.

inline Status WriteRegisters16(
uint32_t register_address,
span<const uint16_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout,
)#

Variant of pw::i2c::RegisterDevice::WriteRegisters() that requires register_data to be exactly 16 bits.

inline Status WriteRegisters32(
uint32_t register_address,
span<const uint32_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout,
)#

Variant of pw::i2c::RegisterDevice::WriteRegisters() that requires register_data to be exactly 32 bits.

Status ReadRegisters(uint32_t register_address, ByteSpan return_data, chrono::SystemClock::duration timeout)#

Reads data from multiple contiguous registers starting from a specific offset or register. This method is byte-addressable.

register_address and return_data use the endianness that was provided when this pw::i2c::RegisterDevice instance was constructed.

Parameters:
  • register_address[in] The register address to begin reading at.

  • return_data[out] The area to read the data into. The amount of data that will be read is equal to the size of this span. Endianness is taken into account if this span is 2 or 4 bytes.

  • timeout[in] The maximum duration to block waiting for both exclusive bus access and the completion of the I2C transaction.

Pre:

This method assumes that you’ve verified that your device supports bulk reads and that return_data is a correct size for your device.

Returns:

A pw::Status object with one of the following statuses:

  • OK - The bulk read was successful.

  • DEADLINE_EXCEEDED - Unable to acquire exclusive bus access and complete the transaction in time.

  • FAILED_PRECONDITION - The interface is not initialized or enabled.

  • INTERNAL - An issue occurred while building return_data.

  • INVALID_ARGUMENT - register_address is larger than the 10-bit address space.

  • UNAVAILABLE - The device took too long to respond to the NACK.

inline Status ReadRegisters8(uint32_t register_address, span<uint8_t> return_data, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::ReadRegisters() that requires return_data to be exactly 8 bits.

inline Status ReadRegisters16(uint32_t register_address, span<uint16_t> return_data, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::ReadRegisters() that requires return_data to be exactly 16 bits.

inline Status ReadRegisters32(uint32_t register_address, span<uint32_t> return_data, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::ReadRegisters() that requires return_data to be exactly 32 bits.

inline Status WriteRegister(uint32_t register_address, std::byte register_data, chrono::SystemClock::duration timeout)#

Sends a register address to write to and then writes to that address.

register_address and register_data use the endianness that was provided when this pw::i2c::RegisterDevice instance was constructed.

Parameters:
  • register_address[in] The register address to write to.

  • register_data[in] The data that should be written at the address. The maximum allowed size is 4 bytes.

  • timeout[in] The maximum duration to block waiting for both exclusive bus access and the completion of the I2C transaction.

Pre:

This method assumes that you’ve verified that register_data is a correct size for your device.

Returns:

A pw::Status object with one of the following statuses:

  • OK - The write was successful.

  • DEADLINE_EXCEEDED - Unable to acquire exclusive bus access and complete the transaction in time.

  • FAILED_PRECONDITION - The interface is not initialized or enabled.

  • INTERNAL - An issue occurred while writing the data.

  • INVALID_ARGUMENT - register_address is larger than the 10-bit address space.

  • UNAVAILABLE - The device took too long to respond to the NACK.

inline Status WriteRegister8(uint32_t register_address, uint8_t register_data, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::WriteRegister() that writes exactly 8 bits.

inline Status WriteRegister16(uint32_t register_address, uint16_t register_data, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevince::WriteRegister() that writes exactly 16 bits.

inline Status WriteRegister32(uint32_t register_address, uint32_t register_data, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::WriteRegister() that writes exactly 32 bits.

inline Result<std::byte> ReadRegister(uint32_t register_address, chrono::SystemClock::duration timeout)#

Sends a register address to read from and then reads from that address.

register_address and the return data use the endianness that was provided when this pw::i2c::RegisterDevice instance was constructed.

Parameters:
  • register_address[in] The register address to read.

  • timeout[in] The maximum duration to block waiting for both exclusive bus access and the completion of the I2C transaction.

Pre:

This method assumes that you’ve verified that the return data size is a correct size for your device.

Returns:

On success, a pw::Result object with a value representing the register data and a status of OK . On error, a pw::Result object with no value and one of the following statuses:

  • DEADLINE_EXCEEDED - Unable to acquire exclusive bus access and complete the transaction in time.

  • FAILED_PRECONDITION - The interface is not initialized or enabled.

  • INTERNAL - An issue occurred while building the return data.

  • INVALID_ARGUMENT - register_address is larger than the 10-bit address space.

  • UNAVAILABLE - The device took too long to respond to the NACK.

inline Result<uint8_t> ReadRegister8(uint32_t register_address, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::ReadRegister() that returns exactly 8 bits.

inline Result<uint16_t> ReadRegister16(uint32_t register_address, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::ReadRegister() that returns exactly 16 bits.

inline Result<uint32_t> ReadRegister32(uint32_t register_address, chrono::SystemClock::duration timeout)#

Variant of pw::i2c::RegisterDevice::ReadRegister() that returns exactly 32 bits.

pw::i2c::MockInitiator#

A generic mocked backend for for pw::i2c::Initiator. This is specifically intended for use when developing drivers for i2c devices. This is structured around a set of ‘transactions’ where each transaction contains a write, read and a timeout. A transaction list can then be passed to the MockInitiator, where each consecutive call to read/write will iterate to the next transaction in the list. An example of this is shown below:

using pw::i2c::Address;
using pw::i2c::MakeExpectedTransactionArray;
using pw::i2c::MockInitiator;
using pw::i2c::WriteTransaction;
using std::literals::chrono_literals::ms;

constexpr Address kAddress1 = Address::SevenBit<0x01>();
constexpr auto kExpectWrite1 = pw::bytes::Array<1, 2, 3, 4, 5>();
constexpr auto kExpectWrite2 = pw::bytes::Array<3, 4, 5>();
auto expected_transactions = MakeExpectedTransactionArray(
    {ProbeTransaction(pw::OkStatus, kAddress1, 2ms),
     WriteTransaction(pw::OkStatus(), kAddress1, kExpectWrite1, 1ms),
     WriteTransaction(pw::OkStatus(), kAddress2, kExpectWrite2, 1ms)});
MockInitiator i2c_mock(expected_transactions);

// Begin driver code
Status status = i2c_mock.ProbeDeviceFor(kAddress1, 2ms);

ConstByteSpan write1 = kExpectWrite1;
// write1 is ok as i2c_mock expects {1, 2, 3, 4, 5} == {1, 2, 3, 4, 5}
Status status = i2c_mock.WriteFor(kAddress1, write1, 2ms);

// Takes the first two bytes from the expected array to build a mismatching
// span to write.
ConstByteSpan write2 = pw::span(kExpectWrite2).first(2);
// write2 fails as i2c_mock expects {3, 4, 5} != {3, 4}
status = i2c_mock.WriteFor(kAddress2, write2, 2ms);
// End driver code

// Optionally check if the mocked transaction list has been exhausted.
// Alternatively this is also called from MockInitiator::~MockInitiator().
EXPECT_EQ(mocked_i2c.Finalize(), OkStatus());

pw::i2c::GmockInitiator#

gMock of Initiator used for testing and mocking out the Initiator.

I2C debug service#

This module implements an I2C register access service for debugging and bringup. To use, provide it with a callback function that returns an Initiator for the specified bus_index.

Example invocations#

Using the pigweed console, you can invoke the service to perform an I2C read:

device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=0x22, register_address=b'\x0e', read_size=1)

The above shows reading register 0x0e on a device located at I2C address 0x22.

For responders that support 4 byte register width, you can specify as:

device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=<address>, register_address=b'\x00\x00\x00\x00', read_size=4)

And similarly, for performing I2C write:

device.rpcs.pw.i2c.I2c.I2cWrite(bus_index=0, target_address=0x22,register_address=b'\x0e', value=b'\xbc')

Similarly, multi-byte writes can also be specified with the bytes fields for register_address and value.

I2C responders that require multi-byte access may expect a specific endianness. The order of bytes specified in the bytes field will match the order of bytes sent/received on the bus. Maximum supported value for multi-byte access is 4 bytes.