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_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() 函数。
中断与轮询
当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。因为轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。相应的代码可以是这样的:
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,如需使用轮询应设置轮询线程较低的优先级,保证其他线程的运行时间。
内核移植
CPU架构移植
移植分成CPU架构移植和BSP(Board Support Package,板级支持包)两部分。
- 实现cpulib抽象:
主要文件:cpuport.c context_rvds.S
主要接口:
1 | /************** context_rvds.S 实现 ***************/ |
在中断服务函数中进程线程切换需要等到中断结束后才能进行切换,在线程中进行线程切换是马上进行切换。
在中断程序处理完事务之后,中断退出之前,检查 rt_thread_switch_interrupt_flag 变量(调用 rt_hw_context_switch_interrupt 函数时会标记该变量和设置from、to线程等待中断处理完成进行线性切换)如果为1,就根据 rt_interrupt_from_thread, rt_interrupt_to_thread 变量,进程一次上下文切换。
向上对内核提供一套统一的函数接口,包括全局中断开关函数、线程栈的初始化函数、上下文切换函数、时钟节拍配置和中断函数。
- 线程栈的初始化

僵尸线程:已经被系统删除,线程不属于任何ipc量,对调度器不可见但是它的资源还未被释放的线程叫僵尸线程。
僵尸线程链表:rt_thread_defunct。
- 线程栈图示
在线程创建时通过 rt_hw_stack_init 函数手动构建一个上下文内容,作为每个线程第一次执行的初始值 ,如下图:


Cortex-M处理器在线程上下文切换过程中,pendsv发生后,r0
psr 由硬件自动压栈/出栈,r4r11需程序手动压栈/出栈(由 rt_hw_context_switch 函数完成)。
- PSR特殊功能寄存器组包括:

特殊功能寄存器是按位来表示的。

- 实现时钟节拍
开启一个定时器中断,可以是systick也可以是rtc,但需要保证这个定时器无论什么时候都在运行就算设备休眠进入低功耗。
1 | void SysTick_Handler(void) |
BSP移植
板卡移植:CPU架构移植已经实现CPU芯片相关的移植,但还需要使RAM、GPIO、UART等外设工作才能建立操作系统运行的基本环境。
传统意义上的嵌入式计算机CPU和RAM是毫无关系的,不同的CPU可以配不同的RAM,只是MCU把CPU和RAM集成在了一个芯片,所以CPU架构移植不包括RAM的部分。
主要工作:
1.初始化CPU内部寄存器,设定RAM工作时序。
2.实现时钟驱动及中断控制器驱动,完善中断管理。
3.实现串口和GPIO驱动。
4.初始化动态内存堆,实现动态内存堆管理。
- 建立rtthread工程
1.拷贝rt-thread文件夹到工程文件夹,添加内核源码、cpulib层、board.c相关文件到工程,添加相关头文件目录。
- 实现时钟管理
1.在boar.c初始化滴答定时器配置中断,在中断函数中添加rt_rt_tick_increase();
- 实现控制台
1.初始化串口。
2.实现rt_hw_console_output()函数。
- 实现动态内存管理
1.在rt_hw_board_init()函数调用rt_system_heap_init()初始化系统堆内存。
最后运行结果:

Env辅助开发环境
支持使用不同编译器,如GCC、ARM_CC等
搭建项目工程
在BSP工程目录下使用:scons –dist命令在BSP目录下生成dist目录,这边是一个新羡慕的工程框架。其中包含rttread源码,不相关的BSP文件夹及libcpu都会被移除。
- 生成工程命令
1 | socons --target=mdk5 # 生成mdk5工程 |
- 根据生成的 makefile 直接使用 arm_gcc 编译器编译
1 | socons # 需arm平台的芯片 |
- 图形化系统配置
menuconfig是一种基于Kconfig的图形化配置工具。通过图形配置最终生成rtconfig.h配置文件,对系统进行配置、裁剪。

