简介:面向嵌入式开发与CAN总线学习者的SJA1000控制器资料包,聚焦PeliCAN模式下的驱动开发与协议理解。资源内容围绕SJA_PeliCan.h头文件展开,该头文件定义了SJA1000在PeliCAN工作模式下的寄存器布局、控制位、模式选择宏、函数原型及常用常量,覆盖从芯片初始化、位定时器配置、错误状态管理到接收FIFO读取与报文发送的完整操作接口。通过解析这些定义,开发者能够深入理解SJA1000与PeliCAN的配合机制,掌握增强型错误处理、灵活位时间配置等高级特性,从而在汽车电子、工业自动化等CAN总线应用场景中编写出稳健的驱动程序。压缩包仅含1个头文件,大小3KB,轻量精炼,既可直接嵌入工程,也可作为对照芯片手册逐项学习的代码注释。已有129人浏览学习,适合具备基础MCU知识、正在调试SJA1000驱动或准备移植CAN协议栈的软硬件工程师。
1. PeliCAN 头文件不是抄来就能用的:SJA1000 的寄存器视图决定一切
拿到网上流传的SJA_PeliCan.h,先别急着往工程里塞。这份头文件看起来就是一堆寄存器宏和结构体,但真正决定 CAN 节点能不能跑起来的,是那些宏背后的位布局:PeliCAN 模式下 SJA1000 的帧 ID 在缓冲区里不是连续存放的,标准帧的 11 位 ID 被拆成高 8 位、低 3 位两截,打包错一位,报文上总线后就变成了另一个 ID。另一个常被忽略的事实是,SJA1000 走的是并行总线接口——片选、地址锁存、读、写、中断一个都不能少,指望像 MCP2515 那样用 SPI 轻声细语地对话,硬件上就走不通。做工业 CAN 节点、给老平台加 CAN 口、或者把 Linux 内核的 sja1000.c 移植到裸机,都得先过寄存器这一关。这篇就把模式切换、复位时序、帧打包、验收滤波这些真正决定收发成败的细节拆开讲。
2. PeliCAN 模式寄存器视图与复位模式的写保护
2.1 BasicCAN 与 PeliCAN:同一颗芯片的两种寄存器视图
SJA1000 不是两颗芯片,而是同一颗控制器通过时钟分频寄存器(CDR,地址 0x1F)的 bit7 切换成两种完全不同的寄存器视图。CDR.7=0 时是 BasicCAN 模式,只支持 CAN 2.0A 的标准帧;CDR.7=1 时进入 PeliCAN 模式,才支持 29 位扩展帧。PeliCAN 这个名字不是新硬件,而是 Philips 给增强型 CAN 控制器协议起的产品代号,社区里流传的头文件也几乎都是为 PeliCAN 模式准备的。
| 对比项 | BasicCAN 模式 | PeliCAN 模式 |
|---|---|---|
| CAN 2.0B 扩展帧 | 不支持 | 支持,29 位 ID |
| 接收 FIFO | 单缓冲 | 64 字节 FIFO,可连续收多帧 |
| 错误寄存器 | 只读且信息少 | RXERR/TXERR 独立可读,带 ECC 错误捕获 |
| 发送重试 | 固定自动重发 | 可选单次发送模式 |
| 中断源 | 较少 | 更细,发送/接收/错误/溢出独立 |
| 切换方式 | CDR.7=0 | CDR.7=1 |
在做新旧平台兼容时,最容易踩的坑就是把 BasicCAN 驱动里的寄存器地址表原样搬到 PeliCAN 上。同一个地址 0x06,BasicCAN 下是 BTR0,PeliCAN 下变成了 RXERR;同理 0x07 从 BTR1 变成 TXERR。如果头文件里同时出现两种模式的宏定义,一定要在初始化时确认 CDR.7 的状态再选寄存器表。
2.2 头文件里那串宏:地址、位定义和 0x1F 的隐藏开关
一份合格的SJA_PeliCan.h,核心应该是 PeliCAN 模式的寄存器地址和位定义。下面这段就是按 SJA1000 数据手册 PeliCAN 寄存器映射整理的典型写法,网上流传的头文件结构基本一致,只是宏名略有差异。
/* SJA1000 PeliCAN 模式寄存器地址映射 */ #define SJA_MOD 0x00 /* Mode 模式控制 */ #define SJA_CMR 0x01 /* Command 命令 */ #define SJA_SR 0x02 /* Status 状态 */ #define SJA_IR 0x03 /* Interrupt 中断 */ #define SJA_IER 0x04 /* Interrupt Enable 中断使能 */ #define SJA_RXERR 0x06 /* RX 错误计数 */ #define SJA_TXERR 0x07 /* TX 错误计数 */ #define SJA_ACR0 0x10 /* 验收代码寄存器 0 */ #define SJA_ACR1 0x11 /* 验收代码寄存器 1 */ #define SJA_AMR0 0x14 /* 验收屏蔽寄存器 0 */ #define SJA_AMR1 0x15 /* 验收屏蔽寄存器 1 */ #define SJA_BTR0 0x18 /* 波特率定时寄存器 0 */ #define SJA_BTR1 0x19 /* 波特率定时寄存器 1 */ #define SJA_OCR 0x1A /* 输出控制寄存器 */ #define SJA_CDR 0x1F /* 时钟分频寄存器 */ /* 常用位定义 */ #define MOD_RM 0x01 /* 复位模式请求 */ #define MOD_STM 0x04 /* 自测模式 */ #define MOD_AFM 0x08 /* 验收滤波模式:1=单滤波 */ #define CMR_TR 0x04 /* 发送请求 */ #define CMR_RRB 0x08 /* 释放接收缓冲 */ #define CMR_SRR 0x20 /* 单次发送请求 */ #define SR_RBS 0x01 /* 接收缓冲状态,1=有帧 */ #define SR_TBS 0x04 /* 发送缓冲状态,1=可写 */ #define SR_TCS 0x08 /* 发送完成状态 */ /* 收发帧缓冲区起始地址 */ #define SJA_TXBUF 0x10 #define SJA_RXBUF 0x10这里SJA_TXBUF和SJA_RXBUF在 PeliCAN 模式下都是 0x10,读方向是接收 FIFO 的当前帧窗口,写方向是发送缓冲。0x06、0x07 这两个地址要特别留意,只有在 PeliCAN 模式下才是错误计数器,BasicCAN 模式对应位定时器。SJA_CDR放在最后一位不是巧合,这个寄存器比看起来重要:bit7 决定 BasicCAN 还是 PeliCAN,bit3 控制 CLKOUT 引脚是否输出时钟。
2.3 复位模式为什么是初始化的一切前置条件
SJA1000 上电后默认处于 BasicCAN 模式,而且大部分配置寄存器只能在复位模式下写入。所谓复位模式不是 MCU 复位,而是把 CAN 控制器的通信接口关掉,让寄存器回到可配置状态。具体做法是往MOD寄存器写MOD_RM(bit0=1),并等待状态寄存器确认,之后才能改波特率、验收滤波、输出控制、时钟分频这些关键寄存器。配置完成后清掉MOD_RM,控制器才进入工作模式并开始参与总线通信。
/* 进入复位模式 */ static void sja_enter_reset(volatile uint8_t *regs) { regs[SJA_MOD] |= MOD_RM; /* 置位 RM,请求进入复位模式 */ while ((regs[SJA_SR] & 0x04) == 0) { /* 等待 SR.TBS=1,表示复位就绪 */ /* TBS 置位说明控制器已停止总线活动 */ } } /* 退出复位模式 */ static void sja_exit_reset(volatile uint8_t *regs) { regs[SJA_MOD] &= ~MOD_RM; /* 清 RM,进入工作模式 */ while (regs[SJA_MOD] & MOD_RM) { /* 等待硬件确实退出复位 */ /* 空循环,确保后续操作不会追尾 */ } }这段代码里有两个容易被忽略的点。第一,进入复位模式的等待条件用SR.TBS而非直接读MOD,因为一些 SJA1000 克隆芯片置位 RM 后需要两个 CAN 时钟周期才真正切换寄存器映射,此时写 BTR0 可能写进的是旧地址。第二,退出复位模式后,接受滤波器立即生效,如果 ACR/AMR 还没配好,总线上一帧匹配数据就可能先进 FIFO,导致后续逻辑读到脏帧。所以标准顺序是:进复位、配 CDR/BTR/OCR/ACR/AMR、最后才清 RM。头文件里如果只给你一组寄存器宏而没有这个顺序说明,那份驱动大概率会在低波特率或高负载总线上出问题。
3. 帧缓冲区布局与发送实现:ID 打包错一位,报文就换了一辆车
3.1 PeliCAN 固定 13 字节帧槽与缓冲区偏移
PeliCAN 模式下,无论标准帧还是扩展帧,发送和接收缓冲区都固定占 13 字节:1 字节帧信息、4 字节 ID、8 字节数据。标准帧只使用前两个 ID 字节,后两个保留字节写 0,但数据区位置不随之前移。这一点和很多 CAN 控制器不同,SJA1000 用固定槽位换来了简单的 FIFO 管理,代价是标准帧在总线上多占几个字节的 SRAM。
| 偏移 | 标准帧含义 | 扩展帧含义 |
|---|---|---|
| 0x00 | 帧信息:FF=0、RTR、DLC | 帧信息:FF=1、RTR、DLC |
| 0x01 | ID.10 ~ ID.3 | ID.28 ~ ID.21 |
| 0x02 | ID.2 ~ ID.0(bit7-5),低 5 位为 0 | ID.20 ~ ID.13 |
| 0x03 | 保留,写 0 | ID.12 ~ ID.5 |
| 0x04 | 保留,写 0 | ID.4 ~ ID.0(bit7-3),低 3 位为 0 |
| 0x05 ~ 0x0C | 数据字节 1 ~ 8 | 数据字节 1 ~ 8 |
帧信息字节的低 4 位是 DLC(数据长度),bit5 在标准帧下保留为 0,bit6 是 RTR(远程帧标志),bit7 是 FF(扩展帧标志)。这个布局和 Linux 内核里drivers/net/can/sja1000/sja1000.c的读写逻辑一致,也是验证头文件是否完整的最快方式:打开SJA_PeliCan.h,找到帧信息结构的位定义,如果 FF/RTR/DLC 的位移和上面这张表对不上,说明头文件可能混入了 BasicCAN 定义。
3.2 标准帧与扩展帧的 ID 打包与解包
拿到头文件里的发送结构体,很多人会直觉地把 11 位 ID 直接塞进两个字节——这是最常见的错误。SJA1000 的 ID 在缓冲区里是左对齐排列的,标准帧 ID 高 8 位放在 0x01,低 3 位放在 0x02 的高 3 位。
/* 标准帧 ID 打包:写入缓冲区 */ static void sja_pack_std_id(uint8_t *buf, uint32_t id, uint8_t rtr, uint8_t dlc) { buf[0] = (rtr ? 0x40 : 0) | (dlc & 0x0F); /* bit7 FF=0,bit6 RTR */ buf[1] = (id >> 3) & 0xFF; /* ID.10 ~ ID.3 高 8 位 */ buf[2] = (id & 0x07) << 5; /* ID.2 ~ ID.0 移到 bit7-5 */ buf[3] = 0x00; /* 保留字节清零 */ buf[4] = 0x00; } /* 扩展帧 ID 打包:4 字节,每字节搬运 8 位 */ static void sja_pack_ext_id(uint8_t *buf, uint32_t id, uint8_t rtr, uint8_t dlc) { buf[0] = 0x80 | (rtr ? 0x40 : 0) | (dlc & 0x0F); /* bit7 FF=1 */ buf[1] = (id >> 21) & 0xFF; /* ID.28 ~ ID.21 */ buf[2] = (id >> 13) & 0xFF; /* ID.20 ~ ID.13 */ buf[3] = (id >> 5) & 0xFF; /* ID.12 ~ ID.5 */ buf[4] = (id & 0x1F) << 3; /* ID.4 ~ ID.0 移到 bit7-3 */ }buf[1] = (id >> 3) & 0xFF这一行是标准帧打包的核心,11 位 ID 右移 3 位后低 8 位正好是 ID.10 到 ID.3,与缓冲区偏移 0x01 对应。buf[2]则取 ID 的最低 3 位左移 5 位,放到字节 2 的最高 3 位。解包就是打包的逆运算:标准帧id = (buf[1] << 3) | (buf[2] >> 5),扩展帧id = (buf[1] << 21) | (buf[2] << 13) | (buf[3] << 5) | (buf[4] >> 3)。调试时如果发现总线上收到的 ID 和代码里设置的不一致,先打印这三个字节,核对高低位顺序,多半能直接定位问题。
3.3 发送函数:TBS 轮询、单次发送与远程帧
发送路径上有一个硬件状态位必须等待:SR.TBS(发送缓冲状态)。只有 TBS=1 时才能向发送缓冲区写入新帧,否则会覆盖上一帧未发出的数据。常见驱动用轮询等待 TBS,再写缓冲、拉CMR_TR触发发送。
void sja_send_frame(volatile uint8_t *regs, uint32_t id, uint8_t *data, uint8_t dlc, uint8_t rtr, uint8_t ext) { uint8_t *tx = (uint8_t *)®s[SJA_TXBUF]; uint8_t i; /* 等待发送缓冲空闲,TBS=1 才允许写 */ while ((regs[SJA_SR] & SR_TBS) == 0) { /* 若长期卡在这里,说明上一帧一直没发出去, 检查总线是否短路或没有终端电阻 */ } if (ext) { sja_pack_ext_id(tx, id, rtr, dlc); } else { sja_pack_std_id(tx, id, rtr, dlc); } /* 数据区从偏移 5 开始,固定 8 字节窗口 */ for (i = 0; i < dlc; i++) { tx[5 + i] = data[i]; } regs[SJA_CMR] = CMR_TR; /* 置 TR 请求发送,硬件自动完成仲裁 */ }rtr参数为 1 时表示这是远程帧,此时 DLC 仍然写入帧信息字节,但数据区的 8 个字节不会被发送出去,接收方也不会把数据存入 FIFO。CMR_TR是普通发送请求,硬件会自动重发直到成功、仲裁丢失或总线关闭;如果做周期性发送却不想让一帧坏帧占住总线,可以改用CMR_SRR单次发送。单次发送模式下控制器只尝试一次,失败不进重发队列,适合传感器周期报文的场景,能避免总线 error passive 期间节点一直重发把总线占满。头文件里如果没有提供SRR宏,可以自己补上#define CMR_SRR 0x20。
4. 初始化、验收滤波、中断:驱动通电后按什么顺序跑
4.1 复位模式下的完整初始化序列
SJA1000 的初始化必须遵循固定顺序:上电后先确认晶振稳定,置位MOD_RM进入复位模式,然后设置时钟分频、波特率定时器、输出控制、验收滤波,最后清RM进入工作模式。任何一步顺序颠倒,都可能导致配置写不进寄存器,这在示波器上表现为 TX 引脚完全静默。
void sja1000_init(volatile uint8_t *regs, uint8_t btr0, uint8_t btr1, uint32_t acc_code, uint32_t acc_mask) { sja_enter_reset(regs); regs[SJA_CDR] = 0x88; /* PeliCAN 模式(bit7=1),关闭 CLKOUT 省功耗 */ regs[SJA_BTR0] = btr0; /* 波特率定时器 0:SJW + 预分频 */ regs[SJA_BTR1] = btr1; /* 波特率定时器 1:TSEG1 + TSEG2 + SAM */ regs[SJA_OCR] = 0xFA; /* 输出控制:TX0 推挽输出,正常极性 */ /* 标准帧单滤波:只使用 ACR0/AMR0 和 ACR1 的高 3 位 */ regs[SJA_ACR0] = (acc_code >> 3) & 0xFF; regs[SJA_AMR0] = (acc_mask >> 3) & 0xFF; regs[SJA_ACR1] = (acc_code & 0x07) << 5; regs[SJA_AMR1] = ((acc_mask & 0x07) << 5) | 0x1F; regs[SJA_IER] = 0x03; /* 使能接收中断和发送完成中断 */ sja_exit_reset(regs); }btr0和btr1是这份头文件里最需要人工核对的两个参数。BTR0 的低 6 位是预分频值减 1,高 2 位是同步跳转宽度减 1;BTR1 的高位是采样次数,中间 3 位是 TSEG2 减 1,低 4 位是 TSEG1 减 1。实际波特率公式为fbit = fosc / ((BRP + 1) * (1 + TSEG1 + TSEG2))。下面是一组常用配置,前提是 SJA1000 的晶振为 16 MHz:
| 目标波特率 | BRP | TSEG1 | TSEG2 | BTR0 | BTR1 | 采样点 |
|---|---|---|---|---|---|---|
| 1 Mbit/s | 1 | 5 | 2 | 0x00 | 0x14 | 75% |
| 500 kbit/s | 1 | 13 | 2 | 0x00 | 0x1C | 87.5% |
| 250 kbit/s | 4 | 13 | 2 | 0x03 | 0x1C | 87.5% |
第一次调驱动时先把 BTR0 设成 0x00、BTR1 设成 0x1C,能跑通再动采样点。注意 SJA1000 不是所有主频都支持任意波特率,TSEG1 最大 16、TSEG2 最大 8,如果算出1 + TSEG1 + TSEG2超过 25,就必须加大预分频,否则位时间超出硬件上限,配置会被静默忽略。
4.2 验收滤波:AMR 的 1 是"不管",不是"必须等于"
验收滤波是 SJA1000 驱动里理解成本最高的一块。ACR(验收代码)里写的位表示"必须匹配",AMR(验收屏蔽)里为 0 的位表示"必须相等",为 1 的位表示"不检查"。把 AMR 全设 0xFF 等于关闭滤波,所有帧都收;把 AMR 全设 0 等于只收固定 ID 的一帧。标准帧在单滤波模式下只用到 ACR0/AMR0 和 ACR1 的高 3 位,ACR1 的低 5 位在物理上对应标准帧不存在的 ID 位,必须通过 AMR1 的低 5 位置 1 来忽略。
/* 例:只接收 ID = 0x305 的标准帧 */ sja1000_init(regs, 0x00, 0x1C, 0x305, /* acc_code */ 0x7FF); /* acc_mask:0x7FF 表示 11 位 ID 全部必须相等 */这个例子里 acc_code=0x305,acc_mask=0x7FF,AMR 全 0,滤波器精确锁定一个 ID。再看一个反例,如果 acc_code=0x300、acc_mask=0x700,意味着 ID 的低 3 位不检查,0x300 到 0x307 共 8 帧都会进接收 FIFO。调试时最诡异的现象是"明明 AMR 设了过滤,却收到一堆别的 ID",原因基本是 AMR1 低 5 位没有置 1,低 3 位的 ID 被当成必须匹配的位,干扰了判断。
4.3 中断服务函数:读 IR 即清除,FIFO 要 while 排空
SJA1000 的中断寄存器 IR 在读取后被硬件自动清除,所以中断服务函数里第一件事就是把 IR 读出来保存,再依次处理各个中断源。不要在中断处理中间再次读 IR,否则会丢掉后到的中断状态。接收方向有一个特别容易漏的逻辑:接收 FIFO 里可能同时积压多帧,释放一帧后还有下一帧,必须通过循环判断SR.RBS来排空 FIFO。
void sja1000_isr(volatile uint8_t *regs) { uint8_t ir = regs[SJA_IR]; /* 读 IR 同时清除全部中断标志 */ uint8_t sr; if (ir & 0x01) { /* RI:接收中断 */ /* 循环处理 FIFO 中所有帧,直到 RBS 降为 0 */ while ((sr = regs[SJA_SR]) & SR_RBS) { uint8_t fi = regs[SJA_RXBUF + 0]; uint8_t len = fi & 0x0F; uint8_t i; /* 按 3.2 节的解包逻辑取 ID */ uint32_t id; if (fi & 0x80) { id = ((uint32_t)regs[SJA_RXBUF + 1] << 21) | ((uint32_t)regs[SJA_RXBUF + 2] << 13) | ((uint32_t)regs[SJA_RXBUF + 3] << 5) | (regs[SJA_RXBUF + 4] >> 3); } else { id = ((uint32_t)regs[SJA_RXBUF + 1] << 3) | (regs[SJA_RXBUF + 2] >> 5); } for (i = 0; i < len; i++) { rx_data[i] = regs[SJA_RXBUF + 5 + i]; } regs[SJA_CMR] = CMR_RRB; /* 释放当前帧,FIFO 自动前移 */ } } if (ir & 0x02) { /* TI:发送完成中断 */ tx_done_flag = 1; /* 置标志位,主循环据此判断发送结束 */ } }中断处理里,CMR_RRB是每处理完一帧就必须写一次的释放命令。只写一次会导致 FIFO 里的第二帧永远无法进入当前帧窗口,表现为主机收到第一帧后,后续帧全部滞留在 FIFO 里,直到溢出。发送完成中断TI对应状态寄存器里的TCS位,它表示帧已经成功送到总线并收到 ACK,而不是"写入缓冲区成功",所以这个中断不能用来触发下一帧的写入——要等 TBS。
5. 三个验证技巧:先确认驱动没写错,再谈调优
5.1 自测模式回环:不上总线也能测收发
SJA1000 有一个被很多人忽略的自测模式,置位MOD_STM(bit2=1)后,发送的帧不经过总线,直接从发送路径回环到接收 FIFO。配合CMR_SRR单次发送,可以在没有任何 CAN 收发器、没有终端电阻的情况下验证初始化、中断和接收逻辑是否正常。自测模式下总线错误检测仍然生效,但帧不会因为无 ACK 而失败,因为回环点位于 CAN 内核内部。
/* 自测发送:发一帧并期望它出现在接收 FIFO 里 */ void sja1000_self_test(volatile uint8_t *regs) { uint8_t data[8] = {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38}; regs[SJA_MOD] |= MOD_STM; /* 进入自测模式 */ sja_send_frame(regs, 0x123, data, 8, 0, 0); regs[SJA_CMR] = CMR_SRR; /* 单次发送,避免重试掩盖问题 */ /* 等待接收中断,或轮询 SR.RBS 检查帧是否回环成功 */ sja1000_isr(regs); /* 复用中断处理函数做接收 */ regs[SJA_MOD] &= ~MOD_STM; /* 退出自测模式 */ }自测通过说明驱动本身的寄存器读写、帧打包、FIFO 释放逻辑没有问题,接下来上总线后的问题就只剩波特率匹配、终端电阻和相位配置了。
5.2 示波器测显性位宽反推波特率
没有 CAN 分析仪时,示波器是最直接的验证工具。把探头夹在 CAN 收发器(如 TJA1050)的 TXD 引脚对地,发送连续帧,测量一个显性位的宽度。CAN 协议里显性位对应逻辑 0,位时间tbit的倒数就是实际波特率。反推配置时用波特率公式复核:fbit = fosc / ((BRP + 1) * (1 + TSEG1 + TSEG2))。测到显性位宽为 2 微秒,说明实际波特率是 500 kbit/s;如果测到 2.08 微秒(480 kbit/s),多半是主频不是预期的 16 MHz,或者 BRP 算错了一位。注意测量时要找连续两个显性位中间没有隐性位翻转的位置,用示波器的光标功能量单个位,比数一帧总时长再除以位数要准确得多。
5.3 错误计数寄存器定位总线故障
PeliCAN 模式下 0x06 和 0x07 是 RXERR/TXERR 错误计数寄存器,这两个值的增长速度可以直接区分故障方向。如果 TXERR 持续增长而 RXERR 基本不动,问题多半在发送侧,比如总线没有终端电阻、两条差分线接反、或者波特率比对方快;如果 RXERR 涨得快,说明总线上有别人在发但你收不进来,先查验收滤波。若现场跑的是嵌入式 Linux,可以用 SocketCAN 顺手对照:candump can0看收包,cansend can0 123#DEADBEEF发包,ip -details -statistics link show can0看错误计数。但裸机环境下没有这些工具,直接在驱动里周期性读 RXERR/TXERR 并打印,是定位总线物理层问题的最快手段。调试完成后,把这几个错误计数器的读接口补进头文件对应的结构体里,后续现场排查会省很多事。
本文还有配套的精品资源,点击获取