商用车CAN高层协议J1939实战:29位标识符与PGN多包传输七步讲透

CAN 2.0B 只规定到"帧怎么发、出错怎么重传",至于 ID 代表什么含义、超过 8 字节的数据怎么拆、两个节点抢同一个地址怎么办,它一概没管。SAE J1939 就是补这一层的标准:强制使用 29 位扩展帧,把标识符切成六段,用 3 字节 PGN 给每条报文编身份,长数据走 TP.CM/TP.DT 多包传输。本文用四张表讲清位分配、PGN 换算、BAM 与 RTS/CTS 时序和初始化落地。

2026-10-07
J1939CAN总线PGN多包传输商用车工程机械

商用车、工程机械和农业装备上,发动机、变速箱、制动、仪表往往来自不同厂家,它们要在一根 CAN 总线上互认。CAN 2.0B 只规定到"帧怎么发、出错怎么重传",至于"这个 ID 代表什么含义、超过 8 字节的数据怎么拆、两个节点抢同一个地址怎么办",一概没管。SAE J1939 补的就是这一层:它要求所有设备必须使用 29 位扩展帧,把标识符切成优先级、保留位、数据页、PDU 格式、特定 PDU、源地址六段,用 3 字节的 PGN(参数组编号) 给每条报文编身份,再用传输协议把长数据拆成多包。三条结论先记住:第一,J1939 的标识符不是"地址",而是"报文身份 + 源地址"的组合;第二,PF 值 240 是分水岭,小于 240 是点对点(PDU1),大于等于 240 是广播(PDU2);第三,超过 8 字节必须走传输协议,广播用 BAM,点对点用 RTS/CTS 握手。下面用四张表把它讲透。

一、CAN 2.0B 没管的,J1939 补上了

很多人以为"会用 CAN 就会用 J1939",实际上两者管的层次差得很远。CAN 2.0B 是数据链路层规范,它保证帧能可靠地传出去、出错能重传;J1939 是建立在它之上的应用层与网络层约定,管的是"这条报文是谁发的、发给谁、什么含义、多长、优先级多少"。两者的分工可以用一张表看清。

对比维度 CAN 2.0B SAE J1939
所属层次 数据链路层 / 物理层 网络层 + 应用层
标识符用法 标识通信数据块,不使用节点地址 标识符内必须含源地址,网络层用节点地址寻址
帧格式 标准帧(11 位)与扩展帧(29 位)并存 设备必须使用扩展帧,标准帧仅作专用消息
报文含义 不定义,由用户自行约定 用 PGN 唯一定义报文用途,用 SPN 定义其中的参数
数据长度 0~8 字节 单帧 0~8 字节,多包可达 1785 字节
节点身份 不涉及 每个 ECU 有 64 位 NAME,靠地址声明加入网络

换句话说,CAN 保证"路上不撞车",J1939 保证"大家说同一种话"。这决定了 J1939 设备比普通 CAN 设备多出一整套网络管理逻辑:上电要声明地址、收到请求要应答、长报文要拆包重组。下面从标识符开始逐层拆解。

二、29 位标识符怎么切:六段位分配

J1939 的扩展帧标识符有 29 位,它被切成六段,每段都有明确含义。理解这张位图,后面所有内容都是它的推论。

优先级 P 3 位 保留 R 1 位 = 0 数据页 DP 1 位 PDU 格式 PF 8 位 特定 PDU PS 8 位 源地址 SA 8 位 位 28 位 0 PGN(3 字节,24 位)= 数据页 + PDU 格式 + 特定 PDU,高位补 0 PF≤239 时 PS 是目标地址(PDU1);PF≥240 时 PS 是组扩展(PDU2)
图1:29 位标识符的六段划分,其中 DP、PF、PS 三段构成 PGN
字段 位数 作用 关键约定
优先级 P 3 仅用于总线仲裁时决定谁先发 0 最高、7 最低;接收方对优先级做全局屏蔽(忽略)
保留 R 1 SAE 保留位,用于将来扩展 传输时必须置 0
数据页 DP 1 选择参数组描述的页 0 或页 1 页 0 分配完才启用页 1
PDU 格式 PF 8 确定 PDU 格式,同时是 PGN 的一部分 PF≤239 为 PDU1,PF≥240 为 PDU2
特定 PDU PS 8 由 PF 决定含义 PDU1 时是目标地址 DA,PDU2 时是组扩展 GE
源地址 SA 8 发送该报文的 ECU 地址 同一网络内每个地址必须唯一

一句话记住位分配:优先级管"谁先发",PF 管"是点对点还是广播",PS 管"发给谁或哪一组",SA 管"谁发的"。PGN 只由中间的数据页、PF、PS 三段拼成,不含源地址——这正是 J1939"报文身份与节点身份分离"的设计。

优先级默认值有规律可循:所有控制消息缺省优先级是 3,其余信息、专用、请求和 ACK 消息缺省优先级是 6。总线通信量变化时,OEM 可以重新调整优先级,所以优先级域应当设计成可重编程的,不要写死在代码里。

三、PGN:给每条报文发一张身份证

PGN 是一个 3 字节(24 位)的值,唯一标识一个参数组的用途。它的构成与标识符是同一套数据,只是不含源地址:PGN = 数据页 DP + PDU 格式 PF + 特定 PDU PS,高位保留补 0。换算时有一条铁律:当 PF 的值小于 240(PDU1 格式)时,PGN 的最后 8 位固定为 0,因为此时 PS 段被目标地址占用,不再属于 PGN 的一部分。

由此,PS 段的含义随 PF 分岔,这是 J1939 最容易记混的一处,用一张表对照最清楚。

格式 PF 取值 PS 段的含义 通信方式 PGN 的 PS 段
PDU1 格式 0 ~ 239 目标地址 DA(可以是特定地址或全局 255) 可点对点,也可请求后主动发送 固定为 0,不含目标地址
PDU2 格式 240 ~ 255 组扩展 GE,用来细分同一 PF 下的报文 只能作为全局广播 等于 GE,参与 PGN 编号

PGN 的取值区间也有明确的划分,尤其是 100ms 这个时间分界——它决定了某类报文允不允许拆成多包。

PGN 区间(十六进制) 格式 典型周期 是否允许拆多包
000000₁₆ ~ 0000FF₁₆ PDU1 100ms 或更短 禁止
000100₁₆ ~ 00EE00₁₆ PDU1 100ms 或更长 允许
00EF00₁₆ PDU1 专用 用户自定义 允许
00F000₁₆ ~ 00FEFF₁₆ PDU2 100ms 或更短 禁止
00FF00₁₆ ~ 00FFFF₁₆ PDU2 专用 用户自定义 允许
010000₁₆ ~ 01FFFF₁₆ 页 1(DP=1) 预留扩展 按定义

举个具体例子把换算走一遍。电子发动机控制器 #2(EEC2) 的定义是:数据页 0、PDU 格式 PF=240、PDU 专用 PS=3、默认优先级 3、数据长度 8 字节、传输循环 50ms。因为 PF=240 属于 PDU2,PS 是组扩展,所以它的 PGN = 0x00F000 + 0x03 = 0xF003 = 61443。若取源地址 SA=0x00,它对应的 29 位标识符按"优先级→保留位→数据页→PF→PS→SA"拼接,就是 0x0CF00300。报文身份(PGN 61443)和节点身份(SA)在这里各归各位,一目了然。

四、网络管理:请求、应答与地址声明

J1939 定义了几条"公共报文",任何节点都要能处理,它们是网络能自组织起来的基础。

请求 PGN(PGN 59904)

请求报文用来向一个或多个设备索取某个参数组。它的数据页 0、PF=234、PS 是目标地址,默认优先级 6,数据场就是 3 个字节的"被请求 PGN"。规则有两条硬约束:对特定目标地址的请求,对方必须响应,即使不支持该 PGN 也要回一条否定应答(NACK);而 对全局地址的请求,响应也发到全局,且不支持时禁止发 NACK——否则一个请求会引发全网设备同时回 NACK。请求频率官方建议每秒不超过 2~3 次,太频繁会把总线占满。

确认 PGN(ACK)

确认报文用数据场第 1 个字节的"控制字节"表达结果,含义只有四种。

控制字节 含义 使用场合
0 肯定确认(ACK) 请求的动作已被理解并完成
1 否定确认(NACK) 不支持被请求的 PGN,仅用于对特定地址的请求
2 拒绝访问 PGN 支持但被拒绝执行
3 无法响应 PGN 支持,但 ECU 忙、稍后重新请求

地址声明(PGN 60928)

每个 J1939 设备有一个 64 位 NAME,上电后先声明"我要用这个地址"。如果两个节点声明了同一个地址,就要比 NAME:NAME 值较小的节点保留地址,另一个必须让出。让出地址的节点此后只能发"无法声明地址"报文,并且只能收全局地址或广播报文。这就是为什么 J1939 设备的地址可以配置、也可以做成"任意地址能力"——只要 NAME 全局唯一,网络就能自动收敛。地址声明 PGN 是 60928(0x00EE00),PF=238,属于 PDU1。

五、超过 8 字节:TP.CM 与 TP.DT 多包传输

CAN 单帧数据场最多 8 字节,J1939 要传 9 到 1785 字节的参数组,就必须拆包。承担这件事的是两个 PGN:传输协议—连接管理 TP.CM(60416,PF=236) 负责"打商量",传输协议—数据传送 TP.DT(60160,PF=235) 负责"搬数据"。两者默认优先级都是 7,数据场都是 8 字节,PS 段都填目标地址(BAM 场景填全局地址 255)。

