
简介这是一份面向Renesas RH850/F1L芯片开发者的CAN通信速率切换驱动示例。RH850/F1L是瑞萨汽车级32位MCU内部集成多路CAN控制器最多支持6路CAN通道。本例演示同一通道先以1Mbps建立通信再由软件切换为125kbps继续收发完整覆盖了CAN位定时配置、速率切换流程以及收发缓冲区的处理思路既适合嵌入式软件工程师参考也可作为单片机学习者的进阶练习。资源共18个文件压缩包仅71KB以C源码、头文件和汇编文件为主体另含CubeSuite工程文件、链接脚本、地址映射文件、MOT烧录文件及Object中间文件并附有ReadMe说明文档打开工程后可直接查看初始化、中断处理与收发缓冲实现。已有274人浏览下载适合需要快速理解RH850/F1L CAN模块初始化、速率动态调整与收发逻辑的开发者。1. 从压缩包名看RH850F1L的CAN速率切换场景看到6SD_RH850F1L_CAN(SpeedChange_1M_125k).7z这个文件名第一反应是工程代号、主控型号、外设、运行参数全被写进命名里了。RH850F1L 是瑞萨面向车身控制、网关和 BMS 常用的 32 位 MCUCAN 控制器支持多通道和灵活的位时序配置而SpeedChange_1M_125k意味着程序要在运行过程中把 CAN 总线从 1Mbps 切到 125kbps或者反向切回来。这种场景最常见于 Bootloader低速 125k 做引导和握手高速 1M 做固件批量升级也见于产线标定低速模式下保证长线缆通信可靠运行阶段切到高速。多数人在这个需求上卡住不是因为 CAN 驱动不会写而是位时序参数怎么配、切换瞬间总线上会发生什么、切换后如何确认两边真的在同一条速率上。下面按理论、实现、踩坑、验证的顺序把这条链路完整讲清楚。2. RH850F1L的CAN位时序把1M和125k放进同一个采样点2.1 先分清CAN相关的三个时钟PCLK、fCAN、TQRH850F1L 的 CAN 模块虽然挂在 APB 总线上但总线上的位时间精度不依赖 CPU 主频而是由 CAN 协议时钟fCAN决定。fCAN通常由外设时钟 PCLK 经过 CAN 控制器的预分频器产生。很多第一次调 RH850 的人直接拿 80MHz 系统主频去算结果怎么都对不上因为 PCLK 往往单独配置且 CAN 模块内部还有一级分频。先确认 PCLK 的实际值。在 CS 或 EB 的时钟配置界面里看 PCLK 的分配比然后用下面的公式计算fCAN PCLK / (BPR 1)BPR是波特率预分频寄存器的写入值。这一步错了后面所有采样点都是白算。CAN 协议规定一个位由多个最小时间单元 TQ 组成RH850 的 CAN 控制器把每一位拆成同步段、传播段、相位缓冲段 1 和相位缓冲段 2。同步段固定消耗 1 个 TQ寄存器TSEG1的写入值加 1 才是相位段 1 的实际 TQ 数TSEG2同理。位时间的总 TQ 数由下面的公式决定bit_time_tq 1 (TSEG1 1) (TSEG2 1) 波特率 fCAN / bit_time_tq 采样点 (1 TSEG1 1) / bit_time_tq注意SJW同步跳转宽度不参与位时间长度计算它只决定硬件在做再同步时最多能调整几个 TQ。采样点的位置完全取决于 TSEG1 和 TSEG2 的配比这也是本文后面反复强调“采样点统一到 80%”的原因。2.2 TSEG1/TSEG2/SJW在RH850里到底要写多少RH850F1L 的 CAN 模块在瑞萨文档里通常被称为 RSCAN。不同子型号对寄存器的命名略有差异有的手册里写成nCFG1、nCFG2有的直接叫nSEG1、nSEG2但字段含义一致。下面以CAN0_BRPR、CAN0_SEG1、CAN0_SEG2、CAN0_SJW为例换成你工程里实际的寄存器宏即可。字段写入值含义典型范围作用BPR实际分频系数 BPR 10 ~ 1023决定 TQ 长度TSEG1相位段1 实际 TQ 数 TSEG1 12 ~ 16吸收传播延迟和晶振偏差TSEG2相位段2 实际 TQ 数 TSEG2 11 ~ 8决定采样点之后的余量SJW再同步补偿 TQ 数 SJW 10 ~ 3硬件最多调整的 TQ 数TSEG1 留得长采样点会后移能吸收更长的总线传播延迟但太靠后又会让 TSEG2 没有足够裕量TSEG2 太短SJW 就没有空间做再同步。SJW 只在检测到相位误差时才起作用不影响正常采样点位置。实际配置中SJW 建议不小于 1且必须小于等于 TSEG2。经典 CAN 和 CAN FD 的采样点计算公式不同RH850F1L 在不启用 CAN FD 时按经典 CAN 算不要拿 CAN FD 帧内速率切换的概念往这里套。2.3 20TQ方案两档速率共用同一组段参数以fCAN 20MHz为例1Mbps 下每位正好 20 个 TQ。如果直接把 20MHz 拿去做 125kbps位时间会变成 160TQ虽然也能工作但采样点的调整步长变得很粗且切换时 TSEG1/TSEG2/SJW 全都要跟着换。更合理的做法是125k 时把预分频改成 8fCAN降到 2.5MHz位时间又回到 20TQ。这个方案的收益很大两个速率的位时间都是 20TQ段参数完全一致采样点完全相同切换时只需要改 BPR 一个值。1M 和 125k 的 TQ 宽度不同但每一位的相位组成比例一样对端节点只要同样使用 80% 采样点两边在任意一档速率下都能对齐。用一段 Python 快速验证参数def bit_timing(bpr, tseg1, tseg2, pclk20_000_000): fcan pclk // (bpr 1) tq_total 1 (tseg1 1) (tseg2 1) baud fcan // tq_total sample_point (1 tseg1 1) / tq_total return baud, sample_point # 1Mbps: 预分频1, TSEG1写14, TSEG2写3 print(bit_timing(0, 14, 3)) # (1000000, 0.8) # 125kbps: 预分频8, TSEG1写14, TSEG2写3 print(bit_timing(7, 14, 3)) # (125000, 0.8)输出显示两档速率下采样点都是 0.8。如果 PCLK 不是 20MHz比如 40MHz把 BPR 改成 1 得到fCAN 20MHz结论不变。核心思想是先把 PCLK 分频到合适的 CAN 协议时钟再让两个目标速率的位时间都落在整数个 TQ 上段参数只在初始化时写一次。3. CAN波特率切换的最小实现换BPR而不是换段参数3.1 先定义波特率配置表在实际工程里我习惯把位时序参数做成结构体表而不是散落在切换函数里的魔法数字。新增一档速率只需要往表里加一行逻辑分支也被统一成查表。typedef struct { uint32_t baud; /* 目标波特率单位 bps */ uint8_t bpr; /* 预分频写值实际分频 BPR 1 */ uint8_t sjw; /* 同步跳转宽度实际 TQ SJW 1 */ uint8_t tseg1; /* 相位段1实际 TQ TSEG1 1 */ uint8_t tseg2; /* 相位段2实际 TQ TSEG2 1 */ } can_timing_t; /* 基于 fCAN 20MHz位时间 20TQ采样点 80% */ static const can_timing_t can_timing_table[] { { 1000000u, 0u, 1u, 14u, 3u }, /* 1Mbps */ { 125000u, 7u, 1u, 14u, 3u }, /* 125kbps */ };1M 档的分频系数为 1TQ 宽度 50ns125k 档分频系数为 8TQ 宽度 400ns。两者位时间都是 20TQ采样点都是 80%。注意表中的sjw写入 1实际是 2 个 TQ。1M 下 2 个 TQ 只有 100ns配合 20ppm 晶振和 5m 以内的短总线足够125k 下 2 个 TQ 等于 800ns容错很充裕。3.2 核心切换函数先进初始化模式再改分频CAN 控制器的位时序寄存器不允许在通信过程中直接改。常见做法是先请求进入初始化模式停止当前帧的收发确认硬件完成状态切换后再改写寄存器最后退出初始化模式。static uint8_t CAN_SetBitTiming(const can_timing_t *tbl) { /* 请求进入初始化模式停止收发释放总线 */ CAN0_CTR.INIT 1u; /* 等待控制器确认进入初始化避免总线上有错误帧时卡死 */ uint32_t timeout 10000u; while (timeout-- 0u) { if (CAN0_STR.INIT 1u) break; } if (CAN0_STR.INIT 0u) { return 1u; /* 进入初始化超时 */ } /* 初始化模式下改写位时序参数 */ CAN0_BRPR tbl-bpr; CAN0_SEG1 tbl-tseg1; CAN0_SEG2 tbl-tseg2; CAN0_SJW tbl-sjw; /* 退出初始化模式 */ CAN0_CTR.INIT 0u; while (CAN0_STR.INIT ! 0u) { /* 等待硬件重新进入正常模式 */ } return 0u; }INIT 1之后控制器会等当前帧结束或总线空闲才真正进入初始化所以必须轮询CAN0_STR.INIT而不是直接睡固定时间。如果总线上存在持续错误帧INIT 请求可能一直得不到确认超时判断在这里是必需的。退出初始化模式时硬件会重新锁存 BRPR、SEG1、SEG2、SJW 的值并且错误计数器会被复位。写入时先写 BPR 再写段参数或反过来都行因为初始化模式下控制器不采样总线新配置不会生效直到 INIT 被清 0。3.3 通知帧加延时让对端和你同时换挡动态切换波特率在单节点自测时看不出问题一旦上了真实总线收发双方必须同步换挡。我的做法是先用旧波特率发一个 DLC0 的通知帧对端收到后进入等待状态双方延时 10ms再同时切换。void CAN_SpeedChange(uint32_t new_baud) { /* 查表得到目标速率的位时序配置 */ for (uint32_t i 0u; i sizeof(can_timing_table) / sizeof(can_timing_t); i) { if (can_timing_table[i].baud new_baud) { /* 1. 按当前波特率发送通知帧ID 不被业务帧占用 */ CAN_TxFrame(0x555u, (const uint8_t*)0, 0u); /* 2. 给对端留出进入初始化模式的时间 */ DelayMs(10u); /* 3. 切换本端位时序 */ CAN_SetBitTiming(can_timing_table[i]); /* 4. 按新波特率发同步帧让对端做硬同步 */ CAN_TxFrame(0x556u, (const uint8_t*)0, 0u); break; } } }这里的 10ms 不是为了对齐两个控制器的时钟。CAN 协议本身有硬同步机制任何显性跳变沿都能让接收方重新对齐位时序只要两边波特率一致就不存在累积漂移问题。10ms 的真正作用是给对端的协议栈留出处理时间让对端完成进入初始化、改写寄存器、退出初始化这一整套动作并且保证总线上没有正在传输的帧。如果对端调度任务的抖动大这个延时需要按最坏情况放大到 20ms 或 50ms。通知帧的 ID 要避开业务报文段DLC0 是为了把总线占用压缩到最小。同步帧则承担握手职责对端收到后说明新波特率已经能正常完成 ACK 握手。3.4 切换失败时的现场特征如果对端没有成功切换新速率下的第一帧就会出问题。对端仍按 125k 采样 1M 的位流会把 SOF 后的第一个位即判断为位错误随后发出积极错误帧。本端的错误计数器每次加 8连续几帧后进入 error passive最终可能 bus off。出现这种情况时排查顺序是先看本端退出 INIT 后是否回到正常模式再看对端错误计数是否累加最后回到通知帧本身确认它确实被总线上的所有节点都收到了。4. 动态切换的踩坑清单错误帧、Bus-Off与采样点不匹配4.1 切换瞬间的错误帧从哪里来很多人误以为错误帧是因为“波特率改错了”实际上切换瞬间总线上几乎没有正常帧在跑错误帧主要来自两端位序时的不一致。一端已经切到 1M另一端还在 125k高速端发出的每一位在低速端看来都落在采样点之外低速端立刻回一个显性错误帧。这个错误帧反过来又破坏了高速端的正常接收双方互相叠加错误计数一两轮下来就有人进 bus off。这种故障的波形特征很典型切换之后的第一帧数据还没到 DATA 段总线上就出现连续 6 个显性位组成的错误帧。如果你用示波器在 CAN_H/CAN_L 上看到这个形状不用怀疑先查对端节点是否真的完成了切换。4.2 Bus-Off恢复策略让控制器自动恢复还是手动复位RH850 的 RSCAN 模块在进入 Bus-Off 后按 CAN 规范需要等待 128 次 11 位隐性位序列才能恢复。硬件默认会自动执行恢复流程但自动恢复的时间取决于总线负载和错误帧频率。如果总线被某个节点持续干扰自动恢复可能反复失败这时候就需要软件介入。if (CAN0_STR.BOFF ! 0u) { /* 总线关闭TEC 已经达到 255 */ CAN0_CTR.INIT 1u; /* 等待进入初始化模式清掉错误计数 */ uint32_t timeout 10000u; while (timeout-- 0u) { if (CAN0_STR.INIT 1u) break; } CAN0_CTR.INIT 0u; while (CAN0_STR.INIT ! 0u) { /* 等待恢复 */ } }请求 INIT 模式会让控制器彻底停止收发并复位错误计数这和单纯等硬件自动恢复的区别在于自动恢复要等 128 个连续的隐性位序列而手动复位可以在总线上仍然存在高频错误帧时强制打断。排查切换问题之前先读一下错误计数寄存器判断当前总线处于什么状态状态位含义处理建议EWARN警告状态TEC 或 REC ≥ 96需要观察尚未影响通信EPASS错误被动TEC 或 REC ≥ 128不能再发主动错误帧发送受限制BOFF总线关闭TEC 达到 255必须按规范恢复或手动复位4.3 采样点不匹配的典型症状采样点差异是低速看不出来、高速必定翻车的一类问题。125k 的位时间是 8us采样点差 5% 就是 400ns通常不影响1M 的位时间是 1us采样点差 5% 只有 50ns加上总线电容导致边沿变缓误码率会显著上升。如果你遇到“125k 一切正常切到 1M 就开始零星错误帧”先别怀疑晶振精度多半是两边采样点差太多。本方案把两档速率都做成 20TQ、采样点 80%就是为了规避这个问题。另一个节点如果用默认配置比如 75% 采样点在 1M 下和本端的采样时刻相差约 50ns正好落在边沿附近就会出现偶发 CRC 错误。这种错误不是每帧都出现但每出现一次都会让错误计数累加时间长了也会导致节点进入 error passive。4.4 多节点切换顺序先就绪再统一换挡双机通信时先发通知帧的一方占主动权。但总线上挂多个节点时切换顺序就变得关键。如果主机先切从机还没切主机发的同步帧必然触发错误帧反而让主机自己 bus off。更稳妥的顺序是所有节点先在旧速率下上报“就绪”主机确认全部就绪后向所有节点广播切换命令每个节点收到命令后延时相同时间再切换。负载率也要按最低速档位预留。同样一组报文125k 下的总线负载率是 1M 下的 8 倍。如果切换后要长时间保持 125k报文周期必须重新计算否则总线一直高负载运行任何错误帧都可能让处理不过来的一方先掉线。5. 验证切换正确性从示波器到连续压测5.1 用示波器直接量位宽确认当前速率切换后第一件事不是看代码而是用示波器确认总线上实际跑的速率。把 CH1 接到 CAN_H探头地接 CAN_GND触发方式设为下降沿单次抓一帧 DLC0 的报文。从 SOF 下降沿到第一个隐性上升沿的时间在 1M 下约等于 1us在 125k 下约等于 8us。速率一位理论时间SOF 到第一个隐性沿1Mbps1us1us125kbps8us8us量到的时间误差在 5% 以内就说明位时序生效了。如果量出来是 8us但代码里明明配的是 1M优先检查 BRPR 写入值是否被后续代码覆盖。5.2 用CAN工具连续压测并统计错误帧CANoe 或 PCAN 这类工具可以设成对应速率监听。测试序列固定为通知帧、延时、切换、同步帧、1000 帧业务报文、统计错误计数。压测时让这个序列循环 100 次重点关注三个指标切换后第一帧是否发送成功、错误计数器是否归零、是否出现过 bus off。压测过程中如果第一次循环失败而后续成功大概率是通知帧发送时的总线状态不对每次都稳定失败优先查对端节点是否真的在延时结束后切换。不要忽略切换瞬间的 REC 值REC 不为 0 说明有节点在切换后发过错误帧即使后续通信看起来正常也说明同步流程有瑕疵。5.3 切换完成后的第一个报文加一条同步帧再进业务最后一个实用技巧切换之后不要立刻发业务数据先发一条 DLC0 的同步帧然后读错误计数寄存器。只有 REC 和 TEC 全部为 0才说明同步帧已经和对端完成 ACK 握手总线处于健康状态之后才能放行业务报文。/* 读取发送和接收错误计数 */ uint32_t ecr CAN0_ECR; uint8_t rec (uint8_t)(ecr 8); /* 接收错误计数 */ uint8_t tec (uint8_t)(ecr 0xFFu); /* 发送错误计数 */ if ((rec 0u) (tec 0u)) { /* 错误计数为0说明同步帧已经成功完成握手 */ CAN_TxFrame(APP_START_ID, data, len); }注意 RSCAN 的 ECR 寄存器在不同型号里位域命名可能不同有的手册里 TEC 在低字节、REC 在高字节按实际手册调整。同步帧本身不要携带业务数据它的作用纯粹是验证新波特率下总线能否正常收发。REC 清零后再放行业务数据整个切换过程才算真正闭环。本文还有配套的精品资源点击获取