RTT操作系统(1)

本笔记为作者再学习RTT操作系统的一些心得体会,如有不对的地方,请包含与谅解!

​ ————by wsoz

RTT操作系统概述

RTT是嵌入式实时多线程操作系统,多线程本质上还是在一块CUP上运行,但是通过调度器的时间片轮询的将时间分给每一个线程任务实现伪并行,同时RTT相较于传统的RTOS而言,还是一款**物联网操作系统**,可以通过软件包的形式实现云端通信。

RTT操作系统架构

在这里插入图片描述

基础概念

为了更好的理解操作系统,我们在进行正式的代码编写运用前务必要将一些操作系统的基础概念简单了解,这样才能更好的深入进行学习。

内核

内核是操作系统的核心,它负责管理系统的线程、线程间通信、系统时钟、中断及内存等。下课图为内核的系统结构图:

在这里插入图片描述

线程与线程间的调度

线程是最小的调度单位,线程调度算法是基于优先级的全抢占式多线程调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。(最多256个抢占优先级,0优先级最高),支持创建多个具有相同优先级的线程,相同优先级的线程间采用时间片的轮转调度算法进行调度,使每个线程运行相应时间。

时钟管理

RT-Thread 的时钟管理以时钟节拍为基础**,时钟节拍是 RT-Thread 操作系统中**最小的时钟单位。

RT-Thread 的定时器可以设置为 HARD_TIMER 模式或者 SOFT_TIMER 模式

硬件定时器:

  • 执行上下文: 中断服务例程 (ISR)
  • 当定时器超时时,其超时回调函数直接在系统时钟中断的上下文环境中被调用。

工作流程

  1. 系统硬件定时器产生一个固定的时钟节拍(Tick),例如每秒 1000 次(1ms/tick)。
  2. 在每个 Tick 的中断服务函数中,系统会检查所有已启动的定时器。
  3. 如果发现某个 HARD_TIMER 模式的定时器超时了,立即在该中断上下文中调用其超时回调函数。

软件定时器:

  • 执行上下文: 定时器线程
  • 当定时器超时时,系统只会设置一个标志。实际的超时回调函数是在一个独立的、专有的系统线程(通常是 timer thread)中被调用执行的。

工作流程

  1. 系统时钟 Tick 中断到来,检查所有定时器。
  2. 如果发现某个 SOFT_TIMER 模式的定时器超时了,系统并不立即执行回调函数,而是将该定时器挂到一个超时队列中。
  3. 时钟中断处理完成后,系统返回到线程模式。
  4. 一个专门处理软件定时器的线程(优先级在 rtconfig.h 中配置,默认为最低优先级)会运行起来,检查这个超时队列。
  5. 该线程从队列中取出超时的定时器,并在线程的上下文中逐一执行它们的回调函数。

裸机对比

在RT-Thread中,内核已经帮你完成了最底层、最繁琐的硬件初始化工作。

RT-Thread已经做了什么:

  1. 它已经在系统启动时,配置好了MCU的SysTick定时器作为系统的心跳(Clock Tick)。
  2. 它建立了一套软件定时器管理系统,可以基于这个SysTick来模拟出无数个“软件定时器”。
  3. 它提供了非常简单统一的API(rt_timer_create, rt_timer_start)来让你创建和管理定时器。

你需要做什么:

  1. 创建定时器:调用rt_timer_create函数,给它起个名字、设置超时时间、设置回调函数、并选择模式(HARD_TIMER或SOFT_TIMER)
  2. 启动定时器:调用rt_timer_start
  3. 编写回调函数:像写一个普通函数一样,实现定时器到点后要执行的逻辑。

线程间同步

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

线程间通信(IPC)

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

==注解:==在操作系统中每一个任务都相当于一个线程,比如LCD显示以及ADC采集任务,然后这两个线程间为了将ADC采集的值显示更新道LCD显示任务去就需要,ADC采集完成线程完成时向LCD显示线程发送标志去刷新显示,这样就不会像裸机一样在while循环中一直轮询,使得CPU占用减少,效率提高。

内存管理

RT-Thread 支持静态内存池管理及动态内存堆管理。静态内存池是向系统申请一块==固定==的内存地址,然后动态内存堆就是从系统中的可用内存申请一个任意的地址来使用。

静态内存池情况

当静态内存池具有可用内存时,系统对内存块分配的时间将是恒定的;

当静态内存池为空时,系统将申请内存块的线程挂起阻塞掉 (即线程等待一段时间后仍未获得内存块就放弃申请并返回,或者立刻返回。等待的时间取决于申请内存块时设置的等待时间参数),当其他线程释放内存块到内存池时,如果有挂起的待分配内存块的线程存在的话,则系统会将这个线程唤醒。

RTT启动流程

为了更好的了解操作系统,我们也需要了解RTT操作系统的启动流程。

下面的框图很好的绘出了RTT操作系统的启动流程:

在这里插入图片描述

首先,整个系统会先从启动文件开始 starup_xx.S 开始执行,之后进入 $Super$$main() 中进行相关配置的初始化,之后再进入道 main()函数中进行相关任务的执行。

$Super$$main() 中的初始化大致可以分为四个部分:

(1)初始化与系统相关的硬件;

(2)初始化系统内核对象,例如定时器、调度器、信号;

