☰
SCons构建STM32F103嵌入式工程:从Keil迁移的声明式实践
2026/10/4 8:30:48 网站建设 项目流程

1. 为什么是 SCons,而不是 Keil、CubeMX 或 CMake?

“用 SCons 编译 STM32F103 工程”——这句话乍看像一句操作指令,实则藏着一个嵌入式开发者在项目规模化、团队协作化、持续集成化过程中必然遭遇的转折点。我第一次在真实产线项目里把 Keil MDK 的 .uvprojx 文件彻底删掉,改用纯文本 SConstruct 脚本构建整个 F103 电机控制固件时,团队里两位十年 Keil 老手盯着终端里scons -Q输出的绿色[CC]和[LD]行,沉默了三分钟。不是因为看不懂,而是突然意识到:原来我们过去十年反复点击“Rebuild”所掩盖的,是编译逻辑的黑盒化、依赖关系的手动维护、跨平台移植的脆弱性,以及——最要命的——当新同事接手项目时,连“为什么 main.c 必须放在 Src 目录下”都得靠口耳相传。

SCons 不是另一个 IDE,它是一套声明式构建系统,核心哲学就一条:源码即构建逻辑。你写下的每一行 Python 脚本,都在显式定义“哪些文件参与编译”、“用什么工具链”、“头文件路径怎么展开”、“链接脚本如何注入”、“生成的 .hex/.bin 如何校验”。这和 Keil 的图形化配置(点选 Device、勾选 Use MicroLIB、手动拖拽 Group)本质不同;也和 CubeMX 生成的 Makefile(一堆自动生成、不敢轻易修改的 shell 片段)形成鲜明对比;甚至和 CMake(语法抽象层厚、新手易陷在 target_link_libraries 和 include_directories 的嵌套里)相比,SCons 的 Python 基底让逻辑更直白——比如你要给所有 .c 文件加-O2 -mthumb -mcpu=cortex-m3,直接写env.Append(CCFLAGS=['-O2', '-mthumb', '-mcpu=cortex-m3']),没有宏、没有变量作用域陷阱,就是 Python 列表追加。

为什么偏偏是 STM32F103?这个芯片太典型了:ARM Cortex-M3 内核、标准 CMSIS 库结构、ST 提供的固件库(非 HAL)、常见外设(USART1/3、TIM2/3、ADC1、DAC、SPI1)全都有,但官方工具链支持却卡在“够用但不开放”层面。Keil 编译慢?本质是每次 rebuild 都扫描整个工程目录做增量判断,而 SCons 的依赖数据库(.sconsign.dblite)基于文件内容哈希,改一行代码,只重编它和所有依赖它的 .o,连带重链接——实测某 3 万行电机 PID+FOC 固件,Keil 全量编译 48 秒,SCons 增量编译稳定在 3.2 秒内。至于“stm32f103 串口1和串口3使用差异”这类问题,SCons 更是天然解法:你在SConscript里为 USART1 定义一组宏(-DUSE_USART1),为 USART3 单独建个子环境(env_usart3 = env.Clone()),再分别指定不同初始化函数入口,编译产物自动隔离,根本不会出现“改了串口3配置,串口1莫名失效”的玄学问题。

它适合谁?不是初学者——刚学 GPIO 点灯的新人,用 Keil 新建工程点五下鼠标就能跑起来,这时候硬推 SCons 是给自己找麻烦。它适合已经用过 Keil/CubeMX、开始写多模块驱动、需要做自动化测试、准备上 CI/CD 流水线、或团队里有 Linux/Python 背景成员的进阶用户。尤其当你遇到“keil5 编译很慢?”“vs2010 编译报 error msb6006 cmd.exe 已退出,代码为3”这类 Windows 下构建环境混乱导致的错误时,SCons 的跨平台一致性(同一份脚本,在 Windows WSL、Ubuntu Docker、macOS 上运行结果完全一致)就成了救命稻草。我经手的三个量产项目,从 F103 到 L432KC,再到 G071RB,构建脚本主体逻辑没变过,只是替换了芯片定义、启动文件、链接脚本——这才是工程可维护性的起点。

2. SCons 构建体系的核心骨架与关键设计决策

SCons 构建 STM32F103 工程,绝不是简单替换 Makefile。它是一套分层明确、职责清晰的 Python 脚本体系,核心由三类文件构成:顶层SConstruct(总控)、模块级SConscript(功能划分)、工具链封装tools/(可复用能力)。这种结构不是为了炫技,而是为了解决嵌入式开发中三个致命痛点:芯片差异、外设组合爆炸、调试与发布双模式切换。

2.1 顶层 SConstruct:全局策略中枢

SConstruct是整个构建系统的“宪法”,它不负责具体编译,只做三件事:加载工具链、定义全局环境、调度子构建脚本。我坚持把它控制在 80 行以内,超过就拆。典型结构如下:

# SConstruct import os import sys # 1. 工具链路径统一管理(避免硬编码) TOOLCHAIN_ROOT = os.environ.get('ARMGCC_PATH', '/opt/gcc-arm-none-eabi') if not os.path.exists(TOOLCHAIN_ROOT): raise EnvironmentError(f"ARM GCC toolchain not found at {TOOLCHAIN_ROOT}") # 2. 创建基础构建环境(关键!) env = Environment( tools=['gcc', 'g++', 'ar', 'as', 'ld', 'objcopy', 'objdump'], toolpath=['tools'], # 指向自定义工具定义 ) # 3. 全局编译选项(所有模块继承) env.Append( CCFLAGS=[ '-mcpu=cortex-m3', '-mthumb', '-mfpu=vfp', '-mfloat-abi=soft', '-O2', '-Wall', '-Wextra', '-ffunction-sections', '-fdata-sections', '-std=gnu99', '-DSTM32F103xB', # 核心芯片定义 '-DUSE_FULL_LL_DRIVER', # 使用标准库而非 HAL ], CPPPATH=[ '#Inc', # 项目头文件根目录 '#Libraries/CMSIS/Include', '#Libraries/STM32F1xx_HAL_Driver/Inc', '#Libraries/STM32F1xx_HAL_Driver/Inc/Legacy', ], LIBPATH=['#Libraries/STM32F1xx_HAL_Driver/Lib'], LIBS=['stm32f1xx_hal'], ) # 4. 关键:按芯片型号动态加载链接脚本 chip_variant = ARGUMENTS.get('chip', 'stm32f103c8t6') link_script = f'Linker/{chip_variant}.ld' if not os.path.exists(link_script): raise ValueError(f"Linker script {link_script} not found") env.Replace(LINKFLAGS=[f'-T{link_script}', '--specs=nosys.specs']) # 5. 调度子构建(这才是业务逻辑所在) SConscript('Src/SConscript', exports='env') SConscript('Drivers/SConscript', exports='env') SConscript('Middlewares/SConscript', exports='env')

这里每个设计都有深意:toolpath=['tools']让你能覆盖 SCons 默认的 gcc 工具定义,比如强制arm-none-eabi-gcc路径;CPPPATH用#开头表示相对于项目根目录,避免相对路径错误;LINKFLAGS中的--specs=nosys.specs是嵌入式关键——它剥离了 libc 中对系统调用(如_sbrk,_write)的依赖,否则链接会失败;而ARGUMENTS.get('chip', 'stm32f103c8t6')支持命令行传参scons chip=stm32f103zet6,瞬间切换不同 Flash/RAM 规格的板子,比 Keil 里手动改 Device 设置快十倍。

2.2 模块化 SConscript:应对外设组合爆炸

STM32F103 的外设不是孤立的。USART1 依赖 RCC 使能、GPIOA 初始化;TIM2 捕获需要 AFIO 重映射配置;DAC 输出正弦波得配 TIM6 触发。如果所有代码堆在main.c里,改一个外设就得通读三千行。SCons 的SConscript就是为解耦而生。以Drivers/SConscript为例:

# Drivers/SConscript Import('env') # 创建独立环境,避免污染全局 driver_env = env.Clone() # 为特定外设添加专属编译选项 usart1_env = driver_env.Clone() usart1_env.Append(CPPDEFINES=['USE_USART1']) usart1_env.Append(CPPPATH=['#Drivers/USART1']) tim2_env = driver_env.Clone() tim2_env.Append(CPPDEFINES=['USE_TIM2_CAPTURE']) tim2_env.Append(CPPPATH=['#Drivers/TIM2']) # 分别编译,生成独立 .o usart1_obj = usart1_env.Object('#Drivers/USART1/usart1_driver.c') tim2_obj = tim2_env.Object('#Drivers/TIM2/tim2_capture.c') # 返回对象供主环境链接 Return('usart1_obj tim2_obj')

看到没?usart1_env和tim2_env是两个克隆环境,它们的CPPDEFINES互不影响。当你在main.c里写#ifdef USE_USART1时,只有usart1_obj被编译进去;同理,#ifdef USE_TIM2_CAPTURE只影响tim2_obj。这直接解决了“stm32f103 多路捕获”场景下,不同通道(CH1/CH2/CH3)配置冲突的问题——你完全可以为 CH1 写tim2_ch1.c,为 CH2 写tim2_ch2.c,各自在SConscript里定义不同宏,编译时按需链接,零耦合。而 Keil 里实现同样效果,得建多个 Target,每个 Target 复制一整套配置,维护成本指数级上升。

2.3 自定义 Tools:掌控底层构建细节

SCons 默认的gcc工具链对嵌入式不够友好。比如objcopy生成.bin时,默认从0x08000000开始,但 F103 的实际 Flash 起始地址是0x08000000,而某些 Bootloader 要求.bin从0x08002000开始(跳过 8KB 用户区)。这时就得自己写tools/arm_gcc.py:

# tools/arm_gcc.py from SCons.Tool import gcc from SCons.Builder import Builder def generate(env, **kw): # 继承默认 gcc 工具 gcc.generate(env) # 替换 objcopy 构建器,支持 offset 参数 env['OBJCOPY'] = 'arm-none-eabi-objcopy' env['OBJCOPYFLAGS'] = ['-O', 'binary'] env['OBJCOPYCOM'] = '$OBJCOPY $OBJCOPYFLAGS $SOURCE $TARGET' # 新增 bin_with_offset 构建器 env.Append(BUILDERS={ 'BinWithOffset': Builder( action='$OBJCOPY $OBJCOPYFLAGS --adjust-section-vma .text=$OFFSET $SOURCE $TARGET', suffix='.bin', src_suffix='.elf' ) }) def exists(env): return True

然后在SConstruct里调用:

env.Tool('arm_gcc') # 加载自定义工具 firmware_bin = env.BinWithOffset('firmware.bin', firmware_elf, OFFSET='0x08002000')

这样生成的firmware.bin就自动偏移了 8KB,烧录到 Bootloader 后能精准跳转。这种细粒度控制,是 Keil 的“Output -> Create HEX File”选项框永远做不到的。

3. 实操全流程:从零搭建可运行的 F103 SCons 工程

现在,我们动手搭一个最小可行工程。目标:编译出能在 STM32F103C8T6(Blue Pill 板)上跑的 LED 闪烁固件,支持scons编译、scons flash烧录、scons clean清理。全程不依赖 Keil、CubeMX 或任何 GUI 工具,所有文件用 VS Code 手动创建。

3.1 项目目录结构与文件准备

先建立清晰目录树(这是 SCons 可维护性的基石):

stm32f103-scons/ ├── SConstruct # 总控脚本 ├── SConscript # (可选)根目录构建入口 ├── tools/ │ └── arm_gcc.py # 自定义工具链 ├── Inc/ │ ├── main.h │ ├── stm32f103xb.h # CMSIS 标准头文件 │ └── stm32f1xx_hal_conf.h # HAL 库配置 ├── Src/ │ ├── main.c # 主程序 │ ├── system_stm32f1xx.c # 系统时钟初始化 │ └── startup_stm32f103xb.s # 启动文件(汇编) ├── Libraries/ │ ├── CMSIS/ # 下载自 ST 官网 │ └── STM32F1xx_HAL_Driver/ # 同上 ├── Linker/ │ └── stm32f103c8t6.ld # 链接脚本(关键!) └── scripts/ └── openocd_flash.tcl # OpenOCD 烧录脚本

提示:startup_stm32f103xb.s和system_stm32f1xx.c必须从 ST 官方固件库(STM32CubeF1)中提取,不能自己写。CMSIS 和 HAL Driver 库建议用 1.8.4 版本,兼容性最好。链接脚本stm32f103c8t6.ld是核心,它定义了 Flash(128KB)、RAM(20KB)的起始地址和内存布局。我提供一个精简版:

/* Linker Script for STM32F103C8T6 */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .isr_vector : { *(.isr_vector) } > FLASH .text : { *(.text) *(.rodata) } > FLASH .data : { *(.data) } > RAM AT > FLASH .bss : { *(.bss) *(COMMON) } > RAM }

3.2 编写核心源码:让 LED 闪起来

Src/main.c是灵魂,必须包含标准 HAL 初始化流程:

#include "main.h" #include "stm32f1xx_hal.h" // 全局 HAL 句柄 UART_HandleTypeDef huart1; TIM_HandleTypeDef htim2; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); static void MX_TIM2_Init(void); int main(void) { HAL_Init(); // HAL 库初始化 SystemClock_Config(); // 系统时钟配置(72MHz) MX_GPIO_Init(); // PA0 输出,接 LED MX_USART1_UART_Init(); // 串口1 初始化 MX_TIM2_Init(); // TIM2 初始化(用于延时) while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); // 翻转 PA0 HAL_Delay(500); // 500ms 延时 } } void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; __HAL_RCC_HSE_CONFIG(RCC_HSE_ON); while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) == RESET) {} RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // HSE*9 = 72MHz if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } static void MX_GPIO_Init(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } } static void MX_TIM2_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 7199; // 72MHz / (7199+1) = 10kHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 9999; // 10kHz / 10000 = 1Hz htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } } void Error_Handler(void) { while(1) {} }

注意:HAL_Delay()依赖 SysTick 中断,所以system_stm32f1xx.c里的SystemCoreClock必须正确设置为 72000000,且HAL_Init()会自动配置 SysTick。这段代码在 Keil 里能跑,在 SCons 里一样能跑——因为 SCons 只管编译链接,不管代码逻辑。

3.3 SConstruct 与 SConscript 的完整实现

SConstruct(已精简,专注可运行):

import os import sys # 工具链路径(根据你的安装调整) ARMGCC_PATH = '/opt/gcc-arm-none-eabi-10-2020-q4-major/bin' if not os.path.exists(ARMGCC_PATH): ARMGCC_PATH = 'C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2020-q4-major/bin' env = Environment( tools=['gcc', 'g++', 'ar', 'as', 'ld', 'objcopy', 'objdump'], toolpath=['tools'], ENV={'PATH': ARMGCC_PATH} ) # 全局编译选项 env.Append( CCFLAGS=[ '-mcpu=cortex-m3', '-mthumb', '-O2', '-Wall', '-Wextra', '-ffunction-sections', '-fdata-sections', '-std=gnu99', '-DSTM32F103xB', '-DUSE_FULL_LL_DRIVER', '-IInc', '-ILibraries/CMSIS/Include', '-ILibraries/STM32F1xx_HAL_Driver/Inc' ], LINKFLAGS=[ '-TLinker/stm32f103c8t6.ld', '--specs=nosys.specs', '-Wl,--gc-sections' ], LIBS=['stm32f1xx_hal'], LIBPATH=['Libraries/STM32F1xx_HAL_Driver/Lib'] ) # 定义构建目标 src_files = Glob('Src/*.c') + Glob('Src/*.s') obj_files = env.Object(src_files) # 链接生成 .elf firmware_elf = env.Program('firmware.elf', obj_files) # 生成 .hex 和 .bin firmware_hex = env.Hex('firmware.hex', firmware_elf) firmware_bin = env.Bin('firmware.bin', firmware_elf) # 自定义烧录命令(依赖 OpenOCD) env.Command('flash', firmware_bin, 'openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program $SOURCE verify reset exit"') # 清理命令 env.Clean('flash', ['firmware.elf', 'firmware.hex', 'firmware.bin']) # 默认目标 Default(firmware_hex)

tools/arm_gcc.py(简化版,仅处理 hex/bin 生成):

from SCons.Tool import gcc from SCons.Builder import Builder def generate(env, **kw): gcc.generate(env) env['HEXCOM'] = '$OBJCOPY -O ihex $SOURCE $TARGET' env['BINCOM'] = '$OBJCOPY -O binary $SOURCE $TARGET' env.Append(BUILDERS={ 'Hex': Builder(action='$HEXCOM', suffix='.hex', src_suffix='.elf'), 'Bin': Builder(action='$BINCOM', suffix='.bin', src_suffix='.elf') }) def exists(env): return True

3.4 编译、烧录与验证

一切就绪,打开终端(Windows 推荐 Git Bash 或 WSL,Linux/macOS 直接 terminal):

# 第一步:安装 SCons(Python 3.7+) pip install scons # 第二步:确保 ARM GCC 工具链在 PATH 中 export PATH="/opt/gcc-arm-none-eabi-10-2020-q4-major/bin:$PATH" # Linux/macOS # 或 Windows:在系统环境变量中添加路径 # 第三步:编译(-Q 参数静默输出,只显示关键步骤) scons -Q # 成功输出应类似: # [CC] Src/main.c # [CC] Src/system_stm32f1xx.c # [AS] Src/startup_stm32f103xb.s # [AR] Libraries/STM32F1xx_HAL_Driver/Lib/libstm32f1xx_hal.a # [LD] firmware.elf # [HEX] firmware.hex # [BIN] firmware.bin # 第四步:烧录(需连接 ST-Link/V2) scons flash # OpenOCD 输出应显示: # Info : STLINK V2J37S7 (API v2) VID:PID 0483:3748 # Info : Target voltage: 3.221442 # Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints # Info : starting download # Info : flash programming... # verified 12345 bytes in 0.234s (51.52 KiB/s) # Info : Resetting target # Info : halted: PC: 0x080001a0

此时 Blue Pill 板上的 LED(通常接 PA0)应以 1Hz 频率稳定闪烁。用串口助手(如 XCOM)连接 USART1(PA9/PA10),波特率 115200,能看到 HAL 库初始化日志(如果加了printf)。整个过程,没有点击、没有弹窗、没有许可证提示——只有命令行和结果。

4. 常见问题排查与实战避坑指南

用 SCons 编译 STM32F103,最大的陷阱不是语法错误,而是环境隐含假设被打破。我踩过的坑,基本都围绕“路径”“符号”“链接”三大雷区。下面按发生频率排序,附真实日志和解决方案。

4.1 “undefined reference to__libc_init_array” —— libc 与裸机的战争

现象:scons编译通过,链接时报错:

arm-none-eabi-gcc: error: unrecognized command line option '-nostdlib' /usr/lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld: firmware.elf: in function `_start': (.text+0x10): undefined reference to `__libc_init_array' collect2: error: ld returned 1 exit status

原因:SCons 默认链接时会尝试链接标准 C 库(libc),但嵌入式裸机程序不需要它。-nostdlib参数没生效,或者--specs=nosys.specs被忽略。

排查步骤:

  1. 运行scons -n -Q(-n表示只打印命令不执行),查看实际链接命令:
    arm-none-eabi-gcc -o firmware.elf ... -TLinker/stm32f103c8t6.ld --specs=nosys.specs ...
  2. 如果命令里没有--specs=nosys.specs,检查SConstruct中LINKFLAGS是否被后续代码覆盖。
  3. 如果存在,但依然报错,说明nosys.specs文件缺失。该文件位于 GCC 工具链的arm-none-eabi/lib/gcc/arm-none-eabi/<version>/目录下。

终极方案:在SConstruct中显式指定 specs 路径:

gcc_path = '/opt/gcc-arm-none-eabi-10-2020-q4-major' env.Replace(LINKFLAGS=[ f'-TLinker/stm32f103c8t6.ld', f'--specs={gcc_path}/arm-none-eabi/lib/gcc/arm-none-eabi/10.2.1/specs', '-Wl,--gc-sections' ])

实操心得:我曾因 Ubuntu 系统自带的gcc-arm-none-eabi包版本太老(4.9),nosys.specs不存在,死磕三天。最终卸载系统包,从 ARM 官网下载gcc-arm-none-eabi-10-2020-q4-major解压使用,问题消失。记住:嵌入式工具链,宁可手动管理,不要信包管理器。

4.2 “fatal error: stm32f1xx_hal.h: No such file or directory” —— 头文件路径的幻影

现象:编译main.c时找不到 HAL 头文件,但ls Libraries/STM32F1xx_HAL_Driver/Inc/确实存在stm32f1xx_hal.h。

原因:CPPPATH路径错误。常见错误有三:

  • 用了相对路径./Libraries/...,而 SCons 的#表示项目根目录;
  • CPPPATH中路径拼写错误,比如STM32F1xx_HAL_Driver/Incl(少了个 u);
  • Glob('Src/*.c')匹配到了Src/subdir/main.c,但CPPPATH没包含#Src/subdir。

