fix(twai): keep the TX frame alive until on_tx_done and wait for completion - #755
Conversation
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
🟡 Changes recommended
There are concurrency/timeout edge cases (semaphore lifetime vs concurrent teardown, and unbounded completion wait when timeout_ms < 0) that can lead to undefined behavior or hangs.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a TWAI transmit lifetime bug by ensuring the twai_frame_t descriptor and its data buffer remain valid until the driver’s on_tx_done ISR callback fires, turning transmit() into a “sent on bus (or timed out)” operation rather than a mere enqueue.
Changes:
- Store the outgoing message/frame in
Twaimember storage to avoid use-after-scope when the TX ISR formats queued frames. - Add an
on_tx_doneISR callback + binary semaphore to wait for completion, and serialize transmitters with a dedicated mutex. - Update API documentation/comments to reflect the driver’s deferred frame formatting behavior.
File summaries
| File | Description |
|---|---|
| components/twai/include/twai.hpp | Makes TX frame storage persistent, adds TX completion signaling, and changes transmit() to wait for actual completion. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
716fed2 to
e176e73
Compare
|
Addressed the review comments:
|
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.
e176e73 to
397c3c8
Compare
|
Second round addressed:
|
…letion twai_node_transmit() only queues the transmission: the esp_driver_twai driver holds the passed twai_frame_t pointer (and its data buffer) and formats the frame later, in the TX ISR. transmit() passed a stack-local frame whose buffer pointed into the caller's Message, so both were dead by the time the ISR read them. Single spaced-out transmits usually survived on stale-but-intact stack memory, but two back-to-back transmits (e.g. a PDO followed by a SYNC) overwrite the first frame while it is still queued -- the ISR then reads garbage, tripping the driver's "byte_len <= TWAIFD_FRAME_MAX_LEN" assert in twaifd_len2dlc(). Copy the message into member storage that outlives the call, register an on_tx_done ISR callback that signals a binary semaphore, serialize transmitters with a dedicated mutex (one frame in flight -- its storage must not be overwritten until completion), and block until the driver reports transmission complete (bounded by timeout_ms; an unacknowledged classic-CAN frame is retransmitted indefinitely). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153z8MvyCu6YT47myGDn4rK
397c3c8 to
2d06850
Compare
|
Self-review — one fix applied, and two tradeoffs I want to flag for reviewers:
Compiles clean; the MIB CANopen app (heavy SDO traffic through this |
Problem
twai_node_transmit()only queues the transmission: theesp_driver_twaidriver holds the passedtwai_frame_tpointer (and its data buffer) and formats the frame later, in the TX ISR.transmit()passed a stack-local frame whose buffer pointed into the caller'sMessage, so both were dead by the time the ISR read them.Single, spaced-out transmits usually survived on stale-but-intact stack memory, but two back-to-back transmits (e.g. a PDO followed by a SYNC) overwrite the first frame while it is still queued — the ISR then reads garbage, tripping the driver's
byte_len <= TWAIFD_FRAME_MAX_LENassert intwaifd_len2dlc():Fix
tx_message_/tx_frame_) that outlives the call.on_tx_doneISR callback that gives a binary semaphore.timeout_ms; an unacknowledged classic-CAN frame is retransmitted indefinitely).As a bonus,
transmit()returning true now means the frame was actually sent on the bus, not merely queued.Testing
Found and fixed while bringing up a CANopen master (SDO + PDO + SYNC traffic) on an ESP32-P4 talking to a Basicmicro MCP266. The crash reproduced deterministically on the first back-to-back transmit pair and is gone after this change; position/velocity control traffic runs indefinitely.
🤖 Generated with Claude Code