(3)创建 main 线程,在 main 线程中对各类模块依次进行初始化;

(4)初始化定时器线程、空闲线程,并启动调度器。

在初始化阶段开启了3个线程

标准工程架构

下面就正式进入我们的代码以及工程的学习了,在学习过程中的一些问题以及概念我们也会进行详细的补充。

下图为标准RTT项目的工程架构

在这里插入图片描述

  1. RT-Thread Settings (RT-Thread 配置):
    这是一个 Keil MDK 中的虚拟视图,提供图形化界面来配置和裁剪 RT-Thread 内核与组件的功能,其配置结果最终保存在 rtconfig.h 文件中。
  2. CubeMX Settings (STM32CubeMX 配置):
    一个通往 STM32CubeMX 配置工具(.ioc 文件)的入口,用于图形化配置芯片引脚、时钟和外设,并生成相应的底层驱动代码。
  3. applications (应用程序目录):
    用户编写自己业务逻辑代码的核心目录,包含 main.c 等文件,是创建用户线程、初始化应用模块的地方。
  4. drivers (驱动层):
    板级支持包(BSP)的核心,包含针对特定开发板的硬件驱动代码(如 drv_usart.c),充当 RT-Thread 内核与底层芯片硬件库之间的桥梁。
  5. libraries (硬件库):
    存放芯片厂商(如 ST)提供的底层硬件库(HAL 库)和 ARM 公司的 CMSIS 标准文件,提供了操作芯片外设和内核的基础 API。
  6. rt-thread (RT-Thread 内核源码):
    包含了 RT-Thread 实时操作系统的完整内核与组件源代码(如线程调度、IPC、文件系统、网络框架等),通常不建议用户直接修改。
  7. rtconfig.h (项目配置头文件):
    整个工程的核心配置文件,通过一系列宏定义来启用或禁用 RT-Thread 的各项功能与组件,并设置其参数,用于系统裁剪。
  8. linkscripts (链接脚本目录):
    包含链接脚本文件(.sct),用于指示编译器如何将代码和数据分配到芯片的 Flash 和 RAM 的特定地址区域。
  9. Debug (编译输出目录):
    由 IDE 自动生成,用于存放编译过程中产生的临时文件、最终的可执行文件(.axf)以及映射文件(.map)。
  10. Includes (头文件路径汇总):
    这是一个 Keil IDE 的汇总视图,显示本工程所有已包含的头文件搜索路径,方便开发者查看和管理。

==注意:==我们在开发过程中大部分时间都在 applications 里编码,并通过修改 rtconfig.h 来裁剪系统功能。如果需要改变硬件配置,则通过 CubeMX Settings 来调整。编译后,输出文件在 Debug 中。

RTT时钟设置

首先对于创建的RTT工程,默认启用的是内部高速时钟(HSI),为了更加精确以及性能我们需要采用外部高速时钟(HSE),我们就需要修改我们的时钟配置了。

CubeMX配置

我们采用的方法是使用CubeMX配合RTT来使用,下面我们先对CubeMX进行配置开启外部时钟以及基础的调试串口以及下载接口

在这里插入图片描述

在这里插入图片描述

修复报错

在生成文件之后,可能会出现报错的情况此时就需要检查我们的生成的文件中有无SConscript文件,这是一个链接文件,如果不存在的情况下,我们就需要自己创建这样一个文件(文本形式即可)

import os
#引入os模块
from building import *
#导入building的所有模块

cwd = GetCurrentDir()
#获取获取当前路径,并保存至变量cwd
src  = Glob('*.c')
#获取当前目录下的所有 C 文件,并保存至src变量

# add cubemx drivers
#由于RT-Thread工程中存在部分相同函数文件,所以对src重新赋值
#文件中的stm32f4xx_it.c 、 system_stm32f4xx.c不加入构建
#其余文件按相同格式填写到下述括号内
src = Split('''
Src/stm32f4xx_hal_msp.c
Src/main.c
Src/dma.c
Src/gpio.c
Src/usart.c
''')

#创建路径列表,并保存至path中
path = [cwd]
path += [cwd + '/Inc']
#这是 RT-Thread 基于 SCons 扩展的一个方法(函数)。
group = DefineGroup('cubemx', src, depend = [''], CPPPATH = path)

Return('group')

之后还有一个必要的步骤检查一下drv_clk.c文件,看文件末尾有无下面的代码

void clk_init(char *clk_source, int source_freq, int target_freq)
{
    /*
     * Use SystemClock_Config generated from STM32CubeMX for clock init
     * system_clock_config(target_freq);
     */
    extern void SystemClock_Config(void);
    SystemClock_Config();
}

==注意:==没有的情况下我们就需要将我们的生成文件中的SystemClock_Config中的代码复制到drv_clk.cvoid system_clock_config(int target_freq_mhz)中去即可。

Shell调试工具

当前版本的RTT已经默认开启了Shell调试工具FinSH ,通过该工具我们可用详细的查看我们的相关配置方便我们进一步检查和调试。

在这里插入图片描述

