内核基础
本章将介绍 RT-Thread 内核的基础知识,为后续章节的学习奠定基础。主要内容包括:内核简介、系统启动流程以及内核配置。
首先,我们将对 RT-Thread 内核进行简要介绍,并从软件架构的角度来讲解实时内核的组成和实现。 这部分内容旨在帮助初学者了解 RT-Thread 内核相关的基本概念和基础知识,例如:线程、调度、同步等,并对内核的组成部分有一个初步的认识。
其次,我们将详细讲解 RT-Thread 系统的启动流程,让初学者了解系统是如何启动的。
最后,我们将介绍 RT-Thread 内核的配置方法, 这部分内容将帮助初学者掌握内核配置的基本方法,并能够根据实际需求对内核进行定制。
通过本章的学习,初学者将能够:
- 了解 RT-Thread 内核的基本概念和架构;
- 理解 RT-Thread 系统的启动流程;
- 掌握 RT-Thread 内核的配置方法。
RT-Thread 内核介绍
操作系统内核是操作系统的核心,是最基础也是最重要的部分。它就像计算机的大脑,负责管理系统的核心资源,包括线程、线程间的通信、时间、中断和内存等。 RT-Thread 内核也承担着这样的角色,它位于硬件之上,由内核库和实时内核实现构成。
内核库可以理解为是内核的工具箱,它提供了一些类似 C 语言库的基本函数,帮助内核独立运行。 这些函数的具体内容会根据所使用的编译器而有所不同。 例如,使用 GNU GCC 编译器时,内核库会包含更多标准 C 库的函数。
注意:
C 库,也称为 C 运行库,提供了一些常用的函数,例如字符串复制函数
strcpy
、内存复制函数memcpy
等,有时也包含格式化输入输出函数printf
和scanf
。 RT-Thread 内核服务库只实现了内核需要的一小部分 C 库函数,为了避免与标准 C 库函数重名,这些函数名前面都加上了rt_
前缀,例如rt_strcpy
、rt_memcpy
。
实时内核是 RT-Thread 的核心,它负责管理系统的各种重要资源,包括:对象的创建和删除、线程的创建、调度和切换、线程之间互相通信、时间的记录和管理以及内存的分配和回收等。 一个功能最精简的 RT-Thread 内核只需要占用 3KB 的 ROM 和 1.2KB 的 RAM。
线程调度
RT-Thread 以线程为最小调度单位,采用基于优先级的全抢占式调度算法。 这意味着除了中断处理、调度器锁定和禁止中断的代码外,系统其他部分,甚至包括调度器自身,都可以被更高优先级的线程抢占。
RT-Thread 支持最多 256 个线程优先级 (可配置为 32 或 8 个,STM32 默认 32 个),0 为最高优先级,最低优先级保留给空闲线程。 相同优先级的线程会轮流执行,确保每个线程都能获得运行时间。 调度器能够快速找到最高优先级的就绪线程,且查找时间固定。 线程数量没有硬性限制,主要取决于硬件平台的内存容量。 线程管理将在《线程管理》章节详细介绍。
时钟管理
RT-Thread 的时间管理基于时钟节拍,它是系统最小的计时单位。RT-Thread 提供两种定时器:单次触发定时器(触发一次后自动停止)和周期触发定时器(周期性触发,直到手动停止)。
根据超时函数的执行环境,定时器分为硬件定时器(HARD_TIMER)和软件定时器(SOFT_TIMER)两种模式。 定时器通常用于定时执行回调函数,提供定时服务。 用户可以根据实时性要求选择合适的定时器类型。定时器将在《时钟管理》章节展开讲解。
线程间同步
RT-Thread 使用信号量、互斥量和事件集来实现线程间的同步。 线程可以通过获取和释放信号量或互斥量来进行同步。 为了避免优先级翻转问题,互斥量采用了优先级继承机制。 线程同步机制支持线程按照优先级等待信号量或互斥量。 此外,线程还可以通过发送和接收事件来进行同步。 事件集支持 “或触发” 和 “与触发” 两种模式,适用于线程需要等待多个事件的情况。 信号量、互斥量与事件集的概念将在《线程间同步》章节详细介绍。
优先级继承和优先级翻转是实时操作系统中用于管理任务优先级和避免死锁的重要概念。
优先级翻转
优先级翻转是指高优先级的任务被低优先级的任务无意中阻塞的情况,这违反了优先级的预期行为,可能导致系统性能下降甚至死锁。
优先级继承
为了解决优先级翻转问题,一种常见的方法是使用优先级继承协议。该协议的基本思想是,当一个高优先级的任务被一个低优先级的任务阻塞时,低优先级的任务会临时提升其优先级,以尽快完成其工作并释放互斥量,从而减少高优先级任务的等待时间。
线程间通信
RT-Thread 提供了邮箱和消息队列两种线程间通信机制。邮箱传递固定长度(4 字节)的数据,效率更高;消息队列则可以传递不固定长度的消息,并将其缓存在内存中。 邮箱和消息队列的发送操作可以在中断服务例程中安全使用。两种机制都支持线程按优先级等待。邮箱和消息队列的概念将在《线程间通信》章节详细介绍。
内存管理
RT-Thread 提供静态内存池和动态内存堆两种内存管理方式。静态内存池分配内存速度恒定(如果内存可用),内存不足时会挂起或阻塞申请线程,直到有内存释放。动态内存堆则根据系统资源情况,提供了针对小内存系统的算法和针对大内存系统的 SLAB 算法。 此外,memheap 能够管理多个不连续的内存堆,方便用户像操作单个内存堆一样使用它们。内存管理的概念将在《内存管理》章节展开讲解。
I/O 设备管理
RT-Thread 将 PIN、I2C、SPI、USB、UART 等外设抽象成设备,并通过统一的设备注册机制进行管理。 这使得应用程序可以通过设备名称访问硬件,并使用统一的 API 接口进行操作。 此外,RT-Thread 还支持为设备挂接事件,当设备事件触发时,驱动程序会通知应用程序。 I/O 设备管理的概念将在《设备模型》及《通用设备》章节展开讲解。
RT-Thread 启动流程
了解 RT-Thread 的代码,通常从启动流程开始。RT-Thread 定义了统一的启动入口 rtthread_startup()
函数,系统启动流程一般为:启动文件 -> rtthread_startup()
-> 用户程序入口 main()
,如下图所示。
为了在 MDK-ARM 环境下,用户程序执行前无感知地完成 RT-Thread 系统初始化,我们巧妙地运用了 MDK 链接器的高级特性:$Sub$$
与 $Super$$
。通过前缀 $Sub$$
声明的 $Sub$$main
函数,我们可以先一步启动并配置好 RT-Thread 实时操作系统,随后再通过 $Super$$main
自然过渡到用户的 main()
入口函数。这一机制的运用,使得开发者无需关心底层系统初始化细节,专注于应用层逻辑的开发,显著提升了开发效率与代码的整洁度。
关于 $Sub$$
和 $Super$$
扩展功能的使用,详见 ARM® Compiler v5.06 for µVision®armlink User Guide。
这段代码详细地阐述了 RT-Thread 操作系统在 MDK-ARM 环境下的启动流程和关键组件的初始化过程。以下是润色后的版本,分别从不同角度出发:
components.c
文件中定义的 $Sub$$main
函数,肩负着启动 RT-Thread 的重任。
/* $Sub$$main 函数 */
int $Sub$$main(void)
{
rtthread_startup();
return 0;
}
它通过调用 rtthread_startup()
函数,分步骤完成系统初始化。
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* 板级初始化:需在该函数内部进行系统堆的初始化 */
rt_hw_board_init();
/* 打印 RT-Thread 版本信息 */
rt_show_version();
/* 定时器初始化 */
rt_system_timer_init();
/* 调度器初始化 */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* 信号初始化 */
rt_system_signal_init();
#endif
/* 由此创建一个用户 main 线程 */
rt_application_init();
/* 定时器线程初始化 */
rt_system_timer_thread_init();
/* 空闲线程初始化 */
rt_thread_idle_init();
/* 启动调度器 */
rt_system_scheduler_start();
/* 不会执行至此 */
return 0;
}
具体流程如下:
-
关闭中断: 首先通过
rt_hw_interrupt_disable()
关闭全局中断,为后续初始化创造一个稳定的环境。 -
板级初始化 (
rt_hw_board_init()
):- 配置系统时钟,提供系统心跳。
- 初始化串口,并将系统的标准输入输出重定向至此串口,用于后续的调试信息输出。
- 关键: 在此函数内完成系统堆的初始化,为动态内存分配提供基础。
-
内核对象初始化:
rt_show_version()
:打印 RT-Thread 版本信息,方便调试。rt_system_timer_init()
:初始化系统定时器,用于时间管理。rt_system_scheduler_init()
:初始化调度器,负责线程的调度与切换。rt_system_signal_init()
(可选):如果启用了信号机制 (RT_USING_SIGNALS
),则初始化信号。
-
创建用户
main
线程 (rt_application_init()
): 创建名为main
的线程,作为用户应用程序的入口点。用户可以在main
线程中对各个模块进行初始化。int main(void) { /* user app entry */ return 0; }
-
系统线程初始化:
rt_system_timer_thread_init()
:初始化定时器线程,处理系统定时器事件。rt_thread_idle_init()
:初始化空闲线程,当没有其他就绪线程时运行。
-
启动调度器 (
rt_system_scheduler_start()
): 启动 RT-Thread 的调度器,开始按照优先级调度就绪线程。调度器一旦启动,控制权将不再返回到rtthread_startup()
函数。
RT-Thread 程序内存分布
嵌入式系统中,MCU 的存储空间通常分为片内 Flash (类似硬盘) 和片内 RAM (类似内存)。程序编译后,会被划分为 Code、RO-data、RW-data 和 ZI-data 四个段,分别存储在 Flash 或 RAM 中。
Keil 编译后会给出程序占用空间信息,
linking...
Program Size: Code=48008 RO-data=5660 RW-data=604 ZI-data=2124
After Build - User command \#1: fromelf --bin.\\build\\rtthread-stm32.axf--output rtthread.bin
".\\build\\rtthread-stm32.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:07
上面提到的Program Size
包含下面几部分。
- Code: 代码段,存储程序指令。
- RO-data: 只读数据段,存储常量。
- RW-data: 已初始化数据段,存储初始值非零的全局变量。
- ZI-data: 未初始化数据段,存储未初始化或初始化为零的全局变量。
编译工程时生成的 .map
文件详细记录了各段的大小和地址。
Total RO Size (Code + RO Data) 53668 ( 52.41kB)
Total RW Size (RW Data + ZI Data) 2728 ( 2.66kB)
Total ROM Size (Code + RO Data + RW Data) 53780 ( 52.52kB)
其中:
- RO Size (Code + RO-data): 程序占用的 Flash 大小。
- RW Size (RW-data + ZI-data): 运行时占用的 RAM 大小。
- ROM Size (Code + RO-data + RW-data): 烧录程序占用的 Flash 大小。
在程序运行前,需将可执行映像文件(通常为 bin 或 hex 格式)烧录至 STM32 的 Flash 存储器中。该映像文件包含了 RO 段(含 Code 及 RO-data)和 RW 段(含 RW-data)。由于 ZI-data 段内容全为零,为节省存储空间,映像文件中并未包含该段数据。烧录后,Flash 中的内存分布如下图左侧所示。
STM32 启动后,会将 RW-data (初始化的全局变量)从 Flash 搬运到 RAM,并在 RAM 中分配 ZI-data 区域并清零。程序代码则直接在 Flash 中执行。
在 RT-Thread 中,未使用的 RAM 空间被用作动态内存堆,供 rt_malloc
等函数进行动态内存分配。
例如:
rt_uint8_t* msg_ptr;
msg_ptr = (rt_uint8_t*) rt_malloc (128);
rt_memset(msg_ptr, 0, 128);
代码中的 msg_ptr 指针指向的 128 字节内存空间位于动态内存堆空间中。
申请的内存就位于堆中。全局变量则根据是否初始化,分别存储在 RW-data (已初始化) 或 ZI-data (未初始化) 中,而常量存储在 RO-data 中。
例如:
#include <rtthread.h>
const static rt_uint32_t sensor_enable = 0x000000FE;// 存储于 RO-data 段
rt_uint32_t sensor_value;// 存储于 ZI-data 段
rt_bool_t sensor_inited = RT_FALSE;// 存储于 RW-data 段
void sensor_init()
{
/* ... */
}
sensor_value 未初始化,存储在 ZI-data 中,系统启动后会被自动清零;sensor_inited 具有初始值,存储在 RW-data 中。
RT-Thread 自动初始化机制
自动初始化机制
RT-Thread 提供了一种便捷的自动初始化机制。开发者无需手动调用初始化函数,只需在函数定义处使用特定的宏进行声明,系统便会在启动过程中自动执行这些函数。
以串口驱动的初始化为例,我们可以在初始化函数后使用 INIT_BOARD_EXPORT
宏来声明该函数需要被自动调用:
int rt_hw_usart_init(void) /* 串口初始化函数 */
{
... ...
/* 注册串口 1 设备 */
rt_hw_serial_register(&serial1, "uart1",
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
uart);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init); /* 使用组件自动初始化机制 */
这样,rt_hw_usart_init()
函数就会在系统启动时被自动调用。那么,具体是在哪个阶段被调用的呢?
在系统启动流程中,rt_components_board_init()
和 rt_components_init()
这两个函数负责执行自动初始化。它们会依次调用通过不同宏声明的初始化函数,这些函数被标记在流程图中带底色的方框内,包括:
- board init functions: 通过
INIT_BOARD_EXPORT(fn)
声明的函数。 - pre-initialization functions: 通过
INIT_PREV_EXPORT(fn)
声明的函数。 - device init functions: 通过
INIT_DEVICE_EXPORT(fn)
声明的函数。 - components init functions: 通过
INIT_COMPONENT_EXPORT(fn)
声明的函数。 - enviroment init functions: 通过
INIT_ENV_EXPORT(fn)
声明的函数。 - application init functions: 通过
INIT_APP_EXPORT(fn)
声明的函数。
rt_components_board_init()
函数执行时机较早,主要负责硬件相关的初始化。它会遍历 INIT_BOARD_EXPORT(fn)
声明的函数表并依次调用。
rt_components_init()
函数则在系统启动并在 main
线程中被调用,此时硬件环境和操作系统均已初始化完毕,可以执行应用程序相关的初始化代码。rt_components_init()
函数负责遍历并调用通过其他几个宏声明的初始化函数。
RT-Thread 的自动初始化机制巧妙地利用了自定义的 RTI (RT-Thread Init) 符号段。这些需要在启动时调用的初始化函数指针会被放入该段,组成一个初始化函数表。系统启动时会遍历此表并依次调用其中的函数,从而实现自动初始化的目的。
下表详细列出了用于实现自动初始化的宏接口:
初始化顺序 | 宏接口 | 描述 |
---|---|---|
1 | INIT_BOARD_EXPORT(fn) | 非常早期的初始化,此时调度器还未启动,主要负责硬件初始化 |
2 | INIT_PREV_EXPORT(fn) | 纯软件的初始化,没有太多依赖的函数 |
3 | INIT_DEVICE_EXPORT(fn) | 外设驱动初始化,例如网卡设备 |
4 | INIT_COMPONENT_EXPORT(fn) | 组件初始化,例如文件系统或 LWIP |
5 | INIT_ENV_EXPORT(fn) | 系统环境初始化,例如挂载文件系统 |
6 | INIT_APP_EXPORT(fn) | 应用初始化,例如 GUI 应用 |
开发者只需在初始化函数定义后使用相应的宏进行声明,例如 INIT_BOARD_EXPORT(rt_hw_usart_init)
。链接器会自动收集所有声明,并将它们放入 RTI 符号段。该符号段位于内存的 RO 段中,其中的函数会在系统初始化时被自动调用。这使得开发者无需手动管理初始化函数的调用顺序,简化了开发流程,提高了代码的可维护性。
RT-Thread 内核对象模型
静态对象和动态对象
RT-Thread 内核采用面向对象的设计思想,将系统级的基础设施抽象为内核对象,例如线程、信号量、互斥量、定时器等。内核对象分为两类:
- 静态内核对象: 编译时分配内存空间,通常存储在 RW-data 或 ZI-data 段中,在系统启动后进行初始化。
- 动态内核对象: 运行时从内存堆中动态创建,并在使用前手动初始化。
以下代码展示了静态线程和动态线程的创建和使用:
/* 线程 1 的对象和运行时用到的栈 */
static struct rt_thread thread1;
static rt_uint8_t thread1_stack[512];
/* 线程 1 入口 */
void thread1_entry(void* parameter)
{
// ... 线程 1 的执行代码 ...
}
/* 线程 2 入口 */
void thread2_entry(void* parameter)
{
// ... 线程 2 的执行代码 ...
}
/* 线程例程初始化 */
int thread_sample_init()
{
rt_thread_t thread2_ptr;
rt_err_t result;
/* 初始化线程 1 (静态线程) */
result = rt_thread_init(&thread1,
"thread1",
thread1_entry, RT_NULL,
&thread1_stack[0], sizeof(thread1_stack),
200, 10);
if (result == RT_EOK) rt_thread_startup(&thread1);
/* 创建线程 2 (动态线程) */
thread2_ptr = rt_thread_create("thread2",
thread2_entry, RT_NULL,
512, 250, 25);
if (thread2_ptr != RT_NULL) rt_thread_startup(thread2_ptr);
return 0;
}
在这个例子中,thread1
是一个静态线程对象,其控制块 thread1
和栈空间 thread1_stack
的内存在编译阶段就已经确定,由于它们没有初始值,因此会被分配到未初始化数据段 (ZI-data) 中。而 thread2
是一个动态线程对象,它的线程控制块和栈空间都是在运行时动态分配的。
静态对象直接占用 RAM 空间,不依赖于内存堆管理器,内存分配时间是固定的。动态对象则依赖于内存堆管理器,在运行时申请 RAM 空间,并在对象被删除后释放所占用的 RAM 空间。两种方式各有优劣,应根据实际应用场景进行选择。
内核对象管理架构
RT-Thread 通过一个内核对象管理系统来统一访问和管理所有内核对象,包括静态对象和动态对象。这种设计使得 RT-Thread 不依赖于具体的内存分配方式,提高了系统的灵活性。
内核对象涵盖了 RT-Thread 中的大部分内核设施,例如线程、信号量、互斥量、事件、邮箱、消息队列、定时器、内存池和设备驱动等。每种类型的内核对象都由一个对象容器进行管理,容器中记录了对象的类型、大小等信息,并维护一个对象链表。所有该类型的内核对象都链接到这个链表上。
下图展示了 RT-Thread 中内核对象的派生和继承关系。每种具体的内核对象都在基类对象 rt_object
的基础上进行扩展,添加了自身的特有属性。例如,线程对象在 rt_object
的基础上增加了状态、优先级等属性。这些扩展属性只在与具体对象相关的操作中才会用到。从面向对象的角度来看,每种具体对象都是从抽象对象 rt_object
派生而来,继承了其通用属性,并扩展了自己的特有属性。
如上图所示,从 rt_object
直接派生出了线程、内存池、定时器、设备以及 IPC 等核心对象类型。而 IPC 对象,作为线程间通信与同步的抽象层,又衍生出了信号量、互斥量、事件、邮箱、消息队列和信号等更为具体的机制。
这种面向对象的设计方法具有以下优点:
- 提高可重用性和扩展性: 添加新的对象类型非常容易,只需从
rt_object
派生并添加少量扩展属性即可。 - 提供统一的对象操作方式: 简化了对象操作,提高了系统的可靠性。
对象控制块
内核对象控制块 rt_object
的数据结构定义如下:
struct rt_object
{
char name[RT_NAME_MAX]; /* 内核对象名称 */
rt_uint8_t type; /* 内核对象类型 */
rt_uint8_t flag; /* 内核对象的参数 */
rt_list_t list; /* 内核对象管理链表 */
};
目前 RT-Thread 支持的内核对象类型如下:
enum rt_object_class_type
{
RT_Object_Class_Thread = 0, /* 线程 */
#ifdef RT_USING_SEMAPHORE
RT_Object_Class_Semaphore, /* 信号量 */
#endif
#ifdef RT_USING_MUTEX
RT_Object_Class_Mutex, /* 互斥量 */
#endif
#ifdef RT_USING_EVENT
RT_Object_Class_Event, /* 事件 */
#endif
#ifdef RT_USING_MAILBOX
RT_Object_Class_MailBox, /* 邮箱 */
#endif
#ifdef RT_USING_MESSAGEQUEUE
RT_Object_Class_MessageQueue, /* 消息队列 */
#endif
#ifdef RT_USING_MEMPOOL
RT_Object_Class_MemPool, /* 内存池 */
#endif
#ifdef RT_USING_DEVICE
RT_Object_Class_Device, /* 设备 */
#endif
RT_Object_Class_Timer, /* 定时器 */
#ifdef RT_USING_MODULE
RT_Object_Class_Module, /* 模块 */
#endif
RT_Object_Class_Unknown, /* 未知类型 */
RT_Object_Class_Static = 0x80 /* 静态对象标识 */
};
静态对象的类型标识的最高位为 1 (即 RT_Object_Class_Static
与其他对象类型进行或操作的结果),否则为动态对象。系统支持的最大对象类型数量为 127。
内核对象管理方式
内核对象容器 rt_object_information
的数据结构定义如下:
struct rt_object_information
{
enum rt_object_class_type type; /* 对象类型 */
rt_list_t object_list; /* 对象链表 */
rt_size_t object_size; /* 对象大小 */
};
每种对象类型都由一个 rt_object_information
结构体进行管理。该类型的每个对象实例都通过 object_list
链表链接起来。object_size
字段记录了该类型对象实例的内存块大小(同一类型的对象实例大小相同)。
初始化对象
在使用静态对象之前,必须先对其进行初始化。
void rt_object_init(struct rt_object* object,
enum rt_object_class_type type,
const char* name);
参数 | 描述 |
---|---|
object | 需要初始化的对象指针,它必须指向具体的对象内存块,而不能是空指针或野指针 |
type | 对象的类型,必须是 rt_object_class_type 枚举类型中列出的除 RT_Object_Class_Static 以外的类型(对于静态对象,或使用 rt_object_init 接口进行初始化的对象,系统会把它标识成 RT_Object_Class_Static 类型) |
name | 对象的名字。每个对象可以设置一个名字,这个名字的最大长度由 RT_NAME_MAX 指定,并且系统不关心它是否是由’\0 ’做为终结符 |
该函数将对象添加到对象容器中进行管理,初始化对象参数,并将其插入到对象容器的链表中。
脱离对象
将静态对象从内核对象管理器中移除。
void rt_object_detach(rt_object_t object);
该函数将对象从对象容器链表中移除,但不会释放对象占用的内存。
分配对象
动态分配一个指定类型的对象。
rt_object_t rt_object_allocate(enum rt_object_class_type type,
const char* name);
参数 | 描述 |
---|---|
type | 分配对象的类型,只能是 rt_object_class_type 中除 RT_Object_Class_Static 以外的类型。并且经过这个接口分配出来的对象类型是动态的,而不是静态的 |
name | 对象的名字。每个对象可以设置一个名字,这个名字的最大长度由 RT_NAME_MAX 指定,并且系统不关心它是否是由’\0 ’做为终结符 |
返回 | —— |
分配成功的对象句柄 | 分配成功 |
RT_NULL | 分配失败 |
该函数根据对象类型从内存堆中分配相应大小的内存空间,初始化对象,并将其添加到对象容器链表中。
删除对象
删除一个动态对象并释放其占用的内存空间。
void rt_object_delete(rt_object_t object);
参数 | 描述 |
---|---|
object | 对象的句柄 |
该函数将对象从对象容器链表中移除,并释放其占用的内存空间。
辨别对象
判断一个对象是否为系统对象(即静态对象)。
rt_err_t rt_object_is_systemobject(rt_object_t object);
参数 | 描述 |
---|---|
object | 对象的句柄 |
在 RT-Thread 中,系统对象即静态对象,其类型标识的 RT_Object_Class_Static
位为 1。
如何遍历内核对象
注意:
RT-Thread5.0 及更高版本将
struct rt_thread
结构体的 name 成员移到了 parent 里,使用时代码需要由thread->name
更改为thread->parent.name
,否则编译会报错!
以下代码展示了如何遍历所有线程:
rt_thread_t thread = RT_NULL;
struct rt_list_node *node = RT_NULL;
struct rt_object_information *information = RT_NULL;
information = rt_object_get_information(RT_Object_Class_Thread);
rt_list_for_each(node, &(information->object_list))
{
thread = (rt_thread_t)rt_list_entry(node, struct rt_object, list);
/* 打印线程名称 */
rt_kprintf("name:%s\n", thread->parent.name); // 注意:RT-Thread 5.0 及以上版本,name 成员移到了 parent 中
}
以下代码展示了如何遍历所有互斥量:
rt_mutex_t mutex = RT_NULL;
struct rt_list_node *node = RT_NULL;
struct rt_object_information *information = RT_NULL;
information = rt_object_get_information(RT_Object_Class_Mutex);
rt_list_for_each(node, &(information->object_list))
{
mutex = (rt_mutex_t)rt_list_entry(node, struct rt_object, list);
/* 打印互斥量名称 */
rt_kprintf("name:%s\n", mutex->parent.parent.name);
}
RT-Thread 内核配置示例
RT-Thread 的一个显著特点是其高度的可裁剪性。用户可以灵活地对内核和组件进行精细调整,以满足不同应用场景的需求。
RT-Thread 的配置主要通过修改工程目录下的 rtconfig.h
文件来实现。rtconfig.h
文件中包含了一系列宏定义,用户可以通过开启或关闭这些宏定义来控制代码的条件编译,从而实现对系统的配置和裁剪。
注意:
在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。
rtconfig.h
文件中的配置选项主要包括以下几个方面:
(1) 内核配置
/* 内核对象名称的最大长度。超过该长度的对象名称将被截断。*/
#define RT_NAME_MAX 8
/* 字节对齐的基准大小。常与 ALIGN(RT_ALIGN_SIZE) 配合使用进行字节对齐。*/
#define RT_ALIGN_SIZE 4
/* 系统线程优先级数。通常用 RT_THREAD_PRIORITY_MAX - 1 定义空闲线程的优先级。*/
#define RT_THREAD_PRIORITY_MAX 32
/* 系统时钟节拍 (Tick) 频率,单位为 Hz。此处定义为 100Hz,即每个 Tick 为 10ms。*/
#define RT_TICK_PER_SECOND 100
/* 启用栈溢出检查。取消定义则关闭该功能。*/
#define RT_USING_OVERFLOW_CHECK
/* 启用调试模式。取消定义则关闭该功能。*/
#define RT_DEBUG
/* 启用调试模式时,打印组件初始化信息。设置为 0 则关闭。*/
#define RT_DEBUG_INIT 0
/* 启用调试模式时,打印线程切换信息。设置为 0 则关闭。*/
#define RT_DEBUG_THREAD 0
/* 启用钩子函数。取消定义则关闭该功能。*/
#define RT_USING_HOOK
/* 空闲线程的栈大小。*/
#define IDLE_THREAD_STACK_SIZE 256
(2) 线程间同步与通信机制配置
这部分配置用于控制是否启用 RT-Thread 提供的线程间同步与通信机制,包括信号量、互斥量、事件集、邮箱、消息队列和信号。
/* 启用信号量。取消定义则关闭。*/
#define RT_USING_SEMAPHORE
/* 启用互斥量。取消定义则关闭。*/
#define RT_USING_MUTEX
/* 启用事件集。取消定义则关闭。*/
#define RT_USING_EVENT
/* 启用邮箱。取消定义则关闭。*/
#define RT_USING_MAILBOX
/* 启用消息队列。取消定义则关闭。*/
#define RT_USING_MESSAGEQUEUE
/* 启用信号。取消定义则关闭。*/
#define RT_USING_SIGNALS
(3) 内存管理配置
/* 启用静态内存池。*/
#define RT_USING_MEMPOOL
/* 启用多内存堆拼接功能。取消定义则关闭。*/
#define RT_USING_MEMHEAP
/* 启用小内存管理算法。*/
#define RT_USING_SMALL_MEM
/* 关闭 SLAB 内存管理算法。默认开启,此处被注释掉。*/
/* #define RT_USING_SLAB */
/* 启用动态内存堆。*/
#define RT_USING_HEAP
(4) 内核设备对象配置
/* 启用设备驱动框架。*/
#define RT_USING_DEVICE
/* 启用控制台设备。取消定义则关闭。*/
#define RT_USING_CONSOLE
/* 控制台设备的缓冲区大小。*/
#define RT_CONSOLEBUF_SIZE 128
/* 控制台设备的名称。*/
#define RT_CONSOLE_DEVICE_NAME "uart1"
(5) 自动初始化配置
/* 启用自动初始化机制。取消定义则关闭。*/
#define RT_USING_COMPONENTS_INIT
/* 设置应用入口为 main 函数。*/
#define RT_USING_USER_MAIN
/* main 线程的栈大小。*/
#define RT_MAIN_THREAD_STACK_SIZE 2048
(6) FinSH 配置 (命令行交互工具)
/* 启用 FinSH 调试工具。取消定义则关闭。*/
#define RT_USING_FINSH
/* FinSH 线程名称。*/
#define FINSH_THREAD_NAME "tshell"
/* 启用 FinSH 历史命令功能。*/
#define FINSH_USING_HISTORY
/* FinSH 历史命令条数。*/
#define FINSH_HISTORY_LINES 5
/* 启用 FinSH 的 Tab 键自动补全功能。取消定义则关闭。*/
#define FINSH_USING_SYMTAB
/* FinSH 线程的优先级。*/
#define FINSH_THREAD_PRIORITY 20
/* FinSH 线程的栈大小。*/
#define FINSH_THREAD_STACK_SIZE 4096
/* FinSH 命令的最大长度。*/
#define FINSH_CMD_SIZE 80
/* 启用 FinSH 的 MSH (Module Shell) 模式。*/
#define FINSH_USING_MSH
/* 启用 MSH 模式时,默认进入 MSH 模式。*/
#define FINSH_USING_MSH_DEFAULT
/* 仅使用 MSH 模式。*/
#define FINSH_USING_MSH_ONLY
(7) 硬件平台相关配置
/* 指定 MCU 型号为 STM32F103ZE。*/
#define STM32F103ZE
/* 外部高速晶振频率 (HSE),单位为 Hz。*/
#define RT_HSE_VALUE 8000000
/* 启用 UART1。*/
#define RT_USING_UART1
通过对 rtconfig.h
文件中上述宏定义的灵活配置,开发者可以根据实际应用需求定制 RT-Thread 内核和组件,构建出符合特定场景的嵌入式系统。这种高度可裁剪的特性使得 RT-Thread 能够适应从资源受限的小型设备到功能丰富的大型系统的各种应用。
常见宏定义说明
RT-Thread 的代码中广泛使用了各种宏定义,这些宏定义可以提高代码的可移植性、可读性和执行效率。以下列举了在 Keil 编译环境下一些常见的宏定义及其作用:
1. rt_inline
#define rt_inline static __inline
- 作用:
rt_inline
用于声明一个静态内联函数。static
关键字限定该函数的作用域仅限于当前文件,__inline
(在 Keil 中是__inline
,GCC 中是inline
) 则建议编译器在调用该函数时进行内联展开。 - 内联展开: 内联展开是指在编译阶段,将函数调用直接替换为函数体代码,从而避免了函数调用的开销(如参数传递、栈帧的创建和销毁等),提高了代码的执行效率。但是,过度使用内联可能会导致代码体积增大。
- 使用场景: 通常用于定义简短且频繁调用的函数。
2. RT_USED
(在 RT-Thread 5.0 及以上版本为 rt_used
)
#define RT_USED __attribute__((used))
- 作用:
RT_USED
用于告诉编译器,即使某个函数或变量在代码中没有被显式调用,也要保留它们,不要在优化过程中被丢弃。 - 使用场景: 这在 RT-Thread 的自动初始化机制等场景中非常有用。自动初始化机制利用了自定义的段来存放初始化函数的指针,而
RT_USED
可以确保这些自定义段中的代码不会被编译器优化掉。
3. RT_UNUSED
#define RT_UNUSED ((void)x)
- 作用:
RT_UNUSED
用于消除编译器对未使用变量或函数的警告。在函数参数中可以使用。比如一个函数的形参你并未使用,有些编译器会给出警告,这时可以RT_UNUSED(形参)
, 这样就不会警告了 - 使用场景: 当某个函数或变量确实不需要使用,但为了保持代码的完整性或兼容性而不能删除时,可以使用
RT_UNUSED
来避免编译警告。
4. RT_WEAK
(在 RT-Thread 5.0 及以上版本为 rt_weak
)
#define RT_WEAK __weak
- 作用:
RT_WEAK
用于声明一个弱函数。弱函数允许用户在应用程序中重新定义同名函数,而不会导致链接错误。链接器在链接时会优先选择非弱定义的函数,如果找不到,才会使用弱定义的函数。 - 使用场景: 这在库函数中非常有用,可以提供一个默认的函数实现,同时允许用户根据需要进行自定义。
5. ALIGN(n)
(在 RT-Thread 5.0 及以上版本为 rt_align
)
#define ALIGN(n) __attribute__((aligned(n)))
- 作用:
ALIGN(n)
用于指定变量或结构体的对齐方式。它指示编译器在分配内存时,将变量或结构体的起始地址对齐到n
字节的边界上(n
必须是 2 的幂次方)。 - 字节对齐的好处:
- 提高访问效率: 许多 CPU 访问对齐的数据更快。例如,如果一个 4 字节的整数的地址是 4 的倍数,那么 CPU 可能只需要一次内存访问就能读取它;否则,可能需要两次内存访问。
- 节省空间: 在某些情况下,合理的字节对齐可以减少结构体的大小,从而节省存储空间。
- 使用场景: 常用于结构体定义、变量声明等需要考虑内存对齐的场景。
6. RT_ALIGN(size, align)
#define RT_ALIGN(size, align) (((size) + (align) - 1) & ~((align) - 1))
- 作用:
RT_ALIGN(size, align)
用于计算将size
向上对齐到align
整数倍后的值。例如,RT_ALIGN(13, 4)
的结果是 16。 - 计算过程:
(size) + (align) - 1
: 将size
加上align - 1
。~((align) - 1)
: 将align - 1
按位取反。&
: 将第 1 步和第 2 步的结果进行按位与运算。
- 使用场景: 常用于内存分配、缓冲区大小计算等需要进行对齐计算的场景。
以上这些宏定义是 RT-Thread 代码中常用的工具,它们可以帮助开发者编写出更高效、更可移植、更易于维护的代码。理解这些宏定义的作用和用法,对于深入学习和使用 RT-Thread 非常有帮助。
标签:RT,rt,Thread,初始化,对象,线程,内核 From: https://blog.csdn.net/2301_79702866/article/details/144489430