Skip to content

refactor(driver): centralize I2C locking and migrate polling to RTOS timers#7230

Open
raphaelcoeffic wants to merge 15 commits into
mainfrom
i2c-conflicts
Open

refactor(driver): centralize I2C locking and migrate polling to RTOS timers#7230
raphaelcoeffic wants to merge 15 commits into
mainfrom
i2c-conflicts

Conversation

@raphaelcoeffic

@raphaelcoeffic raphaelcoeffic commented Mar 29, 2026

Copy link
Copy Markdown
Member

Summary

  • I2C locking moved to HAL layer — new i2c_lock/i2c_trylock/i2c_unlock API in hal/i2c_driver.h; mutex removed from STM32 driver, callers lock explicitly
  • I2C bus recovery — SCL bit-bang per ST AN2824 with auto-retry on transfer failure
  • All I2C callers updated — timer-context uses i2c_trylock (non-blocking), task-context uses i2c_lock, pre-scheduler skips locking
  • IMU polling migrated from mixer task to dedicated RTOS timer with i2c_trylock
  • GX12 switch polling migrated from synchronous mixer-task to async/timer pattern
  • CSD203 power sensor ported to ETX HAL (i2c_read/i2c_write), moved from targets/common/arm/stm32/ to drivers/, polling moved to own RTOS timer
  • Removed obsolete IICReadStatusFlag bus-preemption workaround from CSD203 and GT911 touch driver — the I2C mutex handles bus sharing
  • Removed unused MutexLock RAII wrapper from os/task.h
  • Fixed latent deadlock in 2-arg stm32_i2c_is_dev_ready overload
  • I2C scan via CLI command
  • imuDetect/gyroStart simplifiedimuDetect() returns the matched etx_imu_t* instead of a bare read fn plus a separate &bus out-param; gyroStart() takes that single handle, dropping the redundant bus plumbing at every board
  • PA01 I2C recovery consolidated — dropped the local _track_i2c_error/_recover_i2c path from pa01/bsp_io.cpp; the AW9523B expander goes through the HAL transfer layer, which already does AN2824 recovery + retry. Rebased on top of fix(pa01): optimize key matrix polling with split-cycle scanning #7405 (PA01 split-cycle key scanning); the timer-driven pollKeys() now takes i2c_trylock and falls back to the cached columns when the bus is busy

Test plan

  • Build and flash V12, V14, V16 (CSD203 targets) — verify battery voltage reads correctly
  • Build and flash TX16S, Boxer (non-CSD203 targets) — verify no regressions
  • Verify GX12 switches still work properly
  • On shared-bus targets (ST16, TX15, X12S): verify touch + IMU work without I2C contention
  • Verify IMU/gyro readings are stable after timer migration
  • Build and flash PA01 — verify keys/trims, function-switch keys, and IMU/gyro work (shared AW9523B + IMU on I2C bus 1)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an i2c scan <bus> CLI command to probe and list responding devices.
    • Added CSD203-based power sensing for battery/voltage reporting on targets with CSD203 enabled.
  • Bug Fixes
    • Improved I2C bus coordination across touch input, switch polling/expanders, volume updates, gyro sampling, EEPROM access, and battery monitoring using consistent locking/try-locking.
    • Enhanced I2C reliability by adding bus recovery for failed transfers (including stuck-bus handling) with safer retry behavior.

@richardclli

Copy link
Copy Markdown
Member

I think you need to include PA01 in your test plan as well. I have already implemented a few things in PA01 that maybe similar to what you did. The key driver always returns a cached value and key polling is triggered by 10ms timer. So see how the refactorization works for all devices.

raphaelcoeffic and others added 10 commits June 28, 2026 10:06
Add centralized I2C bus recovery via SCL bit-banging (per ST AN2824) to
handle the well-known STM32F4 SDA-stuck-low condition. All I2C transfer
functions now automatically attempt recovery + retry on failure.

Migrate GX12 PCA9555 switch polling from synchronous mixer-task calls to
the async/timer pattern (matching rm-h750), eliminating I2C bus access
from the mixer task entirely. Add 3-level error recovery to the GX12
IO expander reads (PCA reset -> I2C bus recover -> give up).