每次通过menconfig图形配置界面配置删除或添加软件包后,需要使用 scons –update命令更新软件包。
- 软件包管理
1 | pkgs --upgrade # 同步git服务器上的软件包 |
- 修改工程模板
如果要修改MCU型号,工程配置等,建议直接修改 template 工程,再使用 socons –target=mdk5 重新生成工程。
scons 是按照模板工程生成工程的。
FinSH控制台
- 传统命令行模式(默认)
又称msh(module shell),类似与bash/dos。
- C语言解释器模式(占用空间大)
又称C-style模式,此模式下控制台能解析并执行大部分C语言的表达式,并使用类似C语言的函数调用方式访问系统中的函数及全局变量,也能够通过命令行创建变量。
1 | list_thread() # 打印系统中所有的线程,C-style模式命令必须携带括号 |
- msh自定义命令导入
1 | MSH_CMD_EXPORT(name, desc); |
- C-style自定义命令导入
1 | FINSH_FUNCTION_EXPORT(name, desc); |
- C-style自定义变量导出
I/O设备管理
IO设备管理层框架从下往上一次共三层:设备驱动层 –> 设备驱动框架层 –> IO设备管理层
IO设备框架向下操作硬件,向上提供统一的一套接口给应用程序调用,使得应用程序不用关系具体的硬件操作,更换硬件执行修改设备驱动操作硬件相关的代码。
- IO设备管理层
1.实现了对设备驱动程序的封装,使程序完全脱离了具体的硬件。
2.提供给应用程序的接口:
1 | /************** device.c *******************/ |

3.提供给驱动程序实现的:
1 | // 驱动程序实例化一个设备对象,并通过此接口注册到IO设备管理层 |
- 设备框架层
1.对同类硬件设备驱动做抽象:将不同厂家的同类硬件设备驱动中相同的部分抽取出来。
2.将不同的部分留出接口,由驱动程序实现。
3.负责创建和注册IO设备(就是设备框架层将不同部分留出的接口,设备驱动层需要去实现这些接口,注册给设备框架层使用)。
4.通过IO管理层提供的接口,注册一个串口设备到IO管理层:
5.设备驱动框架是对同类设备的抽象(这一层可以没有),例如:显示设备和ssd1306的关系,显示设备可以是ssd1306或者lcd显示屏,但它们都属于显示设备都能够显示一个字母、显示一个数字。
设备框架层就是对同类设备的抽象,同类设备的一些共性。
4.不同芯片厂家的串口操作方式是不一样的,但他们肯定都有putc、getc、config的这些功能。
1 | /*************** serial.c ******************/ |

对于逻辑操作简单的设备,可以跳过设备框架层,直接注册到IO设备管理层,将设备框架层必要的东西直接在设备驱动层实现。
- 设备驱动层
通常是直接调用厂家提供的操作硬件寄存器函数
1.是一组直接驱使硬件工作的程序,实现访问硬件提供的功能。
2.设备框架层提供的接口,注册一个串口设备到设备框架层。
硬件相关的:配置具体的外设、读、写等,硬件的功能实现函数。
这些硬件相关的操作函数都将被,驱动层使用注意不是调用(在驱动层封装成硬件设备对象的方法,再由驱动层注册到IO设备层),所以IO设备驱动层对设备对象的操作相当于直接调用了这些函数。
以上的这些层都是为应用程序服务的,宏观上这些层都可以叫做驱动,驱动只提供方法不会操作硬件,只有应用程序里面才会调用这些设备对象的方法操作硬件,可能进行设备初始化、打开、关闭、读、写,根据设备提供的方法不一样不一定所有的设备都有这些方法。
1 | /******************** drv_uart.c **************************/ |

- 应用程序
例如shell.c是一个应用程序,它和用户直接交互
应用程序:实现产品具体功能的代码,与用户交互的、显示用的、一个触摸的键盘,分别获取用户的输入做出反应、输出信息给用户看、得到用户的输入。
1 | /******************** shell.c ****************************/ |
从上面的代码可以看出,只有应用程序才会去操作硬件,驱动程序只提供操作硬件的方法。
小结:
分层思想的原则:
1.确定本层的职责,搞清承上启下的对象是谁。
2.实现本层职责时,提供好给上层调用的接口和对象。
3.如果超出本层职责(操作过于细节),那么提供注册函数给自己的下层调用,让下层提供一个实现具体细节的方法给自己使用。
4.一般方法和信息都是通过对象的形式在层与层之间传递。
- I/O设备对象
1 | // 设备类型 |
常见设备:字符设备:允许非结构数据传输,串行传输一次一个字节,块设备:每次传输一个数据块,如512byte,数据块是硬件强制性的,通常情况下操作,先读一个块数据,改变之中的一部分数据然后再写回去。
- 访问IO设备

设备的多种打开方式:
1 |
如果上层应用程序需要设置设备的接收回调函数,则必须以 RT_DEVICE_FLAG_INT_TX 或者 RT_DEVICE_FLAG_DMA_RX 的方式打开设备,否则不会回调函数。
- 打开IO设备
1 | // 该函数主要执行 dev->open(); |

