Skip to content

fix(pa01): Optimize matrix keyboard scanning#7315

Closed
sneone wants to merge 3 commits into
EdgeTX:mainfrom
sneone:pa01_matrix_keyboard
Closed

fix(pa01): Optimize matrix keyboard scanning#7315
sneone wants to merge 3 commits into
EdgeTX:mainfrom
sneone:pa01_matrix_keyboard

Conversation

@sneone
Copy link
Copy Markdown
Contributor

@sneone sneone commented Apr 22, 2026

1.I2C errors can easily occur in certain situations, such as when sound plays during model switching. Switching to a four-cycle step-by-step scan resolved the issue (tested hundreds of times without errors).

Summary by CodeRabbit

  • Bug Fixes
    • Improved keyboard input handling by preventing concurrent read attempts and optimizing polling behavior during key polling operations.

Review Change Stack

@raphaelcoeffic
Copy link
Copy Markdown
Member

Please cleanup (remove commented out block, dead code, etc) and document properly in the code.

From what I understand, it seems that different groups of keys are polled in each pollKeys() cycle, such that it takes a number of cycles to actually all the keys, right?

@sneone
Copy link
Copy Markdown
Contributor Author

sneone commented Apr 23, 2026

Please cleanup (remove commented out block, dead code, etc) and document properly in the code.

From what I understand, it seems that different groups of keys are polled in each pollKeys() cycle, such that it takes a number of cycles to actually all the keys, right?

Yes, testing has shown that it significantly reduces the probability of I2C errors. However, the original method of scanning only once during boot must be retained. Since it's a matrix keyboard, we don't actually need interrupts at all.

Copy link
Copy Markdown
Member

@raphaelcoeffic raphaelcoeffic left a comment

Choose a reason for hiding this comment

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

Overview

The PR reworks pollKeys() for the PA01 target so that instead of driving all four matrix columns and reading all rows inside a single pollKeys() call (1× heavy I²C burst every 10 ms), the scan is split into a 4-step state machine — each invocation drives one column and reads one set of rows. The interrupt-driven shouldReadKeys path is removed. Per the author this eliminates I²C errors seen under contention (e.g. audio playback during model switching).

Correctness issues

  • extern volatile bool errorOccurs; won't link cleanly. In bsp_io.cpp:59, errorOccurs is static (internal linkage), so the new extern in key_driver.cpp can't reference it. It only compiles today because nothing actually reads the variable in key_driver.cpp — please delete the extern (dead code), or drop static in bsp_io.cpp if the intent is really to share it.
  • Stale header declaration. radio/src/targets/pa01/bsp_io.h:48 still declares bool bsp_get_shouldReadKeys(); even though the definition, internal flag, and ISR have all been removed. Remove it to keep the header consistent.
  • IRQ wiring fully removed? The diff drops _io_int_handler and the gpio_init_int(IO_INT_GPIO, ...) call. Double-check that no other path (sleep/wake, bootloader) still expects the AW9523B INT line to drive an interrupt, and consider removing the IO_INT_GPIO define if now unused.
  • State machine never resets on suspend. If suspendI2CTasks toggles mid-cycle, pollkey_step and the static result accumulator are frozen; when I²C resumes, the remaining 1–3 columns get OR'd into a partially-stale result, yielding one spurious combined frame before recovery. A result = 0; pollkey_step = 0; reset on re-enable (or at entry to step 0) would be safer.
  • Uninitialized first cycle. pollkey_step starts at 0, so the first pollKeys() only drives ~OUT1 and returns; the actual column-1 read happens next tick. Not a bug, but it means keyState is zero for ~40 ms after boot.
  • Key latency goes from 10 ms to 40 ms. With keysPollingCycle() called from the 10 ms timer (edgetx.cpp:211) and the full matrix now taking 4 ticks, keyState updates at 25 Hz instead of 100 Hz. Probably imperceptible for short presses, but repeat-rate / long-press debouncing in keys.cpp was tuned around the old cadence — worth testing repeat and combo keys explicitly.

Code quality

