RT-Thread文档读书笔记
- 内核框图

1.内核库是为了保证内核能够独立运行的一套小型的类似 C 库的函数实现子集。
- 线程调度
1.在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
2.最低优先级留给空闲线程使用。
3.调度器在寻找那些处于就绪状态的具有最高优先级的线程时,所经历的时间是恒定的。
- 时钟管理
1.根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以设置为 HARD_TIMER 模式或者 SOFT_TIMER 模式。
2.使用定时器定时回调函数(即超时函数),完成定时服务。
- 线程间通信
1.消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。
2.邮箱和消息队列的发送动作可安全用于中断服务例程中。因为消息队列空间不属于任何线程,归系统所有。
- 设备管理
RT-Thread 将 PIN、I2C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。实现了按名称访问的设备管理子系统,可按照统一的 API 界面访问硬件设备。在设备驱动接口上,根据嵌入式系统的特点,对不同的设备可以挂接相应的事件。当设备事件触发时,由驱动程序通知给上层的应用程序。
- 系统启动
1.启动流程图

2.MCU外设硬件初始化,在调度器运行之前$Sub$$main()中初始化
1 | void rt_hw_board_init(void) |
3.系统或自己实现的模块初始化,在调度器开始后,在mian线程入口main_thread_entry()中初始化
1 | /** |
int value = 555;
!!value二次取反:第一次!将value转换成bool值false,第一次!将bool value = false转换成true,确保value的值一定是bool值,只有两个值true/false。
- 内核对象模型
静态内核对象:存放在RW/ZI段中,在系统启动时对象被自动初始化。
动态内核对象:程序运行时在堆中创建,然后手工初始化。
静态对象会占用RAM空间,不依赖与内存管理器,内存分配时间确定。
动态对象则依赖内存管理器,分配对象时根据系统当时的内存情况决定分配的时间长短,分配好后可再被删除。
- 内核对象管理框架
内核对象管理系统管理所有的内核对象,不管是静态内核对象还是动态内核对象统一动过管理器由对象名字来访问内核对象。
内核对象容器(数组的元素,即内核对象链表头):

内核对象:
此类可作为基类被包含在其它对象中,从而扩展自己的属性,我们可以通过构建一个类(通常是一个device)继承该类,实例化后挂载到内核里面,内核就可以通过名字来找到我们自己创建的对象了。
1 | // 初始化一个静态的内核对象,要求对象已经存在空间 |
只要是位于静态空间的对象都会被rt_object_init()初始化为:RT_Object_Class_Static 对象类型,由此用于区分是否是静态对象或者动态对象。
- 常见宏定义
1 |
线程管理
- 线程
线程包括:线程控制块、线程栈、线程入口函数


线程初始化_rt_thread_init()时默认:

- 优先级
0为最高优先级,一般设置0-31,共32级,idle优先级设置为最低31,用户线程一般不设置优先级为31,设置0-30。

僵尸链表,线程退出是会自动调用rt_thread_exit()应该是放在函数入口lr寄存器,把当前线程假如到僵尸链表,杂空闲任务里面检测该链表并删除线程。
线程状态图

空闲任务钩子函数

钩子函数可以钩入功耗管理、喂狗等工作,因为必须保证空闲线程要一直处于就绪态,钩子函数里面不能出现任何会使线程挂起的操作。
时钟
- 定时器
硬件定时器:上下文是中断中,在定时器外设中断检查定时器是否超时,超时则执行超时函数。
软件定时器:上下文是线程中,创建一个线程专门检查软件定时器是否超时,超时则执行超时函数。
在硬件定时超时函数不应去挂起、等到等操作,不应去申请释放动态内存。

RT_TIMER_SKIP_LIST_LEVEL定时器跳表层数,默认跳表是一层,相当于不使用跳表算法。
- 硬件定时器链表和软件定时器链表

1 | // 硬件定时器初始化:初始化硬件定时器链表 |
单次定时器会在定时器超时后被从定时器链表中移除,而周期定时器会被从定时器移除后,再次按照定时器的超时键先后顺序重新插入定时器链表。
- 只支持小于 1 OS Tick的超短时间延时

两种进入临界区的方式:
1 | void rt_enter_critical(void); // 锁调度器方式,防止线程调度 |
线程间同步和互斥
- 信号量
信号量进行P操作释放一个信号量的时候,唤醒该信号量挂起链表上的第一个线程。
- 计数信号量
计数信号量适用于工作处理速度不匹配的场合,一般计数信号量也应配合互斥量混合使用对资源加锁访问。
- 互斥锁

线程与线程之间的互斥采用互斥锁,而中断与线程之间只能采用开关中断的方式进行互斥。IPC对象的内部实现时需要资源保护也使用开关中断的方式。
互斥锁的特点:
1.所有权:互斥量有所有权只能由同一个线程对互斥量加锁解锁,互斥量会被线程持有,所有权归于此线程,只有这个线程可以对互斥锁解锁,其它线程不可以。
2.递归性:互斥量可以多次获取(需多次释放),二值信号量不能多次获取否则线程会被死锁。
3.继承性:互斥量的持有线程会继承,尝试获取该互斥量比自己高优先级的线性的优先级。
持有互斥量的线程应尽快释放互斥量,且不能在持有互斥量期间手动改变线程的优先级,切记互斥量不能在中断服务函数中使用,因为互斥量只能属于某个线程。中断中若要进行资源保护就使用开关中断的方式。
- 事件集

1.特定事件唤醒线程。
2.任意事件唤醒线程。类似于linux的select()集合,可实现io多路复用。
3.多个事件发生时唤醒线程。
一对多:一个线程对应多个事件。
多对多:多个线程对应多个事件。
逻辑或:线程与任意事件发生同步。
逻辑与:线程与若干事件都发生同步。
事件无排队性:多次向同一线程发送相同事件效果和发送一次一样。
线程间通信
- 邮箱
非阻塞方式的邮件发送可用于中断,邮件的接收通常是以阻塞的方式接收所以不能用于中断。

suspend_sender_thread:发送邮箱的挂起等待线程链表。
邮箱创建时需动态分配一段空间用来存放邮件。
邮箱的使用场景:
1.一次只能发送小于等于4字节内容的情况。
2.发送一个缓冲区信息结构体的指针。
1 | // 缓存区信息结构体 |
- 消息队列
1 | struct rt_messagequeue |
消息队列可用于线程与线程、中断与线程之间通信,中断中发送消息必须为非阻塞方式。
消息队列维护着两条链表:已有消息链表(msg_queue_head 到 msg_queue_free)、空闲消息链表(msg_queue_free 到 msg_queue_tail),最开始msg_queue_head和msg_queue_tail都等于NULL,msg_queue_free包含这个内存池空间,每次入队从msg_queue_free取一个节点填充数据然后插入到msg_queue_tai位置。有第一条新消息时msg_queue_head = new_msg,以后就是msg_queue_head和msg_queue_tail不变,msg_queue_free移动。
1 | rt_err_t rt_mq_recv(rt_mq_t mq, |
同步消息场景:一个线程发送消息给另一个线程,另一个线程收到消息后要给线程发送一个ACK这个ACK可以使用邮箱或信号量。【消息队列 + 信号量或邮箱】
1 | struct msg |
- 信号
信号和信号量等的最大区别是信号是异步通信方式。
1 | struct rt_thread |
1.信号是在软件层次上对中断的一种模拟,线程收到信号 –> 执行创建信号时绑定好的回调函数
线程信号线的三种处理:1.类似中断执行指定的处理函数, 2.忽略信号,不做任何处理, 3.对信号处理保留系统的默认值,系统默认的处理方式。
2.当信号被传递给线程时,如果线程被挂起,线程改为就绪去处理对应信号。当线程正在运行收到信号,那么它会在当前线程栈的基础上建立新栈空间(类似与中断和线程的空间都是独立的)去处理对应的信号。使用的线程栈大小也会相应增加。
1 | rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t handler); // 安装信号,在线程给信号绑定一个回调函数 |
等待信号和信号量等没有区别,都是同步阻塞等待。
1 | // set: 指定等待的信号 |
本章小结:
1.不可以在中断中接收邮件,不可以在中断中以等待方式发送邮件。
2.不可以在中断中接收消息队列的消息。
3.邮箱的邮件必须使用全局、静态、动态申请形式,邮箱内容直接发送邮内容的指针。
4.消息队列消息内容可以使用局部形式,消息会被复制进消息队列的空间。
5.消息队列是一种异步通信方式,如接收方需告诉发送方已经接收到消息,可以通过邮箱或信号量的方式。
内存管理
实时操作系统内存管理的特定和要求:
1.分配内存块事件必须确定。
2.可自动处理内存碎片的产生,不能重启。
3.嵌入式系统需针对不同的硬件内存大小给出不同的内存管理算法。
小内存算法:针对内存小于2M
memheap内存算法:多内存池的算法,可将多个内存堆合并在一起用。
slab内存算法:多内存池的通用算法。
因为内存算法需考虑多线程间的互斥问题,所以不能在中断中分配或释放内存,否则可能引起当前线程被挂起等待。
- RT-Thread 将 “ZI 段结尾处” 到内存尾部的空间用作内存堆

1 | // __CC_ARM编译器(kile), ZI 段结尾处表示 |
- 小内存算法
1 |
|


mem.c 对应小内存分配算法,memheap.c对应有多个内存堆的算法。
- 内存池
1.malloc一块大内存,把这块内存分成指定数量的若干小块内存并用链表管理,空闲链表和使用的链表。
2.宏观来看内存池和消息队列类似,消息队列是对内存池的进一步封装。
1 | rt_mp_t rt_mp_create(const char *name, |

本章小结:
1.内存池可以极大的加快内存分配的速度,不用去遍历mem链表寻找合适大小的内存块。
2.从内存池分配的内存块大小是固定的。
中断
- Cortex-M 系列 CPU寄存器


Cortex-M 有两个运行级别,分别为特权级和用户级,线程模式可以工作在特权级或者用户级,而处理模式总工作在特权级,可通过 CONTROL 特殊寄存器控制。工作模式状态切换情况如上图所示。
Cortex-M 的堆栈寄存器 SP 对应两个物理寄存器 MSP 和 PSP,MSP 为主堆栈,PSP 为进程堆栈,处理模式总是使用 MSP 作为堆栈,线程模式可以选择使用 MSP 或 PSP 作为堆栈,同样通过 CONTROL 特殊寄存器控制。复位后,Cortex-M 默认进入线程模式、特权级、使用 MSP 堆栈
中断前导程序
中断前导程序主要工作如下:
1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
2)通知内核进入中断状态,调用 rt_interrupt_enter() 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。
1 | void rt_interrupt_enter(void) // 通知内核进入中断状态,把全局变量 rt_interrupt_nest 加 1 |
中断后续程序
中断后续程序主要完成的工作是:
1 通知内核离开中断状态,通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1,代码如下所示。
1 | void rt_interrupt_leave(void) // 通知内核离开中断状态,将全局变量 rt_interrupt_nest 减 1 |
- 中断栈
1.在中断处理过程中,在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务程序进行中断响应、处理。
2.中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。
3.中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。
4.中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。
RT-Thread 采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。
- 中断分步处理
1.中段上半步:在中断中执行,负责取得硬件状态和数据,做好一些软件上的标记或者发送信号量、消息等,用来启动中断的下半部操作。
2.中断下半步:在线程中执行,线程收到中断发来的信号量、消息,进行具体的解析业务操作,此部分比较耗时。
- 全局中断开关
全局中断开关也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个线程主动放弃了处理器控制权。当需要关闭整个系统的中断时,可调用下面的函数接口:
1 | rt_base_t rt_hw_interrupt_disable(void); // 关中断 |
使用中断锁来操作临界区的方法可以应用于任何场合,且其他几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则会变成一种快速、高效的同步方式。
例如,为了保证一行代码(例如赋值)的互斥运行,最快速的方法是使用中断锁而不是信号量或互斥量:
1 | /* 关闭中断 */ |
在使用中断锁时,需要确保关闭中断的时间非常短,例如上面代码中的 a = a + value; 也可换成另外一种方式,例如使用信号量:
1 | /* 获得信号量锁 */ |
这段代码在 rt_sem_take 、rt_sem_release 的实现中,已经存在使用中断锁保护信号量内部变量的行为,所以对于简单如 a = a + value; 的操作,使用中断锁将更为简洁快速。
简单嵌套中断使用
1 |
|
使用 rt_interrupt_enter/leave() 的作用是,在中断服务程序中,如果调用了内核相关的函数(如释放信号量等操作),则可以通过判断当前中断状态,让内核及时调整相应的行为。例如:在中断中释放了一个信号量,唤醒了某线程,但通过判断发现当前系统处于中断上下文环境中,那么在进行线程切换时应该采取中断中线程切换的策略,而不是立即进行切换。
1 | void rt_interrupt_enter(void); // rt_interrupt_nest++ |
但如果中断服务程序不会调用内核相关的函数(释放信号量等操作),这个时候,也可以不调用 rt_interrupt_enter/leave() 函数。
补充:
FreeRTOS是将各种IPC函数区分为在中断中使用和在线程中使用的两种(显式方式),而不是通过告诉内核处于中断还是线程状态让内核自动执行对应的线程切换方法的隐式方式。
中断与轮询
当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。因为轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。相应的代码可以是这样的:
1 | /* 轮询模式向串口写入数据 */ |
在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所在的线程会一直运行,比它优先级低的线程都不会得到运行。而分时系统中,这点恰恰相反,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。
所以通常情况下,实时系统中更多采用的是中断模式来驱动外设。当数据达到时,由中断唤醒相关的处理线程,再继续进行后续的动作。例如一些携带 FIFO(包含一定数据量的先进先出队列)的串口外设,其写入过程可以是这样的,如下图所示:

线程先向串口的 FIFO 中写入数据,当 FIFO 满时,线程主动挂起。串口控制器持续地从 FIFO 中取出数据并以配置的波特率(例如 115200bps)发送出去。当 FIFO 中所有数据都发送完成时,将向处理器触发一个中断;当中断服务程序得到执行时,可以唤醒这个线程。这里举例的是 FIFO 类型的设备,在现实中也有 DMA 类型的设备,原理类似。
对于低速设备来说,运用这种模式非常好,因为在串口外设把 FIFO 中的数据发送出去前,处理器可以运行其他的线程,这样就提高了系统的整体运行效率(甚至对于分时系统来说,这样的设计也是非常必要)。但是对于一些高速设备,例如传输速度达到 10Mbps 的时候,假设一次发送的数据量是 32 字节,我们可以计算出发送这样一段数据量需要的时间是:(32 X 8) X 1/10Mbps = 25us。当数据需要持续传输时,系统将在 25us 后触发一个中断以唤醒上层线程继续下次传递。假设系统的线程切换时间是 8us(通常实时操作系统的线程上下文切换时间只有几个 us),那么当整个系统运行时,对于数据带宽利用率将只有 25/(25+8) =75.8%。但是采用轮询模式,数据带宽的利用率则可能达到 100%。这个也是大家普遍认为实时系统中数据吞吐量不足的缘故,系统开销消耗在了线程切换上(有些实时系统甚至会如本章前面说的,采用底半处理,分级的中断处理方式,相当于又拉长中断到发送线程的时间开销,效率会更进一步下降)。
通过上述的计算过程,我们可以看出其中的一些关键因素:发送数据量越小,发送速度越快,对于数据吞吐量的影响也将越大。归根结底,取决于系统中产生中断的频度如何。当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式:
1)增加每次数据量发送的长度,每次尽量让外设尽量多地发送数据;
2)必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。
注:由于关闭全局中断会导致整个系统不能响应中断,所以在使用关闭全局中断做为互斥访问临界区的手段时,必须需要保证关闭全局中断的时间非常短,例如运行数条机器指令的时间。
本章小结:
1.中断服务程序应该尽可能精简。
2.对于是否使用中断下半部处理,需要考虑中断服务下半部的的处理时间是否大于发送信号量的时间。
3.建议实时系统尽量使用中断的模式驱动外设,配合DMA缓冲使用,一次发送尽可能多的数据,然后尝试一次中断和线程切换8us,如需使用轮询应设置轮询线程较低的优先级,保证其他线程的运行时间。





