rt_device_open() 和 rt_device_close() 需成对使用。
- 控制设备
1 | // 该函数主要执行 dev->control(); |
- 设备读写
1 | // pos:根据设备的不同有不同的意义,读串口设备底层会忽略这个参数,pos参数没有一样 |
- 数据收发回调
接收回调:
1 | // 设置接收数据回调:设置一个回调函数,当硬件设备接收到数据时, |
发送回调:
1 | // 设置发送完数据回调:当硬件设备发送完数据,驱动程序回调这个函数并把发送完成的数据块地址buffer作为参数传递给上层应用, |
上面两个函数都是应用程序注册给底层硬件驱动回调用的,相当于是底层驱动异步通知应用程序执行的操作结果。是一种层与层之间的沟通方式,和底层注册到上层类似。
本章小结:
1.没有提供驱动的设备,或者自己另外需要添加的设备,需要根据驱动框架自己实现设备的方法。
2.动态创建的设备销毁时要记得释放设备控制块占用的内存。
1 | int xxx_writedata(uint8_t* buffer, size_t buff_size) |
通用外设接口
rtthread对常用的片内外设做了抽象,为同一类外设提供了通用接口,对于不同的MCU片内外设都可以使用同一套外设接口进行访问。
串口驱动框架
serial.c serial.h
1 | struct rt_serial_device |
举例说明下ops里面串口的配置方法:
1 | // control方法是可以说是多个方法的集合,它根据 cmd 的不同,可以执行不同的操作:比如:开中断、关中断、配置串口dma、配置dma中断等 |
串口设备使用步骤:
1.串口设备实例化一个串口驱动设备对象,并通过rt_hw_serial_register()接口注册到设备框架中。
2.串口设备驱动框架通过rt_device_register()接口将设备注册到IO设备管理中。
3.应用程序通过IO设备管理提供的接口访问串口设备硬件。
串口设备时序图:

1 | @startuml |
时序图软件:plantuml
1.驱动注册到驱动框架:注册设备的操作方法
1 | // 驱动注册到驱动框架 |
2.驱动框架注册到IO设备管理器:驱动框架设备挂载到内核对象链表
1 | // 驱动框架设备注册到IO设备管理器 |
注册设备时的flags参数:
1 |
驱动层到io设备管理层对象空间占用是一层一层减小

串口回调:
1 | // 在串口中断中回调 |
SPI设备管理
1 | // spi总线对象 |
- SPI总线创建、SPI从设备挂载到总线时序图

SPI总线是一个虚拟的设备(对应MCU硬件上的SPI控制器),总线可以挂载多个从设备,靠CS引脚来区分,总线现在被哪个从设备占用。 struct rt_spi_bus总线对象里的owner用来记录当前总线被哪个从设备占用。struct rt_spi_device从设备对象里的bus用来记录当前从设备挂载在哪个总线上。
1.SPI总线设备创建,drv_spi.c 驱动层里面实例化一个SPI总线对象。
2.SPI总线设备注册到 SPI设备驱动框架层:spi_core.c、spi_device.c ,通过 rt_spi_bus_register() 接口。
3.SPI设备驱动框架对象注册到IO管理层:rt_device.c ,通过 rt_device_register() 接口。
SPI设备驱动框架层由两个文件组成:spi_core.c、spi_device.c
4.在SPI从机设备驱动创建一个SPI从设备:对应具体的SPI外设芯片,在设备的驱动程序实现:比如 ssd1306.c ,使用 rt_spi_bus_attach_device() 里面会实例化一个spi从设备对象,并把从设备对象挂载到指定的一个总线上。
由于 rt_spi_bus_attach_device() 只是创建了一个从设备对象,并把它挂载到一个总线,但并没有配置该从设备通信时主设备该设置成什么参数配置(每个从设备占用总线通信的时候可能需要的总线配置是不一样的,默认的配置是 0),这里需要接着调用 rt_spi_configure() 接口配置总线以适合该从设备和总线(主设备)通信:数据宽度、时钟速度、模式等配置。配置完这么参数后总线上的主设备从能和这个从设备通信。
1 | int ssd1306_attach_to_spi_bus(void) |
总线:主设备+虚拟总线的统称。
5.挂载从设备对象到SPI总线:ssd1306.c 和步骤 4 创建对象时合成在一起了,通过 rt_spi_bus_attach_device() 接口。
6.注册spi从设备对象到IO设备管理层:rt_device.c ,通过 rt_device_register() 接口。
7.通过从设备的设备句柄调用 rt_spi_transfer_message() 就可以对从设备收发数据。
8.spi_drv.c 驱动中在检测到该从机是首次收发数据的时候,会调用congfig() 先配置从机,接着在 通过 xfer() 收发本次的数据。
spi_drv.c 驱动中动过检测发送数据的总线对象里面的 owner 是否是本从设备,是代表总线之前就已经被该设备占用,之前肯定是配置过从设备了,不是就代表之前总线不是被该从设备占用需要配置该从设备。

小结:
1.rtthread中把MUC内部的SPI控制器虚拟成了一个虚拟总线。
2.总线在系统启动是就已经创建并注册到IO设备管理器。
3.当我们在应用程序中需要创建挂载一个从设备的时候,首先创建从设备并挂载到总线,还要设置好该从设备与总线通信时,总线的配置参数。
I2C总线
重复条件:在一次通信过程中,当主机需要和不同的从机传输数据或者需要切换读写操作,主机可再发送一个开始条件。
例如:主机需要读取从机某个寄存器的数据
1.主机发送起始信号。
2.主机发送从机地址,写标志。
3.从机应答后,主机再发送一次起始信号,发送寄存器地址,读标志。
4.主机释放总线,接收从机发来的数据。
5.主机收到数据ACK,不想再收从机的数据了就发NACK。
6.主机发送停止位,结束本次通信。

主机向从机的某个寄存器写数据:
1.主机发送起始信号。
2.主机发送从机地址,写标志。
3.从机应答后,主机接着发送寄存地址。
4.从机应答后,主机接着发送要写入的数据。
5.从机应答后,主机发送停止位结束本次通信。

本章小结:
1.i2c设备接口使用的从机地址均不包含读写的地址。
虚拟文件系统
- DFS虚拟文件系统组件
全称:Device File System,即设备虚拟文件系统。
- DFS架构
1.提供POSIX标准接口:read、write、poll/select
2.支持多种文件系统,例如:FatFS、RomFS、DevFS,提供普通文件、设备文件、网络文件描述符管理。
3.支持多种类型的储存设备,如:SDCard、SPI Flash、Nand Flash等。
4.主要分为:POSIX接口层dfs_posix.c 、DFS虚拟文件系统层dfs_file.c 、设备抽象层。

- POSIX接口层
可移植性操作系统接口,规定的一组api函数接口。
- 虚拟文件系统层
用户将具体的文件系统注册到DFS框架中,如:FatFS、RomFS、DevFS 这些文件系统。
1.FatFS:兼容微软FAT格式文件的开源文件系统,专为小型嵌入式设备开发,采用 ANSI C 编写,具有良好的硬件无关性以及可移植性,是 RT-Thread 中最常用的文件系统类型。
2.传统型的 RomFS 文件系统是一种简单的、紧凑的、只读的文件系统。
3.Jffs2 文件系统是一种日志闪存文件系统。主要用于 NOR 型闪存,基于 MTD 驱动层,特点是:可读写的、支持数据压缩的、基于哈希表的日志型文件系统,并提供了崩溃 / 掉电安全保护,提供写平衡支持等。
4.DevFS 即设备文件系统,在 RT-Thread 操作系统中开启该功能后,可以将系统中的设备在 /dev 文件夹下虚拟成文件,使得设备可以按照文件的操作方式使用 read、write 等接口进行操作。
5.UFFS 是 Ultra-low-cost Flash File System(超低功耗的闪存文件系统)的简称。它是国人开发的、专为嵌入式设备等小内存环境中使用 Nand Flash 的开源文件系统。与嵌入式中常使用的 Yaffs 文件系统相比具有资源占用少、启动速度快、免费等优势。
设备抽象层
设备抽象层将物理设备如 SD Card、SPI Flash、Nand Flash,抽象成符合文件系统能够访问的设备,例如 FAT 文件系统要求存储设备必须是块设备类型。
- 注册文件系统

- 将储存设备注册为块设备

- 格式化文件系统
1 | int dfs_mkfs(const char * fs_name, const char * device_name); |

- 挂在文件系统
1 | int dfs_mount(const char *device_name, |
其他在C语言代码中的操作直接调用文件系统提供的POSIX接口就行。shell中也可使用文件操作的命令,创建删除文件和目录文件、切换目录等命令。
- 开机初始化块设备和DFS文件系统完后,在main中挂载文件系统到根目录:
1 |
|

rtconfig.h中的宏添加:
1 | /* DFS虚拟文件系统所需宏 */ |
RT-Thread网络框架
AT命令、SAL层(套接字抽象层)、BSD Socket API(标志网络套接字API)、LwIP

- 本文作者: 龙兄嵌入式
- 本文链接: https://hexo.880755.xyz/2022/04/19/test/2022-04-19--RT-Thread文档读书笔记/