问题现象:重定义__weak函数为何不生效?

在STM32 HAL库中,许多回调函数(如HAL_UART_RxCpltCallback)被定义为__weak,允许用户重定义。但有时用户明明在main.c中重写了该函数,运行时却仍进入默认空实现。例如:

// 用户代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 自定义逻辑
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

调试发现该函数从未被调用,而默认的__weak版本(空函数)却执行了。这通常与链接顺序有关。

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

  • __weak是编译器扩展,将函数标记为弱符号。链接时,若存在强符号(用户定义),则强符号覆盖弱符号;否则使用弱符号。
  • 关键点:链接器按命令行中目标文件/库的顺序解析符号。对于静态库(.a),链接器只提取满足当前未定义符号的成员。
  • 若用户目标文件在静态库之后出现,链接器处理用户文件时,弱符号已从库中提取并解析,后续强符号无法覆盖已解析的弱符号(除非使用特殊选项)。

典型错误链接顺序

假设工程结构:

  • main.o(含用户重定义)
  • libHAL.a(含弱定义)

错误链接顺序:libHAL.a 在 main.o 之前。链接器先处理库,发现HAL_UART_RxCpltCallback未定义,从库中提取弱定义并解析。之后处理main.o,强符号已无法覆盖。

正确顺序:用户目标文件在前,库在后。

排查步骤

1. 检查链接命令顺序

在IDE(如STM32CubeIDE)中查看链接器命令,确保用户目标文件(.o)在HAL库(.a)之前。对于Makefile,调整LDFLAGS中文件顺序。

2. 分析map文件

生成map文件(链接选项-Wl,-Map=output.map),搜索函数名:

HAL_UART_RxCpltCallback

查看其地址和来源。若来自libHAL.a,说明弱符号被采用;若来自用户.o,则正确。

3. 使用--whole-archive强制包含

若无法调整顺序,可强制链接器包含库中所有目标文件:

LDFLAGS += -Wl,--whole-archive -lHAL -Wl,--no-whole-archive

但此法增大代码体积,慎用。

完整代码示例

用户重定义(main.c):

#include "main.h"

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

HAL库弱定义(stm32f1xx_hal_uart.c):

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

正确链接顺序(Makefile片段):

OBJS = main.o stm32f1xx_it.o system_stm32f1xx.o
LIBS = -lHAL -lc -lm

$(TARGET).elf: $(OBJS) $(LIBS)
    $(CC) $(OBJS) $(LIBS) -o $@

注意事项

  • 确保用户函数签名与弱定义完全一致(参数、返回类型),否则视为不同函数。
  • 若使用C++,需用extern "C"包裹重定义函数,避免名称修饰。
  • 某些IDE(如Keil MDK)默认链接顺序可能不同,需在“Linker”选项卡中调整“Objects”顺序。
  • 使用--whole-archive时,注意可能引入未使用的代码,增加Flash占用。
  • 定期检查map文件,确认关键函数来源,避免隐式链接问题。

通过以上排查,可确保用户重定义的__weak函数正确覆盖HAL库默认实现。