
简介面向瑞萨RH850/F1L汽车级32位MCU的嵌入式开发场景提供一份CAN驱动程序示例围绕速率切换需求演示先以1Mbps通信再切换为125kbps的完整处理流程适合整车控制器、网关等模块开发工程师及相关专业学习者参考。压缩包共18个文件以C源码、汇编启动文件、头文件为核心涵盖驱动与主程序、启动汇编、头文件等以及obj、abs、mot、map等编译输出文件便于对照工程结构理解RS-CAN模块的寄存器配置和速率切换逻辑整体约71KB轻量易用。目前已有274人学习资料内附说明文档和CubeSuite工程文件可直接导入环境查看代码运行效果有助于快速掌握RH850F1L的CAN通信开发要点并为实际项目提供可复用的驱动参考。1. 为什么 RH850F1L 的 CAN 动态切 125k 比初始波特率更麻烦做车载 Bootloader 时第一阶段用 1Mbps 把固件数据灌进去刷完再切到 125kbps 和仪表、传感器通信。RH850F1L 内部 RS-CAN 一共支持 6 路 CAN 通道看着只是把位时序寄存器里的预分频改一下可真正上总线才发现问题另一端还停留在 1M本地切到 125k 后第一帧就出现 ACK 错误严重时直接 Bus Off。原因在于 CAN 没有独立时钟线波特率完全靠每个节点对位的采样时序维持两边只要相差几个 Tq就会在同步段内取错电平。这个名为6SD_RH850F1L_CAN(SpeedChange_1M_125k)的 CS 工程把变速过程拆成了两个显式步骤先以 1M 正常收发再通过r_can.c里的位时序切换函数切到 125k同时保留发送缓冲与接收缓冲。它适合正在调 RH850F1L 驱动的人也适合从 CubeSuite 转过来的单片机学习者建议重点看r_can.c里对CnTSC和通道复位状态的处理而不是只看主循环里怎么发帧。2. RS-CAN 位时序与采样点1M 和 125k 参数如何兼得2.1 CAN 位时间的四段结构与采样点含义CAN 的一个位时间被划分成四段同步段 Sync_Seg 固定为 1 Tq用于识别总线上跳变沿传播段 Prop_Seg 补偿总线传播延迟和收发器输入输出延迟相位缓冲段 PS1 与 PS2 用来吸收晶振误差和重同步采样点位于 PS1 与 PS2 的边界。用公式表达就是位时间 1 TqSync_Seg Prop_Seg PS1 PS2采样点位置 1 Prop_Seg PS1/ 总 Tq 数。采样点用百分比衡量常见值是 75% 到 80%。采样点靠前对线缆传播延迟更宽容采样点靠后对晶振频率偏差更宽容。1Mbps 时整个位时间只有 1 微秒线缆容性稍大就会让采样点附近的电平还没稳定下来。125kbps 时位时间放大到 8 微秒相对容错率更高但如果切换前后采样点百分比不一致对端节点解读边沿位置的基准就变了仍会误判电平。所以这里的关键思路不是单独为 125k 找一组 Tq而是让 1M 和 125k 共用同一套 TSEG1/TSEG2 占比只改变预分频 BRP。这样切换前后的采样点完全重合重同步跳转范围一致线上波形只是被整体拉长协议层看到的时间结构没有变化。这个工程取T_TransmitBuf___R_RcvBuf作为目录后缀说明作者把发送缓冲、接收缓冲和位时序切换看成同一层问题切换时不光要改速率还要管住缓冲里的旧报文。2.2 RS-CAN 寄存器字段分配与实际限制RH850F1L 的 RS-CAN 每个通道都有独立位时序寄存器CnTSC常用字段包括BRP波特率预分频、TSEG1相当于传播段加相位缓冲段1、TSEG2相当于相位缓冲段2以及SJW同步跳转宽度。位时间计算公式为tBit (BRP 1) / fCAN * (1 TSEG1 TSEG2)fCAN是 CAN 模块输入时钟。以 20MHz 为例1Mbps 需要位时间等于 20 Tq125kbps 需要位时间等于 160 Tq。如果保持 TSEG 不变只改 BRP那么 125k 的预分频必须是 8也就是BRP写 7。寄存器不是随便填的。RS-CAN 对位时序有两类硬性约束第一TSEG2至少要 2 Tq给采样点后的相位缓冲留空间第二SJW不能大于TSEG2否则重同步会把相位段推过采样边界。另外1 TSEG1 TSEG2不能小于 4 Tq否则 CAN 控制器无法稳定识别显性到隐性的跳变。很多初稿直接把任意组合填进去位时间算出来是 20 Tq但SJW设置过宽导致切换后偶发 CRC 错误这种问题在示波器上很难复现。2.3 1M/125k 双速率参数表以 CAN 输入时钟 20MHz 为例这个工程演示的一组可用参数如下速率BRPTSEG1TSEG2SJW组成 Tq 数采样点1Mbps015431 15 4 20(115)/20 80%125kbps71543每 Tq 乘以 8位时间 160 Tq(115)/20 80%这里BRP7代表预分频为 820MHz 下每位时间等于8 * 20 Tq 160 Tq对应 125000 bit/s。选择 80% 采样点是为了兼容较长总线SJW3允许在每个报文仲裁字段做最多 3 Tq 的重新同步足以覆盖常见晶振偏差。如果你把SJW改成 1 或者 2系统通常也能运行但切换完成后第一个报文最容易出现位填充错误因为新位时序写入瞬间总线上可能还残留上一速率下的跳变沿。另外要留意不同通道的 CAN 时钟源可能不是同一个。RH850F1L 的时钟树允许给不同 CAN 通道配置不同外设时钟如果两个通道共用同一时钟切换一个通道不会影响另一个通道如果各自独立那么切换前要把目标通道的时钟源也纳入参数表。本工程的r_clock.c是统一配置时钟的所以通道之间不存在时钟树不一致的问题。3. 从位时序表到 r_can.c 初始化与速率切换例程3.1 把参数表映射为寄存器常量CS 工程里的寄存器访问通常通过头文件iodefine.h定义r_can.c内部使用结构体指针访问通道寄存器。我一般会把第 2 章那张表直接写成一张常量查找表便于在 1M 与 125k 之间反复切换而不是在代码里散落魔法数字。参考实现如下typedef enum { CAN_SPEED_1M 0, CAN_SPEED_125K, CAN_SPEED_MAX } can_speed_t; const uint8_t can_brp_tbl[CAN_SPEED_MAX] { 0, 7 }; const uint8_t can_tseg1_tbl[CAN_SPEED_MAX] { 15, 15 }; const uint8_t can_tseg2_tbl[CAN_SPEED_MAX] { 4, 4 }; const uint8_t can_sjw_tbl[CAN_SPEED_MAX] { 3, 3 };索引CAN_SPEED_1M对应 1Mbps索引CAN_SPEED_125K对应 125kbps。这样切速率时只传入枚举值驱动层统一去表里取配置。如果将来要支持 500k 或者 250k只需要扩展表和枚举不用改动切换逻辑。上层业务代码里也不会出现0x07、0x0F这类含义不明的数值。真正的写入函数长这样void R_CAN_SetBitrate(uint8_t ch, can_speed_t rate) { __DI(); /* 切换期间关中断防止接收中断插入 */ /* 进入复位模式 */ CAN0-CnCTR[ch].bit.CEN 0; while (CAN0-CnSTR[ch].bit.RSTST ! 1) { ; } /* 只改 BRPTSEG1/TSEG2/SJW 保持一致 */ CAN0-CnTSC[ch].bit.BRP can_brp_tbl[rate]; CAN0-CnTSC[ch].bit.TSEG1 can_tseg1_tbl[rate]; CAN0-CnTSC[ch].bit.TSEG2 can_tseg2_tbl[rate]; CAN0-CnTSC[ch].bit.SJW can_sjw_tbl[rate]; /* 回到运行模式 */ CAN0-CnCTR[ch].bit.CEN 1; while (CAN0-CnSTR[ch].bit.RSTST ! 0) { ; } __EI(); }__DI()用于关中断避免在写位时序寄存器时被 CAN 接收中断打断。CEN0是进入复位模式的软复位入口RSTST表示通道已经稳定在复位状态。只有等RSTST置位写BRP才安全。CEN1后继续等RSTST变成 0表示通道重新参与总线同步。两个等待循环都不能省略省略后会在硬件尚未完成内部状态切换时直接改写时序造成一帧只识别了一半新速率、一半旧速率的畸变波形。3.2 初始化代码不要重复设置位时序CS 生成的r_main.c一般在主函数开始时调用R_CAN_Init()。我的习惯是让初始化函数只做时钟门控、中断映射和运行模式使能位时序单独交给R_CAN_SetBitrate()设置。如果初始化里写死 1M再调用一次切换函数代码会走两条路径后续维护时很容易看漏。主循环示例void main(void) { R_MAIN_Init(); R_CAN_Init(); /* 先按 1M 跑 */ R_CAN_SetBitrate(0, CAN_SPEED_1M); R_CAN_Start(0); while (1) { R_CAN_TxRequest(0, 0x123, msg, 8); R_CAN_DelayMs(100); if (R_CAN_GetRxCount(0) 0) { R_CAN_ReadRxFifo(0, rx_msg); } } }这里的R_CAN_SetBitrate(0, CAN_SPEED_1M)放在R_CAN_Start之前保证通道处于复位模式时写入位时序。R_CAN_Start负责清错误计数、使能中断并切到运行模式。如果你把R_CAN_SetBitrate放到R_CAN_Start之后就必须自己在函数内部先做复位请求否则寄存器写入可能被硬件忽略。3.3 切换前的缓冲水位处理项目名里的T_TransmitBuf___R_RcvBuf点出了发送缓冲与接收缓冲。切换速率前如果发送缓冲里还有待发送报文最好等它发完或者显式取消。否则复位模式关闭发送器缓冲队列里的帧会在重新打开 CAN 后以新速率发到旧速率网络立刻触发错误帧。我习惯在切速率函数开头检查发送状态for (volatile uint16_t timeout 0; timeout 1000; timeout) { if ((CAN0-CnSTR[ch].bit.TRF 0) (CAN0-CnSTR[ch].bit.TEND 1)) { break; } }TRF表示发送请求状态TEND表示发送完成。两个条件同时满足说明所有发送请求都被处理完。超时则说明有远程帧或错误恢复在持续重传此时不应强制切换应该在r_can.c里记录一个错误状态位让上层决定是回退到 1M 还是重新初始化通道。接收缓冲不用清空因为复位模式下接收功能不更新重新打开后可以继续读残留数据也可以直接丢弃。4. 运行中从 1M 切 125k同步、BusOff 与状态机设计4.1 不能单边切速率必须先握手如果总线上只有两个节点先切的一端会因为找不到匹配的同步段而进入错误处理。所以多节点场景下需要应用层握手主节点先发一条“准备切速率”帧数据区携带目标速率编码0x7D从节点收到后回 ACK双方再约定一个时间点同时切换。CAN 控制器自身没有切速率协议字段这个握手只能由应用层自己定义。在驱动层切换过程必须“串行化”一个任务请求切换其他任务不能插入发送。一种简单做法是在r_can.c里加一个g_can_switch_flag切换完成后由发送函数检查这个标志。示例工程里没有实现完整握手只演示了单节点速率改变所以实际接入网络时要补上这层状态同步否则切换瞬间总线上会出现错误帧风暴。4.2 切换过程的寄存器序列RS-CAN 从运行模式切换到复位模式不是瞬间完成需要等待CnSTR的RSTST位置位。这个等待很重要因为总线可能正处在一个帧的接收过程强行改位时序会让当前帧断裂。推荐序列如下void R_CAN_SwitchTo125k(uint8_t ch) { __DI(); /* 进入复位模式 */ CAN0-CnCTR[ch].bit.CEN 0; while (CAN0-CnSTR[ch].bit.RSTST ! 1) { ; } /* 清发送请求 */ CAN0-CnTXMC[ch].bit.TMTR 0xFF; /* 只改 BRP从 0 改为 7 */ CAN0-CnTSC[ch].bit.BRP 7; /* 回到运行模式 */ CAN0-CnCTR[ch].bit.CEN 1; while (CAN0-CnSTR[ch].bit.RSTST ! 0) { ; } __EI(); }R_CAN_SwitchTo125k是专门给 125k 切换用的函数它把BRP7写成固定值。为什么要先等RSTST因为 CAN 控制器可能在等待总线空闲才会真正进入复位模式此时去写位时序寄存器等于在一帧中间修改采样点轻则当前帧 CRC 错误重则错误计数暴涨。TMTR0xFF的作用是取消所有发送请求防止旧速率残帧发出。整个函数要在中断关闭状态下执行否则定时器回调可能在切换中间插入发送。4.3 远端未同步时的 Bus Off 恢复状态机如果握手失败远端还在 1M本机已经切到 125k本机会持续产生错误帧最终进入 Bus Off。RS-CAN 在 Bus Off 后会自动等待 128 次总线空闲但对于切换场景更好的策略是让驱动在 Bus Off 时回退到 1M而不是盲目恢复在 125k。状态机可以写成typedef enum { CAN_SM_1M, CAN_SM_WAIT_SWITCH, CAN_SM_125K, CAN_SM_BUSOFF } can_sm_t; void R_CAN_Task(void) { switch (g_can_sm) { case CAN_SM_1M: if (g_switch_req_ready 1) { R_CAN_SendSwitchReq(); g_can_sm CAN_SM_WAIT_SWITCH; } break; case CAN_SM_WAIT_SWITCH: if (g_switch_ack 1) { R_CAN_SwitchTo125k(0); g_can_sm CAN_SM_125K; } break; case CAN_SM_125K: R_CAN_PollError(); break; case CAN_SM_BUSOFF: R_CAN_SwitchTo1M(); R_CAN_ReInit(); g_can_sm CAN_SM_1M; break; } }在CAN_SM_BUSOFF里回退到 1M是为了避免重新进入 125k 后继续错误风暴。R_CAN_SwitchTo1M内部做的是把BRP从 7 改回 0并重新初始化错误计数。RS-CAN 的 Bus Off 恢复时序由硬件完成但驱动不要立刻发第一帧最好再等待一个应用层超时等总线上已经没有错误帧残留。4.4 采样点不变不等于重同步余量不变切换前后保持SJW3对 125k 这种 160 Tq 的长位时间来说3 Tq 的重同步范围并不大。如果整条总线上混用 16MHz 和 20MHz 晶振的节点频率偏差叠加后可能接近 1 TqSJW3通常够用但余量并不充裕。要把切换做得更稳可以适当放大SJW前提是SJW不能大于TSEG2。如果把TSEG2从 4 改成 5采样点会从 80% 下降到(115)/(1155)76.2%仍然在可接受范围但位时间不再与 1M 保持 8 倍整数关系。这种改动适合总线长度超过 20 米的情况在 Bootloader 刷写场景里固定采样点优先级更高保持TSEG115、TSEG24、SJW3是最稳妥的做法。5. 切换效果的边界验证错误计数、位宽测量与参数枚举5.1 切换后用错误计数判断是否稳定切换是否成功最直接的办法是读通道错误计数器。切换完成后以 1ms 为间隔连续读取 3000 次如果REC和TEC始终为 0说明总线稳定。参考代码uint8_t can_error_count[2]; void R_CAN_CheckStable(uint8_t ch) { for (uint16_t i 0; i 3000; i) { can_error_count[0] CAN0-CnEC[ch].bit.REC; can_error_count[1] CAN0-CnEC[ch].bit.TEC; if (can_error_count[0] || can_error_count[1]) { /* 第一次出现错误帧时立刻退出便于上层重试 */ break; } } }如果错误计数先增长后回落往往是总线容性负载偏大采样点边缘不够稳定。这种问题在示波器上不容易看出来因为波形看起来仍是完整方波只有统计错误帧才能暴露。5.2 逻辑分析仪直接测单比特宽度用逻辑分析仪抓 CAN_H 对 GND 的波形最直观。1M 模式下单个显性位接近 1 微秒125k 模式下接近 8 微秒。如果解码器设置成 125k 后能持续解码并且显示位率偏差小于 0.5%说明位时序参数正确。注意 CAN 帧里有填充位不要用连续相同电位的长度除以位数来估算位宽要直接看解码器的位率统计。5.3 用 Python 枚举合法位时序组合适配不同晶振如果你的板子不用 20MHz而是用 16MHz 晶振或内部时钟 40MHz就需要重新算 BRP 和 TSEG。我一般用下面的短脚本枚举落在 75% 到 85% 采样点之间的组合def search_bit_timing(f_can, bitrate): target f_can // bitrate for brp in range(0, 256): tq_total target // (brp 1) if tq_total not in range(16, 26): continue for tseg1 in range(2, 16): for tseg2 in range(2, 8): if 1 tseg1 tseg2 ! tq_total: continue sp (1 tseg1) / tq_total * 100 if 75 sp 85: print(brp, tseg1, tseg2, sp) search_bit_timing(20_000_000, 125_000)脚本把位时间限制在 16 到 25 Tq这个范围能兼顾采样精度和晶振误差积累。输出组合后再结合当前 1M 参数选择最接近的一组。如果输出为空可以放宽 Tq 范围到 1030并把采样点范围收窄到 78%82%更容易找到一组保持采样点一致的配置。本文还有配套的精品资源点击获取