通过串口的终端模式我们呢可用很清晰的看见通过我们的调试界面可以查看什么

  1. clear:清空屏幕。
  2. version:查看 RT-Thread 版本号。
  3. list_thread / ps查看所有线程的状态(运行、挂起等)和栈使用情况,最常用。
  4. list_sem:查看系统中所有信号量的状态。
  5. list_event:查看系统中所有事件集的状态。
  6. list_mutex:查看系统中所有互斥量的状态。
  7. list_mailbox:查看系统中所有邮箱的状态。
  8. list_msgqueue:查看系统中所有消息队列的状态。
  9. list_mempool:查看系统中所有内存池的状态。
  10. list_timer:查看系统中所有定时器的状态。
  11. list_device:查看系统中所有设备(如串口、SPI)的状态。
  12. list:一次性列出所有内核对象。
  13. help:显示命令帮助列表。
  14. free查看系统内存使用情况,检查内存泄漏。
  15. pin直接读写和控制 GPIO 引脚,调试硬件。
  16. reboot:软件重启系统。

总结:这些命令让你在不连接仿真器的情况下,动态监控系统内核(线程、内存、IPC对象)的状态和调试外设,是定位系统卡死、内存泄漏、线程阻塞等问题的最直接工具。psfree 是最常用的命令。

线程管理

对于在线程管理中,我们要学习的是如何对线程进行创建删除等以及一些常用的线程控制的api和常用的钩子hook函数。

概念理解

线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位

线程在运行的时候,它自己会认为独占CPU运行(伪)。

线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。

线程管理的功能特点

RT-Thread 线程的线程分为两类线程:系统线程和用户线程

