为什么要读生成后的代码
AutoSAR 是「配置」出来的,不是「写」出来的。你把 ComIPduSignalProcessing 设成 DEFERRED,生成器就吐出一段不一样的函数体。这既是标准存在的意义,也正是调试变难的地方:配置是声明式的,行为是命令式的,两者之间的映射只存在于生成出来的 C 代码里。
所以当一帧报文发不出去时,配置工具上那个勾选看起来是对的,而 C 代码里走的是你没预料到的分支。把这条链从头到尾读一遍,这件事就从「玄学」变成「查表」。
这是一次读码,不是实测。下面全部内容来自顺着普华 AutoSAR CP 的一套生成代码(Com → PduR → CanIf → FlexCAN)走一遍,并把每个调用对回产生它的 ECUC 容器。第 6、7 节里的失效模式是从这条链推出来的,不是在台架上观察到的 —— 我明确标注,因为把推演当成实测写,是浪费读者一个下午的最快方式。
还有一句要说在前面:这里的模块名和容器路径来自一家厂商的生成代码。分层是标准规定的,具体函数名不是 —— 换一个生成器名字就会变,你应该读自己的输出,而不是信我的拼写。
先看整体形状:三条平行的路
整件事在一张图上。从左往右看:三列不是先后步骤,是三条独立的流,只是碰巧动的是同一个 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。