RT-Thread学习笔记

RT-Thread学习笔记

H_Haozi Lv2

A 概念相关

1. RT-Thread中提供的线程调度器是基于优先级的全抢占式调度

全抢占式调度

线程调度器会通过线程就绪表来查找当前就绪态任务中 最高优先级 的任务来执行

在系统中除了

  1. 中断处理函数(硬件中断等)
  2. 调度器上锁部分的代码(暂时停止线程调度,但依旧会被硬件中断等响应)
  3. 禁止中断的代码(​不被任何可屏蔽中断和线程抢占)是不可被强占的,系统的其他部分都是可以抢占的,包括线程调度器自身。

其中,禁止中断的实现原理是 rt_hw_interrupt_disable()和 rt_hw_interrupt_enable()函数来实现的,本质上是执行特定的CPU指令,但也只能关闭可屏蔽中断

不可屏蔽中断:这种中断通常用于处理极其严重的硬件错误(如内存访问错误、看门狗超时等),其优先级最高,​无法通过软件禁止。因此,即使在临界区内,如果发生NMI,CPU也会立即暂停当前代码的执行,转去执行NMI的中断服务程序。待NMI处理完毕后,再返回临界区继续执行。

时间片轮转(分时调度器)

RT-Thread内核中也允许创建相同优先级的线程。相同优先级的线程采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪线程存在的情况下才有效。

2. 对于运行RT-Thread操作系统,线程都处于五种状态之一

初始状态、就绪状态、运行状态、挂起状态、关闭状态

通过调用操作系统提供的接口函数,可以让线程在这五种状态中进行来回切换

graph TD
    A[初始状态
RT_THREAD_INIT] -->|rt_thread_startup| B(就绪状态
RT_THREAD_READY) B -->|调度器调度| C{运行状态
RT_THREAD_RUNNING} C -->|主动延时: rt_thread_delay/
等待资源: rt_sem_take, rt_mb_recv等
或资源不可用| D[挂起状态
RT_THREAD_SUSPEND] D -->|等待超时或资源可用| B C -->|线程执行完毕| E[[关闭状态
RT_THREAD_CLOSE]] D -->|rt_thread_delete/detach| E C -->|时间片用完或更高优先级线程就绪| B style A fill:#e6f7ff style B fill:#e6ffe6 style C fill:#fff0e6 style D fill:#ffe6e6 style E fill:#f9f9f9

空闲线程

空闲线程是系统线程中一个比较特殊的线程,它具有最低的优先级,当系统中无其他线程可运行时,调度器将调度到空闲线程。空闲线程通常是一个死循环,永远不被挂起

其中,空闲线程还有一个重要作用:
因为RT-Thread 的线程删除并非一步到位,而是分为设置关闭状态空闲线程回收​两个阶段

  1. 当我们在代码中调用 rt_thread_delete()或 rt_thread_detach()时,内核并不会立即释放该线程的资源(如控制块和栈空间)。它只是将该线程的状态标记为 ​RT_THREAD_CLOSE,并将其从就绪队列等调度列表中移除,同时挂入一个名为 rt_thread_defunct的队列(常被称为“僵尸线程队列”)至此,该线程不再参与系统调度。

  2. 真正的资源释放工作由空闲线程完成,包括释放线程控制块和线程栈所占用的内存

3. RT-Thread的程序内存分布

​Flash(ROM) 和 RAM 是两种不同的物理介质

1.​Flash (ROM)​​:相当于“仓库”或“硬盘”。特点是非易失性​(断电后数据不丢失),但读写速度较慢。用于永久存储程序代码、常量数据等。

​2. RAM​:相当于“工作台”或“内存条”。特点是易失性​(断电后数据丢失),但读写速度极快。用于程序运行时存放临时变量、函数栈、以及需要被更改的数据。

程序要运行,CPU 必须从 ​RAM​ 中取指令、读/写数据。而上电时,所有程序和数据都安静地躺在 ​Flash​ 里。因此,需要一个机制将必要的内容从“仓库”(Flash)搬运到“工作台”(RAM)上,这个工作就是由启动文件中的汇编代码完成的。