系统线程:由 RT-Thread 内核创建的线程(空闲线程和主线程

用户线程:用户线程是由应用程序创建的线程

RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。(类似中断)

线程的工作机制

线程控制块

线程控制块由结构体 struct rt_thread 表示,具体的完整定义如下:

struct rt_thread
{
    /* rt对象 */
    char        name[RT_NAME_MAX];                      /**< 线程名称 */
    rt_uint8_t  type;                                   /**< 对象类型 */
    rt_uint8_t  flags;                                  /**< 线程标志位 */

#ifdef RT_USING_MODULE
    void       *module_id;                              /**< 应用程序模块id */
#endif /* RT_USING_MODULE */

    rt_list_t   list;                                   /**< 内核对象列表节点 */
    rt_list_t   tlist;                                  /**< 线程列表节点 */

    /* 栈指针和入口函数 */
    void       *sp;                                     /**< 当前栈指针 */
    void       *entry;                                  /**< 线程入口函数 */
    void       *parameter;                              /**< 线程参数 */
    void       *stack_addr;                             /**< 栈起始地址 */
    rt_uint32_t stack_size;                             /**< 栈大小 */

    /* 错误代码 */
    rt_err_t    error;                                  /**< 错误代码 */

    rt_uint8_t  stat;                                   /**< 线程状态 */

#ifdef RT_USING_SMP
    rt_uint8_t  bind_cpu;                               /**< 线程绑定的CPU核 */
    rt_uint8_t  oncpu;                                  /**< 正在运行的CPU核 */

    rt_uint16_t scheduler_lock_nest;                    /**< 调度器锁嵌套计数 */
    rt_uint16_t cpus_lock_nest;                         /**< CPU锁嵌套计数 */
    rt_uint16_t critical_lock_nest;                     /**< 临界区锁嵌套计数 */
#endif /*RT_USING_SMP*/

    /* 优先级 */
    rt_uint8_t  current_priority;                       /**< 当前优先级 */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_uint8_t  number;
    rt_uint8_t  high_mask;
#endif /* RT_THREAD_PRIORITY_MAX > 32 */
    rt_uint32_t number_mask;                            /**< 优先级位图掩码 */

#ifdef RT_USING_EVENT
    /* 线程事件 */
    rt_uint32_t event_set;                              /**< 接收到的事件集合 */
    rt_uint8_t  event_info;                             /**< 事件信息 */
#endif /* RT_USING_EVENT */

#ifdef RT_USING_SIGNALS
    rt_sigset_t     sig_pending;                        /**< 待处理信号集 */
    rt_sigset_t     sig_mask;                           /**< 信号掩码 */

#ifndef RT_USING_SMP
    void            *sig_ret;                           /**< 信号返回栈指针 */
#endif /* RT_USING_SMP */
    rt_sighandler_t *sig_vectors;                       /**< 信号处理函数向量表 */
    void            *si_list;                           /**< 信号信息列表 */
#endif /* RT_USING_SIGNALS */

    rt_ubase_t  init_tick;                              /**< 线程初始时间片节拍数 */
    rt_ubase_t  remaining_tick;                         /**< 剩余时间片节拍数 */

#ifdef RT_USING_CPU_USAGE
    rt_uint64_t  duration_tick;                         /**< CPU使用率统计节拍数 */
#endif /* RT_USING_CPU_USAGE */

#ifdef RT_USING_PTHREADS
    void  *pthread_data;                                /**< pthread数据句柄,适配32/64位 */
#endif /* RT_USING_PTHREADS */

    struct rt_timer thread_timer;                       /**< 内置线程定时器 */

    void (*cleanup)(struct rt_thread *tid);             /**< 线程退出时的清理函数 */

    /* 轻量级进程(如果存在) */
#ifdef RT_USING_LWP
    void        *lwp;                                   /**< 关联的轻量级进程 */
#endif /* RT_USING_LWP */

    rt_ubase_t user_data;                             /**< 线程私有用户数据 */
};

但是其中有大部分都是需要通过宏定义来设置,需要时在rt的配置文件修改即可,最基础必要部分的如下:

struct rt_thread
{
    /* 对象基本信息 */
    char        name[RT_NAME_MAX];  /* 线程名称 */
    rt_uint8_t  type;               /* 对象类型 */
    rt_uint8_t  flags;              /* 线程标志 */

    rt_list_t   list;               /* 内核对象列表节点 */
    rt_list_t   tlist;              /* 线程列表节点 */

    /* 栈和入口函数 */
    void       *sp;                 /* 当前栈指针 */
    void       *entry;              /* 线程入口函数 */
    void       *parameter;          /* 线程参数 */
    void       *stack_addr;         /* 栈地址 */
    rt_uint32_t stack_size;         /* 栈大小 */

    /* 状态和错误 */
    rt_uint8_t  stat;               /* 线程状态 */
    rt_err_t    error;              /* 错误代码 */

    /* 优先级 */
    rt_uint8_t  current_priority;   /* 当前优先级 */
    rt_uint32_t number_mask;        /* 优先级位掩码 */

    /* 时间片 */
    rt_ubase_t  init_tick;          /* 初始时间片 */
    rt_ubase_t  remaining_tick;     /* 剩余时间片 */

    /* 线程定时器 */
    struct rt_timer thread_timer;   /* 内置定时器 */

    /* 清理函数 */
    void (*cleanup)(struct rt_thread *tid); /* 线程退出清理函数 */

    /* 用户数据 */
    rt_ubase_t user_data;           /* 用户私有数据 */
};

其中像类型以及标志等可以在rtdef.h文件中找到相关的宏定义或者枚举类型。

注意: cleanup函数指针指向的函数,会在线程退出的时候,被idle线程回调一次,执行用户设置的清理现场等工作。

详解:cleanup函数不是必须的,首先我们要清楚的是这个回调函数运行的时机是在空闲线程中执行的,然后这个函数的作用是清除手动分配的内存空间(malloc/rt_malloc)等,下面为总结表

内存类型 分配方式 存储位置 生命周期 由谁释放 需要cleanup吗?
线程栈本身 rt_thread_create 堆或静态区 与线程共存亡 空闲线程自动 不需要
局部变量/数组 在函数内定义 线程栈上 与线程共存亡 空闲线程自动 不需要
动态内存 malloc/rt_malloc 系统堆 mallocfree 开发者手动 必须需要
信号量/互斥锁等 rt_sem_create 内核对象池 createdelete 开发者手动 必须需要

相当于就是我们进行线程切换的时候,我们的内存空间并不会立马释放,为了更快(低延时)的把cpu交给下一个执行线程,就会把这个清理工作放到空闲线程中执行(因为空闲线程执行时意味着上面的线程全都已经执行完了或者处于挂载状态),此时就会进行检查僵死链表(执行完了的线程,不包括处于挂载的线程)进行空闲线程自动清除以及清除回调函数的操作。

线程重要属性
线程栈

RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。

线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器(寄存器是CPU内部的高速存储单元)中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。(即寄存器空间小但是速度快,同时这个寄存器地址也会给其他变量运行时使用不是永久地址,此时就需要copy到线程栈中进行存储保证我们的这个变量的生命周期)

线程状态

线程状态有5种状态,下表中列出

状态 描述
初始状态 当线程刚开始创建还没开始运行时就处于初始状态;**在初始状态下,线程不参与调度。**此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT
就绪状态 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY
运行状态 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING
挂起状态 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND
关闭状态 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE
线程优先级

RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

时间片

每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。就是按照特定的时间片(时钟节拍)进行交替调用线程运行**(即线程进行轮询)**

线程的入口函数

线程的入口函数由用户自己定义常分为两种

第一种死循环模式

void thread_entry(void* paramenter)
{
    while (1)
    {
    /* 等待事件的发生 */

    /* 对事件进行服务、进行处理 */
    }
}

注意:作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU使用权的动作,如循环中调用延时函数或者主动挂起

第二种顺序执行或有限次循环模式

void thread_entry(void* parameter)
{
    /* 处理事务 #1 *//* 处理事务 #2 *//* 处理事务 #3 */ 
}
//或者使用for等有限循环

这种线程调度就是只会调用特定次数后便会完成线程任务,后续会被释放掉内存空间。

线程状态切换

RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。几种状态间的转换关系(一一对应)如下图所示:

在这里插入图片描述

注意:就绪态和运行态本质上是属于相同等级的状态,只是看是谁占用了cpu

线程常用函数

线程常用的函数我们介绍两种,一种是线程的控制管理函数,另一种是线程辅助函数

控制管理函数

线程主要的管理函数就是实现对于线程的创建以及删除等操作,其中又可以分为创建/删除一个动态线程和初始化一个静态线程

在这里插入图片描述

区分动态线程和静态线程:

核心区别其实就在于一句话:线程控制块和线程栈的内存是从哪里分配的。

  • 动态线程:内核自动从内存堆中分配所需的内存。

    特点:

    • 优点

      • 灵活:按需分配内存,不用时不占用资源。
      • 节省RAM:当线程被删除后,内存会被释放回堆中,可供其他线程或模块使用。
    • 缺点

      • 有失败风险:如果内存堆空间不足,rt_thread_create() 会返回 RT_NULL,表示创建失败。

      • 容易产生碎片:频繁地创建和删除可能会造成内存碎片。

  • 静态线程:用户自己(程序员)提前分配好所需的内存(通常是全局变量)。

    • 优点
      • 可靠:因为内存是预先分配好的(比如全局变量),所以绝对不会出现因内存不足而初始化失败的情况。
      • 实时性高:没有动态内存分配的过程,时间确定性更高,适合硬实时场景。
      • 无碎片:内存生命周期与程序相同,不会产生碎片。
    • 缺点
      • 不灵活:即使线程没有被启用或已经删除,占用的 RAM 资源也一直存在,无法被回收再利用。
      • 浪费资源:如果线程数量多且不常运行,会导致大量 RAM 被闲置占用。
+-------------------+ 0x2000 0000
|    .data Section  |  // 已初始化的全局变量和静态变量
|                   |  // (编译器在编译时分配)
+-------------------+
|    .bss Section   |  // 未初始化或初始化为0的全局变量和静态变量
|                   |  // (编译器在编译时分配)
+-------------------+
|                   |
|                   |
|      Heap         |  // 动态内存区 -> 由 rt_malloc/rt_free 管理
|   (向上增长)      |  // rt_thread_create() 从这里分配内存
|                   |
|                   |
+ - - - - - - - - - +  // Heap 和 Stack 之间的空闲区域
|                   |
|                   |
+-------------------+
|                   |
|      Stack        |  // 主栈/中断栈 -> 由编译器自动管理
|   (向下增长)      |  // 局部变量、函数调用信息存放于此
|                   |
+-------------------+ 0x2000 FFFF
动态线程创建

函数原型:

rt_thread_t rt_thread_create(const char *name,	//线程名
                             void (*entry)(void *parameter),	//线程入口函数函数
                             void       *parameter,		//线程参数,无即NULL
                             rt_uint32_t stack_size,	//线程栈空间
                             rt_uint8_t  priority,	//线程优先级
                             rt_uint32_t tick)	//线程时间片 单位是操作系统的时钟节拍

返回值:

返回 说明
thread 线程创建成功,返回线程句柄
RT_NULL 线程创建失败

rt_thread_t 的原型为typedef struct rt_thread *rt_thread_t;即表示线程控制块的地址即句柄

动态线程删除

函数原型:

rt_err_t rt_thread_delete(rt_thread_t thread)	//传入线程句柄

注意:用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到僵尸队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程

返回值:

返回 描述
RT_EOK 删除线程成功
-RT_ERROR 删除线程失败

**注意:**rt_thread_create() 和 rt_thread_delete() 函数仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)。位于rt_config.h中看是否定义

静态线程初始化

函数原型:

rt_err_t rt_thread_init(struct rt_thread *thread,	//线程句柄用户提供不返回
                        const char       *name,	//线程名
                        void (*entry)(void *parameter),	//线程入口函数
                        void             *parameter,	//线程参数,无NULL
                        void             *stack_start,	//线程栈起始地址,全局数组的首元素表地址
                        rt_uint32_t       stack_size,	//线程栈大小
                        rt_uint8_t        priority,		//线程优先级
                        rt_uint32_t       tick)	//线程时间片

返回:

返回 描述
RT_EOK 线程创建成功
-RT_ERROR 线程创建失败
静态线程脱离

函数原型:

rt_err_t rt_thread_detach(rt_thread_t thread)	//传入线程句柄

返回值:

返回 描述
RT_EOK 线程脱离成功
-RT_ERROR 线程脱离失败
启动线程

原型:

rt_err_t rt_thread_startup(rt_thread_t thread)	//线程句柄

返回值:

返回 描述
RT_EOK 线程启动成功
-RT_ERROR 线程启动失败
获取当前线程

原型:

rt_thread_t rt_thread_self(void);	//线程句柄

返回值:

返回 描述
thread 返回当前运行的线程句柄
RT_NULL 失败,调度器还未启动
线程主动让出资源

原型:

rt_err_t rt_thread_yield(void)

当前线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。(由运行态转到就绪态,当前线程主动将自己移动到同优先级就绪队列的末尾。)

这个函数只是在相同优先级的线程的情况下有效

返回值:

通常忽略,不用管

线程睡眠

原型:

rt_err_t rt_thread_sleep(rt_tick_t tick);	//睡眠时钟节拍
rt_err_t rt_thread_delay(rt_tick_t tick);	//延时时钟节拍
rt_err_t rt_thread_mdelay(rt_int32_t ms);	//延时ms
  • 本质无区别:这三个函数最终都调用同一个底层实现 rt_thread_delay
  • 都会导致挂起:它们都会使当前线程进入延时挂起状态,主动放弃CPU。
  • 唯一区别:在于参数的表示方式(tick 还是 毫秒)。

返回值:

返回 描述
RT_EOK 操作成功
线程主动挂起

原型:

rt_err_t rt_thread_suspend (rt_thread_t thread)		//自己线程句柄

这个只用于自己挂自己,并且挂起后必须立刻手动调度 (rt_schedule())。恢复必须依赖其他线程手动调用 rt_thread_resume()

