【RT-Thread Studio】RT-Thread学习笔记

文章目录

RT-Thread学习笔记

2024.8.2-2014.8.15学习RT-Thread后的笔记

参考文档https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/README

内核

1 内核基础

1.1 内核介绍

image-20240802103525304

内核库是为了保证内核能够独立运行的一套小型的类似 C 库的函数实现子集。

提示:C 库:也叫 C 运行库(C Runtime Library),它提供了类似 “strcpy”、“memcpy” 等函数,有些也会包括 “printf”、“scanf” 函数的实现。RT-Thread Kernel Service Library 仅提供内核用到的一小部分 C 库函数实现,为了避免与标准 C 库重名,在这些函数前都会添加上 rt_前缀。

1.1.2 线程调度
  • 全抢占式多线程调度算法
  • 支持256个线程优先级,STM32默认32个线程优先级,0代表最高
  • 支持相同优先级,采用时间片轮转调度算法调度
1.1.3 时钟管理

​ RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止定时器否则将永远持续执行下去。

1.1.4 线程间同步

​ RT-Thread 采用信号量、互斥量与事件集实现线程间同步。线程通过对信号量、互斥量的获取与释放进行同步;互斥量采用优先级继承的方式解决了实时系统常见的优先级翻转问题。

1.1.5 线程间通信

支持邮箱和消息队列等通信机制

  • 邮箱:一封邮件长度固定4个字节
  • 消息队列:能接受不固定长度消息,缓存在自己内存空间

差异:邮箱效率比消息队列高效

1.2 启动流程

​ 一般执行顺序是:系统先从启动文件开始运行,然后进入 RT-Thread 的启动函数 rtthread_startup() ,最后进入用户入口函数 main(),如下图所示:

启动流程

先调用starup(汇编):

​ 在MDK中,可以使用拓展功能,给main添加$Sub$$成为一个新功能函数,可以调用要补充在main之前的功能函数(添加RT-Thread系统启动,初始化),再调用$Super$$main转到main()函数执行

/* $Sub$$main 函数 */
int $Sub$$main(void)
{
  rtthread_startup();  // 在main之前启动RT-Thread,初始化系统
  return 0;
}

其中 rtthread_startup() 函数的代码如下所示:

int rtthread_startup(void)
{
    rt_hw_interrupt_disable();

    /* 板级初始化:需在该函数内部进行系统堆的初始化 */
    rt_hw_board_init();

    /* 打印 RT-Thread 版本信息 */
    rt_show_version();

    /* 定时器初始化 */
    rt_system_timer_init();

    /* 调度器初始化 */
    rt_system_scheduler_init();

#ifdef RT_USING_SIGNALS
    /* 信号初始化 */
    rt_system_signal_init();
#endif

    /* 由此创建一个用户 main 线程 */
    rt_application_init();

    /* 定时器线程初始化 */
    rt_system_timer_thread_init();

    /* 空闲线程初始化 */
    rt_thread_idle_init();

    /* 启动调度器 */
    rt_system_scheduler_start();

    /* 不会执行至此 */
    return 0;
}

这部分启动代码,大致可以分为四个部分:

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

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

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

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

rt_application_init()中:

主线程中,调用$Super$main()main

1.3 程序内存分布

一般MCU包含的存储空间有两个:

  • 片内Flash(硬盘)
  • 片内RAM(内存)

编译器会将程序分成好几个部分存储在不同区域:

1)Code:代码段,存放程序的代码部分;

2)RO-data:只读数据段,存放程序中定义的常量;

3)RW-data:读写数据段,存放初始化为非 0 值的全局变量;

4)ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量;

​ 编译完工程会生成一个.map 的文件,该文件说明了各个函数占用的尺寸和地址,在文件的最后几行也说明了上面几个字段的关系:

Total RO Size (Code + RO Data) 53668 ( 52.41kB)
Total RW Size (RW Data + ZI Data) 2728 ( 2.66kB)
Total ROM Size (Code + RO Data + RW Data) 53780 ( 52.52kB)

1)RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小;

2)RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小;

3)ROM Size 包含了 Code、RO-data 以及 RW-data,表示烧写程序所占用的 Flash 空间的大小;

将hex文件烧录到32中的内存分布包含RO段和RW段:

  • RO段:Code、RO-data
  • RW段:RW-data
  • ZI-data都是0,不包含在映像中

​ 32上电启动默认从Flash,启动后会将RW段中的RW-data搬到RAM,不搬RO,另外根据编译器给出的ZI地址分配出ZI段,并将这区域清零:

RT-Thread 内存分布

应用程序申请和释放的内存块都在动态内存堆,例,

rt_uint8_t* msg_ptr;
msg_ptr = (rt_uint8_t*) rt_malloc (128);
rt_memset(msg_ptr, 0, 128);
1.4 自动初始化机制

​ 初始化函数不需要被显式调用,只需要添加宏定义申明,就会在系统启动过程中执行,例

int rt_hw_usart_init(void)  /* 串口初始化函数 */
{
     ... ...
     /* 注册串口 1 设备 */
     rt_hw_serial_register(&serial1, "uart1",
                        RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
                        uart);
     return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init);    /* 使用组件自动初始化机制 */

示例代码最后的 INIT_BOARD_EXPORT(rt_hw_usart_init) 表示使用自动初始化功能,按照这种方式,rt_hw_usart_init() 函数就会被系统自动调用。

​ RT-Thread 的自动初始化机制使用了自定义 RTI 符号段,将需要在启动时进行初始化的函数指针放到了该段中,形成一张初始化函数表,在系统启动过程中会遍历该表,并调用表中的函数,达到自动初始化的目的。

​ 用来实现自动初始化功能的宏接口定义详细描述如下表所示:

初始化顺序宏接口描述
1INIT_BOARD_EXPORT(fn)非常早期的初始化,此时调度器还未启动
2INIT_PREV_EXPORT(fn)主要是用于纯软件的初始化、没有太多依赖的函数
3INIT_DEVICE_EXPORT(fn)外设驱动初始化相关,比如网卡设备
4INIT_COMPONENT_EXPORT(fn)组件初始化,比如文件系统或者 LWIP
5INIT_ENV_EXPORT(fn)系统环境初始化,比如挂载文件系统
6INIT_APP_EXPORT(fn)应用初始化,比如 GUI 应用
1.5 内核对象模型
1.5.1 静态对象和动态对象

thread1是静态线程对象,thread2是动态线程对象

/* 线程 1 的对象和运行时用到的栈 */
static struct rt_thread thread1;
static rt_uint8_t thread1_stack[512];

/* 线程 1 入口 */
void thread1_entry(void* parameter)
{
     int i;

    while (1)
    {
        for (i = 0; i < 10; i ++)
        {
            rt_kprintf("%d\n", i);

            /* 延时 100ms */
            rt_thread_mdelay(100);
        }
    }
}

/* 线程 2 入口 */
void thread2_entry(void* parameter)
{
     int count = 0;
     while (1)
     {
         rt_kprintf("Thread2 count:%d\n", ++count);

        /* 延时 50ms */
        rt_thread_mdelay(50);
    }
}

/* 线程例程初始化 */
int thread_sample_init()
{
     rt_thread_t thread2_ptr;
     rt_err_t result;

    /* 初始化线程 1 */
    /* 线程的入口是 thread1_entry,参数是 RT_NULL
     * 线程栈是 thread1_stack
     * 优先级是 200,时间片是 10 个 OS Tick
     */
    result = rt_thread_init(&thread1,
                            "thread1",
                            thread1_entry, RT_NULL,
                            &thread1_stack[0], sizeof(thread1_stack),
                            200, 10);

    /* 启动线程 */
    if (result == RT_EOK) rt_thread_startup(&thread1);

    /* 创建线程 2 */
    /* 线程的入口是 thread2_entry, 参数是 RT_NULL
     * 栈空间是 512,优先级是 250,时间片是 25 个 OS Tick
     */
    thread2_ptr = rt_thread_create("thread2",
                                thread2_entry, RT_NULL,
                                512, 250, 25);

    /* 启动线程 */
    if (thread2_ptr != RT_NULL) rt_thread_startup(thread2_ptr);

    return 0;
}

区别:静态对象会一直占用RAM空间,动态对象只有在运行时才会申请RAM空间,对象被删除后空间释放

1.5.2 内核对象管理架构

所有内核对象的信息(对象类型、大小)都存放在对象容器中,每类对象都被连接在一个链表上:

RT-Thread 的内核对象容器及链表

1.5.3 对象控制块

内核对象的数据结构:

struct rt_object
{
     /* 内核对象名称     */
     char name[RT_NAME_MAX];
     /* 内核对象类型     */
     rt_uint8_t  type;
     /* 内核对象的参数   */
     rt_uint8_t  flag;
     /* 内核对象管理链表 */
     rt_list_t   list;
};

目前内核对象支持的类型如下:

enum rt_object_class_type
{
     RT_Object_Class_Thread = 0,             /* 对象为线程类型      */
#ifdef RT_USING_SEMAPHORE
    RT_Object_Class_Semaphore,              /* 对象为信号量类型    */
#endif
#ifdef RT_USING_MUTEX
    RT_Object_Class_Mutex,                  /* 对象为互斥量类型    */
#endif
#ifdef RT_USING_EVENT
    RT_Object_Class_Event,                  /* 对象为事件类型      */
#endif
#ifdef RT_USING_MAILBOX
    RT_Object_Class_MailBox,                /* 对象为邮箱类型      */
#endif
#ifdef RT_USING_MESSAGEQUEUE
    RT_Object_Class_MessageQueue,           /* 对象为消息队列类型   */
#endif
#ifdef RT_USING_MEMPOOL
    RT_Object_Class_MemPool,                /* 对象为内存池类型     */
#endif
#ifdef RT_USING_DEVICE
    RT_Object_Class_Device,                 /* 对象为设备类型       */
#endif
    RT_Object_Class_Timer,                  /* 对象为定时器类型     */
#ifdef RT_USING_MODULE
    RT_Object_Class_Module,                 /* 对象为模块          */
#endif
    RT_Object_Class_Unknown,                /* 对象类型未知        */
    RT_Object_Class_Static = 0x80           /* 对象为静态对象      */
};

其中最后一项说明,如果是静态对象,其最高位是1(与其他对象类型的或操作),否则就是动态对象。系统最多能容纳127个对象类别。

1.5.4 内核对象管理方式

内核对象容器的数据结构:

struct rt_object_information
{
     /* 对象类型 */
     enum rt_object_class_type type;
     /* 对象链表 */
     rt_list_t object_list;
     /* 对象大小 */
     rt_size_t object_size;
};

​ 一类对象由一个 rt_object_information 结构体来管理,每一个这类对象的具体实例都通过链表的形式挂接在 object_list 上。而这一类对象的内存块尺寸由 object_size 标识出来(每一类对象的具体实例,他们占有的内存块大小都是相同的)。

1.5.5 初始化对象(静态)

静态对象使用前必须先初始化,初始化的接口:

void rt_object_init(struct  rt_object*  object ,
                    enum rt_object_class_type  type ,
                    const char* name)

​ 调用这个函数初始化对象后,系统就会把这个对象放到对象容器中进行管理-初始化参数,把对象结点插入到对象容器的对象链表中。参数说明:

参数描述
object需要初始化的对象指针,它必须指向具体的对象内存块,而不能是空指针或野指针
type对象的类型,必须是 rt_object_class_type 枚举类型中列出的除 RT_Object_Class_Static 以外的类型(对于静态对象,或使用 rt_object_init 接口进行初始化的对象,系统会把它标识成 RT_Object_Class_Static 类型)
name对象的名字。每个对象可以设置一个名字,这个名字的最大长度由 RT_NAME_MAX 指定,并且系统不关心它是否是由’\0’做为终结符
1.5.6 脱离对象(静态)
void rt_object_detach(rt_object_t object);

​ 调用该接口是一个静态内核对象从对象容器中脱离-从对象容器链表中删除相应对象节点。对象脱离后,其占用内存不会被税释放。

1.5.7 分配对象(动态)

动态对象在需要时申请,不需要时释放内存。申请分配新的对象使用以下接口:

rt_object_t rt_object_allocate(enum  rt_object_class_type type, const  char*  name)

​ 系统根据对象类型获取对象信息,分配对应大小内存空间,初始化,最后插入所在对象容器链表中。参数说明:

参数描述
type分配对象的类型,只能是 rt_object_class_type 中除 RT_Object_Class_Static 以外的类型。并且经过这个接口分配出来的对象类型是动态的,而不是静态的
name对象的名字。每个对象可以设置一个名字,这个名字的最大长度由 RT_NAME_MAX 指定,并且系统不关心它是否是由’\0’做为终结符
返回——
分配成功的对象句柄分配成功
RT_NULL分配失败
1.5.8 删除对象(动态)

动态对象不用时可以删除,调用如下接口:

void rt_object_delete(rt_object_t object);

​ 调用后,首先从对象容器链表中脱离对象,然后释放占用内存。参数说明:

参数描述
object对象的句柄
1.5.9 辨别对象(静态)

​ 调用以下接口判断对象是否是系统对象(静态内核对象):

rt_err_t rt_object_is_systemobject(rt_object_t object);

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

1.5.10 如何遍历内核对象

遍历所有线程:

注意:RT-Thread5.0 及更高版本将 struct rt_thread 结构体的 name 成员移到了 parent 里,使用时代码需要由 thread->name 更改为 thread->parent.name,否则编译会报错!

// 创建并初始化线程对象
rt_thread_t thread = RT_NULL;
// 创建并初始化节点链表
struct rt_list_node *node = RT_NULL;
// 创建并初始化对象容器
struct rt_object_information *information = RT_NULL;

// 将对象容器类型设置为线程类型
information = rt_object_get_information(RT_Object_Class_Thread);

// 遍历线程容器中所有节点并打印名字
rt_list_for_each(node, &(information->object_list))
{
    thread = (rt_thread_t)rt_list_entry(node, struct rt_object, list);
    /* 比如打印所有thread的名字 */
    rt_kprintf("name:%s\n", thread->name);
}

遍历所有互斥量:

// 创建并初始化互斥量对象
rt_mutex_t mutex = RT_NULL;
// 创建并初始化对象节点
struct rt_list_node *node = RT_NULL;
// 创建并初始化对象容器
struct rt_object_information *information = RT_NULL;

// 将对象容器类型设置为互斥量类型
information = rt_object_get_information(RT_Object_Class_Mutex);

// 遍历容器中所有互斥量节点并打印名字
rt_list_for_each(node, &(information->object_list))
{
    mutex = (rt_mutex_t)rt_list_entry(node, struct rt_object, list);
    /* 比如打印所有mutex的名字 */
    rt_kprintf("name:%s\n", mutex->parent.parent.name);
}
1.5.11 内核配置示例

​ 可以修改工程目录下的rtconfig.h文件进行内核配置,通过修改、打开/关闭文件中的宏定义,实现系统配置和裁剪。

(1) RT-Thread内核部分

/* 表示内核对象的名称的最大长度,若代码中对象名称的最大长度大于宏定义的长度,
 * 多余的部分将被截掉。*/
#define RT_NAME_MAX 8

/* 字节对齐时设定对齐的字节个数。常使用 ALIGN(RT_ALIGN_SIZE) 进行字节对齐。*/
#define RT_ALIGN_SIZE 4

/* 定义系统线程优先级数;通常用 RT_THREAD_PRIORITY_MAX-1 定义空闲线程的优先级 */
#define RT_THREAD_PRIORITY_MAX 32

/* 定义时钟节拍,为 100 时表示 100 个 tick 每秒,一个 tick 为 10ms */
#define RT_TICK_PER_SECOND 100

/* 检查栈是否溢出,未定义则关闭 */
#define RT_USING_OVERFLOW_CHECK

/* 定义该宏开启 debug 模式,未定义则关闭 */
#define RT_DEBUG
/* 开启 debug 模式时:该宏定义为 0 时表示关闭打印组件初始化信息,定义为 1 时表示启用 */
#define RT_DEBUG_INIT 0
/* 开启 debug 模式时:该宏定义为 0 时表示关闭打印线程切换信息,定义为 1 时表示启用 */
#define RT_DEBUG_THREAD 0

/* 定义该宏表示开启钩子函数的使用,未定义则关闭 */
#define RT_USING_HOOK

/* 定义了空闲线程的栈大小 */
#define IDLE_THREAD_STACK_SIZE 256

(2) 线程间同步与通信部分,会用到的对象有信号量、互斥量、时间、邮箱、消息队列、信号等。

/* 定义该宏可开启信号量的使用,未定义则关闭 */
#define RT_USING_SEMAPHORE

/* 定义该宏可开启互斥量的使用,未定义则关闭 */
#define RT_USING_MUTEX

/* 定义该宏可开启事件集的使用,未定义则关闭 */
#define RT_USING_EVENT

/* 定义该宏可开启邮箱的使用,未定义则关闭 */
#define RT_USING_MAILBOX

/* 定义该宏可开启消息队列的使用,未定义则关闭 */
#define RT_USING_MESSAGEQUEUE

/* 定义该宏可开启信号的使用,未定义则关闭 */
#define RT_USING_SIGNALS

(3) 内存管理部分

/* 开启静态内存池的使用 */
#define RT_USING_MEMPOOL

/* 定义该宏可开启两个或以上内存堆拼接的使用,未定义则关闭 */
#define RT_USING_MEMHEAP

/* 开启小内存管理算法 */
#define RT_USING_SMALL_MEM

/* 关闭 SLAB 内存管理算法 */
/* #define RT_USING_SLAB */

/* 开启堆的使用 */
#define RT_USING_HEAP

(4) 内核设备对象

/* 表示开启了系统设备的使用 */
#define RT_USING_DEVICE

/* 定义该宏可开启系统控制台设备的使用,未定义则关闭 */
#define RT_USING_CONSOLE
/* 定义控制台设备的缓冲区大小 */
#define RT_CONSOLEBUF_SIZE 128
/* 控制台设备的名称 */
#define RT_CONSOLE_DEVICE_NAME "uart1"

(5) 自动初始化方式

/* 定义该宏开启自动初始化机制,未定义则关闭 */
#define RT_USING_COMPONENTS_INIT

/* 定义该宏开启设置应用入口为 main 函数 */
#define RT_USING_USER_MAIN
/* 定义 main 线程的栈大小 */
#define RT_MAIN_THREAD_STACK_SIZE 2048

(6) FinSH

/* 定义该宏可开启系统 FinSH 调试工具的使用,未定义则关闭 */
#define RT_USING_FINSH

/* 开启系统 FinSH 时:将该线程名称定义为 tshell */
#define FINSH_THREAD_NAME "tshell"

/* 开启系统 FinSH 时:使用历史命令 */
#define FINSH_USING_HISTORY
/* 开启系统 FinSH 时:对历史命令行数的定义 */
#define FINSH_HISTORY_LINES 5

/* 开启系统 FinSH 时:定义该宏开启使用 Tab 键,未定义则关闭 */
#define FINSH_USING_SYMTAB

/* 开启系统 FinSH 时:定义该线程的优先级 */
#define FINSH_THREAD_PRIORITY 20
/* 开启系统 FinSH 时:定义该线程的栈大小 */
#define FINSH_THREAD_STACK_SIZE 4096
/* 开启系统 FinSH 时:定义命令字符长度 */
#define FINSH_CMD_SIZE 80

/* 开启系统 FinSH 时:定义该宏开启 MSH 功能 */
#define FINSH_USING_MSH
/* 开启系统 FinSH 时:开启 MSH 功能时,定义该宏默认使用 MSH 功能 */
#define FINSH_USING_MSH_DEFAULT
/* 开启系统 FinSH 时:定义该宏,仅使用 MSH 功能 */
#define FINSH_USING_MSH_ONLY

(7) 关于MCU

/* 定义该工程使用的 MCU 为 STM32F103ZE;系统通过对芯片类型的定义,来定义芯片的管脚 */
#define STM32F103ZE

/* 定义时钟源频率 */
#define RT_HSE_VALUE 8000000

/* 定义该宏开启 UART1 的使用 */
#define RT_USING_UART1

注:在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。

1.5.12 常见宏定义说明
  1. rt_inline
  • 定义
#define rt_inline                   static __inline
  • 说明

static是函数只能在当前文件中使用;inline表示内联,只有在符合编译器要求才会展开,是对宏定义的优化,以空间换时间

  1. RT_USED
  • 定义
#define RT_USED                     __attribute__((used))
  • 说明

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

注意:RT-Thread 5.0 及更高的版本将 RT_USED 关键字改成了 rt_used,使用时注意修改。

  1. RT_UNUSED
  • 定义
#define RT_UNUSED                   ((void)x)
  • 说明

​ 表示函数或变量不可以用,屏蔽warning信息。

  1. RT_WEAK
  • 定义
#define RT_WEAK                     __weak
  • 说明

​ 用于定义函数,编译器链接函数时先链接没有该关键字前缀的函数,找不到再链接由weak修饰的函数。

注意:RT-Thread 5.0 及更高的版本将 RT_WEAK 关键字改成了 rt_weak,使用时注意修改。

  1. ALIGN(n)
  • 定义
#define ALIGN(n)                    __attribute__((aligned(n)))
  • 说明

​ 给对象分配地址空间时,将其地址按n字节对齐,n取2的幂次方。

  • 作用

​ 便于CPU快速访问,合理使用可以节省存储空间(调整好定义的顺序)。

注意:RT-Thread 5.0 及更高的版本将 ALIGN 关键字改成了 rt_align,使用时注意修改。

  1. RT_ALIGN(size, align)
  • 定义
#define RT_ALIGN(size, align)      (((size) + (align) - 1) & ~((align) - 1))
  • 说明

​ 将size提升为align定义的最小整数倍,例,RT_ALIGN(13, 4)将返回16。

2 线程管理

2.1 线程的工作机制
2.1.1 线程控制块
  • 说明

​ 用于管理线程的一个数据结构,存放线程信息:优先级、线程名称、线程状态等,也包含线程与线程间的链表结构,线程等待时间集合等。

  • 定义
/* 线程控制块 */
struct rt_thread
{
    /* rt 对象 */
    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_err_t    error;                  /* 线程错误代码 */
    rt_uint8_t  stat;                   /* 线程状态 */

    /* 优先级 */
    rt_uint8_t  current_priority;    /* 当前优先级 */
    rt_uint8_t  init_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_uint32_t user_data;                      /* 用户数据 */
};

​ 其中init_priority是创建线程时要指定的优先级。cleanup会在线程退出时被空闲线程回调一次来清理现场。最后一个成员user_data可又用户挂接一些数据信息到线程数据块中,以实现线程私有数据。

2.1.2 线程的重要属性

线程栈:

​ 线程有独立的栈,线程切换时,会将当前线程的上下文入栈,保存现场,线程恢复运行时出栈(读取),恢复现场。

​ 线程栈还用来存放函数中的局部变量:函数局部变量从栈空间中申请;一个函数调用另一个函数时,其局部变量存入栈中。

线程状态:

image-20240802180506587

状态描述
初始状态当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 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),ARM Cortex-M系列普遍采用32个优先级。对于优先级相同的线程,调度器按照先进先出(FIFO)的原则:先被加入就绪对应列的线程会先被执行。

时间片:

​ 时间片是专门约束线程运行运行时长,单位是一个系统节拍(OS Tick)。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度。

线程入口函数:

​ 线程控制块中的 entry 是线程的入口函数,一般有以下两种代码形式:

  • 无限循环模式:

​ 持续等待外界事件发生,而后进行相应服务:

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

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

​ 线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直.被系统循环调度运行,永不删除。

  • 顺序执行或有限次循环模式:

​ do while()或者for()循环,不会永久循环,一定会执行完毕,而后线程被系统自动删除。

static void thread_entry(void* parameter)
{
    /* 处理事务 #1 *//* 处理事务 #2 *//* 处理事务 #3 */
}

线程错误码:

​ 每个线程配备一个变量用于保存错误码:

#define RT_EOK           0 /* 无错误     */
#define RT_ERROR         1 /* 普通错误     */
#define RT_ETIMEOUT      2 /* 超时错误     */
#define RT_EFULL         3 /* 资源已满     */
#define RT_EEMPTY        4 /* 无资源     */
#define RT_ENOMEM        5 /* 无内存     */
#define RT_ENOSYS        6 /* 系统不支持     */
#define RT_EBUSY         7 /* 系统忙     */
#define RT_EIO           8 /* IO 错误       */
#define RT_EINTR         9 /* 中断系统调用   */
#define RT_EINVAL       10 /* 非法参数      */
2.1.3 线程状态切换

线程状态转换图

​ 线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数rt_thread_startup()进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay()rt_sem_take()rt_mutex_take()rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行rt_thread_exit()函数,将状态更改为关闭状态。

2.1.4 系统线程

空闲线程:

​ 优先级最低,永远为就绪态,系统没有就绪线程时,调度器调度到空闲线程,通常为死循环,且永远不要能被挂起。

用途:

  • 回收僵尸线程:

​ 某线程执行完毕,系统自动执行rt_thread_exit(),删除线程(从系统就绪队列中),再将线程状态改为关闭,挂入rt_thread_defunct僵尸队列,由空闲线程回收被删除线程资源。

  • 运行用户设置钩子函数(功耗管理、看门狗喂狗等):

​ 必须得到执行,即其他线程不允许一直死循环,必须调用具有阻塞性质的函数。

主线程:

​ 主线程main线程,入口函数为main_thread_entry(),系统调度器启动,main线程就开始运行,main线程初始化过程如下:

主线程调用过程

2.2 线程的管理方式

​ 可以使用rt_thread_create()创建一个动态线程(系统分配),使用rt_thread_init()初始化一个静态线程(用户分配)。

线程相关操作

2.2.1 创建和删除动态线程

创建动态线程的接口:

rt_thread_t rt_thread_create(const char* name,
                            void (*entry)(void* parameter),
                            void* parameter,
                            rt_uint32_t stack_size,
                            rt_uint8_t priority,
                            rt_uint32_t tick);

​ 调用该函数,系统从动态堆内存分配一个线程句柄并按照参数中指定栈大小从动态堆内存中分配相应空间,按rtconfig.h中配置的RT_ALIGN_SIZE 方式对齐。参数说明:

参数描述
name线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
entry线程入口函数
parameter线程入口函数参数
stack_size线程栈大小,单位是字节
priority线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
tick线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回——
thread线程创建成功,返回线程句柄
RT_NULL线程创建失败

​ 通过rt_thread_create(),创建出来的线程不需使用或者运行出错时,用以下接口完全删除线程:

rt_err_t rt_thread_delete(rt_thread_t thread);

​ 调用后把线程从就绪队列中删除,更改为关闭状态,到下一次执行空闲线程时,回收内存。参数说明:

参数描述
thread要删除的线程句柄
返回——
RT_EOK删除线程成功
-RT_ERROR删除线程失败

注:rt_thread_create() 和 rt_thread_delete() 函数仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)。

2.2.2 初始化和脱离静态线程

初始化静态线程的接口:

rt_err_t rt_thread_init(struct rt_thread* thread,
                        const char* name,
                        void (*entry)(void* parameter), void* parameter,
                        void* stack_start, rt_uint32_t stack_size,
                        rt_uint8_t priority, rt_uint32_t tick);

​ 线程控制块、线程运行栈都设置为全局变量,在编译时就被确定和分配,内核不负责动态分配内存空间。参数说明:

参数描述
thread线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址
name线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉
entry线程入口函数
parameter线程入口函数参数
stack_start线程栈起始地址
stack_size线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐)
priority线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级
tick线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回——
RT_EOK线程创建成功
-RT_ERROR线程创建失败

静态线程脱离的接口:

rt_err_t rt_thread_detach (rt_thread_t thread);

参数说明:

参数描述
thread线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。
返回——
RT_EOK线程脱离成功
-RT_ERROR线程脱离失败
2.2.3 启动线程

静态动态线程启动的接口都是:

rt_err_t rt_thread_startup(rt_thread_t thread);

​ 把线程状态更改为就绪状态,放在优先级队列中等待调度。参数说明:

参数描述
thread线程句柄
返回——
RT_EOK线程启动成功
-RT_ERROR线程启动失败
2.2.4 获得当前线程

接口:

rt_thread_t rt_thread_self(void);

返回值:

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

​ 当线程时间片用完或者主动让出时,调度器选择相同优先级的下一个线程执行。调用后,线程仍在就绪队列中:

rt_err_t rt_thread_yield(void);

rt_thread_yield() 函数和 rt_schedule() 函数比较相像,但在有相同优先级的其他就绪态线程存在时,系统的行为却完全不一样。执行rt_thread_yield()函数后,当前线程被换出,相同优先级的下一个就绪线程将被执行。而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。

2.2.6 使线程睡眠

使当前运行的线程延迟一段时间:

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);

​ 指定一段时间,当时间过后,线程会被唤醒并再次进入就绪状态。参数说明:

参数描述
tick/ms线程睡眠的时间: sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ; mdelay 的传入参数 ms 以 1ms 为单位;
返回——
RT_EOK操作成功
2.2.7 挂起和恢复线程
  • 挂起线程

​ 线程调用rt_thread_delay()时,线程主动挂起;当调用rt_sem_take()rt_mb_recv()等函数时,资源不可使用也将导致线程挂起。挂起的线程等待资源超时时会返回就绪状态,如果其他线程释放掉该线程所等待的资源,该线程也会返回到就绪状态。

线程挂起函数接口:

rt_err_t rt_thread_suspend (rt_thread_t thread);

参数说明:

参数描述
thread线程句柄
返回——
RT_EOK线程挂起成功
-RT_ERROR线程挂起失败,因为该线程的状态并不是就绪状态

注:一般不允许一个线程挂起另外一个线程,只能挂起自己,并且挂起后需要立刻调用rt_schedule()进行手动的线程上下文切换

  • 恢复线程

​ 就是挂起的线程重新进入就绪状态;如果被恢复的线程在所有就绪线程中位于最高优先级链表的第一位,那系统将进行线程上下文切换。接口:

rt_err_t rt_thread_resume (rt_thread_t thread);

参数说明:

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

控制线程和更改属性,如优先级:

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

参数说明:

函数参数描述
thread线程句柄
cmd指示控制命令
arg控制参数
返回——
RT_EOK控制执行正确
-RT_ERROR失败

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

  • RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
  • RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;
  • RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。
2.2.9 设置和删除空闲钩子

​ 系统执行空闲线程时调用,做一些其他事情,比如系统指示灯,设置/删除钩子函数的接口:

// 设置
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
// 删除
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

rt_thread_idle_sethook()的参数说明:

函数参数描述
hook设置的钩子函数
返回——
RT_EOK设置成功
-RT_EFULL设置失败

rt_thread_idle_delhook() 的参数说明:

函数参数描述
hook删除的钩子函数
返回——
RT_EOK删除成功
-RT_ENOSYS删除失败

注:空闲线程永远就绪,因此设置的钩子函数不能使线程挂起,如 rt_thread_delay()rt_sem_take() 等可能会导致线程挂起的函数都不能使用。并且钩子函数内部不允许调用 malloc、free等内存相关的函数,因为其内部使用了信号量作为临界保护

2.2.10 设置调度器钩子

​ 系统运行时都处在线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数:

void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));

参数说明:

函数参数描述
hook表示用户定义的钩子函数指针

钩子函数 hook() 的声明如下:

void hook(struct rt_thread* from, struct rt_thread* to);

参数说明:

函数参数描述
from表示系统所要切换出的线程控制块指针
to表示系统所要切换到的线程控制块指针

注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。

image-20240812093112336

2.3 线程应用实例
2.3.1 创建线程示例

​ 创建一个动态线程初始化静态线程,一个线程运行完毕自动被系统删除,另一个线程一直打印计数:

注意:RT-Thread 5.0 及更高的版本将 ALIGN 关键字改成了 rt_align,使用时注意修改。

/*
线程2计数到一定值会执行完毕,线程2被系统自动删除,计数停止。
线程1一直打印计数。
*/
#include <rtthread.h>

// 线程优先级
#define THREAD_PRIORITY         25
// 线程栈空间
#define THREAD_STACK_SIZE       512
// 线程时间片
#define THREAD_TIMESLICE        5

static rt_thread_t tid1 = RT_NULL;

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
    rt_uint32_t count = 0;

    while (1)
    {
        /* 线程 1 采用低优先级运行,一直打印计数值 */
        rt_kprintf("thread1 count: %d\n", count ++);
        rt_thread_mdelay(500);
    }
}

// 字节对齐
ALIGN(RT_ALIGN_SIZE);
// 静态线程2
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *param)
{
    rt_uint32_t count = 0;

    /* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
    for (count = 0; count < 10 ; count++)
    {
        /* 线程 2 打印计数值 */
        rt_kprintf("thread2 count: %d\n", count);
    }
    rt_kprintf("thread2 exit\n");
    /* 线程 2 运行结束后也将自动被系统脱离 */
}

/* 线程示例 */
int thread_sample(void)
{
    /* 创建线程 1,名称是 thread1,入口是 thread1_entry*/
    tid1 = rt_thread_create("thread1",
                            thread1_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);

    /* 如果获得线程控制块,启动这个线程 */
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    /* 初始化线程 2,名称是 thread2,入口是 thread2_entry */
    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
//MSH_CMD_EXPORT(thread_sample, thread sample);

int main()
{
    thread_sample();
}


注:关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit() 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。

  • 步骤
  1. 在D:\study\RT-Thread\rt-thread-5.1.0\bsp\qemu-vexpress-a9下右键打开Env控制台:

image-20240806092407385

  1. 在终端中输入code .打开vscode:

打开 Env 控制台

  1. 点击 VS Code “查看 -> 终端” 打开 VS Code 内部终端,在终端里输入命令 scons 即可编译工程,终端会打印出编译信息:

编译工程

  1. 编译完成后输入 .\qemu.bat 命令就可以运行工程。终端会输出 RT-Thread 启动 logo 信息,QEMU 也运行了起来:

运行工程

注:1、调试 BSP 工程前需要先编译工程生成 rtthread.elf 文件。 2、可以使用 scons --target=vsc -s 命令更新 VS Code 需要用到的 C/C++ 头文件搜索路径信息。不是每次都需要更新,只有在使用了 menuconfig 重新配置了 RT-Thread 或更改了 rtconfig.h 头文件时才需要

  1. 如果要debug的话要先配置一下qemu.bat文件,开始调试前需要编辑 qemu-vexpress-a9 目录下的 qemu-dbg.bat 文件,在 qemu-system-arm 前加入 start :
@echo off
if exist sd.bin goto run
qemu-img create -f raw sd.bin 64M

:run
start qemu-system-arm -M vexpress-a9 -kernel rtthread.elf -serial stdio -sd sd.bin -S -s

如下图所示,在 VS Code 里点击调试菜单(小虫子图标),调试平台选择 Windows,然后按 F5 就可以开启 QEMU 调试模式,断点停留在 main 函数。VS Code 调试选项如下图所示:

Env 调试界面

QEMU 也运行了起来,如下图所示:

在 VS Code 里可以使用 GDB 命令,需要在最前面加上 -exec。 例如 -exec info registers 命令可以查看寄存器的内容:

gdb 查看内存

其他一些主要命令介绍如下所示:

查看内存地址内容:x/<n/f/u> <addr>,各个参数说明如下所示:

  • n 是一个正整数,表示需要显示的内存单元的个数,也就是说从当前地址向后显示几个内存单元的内容,一个内存单元的大小由后面的 u 定义
  • f 表示显示的格式,参见下面。如果地址所指的是字符串,那么格式可以是 s。其他格式如下表所示:
参数描述
x按十六进制格式显示变量
d按十进制格式显示变量
u按十六进制格式显示无符号整型
o按八进制格式显示变量
t按二进制格式显示变量
a按十六进制格式显示变量
c按字符格式显示变量
f按浮点数格式显示变量
  • u 表示从当前地址往后请求的字节数,如果不指定的话,GDB 默认是 4 个 bytes。u 参数可以用下面的字符来代替,b 表示单字节,h 表示双字节,w 表示四字 节,g 表示八字节。当我们指定了字节长度后,GDB 会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。
  • addr 表示一个内存地址。

注:严格区分 n 和 u 的关系,n 表示单元个数,u 表示每个单元的大小。

示例: x/3uh 0x54320 表示从内存地址 0x54320 读取内容,h 表示以双字节为一个单位,3 表示输出三个单位,u 表示按十六进制显示。

  • 查看当前程序栈的内容: x/10x $sp–> 打印 stack 的前 10 个元素
  • 查看当前程序栈的信息: info frame----list general info about the frame
  • 查看当前程序栈的参数: info args—lists arguments to the function
  • 查看当前程序栈的局部变量: info locals—list variables stored in the frame
  • 查看当前寄存器的值:info registers(不包括浮点寄存器) info all-registers(包括浮点寄存器)
  • 查看当前栈帧中的异常处理器:info catch(exception handlers)

提示:输入命令时可以只输入每个命令的第一个字母。例如:info registers 可以只输入 i r

注:如果在 VS Code 目录中额外添加了文件夹,会导致调试不能够启动。 每次开始调试都需要使用 Env 工具在 BSP 根目录使用code .命令打开 VS Code 才能正常调试工程。

2.3.2 线程时间片轮转示例
/*
这个例子创建两个线程,在执行时会一直打印计数。
由运行的计数结果可以看出,线程 2 的运行时间是线程 1 的一半。
*/
#include <rtthread.h>

// 线程栈空间
#define THREAD_STACK_SIZE   1024
// 线程优先级
#define THREAD_PRIORITY     20
// 线程时间片
#define THREAD_TIMESLICE    10

/* 线程入口 */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;
    rt_uint32_t count = 0;

    value = (rt_uint32_t)parameter;
    while (1)
    {
        // 当计数器计到5的倍数就打印
        if(0 == (count % 5))
        {
            rt_kprintf("thread %d is running ,thread %d count = %d\n", value , value , count);

            if(count> 200)
                return;
        }
         count++;
     }
}

int timeslice_sample(void)
{
    rt_thread_t tid = RT_NULL;
    /* 创建线程 1 */
    tid = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid != RT_NULL)
        rt_thread_startup(tid);


    /* 创建线程 2 */
    tid = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE-5);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timeslice_sample, timeslice sample);

2.3.3 线程调度器钩子示例

在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息,如下代码:

注意:RT-Thread5.0 及更高版本将 struct rt_thread 结构体的 name 成员移到了 parent 里,使用时代码需要由 thread->name 更改为 thread->parent.name,否则编译会报错!

#include <rtthread.h>

#define THREAD_STACK_SIZE   1024
#define THREAD_PRIORITY     20
#define THREAD_TIMESLICE    10

/* 针对每个线程的计数器 */
volatile rt_uint32_t count[2];

/* 线程 1、2 共用一个入口,但入口参数不同 */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;

    value = (rt_uint32_t)parameter;
    while (1)
    {
        rt_kprintf("thread %d is running\n", value);
        rt_thread_mdelay(1000); // 延时一段时间
    }
}

static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;

// 钩子函数
static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
{
    rt_kprintf("from: %s -->  to: %s \n", from->name , to->name);
}

int scheduler_hook(void)
{
    /* 设置调度器钩子 */
    rt_scheduler_sethook(hook_of_scheduler);

    /* 创建线程 1 */
    tid1 = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    /* 创建线程 2 */
    tid2 = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY,THREAD_TIMESLICE - 5);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(scheduler_hook, scheduler_hook sample);

仿真运行结果如下:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 27 2018
 2006 - 2018 Copyright by rt-thread team
msh > scheduler_hook
msh >from: tshell -->  to: thread1
thread 1 is running
from: thread1 -->  to: thread2
thread 2 is running
from: thread2 -->  to: tidle
from: tidle -->  to: thread1
thread 1 is running
from: thread1 -->  to: tidle
from: tidle -->  to: thread2
thread 2 is running
from: thread2 -->  to: tidle
…

由仿真的结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。

3 时钟管理

3.1 时钟节拍

系统时钟节拍设置(单位Hz):

image-20240812111458582

3.1.1 获取时钟节拍

​ 全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,通过调用 rt_tick_get 会返回当前 rt_tick 的值,即可以获取到当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的时间。接口函数如下:

rt_tick_t rt_tick_get(void);

参数说明:

返回描述
rt_tick当前时钟节拍值
3.2 定时器管理
  1. 硬件定时器由晶振提供输入时钟,精度高,纳秒级别,中断触发方式;
  1. 软件定时器由操作系统提供的接口。
3.2.1 RT-Thread定时器介绍

​ RT-Thread定时器有两种机制,第一种是单次触发,只会触发一次定时器事件,然后定时停止,第二种是周期触发(自动重装)

​ 超时函数分为HARD_TIMER模式与 SOFT_TIMER 模式,如下图:

定时器上下文环境

3.2.2 定时器工作机制

​ RT-Thread定时器模块维护两个重要的全局变量:

(1) 当前系统经过的tick时间rt_tick;

(2) 定时器链表rt_timer_list,用于存放定时器的队列。

​ 如下图所示,系统当前 tick 值为 20,在当前系统中已经创建并启动了三个定时器,分别是定时时间为 50 个 tick 的 Timer1、100 个 tick 的 Timer2 和 500 个 tick 的 Timer3,这三个定时器分别加上系统当前时间 rt_tick=20,从小到大排序链接在 rt_timer_list 链表中,形成如图所示的定时器链表结构。

定时器链表示意图

​ 而 rt_tick 随着硬件定时器的触发一直在增长(每一次硬件定时器中断来临,rt_tick 变量会加 1),50 个 tick 以后,rt_tick 从 20 增长到 70,与 Timer1 的 timeout 值相等,这时会触发与 Timer1 定时器相关联的超时函数,同时将 Timer1 从 rt_timer_list 链表上删除。同理,100 个 tick 和 500 个 tick 过去后,与 Timer2 和 Timer3 定时器相关联的超时函数会被触发,接着将 Timer2 和 Timer3 定时器从 rt_timer_list 链表中删除。

​ 如果系统当前定时器状态在 10 个 tick 以后(rt_tick=30)有一个任务新创建了一个 tick 值为 300 的 Timer4 定时器,由于 Timer4 定时器的 timeout=rt_tick+300=330, 因此它将被插入到 Timer2 和 Timer3 定时器中间,形成如下图所示链表结构:

定时器链表插入示意图

3.2.3 定时器控制块
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;
3.2.4 定时器跳表算法

​ t_timer_list 链表是一个有序链表,RT-Thread 中使用了跳表算法来加快搜索链表元素的速度。空间换时间,时间复杂度为O(log n)。

​ 一个有序的链表,如下图所示,从该有序链表中搜索元素 {13, 39},需要比较的次数分别为 {3, 5},总共比较的次数为 3 + 5 = 8 次。

有序链表示意图

​ 使用跳表算法后可以采用类似二叉搜索树的方法,把一些节点提取出来作为索引,得到如下图所示的结构:

有序链表索引示意图

​ 在这个结构里把 {3, 18,77} 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了, 例如在搜索 39 时仅比较了 3 次(通过比较 3,18,39)。当然我们还可以再从一级索引提取一些元素出来,作为二级索引,这样更能加快元素搜索。

三层跳表示意图

3.2.5 定时器的管理方式

定时器相关操作

初始化定时器:

void rt_system_timer_init(void);

如果需要使用 SOFT_TIMER,则系统初始化时,应该调用下面这个函数接口:

void rt_system_timer_thread_init(void);

创建和删除定时器:

rt_timer_t rt_timer_create(const char* name,
                           void (*timeout)(void* parameter),
                           void* parameter,
                           rt_tick_t time,
                           rt_uint8_t flag);

​ 调用该函数接口后,内核首先从动态内存堆中分配一个定时器控制块,然后对该控制块进行基本的初始化。参数说明:

参数描述
name定时器的名称
void (timeout) (void parameter)定时器超时函数指针(当定时器超时时,系统会调用这个函数)
parameter定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
time定时器的超时时间,单位是时钟节拍
flag定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值)
返回——
RT_NULL创建失败(通常会由于系统内存不够用而返回 RT_NULL)
定时器的句柄定时器创建成功

include/rtdef.h 中定义了一些定时器相关的宏,如下:

#define RT_TIMER_FLAG_ONE_SHOT      0x0     /* 单次定时     */
#define RT_TIMER_FLAG_PERIODIC      0x2     /* 周期定时     */

#define RT_TIMER_FLAG_HARD_TIMER    0x0     /* 硬件定时器   */
#define RT_TIMER_FLAG_SOFT_TIMER    0x4     /* 软件定时器   */

​ 上面 2 组值可以以 “或” 逻辑的方式赋给 flag。当指定的 flag 为 RT_TIMER_FLAG_HARD_TIMER 时,如果定时器超时,定时器的回调函数将在时钟中断的服务例程上下文中被调用;当指定的 flag 为 RT_TIMER_FLAG_SOFT_TIMER 时,如果定时器超时,定时器的回调函数将在系统时钟 timer 线程的上下文中被调用。

​ 系统不再使用动态定时器时,可使用下面的函数接口:

rt_err_t rt_timer_delete(rt_timer_t timer);

​ 调用这个函数接口后,系统会把这个定时器从 rt_timer_list 链表中删除,然后释放相应的定时器控制块占有的内存。参数说明:

参数描述
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);

参数说明:

参数描述
timer定时器句柄,指向要初始化的定时器控制块
name定时器的名称
void (timeout) (void parameter)定时器超时函数指针(当定时器超时时,系统会调用这个函数)
parameter定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
time定时器的超时时间,单位是时钟节拍
flag定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器(可以用 “或” 关系取多个值),详见创建定时器小节

当一个静态定时器不需要再使用时,可以使用下面的函数接口:

rt_err_t rt_timer_detach(rt_timer_t timer);

参数说明:

参数描述
timer定时器句柄,指向要脱离的定时器控制块
返回——
RT_EOK脱离成功

启动定时器以后,若想使它停止,可以使用下面的函数接口:

rt_err_t rt_timer_stop(rt_timer_t timer);

​ 定时器状态将更改为停止状态,并从 rt_timer_list 链表中脱离出来不参与定时器超时检查。当一个(周期性)定时器超时时,也可以调用这个函数接口停止这个(周期性)定时器本身。参数说明:

参数描述
timer定时器句柄,指向要停止的定时器控制块
返回——
RT_EOK成功停止定时器
- RT_ERRORtimer 已经处于停止状态

控制定时器:

rt_err_t rt_timer_control(rt_timer_t timer, rt_uint8_t cmd, void* arg);

参数说明:

参数描述
timer定时器句柄,指向要控制的定时器控制块
cmd用于控制定时器的命令,当前支持四个命令,分别是设置定时时间,查看定时时间,设置单次触发,设置周期触发
arg与 cmd 相对应的控制命令参数 比如,cmd 为设定超时时间时,就可以将超时时间参数通过 arg 进行设定
返回——
RT_EOK成功

函数参数 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     /* 设置定时器为周期型定时器 */
3.3 定时器应用实例

创建动态定时器:

#include <rtthread.h>

/* 定时器的控制块 */
static rt_timer_t timer1;
static rt_timer_t timer2;
static int cnt = 0;

/* 定时器 1 超时函数 */
static void timeout1(void *parameter)
{
    rt_kprintf("periodic timer is timeout %d\n", cnt);

    /* 运行第 10 次,停止周期定时器 */
    if (cnt++>= 9)
    {
        rt_timer_stop(timer1);
        rt_kprintf("periodic timer was stopped! \n");
    }
}

/* 定时器 2 超时函数 */
static void timeout2(void *parameter)
{
    rt_kprintf("one shot timer is timeout\n");
}

int timer_sample(void)
{
    /* 创建定时器 1  周期定时器 */
    timer1 = rt_timer_create("timer1", timeout1,
                             RT_NULL, 10,
                             RT_TIMER_FLAG_PERIODIC);

    /* 启动定时器 1 */
    if (timer1 != RT_NULL) rt_timer_start(timer1);

    /* 创建定时器 2 单次定时器 */
    timer2 = rt_timer_create("timer2", timeout2,
                             RT_NULL,  30,
                             RT_TIMER_FLAG_ONE_SHOT);

    /* 启动定时器 2 */
    if (timer2 != RT_NULL) rt_timer_start(timer2);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timer_sample, timer sample);

初始化静态定时器:

#include <rtthread.h>

/* 定时器的控制块 */
static struct rt_timer timer1;
static struct rt_timer timer2;
static int cnt = 0;

/* 定时器 1 超时函数 */
static void timeout1(void* parameter)
{
    rt_kprintf("periodic timer is timeout\n");
    /* 运行 10 次 */
    if (cnt++>= 9)
    {
        rt_timer_stop(&timer1);
    }
}

/* 定时器 2 超时函数 */
static void timeout2(void* parameter)
{
    rt_kprintf("one shot timer is timeout\n");
}

int timer_static_sample(void)
{
    /* 初始化定时器 */
    rt_timer_init(&timer1, "timer1",  /* 定时器名字是 timer1 */
                    timeout1, /* 超时时回调的处理函数 */
                    RT_NULL, /* 超时函数的入口参数 */
                    10, /* 定时长度,以 OS Tick 为单位,即 10 个 OS Tick */
                    RT_TIMER_FLAG_PERIODIC); /* 周期性定时器 */
    rt_timer_init(&timer2, "timer2",   /* 定时器名字是 timer2 */
                    timeout2, /* 超时时回调的处理函数 */
                      RT_NULL, /* 超时函数的入口参数 */
                      30, /* 定时长度为 30 个 OS Tick */
                    RT_TIMER_FLAG_ONE_SHOT); /* 单次定时器 */

    /* 启动定时器 */
    rt_timer_start(&timer1);
    rt_timer_start(&timer2);
    return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timer_static_sample, timer_static sample);
3.4 高精度延时

如果延时大于一个OS Tick(毫秒ms级别):

image-20240812111645105

如果小于一个OS Tick(微秒us级别):

image-20240812111722993

4 线程间同步

线程间数据传递示意图

​ 如上图,线程1读取传感器数据存入共享内存块,线程2从共享内存块中读出数据输出显示,那两个线程对共享内存块的访问必须是排他性的,写完才读,读完才写,两线程互斥。多个线程操作 / 访问同一块区域(代码),这块代码就称为临界区,如上图的共享内存块。

​ 线程的同步方式有很多种,其核心思想都是:**在访问临界区的时候只允许一个 (或一类) 线程运行。**进入 / 退出临界区的方式有很多种:

1)调用 rt_hw_interrupt_disable() 进入临界区,调用 rt_hw_interrupt_enable() 退出临界区;

2)调用 rt_enter_critical() 进入临界区,调用 rt_exit_critical() 退出临界区。

4.1 信号量(semaphore)

​ 停车场管理员通过查看停车场车位,如果有空位就放车进来,空位满了就禁止车进来,管理员就相当于信号量,空车位个数就是信号量的值(非负、动态变化),停车位相当于临界区,车辆相当于线程。

4.1.1 信号量的工作机制

​ 每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。

信号量工作示意图

4.1.2 信号量控制块
  • 在 RT-Thread 中,信号量控制块是操作系统用于管理信号量的一个数据结构,由结构体 struct rt_semaphore 表示。
  • 另外一种 C 表达方式 rt_sem_t,表示的是信号量的句柄,在 C 语言中的实现是指向信号量控制块的指针。信号量控制块结构的详细定义如下:
struct rt_semaphore
{
   struct rt_ipc_object parent;  /* 继承自 ipc_object 类 */
   rt_uint16_t value;            /* 信号量的值 */
};
/* rt_sem_t 是指向 semaphore 结构体的指针类型 */
typedef struct rt_semaphore* rt_sem_t;

rt_semaphore 对象从rt_ipc_object中派生,由 IPC 容器所管理,信号量的最大值是 65535。

4.1.3 信号量的管理方式

信号量相关接口

创建和删除动态信号量:

 rt_sem_t rt_sem_create(const char *name,
                        rt_uint32_t value,
                        rt_uint8_t flag);

参数说明:

参数描述
name信号量名称
value信号量初始值
flag信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_NULL创建失败
信号量的控制块指针创建成功

flag当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择 RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。

注:RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该信号量的线程都将会变为非实时线程,方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。

删除动态信号量

rt_err_t rt_sem_delete(rt_sem_t sem);

​ 如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是 - RT_ERROR),然后再释放信号量的内存资源。参数说明:

参数描述
semrt_sem_create() 创建的信号量对象
返回——
RT_EOK删除成功

初始化和脱离静态信号量:

  • 初始化
rt_err_t rt_sem_init(rt_sem_t       sem,
                    const char     *name,
                    rt_uint32_t    value,
                    rt_uint8_t     flag)

信号量标志可用上面创建信号量函数里提到的标志。参数说明:

参数描述
sem信号量对象的句柄
name信号量名称
value信号量初始值
flag信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK初始化成功
  • 脱离
rt_err_t rt_sem_detach(rt_sem_t sem);

​ 使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离。原来挂起在信号量上的等待线程将获得 - RT_ERROR 的返回值。参数说明:

参数描述
sem信号量对象的句柄
返回——
RT_EOK脱离成功

获取信号量:

​ 信号量值大于零时,线程获得信号量,并且相应信号量值-1。函数接口:

rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);

​ 在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据 time 参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数 time 指定的时间内依然得不到信号量,线程将超时返回,返回值是 - RT_ETIMEOUT。参数说明:

参数描述
sem信号量对象的句柄
time指定的等待时间,单位是操作系统时钟节拍(OS Tick)
返回——
RT_EOK成功获得信号量
-RT_ETIMEOUT超时依然未获得信号量
-RT_ERROR其他错误

无等待获取信号量:

rt_err_t rt_sem_trytake(rt_sem_t sem);

​ 这个函数与 rt_sem_take(sem, RT_WAITING_NO) 的作用相同,即当线程申请的信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回 - RT_ETIMEOUT。参数说明:

参数描述
sem信号量对象的句柄
返回——
RT_EOK成功获得信号量
-RT_ETIMEOUT获取失败

释放信号量:

rt_err_t rt_sem_release(rt_sem_t sem);

​ 例如当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量;否则将把信号量的值加 1。参数说明:

参数描述
sem信号量对象的句柄
返回——
RT_EOK成功释放信号量
4.1.4 信号量应用示例

注意:RT-Thread 5.0 及更高的版本将 ALIGN 关键字改成了 rt_align,使用时注意修改。

信号量的使用:

/*
该例程创建了一个动态信号量,初始化两个线程,线程1在 count计数为10的倍数时(count 计数为100之后线程退出),发送一个信号量,线程2在接收信号量后,对number进行加1操作。
*/
#include <rtthread.h>

#define THREAD_PRIORITY         25
#define THREAD_TIMESLICE        5

/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void *parameter)
{
    static rt_uint8_t count = 0;

    while(1)
    {
        if(count <= 100)
        {
            count++;
        }
        else
            return;

        /* count 每计数 10 次,就释放一次信号量 */
         if(0 == (count % 10))
        {
            rt_kprintf("t1 release a dynamic semaphore.\n");
            rt_sem_release(dynamic_sem);
        }
    }
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
    static rt_err_t result;
    static rt_uint8_t number = 0;
    while(1)
    {
        /* 永久方式等待信号量,获取到信号量,则执行 number 自加的操作 */
        result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
        if (result != RT_EOK)
        {
            rt_kprintf("t2 take a dynamic semaphore, failed.\n");
            rt_sem_delete(dynamic_sem);
            return;
        }
        else
        {
            number++;
            rt_kprintf("t2 take a dynamic semaphore. number = %d\n" ,number);
        }
    }
}

/* 信号量示例的初始化 */
int semaphore_sample(void)
{
    /* 创建一个动态信号量,初始值是 0 */
    dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
    if (dynamic_sem == RT_NULL)
    {
        rt_kprintf("create dynamic semaphore failed.\n");
        return -1;
    }
    else
    {
        rt_kprintf("create done. dynamic semaphore value = 0.\n");
    }

    rt_thread_init(&thread1,
                   "thread1",
                   rt_thread1_entry,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack),
                   THREAD_PRIORITY, THREAD_TIMESLICE);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   rt_thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY-1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

    return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(semaphore_sample, semaphore sample);

生产者消费者例程:

获取信号量就是使其值减一,释放则加一

/*
本例程将使用2个线程、3个信号量实现生产者与消费者的例子。其中:

3个信号量分别为:
①lock:信号量锁的作用,因为2个线程都会对同一个数组 array 进行操作,所以该数组是一个共享资源,锁用来保护这个共享资源。
②empty:空位个数,初始化为5个空位。
③full:满位个数,初始化为0个满位。

2个线程分别为:
①生产者线程:获取到空位后,产生一个数字,循环放入数组中,然后释放一个满位。
②消费者线程:获取到满位后,读取数组内容并相加,然后释放一个空位。
*/
#include <rtthread.h>

#define THREAD_PRIORITY       6
#define THREAD_STACK_SIZE     512
#define THREAD_TIMESLICE      5

/* 定义最大 5 个元素能够被产生 */
#define MAXSEM 5 

/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];

/* 指向生产者、消费者在 array 数组中的读写位置 */
static rt_uint32_t set, get;

/* 指向线程控制块的指针 */
static rt_thread_t producer_tid = RT_NULL;
static rt_thread_t consumer_tid = RT_NULL;

struct rt_semaphore sem_lock;
struct rt_semaphore sem_empty, sem_full;

/* 生产者线程入口 */
void producer_thread_entry(void *parameter)
{
    int cnt = 0;

    /* 运行 10 次 */
    while (cnt < 10)
    {
        /* 获取一个空位 */
        rt_sem_take(&sem_empty, RT_WAITING_FOREVER);

        /* 修改 array 内容,上锁 */
        rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
        array[set % MAXSEM] = cnt + 1;
        rt_kprintf("the producer generates a number: %d\n", array[set % MAXSEM]);
        set++;
        rt_sem_release(&sem_lock);

        /* 发布一个满位 */
        rt_sem_release(&sem_full);
        cnt++;

        /* 暂停一段时间 */
        rt_thread_mdelay(20);
    }

    rt_kprintf("the producer exit!\n");
}

