简介:面向STM32H7嵌入式开发者的FDCAN与CAN兼容通信完整工程,聚焦Cortex-M7平台下CAN-FD高速通信的实际落地。工程包含从STM32CubeMX引脚初始化到FDCAN位速率设定、滤波器配置、中断处理,以及音频卡场景中CAN总线控制与数据收发的测试代码。压缩包共344个文件、约25.95MB,以h头文件、c源程序、uvprojx工程、ioc配置为主,同时包含o/axf/hex等编译链接产物和map映射文件,可直接打开查看、重新编译和烧录验证。已有4198人学习下载,由Fairchild_1947整理发布。深入实践该工程,可理解CAN-FD与传统CAN的速率差异、自动生成初始化代码的结构,掌握错误帧诊断、状态机管理及针对总线通信的测试策略,对嵌入式硬件与单片机项目的通信设计和排错具有直接参考价值。 前阵子整理了一套基于 STM32H7 的 CAN 总线工程模板,核心就一句话:用 FDCAN 外设同时兼容 CAN FD 和经典 CAN 2.0。这个需求在车载和工控里太常见了,产线上老的 250kbps 经典 CAN 节点还没淘汰,新的控制单元又想用 2Mbps 甚至 5Mbps 的 CAN FD 拉数据,这时候换 H7 是现实的选择——它内置的 FDCAN 硬件支持两种帧格式,但真正把工程排顺手还得处理一堆细节。
这篇把完整思路捋一遍:工程架构怎么设计、波特率和采样点怎么算、初始化代码怎么配、实测中踩了哪些坑。文字尽量落在可操作层面,如果你正在从 F1/F4 往 H7 迁移,或者刚接触 FDCAN 准备和经典 CAN 节点混跑,应该能直接用上。
1. 从需求出发:为什么选 FDCAN 还要兼容经典 CAN
1.1 这个工程要解决的实际问题
先还原一下需求场景。手里的项目是一个网关控制板,CAN1 总线上挂了好几个老传感器,这些传感器的 MCU 只实现了经典 CAN 2.0,速率固定在 250kbps;CAN2 总线准备接新的摄像头和控制单元,数据量大,想跑 CAN FD。板子用 STM32H7,因为内部 FDCAN 外设既能发经典帧也能发 FD 帧,理论上一个外设就能吃下两种总线需求。但实际工程里有两个问题:一是 FDCAN 的 FIFO、滤波器、错误处理机制和 F1/F4 的 bxCAN 完全不同,寄存器基本没法平移;二是源码层如果写两套驱动,经典一套、FD 一套,维护成本太高,而且发错帧格式会直接导致总线上错误帧一片。
所以“兼容完整工程”的目标可以拆成三条。第一,协议层统一,上层应用只调一个发送接口,内部根据参数自动选择帧格式;第二,配置层统一,滤波、采样点、错误中断一次配好,经典帧和 FD 帧共用;第三,调试层友好,帧统计、总线错误状态能直接读出来。这套思路做完之后,同样代码可以直接在下个项目的 FDCAN 实例上复制一份,改改参数就能用。
1.2 FDCAN 与 bxCAN 的硬件差异
很多从 F1/F4 转过来的工程师第一反应是按照 bxCAN 的思维去填邮箱,然后发现 H7 的 FDCAN 根本不是那个玩法。bxCAN 管的是 28 个邮箱,逻辑简单;FDCAN 则把所有消息对象集中放在一块 Message RAM 里,滤波器、接收 FIFO0、接收 FIFO1、发送缓冲、发送事件 FIFO 都是通过寄存器指定基地址和长度来划分的。你看 CubeMX 里 FDCAN 配置页面有一排选项是 Message RAM 分区,就是这个原因。
帧格式上也有明显区别。经典 CAN 帧最长 8 字节,FDCAN 支持最长 64 字节;经典 CAN 的波特率是全帧固定速率,CAN FD 则把仲裁段和数据段分开,仲裁段用较低速率,数据段可以切到更高速率,这就是 BRS 位。还有 ESI 位用来标识发送节点是否处于错误被动状态,这个在 bxCAN 里没有。这些差异决定了工程一定要做抽象层,否则上层业务会被底层外设差异绑死。
2. 工程架构设计:一个接口收编两种帧格式
2.1 抽象层:上层不关心是经典帧还是 FD 帧
我在这套工程里定义了一个统一的can_msg_t结构体,包含 ID、ID 类型、DLC、数据指针、以及两个关键标志:is_fd和brs。上层应用填充这个结构体,然后调用can_send(channel, &msg),完全不关心底层是 FDCAN1 还是 FDCAN2,也不关心是哪种帧格式。
接收端用回调机制,can_register_rx_cb(channel, callback)注册回调函数。底层中断里收到帧之后,把 FDCAN 的 RxHeader 转换成统一的can_msg_t,再交给解包函数处理。这样做的直接收益是:后面如果要把工程从 H7 换到别的带 FDCAN 或者同类控制器的平台,只改驱动文件即可,业务代码一行不用动。
2.2 帧类型、DLC 与发送策略
经典 CAN 的 DLC 最大 8,CAN FD 的 DLC 映射不是简单的“值等于字节数”,0~8 是直接对应,8 以上被映射到 12、16、20、24、32、48、64 这些档位。所以代码里一定不要用裸数字写死 DataLength 字段,HAL 库提供了FDCAN_DLC_BYTES_64这类宏,能查表就查表。
发送策略上有个很容易踩的坑:总线上只要还有不支持 FD 帧的节点,广播帧就必须发经典格式。因为经典 CAN 控制器识别到 FD 帧里的非标准位,会把它判定成格式错误,然后主动在总线上置错误帧,整个总线通信都会被拖垮。这不是“大数据用 FD、小数据用经典”这么简单的事,而是要看对端节点能力。我的处理方式是:点对点的高速数据通道走 FD,广播类消息和诊断类消息统一走经典格式。
2.3 滤波器和 Message RAM 规划
FDCAN 的滤波器分标准 ID 滤波器(SIDFC)和扩展 ID 滤波器(XIDFC),两种滤波器元素支持范围、位掩码、双 ID 等模式。一个总线同时收经典帧和 FD 帧时,滤波器并不需要为帧格式额外做什么,因为过滤只管 ID,不管帧类型。
Message RAM 规划上,我建议 FIFO0 分配 8 条、FIFO1 分配 8 条、TX buffer 用 16 条左右,够大多数场景。H7 每个 FDCAN 实例的 Message RAM 大约 10KB,这部分空间既要给滤波器和 FIFO,也要给发送缓冲。配太大后面代码里没有别的用途,配太小高峰期容易丢帧。多实例共用时还要注意偏移量,FDCAN1 和 FDCAN2 的 Message RAM 区域不能重叠,否则会出现一个实例的帧被另一个实例覆盖的诡异问题。
3. 核心配置:波特率、采样点与时间量子计算
3.1 位时序的几个基础概念
FDCAN 的每一个位时间由同步段(Sync Seg)、传播段、相位缓冲段 1(PS1)、相位缓冲段 2(PS2)组成。采样点落在 PS1 和 PS2 之间。时间量子 tq 是基本单位,每一位由若干个 tq 组成,整体按预分频器从外设时钟分频得到。采样点的含义就是“在一个位周期内,已经走过的 tq 数占总 tq 数的百分比”。
采样点太靠前,对线路传播延迟的容忍度差;采样点太靠后,重同步空间不够,容易失步。一般取 70%~87.5%,经典 CAN 常用 75%~80%,CAN FD 数据段因为速率高,常取 75% 左右。这里要多说一句:接线距离长、终端电阻匹配不好,采样点的影响会更明显。我曾经在一根 2 米长的测试线缆上看到连续错误帧,最后排查下来根本不是速率问题,而是采样点差了 3%,调整之后错误帧立刻消失。所以不要随便抄例程里的采样点参数。
3.2 一个 500kbps + 2Mbps 的完整计算实例
拿我的工程举例,仲裁段 500kbps,数据段 2Mbps,FDCAN 外设时钟 40MHz。这里用的是 H7 内部 PLL 单独分配出来的时钟源,不是简单等于 APB1。
仲裁段:
- 预分频器 prescaler = 4,得到 10MHz 的 tq 频率。
- 每个位 = 10MHz / 500kbps = 20 tq。
- 同步段 1 tq,PS1 = 14 tq,PS2 = 5 tq。
- 采样点 = (1 + 14) / 20 = 75%。
- SJW 取 4 tq。
数据段:
- 预分频器 DBRP = 1,得到 40MHz 的 tq 频率。
- 每个位 = 40MHz / 2Mbps = 20 tq。
- 同步段 1 tq,PS1 = 14 tq,PS2 = 5 tq,采样点同样 75%。
- SJW 取 4 tq。
CAN FD 仲裁段和数据段使用独立的位时序寄存器,HAL 库配置项里 Nominal 开头的对应仲裁段,Data 开头的对应数据段。如果数据段要跑 5Mbps,40MHz 时钟下每个位只有 8 tq,PS1 取 5、PS2 取 2,采样点 (1+5)/8 = 75%,可做,但容错窗口很小,SJW 只能取 1~2,对晶振精度要求高。所以新项目如果器件允许,我通常把 FDCAN 时钟喂到 80MHz,这样数据段 5Mbps 时还能留 16 tq/位,配置空间宽裕很多。
提示:每次改动 CAN 时钟源,哪怕只是从一个 PLL 输出切到另一个,都建议把采样点重新算一遍。采样点错了,示波器上会看到连续错误帧,控制器不会自动恢复。
3.3 采样点与 SJW 怎么选
给一个实际可参考的配置推荐表,方便直接套用:
| 速率组合 | 晶振精度 | 采样点 | SJW | 备注 |
|---|---|---|---|---|
| 仲裁 500kbps / 数据 2Mbps | 20ppm | 75%~80% | 4~5 tq | 常规车载场景 |
| 仲裁 1Mbps / 数据 5Mbps | 50ppm | 75% | 2~3 tq | 数据段窗口较小,注意收发器 |
| 仲裁 250kbps / 数据 1Mbps | 50ppm | 80% | 4~5 tq | 适合老节点混跑 |
SJW 的作用是实现重同步。总线空闲之后如果出现边沿偏移,控制器允许每次重同步时把相位缓冲段扩展或者缩短的值就是 SJW。取值太小,时钟偏差一大就追不上;取值太大,毛刺可能被当成有效边沿而误采样。一般取 1~4 tq,并且不能超过 PS2 的值。
4. 实操:从零配置 H7 FDCAN 的关键代码与步骤
4.1 时钟、引脚与 CubeMX 配置要点
CubeMX 下先把 FDCAN 外设时钟配好。H7 的 FDCAN 时钟可以从 APB1 或 PLL2 来,建议在 Clock Configuration 里给 FDCAN 单独安排一个 40MHz 或 80MHz 时钟,然后在 NVIC 里打开 FDCAN1_IT0、FDCAN1_IT1 中断。引脚常用 PA11/PA12 或 PB8/PB9,在芯片手册的 AF 复用表里查对应编号即可。
FDCAN 的 HAL 初始化配置里有一个关键点:MessageRAMOffset参数。单实例使用填 0,多实例时要根据实际地址把偏移拉开。另外别忘了调低滤波器的数量,如果 CubeMX 默认给了 128 个标准滤波器,而 Message RAM 总共就那么大,配置不当会导致初始化失败或者空间冲突。
4.2 FDCAN 初始化代码
关键配置如下,注意 Nominal 和 Data 两组参数分别对应仲裁段和数据段:
FDCAN_HandleTypeDef hfdcan1; hfdcan1.Instance = FDCAN1; hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1; hfdcan1.Init.FrameFormat = FDCAN_FRAME_FD_BRS; hfdcan1.Init.Mode = FDCAN_MODE_NORMAL; // 仲裁段 500kbps hfdcan1.Init.NominalPrescaler = 4; hfdcan1.Init.NominalTimeSeg1 = 14; hfdcan1.Init.NominalTimeSeg2 = 5; hfdcan1.Init.NominalSyncJumpWidth = 4; // 数据段 2Mbps hfdcan1.Init.DataPrescaler = 1; hfdcan1.Init.DataTimeSeg1 = 14; hfdcan1.Init.DataTimeSeg2 = 5; hfdcan1.Init.DataSyncJumpWidth = 4; hfdcan1.Init.MessageRAMOffset = 0; HAL_FDCAN_Init(&hfdcan1);滤波器配置,这里配了一个标准 ID 掩码模式,所有经过掩码匹配的帧都进入 FIFO0:
FDCAN_FilterTypeDef sFilter; sFilter.IdType = FDCAN_STANDARD_ID; sFilter.FilterType = FDCAN_FILTER_MASK; sFilter.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; sFilter.FilterID1 = 0x100; sFilter.FilterID2 = 0x7FF; /* mask,0x7FF 表示不掩码 */ HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilter);帧过滤只按 ID 匹配,经典帧和 FD 帧都会进 FIFO0。要同时接收扩展 ID,还需要再配置扩展滤波器,类型选FDCAN_EXTENDED_ID。
4.3 发送与接收的完整逻辑
发送接口的核心逻辑是根据is_fd标志决定 FDFormat 字段:
int can_send(CAN_Channel ch, const can_msg_t *msg) { FDCAN_TxHeaderTypeDef txh; memset(&txh, 0, sizeof(txh)); txh.Identifier = msg->id; txh.IdType = (msg->id_type == CAN_ID_EXT) ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID; txh.TxFrameType = FDCAN_DATA_FRAME; if (msg->is_fd) { txh.DataLength = dlc_to_fdcan(msg->dlc); txh.FDFormat = FDCAN_FD_CAN; txh.BitRateSwitch = msg->brs ? FDCAN_BRS_ON : FDCAN_BRS_OFF; } else { txh.DataLength = dlc_to_fdcan(msg->dlc); txh.FDFormat = FDCAN_CLASSIC_CAN; txh.BitRateSwitch = FDCAN_BRS_OFF; } return HAL_FDCAN_AddMessageToTxMailbox(&hfdcan1, &txh, msg->data) == HAL_OK ? 0 : -1; }接收端在中断回调里取帧,然后转换成长度信息交给上层:
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t fifoIndex) { FDCAN_RxHeaderTypeDef rxh; uint8_t data[64]; HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &rxh, data); can_msg_t msg; msg.id = rxh.Identifier; msg.is_fd = (rxh.FDFormat == FDCAN_FD_CAN); msg.dlc = fdcan_to_dlc(rxh.DataLength); memcpy(msg.data, data, msg.dlc); notify_rx(0, &msg); }接收时有一个细节需要留意:rxh.DataLength返回的是一个编码值,不能直接当字节个数来用,要经过映射表转成真正的字节数。否则 64 字节的 FD 帧会被错误地截断。
4.4 用 USB-CAN 工具做冒烟测试
我调试这套工程时习惯用 USB-CAN 分析仪,无论是周立功还是创芯这类工具都可以。先在 PC 端软件里建一个 500kbps 的经典通道,确认发过去的经典帧 ID、数据都对;再把工具切到 CAN FD 模式,数据段速率选 2Mbps,发一条 64 字节的 FD 帧验证双向通信。这一步能提前暴露不少问题,比如收发器只支持经典 CAN,在 FD 模式下数据段会直接采不到数据。
板上再放一个周期任务,每 100ms 交替发送经典帧和 FD 帧,用分析软件的时间戳和帧类型列表确认两种格式在总线上是否和平共处。如果看到错误帧计数器在涨,基本可以判定是某个节点不支持其中一种帧格式。
5. 实测中踩过的坑与排查思路
5.1 经典 CAN 节点被 FD 帧“冲”出错误帧
第一次把 FD 帧发到带经典节点总线上,总线上立刻冒出连续错误帧,经典节点像被干扰了一样疯狂报错。原因不复杂:经典 CAN 控制器不识别 FD 帧的连续位形式、BRS 位等,把它判定成格式错误,一旦检测到就会主动发错误帧。看起来像干扰,其实是协议协商失败。
解决方式要回到总线规划上:把“这条总线是否能跑 FD 帧”作为硬约束。我当时的做法是广播帧永远发经典格式,点对点的高速数据通道才切 FD。如果混跑节点特别多,更稳妥的办法是把 FD 节点和经典节点放到两条物理总线,网关板做桥接,彻底隔离两种帧格式。
5.2 数据段乱码,最后发现是收发器不支持 FD
把工程烧到另一块板子上,仲裁段 500kbps 通信正常,数据段一开 BRS 就乱码。查了一整天,最后发现那块板子上的 CAN 收发器型号只支持经典 CAN,数据段从 500kbps 切到 2Mbps 时收发器根本跟不上。
现在市面上很多型号标着“CAN 收发器”,实际上对 CAN FD 的支持程度差别很大。选型时一定要看数据手册有没有明确写支持 CAN FD 或 2Mbps/5Mbps 数据段。另外 PCB 层的处理也很关键,FD 数据段速率高,走线长度和终端匹配的影响比经典 CAN 明显得多,120Ω 终端电阻的位置、是否有共模电感,最好严格按参考设计来。
5.3 CAN 矩阵里字节序和位序带来的信号错位
CAN 矩阵(DBC 文件)里每个信号都会定义字节序:Intel(小端)和 Motorola(大端)。很多人在代码里直接 memcpy,结果发现信号值对不上。一个 16 位信号,Intel 字节序就是低字节在前,可以直接把小端结构体数据发过去;Motorola 字节序则不同,某些跨字节信号不但字节要交换,位也要按矩阵指定的 bit 顺序重新排列。
建议在工程里写一个通用的信号打包、解包层,所有信号按 DBC 矩阵的起始字节、起始位、长度来映射,不要在业务代码里裸拼数据。我自己的做法是写一个脚本,把 DBC 矩阵导出成 C 结构定义和打包、解包函数。后来换 CAN ID 或者调整信号位置,只改脚本生成的映射表,业务逻辑一行不动。这个习惯帮我省了很多排查信号错位的精力。
5.4 一帧时间的快速估算,评估总线负载
有时候要评估总线负载,得先算一帧 CAN FD 帧占多长时间。这里给个快速估算方法。经典 CAN 标准数据帧固定开销约 44 位,加上 8 倍 DLC 位,除以波特率就是时间。CAN FD 帧要复杂一些,因为仲裁段和数据段速率不同,公式可以写成:
T_total = 仲裁段位数 / 仲裁段速率 + 数据段位数 / 数据段速率。
举一个实际例子,64 字节 FD 帧,仲裁 500kbps、数据 2Mbps:
- 仲裁段大约 70us 左右。
- 数据段 64 字节就是 512 位,加上协议开销大约 300us。
- 一帧总时间不到 400us。
算完会有一个直观感受:CAN FD 的价值不只是单帧能放 64 字节,而是同样的周期窗口里能塞进更多有效载荷,总线负载压力小很多。做总线规划时,我一般把负载控制在 40% 以下,留足峰值余量。
5.5 错误状态与总线恢复,别把恢复当常态
FDCAN 有错误主动、错误被动、总线关闭三个状态。错误计数器 TEC 和 REC 可以从寄存器里读出来,总线关闭后 FDCAN 会执行恢复流程,检测到 128 个连续空闲位后回到错误主动。这个恢复时间对某些实时控制场景来说可能太久了,不能把总线关闭后的自动恢复当成正常流程。
建议在工程里打开错误中断,记录当前是哪个节点在发错误帧,而不是等总线自己恢复。之前排查一个老问题时,发现某个节点周期性搞挂总线,就是靠 TEC/REC 的数值定位到软件里某条消息 ID 的 DLC 填错导致的。错误中断里加一个计数器和标志位,比事后看波形高效得多。
后面我在这个模板上又做了些扩展,加了发送事件 FIFO,用来确认帧确实在总线上发出去;也接了 FreeMASTER 在线看变量。FDCAN 和经典 CAN 兼容这件事本身难度不大,真正容易出问题的地方在于一开始就把帧格式策略定清楚。如果你也在做类似的混合总线工程,建议先把总线拓扑和帧格式规划写进设计文档,再碰代码。这比我当初直接上来改寄存器省事得多。
本文还有配套的精品资源,点击获取