Following up on the earlier cleanup request:

  • Duplicated function body. The #if !defined(BOOT) … #else … #endif gives two full pollKeys() implementations. Consider keeping a single implementation and factoring the per-column read into a helper, instead of duplicating the scan code.
  • Magic hex step values. 0x01 / 0x03 / 0x07 / 0x0F look bitmask-like but are just sequence markers. An enum (STEP_COL1 … STEP_COL4) or plain 0..3 + a switch would be much clearer. Yoda comparisons (0x03==pollkey_step) also don't match the project style.
  • volatile static on result / pollkey_step. volatile isn't buying anything if pollKeys() is only invoked from the 10 ms timer task — it just inhibits optimization. If you do want to defend against concurrent callers, use a proper atomic/critical section; otherwise drop volatile.
  • Concurrency guard removed without a note. The old syncelem.ui8ReadInProgress block is gone. That's probably fine under a single-caller assumption, but please add a brief comment stating that assumption so the guard isn't reintroduced later "just in case."
  • READ_ENTER_KEY macro. A static inline helper would be clearer and avoid macro scoping pitfalls. It's also repeated in every step branch — one call per pollKeys() would do.
  • Formatting. Missing spaces around operators (if(!pollkey_step), 1<<TR1U, ?true:false), trailing blank lines, inconsistent indent — please run the formatter used elsewhere in this tree.
  • Comments. Add a short block at the top of pollKeys() describing the state machine (column order, why it was split, effective scan rate) per the prior review request.

Suggestions

  1. Delete the now-unused bsp_get_shouldReadKeys, shouldReadKeys, _io_int_handler, and IO_INT_GPIO references (header + source).
  2. Remove extern volatile bool errorOccurs; in key_driver.cpp (unused), or make it a real shared symbol with a consumer.
  3. Collapse the #if !defined(BOOT) / #else duplication into a single implementation with a helper.
  4. Replace hex step codes with an enum + switch.
  5. Drop volatile on the statics; document the single-caller assumption.
  6. Add a one-line comment where the old delay_us(BSP_READ_AFTER_WRITE_DELAY) used to be, explaining it's no longer needed because the 10 ms tick provides settle time between column drive and read — so nobody "fixes" it back in.
  7. Quick manual test of repeat / long-press / trim-hold behaviors to confirm the 40 ms scan period doesn't regress UX.

Risk

Low blast radius — PA01-only, no shared-driver touches. Main residual risks are the linker/stale-header hygiene items and the increased matrix latency. Functional regressions would show up as lost repeat events or laggy trims.

@richardclli
Copy link
Copy Markdown
Member

I suggested this change, polling matrix with I2C added a lot of waits in between each write and read. Pipeline reads and writes with 10ms timer task will be more efficient and less error prone.

@richardclli
Copy link
Copy Markdown
Member

@raphaelcoeffic You used claude code to do code review?

@richardclli richardclli changed the title Optimize matrix keyboard scanning fix(pa01): Optimize matrix keyboard scanning Apr 28, 2026
@richardclli richardclli added this to the 2.11.7 milestone Apr 29, 2026
@richardclli richardclli added backport/2.11 To be backported to a 2.11 release also. backport/2.12 To be backported to a 2.12 release also. bug 🪲 Something isn't working labels Apr 30, 2026
@richardclli
Copy link
Copy Markdown
Member

@sneone This PR has conflicts with 2.11 branch, you better make a 2.11 version for fixing 2.11 branch

@richardclli richardclli self-requested a review April 30, 2026 04:58
@richardclli
Copy link
Copy Markdown
Member

richardclli commented Apr 30, 2026

@sneone Why you removing the interrupt handling? I remember this is necessary otherwise the MCU load can be much higher. Because I have once disabled the interrupt and @gagarinlg spot the problem and add back the interrupt.

@sneone
Copy link
Copy Markdown
Contributor Author

sneone commented May 7, 2026

If only one row (or column) is scanned per cycle, interrupts must be disabled, since it is impossible to determine which specific row (or column) the key press originates from. If interrupts are enabled, a full scan of all rows and columns is mandatory. If the MCU load is too high, it is still recommended to keep the original approach of scanning the entire matrix in one cycle. What do you think? @richardclli

@richardclli
Copy link
Copy Markdown
Member

I think interrupt is useful, it can save you from one read. Only need to store the latest read, so after you change the output and input changes, then it will interrupt and change the dirty flag, and when you see the dirty flag, you do a read. And because this polling will be called every 10ms, so interrupt will occurs before next read. And if not interrupt occurs, then you will save one read. And this should happens 9x% of the time because usually user will not press any buttons for most cases.

If only one row (or column) is scanned per cycle, interrupts must be disabled, since it is impossible to determine which specific row (or column) the key press originates from. If interrupts are enabled, a full scan of all rows and columns is mandatory. If the MCU load is too high, it is still recommended to keep the original approach of scanning the entire matrix in one cycle. What do you think? @richardclli

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

The PR adds two early-exit guards to the pollKeys() function in the PA01 key driver. The first exit prevents matrix scanning when key reading is disabled and nonReadCount is below the threshold. The second exit prevents scanning when concurrent read attempts are detected.

Changes

Key polling early-exit conditions

Layer / File(s) Summary
Early-exit guards in pollKeys
radio/src/targets/pa01/key_driver.cpp
Two return statements halt matrix polling: one after non-reading path updates stop further execution when nonReadCount < 10, and another when ui8ReadInProgress > 1 is detected, preventing concurrent access conflicts.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description lacks required template sections (issue reference, structured summary format). It provides context about I2C errors and mentions testing, but does not follow the repository's pull request template structure. Add the missing issue reference in 'Fixes #' section and restructure the description to follow the template with a proper 'Summary of changes' section.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: optimizing matrix keyboard scanning by implementing a four-cycle step-by-step scan approach to reduce I2C errors.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
radio/src/targets/pa01/key_driver.cpp (1)

108-119: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent ui8ReadInProgress from getting stuck non-zero on concurrent calls.

Line 118 can deadlock scanning under a race: two callers may both hit the > 1 return path, and neither reaches Line 174 to clear the flag. That leaves pollKeys() permanently returning cached state.

Proposed fix
-  if (syncelem.ui8ReadInProgress != 0) {
-    keyState = syncelem.oldResult;
-  }
-
-  // ui8ReadInProgress was 0, increment it
-  syncelem.ui8ReadInProgress++;
-  // Double check before continuing, as non-atomic, non-blocking so far
-  // If ui8ReadInProgress is above 1, then there was concurrent task calling it, exit
-  if (syncelem.ui8ReadInProgress > 1) {
-    keyState = syncelem.oldResult;
-    return;
-  }
+  if (syncelem.ui8ReadInProgress != 0) {
+    keyState = syncelem.oldResult;
+    return;
+  }
+
+  // Claim scan ownership
+  syncelem.ui8ReadInProgress = 1;
🤖 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/pa01/key_driver.cpp` around lines 108 - 119, A race can
leave syncelem.ui8ReadInProgress stuck >0 because both callers take the
early-return path and never clear the flag; modify the concurrent-path in the
pollKeys() logic so that when you detect syncelem.ui8ReadInProgress > 1 you
restore keyState = syncelem.oldResult and then decrement/clear
syncelem.ui8ReadInProgress (or use an atomic compare-and-swap clear) before
returning so the flag is never left non-zero; ensure every exit path from the
read section (including the >1 branch) clears or decrements
syncelem.ui8ReadInProgress to avoid permanent stuck state.
🤖 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.

Outside diff comments:
In `@radio/src/targets/pa01/key_driver.cpp`:
- Around line 108-119: A race can leave syncelem.ui8ReadInProgress stuck >0
because both callers take the early-return path and never clear the flag; modify
the concurrent-path in the pollKeys() logic so that when you detect
syncelem.ui8ReadInProgress > 1 you restore keyState = syncelem.oldResult and
then decrement/clear syncelem.ui8ReadInProgress (or use an atomic
compare-and-swap clear) before returning so the flag is never left non-zero;
ensure every exit path from the read section (including the >1 branch) clears or
decrements syncelem.ui8ReadInProgress to avoid permanent stuck state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 09da2ff2-90c9-4293-8c1a-90791a104001

📥 Commits

Reviewing files that changed from the base of the PR and between 669b5fb and c819381.

📒 Files selected for processing (1)
  • radio/src/targets/pa01/key_driver.cpp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/2.11 To be backported to a 2.11 release also. backport/2.12 To be backported to a 2.12 release also. bug 🪲 Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants