ZZBX / NOTES ← 全部笔记
AutoSAR · Com · CAN

一条报文的
三条路径

把普华 AutoSAR CP 生成的代码从 Com_SendSignal 一路读到 FlexCAN_Ip_Send,再读回来。值得写下来的不是函数名单 —— 而是发送、确认、接收是三条独立路径,由三个不同的主函数驱动。确认不嵌在发送调用里,而这一层通信栈里几乎所有让人困惑的现场症状,都源自这一件事。

2026-11-05 ◆ 约 13 分钟 ◆ 读码推演,非实测 ◆ 普华 AutoSAR CP
3条独立路径
4层被穿越
2个 PduR 开关
1个运行时标志

为什么要读生成后的代码

AutoSAR 是「配置」出来的,不是「写」出来的。你把 ComIPduSignalProcessing 设成 DEFERRED,生成器就吐出一段不一样的函数体。这既是标准存在的意义,也正是调试变难的地方:配置是声明式的,行为是命令式的,两者之间的映射只存在于生成出来的 C 代码里。

所以当一帧报文发不出去时,配置工具上那个勾选看起来是对的,而 C 代码里走的是你没预料到的分支。把这条链从头到尾读一遍,这件事就从「玄学」变成「查表」。

这篇的范围

这是一次读码,不是实测。下面全部内容来自顺着普华 AutoSAR CP 的一套生成代码(Com → PduR → CanIf → FlexCAN)走一遍,并把每个调用对回产生它的 ECUC 容器。第 6、7 节里的失效模式是从这条链推出来的,不是在台架上观察到的 —— 我明确标注,因为把推演当成实测写,是浪费读者一个下午的最快方式。

还有一句要说在前面:这里的模块名和容器路径来自一家厂商的生成代码。分层是标准规定的,具体函数名不是 —— 换一个生成器名字就会变,你应该读自己的输出,而不是信我的拼写。

先看整体形状:三条平行的路

整件事在一张图上。从左往右看:三列不是先后步骤,是三条独立的流,只是碰巧动的是同一个 PDU。

AutoSAR 通信栈的发送、发送确认、接收三条路径
发送由 Com 主函数驱动,确认由 CAN 驱动主函数驱动,接收靠轮询。三个驱动源,一个 PDU

这张图有两个性质,占了大部分实用价值。第一,只有左边那列是你主动发起的 —— 中间和右边都由 CAN 驱动自己的主函数按自己的节奏驱动。第二,中间那列从 CAN 驱动开始,不从 Com 开始。确认是往上走的,不是一个返回值。

发送:为什么 SendSignal 不负责发

左边那列,按顺序。把它当成一条调用栈是第一个常见误解,因为最上面两个并不属于下面的栈 —— 它们写数据,发出去的是主函数。

Com_SendSignal            ← 应用/RTE 调用,写信号
  Com_SetTxSignalBuff     ← 写进 Com 自己的运行缓冲
    Com_Signal8BitRuntimeBuff[]

      ……〈这里返回了。下面由主函数驱动〉……

Com_MainFunctionTx        ← 周期任务,判断哪些 PDU 该发
  Com_MainFunction_SendPdu
    PduR_ComTransmit      ← 进入 PduR,带上 Com 的 PDU 句柄
      PduR_Transmit
        PduR_UpModeTransmit        ← 确认当前通信模式允许发送
          ModuleIfTransmitApi      ← 按 PduRBswModuleType 分派
            CanIf_Transmit
              CanIf_TransmitHandle
                CanWriteApi            ← 按 Can_DriverApiType 分派
                  Can_Write
                    Can_43_FLEXCAN_Write
                      Can_43_FLEXCAN_Ipw_Write
                        FlexCAN_Ip_Send
                          FlexCAN_StartSendData

顶上那个分界不是实现细节,是 ComIPduSignalProcessing 这个参数在起作用。同一个 PDU,两种设置的行为是这样的:

配置 Com_SendSignal 做什么 报文什么时候出去
DEFERRED 只写缓冲,立即返回 下一个 Com_MainFunctionTx 周期
IMMEDIATE 写缓冲并当场触发发送路径 就在调用者的上下文里

DEFERRED 是周期帧的常规选择,理由是带宽:五个信号在五个地方更新,会合并成一次总线访问而不是五次。代价是调用者不再控制发出时刻 —— 而这恰恰是排查时序问题的人最想控制的东西。这个取舍就是这个参数的全部意义,在读日志之前,值得先弄清楚你这帧站在哪一边。

在哪看的

值来自 ComIPdu。我读的这套里,一个 Tx PDU 带着 ComIPduDirection = SEND、ComIPduSignalProcessing = DEFERRED,周期在 ComTxIPdu/ComTxModeFalse/ComTxMode 下:ComTxModeMode = PERIODIC、ComTxModeTimePeriod = 0.02,也就是 20 ms。这四个参数合起来决定了一帧由主函数发、多久发一次、从什么时候开始发。

CanIf 往下就机械了:PduR 对每个下层模块(CAN、LIN、FlexRay)各有一个发送入口,ModuleIfTransmitApi 那层是按 PduRBswModuleType 查表分派。底下没有聪明东西。这反而是好消息:如果一帧死在 CanIf 到驱动之间,问题几乎总是 CAN ID / 句柄映射,而不是逻辑。

确认:晚一层的回调

这一列是最容易理解错的,所以单独一节。确认从 CAN 驱动的主函数开始,往上穿过 CanIf、PduR,最后在 Com 里清一个标志:

Can_MainFunction_Write    ← CAN 驱动自己的周期任务(如 Periods_0)
  Can_MainFunction_MultipleWrite
    Can_43_FLEXCAN_Ipw_MainFunction_Write   ← 查发送邮箱是否真发完
      CanIf_TxConfirmation
        CanIfTxPduUserTxConfirmationName    ← 配置里的回调名,按名反查
          PduR_CanIfTxConfirmation
            PduR_IfTxConfirmation
              ModuleTxConfirmationApi       ← 又一次按模块类型分派
                Com_TxConfirmation
                  Com_TxIPduRunTimeState[comTxPduId].Transmiting = FALSE;

最后一行是这条路径存在的全部理由。Com 需要一个 per-PDU 的状态位 Transmiting,因为它无从得知硬件什么时候发完。发送时置起,确认时清掉。没有这一位,Com 就回答不了它唯一需要回答的问题:这个 PDU 现在能不能再发。

注意这条链里两次分派 —— CanIfTxPduUserTxConfirmationName 按配置里的回调名反查,ModuleTxConfirmationApi 再按模块类型分派一次。两次都是生成代码在运行时查配置。这也解释了一件事:配置里回调名写错,既不会有编译错误也不会有链接错误 —— 那个函数就是不会被调用而已。

接收:轮询上浮,不是中断

接收是发送的镜像,方向相反。在这一层同样是轮询,不是中断驱动:

Can_MainFunction_Read     ← CAN 驱动周期任务
  Can_Ipw_MainFunction_Read
    Can_Ipw_ParseData     ← 解析报文内容
      CanIf_RxIndication
        CanIf_RxIndicationHandle
          CanIfRxPduUserRxIndicationName   ← 按名反查上层回调
            PduR_CanIfRxIndication
              PduR_IfRxIndication
                ModuleIfRxIndicationApi
                  Com_RxIndication
                    Com_CopyPduDataToRxBuffer   ← 先落进接收缓冲
                      Com_RxIPduRuntimeBuff

  ……〈主函数接手〉……

Com_MainFunctionRx
  Com_IndicationProcess   ← 拆信号
    Com_SignalRxIndication
      Com_RxSignalHandle  ← 单个信号处理(过滤、通知 RTE)

和发送一样分两步,理由也一样:数据先落进缓冲,主函数再做解释。Com_CopyPduDataToRxBuffer 搬字节,Com_MainFunctionRx 决定字节是什么意思。所以如果你在找一个「值不对」的信号:缓冲拷贝是字节放错位置的地方,主函数是按位位置取值的地方 —— 两个完全不同的 bug,从应用层看长得一模一样。

超时监控也挂在这个主函数上。Com_MainFunctionRx 会调 Com_RxSignalTimeOutHandle,触发 ComTimeoutNotification,最后落到 RTE 回调:

Com_MainFunctionRx
  Com_RxSignalTimeOutHandle
    ComTimeoutNotification
      Rte_COMCbkRxTOut_<Signal>_<IPdu>
监控是运行时可开关的

超时监控不是常开的,这一点值得在判定「监控坏了」之前先知道。Com_ReceptionDMControl 由 BswM 经 BSWM_DEADLINE_MONITOR_CONTROL 驱动,所以一次模式切换就可能把一批 PDU 的监控关掉。超时回调不来的时候,先查监控是不是被关了,再查超时值。

两个布尔量决定链路是否闭环

接下来这一段,我会直接交给任何在调这套栈的人。确认路径只有在 PduR 被配置成携带它时才存在。两个独立的参数,都在 PduRRoutingPath 下,两个都必须为 true:

参数 位置 它管什么
PduRSrcPduUpTxConf PduRSrcPdu 源模块(Com)要不要收到完成通知
PduRTransmissionConfirmation PduRDestPdu 目标方向是否上报完成

顺着后果走一遍。任一为 false,PduR_IfTxConfirmation 就没有东西往上递,Com_TxConfirmation 永远不被调用,Transmiting 一直是 TRUE。对一个周期 PDU 来说,这意味着第一帧发出去了,之后每一帧都被静默丢弃 —— 没有 DET,没有错误计数,日志里什么都没有。总线是安静的,而每一层都报告成功。

推演,非实测

明确说一次:我没有在台架上造出这个故障。它是这条代码路径蕴含的结论 —— 那个标志只有一个清除者 Com_TxConfirmation,而那个函数在这条链里只有一个调用者。这种形状的结论可以拿去行动,但它毕竟是读出来的,拿到之后第一件事应该是在你自己的构建里确认,而不是信我。

这也是全文最有用的一条诊断。「发一次就不发了」是一个非常特定的指纹:说明发送路径从头到尾是通的,坏的是确认路径。把它和「从来没发出去过」对比 —— 那是另一种故障,在上面(模式、时序),不在下面的确认管道里。

报文停了,先看哪里

一个简短的判断顺序,按三列的形状排,而不是靠猜。每一步都在缩小故障落在哪一列。

现象 先看 为什么
从来没发出去 Com 模式、IPduGroup、发送模式 PduR_UpModeTransmit 把整条发送路径挡住了
发一次就不发 上面那两个 PduR 确认开关 Transmiting 没被清掉,后续全丢
发了,值不对 位位置、字节序,在驱动之前 搬字节和取位是两步,别混
收不到,超时在报 过滤、CanIf 的 Rx PDU 映射 超时能报说明主函数在跑,问题在更下层
连超时都没有 Com_ReceptionDMControl、BswM 监控可能压根被关掉了

注意五行里有四行指向配置而不是代码。这就是这套栈的本质:代码是生成的、是对的,而你对抗的行为住在离症状三个容器远的某个勾选框里。

三个值得点名的坑

1. Com_TriggerTransmit 不是给 CAN 用的

它出现在 PduR 里,紧挨着 CAN 的入口,看起来像「立即发送」的接口。不是:它是 LIN 和 FlexRay 用的,由下层按自己的节奏来拉数据。在 CAN 上,立即发送是 Com 侧的参数(IMMEDIATE),不是这个 API。拿错的结果是代码能链接、什么都不做。

2. 两次分派都是查表,不是逻辑

PduRBswModuleType 选调哪个模块,Can_DriverApiType 选调哪个驱动。两个都是生成出来的查表。链子在中途断掉时,查表命中错项和函数没被调用看起来完全一样 —— 因为从症状那一侧看,两者都是沉默。

3. 超时值不等于监控

ComTimeout 是死线。死线有没有被看着是另一件可被运行时切换的事,走 BswM 控制。在监控被关着的时候调超时值,是一种非常安静的浪费一天的方式。

共同点:在一套配置驱动的栈里,有意思的信息几乎从来不在代码里。代码告诉你「可能发生什么」,配置告诉你「实际发生什么」。

常见问题

为什么 Com_SendSignal 不直接把报文发出去?

因为它只写 Com 自己的发送缓冲,真正下沉到 CAN 驱动是 Com_MainFunctionTx 的事。这个拆分由 ComIPduSignalProcessing 决定:配成 DEFERRED 时两者解耦,多个信号可以攒进同一个 PDU 一次发出;配成 IMMEDIATE 才会当场触发。DEFERRED 把 N 次信号更新压成一次总线访问,代价是发送时刻不由调用者决定。

发送确认为什么不在发送调用链里?

因为发送确认来自硬件,而硬件发完的时刻晚于 Can_Write 返回的时刻。Can_Write 只是把报文放进发送邮箱,真正的完成回调要等 CAN 驱动自己的主函数(Can_MainFunction_Write)轮询邮箱状态后才触发,再经 CanIf_TxConfirmation、PduR_IfTxConfirmation 传回 Com_TxConfirmation。所以这三条路径是并列的,不是嵌套的。

Com 怎么知道上一帧已经发完了?

靠 Com_TxIPduRunTimeState[comTxPduId].Transmiting。发送时置起,Com_TxConfirmation 里清掉。确认不来,标志就一直是 TRUE,该 PDU 之后再也发不出去 —— 表现为第一帧正常,之后全部消失。

PduR 里哪个配置项漏了会导致发送确认丢失?

两个,都要为 true:PduRSrcPdu 下的 PduRSrcPduUpTxConf(源模块要不要收到完成通知),和 PduRDestPdu 下的 PduRTransmissionConfirmation(目标方向是否上报完成)。任一为 false,PduR_IfTxConfirmation 就没东西往上递,Com_TxConfirmation 不被调用,Transmiting 卡在 TRUE。

Harry Zhao
汽车嵌入式 · AutoSAR BSW / 平台工具链

12 年汽车电子底层软件经验,从 ECU 底层驱动做到 AutoSAR CP 平台架构:编译流程、测试脚本、 Git 代码服务器、Jenkins CI/CD,以及 TC397 / S32K314 芯片级功能安全。 日常读生成代码 —— 因为配置变成行为的地方就在那里。

手写 HTML / CSS · Cloudflare Workers ← 全部笔记 me.tech-harry.top