编译后,程序被分成了几个重要的“段(Section)”,三个最关键的段:RO,RW,ZI

  • RO段 (ReadOnly) - 只读段

主要存放 Code(代码) 和 RO Data(只读数据),比如const修饰的全局变量和静态变量,字符串常量等等

  • RW段 (ReadWrite) - 可读写数据段

主要存放 所有已初始化且初值不为 0​ 的全局变量和静态变量等等

  • ZI段 (ZeroInitialized) - 零初始化数据段

主要存放 所有未初始化的全局变量/静态变量,以及所有显式初始化为 0​ 的全局变量/静态变量。
上电后,启动代码会将在 RAM 中为 ZI 段分配的整个区域全部清零,所以未初始化的变量默认为0

  • 动态内存堆 主栈空间​- 剩余的RAM区域

在RT-Thread中,栈主要分为 主栈(存储main函数的局部变量、函数调用返回地址、参数传递、以及中断服务例程(ISR)的上下文),线程栈(创建线程的时候分配),中断栈(通常会使用调用中断的线程的栈,有时候为了防止线程栈不足,也可以配置一个独立的中断栈))

上电启动流程图

flowchart TD
    A[上电复位] --> B[CPU从Flash固定地址
取复位向量执行] B --> C[初始化必要硬件
设置堆栈指针/系统时钟] C --> D[复制RW段数据
从Flash至RAM] D --> E[清零ZI段
对应RAM区域初始化] E --> F{C语言运行环境
准备就绪} F -->|是| G[跳转至main函数] G --> H[执行用户程序] style A fill:#7e9,stroke:#333,stroke-width:2px style H fill:#7e9,stroke:#333,stroke-width:2px style C fill:#69f,stroke:#333,stroke-width:2px style D fill:#69f,stroke:#333,stroke-width:2px style E fill:#69f,stroke:#333,stroke-width:2px style F fill:#fc3,stroke:#333,stroke-width:2px

SRAM、DRAM、SDRAM的区别

SRAM:静态的随机存储器,加电情况下,不需要刷新,数据不会丢失,CPU的缓存就是SRAM
DRAM:动态随机存储器,加电情况下,也需要不断刷新,才能保存数据,最为常见的系统内存
SDRAM:同步动态随机存储器,即数据的读取需要时钟来同步,也可用作内存

两种创建进程的区别(静态/动态)

  • 创建线程(静态)—–占用RAM空间(RW/ZI 空间),用户分配栈空间和线程句柄,位置由编译的时候确定

优点: 运行的时候不用动态分配内存,运行效率高,实时性好
缺点: 内存不能被释放,之内使用 rt_thread_detach() 函数该线程脱离管理

  • 创建线程(动态)—–由RT-Thread内核自动从内存堆中为线程控制块和线程栈分配所需的内存

优点: 灵活性高,可以随时根据系统状态和任务需求来创建或删除线程,更好的利用内存资源
缺点: 动态分配内存需要时间,实时性降低; 并且频繁创建删除线程可能产生内存碎片,影响后续的内存分配

4. 定时器管理

硬件定时器和软件定时器

  • 硬件定时器
    可以理解成裸机stm32使用的定时器外设,一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式

  • 软件定时器
    软件定时器是由操作系统提供的一类系统接口(函数),它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务,但精度越高,对系统开销也将越大(在1秒中系统用于处理时钟中断的次数也就越多))

在操作系统中,通常软件定时器以系统节拍(tick)为单位。节拍长度指的是周期性硬件定时器两次中断间的间隔时间长度。这个周期性硬件定时器也称之为操作系统时钟
软件定时器以这个节拍时间长度为单位,数值必须是这个节拍的整数倍

5. 任务间同步及通信

处理临界区

为了防止多个线程在同一时间访问同一个地址内存,这将引起数据一致性的问题,所以引入信号量等多个方法
对于操作/访问同一块区域,称之为临界区。任务的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个(或一类)任务运行。

