ZZBX / NOTES ← All notes
AutoSAR · Com · CAN BusOff

AUTOSAR Com tx stuck:
after BusOff, Transmiting never clears.

Short CANH to CANL, send one event-triggered frame, and that frame never leaves again — while everything else looks healthy. The cause is not the CAN controller and not the bus: it is one boolean inside Com whose only way back to FALSE is a callback that BusOff silently swallowed.

2026-11-07 ◆ 11 min read ◆ AutoSAR CP / Com ◆ 1 leaked flag
1boolean that blocks everything
3ways to set it
1way to clear it
0timeouts behind it

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.

ObservationWhat 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.

WhereTrigger
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

OptionHowCost
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:

  1. Short CANH to CANL at the PCAN connector, or use a VH6501 / CANoe fault-injection channel to force bus-off deterministically.
  2. Trigger one event-triggered frame from the application — press the button, change the signal, whatever makes that PDU due.
  3. Read Com_TxIPduRunTimeState[pduId].Transmiting. It should be TRUE and stay there.
  4. Count calls into Com_TxConfirmation before and after the short. If the counter stops advancing while sends keep being attempted, the chain is broken.
  5. 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 +8 per failed transmission, +1 per receive error, and -1 per successful transmission.
  • TEC > 255 moves the controller into BusOff: it stops transmitting and receiving entirely and removes itself from the bus.
  • Recovery requires observing 128 × 11 consecutive 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 solvedWhy 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.

Harry Zhao
AUTOMOTIVE EMBEDDED · AUTOSAR BSW / DIAGNOSTIC TOOLING

12 years in automotive embedded software, from ECU low-level drivers up to AutoSAR CP platform architecture: Com/CanIf/CanSM integration, build flows, test scripting, Git servers, Jenkins CI/CD, and chip-level functional safety on Infineon TC397 / NXP S32K314. The debugging records here come from real supplier projects, and most of them started as a symptom nobody could explain.

All example data is synthetic; no real vehicle, supplier or customer identifiers appear.