快速验证法:在main.c顶部加一行:

#pragma message "HAL include path test" #include "stm32f1xx_hal.h"

如果编译时看到HAL include path test,说明头文件找到了;否则,路径必错。

解决方案:统一用#前缀,并用os.path.join拼接(防 Windows 路径斜杠问题):

import os env.Append(CPPPATH=[ '#Inc', os.path.join('#Libraries', 'CMSIS', 'Include'), os.path.join('#Libraries', 'STM32F1xx_HAL_Driver', 'Inc'), ])

4.3 “section.isr_vectorwill not fit in regionFLASH” —— 链接脚本的容量陷阱

现象:链接时报错,提示.isr_vector段放不下:

arm-none-eabi-gcc: error: ld returned 1 exit status /opt/gcc-arm-none-eabi-10-2020-q4-major/arm-none-eabi/bin/ld: firmware.elf section `.isr_vector' will not fit in region `FLASH'

原因:.isr_vector是中断向量表,固定占 256 字节(64 个 4 字节指针)。但链接脚本里FLASH区域长度设小了,或者startup_stm32f103xb.s里向量表定义超长。

排查:

  1. 检查stm32f103c8t6.ld中LENGTH = 128K是否正确(C8T6 是 128KB Flash);
  2. 用arm-none-eabi-objdump -h firmware.elf查看各段大小:
    Sections: Idx Name Size VMA LMA File off Algn 0 .isr_vector 00000100 08000000 08000000 00010000 2**0 1 .text 00002a3c 08000100 08000100 00010100 2**0
    如果.isr_vector的VMA(虚拟地址)不是0x08000000,说明链接脚本没生效。

修复:确保链接脚本第一行是MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K },且SECTIONS中.isr_vector : { *(.isr_vector) } > FLASH在最前面。

4.4 “scons: *** [firmware.elf] SourceSrc/startup_stm32f103xb.snot found” —— 汇编文件的后缀玄学

现象:SCons 报错找不到启动文件,但文件明明存在,且ls Src/能看到。

原因:SCons 默认不识别.s后缀为汇编源文件。它只认.S(大写)或.asm。

解决方案:两种方法任选其一:

  • 把startup_stm32f103xb.s重命名为startup_stm32f103xb.S(Linux/macOS 区分大小写,Windows 不区分,但 SCons 统一按 Unix 规则);
  • 在SConstruct中显式告诉 SCons.s是汇编:
    env.Append(ASFLAGS=['-x', 'assembler-with-cpp']) # 允许 .s 文件预处理 env['ASCOM'] = '$CC $ASFLAGS -c -o $TARGET $SOURCE' # 用 gcc 处理 .s

4.5 “OpenOCD: JTAG scan chain interrogation failed” —— 烧录失败的物理层真相

现象:scons flash执行后,OpenOCD 报错无法连接芯片:

Error: JTAG scan chain interrogation failed: all zeroes Error: Check JTAG interface, cables and power

这不是 SCons 的问题,但常被误认为构建失败。真实原因有:

  • ST-Link 驱动未安装(Windows 需装 STSW-LINK009);
  • 板子未上电(Blue Pill 的 3.3V 灯没亮);
  • SWD 引脚(SWCLK/SWDIO)接触不良(检查杜邦线是否松动);
  • BOOT0 引脚被拉高(F103 进入系统存储器启动模式,无法烧录)。

速查清单:

  • 用万用表测BOOT0对地电压,应为 0V(低电平);
  • 拔掉 USB 线,短接BOOT0和3.3V,再插 USB,LED 应快闪(进入 Bootloader 模式);此时BOOT0=1,正常工作时BOOT0=0;
  • 换一根质量好的 USB 线(劣质线供电不足,ST-Link 无法识别)。

注意事项:我见过最诡异的一次,是 USB 扩展坞供电不足,导致 ST-Link 电压跌到 2.8V,OpenOCD 一直报错。直接插主板 USB 口,秒好。硬件问题,永远先查供电和物理连接。

5. 进阶技巧:让 SCons 工程真正工业级可用

做到“能编译、能烧录”,只是 SCons 的入门。真正的价值,在于它能把嵌入式开发从“手工作坊”推向“现代工程”。以下是我在线上项目中验证过的五个进阶技巧

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询