rt_thread_suspend(my_rt_thread); // 步骤1:将自己状态设为“挂起”
rt_schedule();                       // 步骤2:强制立刻进行线程切换

rt_schedule()这个函数是调度器的核心。它会检查所有线程的状态,找出最高优先级的、处于“就绪”状态的线程,然后执行上下文切换——即保存当前线程的现场(寄存器等),恢复下一个线程的现场,并跳转到下一个线程去执行。

返回值:

返回 描述
RT_EOK 线程挂起成功
-RT_ERROR 线程挂起失败,因为该线程的状态并不是就绪状态
恢复挂起线程

原型:

rt_err_t rt_thread_resume (rt_thread_t thread) 	//线程句柄

返回值:

返回 描述
RT_EOK 线程恢复成功
-RT_ERROR 线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态
控制线程

原型:

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
函数参数 描述
thread 线程句柄
cmd 指示控制命令
arg 控制参数

指示控制命令 cmd 当前支持的命令包括:

  • RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
  • RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;
  • RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。
线程辅助函数

线程辅助函数我们需要学会使用**钩子函数(hook)**来帮我们完成像特定的任务以及监控等任务。

**钩子函数(hook):**就是在特定的场合会进行调用的函数,比如空闲线程钩子函数会在进入到空闲线程时调用一次

空闲线程钩子函数创建

原型:

rt_err_t rt_thread_idle_sethook(void (*hook)(void));	////钩子函数入口

返回值:

返回 描述
RT_EOK 设置成功
-RT_EFULL 设置失败

空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态。同时不允许使用动态内存分配的函数如mallocfree等,因为会造成死锁现象。

下面为具体的解释

  1. 钩子函数的执行上下文:空闲线程钩子函数(Hook)虽然是你写的代码,但它是在空闲线程的上下文中运行的。这意味着,它的执行时机和空闲线程一样,是系统“无事可做”的时候。
  2. 内存管理函数的内部机制:像 mallocfreert_mallocrt_free 这样的动态内存管理函数,为了保证在多线程环境下的安全(即不会出现两个线程同时修改内存管理链表而导致数据错乱),其内部使用了信号量(Semaphore)或互斥锁(Mutex)来进行保护。当你调用这些函数时,它们会先获取(Take) 这个锁。
  3. 致命的死锁(Deadlock)场景
    • 假设现在系统内存不足,某个高优先级线程(Thread A)正在尝试申请内存(rt_malloc),但它因为内存不足而阻塞在了内存管理的信号量上,它在等待其他线程释放内存。
    • 系统没有其他就绪线程,于是切换到空闲线程执行,并运行到了你的钩子函数
    • 你的钩子函数也调用了 rt_malloc
    • rt_malloc 内部试图去获取那个保护内存堆的信号量
    • 问题来了:这个信号量已经被那个高优先级的 Thread A 持有了(或者说它在等待这个信号量),空闲线程(钩子函数)也要等待这个信号量。
    • 但是,Thread A 之所以无法释放信号量,是因为它在等待内存被释放,而能释放内存的空闲线程(钩子函数)却又在等待信号量!
    • 这就形成了一个 “我等你,你等我”的循环等待,系统就此死锁,永远无法恢复。因为空闲线程是优先级最低的,它无法抢占Thread A,而Thread A又在阻塞中。
空闲线程钩子函数删除

原型:

rt_err_t rt_thread_idle_delhook(void (*hook)(void));	//钩子函数入口

返回值:

返回 描述
RT_EOK 删除成功
-RT_ENOSYS 删除失败
调度器钩子函数

原型:

void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to))	////钩子函数入口

所以我们设置的钩子函数原型应该为:

void hook(struct rt_thread* from, struct rt_thread* to)		//从线程到另一个线程

应用代码

#include <rtthread.h>

#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>

uint32_t count=0;

void my_thread1(void*param)
{
    while(1)
    {
//        rt_kprintf("goto idel_thread count:%d\r\n",count);  //测试空闲线程hook
        rt_kprintf("my_thread1 is working.....\r\n");
        rt_thread_mdelay(1000);
    }

}


void my_thread2(void*param)
{
    static uint8_t i=0;
    for (i = 0; i < 10; ++i) {
        rt_kprintf("my_thread2 is working.....\r\n");
        rt_thread_mdelay(1000);
    }
}

void idle_hook(void)
{
    count++;
}

void hook_from_go(struct rt_thread* from, struct rt_thread* to)
{
    rt_kprintf("%s goto %s\r\n",from->name,to->name);
}

int main(void)
{

    rt_kprintf("||main start!||\r\n");

    rt_thread_t thread1;
    thread1=rt_thread_create("thread1", my_thread1, NULL, 1024, 11, 20);
    rt_thread_startup(thread1);

    static struct rt_thread thread2;  // 使用静态结构体,不是指针
    static uint8_t stack[512];  //必须使用静态栈
    rt_thread_init(&thread2, "thread2", my_thread2, NULL, stack, 512, 10, 20);
    rt_thread_startup(&thread2);

    rt_thread_idle_sethook(idle_hook);    //空闲钩子函数
    rt_scheduler_sethook(hook_from_go);  //调度钩子函数

    return RT_EOK;
}

补充说明:

主线程可以配置成两种模式:一种是初始化模式,只用来初始化,另一种是也配置成工作模式,相当就是一个工作的线程。

注意区分一下函数调用和线程调度的区别:函数调用是从函数开始执行,线程调度就是类似于中断进行调度。

时钟管理

时钟是我们整个调度运行的基本,我们需要了解时钟的运行机制以及学会使用定时器资源

时钟节拍

任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是 1ms–100ms,时钟节拍率越快,系统的实时响应越快,但是系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间

系统时钟节拍的定义是在 rtconfig.h 中宏定义 RT_TICK_PER_SECOND 的,1000表示1s中计数1000次,故时钟节拍就为1ms。

时钟节拍实现方式:就是在滴答定时器中每特定时间中断一次,然后对系统节拍数rt_tick++,rt_tick 的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。RT-Thread 定时器的最小精度是由系统时钟节拍所决定的(1 OS Tick = 1/RT_TICK_PER_SECOND 秒,RT_TICK_PER_SECOND 值在 rtconfig.h 文件中定义),定时器设定的时间必须是 OS Tick 的整数倍。

定时器及其工作机制

定时器分类分为:软件定时器以及硬件定时器,下面将详细解释工作机制以及特点

硬件定时器

硬件定时器直接依赖微控制器(MCU)内部的硬件定时器外设,精度非常高,回调函数在中断上下文执行,应简短快速,避免调用可能导致挂起的函数(如rt_thread_mdelay)或进行长时间操作。

软件定时器

软件定时器由RT-Thread系统基于硬件定时器(如SysTick) 提供的系统时钟节拍(OS Tick)模拟实现。其精度取决于RT_TICK_PER_SECOND的配置(例如配置为1000时,一个Tick是1ms)。回调函数是在线程上下文中执行的,所以可以工作时间拉长一点,但同时也不推荐使用延时函数**(挂载),因为会阻塞整个定时器线程**运行。

注意:所有软件定时器(SOFT_TIMER)的共享一个 timer 线程

定时器工作机制

通过构建一个软件定时器链表是动态的链表rt_timer_list ,系统新创建并激活的定时器都会按照以超时时间排序的方式插入到该链表中。

在这里插入图片描述

当该定时器触发后,会从系统定时器链表中删除,如果是单次定时器的话就不会再次加入该链表,如果为周期性定时器的话就会重新计算超时时间然后再次加入该链表。

定时器控制块

定时器控制块类似于结构体包含了定时器的所有信息,有助于我们了解工作。

struct rt_timer
{
    struct rt_object parent;
    rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];  /* 定时器链表节点 */

    void (*timeout_func)(void *parameter);    /* 定时器超时调用的函数 */
    void      *parameter;                         /* 超时函数的参数 */
    rt_tick_t init_tick;                         /* 定时器初始超时节拍数 */
    rt_tick_t timeout_tick;                     /* 定时器实际超时时的节拍数 */
};
typedef struct rt_timer *rt_timer_t;

定时器常用函数

定时器常用函数就是初始化启动停止定时器等。

在这里插入图片描述

获取时钟节拍(系统运行时间)

原型:

rt_tick_t rt_tick_get(void);

返回值:

返回 描述
rt_tick 当前时钟节拍值
动态创建定时器

原型:

rt_timer_t rt_timer_create(const char* name,	//定时器名
                           void (*timeout)(void* parameter),	//定时器超时函数
                           void* parameter,	//回调函数参数
                           rt_tick_t time,	//定时器超时时间
                           rt_uint8_t flag);	//定时器标志(可以用 “或” 关系取多个值)

定时器标志(flag)

#define RT_TIMER_FLAG_DEACTIVATED       0x0             /**< 定时器处于未激活状态--无需设置 */
#define RT_TIMER_FLAG_ACTIVATED         0x1             /**< 定时器处于激活状态--无需设置 */
#define RT_TIMER_FLAG_ONE_SHOT          0x0             /**< 单次定时器 */
#define RT_TIMER_FLAG_PERIODIC          0x2             /**< 周期性定时器 */

#define RT_TIMER_FLAG_HARD_TIMER        0x0             /**< 硬件定时器,定时器的回调函数将在 tick 中断服务程序中调用 */
#define RT_TIMER_FLAG_SOFT_TIMER        0x4             /**< 软定时器,定时器的回调函数将在定时器线程中调用 */

返回值:

返回 描述
RT_NULL 创建失败(通常会由于系统内存不够用而返回 RT_NULL)
定时器的句柄 定时器创建成功
动态定时器删除

原型:

rt_err_t rt_timer_delete(rt_timer_t timer);	//定时器句柄

返回值:

返回 描述
RT_EOK 删除成功(如果参数 timer 句柄是一个 RT_NULL,将会导致一个 ASSERT 断言)
静态初始化定时器

原型:

void rt_timer_init(rt_timer_t timer,	//定时器句柄
                   const char* name,	//定时器名
                   void (*timeout)(void* parameter),	//定时器回调函数
                   void* parameter,	//回调函数参数
                   rt_tick_t time, rt_uint8_t flag);	//定时器标志(可以用 “或” 关系取多个值)
静态定时器移除

原型:

rt_err_t rt_timer_detach(rt_timer_t timer);	//定时器句柄

