[STM32U3] 【NUCLEO-U3C5ZI 测评】 FDCAN1 低功耗 实战记录

[复制链接]
8|0
lulugl 发表于 2026-6-4 15:14 | 显示全部楼层 |阅读模式
CAN, , Nucleo, ,

NUCLEO-U3C5ZI-Q 裸机低功耗 + FDCAN1 实战记录(HAL库,含完整踩坑链)

一、需求是什么

在 NUCLEO-U3C5ZI-Q (STM32U3C5ZIT6Q) 上跑裸机 HAL,不上 RTOS,验证两件事:

  1. 低功耗:上电 2 秒后进 STOP2,按 B1 唤醒,翻 LD1,500ms 后再进 STOP2 循环;串口能看到 log;
  2. FDCAN 收发:按 B1 唤醒后,FDCAN1 发 1 帧 CAN FD 数据(对方是另一块 Zephyr 板子),然后进 500ms 延时再回 STOP。

参考 ST 例程 PWR/PWR_ModesSelectionFDCAN/FDCAN_Power_Down(U385RG-Q 板子),但只搬 HAL API 那一行——它们的应用层(UART 菜单 / loopback 测功耗)跟我们的"按键触发"完全两码事。

二、最终长这样

image.png

对方 Zephyr 板子(Zephyr shell 端 log):

image.png

功耗(STOP 状态):NUCLEO 板子上**~8.2 µA**,LD1 亮时**~125 µA**(PA5 推挽 + 板载限流电阻)。

image.png

三、工程结构

整个工程是从零搬的"最小集"——HAL + CMSIS 全套,加上应用层那几个文件:

led_pm/
├── CMakeLists.txt                   顶层
├── CMakePresets.json                Debug / Release preset (Ninja)
├── STM32U3c5xx_FLASH.ld             链接脚本
├── startup_stm32u3c5xx.s            启动文件
├── cmake/
│   ├── gcc-arm-none-eabi.cmake      工具链
│   └── stm32cubemx/
│       └── CMakeLists.txt           源文件清单
├── Drivers/
│   ├── CMSIS/                       core_*.h, stm32u3c5xx.h
│   └── STM32U3xx_HAL_Driver/        105 .h + 97 .c(全搬, 按需引)
├── Inc/
│   ├── main.h
│   ├── stm32u3xx_hal_conf.h
│   └── stm32u3xx_it.h
├── Src/
│   ├── main.c                        主程序
│   ├── stm32u3xx_hal_msp.c           USART1 + FDCAN1 MSP
│   ├── stm32u3xx_it.c                SVC/PendSV/SysTick + EXTI13
│   ├── syscalls.c, sysmem.c          newlib 桩
└── doc/                              文档 + 代码附件

四、硬件踩坑

4.1 NUCLEO-U3C5ZI-Q 的 FDCAN 收发器在板子上

这一条不知道的话,会怀疑 FDCAN1 没初始化对。

板子手册 UM3599 page 27 写得很清楚:

The NUCLEO-U3C5ZI-Q Nucleo-144 board supports the FDCAN feature. A CAN transceiver (U19) is implemented on the board to convert Tx (PB9) and Rx (PB8) signals into differential CAN-H (CAN_P) and CAN-L (CAN_N) signals. CAN_P and CAN_N are accessible through the FDCAN connector (CN18). A 120 Ω termination resistor (R57) is included on the board, and users can enable or disable this resistor through the JP4 jumper.

关键事实

  • FDCAN1 TX = PB9, RX = PB8(不是 U385 例程里的 PA11/PA12!U385 是 64-pin 板,引脚不同)
  • 板子自带收发器 U19——不要外接 MCP2551 / TJA1050
  • 收发器输出走 CN18 接插件
  • 120Ω 终端电阻 R57 + JP4 跳线——两端各 120Ω 是标准,4M baud 几乎必开

4.2 GPIO 复用 AF9

PB8/PB9 的 FDCAN 复用功能是 GPIO_AF9_FDCAN1(在 stm32u3xx_hal_gpio_ex.h 里),不是 AF7 也不是 AF8。ST 例程不能直接抄——U385 例程里是 PA11/PA12,你把那套抄过来一定跑不通

五、低功耗这一路

5.1 STOP 之前的关闭顺序

进 STOP 前要关的设备:

/* UART: 等 TX 排空, DeInit -> MspDeInit 关 clk + 释放 PA9/PA10 */
while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET) {}
while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET) {}
HAL_UART_DeInit(&huart1);

/* FDCAN: 用 PowerDown 模式, 不要 DeInit
 * - PowerDown 让 FDCAN 核心忽略总线活动, 不再产生 NVIC 唤醒
 * - DeInit 每次进 STOP 都要重配 bit timing + filter, 反而麻烦
 * - 唤醒后 ExitPowerDownMode 一下, FDCAN 继续工作 */
HAL_FDCAN_EnterPowerDownMode(&hfdcan1);

/* Flash: 一次性配 LPM + Bank 2 Power Down (boot 时各做一次) */
HAL_FLASHEx_ConfigLowPowerRead(FLASH_LPM_ENABLE);
(void)HAL_FLASHEx_EnablePowerDown(FLASH_BANK_2);

/* ICache: 只 Disable, 不 DeInit */
HAL_ICACHE_Disable();

/* 然后进 STOP */
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERMODE_STOP2, PWR_SLEEPENTRY_WFI);

5.2 STOP 唤醒后的恢复顺序

/* 1) 重新配时钟 (MSI 4MHz -> 96MHz), 否则后面都慢 24 倍 */
SystemClock_Config();

/* 2) 重新开 ICache (disable 没动 WAYSEL/CRRx, Enable 一行就行) */
HAL_ICACHE_Enable();

/* 3) 重新开 UART */
MX_USART1_UART_Init();

/* 4) 重新开 FDCAN (ExitPowerDown) */
MX_FDCAN1_Init();   /* 内部会调 ExitPowerDownMode */

/* 5) 发一帧 */
FDCAN_Send_Frame();

/* 6) 翻灯 + delay + 再进 STOP */

5.3 实际省电情况

优化项 一次性 or 每次 预期省电 实测
Flash LPM boot 1-2 µA
Bank 2 Power Down boot 2-3 µA
ICache 关 (STOP) 每次 5-10 µA +0.1 µA(实测比想象小)
ULPM 调压器 boot 3-5 µA
UART 关 (STOP) 每次 2-3 µA
FDCAN Power Down 每次 3-5 µA
合计 17-30 µA 8.2 µA(NUCLEO 板子受 STLINK V3EC 漏电限制)

注意:理论上 STOP2 应该能到 datasheet 标的 ~1.8 µA,但 NUCLEO 板载 STLINK-V3EC 会从 3V3 反向供电,多加 5-6 µA 的底噪。要真正看到 1.8 µA,得用自己的 bare board。

六、FDCAN 这一路(重头戏)

6.1 例程抄来的 FDCAN1 初始化

直接拿 ST 例程 FDCAN_Power_DownMX_FDCAN1_Init 框架,但要改三处:

  • 引脚:PA11/PA12 → PB8/PB9(U3C5ZI-Q 板子)
  • 复用:GPIO_AF9_USART1GPIO_AF9_FDCAN1
  • 速率:例程是 1M nominal / 8M data,我们要看对方配多少

6.2 第一次翻车:4M/4M 自己定

我一开始按"4M 通信"字面意思,把位时序配成 Nominal 4M, Data 4M

hfdcan1.Init.NominalPrescaler     = 1U;
hfdcan1.Init.NominalSyncJumpWidth = 6U;
hfdcan1.Init.NominalTimeSeg1      = 17U;
hfdcan1.Init.NominalTimeSeg2      = 6U;   /* sample 75% */
hfdcan1.Init.DataPrescaler        = 1U;
hfdcan1.Init.DataSyncJumpWidth    = 6U;
hfdcan1.Init.DataTimeSeg1         = 17U;
hfdcan1.Init.DataTimeSeg2         = 6U;

对方 Zephyr 板子 log:

fdcan_eval: BoardB-RX started (fd_mode=1, brs=1)
fdcan_eval: BoardB-RX: stats tx=0 rx=0 err=0 ...  <-- 一直接不到

我这边 log 倒是"发"了:

[CAN] TX done. ECR=0x PSR=0x LEC=Bit0
[CAN] TX StdID=0x444, seq=0

6.3 用 ECR/PSR/LEC 定位

LEC=Bit0 含义:节点发送 dominant (0) 时,监测到 recessive (1)

误判了一开始以为是物理层(CANH/CANL 反了、缺 GND),但加 ECR/PSR/LEC 打印后所有错误都是 Bit0——典型位时序不匹配

/* 在 AddMessageToTxFifoQ 成功之后加诊断 */
uint32_t ecr = hfdcan1.Instance->ECR;
uint32_t psr = hfdcan1.Instance->PSR;
log_str("[CAN] TX done. ECR=0x"); printf("%04lX", ecr & 0xFFFF);
log_str(" PSR=0x"); printf("%03lX", psr & 0xFFF);
uint32_t lec = psr & 0x07U;
static const char *const lec_str[] =
  {"NoErr","Stuff","Form","ACK","Bit1","Bit0","CRC","NoChg"};
log_str(" LEC="); log_str(lec_str[lec]);

LEC 速查

LEC 含义 优先排查
0 NoErr 没错误
1 Stuff 位填充违例 物理层干扰 / 速率严重不匹配
2 Form 固定位格式错 物理层问题
3 ACK 没人 ACK 物理层问题(CAN 收发器 / 线序 / 缺 GND)
4 Bit1 发 recessive 收到 dominant 物理层短路 / 速率错
5 Bit0 发 dominant 收到 recessive 位时序不匹配(仲裁段)
6 CRC CRC 错 物理层干扰
7 NoChg 没新错误

6.4 关键教训:CAN FD 仲裁段必须跟对方 100% 兼容

LEC=Bit0 不是物理问题——是位时序问题。Zephyr 那边配的是** nominal 125k / data 4M**,我配的 4M/4M。CAN FD 仲裁段必须跟对方 100% 一致(CAN 2.0 兼容层),数据段才允许 BRS 切到 4M。

根因:CAN 总线在仲裁期按 nominal 速率通信,两边 nominal 不一致就互相听不懂——4M vs 125k,差 32 倍,根本收不到 ACK 位。

6.5 拿到对方 dts 才算真修

Zephyr dts 关键三行(用户给的关键资料):

&fdcan1 {
    bitrate      = <125000>;     /* 仲裁段 */
    bitrate-data = <4000000>;    /* 数据段 */
    sample-point      = <875>;   /* 87.5% */
    sample-point-data = <750>;   /* 75% */
    can-transceiver { max-bitrate = <5000000>; };
};

Bit timing 计算(96MHz 时钟,96 tq/bit 习惯改成 N tq/bit):

Prescaler t1 t2 SJW tq/bit Baud Sample
Nominal 32 20 3 4 24 125 kBit/s 21/24 = 87.5%
Data 1 17 6 6 24 4 Mbit/s 18/24 = 75%

验证

  • Nominal: 96M / 32 / 24 = 125000 ✓
  • Data: 96M / 1 / 24 = 4000000 ✓
  • Nominal sample: 21/24 = 0.875 = 87.5% ✓
  • Data sample: 18/24 = 0.75 = 75% ✓

6.6 改完之后

改完烧上去,对端 Zephyr 立刻收到帧:

fdcan_eval: BoardB-RX: stats tx=0 rx=1 err=0 state=0 ...   <-- 第一帧进来了

我这边 log:

[CAN] TX done. ECR=0x PSR=0x LEC=NoChg     <-- 持续 NoChg = 没新错误
[CAN] TX StdID=0x444, seq=0
[PM] B1 wake, LD1 -> ON

LEC=NoChg 持续稳定——Bit timing 修对了。

6.7 魔数 payload 替换全 0x00

修好之后 Zephyr 端 log 显示 00 00 00 00 00 00 00 00——我前面图省事,剩下 7 字节没填

g_tx_buf[0] = g_tx_seq++;
for (int i = 1; i < 8; i++) g_tx_buf[i] = 0U;   // 全 0 看着像没初始化

改成 8 字节有意义的:

/* 8 字节 payload
 *   [0] = 唤醒计数 (g_tx_seq, 0..255 循环)
 *   [1] = LD1 状态 (0xAA ON, 0x55 OFF)
 *   [2] = 0xCC marker
 *   [3..6] = 0xDE 0xAD 0xBE 0xEF
 *   [7] = 0x42 */
GPIO_PinState ld = HAL_GPIO_ReadPin(LED1_PORT, LED1_PIN);
g_tx_buf[0] = g_tx_seq++;
g_tx_buf[1] = (ld == GPIO_PIN_SET) ? 0xAAU : 0x55U;
g_tx_buf[2] = 0xCCU;
g_tx_buf[3] = 0xDEU;
g_tx_buf[4] = 0xADU;
g_tx_buf[5] = 0xBEU;
g_tx_buf[6] = 0xEFU;
g_tx_buf[7] = 0x42U;

Zephyr 端 log 就漂亮了:

fdcan_eval: BoardB-RX: rx_id=0x444 len=8 data=00 AA CC DE AD BE EF 42
fdcan_eval: BoardB-RX: rx_id=0x444 len=8 data=01 55 CC DE AD BE EF 42

七、所有踩过的坑(汇总)

7.1 工程启动期

解决
led_pm 目录是空的 从 PWR_ModesSelection 例程搬 HAL/CMSIS/ld/startup + 写最小 main.c
stm32u3xx_hal_conf.h 从例程 Inc/ 复制
MSIDiv 不存在 U3 系列字段名是 MSISDiv(MSIS/MSIK 分开了)
CMake 找不到 Ninja winget install Ninja-build.Ninja + CMakePresets.json 里硬指定 CMAKE_MAKE_PROGRAM
system_stm32u3xx.c 没加进源文件 cmake/stm32cubemx/CMakeLists.txt 加进 STM32_Drivers_Src
lowpower_scenarios.c / system_config.c / console.c 都不该搬 业务不匹配,只搬 HAL API 那一行

7.2 低功耗期

解决
UART 残字节 进 STOP 前等 TC + TXE
STOP 唤醒后 CPU 跑 4MHz 立即 SystemClock_Config()
HAL_SuspendTickuwTick 联** 用 SysTick suspend
4 项优化一起加,bug 找不到 一次只加一项,电流表测
SRAM1 时钟门控 撤掉——EXTI13 ISR 写 g_wakeup_flag 时 SRAM1 clk gated,唤醒挂死
ICache DeInit() 第二次唤醒挂死 只用 Disable + Enable 一对DeInit 会重置 WAYSEL,Enable 不恢复
FDCAN 不用 PowerDown 反复触发 wakeup HAL_FDCAN_EnterPowerDownMode,唤醒后 ExitPowerDownMode

7.3 FDCAN 期

解决
FDCAN 时钟源 RCC_FDCANCLKSOURCE_SYSCLK(96MHz 走 PLL)
引脚 PA11/PA12 用例程的就行? U3C5ZI-Q 是 PB8/PB9,查板子手册 page 27
GPIO 复用号 GPIO_AF9_FDCAN1,不是 7 不是 8
hal_fdcan_ex.c 不存在 HAL 只有 hal_fdcan.c,删 cmake 里的 _ex 引用
4M/4M 跟对方 125k/4M 不兼容 仲裁段必须跟对方 100% 一致,CAN FD 数据段才能 BRS 切
LEC=Bit0 怎么定位 ECR/PSR/LEC 打印,看错误码直接定位
Zephyr dts 字段 bitrate / bitrate-data / sample-point 拿来直接换算 HAL 的 prescaler/t1/t2

八、关键 API 速查

用途 API
进 STOP2 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERMODE_STOP2, PWR_SLEEPENTRY_WFI)
唤醒后恢复时钟 HAL_RCC_OscConfig + HAL_RCC_ClockConfig
ULPM 调压器 HAL_PWREx_EnableUltraLowPowerMode()
Flash LPM HAL_FLASHEx_ConfigLowPowerRead(FLASH_LPM_ENABLE)
Bank 2 PD HAL_FLASHEx_EnablePowerDown(FLASH_BANK_2)
ICache off HAL_ICACHE_Disable()(不用 DeInit)
UART 关闭 HAL_UART_DeInit → MspDeInit
FDCAN 关闭 HAL_FDCAN_EnterPowerDownMode(不要 DeInit)
FDCAN 重新开 MX_FDCAN1_Init + ExitPowerDownMode
FDCAN 发送 HAL_FDCAN_AddMessageToTxFifoQ
FDCAN 错误诊断 hfdcan1.Instance->ECR->PSRPSR[2:0]=LEC

九、关键经验

  1. CAN FD 仲裁段必须跟对方 100% 兼容——这是 CAN 2.0 fallback 层硬性要求。想着"自己定 4M/4M"省得调,对方多少你就多少
  2. 要跟成熟系统对接时,先看对方 dts / 配置。Zephyr 端 dts 三行就把 bit timing 全说了,比 STM32CubeMX 的图形界面更直接。
  3. LEC 错误码能直接定位ACK=物理层,Bit0/Bit1=位时序,CRC/Stuff=干扰。省得在物理层和位时序之间来回猜
  4. 低功耗不要 4 项优化一起加——出问题无法定位是哪个。一次只加一项,电流表测
  5. 省 0.5-1 µA 的小优化不可靠——SRAM1 时钟门控省那 0.5 µA 跟唤醒可靠性冲突,不值得
  6. ICache DeInitDisable 不是同一类操作——DeInit 重置 WAYSEL,Enable 不恢复,第二次 STOP 必挂。STOP 场景下只用 Disable/Enable
  7. FDCAN 关闭用 EnterPowerDownMode,不要 DeInit——DeInit 每次进 STOP 都要重配 bit timing + filter,PowerDown 模式保留所有配置,唤醒一行 Exit 就恢复。

十、附件

upload 附件:led_pm.zip

十一、参考资料

  • UM3599 - STM32U3 Nucleo-144 board MB2222(板子手册,page 27 是 FDCAN 收发器说明)
  • RM0487 - STM32U3 series reference manual
  • UM3439 - STM32U3 HAL/LL driver description
  • STM32Cube_FW_U3_V1.3.0 - PWR_ModesSelection + FDCAN_Power_Down 例程
  • CiA 601-2 - CAN FD data phase sampling point 推荐
您需要登录后才可以回帖 登录 | 注册

本版积分规则

215

主题

924

帖子

12

粉丝
快速回复 在线客服 返回列表 返回顶部
0