一、问题现象与背景

在STM32 HAL库项目中,开发者常通过重定义HAL_GPIO_EXTI_Callback、HAL_UART_RxCpltCallback等__weak函数实现业务逻辑。典型现象是:

  • 中断确实进入,但用户回调不执行;
  • 单步调试能进回调,全编译后失效;
  • 换优化等级、增删文件后行为变化;
  • nm查看符号存在,运行时却跳到默认空实现。

根因通常不是中断配置,而是弱符号与链接顺序导致用户强符号未覆盖库中弱符号。

二、原理:__weak与链接器符号解析

HAL库在stm32xx_hal_xxx.c中定义:

__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  UNUSED(huart);
}

__weak展开为编译器属性(如GCC的__attribute__((weak))),生成弱符号。链接规则:

  • 强符号只能有一个,弱符号可有多个;
  • 同名强符号存在时,弱符号被丢弃;
  • 若只有弱符号,则保留其中一个;
  • 静态库成员只有在解析未定义符号时才被抽取。

关键点:如果用户回调所在的目标文件/静态库在链接命令行中位置不当,链接器可能先抽取了HAL库中含弱定义的目标文件,之后用户强符号虽存在,却因“已解析”而不再参与覆盖,最终调用弱实现。

三、排查步骤

1. 确认符号强弱与地址

arm-none-eabi-nm -C build/app.elf | grep HAL_UART_RxCpltCallback

输出示例:

0800a1b0 W HAL_UART_RxCpltCallback
0800b2c4 T HAL_UART_RxCpltCallback

W为弱符号,T为强符号。若只有W,说明用户定义未被链接;若两者并存,需看调用点实际跳转地址。

2. 反汇编调用点

arm-none-eabi-objdump -d build/app.elf | grep -A6 "<HAL_UART_IRQHandler>"

观察bl目标地址是否指向用户函数。若指向弱实现地址,即可确认覆盖失败。

3. 检查map文件

在.map中搜索回调符号:

.text.HAL_UART_RxCpltCallback
  0x0800a1b0  HAL_UART_RxCpltCallback  (stm32f4xx_hal_uart.o)

若只出现HAL库目标文件,说明用户目标文件未被抽取或链接顺序错误。

4. 检查链接顺序

GCC链接顺序原则:被依赖库放在依赖者之后。推荐:

app.o driver.o -lhal -lc -lm

若用户回调封装在静态库libapp.a中,应确保libapp.a位于libhal.a之前,或使用--whole-archive强制抽取。

四、完整代码示例

用户回调文件app_callback.c:

#include "main.h"

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        g_rx_flag = 1;
        HAL_UART_Receive_IT(&huart1, &g_rx_byte, 1);
    }
}

中断处理中调用:

void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);
}

HAL内部调用链:

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
    /* ... */
    HAL_UART_RxCpltCallback(huart);
}

若链接正确,bl应跳到app_callback.o中的强符号。

五、工程配置建议

  • 用户回调单独放.c文件,避免与HAL源文件同文件定义;
  • 使用Makefile/CMake时显式控制库顺序;
  • 静态库场景可加-Wl,--whole-archive libapp.a -Wl,--no-whole-archive;
  • 开启-ffunction-sections -fdata-sections与--gc-sections时,确认回调未被误裁剪;
  • 用__attribute__((used))保护仅由中断调用的函数;
  • 定期用nm与map文件做符号审计。

六、注意事项

  • 不要同时保留多个同名强符号,否则链接报错;
  • 回调函数签名必须与HAL声明完全一致,否则是另一个符号;
  • C++工程需用extern "C"包裹;
  • 优化等级变化可能改变内联与抽取行为,需全量重编译验证;
  • 若使用IDE,检查“排除构建”与链接脚本是否遗漏目标文件。

通过符号强弱分析、反汇编确认和链接顺序调整,可系统定位并解决__weak回调失效问题。