刚接触RT-Thread那会儿,我对它的构建与配置系统是又爱又恨。爱的是,换芯片、加组件、切工具链,几乎所有操作都能用两条命令搞定;恨的是,scons、menuconfig、rtconfig.h、SConscript这些名词叠在一起,前期光搞清楚谁在什么时候干什么,就够喝一壶的了。后来我总算把这条链路捋顺了:RT-Thread在“配置”这件事上用了Kconfig这一套,在“构建”这件事上用了SCons这一套,两套系统各自管好各自的地盘,然后通过rtconfig.h这个桥梁接在一起。这篇文章我把整条链路从原理到实操完整拆一遍,对象是刚开始用RT-Thread、或者已经在用但经常被编译/配置问题卡住的人。读完你应该能理解每个文件存在的意义,并且能自如地给自己加驱动、开组件、生成MDK/IAR工程。
1. 先把两套系统分开看:配置管“有没有”,构建管“怎么编”
1.1 为什么RT-Thread不走“点鼠标建工程”的老路
很多人刚从裸机开发切过来,第一反应是:我能不能像Keil里那样,右键Add Existing Files把.c文件加进去,然后在Options里手动加几个宏定义?在RT-Thread上,这套思路基本走不通,不是因为不能做,而是因为做不过来。
RT-Thread要覆盖ARM、RISC-V、MIPS等不同架构,光官方BSP就几百个,组件库更是有几百个软件包。如果每个BSP都靠人工维护一份工程文件清单,组件一多、芯片一换,维护成本立刻爆炸。所以它采用了一个更工程化的思路:配置系统决定编译哪些模块,构建系统决定怎么编译这些模块。
我用一个自助餐厅的比喻来理解这套结构:
- Kconfig文件是餐厅的菜单,声明了“有哪些菜可以点”。
- menuconfig是点餐平板,你在这里勾选需要的菜。
- rtconfig.h是后厨的备料单,告诉所有厨师哪些食材要处理。
- SConscript是每个窗口的菜谱,告诉SCons具体加工哪些文件。
- SConstruct是总厨师长,负责统筹整个出餐流程。
你在menuconfig上勾选,菜单选项落盘成.config,同步生成rtconfig.h里的宏;SCons在构建时读rtconfig.h,知道哪些功能开关打开了,再通过各个子目录的SConscript把对应的源文件收进来编译。整个链路里,配置管“有没有”,构建管“怎么编”,两者分工非常清晰。
1.2 一次scons命令背后的完整旅程
弄清楚每个文件的定位之后,再来看一次scons命令实际发生了什么。你在BSP目录下敲下scons,SCons会先加载当前目录下的SConstruct。这个文件做的事很模式化:拼接出RT-Thread源码根目录RTT_ROOT,把tools目录加入Python模块搜索路径,导入rtconfig.py拿到工具链信息,然后建立SCons的Environment对象,最后调用构建库里的PrepareBuilding去递归加载所有SConscript。
rtconfig.py里装着的是CROSS_TOOL、EXEC_PATH、PREFIX、CFLAGS、LFLAGS这些工具链和编译参数。SCons靠它们找到编译器、确定优化选项和链接脚本。然后从顶层SConscript开始,一层一层往下走,每个子目录的SConscript通过DefineGroup把自己的源文件、头文件路径、依赖宏声明进去。如果某组依赖的宏在rtconfig.h里不存在,这组文件就直接被跳过,不参与编译。最后SCons统一编译所有被收集的源文件,再链接生成目标固件。GCC工具链默认产出rtthread.elf和rtthread.bin,Keil产出.axf,IAR产出.out或.hex。
这套流程和传统Makefile最大的区别,在于SCons会自动做依赖分析。Makefile里你经常要写清楚每个目标依赖哪些头文件,SCons会在编译时自动监控头文件变化,增量编译的准确性高很多。我自己的体验是,在组件多、文件几百个的工程里,SCons的增量构建比手写Makefile省心得多,基本不用担心“改了头文件但目标没重新编译”这种玄学问题。
1.3 rtconfig.h和rtconfig.py,一字之差别搞混
这两个文件名字就差一个后缀,位置也都在BSP根目录,非常容易搞混,但它们的定位完全不同。
rtconfig.h是C语言头文件,里面的内容长这样:
#define RT_USING_RT_SIZE_TYPE_LONG 1 #define RT_USING_COMPONENTS_INIT 1 #define RT_USING_USER_MAIN 1 #define RT_USING_DEVICE 1 #define RT_USING_SERIAL 1C源码在编译时通过#ifdef判断这些宏是否存在,从而决定要不要编译某段代码。所以rtconfig.h本质上是“后厨备料单”,告诉所有源码文件哪些功能开了。
rtconfig.py是Python脚本,里面是这种变量:
CROSS_TOOL = 'gcc' EXEC_PATH = 'C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2021.10/bin' PREFIX = 'arm-none-eabi-' CFLAGS = ['-mcpu=cortex-m4', '-mthumb', '-Os', '-Wall']SCons靠它决定用什么编译器、什么参数去编译。所以rtconfig.py是“厨房设备清单”,告诉构建系统用哪套锅碗瓢盆。
这里有一条黄金法则:永久生效的配置改动,一定在menuconfig里做,不要手改rtconfig.h。因为menuconfig每次保存都会重新生成rtconfig.h,你手改的内容会被覆盖。临时验证某个宏可以手改,但心里要清楚它活不过下一次配置。同理,换工具链要改rtconfig.py,而不是去翻菜单,menuconfig管不到编译器路径。
2. Kconfig链路:一个菜单选项是如何钻进rtconfig.h的
2.1 Kconfig文件的组织方式:从BSP顶层一路source下去
Kconfig文件本身是文本文件,描述“有哪些可配置项、类型是什么、默认值多少、依赖什么条件”。它不像Makefile要按顺序执行,而是通过source关键字把树状的配置定义串起来。
通常每个BSP目录下有一个顶层Kconfig,内容大致长这样:
mainmenu "RT-Thread Configuration" source "lib/Kconfig" source "$RTT_DIR/Kconfig" source "../libraries/HAL_Drivers/Kconfig" config BSP_DIR string option env="BSP_ROOT"这个文件先定义主菜单名,然后通过source把RT-Thread内核的Kconfig、驱动框架的Kconfig、芯片厂商库的Board级Kconfig全部引进来。各驱动子目录也可以有自己独立的Kconfig,再被上层source进来。这样设计的好处是,每个驱动模块自己维护自己的菜单项,不需要在一个巨大的配置文件里堆几千行。
你启动menuconfig时,工具会从当前目录的Kconfig开始,把整个source链展开成一棵可交互的菜单树。
2.2 最常用的几个Kconfig关键字,够用就行
Kconfig语法本身挺丰富,但日常打交道的就那几个。我把含义列出来:
| 关键字 | 作用 | 例子 |
|---|---|---|
config | 声明一个配置项 | config BSP_USING_MY_SENSOR |
bool | 配置项类型:布尔开关 | bool "Enable MY-SENSOR" |
default | 默认值 | default y |
depends on | 依赖条件,不满足时菜单项灰色或不可见 | depends on RT_USING_ADC |
select | 选中本项时强制选中另一项 | select RT_USING_ADC |
choice | 一组单选选项 | choice...endchoice |
help | 菜单里的帮助文本 | help后跟说明 |
给一个实际例子。假设我要给板子加一个自定义传感器驱动,在驱动目录的Kconfig里写:
config BSP_USING_MY_SENSOR bool "Enable MY-SENSOR driver" default y select RT_USING_ADC help MY-SENSOR is an analog output sensor, need ADC to read.这段配置的意思是:菜单里出现一个“Enable MY-SENSOR driver”的开关,默认打开;一旦打开,自动把RT_USING_ADC这个宏也选中。这里select特别有用,它能在用户不知道有依赖的情况下自动把依赖项带上。但select要慎用,它属于强制拉依赖,如果被拉的项本身又有depends on约束,可能产生矛盾,我对select的使用原则是:只拉那些“没有依赖前提”的基础配置项。
2.3 从menuconfig保存到rtconfig.h的同步过程
在Env工具里执行menuconfig,进入图形化菜单。你在里面勾选、取消、保存退出时,工具会做三件事:把选项写入.config文件、刷新rtconfig.h、把菜单中文注释处理好。所以Env里保存退出后,不用手动跑任何同步脚本。
假设我之前打开“Enable MY-SENSOR driver”,保存后去看BSP目录下的rtconfig.h,能看到多出两行:
#define BSP_USING_MY_SENSOR 1 #define RT_USING_ADC 1同时.config文件里也记录了这个选择。.config是menuconfig的“记忆文件”,下次再打开menuconfig时会恢复你上次的选择;它还供pkgs --update读取,判断要下载哪些在线包。
这里有个高频坑:menuconfig一定要在BSP目录下运行,不要在rt-thread源码根目录运行。根目录的Kconfig读不到你当前板子的板级配置,你配出来的东西和实际BSP对不上。我见过几次“我明明开了某个驱动,编译却说宏未定义”,最后发现是在根目录跑了menuconfig,把配置写到了别的地方。
2.4 依赖关系是怎么让菜单项“灰掉”的
用过menuconfig的朋友一定遇到过:某个菜单项是灰色的,怎么都选不了。这通常就是depends on在起作用。比如在线包里的某个网络库,它的Kconfig里写了depends on RT_USING_LWIP,当你在内核配置里没打开LWIP时,这个网络库的菜单项就不可选。
理解这一点对排查配置问题很有帮助。我在配置在线包时,习惯先去它的Kconfig里看一眼依赖了什么宏,再去上层菜单把这些宏打开。不是所有依赖都有图形提示,有些包的Kconfig写得比较糙,依赖写得不直观,直接去看Kconfig是最快的方式。Kconfig文件一般在包源码根目录,用文本编辑器打开搜depends on就行。
3. SConscript与SCons的配合:文件是怎样被“收编”进工程的
3.1 SConstruct是入口,rtconfig.py是参数表
整个构建流程的入口是BSP目录下的SConstruct。SCons读这个文件的顺序,就像Python执行脚本一样逐行跑。SConstruct里会拼接出RTT_ROOT路径,把RT-Thread构建库所在的tools目录加进sys.path,然后import rtconfig读取工具链配置,最终调用PrepareBuilding。
这部分我调整得不多,因为官方模板已经非常成熟。真正需要按项目定制的是rtconfig.py。下表是里面对我来说最重要的几个字段:
| 字段 | 含义 | 举例 |
|---|---|---|
ARCH | 处理器架构 | 'arm'、'risc-v' |
CPU | 具体内核型号 | 'cortex-m4' |
CROSS_TOOL | 工具链类型 | 'gcc'、'keil'、'iar' |
EXEC_PATH | 工具链安装路径 | 'C:/Keil_v5/ARM' |
PREFIX | 编译器前缀 | 'arm-none-eabi-' |
CFLAGS | C编译参数 | ['-mcpu=cortex-m4', '-Os'] |
LFLAGS | 链接参数 | ['-T', 'link.lds'] |
TARGET_EXT | 输出文件后缀 | 'bin'、'elf'、'axf' |
换工具链、换优化等级,都是改这个文件。改完之后最好scons -c清理一遍再编译,否则旧的编译产物可能残留。
3.2 一个标准SConscript模板,逐行读给你听
每个参与构建的目录下都应该有一个SConscript,它告诉SCons这个目录里有哪些源文件、头文件路径在哪、依赖什么配置宏。RT-Thread的模板非常固定,看一个例子:
from building import * cwd = GetCurrentDir() src = Glob('*.c') CPPPATH = [cwd] group = DefineGroup('MySensor', src, depend=['BSP_USING_MY_SENSOR'], CPPPATH=CPPPATH) Return('group')逐行解释:
from building import *:导入RT-Thread的构建辅助函数,DefineGroup、GetCurrentDir、Glob都来自这里。cwd = GetCurrentDir():获取当前目录路径。src = Glob('*.c'):自动收集当前目录下所有.c文件。这是SCons和Keil工程最大的不同,新增.c文件后不需要在列表里加一行,下次编译自动带上。CPPPATH = [cwd]:把当前目录加入头文件搜索路径。如果这个目录里的.c文件使用了同目录下的自定义头文件,没有这一行,编译会报“No such file or directory”。DefineGroup(...):把所有信息打包成一组。第一个参数是组名,会用在Keil/IAR工程的分组显示里;第二个是源文件列表;第三个是依赖宏,如果BSP_USING_MY_SENSOR不在rtconfig.h里,整个组都会被跳过。
这里最关键的是depend参数。它把配置系统和构建系统连接了起来:配置系统负责决定宏是否存在,构建系统根据宏是否存在决定要不要编译这些文件。如果没有depend或写成空字符串[''],则代表无条件编译。
3.3 DefineGroup和IDE工程是怎么联动的
DefineGroup不只是给SCons内部用的。当你执行scons --target=mdk5时,RT-Thread会根据所有组的信息生成一个全新的Keil MDK工程文件project.uvprojx。DefineGroup的第一个参数会变成Keil工程里的分组名,源文件按组归到对应分组下。
这意味着什么呢?意味着你完全不需要在Keil里手动添加文件。在menuconfig里打开一个新组件,编译没问题,但Keil工程里看不到这个组件的源码,怎么办?回到BSP目录,执行scons --target=mdk5重新生成工程,再打开Keil,新组件已经躺在对应分组里了。
IAR工程对应的是scons --target=iar,Visual Studio工程对应scons --target=vs。VS工程一般是拿来看代码和做静态分析的,编译还是回到命令行。
所以标准的工作流是:
- 在BSP目录执行
menuconfig,配置功能。 - 如果有在线包,执行
pkgs --update拉取源码。 - 执行
scons -j8命令行编译,验证编译通过。 - 如果要用IDE调试,执行
scons --target=mdk5重新生成Keil工程。 - 打开Keil编译并下载调试。
这一步的顺序经常有人搞反:先打开Keil编译,发现文件缺失,然后去menuconfig里一顿乱开,也不回来重新生成工程,最后越搞越乱。按上面的顺序走,基本不会出问题。
3.4 查看构建过程的三板斧
SCons的默认输出比较省:每个文件编译时只显示一个短文件名加.o。想要看到完整编译命令,用:
scons --verbose加了--verbose之后,每条命令都会完整打出来,方便检查CFLAGS有没有生效、头文件路径对不对、工具链路径是不是你期望的那个。
想要并行编译加快速度用-j参数:
scons -j8但报错排查时不建议加-j。并行编译时错误信息会交错在一起,本来挺好定位的报错可能看得一头雾水。我一般先不加-j编一次定位问题,确认无误后再-j全量编译。
清理编译产物用:
scons -c修改了rtconfig.py里的工具链路径、优化选项,或者从GCC切换到Keil之后,一定要先scons -c再重新编译。旧的目标文件用的是旧参数,不清理的话可能出现各种莫名其妙的链接问题。
4. 实战走一遍:从menuconfig选择到IDE工程生成
4.1 准备工作:一个干净的BSP环境
拿一个常见的STM32F407 BSP举例,假设路径是bsp/stm32/stm32f407-atk-explorer。如果你拿的是别的BSP,操作逻辑完全一样,只是菜单路径略有差异。
第一步,确认项目路径里没有中文和空格。SCons在Windows下对路径很敏感,中文路径和带空格的路径会引发各种奇怪问题,比如找不到工具链、编译报路径错误。这一点看着像小事,实际上是我见过最多的环境问题来源。
第二步,如果之前编译过,先清一下:
scons -c第三步,打开Env工具,用cd命令切换到这个BSP目录。注意Env和普通CMD不一样,它初始化了RT-Thread的编译环境变量,一定要用Env,而不是直接用CMD。
4.2 自己写一个驱动目录,从零加入构建链
理解了原理之后,动手在工程里加一个自定义驱动。我们假设要加一个my_sensor传感器驱动,放在applications/my_sensor/目录下。
首先创建目录,在里面放一个my_sensor.c,内容随意,比如一个init函数:
#include <rtthread.h> #include "my_sensor.h" int my_sensor_init(void) { rt_kprintf("my_sensor driver init\n"); return 0; } INIT_APP_EXPORT(my_sensor_init);再放一个my_sensor.h:
#ifndef MY_SENSOR_H #define MY_SENSOR_H int my_sensor_init(void); #endif然后在同目录创建SConscript:
from building import * cwd = GetCurrentDir() src = Glob('*.c') CPPPATH = [cwd] group = DefineGroup('MySensor', src, depend=['BSP_USING_MY_SENSOR'], CPPPATH=CPPPATH) Return('group')注意depend写的是BSP_USING_MY_SENSOR,这个宏我们不手动定义,而是通过Kconfig在menuconfig里生成。所以还需要创建一个Kconfig文件,内容就是之前写过的那段:
config BSP_USING_MY_SENSOR bool "Enable MY-SENSOR driver" default y help MY-SENSOR is an analog output sensor.最后,在applications/Kconfig或者BSP顶层Kconfig里把这个子目录的Kconfig引进来:
source "applications/my_sensor/Kconfig"4.3 menuconfig里找到开关,保存后检查宏
执行menuconfig,在菜单里找到“Enable MY-SENSOR driver”,打开它,保存退出。然后打开rtconfig.h,应该能看到:
#define BSP_USING_MY_SENSOR 1再看.config文件,里面也记录了这个选项。这时候还没有编译,只是配置系统生成了宏。紧接着执行:
scons -j8构建系统读取rtconfig.h,发现BSP_USING_MY_SENSOR存在,就会把applications/my_sensor/SConscript里收集到的my_sensor.c编译进去。如果我把Kconfig里default y改成default n,再走一遍menuconfig,宏消失了,scons编译时就不会碰这个目录下的任何文件。
这个“用一个宏控制一个目录是否参与编译”的机制,就是RT-Thread构建与配置系统的精髓。组件、驱动、在线包,底层全都是这个玩法。
4.4 从命令行编译切到Keil调试
命令行编译通过后,想用Keil打开工程调试。执行:
scons --target=mdk5SCons会在BSP目录下重新生成project.uvprojx,你用Keil打开这个工程,左侧分组里会多出一个“MySensor”分组,my_sensor.c就在里面。勾选的宏也会同步到Keil的C/C++设置里。
这里有一个很常见的误解:有人觉得rtconfig.h在源码里,Keil编译时肯定会自动读,所以不需要重新生成工程。这个理解对了一半。rtconfig.h里的宏Keil确实直接可见,但Keil工程文件里列出的源文件清单是固定快照,新增的文件不会自动出现。所以每次menuconfig里开了新组件、加了新驱动,都要重新scons --target=mdk5,否则Keil工程里根本没有这些源文件,编译时必然报符号未定义或找不到文件。
4.5 加一个在线包试试
在线包是RT-Thread的一大特色。在menuconfig里进入RT-Thread online packages,选一个软件包,比如IoT - internet of things下的cJSON,选中后保存退出。然后在BSP目录执行:
pkgs --updatepkgs会读取.config里包相关的配置,把对应源码下载到BSP目录的packages/下。下载完成后再编译:
scons -j8构建系统会自动把packages/下的包源码纳入构建,不需要手动改任何文件列表。如果下载的包有依赖其他包,比如某些网络库依赖LWIP,pkgs会在更新时一并处理依赖关系。这一步做多了之后你就发现,配置加包、更新、编译,整个流程闭环非常顺。
5. 真实翻车现场:构建配置最常见的几个报错与修复
前面讲了原理和流程,这一节我整理几个自己真实踩过的坑,每个都给出排查链路和修复方法。先放一个速查表,后面逐个展开。
| 现象 | 大概率原因 | 快速解法 |
|---|---|---|
| 链接报Undefined symbol xxx,但文件明明在工程里 | DefineGroup的depend宏未打开,整组被跳过 | 检查rtconfig.h有没有对应宏 |
| menuconfig保存后手改的宏丢了 | rtconfig.h每次保存时重新生成 | 配置走Kconfig,别手改rtconfig.h |
| Keil工程里没有新组件源码 | 没有重新生成IDE工程 | 执行scons --target=mdk5 |
| SCons报路径相关错误 | 工程目录或工具链路径有中文/空格 | 移到纯英文路径 |
| 切换工具链后各种莫名报错 | 旧编译产物残留 | scons -c清理后重新编译 |
| pkgs update后还是找不到包的头文件 | 包没下载成功或版本目录不对 | 重新pkgs --update,检查packages目录 |
5.1 Undefined symbol xxx,但文件就在工程里
这个报错我遇到过好几次,每次都会先怀疑是不是链接脚本出了问题,其实九成不是。
现象是:链接阶段报Undefined symbol rt_sem_take或者自定义函数找不到,但在IDE工程里能看到对应的源码文件。这时候去看编译日志,搜索这个文件对应的.o,如果压根没有这个.o,说明这个文件根本没被编译——不是链接问题,是收集阶段被过滤了。
排查链路:
- 打开这个文件所在目录的
SConscript,看DefineGroup的depend参数。 - 在
rtconfig.h里搜索对应的宏,例如BSP_USING_MY_SENSOR。 - 如果宏不存在,说明menuconfig里没开这个组件,或者开了但没保存成功。
- 如果宏存在但文件还是没编译,检查
Glob('*.c')是否写错路径,或者文件后缀是不是.cpp但没有被Glob('*.cpp')收集。
我后来养成一个习惯:编译完先看一眼最前面的日志,确认自己关心的新文件确实被编进去了。这一步十秒钟,能省后面半小时的链接排错。
5.2 rtconfig.h被手改过,menuconfig一保存全没了
这种问题在调试阶段特别多。为了临时验证某个功能,直接在rtconfig.h里加了一行#define DEBUG 1,编译确实生效了,很爽。但过几天再进menuconfig改点别的东西,保存退出后,rtconfig.h被重新生成,DEBUG没了。
我理解这种“手改一时爽”的冲动,但得认清机制:menuconfig保存时会根据.config重新生成rtconfig.h,所有未在Kconfig里声明的宏都会被抹掉。长期有效的配置,不要绕过Kconfig。
如果确实要加一个自己的宏,正确做法是在板级Kconfig里加一个config BSP_USING_DEBUG之类的开关,然后通过menuconfig打开。这样配置项有了图形界面,下次也不会被覆盖。临时调试用的宏,我建议直接写在源码里,比如某文件开头#define DEBUG 1,调试完删掉,不污染全局配置。
5.3 Keil工程里没有新组件,编译又找不到文件
这个坑是我刚转RT-Thread时踩的。在menuconfig里开了一个软件包,命令行scons编译非常顺利,于是直接打开Keil想调试,结果工程里压根没有这个包的分组,代码跳转也找不到头文件。
原因前面已经讲过:Keil的工程文件是scons --target=mdk5生成出来的快照,menuconfig里新开的组件不会自动同步进.uvprojx。解决办法就是重新生成一次:
scons --target=mdk5重新生成后,再打开Keil工程,新分组、新文件全都有了。记住一个顺序:先命令行编译验证,再生成IDE工程。
5.4 Windows路径带中文或空格,SCons加载工具链失败
这个属于环境问题,但污染面极大。项目放在C:\Users\张三\我的项目\下,打开Env执行scons,出现各种路径拼接错误,比如找不到arm-none-eabi-gcc,或者编译命令里路径被截断。
SCons在Windows下对路径空格的处理一直比较保守,中文路径更是重灾区。解决方案很笨但极其有效:把所有工具链、RT-Thread源码、工程目录统一放到纯英文路径下。比如:
D:\workspace\rt-thread\bsp\stm32\stm32f407-atk-explorer如果你用的是Keil工具链,还要确认Keil安装路径是默认的C:\Keil_v5,如果装在带空格的C:\Program Files下,最好重装或者用目录联接映射一下。这问题不是RT-Thread独有的,SCons、GCC、Python在Windows下对路径都很敏感,越早统一路径,后面的坑越少。
5.5 切换工具链后没清理,报一堆莫名错误
用GCC工具链编译过的工程,改成Keil工具链,或者反过来改,如果不清理直接编译,会报一些非常奇怪的错误:undefined symbol、file format not recognized、linker script not found,看起来哪里都不对。
原因很直接:SCons的增量构建按文件时间戳和参数判断是否重新编译,但工具链切换后,.o文件还是旧编译器生成的,格式和内容都对不上。解决办法是切换工具链后先执行:
scons -c彻底清空.o和可执行文件,再重新scons。极端情况下,比如从GCC切到IAR,我会连.config和rtconfig.h一起删掉,重新配置一遍。这不是热身,是确保配置、宏、启动文件全部和当前工具链匹配,尤其是启动文件和链接脚本,不同工具链差异相当大。
5.6 选了在线包,pkgs --update后还是找不到头文件
在线包报错大部分出现在头文件找不到上。比如选了cJSON,编译时提示找不到cJSON.h。先去packages/目录看包源码到底下载下来没有,如果目录是空的,说明pkgs --update没成功,可能原因:
- 网络问题,包索引更新或源码下载失败。
.config里包的选择没保存成功,pkgs读不到。- 包索引本身需要先更新,可以试试
pkgs --upgrade先升级索引。
如果packages/目录里有源码,但编译还是找不到头文件,检查包源码里自己的SConscript是否把CPPPATH正确设置。有些社区包的SConscript写得比较粗糙,可能需要自己补CPPPATH,或者依赖没选全导致部分源文件没进编译。
另外,每次配置变化后,都应当先pkgs --update再scons。直接编译会提示缺少文件,看起来像报错,其实是忘了同步包。
6. 进阶玩法:工程瘦身、自定义工具链与离线包管理
6.1 用scons --dist给工程瘦身
RT-Thread源码仓库里有几百个BSP和大量组件,如果你想把自己做好的工程发给同事或客户,直接把整个仓库打包过去显然不现实。官方提供了一条命令:
scons --dist在BSP目录执行后,SCons会根据当前配置生成一个精简版工程到dist/目录。这个目录里只包含当前BSP、依赖的RT-Thread源代码、以及当前配置需要的组件。收到的这一份可以直接独立编译,不需要再去克隆完整的RT-Thread仓库。
用--dist有个前置条件:先保证当前工程能正常编译,并且配置是你想要的状态。因为dist是“按需复制”,它根据rtconfig.h和依赖关系裁剪源码,配置不对,裁剪出来的工程自然也不对。我一般在项目收尾阶段执行一次,把dist目录作为交付物。
6.2 自定义工具链和编译参数,改rtconfig.py就够了
换工具链看起来是大工程,其实在RT-Thread里就是改rtconfig.py。比如我把STM32F407这类ARM芯片的GCC编译参数换成11版本的工具链,只需要改两处:
EXEC_PATH = 'C:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/11.3 2022.12/bin' PREFIX = 'arm-none-eabi-'工具链版本差异比较大的时候,比如从ARMCC换到GCC,CFLAGS也要跟着调整。-mcpu、-mthumb这类架构参数保持一致,优化选项可以根据需要改:
CFLAGS = ['-mcpu=cortex-m4', '-mthumb', '-Os', '-ffunction-sections', '-fdata-sections', '-Wall'] LFLAGS = ['-mcpu=cortex-m4', '-mthumb', '-Wl,--gc-sections', '-T', 'link.lds']-ffunction-sections和--gc-sections的组合能有效缩小固件体积,对Flash紧张的板子特别有用。改完之后记得scons -c再重新编译。
6.3 离线包管理:把在线包带到没有网的环境
有些项目处在完全内网环境,没法访问在线包仓库。我常用的做法是有网环境下先把包下载好,把packages/整个目录拷贝到内网工程里。同时.config里的包配置保持不变,这样构建和源码引用都不会断。
另一个关键是包目录里的版本结构要一致。比如packages/cJSON-v1.7.15,目录名里的版本号是PKG_CJSON_VERSION等宏决定的,拷贝过去时不要改动目录名,否则包自带SConscript里的路径拼接会对不上。离线环境下如果pkgs --update还是要联网检查,可以手动把确认无误的包源码冻结在packages/里,不随便执行更新命令即可。
6.4 在CI里跑构建
如果团队要做持续集成,RT-Thread也是支持纯命令行构建的。在Linux或Windows的CI机器上装好Python、SCons和对应工具链,把Env工具的操作换成直接用kconfig-frontends或者Python的Kconfig库处理菜单配置,然后执行scons。具体配置方式在不同CI平台差异比较大,但核心就是两步:先通过Kconfig生成rtconfig.h,再scons编译。理解了这套构建与配置系统的原理,迁移到CI只是换了个命令入口而已。
最后分享一个我自己的检查习惯。每次配置完开始编译之前,我会先打开rtconfig.h,搜索几个关键的新增宏,确认它们在;同时看一眼SConscript的depend是否和这些宏对得上。这个过程大概十秒,但基本能避免我后面花一小时在链接报错里翻来覆去找原因。这套构建与配置系统看着唬人,摸清之后其实就是“菜单点菜、后厨炒菜”各干各的活,你只要保证菜单和菜谱对得上,厨房就能稳定出餐。