返回值:

返回 描述
RT_EOK 脱离成功
启动定时器

原型:

rt_err_t rt_timer_start(rt_timer_t timer);	//定时器句柄

定时器的状态将更改为激活状态(RT_TIMER_FLAG_ACTIVATED),并按照超时顺序插入到 rt_timer_list 队列链表中

返回值:

返回 描述
RT_EOK 启动成功
停止定时器

原型:

rt_err_t rt_timer_stop(rt_timer_t timer);

定时器的状态将更改为未激活状态(RT_TIMER_FLAG_DEACTIVATED),同时从定时器链表中删除。

返回值:

返回 描述
RT_EOK 成功停止定时器
- RT_ERROR timer 已经处于停止状态
定时器控制函数

原型:

rt_err_t rt_timer_control(rt_timer_t timer, rt_uint8_t cmd, void* arg);	//定时器句柄  命令  参数

cmd命令说明:

#define RT_TIMER_CTRL_SET_TIME          0x0             /**< 设置定时器时间的控制命令 */
#define RT_TIMER_CTRL_GET_TIME          0x1             /**< 获取定时器时间的控制命令 */
#define RT_TIMER_CTRL_SET_ONESHOT       0x2             /**< 将定时器设置为单次模式 */
#define RT_TIMER_CTRL_SET_PERIODIC      0x3             /**< 将定时器设置为周期模式 */
#define RT_TIMER_CTRL_GET_STATE         0x4             /**< 获取定时器运行状态(激活或未激活)*/
#define RT_TIMER_CTRL_GET_REMAIN_TIME   0x5             /**< 获取定时器剩余时间 */

注:如果为获取时间,则会通过arg变量返回,存储在该变量里。

返回值:

返回 描述
RT_EOK 成功
高精度延时函数

原型:

void rt_hw_us_delay(rt_uint32_t us)	//延时us

注意:

该函数的实现利用的是滴答定时器的寄存器的值,与滴答定时器中断相当于是并行的,它是直接读取定时器寄存器(该值是递减的,直到0)的值实现的,同时它是一个阻塞式的,所以该函数的us延时必须小于一个时钟节拍,避免影响滴答定时器中断导致时钟节拍不具备实时性。如:时钟节拍是1ms,则传入的us就应该小于999。

应用代码

#include <rtthread.h>

#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>


void soft1_timeout(void*param)
{
    rt_kprintf("soft1_timer is working...\r\n");
    rt_kprintf("rt_tick=%d\r\n",rt_tick_get());
}

void hard1_timeout(void*param)
{
    rt_kprintf("hard1_timer is working...\r\n");
    rt_kprintf("rt_tick=%d\r\n",rt_tick_get());
}

int main(void)
{
    rt_kprintf("||main start!||\r\n");

    //动态定时器创建2000ms定时器
    rt_timer_t soft1_timer=NULL;
    soft1_timer=rt_timer_create("soft1", soft1_timeout, NULL, 2000, RT_TIMER_FLAG_SOFT_TIMER|RT_TIMER_FLAG_PERIODIC);
    //静态初始化一格5000ms定时器
    static struct rt_timer hard1_timer;
    rt_timer_init(&hard1_timer, "hard1", hard1_timeout, NULL, 5000, RT_TIMER_FLAG_HARD_TIMER|RT_TIMER_FLAG_ONE_SHOT);

    //启动定时器
    rt_timer_start(soft1_timer);
    rt_timer_start(&hard1_timer);


    return RT_EOK;
}

----- | ---- |
| RT_EOK | 成功 |

高精度延时函数

原型:

void rt_hw_us_delay(rt_uint32_t us)	//延时us

注意:

该函数的实现利用的是滴答定时器的寄存器的值,与滴答定时器中断相当于是并行的,它是直接读取定时器寄存器(该值是递减的,直到0)的值实现的,同时它是一个阻塞式的,所以该函数的us延时必须小于一个时钟节拍,避免影响滴答定时器中断导致时钟节拍不具备实时性。如:时钟节拍是1ms,则传入的us就应该小于999。

应用代码

#include <rtthread.h>

#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>


void soft1_timeout(void*param)
{
    rt_kprintf("soft1_timer is working...\r\n");
    rt_kprintf("rt_tick=%d\r\n",rt_tick_get());
}

void hard1_timeout(void*param)
{
    rt_kprintf("hard1_timer is working...\r\n");
    rt_kprintf("rt_tick=%d\r\n",rt_tick_get());
}

int main(void)
{
    rt_kprintf("||main start!||\r\n");

    //动态定时器创建2000ms定时器
    rt_timer_t soft1_timer=NULL;
    soft1_timer=rt_timer_create("soft1", soft1_timeout, NULL, 2000, RT_TIMER_FLAG_SOFT_TIMER|RT_TIMER_FLAG_PERIODIC);
    //静态初始化一格5000ms定时器
    static struct rt_timer hard1_timer;
    rt_timer_init(&hard1_timer, "hard1", hard1_timeout, NULL, 5000, RT_TIMER_FLAG_HARD_TIMER|RT_TIMER_FLAG_ONE_SHOT);

    //启动定时器
    rt_timer_start(soft1_timer);
    rt_timer_start(&hard1_timer);


    return RT_EOK;
}
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