RT-Thread--内核基础

2023-06-08,,

内核介绍

内核处于硬件层之上,内核部分包括内核库、实时内核实现。

实时内核的实现包括:对象管理、线程管理及调度器、线程间通信管理、时钟管理及内存管理等等,内核最小的资源占用情况是 3KB ROM,1.2KB RAM。

线程调度

线程是 RT-Thread 操作系统中最小的调度单位,线程调度算法是基于优先级的全抢占式多线程调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
支持 256 个线程优先级(也可通过配置文件更改为最大支持 32 个或 8 个线程优先级,针对 STM32 默认配置是 32 个线程优先级),0 优先级代表最高优先级,最低优先级留给空闲线程使用;
同时它也支持创建多个具有相同优先级的线程,相同优先级的线程间采用时间片的轮转调度算法进行调度,使每个线程运行相同时间;
系统也不限制线程数量的多少,线程数目只和硬件平台的具体内存相关。

时钟管理

RT-Thread 的时钟管理以时钟节拍为基础,时钟节拍是 RT-Thread 操作系统中最小的时钟单位。
RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止定时器否则将永远持续执行下去。

线程间同步

RT-Thread 采用信号量、互斥量与事件集实现线程间同步。
互斥量采用优先级继承的方式解决了实时系统常见的优先级翻转问题。
线程同步机制支持线程按优先级等待或按先进先出方式获取信号量或互斥量。线程通过对事件的发送与接收进行同步;事件集支持多事件的 “或触发” 和 “与触发”,适合于线程等待多个事件的情况。

线程间通信

RT-Thread 支持邮箱和消息队列等通信机制。邮箱中一封邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。
邮箱效率较消息队列更为高效。邮箱和消息队列的发送动作可安全用于中断服务例程中。
通信机制支持线程按优先级等待或按先进先出方式获取。

内存管理

RT-Thread 支持静态内存池管理及动态内存堆管理。
当静态内存池具有可用内存时,系统对内存块分配的时间将是恒定的;当静态内存池为空时,系统将申请内存块的线程挂起或阻塞掉 (即线程等待一段时间后仍未获得内存块就放弃申请并返回,或者立刻返回等待的时间取决于申请内存块时设置的等待时间参数),当其他线程释放内存块到内存池时,如果有挂起的待分配内存块的线程存在的话,则系统会将这个线程唤醒。
动态内存堆管理模块在系统资源不同的情况下,分别提供了面向小内存系统的内存管理算法及面向大内存系统的 SLAB 内存管理算法。

I/O 设备管理

RT-Thread 将 PIN、I2C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。实现了按名称访问的设备管理子系统,可按照统一的 API 界面访问硬件设备。
对不同的设备可以挂接相应的事件。当设备事件触发时,由驱动程序通知给上层的应用程序。

RT-Thread 启动流程

MDK裸机启动流程:系统启动后,先从汇编代码startup_stm32f103xe.s 开始运行,然后跳转到 C 代码的main()函数进行运行

 IMPORT  __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

RT-Thread在进入main()之前会进行RT-Thread系统功能初始化,

我们使用了 MDK 的扩展功能 $Sub$$$Super$$ 。可以给 main 添加 $Sub$$ 的前缀符号作为一个新功能函数 $Sub$$main ,这个 $Sub$$main 可以先调用一些要补充在 main 之前的功能函数(这里添加 RT-Thread 系统初始化功能),再调用 $Super$$main
转到 main() 函数执行,这样可以让用户不用去管 main() 之前的系统初始化操作。

T-Thread 支持多种平台和多种编译器,而 rtthread_startup() 函数是 RT-Thread 规定的统一入口点,所以 $Sub$$main 函数只需调用rtthread_startup() 函数即可;

在 components.c 的代码中找到rtthread_startup() 函数,我们看到 RT-Thread 的启动流程如下图所示:

 /* components.c 中定义的这段代码*/
/* re-define main function */
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return ;
}
 int rtthread_startup(void)
{
rt_hw_interrupt_disable(); /* board level initialization
* NOTE: please initialize heap inside board initialization.
*/
rt_hw_board_init(); /* show RT-Thread version */
rt_show_version(); /* timer system initialization */
rt_system_timer_init(); /* scheduler system initialization */
rt_system_scheduler_init(); #ifdef RT_USING_SIGNALS
/* signal system initialization */
rt_system_signal_init();
#endif /* create init_thread */
rt_application_init(); /* timer thread initialization */
rt_system_timer_thread_init(); /* idle thread initialization */
rt_thread_idle_init(); /* start scheduler */
rt_system_scheduler_start(); /* never reach here */
return ;
}
 /* the system main thread */
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void); /* RT-Thread components initialization */
rt_components_init(); /* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
} void rt_application_init(void)
{
rt_thread_t tid; #ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
*, , );
RT_ASSERT(tid != RT_NULL);

rt_hw_board_init() 中完成系统时钟设置,为系统提供心跳、串口初始化,将系统输入输出终端绑定到这个串口,后续系统运行信息就会从串口打印出来。
main() 函数是 RT-Thread 的用户代码入口,用户可以在 main() 函数里添加自己的应用。

 int main(void)
{
/* user app entry */
return ;
}

RT-Thread 程序内存分布

一般 MCU 包含的存储空间有:片内 Flash 与片内 RAM,RAM 相当于内存,Flash 相当于硬盘。编译器会将一个程序分类为好几个部分,分别存储在 MCU 不同的存储区。

Code:代码段,存放程序的代码部分;
RO-data:只读数据段,存放程序中定义的常量;
RW-data:读写数据段,存放初始化为非 0 值的全局变量;
ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量;

RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小;
RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小;
ROM Size 包含了 Code、RO Data 以及 RW Data,表示烧写程序所占用的 Flash 空间的大小;

程序运行之前,需要有文件实体被烧录到 STM32 的 Flash 中,一般是 bin 或者 hex 文件,该被烧录文件称为可执行映像文件。如图下图中左图所示,是可执行映像文件烧录到 STM32 后的内存分布,它包含 RO 段和 RW 段两个部分:其中 RO 段中保存了 Code、RO-data 的数据,RW 段保存了 RW-data 的数据,由于 ZI-data 都是 0,所以未包含在映像文件中。

STM32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段,并将这块 RAM 区域清零。

RT-Thread 自动初始化机制

自动初始化机制是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。

 int stm32_hw_usart_init(void)
{
struct stm32_uart *uart;
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; #ifdef RT_USING_UART1
uart = &uart1;
uart->UartHandle.Instance = USART1; serial1.ops = &stm32_uart_ops;
serial1.config = config; /* register UART1 device */
rt_hw_serial_register(&serial1,
"uart1",
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
uart);
#endif /* RT_USING_UART1 */
......
......
}
INIT_BOARD_EXPORT(stm32_hw_usart_init);

代码最后的 INIT_BOARD_EXPORT(stm32_hw_usart_init) 表示使用自动初始化功能,按照这种方式,stm32_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-Thread 的自动初始化机制使用了自定义 RTI 符号段,将需要在启动时进行初始化的函数指针放到了该段中,形成一张初始化函数表,在系统启动过程中会遍历该表,并调用表中的函数,达到自动初始化的目的。
用来实现自动初始化功能的宏接口定义详细描述如下

初始化函数主动通过这些宏接口进行申明,如 INIT_BOARD_EXPORT(rt_hw_usart_init),链接器会自动收集所有被申明的初始化函数,放到 RTI 符号段中,该符号段位于内存分布的 RO 段中,该 RTI符号段中的所有函数在系统初始化时会被自动调用。

RT-Thread 内核对象模型

静态对象和动态对象

静态内核对象和动态内核对象,静态内核对象通常放在RW 段和 ZI 段中,在系统启动后在程序中初始化;动态内核对象则是从内存堆中创建的,而后手工做初始化。

 static rt_uint8_t main_stack[];
struct rt_thread main_thread; void rt_application_init(void)
{
rt_thread_t tid; #ifdef RT_USING_HEAP
/*
* 线程名:main
* 线程入口:main_thread_entry
* 参数:RT_NULL
* 站空间大小:1024*100
* 优先级:0
* 时间片:20个OS Tick
*/
tid = rt_thread_create("main", main_thread_entry, RT_NULL,    //动态线程
*, , );
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result; tid = &main_thread;
/*
* 静态线程对象:main_thread
* 线程名:main
* 线程入口:main_thread_entry
* 参数:RT_NULL
* 线程起始地址:main_stack
* 线程堆栈大小:sizeof(main_stack)
* 优先级:RT_MAIN_THREAD_PRIORITY
* 时间片:20个OS Tick
*/
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL, //静态线程
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, );
RT_ASSERT(result == RT_EOK); /* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif rt_thread_startup(tid); //启动线程
}

main_thread 是一个静态线程对象,线程控制块 main_thread 与栈空间 main_stack 都是编译时决定的;
mian是一个动态线程对象,用到的空间是动态分配的,包括线程控制块和栈空间;
静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放;

内核对象管理架构

RT-Thread 采用内核对象管理系统来访问 / 管理所有内核对象,内核对象包含了内核中绝大部分设施,这些内核对象可以是静态分配的静态对象,也可以是从系统内存堆中分配的动态对象。
RT-Thread 内核对象包括:线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等。对象容器中包含了每类内核对象的信息,包括对象类型,大小等。对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上

RT-Thread 中各类内核对象的派生和继承关系

rt_object 派生出来的有:线程对象、内存池对象、定时器对象、设备对象和 IPC对象(IPC:Inter-Process Communication,进程间通信。在 RT-Thread 实时操作系统中,IPC 对象的作用是进行线程间同步与通信);由 IPC 对象派生出信号量、互斥量、事件、邮箱与消息队列、信号等对象。

对于每一种具体内核对象和对象控制块,除了基本结构外,还有自己的扩展属性(私有属性),例如,对于线程控制块,在基类对象基础上进行扩展,增加了线程状态、优先级等属性。这些属性在基类对象的操作中不会用到,只有在与具体线程相关的操作中才会使用。因此从面向对象的观点,可以认为每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性。

对象控制块

内核对象控制块的数据结构

 /**
* Base structure of Kernel object
*/
struct rt_object
{
char name[RT_NAME_MAX]; /**< name of kernel object 内核对象名称*/
rt_uint8_t type; /**< type of kernel object 内核对象类型*/
rt_uint8_t flag; /**< flag of kernel object 内核对象的参数*/ #ifdef RT_USING_MODULE
void *module_id; /**< id of application module 内核对象模型id*/
#endif
rt_list_t list; /**< list node of kernel object 内核对象管理链表*/
};
typedef struct rt_object *rt_object_t; /**< Type for kernel objects. */

内核对象支持的类型:

enum rt_object_class_type
{
RT_Object_Class_Thread = , /**< The object is a thread. 线程类型*/
RT_Object_Class_Semaphore, /**< The object is a semaphore. 信号量类型*/
RT_Object_Class_Mutex, /**< The object is a mutex. 互斥量类型*/
RT_Object_Class_Event, /**< The object is a event. 时间类型*/
RT_Object_Class_MailBox, /**< The object is a mail box. 邮箱类型*/
RT_Object_Class_MessageQueue, /**< The object is a message queue. 消息队列类型*/
RT_Object_Class_MemHeap, /**< The object is a memory heap 内存堆类型*/
RT_Object_Class_MemPool, /**< The object is a memory pool. 内存池类型*/
RT_Object_Class_Device, /**< The object is a device 设备类型*/
RT_Object_Class_Timer, /**< The object is a timer. 定时器类型*/
RT_Object_Class_Module, /**< The object is a module. 模块*/
RT_Object_Class_Unknown, /**< The object is unknown. 未知类型*/
RT_Object_Class_Static = 0x80 /**< The object is a static object. 静态类型*/
};

如果是静态对象,那么对象类型的最高位将是 1(是RT_Object_Class_Static 与其他对象类型的与操作),否则就是动态对象,系统最多能够容纳的对象类别数目是 127 个。

内核对象管理方式

内核对象容器的数据结构

/**
* The information of the kernel object
*/
struct rt_object_information
{
enum rt_object_class_type type; /**< object class type */
rt_list_t object_list; /**< object list */
rt_size_t object_size; /**< object size */
};

一类对象由一个 rt_object_information 结构体来管理,每一个这类对象的具体实例都通过链表的形式挂接在 object_list 上。而这一类对象的内存块尺寸由 object_size 标识出来

初始化对象

在使用一个未初始化的静态对象前必须先对其进行初始化。初始化对象使用接口:

void rt_object_init(struct rt_object         *object,          //需要初始化的对象指针,它必须指向具体的对象内存块,而不能是空指针或野指针
enum rt_object_class_type type, //对象的类型,必须是 rt_object_class_type 枚举类型中列出的除RT_Object_Class_Static 以外的类型
const char *name) //对象的名字。每个对象可以设置一个名字,这个名字的最大长度由 RT_NAME_MAX指定

脱离对象

从内核对象管理器中脱离一个对象。脱离对象使用以下接口:

/**
* This function will detach a static object from object system,
* and the memory of static object is not freed.
*
* @param object the specified object to be detached.
*/
void rt_object_detach(rt_object_t object)

分配对象

对象初始化、脱离的接口,都是面向对象内存块已经有的情况下,而动态的对象则可以在需要时申请,不需要时释放出内存空间给其他应用使用。申请分配新的对象可以使用以下接口:

/**
* This function will allocate an object from object system
*
* @param type the type of object
* @param name the object name. In system, the object's name must be unique.
*
* @return object
*/
rt_object_t rt_object_allocate(enum rt_object_class_type type, const char *name)

删除对象

对于一个动态对象,当不再使用时,可以调用如下接口删除对象,并释放相应的系统资源:

/**
* This function will delete an object and release object memory.
*
* @param object the specified object to be deleted.
*/
void rt_object_delete(rt_object_t object)

当调用以上接口时,首先从对象容器链表中脱离对象,然后释放对象所占用的内存。

辨别对象

判断指定对象是否是系统对象(静态内核对象)。辨别对象使用以下接口:

/**
* This function will judge the object is system object or not.
* Normally, the system object is a static object and the type
* of object set to RT_Object_Class_Static.
*
* @param object the specified object to be judged.
*
* @return RT_TRUE if a system object, RT_FALSE for others.
*/
rt_bool_t rt_object_is_systemobject(rt_object_t object)

在 RT-Thread 操作系统中,一个系统对象也就是一个静态对象,对象类型标识上 RT_Object_Class_Static 位置位。通常使用 rt_object_init()  方式初始化的对象都是系统对象。

RT-Thread 内核配置示例

RT-Thread 的一个重要特性是高度可裁剪性,支持对内核进行精细调整,对组件进行灵活拆卸。配置主要是通过修改工程目录下的 rtconfig.h 文件来进行,用户可以通过打开 / 关闭该文件中的宏定义来对代码进行条件编译,最终达到系统配置和裁剪的目的。
在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。

/* RT-Thread 内核部分 */

#define RT_NAME_MAX 8                                    // 表示内核对象的名称的最大长度,若代码中对象名称的最大长度大于宏定义的长度,*多余的部分将被截掉。
#define RT_ALIGN_SIZE 4 // 字节对齐时设定对齐的字节个数。常使用 ALIGN(RT_ALIGN_SIZE) 进行字节对齐。
#define RT_THREAD_PRIORITY_32
#define RT_THREAD_PRIORITY_MAX 32 // 定义系统线程优先级数;通常用 RT_THREAD_PRIORITY_MAX-1 定义空闲线程的优先级
#define RT_TICK_PER_SECOND 1000 // 定义时钟节拍,为 1000 时表示 1000 个 tick 每秒,一个 tick 为 10ms
#define RT_USING_OVERFLOW_CHECK // 检查栈是否溢出,未定义则关闭
#define RT_USING_HOOK // 定义该宏使用 HOOK
#define RT_IDEL_HOOK_LIST_SIZE 4 // 定义HOOK链表大小
#define IDLE_THREAD_STACK_SIZE 1024 // 定义线程栈大小
#define RT_DEBUG // 定义该宏开启 debug 模式,未定义则关闭 /* 线程间同步与通信部分,该部分会使用到的对象有信号量、互斥量、事件、邮箱、消息队列、信号等。 */ #define RT_USING_SEMAPHORE // 定义该宏可开启信号量的使用,未定义则关闭
#define RT_USING_MUTEX // 定义该宏可开启互斥量的使用,未定义则关闭
#define RT_USING_EVENT // 定义该宏可开启事件集的使用,未定义则关闭
#define RT_USING_MAILBOX // 定义该宏可开启邮箱的使用,未定义则关闭
#define RT_USING_MESSAGEQUEUE // 定义该宏可开启消息队列的使用,未定义则关闭 /* 内存管理部分 */ #define RT_USING_MEMPOOL // 开启静态内存池的使用
#define RT_USING_MEMHEAP // 定义该宏可开启两个或以上内存堆拼接的使用,未定义则关闭
#define RT_USING_SMALL_MEM // 开启小内存管理算法
#define RT_USING_HEAP // 开启堆的使用 /* 内核设备对象 */ #define RT_USING_DEVICE // 表示开启了系统设备的使用
#define RT_USING_CONSOLE // 定义该宏可开启系统控制台设备的使用,未定义则关闭
#define RT_CONSOLEBUF_SIZE 128 // 定义控制台设备的缓冲区大小
#define RT_CONSOLE_DEVICE_NAME "uart1" // 控制台设备的名称 /* 自动初始化方式 */ #define RT_USING_COMPONENTS_INIT // 定义该宏开启自动初始化机制,未定义则关闭 /* Command shell */ #define RT_USING_FINSH // 定义该宏可开启系统 FinSH 调试工具的使用,未定义则关闭
#define FINSH_THREAD_NAME "tshell" // 开启系统 FinSH 时:将该线程名称定义为 tshell
#define FINSH_USING_HISTORY // 开启系统 FinSH 时:使用历史命令
#define FINSH_HISTORY_LINES 5 // 开启系统 FinSH 时:对历史命令行数的定义
#define FINSH_USING_SYMTAB // 开启系统 FinSH 时:定义该宏开启使用 Tab 键,未定义则关闭
#define FINSH_USING_DESCRIPTION // 开启系统 FinSH 时:定义该宏使用描述,未定义则关闭
#define FINSH_THREAD_PRIORITY 20 // 开启系统 FinSH 时:定义该线程的优先级
#define FINSH_THREAD_STACK_SIZE 2048 // 开启系统 FinSH 时:定义该线程的栈大小
#define FINSH_CMD_SIZE 80 // 开启系统 FinSH 时:定义命令字符长度 /* Device virtual file system */ #define RT_USING_DFS // 使用DFS文件系统
#define DFS_USING_WORKDIR // 使用工作目录
#define DFS_FILESYSTEMS_MAX 4 // 文件系统
#define DFS_FILESYSTEM_TYPES_MAX 4 // 文件系统类型
#define DFS_FD_MAX 8 // DF容器大小
#define RT_USING_DFS_ELMFAT // 使用elmfat文件系统

常见宏定义说明

RT-Thread 中经常使用一些宏定义,举例 Keil 编译环境下一些常见的宏定义:rtdef.h

rt_inline,定义如下,static 关键字的作用是令函数只能在当前的文件中使用;inline 表示内联,用static 修饰后在调用函数时会建议编译器进行内联展开。

#define rt_inline    static __inline

RT_USED,定义如下,该宏的作用是向编译器说明这段代码有用,即使函数中没有调用也要保留编译。例如 RT-Thread 自动初始化功能使用了自定义的段,使用 RT_USED 会将自定义的代码段保留。

#define RT_USED    __attribute__((used))

RT_UNUSED,定义如下,表示函数或变量可能不使用,这个属性可以避免编译器产生警告信息。

#define RT_UNUSED     __attribute__((unused))

RT_WEAK,定义如下,常用于定义函数,编译器在链接函数时会优先链接没有该关键字前缀的函数,如果找不到则再链接由 weak 修饰的函数

#define RT_WEAK    __weak

ALIGN(n),定义如下,作用是在给某对象分配地址空间时,将其存放的地址按照 n 字节对齐,这里 n 可取 2 的幂次方。字节对齐的作用不仅是便于 CPU 快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

#define ALIGN(n)    __attribute__((aligned(n)))

RT_ALIGN(size,align),定义如下,作用是将 size 提升为 align 定义的整数的倍数,例如,RT_ALIGN(13,4) 将返回 16。

#define RT_ALIGN(size, align)    (((size) + (align) - 1) & ~((align) - 1)) 

参考

《RT-Thread 编程指南》

RT-Thread--内核基础的相关教程结束。

《RT-Thread--内核基础.doc》

下载本文的Word格式文档,以方便收藏与打印。