衡山派D133EBS入门笔记

0、前言与环境搭建

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

那么既然官方有文档那我们就可以跟着官方的文档的学习

然后我们直接从这个快速入门开始看

然后是环境搭建

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

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

然后看到变蓝就可以点击开始然后烧录了

然后我们开始进军驱动

然后我们看到了一个陌生的东西Scons,

他让我们去RT-Thread的官网上阅读,链接如下:

SCons

但是感觉还是有点晦涩,所以我让codex去阅读然后给我讲

codex的讲解如下,讲得还是很好的,可以阅读一下

Scons

1. SCons 是什么

SCons 是一种构建系统(Build System),不是编译器本身。

它的作用是:

  • 决定要编译哪些源码

  • 决定使用哪个编译器

  • 决定使用哪些编译参数、头文件路径、宏定义

  • 组织编译、链接、打包的整个流程

  • 最终生成可以运行或烧录到开发板上的文件

可以把它理解成:

  • gcc / arm-none-eabi-gcc:真正干活的工人

  • SCons:负责指挥整个编译流程的工头

  • SConstruct / SConscript / rtconfig.py:编译规则和配置文件

一句话总结:

SCons 不是编译器,而是"管理编译器怎么工作"的构建系统。


2. 什么叫"构建"

很多初学者容易把"构建"和"编译"混为一谈。

其实更准确地说:

  • 编译 :把 .c.cpp 等源码文件变成 .o 目标文件

  • 链接 :把多个 .o 文件和库文件组合起来,生成完整程序

  • 打包/转换:把程序转换成适合烧录或运行的格式

所以"构建"通常包含:

  1. 选择配置

  2. 编译源码

  3. 链接程序

  4. 生成镜像

  5. 有时还会生成 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 Kconfigmenuconfigrtconfig.h

虽然它们不属于 SCons 本体,但和 SCons 密切相关。

它们之间的关系是:

  1. Kconfig 定义有哪些功能项可以选择

  2. menuconfig 用菜单方式勾选功能

  3. rtconfig.h 保存最终勾选结果,生成宏定义

  4. SConscript 根据这些宏,决定哪些代码参与编译

一句话总结:

menuconfig 负责选功能,rtconfig.h 负责记结果,SConscript 负责按结果编代码。


5. SCons 的工作流程

运行 scons 之后,通常会发生这些事情:

  1. 读取顶层 SConstruct

  2. 继续读取各目录下的 SConscript

  3. 根据 rtconfig.py 确定编译工具链和参数

  4. 根据 rtconfig.h 判断启用了哪些功能

  5. 收集需要参与编译的源码文件

  6. 执行编译

  7. 执行链接

  8. 生成 .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 很强调"可裁剪"。

这是什么意思?

就是:

  • 功能开了,就编进去

  • 功能没开,就不编进去

举个例子:

  • 如果启用了串口功能

  • 那么串口驱动文件就会参与编译

  • 如果没启用,相关代码就不会进入最终程序

这背后的实现逻辑通常就是:

  1. menuconfig 勾选功能

  2. 生成 rtconfig.h 中的宏

  3. SConscript 通过 GetDepend(...) 判断

  4. 决定是否把对应源码加入编译

一句话总结:

SCons 帮助 RT-Thread 做到"只编需要的代码"。


9. 为什么 applications 目录对新手很友好

很多工程的 applications/SConscript 会直接写成类似下面的逻辑:

复制代码
 src = Glob('*.c')

这表示:

  • 当前目录下所有 .c 文件都会自动参与编译

这对新手非常友好,因为你只要:

  1. 写一个自己的 test.c

  2. 放进 applications

  3. 重新执行 scons

通常它就会自动被编进去。

所以初学阶段可以这样理解:

applications 是最适合做实验和写自己代码的地方。


10. 添加应用代码 和 添加模块 的区别

10.1 添加应用代码

方式:

  • 直接把 .c 文件放进 applications

特点:

  • 简单

  • 快速

  • 适合学习和小实验


10.2 添加模块

方式:

  • 单独建立文件夹

  • 自己写 SConscript

  • 有时还会加 Kconfig

特点:

  • 更规范

  • 更适合中大型功能

  • 可以按配置开关控制

你可以这样理解:

  • applications:临时实验台

  • 独立模块:正式封装件


11. 把这些知识放回你的开发板场景

以衡山派 D133EBS 这类 SDK 为例,整体逻辑其实和 RT-Thread 文档讲的是同一类东西。

你已经看到的流程大致是:

  1. 运行 win_env.bat 进入厂商准备好的编译环境

  2. 执行 list 查看有哪些板级配置

  3. 选择配置 例如:

    复制代码
     lunch 3

    或:

    复制代码
     scons --apply-def=xxx_defconfig
  4. 运行 scons 开始编译整个工程

也就是说,厂商 SDK 的这套命令,本质上也是围绕 SCons 工作的。


12. 初学者最容易混淆的几个点

12.1 SCons 是不是编译器?

不是。

SCons 只是组织构建流程,真正编译源码的是工具链里的编译器。


12.2 sconsSConstruct 是一回事吗?

不是。

  • scons:你在终端里运行的命令

  • SConstruct:这个命令启动后会读取的顶层规则文件


12.3 SConstructSConscript 有什么区别?
  • SConstruct:总入口

  • SConscript:子模块规则文件

可以理解成:

  • SConstruct 管总控

  • SConscript 管局部模块


  • menuconfig 负责让你选择

  • SCons 负责根据选择结果来决定编哪些代码


13. 学习 SCons 的推荐顺序

建议按这个顺序学:

第一步:先会用
  • 会进入编译环境

  • 会选择配置

  • 会执行 scons

  • 会看编译输出

第二步:看懂工程结构
  • 认识 rtconfig.py

  • 认识 SConstruct

  • 认识 SConscript

第三步:理解代码裁剪
  • 知道 Kconfig

  • 知道 menuconfig

  • 知道 rtconfig.h

  • 知道 GetDepend(...)

第四步:开始自己改代码
  • applications 中增加自己的 .c 文件

  • 重新编译并验证是否被编进去

  • 再去看 SConscript 是怎么工作的


14. 新手版最终总结

如果只记住最重要的 5 句话,就记这 5 句:

  1. SCons 是构建系统,不是编译器。

  2. scons 运行时会读取 SConstructSConscript

  3. rtconfig.py 决定工具链和编译参数。

  4. menuconfig -> rtconfig.h -> GetDepend() 决定哪些模块被编进去。

  5. applications 是新手最适合放实验代码的地方。


15. 给自己的一个学习动作

为了真正学会,不建议只看概念。

最有效的练习方法是:

  1. 找到工程里的 applications 目录

  2. 新建一个最简单的 hello.ctest.c

  3. 重新执行 scons

  4. 观察它是否参与编译

  5. 再去看 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 个作用:

  1. 把同一功能的文件归在一起 比如 LED 相关源码统一放一组,方便管理。

  2. 按条件决定编不编 比如 USER_LED_ON 打开才编,不打开就不编。

  3. 给这一组单独设置编译选项 比如单独设置头文件路径、宏定义、编译参数。

  4. 最后汇总到整个固件/程序里 上层脚本把各个组收集起来,最后一起链接成完整程序。


你可以把它类比成"施工队"

整个工程像盖一栋楼:

  • 一个组负责电路

  • 一个组负责水管

  • 一个组负责门窗

  • 一个组负责 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 源码 否则: 不编译 然后把这部分模块注册到整个工程里


你没看懂的根本原因,通常是这几点混在一起了

  1. Import 和 import 不是一回事

  2. DefineGroup 不是 Python 自带函数,是项目自己封装的构建函数

  3. "构建组"不是生成一个单独程序,而是"向总工程登记一批源码和选项"

  4. Return('group') 不是普通 Python 函数返回值语义,而是 SCons 的变量导出机制

复制代码
 Q:我想问一下“如果打开了 USER_LED_ON 这个功能   请问怎么看这个功能大没打开呢,这个是宏定义还是什么,是在别的代码文件里定义的吗

USER_LED_ON

关于 USER_LED_ON,它大概率是"配置开关",常见表现有两层:

  1. 构建系统里的配置项 GetDepend('USER_LED_ON') 这一句通常不是直接去读某个 .c 文件,而是去读工程配置。也就是说,它先问一句:USER_LED_ON 这 个选项在当前工程配置里是不是启用状态。

  2. 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

  • rtconfig.py

  • 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。

你看完后最该记住的重点

  1. 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

  2. RT-Thread 的"引脚编号"不等于芯片手册里的 PA0/PB3 这种名字。 文档特别强调:pin 参数传的是"RT-Thread 定义的引脚编号",它由 BSP 驱动决定,和具体芯片相关。 也就是说,PA15 不一定直接写成 15,你要先转成 RT-Thread 认的编号。 常见两种方式:

  3. 使用 GPIO 的标准流程很固定:先确定编号,再设模式,再读写或挂中断。 最常见流程:

  4. 文档给了 5 种常用引脚模式。 你平时最常用的是这几个:

  5. 中断部分是这篇文档的另一个重点。 文档支持 5 种触发方式:

  6. 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 句话:

  1. PIN 是 RT-Thread 对 GPIO 的统一接口。

  2. 先弄清"RT-Thread 引脚编号",别直接拿芯片脚号硬用。

  3. 输出前先 rt_pin_mode(..., PIN_MODE_OUTPUT)。

  4. 输入读取用 rt_pin_read()。

  5. 按键中断要先 attach_irq(),再 irq_enable()。

  6. detach_irq() 是解绑回调,不是等于一切都彻底关掉。

程序线程说明

线程管理

线程管理速查表

这张表就是把 RT-Thread 那篇线程文档压成"随手查"的版本。你以后写代码时,不用再翻整章,先看这张就够了。

线程是什么

调度规则

线程常见状态

两种创建方式

最常用 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

栈怎么理解

时间片怎么理解

  • 只有多个同优先级线程同时处于就绪态时,时间片轮转才有意义。

  • 如果系统里只有一个该优先级线程,它可能一直跑,直到自己阻塞或被更高优先级抢占。 来源:线程管理 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.

系统自带的重要线程

最容易踩坑的点

  • 优先级理解: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

之前没学过Kconfig? 感觉官方文档写了一堆东西不仅读起来吃力,半天还没搞懂。--没关系,请出我们的老伙伴ai给我们讲讲。

把你当完全没接触过的人来讲的话,Kconfig 本质上就是"工程的配置说明书"。

你可以先记这一句:

Kconfig 不是业务代码,也不是编译器。它是用来"定义有哪些可选功能、这些功能之间有什么依赖、用户可以在配置界面里怎么开关它们"的。

RT-Thread 官方这页先说明了两件事:


Kconfig 到底"管什么"

它主要管 3 件事:

  1. ++这个工程有哪些配置项++ 比如:

    • 是否启用 USER_LED_ON

    • 线程优先级默认值是多少

    • 是否打开某个驱动

  2. ++这些配置项怎么展示给人看++ 比如:

    • 菜单名字叫什么

    • 默认值是什么

    • 帮助说明写什么

  3. ++这些配置项之间有什么依赖关系++ 比如:

    • "只有开启 GPIO 驱动,才显示 LED 选项"

    • "只有某块板子支持,才允许打开这个功能"

Linux 官方文档把它描述为一个树状结构的配置数据库,每个条目都有自己的依赖关系,子项是否可见也受父项影响。 来源:Linux Kconfig 文档 https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html


这个点很容易混。

  • Kconfig:是你写的"配置规则文件"

  • menuconfig / pyconfig:是读取这些规则、让你点选配置的"界面工具"

也就是说:

  • Kconfig 是"规则"

  • menuconfig 是"操作界面"

RT-Thread 那页也提到,Kconfig 机制包括 Kconfig 文件和配置 UI 界面,比如 menuconfig、pyconfig。


Kconfig 和 rtconfig.h 是什么关系

这是 RT-Thread 里最关键的理解点。

流程通常是这样:

  1. 你写 Kconfig

  2. 配置工具读取这些 Kconfig 规则

  3. 你在图形/文本界面里勾选功能

  4. 系统生成配置结果

  5. 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。

一条典型链路可能是:

  1. Kconfig 定义 USER_LED_ON

  2. 你在 menuconfig 里把它打开

  3. 配置结果生成到 rtconfig.h

  4. 构建系统也能读到这个配置

  5. GetDepend('USER_LED_ON') 判断它已开启

  6. 对应 .c 文件被加入编译

也就是说,这不是"凭空突然冒出来的一个宏",而通常是配置系统产物。


对小白来说,最该掌握的不是全部语法,而是这几个关键词

先记住这 7 个就够了:

  • config:定义一个配置项

  • bool:开/关

  • int:整数

  • string:字符串

  • default:默认值

  • depends on:依赖条件

  • help:帮助文本

你以后看到 Kconfig 文件,先盯这几个词,就不会完全看不懂。


最简单的心智模型

如果让我用最土的话讲:

  • .c 文件是在写"功能"

  • Kconfig 文件是在写"这个功能让不让用户选"

  • rtconfig.h 是"用户选完之后的结果单"


四句真言

  1. Kconfig 是配置规则,不是业务代码。

  2. 它定义工程有哪些选项、默认值和依赖关系。

  3. 用户通过 menuconfig/pyconfig 之类界面选择配置。

  4. RT-Thread 会把结果映射到 rtconfig.h,然后代码和构建系统再去使用这些宏。

我的疑问

复制代码
 我拿stm32为例 相当于kconfig就是编写Cubemax中的选项的一些参数堆吗 然后你说的挺喜欢配置就相当于Cubemax   然后一些源文件和头文件就是我们在keil开发的文件,请问我这么理解对吗,如果不对请你先纠正我的观点

你的理解 有一部分是对的,但不能完全等同。更准确地说:

Kconfig 有点像 CubeMX 里的"功能配置系统",但它不是外设初始化代码生成器。

你这个类比里,哪些地方对,哪些地方要修正,我给你拆开说。

你理解对的部分

  • Kconfig 确实是在描述"有哪些可选项、这些选项能不能开、彼此有什么依赖关系"。

  • menuconfig/pyconfig 这种配置界面,确实有点像 CubeMX 里让你点选功能的界面。

  • .c 和 .h 文件,确实就是最后真正参与工程编译、在 IDE 里开发调试的代码文件,这一点和你说的 Keil 里的源码文件很接近。

所以你可以先保留一个粗略印象:

  • Kconfig / menuconfig:像"功能开关配置层"

  • .c / .h:像"真正写逻辑的代码层"

你理解得不太准确的部分

  1. Kconfig 不是"参数堆"那么简单。 它不只是罗列选项,还会定义:

    • 这个选项是什么类型
    • 默认值是什么
    • 它依赖谁
    • 它显示给用户的名字是什么
    • 帮助说明是什么
    • 打开它时会不会联动别的配置

所以它更像:

"配置规则文件",不是单纯的一堆参数表。

  1. Kconfig 不像 CubeMX 那样主要负责"生成初始化代码"。 CubeMX 的典型工作方式是:

    • 你点选串口、GPIO、时钟
    • 它帮你生成一堆初始化代码,比如 MX_GPIO_Init()、MX_USART1_UART_Init()

而 Kconfig 更偏向:

  • 你点选"启用这个功能"

  • 系统生成配置结果,比如宏定义

  • 构建系统和代码根据这些宏,决定"编不编、怎么编、代码走哪条分支"

所以:

  • CubeMX 更偏"外设配置 + 代码生成"

  • Kconfig 更偏"功能配置 + 条件编译/构建"

  1. 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闪烁一次

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

相关推荐
wltx16881 小时前
独立站搭建需要做氨氮检测仪展示吗?
人工智能·python
Air_July1 小时前
Brower User Web UI部署详细步骤
人工智能·python·测试工具
handler011 小时前
Git 核心指令速查
linux·c语言·c++·笔记·git·学习
m0_613856291 小时前
mysql如何优化重复索引_mysql冗余索引查找与处理
jvm·数据库·python
四维迁跃1 小时前
Python Web开发如何防范SQL注入_使用参数化查询与ORM实践
jvm·数据库·python
噜噜噜阿鲁~1 小时前
python学习笔记 | 8.3、函数式编程-匿名函数
笔记·python·学习
YJlio1 小时前
Windows Internals 读书笔记 10.3.3:Task Scheduler 架构详解
人工智能·windows·笔记·python·学习·chatgpt·架构
2401_833033622 小时前
如何自动更新SQL标签状态_利用触发器实现基于逻辑的状态机
jvm·数据库·python
Lhan.zzZ2 小时前
笔记_2026.4.28_003
c++·笔记·qt·opencv