【AUTOSAR】 CP PDU Router(PduR)–从入门到放弃
基于 AUTOSAR Classic Platform R23-11 的 PduR 规范(AUTOSAR_CP_SWS_PDURouter.pdf),配合 EXP_LayeredSoftwareArchitecture、SWS_CommunicationStackTypes、SWS_CANTransportLayer、TPS_ECUConfiguration 几份一起看。
目录
1. PduR 是干什么的
通信栈往上数是懂信号的模块(COM 知道 PDU 里第几个字节是车速),往下数是懂帧和总线的模块(CanIf 只知道"这 8 个字节要发到 CAN ID 0x123 上")。这两拨人中间需要一个翻译,就是 PduR。
PduR 只做一件事:按静态配置的路由表,把收到的 I-PDU 转发到该去的地方。它不拆信号、不校验数据内容、不改载荷,也不认识任何具体总线。

1.1 没有它会怎样
以前的做法是上层直接调底层驱动接口,比如 COM 自己想发就调 CanIf_Transmit。这样写会碰到三个问题:
PduR 夹在中间之后,这三个问题都收了回去:上层只认识 PduR 一个对象,网关逻辑集中在路由表里,ID 之间的对应关系也统一由 PduR 维护。
1.2 设计上坚持的三件事
- 位置透明。 上层把 PDU 交给 PduR 就完事,底层是 CAN、LIN、FlexRay 还是 Ethernet,它不需要知道。
- 对载荷透明。 PduR 不关心 PDU 里装的是什么字节,只按 ID 转发。IF 和 TP 两种接口它都统一处理,但内容一个字节都不碰。
- IF 路径上不开销外的事。 单帧直通路由就是查一次表、调一次函数,没有内存拷贝,也没有额外队列。
有一点提前说清楚:PduR 出问题很安静。路由表里少配一条,那个 PDU 就在系统里凭空消失,不会报错、不会回调、也没有日志——除非开了 DET 钩子。所以配置阶段的疏忽,往往要到实车联调才被发现。
2. 两种通信模式:IF API 和 TP API
PduR 的两套接口是分开的,按关联 PDU 的类型走其中一条,不会混。
2.1 IF API:单帧定长报文
IF API 处理的是单帧、长度固定的短报文,比如标准 CAN、CAN FD、LIN、FlexRay 上的单帧 I-PDU。三条路径分别是发送、接收、触发发送。

发送路径
接收路径
这条路上没有缓冲,也没有队列——CanIf 调上来,PduR 换个 ID 就调下去,整件事在一次调用里做完。
触发发送路径
FlexRay、LIN 这类时间触发或主从调度的总线,数据不是上层主动推下去的,而是总线接口到点了来要:
注意这个方向是反的:由下层发起,向上要数据。
2.2 TP API:长报文分片
TP API 处理超过单帧容量的长报文,比如诊断报文、SOME/IP 的大报文、LdCom 的数据。因为一次装不下,所以走的是分阶段握手,一层一层往上转。

第一步:StartOfReception
CanTp 收到首帧(FF)或单帧(SF)时调用 PduR_CanTpStartOfReception(PduId, PduInfoPtr, TpSduLength, bufferSizePtr)。PduR 把请求转给上层(比如 Dcm),上层按总长度 TpSduLength 准备好缓冲区,并通过 bufferSizePtr 回填自己最多能提供多少容量。
如果上层装不下,返回 BUFREQ_E_OVFL,CanTp 据此回一帧 FC(OVFLW) 结束本次接收。这也是 TP 接收唯一一次"还没开始就结束"的情况。
第二步:CopyRxData
之后每来一个连续帧(CF),CanTp 调一次 PduR_CanTpCopyRxData(PduId, PduInfoPtr, bufferSizePtr)。PduR 转给上层的 Dcm_CopyRxData,把这片的字节写进上层的 RAM 缓冲区,同时更新剩余可用空间。
上层如果一时拿不出空间,可以返回 BUFREQ_E_BUSY。接收端 TP 收到这个返回值会向对端回一帧 FC(WAIT),等上层腾出空间再继续收。
第三步:RxIndication
最后一个分片复制完,CanTp 调 PduR_CanTpRxIndication(PduId, result),通知上层"整包齐了,可以开始解析"。
发送方向只有一个回调需要记:CanTp 要发下一片时调 PduR_CanTpCopyTxData,上层把数据填进去。如果上层暂时没准备好,返回 BUFREQ_E_BUSY,CanTp 会在计时窗口内重试。
3. 网关与几种路由形态
前面讲的是 ECU 内部的垂直路由(上下层之间收发)。PduR 还能干水平的事:把一条总线上收到的 PDU 直接转到另一条总线,中间不经过应用层解包。
3.1 跨总线网关
按源和目标的类型分成两种,两者不能混着配。

IF 网关:源和目标都是接口模块。CanIf 调 PduR_CanIfRxIndication 上来,PduR 查表发现这条路径标了网关,就不往上送,而是直接调到目标接口去,比如 CanIf_Transmit 或 FrIf_Transmit。IF 网关支持 1:N,一个源可以同时转到多个目标。
TP 网关:源和目标都是传输协议模块。比如 CanTp 收完一包长报文,PduR 转到 SomeIpTp_Transmit 发到以太网上。TP 网关只支持 1:1,一个源对一种目标 TP,没有一对多。
两条硬约束:
- IF 和 TP 不能互换。 收到的是 IF PDU,只能从 IF PDU 转出去;收到的是 TP PDU,只能从 TP PDU 转出去。配错了工具链直接报错。
- 网关路径上的发送确认 PduR 基本不管。 目的模块发上来的 PduR_TxConfirmation 对网关 PDU 没有意义,PduR 收到后不做处理。只有配了 FIFO 的直接数据提供型 PDU 是例外。
- PduR 不重试。 如果目的模块返回 E_NOT_OK,PduR 不会换条路再试一次,直接把这个 PDU 丢掉。
3.2 TP On-The-Fly:边收边转
TP 网关有两种做法,差别在延迟和内存。

整包转发要等所有 N-PDU 都收完,才开始向目标总线重新分片发送。一块 4095 字节的诊断报文,就得按整包长度开缓冲区,而且在收完之前目标总线一直是空着的。
On-The-Fly 是收到一定字节数之后就开始转,一边从源总线收、一边往目标总线写。内存上只需要一个很小的 FIFO,暂存几个分片就够了;延迟也从"整包时间"缩短到"一个分片左右"。
切换开关是系统模板里的 PduRTpThreshold——它决定先收够多少字节才起转。配成 0 就是收到首帧立刻开始,配大一点相当于多缓几帧。
代价是两侧速率不匹配时,快的一侧会把 FIFO 填满,所以 FIFO 深度要按最坏情况下的速率差来算。
3.3 Fan-out:一发多收
一个源 PDU 要同时分发给多个目标时用。配置上的表现是一个 PduRSrcPdu 对应多个 PduRDestPdu。

几个要留意的地方:
- 确认只有一份,而且是最后一个目的模块确认到达时才上报。 只要有一个目标没发出去,上层看到的就是整次发送失败。
- 带 Update Bit 的 PDU 不要配 Fan-out。 COM 的更新位本来表示"这一帧的值变了",一个源对多个目标之后,没法再用一个位表达这个意思。
- FlexRay 有个例外。 如果 Fan-out 的多个目标落在同一个 FlexRay Cluster 的不同 Channel 上,PduR 只向 FlexRayIf 提交一次发送请求——两个 Channel 由同一份数据驱动,重复提交反而会造成调度冲突。
3.4 Fan-in:多收一
反过来,多个源往同一个目标送。分两种情况:
- 纯网关的 Fan-in:多条路径可以同时处于激活状态,互不干扰。
- 转到本地模块的 Fan-in:任意时刻最多只能有一条路径激活,需要由 BswM 用 PduR_EnableRouting 保证。两条路径同时开,会往同一块接收缓冲区里写。
另外,COM 的 Update Bit 和序列计数在 Fan-in 接收上也不生效——多个源共用一个目标,这两个字段本身就失去了唯一性。
3.5 两侧长度不一致怎么办
跨总线网关经常碰到这个问题:源总线是 CAN FD 的 64 字节,目标总线是标准 CAN,只能装 8 字节。

PduR 的原则是取最小值,具体分两种情况:
- 不缓冲(直通转发):转发长度 = min(收到的长度, 目标 I-PDU 配置的长度)。目标 I-PDU 的长度本来就在配置里写好了,不用额外配参数。超出的字节直接丢弃。
- 带缓冲转发:复制长度 = min(收到的长度, PduRPduMaxLength)。这个参数在 PduRBuffer 容器里配,要配得不大于目标 I-PDU 的长度——配大了,Trigger Transmit 时目标缓冲区装不下,PduR 会返回 E_NOT_OK 并且不处理这次调用。
要理解的是,截断不是错误处理,是设计上的保护:两条总线的长度本来就不可能一样,PduR 只保证不越界,不保证数据完整。如果长度的变化有业务含义(比如过了网关信号布局要重新排),那就不能走网关,得让 COM 接收后重新打包再发到目标总线。
4. 运行时开关路由:BswM 与路由路径组
路由路径不是配完就定死的。BswM 会根据车辆当前的工作模式(点火、诊断激活、休眠准备、OTA 刷写)在运行时把某些路径关掉或打开。
控制的最小单位是路由路径组(PduRRoutingPathGroup),不是单个 PDU。

- PduR_EnableRouting(GroupId):激活指定的路由路径组,组内所有 PDU 恢复转发。
- PduR_DisableRouting(GroupId):禁用指定的路由路径组,组内所有 PDU 被丢弃。
- 每个组在配置里有 PduRIsEnabledAtInit,决定初始化之后它是开还是关。
- 没被任何路由路径组关联的 PduRDestPdu,初始化之后一直是使能状态,运行时改不了。 想动态开关,就得把它挂进某个组。
两个典型用法:
被禁用的路径直接丢弃,不报错也不回调。这点和配置错误的表现一样,排障时容易被绕进去——所以查问题时要先确认组的使能状态,再怀疑 ID 配错。
5. 配置模型与 Handle ID 映射
这部分是 PduR 配置里最需要先搞懂的东西。
5.1 Global PDU:一个 PDU 只有一份定义
在 ECU 配置里,每个 PDU 对应一个全局的 System_Pdu 容器。Com、PduR、CanIf、CanTp 各自那份"本地 PDU"配置,最后都指向同一个全局对象。
这么做解决的是配置依赖:配 PduR 的时候只需要引用全局 PDU,不用等 CanIf 或 COM 的内部容器都建好。工具链可以并行推进各个模块的配置。
代价也要知道:改这个 Pdu 的长度,会同时影响 COM 那边的信号布局和 CanIf 那边的 DLC。 工具链不会告诉你"你改的这个长度影响了哪些模块",得自己盯住。
5.2 一条 PduRRoutingPath 的组成
路由表就是一堆 PduRRoutingPath 条目,每条定义一条从源到目标的单向转发规则。没有路由表,PduR 就是个空壳,所有 PDU 进来都找不到出口。

- PduRSrcPdu:源。里面是 SrcPduRef(引用哪个全局 Pdu)和源侧的 Handle ID。
- PduRDestPdu:目标,可以有一个或多个。里面是 DestPduRef(同样引用那个全局 Pdu)、目标侧的 Handle ID、PduRDestPduDataProvision(Direct 还是 TriggerTransmit),以及网关或触发发送场景才需要的 PduRQueueDepth。
上下行要成对配。 只配了 Com → CanIf 没配 CanIf → Com,总线上能看到报文,但应用层读不到新值。这是 PduR 配置里出现频率最高的一种错误。
5.3 Handle ID 是怎么换的
每条调用都涉及两套编号:调用方那边有一套,被调方那边有一套。中间靠 PduR 的路由表对接。

以 CAN 总线上收到一条引擎转速报文为例:
顺带一步也在查表时完成:检查这条路径所属的路由路径组是不是使能状态。
两个编号之间没有算术关系,17 和 3 之间不存在加减乘除的换算,全靠路由表映射。所以改完配置必须重新生成两边的代码,只改一边会出现"句柄对不上、数据送到别的模块去了"这种很难查的问题。
6. 用纯 C 模拟一下查表转发
把上面的机制落成代码,下面这段可以直接编译运行,方便对着看 PduR 内部到底做了什么。
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
/* 1. 通信栈基本类型(实际工程里由 ComStack_Types.h + ComStack_Cfg.h 提供) */
typedef uint16_t PduIdType;
typedef uint16_t PduLengthType;
typedef struct {
uint8_t* SduDataPtr;
uint8_t* MetaDataPtr;
PduLengthType SduLength;
} PduInfoType;
/* 2. 上层接收回调的函数指针类型 */
typedef void (*PduR_RxIndicationFctPtr)(PduIdType RxPduId, const PduInfoType* PduInfoPtr);
/* 3. 一条路由表项 */
typedef struct {
PduIdType TargetPduId; /* 换出来的目标 Handle ID */
PduR_RxIndicationFctPtr RxIndicationFunc; /* 目标模块的回调 */
uint16_t RoutingPathGroupId;/* 所属路由路径组 */
bool IsEnabled; /* 运行时使能状态,BswM 控制 */
} PduR_RxRoutingTableType;
/* 模拟上层的两个接收回调 */
void Com_RxIndication(PduIdType ComRxPduId, const PduInfoType* PduInfoPtr) {
printf("[COM] 收到 PDU (HandleID: %d), 长度: %d, Data[0]: 0x%02X\\n",
ComRxPduId, PduInfoPtr->SduLength, PduInfoPtr->SduDataPtr[0]);
}
void Dcm_RxIndication(PduIdType DcmRxPduId, const PduInfoType* PduInfoPtr) {
printf("[DCM] 收到诊断 PDU (HandleID: %d), 长度: %d\\n",
DcmRxPduId, PduInfoPtr->SduLength);
}
/* 4. 静态路由表:下标就是 CanIf 那边的句柄编号 */
#define ROUTING_GROUP_APP 1
#define ROUTING_GROUP_DIAG 2
#define RX_TABLE_SIZE (sizeof(PduR_CanIfRxRoutingTable) / sizeof(PduR_RxRoutingTableType))
PduR_RxRoutingTableType PduR_CanIfRxRoutingTable[] = {
/* [0] CanIfRxPduId = 0 -> ComRxPduId = 10(应用组,初始使能) */
{ .TargetPduId = 10, .RxIndicationFunc = Com_RxIndication,
.RoutingPathGroupId = ROUTING_GROUP_APP, .IsEnabled = true },
/* [1] CanIfRxPduId = 1 -> DcmRxPduId = 100(诊断组,初始禁用) */
{ .TargetPduId = 100, .RxIndicationFunc = Dcm_RxIndication,
.RoutingPathGroupId = ROUTING_GROUP_DIAG, .IsEnabled = false },
};
/* 5. PduR 接收入口(由 CanIf 调用) */
void PduR_CanIfRxIndication(PduIdType RxPduId, const PduInfoType* PduInfoPtr) {
PduR_RxRoutingTableType* route;
/* 下标越界检查:实际工程里靠 DET 报错 */
if (RxPduId >= RX_TABLE_SIZE) {
printf("[PduR] 无效的 RxPduId: %d,丢弃\\n", RxPduId);
return;
}
route = &PduR_CanIfRxRoutingTable[RxPduId];
/* 路由路径组的使能检查 */
if (!route->IsEnabled) {
printf("[PduR] RxPduId %d 所属路径组被禁用,丢弃\\n", RxPduId);
return;
}
/* 查表转发 */
if (route->RxIndicationFunc != NULL) {
route->RxIndicationFunc(route->TargetPduId, PduInfoPtr);
}
}
/* 6. BswM 用来开关路由路径组 */
void PduR_SetRouting(uint16_t GroupId, bool enable) {
size_t i;
for (i = 0; i < RX_TABLE_SIZE; i++) {
if (PduR_CanIfRxRoutingTable[i].RoutingPathGroupId == GroupId) {
PduR_CanIfRxRoutingTable[i].IsEnabled = enable;
printf("[PduR] 路由路径组 %d 已%s\\n", GroupId, enable ? "使能" : "禁用");
}
}
}
/* 7. 跑一下 */
int main(void) {
uint8_t payload[8] = {0xAB, 0x12, 0x34, 0x56, 0x78, 0x90, 0xEF, 0x00};
PduInfoType pdu = { .SduDataPtr = payload, .SduLength = 8, .MetaDataPtr = NULL };
printf("— 场景 1:常规应用报文(CanIfRxPduId = 0) —\\n");
PduR_CanIfRxIndication(0, &pdu);
printf("\\n— 场景 2:诊断报文(CanIfRxPduId = 1,初始被禁用) —\\n");
PduR_CanIfRxIndication(1, &pdu);
printf("\\n— 场景 3:BswM 打开诊断组,再发一次 —\\n");
PduR_SetRouting(ROUTING_GROUP_DIAG, true);
PduR_CanIfRxIndication(1, &pdu);
return 0;
}
这段代码把三个要点露了出来:下标越界的检查、路由路径组的使能检查、以及"查表拿函数指针再调用"这个动作本身。真实的 PduR 生成代码比这复杂得多(发送侧还要处理网关缓冲、Fan-out 多目标、TP 的复制回调),但主干就是这个样子。
7. 几个常见故障
| 总线上有报文,上层 COM 或 DCM 收不到 | 1. BswM 没调 PduR_EnableRouting,路由路径组还关着。2. PduRSrcPdu 的 Handle ID 和 CanIfCanRxPduId 对不上。 | 在 PduR_CanIfRxIndication 下断点,看传进来的 RxPduId 在不在路由表里;再检查这条路径所属组的使能状态。 |
| 长报文收到一半中断,返回 BUFREQ_E_NOT_OK | 上层在 StartOfReception 阶段就没能申请到接收缓冲区,可能是缓冲区配小了,也可能是上一次诊断请求没释放。 | 看 StartOfReception 的返回值和 TpSduLength 对不对得上,再核对 DCM 的 DcmDslBufferSize。 |
| 跨总线网关转发的报文,目标侧末尾数据不对 | 源总线 PDU 比目标总线长,超出的部分被截断了。 | 对比源侧和目标的报文长度,检查目标 I-PDU 配置的长度,以及带缓冲时的 PduRPduMaxLength。 |
| 初始化阶段就复位,定位到空指针 | PduR 的配置结构体没传进来,或者 PduR_Init 调的时机太晚。 | 检查 EcuM_Init / BswM_Init 里的调用顺序,确保 PduR_Init 在任何接收中断被使能之前就执行完。 |
8. 一页小结

如果只记五条,就记这五条:
想再往下深入,建议按这个顺序读规范:先看 SWS_PDURouter 第 9 章的序列图(收 / 发 / 网关各一条),再回来对着第 10 章的配置容器看一遍 PduRRoutingPath、PduRSrcPdu、PduRDestPdu 这三个容器的参数。把一条真实的路由路径从配置到运行时代码串一遍,比看十遍文字管用。