Fix GX12 EXTI config: PE14 (PCA9555 INT) needs EXTI15_10, not EXTI1.
Fix latent deadlock in 2-arg stm32_i2c_is_dev_ready overload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move per-bus mutex ownership from the STM32 I2C driver to the HAL layer
(i2c_bus.cpp). Transfer functions no longer lock internally — callers
are responsible for explicit locking matching their execution context:

  - i2c_lock/unlock: blocking, for regular task context
  - i2c_trylock/unlock: non-blocking, for timer task context
  - no locking needed before RTOS scheduler starts

This avoids blocking the FreeRTOS timer service task (which must never
block per FreeRTOS documentation) when I2C buses are shared between
timer-polled devices (IMU, IO expanders) and task-polled devices (touch).

Update GX12 bsp_io to use i2c_trylock from timer callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update all I2C device drivers to use the new explicit locking API:

Timer/ISR context (i2c_trylock, skip if busy):
  - GX12 bsp_io (PCA9555 switch polling)
  - rm-h750 bsp_io (PCA9555 switch polling)
  - PA01 key_driver (AW9523B matrix scanning)
  - CSD203 power sensor

Regular task context (i2c_lock/unlock):
  - Touch drivers (GT911, CST8xx, CST340, CST836U, FT6236, CHSC5448)
  - IMU drivers (LSM6DS, ICM42607C, SC7U22)
  - TAS2505 audio codec volume control
  - EEPROM driver
  - Volume I2C driver
  - Seesaw GPIO expander

Init-only drivers (no locking, pre-scheduler):
  - WM8904 audio codec
  - PCA95xx (locking at call site)
  - AW9523B (locking at call site)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move gyro/accelerometer I2C reads from the mixer task to a dedicated
10ms RTOS timer callback. Uses i2c_trylock so the timer service task
never blocks — if the bus is held (e.g. by touch controller in menu
task), the IMU sample is skipped and retried next cycle.

This eliminates I2C bus contention between IMU and touch on targets
where they share a bus (ST16, TX15, Horus X12S).

Changes:
- gyro.cpp: replace gyroWakeup() with timer callback using trylock
- gyroStart() now takes bus parameter, starts the timer
- IMU read functions (lsm6ds, icm42607C, sc7u22): remove internal
  i2c_lock since gyro timer callback handles locking
- imuDetect() returns detected bus via output parameter
- Remove gyroWakeup() from mixer task execMixerFrequentActions()
- Update all board.cpp gyroInit() to pass bus to gyroStart()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The minimal_board_lib native target compiles i2c_bus.cpp without
BOOT or FREE_RTOS defined. Change the preprocessor guard from
!defined(BOOT) to defined(FREE_RTOS) so mutex code is only compiled
when FreeRTOS is actually available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move CSD203 power sensor from targets/common/arm/stm32/ to
drivers/, replacing STM32-specific stm32_i2c_master_tx/rx with
HAL i2c_read/write. Polling migrated from per10ms() to a
dedicated RTOS timer (matching the IMU pattern), and the obsolete
IICReadStatusFlag bus-preemption workaround removed from
tp_gt911.cpp since the I2C mutex now handles bus sharing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gyro.cpp now calls i2c_trylock/i2c_unlock directly, which are
only provided by the STM32 i2c_bus.cpp. Add no-op stubs for
all i2c_driver.h functions in the simu target so native builds
(simulator and unit tests) link correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
imuDetect() now returns the matched etx_imu_t* (a stable static copy)
instead of a bare read fn plus a separate &bus out-param, and gyroStart()
takes that single handle. This removes the redundant bus plumbing through
the call sites and drops the etx_i2c_bus_t boilerplate at each board.

Also drops the s_detected_valid flag, using s_detected.driver as the
detection indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 02452051-b6ab-42d6-bde1-592dfe0cdb51

📥 Commits

Reviewing files that changed from the base of the PR and between df6cf73 and a2453f7.

📒 Files selected for processing (1)
  • radio/src/pdm_wav_recorder.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • radio/src/pdm_wav_recorder.cpp

📝 Walkthrough

Walkthrough

Adds I2C locking, bus recovery, and related driver updates across the HAL, IMU, CSD203, GX12 BSP I/O, touch, EEPROM, volume, CLI, and peripheral code paths. It also changes gyro sampling to a timer callback and refactors PDM WAV recorder locking.

Changes

I2C synchronization and driver updates

Layer / File(s) Summary
I2C lock API and backend plumbing
radio/src/hal/i2c_driver.h, radio/src/boards/generic_stm32/i2c_bus.cpp, radio/src/targets/common/arm/stm32/stm32_i2c_driver.cpp, radio/src/os/task.h, radio/src/targets/simu/CMakeLists.txt, radio/src/targets/simu/i2c_driver.cpp
The HAL now declares I2C lock, trylock, and unlock functions; the generic STM32 bus implementation provides FreeRTOS-backed per-bus mutex state; the STM32 driver removes internal mutex storage; the simulator adds stub I2C functions; and the task mutex RAII helper is removed.
STM32 I2C recovery and retry flow
radio/src/targets/common/arm/stm32/stm32_i2c_driver.cpp, radio/src/targets/common/arm/stm32/stm32_i2c_driver.h
The STM32 I2C driver adds peripheral reinitialization, SCL/SDA bit-bang recovery, a public bus-recover API, retry-on-failure for master transfers, and updated device-ready behavior without internal mutex locking.
IMU detection and gyro timer polling
radio/src/hal/imu.h, radio/src/hal/imu.cpp, radio/src/gyro.h, radio/src/gyro.cpp, radio/src/tasks/mixer_task.cpp
IMU detection now returns the detected IMU instance, gyro startup takes that instance, gyro sampling runs from a repeating timer with I2C trylock protection, and the mixer task no longer calls the removed wakeup entry point.
CSD203 driver and board wiring
radio/src/drivers/csd203.h, radio/src/drivers/csd203.cpp, radio/src/targets/common/arm/stm32/CMakeLists.txt, radio/src/edgetx.cpp, radio/src/targets/horus/board.cpp, radio/src/targets/taranis/board.cpp
The CSD203 support is replaced with an instance-based driver, timer polling, and board startup wiring that points boards at csd203_start(I2C_Bus_1) and removes the legacy per-10ms sensor read path.
GX12 BSP polling and interrupt changes
radio/src/targets/taranis/gx12/bsp_io.h, radio/src/targets/taranis/gx12/bsp_io.cpp, radio/src/targets/taranis/hal.h, radio/src/mixer.cpp, radio/src/switches.cpp, radio/src/targets/simu/switch_driver.cpp
The GX12 BSP I/O path switches to timer-based polling with async interrupt scheduling, I2C recovery on repeated expander failures, updated init entry points, and a new EXTI15_10 interrupt configuration; GX12 switch polling call sites and simulator stubs are removed.
Touch, peripheral, and CLI I2C locking
radio/src/boards/*/touch_driver.cpp, radio/src/boards/rm-h750/bsp_io.cpp, radio/src/targets/*/touch_driver.cpp, radio/src/targets/*/tp_gt911.cpp, radio/src/targets/st16/tp_cst340.cpp, radio/src/targets/stm32h7s78-dk/seesaw.cpp, radio/src/hal/eeprom/eeprom_driver.cpp, radio/src/targets/taranis/volume_i2c.cpp, radio/src/drivers/tas2505.cpp, radio/src/targets/pa01/key_driver.cpp, radio/src/cli.cpp, radio/src/targets/horus/cst8xx_driver.cpp
Touch drivers on multiple boards now lock the shared touch bus around controller reads and unlock on normal and error paths; the rm-h750 expander poll uses trylock and unlock around reads; EEPROM, volume, TAS2505, PA01 key, CLI scan, and Seesaw helpers bracket their I2C transactions with bus locks.
PA01 BSP I2C error handling cleanup
radio/src/targets/pa01/bsp_io.cpp
The PA01 BSP I2C helpers drop local error tracking and recovery logic, remove debug and delay includes, and call the AW9523B read and write functions directly.

PDM WAV recorder locking refactor

Layer / File(s) Summary
Recorder state and helper declarations
radio/src/pdm_wav_recorder.h, radio/src/pdm_wav_recorder.cpp
The recorder header adds task mutex types, class static coordination state, and a locked start helper; the source moves the shared mutex state into class statics.
Recorder start, tick, and stop locking
radio/src/pdm_wav_recorder.cpp
Start now locks around setup, audioTick uses trylock before tick work, and stop uses explicit lock management with the close result returned after unlocking.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • EdgeTX/edgetx#7328: Shares the PdmWavRecorder locking and runtime-recording code path.
  • EdgeTX/edgetx#7405: Directly overlaps with radio/src/targets/pa01/key_driver.cpp, where both PRs change pollKeys() I2C handling.

Suggested labels

house keeping 🧹

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: centralized I2C locking and timer-based polling.
Description check ✅ Passed The description matches the template well with a clear summary and test plan; only an issue reference is omitted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch i2c-conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The aw9523b expander accesses go through the HAL i2c_read/i2c_write
transfer layer, which already performs SCL bit-bang bus recovery
(ST AN2824) plus a retry on every failed transfer. The local
_track_i2c_error/_recover_i2c path (peripheral deinit/init after 3
consecutive errors) is strictly weaker and only triggers after the
central recovery has already failed, so remove it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
radio/src/targets/taranis/gx12/bsp_io.cpp (1)

154-166: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Enable the GPIO interrupt after the poll timer is created.

gpio_init_int() can allow _io_int_handler() to queue _poll_switches() before start_poll_timer() runs; the IRQ path then calls timer_reset(&_poll_timer), which is just timer_start(), on an uncreated timer. Move interrupt registration after initial read/timer creation.

Proposed fix
-  // setup expanders pin change interrupt
-  gpio_init_int(IO_INT_GPIO, GPIO_IN, GPIO_FALLING, _io_int_handler);
-
   // setup expanders reset pin
   gpio_init(IO_RESET_GPIO, GPIO_OUT, GPIO_PIN_SPEED_LOW);
   gpio_set(IO_RESET_GPIO);
@@
 `#if` !defined(BOOT)
   start_poll_timer();
 `#endif`
+
+  // setup expanders pin change interrupt
+  gpio_init_int(IO_INT_GPIO, GPIO_IN, GPIO_FALLING, _io_int_handler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/targets/taranis/gx12/bsp_io.cpp` around lines 154 - 166, Move the
GPIO interrupt setup in bsp_io.cpp so that gpio_init_int() for IO_INT_GPIO
happens only after the initial expander reads and after start_poll_timer() has
created _poll_timer; _io_int_handler() can otherwise trigger _poll_switches()
and call timer_reset() before the timer exists. Keep the existing initialization
flow in this area, but reorder the interrupt registration in the startup path
for the Taranis GX12 I/O expander so the IRQ cannot fire until the polling timer
is ready.
radio/src/hal/eeprom/eeprom_driver.cpp (1)

141-158: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Avoid holding the bus mutex across the unbounded EEPROM wait loops.

eepromPageWrite() and eepromWaitEepromStandbyState() already spin until success. With the new outer i2c_lock(), one unrecoverable EEPROM failure now blocks every other user of EEPROM_I2C_BUS indefinitely instead of only stalling this caller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/hal/eeprom/eeprom_driver.cpp` around lines 141 - 158, The EEPROM
write path is holding EEPROM_I2C_BUS locked across the unbounded retry/wait
sequence in the page write loop, which can block all other bus users if the
device never recovers. Update the logic around eepromPageWrite() and
eepromWaitEepromStandbyState() so the bus mutex is only held for the actual I2C
transaction and released before any spin/wait retry behavior, keeping the lock
scope minimal while preserving the existing write flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/boards/generic_stm32/i2c_bus.cpp`:
- Around line 118-120: The mutex access in the I2C bus helpers is indexing
_i2c_mutex and _i2c_mutex_initialized without validating the bus value at use
sites. Update the relevant logic in i2c_ensure_mutex, the scheduler_is_running()
lock path, and i2c_unlock() to guard the bus index before any array access,
matching the protection already needed in i2c_unlock. Keep the validation local
to these helper functions so unexpected bus values cannot walk past the mutex
arrays.

In `@radio/src/cli.cpp`:
- Around line 1853-1855: The i2c CLI help text and command registration are out
of sync with the actual handler behavior in cli.cpp: the implementation under
the i2c subcommand only supports scan, while the advertised entries still
include lockup and recover. Update the command table and any usage/help strings
in the i2c handler so they list only the subcommands that are actually
implemented, or add the missing subcommand handlers if they are intended to be
supported, keeping the help text, registration, and dispatch logic consistent.
- Around line 1859-1869: The i2c scan command in cli.cpp accepts user-provided
bus input and uses it directly in i2c_lock(bus) and i2c_dev_ready(bus, addr), so
add validation in the i2c scan handling path before any HAL call. In the command
block that parses bus with toInt and prints “Scanning I2C bus...”, reject
negative values and any bus outside the supported range, return the usage/error
path, and only proceed to locking and probing when bus is valid.

In `@radio/src/drivers/csd203.cpp`:
- Around line 160-172: The startup init sequence in csd203_start() calls
csd203_init() three times after i2c_init(), but those I2C transfers are not
protected by the HAL lock. Wrap the entire
csd203_main/csd203_internal/csd203_external initialization block with the same
bus lock used for timer polling, or move locking into csd203_init() so the
locking model is consistent for all I2C accesses.
- Around line 66-79: csd203_init() computes the calibration value using
current_lsb * rshunt, but that denominator can be zero for valid public inputs
and crash the init path. Add an early validation check in csd203_init before
calculating cal, and return a failure code if current_lsb or rshunt makes the
denominator zero; keep the check close to the existing config/calibration setup
so the failure is handled before calling csd203_write_reg().
- Around line 171-184: Prime the external battery-voltage cache during
csd203_start() so getBatteryVoltage() does not return a false 0V before timer
polling begins. After csd203_init() for csd203_external succeeds, take one
immediate external voltage reading and store it in csd203_ext_vbus, reusing the
same sensor access path used by csd203TimerCb. Keep the async timer start
behavior intact, but ensure the cached value is populated on initialization for
immediate board-init reads.

In `@radio/src/drivers/csd203.h`:
- Around line 35-36: The csd203_read_voltage and csd203_read_current APIs
currently return the sampled value directly, which hides I2C failures when they
fall back to zero. Update the csd203_t driver interface to return a status code
and move the measured voltage/current into an output parameter, then adjust the
implementations and all call sites accordingly so callers can distinguish read
errors from legitimate zero readings.

In `@radio/src/gyro.cpp`:
- Around line 33-34: The complementary filter in gyroFilter() is using a fixed
SAMPLE_TIME_S, which causes under-integration when i2c_trylock() skips reads or
timing jitters. Update the gyro sampling flow to track the timestamp of the last
successful sample, compute the real elapsed dt for each accepted update, and
pass that dt into gyroFilter() instead of relying on the constant SAMPLE_TIME_S.
Use the existing gyroFilter, i2c_trylock, and GYRO_POLL_PERIOD_MS symbols to
locate the sample loop and filter integration path.
- Around line 115-127: The gyro polling logic in the callback is permanently
disabling itself once the error counter reaches 100, so transient I2C failures
can latch the sensor offline forever. Update the error handling around the read
path in the gyro callback to use a capped retry/backoff strategy instead of an
unconditional early return on `errors >= 100`, and make sure the
`readFn`/`i2c_trylock` path still periodically probes after failures so recovery
is possible.
- Around line 111-123: Move the IMU acquisition out of gyroTimerCb() so the
FreeRTOS timer service task never performs blocking I2C work. Keep gyroTimerCb()
limited to lightweight signaling, then have the actual
readFn()/i2c_trylock()/i2c_unlock() sequence run in a dedicated task or worker
associated with the gyro path. Use the existing gyroTimerCb and readFn symbols
to locate the callback and preserve the current error
handling/usbPluggedInStorageMode gating in the new execution path.

In `@radio/src/hal/eeprom/eeprom_driver.cpp`:
- Around line 96-100: The retry loop in eeprom_driver::I2C_EE_ReadBlock handling
currently unlocks EEPROM_I2C_BUS before calling eepromInit(), which lets other
tasks use the shared I2C peripheral during reinitialization. Keep the bus lock
held across the reinit path in this loop, and only release it after the retry
sequence is complete so the caller-owned locking contract is preserved.

In `@radio/src/hal/i2c_driver.h`:
- Around line 31-35: The current `i2c_trylock()` contract is too broad because
the FreeRTOS implementation behind `mutex_trylock()` uses `xSemaphoreTake()`,
which is not safe from a real ISR. Update the API documentation/comments in
`i2c_driver.h` to restrict `i2c_trylock()` to task or timer-callback context
only, and keep `i2c_lock()` as the blocking task-context path. If true ISR usage
is needed, add a separate `FromISR`-based path rather than reusing
`i2c_trylock()`.

In `@radio/src/targets/common/arm/stm32/stm32_i2c_driver.cpp`:
- Around line 628-643: The retry logic in stm32_i2c_master_tx (and the related
stm32_i2c_write path) is blindly replaying write-like transfers after a partial
HAL failure. Remove the automatic second HAL_I2C_Master_Transmit attempt after
i2c_bus_recover, and keep only the recovery/reset path so the caller can decide
whether to retry a potentially non-idempotent write. Make the change in the
stm32_i2c_master_tx flow and apply the same fix to the matching write helper
referenced by the review.

---

Outside diff comments:
In `@radio/src/hal/eeprom/eeprom_driver.cpp`:
- Around line 141-158: The EEPROM write path is holding EEPROM_I2C_BUS locked
across the unbounded retry/wait sequence in the page write loop, which can block
all other bus users if the device never recovers. Update the logic around
eepromPageWrite() and eepromWaitEepromStandbyState() so the bus mutex is only
held for the actual I2C transaction and released before any spin/wait retry
behavior, keeping the lock scope minimal while preserving the existing write
flow.

In `@radio/src/targets/taranis/gx12/bsp_io.cpp`:
- Around line 154-166: Move the GPIO interrupt setup in bsp_io.cpp so that
gpio_init_int() for IO_INT_GPIO happens only after the initial expander reads
and after start_poll_timer() has created _poll_timer; _io_int_handler() can
otherwise trigger _poll_switches() and call timer_reset() before the timer
exists. Keep the existing initialization flow in this area, but reorder the
interrupt registration in the startup path for the Taranis GX12 I/O expander so
the IRQ cannot fire until the polling timer is ready.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 687dbdf7-08d4-435a-a32c-da76442c13aa

📥 Commits

Reviewing files that changed from the base of the PR and between eae6aa1 and ea018cc.

📒 Files selected for processing (40)
  • radio/src/boards/generic_stm32/i2c_bus.cpp
  • radio/src/boards/jumper-h750/touch_driver.cpp
  • radio/src/boards/rm-h750/bsp_io.cpp
  • radio/src/boards/rm-h750/tp_gt911.cpp
  • radio/src/cli.cpp
  • radio/src/drivers/csd203.cpp
  • radio/src/drivers/csd203.h
  • radio/src/drivers/tas2505.cpp
  • radio/src/edgetx.cpp
  • radio/src/gyro.cpp
  • radio/src/gyro.h
  • radio/src/hal/eeprom/eeprom_driver.cpp
  • radio/src/hal/i2c_driver.h
  • radio/src/hal/imu.cpp
  • radio/src/hal/imu.h
  • radio/src/mixer.cpp
  • radio/src/os/task.h
  • radio/src/switches.cpp
  • radio/src/targets/common/arm/stm32/CMakeLists.txt
  • radio/src/targets/common/arm/stm32/csd203_sensor.cpp
  • radio/src/targets/common/arm/stm32/stm32_i2c_driver.cpp
  • radio/src/targets/common/arm/stm32/stm32_i2c_driver.h
  • radio/src/targets/horus/board.cpp
  • radio/src/targets/horus/cst8xx_driver.cpp
  • radio/src/targets/horus/tp_gt911.cpp
  • radio/src/targets/pa01/key_driver.cpp
  • radio/src/targets/pl18/touch_driver.cpp
  • radio/src/targets/simu/CMakeLists.txt
  • radio/src/targets/simu/i2c_driver.cpp
  • radio/src/targets/simu/switch_driver.cpp
  • radio/src/targets/st16/touch_driver.cpp
  • radio/src/targets/st16/tp_cst340.cpp
  • radio/src/targets/stm32h7s78-dk/seesaw.cpp
  • radio/src/targets/stm32h7s78-dk/tp_gt911.cpp
  • radio/src/targets/taranis/board.cpp
  • radio/src/targets/taranis/gx12/bsp_io.cpp
  • radio/src/targets/taranis/gx12/bsp_io.h
  • radio/src/targets/taranis/hal.h
  • radio/src/targets/taranis/volume_i2c.cpp
  • radio/src/tasks/mixer_task.cpp
💤 Files with no reviewable changes (7)
  • radio/src/tasks/mixer_task.cpp
  • radio/src/targets/simu/switch_driver.cpp
  • radio/src/switches.cpp
  • radio/src/targets/common/arm/stm32/csd203_sensor.cpp
  • radio/src/mixer.cpp
  • radio/src/edgetx.cpp
  • radio/src/os/task.h

Comment thread radio/src/boards/generic_stm32/i2c_bus.cpp
Comment thread radio/src/cli.cpp
Comment thread radio/src/cli.cpp Outdated
Comment thread radio/src/drivers/csd203.cpp
Comment thread radio/src/drivers/csd203.cpp
Comment thread radio/src/gyro.cpp
Comment thread radio/src/gyro.cpp
Comment thread radio/src/hal/eeprom/eeprom_driver.cpp
Comment thread radio/src/hal/i2c_driver.h
Comment thread radio/src/targets/common/arm/stm32/stm32_i2c_driver.cpp
MutexLock was removed earlier in this branch to make locking explicit and
expose the lock-vs-trylock distinction. The MEMS-microphone feature (#7328)
merged from main reintroduced it via PdmWavRecorder, which would have made
the audio task block on the recorder mutex.

Use explicit mutex calls with the right policy per context:

- audioTick() runs in the high-priority audio task (prio +3). It now uses
  mutex_trylock() and skips the chunk on contention, so it never stalls
  waiting on the lower-priority CLI/GUI task (prio +1) holding the lock
  through slow SD-card I/O in start()/stop().
- start()/stop() run in the owner thread where blocking is fine, so they
  use mutex_lock(). start()'s body moves into startLocked() to keep a
  single unlock point across its early returns.

FatFs reentrancy (FF_FS_REENTRANT) only serializes individual f_* calls;
it does not protect the s_active/file lifecycle, so this lock is still
required. Also moves the shared mutex/s_active statics into the class as
private static members for encapsulation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
radio/src/pdm_wav_recorder.cpp (2)

160-170: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate header patch failures.

stop() currently returns only f_close(). If either WAV header seek/write fails but close succeeds, callers see FR_OK for a corrupted header.

Suggested change
+  FRESULT res = FR_OK;
+
   writeLE32(buf, 36U + dataBytes);
-  f_lseek(&file, 4);
-  f_write(&file, buf, 4, &written);
+  res = f_lseek(&file, 4);
+  if (res == FR_OK) {
+    written = 0;
+    res = f_write(&file, buf, 4, &written);
+    if (res == FR_OK && written != 4) res = FR_DISK_ERR;
+  }
 
   writeLE32(buf, dataBytes);
-  f_lseek(&file, 40);
-  f_write(&file, buf, 4, &written);
+  if (res == FR_OK) {
+    res = f_lseek(&file, 40);
+  }
+  if (res == FR_OK) {
+    written = 0;
+    res = f_write(&file, buf, 4, &written);
+    if (res == FR_OK && written != 4) res = FR_DISK_ERR;
+  }
 
-  FRESULT res = f_close(&file);
+  FRESULT closeRes = f_close(&file);
   mutex_unlock(&s_mutex);
-  return res;
+  return res != FR_OK ? res : closeRes;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/pdm_wav_recorder.cpp` around lines 160 - 170, The stop() path in
PdmWavRecorder currently ignores failures from the WAV header patching steps and
only returns the result of f_close(), so propagate errors from the f_lseek and
f_write calls as well. Update the logic around the header update in the
recorder’s stop/close flow to capture each FatFs result, preserve the first
failure if any, and return that instead of blindly returning f_close() so
callers can detect a corrupted header.

32-38: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make mutex initialization thread-safe.

s_mutexInited is read/written without synchronization, so concurrent first use can create s_mutex twice. Initialize it from a single-threaded startup path or use a real one-time/critical-section guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/pdm_wav_recorder.cpp` around lines 32 - 38, Make the mutex setup in
PdmWavRecorder::ensureMutex thread-safe: the current s_mutexInited check/set can
race and create s_mutex more than once on concurrent first use. Replace the ad
hoc boolean guard with a real one-time initialization mechanism or move mutex
creation into a guaranteed single-threaded startup path, and ensure all callers
of ensureMutex() rely on that safe initialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/pdm_wav_recorder.cpp`:
- Around line 133-138: The null check on s_active in pdm_wav_recorder’s tick
path is racing with start()/stop() because it reads the shared pointer before
s_mutex is held. In the same block that calls ensureMutex() and mutex_trylock(),
move the s_active check to after a successful lock acquisition, then call
s_active->tickLocked() only while the mutex is held.

---

Outside diff comments:
In `@radio/src/pdm_wav_recorder.cpp`:
- Around line 160-170: The stop() path in PdmWavRecorder currently ignores
failures from the WAV header patching steps and only returns the result of
f_close(), so propagate errors from the f_lseek and f_write calls as well.
Update the logic around the header update in the recorder’s stop/close flow to
capture each FatFs result, preserve the first failure if any, and return that
instead of blindly returning f_close() so callers can detect a corrupted header.
- Around line 32-38: Make the mutex setup in PdmWavRecorder::ensureMutex
thread-safe: the current s_mutexInited check/set can race and create s_mutex
more than once on concurrent first use. Replace the ad hoc boolean guard with a
real one-time initialization mechanism or move mutex creation into a guaranteed
single-threaded startup path, and ensure all callers of ensureMutex() rely on
that safe initialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c31838e3-5eba-4680-baa8-27e3a6b72eab

📥 Commits

Reviewing files that changed from the base of the PR and between 588bfc8 and 611ae83.

📒 Files selected for processing (2)
  • radio/src/pdm_wav_recorder.cpp
  • radio/src/pdm_wav_recorder.h

Comment thread radio/src/pdm_wav_recorder.cpp Outdated
raphaelcoeffic and others added 3 commits June 28, 2026 11:06
From the CodeRabbit review of the I2C refactoring:

- i2c_lock/i2c_trylock: guard the bus index against MAX_I2C_BUSES before
  dereferencing the mutex array, matching i2c_unlock. Reachable now that the
  CLI scan command takes a user-supplied bus.
- cli i2c: validate the bus argument before locking/probing, and trim the
  command help to the only implemented subcommand ("scan <bus>").
- csd203: prime the external Vbus cache in csd203_start() so getBatteryVoltage()
  does not report a false 0V before the timer's first external-sensor poll.
- i2c_driver.h: clarify that i2c_trylock is task/timer-callback only, not
  ISR-safe (uses xSemaphoreTake, no FromISR path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the MAX_I2C_BUSES definition from the generic_stm32 i2c_bus.cpp into
hal/i2c_driver.h so it is part of the HAL contract (valid bus ids are
0 .. MAX_I2C_BUSES - 1). This lets the CLI bus bounds-check use it instead
of the stm32-specific I2C_Bus_2 enum, and drops the direct
stm32_i2c_driver.h include from cli.cpp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the unlocked early-return null check on s_active. start()/stop()
write s_active under s_mutex, so the read belongs under the lock too. The
authoritative check now happens after mutex_trylock(); the removed fast
path only saved a cheap trylock on idle ticks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants