1. 为什么我盯上了 STM32C092 的 FDCAN 重映射
先交代一下背景。前阵子手头一个项目,主控选了 STM32C092FCP6,看重的是它价格低、封装小、还带了双 FDCAN,做车载低速网关或者工业互联节点都够用。可问题恰恰出在这颗芯片的 FDCAN 引脚重映射上,我按照老经验直接用 STM32CubeMX 把 FDCAN 从默认引脚挪到 PB 组,结果烧进去之后总线死活没有 ACK,示波器一抓,TX 引脚压根没有波形。
排查了两天,最后发现是重映射配置里一个非常不起眼但致命的细节。这颗芯片不像 F103 那样有 AFIO 的MAPR寄存器,也不完全像 H7/G4 那样靠SYSCFG,而是把重映射逻辑藏在了FDCAN_TTTS相关的时钟和Alternate Function复用表里,一个不留神就会踩坑。
这篇文章就把我在 STM32C092FCP6 上做 FDCAN 重映射的全过程、踩坑记录和最终可用的配置方式整理出来。适合正在用 C0 系列做 FDCAN 通信,或者打算从 F103/F072 迁移到 C0 平台的开发者参考。
2. FDCAN 重映射的整体思路与方案选型
2.1 先搞清楚 C0 系列和传统 F1/F0 重映射的区别
STM32C0 系列从定位上说是 F0 的继任者,内核还是 Cortex-M0+,但外设设计思路明显向 G4/H7 靠拢。尤其是 FDCAN,它并不是简单地把 F0 的 bxCAN 改个名,而是把 G4 的 FDCAN 模块整体搬了过来,只是砍掉了一些时钟频率和缓存深度。
这意味着什么?意味着你做引脚重映射时,不能再用 F103 时代“AFIO 拉一个SWJ_CFG或MAPR位就完事”的思路。C0 的每个引脚都有独立的AFR[0]/AFR[1]寄存器,复用功能靠 AF 编号选择,这就和 G4 一致。而 FDCAN 的外设时钟也不是挂在 APB1 上,而是由独立的FDCAN_CLK提供,这个时钟源选择又和RCC_CCIPR里的FDCANSEL位有关。
我在拿到样片后第一件事就是翻参考手册 RM0490(STM32C0 系列参考手册),翻到 GPIO 复用表才发现,FDCAN1 的 RX/TX 在 C092 上有三组可选引脚,默认是 PA11/PA12,第二组是 PB8/PB9,第三组是 PB12/PB13。而 FDCAN2 也有两组。这一点很多人没注意,因为 CubeMX 里的Pinout & Configuration面板如果只拉 GPIO 不复用外设,是看不到这些可选项的。
2.2 为什么选择 PB8/PB9 而不是默认引脚
我的项目 PCB 已经画完了,PA11/PA12 被一个自定义的 LIN 类调试接口占用了,PA11 同时还兼着 USB DP 检测,虽然 C092 不带 USB 外设,但引脚已经被别的信号占用。所以只能把 FDCAN1 挪到 PB8/PB9。
这里有个很容易被忽略的坑:C092 的 PB8/PB9 如果工作在 FDCAN 复用功能下,会和 I2C1 的 SCL/SDA 冲突。如果你的代码里同时初始化了 I2C1 软件模拟或者硬件 I2C,并且没有把 PB8/PB9 的复用功能正确设置成 FDCAN,那么 I2C 的初始化代码可能会把这两个引脚重新拉成开漏模式,导致 FDCAN 的 TX 根本无法正常输出推挽电平。
我在排查时用逻辑分析仪抓 PB9 的电平,发现它一直保持高电平,偶尔有极低的毛刺,这说明引脚被外部或内部别的配置影响,而不是 FDCAN 模块没工作。
2.3 重映射的技术路径对比
做完调研我把可行路径列出来对比了一下:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| CubeMX 图形配置 | 在 Pinout 里选择 FDCAN1,勾选 PB8/PB9,生成代码 | 直观、不容易漏配置 | 生成代码后仍需手动检查时钟源和 AF 值 |
| 纯寄存器操作 | 直接操作RCC_AHBENR、GPIOB_AFR[1]、FDCAN_CCCR等 | 可控性强、适合 bootloader | 工作量大,出错后排查时间长 |
| HAL 库手动初始化 | 在HAL_FDCAN_MspInit里手动做 GPIO 和时钟配置 | 代码可读性好、便于 Git 管理 | 仍需理解底层寄存器 |
我最终建议用 CubeMX 生成基础工程,再手动核对关键寄存器,而不是完全依赖 CubeMX。原因后面会详细讲。
3. 核心细节解析:AF 编号、时钟源和 FDCAN 初始化顺序
3.1 Alternate Function 编号怎么确认
STM32C092FCP6 的 GPIO 复用功能表里,PB8 的 FDCAN1_RX 对应的是 AF3,PB9 的 FDCAN1_TX 对应的是 AF3,而 PA11/PA12 也是 AF3。这一点和 G4 系列有点不一样,G4 的 FDCAN 某些引脚是 AF9,所以如果你拿 G4 的代码直接改芯片型号,很容易死在 AF 编号上。
确认 AF 编号最稳妥的方法是查 RM0490 的 GPIO 复用功能表,而不是靠猜。CubeMX 生成的代码里会看到类似这样的配置:
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF3_FDCAN1;这里GPIO_AF3_FDCAN1在 HAL 头文件里定义,如果发现生成的代码里Alternate是 0,那就要怀疑 CubeMX 版本对 C092 的支持不完整。我手头用的 CubeMX 是 6.10 以后才完整支持 C092,如果你用的还是老版本,建议先升级。
3.2 时钟源:FDCAN 的命门
STM32C092 的 FDCAN 时钟源选择是另一个重映射后不出波形的核心原因。这颗芯片的主频最高 48MHz,内部有 HSI48,外部可以接 HSE。FDCAN 模块的时钟并不是直接拿SYSCLK,而是通过RCC_CCIPR寄存器的FDCANSEL[1:0]位选择:
00:PCLK(APB 时钟)01:SYSCLK10:HSI4811:HSE(如果存在)
我踩的坑就在这里。CubeMX 默认生成的代码里FDCANSEL选择的是PCLK,而 PCLK 在 C092 上默认是SYSCLK/1,理论上也没问题。但如果你在SystemClock_Config里把 APB 分频系数设成了 2 甚至 4,FDCAN 的时钟频率就会跟着降,导致波特率配置算出来的 prescaler 完全不对。
我实测发现,FDCANSEL配成HSI48是最省心的,因为 HSI48 是个独立的 48MHz 时钟,不受 APB 分频影响。这样 FDCAN 的比特率配置只需要针对 48MHz 计算,简单且稳定。配置代码:
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN; PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_HSI48; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { Error_Handler(); }注意 C0 系列的 HAL 库这个HAL_RCCEx_PeriphCLKConfig函数和 F4 不太一样,参数类型略有差别,但功能类似。编译的时候如果报FdcanClockSelection不是结构体成员,说明你的 HAL 库版本太旧,需要更新到支持 C0 的版本。
3.3 初始化顺序:先时钟后 GPIO 再 FDCAN
很多人在重映射后 FDCAN 不工作,其实和初始化顺序有关。我推荐的顺序是:
- 配置系统时钟(
HAL_RCC_ClockConfig) - 配置外设时钟源(
HAL_RCCEx_PeriphCLKConfig,设置 FDCANSEL) - 初始化 FDCAN 引脚(GPIO 时钟 + AF 复用)
- 初始化 FDCAN 外设(
HAL_FDCAN_Init) - 配置 FDCAN 滤波器
- 启动 FDCAN(
HAL_FDCAN_Start)
如果第 2 步放在第 4 步之后,FDCAN 初始化时读到的时钟源还是旧值,导致比特率配置对象里的NominalPrescaler计算错误。我一开始手写寄存器时就是先开的FDCAN_CCCR里的INIT位,再配的时钟源,结果 CRC 校验一直报错。
4. 实操过程:从 CubeMX 到最终跑通 FDCAN
4.1 CubeMX 工程配置(含关键选择)
我以 CubeMX 6.10 为例,选择 STM32C092FCP6,在Pinout & Configuration里按以下步骤操作:
- 左侧
Connectivity展开,选择FDCAN1,勾选Activate。 - 在
FDCAN1的Pinout视图里,手动点击 PB8,选择FDCAN1_RX(AF3);点击 PB9,选择FDCAN1_TX(AF3)。 - 如果 PB8/PB9 同时被 I2C1 占用,把 I2C1 的
Activate关掉,或者迁移到别的引脚。 - 左侧
System Core下选择RCC,HSE 设为Crystal/Ceramic Resonator或Bypass Clock,根据你的硬件来。 Clock Configuration里确认 FDCAN 时钟源,我直接选HSI48,将FDCANSEL设为HSI48。- 工程生成后,在
main.c的MX_FDCAN1_Init里检查hfdcan1.Init.ClockDivider,如果是FDCAN_CLOCK_DIV1就没问题。
CubeMX 有个坑:如果你在 FDCAN 的Parameter Settings里把Protocol设成了FDCAN,而Frame Format还是Classic CAN,生成的代码里会默认把FDCAN帧格式关掉。你要确认自己的总线另一端是只支持 Classic CAN 还是也要支持 FD 帧,比如只接一个 120Ω 终端电阻测试时,如果对面是普通 CAN 收发器而不是 FDCAN 收发器,而你把 FDCAN 扩展帧打开了,就会导致通信失败。
4.2 终端电阻的问题:为什么"只接 1 个 120Ω"也能工作
说一个很多人误解的点:“FDCAN 总线只接 1 个 120Ω 终端电阻能不能工作?”
标准 CAN 总线要求在总线两端各接一个 120Ω 电阻,等效并联后是 60Ω。但在实验室简单测试时,只接一个 120Ω 电阻在总线一端,另一端悬空,通信依然可以工作。
这背后是差分信号和隐性/显性电平的容忍度问题。CAN/FDCAN 物理层用的是差分电压,显性时 CANH 拉高、CANL 拉低,差分电压约 2V;隐性时两端电压接近相同,差分约 0V。终端电阻的主要作用不是“让信号反射消失”这么简单,而是为总线提供直流负载,同时抑制信号边缘反射。
只接一个 120Ω 电阻时,总线直流负载是 120Ω 而不是标准的 60Ω,这会导致显性电平比标准稍低,但只要收发器芯片的接收阈值还能正确识别,通信照常。你如果用 PCAN 或者 USB-CAN 分析仪挂上去,可能看到 ACK 正常、无错误帧,但这不代表可以量产,这只是测试手段。
我这次调试时就是用一块 STM32C092 板子接了一个 120Ω 电阻,另一头用 PCAN-USB 适配器,同样也只接了一个 120Ω 电阻,结果通信正常。所以如果你发现 FDCAN 通信不稳定,先别急着怀疑“电阻少了一个”,先确认波特率、采样点、终端电阻的总负载是否在收发器允许范围内。
4.3 收发器选型对重映射的影响
C092 的 FDCAN 引脚是 3.3V 逻辑电平,不能直接挂到 CAN 总线上,必须经过 CAN 收发器。我用的收发器是 TJA1042,供电 5V,TXD/RXD 逻辑电平兼容 3.3V 微控制器,但需要注意 RXD 输出高电平的VOH是否超过 MCU 的容忍电压。TJA1042 的 RXD 高电平输出是 4V 左右(5V 供电时),如果 MCU 引脚不是 5V 容忍,可能会通过内部钳位二极管漏电。
C092 的 PB8 和 PB9 是否 5V 容忍,我在数据手册里查的结果是:C0 系列的 PB 组大部分引脚不是 5V 容忍,而是 3.6V 耐压。所以你如果直接接 5V 供电的收发器,RXD 高电平可能超压,长期工作有风险。
解决办法有两个:
- 在 MCU 和收发器之间串联 1kΩ 电阻,限制电流,RXD 高电平经过分压后落在 3.3V 附近。
- 选用 3.3V 供电的收发器,比如 TJA1051/3、SN65HVD230、MCP2562FD 等。
我这次用的是 SN65HVD230,3.3V 供电,直接和 C092 电平匹配,省事很多。调试时发现它的隐性电平为 2.5V,显性时 CANH 3.5V、CANL 1.5V,差分 2V,符合标准。
4.4 关键代码片段:手动配置重映射后的引脚和滤波器
CubeMX 生成的代码之外,我手动增加了一段代码用于确保重映射生效和滤波器配置正确:
void FDCAN1_Remap_Check(void) { // 确保 GPIOB 时钟已使能 __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF3_FDCAN1; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); }滤波器配置,我用的是经典接收所有帧的方式,方便调试:
FDCAN_FilterTypeDef sFilterConfig = {0}; sFilterConfig.IdType = FDCAN_STANDARD_ID; sFilterConfig.FilterIndex = 0; sFilterConfig.FilterType = FDCAN_FILTER_MASK; sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; sFilterConfig.FilterID1 = 0x000; sFilterConfig.FilterID2 = 0x000; HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig);注意FilterID2 = 0在掩码模式下表示“所有位都必须匹配 0”,等价于只接收 ID 为 0 的帧。如果你要接收所有帧,应该把FilterID2设为0x7FF(标准帧 11 位全部不关心),或者设置FilterType = FDCAN_FILTER_RANGE并打开范围过滤。这里我实际用的是:
sFilterConfig.FilterID2 = 0x7FF;这样掩码为0x7FF,表示不关心任何 ID 位,匹配所有标准帧。这个细节坑了不少人,因为 HAL 库的示例代码里经常是FilterID2 = 0,直接抄会导致只能收到 ID 为 0 的帧。
启动 FDCAN 接收中断:
HAL_FDCAN_Start(&hfdcan1); HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);中断回调函数:
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) { FDCAN_RxHeaderTypeDef RxHeader = {0}; uint8_t RxData[8] = {0}; if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK) { // 处理收到的报文 } }这是我调试时跑通的代码,直接抄可以用,但波特率参数要按你的总线环境改。
5. 常见问题与排查技巧实录
5.1 现象:烧录后 TX 引脚无波形
这是重映射后最常见的现象。排查步骤我按优先级整理如下:
- 先查 GPIO 复用是否真的设置为 AF3。在调试器里看
GPIOB->AFR[1]的第 8/9 位,也就是AFR8和AFR9,值必须是0b0011(AF3)。如果显示0b0000,说明 GPIO 初始化没生效,或者在初始化之后被别的代码覆盖了。 - 再查 FDCAN1 的时钟有没有使能,
RCC_AHBENR里的FDCAN1EN位是否为 1。 - 查
RCC_CCIPR的FDCANSEL位,确认时钟源。如果你期望 HSI48,但实际是 PCLK,就可能导致波特率配置错误,TX 仍然不会有正常波形,因为 FDCAN 模块进入 BusOff 或初始化失败。 - 查
FDCAN_CCCR寄存器,确认INIT位是否成功清零。如果一直为 1,说明 FDCAN 初始化没完成,可能是时钟源没选对或者波特率配置超限。
5.2 现象:TX 有波形但无 ACK
当 TX 有波形,却收不到 ACK,说明发送方已经成功发送到总线,但没有接收节点回应。常见原因:
- 总线上只有一个节点,自发自收时必须开启
LoopBack模式或者外部回环。FDCAN 模块自带 LoopBack 模式,可以在没有外部收发器的情况下测试协议栈。 - 终端电阻缺失导致电平不正确。注意我前面说的,一个 120Ω 在测试场景下可以工作,但不代表稳定,长时间运行可能出现偶发错误帧。
- 波特率不匹配。双方采样点不一致时,如果相位裕量不足,会导致 ACK 偶尔失败。
- 使用了 FDCAN 帧格式,但对端只支持 Classic CAN。这时候要把
hfdcan1.Init.FrameFormat设为FDCAN_FRAME_CLASSIC,或者用FDCAN_FRAME_FD_NO_BRS。
检查 ACK 是否有问题,可以直接在调试器里看FDCAN_PSR寄存器的LEC字段,LEC = 010表示 ACK 错误,000表示无错误。这个比看波形还直观。
5.3 现象:FDCAN 初始化返回 HAL_ERROR
初始化返回错误,多半和HAL_FDCAN_Init里的初始化参数有关。我把检查列表写出来:
| 检查项 | 对应代码 | 常见错误 |
|---|---|---|
| 时钟分频 | hfdcan1.Init.ClockDivider | 设成FDCAN_CLOCK_DIV10会导致比特率大幅降低 |
| 数据段比特率 | hfdcan1.Init.DataBitTimePrescaler | FD 帧模式下必须和仲裁段分开配置 |
| 采样点 | hfdcan1.Init.SyncJumpWidth | SJW 设太小会在总线上出现尖峰时失步 |
| 帧格式 | hfdcan1.Init.FrameFormat | FD 帧误配导致对端不响应 |
| 自动重传 | hfdcan1.Init.AutoRetransmission | 关闭后发送失败不重传,可能误判为总线错误 |
5.4 现象:切换引脚后,CubeMX 生成的代码还是旧的引脚定义
这个问题特别隐蔽。CubeMX 有时候会在.ioc文件里保留旧引脚的映射,即使你在图形界面里改了引脚,生成的 GPIO 初始化代码里还残留 PA11/PA12 的HAL_GPIO_Init。我遇到过两次,解决办法是:
- 删掉工程里的
main.c和fdcan.c,重新生成。 - 或者手动搜索
PA11、PA12关键字,删除残留代码。 - 确认
.ioc文件里FDCAN1_RX和FDCAN1_TX的引脚配置已经更新,搜索PB8、PB9是否存在。
5.5 避坑技巧总结
根据这次调试,我把几个容易踩的坑汇总一下:
- 不要盲目信任 CubeMX 对 C0 系列的引脚映射,生成后一定打开
fdcan.c看MX_FDCAN1_Init里的 GPIO 配置,确认Alternate值是GPIO_AF3_FDCAN1,而不是GPIO_AF0或 0。 - 如果调试时 FDCAN 一直初始化失败,优先检查时钟源选择,用 HSI48 会比 PCLK 更稳定,尤其是当 APB 分频系数不为 1 时。
- 收发器的 RXD 输出高电平和 MCU 引脚耐压要匹配。C092 引脚不是 5V 容忍,最好用 3.3V 收发器,或者串电阻分压。
- FDCAN 的滤波器掩码模式默认
FilterID2 = 0是“只匹配 ID 等于 FilterID1”,不是“匹配所有 ID”。要匹配所有帧时FilterID2要设成0x7FF(标准帧)或0x1FFFFFFF(扩展帧)。 - 调试时在
FDCAN_PSR寄存器里看LEC错误码,比反复量波形效率高得多。
6. 实际测试结果与经验总结
最终我在 C092FCP6 上跑通了 FDCAN1 重映射到 PB8/PB9,和 PCAN 适配器通信,波特率 500kbps,经典 CAN 帧,数据长度 8 字节,收发 100 万帧无错误。测试时总线只接了远近端各一个 120Ω 电阻,共 60Ω 等效负载,终端匹配正常。
如果只接一个 120Ω 电阻,短距离(1 米以内)也能工作,但我测试时发现总线长度超过 2 米后,偶尔会出现 CRC 错误,这印证了阻抗匹配对信号完整性的影响。所以实验室玩一玩可以只接一个,项目上还是老老实实两个都上。用 FDCAN 收发器做自测时也可以直接开HAL_FDCAN_Start后,在FDCAN_CCCR里把TEST位打开,用LoopBack模式验证 MCU 内部数据链路,这时候完全不需要外部收发器和总线电阻。
这个芯片的 FDCAN 重映射本身并不复杂,难点在于信息分散在参考手册的不同章节,以及 CubeMX 生成代码的“陷阱”会误导你。如果你也遇到类似问题,按我上面的排查顺序走一遍,基本十分钟内能定位。最后再分享一个小技巧:C092 的 FDCAN 内核是 Bosch M_CAN 的裁剪版,它的FDCAN_TTTS寄存器名字看起来和 TTCAN 有关,实际上部分位还兼职了内部测试模式的使能。如果你以后要做量产自检,可以研究一下TEST模式下的外部回环,比外部接线回环少很多物理层干扰,定位问题更快。