ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32N6链接报错undefined reference?一文教你排查MX_USART1_UART_Init缺失问题

STM32N6链接报错undefined reference?一文教你排查MX_USART1_UART_Init缺失问题 最近在折腾 STM32N6这颗芯片和之前玩过的 M 系列有个很不一样的地方内部直接集成了 Neural-ART NPU跑 AI 模型不再依赖 CPU 纯算硬扛。我照着官方 Neural-ART 教程拉了一个示例工程目标是在一个轻量分类模型上把推理结果通过串口打印出来。结果倒好编译阶段一路绿灯链接阶段突然报错undefined reference to MX_USART1_UART_Init这类错误对老手来说不算陌生基本就是“函数声明能找到但实现没被链接进来”。奇怪的是我在工程里明明看到了 usart.c函数也写得清清楚楚为什么还是 undefined reference这个坑我前前后后折腾了很久最后定位到问题出在构建系统的源文件列表上——外设初始化文件压根没被收进编译目标。这篇文章把完整排查过程、修复操作以及一套通用的 undefined reference 排查方法都整理出来。不管你是用 STM32CubeIDE还是跟我一样用 CMake 命令行构建都应该用得上。1. 先搞懂错误本质链接器到底在抱怨什么1.1 undefined reference 不等于代码写错了很多人看到 “undefined reference” 会下意识觉得是“函数没定义”。对但也不全对。把编译和链接两个阶段拆开看就很清楚了编译阶段编译器拿.c文件做语法分析和代码生成它只需要在头文件里看到函数的声明知道“有这么个函数、参数长这样”就够了所以main.c调用MX_USART1_UART_Init()时只要包含了usart.h编译就能过。真正的问题出在最后的链接阶段链接器要把所有.o目标文件、静态库和启动文件拼成最终的可执行程序这时它必须找到每一个被调用函数的具体实现也就是符号对应的机器码。找遍所有输入文件都没找到就报undefined reference。打个比方编译阶段就像你在一份 API 手册上看到了某函数的介绍可以放心在代码里写调用链接阶段则像你真正打电话得确保对方手机开机、号码也有对应的终端。如果手册有记录但电话打不通那问题就不在“手册写错了”而在“对方根本没接入网络”。对应到工程里常见的原因是源文件没参与编译、函数实现被条件编译关掉了、文件被构建系统排除或者链接时库的顺序不对。所以看到这个报错不要急着怀疑是不是算法模型出了问题也不要觉得是工具链坏了。这是一个纯粹的构建配置问题排查路径非常固定下面我会按顺序拆开讲。1.2 为什么偏偏是 MX_USART1_UART_InitMX_前缀是 STM32CubeMX 生成代码的标志。USART1表示目标外设是 USART1UART_Init是初始化函数合起来就是“初始化 USART1 的 CubeMX 生成函数”。在 Neural-ART 教程工程里串口通常是用来打印推理结果、模型运行耗时和日志信息的。模型跑完 NPU 之后分类结果、置信度、帧率这些数据都要靠串口送到 PC 端调试助手所以 UART 几乎是所有 AI 示例工程的标配外设。这里要特别注意MX_USART1_UART_Init不是 HAL 库自带的函数它是 CubeMX 根据你在.ioc文件里的外设配置动态生成的用户层代码。它的定义在Core/Src/usart.c里声明在Core/Inc/usart.h里。如果链接器说找不到这个符号问题大概率不是 ST 官方 HAL 库的问题而是工程自己生成的外设初始化代码没有被完整纳入构建流程。搞清这一点后面的排查思路就清晰了不要先去查 HAL 库的源码先把“这个函数定义在哪个文件、这个文件是否被编译”这两件事弄清楚。1.3 STM32N6 工程中这个符号的“出身”STM32N6 的工程结构和老 M 系列有一点明显不同。老工程通常是 CubeMX 生成一个 IDE 工程源文件集中在Core/Src然后用 IDE 直接编译。N6 的 AI 示例工程很多时候是从 ST 的 Edge AI package 拉下来的构建方式可能是 CMake、Makefile甚至 ST 自己的命令行工具链。这类工程里Core/Src/usart.c不一定被自动收集尤其当构建脚本用的是显式源文件列表时多一个少一个文件非常常见。另外N6 的教程工程为了封装模型处理逻辑往往会把代码拆成App、Core、Middlewares多个目录。有的版本甚至会把外设初始化文件放到 App 层比如App/app_usart.c。这种情况下符号名可能变成APP_USART1_Init或者文件位置变了但main.c里的调用还是旧的MX_USART1_UART_Init名字对不上也会出现 undefined reference。先理解符号的“出身”再动手排查比一股脑重装工具链高效得多。2. 五步排查法定位符号为什么没被链接进来2.1 第一步先确认定义真的存在排查的第一步永远是搜代码。在工程根目录执行grep -rn MX_USART1_UART_Init --include*.c --include*.h .正常情况下会看到两种结果一个在.h文件里的声明一个在.c文件里的函数定义。如果连定义都搜不到那就说明 CubeMX 生成代码时没把 USART 外设的初始化代码生成出来或者生成到了别的目录。打开.ioc文件在 Pinout Configuration 里确认 USART1 是否已经配置为启用状态然后在 CubeMX 里重新生成一次代码。如果定义存在但链接还是报错先别急着看 IDE用nm看一眼目标文件到底有没有把符号编译出来。假设你的构建输出目录是buildarm-none-eabi-nm build/Core/Src/usart.o | grep MX_USART1如果输出类似00000000 T MX_USART1_UART_Init说明目标文件里确实有这个定义如果没有任何输出说明你搜到的.c文件可能没有参与编译。我在实际排查中就遇到过一种情况源码目录里有一个usart.c但它根本没被 CMake 的源文件列表引用所以编出来的usart.o压根不存在自然搜不到符号。2.2 第二步确认定义没有被条件编译掐掉还有一种情况是函数定义存在但被预处理指令包住了实际编译时被跳过。CubeMX 生成的usart.c里虽然一般不会给MX_USART1_UART_Init套#ifdef但 HAL 库整体的外设模块可以裁剪。stm32n6xx_hal_conf.h中有一个HAL_UART_MODULE_ENABLED宏如果它被注释掉或者没定义HAL 层就不会编译 UART 相关代码某些版本的生成逻辑甚至会把usart.c的内容也弱化。检查方法很简单打开stm32n6xx_hal_conf.h搜HAL_UART_MODULE_ENABLED。正常情况下应该是#define HAL_UART_MODULE_ENABLED如果是/* #define HAL_UART_MODULE_ENABLED */这种注释状态说明在 CubeMX 的 Advanced Settings 里把 UART 模块裁剪掉了。重新打开.ioc把 UART 模块勾回来重新生成代码。这里还要留意不要只改宏因为 CubeMX 重新生成时可能会覆盖掉其他手动修改最好是走.ioc配置流程而不是手改配置头文件。2.3 第三步确认源文件真的参与编译了这一步是大多数人卡住的地方也是我这次踩坑的核心原因。如果你用 STM32CubeIDE右键项目在 Project Explorer 里找到Core/Src/usart.c看文件名上有没有“排除出构建”的灰色减号标记。如果有右键文件 → Resource Configurations → Exclude from Build取消勾选即可。但更隐蔽的情况是文件看起来在工程里IDE 也能看到但构建系统压根没把它编进去。CubeIDE 底层如果是 CMake 工程源文件列表由CMakeLists.txt控制如果是普通 Managed Build 工程Eclipse 会从项目路径扫描源文件。后者一般不会漏前者特别容易漏。命令行构建的话直接检查构建脚本。教程工程最常见的是 CMake 的显式源文件列表set(SOURCES Core/Src/main.c Core/Src/stm32n6xx_it.c App/ai_runtime.c )如果这个列表里没有Core/Src/usart.c那链接器永远找不到这个函数。修复就是加一行set(SOURCES Core/Src/main.c Core/Src/usart.c Core/Src/stm32n6xx_it.c App/ai_runtime.c )如果工程用的是file(GLOB_RECURSE ...)方式收集源文件那新增文件后没有重新运行 cmake 配置也会导致文件没进构建。解决办法是删掉build目录重新执行cmake -S . -B build让 GLOB 重新扫描。2.4 第四步检查链接脚本和库依赖顺序如果MX_USART1_UART_Init的实现被打包到了静态库里比如官方的某个板级支持库.a那链接顺序就变得非常关键。GNU ld 处理静态库时原则是“遇到未解析符号才从库里抽取目标文件”。如果链接命令里引用这个符号的目标文件排在库文件后面符号可能就无法被解析。典型报错场景是arm-none-eabi-gcc ... main.o libbsp.a -o app.elf有些情况下这样能过有些情况下就必须把库放到引用它的人后面。最保险的办法是给链接器加组选项arm-none-eabi-gcc ... -Wl,--start-group main.o libbsp.a -Wl,--end-group -o app.elf--start-group会让链接器在库组内反复扫描直到符号解析完成或没有新符号可以被解析。不过在我这次遇到的问题里usart.c并不是库而是被遗漏的源文件所以更可能是源文件列表的问题库顺序是后面才需要考虑的排查方向。如果你在链接日志里看到了libneural-art.a之类的库并且错误恰好出现在库后面的符号引用那可以先尝试调整顺序再用--start-group包住库组。不要一上来就改链接脚本很多时候不是链接脚本的问题。2.5 第五步警惕 C/C 符号修饰差异还有一个不太常见但一碰就容易懵的原因C 和 C 的符号修饰规则不同。C 编译器会对函数名做 name mangling比如void MX_USART1_UART_Init(void)在 GCC ARM C 编译下会变成_Z21MX_USART1_UART_Initv。如果调用方的文件被当成 C 编译而被调用的定义文件被当成 C 编译两边符号对不上就会报 undefined reference。用nm一眼就能看出来arm-none-eabi-nm build/App/main.o | grep MX_USART1 arm-none-eabi-nm build/Core/Src/usart.o | grep MX_USART1如果一边是U MX_USART1_UART_Init一边是T _Z21MX_USART1_UART_Initv那基本就是编译语言不统一。解决办法是给头文件加extern C保护#ifdef __cplusplus extern C { #endif void MX_USART1_UART_Init(void); #ifdef __cplusplus } #endif如果是 CubeMX 生成的头文件新版本一般已经带了保护但有些旧版本没有。另外如果教程工程把工程拆成了安全区/非安全区两个子工程也就是 TrustZone 场景还要确认MX_USART1_UART_Init的调用方和定义方在同一个子工程里。跨工程访问外设初始化函数需要额外做符号导出或者放到共享库中不能默认两边都能看到。3. 针对 Neural-ART 教程工程的实操修复过程3.1 复现我的失败现场我这次用的是官方某个 STM32N6 AI package 里带的人脸检测示例构建流程是 CMake。目录结构大概是project/ ├── CMakeLists.txt ├── App/ │ ├── main.c │ └── ai_runtime.c ├── Core/ │ ├── Inc/ │ └── Src/ │ ├── main.c │ ├── usart.c │ └── stm32n6xx_it.c └── Middlewares/ └── neural_art/执行cmake -S . -B build cmake --build build结果在链接阶段报错[ 90%] Linking C executable test_fd /usr/bin/ld: CMakeFiles/test_fd.dir/App/main.c.o: in function main: main.c:(.text0xa4): undefined reference to MX_USART1_UART_Init collect2: error: ld returned 1 exit status我第一时间在源码里搜索usart.c确实存在MX_USART1_UART_Init定义也清清楚楚。我用nm检查却发现build目录下压根没有生成usart.o。这时才意识到问题出在CMakeLists.txt的源文件列表。这个教程工程为了便于用户阅读把主程序放在App/main.c但外设初始化的usart.c还留在Core/Src而 CMake 列表里只写了Core/Src/main.c和Core/Src/stm32n6xx_it.c忘了加usart.c。3.2 CubeIDE 工程里的操作路径如果你不是在命令行用 CMake而是在 STM32CubeIDE 里直接打开工程处理路径稍有不同。先看 Project Explorer 里Core/Src/usart.c是否存在。文件存在时确认它没有被标记为“排除出构建”。Eclipse 对这种文件会在文件图标上叠加一个减号鼠标放上去会提示“Exclude from build”。如果有排除标记右键文件 → Resource Configurations → Exclude from Build把勾去掉。如果文件根本不在工程树里右键工程 → Refresh让 IDE 重新扫描磁盘。如果扫描后还是没有大概率是工程文件.project里的 source entry 配置不对或者工程目录和源码目录不一致。这时候别死磕 IDE打开.cproject和.project看 source entry 路径把Core/Src目录加回来。CubeIDE 还有一种情况如果你使用 CubeMX 重新生成代码时选错了“Application Structure”比如从“Advanced”切回“Basic”生成的外设文件位置会变化。旧文件被留在磁盘上但新的构建列表已经不再包含它。碰到这种“文件在但参与不了构建”的情况最好的办法是备份自己的修改然后用 CubeMX 重新生成一个干净的工程再手动合并修改。3.3 Makefile/CMake 工程里的处理路径Makefile 工程相对好处理。打开 Makefile找到C_SOURCES变量的定义看里面有没有Core/Src/usart.c或者wildcard方式。STM32CubeMX 默认生成的 Makefile 会通过通配符自动收集Core/Src/*.c一般不会漏。但教程工程的 Makefile 可能是手写的源文件列表写得很随意这种就要手动补。C_SOURCES \ Core/Src/main.c \ Core/Src/usart.c \ Core/Src/stm32n6xx_it.c改完之后执行make clean再重新make。为什么要 clean因为旧的main.o可能还残留着 undefined reference 的信息如果 Makefile 没正确追踪头文件依赖你可能改了构建列表但没有触发重新链接这时候全量重建是最省心的。CMake 工程更简单把usart.c添加进 target 的源文件列表即可。但要注意如果你用的是target_sources要确保在正确的 target 下添加。比如add_executable(test_fd ${SOURCES}) target_include_directories(test_fd PRIVATE Core/Inc) target_sources(test_fd PRIVATE Core/Src/usart.c)添加后建议rm -rf build再重新配置。因为 CMake 的配置缓存有时会保留旧的源文件列表尤其当你不是从CMakeLists.txt的根目标添加时增量配置不一定生效。删除 build 目录是最不费脑子的做法。3.4 重新生成后的验证清单修好构建列表后重新编译。链接通过只是第一步我建议按下面的清单做一遍验证避免表面上修好了烧到板子上又发现串口不工作确认编译日志里真的出现了usart.c的编译命令。开着make VERBOSE1或者 CMake 的CMAKE_VERBOSE_MAKEFILEON看到usart.c被编译到目标文件才说明文件真正参与了构建。用nm确认符号类型。arm-none-eabi-nm build/.../usart.o | grep MX_USART1看到T MX_USART1_UART_Init表示定义存在且是全局可链接符号。烧录后打开串口调试助手看有没有模型初始化日志。如果没有任何输出先用调试器检查huart1.gState是否是HAL_UART_STATE_READY检查 USART1 的 GPIO 复用配置是否正确。检查时钟树。USART1 在 STM32N6 上的时钟源可能来自某个 PLL 或 HSI如果时钟树配置不对波特率会偏移串口打印乱码也是常有的事。链接成功不代表外设配置成功这是两件事别混在一起。4. 这类错误的常见变体和速查表4.1 不只是 MX_ 函数其他高频 undefined reference 变体嵌入式里undefined reference太常见了远不止MX_函数这一种。比较高频的几个undefined reference to main整个工程的入口函数缺失。常见于启动文件选错、删了 main 函数、或者链接时没把包含 main 的目标文件加进来。undefined reference to HAL_UART_Transmit这是 HAL 库函数缺失。原因通常是 HAL 库源文件没加入构建或者stm32n6xx_hal_conf.h里裁剪了 UART 模块。undefined reference to _exit、undefined reference to __aeabi_*这是编译器 runtime 库缺失。多半是工具链安装不完整或者链接选项里少了--specsnano.specs之类的参数。undefined reference to printf常见于使用半主机模式时标准库实现不完整。嵌入式里一般用 retarget 重定向或者勾选 MicroLib。这些变体的排查思路和MX_USART1_UART_Init是相通的先定位符号属于哪个源文件或库再确认这个文件或库是否被链接器作为输入。不要去背每个符号的含义掌握方法比记结论重要。4.2 问题排查速查表线索可能原因推荐操作grep 整个工程都搜不到函数定义CubeMX 没生成该外设代码或文件在别的目录打开.ioc确认 USART 外设已启用重新生成代码usart.c存在但文件上有灰色减号IDE 将文件排除出构建右键文件取消 Exclude from Buildusart.c存在但构建日志里没有它CMakeLists 或 Makefile 源文件列表遗漏把文件路径加入构建列表重新配置只有声明没有定义定义文件被重命名或移动对比官方例程统一函数名和路径nm发现符号带_Z前缀C/C 编译语言不一致给头文件加extern C链接命令里库顺序不对静态库解析符号时序问题使用-Wl,--start-group/--end-groupHAL_UART_MODULE_ENABLED被注释外设宏被裁剪HAL 源码未编译在 CubeMX 中恢复 UART 模块并重新生成工程拆分为安全区/非安全区子工程函数定义和调用不在同一侧将定义移到调用方同一子工程或通过接口导出这张表基本覆盖了这类错误的 90% 场景。剩下的 10% 属于工具链本身的 bug 或工程缓存问题一般通过 clean 和重新配置就能解决。4.3 为什么教程工程最容易被这种坑到Neural-ART 这类 AI 教程工程和普通外设例程不同它同时涉及 CubeMX 生成代码、NPU 库、AI 模型文件、可能的 C 接口层甚至还有 RTOS。工程结构比“点灯工程”复杂一个量级构建配置很容易出问题。很多教程压缩包发布时作者是为了在某个特定版本的工具链上演示他可能忘了更新 CMakeLists或者发出来的工程已经经过本地手工调整换一台电脑、换一个工具链版本就露馅。还有一个现实因素是STM32N6 比较新各种 AI package 的版本迭代很快。有时候你下载的是旧教程配套的 CubeMX 版本却是新的重新生成代码后文件结构变了但教程里的构建脚本还是老一套两边对不上就报错。所以我的建议是先把官方压缩包存一份不动然后另建目录做修改。一旦遇到奇怪的构建问题就用git diff或者直接对比官方目录重点看源文件列表差异往往几秒就能发现问题。5. 写到最后三个实用小习惯踩过这次坑之后我给自己定了三条规矩。第一从 GitHub 或者官方包拉教程工程时先跑一遍构建确认基础环境没问题再做任何代码修改。如果这步就报链接错误优先看源文件列表别上来就怪编译器。第二CubeMX 生成的外设代码尽量不要手动重命名比如把MX_USART1_UART_Init改成自己的InitUart之类的除非你能保证同步修改所有声明、定义和头文件保护规则。保持默认链路以后重新生成代码、升级版本都能省很多事。第三凡是链接错误报的是MX_开头的符号先怀疑构建配置再去翻代码。这类函数是 CubeMX 自动生成的几乎不会有逻辑问题大概率是文件没有参与构建。修完这个报错后面真正让我头疼的其实是 NPU 模型推理性能和串口打印格式的调优。但反过来想如果当时没有把链接错误这一套彻底研究透后面遇到模型库链接失败、符号冲突的时候我可能还会走很多弯路。希望这篇记录能帮你早点从“编译不过”的泥潭里跳出来把时间花
返回列表