简介:面向 STM32 开发者,这是一份基于 STM32CubeMX 的 CAN-FD 实战工程合集,涵盖 STM32G474 的 FDCAN 外设驱动与 STM32F405RGT6 的经典 CAN 接口配置,也包含闪存编程相关内容。压缩包共 164 个文件,以 h 头文件与 c 源码为主,另有 ioc、uvprojx、uvoptx 等 STM32CubeMX 与 Keil 工程配置文件,以及启动汇编 s 文件和 mxproject 工程说明,整体仅 1.32MB,结构清晰,适合需要快速上手 STM32 CAN-FD 通信的嵌入式工程师参考。资源来自 CSDN 作者 weixin_42696271,已有 1061 人学习下载。通过示例代码可掌握 HAL 库下 FDCAN/CAN 的初始化、滤波器设置、报文收发与中断处理,并了解如何在 STM32G474 上结合闪存读写实现参数保存;两个系列工程的对照便于迁移和复用,对做实时控制、车载通信或工业总线开发都有直接参考价值。
1. FDCAN 在 stm32g474 上为什么值得单独研究
STM32G474 的 FDCAN 外设是 Bosch M_CAN 核心的实例,它同时兼容经典 CAN 协议和 CAN FD 协议。很多从 stm32f103 时代过来的工程师,习惯把 CAN 收发当成一个改改波特率就能跑的中断外设,但换到 stm32g474 的 fdcan,事情开始不一样:数据场最长 64 字节,仲裁段和数据段可以跑两套波特率,外设自带 128 个过滤元素,收发路径上有 RX FIFO、TX FIFO、TX Queue、TX Buffer 四种缓冲组合。用 stm32cubemx 生成工程只是起点,位时序怎么算、滤波器怎么分、上电后收到的标定数据怎么安全写进 G4 的 Flash,这些决定了板子挂在真实 can 总线上能不能稳定跑。这篇文章按 stm32cubemx 配置 FDCAN、收发送数据、Flash 存储这条链路往下走,把参数和坑一次说清楚。
2. FDCAN 位时序与外设架构:先把 stm32g474 的 can/fdcan 资源理清
2.1 经典 CAN 到 FDCAN:速率、帧格式和总线仲裁的变化
FDCAN 与经典 CAN 的差异不是「更快」两个字能带过的。经典 can 总线数据场固定 8 字节,波特率全帧统一,比如 500kbps,整帧都是 500k。FDCAN 在帧头的仲裁段仍用较低速率保证与 CAN 2.0 节点共存,而从控制场 BRS 位之后的数据段可以切到更高速率,常见配置是仲裁段 1Mbps、数据段 5Mbps。这意味着总线上的老节点能识别帧头,但读到 BRS 位之后就可能出错,所以 FD 帧不能混进只支持 CAN 2.0 的网络,这是设计整车或工业网络拓扑时第一件要确认的事。
帧格式上,FD 帧数据场从 8 字节跳到 12、16、20、24、32、48 直到 64 字节,同时 CRC 段也换成 17 位或 21 位多项式,数据段速率越高,对晶振精度和位时序的余量要求越苛刻。stm32g474 的 fdcan 还在仲裁机制上和经典 CAN 保持一致:多个节点同时发送时,ID 越小优先级越高,隐性位被显性位覆盖的裁决过程发生在仲裁段。由于仲裁段是低速段,数据段高速切换后不再存在总线仲裁,这也正是 FDCAN 能提升有效带宽的原因之一。
2.2 FDCAN1/FDCAN2 实例与消息 RAM 分配
STM32G474 集成两个 FDCAN 实例,FDCAN1 和 FDCAN2,它们共用一个消息 RAM,但各自可以通过寄存器配置滤波器、FIFO 和 TX Buffer。消息 RAM 不是简单的一片数据缓冲区,它被划分成几个可配置区域:标准/扩展过滤元素列表、RX FIFO 0、RX FIFO 1、TX Event FIFO、TX Buffer / TX Queue / TX FIFO。用 stm32cubemx 配置时,界面上会出现这些区域的大小选项,比如 RX FIFO 0 深度、TX Buffer 个数,每个选项改动都会改变整个消息 RAM 的地址布局。
G4 系列 FDCAN 的过滤资源是它的强项,每个 FDCAN 实例可以配置最多 128 个过滤元素,标准 ID 用 28 位过滤元素,扩展 ID 用 32 位过滤元素。所谓元素,就是一条条过滤规则,每一条有两种工作方式:Mask 方式和 List 方式。Mask 方式拿一个 ID 加一个掩码去匹配一段 ID 区间,List 方式直接精确匹配连续列表里的 ID。实际做汽车网关的时候,我一般把关键报文用 List 精确收,把一组同类型报文用 Mask 归并收,两种方式按需混在同一张过滤表里。
2.3 FDCAN 位时间与采样点计算
FDCAN 的位时间由四段组成:同步段 SYNC_SEG、传播时间段、相位缓冲段 BS1、相位缓冲段 BS2。一个位时间对应一个或多个 Tq(Time Quantum),Tq 由外设时钟和波特率预分频器决定。位时间配置有几个参数需要关注:预分频器设有总线时钟分频系数,此外还有 SJW、采样点偏移、数据段位时间。数据段位时间的相位缓冲段与仲裁段是分开配置的,因为数据段跑 5Mbps 时,一个位时间只有 200ns,任何一段配多了,采样点误差都会被放大。
| 参数 | 仲裁段典型配置 | 数据段典型配置 | 说明 |
|---|---|---|---|
| 预分频器 | 2 | 1 | 分频后得到 Tq 时钟 |
| 传播时间段 | 1 | 1 | G4 的 BS1 配置里通常合并传播段 |
| BS1 | 13 | 4 | 决定位宽前半段 |
| BS2 | 2 | 3 | 决定位宽后半段 |
| 采样点 | 约 80% | 约 75% | 高速率下采样点不宜过高 |
| SJW | 4 | 2 | 同步补偿宽度 |
以 FDCAN 时钟 80MHz、仲裁段 1Mbps 为例,一个位时间需要 80 个 Tq,预分频器取 1,Tq 为 12.5ns。SYNC_SEG 固定占 1 个 Tq,BS1 加传播时间段共 13 个 Tq,BS2 取 2 个 Tq,采样点约在 87.5% 处,对 1Mbps 足够。数据段 5Mbps 时一个位时间 16 个 Tq,预分频器保持 1,BS1 取 10、BS2 取 5,采样点约 68.8%。这里不用手算到小数点后两位,stm32cubemx 界面里输入期望波特率和采样点,自动反推出位时序参数,但手算能力能帮你在总线报错时判断是参数给过头还是时钟源不对。
计算 Tq 的公式是:Tq = (Prescaler + 1) / FDCAN_kernel_clock,位时间 = (1 + BS1 + BS2) 个 Tq,采样点 = (1 + BS1) / (1 + BS1 + BS2)。在 stm32 的 hal 库FDCAN_InitTypeDef里对应NominalPrescaler、NominalSyncJumpWidth、NominalTimeSeg1、NominalTimeSeg2和DataPrescaler、DataSyncJumpWidth、DataTimeSeg1、DataTimeSeg2两组字段,配置时容易漏的是 Data 组,如果只填仲裁段不填数据段,FD 帧根本发不出去。
3. 用 stm32cubemx 配置 stm32g474 FDCAN 与生成工程
3.1 时钟树:FDCAN 内核时钟到底走哪条路
不少人在 stm32cubemx 里配完 FDCAN 外设却发现波特率差一倍,问题出在时钟树上。FDCAN 外设的时钟不是直接挂 PCLK1,而是有独立的内核时钟源,可以在 RCC 配置页里选择。常见的做法是把 PLL1Q 输出接到 FDCAN kernel clock,让 FDCAN 时钟与串口、定时器各自独立,避免 PCLK1 被其他外设的分频影响。
进入 Clock Configuration 页面后,找到 FDCAN1 kernel clock 的选项,候选一般是 PCLK1 或 PLL1Q。PCLK1 走 APB1 分频链,如果总线外设多、分频系数改来改去,FDCAN 时钟会被牵动。PLL1Q 是独立的 PLL 输出,配置为 80MHz 或 100MHz 之后长期不变,位时序分析起来更可控。stm32g474 的 FDCAN 总线时钟范围在数据手册里有上限,比如 100MHz,配置时建议在 CubeMX 里把 PLL1Q 调到 80MHz,留出裕量。
3.2 Connectivity 页面的 FDCAN 参数配置
在 Pinout & Configuration 页面选中 FDCAN1,打开 Parameter Settings,重点看这几个分组:
| 配置分组 | 关键字段 | 推荐值 | 作用 |
|---|---|---|---|
| Protocol | Frame Format | FD 还是 Classic | 决定只发经典帧还是支持 FD 帧 |
| Protocol | Bit Timings | Nominal 1M, Data 5M | 仲裁段与数据段波特率 |
| Protocol | Automatic Retransmission | Disable | 调试期建议关闭,避免总线错误风暴 |
| Message RAM | Standard Filters | 28 | 标准 ID 过滤元素个数 |
| Message RAM | Extended Filters | 12 | 扩展 ID 过滤元素个数 |
| Message RAM | Rx FIFO0 Elements | 32 | 接收 FIFO 深度 |
| Message RAM | Tx Buffers | 16 | 发送缓冲个数 |
| Message RAM | Tx Event FIFO | 16 | 发送完成事件记录 |
| Interrupt | Notification Selection | RX FIFO0 Message | 使能接收中断 |
Automatic Retransmission这个选项建议在调试初期设为 Disable。如果网络上有节点没回 ACK,硬件默认会不停重发,总线一直被占住,其他报文全被拖死。关闭重发后发送失败会进FDCAN_TX_MAILBOX状态错误回调,日志里能直接看到哪一帧没发出去,定位问题快得多。
Filter 数量按实际报文规划,标准 ID 报文多就多分标准过滤元素,扩展 ID 报文是少数就少分几个。消息 RAM 总量有限,Filter 分多了 FIFO 就浅,报文高频率进来时容易溢出。
3.3 生成代码后的 FDCAN 初始化骨架
CubeMX 生成工程后,MX_FDCAN1_Init()函数会给出外设初始化,但在调用HAL_FDCAN_Start()之前,还缺滤波器和回调注册。先看生成的初始化函数:
static void MX_FDCAN1_Init(void) { hfdcan1.Instance = FDCAN1; hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1; hfdcan1.Init.FrameFormat = FDCAN_FRAME_FD_BRS; hfdcan1.Init.Mode = FDCAN_MODE_NORMAL; hfdcan1.Init.AutoRetransmission = DISABLE; hfdcan1.Init.TransmitPause = DISABLE; hfdcan1.Init.ProtocolException = DISABLE; hfdcan1.Init.NominalPrescaler = 1; hfdcan1.Init.NominalSyncJumpWidth = 4; hfdcan1.Init.NominalTimeSeg1 = 13; hfdcan1.Init.NominalTimeSeg2 = 2; hfdcan1.Init.DataPrescaler = 1; hfdcan1.Init.DataSyncJumpWidth = 2; hfdcan1.Init.DataTimeSeg1 = 10; hfdcan1.Init.DataTimeSeg2 = 5; hfdcan1.Init.StdFiltersNbr = 28; hfdcan1.Init.ExtFiltersNbr = 12; hfdcan1.Init.RxFifo0ElmtsNbr = 32; hfdcan1.Init.RxFifo0ElmtSize = FDCAN_DATA_BYTES_64; hfdcan1.Init.RxBuffersNbr = 0; hfdcan1.Init.TxEventsNbr = 16; hfdcan1.Init.TxBuffersNbr = 16; hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK) { Error_Handler(); } }时钟分频系数ClockDivider只有FDCAN_CLOCK_DIV1和FDCAN_CLOCK_DIV2两种选择,如果内核时钟选太高,靠这一个分频可以降到可用的 Tq 范围。FrameFormat设为FDCAN_FRAME_FD_BRS表示支持 FD 帧且带位速率切换,如果对端是经典 CAN 节点,需要另起一个配置FDCAN_FRAME_CLASSIC的实例。RxFifo0ElmtSize决定 FIFO 里每个元素的最大字节数,设成 64 字节更保险,哪怕当前只收发 8 字节的经典帧,也不会浪费太多。
初始化函数只是前半段,main()里要调用它,紧接着做过滤配置和启动:
MX_FDCAN1_Init(); if (FDCAN_ConfigFilter() != HAL_OK) { Error_Handler(); } HAL_FDCAN_Start(&hfdcan1); HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);HAL_FDCAN_ActivateNotification的第三个参数是写指针,非 DMA 模式下不关心,填 0 即可。使能中断通知要在启动之后,否则消息可能在使能前进来丢了,导致回调永远不触发。
4. FDCAN 收发驱动与滤波器:从初始化到中断回调
4.1 发送路径:TX FIFO、TX Buffer 与 DLC 字节映射
stm32g474 的 FDCAN 发送有三种路径:TX Buffer、TX FIFO、TX Queue。CubeMX 配置时如果打开FDCAN_TX_FIFO_OPERATION模式,发送走 FIFO,可以连续压入多帧,硬件自动按顺序发送;如果配成 Queue 模式,它会按消息 ID 优先级排队。TX Buffer 适合做周期发送,比如每 10ms 发一帧状态报文,可以把帧预存进固定 Buffer,下次只改数据场。TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION时用 HAL 的HAL_FDCAN_AddMessageToTxFifoQ()入队。
FDCAN_TxHeaderTypeDef pTxHeader; uint8_t txData[16]; pTxHeader.Identifier = 0x123; pTxHeader.IdType = FDCAN_STANDARD_ID; pTxHeader.TxFrameType = FDCAN_DATA_FRAME; pTxHeader.DataLength = FDCAN_DLC_BYTES_16; pTxHeader.FDFormat = FDCAN_FD_CAN; pTxHeader.BitRateSwitch = FDCAN_BRS_ON; pTxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &pTxHeader, txData) != HAL_OK) { printf("tx fifo full\r\n"); }DataLength字段用的是 DLC 编码,不是字节数直接填进去,16 字节对应FDCAN_DLC_BYTES_16,24、32、48、64 各自有宏。很多调试问题出在DataLength填了 16、但txData缓冲区只有 8 字节,硬件读走 16 字节,后面 8 字节是缓冲区的脏数据。BitRateSwitch设FDCAN_BRS_ON才走数据段高速率,设 OFF 时整个帧都按仲裁段速率发,这是 FD 帧兼容性的重要开关。
4.2 接收中断与数据解析
接收侧配置 FIFO0 后,中断回调里取帧即可:
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) { FDCAN_RxHeaderTypeDef pRxHeader; uint8_t rxData[64]; if ((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != 0) { HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &pRxHeader, rxData); uint8_t dlc = pRxHeader.DataLength; uint8_t len = 1 << ((dlc >> 4) - 1); // 解析 pRxHeader.Identifier, pRxHeader.FDFormat, pRxHeader.BitRateSwitch } }HAL 的HAL_FDCAN_RxFifo0Callback是弱定义回调,重叠定义后在文件里会覆盖 HAL 默认函数,链路保证FDCAN_IT_RX_FIFO0_NEW_MESSAGE被即时消费。数据长度转换1 << ((dlc >> 4) - 1)在 HAL 里是常见解析做法,把 DLC 位域还原成字节数,FD 帧 64 字节时 DLC 编码为 0x0F,右移再减 1 得到 7,1 左移 7 是 128?不对,64 字节对应的 FDCAN_DLC_BYTES_64 的值是 0x0F,dlc >> 4是 0,减 1 成负数,移位出错。实际这行转换写错的概率不低,建议直接查表:
static const uint8_t fdcan_dlc_to_len[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64 }; uint8_t len = fdcan_dlc_to_len[pRxHeader.DataLength >> 4];FDCAN 的数据长度在 DLC 位里用 0x9 到 0xF 表示大于 8 字节的帧,这个映射表比移位公式可靠得多。
4.3 过滤器:Mask、List 与接收 Buffer 的绑定
配置过滤器的结构体要把过滤类型和接收 FIFO 绑定,常见配置如下:
FDCAN_FilterTypeDef sFilterConfig; sFilterConfig.IdType = FDCAN_STANDARD_ID; sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; sFilterConfig.FilterType = FDCAN_FILTER_MASK; sFilterConfig.FilterID1 = 0x120; sFilterConfig.FilterID2 = 0x7FF; sFilterConfig.FilterIndex = 0; sFilterConfig.IsCalibrationMsg = DISABLE; HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig);FilterType有三个取值:FDCAN_FILTER_MASK模式下FilterID1是关注 ID,FilterID2是掩码,掩码对应位为 1 时必须匹配,为 0 时不关心;FDCAN_FILTER_RANGE模式下FilterID1和FilterID2组成闭区间;FDCAN_FILTER_DUAL精确匹配两个独立 ID。同类过滤元素从低索引开始分,标准 ID 和扩展 ID 的过滤索引段是分开的,两者互不挤占。
接收 FIFO 的对应关系写在FDCAN_FILTER_TO_RXFIFO0里,这条过滤器把所有符合 Mask 规则的帧送进 FIFO0。中断回调里读 FIFO0,就只会拿到被过滤过的报文。如果不配过滤器,FDCAN 默认丢弃所有报文,这是新手最容易卡住的地方:外设能初始化、能进发送回调,但接收永远没数据,因为过滤表是空的。
5. 把 FDCAN 收到的标定数据写进 STM32G474 Flash 与验证技巧
FDCAN 最常见的落地场景是远程标定:上位机通过 can 总线把 PID 参数或出厂配置发过来,设备掉电后要保存下来。STM32G474 没有外部 EEPROM 时,可以直接写内部 Flash 的一个页,但要遵守两条规则:写入前必须按页擦除,擦除动作会把整页清成 0xFF;写入方向只能从 1 变 0,所以修改一页里的几个字节也必须先擦整页。G474 的 Flash 按双字(64 位)编程,写入地址必须 8 字节对齐,数据也要凑成 8 字节的整数倍。
#define PARAM_FLASH_ADDR 0x08060000 /* 选在 Bank2 的用户区 */ void Flash_WriteParams(uint8_t *buf, uint32_t len) { FLASH_EraseInitTypeDef erase; uint32_t page_err = 0; HAL_FLASH_Unlock(); erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.Banks = FLASH_BANK_2; erase.Page = 32; erase.NbPages = 1; HAL_FLASHEx_Erase(&erase, &page_err); for (uint32_t i = 0; i < len; i += 8) { uint64_t data = 0; for (uint32_t j = 0; j < 8; j++) { if (i + j < len) data |= ((uint64_t)buf[i + j]) << (8 * j); } HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, PARAM_FLASH_ADDR + i, data); } HAL_FLASH_Lock(); }擦除期间 CPU 从 Flash 取指会停顿,如果系统里还有 FDCAN 在收发,这几十毫秒内收到的帧会丢,标定场景要在写 Flash 前通知对方暂停发送,或者先把数据搬到中断里收完。HAL_FLASHEx_Erase的page_err在失败时记录出错页,正常流程里返回值加上对page_err == 0xFFFFFFFF的判断。Flash 写入地址要避开程序区,G474B 的 Flash 分两 Bank,把参数页放在 Bank2 尾部最稳妥,程序在 Bank1,即使擦写失误也不会把代码清掉。
验证写入结果时用内存比对:
for (uint32_t i = 0; i < len; i++) { if (*(uint8_t *)(PARAM_FLASH_ADDR + i) != buf[i]) return FLASH_READBACK_ERR; } return FLASH_READBACK_OK;读取 Flash 不需要解锁,直接按地址取值即可。我在实际板上验证 FDCAN 链路时会先做回环测试:把 FDCAN 配成 Loopback 模式,自发自收,再把收到的参数写入上面这段 Flash 逻辑,然后复位重启,从 Flash 读回参数初始化 FDCAN 位时序。如果在回环里收到 64 字节 FD 帧且读回参数一致,说明收发链路、过滤器、Flash 存储三段都通了;此时再换 Normal 模式接真实 can 总线,用另一个 stm32g474 节点对发,同时抓取错误帧计数寄存器,错误计数持续上涨就先查数据段采样点,稳定为零再确认双方位时序一致。这种验证顺序能隔离问题,避免总线错误和 Flash 写入问题混在一起难排查。
本文还有配套的精品资源,点击获取