/* 消费者线程入口 */
void consumer_thread_entry(void *parameter)
{
    rt_uint32_t sum = 0;

    while (1)
    {
        /* 获取一个满位 */
        rt_sem_take(&sem_full, RT_WAITING_FOREVER);

        /* 临界区,上锁进行操作 */
        rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
        // 用取余实现循环覆盖,因此get和set都不需要清零
        sum += array[get % MAXSEM];
        rt_kprintf("the consumer[%d] get a number: %d\n", (get % MAXSEM), array[get % MAXSEM]);
        get++;
        rt_sem_release(&sem_lock);

        /* 释放一个空位 */
        rt_sem_release(&sem_empty);

        /* 生产者生产到 10 个数目,停止,消费者线程相应停止 */
        if (get == 10) break;

        /* 暂停一小会时间 */
        rt_thread_mdelay(50);
    }

    rt_kprintf("the consumer sum is: %d\n", sum);
    rt_kprintf("the consumer exit!\n");
}

int producer_consumer(void)
{
    set = 0;
    get = 0;

    /* 初始化 3 个信号量 */
    rt_sem_init(&sem_lock, "lock",     1,      RT_IPC_FLAG_PRIO);
    rt_sem_init(&sem_empty, "empty",   MAXSEM, RT_IPC_FLAG_PRIO);
    rt_sem_init(&sem_full, "full",     0,      RT_IPC_FLAG_PRIO);

    /* 创建生产者线程 */
    producer_tid = rt_thread_create("producer",
                                    producer_thread_entry, RT_NULL,
                                    THREAD_STACK_SIZE,
                                    THREAD_PRIORITY - 1,
                                    THREAD_TIMESLICE);
    if (producer_tid != RT_NULL)
    {
        rt_thread_startup(producer_tid);
    }
    else
    {
        rt_kprintf("create thread producer failed");
        return -1;
    }

    /* 创建消费者线程 */
    consumer_tid = rt_thread_create("consumer",
                                    consumer_thread_entry, RT_NULL,
                                    THREAD_STACK_SIZE,
                                    THREAD_PRIORITY + 1,
                                    THREAD_TIMESLICE);
    if (consumer_tid != RT_NULL)
    {
        rt_thread_startup(consumer_tid);
    }
    else
    {
        rt_kprintf("create thread consumer failed");
        return -1;
    }

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(producer_consumer, producer_consumer sample);
4.1.5 信号量的作用
  • 线程同步

​ 信号量的作用可以用于线程同步,当持有信号量的线程完成它处理的工作,释放信号量,才能把等待在这个信号量的线程唤醒,让它执行下一部分工作。

  • 中断与线程同步

​ 信号量也能够方便地应用于中断与线程间的同步,例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。这个时候可以设置信号量的初始值是 0,线程在试图持有这个信号量时,由于信号量的初始值是 0,线程直接在这个信号量上挂起直到信号量被释放。当中断触发时,先进行与硬件相关的动作,例如从硬件的 I/O 口中读取相应的数据,并确认中断以清除中断源,而后释放一个信号量来唤醒相应的线程以做后续的数据处理。例如 FinSH 线程的处理方式,如下图所示。

FinSH 的中断、线程间同步示意图

​ 信号量的值初始为 0,当 FinSH 线程试图取得信号量时,因为信号量值是 0,所以它会被挂起。当 console 设备有数据输入时,产生中断,从而进入中断服务例程。在中断服务例程中,它会读取 console 设备的数据,并把读得的数据放入 UART buffer 中进行缓冲,而后释放信号量,释放信号量的操作将唤醒 shell 线程。在中断服务例程运行完毕后,如果系统中没有比 shell 线程优先级更高的就绪线程存在时,shell 线程将持有信号量并运行,从 UART buffer 缓冲区中获取输入的数据。

  • 资源计数

​ 信号量也可以认为是一个递增或递减的计数器,需要注意的是信号量的值非负。例如:初始化一个信号量的值为 5,则这个信号量可最大连续减少 5 次,直到计数器减为 0。资源计数适合于线程间工作处理速度不匹配的场合,这个时候信号量可以做为前一线程工作完成个数的计数,而当调度到后一线程时,它也可以以一种连续的方式一次处理多个事件。例如,生产者与消费者问题中,生产者可以对信号量进行多次释放,而后消费者被调度到时能够一次处理多个信号量资源。

4.2 互斥量(mutex)
4.2.1 互斥量的工作机制

​ 互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。

​ 互斥量只有开锁和闭锁两种状态值。当线程持有它时闭锁,释放它时开锁,其他线程不能对它进行开锁或者持有,持有互斥量的也能够再次获得这个锁而不被挂起。

​ 使用信号量会导致的另一个潜在问题是线程优先级翻转问题。所谓优先级翻转,即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。如下图所示:有优先级为 A、B 和 C 的三个线程,优先级 A> B > C。线程 A,B 处于挂起状态,等待某一事件触发,线程 C 正在运行,此时线程 C 开始使用某一共享资源 M。在使用过程中,线程 A 等待的事件到来,线程 A 转为就绪态,因为它比线程 C 优先级高,所以立即执行。但是当线程 A 要使用共享资源 M 时,由于其正在被线程 C 使用,因此线程 A 被挂起切换到线程 C 运行。如果此时线程 B 等待的事件到来,则线程 B 转为就绪态。由于线程 B 的优先级比线程 C 高,且线程B没有用到共享资源 M ,因此线程 B 开始运行,直到其运行完毕,线程 C 才开始运行。只有当线程 C 释放共享资源 M 后,线程 A 才得以执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 运行。这样便不能保证高优先级线程的响应时间。

​ 互斥量可以解决线程优先级反转的问题,采用的时优先级继承协议,在上述例子中,线程C和线程A都会获取共享资源,而线程C正在运行,线程A尝试获取而被挂起,那么在这期间会将线程C的优先级提升到和A相同级别,防止C和A被线程B抢占。

注:在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级,否则可能人为引入无界优先级反转的问题。

4.2.2 互斥量控制块

​ 互斥量控制块是操作系统用于管理互斥量的一个数据结构,由结构体 struct rt_mutex 表示。另外一种 C 表达方式 rt_mutex_t,表示的是互斥量的句柄,在 C 语言中的实现是指互斥量控制块的指针。互斥量控制块结构的详细定义请见以下代码:

struct rt_mutex
    {
        struct rt_ipc_object parent;                /* 继承自 ipc_object 类 */

        rt_uint16_t          value;                   /* 互斥量的值 */
        rt_uint8_t           original_priority;     /* 持有线程的原始优先级 */
        rt_uint8_t           hold;                     /* 持有线程的持有次数   */
        struct rt_thread    *owner;                 /* 当前拥有互斥量的线程 */
    };
    /* rt_mutext_t 为指向互斥量结构体的指针类型  */
    typedef struct rt_mutex* rt_mutex_t;

rt_mutex 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。

4.2.3 互斥量的管理方式

互斥量相关接口

创建和删除动态互斥量:

  • 创建
rt_mutex_t rt_mutex_create (const char* name, rt_uint8_t flag);

​ 互斥量的 flag 标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO,内核均按照 RT_IPC_FLAG_PRIO 处理。参数说明:

参数描述
name互斥量的名称
flag该标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO,内核均按照 RT_IPC_FLAG_PRIO 处理
返回——
互斥量句柄创建成功
RT_NULL创建失败
  • 删除
rt_err_t rt_mutex_delete (rt_mutex_t mutex);

​ 当删除一个互斥量时,所有等待此互斥量的线程都将被唤醒,等待线程获得的返回值是 - RT_ERROR。参数说明:

参数描述
mutex互斥量对象的句柄
返回——
RT_EOK删除成功

初始化和脱离静态互斥量:

  • 初始化
rt_err_t rt_mutex_init (rt_mutex_t mutex, const char* name, rt_uint8_t flag);

​ 互斥量标志可用上面创建互斥量函数里提到的标志。参数说明:

参数描述
mutex互斥量对象的句柄,它由用户提供,并指向互斥量对象的内存块
name互斥量的名称
flag该标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO,内核均按照 RT_IPC_FLAG_PRIO 处理
返回——
RT_EOK初始化成功
  • 脱离
rt_err_t rt_mutex_detach (rt_mutex_t mutex);

​ 使用该函数接口后,内核先唤醒所有挂在该互斥量上的线程(线程的返回值是 -RT_ERROR),然后系统将该互斥量从内核对象管理器中脱离。参数说明:

参数描述
mutex互斥量对象的句柄
返回——
RT_EOK成功

获取互斥量:

rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time);

​ 如果互斥量没被其他线程控制,则获取成功,如果互斥量已经被当前线程控制,互斥量持有计数加1,当前线程不会被挂起。如果互斥量被其他线程占有,则挂起等待,知道其他线程释放它或者等待时间超时。参数说明:

参数描述
mutex互斥量对象的句柄
time指定等待的时间
返回——
RT_EOK成功获得互斥量
-RT_ETIMEOUT超时
-RT_ERROR获取失败

无等待获取互斥量:

​ 当用户不想在申请的互斥量上挂起线程进行等待时,可以使用无等待方式获取互斥量,无等待获取互斥量使用下面的函数接口:

rt_err_t rt_mutex_trytake(rt_mutex_t mutex);

​ 这个函数与 rt_mutex_take(mutex, RT_WAITING_NO) 的作用相同,即当线程申请的互斥量资源实例不可用的时候,它不会等待在该互斥量上,而是直接返回 - RT_ETIMEOUT。参数说明:

参数描述
mutex互斥量对象的句柄
返回——
RT_EOK成功获得互斥量
-RT_ETIMEOUT获取失败

释放互斥量:

当线程完成互斥资源的访问后,应尽快释放它占据的互斥量,使得其他线程能及时获取该互斥量。

rt_err_t rt_mutex_release(rt_mutex_t mutex);

​ 只有已经拥有互斥量控制权的线程才能释放它,每释放一次该互斥量,它的持有计数就减 1。当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该互斥量上的线程将被唤醒。如果线程的运行优先级被互斥量提升,那么当互斥量被释放后,线程恢复为持有互斥量前的优先级。参数说明:

参数描述
mutex互斥量对象的句柄
返回——
RT_EOK成功
4.2.4 互斥量应用示例

互斥量例程:

/*
线程1和线程2,线程1对2个number分别进行加1操作;线程2也对2个number分别进行加1操作,使用互斥量保证线程改变2个number值的操作不被打断。
*/
#include <rtthread.h>

#define THREAD_PRIORITY         8
#define THREAD_TIMESLICE        5

/* 指向互斥量的指针 */
static rt_mutex_t dynamic_mutex = RT_NULL;
static rt_uint8_t number1, number2 = 0;

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread_entry1(void *parameter)
{
      while(1)
      {
          /* 线程 1 获取到互斥量后,先后对 number1、number2 进行加 1 操作,然后释放互斥量 */
          rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
          number1++;
          rt_thread_mdelay(10);
          number2++;
          rt_mutex_release(dynamic_mutex);
       }
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread_entry2(void *parameter)
{
      while(1)
      {
          /* 线程 2 获取到互斥量后,检查 number1、number2 的值是否相同,相同则表示 mutex 起到了锁的作用 */
          rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
          if(number1 != number2)
          {
            rt_kprintf("not protect.number1 = %d, mumber2 = %d \n",number1 ,number2);
          }
          else
          {
            rt_kprintf("mutex protect ,number1 = mumber2 is %d\n",number1);
          }

           number1++;
           number2++;
           rt_mutex_release(dynamic_mutex);

          if(number1>=50)
              return;
      }
}

/* 互斥量示例的初始化 */
int mutex_sample(void)
{
    /* 创建一个动态互斥量 */
    dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_PRIO);
    if (dynamic_mutex == RT_NULL)
    {
        rt_kprintf("create dynamic mutex failed.\n");
        return -1;
    }

    rt_thread_init(&thread1,
                   "thread1",
                   rt_thread_entry1,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack),
                   THREAD_PRIORITY, THREAD_TIMESLICE);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   rt_thread_entry2,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY-1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);
    return 0;
}

/* 导出到 MSH 命令列表中 */
MSH_CMD_EXPORT(mutex_sample, mutex sample);

防止优先级翻转特性例程:

/*
这个例子将创建 3 个动态线程以检查持有互斥量时,持有的线程优先级是否被调整到等待线程优先级中的最高优先级。
*/
#include <rtthread.h>

/* 指向线程控制块的指针 */
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
static rt_thread_t tid3 = RT_NULL;
static rt_mutex_t mutex = RT_NULL;


#define THREAD_PRIORITY       10
#define THREAD_STACK_SIZE     512
#define THREAD_TIMESLICE    5

/* 线程 1 入口 */
static void thread1_entry(void *parameter)
{
    /* 先让低优先级线程运行 */
    rt_thread_mdelay(100);

    /* 此时 thread3 持有 mutex,并且 thread2 等待持有 mutex */

    /* 检查 thread2 与 thread3 的优先级情况 */
    if (tid2->current_priority != tid3->current_priority)
    {
        /* 优先级不相同,测试失败 */
        rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
        rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
        rt_kprintf("test failed.\n");
        return;
    }
    else
    {
        rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
        rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
        rt_kprintf("test OK.\n");
    }
}

/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
    rt_err_t result;

    rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);

    /* 先让低优先级线程运行 */
    rt_thread_mdelay(50);

    /*
     * 试图持有互斥锁,此时 thread3 持有,应把 thread3 的优先级提升
     * 到 thread2 相同的优先级
     */
    result = rt_mutex_take(mutex, RT_WAITING_FOREVER);

    if (result == RT_EOK)
    {
        /* 释放互斥锁 */
        rt_mutex_release(mutex);
    }
}

/* 线程 3 入口 */
static void thread3_entry(void *parameter)
{
    rt_tick_t tick;
    rt_err_t result;

    rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);

    result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        rt_kprintf("thread3 take a mutex, failed.\n");
    }

    /* 做一个长时间的循环,500ms */
    tick = rt_tick_get();
    while (rt_tick_get() - tick < (RT_TICK_PER_SECOND / 2)) ;

    rt_mutex_release(mutex);
}

int pri_inversion(void)
{
    /* 创建互斥锁 */
    mutex = rt_mutex_create("mutex", RT_IPC_FLAG_PRIO);
    if (mutex == RT_NULL)
    {
        rt_kprintf("create dynamic mutex failed.\n");
        return -1;
    }

    /* 创建线程 1 */
    tid1 = rt_thread_create("thread1",
                            thread1_entry,
                            RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
         rt_thread_startup(tid1);

    /* 创建线程 2 */
    tid2 = rt_thread_create("thread2",
                            thread2_entry,
                            RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);

    /* 创建线程 3 */
    tid3 = rt_thread_create("thread3",
                            thread3_entry,
                            RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY + 1, THREAD_TIMESLICE);
    if (tid3 != RT_NULL)
        rt_thread_startup(tid3);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(pri_inversion, prio_inversion sample);

注:需要切记的是互斥量不能在中断服务例程中使用。

4.2.5 互斥量的作用

(1)线程多次持有互斥量的情况下,可以避免同一线程多次递归持有而造成死锁的问题。

(2)可以防止多线程同步而造成优先级翻转。

4.3 事件集(event)

​ 小明约了小光坐公交去玩,只有同时满足“小光到公交站”和“公交到了公交站”才能出发,这两种情况就称为事件。小明出发相当于线程。

4.3.1 事件集的工作机制

​ 事件集主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。

RT-Thread 定义的事件集有以下特点:

1)事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;

2)事件仅用于同步,不提供数据传输功能;

3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。

在 RT-Thread 中,每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及 RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过 32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。

​ 如果信息标记设置了清除标记位,则当事件满足条件使线程 #1 唤醒后,将主动把事件清为零,否则事件标志将依然存在(即置 1)。

4.3.2 时间控制块

​ 在 RT-Thread 中,事件集控制块是操作系统用于管理事件的一个数据结构,由结构体 struct rt_event 表示。另外一种 C 表达方式 rt_event_t,表示的是事件集的句柄,在 C 语言中的实现是事件集控制块的指针。

struct rt_event
{
    struct rt_ipc_object parent;    /* 继承自 ipc_object 类 */

    /* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */
    rt_uint32_t set;
};
/* rt_event_t 是指向事件结构体的指针类型  */
typedef struct rt_event* rt_event_t;

rt_event 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。

4.3.3 事件集的管理方式

事件相关接口

创建和删除动态事件集:

  • 创建
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);

参数说明:

参数描述
name事件集的名称
flag事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_NULL创建失败
事件对象的句柄创建成功

注:RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该事件集的线程都将会变为非实时线程,方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。

  • 删除
rt_err_t rt_event_delete(rt_event_t event);

​ 在调用 rt_event_delete 函数删除一个事件集对象时,应该确保该事件集不再被使用。在删除前会唤醒所有挂起在该事件集上的线程(线程的返回值是 - RT_ERROR),然后释放事件集对象占用的内存块。参数说明:

参数描述
event事件集对象的句柄
返回——
RT_EOK成功

初始化和脱离静态事件集:

  • 初始化
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);

参数说明:

参数描述
event事件集对象的句柄
name事件集的名称
flag事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功
  • 脱离
rt_err_t rt_event_detach(rt_event_t event);

​ 用户调用这个函数时,系统首先唤醒所有挂在该事件集等待队列上的线程(线程的返回值是 - RT_ERROR),然后将该事件集从内核对象管理器中脱离。参数说明:

参数描述
event事件集对象的句柄
返回——
RT_EOK成功

发送事件:

​ 发送事件函数可以发送事件集中的一个或多个事件,如下:

rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);

​ 使用该函数接口时,通过参数 set 指定的事件标志来设定 event 事件集对象的事件标志值,然后遍历等待在 event 事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,如果有,则唤醒该线程。参数说明:

参数描述
event事件集对象的句柄
set发送的一个或多个事件的标志值
返回——
RT_EOK成功

接收事件:

​ 内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收 32 个事件,内核可以通过指定选择参数 “逻辑与” 或“逻辑或”来选择如何激活线程,使用 “逻辑与” 参数表示只有当所有等待的事件都发生时才激活线程,而使用 “逻辑或” 参数则表示只要有一个等待的事件发生就激活线程。接收事件使用下面的函数接口:

rt_err_t rt_event_recv(rt_event_t event,
                           rt_uint32_t set,
                           rt_uint8_t option,
                           rt_int32_t timeout,
                           rt_uint32_t* recved);

​ 当用户调用这个接口时,系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生,如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位,然后返回(其中 recved 参数返回接收到的事件);如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回 - RT_ETIMEOUT。参数说明:

参数描述
event事件集对象的句柄
set接收线程感兴趣的事件
option接收选项
timeout指定超时时间
recved指向接收到的事件
返回——
RT_EOK成功
-RT_ETIMEOUT超时
-RT_ERROR错误

option 的值可取:

/* 选择 逻辑与 或 逻辑或 的方式接收事件 */
RT_EVENT_FLAG_OR
RT_EVENT_FLAG_AND

/* 选择清除重置事件标志位 */
RT_EVENT_FLAG_CLEAR
4.3.4 事件应用示例

事件集的使用例程:

/*
例子中初始化了一个事件集,两个线程。一个线程等待自己关心的事件发生,另外一个线程发送事件.
*/
#include <rtthread.h>

#define THREAD_PRIORITY      9
#define THREAD_TIMESLICE     5

#define EVENT_FLAG3 (1 << 3)
#define EVENT_FLAG5 (1 << 5)

/* 事件控制块 */
static struct rt_event event;

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;

/* 线程 1 入口函数 */
static void thread1_recv_event(void *param)
{
    rt_uint32_t e;

    /* 第一次接收事件,事件 3 或事件 5 任意一个可以触发线程 1,接收完后清除事件标志 */
    if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
                      RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
                      RT_WAITING_FOREVER, &e) == RT_EOK)
    {
        rt_kprintf("thread1: OR recv event 0x%x\n", e);
    }

    rt_kprintf("thread1: delay 1s to prepare the second event\n");
    rt_thread_mdelay(1000);

    /* 第二次接收事件,事件 3 和事件 5 均发生时才可以触发线程 1,接收完后清除事件标志 */
    if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
                      RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
                      RT_WAITING_FOREVER, &e) == RT_EOK)
    {
        rt_kprintf("thread1: AND recv event 0x%x\n", e);
    }
    rt_kprintf("thread1 leave.\n");
}


ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;

/* 线程 2 入口 */
static void thread2_send_event(void *param)
{
    rt_kprintf("thread2: send event3\n");
    rt_event_send(&event, EVENT_FLAG3);
    rt_thread_mdelay(200);

    rt_kprintf("thread2: send event5\n");
    rt_event_send(&event, EVENT_FLAG5);
    rt_thread_mdelay(200);

    rt_kprintf("thread2: send event3\n");
    rt_event_send(&event, EVENT_FLAG3);
    rt_kprintf("thread2 leave.\n");
}

int event_sample(void)
{
    rt_err_t result;

    /* 初始化事件对象 */
    result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO);
    if (result != RT_EOK)
    {
        rt_kprintf("init event failed.\n");
        return -1;
    }

    rt_thread_init(&thread1,
                   "thread1",
                   thread1_recv_event,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack),
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   thread2_send_event,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(event_sample, event sample);

仿真运行结果如下:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 24 2018
 2006 - 2018 Copyright by rt-thread team
msh >event_sample
thread2: send event3
thread1: OR recv event 0x8
thread1: delay 1s to prepare the second event
msh >thread2: send event5
thread2: send event3
thread2 leave.
thread1: AND recv event 0x28
thread1 leave.
4.3.5 小结

​ 事件集与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。

​ 事件的另一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是 “逻辑或” 触发还是 “逻辑与” 触发。

5 线程间通信

​ 在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取,根据读取到的全局变量值执行相应的动作,达到通信协作的目的。

5.1 邮箱(mailbox)

​ 举个例子,有两个线程,线程1检测按键状态并发送,线程2读取按键状态并改变LED的亮灭,那线程1就可以将按键状态作为邮件发送到邮箱,线程2在邮箱中读取邮件获得按键状态并执行亮灭LED。

5.1.1 邮箱的工作机制

​ 邮箱用于线程间通信,特点是开销比较低,效率较高。每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)。

​ 邮件的发送时非阻塞的,而邮件的收取可能阻塞的(当邮箱中没有邮件或者超时时间不为0时)。

​ 当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。

​ 当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中。

5.1.2 邮箱控制块

​ 在 RT-Thread 中,邮箱控制块是操作系统用于管理邮箱的一个数据结构,由结构体 struct rt_mailbox 表示。另外一种 C 表达方式 rt_mailbox_t,表示的是邮箱的句柄,在 C 语言中的实现是邮箱控制块的指针。邮箱控制块结构的详细定义请见以下代码:

struct rt_mailbox
{
    struct rt_ipc_object parent;

    rt_uint32_t* msg_pool;                /* 邮箱缓冲区的开始地址 */
    rt_uint16_t size;                     /* 邮箱缓冲区的大小     */

    rt_uint16_t entry;                    /* 邮箱中邮件的数目     */
    rt_uint16_t in_offset, out_offset;    /* 邮箱缓冲的进出指针   */
    rt_list_t suspend_sender_thread;      /* 发送线程的挂起等待队列 */
};
typedef struct rt_mailbox* rt_mailbox_t;

rt_mailbox 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。

5.1.3 邮箱的管理方式

邮箱相关接口

创建和删除动态邮箱:

  • 创建
rt_mailbox_t rt_mb_create (const char* name, rt_size_t size, rt_uint8_t flag);

​ 创建邮箱对象时会先从对象管理器中分配一个邮箱对象,然后给邮箱动态分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件数目和发送邮件在邮箱中的偏移量。参数说明:

参数描述
name邮箱名称
size邮箱容量
flag邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_NULL创建失败
邮箱对象的句柄创建成功
  • 删除
rt_err_t rt_mb_delete (rt_mailbox_t mb);

删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程返回值是 -RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。参数说明:

参数描述
mb邮箱对象的句柄
返回——
RT_EOK成功

初始化和脱离邮箱:

  • 初始化
  rt_err_t rt_mb_init(rt_mailbox_t mb,
                    const char* name,
                    void* msgpool,
                    rt_size_t size,
                    rt_uint8_t flag)

参数说明:

参数描述
mb邮箱对象的句柄
name邮箱名称
msgpool缓冲区指针
size邮箱容量
flag邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功

​ 这里的 size 参数指定的是邮箱的容量,即如果 msgpool 指向的缓冲区的字节数是 N,那么邮箱容量应该是 N/4。

  • 脱离
rt_err_t rt_mb_detach(rt_mailbox_t mb);

​ 使用该函数接口后,内核先唤醒所有挂在该邮箱上的线程(线程获得返回值是 - RT_ERROR),然后将该邮箱对象从内核对象管理器中脱离。参数说明:

参数描述
mb邮箱对象的句柄
返回——
RT_EOK成功

发送邮件:

线程或者中断服务程序可以通过邮箱给其他线程发送邮件

rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

​ 发送的邮件可以是 32 位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到 -RT_EFULL 的返回值。参数说明:

参数描述
mb邮箱对象的句柄
value邮件内容
返回——
RT_EOK发送成功
-RT_EFULL邮箱已经满了

等待方式发送邮件:

rt_err_t rt_mb_send_wait (rt_mailbox_t mb,
                      rt_uint32_t value,
                      rt_int32_t timeout);

​ rt_mb_send_wait() 与 rt_mb_send() 的区别在于多了等待时间,如果超时,发送线程将被唤醒并返回错误代码。参数说明:

参数描述
mb邮箱对象的句柄
value邮件内容
timeout超时时间
返回——
RT_EOK发送成功
-RT_ETIMEOUT超时
-RT_ERROR失败,返回错误

发送紧急邮件:

​ 发送紧急邮件该邮件直接插队放入邮件队首,接收者就能优先收到紧急邮件并处理。

rt_err_t rt_mb_urgent (rt_mailbox_t mb, rt_ubase_t value);

参数说明:

参数描述
mb邮箱对象的句柄
value邮件内容
返回——
RT_EOK发送成功
-RT_EFULL邮箱已满

接收邮件:

​ 只有当接收的邮箱中有邮件时,才能立即取到邮件并返回RT_ROK,否则根据超时时间设置,或挂起在邮箱的等待线程队列上,或直接返回。

rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);

参数说明:

参数描述
mb邮箱对象的句柄
value邮件内容
timeout超时时间
返回——
RT_EOK接收成功
-RT_ETIMEOUT超时
-RT_ERROR失败,返回错误
5.1.4 邮箱使用示例
/*
初始化 2 个静态线程,一个静态的邮箱对象,其中一个线程往邮箱中发送邮件,一个线程往邮箱中收取邮件。
*/
#include <rtthread.h>

#define THREAD_PRIORITY      10
#define THREAD_TIMESLICE     5

/* 邮箱控制块 */
static struct rt_mailbox mb;
/* 用于放邮件的内存池 */
static char mb_pool[128];

static char mb_str1[] = "I'm a mail!";
static char mb_str2[] = "this is another mail!";
static char mb_str3[] = "over";

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;

/* 线程 1 入口 */
static void thread1_entry(void *parameter)
{
    char *str;

    while (1)
    {
        rt_kprintf("thread1: try to recv a mail\n");

        /* 从邮箱中收取邮件 */
        if (rt_mb_recv(&mb, (rt_uint32_t *)&str, RT_WAITING_FOREVER) == RT_EOK)
        {
            rt_kprintf("thread1: get a mail from mailbox, the content:%s\n", str);
            if (str == mb_str3)
                break;

            /* 延时 100ms */
            rt_thread_mdelay(100);
        }
    }
    /* 执行邮箱对象脱离 */
    rt_mb_detach(&mb);
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;

/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
    rt_uint8_t count;

    count = 0;
    while (count < 10)
    {
        count ++;
        if (count & 0x1)  //切换
        {
            /* 发送 mb_str1 地址到邮箱中 */
            rt_mb_send(&mb, (rt_uint32_t)&mb_str1);
        }
        else
        {
            /* 发送 mb_str2 地址到邮箱中 */
            rt_mb_send(&mb, (rt_uint32_t)&mb_str2);
        }

        /* 延时 200ms */
        rt_thread_mdelay(200);
    }

    /* 发送邮件告诉线程 1,线程 2 已经运行结束 */
    rt_mb_send(&mb, (rt_uint32_t)&mb_str3);
}

int mailbox_sample(void)
{
    rt_err_t result;

    /* 初始化一个 mailbox */
    result = rt_mb_init(&mb,
                        "mbt",                      /* 名称是 mbt */
                        &mb_pool[0],                /* 邮箱用到的内存池是 mb_pool */
                        sizeof(mb_pool) / 4,        /* 邮箱中的邮件数目,因为一封邮件占 4 字节 */
                        RT_IPC_FLAG_FIFO);          /* 采用 FIFO 方式进行线程等待 */
    if (result != RT_EOK)
    {
        rt_kprintf("init mailbox failed.\n");
        return -1;
    }

    rt_thread_init(&thread1,
                   "thread1",
                   thread1_entry,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack),
                   THREAD_PRIORITY, THREAD_TIMESLICE);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(mailbox_sample, mailbox sample);
5.1.5 邮箱的使用场合

​ 由于在 32 系统上 4 字节的内容恰好可以放置一个指针,因此当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中,即邮箱也可以传递指针,例如:

struct msg
{
    rt_uint8_t *data_ptr;
    rt_uint32_t data_size;
};

struct msg* msg_ptr;

msg_ptr = (struct msg*)rt_malloc(sizeof(struct msg));
msg_ptr->data_ptr = ...; /* 指向相应的数据块地址 */
msg_ptr->data_size = len; /* 数据块的长度 */
/* 发送这个消息指针给 mb 邮箱 */
rt_mb_send(mb, (rt_uint32_t)msg_ptr);

​ 即,虽然这个结构体占用字节超过4字节,但可以定义一个结构体指针(4 bytes)指向它,再把这个指针发到邮箱,实现用邮箱传输较大数据的功能。

​ 而在接收线程中,因为收取过来的是指针,而 msg_ptr 是一个新分配出来的内存块,所以在接收线程处理完毕后,需要释放相应的内存块:

struct msg* msg_ptr;
if (rt_mb_recv(mb, (rt_uint32_t*)&msg_ptr) == RT_EOK)
{
    /* 在接收线程处理完毕后,需要释放相应的内存块 */
    rt_free(msg_ptr);
}
5.2 消息队列(messagequeue)
5.2.1 消息队列的工作机制

​ 消息队列能接受来自线程或者终端服务例程中不固定长的消息,并把消息缓存在自己的内存空间中。其他线程可以从消息队列中读取相应的消息,如果消息队列为空,可以挂起想读取消息的线程,直到有新消息到达,线程唤醒,接收并处理消息。异步的通信方式。

​ 线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。消息队列遵从先进先出原则(FIFO)。

  • 消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。
  • 每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息
  • 消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;
  • 有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。
  • 所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。
5.2.2 消息队列控制块

​ 消息队列控制块是操作系统用于管理消息队列的一个数据结构,由结构体 struct rt_messagequeue 表示。另外一种 C 表达方式 rt_mq_t,表示的是消息队列的句柄,在 C 语言中的实现是消息队列控制块的指针。消息队列控制块结构的详细定义请见以下代码:

struct rt_messagequeue
{
    struct rt_ipc_object parent;

    void* msg_pool;                     /* 指向存放消息的缓冲区的指针 */

    rt_uint16_t msg_size;               /* 每个消息的长度 */
    rt_uint16_t max_msgs;               /* 最大能够容纳的消息数 */

    rt_uint16_t entry;                  /* 队列中已有的消息数 */

    void* msg_queue_head;               /* 消息链表头 */
    void* msg_queue_tail;               /* 消息链表尾 */
    void* msg_queue_free;               /* 空闲消息链表 */

    rt_list_t suspend_sender_thread;    /* 发送线程的挂起等待队列 */
};
typedef struct rt_messagequeue* rt_mq_t;

rt_messagequeue 对象从 rt_ipc_object 中派生,由 IPC 容器所管理。

5.2.3 消息队列的管理方式

消息队列相关接口

创建和删除动态消息队列:

  • 创建
rt_mq_t rt_mq_create(const char* name, rt_size_t msg_size,
            rt_size_t max_msgs, rt_uint8_t flag);

创建消息队列时先从对象管理器中分配一个消息队列对象,然后给消息队列对象分配一块内存空间,组织成空闲消息链表,这块 内存的大小 = [ 消息大小 + 消息头(用于链表连接)的大小 ] × 消息队列最大个数 内存的大小 = [消息大小 + 消息头(用于链表连接)的大小]×消息队列最大个数 内存的大小=[消息大小+消息头(用于链表连接)的大小]×消息队列最大个数,接着再初始化消息队列,此时消息队列为空。参数说明:

参数描述
name消息队列的名称
msg_size消息队列中一条消息的最大长度,单位字节
max_msgs消息队列的最大个数
flag消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK发送成功
消息队列对象的句柄成功
RT_NULL失败
  • 删除
rt_err_t rt_mq_delete(rt_mq_t mq);

​ 删除消息队列时,如果有线程被挂起在该消息队列等待队列上,则内核先唤醒挂起在该消息等待队列上的所有线程(线程返回值是 - RT_ERROR),然后再释放消息队列使用的内存,最后删除消息队列对象。参数说明:

参数描述
mq消息队列对象的句柄
返回——
RT_EOK成功

初始化和脱离静态消息队列:

  • 初始化
rt_err_t rt_mq_init(rt_mq_t mq, const char* name,
                        void *msgpool, rt_size_t msg_size,
                        rt_size_t pool_size, rt_uint8_t flag);

参数说明:

参数描述
mq消息队列对象的句柄
name消息队列的名称
msgpool指向存放消息的缓冲区的指针
msg_size消息队列中一条消息的最大长度,单位字节
pool_size存放消息的缓冲区大小
flag消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功
  • 脱离
rt_err_t rt_mq_detach(rt_mq_t mq);

​ 使用该函数接口后,内核先唤醒所有挂在该消息等待队列对象上的线程(线程返回值是 -RT_ERROR),然后将该消息队列对象从内核对象管理器中脱离。

参数描述
mq消息队列对象的句柄
返回——
RT_EOK成功

发送消息:

​ 线程或者中断服务程序都可以给消息队列发送消息。发消息时消息队列对象从消息链表上取下一个空闲消息快,把发送的内容复制到消息快上,然后把消息块挂到队列尾部。如果队列无可用空闲块(消息队列已满),则返回错误码-RT_EFULL

rt_err_t rt_mq_send (rt_mq_t mq, void* buffer, rt_size_t size);

参数说明:

参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
返回——
RT_EOK成功
-RT_EFULL消息队列已满
-RT_ERROR失败,表示发送的消息长度大于消息队列中消息的最大长度

等待方式发送消息:

rt_err_t rt_mq_send_wait(rt_mq_t     mq,
                         const void *buffer,
                         rt_size_t   size,
                         rt_int32_t  timeout);

多了超时时间,如果超过,则发送线程被唤醒并返回错误码。参数说明:

参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
timeout超时时间
返回——
RT_EOK成功
-RT_EFULL消息队列已满
-RT_ERROR失败,表示发送的消息长度大于消息队列中消息的最大长度

发送紧急消息:

紧急消息复制到空闲消息块上后被直接挂到队列队首。

rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);

参数说明:

参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
返回——
RT_EOK成功
-RT_EFULL消息队列已满
-RT_ERROR失败

接收消息:

消息队列有消息才能接收,否则根据超时时间或挂起或直接返回。

rt_ssize_t rt_mq_recv (rt_mq_t mq, void* buffer,
                    rt_size_t size, rt_int32_t timeout);

接收消息后队列上的队首信息被转移到空闲消息链表尾部。参数说明:

参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
timeout指定的超时时间
返回——
接收到消息的长度成功收到
-RT_ETIMEOUT超时
-RT_ERROR失败,返回错误

5.2.4 消息队列应用示例:

/*
例程中初始化了 2 个静态线程,一个线程会从消息队列中收取消息;另一个线程会定时给消息队列发送普通消息和紧急消息
*/
#include <rtthread.h>

/* 消息队列控制块 */
static struct rt_messagequeue mq;
/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程 1 入口函数 */
static void thread1_entry(void *parameter)
{
    char buf = 0;
    rt_uint8_t cnt = 0;

    while (1)
    {
        /* 从消息队列中接收消息 */
        if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) > 0) // 大于0表示成功收到
        {
            rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);
            if (cnt == 19)
            {
                break;
            }
        }
        /* 延时 50ms */
        cnt++;
        rt_thread_mdelay(50);
    }
    rt_kprintf("thread1: detach mq \n");
    rt_mq_detach(&mq);
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
    int result;
    char buf = 'A';
    rt_uint8_t cnt = 0;

    while (1)
    {
        if (cnt == 8)
        {
            /* 发送紧急消息到消息队列中 */
            result = rt_mq_urgent(&mq, &buf, 1);
            if (result != RT_EOK)
            {
                rt_kprintf("rt_mq_urgent ERR\n");
            }
            else
            {
                rt_kprintf("thread2: send urgent message - %c\n", buf);
            }
        }
        else if (cnt>= 20)/* 发送 20 次消息之后退出 */
        {
            rt_kprintf("message queue stop send, thread2 quit\n");
            break;
        }
        else
        {
            /* 发送消息到消息队列中 */
            result = rt_mq_send(&mq, &buf, 1);
            if (result != RT_EOK)
            {
                rt_kprintf("rt_mq_send ERR\n");
            }

            rt_kprintf("thread2: send message - %c\n", buf);
        }
        buf++;
        cnt++;
        /* 延时 5ms */
        rt_thread_mdelay(5);
    }
}

/* 消息队列示例的初始化 */
int msgq_sample(void)
{
    rt_err_t result;

    /* 初始化消息队列 */
    result = rt_mq_init(&mq,
                        "mqt",
                        &msg_pool[0],             /* 内存池指向 msg_pool */
                        1,                          /* 每个消息的大小是 1 字节 */
                        sizeof(msg_pool),        /* 内存池的大小是 msg_pool 的大小 */
                        RT_IPC_FLAG_PRIO);       /* 如果有多个线程等待,优先级大小的方法分配消息 */

    if (result != RT_EOK)
    {
        rt_kprintf("init message queue failed.\n");
        return -1;
    }

    rt_thread_init(&thread1,
                   "thread1",
                   thread1_entry,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack), 25, 5);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack), 25, 5);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(msgq_sample, msgq sample);

仿真结果:

\ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 24 2018
 2006 - 2018 Copyright by rt-thread team
msh > msgq_sample
msh >thread2: send message - A
thread1: recv msg from msg queue, the content:A
thread2: send message - B
thread2: send message - C
thread2: send message - D
thread2: send message - E
thread1: recv msg from msg queue, the content:B
thread2: send message - F
thread2: send message - G
thread2: send message - H
thread2: send urgent message - I
thread2: send message - J
thread1: recv msg from msg queue, the content:I
thread2: send message - K
thread2: send message - L
thread2: send message - M
thread2: send message - N
thread2: send message - O
thread1: recv msg from msg queue, the content:C
thread2: send message - P
thread2: send message - Q
thread2: send message - R
thread2: send message - S
thread2: send message - T
thread1: recv msg from msg queue, the content:D
message queue stop send, thread2 quit
thread1: recv msg from msg queue, the content:E
thread1: recv msg from msg queue, the content:F
thread1: recv msg from msg queue, the content:G
…
thread1: recv msg from msg queue, the content:T
thread1: detach mq
5.2.4 消息队列的使用场合

发送消息:

​ 当创建的消息队列中的所有消息最大长度都是4字节时,消息队列蜕化成邮箱。如果不是4个字节,就会有差别:

struct msg
{
    rt_uint8_t *data_ptr;    /* 数据块首地址 */
    rt_uint32_t data_size;   /* 数据块大小   */
};

// 将消息发送到消息队列
void send_op(void *data, rt_size_t length)
{
    // 定义局部变量
    struct msg msg_ptr;

    msg_ptr.data_ptr = data;  /* 指向相应的数据块地址 */
    msg_ptr.data_size = length; /* 数据块的长度 */

    /* 发送这个消息指针给 mq 消息队列 */
    rt_mq_send(mq, (void*)&msg_ptr, sizeof(struct msg)); // 可以把整个结构体发过去,不用只发指针
}

// 从消息队列中接收消息
void message_handler()
{
    // 也定义一个局部变量
    struct msg msg_ptr; /* 用于放置消息的局部变量 */

    /* 从消息队列中接收消息到 msg_ptr 中 */
    if (rt_mq_recv(mq, (void*)&msg_ptr, sizeof(struct msg), RT_WAITING_FOREVER) > 0)
    {
        /* 成功接收到消息,进行相应的数据处理 */
    }
}

​ 上面的代码中,是把一个局部变量的数据内容发送到了消息队列中。在邮箱例子中,这个结构只能够发送指向这个结构的指针(在函数指针被发送过去后,接收线程能够正确的访问指向这个地址的内容,通常这块数据需要留给接收线程来释放)。而使用消息队列时,在接收线程中,同样也采用局部变量进行消息接收的结构体。因为发送和接收消息都是进行内容复制,用局部变量可以免去动态内存分配的烦恼,接收的时候也不用担心消息内存空间是否已经被释放了的问题。

同步消息:

​ 在一般的系统设计中会经常遇到要发送同步消息的问题,这个时候就可以根据当时状态的不同选择相应的实现:两个线程间可以采用 [消息队列 + 信号量或邮箱] 的形式实现。发送线程通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确认,工作示意图如下图所示:

同步消息示意图

根据消息确认的不同,可以把消息结构体定义成:

struct msg
{
    /* 消息结构其他成员 */
    struct rt_mailbox ack;
};
/* 或者 */
struct msg
{
    /* 消息结构其他成员 */
    struct rt_semaphore ack;
};

​ 第一种类型的消息使用了邮箱来作为确认标志,而第二种类型的消息采用了信号量来作为确认标志。邮箱作为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号量作为确认标志只能够单一的通知发送线程,消息已经确认接收。

5.3 信号(signal)

​ 信号(又称为软中断信号),在软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。

5.3.1 信号的工作机制

​ 信号用做线程之间的异常通知、应急处理(异步事件)。一个线程不需要通过任何操作等待信号到达,也不知道信号何时到达,线程之间互相通过调用rt_thread_kill()发送软中断信号。

处理方式:

(1) 类似中断的处理程序,对于需要处理的信号,线程可以指定处理函数,由该函数来处理。

(2) 忽略某个信号,对该信号不做任何处理,就像未发生过一样。

(3) 对该信号的处理保留系统的默认值。

当信号传递到的线程处于挂起状态时,会把状态改为就绪去处理对应信号。如果处于运行,则在他当前线程栈上建立新栈空间去处理对应信号,也就意味着之用的线程栈大小会增加。

5.3.2 信号的管理方式

信号相关接口

安装信号:

​ 用来定义线程要处理哪个信号,当信号传递到时,要执行哪种操作。

rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);

其中 rt_sighandler_t 是定义信号处理函数的函数指针类型。参数说明:

参数描述
signo信号值[只有 SIGUSR1(10) 和 SIGUSR2 (12)是开放给用户使用的,下同]
handler设置对信号值的处理方式
返回——
SIG_ERR错误的信号
安装信号前的 handler 值成功

参数handler可以设置成三种类型,对应上面说的三种处理方式:

1)类似中断的处理方式,参数指向当信号发生时用户自定义的处理函数,由该函数来处理。

2)参数设为 SIG_IGN,忽略某个信号,对该信号不做任何处理,就像未发生过一样。

3)参数设为 SIG_DFL,系统会调用默认的处理函数_signal_default_handler()。

阻塞(屏蔽)信号:

​ 如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。

void rt_signal_mask(int signo);

参数说明:

参数描述
signo信号值

解除信号阻塞:

void rt_signal_unmask(int signo);

参数说明:

参数描述
signo信号值

发送信号:

​ 当需要进行异常处理时,可以给设定了处理异常的线程发送信号。

int rt_thread_kill(rt_thread_t tid, int sig);

参数说明:

参数描述
tid接收信号的线程
sig信号值
返回——
RT_EOK发送成功
-RT_EINVAL参数错误

等待信号:

​ 根据超时时间等待信号,挂起或者返回,如果等到了就将指向该信号体的指针存入si。

int rt_signal_wait(const rt_sigset_t *set,
                        rt_siginfo_t[] *si, rt_int32_t timeout);

其中 rt_siginfo_t 是定义信号信息的数据类型。参数说明:

参数描述
set指定等待的信号
si指向存储等到信号信息的指针
timeout指定的等待时间
返回——
RT_EOK等到信号
-RT_ETIMEOUT超时
-RT_EINVAL参数错误
5.3.3 信号应用示例
/*
此例程创建了 1 个线程,在安装信号时,信号处理方式设为自定义处理,定义的信号的处理函数为 thread1_signal_handler()。待此线程运行起来安装好信号之后,给此线程发送信号。此线程将接收到信号,并打印信息。
*/
#include <rtthread.h>

#define THREAD_PRIORITY         25
#define THREAD_STACK_SIZE       512
#define THREAD_TIMESLICE        5

static rt_thread_t tid1 = RT_NULL;

/* 线程 1 的信号处理函数 */
void thread1_signal_handler(int sig)
{
    rt_kprintf("thread1 received signal %d\n", sig);
}

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
    int cnt = 0;

    /* 安装信号 */
    rt_signal_install(SIGUSR1, thread1_signal_handler) ;// 安装
    rt_signal_unmask(SIGUSR1); // 解除阻塞
	//SIGUSR1为信号值,只有SIGUSR1(10)和SIGUSR2(12)两种供用户使用
    
    /* 运行 10 次 */
    while (cnt < 10)
    {
        /* 线程 1 采用低优先级运行,一直打印计数值 */
        rt_kprintf("thread1 count : %d\n", cnt);

        cnt++;
        rt_thread_mdelay(100);
    }
}

/* 信号示例的初始化 */
int signal_sample(void)
{
    /* 创建线程 1 */
    tid1 = rt_thread_create("thread1",
                            thread1_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);

    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    rt_thread_mdelay(300);

    /* 发送信号 SIGUSR1 给线程 1 */
    rt_thread_kill(tid1, SIGUSR1);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(signal_sample, signal sample);

6 内存管理

​ 计算机系统中,存储空间分为两种:

  • RAM (随机存储器)

变量、中间数据一般存放在 RAM 中,只有在实际使用时才将它们从 RAM 调入到 CPU 中进行运算。需要时申请,由系统分配,使用完毕释放。访问速度快,按变量地址随机访问,掉电丢失——内存

  • ROM (只读存储器)

存储内容固定,掉电数据不都是——硬盘

​ RT-Thread 中有两种内存管理方式,分别是动态内存堆管理静态内存池管理

6.1 内存管理的功能特点
  1. 内存分配的时间要确定
  2. 内存随着不断分配和释放会产生碎片——地址不连续,不能作为一整块大的内存块分配出去
  3. 嵌入式资源环境有些紧张,只有数十KB可以分配,而有些能分配数MB

​ 因此为了应对以上情况,需要合适的内存分配管理算法,RT-Thread在内存堆管理根据具体内存设备划分三种情况:

第一种是针对小内存块的分配管理(小内存管理算法);

第二种是针对大内存块的分配管理(slab 管理算法);

第三种是针对多内存堆的分配情况(memheap 管理算法)。

6.2 内存堆管理

​ RT-Thread 系统为了满足不同的需求,提供了不同的内存管理算法,分别是小内存管理算法、slab 管理算法和 memheap 管理算法。

6.2.1 小内存管理算法

​ 主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统。

​ 初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来。

img

6.2.2 slab管理算法

​ slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。

​ slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:

slab 内存分配结构图

下面是内存分配器主要的两种操作:

(1)内存分配

​ 假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个 zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone 中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除。

(2)内存释放

​ 分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时 zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。

6.2.3 memheap管理算法

​ memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆,用户使用起来会非常方便。

​ memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。

memheap 处理多内存堆

6.2.4 内存配置和初始化

​ 在使用内存堆时,必须要在系统初始化的时候进行堆的初始化。

void rt_system_heap_init(void* begin_addr, void* end_addr);

​ 这个函数会把参数 begin_addr,end_addr 区域的内存空间作为内存堆来使用。参数说明:

参数描述
begin_addr堆内存区域起始地址
end_addr堆内存区域结束地址

​ 在使用 memheap 堆内存时,必须要在系统初始化的时候进行堆内存的初始化,可以通过下面的函数接口完成:

rt_err_t rt_memheap_init(struct rt_memheap  *memheap,
                        const char  *name,
                        void        *start_addr,
                        rt_uint32_t size)

​ 如果有多个不连续的 memheap 可以多次调用该函数将其初始化并加入 memheap_item 链表。参数说明:

参数描述
memheapmemheap 控制块
name内存堆的名称
start_addr堆内存区域起始地址
size堆内存大小
返回——
RT_EOK成功
6.2.5 内存堆的管理方式

内存堆的操作

分配和释放内存:

  • 分配
void *rt_malloc(rt_size_t nbytes);

​ rt_malloc 函数会从系统堆空间中找到合适大小的内存块,然后把内存块可用地址返回给用户。参数说明:

参数描述
nbytes需要分配的内存块的大小,单位为字节
返回——
分配的内存块地址成功
RT_NULL失败

​ 申请内存后必须对其返回值进行判空,申请的内存必须及时释放,否则会造成内存泄漏。

int *pi;
pi = rt_malloc(100);
if(pi == NULL) 
{
    rt_kprintf("malloc failed\r\n");
}
  • 释放
void rt_free (void *ptr);

​ rt_free 函数会把待释放的内存还回给堆管理器中。在调用这个函数时用户需传递待释放的内存块指针,如果是空指针直接返回。参数说明:

参数描述
ptr待释放的内存块指针

rt_free 的参数必须是以下其一:

  • 是 NULL 。
  • 是一个先前从 rt_malloc、 rt_calloc 或 rt_realloc 返回的值。

重分配内存块:

​ 在已分配内存块的基础上重新分配内存块的大小(增加或缩小)。

void *rt_realloc(void *rmem, rt_size_t newsize);

​ 原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)。参数说明:

参数描述
rmem指向已分配的内存块
newsize重新分配的内存大小
返回——
重新分配的内存块地址成功
  • 如果原先的内存块无法改变大小,rt_realloc 将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用 rt_realloc 之后,你就不能再使用指向旧内存块的指针,而是应该改用 rt_realloc 所返回的新指针。
  • 如果 rt_realloc 的第一个参数为 NULL, 那么它的行为就和 rt_malloc 一样。

分配多内存块:

从内存堆中分配连续内存地址的多个内存块。

void *rt_calloc(rt_size_t count, rt_size_t size);

参数说明:

参数描述
count内存块数量
size内存块容量
返回——
指向第一个内存块地址的指针成功 ,并且所有分配的内存块都被初始化成零。
RT_NULL分配失败

设置钩子函数:

void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));

​ 设置的钩子函数会在内存分配完成后进行回调。回调时,会把分配到的内存块地址和大小做为入口参数传递进去。参数说明:

参数描述
hook钩子函数指针

其中 hook 函数接口如下:

void hook(void *ptr, rt_size_t size)

参数说明:

参数描述
ptr分配到的内存块指针
size分配到的内存块的大小

​ 在释放内存时,用户可设置一个钩子函数,调用的函数接口如下:

void rt_free_sethook(void (*hook)(void *ptr));

​ 设置的钩子函数会在调用内存释放完成前进行回调。回调时,释放的内存块地址会做为入口参数传递进去(此时内存块并没有被释放)。参数说明:

参数描述
hook钩子函数指针

其中 hook 函数接口如下:

void hook(void *ptr);

参数说明:

参数描述
ptr待释放的内存块指针
6.2.6 总结

动态内存使用总结:

  • 检查从 rt_malloc 函数返回的指针是否为 NULL
  • 不要访问动态分配内存之外的内存
  • 不要向 rt_free 传递一个并非由 rt_malloc 函数返回的指针
  • 在释放动态内存之后不要再访问它
  • 使用 sizeof 计算数据类型的长度,提高程序的可移植性

常见的动态内存错误:

  • 对 NULL 指针进行解引用
  • 对分配的内存进行操作时越过边界
  • 释放并非动态分配的内存
  • 释放一块动态分配的内存的一部分 (rt_free(ptr + 4))
  • 动态内存被释放后继续使用

内存碎片:频繁的调用内存分配和释放接口会导致内存碎片,一个避免内存碎片的策略是使用 内存池 + 内存堆 混用的方法。

内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。

6.2.7 内存堆管理应用示例
/*
这个程序会创建一个动态的线程,这个线程会动态申请内存并释放,每次申请更大的内存,当申请不到的时候就结束
*/
#include <rtthread.h>

#define THREAD_PRIORITY      25
#define THREAD_STACK_SIZE    512
#define THREAD_TIMESLICE     5

/* 线程入口 */
void thread1_entry(void *parameter)
{
    int i;
    char *ptr = RT_NULL; /* 内存块的指针 */

    for (i = 0; ; i++)
    {
        /* 每次分配 (1 << i) 大小字节数的内存空间 */
        ptr = rt_malloc(1 << i);

        /* 如果分配成功 */
        if (ptr != RT_NULL)
        {
            rt_kprintf("get memory :%d byte\n", (1 << i));
            /* 释放内存块 */
            rt_free(ptr);
            rt_kprintf("free memory :%d byte\n", (1 << i));
            ptr = RT_NULL;
        }
        else
        {
            rt_kprintf("try to get %d byte memory failed!\n", (1 << i));
            return;
        }
    }
}

int dynmem_sample(void)
{
    rt_thread_t tid = RT_NULL;

    /* 创建线程 1 */
    tid = rt_thread_create("thread1",
                           thread1_entry, RT_NULL,
                           THREAD_STACK_SIZE,
                           THREAD_PRIORITY,
                           THREAD_TIMESLICE);
    if (tid != RT_NULL)
        rt_thread_startup(tid);

    return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(dynmem_sample, dynmem sample);

​ 例程中分配内存成功并打印信息;当试图申请 65536 byte 即 64KB 内存时,由于 RAM 总大小只有 64K,而可用 RAM 小于 64K,所以分配失败。

6.3 内存池

​ 为了提高内存分配的效率,并且避免内存碎片,RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。

​ 内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。此外,RT-Thread 的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒。

​ 内存池的线程挂起功能非常适合需要通过内存资源进行同步的场景,例如播放音乐时,播放器线程会对音乐文件进行解码,然后发送到声卡驱动,从而驱动硬件播放音乐。

播放器线程与声卡驱动关系

6.3.1 内存池的工作机制

内存池控制块:

在 RT-Thread 实时操作系统中,内存池控制块由结构体 struct rt_mempool 表示。另外一种 C 表达方式 rt_mp_t,表示的是内存块句柄,在 C 语言中的实现是指向内存池控制块的指针,详细定义情况见以下代码:

struct rt_mempool
{
    struct rt_object parent;

    void        *start_address;  /* 内存池数据区域开始地址 */
    rt_size_t     size;           /* 内存池数据区域大小 */

    rt_size_t     block_size;    /* 内存块大小  */
    rt_uint8_t    *block_list;   /* 内存块列表  */

    /* 内存池数据区域中能够容纳的最大内存块数  */
    rt_size_t     block_total_count;
    /* 内存池中空闲的内存块数  */
    rt_size_t     block_free_count;
    /* 因为内存块不可用而挂起的线程列表 */
    rt_list_t     suspend_thread;
    /* 因为内存块不可用而挂起的线程数 */
    rt_size_t     suspend_thread_count;
};
typedef struct rt_mempool* rt_mp_t;

内存块分配机制:

​ 内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。

内存池工作机制图

​ 内存池一旦初始化完成,内部的内存块大小将不能再做调整。每一个内存池对象由上述结构组成,其中 suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上。

6.3.2 内存池的管理方式

内存池相关接口

创建和删除内存池:

  • 创建
rt_mp_t rt_mp_create(const char* name,
                         rt_size_t block_count,
                         rt_size_t block_size);

​ 使用该函数接口可以创建一个与需求的内存块大小、数目相匹配的内存池,前提当然是在系统资源允许的情况下(最主要的是内存堆内存资源)才能创建成功。创建内存池时,需要给内存池指定一个名称。然后内核从系统中申请一个内存池对象,然后从内存堆中分配一块由块数目和块大小计算得来的内存缓冲区,接着初始化内存池对象,并将申请成功的内存缓冲区组织成可用于分配的空闲块链表。参数说明:

参数描述
name内存池名
block_count内存块数量
block_size内存块容量
返回——
内存池的句柄创建内存池对象成功
RT_NULL创建失败
  • 删除
rt_err_t rt_mp_delete(rt_mp_t mp);

​ 删除内存池时,会首先唤醒等待在该内存池对象上的所有线程(返回 -RT_ERROR),然后再释放已从内存堆上分配的内存池数据存放区域,然后删除内存池对象。参数说明:

参数描述
mprt_mp_create 返回的内存池对象句柄
返回——
RT_EOK删除成功

初始化和脱离内存池:

  • 初始化
rt_err_t rt_mp_init(rt_mp_t mp,
                        const char* name,
                        void *start, rt_size_t size,
                        rt_size_t block_size);

​ 此处内存池对象所使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池控制块,其余的初始化工作与创建内存池相同。参数说明:

参数描述
mp内存池对象
name内存池名
start内存池的起始位置
size内存池数据区域大小
block_size内存块容量
返回——
RT_EOK初始化成功
- RT_ERROR失败

内存池块个数 = size / (block_size + 4 链表指针大小),计算结果取整数。

例如:内存池数据区总大小 size 设为 4096 字节,内存块大小 block_size 设为 80 字节;则申请的内存块个数为 4096/ (80+4)= 48 个

  • 脱离

脱离内存池将把内存池对象从内核对象管理器中脱离。

rt_err_t rt_mp_detach(rt_mp_t mp);

​ 使用该函数接口后,内核先唤醒所有等待在该内存池对象上的线程,然后将内存池对象从内核对象管理器中脱离。参数说明:

参数描述
mp内存池对象
返回——
RT_EOK成功

分配和释放内存块:

  • 分配

从指定内存池中分配一个内存块。

void *rt_mp_alloc (rt_mp_t mp, rt_int32_t time);

​ 其中参数time表示申请分配内存块的超时时间。如果内存池有空闲内存块,则从空闲块链表中取下一个内存块,减少空闲块数目并返回这个内存块;如果没有空闲,且如果超时时间设置为零,则立刻返回空内存块,如果等待时间大于零,则把线程挂起在该内存对象上,直到有空闲可用内存块,或等待时间到。参数说明:

参数描述
mp内存池对象
time超时时间
返回——
分配的内存块地址成功
RT_NULL失败
  • 释放

任何内存块使用完后都必须被释放,否则会造成内存泄露。

void rt_mp_free (void *block);

​ 使用该函数接口时,首先通过需要被释放的内存块指针计算出该内存块所在的(或所属于的)内存池对象,然后增加内存池对象的可用内存块数目,并把该被释放的内存块加入空闲内存块链表上。接着判断该内存池对象上是否有挂起的线程,如果有,则唤醒挂起线程链表上的首线程。参数说明:

参数描述
block内存块指针
6.3.3 内存池应用示例
/*
这个例程会创建一个静态的内存池对象,2 个动态线程。一个线程会试图从内存池中获得内存块,另一个线程释放内存块内存块。
*/
#include <rtthread.h>

static rt_uint8_t *ptr[50];
static rt_uint8_t mempool[4096];
static struct rt_mempool mp;

#define THREAD_PRIORITY      25
#define THREAD_STACK_SIZE    512
#define THREAD_TIMESLICE     5

/* 指向线程控制块的指针 */
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;

/* 线程 1 入口 */
static void thread1_mp_alloc(void *parameter)
{
    int i;
    for (i = 0 ; i < 50 ; i++)
    {
        if (ptr[i] == RT_NULL)
        {
            /* 试图申请内存块 50 次,当申请不到内存块时,
               线程 1 挂起,转至线程 2 运行 */
            ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
            if (ptr[i] != RT_NULL)
                rt_kprintf("allocate No.%d\n", i);
        }
    }
}

/* 线程 2 入口,线程 2 的优先级比线程 1 低,应该线程 1 先获得执行。*/
static void thread2_mp_release(void *parameter)
{
    int i;

    rt_kprintf("thread2 try to release block\n");
    for (i = 0; i < 50 ; i++)
    {
        /* 释放所有分配成功的内存块 */
        if (ptr[i] != RT_NULL)
        {
            rt_kprintf("release block %d\n", i);
            rt_mp_free(ptr[i]);
            ptr[i] = RT_NULL;
        }
    }
}

int mempool_sample(void)
{
    int i;
    for (i = 0; i < 50; i ++) ptr[i] = RT_NULL;

    /* 初始化内存池对象 */
    rt_mp_init(&mp, "mp1", &mempool[0], sizeof(mempool), 80);

    /* 创建线程 1:申请内存池 */
    tid1 = rt_thread_create("thread1", thread1_mp_alloc, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);


    /* 创建线程 2:释放内存池 */
    tid2 = rt_thread_create("thread2", thread2_mp_release, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY + 1, THREAD_TIMESLICE);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(mempool_sample, mempool sample);

本例程在初始化内存池对象时,初始化了 4096 /(80+4) = 48 个内存块。

①线程 1 申请了 48 个内存块之后,此时内存块已经被用完,需要其他地方释放才能再次申请;但此时,线程 1 以一直等待的方式又申请了 1 个,由于无法分配,所以线程 1 挂起;

②线程 2 开始执行释放内存的操作;当线程 2 释放一个内存块的时候,就有一个内存块空闲出来,唤醒线程 1 申请内存,申请成功后再申请,线程 1 又挂起,再循环一次②;

③线程 2 继续释放剩余的内存块,释放完毕。

设备和驱动

1 I/O设备模型

1.1 I/O设备介绍

​ RT-Thread 提供了一套简单的 I/O 设备模型框架,如下图所示,它位于硬件和应用程序之间,共分成三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层。

I/O 设备模型框架

​ 设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册 I/O 设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到 I/O 设备管理器中,使用序列图如下图所示,主要有以下 2 点:

  • 设备驱动根据设备模型定义,创建出具备硬件访问能力的设备实例,将该设备通过 rt_device_register() 接口注册到 I/O 设备管理器中。
  • 应用程序通过 rt_device_find() 接口查找到设备,然后使用 I/O 设备管理接口来访问硬件。

简单 I/O 设备使用序列图

1.1.1 I/O设备模型

​ RT-Thread 的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来,每个具体设备都可以继承其父类对象的属性,并派生出其私有属性,下图是设备对象的继承和派生关系示意图。

I/O 设备模型框架补充图

设备继承关系图

设备对象具体定义如下所示:

struct rt_device
{
    struct rt_object          parent;        /* 内核对象基类 */
    enum rt_device_class_type type;          /* 设备类型 */
    rt_uint16_t               flag;          /* 设备参数 */
    rt_uint16_t               open_flag;     /* 设备打开标志 */
    rt_uint8_t                ref_count;     /* 设备被引用次数 */
    rt_uint8_t                device_id;     /* 设备 ID,0 - 255 */

    /* 数据收发回调函数 */
    rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
    rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);

    const struct rt_device_ops *ops;    /* 设备操作方法 */

    /* 设备的私有数据 */
    void *user_data;
};
typedef struct rt_device *rt_device_t;
1.1.2 I/O设备类型

RT-Thread 支持多种 I/O 设备类型,主要设备类型如下所示:

RT_Device_Class_Char             /* 字符设备       */
RT_Device_Class_Block            /* 块设备         */
RT_Device_Class_NetIf            /* 网络接口设备    */
RT_Device_Class_MTD              /* 内存设备       */
RT_Device_Class_RTC              /* RTC 设备        */
RT_Device_Class_Sound            /* 声音设备        */
RT_Device_Class_Graphic          /* 图形设备        */
RT_Device_Class_I2CBUS           /* I2C 总线设备     */
RT_Device_Class_USBDevice        /* USB device 设备  */
RT_Device_Class_USBHost          /* USB host 设备   */
RT_Device_Class_SPIBUS           /* SPI 总线设备     */
RT_Device_Class_SPIDevice        /* SPI 设备        */
RT_Device_Class_SDIO             /* SDIO 设备       */
RT_Device_Class_Miscellaneous    /* 杂类设备        */

字符模式设备允许非结构的数据传输,即通常数据传输采用串行的形式,每次一个字节。字符设备通常是一些简单设备,如串口、按键。

块设备每次传输一个数据块,例如每次传输 512 个字节数据。这个数据块是硬件强制性的,数据块可能使用某类数据接口或某些强制性的传输协议,否则就可能发生错误。因此,有时块设备驱动程序对读或写操作必须执行附加的工作,如下图所示:

块设备

​ 如上图,当要进行大量数据写操作时,设备驱动必须先将数据划分成多个包,即切成多个块依次写入,这样就会导致,最后一个数据块的尺寸小于设备块尺寸,则当写入最后一个块的操作就会有一些差异:例如上图中的块 4,驱动程序需要先把块 4 所对应的设备块读出来,然后将需要写入的数据覆盖至从设备块读出的数据上,使其合并成一个新的块,最后再写回到块设备中。

1.2 创建和注册I/O设备
1.2.1 创建I/O设备
rt_device_t rt_device_create(int type, int attach_size);

参数说明:

参数描述
type设备类型,可取前面小节列出的设备类型值
attach_size用户数据大小
返回——
设备句柄创建成功
RT_NULL创建失败,动态内存分配失败

调用该接口时,系统会从动态堆内存中分配一个设备控制块,大小为 struct rt_device 和 attach_size 的和,设备的类型由参数 type 设定。设备被创建后,需要实现它访问硬件的操作方法。

struct rt_device_ops
{
    /* common device interface */
    rt_err_t  (*init)   (rt_device_t dev);
    rt_err_t  (*open)   (rt_device_t dev, rt_uint16_t oflag);
    rt_err_t  (*close)  (rt_device_t dev);
    rt_size_t (*read)   (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
    rt_size_t (*write)  (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
    rt_err_t  (*control)(rt_device_t dev, int cmd, void *args);
};

各个操作方法的描述如下表所示:

方法名称方法描述
init初始化设备。设备初始化完成后,设备控制块的 flag 会被置成已激活状态 (RT_DEVICE_FLAG_ACTIVATED)。如果设备控制块中的 flag 标志已经设置成激活状态,那么再运行初始化接口时会立刻返回,而不会重新进行初始化。
open打开设备。有些设备并不是系统一启动就已经打开开始运行,或者设备需要进行数据收发,但如果上层应用还未准备好,设备也不应默认已经使能并开始接收数据。所以建议在写底层驱动程序时,在调用 open 接口时才使能设备。
close关闭设备。在打开设备时,设备控制块会维护一个打开计数,在打开设备时进行 + 1 操作,在关闭设备时进行 - 1 操作,当计数器变为 0 时,才会进行真正的关闭操作。
read从设备读取数据。参数 pos 是读取数据的偏移量,但是有些设备并不一定需要指定偏移量,例如串口设备,设备驱动应忽略这个参数。而对于块设备来说,pos 以及 size 都是以块设备的数据块大小为单位的。例如块设备的数据块大小是 512,而参数中 pos = 10, size = 2,那么驱动应该返回设备中第 10 个块 (从第 0 个块做为起始),共计 2 个块的数据。这个接口返回的类型是 rt_size_t,即读到的字节数或块数目。正常情况下应该会返回参数中 size 的数值,如果返回零请设置对应的 errno 值。
write向设备写入数据。参数 pos 是写入数据的偏移量。与读操作类似,对于块设备来说,pos 以及 size 都是以块设备的数据块大小为单位的。这个接口返回的类型是 rt_size_t,即真实写入数据的字节数或块数目。正常情况下应该会返回参数中 size 的数值,如果返回零请设置对应的 errno 值。
control根据 cmd 命令控制设备。命令往往是由底层各类设备驱动自定义实现。例如参数 RT_DEVICE_CTRL_BLK_GETGEOME,意思是获取块设备的大小信息。
1.2.2 销毁I/O设备

​ 当一个动态创建的设备不再需要使用时可以通过如下函数来销毁:

void rt_device_destroy(rt_device_t device);

参数说明:

参数描述
device设备句柄
返回
1.2.3 注册I/O设备

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

rt_err_t rt_device_register(rt_device_t dev, const char* name, rt_uint8_t flags);

参数说明:

参数描述
dev设备句柄
name设备名称,设备名称的最大长度由 rtconfig.h 中定义的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
flags设备模式标志
返回——
RT_EOK注册成功
-RT_ERROR注册失败,dev 为空或者 name 已经存在

注:应当避免重复注册已经注册的设备,以及注册相同名字的设备。

flags 参数支持下列参数 (可以采用或的方式支持多种参数):

#define RT_DEVICE_FLAG_RDONLY       0x001 /* 只读 */
#define RT_DEVICE_FLAG_WRONLY       0x002 /* 只写  */
#define RT_DEVICE_FLAG_RDWR         0x003 /* 读写  */
#define RT_DEVICE_FLAG_REMOVABLE    0x004 /* 可移除  */
#define RT_DEVICE_FLAG_STANDALONE   0x008 /* 独立   */
#define RT_DEVICE_FLAG_SUSPENDED    0x020 /* 挂起  */
#define RT_DEVICE_FLAG_STREAM       0x040 /* 流模式  */
#define RT_DEVICE_FLAG_INT_RX       0x100 /* 中断接收 */
#define RT_DEVICE_FLAG_DMA_RX       0x200 /* DMA 接收 */
#define RT_DEVICE_FLAG_INT_TX       0x400 /* 中断发送 */
#define RT_DEVICE_FLAG_DMA_TX       0x800 /* DMA 发送 */

​ 设备流模式 RT_DEVICE_FLAG_STREAM 参数用于向串口终端输出字符串:当输出的字符是 “\n” 时,自动在前面补一个 “\r” 做分行。

​ 注册成功的设备可以在 FinSH 命令行使用 list_device 命令查看系统中所有的设备信息,包括设备名称、设备类型和设备被打开次数:

msh />list_device
device           type         ref count
-------- -------------------- ----------
e0       Network Interface    0
sd0      Block Device         1
rtc      RTC                  0
uart1    Character Device     0
uart0    Character Device     2
msh />
  • 实例
rt_err_t demo_init(rt_device_t dev)
{
    rt_kprintf("demo_init...\n");
    return 0;
}

rt_err_t demo_open(rt_device_t dev)
{
    rt_kprintf("demo_open...\n");
    return 0;
}

int rt_demo_init(void)
{
    rt_device_t demo_dev;
    demo_dev = rt_device_create(RT_Device_Class_Char,32);
    if(demo_dev == RT_NULL)
    {
        LOG_E(“rt_device_create failed…\n”);
        return -ENOMEM;
    }

    demo_dev->init = demo_init;
    demo_dev->open = demo_open;
    demo_dev->close = demo_close;
    
    rt_device_register(demo_dev,“demo”,RT_DEVICE_FLAG_RDWR);
    return 0;
    
}INIT_BOARD_EXPORT(rt_demo_init);
1.2.4 注销I/O设备

​ 当设备注销后的,设备将从设备管理器中移除,也就不能再通过设备查找搜索到该设备。注销设备不会释放设备控制块占用的内存(注意区分销毁和注销)。注销设备的函数如下所示:

rt_err_t rt_device_unregister(rt_device_t dev);

参数说明:

参数描述
dev设备句柄
返回——
RT_EOK成功

下面代码为看门狗设备的注册示例,调用 rt_hw_watchdog_register() 接口后,设备通过 rt_device_register() 接口被注册到 I/O 设备管理器中。

const static struct rt_device_ops wdt_ops =
{
    rt_watchdog_init,
    rt_watchdog_open,
    rt_watchdog_close,
    RT_NULL,
    RT_NULL,
    rt_watchdog_control,
};

rt_err_t rt_hw_watchdog_register(struct rt_watchdog_device *wtd,
                                 const char                *name,
                                 rt_uint32_t                flag,
                                 void                      *data)
{
    struct rt_device *device;
    RT_ASSERT(wtd != RT_NULL);

    device = &(wtd->parent);

    device->type        = RT_Device_Class_Miscellaneous;
    device->rx_indicate = RT_NULL;
    device->tx_complete = RT_NULL;

    device->ops         = &wdt_ops;
    device->user_data   = data;

    /* register a character device */
    return rt_device_register(device, name, flag);
}
1.3 访问I/O设备

​ 应用程序通过 I/O 设备管理接口来访问硬件设备,当设备驱动实现后,应用程序就可以访问该硬件。I/O 设备管理接口与 I/O 设备的操作方法的映射关系下图所示:

I/O 设备管理接口与 I/O 设备的操作方法的映射关系

1.3.1 查找设备

​ 应用程序根据设备名称获取设备句柄,进而可以操作设备。

rt_device_t rt_device_find(const char* name);

参数说明:

参数描述
name设备名称
返回——
设备句柄查找到对应设备将返回相应的设备句柄
RT_NULL没有找到相应的设备对象
1.3.2 初始化设备

​ 获得设备句柄后,应用程序可使用如下函数对设备进行初始化操作:

rt_err_t rt_device_init(rt_device_t dev);

参数说明:

参数描述
dev设备句柄
返回——
RT_EOK设备初始化成功
错误码设备初始化失败

注:当一个设备已经初始化成功后,调用这个接口将不再重复做初始化 0。

1.3.3 打开和关闭设备

打开设备:

​ 打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。

rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);

参数说明:

参数描述
dev设备句柄
oflags设备打开模式标志
返回——
RT_EOK设备打开成功
-RT_EBUSY如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开
其他错误码设备打开失败

oflags 支持以下的参数:

#define RT_DEVICE_OFLAG_CLOSE 0x000   /* 设备已经关闭(内部使用)*/
#define RT_DEVICE_OFLAG_RDONLY 0x001  /* 以只读方式打开设备 */
#define RT_DEVICE_OFLAG_WRONLY 0x002  /* 以只写方式打开设备 */
#define RT_DEVICE_OFLAG_RDWR 0x003    /* 以读写方式打开设备 */
#define RT_DEVICE_OFLAG_OPEN 0x008    /* 设备已经打开(内部使用)*/
#define RT_DEVICE_FLAG_STREAM 0x040   /* 设备以流模式打开 */
#define RT_DEVICE_FLAG_INT_RX 0x100   /* 设备以中断接收模式打开 */
#define RT_DEVICE_FLAG_DMA_RX 0x200   /* 设备以 DMA 接收模式打开 */
#define RT_DEVICE_FLAG_INT_TX 0x400   /* 设备以中断发送模式打开 */
#define RT_DEVICE_FLAG_DMA_TX 0x800   /* 设备以 DMA 发送模式打开 */

注:如果上层应用程序需要设置设备的接收回调函数,则必须以 RT_DEVICE_FLAG_INT_RX 或者 RT_DEVICE_FLAG_DMA_RX 的方式打开设备,否则不会回调函数。

关闭设备:

​ 应用程序打开设备完成读写等操作后,如果不需要再对设备进行操作则可以关闭设备。

rt_err_t rt_device_close(rt_device_t dev);

参数说明:

参数描述
dev设备句柄
返回——
RT_EOK关闭设备成功
-RT_ERROR设备已经完全关闭,不能重复关闭设备
其他错误码关闭设备失败

注:关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。

1.3.4 控制设备
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);

参数说明:

参数描述
dev设备句柄
cmd命令控制字,这个参数通常与设备驱动程序相关
arg控制的参数
返回——
RT_EOK函数执行成功
-RT_ENOSYS执行失败,dev 为空
其他错误码执行失败

参数 cmd 的通用设备命令可取如下宏定义:

#define RT_DEVICE_CTRL_RESUME           0x01   /* 恢复设备 */
#define RT_DEVICE_CTRL_SUSPEND          0x02   /* 挂起设备 */
#define RT_DEVICE_CTRL_CONFIG           0x03   /* 配置设备 */
#define RT_DEVICE_CTRL_SET_INT          0x10   /* 设置中断 */
#define RT_DEVICE_CTRL_CLR_INT          0x11   /* 清中断 */
#define RT_DEVICE_CTRL_GET_INT          0x12   /* 获取中断状态 */

arg 参数是一个指向任意类型数据的指针,其具体含义取决于 cmd 控制命令。arg 参数用于传递额外的信息给设备驱动程序,这些信息可能是配置参数、状态信息、数据缓冲区指针等。如在后面应用实例中,cmd的命令设置为超时,则arg就传入超时时间的指针。

1.3.5 读写设备

读取数据:

rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos,void* buffer, rt_size_t size);

参数说明:

参数描述
dev设备句柄
pos读取数据偏移量
buffer内存缓冲区指针,读取的数据将会被保存在缓冲区中
size读取数据的大小
返回——
读到数据的实际大小如果是字符设备,返回大小以字节为单位,如果是块设备,返回的大小以块为单位
0需要读取当前线程的 errno 来判断错误状态

​ 调用这个函数,会从dev设备中读取数据,并存放在buffer缓冲区中,这个缓冲区的最大长度是 sizepos 根据不同的设备类别有不同的意义。

写入设备:

rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos,const void* buffer, rt_size_t size);

参数说明:

参数描述
dev设备句柄
pos写入数据偏移量
buffer内存缓冲区指针,放置要写入的数据
size写入数据的大小
返回——
写入数据的实际大小如果是字符设备,返回大小以字节为单位;如果是块设备,返回的大小以块为单位
0需要读取当前线程的 errno 来判断错误状态

​ 调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的最大长度是 size,pos 根据不同的设备类别存在不同的意义。

1.3.6 数据收发和回调

​ 当硬件设备收到数据时,可以通过如下函数回调另一个函数来设置数据接收指示,通知上层应用线程有数据到达:

rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));

参数说明:

参数描述
dev设备句柄
rx_ind回调函数指针
返回——
RT_EOK设置成功

​ 该函数的回调函数由调用者提供。当硬件设备接收到数据时,会回调这个函数并把收到的数据长度放在 size 参数中传递给上层应用。上层应用线程应在收到指示后,立刻从设备中读取数据。

​ 在应用程序调用 rt_device_write() 写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示:

rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));

参数说明:

参数描述
dev设备句柄
tx_done回调函数指针
返回——
RT_EOK设置成功

​ 调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。

1.3.7 设备访问示例
/*
首先通过 rt_device_find()口查找到看门狗设备,获得设备句柄,然后通过 rt_device_init() 口初始化设备,通过 rt_device_control() 口设置看门狗设备溢出时间。
*/
#include <rtthread.h>
#include <rtdevice.h>

#define IWDG_DEVICE_NAME    "wdt"

static rt_device_t wdg_dev;

static void idle_hook(void)
{
    /* 在空闲线程的回调函数里喂狗 */
    rt_device_control(wdg_dev, RT_DEVICE_CTRL_WDT_KEEPALIVE, NULL);
    rt_kprintf("feed the dog!\n ");
}

int main(void)
{
    rt_err_t res = RT_EOK;
    rt_uint32_t timeout = 10;    /* 溢出时间 */

    /* 根据设备名称查找看门狗设备,获取设备句柄 */
    wdg_dev = rt_device_find(IWDG_DEVICE_NAME);
    if (!wdg_dev)
    {
        rt_kprintf("find %s failed!\n", IWDG_DEVICE_NAME);
        return RT_ERROR;
    }
    /* 初始化设备 */
    res = rt_device_init(wdg_dev);
    if (res != RT_EOK)
    {
        rt_kprintf("initialize %s failed!\n", IWDG_DEVICE_NAME);
        return res;
    }
    /* 设置看门狗溢出时间 */
    res = rt_device_control(wdg_dev, RT_DEVICE_CTRL_WDT_SET_TIMEOUT, &timeout);
    if (res != RT_EOK)
    {
        rt_kprintf("set %s timeout failed!\n", IWDG_DEVICE_NAME);
        return res;
    }
    /* 设置空闲线程回调函数 */
    rt_thread_idle_sethook(idle_hook);

    return res;
}

2 UART设备

2.1 UART简介

​ UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。

​ UART 串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,对于两个使用 UART 串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。

串口传输数据格式

  • 起始位:表示数据传输的开始,电平逻辑为 “0” 。
  • 数据位:可能值有 5、6、7、8、9,表示传输这几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
  • 奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
  • 停止位: 表示一帧数据的结束。电平逻辑为 “1”。
  • 波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200等,数值越大数据传输的越快,波特率为 115200 表示每秒钟传输 115200 位数据。

其中,起始位为"0",停止位为"1"的原因是,串口没有接收数据时,其总线上的电平默认保持高电平,当数据来到时会将电平置成低电平,告诉串口数据来了。当接收完数据时,置回高电平。

2.2 访问串口设备

​ 应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:

函数描述
rt_device_find()查找设备
rt_device_open()打开设备
rt_device_read()读取数据
rt_device_write()写入数据
rt_device_control()控制设备
rt_device_set_rx_indicate()设置接收回调函数
rt_device_set_tx_complete()设置发送完成回调函数
rt_device_close()关闭设备
2.2.1 查找串口设备

​ 应用程序根据串口设备名称获取设备句柄,进而可以操作串口设备,查找设备函数如下所示,

rt_device_t rt_device_find(const char* name);

参数说明:

参数描述
name设备名称
返回——
设备句柄查找到对应设备将返回相应的设备句柄
RT_NULL没有找到相应的设备对象

​ 一般情况下,注册到系统的串口设备名称为 uart0,uart1等,使用示例如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
2.2.2 打开串口设备

​ 打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。

rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);

参数说明:

参数描述
dev设备句柄
oflags设备模式标志
返回——
RT_EOK设备打开成功
-RT_EBUSY如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开
其他错误码设备打开失败

oflags 参数支持下列取值 (可以采用或的方式支持多种取值):

#define RT_DEVICE_FLAG_STREAM       0x040     /* 流模式      */
/* 接收模式参数 */
#define RT_DEVICE_FLAG_INT_RX       0x100     /* 中断接收模式 */
#define RT_DEVICE_FLAG_DMA_RX       0x200     /* DMA 接收模式 */
/* 发送模式参数 */
#define RT_DEVICE_FLAG_INT_TX       0x400     /* 中断发送模式 */
#define RT_DEVICE_FLAG_DMA_TX       0x800     /* DMA 发送模式 */

​ 串口收发模式只有3种:中断模式、轮询模式、DMA 模式。只能选其一,如果oflags 参数没有指定中断或者DMA模式,默认使用轮询模式。

​ DMA(Direct Memory Access)即直接存储器访问。 DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过 DMA 控制器为 RAM 与 I/O 设备开辟一条直接传送数据的通路,这就节省了 CPU 的资源来做其他操作。使用 DMA 传输可以连续获取或发送一段信息而不占用中断或延时,在通信频繁或有大段信息要传输时非常有用。

注:* RT_DEVICE_FLAG_STREAM:流模式用于向串口终端输出字符串:当输出的字符是 "\n" (对应 16 进制值为 0x0A)时,自动在前面输出一个 "\r"(对应 16 进制值为 0x0D) 做分行。

​ 流模式 RT_DEVICE_FLAG_STREAM 可以和接收发送模式参数使用或 “|” 运算符一起使用。

​ 以中断接收及轮询发送模式使用串口设备的示例如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);

​ 若串口要使用 DMA 接收模式,oflags 取值 RT_DEVICE_FLAG_DMA_RX。以DMA 接收及轮询发送模式使用串口设备的示例如下所示:

#define SAMPLE_UART_NAME       "uart2"  /* 串口设备名称 */
static rt_device_t serial;              /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

/* 以 DMA 接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);
2.2.3 控制串口设备

​ 通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。

rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);

参数说明:

参数描述
dev设备句柄
cmd命令控制字,可取值:RT_DEVICE_CTRL_CONFIG
arg控制的参数,可取类型: struct serial_configure
返回——
RT_EOK函数执行成功
-RT_ENOSYS执行失败,dev 为空
其他错误码执行失败

控制参数结构体 struct serial_configure 原型如下:

struct serial_configure
{
    rt_uint32_t baud_rate;            /* 波特率 */
    rt_uint32_t data_bits    :4;      /* 数据位 */
    rt_uint32_t stop_bits    :2;      /* 停止位 */
    rt_uint32_t parity       :2;      /* 奇偶校验位 */
    rt_uint32_t bit_order    :1;      /* 高位在前或者低位在前 */
    rt_uint32_t invert       :1;      /* 模式 */
    rt_uint32_t bufsz        :16;     /* 接收数据缓冲区大小 */
    rt_uint32_t reserved     :4;      /* 保留位 */
};

RT-Thread 提供的配置参数可取值为如下宏定义:

/* 波特率可取值 */
#define BAUD_RATE_2400                  2400
#define BAUD_RATE_4800                  4800
#define BAUD_RATE_9600                  9600
#define BAUD_RATE_19200                 19200
#define BAUD_RATE_38400                 38400
#define BAUD_RATE_57600                 57600
#define BAUD_RATE_115200                115200
#define BAUD_RATE_230400                230400
#define BAUD_RATE_460800                460800
#define BAUD_RATE_921600                921600
#define BAUD_RATE_2000000               2000000
#define BAUD_RATE_3000000               3000000
/* 数据位可取值 */
#define DATA_BITS_5                     5
#define DATA_BITS_6                     6
#define DATA_BITS_7                     7
#define DATA_BITS_8                     8
#define DATA_BITS_9                     9
/* 停止位可取值 */
#define STOP_BITS_1                     0
#define STOP_BITS_2                     1
#define STOP_BITS_3                     2
#define STOP_BITS_4                     3
/* 极性位可取值 */
#define PARITY_NONE                     0
#define PARITY_ODD                      1
#define PARITY_EVEN                     2
/* 高低位顺序可取值 */
#define BIT_ORDER_LSB                   0
#define BIT_ORDER_MSB                   1
/* 模式可取值 */
#define NRZ_NORMAL                      0     /* normal mode */
#define NRZ_INVERTED                    1     /* inverted mode */
/* 接收数据缓冲区默认大小 */
#define RT_SERIAL_RB_BUFSZ              64

​ 接收缓冲区:当串口使用中断接收模式打开时,串口驱动框架会根据RT_SERIAL_RB_BUFSZ大小开辟一块缓冲区用于保存接收到的数据,底层驱动接收到一个数据,都会在中断服务程序里面将数据放入缓冲区。

​ RT-Thread 提供的默认串口配置如下,即 RT-Thread 系统中默认每个串口设备都使用如下配置:

#define RT_SERIAL_CONFIG_DEFAULT           \
{                                          \
    BAUD_RATE_115200, /* 115200 bits/s */  \
    DATA_BITS_8,      /* 8 databits */     \
    STOP_BITS_1,      /* 1 stopbit */      \
    PARITY_NONE,      /* No parity  */     \
    BIT_ORDER_LSB,    /* LSB first sent */ \
    NRZ_NORMAL,       /* Normal mode */    \
    RT_SERIAL_RB_BUFSZ, /* Buffer size */  \
    0                                      \
}

注:默认串口配置接收数据缓冲区大小为 RT_SERIAL_RB_BUFSZ,即 64 字节。若一次性数据接收字节数很多,没有及时读取数据,那么缓冲区的数据将会被新接收到的数据覆盖,造成数据丢失,建议调大缓冲区,即通过 control 接口修改。在修改缓冲区大小时请注意,缓冲区大小无法动态改变,只有在 open 设备之前可以配置。open 设备之后,缓冲区大小不可再进行更改。但除缓冲区之外的其他参数,在 open 设备前 / 后,均可进行更改。

​ 若实际使用串口的配置参数与默认配置参数不符,则用户可以通过应用代码进行修改。修改串口配置参数,如波特率、数据位、校验位、缓冲区接收 buffsize、停止位等的示例程序如下:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;  /* 初始化配置参数 */

/* step1:查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

/* step2:修改串口配置参数 */
config.baud_rate = BAUD_RATE_9600;        //修改波特率为 9600
config.data_bits = DATA_BITS_8;           //数据位 8
config.stop_bits = STOP_BITS_1;           //停止位 1
config.bufsz     = 128;                   //修改缓冲区 buff size 为 128
config.parity    = PARITY_NONE;           //无奇偶校验位

/* step3:控制串口设备。通过控制接口传入命令控制字,与控制参数 */
rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);

/* step4:打开串口设备。以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
2.2.5 发送数据

向串口中写入数据,可以通过如下函数完成:

rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);

参数说明:

参数描述
dev设备句柄
pos写入数据偏移量,此参数串口设备未使用
buffer内存缓冲区指针,放置要写入的数据
size写入数据的大小
返回——
写入数据的实际大小如果是字符设备,返回大小以字节为单位;
0需要读取当前线程的 errno 来判断错误状态

​ 调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的大小是 size。

​ 向串口写入数据示例程序如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
char str[] = "hello RT-Thread!\r\n";
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; /* 配置参数 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
/* 发送字符串 */
rt_device_write(serial, 0, str, (sizeof(str) - 1));
2.2.6 设置发送完成回调函数

rt_device_write() 写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示 :

rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));

参数说明:

参数描述
dev设备句柄
tx_done回调函数指针
返回——
RT_EOK设置成功

​ 当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。

2.2.7 设置接收回调函数

​ 可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达 :

rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));

参数说明:

参数描述
dev设备句柄
rx_ind回调函数指针
dev设备句柄(回调函数参数)
size缓冲区数据大小(回调函数参数)
返回——
RT_EOK设置成功

该函数的回调函数由调用者提供。

  • 若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取。

  • 若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。

一般情况下接收回调函数可以发送一个信号量或者事件通知串口数据处理线程有数据到达。使用示例如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
static struct rt_semaphore rx_sem;    /* 用于接收消息的信号量 */

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);

    return RT_EOK;
}

static int uart_sample(int argc, char *argv[])
{
    serial = rt_device_find(SAMPLE_UART_NAME);

    /* 以中断接收及轮询发送模式打开串口设备 */
    rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);

    /* 初始化信号量 */
    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);

    /* 设置接收回调函数 */
    rt_device_set_rx_indicate(serial, uart_input);
}
2.2.8 接收数据

可调用如下函数读取串口接收到的数据:

rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);

参数说明:

参数描述
dev设备句柄
pos读取数据偏移量,此参数串口设备未使用,可设置为-1
buffer缓冲区指针,读取的数据将会被保存在缓冲区中
size读取数据的大小
返回——
读到数据的实际大小如果是字符设备,返回大小以字节为单位
0需要读取当前线程的 errno 来判断错误状态

读取数据偏移量 pos 针对字符设备无效,此参数主要用于块设备中。

串口使用中断接收模式并配合接收回调函数的使用示例如下所示:

static rt_device_t serial;                /* 串口设备句柄 */
static struct rt_semaphore rx_sem;    /* 用于接收消息的信号量 */

/* 接收数据的线程 */
static void serial_thread_entry(void *parameter)
{
    char ch;

    while (1)
    {
        /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
        while (rt_device_read(serial, -1, &ch, 1) != 1)
        {
            /* 阻塞等待接收信号量,等到信号量后再次读取数据 */
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }
        /* 读取到的数据通过串口错位输出 */
        ch = ch + 1;
        rt_device_write(serial, 0, &ch, 1);
    }
}
2.2.9 关闭串口设备

当应用程序完成串口操作后,可以关闭串口设备。

rt_err_t rt_device_close(rt_device_t dev);

参数说明:

参数描述
dev设备句柄
返回——
RT_EOK关闭设备成功
-RT_ERROR设备已经完全关闭,不能重复关闭设备
其他错误码关闭设备失败

​ 关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。

2.3 串口设备使用示例

中断回调接收字符,DMA回调接收一段数据

2.3.1 中断接收及轮询发送

示例代码的主要步骤如下所示:

  1. 首先查找串口设备获取设备句柄。
  2. 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
  3. 设置串口设备的接收回调函数,之后发送字符串,并创建读取数据线程。
  • 读取数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。
  • 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。

运行序列图如下图所示:

串口中断接收及轮询发送序列图

​ 在这个示例中,信号量的作用就是建立了串口外设和线程的桥梁,串口接收到数据触发中断回调函数,回调函数释放信号量唤醒线程接收。

/*
 * 程序清单:这是一个 串口 设备使用例程
 * 例程导出了 uart_sample 命令到控制终端
 * 命令调用格式:uart_sample uart2
 * 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 * 程序功能:通过串口输出字符串"hello RT-Thread!",然后错位输出输入的字符
*/

#include <rtthread.h>

#define SAMPLE_UART_NAME       "uart2"

/* 用于接收消息的信号量 */
static struct rt_semaphore rx_sem;
static rt_device_t serial;

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);

    return RT_EOK;
}

static void serial_thread_entry(void *parameter)
{
    char ch;

    while (1)
    {
        /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
        while (rt_device_read(serial, -1, &ch, 1) != 1)
        {
            /* 阻塞等待接收信号量,等到信号量后再次读取数据 */
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }
        /* 读取到的数据通过串口错位输出 */
        ch = ch + 1;
        rt_device_write(serial, 0, &ch, 1);
    }
}

static int uart_sample(int argc, char *argv[])
{
    rt_err_t ret = RT_EOK;
    char uart_name[RT_NAME_MAX];
    char str[] = "hello RT-Thread!\r\n";

    if (argc == 2)
    {
        rt_strncpy(uart_name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
    }

    /* 查找系统中的串口设备 */
    serial = rt_device_find(uart_name);
    if (!serial)
    {
        rt_kprintf("find %s failed!\n", uart_name);
        return RT_ERROR;
    }

    /* 初始化信号量 */
    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
    /* 以中断接收及轮询发送模式打开串口设备 */
    rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
    /* 设置接收回调函数 */
    rt_device_set_rx_indicate(serial, uart_input);
    /* 发送字符串 */
    rt_device_write(serial, 0, str, (sizeof(str) - 1));

    /* 创建 serial 线程 */
    rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    /* 创建成功则启动线程 */
    if (thread != RT_NULL)
    {
        rt_thread_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(uart_sample, uart device sample);
2.3.2 DMA接收及轮询发送

​ 当串口接收到一批数据后会调用接收回调函数,接收回调函数会把此时缓冲区的数据大小通过消息队列发送给等待的数据处理线程。线程获取到消息后被激活,并读取数据。一般情况下 DMA 接收模式会结合 DMA 接收完成中断和串口空闲中断完成数据接收。

  • 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。

运行序列图如下图所示:

串口DMA接收及轮询发送序列图

/*
 * 程序清单:这是一个串口设备 DMA 接收使用例程
 * 例程导出了 uart_dma_sample 命令到控制终端
 * 命令调用格式:uart_dma_sample uart3
 * 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 * 程序功能:通过串口输出字符串"hello RT-Thread!",并通过串口输出接收到的数据,然后打印接收到的数据。
*/

#include <rtthread.h>

#define SAMPLE_UART_NAME       "uart3"      /* 串口设备名称 */

/* 串口接收消息结构*/
struct rx_msg
{
    rt_device_t dev;
    rt_size_t size;
};
/* 串口设备句柄 */
static rt_device_t serial;
/* 消息队列控制块 */
static struct rt_messagequeue rx_mq;

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    struct rx_msg msg;
    rt_err_t result;
    msg.dev = dev;
    msg.size = size;

    result = rt_mq_send(&rx_mq, &msg, sizeof(msg));
    if ( result == -RT_EFULL)
    {
        /* 消息队列满 */
        rt_kprintf("message queue full!\n");
    }
    return result;
}

static void serial_thread_entry(void *parameter)
{
    struct rx_msg msg;
    rt_err_t result;
    rt_uint32_t rx_length;
    static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];

    while (1)
    {
        rt_memset(&msg, 0, sizeof(msg));
        /* 从消息队列中读取消息*/
        result = rt_mq_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
        if (result > 0)
        {
            /* 从串口读取数据*/
            rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size);
            rx_buffer[rx_length] = '\0';
            /* 通过串口设备 serial 输出读取到的消息 */
            rt_device_write(serial, 0, rx_buffer, rx_length);
            /* 打印数据 */
            rt_kprintf("%s\n",rx_buffer);
        }
    }
}

static int uart_dma_sample(int argc, char *argv[])
{
    rt_err_t ret = RT_EOK;
    char uart_name[RT_NAME_MAX];
    static char msg_pool[256];
    char str[] = "hello RT-Thread!\r\n";

    if (argc == 2)
    {
        rt_strncpy(uart_name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
    }

    /* 查找串口设备 */
    serial = rt_device_find(uart_name);
    if (!serial)
    {
        rt_kprintf("find %s failed!\n", uart_name);
        return RT_ERROR;
    }

    /* 初始化消息队列 */
    rt_mq_init(&rx_mq, "rx_mq",
               msg_pool,                 /* 存放消息的缓冲区 */
               sizeof(struct rx_msg),    /* 一条消息的最大长度 */
               sizeof(msg_pool),         /* 存放消息的缓冲区大小 */
               RT_IPC_FLAG_FIFO);        /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

    /* 以 DMA 接收及轮询发送方式打开串口设备 */
    rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);
    /* 设置接收回调函数 */
    rt_device_set_rx_indicate(serial, uart_input);
    /* 发送字符串 */
    rt_device_write(serial, 0, str, (sizeof(str) - 1));

    /* 创建 serial 线程 */
    rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    /* 创建成功则启动线程 */
    if (thread != RT_NULL)
    {
        rt_thread_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(uart_dma_sample, uart device dma sample);
  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值