当然,也可以原子操作,防止多个线程通常修改某个变量


  • 中断锁(关闭中断)

这便是最开始说的关闭中断的代码提到的关闭中断,硬件中断也不会打断当前任务进行
当中断关闭的时候,就意味着当前任务不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个任务主动放弃了处理器控制权

优点:使用得当的时候一种快速、高效的同步方式
缺点:中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言,通常只会在中断锁里面执行几句机器指令


  • 调度器锁

对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进行相应的响应
也就是使得当前线程不会被其它线程抢占,但依旧会被硬件中断等抢占,所以要考虑硬件中断中是否会修改临界资源


  • 条件变量

条件变量其实就是一个信号量,用于线程间同步。条件变量用来阻塞一个线程,当条件满足时向阻塞的线程发送一个条件,阻塞线程就被唤醒,条件变量需要和互斥锁配合使用,互斥锁用来保护共享数据。


  • 读写锁

读写锁也称为多读者单写者锁。读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。同一时间只能有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。读写锁适合于对数据结构的读次数比写次数多得多的情况,因为读模式锁定时可以共享,写模式锁定时意味着独占。

读写锁通常是基于互斥锁和条件变量实现的。一个线程可以对一个读写锁进行多次读写锁定,同样必须有对应次数的解锁。


  • 信号量

线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值都会减1
若信号量值等于零的时候,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据time参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。
如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是-RT_ETIMEOUT
当线程完成资源的访问后,应该释放它持有的信号量

信号量是一种非常灵活的同步方式,可以运用在多种场合中
形成锁,中断与线程等之间的同步,资源计数等关系,也能方便的用于线程与线程,中断与线程的同步中。


  • 互斥量

互斥量是一种特殊的二值性信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性

互斥量所有权,即谁拥有,只能谁释放,会降低出现死锁的概率


  • 优先级翻转:

优先级翻转是实时系统中一种特殊现象,指高优先级任务因所需资源被低优先级任务占用而被阻塞,而低优先级任务又可能被中优先级任务抢占,导致高优先级任务长时间得不到执行,系统实时性遭到破坏,此时优先级顺序会变成中等优先级>低优先级>高优先级

解决方法:

  1. 优先级继承协议​​
    当高优先级任务(H)被低优先级任务(L)阻塞时,​临时将L的优先级提升至H的优先级​
    优点:实现相对简单,仅在发生阻塞时动态提升,比较精准
    缺点:仍需处理可能的阻塞链,若嵌套复杂需多次判断和提升优先级

  2. ​优先级天花板​
    为每个资源预设一个​天花板优先级​​(等于可能访问该资源的最高任务优先级)
    优点:能完全避免优先级翻转,且同时可预防死锁
    缺点:需预先静态设置天花板优先级,可能不够灵活,有时会过度提升优先级


  • 事件

事件主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步,即一个线程可等待多个事件的触发
线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联


  • 邮箱

邮箱服务是实时操作系统中一种典型的任务间通信方法,特点是开销比较低,效率较高
邮箱中的每一封邮件只能容纳固定的4字节内容(针对32位处理系统,指针的大小即为4个字节,所以一封邮件恰好能够容纳一个指针))
不可以在中断中接收,或等待发送邮件,中断中阻塞会影响系统实时性


  • 消息队列

它能够接收来自线程或中断服务例程中不固定长度的消息
消息队列是一种异步通信方式。
不可以在中断中接收

6. 虚拟文件系统

RT-Thread 的文件系统采用了三层的结构,对上层提供的接口主要以POSIX 标准接口为主

flowchart TD
    A["应用程序调用POSIX接口
e.g. open, write"] --> B["DFS 虚拟文件系统层
接收请求"] B --> C{"查找文件路径对应的
具体文件系统"} C --> D["内存文件系统
(如tmpfs, ramfs)"] D --> E["数据保存在
系统RAM内存中"] E --> F["特点: 速度快
断电丢失"] C --> G["存储设备文件系统
(如FatFS, LittleFS)"] G --> H["数据写入块设备
(如SPI Flash, SD卡)"] H --> I["特点: 持久化存储
读写速度相对较慢"]

在使用文件系统接口前,需要对文件系统进行初始化,也就是挂载相关操作,这里定义挂载的文件系统及其类型和挂载点

在使用文件系统接口的时候,虚拟文件系统会根据你的路径,判断是保存在SD卡,或者是内存中,而我们只用通过统一的 POSIX 接口(如 open、write)操作文件,不需要关心底层如何实现

B.设备和驱动

1. I/O设备模型

这里的操作和学习imx6ull中的驱动操作类似,这是这里我们主要操作应用态,而linux驱动主要在操作内核态

在 Linux 驱动开发中,我们通常需要直接编写运行在内核态的字符设备/块设备驱动,涉及大量的内核 API(file_operations、register_chrdev等)。而在 RT-Thread 中,这套 I/O 设备模型为我们完成了绝大部分的“内核态”工作。

flowchart TD
    A[应用程序]
    
    subgraph B [I/O 设备管理层]
        B1[I/O设备管理接口]
        B2[字符设备]
        B3[块设备]
        B4[SPI总线设备]
        B5[SPI从设备]
        B6[I2C总线设备]
        B7[其他设备类型...]
    end

    subgraph C [设备驱动框架层]
        C1[串口设备驱动框架]
        C2[SPI设备驱动框架]
        C3[I2C设备驱动框架]
        C4[PIN设备驱动框架]
        C5[...]
    end

    subgraph D [设备驱动层]
        D1[STM32/NXP 串口驱动]
        D2[各类SPI控制器驱动]
        D3[SPI Flash驱动]
        D4[STM32/NXP I2C驱动]
        D5[STM32/NXP GPIO驱动]
        D6[...]
    end

    E[硬件]

    A --> B1
    B1 --> B2
    B1 --> B3
    B1 --> B4
    B1 --> B5
    B1 --> B6
    B1 --> B7

    B2 --> C1
    B3 --> C1
    B4 --> C2
    B5 --> C2
    B6 --> C3
    B7 --> C4 & C5

    C1 --> D1
    C2 --> D2 & D3
    C3 --> D4
    C4 --> D5
    C5 --> D6

    D1 --> E
    D2 --> E
    D3 --> E
    D4 --> E
    D5 --> E
    D6 --> E

驱动层负责创建设备实例,并注册到 I/O 设备管理器中,可以通过静态申明的方式创建设备实例,也可以用下面的接口进行动态创建,不使用后也需要销毁

1
2
rt_device_t rt_device_create(int type, int attach_size);  //创建
void rt_device_destroy(rt_device_t device); //销毁

设备被创建后,需要注册到 I/O 设备管理器中,应用程序才能够访问,注册设备的函数如下所示,不使用之后也需要注销

1
2
rt_err_t rt_device_register(rt_device_t dev, const char* name, rt_uint8_t flags); //注册
rt_err_t rt_device_unregister(rt_device_t dev); //注销

创建,注册之后,下一步就是使用这个设备,首先我们需要根据设备名称获取设备句柄,进而可以操作设备,获得设备句柄后,应用程序可使用如下函数对设备进行初始化操作,然后就可以通过设备句柄打开或者关闭设备。(如果调用时没有初始化设备,则会按默认参数初始化设备)

1
2
3
rt_device_t rt_device_find(const char* name); //查找设备
rt_err_t rt_device_init(rt_device_t dev); //初始化设备
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags); //打开或关闭设备

后期操作设备,也可以通过句柄来实现读写操作等,下面附rt-thread手册的一张补充图片:

2. 待补充

参考文献

  1. RT-Thread编程手册

留言

有问题请指出,你可以选择以下方式:

  1. 在下方评论区留言
  2. 邮箱留言
  • Title: RT-Thread学习笔记
  • Author: H_Haozi
  • Created at : 2025-03-15 11:22:45
  • Updated at : 2025-09-21 17:49:34
  • Link: https://redefine.ohevan.com/2025/03/15/embedded_rt_thread/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments