ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32N6 Neural-ART工程undefined reference链接错误排查指南

STM32N6 Neural-ART工程undefined reference链接错误排查指南 刚拿到这套 Neural-ART 教程工程的时候我其实挺兴奋的STM32N6 这颗带 NPU 的片子终于能正经跑神经网络了。结果兴致勃勃地一按 Build编译器直接甩给我一行undefined reference to MX_USART1_UART_Init我当时第一反应是这玩意儿怎么可能找不到CubeMX 不是都帮我生成好了吗但现实就是这么打脸。这个报错卡了我快两个小时后来排查下来发现它根本不是缺一个函数那么简单而是整套构建链路里好几处细节被忽略导致的连锁反应。如果你也在折腾 STM32N6 的 Neural-ART 工程或者以后打算往这个平台迁移这篇文章大概率能帮你省下不少冤枉时间。1. 先把问题定性undefined reference 到底在说什么1.1 链接错误和编译错误是两码事很多刚接触嵌入式开发的朋友一看到 undefined reference 就慌以为是自己语法写错了。其实这个报错和语法半毛钱关系都没有。编译Compile和链接Link是两个完全独立的阶段。编译阶段做的是语法检查、类型检查、头文件展开最终把.c文件翻译成目标文件.o或者是 Keil 里的.obj。如果只是语法错误编译器会直接报错不会走到链接那一步。链接阶段做的事情就简单粗暴了把所有编译好的目标文件、静态库打包在一起把各个文件里的函数调用和函数定义对上号。你这个文件里写了MX_USART1_UART_Init()的调用链接器翻遍了整个工程找不到这个函数的定义于是告诉你 undefined reference。所以这个报错的本质是你的代码调用了这个函数但链接器在整个工程输出里找不到它的实体。为什么会找不到无外乎三种可能文件没参与编译、函数被条件编译屏蔽了、函数名和声明不一致。1.2 STM32N6 工程构建流程的特殊性STM32N6 和传统 STM32 家族最大的不同在于它集成了神经网络处理单元NPU。Neural-ART 这套工具链的出现就是专门为了把训练好的模型编译成能在 NPU 上跑的指令集。但这套工具链并不是独立运行的——它通常会和 STM32CubeMX 生成的工程代码协同工作。简单说Neural-ART 的 build 过程一般分成两段第一段把模型ONNX、TensorFlow Lite 等格式通过 Neural-ART 编译器转成 NPU 可执行的目标代码。第二段把生成的 NPU 代码和 CubeMX 生成的 MCU 工程代码包括外设初始化、主循环、中断处理放在同一个 IDE 工程里编译链接。问题往往出在第二段。因为 Neural-ART 工具在生成工程文件时可能只会复制一部分 CubeMX 生成的源文件或者它生成的链接脚本和你当前 CubeMX 配置不一致。一旦有某个外设初始化文件没被包含进构建系统链接阶段就会出现 undefined reference。这个MX_USART1_UART_Init就是典型代表。它通常定义在main.c或者单独的usart.c里而你当前工程里的某段代码可能是 Neural-ART 生成的示例代码也可能是你自己加的调试打印逻辑调用了它。两边没对上链接器就炸了。2. 一层一层拆解到底是哪些原因导致这个函数“消失”了2.1 最常见的坑源文件压根没加入构建系统这种情况最容易出现在“手残”或者“工具自动生成的工程结构不完整”的时候。Keil 工程里你要在左侧项目管理器里看到某个.c文件它才会被编译。如果你在文件夹里能看到usart.c但是工程树里没有它那这个文件就是空气——编译器根本没机会编译它。怎么快速判断看编译日志。如果整个编译过程里你从来没看到过usart.c被编译的记录比如日志里没有usart.o或者usart.obj的生成行那基本就是没加入构建。注意在 STM32CubeMX 生成的工程里MX_USART1_UART_Init()这个函数在 CubeMX 使用较新版本时会生成在main.c里在部分版本配置下则可能拆分到usart.c。具体看你的 CubeMX 版本和配置。但无论它放在哪个文件里那个文件必须在构建系统里。2.2 名字对不上头文件声明和实现不一致还有种很隐蔽的情况你在main.c里包含了usart.h头文件里声明了MX_USART1_UART_Init这个函数原型但是实际实现的时候函数名打错了或者在某个#if条件里被包住了。比如你的usart.c里写的是#if 0 void MX_USART1_UART_Init(void) { // 初始化代码 } #endif这种情况编译阶段不会报错因为调用方只要看到头文件里的声明就够了。到了链接阶段链接器一找函数体根本不存在被预处理阶段删掉了于是 undefined reference。或者更简单的——你在 CubeMX 里明明配置的是USART1但后来手动改代码时不小心把函数命名成了MX_USART1_UART_Init_1这种低级错误也是有的。2.3 条件编译宏不一致同样的代码不同的“世界线”这是嵌入式项目里最让人抓狂的一类问题。CubeMX 生成的代码里某些外设驱动会根据#ifdef决定是否编译。尤其是当工程支持多种板卡型号时HAL 库会把不同型号的驱动封装在条件编译块里。我举个真实案例某次我在一个 H750 的工程里也遇到了类似的问题MX_I2C1_Init找不到。查了半天发现stm32h7xx_hal_conf.h里有一行#define HAL_I2C_MODULE_ENABLED被我不知什么时候给注释掉了。HAL 库没启用这个模块整个stm32h7xx_hal_i2c.c里的函数全都不参与编译自然就找不到定义了。对应到 STM32N6 和 USART你需要检查main.h和stm32n6xx_hal_conf.h里外设模块是否被正确启用了。如果 USART 的宏没定义或者被注释掉同样会出现这种 undefined reference。2.4 Neural-ART 工具的特殊干扰构建脚本覆盖了你的设置这个点可能是 STM32N6 工程特有的坑。Neural-ART 在 build 的时候有可能会去改写工程文件比如在你原有的 CubeMX 工程基础上追加一些生成文件、调整 include 路径、甚至改变链接脚本的配置。如果你用的是它自带的 build 脚本脚本内部会把一些文件区分成“模型生成部分”和“应用部分”。在应用部分的构建列表里如果它漏掉了包含 USART 初始化的那个.c文件那你无论怎么点 IDE 的 Build 按钮问题都会反复出现。所以排查的时候一定要先看清楚你当前工程跑的是 IDE 原生的 build 流程还是 Neural-ART 工具的 build 流程。两者的编译对象列表可能完全不一样。这个点极易被忽略但往往是问题真正所在。3. 我的完整排查过程一条路走到黑再回头3.1 第一轮排查确认报错来源遇到这个报错第一步永远是先看构建日志的完整片段而不是只看 IDE 底部那几行红色的错误提示。我当时的日志大概是这样的./App/neural_art_main.o: In function neural_art_task: neural_art_main.c:(.text.neural_art_task0x1a): undefined reference to MX_USART1_UART_Init注意看我加粗的这个文件neural_art_main.o。编译失败的位置不在 main.c而是在 Neural-ART 生成的示例应用文件里。也就是说Neural-ART 的示例代码默认是要调用一个外部函数MX_USART1_UART_Init它假设你已经在工程里写好了这个函数。这下线索就清楚了问题大概率出在“Neural-ART 示例代码期望有这个函数”和“我的工程里没有提供这个函数”的空档之间。3.2 第二轮排查全工程搜函数定义我用 IDE 自带的全局搜索功能在整个工程范围搜索MX_USART1_UART_Init。搜索结果发现函数声明在usart.h里有函数定义预期在usart.c里但usart.c里的函数体前面有一个条件编译宏把代码块包了起来。搜到归搜到我的工程里其实是有这个函数的但它被我 CubeMX 再生成代码时的“用户代码保护段”冲突问题给弄消失了。CubeMX 生成代码时/* USER CODE BEGIN */和/* USER CODE END */之间的内容不会被动但这之外的代码是会被重新生成的。如果你之前手动修改过函数实体然后 CubeMX 一刷新这部分代码就会被冲掉或者改动。如果函数体还在考虑用下面的命令直接查看库函数符号列表。我知道很多人是 Windows 环境用的 Keil 或者 STM32CubeIDE命令行工具略有不同但思路一样nm ./build/usart.o | grep -i uart_init如果这条命令没有任何输出说明这个.o文件里压根没有导出这个符号进一步确认函数定义没被编译进去。3.3 第三轮排查检查构建系统文件列表这一步是关键。我在 STM32CubeIDE 里检查了项目资源管理器确认usart.c确实在工程树里。但问题恰恰出在这——STM32N6 的工程会被 Neural-ART 的生成脚本处理过它有一个独立的CMakeLists.txt或者.cproject文件加载源文件列表的逻辑可能和 IDE 树里显示的并不一致。当时我做的事情是把工程里的CMakeLists.txt打开搜索usart。结果发现usart.c根本不在add_executable的源文件列表里。也就是说IDE 树里看到它但实际的 make 系统根本不知道它存在。这就是典型的“看起来在实际不在”的灵异事件。如果你也用 STM32CubeIDE可以在 Project Properties 里查看 C/C Build 的 Settings确认源文件列表。或者直接打开工程根目录下的.cproject文件搜索usart.c看看有没有在sourceEntries里。3.4 第三轮修改不同工具下的三种解法我的排查环境是 STM32CubeIDE 搭配 CMake 构建。但我知道不少人用的是 Keil MDK两者解法不同STM32CubeIDE / CMake 场景手动把usart.c加进add_executable。如果你不想改 CMakeLists可以直接把函数定义挪到main.c里因为 main.c 一定在构建列表里然后在main.c里放到用户代码保护区段。这是最暴力但最有效的方法。Keil MDK 场景在项目管理器右键点击项目文件夹选择 Add Existing Files to Group把usart.c加进 Application/User 组。然后重点检查 Target Options 里的 C/C 选项卡确认 Include Paths 包含Inc目录。IAR 场景右键工程选择 Add Add Files把.c文件加进去。同时检查 Options C/C Compiler Preprocessor 里的包含路径。我给自己的判断依据很简单既然 Neural-ART 生成的示例代码里强制调用了这个函数那我就要找到一个一定参与编译的文件把函数放进去。main.c必然参与编译而且 CubeMX 生成的MX_USART1_UART_Init函数体其实也不长无非就是 GPIO、时钟、中断优先级和外设句柄注册这几段搬运过来是一点问题没有。提示这里有个“治标不治本”的问题。如果后续你再跑一次 CubeMX 重新生成代码main.c 的 USER CODE 区段会保留但你手动加进去的初始化函数如果没放到 USER CODE 段里就会被冲掉。所以要么把函数放进 USER CODE 段要么干脆把构建系统里的源文件列表问题根治掉。我的做法是两者都做先用最快的方法让工程跑起来再回头修 CMakeLists。4. 从根上解决给 STM32N6 Neural-ART 工程的构建配置做一次全面体检4.1 对齐 CubeMX 配置和 Neural-ART 构建脚本让我说一句经验之谈Neural-ART 工具现在还远不算成熟它的构建脚本对 CubeMX 版本的敏感度很高。我试过的版本里CubeMX 生成的项目结构和 Neural-ART 文档里示例的差异是导致这类 undefined reference 的高发原因。建议你从这三个方面逐项对齐芯片包版本STM32CubeFW_N6 的版本和 Neural-ART 工具包要求的是否一致。CubeMX 版本Neural-ART 文档里指定的 CubeMX 版本通常是一个范围内的跨大版本可能有生成代码结构的差异。中间件选择Neural-ART 示例模板里依赖的中间件比如 USB、ETH、或者 RTOS你必须在 CubeMX 里配置进去不然模板代码会引用到不存在的函数。4.2 系统化检查 HAL 库的模块开关接前面说的条件编译问题STM32N6 的 HAL 驱动也有类似全局开关。你需要打开stm32n6xx_hal_conf.h确认这几个宏的状态#define HAL_UART_MODULE_ENABLED #define HAL_UART_EX_MODULE_ENABLED #define HAL_GPIO_MODULE_ENABLED #define HAL_DMA_MODULE_ENABLEDUSART 外设通常依赖 GPIO 和 DMA如果 DMA 的宏被注释掉某些工程配置下 UART 初始化代码会被 HAL 库的#if条件选择性编译掉。这里不是说你配置了 USART1 就万事大吉HAL 库层面的开关没打开驱动函数一样不会出现在库里。4.3 注意 TrustZone 相关影响STM32N6 是支持 TrustZone 的也就是安全区Secure和非安全区Non-Secure两个世界。如果你的工程启用了 TrustZone事情会变得更有意思。在 TrustZone 工程里CubeMX 会生成独立的 Secure 和 Non-Secure 两个工程。MX_USART1_UART_Init如果被规划在 Secure 工程里定义而你的 Neural-ART 应用被规划在 Non-Secure 工程里链接的时候两边隔离Non-Secure 工程自然是找不到这个符号的。这个时候要在 CubeMX 的 TrustZone 配置界面里确认 USART1 分配到哪个执行区域。如果你想在非安全区里也能用 USART1那要么把外设配置放到非安全工程里生成要么在 Secure 工程里通过 IPC 机制暴露一个安全调用接口。但普通调试场景下最简单的做法就是关闭 TrustZone裸跑搞定问题再说。这个坑我本来没打算细讲但 STM32N6 毕竟是带这个功能的芯片很多新手从 CubeMX 默认模板建工程一不小心就开了 TrustZone。后面你会多出一堆没必要的麻烦。如果只是想跑通 Neural-ART 的 tensor flow 示例我建议先关闭 TrustZone等整套流程验证通过后再考虑安全分区。4.4 检查链接脚本和内存布局还有一个不太容易联想到的维度链接脚本。STM32N6 的 RAM 空间分了好几个块有些工程里 NPU 用到的内存区域和 MCU 应用区域是分开管理的。Neural-ART 生成的模型文件比较大会占用大量内存如果链接脚本里的 RAM 区域划分和实际硬件不匹配链接器在链接过程中可能会因为地址溢出跳过部分符号解析产生诡异的错误。虽然这个概率相对较小但如果你折腾了半天还搞不定不妨看一下.ld文件GCC或者.sct文件Keil里的内存段配置。尤其是 Neurl-ART 的示例工程和你的具体芯片型号不完全匹配时内存大小差异会导致链接错乱。5. 速查清单以后遇到这个问题直接照着排排查步骤操作方式核心验证点1. 看构建日志定位引用方查找报错代码里的文件路径明确是哪个文件调用了MX_USART1_UART_Init2. 全局搜函数声明和定义在工程管理器里搜索函数名确认声明和定义是否存在、拼写是否一致3. 检查源文件是否在构建列表查看 CMakeLists.txt / .cproject / Keil工程树确保定义所在文件参与编译4. 检查条件编译宏搜索#ifdef/#if块检查 HAL 模块开关确认函数实体没有被预处理剔除5. 检查 TrustZone 配置CubeMX 里查看安全属性确认外设在当前执行区域可见6. 检查链接脚本内存布局查看 .ld / .sct 文件确认内存区域定义正确、无溢出风险7. 临时方案把函数定义复制到 main.c 的 USER CODE 段绕开构建系统性问题先跑通流程这个清单的排序是有讲究的。前两步是快速定位成本最低中间三步是从编译到链接的完整链路覆盖面最广最后两步是兜底方案和临时方案。一般到第 4 步就能解决九成问题我自己那次是折在第 3 步和第 5 步的组合拳里。6. 给你几个“过来人”的避坑建议再说几个不在报错直接原因里、但能显著降低你踩坑概率的点。第一STM32N6 的 Neural-ART 教程工程基本是面向“你已经会用 STM32CubeMX 和一块有基础调试能力的板卡”来设计的。如果你是直接从 Arduino 或者其他平台切过来的建议先单独创建一个不带 Neural-ART 的纯 CubeMX 工程确认 GPIO、UART、LED 这些都能正常工作再引入 Neural-ART 组件。这样可以把问题的边界控制在一个很小的范围内排查起来清晰很多。第二重视编译日志的完整输出。IDE 底部显示的红色报错只是冰山一角前面那些黄色警告、后面的链接器详细输出往往藏着真正的线索。遇到链接错误第一要务就是把日志完整复制下来搜索 undefined reference 前面那个文件名那基本上就是问题的起点。第三善用nm这类符号检查工具。不要只看代码要直接观察.o文件的符号表。尤其在那些“文件引用了但工程没编译”的诡异场景下nm能帮你在一分钟内确认到底哪个目标文件没导出该符号比盲改代码高效得多。我个人在实际操作中的体会是这种报错不像语法错误那样一眼就能看出来它更多地考验你对工程构建机制的理解。只要按照“编译 → 链接 → 配置”这个顺序一层层去验证绝大多数 undefined reference 都能在一个小时内解决。我的那次排查最后倒是没改代码就是把 CMakeLists 里的源文件列表补全顺便在 main.c 里加了一个备用函数兜底从此再没出过幺蛾子。
返回列表