资讯详情

资讯详情

从寄存器层读懂CAN例程:初始化、收发与排错实践

简介面向嵌入式开发者和刚开始接触现场总线的工程师这份由周立功整理的 CAN 学习资料围绕 SJA1000 控制器系统讲解了 CAN 总线协议、标准帧与扩展帧结构、芯片内部错误检测与接收滤波机制以及硬件设计中的终端电阻配置要点。资源共 44 个文件压缩包约 490KB主体为 asm 与 c 源码配合 uv2 工程、hex 固件、pdf 文档及 lst/m51 等辅助文件可在 Keil 环境下直接编译运行也便于按 BasicCAN 模式逐步学习初始化、数据发送接收、中断服务程序和错误处理流程。资料还附带实验测试程序与控制器模块说明文档覆盖从电路连接、驱动移植到协议栈实现的完整链路并给出常见通信故障的排查思路。该资源已有 4000 余人学习使用内容编排清晰、实例具体适合需要快速搭建 CAN 通信功能或进行项目移植的开发者作为入门到进阶的配套参考。1. 这套CAN例程和资料为什么值得从寄存器层读一遍CAN总线开发里有一句玄学照抄例程能点灯一联调就断线。标题里提到的这套CAN例程和资料在嵌入式圈子流传了很多年几乎成了CAN入门标配。它不是给你一段能编译的代码就完事而是把控制器内部怎么初始化、位时序怎么算、收发缓冲怎么管理用寄存器层的方式讲了一遍。对新手来说它能让你在写第一行驱动之前先理解总线上那些帧到底是怎么被送出去的对熟手来说真正值得抄的不是代码是它处理边界条件的那套思路。我见过A同学把例程里的波特率寄存器原样扒到自己的板子上结果两边界面都写着500kbps却怎么都连不上也见过某公司在量产前的最后一晚因为总线掉线恢复逻辑没写对整批设备做不了老化测试。这类问题资料里其实都有答案但很少有人按顺序把它读完。这篇文章就把这套资料的目录逻辑、最小复现路径和排错顺序拆开讲一遍读完你能照着做出一份自己的CAN驱动框架而不是停留在我抄了谁的例程。2. 例程包的内部结构为什么直接抄main会跑不通拿到一份很详细的CAN例程包很多人的第一反应是点开工程文件夹找到main.c看它调了哪些函数。这个动作很危险因为CAN不像UART那样配置好就能自发自收它牵涉到控制器、收发器、总线三层任何一层不匹配代码看起来都对线上就是没波形。2.1 例程包的三层责任边界控制器、收发器、总线先明确一件事你写的驱动操作的是CAN控制器不是物理总线。控制器负责按协议把数据变成一串差分电平信号或者从线上把差分信号解出来。收发器负责电平转换与驱动能力一边连接控制器的TX/RX引脚一边连接总线上的CANH和CANL。总线本身则是双绞线两端各需要一颗120Ω终端电阻。例程包里通常会把这三层拆开。控制器这层是最厚的包括时钟切换、复位模式、位时序寄存器、验收滤波器、收发缓冲。收发器这层代码极少往往只是初始化GPIO和供电但硬件上最容易翻车。总线这层根本没有代码全是接线和终端电阻的事。我一般会按这个顺序讲解先把控制器抽出单独看它怎么产生一个位时间再把收发器当成“透明”器件最后才回到总线上验证。这样看例程就不会被“为什么既要配置控制器又要配置收发器”这种问题绊住。2.2 例程里三类文件每一类都有不同的阅读目的详细例程包通常会按功能把代码拆成几层而不是把几百行驱动全塞在main里。常见组织方式是寄存器驱动文件、帧收发文件、应用示例文件三层。第一层是寄存器驱动文件名通常叫can_init或者类似的名字里面是复位模式切换、波特率计算、总线时序配置。读这一层的目的是搞懂硬件的初始化状态机哪些寄存器只能在复位模式下改改完怎么退出退出之后要先发哪个命令。第二层是帧收发里面是发送缓冲区写入、发送请求置位、接收中断处理、释放接收缓冲区这一类操作。读这一层要结合状态寄存器的每一位去看比如TBS表示发送缓冲是否空闲RBS表示接收缓冲是否有数据。不结合状态位去读收发函数等于看代码只看动作不看条件。第三层是应用示例讲的是怎么把收到的帧解释成转速、温度、开合状态或者怎么按一定周期把设备状态组装成帧发出去。这一层可移植性最低和具体产品协议强相关但例程的价值在于告诉你DLC怎么设置、ID用标准帧还是扩展帧、数据字节怎么排。建议阅读顺序是驱动到收发再到应用每一层都要能回答“如果换一颗不同主频的芯片我改哪里”这个问题。很多人直接把第三层代码复用第一层照抄结果一上真实总线和旁站设备对不上这就是典型的主次倒置。2.3 最小硬件拓扑MCU、CAN收发器和120Ω终端电阻怎么接看例程包里提供的原理图会发现它裸露硬件非常克制MCU的CAN TX引脚接收发器的TXDCAN RX引脚接收发器的RXD收发器的CANH接总线CANHCANL接总线CANLVCC和GND接好然后总线两端各挂一颗120Ω电阻。这里有几个最常见的坑值得提前说。第一收发器的型号决定它支持的电平范围和工作模式有些自带待机控制引脚需要拉低或拉高才能进入发送状态否则总线表现为一直显性其他设备一上来就总线忙。第二CANH和CANL接反之后静态看起来正常一通信就是错误帧。第三终端电阻不是随便贴一颗就行总线两头各一颗中间设备不要加。如果只有两块板互连每块板都该有一个终端电阻。从例程的角度看硬件不吃透代码里看到的状态位全部失真。比如你发一个帧状态寄存器显示发送成功但如果总线没有ACK应答其实数据根本没有被任何节点确认。这种情况在只有一块板子自测的时候特别迷惑因为控制器自身会认为发送完成实际上线上没有任何波形。所以读例程的第一个动作是先检查自己的硬件拓扑是否和资料里的一致。3. 跑通一帧的最短路径初始化、波特率参数与发送代码在例程包里找到最短路径并不难初始化控制器、配置波特率、写一个发送函数。难的是理解这两三步背后为什么必须按固定顺序走。下面这段代码是常见例程的寄存器驱动写法寄存器名沿用了独立CAN控制器的定义结构上可以直接对照你手上的MCU内置CAN外设。3.1 初始化为什么非要先进复位模式CAN控制器内部有一组寄存器只能在复位模式下修改其中最关键的是总线时序寄存器。你直接在主程序里赋初值寄存器概不接收。例程里通常会写一个while循环等待复位模式标志这段代码看起来像在磨洋工实际上是在等控制器内部状态机稳定。// can_init.c —— 初始化的最小骨架 #include can_plat.h void can_init(uint16_t btr0, uint16_t btr1) { // 1. 申请进入复位模式 CAN_MOD 0x01; // MOD寄存器的ResetMode位置1 while ((CAN_SR 0x01) 0); // 等待SR的ResetMode位被硬件置位 // 不加这个while直接改寄存器会改不进去 // 2. 复位模式下写总线时序寄存器 CAN_BTR0 btr0; // 波特率预分频和同步跳转宽度 CAN_BTR1 btr1; // 采样点、相位缓冲段1和相位缓冲段2 // 3. 验收滤波器先放全通后面再细化 CAN_ACR0 0x00; CAN_AMR0 0xFF; // 掩码全1表示不需要匹配 CAN_ACR1 0x00; CAN_AMR1 0xFF; // 4. 退出复位模式 CAN_MOD 0x00; while ((CAN_SR 0x01) ! 0); // 等硬件跳回正常模式 }这段代码里的两个while循环第一个比第二个更重要。进入复位模式是硬件异步过程写完Mod位需要等它内部完成时钟同步不等就写寄存器会出现你明明赋值了读出还是原来的值。第二个while除了保证退出还顺带保证后面的发送命令不会落到未就绪的控制器上。参数btr0和btr1不是随便填的断口号。btr0高两位是SJW同步跳转宽度低6位是BRP预分频器btr1里承载TSEG1和TSEG2。例程包通常把这两个寄存器以配置表的形式写在注释里但实际工程必须根据芯片主频重算不能照搬。3.2 波特率换算16MHz时钟下的BTR0/BTR1参数表CAN波特率计算公式可以统一写成实际波特率 控制器时钟频率 /2 × (BRP 1) × (1 TSEG1 TSEG2)。判断例程包某个参数能不能直接抄就看你MCU的时钟是不是和资料里一致。下面这张表是外部时钟16MHz时的常见组合我删掉了例程注释里那些没用的备注留着真正要写的值。目标波特率BRP寄存器值BTR0TSEG1TSEG2BTR1实际波特率采样点1Mbps00x40610x161.000Mbps87.5%500kbps10x41520x25500.000kbps75.0%250kbps30x43520x25250.000kbps75.0%125kbps70x47520x25125.000kbps75.0%表中BTR0已经把SJW设为1取的是TSEG1和TSEG2的平衡值很多例程默认就这组。你看到这套组合时第一反应应该是这个分频值是不是基于16MHz得到的。如果换成其他主频要用上面那个公式重新枚举一遍BRP、TSEG1、TSEG2。另一个容易忽视的是采样点。我们常挂在嘴边的采样点指的是位时间内采样时刻相对位起始的位置。车辆网络通常喜欢85%左右工业现场用75%也不少见。例程里的默认值未必适合你的实际网络现场总线抖动大时采样点靠后能提高容错性但代价是布线距离变短。想图省事你可以长期用表中75%这组。3.3 发送一帧标准数据帧查询式发送的寄存器操作初始化完成之后直接写一个简单的发送函数把数据塞进发送缓冲再触发发送命令这是例程包所有发送示例的雏形。关键在于写发送缓冲之前必须查询发送缓冲状态位缓冲没空出来就写会覆盖上一次还没发出去的帧。// can_send.c —— 查询式发送标准数据帧 #include can_plat.h int can_send_frame(uint32_t id, uint8_t dlc, const uint8_t *data) { if (dlc 8) { return -1; // CAN单帧最多8字节超出就不合法 } if ((CAN_SR 0x04) 0) { // SR的TBS位为0发送缓冲还在忙 return -2; // 返回发送忙由上层决定重试或丢弃 } // 写帧信息标准帧DLC数据长度 CAN_TX_FRAME_INFO (uint8_t)(0x00 | dlc); // 写11位标准ID拆成两个字节 CAN_TX_ID_H (uint8_t)(id 3); // 高8位 CAN_TX_ID_L (uint8_t)((id 0x07) 5); // 低3位放在bit7~bit5 // 写数据场 for (int i 0; i dlc; i) { CAN_TX_DATA[i] data[i]; } // 触发发送命令 CAN_CMR 0x01; // CMR的TR位置1请求发送 return 0; }函数开头对DLC的判断不是多余的防护很多例程因为省掉这行出现溢出帧时会把相邻内存踩掉定位起来极其痛苦。发送缓冲占用判断则直接影响系统实时性查询式发送框架里这个判断承担流控作用高负载时应该配合重试次数统计。写完发送函数别急着写接收先做一件最简单的事把发送函数里ID填成任意值发送一帧8字节数据然后用示波器或逻辑分析仪量CANH和CANL之间的差分波形看有没有帧起始、仲裁场、数据场、CRC场最后看ACK位是不是高电平。如果ACK一直是高说明总线上没有其他节点应答这时候先检查收发器和终端电阻。整个发送路径里寄存器判断只是编码规范问题物理层才是第一个真正卡人的地方。4. 从轮询改成中断收发链路里必须改的寄存器流程很多项目跑通查询式收发后马上就遇到一个问题CPU忙等发送缓冲、忙等接收标志其他任务全被拖死。这时你就得把收发链路从轮询改成中断驱动。例程包通常两种写法都提供了但中断版本的细节比查询版本多不少尤其是在接收缓冲释放和错误中断处理上。4.1 查询和中断的取舍不是所有工程都适合中断查询式收发的好处是逻辑清晰写起来没有嵌套调试的时候单步走就能看到状态位的完整变化。缺点也明显控制器接收数据是异步发生的查询间隔太长会丢帧查询太频繁又浪费CPU。中断式收发看起来高效但它引入的是并发问题。你的主程序正在处理一组传感器数据时接收中断进来改写接收缓冲区处理到一半的数据就毁了。所以例程包里的工程模板要求你把接收数据和消费数据分开中断只负责把数据搬进缓冲区和置标志主循环检查标志后再拷贝出来处理。我自己的项目习惯是总线负载率低于30%且帧间隔稳定用查询就够负载率高于40%或者帧到达时间不固定必须改中断。判断依据很简单算一下你主循环的响应周期如果10ms一个周期而总线上1ms就有一帧数据要收查询方式必然丢帧。4.2 接收中断里最容易写错的释放缓冲区时序接收中断的标准流程看起来很简单读帧信息读ID读数据清标志。但很多人在第三步之后就忘记释放接收缓冲区。接收缓冲区在硬件上只有一个你不释放下一帧进来就没地方放硬件会把新帧丢掉。更麻烦的是新帧到来还会继续触发中断形成一个永远进不完的中断状态。// can_isr.c —— 接收中断处理 #include can_plat.h volatile uint8_t rx_flag 0; volatile uint32_t rx_id 0; volatile uint8_t rx_buf[8]; volatile uint8_t rx_len 0; void CAN_ISR(void) { uint8_t ir CAN_IR; // 读中断寄存器硬件自动清中断标志 if (ir 0x01) { // RI位置1表示接收中断 uint8_t fi CAN_RX_FRAME_INFO; rx_len fi 0x0F; // 低4位是DLC rx_id ((uint32_t)CAN_RX_ID_H 3) | ((uint32_t)CAN_RX_ID_L 5); // 拼回11位ID for (int i 0; i rx_len; i) { rx_buf[i] CAN_RX_DATA[i]; } CAN_CMR 0x04; // 释放接收缓冲区RRB位置1 rx_flag 1; // 通知主循环取走数据 } }这段代码体现出中断处理的三个纪律。第一CAN_IR读一次就够了它是中断源的汇总读它会清掉当前所有中断标志分多次读反而会漏中断。第二rx_flag在中断里置位主循环消费完后必须手动清0如果没有这个标志主循环会反复处理同一帧数据。第三释放缓冲区命令必须在读取完成后、退出中断前执行顺序反了会出现缓冲区溢出中断风暴。还有一种翻车写法是在中断里面直接做协议解析比如调一个解析函数去更新几个全局变量。看起来没问题但一旦解析函数里有一个浮点运算或除法中断响应时间就不可控总线稍微繁忙其他中断就延后。正确做法永远是把中断压到最薄搬数据、置标志、退出。4.3 错误中断与总线关闭恢复逻辑查询式收发时你几乎感知不到控制器内部还有一个错误状态机但改中断后错误中断会直接打到你的中断入口不处理就是死循环。CAN控制器会维护发送错误计数器和接收错误计数器错误超过阈值会进入被动错误状态再超过就是总线关闭状态不再参与通信。// can_error.c —— 错误中断与总线关闭恢复 #include can_plat.h void can_handle_error(void) { uint8_t ir CAN_IR; if (ir 0x80) { // EI位置1错误中断 uint8_t ecc CAN_ECC; // 错误代码寄存器用于定位错误类型 // 如果控制器进入总线关闭状态 if (CAN_SR 0x40) { // SR的BS位置1 CAN_MOD 0x01; // 回到复位模式 // 此处清空内部错误计数器然后重新初始化 CAN_MOD 0x00; // 回正常模式 // 至少等一个位时间再参与总线 } } }这里不推荐在中断里做完整恢复清错误计数器、重新初始化配置的过程比较耗时放在中断里会让其他中断饿死。我通常的做法是错误中断里只置一个标志然后立刻退出主循环检测到标志后再做一个几十微秒的延迟把控制器复位回正常状态。判断错误来源比恢复更重要。硬件错误、总线短路、波特率不匹配都会表现为错误中断风暴。例程包的错误代码寄存器注释虽然少但你得很熟悉它表示的位模式比如位错误说明发送的电平和采到的电平不一致多数是总线负载过重或多个节点同时开启了强驱动格式错误常常和波特率采样点有关ACK错误则是总线上没有其他节点应答终端电阻和接线问题优先查。4.4 多帧通信与长数据拆分DLC上限8字节这在协议设计层面很要命。例程包里的收发示例都是单帧但真实工程经常要传几个字节的固件版本、升级包或者诊断数据这时候必须自己制定拆帧协议。常见做法是用消息ID区分帧序号第一帧作为起始帧后面每帧带序列号接收方把序列号填进本地缓存。比如要传一条30字节的升级记录最简单的拆法是第一帧ID用0x100DLC填8第一个字节0xAA表示协议起始第二字节是总帧数后面带数据后续帧ID用0x101第一个字节是帧序号后面7字节是数据。接收端按序号写入数组。注意每个节点发送间隔要留余量特别是总线负载高时连续帧之间至少要等一个时间片的其余帧空位否则仲裁会把你的帧延迟到无法预测。这套拆帧逻辑不应该散落在中断里而是放在应用层。中断只把原始帧交给一个环形缓冲区主循环从缓冲区取帧后按协议拼装。例程包的应用示例里如果直接处理的是完整字段别急着精简那就是给你参考的拼装骨架。5. CAN例程移植避坑5个高频问题与排查顺序前面四章的方向都是怎么把例程跑起来但真正让工程交付卡住的永远是那几个老毛病。下面按我遇到过的频率排序每一条都是现象、原因、解决三步说清。5.1 发送进不去先看TBS还是先量差分电压现象发送函数返回-2或者调用后状态寄存器的TBS一直为0发送请求发不出去。 原因发送缓冲一直被占用通常是上一帧还没发完物理层又不应答控制器反复重发导致发送缓冲不释放。这时候代码层面的判断已经没意义问题在线下。 解决先查收发器的TXD和RXD引脚有没有反过来再查CANH和CANL是否接反最后确认终端电阻有没有漏焊。用示波器探头点在收发器的CANH和CANL之间正常空闲时应是一个约2.5V的差分电平如果一直显性说明有节点在占用总线或总线被短路。量完波形再回到TBS位判断才有意义。5.2 两边都是500kbps却互不通信时钟带来的隐性偏差现象两块板程序里的波特率都标注为500kbps接线没问题却持续报错。 原因两边的控制器主频不一样一方用的是16MHz外部晶振另一方是72MHz锁相环输出波特率寄存器却是同一组抄来的。看起来都标500k实际一个已经偏到510k另一个偏到460k。 解决用第3章的公式重新计算别相信配置注释里的波特率标注。把两边的外部时钟频率写进程序注释里并且打印出来在联调时先核对这个值。更稳妥的做法是采购一批相同主频的芯片固件里做编译期断言主频对不上直接编译失败。5.3 接收中断进了出不来典型的接收缓冲未释放现象程序一上电就死循环调试器停在接收中断里不断触发。 原因读取接收数据后没有给控制器发送释放接收缓冲区命令下一帧到达时无法写入硬件持续阻塞中断源。 解决在中断处理尾部补上CAN_CMR 0x04并且保证这行语句在清除中断标志之后执行。如果你用了带FIFO的外设还得确认FIFO的弹出命令写对了不少MCU的CAN外设是用读数据寄存器的方式自动释放和独立控制器的RRB命令不完全一样。5.4 一条也收不到验收滤波器的掩码黑洞现象总线上有其他设备发数据控制器就是收不到中断也不触发。 原因验收滤波器配置成全屏蔽或者掩码算错了把关心的ID全部过滤掉。例程包默认全通但你在做项目时一定是为了过滤噪声才配置它一改就容易把有效帧也挡了。 解决先用全通配置跑一遍确认能收到帧再逐步收紧掩码。搞清楚你要过滤的是11位标准ID还是29位扩展ID标准帧和扩展帧在掩码寄存器里的位分布不一样不少老例程只演示了标准帧扩展帧的字节对齐是完全另一套规则。配置后可以在线修改ID一条条测试别一次性写一堆进去。5.5 跑一会儿就离线总线关闭状态机恢复现象系统运行十几分钟后再也不通信重新上电能好过一会儿又坏。 原因某条线束出现间歇性短路或插头松动产生大量错误帧控制器错误计数器累加超阈值进入总线关闭状态不再收发任何数据。如果你的程序没有错误中断处理它就永远离线。 解决把第4章的错误中断恢复逻辑挂上去进入总线关闭状态后延迟复位。更重要的是一定要统计错误中断次数连续几次触发错误中断说明物理链路有问题软件恢复只是保命不是治病。量产阶段有这个问题优先排查线束和接插件。5.6 排查顺序工具箱如果思路乱我建议按递进顺序排查先示波器量收发器输出确认物理层有信号再用上位机软件抓总线报文确认总线上存在数据流随后检查控制器状态寄存器看有没有错误标志最后才回到程序逻辑查滤波器和中断配置。一次只改一个变量改完验证一个现象这是CAN调试效率最高的方式。不要同时改滤波器和波特率两个一起调整后虽然能通但你永远不知道之前是哪个参数卡住。6. 把例程变成自己的调试工具自测模式、总线抓包与参数脚本当你已经按前面章节把收发驱动跑起来接下来要思考的是怎么提高验证效率。下面这三个习惯是我自己移植CAN驱动后反复用到的能让你在半小时内判断一个驱动是不是真的能用而不是被“看起来能跑”骗过去。6.1 用自测模式做“自己发自己收”把控制器切到自测模式发送的帧不经过外部总线直接在内部回环到接收通道。适合在还没有任何节点、只有一块板的阶段验证收发逻辑。步骤是先初始化波特率然后置位自测模式发送一帧看接收中断是否触发。// 自测模式使能 CAN_MOD 0x04; // 自测模式位具体位定义看数据手册 can_send_frame(0x123, 4, test_data); // 如果接收中断里能拼出同一帧说明收发路径完整走通自测模式的好处是隔离了物理层问题。自测都不通问题在你代码自测通但外部联调不通问题再回到收发器和接线。这个定位效率比反复换板子测试高得多。6.2 用上位机软件抓总线报文对照代码时序联调阶段别只盯着自己的设备发呆。把配套的上位机软件挂在同一条总线上软件界面里能看到每个ID的帧间隔、数据值、错误帧标记。你代码里构造成10ms周期发送的帧在界面上应该稳定地以10ms间隔出现间隔抖动超过2ms就要怀疑驱动里有没有被其他中断卡住。遇到错误帧标记时单选一个ID做点对点通信用软件同时观察发送方和接收方的统计数据。你还可以用软件自带的报文回放功能把现场录下来的异常报文重新发到总线上测试设备能否正确处理错误帧和恢复流程。这个回放习惯比任何单元测试都能模拟真实工况。6.3 保存一份波特率换算脚本别每次重新翻数据手册我见过太多人每次换平台都要现算波特率算错一次就要折腾一晚上。建议把第3章的公式固化成一个小脚本下次直接填时钟频率和目标波特率让脚本帮你枚举参数组合。# btr_calc.py —— 给定主频和目标波特率枚举BRP/TSEG组合 def calc(f_clk, baud, max_err0.005): best None for brp in range(64): for tseg2 in range(1, 8): for tseg1 in range(1, 16): tq_total 1 tseg1 tseg2 real f_clk / (2 * (brp 1) * tq_total) err abs(real / baud - 1) if err max_err: if best is None or err best[0]: best (err, brp, tseg1, tseg2) return best print(calc(16_000_000, 500_000))输出结果包括BRP、TSEG1、TSEG2再按照寄存器位定义组装成最终的寄存器值。脚本还可以顺手算采样点帮你决定要不要为了更靠后的采样点牺牲一点误差。脚本不复杂但省下来的全是项目现场排障的时间。另外一个更省事的习惯在你电路板和代码里保留一个版本打印命令启动时把主频、波特率、采样点三个值通过串口打出来。这样每块板子是不是同一批参数一眼就能区分不用拿着万用表猜一块已经封壳的板子内部配置到底是什么。CAN驱动这事真正花时间的不是把发送帧写出来而是把每种异常都想清楚。我自己的教训是第一次做CAN项目时没写错误恢复结果现场设备离线后只能整机断电重启客户对稳定性印象大打折扣。从那以后我每换一个平台至少先把自测模式、错误中断、恢复流程这三件事在一个小时内验证完再往上加业务层协议。这套从寄存器层读例程的方法已经帮我避过了好几个项目最难看的问题希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →