NUCLEO-U3C5ZI-Q 裸机低功耗 + FDCAN1 实战记录(HAL库,含完整踩坑链)
一、需求是什么
在 NUCLEO-U3C5ZI-Q (STM32U3C5ZIT6Q) 上跑裸机 HAL,不上 RTOS,验证两件事:
- 低功耗:上电 2 秒后进 STOP2,按 B1 唤醒,翻 LD1,500ms 后再进 STOP2 循环;串口能看到 log;
- FDCAN 收发:按 B1 唤醒后,FDCAN1 发 1 帧 CAN FD 数据(对方是另一块 Zephyr 板子),然后进 500ms 延时再回 STOP。
参考 ST 例程 PWR/PWR_ModesSelection 和 FDCAN/FDCAN_Power_Down(U385RG-Q 板子),但只搬 HAL API 那一行——它们的应用层(UART 菜单 / loopback 测功耗)跟我们的"按键触发"完全两码事。
二、最终长这样

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

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

三、工程结构
整个工程是从零搬的"最小集"——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_Down 的 MX_FDCAN1_Init 框架,但要改三处:
- 引脚:PA11/PA12 → PB8/PB9(U3C5ZI-Q 板子)
- 复用:
GPIO_AF9_USART1 → GPIO_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_SuspendTick 跟 uwTick 联** |
不用 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 和 ->PSR,PSR[2:0]=LEC |
九、关键经验
- CAN FD 仲裁段必须跟对方 100% 兼容——这是 CAN 2.0 fallback 层硬性要求。别想着"自己定 4M/4M"省得调,对方多少你就多少。
- 要跟成熟系统对接时,先看对方 dts / 配置。Zephyr 端 dts 三行就把 bit timing 全说了,比 STM32CubeMX 的图形界面更直接。
LEC 错误码能直接定位:ACK=物理层,Bit0/Bit1=位时序,CRC/Stuff=干扰。省得在物理层和位时序之间来回猜。
- 低功耗不要 4 项优化一起加——出问题无法定位是哪个。一次只加一项,电流表测。
- 省 0.5-1 µA 的小优化不可靠——SRAM1 时钟门控省那 0.5 µA 跟唤醒可靠性冲突,不值得。
- ICache
DeInit 跟 Disable 不是同一类操作——DeInit 重置 WAYSEL,Enable 不恢复,第二次 STOP 必挂。STOP 场景下只用 Disable/Enable。
- FDCAN 关闭用
EnterPowerDownMode,不要 DeInit——DeInit 每次进 STOP 都要重配 bit timing + filter,PowerDown 模式保留所有配置,唤醒一行 Exit 就恢复。
十、附件
附件: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 推荐