Skip to content

✨ (minor) v6: Add hal::thread_safe_i2c - #117

Merged
kammce merged 6 commits into
mainfrom
v6-locked-i2c
Oct 27, 2026
Merged

kammce merged 6 commits into
mainfrom
v6-locked-i2c

Conversation

@kammce

@kammce kammce commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Adds hal::thread_safe_i2c, a hal::i2c that holds a hal::basic_lock around each transaction() it forwards to a bus, and hal::i2c_bus_rate(), which computes a bus's clock rate at compile time from the devices on it. It makes one bus safe to use from several threads or contexts: a transaction that starts while another is running waits for it instead of failing with hal::device_or_resource_busy. Platform I2C drivers then don't take a lock themselves, and programs that run in one context don't pay for one (libhal/libhal-arm-mcu#237 drops it from hal::stm32f1::i2c).

// Owns the bus, built in place by the factory: thread_safe_i2c<hal::stm32f1::i2c>
static hal::thread_safe_i2c i2c1(
  [] { return hal::stm32f1::i2c(hal::stm32f1::peripheral::i2c1, clock); },
  rtos_mutex);

// Refers to a bus that lives elsewhere: thread_safe_i2c<>
static hal::stm32f1::i2c bus2(hal::stm32f1::peripheral::i2c2, clock);
static hal::thread_safe_i2c i2c2(bus2, rtos_mutex);

static hal::sensors::mpu6050 imu(i2c1);
  • Owning or referring: as with hal::spi_bus_manager, deduction guides pick the type. A factory gives one that owns the bus, built in place by guaranteed copy elision, so non-movable buses work and nothing else can reach the bus around the lock. A bus object gives one that refers to it.
  • Shared code: the forwarding is in a non-template base, thread_safe_i2c_base, so each bus type adds only its storage.
  • Lifetimes: the referred-to bus, the factory and the lock are [[clang::lifetimebound]], so Clang warns when the object outlives a bus, a lock, or anything the factory captures by reference. It is non-copyable and non-movable.
  • Exceptions: whatever the bus throws propagates, and std::lock_guard releases the lock first.
  • Lock: it should block, such as an RTOS mutex. A program in one context doesn't need thread_safe_i2c at all.

It lives in libhal-util rather than core: unlike spi_bus_manager, which is the only way to turn a raw_spi_bus into spi_channels, it is optional. Platform drivers already implement hal::i2c directly.

An owned bus gets its settings from the factory that builds it, since hal::i2c has no configure() as of libhal 5.0.0-alpha.14 (libhal/libhal#262).

hal::i2c_bus_rate

Every device on a bus sees its clock, so the bus can run no faster than its slowest device, and the rate has to be fixed before the first transaction. Device drivers declare static constexpr hal::hertz max_clock_rate, and the application computes the bus rate from the drivers of every device wired to the bus:

static hal::stm32f1::i2c i2c1(hal::stm32f1::peripheral::i2c1, clock, {
  .clock_rate = hal::i2c_bus_rate<mpu6050, at24c02>(400 * kHz),
});
  • The board limit is required, since pull-up resistors and bus capacitance also limit the rate.
  • The result is the lowest of the board limit and every device's max_clock_rate.
  • A device type that doesn't declare max_clock_rate fails to compile, through the hal::i2c_rated_device concept.
  • The compiler can't check that the list matches the board, so the docs say to list every device on the bus, used or not.

Other changes

  • The test fakes, including test_package's, no longer override driver_configure().
  • The package requires libhal/[>=5.0.0-alpha.14 <6], the first release without hal::i2c::configure(). Removing it moved driver_transaction() to a different vtable slot, so this code can't be built against an earlier release.

Testing

New tests in tests/i2c.test.cpp:

  • thread_safe_i2c, with a lock that records whether it is held when each transaction reaches the bus:
    • a transaction runs with the lock held, and locks and unlocks exactly once;
    • the lock is released when the bus throws;
    • a factory builds a non-movable bus in place, and the deduced type is thread_safe_i2c<that bus>.
  • i2c_bus_rate:
    • it picks the slowest device;
    • it is limited by the board;
    • with no devices, it returns the board limit;
    • the concept rejects a type without max_clock_rate.

All 16 suites pass on the host (macOS, LLVM 23), built against libhal 5.0.0-alpha.14. The package and test_package build for the host and the stm32f103c8. Code size on a target hasn't been measured, since no demo uses thread_safe_i2c yet.

A hal::i2c that holds a hal::basic_lock around each configure() and
transaction() it forwards to a bus, so device drivers in different
contexts take turns on it. Platform I2C drivers no longer need to take a
lock themselves, and programs that run in one context skip it entirely.

Like hal::spi_bus_manager, it either refers to a bus or owns one built
in place by a factory, which keeps every user of the bus behind the lock.
hal::i2c no longer has configure() (libhal/libhal#262), so
hal::locked_i2c forwards transactions only, and the test fakes no longer
override driver_configure(). Requires libhal 5.0.0-alpha.14.

hal::i2c_bus_rate<Devices...>(board_limit) computes a bus's clock rate
at compile time: the lowest of the board's limit and each device
driver's static constexpr max_clock_rate. A device type that doesn't
declare one fails to compile, through the hal::i2c_rated_device concept.
The name now says what the wrapper is for. The class docs explain that it is only as safe as the lock it holds.
@kammce kammce changed the title ✨ (minor) v6: Add hal::locked_i2c ✨ (minor) v6: Add hal::thread_safe_i2c Oct 27, 2026

@kammce kammce left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Lgtm

Clang now warns when a thread_safe_i2c outlives something its bus factory captured by reference, matching hal::spi_bus_manager.
@kammce
kammce merged commit c50f8c2 into main Oct 27, 2026
10 checks passed
@kammce
kammce deleted the v6-locked-i2c branch October 27, 2026 01:28
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.

1 participant