多包传输分两条路线,先选路线再拆包,这是设计的第一个决策点。

对比维度 BAM(广播公告) RTS/CTS(点对点)
目标地址 全局地址 255,一对多 特定目标地址,一对一
握手 无,公告后直接连发数据包 有,RTS 请求、CTS 允许、结束应答确认
流控 无,发送方自己控制节奏 由 CTS 指定"下一批发几个包"
重传 不支持 CTS 可要求重发指定包
典型用途 广播长数据,如整车故障列表 点对点长数据,如诊断/标定数据

TP.CM 用数据场第 1 字节的"控制字节"区分五种消息,字节排布各不相同。这张表是写收发状态机时最常查的一张。

消息类型 控制字节 数据场字节排布(字节 1 → 8)
TP.CM_RTS(请求发送) 16 控制字节、整个消息字节数(2B)、全部数据包数、FF₁₆、打包消息 PGN(3B)
TP.CM_CTS(准备发送) 17 控制字节、可发送数据包数、下一个包编号、FF₁₆、FF₁₆、打包消息 PGN(3B)
TP.CM_EndOfMsgAck(结束应答) 19 控制字节、整个消息字节数(2B)、全部数据包数、FF₁₆、打包消息 PGN(3B)
TP.CM_Abort(放弃连接) 255 控制字节、FF₁₆、FF₁₆、FF₁₆、FF₁₆、打包消息 PGN(3B)
TP.CM_BAM(广播公告) 32 控制字节、整个消息字节数(2B)、全部数据包数、FF₁₆、打包消息 PGN(3B)

数据包本身由 TP.DT 承载:字节 1 是序列号(从 1 开始),字节 2~8 是 7 个字节的有效数据,最后一个包不足 7 字节时,空余字节填 FF₁₆。因为每包只能装 7 字节,所以 255 个包的上限对应 1785 字节的最大消息长度。超时与流控参数是可靠性的关键:响应方必须在 0.20 秒(Tr)内应答,等待方在放弃或重试前至少要等 1.25 秒(T3);点对点传输中数据包之间的间隔为 0~200ms,若需要暂停数据流,响应方要每 0.5 秒(Th)重发一次 CTS 维持连接。BAM 没有握手,接收方全靠公告报文里的"总字节数、总包数"来重组,因此发送方必须控制好包间隔。

一个容易被忽略的限制:TP.DT 报文里只有源地址和目标地址,不含 PGN。所以同一时刻,一个发送者对同一目标地址只能有一个 RTS/CTS 会话,也只能发一个 BAM 消息。要支持并发,就得靠"目标地址"来区分不同会话——这也是为什么规范强调发送方与接收方都要用源地址加目标地址共同校验。

六、落地:从 PGN 到帧的代码骨架

把上面的规则翻译成代码,核心只有两个函数:一个是"PGN 加优先级、目标地址、源地址拼成 29 位标识符",一个是"把长数据按 BAM 拆包发出"。下面这段 C 代码可以直接移植到 STM32 bxCAN 或外扩 MCP2515 的驱动里,其中 can_send() 与 delay_ms() 是占位符,按你的硬件替换。

/* J1939 标识符与 PGN 互转(页 0,保留位 R = 0)
 * PGN 24 位 = 数据页 DP + PDU 格式 PF + 特定 PDU PS,高位补 0
 * 29 位 ID = P(位28~26) + R(位25) + DP(位24) + PF(位23~16) + PS(位15~8) + SA(位7~0)
 */
#define J1939_PDU1_MAX_PF   239u          /* PF <= 239 为 PDU1(PS = 目标地址) */

/* PGN -> 29 位标识符;dest 仅 PDU1 有效,PDU2 时被忽略 */
unsigned long j1939_pgn_to_id(unsigned long pgn, unsigned char priority,
                              unsigned char dest, unsigned char src)
{
    unsigned char pf = (pgn >> 8) & 0xFF;      /* PDU 格式 */
    unsigned char ps = pgn & 0xFF;             /* PDU1=目标地址 / PDU2=组扩展 */
    unsigned char dp = (pgn >> 16) & 0x01;     /* 数据页 */
    unsigned long id;

    if (pf <= J1939_PDU1_MAX_PF) {
        ps = dest;                             /* PDU1:PS 段改填目标地址 */
    }
    id  = ((unsigned long)(priority & 0x07)) << 26;   /* 优先级 P */
    id |= ((unsigned long)dp) << 24;                  /* 数据页 DP */
    id |= ((unsigned long)pf) << 16;                  /* PDU 格式 PF */
    id |= ((unsigned long)ps) << 8;                   /* 特定 PDU PS */
    id |= (unsigned long)src;                         /* 源地址 SA */
    return id;
}

