0、前言与环境搭建
首先学习的话我们肯定是先去官方看看有没有官方文档,文档连接:【衡山派介绍】 | 立创开发板技术文档中心

那么既然官方有文档那我们就可以跟着官方的文档的学习
然后我们直接从这个快速入门开始看

然后是环境搭建

前面这些官方文档写的都很详细,所以没什么注意的,然后注意要烧录镜像时

如果你按照他说的那样按住开发板上面的BOOT按键和RST按键,然后我们松开RST按键,一秒之后再松开BOOT按键 没有进入升级模式,可以换个数据线试试,有可能是数据线的问题,我就是数据线的问题。
然后看到变蓝就可以点击开始然后烧录了

然后我们开始进军驱动

然后我们看到了一个陌生的东西Scons,
他让我们去RT-Thread的官网上阅读,链接如下:
但是感觉还是有点晦涩,所以我让codex去阅读然后给我讲
codex的讲解如下,讲得还是很好的,可以阅读一下
Scons
1. SCons 是什么
SCons 是一种构建系统(Build System),不是编译器本身。
它的作用是:
决定要编译哪些源码
决定使用哪个编译器
决定使用哪些编译参数、头文件路径、宏定义
组织编译、链接、打包的整个流程
最终生成可以运行或烧录到开发板上的文件
可以把它理解成:
-
gcc/arm-none-eabi-gcc:真正干活的工人 -
SCons:负责指挥整个编译流程的工头 -
SConstruct/SConscript/rtconfig.py:编译规则和配置文件
一句话总结:
SCons 不是编译器,而是"管理编译器怎么工作"的构建系统。
2. 什么叫"构建"
很多初学者容易把"构建"和"编译"混为一谈。
其实更准确地说:
编译 :把
.c、.cpp等源码文件变成.o目标文件链接 :把多个
.o文件和库文件组合起来,生成完整程序打包/转换:把程序转换成适合烧录或运行的格式
所以"构建"通常包含:
选择配置
编译源码
链接程序
生成镜像
有时还会生成 IDE 工程文件
嵌入式中常见输出文件:
.o:目标文件
.a:静态库
.elf:可执行映像,通常带调试符号
.bin:纯二进制文件
.hex:十六进制格式镜像
.img:烧录镜像
一句话总结:
构建不只是"把代码编译一下",而是把源码一步步变成最终可运行、可烧录的产物。
3. 为什么 RT-Thread 使用 SCons
RT-Thread 早期也使用 Make/Makefile 构建,后来逐渐引入了 SCons。
这么做的主要原因是:
Makefile 对初学者不够友好
Windows 下使用 Make 不太方便
SCons 使用 Python 风格脚本,逻辑更灵活
便于管理复杂工程和模块裁剪
你可以这样理解:
Makefile更像传统规则表
SCons更像可以编程的规则系统
4. 在 RT-Thread 工程里,几个核心文件分别是什么
4.1 rtconfig.py
作用:
配置工具链
指定编译器路径
配置编译参数、汇编参数、链接参数
它更像:
编译环境设置中心
常见内容包括:
CROSS_TOOL
EXEC_PATH
CFLAGS
AFLAGS
LFLAGS
如果以后出现下面这些问题,要优先怀疑它:
编译器找不到
工具链路径不对
链接参数异常
4.2 SConstruct
作用:
-
整个工程的顶层入口
-
scons运行时首先会读取它
它更像:
工程的总控脚本
4.3 SConscript
作用:
-
管理各个目录、各个模块的构建规则
-
一个工程里往往会有很多个
SConscript
它更像:
每个子模块自己的施工单
例如:
-
drivers/SConscript -
applications/SConscript -
components/.../SConscript
4.4 Kconfig、menuconfig、rtconfig.h
虽然它们不属于 SCons 本体,但和 SCons 密切相关。
它们之间的关系是:
-
Kconfig定义有哪些功能项可以选择 -
menuconfig用菜单方式勾选功能 -
rtconfig.h保存最终勾选结果,生成宏定义 -
SConscript根据这些宏,决定哪些代码参与编译
一句话总结:
menuconfig负责选功能,rtconfig.h负责记结果,SConscript负责按结果编代码。
5. SCons 的工作流程
运行 scons 之后,通常会发生这些事情:
-
读取顶层
SConstruct -
继续读取各目录下的
SConscript -
根据
rtconfig.py确定编译工具链和参数 -
根据
rtconfig.h判断启用了哪些功能 -
收集需要参与编译的源码文件
-
执行编译
-
执行链接
-
生成
.elf、.bin、.img等最终产物
所以在终端里看到:
Reading SConscript files ...
它的意思就是:
SCons 正在读取工程中的构建规则文件。
6. 常用 SCons 命令
6.1 scons
最常用命令,直接编译工程。
特点:
-
初次构建时会全量编译
-
之后如果只改了少量文件,通常会增量编译
6.2 scons -c
作用:
- 清理编译生成的中间文件和目标文件
适合:
-
怀疑缓存导致编译异常
-
想重新全量编译
6.3 scons --target=mdk5
作用:
- 生成 Keil MDK5 工程文件
注意:
-
这不是编译固件
-
这是为 IDE 生成工程
类似还有:
scons --target=iar
6.4 scons -j4
作用:
- 并行编译,加快速度
说明:
-
4表示同时使用 4 个任务 -
新手调错时不建议先开太高,否则报错信息不够清晰
6.5 scons --verbose
作用:
- 显示完整的编译命令
适合排查:
-
头文件路径问题
-
宏定义问题
-
编译参数问题
7. 文档中常见的 SCons 函数
下面这些函数在 SConscript 中经常能看到。
7.1 GetCurrentDir()
作用:
- 获取当前
SConscript文件所在目录
可以理解成:
我现在这个模块所在的路径是什么
7.2 Glob('*.c')
作用:
- 搜索当前目录下所有
.c文件
可以理解成:
把当前目录里的所有 C 源文件都加入进来
这对 applications 目录很常见。
7.3 Split("a.c b.c c.c")
作用:
- 把一个字符串拆成文件列表
可以理解成:
手动指定要编译哪些源文件
7.4 GetDepend('RT_USING_XXX')
作用:
- 检查某个配置宏是否启用
可以理解成:
如果这个功能打开了,就把相关源码编进去
这是代码裁剪的关键。
7.5 DefineGroup(...)
作用:
-
定义一个代码分组或模块
-
指定这个模块有哪些源文件、依赖、头文件路径等
可以理解成:
建立一个可管理的代码模块
在生成 IDE 工程时,这个分组通常也会反映到工程结构里。
7.6 SConscript(...)
作用:
- 继续加载其他目录中的
SConscript
可以理解成:
告诉 SCons 去下一个模块继续读取规则
8. 一个最关键的思想:按配置裁剪代码
RT-Thread 很强调"可裁剪"。
这是什么意思?
就是:
-
功能开了,就编进去
-
功能没开,就不编进去
举个例子:
-
如果启用了串口功能
-
那么串口驱动文件就会参与编译
-
如果没启用,相关代码就不会进入最终程序
这背后的实现逻辑通常就是:
-
menuconfig勾选功能 -
生成
rtconfig.h中的宏 -
SConscript通过GetDepend(...)判断 -
决定是否把对应源码加入编译
一句话总结:
SCons 帮助 RT-Thread 做到"只编需要的代码"。
9. 为什么 applications 目录对新手很友好
很多工程的 applications/SConscript 会直接写成类似下面的逻辑:
src = Glob('*.c')
这表示:
- 当前目录下所有
.c文件都会自动参与编译
这对新手非常友好,因为你只要:
-
写一个自己的
test.c -
放进
applications -
重新执行
scons
通常它就会自动被编进去。
所以初学阶段可以这样理解:
applications是最适合做实验和写自己代码的地方。
10. 添加应用代码 和 添加模块 的区别
10.1 添加应用代码
方式:
- 直接把
.c文件放进applications
特点:
-
简单
-
快速
-
适合学习和小实验
10.2 添加模块
方式:
-
单独建立文件夹
-
自己写
SConscript -
有时还会加
Kconfig
特点:
-
更规范
-
更适合中大型功能
-
可以按配置开关控制
你可以这样理解:
-
applications:临时实验台 -
独立模块:正式封装件
11. 把这些知识放回你的开发板场景
以衡山派 D133EBS 这类 SDK 为例,整体逻辑其实和 RT-Thread 文档讲的是同一类东西。
你已经看到的流程大致是:
-
运行
win_env.bat进入厂商准备好的编译环境 -
执行
list查看有哪些板级配置 -
选择配置 例如:
lunch 3或:
scons --apply-def=xxx_defconfig -
运行
scons开始编译整个工程
也就是说,厂商 SDK 的这套命令,本质上也是围绕 SCons 工作的。
12. 初学者最容易混淆的几个点
12.1 SCons 是不是编译器?
不是。
SCons 只是组织构建流程,真正编译源码的是工具链里的编译器。
12.2 scons 和 SConstruct 是一回事吗?
不是。
-
scons:你在终端里运行的命令 -
SConstruct:这个命令启动后会读取的顶层规则文件
12.3 SConstruct 和 SConscript 有什么区别?
-
SConstruct:总入口 -
SConscript:子模块规则文件
可以理解成:
-
SConstruct管总控 -
SConscript管局部模块
12.4 menuconfig 和 SCons 谁负责"开关功能"?
-
menuconfig负责让你选择 -
SCons负责根据选择结果来决定编哪些代码
13. 学习 SCons 的推荐顺序
建议按这个顺序学:
第一步:先会用
-
会进入编译环境
-
会选择配置
-
会执行
scons -
会看编译输出
第二步:看懂工程结构
-
认识
rtconfig.py -
认识
SConstruct -
认识
SConscript
第三步:理解代码裁剪
-
知道
Kconfig -
知道
menuconfig -
知道
rtconfig.h -
知道
GetDepend(...)
第四步:开始自己改代码
-
在
applications中增加自己的.c文件 -
重新编译并验证是否被编进去
-
再去看
SConscript是怎么工作的
14. 新手版最终总结
如果只记住最重要的 5 句话,就记这 5 句:
-
SCons 是构建系统,不是编译器。
-
scons运行时会读取SConstruct和SConscript。 -
rtconfig.py决定工具链和编译参数。 -
menuconfig -> rtconfig.h -> GetDepend()决定哪些模块被编进去。 -
applications是新手最适合放实验代码的地方。
15. 给自己的一个学习动作
为了真正学会,不建议只看概念。
最有效的练习方法是:
-
找到工程里的
applications目录 -
新建一个最简单的
hello.c或test.c -
重新执行
scons -
观察它是否参与编译
-
再去看
applications/SConscript
当你亲手做完这一遍,SCons 对你来说就不再是抽象概念了。
16. 一句话收尾
SCons 的本质,就是根据配置和规则,把源码组织成最终可运行、可烧录程序的"总调度系统"。
接下来让我们点亮第一个Led小灯
1、LED驱动编写

1.1、Sconscript文件
官方的文档让我们将以下代码粘贴进去
python
# 导入预定义的变量和函数
Import('AIC_ROOT') # 导入AIC_ROOT变量,通常用于表示项目根目录
Import('rtconfig') # 导入rtconfig变量,通常包含项目的配置信息
# 导入Python模块
import rtconfig # 导入rtconfig模块,以便直接使用其中的配置
from building import * # 从building模块导入所有内容,可能包含自定义的构建函数和变量
# 获取当前目录的路径
cwd = GetCurrentDir() # 获取当前SConscript文件所在的目录路径
# 设置头文件搜索路径
CPPPATH = [cwd] # 将当前目录添加到编译器的头文件搜索路径中
# 初始化源文件列表
src = [] # 创建一个空列表,用于存放源文件
# 如果在rtconfig中定义了USER_LED_ON,那么获取当前目录下所有的.c源文件,反之则不获取!
if GetDepend('USER_LED_ON'):
src = Glob(os.path.join(cwd, '*.c')) # 使用Glob函数查找当前目录下所有的.c文件,并将它们添加到src列表中
# 定义构建组
group = DefineGroup('lckfb-user-led', src, depend = [''], CPPPATH = CPPPATH)
# 创建一个名为'lckfb-user-led'的构建组,包含src列表中的所有源文件
# depend参数指定了构建这个组之前需要先构建的其他组或目标,这里为空字符串,表示没有依赖
# CPPPATH参数设置了头文件搜索路径,这里使用了前面定义的CPPPATH
# 返回构建组,供上层SConscript文件或其他SConscript文件使用
Return('group') # 将定义的构建组作为返回值,这样其他SConscript文件可以引用这个组
现在让我们来简单分析一下这个代码
这段 SConscript 的作用很简单,就是"如果打开了 USER_LED_ON 这个功能,就把当前目录下的 .c 文件加入工程里一起编译;然后把这一组源码和编译选项打包返回给上层构建系统"。
你可以把它理解成:
"这个目录里有一小块功能模块,满足条件时就编进去,不满足就不编。"
逐行解释
python
`Import('AIC_ROOT')`
`Import('rtconfig')`
这两个不是 ++Python++ 的 import,而是 ++SCons++ 的 Import。 意思是:从上一级 SConstruct 或别的构建脚本里,把已经定义好的变量拿进来用。
你可以把它理解成:
-
AIC_ROOT:工程根目录之类的全局变量
-
rtconfig:工程的配置变量
python
`import rtconfig`
这个才是 Python 真正的导入模块。 也就是说:
-
Import('rtconfig') 是从 SCons 构建环境里拿变量
-
import rtconfig 是导入 Python 模块文件
这两个名字一样,但不是一回事。
python
from building import *
从 building 模块里导入构建辅助函数。 像下面这些函数通常就是从这里来的:
-
GetCurrentDir() -
GetDepend() -
DefineGroup()
也就是说,这个脚本很多"构建动作"其实是靠 building.py 里封装好的函数完成的。
cwd = GetCurrentDir()
获取当前这个 SConscript 所在目录。
比如这个脚本在:
project/user_led/SConscript
那 cwd 大概率就是:
project/user_led
CPPPATH = [cwd]
设置头文件搜索路径。
意思是:编译这个模块时,编译器会去当前目录找头文件。 比如代码里写了:include "user_led.h"
编译器就能在当前目录里找到它。
src = []
先创建一个空的源文件列表。
意思是:默认先不加任何 .c 文件进去。
if GetDepend('USER_LED_ON'):
这是条件判断。
意思是:检查配置里有没有打开 USER_LED_ON 这个功能。 如果开了,就执行下面这句;没开,就跳过。
你可以把 USER_LED_ON 理解成一个"开关":
-
开:编译 LED 模块
-
关:不编译 LED 模块
python
src = Glob(os.path.join(cwd, '*.c'))
如果 LED 功能打开了,就把当前目录下所有 .c 文件找出来,放进 src。
比如当前目录里有:
user_led.c led_drv.c led_test.c
那 src 最后就可能变成:
'user_led.c', 'led_drv.c', 'led_test.c'
Glob('*.c') 就是"按模式匹配文件"。
python
group = DefineGroup('lckfb-user-led', src, depend=[''], CPPPATH=CPPPATH)
这是核心。
它的意思是:定义一个名字叫 lckfb-user-led 的"构建组",这个组里包括:
src:这一组要编译的源文件
CPPPATH:这一组编译时用的头文件路径
depend:这一组依赖的条件或组件
构建组
那"构建组"到底是什么?
"构建组"不是 C 语言语法概念,也不是操作系统概念。 它只是构建系统里的一个"逻辑分组"。
你可以把它理解成:
把某个功能模块相关的源码、头文件路径、宏、依赖条件,打包成一个单元,交给构建系统管理。
比如:
uart 是一个构建组
spi 是一个构建组
user_led 是一个构建组
每个组都描述一件事:
"我要编哪些文件,用哪些编译选项,在什么条件下参与编译。"
构建组有什么用
主要有 4 个作用:
-
把同一功能的文件归在一起 比如 LED 相关源码统一放一组,方便管理。
-
按条件决定编不编 比如 USER_LED_ON 打开才编,不打开就不编。
-
给这一组单独设置编译选项 比如单独设置头文件路径、宏定义、编译参数。
-
最后汇总到整个固件/程序里 上层脚本把各个组收集起来,最后一起链接成完整程序。
你可以把它类比成"施工队"
整个工程像盖一栋楼:
-
一个组负责电路
-
一个组负责水管
-
一个组负责门窗
-
一个组负责 LED
每个"构建组"就是一个小施工队,带着自己的材料和规则。 最后总包把所有施工队的成果拼成一个完整工程。
depend=[''] 这里怎么看
这个写法有点奇怪,通常看项目封装方式。
一般 depend 表示这组的依赖条件,比如:
depend = ['USER_LED_ON']
表示只有开启 USER_LED_ON 时,这组才生效。
但你这里前面已经有了:
if GetDepend('USER_LED_ON'):
所以作者已经先手动判断过一次了。 这样的话,depend=[''] 很可能只是"占位"或者表示"没有额外依赖"。
也就是说,真正控制是否加入源码的,是前面的 GetDepend('USER_LED_ON')。
Return('group') 是什么
这句也是 SCons 的机制。 它的意思不是返回字符串 "group",而是把变量 group 返回给调用这个 SConscript 的上层脚本。
"我这个目录的构建信息已经准备好了,你拿去汇总吧。"
整段脚本连起来翻译成人话
这段代码其实可以翻成一句非常直白的话:
先拿到工程配置和构建工具; 再确定当前目录; 把当前目录设成头文件搜索路径; 如果 USER_LED_ON 功能被开启,就把当前目录下所有 .c 文件加入源码列表; 然后把这些源码和编译选项定义成一个叫 lckfb-user-led 的构建组; 最后把这个构建组返回给上层工程。
再给你一个最短版理解
这段脚本本质上就是:
如果启用了 LED 功能: 编译当前目录下的 LED 源码 否则: 不编译 然后把这部分模块注册到整个工程里
你没看懂的根本原因,通常是这几点混在一起了
-
Import 和 import 不是一回事
-
DefineGroup 不是 Python 自带函数,是项目自己封装的构建函数
-
"构建组"不是生成一个单独程序,而是"向总工程登记一批源码和选项"
-
Return('group') 不是普通 Python 函数返回值语义,而是 SCons 的变量导出机制

Q:我想问一下“如果打开了 USER_LED_ON 这个功能 请问怎么看这个功能大没打开呢,这个是宏定义还是什么,是在别的代码文件里定义的吗
USER_LED_ON
关于 USER_LED_ON,它大概率是"配置开关",常见表现有两层:
-
构建系统里的配置项 GetDepend('USER_LED_ON') 这一句通常不是直接去读某个 .c 文件,而是去读工程配置。也就是说,它先问一句:USER_LED_ON 这 个选项在当前工程配置里是不是启用状态。
-
C 代码里的宏 很多嵌入式工程会把配置项再生成到头文件里,比如:
define USER_LED_ON或者:
define USER_LED_ON 1这样 C 文件里就可以写:
ifdef USER_LED_ON所以它"通常最终会变成宏",但 GetDepend('USER_LED_ON') 自己检查的首先是构建配置,不一定是直接查某个 C 宏。
你可以这样理解:
-
GetDepend('USER_LED_ON'):给 SCons/构建系统看的 -
define USER_LED_ON:给 C 编译器看的
很多项目里,这两者其实来自同一个配置源,只是分别服务于构建阶段和编译阶段。
最常见的定义位置有这些:
rtconfig.h
Kconfig
menuconfig 生成的配置文件
板级配置文件
某个 SConstruct / SConscript / building.py
你要判断它有没有打开,最直接就是全局搜索 USER_LED_ON,重点看这几种写法:
python
define USER_LED_ON
define USER_LED_ON 1
GetDepend('USER_LED_ON')
USER_LED_ON = True
CONFIG_USER_LED_ON=y

然后官方文档让我们看看RT-Thread的关于Pin的官方文档
PIN设备
这里我为节省时间,直接把这个网址给ai,让ai帮我看看,然后总结出重点内容给我
我读了你给的这份 RT-Thread PIN 设备文档。把它压缩成一句话就是:
这篇文档在讲:RT-Thread 把 GPIO 封装成统一的 PIN 设备接口,你平时控制 LED、读取按键、配置上下拉、挂接外部中断,基本都靠这一套 API。
你看完后最该记住的重点
PIN 就是 RT-Thread 对 GPIO 的统一抽象。 你不用直接操作寄存器,通常通过 rt_pin_mode、rt_pin_write、rt_pin_read、rt_pin_attach_irq 这些接口来操作引脚。 来源:官方编程指南第 13 章、API 参考 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/group__pin.html
RT-Thread 的"引脚编号"不等于芯片手册里的 PA0/PB3 这种名字。 文档特别强调:pin 参数传的是"RT-Thread 定义的引脚编号",它由 BSP 驱动决定,和具体芯片相关。 也就是说,PA15 不一定直接写成 15,你要先转成 RT-Thread 认的编号。 常见两种方式:
STM32 BSP 里常用 GET_PIN(port, pin),例如 GET_PIN(F, 9)
其他 BSP 需要去看 drv_gpio.c 里的映射表 来源:编程指南 13.2.1 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf
使用 GPIO 的标准流程很固定:先确定编号,再设模式,再读写或挂中断。 最常见流程:
找到引脚编号
rt_pin_mode(pin, mode) 设置成输入/输出
输出引脚用 rt_pin_write(pin, value)
输入引脚用 rt_pin_read(pin)
如果是按键中断,先 rt_pin_attach_irq(...),再 rt_pin_irq_enable(...) 这篇文档本质上就是围绕这几个步骤展开。 来源:编程指南 13.2 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf
文档给了 5 种常用引脚模式。 你平时最常用的是这几个:
PIN_MODE_OUTPUT:普通输出,控制 LED、蜂鸣器最常见
PIN_MODE_INPUT:普通输入
PIN_MODE_INPUT_PULLUP:上拉输入,按键很常见
PIN_MODE_INPUT_PULLDOWN:下拉输入
PIN_MODE_OUTPUT_OD:开漏输出 但文档也提醒了:具体芯片到底支持哪些模式,要看底层驱动实现,不是每块芯片都完全一样。 来源:编程指南 13.2.2、API 参考 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/group__pin.html
中断部分是这篇文档的另一个重点。 文档支持 5 种触发方式:
上升沿
下降沿
双边沿
高电平
低电平 用法分两步,不是一句就完事:
rt_pin_attach_irq(pin, mode, callback, args):绑定中断回调
rt_pin_irq_enable(pin, PIN_IRQ_ENABLE):真正使能中断 这是很关键的点,很多人会漏掉第二步。 来源:编程指南 13.2.5、13.2.6;API 参考 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/group__pin.html
detach_irq 只是解绑回调,不等于自动把所有中断逻辑彻底关掉。 文档明确提到:引脚脱离中断回调函数后,中断并没有关闭,还可以再次绑定其他回调。 这个细节很容易忽略。 来源:编程指南 13.2.7 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf
文档里的示例到底在演示什么
它用了一个"按键控制蜂鸣器"的例子,逻辑非常典型:
把蜂鸣器对应引脚设为输出
默认先输出低电平
把按键 0、按键 1 设为输入上拉
给两个按键分别绑定下降沿中断
按键 0 的回调里把蜂鸣器拉高
按键 1 的回调里把蜂鸣器拉低
这段示例想让你学会的,不是"蜂鸣器"本身,而是这套思路:
一个输出设备 + 一个输入按键 + 中断回调 = 最基本的 GPIO 事件驱动模型。
示例来源: https://www.rt-thread.org/document/api/pin_beep_sample_8c-example.html
如果把整篇文档翻译成大白话
它其实就在回答 4 个问题:
我怎么知道一个脚在 RT-Thread 里该写成哪个编号?
我怎么把这个脚设成输入或者输出?
我怎么把这个脚拉高、拉低,或者读它当前电平?
我怎么让按键一按下就触发回调,而不是一直轮询?
你最容易混淆的几个点
不要把 PA0、PB8 直接当成 API 里的 pin 参数。 API 要的是 RT-Thread 的引脚编号。
不是所有板子都一定能直接用 GET_PIN()。 文档说了,STM32 BSP 常见这样用;其他 BSP 可能要看驱动文件。
中断不是只 attach 就行。 还要 enable。
模式和触发方式是两回事。 PIN_MODE_INPUT_PULLUP 是输入配置,PIN_IRQ_MODE_FALLING 是中断触发方式,别混到一起。
这套接口是"应用层统一接口",不是底层寄存器教学。 所以文档重点是"怎么调用",不是"GPIO 硬件内部电路原理"。
如果你现在就想上手,最实用的最小模板是这个
cpp
include <rtthread.h>
include <rtdevice.h>
define LED_PIN GET_PIN(F, 9)
static void sample(void)
{
rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
rt_pin_write(LED_PIN, PIN_HIGH);
}
如果是按键中断版,最小模板是:
python
include <rtthread.h>
include <rtdevice.h>
define KEY_PIN GET_PIN(D, 8)
static void key_cb(void *args)
{
rt_kprintf("key irq\n");
}
static void sample(void)
{
rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP);
rt_pin_attach_irq(KEY_PIN, PIN_IRQ_MODE_FALLING, key_cb, RT_NULL);
rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE);
}
你可以把这篇文档的重点直接背成这 6 句话:
PIN 是 RT-Thread 对 GPIO 的统一接口。
先弄清"RT-Thread 引脚编号",别直接拿芯片脚号硬用。
输出前先 rt_pin_mode(..., PIN_MODE_OUTPUT)。
输入读取用 rt_pin_read()。
按键中断要先 attach_irq(),再 irq_enable()。
detach_irq() 是解绑回调,不是等于一切都彻底关掉。
程序线程说明
线程管理速查表
这张表就是把 RT-Thread 那篇线程文档压成"随手查"的版本。你以后写代码时,不用再翻整章,先看这张就够了。
线程是什么
-
线程是 RT-Thread 的最小调度单位。
-
每个线程至少包含:入口函数、栈、优先级、时间片、状态。
-
系统会在多个线程之间切换执行,不是一个函数从头跑到尾。 来源:RT-Thread 编程指南、线程管理 API https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/group___thread.html
调度规则
-
优先级数字越小,优先级越高。
-
高优先级线程可以抢占低优先级线程。
-
同优先级线程按时间片轮转。
-
tick / 时间片只对"同优先级线程之间"有意义。 来源:线程管理 API https://www.rt-thread.org/document/api/group___thread.html
线程常见状态
-
INIT:刚创建,还没运行
-
READY:准备好了,等 CPU
-
RUNNING:正在运行
-
SUSPEND:挂起/阻塞,等时间、等资源、等事件
-
CLOSE:线程结束 理解重点:线程"停住"很多时候不是死机,而是在阻塞等待。 来源:编程指南、rt_thread 结构说明 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/structrt__thread.html
两种创建方式
-
动态线程 :
rt_thread_create()-
系统从堆里分配线程对象和栈
-
用起来简单
-
-
静态线程 :
rt_thread_init()-
线程对象和栈空间由你自己提供
-
更可控,常用于资源严格场景 来源:线程管理 API、线程示例 https://www.rt-thread.org/document/api/group___thread.html https://www.rt-thread.org/document/api/thread_sample_8c-example.html
-
最常用 API
-
创建动态线程:rt_thread_create()
-
初始化静态线程:rt_thread_init()
-
启动线程:rt_thread_startup()
-
删除动态线程:rt_thread_delete()
-
脱离静态线程:rt_thread_detach()
-
延时:rt_thread_delay() / rt_thread_mdelay() / rt_thread_sleep()
-
让出 CPU:rt_thread_yield()
-
挂起线程:rt_thread_suspend()
-
恢复线程:rt_thread_resume() 来源:线程管理 API https://www.rt-thread.org/document/api/group___thread.html
这些参数最关键
-
name:线程名,长度受 RT_NAME_MAX 限制
-
entry:线程入口函数
-
parameter:传给入口函数的参数
-
stack_size:线程栈大小
-
priority:线程优先级
-
tick:时间片大小 你创建线程时,本质上就在设置这 6 个东西。 来源:线程管理 API https://www.rt-thread.org/document/api/group___thread.html
栈怎么理解
-
每个线程都要有自己的栈。
-
栈太小会出问题,常见表现是跑飞、异常、莫名其妙死掉。
-
栈里会放局部变量、函数调用现场、中断切换保存内容等。 文档的核心意思是:栈大小不是随便写的,要根据实际任务调整。 来源:编程指南、rt_thread 结构说明 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf https://www.rt-thread.org/document/api/structrt__thread.html
时间片怎么理解
-
只有多个同优先级线程同时处于就绪态时,时间片轮转才有意义。
-
如果系统里只有一个该优先级线程,它可能一直跑,直到自己阻塞或被更高优先级抢占。 来源:线程管理 API https://www.rt-thread.org/document/api/group___thread.html
文档里的典型线程流程
1、写线程入口函数
2、创建或初始化线程
3、启动线程
4、在线程里循环执行任务
5、需要等待时主动延时/阻塞
6、结束后删除或脱离
这就是 RT-Thread 最典型的线程开发模式。 来源:线程示例 https://www.rt-thread.org/document/api/thread_sample_8c-example.html
2.
系统自带的重要线程
-
空闲线程:系统没别的线程可跑时运行,也负责回收某些资源
-
主线程:系统启动后做初始化工作 意思是:系统本身就在多线程运行,不只是你自己新建的线程。 来源:编程指南 https://www.rt-thread.org/download/manual/um4003-rtthread-programming-manual.pdf
最容易踩坑的点
-
优先级理解:RT-Thread 是数字越小优先级越高
-
只创建线程,不 startup
-
栈设太小
-
同优先级线程卡住了,却以为时间片没生效,实际可能是某个线程没主动阻塞
-
在线程里写死循环但不延时、不等待,导致长期占用 CPU
-
把线程通信问题误当成线程创建问题,实际应该用 IPC 线程通信相关文档: https://www.rt-thread.org/document/api/group___i_p_c.html
最小记忆版
-
线程 = 入口函数 + 栈 + 优先级 + 时间片 + 状态
-
小数字优先级高
-
高优先级抢占,低优先级让路
-
同优先级靠时间片轮转
-
动态创建用 rt_thread_create()
-
静态创建用 rt_thread_init()
-
创建后还要 rt_thread_startup()
-
线程里要学会延时、等待、让出 CPU

1.2、user_led.c文件
我们在user_led.c文件中编写以下代码:
python
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 文档网站:wiki.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 嘉立创社区问答:https://www.jlc-bbs.com/lckfb
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <getopt.h>
#include <sys/time.h>
#include <rtthread.h>
#include "rtdevice.h"
#include "aic_core.h"
#include "aic_hal_gpio.h"
#define THREAD_PRIORITY 25 // 线程优先级,数字越大优先级越高
#define THREAD_STACK_SIZE 512 // 线程堆栈大小,决定了线程可以使用的内存空间
#define THREAD_TIMESLICE 5 // 线程时间片,决定了线程在调度时能占用CPU的最长时间
static rt_thread_t led_thread = RT_NULL; // 声明线程控制块指针,用于后续创建和管理线程
static rt_base_t user_led_pin = RT_NULL; // 声明引脚编号变量,初始化为NULL,用于存储LED的引脚编号
// 线程入口函数,这是线程启动后执行的函数
static void user_led_thread_entry(void *param)
{
while(1) // 无限循环,使线程持续运行
{
rt_pin_write(user_led_pin, PIN_HIGH); // 将LED引脚电平设置为高
rt_thread_mdelay(100); // 线程延时100毫秒,让LED保持高电平一段时间
rt_pin_write(user_led_pin, PIN_LOW); // 将LED引脚电平设置为低
rt_thread_mdelay(100); // 线程延时100毫秒,让LED保持低电平一段时间
}
}
// 初始化LED的函数,在系统启动时调用
static void usr_led_run(int argc, char **argv)
{
user_led_pin = rt_pin_get("PE.17"); // 获取名为"PE.17"的引脚编号
if ((user_led_pin == -RT_EINVAL) || (user_led_pin == -RT_ENOSYS)) // 如果获取失败,打印错误信息并返回
{
rt_kprintf("Failed to get the pin PE.17\n");
return; // 获取引脚失败,直接返回,避免后续操作导致错误
}
rt_pin_mode(user_led_pin, PIN_MODE_OUTPUT); // 设置引脚为输出模式
rt_pin_write(user_led_pin, PIN_LOW); // 初始时将LED设置为低电平
/* 创建线程,名称是 led_thread,入口是 user_led_thread_entry */
led_thread = rt_thread_create("led_thread", // 线程名称
user_led_thread_entry, RT_NULL, // 线程入口函数和参数
THREAD_STACK_SIZE, // 线程堆栈大小
THREAD_PRIORITY, // 线程优先级
THREAD_TIMESLICE); // 线程时间片
if (led_thread == RT_NULL) // 如果线程创建失败,打印错误信息并返回
{
rt_kprintf("Failed to create the led_thread\n");
return; // 线程创建失败,直接返回,防止对无效的线程进行操作
}
/* 如果获得线程控制块,启动这个线程 */
rt_thread_startup(led_thread); // 启动线程,使其开始执行
}
// 导出函数自动运行,在系统初始化时调用usr_led_run函数
INIT_APP_EXPORT(usr_led_run);

1.3、编写Kconfig

Kconfig 语言 --- Linux 内核文档 --- Kconfig Language --- The Linux Kernel documentation
之前没学过Kconfig? 感觉官方文档写了一堆东西不仅读起来吃力,半天还没搞懂。--没关系,请出我们的老伙伴ai给我们讲讲。
把你当完全没接触过的人来讲的话,Kconfig 本质上就是"工程的配置说明书"。
你可以先记这一句:
Kconfig 不是业务代码,也不是编译器。它是用来"定义有哪些可选功能、这些功能之间有什么依赖、用户可以在配置界面里怎么开关它们"的。
RT-Thread 官方这页先说明了两件事:
-
Kconfig 源于 Linux 内核的配置构建系统。
-
在 RT-Thread 里,Kconfig 里的配置项最终会映射到 rtconfig.h 中;Kconfig 文件也可以分散在各级源码目录里。 链接: https://www.rt-thread.org/document/site/#/development-tools/build-config-system/Kconfig Linux 官方 Kconfig 语言文档: https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html
Kconfig 到底"管什么"
它主要管 3 件事:
-
++这个工程有哪些配置项++ 比如:
-
是否启用 USER_LED_ON
-
线程优先级默认值是多少
-
是否打开某个驱动
-
-
++这些配置项怎么展示给人看++ 比如:
-
菜单名字叫什么
-
默认值是什么
-
帮助说明写什么
-
-
++这些配置项之间有什么依赖关系++ 比如:
-
"只有开启 GPIO 驱动,才显示 LED 选项"
-
"只有某块板子支持,才允许打开这个功能"
-
Linux 官方文档把它描述为一个树状结构的配置数据库,每个条目都有自己的依赖关系,子项是否可见也受父项影响。 来源:Linux Kconfig 文档 https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html
Kconfig 不等于 menuconfig
这个点很容易混。
-
Kconfig:是你写的"配置规则文件"
-
menuconfig / pyconfig:是读取这些规则、让你点选配置的"界面工具"
也就是说:
-
Kconfig 是"规则"
-
menuconfig 是"操作界面"
RT-Thread 那页也提到,Kconfig 机制包括 Kconfig 文件和配置 UI 界面,比如 menuconfig、pyconfig。
Kconfig 和 rtconfig.h 是什么关系
这是 RT-Thread 里最关键的理解点。
流程通常是这样:
-
你写 Kconfig
-
配置工具读取这些 Kconfig 规则
-
你在图形/文本界面里勾选功能
-
系统生成配置结果
-
RT-Thread 把这些结果映射到 rtconfig.h
然后你的 C 代码里就能这样用:
python
ifdef USER_LED_ON
/* 编译 LED 功能 */
endif
或者:
python
if USER_LED_ON
/* 打开某功能 */
endif
所以你之前问的 USER_LED_ON,很多时候它的根源就是:
先在 Kconfig 里定义,用户勾选后,再生成到 rtconfig.h 里变成宏。
这就是"配置系统"和"宏定义"之间的关系:
-
Kconfig 是配置来源
-
rtconfig.h 是生成结果
-
ifdef 是代码使用方式
看一个最小例子
cpp
config USER_LED_ON
bool "Enable user LED"
default n
help
Enable the user LED demo.
这段话的意思是:
-
定义一个配置项,名字叫 USER_LED_ON
-
它是布尔型,也就是开/关
-
菜单里显示成 Enable user LED
-
默认关闭
-
下面那几行是帮助说明
如果你在配置界面里把它打开,它后面就可能生成成类似:
define USER_LED_ON
或者:
define USER_LED_ON 1
具体生成形式要看 RT-Thread 的实现方式。
再看一个带依赖的例子
cpp
config USER_LED_ON
bool "Enable user LED"
depends on BSP_USING_GPIO
default n
这句 depends on BSP_USING_GPIO 的意思是:
只有当 GPIO 相关功能已经打开时,这个 LED 选项才有意义,才会显示或可选。
Linux 文档对 depends on 的说明是:依赖关系决定条目的可见性,也会约束它的可选范围。 来源:Linux Kconfig 文档 https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html
depends on 和 select 很容易混
这是初学者常见坑。
depends on
意思是: "我想成立,前提是别人已经成立。"
例如:
cpp
config LED_DEMO
bool "LED demo"
depends on BSP_USING_GPIO
含义:没有 GPIO,就别谈 LED demo。
select
意思是: "如果我被打开,就顺手把别人也打开。"
例如:
cpp
config LED_DEMO
bool "LED demo"
select BSP_USING_GPIO
但 Linux 官方文档特别提醒:select 要谨慎,因为它会强行把别的符号拉起来,可能绕过对方本来的依赖检查。 来源:Linux Kconfig 文档 https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html
所以初学阶段你可以先这样记:
-
前提条件用 depends on
-
强制带起别人才用 select
它和你平时写的 .c / .h / SCons 有什么分工
你可以这样分:
-
Kconfig- 决定"有什么选项、能不能开"
-
menuconfig/pyconfig- 决定"用户最终选了什么"
-
rtconfig.h
- 把选择结果变成宏
-
SConscript/SConstruct
- 决定"根据这些开关编译哪些文件"
-
.c / .h
- 决定"代码在功能打开时怎么运行"
所以:
Kconfig 决定"配什么",构建脚本决定"编什么",C 代码决定"怎么跑"。
这就能解释你之前看到的 GetDepend('USER_LED_ON')
它的上游很可能就是 Kconfig。
一条典型链路可能是:
Kconfig 定义 USER_LED_ON
你在 menuconfig 里把它打开
配置结果生成到 rtconfig.h
构建系统也能读到这个配置
GetDepend('USER_LED_ON') 判断它已开启
对应 .c 文件被加入编译
也就是说,这不是"凭空突然冒出来的一个宏",而通常是配置系统产物。
对小白来说,最该掌握的不是全部语法,而是这几个关键词
先记住这 7 个就够了:
config:定义一个配置项
bool:开/关
int:整数
string:字符串
default:默认值
depends on:依赖条件
help:帮助文本
你以后看到 Kconfig 文件,先盯这几个词,就不会完全看不懂。
最简单的心智模型
如果让我用最土的话讲:
.c 文件是在写"功能"
Kconfig 文件是在写"这个功能让不让用户选"
rtconfig.h 是"用户选完之后的结果单"
四句真言
Kconfig 是配置规则,不是业务代码。
它定义工程有哪些选项、默认值和依赖关系。
用户通过 menuconfig/pyconfig 之类界面选择配置。
RT-Thread 会把结果映射到 rtconfig.h,然后代码和构建系统再去使用这些宏。
我的疑问
我拿stm32为例 相当于kconfig就是编写Cubemax中的选项的一些参数堆吗 然后你说的挺喜欢配置就相当于Cubemax 然后一些源文件和头文件就是我们在keil开发的文件,请问我这么理解对吗,如果不对请你先纠正我的观点
你的理解 有一部分是对的,但不能完全等同。更准确地说:
Kconfig 有点像 CubeMX 里的"功能配置系统",但它不是外设初始化代码生成器。
你这个类比里,哪些地方对,哪些地方要修正,我给你拆开说。
你理解对的部分
-
Kconfig 确实是在描述"有哪些可选项、这些选项能不能开、彼此有什么依赖关系"。
-
menuconfig/pyconfig 这种配置界面,确实有点像 CubeMX 里让你点选功能的界面。
-
.c 和 .h 文件,确实就是最后真正参与工程编译、在 IDE 里开发调试的代码文件,这一点和你说的 Keil 里的源码文件很接近。
所以你可以先保留一个粗略印象:
-
Kconfig / menuconfig:像"功能开关配置层"
-
.c / .h:像"真正写逻辑的代码层"
你理解得不太准确的部分
-
Kconfig 不是"参数堆"那么简单。 它不只是罗列选项,还会定义:
- 这个选项是什么类型
- 默认值是什么
- 它依赖谁
- 它显示给用户的名字是什么
- 帮助说明是什么
- 打开它时会不会联动别的配置
所以它更像:
"配置规则文件",不是单纯的一堆参数表。
-
Kconfig 不像 CubeMX 那样主要负责"生成初始化代码"。 CubeMX 的典型工作方式是:
- 你点选串口、GPIO、时钟
- 它帮你生成一堆初始化代码,比如 MX_GPIO_Init()、MX_USART1_UART_Init()
而 Kconfig 更偏向:
-
你点选"启用这个功能"
-
系统生成配置结果,比如宏定义
-
构建系统和代码根据这些宏,决定"编不编、怎么编、代码走哪条分支"
所以:
-
CubeMX 更偏"外设配置 + 代码生成"
-
Kconfig 更偏"功能配置 + 条件编译/构建"
- menuconfig 不等于 CubeMX。 它们相似的地方只是"都有一个配置界面"。
但本质不同:
-
CubeMX 主要围绕芯片外设和初始化
- menuconfig 主要围绕软件组件、驱动、内核功能、示例、裁剪选项
也就是说,menuconfig 更像一个"软件工程配置中心",不只是"外设图形化配置器"。
更准确的类比
如果拿 STM32 + CubeMX + Keil 来类比 RT-Thread 这套东西,我建议你这样理解:
-
Kconfig:定义"这个工程有哪些可选功能、依赖和默认值"
-
menuconfig/pyconfig:让你勾选这些功能的界面
-
rtconfig.h:相当于"配置结果清单",里面是生成出来的宏
-
SConscript/SConstruct:相当于"根据配置结果决定哪些源码要加入工程"
-
.c/.h:真正实现功能的代码
-
Keil:更像最终查看、编译、调试这些代码的 IDE
-
所以你之前那个理解,我帮你修正成一句更准确的话:
Kconfig 有点像 CubeMX 的"配置界面背后的规则系统",但它更偏软件功能裁剪和条件编译,不是主要用来生成外设初始化代码的。
再用 USER_LED_ON 举个最贴近你的例子
这条链可能是:
-
Kconfig 里定义:有没有 USER_LED_ON 这个选项
-
你在 menuconfig 里把它打开
-
系统生成到 rtconfig.h
-
SConscript 看到这个配置打开了,就把 user_led.c 加进编译
-
user_led.c 里面的 LED 功能最终参与编译和运行
-
这个过程和 CubeMX 最大的区别是:
-
CubeMX 常常会直接给你生成 GPIO 初始化代码
- Kconfig 往往只是决定"这个模块要不要进工程、哪些宏生效"
你的方向是对的,只是要把"CubeMX=代码生成器"和"Kconfig=配置规则系统"这两个角色分开。


1.4、进入menuconfig配置

然后进入这个界面后就按照上面说的操作,
然后按键盘【回车键】是ok 想要退出就连续按两下【Esc】

这时候我们可以打开**\luban-lite\rtconfig.h** 文件查看我们在 Kconfig文件 中设定的语句。
可以看到在**\luban-lite\rtconfig.h** 文件中已经定义了 USER_LED_ON 这个字段:


1.5、编译SDK
这里我把user_led.c的代码修改了一下,修改成了1s闪烁一次


成功烧录之后即可解锁成就--点灯大师
