The answer in one paragraph
Root cause: Com uses Com_TxIPduRunTimeState[pduId].Transmiting to mean "this I-PDU is being transmitted". It is set to TRUE inside the transmit main function and cleared back to FALSE in exactly one place: Com_TxConfirmation. During BusOff the controller is off the bus, that frame's confirmation never arrives, so Transmiting stays TRUE for good — and the transmit main function keeps concluding the previous frame is still in flight, skipping that PDU forever. Fix: on BusOff entry, have BswM close the IPduGroup via Com_IpduGroupControl (ActiveEnable = FALSE); on recovery it must be reopened, or the whole group stays silent permanently.
This is not a bug in one vendor's code. It is a structural property of how Classic Platform Com does transmission: the right to send is granted by a flag, and the flag is returned by a callback. Any condition that eats the callback — bus-off, a mode transition mid-flight, a lower layer that drops silently — leaks the flag. BusOff is simply the easiest one to reproduce deliberately.
The symptom: one frame disappears
The reproduction is deliberately crude: short CANH and CANL on the PCAN side, then let the application send one event-triggered frame. What follows is not a bus error storm — it is silence. That specific frame never appears on the bus again, not after removing the short circuit, not after the controller finishes BusOff recovery.
What makes this expensive to diagnose is that nothing else breaks. Other periodic frames keep cycling, the application keeps running, CANoe keeps showing traffic. Whoever is looking at the trace sees a busy bus with one missing id and reasonably concludes it is a timing or filtering issue, not a stuck state machine.
| Observation | What it rules out |
|---|---|
| Bus keeps running, other frames normal | Controller dead, CAN driver stopped, whole stack hung |
| Removing the short does not help | A transient physical-layer disturbance |
| Only that one id is gone | Group-level shutdown, mode switch |
| App still calls the send API | Application stopped producing the signal |
That last row matters most. The application is not idle — it keeps asking to send. The request is accepted by the API and then quietly dropped one layer down. A send that is accepted and never happens is the signature of a state flag, not of a buffer.
Locating it: the flag is stuck
Attach the debugger and read the runtime state of that I-PDU. One boolean tells the whole story:
Com_TxIPduRunTimeState[IPduId].Transmiting == TRUE ← stuck at TRUE
Com_TxIPduRunTimeState[IPduId].ActiveEnable == TRUE ← the group is enabled
Application: Com_SendSignal() returns E_OK; the signal really is written to Com's buffer
Bus: that id has not appeared once since the short circuit
Note the combination: the group is enabled, the signal was written, and the PDU still does not go out. Those three facts together narrow it to exactly one place — the guard clause in the transmit main function.
In the PuHua generated code the path looks like this. The structure is what matters, not the exact names — Vector, ETAS and others use their own prefixes for the same state:
Com_MainFunctionTx() ← cyclic task
for each Tx I-PDU:
Com_MainFuncTxDirect(IPduId) ← the event-triggered path
if (Com_TxIPduRunTimeState[IPduId].ActiveEnable == TRUE)
{
if (Com_TxIPduRunTimeState[IPduId].Transmiting == FALSE) ← ★ stuck here
{
if (event pending / period due)
{
ret = PduR_ComTransmit(...)
if (ret == E_OK)
Com_TxIPduRunTimeState[IPduId].Transmiting = TRUE; ← set
}
}
/* Transmiting == TRUE -> do nothing, skip silently */
}
The else branch is not a comment in the source — it is nothing at all. There is no counter, no log, no diagnostic event. That is precisely why this failure produces a perfectly quiet system: the code that decides not to send leaves no trace whatsoever.
Why the flag can be cleared at all
To see why BusOff breaks it, first look at how asymmetric the two sides are. Setting the flag has several entry points; clearing it has exactly one.
| Where | Trigger | |
|---|---|---|
| Set TRUE | Com_MainFuncTxDirect |
Event pending or period due, and the flag is FALSE |
| Set TRUE (repeat) | Repetition branch of the main function | ComTxModeNumberOfRepetitions not exhausted |
| Clear FALSE | Com_TxConfirmation |
Lower layer confirms the frame left |
Three ways in, one way out. And the way out depends on a callback originating from hardware, travelling up through the CAN driver's own main function, then CanIf_TxConfirmation, then PduR_IfTxConfirmation, then finally Com_TxConfirmation. Anywhere along that chain a dropped frame means no confirmation. The flag is a lease with no expiry.
This is also why the companion article on the send/confirm/receive chain matters here: if you assume confirmation is part of the send call, this failure is impossible to reason about, because in that mental model a send either succeeds or returns an error — there is no third state of 'accepted and never finished'.
Where the chain breaks
BusOff is the CAN controller taking itself off the bus after its transmit error counter exceeds 255. From that moment until recovery completes, it does not transmit. The subtlety is what happens to the frames already handed down the stack:
t0 Com decides to send -> PduR_ComTransmit → CanIf_Transmit → Can_Write
Can_Write returns E_OK (frame is in a TX mailbox)
Com immediately sets Transmiting = TRUE ← lease issued
t1 CANH / CANL shorted -> transmit error -> TEC +8 -> keeps accumulating
TEC > 255 -> BusOff; the controller detaches from the bus
t2 the frame in the mailbox cannot leave; dropped / flushed
★ no TxConfirmation is produced ← the lease is never returned
t3 BusOff recovery (128 × 11 recessive bits); controller returns to Error Active
Com side: Transmiting is still TRUE
-> Com_MainFuncTxDirect sees 'still in flight' every time and skips
-> that I-PDU is silent forever
t4 removing the short or power-cycling does not reset Com's runtime state
-> only Com re-init, or explicitly clearing the flag, recovers it
The critical row is t2. Note that Can_Write returning E_OK at t0 does not mean the frame was sent — it means the frame was accepted into a mailbox. That distinction is invisible in normal operation and becomes the whole failure during BusOff.
One honest caveat: stacks do not behave identically here. Some CanIf implementations actively confirm every queued frame with E_NOT_OK when BusOff occurs or the controller changes mode — in which case Com's Transmiting is cleared normally and this failure never happens. So this is not behaviour the standard mandates; it is an implementation choice. The check is trivial: put a counter inside Com_TxConfirmation and compare it before and after shorting the bus. If it stops advancing, you are in the case described here.
The fix: close the group, reopen it
The fix does not touch the state flag directly. It removes the need for the flag's correctness during the window when the bus cannot work at all: on BusOff, tell Com to stop trying to send this group; on recovery, tell it to resume. The mechanism is Com_IpduGroupControl, driven by BswM in response to CanSM's bus-off state.
/* BusOff entry: close the IPduGroup holding TX */
void BswM_CanSM_CurrentState(Network, CurrentState)
{
if (CurrentState == CANSM_BSWM_BUS_OFF)
{
/* FALSE = stop the group; every I-PDU in it gets ActiveEnable = FALSE */
Com_IpduGroupControl(COM_IPDU_GROUP_TX, FALSE);
}
else if (CurrentState == CANSM_BSWM_NO_COMMUNICATION ||
CurrentState == CANSM_BSWM_FULL_COMMUNICATION)
{
/* ★ recovery: must reopen explicitly, or the whole group stays silent */
Com_IpduGroupControl(COM_IPDU_GROUP_TX, TRUE);
}
}
Two details decide whether this actually works. First, the second argument. AUTOSAR's signature is Com_IpduGroupControl(IpduGroupId, Initialize). Initialize = TRUE re-initialises the I-PDUs in the group — counters, deadlines, and in most implementations the runtime state including the transmitting flag. FALSE only starts or stops the group without touching that state. If your goal is to clear a leaked flag, you generally need TRUE on the way back. This must be verified against your own implementation, because the exact reset scope is not fully pinned down by the specification.
Second, the group split. If every I-PDU lives in one group, closing it on BusOff also stops the frames you might still want for diagnosis, and reopening becomes an all-or-nothing event. Give transmission its own group, or split by criticality, so that the reaction is proportional.
And the step that gets forgotten: the recovery branch. A BusOff recovery is not an event that obviously demands attention — the bus simply starts working again. If nothing reopens the group, the controller is healthy, the bus is quiet, and the frame is still missing. Symptom identical to the original bug, cause completely different. This is the single most common way this fix is implemented incorrectly.
Three options compared
| Option | How | Cost |
|---|---|---|
| A · close the group | BswM closes the TX IPduGroup on BusOff, reopens on recovery | Must guarantee the recovery order yourself; requires a sane group split |
| B · rely on timeout | Let Com or PduR time out the transmission and clear the flag | Classic Platform Com has no standard 'timeout clears transmitting'; depends on the implementation |
| C · fix CanIf | On BusOff, confirm all queued frames with E_NOT_OK | Touches supplier BSW; highest certification and regression cost |
C is the technically cleanest: the contract says every accepted transmission gets a confirmation, so the lower layer should confirm failures too. But in a supplier-managed BSW stack, changing CanIf means a change request, a re-qualification, and a regression over every project using that stack. The cost is not in the code, it is in the process.
A is chosen here for one reason: it is entirely inside the project's own configuration space. BswM rules and IPduGroup membership are project artefacts; nobody has to re-certify the supplier's Com or CanIf. The price is that you now own the recovery sequence, and you must test it — specifically the case where BusOff recovers and the group must come back.
A note on B: it is tempting because it looks like a free safety net. Before relying on it, verify that the mechanism actually exists in your stack and actually clears the transmitting state, not just reports a timeout. A timeout that raises a DTC but leaves the flag set changes the symptom from 'silent' to 'logged and silent', which is better but still not fixed.
Reproducing it on the bench
Do not test this by waiting for a real BusOff — you will wait a long time and then not be sure what happened. Short the bus deliberately:
- Short
CANHtoCANLat the PCAN connector, or use a VH6501 / CANoe fault-injection channel to force bus-off deterministically. - Trigger one event-triggered frame from the application — press the button, change the signal, whatever makes that PDU due.
- Read
Com_TxIPduRunTimeState[pduId].Transmiting. It should beTRUEand stay there. - Count calls into Com_TxConfirmation before and after the short. If the counter stops advancing while sends keep being attempted, the chain is broken.
- Remove the short. If the frame comes back, it was a transient; if it does not, the flag leaked. This is the decisive test — and the one people skip.
With the fix in place, repeat the same sequence and watch for the opposite: the group closes on BusOff, the PDU stops being attempted at all (ActiveEnable goes FALSE), and on recovery both the group and the frame come back without a power cycle.
Test the recovery path at least as hard as the failure path. Half of the value of this fix is in code that only runs when the bus comes back — which is the code path that gets the least exercise in normal testing.
BusOff itself: TEC and recovery
Since BusOff is the trigger, it is worth being precise about what it is, because the timing explains why the failure is not always reproducible at the same frame count:
- Transmit error counter (
TEC) goes+8per failed transmission,+1per receive error, and-1per successful transmission. TEC > 255moves the controller into BusOff: it stops transmitting and receiving entirely and removes itself from the bus.- Recovery requires observing
128 × 11consecutive recessive bits. If the short circuit is still there, those bits never appear, so the controller keeps retrying and never recovers. - After a recovery, a second bus-off within a short window triggers slow recovery — the wait before retrying grows, typically in ~100 ms increments, to avoid hammering a broken bus.
That last point is why the failure can look random: whether the flag leaks depends on which frame happened to be in flight when TEC crossed 255. A frame that completed normally before the controller detached confirms fine; the one in the mailbox at the moment of detachment does not. Same test, different frame count, different outcome.
What this fix does not solve
Being clear about the boundary matters more than the fix itself, because the fix is easy to over-claim:
| Not solved | Why not |
|---|---|
| Frames lost during BusOff | Physical fact — the bus could not carry them. Whether they must be resent is an application decision, not a Com one |
| The BusOff cause itself | Closing a group makes the stack survive a bus fault, it does not stop short circuits |
| Other flags with the same shape | Any state bit cleared only by a callback has the same failure mode; this fix only covers the transmit path |
| Confirmation lost for other reasons | Mode transitions mid-send, queue overruns and PduR misconfiguration have the same effect and need their own fix |
The general lesson is worth extracting: whenever a state machine grants permission and waits for a callback to revoke it, ask what happens if the callback never comes. In this stack the answer was 'the permission is permanent' — and nothing in the code, the logs or the bus trace said so. That question is cheap to ask during a design review and expensive to answer during a bench failure.
Source and context: the debugging record behind this post comes from a supplier project using PuHua AutoSAR CP. All identifiers above are generic; no real vehicle, supplier or customer identifiers appear.
Common questions
Why does CAN BusOff permanently stop an AUTOSAR Com frame from being sent?
Because Com's Transmiting flag can only be cleared by the transmit confirmation callback. Com sets it to TRUE after handing the frame to PduR; the only place it returns to FALSE is Com_TxConfirmation. During BusOff the controller is off the bus, the frame does not leave, and the confirmation may never arrive — so the flag stays TRUE, the transmit main function keeps concluding the previous frame is still in flight, and that I-PDU is skipped forever. The flag is a lease with no expiry.
Who sets and who clears Com_TxIPduRunTimeState's Transmiting?
Setting happens in the transmit main function: Com_MainFunctionTx decides a PDU is due and the flag is FALSE, calls PduR_ComTransmit, and after E_OK sets the flag TRUE. Clearing has exactly one entry point: Com_TxConfirmation(pduId), called by PduR once CanIf reports completion. The two sides are asymmetric — several ways in, one way out — and one break on the outbound path leaks the flag permanently.
Why do periodic frames look fine while only event-triggered frames die?
Usually they are not fine — the fault is just disguised. In most implementations periodic and event-triggered I-PDUs share the same Transmiting guard, so both stop. The difference is observability: a stopped periodic frame just removes a heartbeat from the trace and gets blamed on bus load or sampling, while a stopped event frame is visibly absent. That is why the event-triggered frame is the most sensitive probe when diagnosing this.
After BusOff recovers, why is the frame still not sent?
Two causes, and they must be told apart. Either the flag already leaked during BusOff — recovery restores the controller, not Com's runtime state — or you implemented the group-close fix and forgot to reopen the group, leaving ActiveEnable FALSE for the whole group. The second is the most common implementation mistake of this fix, and its symptom is identical to the original bug.
Are there alternatives to closing the IPduGroup on BusOff?
Three。Close the group via BswM (this article): no supplier code change, but you own the recovery sequence. Rely on a transmit timeout: classic Platform Com has no standard 'timeout clears transmitting' behaviour, so whether it works depends entirely on your implementation. Fix CanIf so queued frames are confirmed with E_NOT_OK on BusOff: technically cleanest, highest certification and regression cost.
How do I confirm quickly that a field failure is this cause?
Three checks. Read the I-PDU's transmitting flag in the debugger — if it is stuck TRUE, that is the fastest evidence. Put a counter in both PduR_ComTransmit and Com_TxConfirmation; they should track one-to-one, and if sends keep increasing while confirmations do not, the chain is broken. Finally, short CANH to CANL, then remove the short: no recovery means a leaked flag rather than a physical-layer transient.