/* 用 BAM 广播一条长消息(数据长度 9~1785 字节)
 * 顺序:先发 TP.CM_BAM 公告,再依次发 TP.DT 数据包
 */
#define J1939_PGN_TPCM   60416UL    /* 0x00EC00 传输协议-连接管理 */
#define J1939_PGN_TPDT   60160UL    /* 0x00EB00 传输协议-数据传送 */

void j1939_send_bam(unsigned long pgn, const unsigned char *buf,
                    unsigned short len, unsigned char src)
{
    unsigned char total_pkt = (len + 6) / 7;      /* 每包 7 字节有效数据 */
    unsigned char frame[8];
    unsigned short i, n = 0;
    unsigned char seq;

    /* ① TP.CM_BAM:控制字节 32,公告总字节数与总包数 */
    frame[0] = 32;                                /* 广播公告消息 */
    frame[1] = (unsigned char)(len & 0xFF);       /* 总字节数 LSB */
    frame[2] = (unsigned char)(len >> 8);         /* 总字节数 MSB */
    frame[3] = total_pkt;                         /* 全部数据包数 */
    frame[4] = 0xFF;                              /* 保留字节,固定 FF */
    frame[5] = (unsigned char)(pgn & 0xFF);       /* 打包消息 PGN LSB */
    frame[6] = (unsigned char)((pgn >> 8) & 0xFF);
    frame[7] = (unsigned char)((pgn >> 16) & 0xFF);
    can_send(j1939_pgn_to_id(J1939_PGN_TPCM, 7, 0xFF, src), frame, 8);

    /* ② 依次发送 TP.DT:字节1 为序列号(从 1 起),字节2~8 为 7 字节数据 */
    for (seq = 1; seq <= total_pkt; seq++) {
        frame[0] = seq;
        for (i = 1; i < 8; i++) {
            frame[i] = (n < len) ? buf[n++] : 0xFF;   /* 不足补 FF */
        }
        can_send(j1939_pgn_to_id(J1939_PGN_TPDT, 7, 0xFF, src), frame, 8);
        delay_ms(50);   /* 包间需留间隔,本文取 50ms,按总线负载调整 */
    }
}

两个细节值得强调。第一,PDU1 与 PDU2 在拼 ID 时的分支只影响 PS 段:PDU1 把 PS 换成目标地址,PDU2 原样使用组扩展;如果忘了这个分支,PDU1 报文会被发成广播。第二,TP.CM 与 TP.DT 都填全局地址 255 才能构成 BAM,若换成特定地址就变成 RTS/CTS 流程,需要额外实现 CTS 应答与结束应答状态机。

七、六个最容易踩的坑

现象 根因 处理
自研设备发不出去、别人不理 用了标准帧(11 位) J1939 设备必须使用 29 位扩展帧
点对点报文变成全网广播 PDU1 拼 ID 时 PS 段仍填了组扩展 PF≤239 时 PS 段改填目标地址 DA
全局请求引发满总线 NACK 对全局请求回了否定应答 全局请求不支持时禁止发 NACK
两个节点同地址、网络混乱 未做地址声明,或未处理地址冲突 实现 NAME 比较仲裁,小者保留地址
长报文重组失败、丢包 TP.DT 序列号错、空余字节未填 FF,或未按 T3 超时 序列号从 1 起连续递增,尾部补 FF₁₆,超时按 1.25s 处理
高优先级报文发不出去 把优先级当成固定值写死,未按报文类型设置 控制消息缺省 3、其余 6、传输协议 7,并留出可重编程接口

八、总结

J1939 的价值在于给 CAN 总线补上"说同一种话"的规则:强制 29 位扩展帧,把标识符切成优先级、保留位、数据页、PDU 格式、特定 PDU、源地址六段;用 3 字节 PGN 唯一标识报文用途,PF 值 240 是 PDU1 与 PDU2 的分水岭;超过 8 字节的数据交给 TP.CM/TP.DT,广播走 BAM、点对点走 RTS/CTS。

落地记住三点:PDU1 拼 ID 时 PS 要换成目标地址、PGN 不含源地址、长报文按 1785 字节上限与 T3=1.25s 超时设计。先把 PGN 表和地址规划做出来,再写收发状态机,商用车与工程机械这类多厂家协作的网络才谈得上可靠互通。

还想接着看

本文基于《SAE J1939标准下的汽车CAN通讯报文/帧格式》《J1939 C Library for PIC16 Microcontrollers and MCP2515 User's Guide》以及《商用车控制系统局域网络(CAN)通信协议 第4部分:数据链路层(征求意见稿,对应 SAE J1939-21)》等技术资料整理优化,并结合商用车与工程机械 CAN 网络的工程实践进行了系统化梳理。

需要定制开发?

沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付

立即微信咨询

电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应

🎧 本文已制作播客节目
双主持对话音频,随时随地收听本文内容
收听播客 →