硬件:STM32F103ZET6、ST-LINK、usb转串口工具、4个LED灯、1个蜂鸣器、4个1k电阻、2个按键、面包板、杜邦线
本章主要讲线程的工作机制和管理方法,通过实例讲解如何使用多线程完成多任务开发。
RT-Thread用线程控制块来描述和管理一个线程,一个线程对应一个线程控制块。线程控制块由结构体struct rt_thread表示,它用于存放线程的所有一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等,详细定义如下:
/* 线程控制块 */
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; /* 用户数据 */
};
线程的创建、启动、删除等操作,都与线程控制块相关,RT-Thread提供线程管理和控制的一些函数,具体如下:
任务描述:在main线程中实现LED灯闪烁的功能,同时创建一个线程单独实现按键控制喇叭的功能,按键的识别使用扫描法。另外,要求可以通过命令控制灯的闪烁模式。硬件电路如下:
一个线程要成为可执行的对象,就必须由操作系统的内核来为它创建一个线程。线程可以动态创建和删除。
① 创建线程
可以通过如下的接口创建一个动态线程:
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 方式对齐。
线程创建rt_thread_create() 的参数和返回值见下表:
参数 | 描述 |
---|---|
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);
调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。
实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;
而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
线程删除 rt_thread_delete() 接口的参数和返回值见下表:
参数 | 描述 |
---|---|
thread | 要删除的线程句柄 |
返回 | RT_EOK 删除线程成功,-RT_ERROR 删除线程失败 |
注:rt_thread_create() 和 rt_thread_delete() 函数仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)。
静态方法使用线程
① 初始化线程
线程的初始化可以使用下面的函数接口完成,来初始化静态线程对象:
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);
静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。
静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。
需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)。线程初始化接口 rt_thread_init() 的参数和返回值见下表:
参数 | 描述 |
---|---|
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_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:
rt_err_t rt_thread_detach (rt_thread_t thread);
线程脱离接口 rt_thread_detach() 的参数和返回值见下表:
参数 | 描述 |
---|---|
thread | 线程句柄,它应该是由rt_thread_init进行初始化的线程句柄 |
返回 | RT_EOK: 线程脱离成功,-RT_ERROR: 线程脱离失败 |
**这个接口函数与 rt_thread_delete() 函数相对应, rt_thread_delete() 函数操作的对象是 rt_thread_create()创建的句柄,而 rt_thread_detach()函数操作的对象是 rt_thread_init()初始化的线程控制块。**同样线程本身不应调用这个接口脱离线程本身。
创建(初始化)的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程初始化 / 创建成功后调用下面的函数接口让该线程进入就绪态:
rt_err_t rt_thread_startup(rt_thread_t thread);
当调用这个函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程。线程启动接口 rt_thread_startup() 的参数和返回值见下表:
参数 | 描述 |
---|---|
thread | 线程句柄 |
返回 | RT_EOK:线程启动成功,-RT_ERROR 线程启动失败 |
在实际应用中,我们有时需要让运行的当前线程延迟一段时间,在指定的时间到达后重新运行,这就叫做 “线程睡眠”。线程睡眠可使用以下三个函数接口:
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);
这三个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。
这个函数接受一个参数,该参数指定了线程的休眠时间。线程睡眠接口 rt_thread_sleep/delay/mdelay() 的参数和返回值见下表:
参数 | 描述 |
---|---|
tick/ms | 线程睡眠的时间:sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ;mdelay 的传入参数 ms 以 1ms 为单位; |
返回 | RT_EOK: 操作成功 |
本任务主要实现两个功能:灯闪烁、按键控制喇叭。这个两个功能都可以采用循环结构来设计,但是在第3章的任务中发现,单线程中采用循环结构同时实现这两个功能,在代码实现上比较困难,所以本任务分两个线程来实现:一个线程实现灯闪烁,另一个线程实现按键扫描并控制喇叭开关。
构建两个小功能模块,分别是灯闪烁和按键控制喇叭。
car_led.h文件主要用于实现变量类型的定义和函数的声明,代码如下:
#ifndef APPLICATIONS_CAR_LED_H_
#define APPLICATIONS_CAR_LED_H_
/* 枚举方式定义车灯闪烁模式 */
enum led_mode {
LED_MODE_STOP=0, //停止闪烁
LED_MODE_Double, //双闪
LED_MODE_LEFT, //左灯闪烁
LED_MODE_RIGHT //右灯闪烁
};
void led_thread_entry(); //线程入口函数声明
void led_set_mode(enum led_mode m); //车灯闪烁模式设置声明
#endif /* APPLICATIONS_CAR_LED_H_ */
car_led.c是具体的函数实现,本文件除了实现闪灯线程的入口函数以外,还按任务要求,实现闪灯模式设置接口,并把设置接口导出到msh命令列表中,导出后,用户就可以通过在终端输入命令来改变闪烁的模式,代码如下:
#include
#include
#include "drv_common.h"
#define DBG_TAG "LED" //定义日志打印标签
#define DBG_LVL DBG_LOG //定义日志打印级别
/* 包含日志打印头文件。注意,以上2个宏定义一定要在这个头文件前定义才有效*/
#include
#include //atoi()函数需要包含的头文件
#include "car_led.h"
/* 定义左右转向灯的控制引脚 */
#define LedLeft GET_PIN(D, 8)
#define LedRight GET_PIN(D, 9)
/* 定义开灯/关灯接口 */
#define LedOn(pin) rt_pin_write(pin, PIN_LOW)
#define LedOff(pin) rt_pin_write(pin, PIN_HIGH)
enum led_mode LedMod=LED_MODE_STOP; //车灯默认为不闪烁
/* 设置闪灯模式的接口 */
void led_set_mode(enum led_mode m)
{
LedMod = m;
}
/* 车灯控制线程入口函数定义 */
void led_thread_entry()
{
/*设置车灯控制引脚模式为输出模式*/
rt_pin_mode(LedLeft, PIN_MODE_OUTPUT);
rt_pin_mode(LedRight, PIN_MODE_OUTPUT);
while(1){
switch(LedMod)//判断模式
{
case LED_MODE_STOP://停止闪烁
LedOff(LedLeft);
LedOff(LedRight);
break;
case LED_MODE_Double://双闪
LedOn(LedLeft);
LedOn(LedRight);
rt_thread_mdelay(500);
LedOff(LedLeft);
LedOff(LedRight);
break;
case LED_MODE_LEFT://左灯闪
LedOff(LedRight);
LedOn(LedLeft);
rt_thread_mdelay(250);
LedOff(LedLeft);
break;
case LED_MODE_RIGHT://右灯闪
LedOff(LedLeft);
LedOn(LedRight);
rt_thread_mdelay(250);
LedOff(LedRight);
break;
default:
LOG_D("mode error\n");
}
rt_thread_mdelay(250);//此处延时主要是为了让出CPU,不能让线程一直占用CPU
}
}
/*msh命令接口*/
void ledmode(int argn, char *argv[])
{
if(argn<2){
LOG_W("ledmode #mode");
return ;
}
led_set_mode(atoi(argv[1]));//通过atoi函数把字符变量转为整型变量
}
/* 导出到 msh 命令列表中,MSH_CMD_EXPORT(command,desc),其中command为函数名字,desc为用于描述命令用法或功能的字符串,可以自定 */
MSH_CMD_EXPORT(ledmode, set led flash mode);
扫描法进行按键扫描。
car_beep.h代码如下:
#ifndef APPLICATIONS_CAR_BEEP_H_
#define APPLICATIONS_CAR_BEEP_H_
void beep_thread_entry();//按键扫描并控制蜂鸣器的接口声明
#endif /* APPLICATIONS_CAR_BEEP_H_ */
car_beep.c
#include
#include
#include "drv_common.h"
#define DBG_TAG "BEEP" //定义日志打印标签
#define DBG_LVL DBG_LOG //定义日志打印级别
#include //以上2个宏定义一定要在这个头文件前定义才有效
#define KeyBeep GET_PIN(A, 0) //按键引脚定义
#define Beep GET_PIN(A, 5) //蜂鸣器引脚定义
/*按键扫描并控制蜂鸣器的接口实现*/
int beep_thread_entry(void)
{
/* 把蜂鸣器引脚设置为推拉模式 */
rt_pin_mode(Beep, PIN_MODE_OUTPUT);
/* 把按键引脚设置为上拉输入模式 */
rt_pin_mode(KeyBeep, PIN_MODE_INPUT_PULLUP);
while (1)
{
if(PIN_LOW==rt_pin_read(KeyBeep)){//按键按下
rt_thread_mdelay(20);//延时去抖
if(PIN_LOW==rt_pin_read(KeyBeep))
rt_pin_write(Beep, PIN_LOW);//蜂鸣器响
}
else
rt_pin_write(Beep, PIN_HIGH);//否则,蜂鸣器不响
rt_thread_mdelay(100);//每0.1秒进行一次按键扫描,线程要有让出CPU的时候
}
return RT_EOK;
}
注:本任务,两个线程都采用了无线循环模式,为了不让线程陷入死循环操作,两个线程都在每次循环后通过调用延时函数来让出CPU使用权。但这种让出方式还不是最好的方式,通常可以通过让线程灯带信号量的方式使线程挂起,从而达到在线程空闲时让出CPU的效果,具体参考后面线程同步相关章节。
main()程序主要实现线程的创建,在本任务中,我们分别采用动态方法和静态方法来创建线程,主要是方便了解两种方法的使用,具体代码如下:
#include
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include
#include "car_led.h" //包含LED控制模块头文件
#include "car_beep.h" //包含蜂鸣器控制模块头文件
#define THREAD_STACK_SIZE 1024 //定义线程栈大小
#define THREAD_PRIORITY 20 //定义线程优先级
#define THREAD_TIMESLICE 10 //定义线程时间片
/* 栈首地址必须系统对齐 */
ALIGN(RT_ALIGN_SIZE)
static char beep_stack[THREAD_STACK_SIZE]; //定义栈空间
static struct rt_thread beepThread; //静态方式定义beep线程控制块
rt_thread_t TidLed = RT_NULL; //动态方式定义LED线程句柄
int main(void)
{
int ret;
/* 动态方式创建线程 */
TidLed = rt_thread_create("LED",
led_thread_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (TidLed != RT_NULL)//判断线程是否成功创建
rt_thread_startup(TidLed);//成功则启动线程
else {//否则打印日志并即出
LOG_D("can not create LED thread!");
return -1;
}
/* 采用静态方式初始化线程 */
ret = rt_thread_init(&beepThread,
"BEEP",
beep_thread_entry,
RT_NULL,
&beep_stack[0],
sizeof(beep_stack),
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (ret == RT_EOK) //判断线程是否成功创建
rt_thread_startup(&beepThread); //成功则启动线程
else { //否则打印日志并即出
LOG_D("can not init beep thread!");
return -1;
}
return RT_EOK;
}
void stop_led_thread(void)//删除led线程命令
{
rt_thread_delete(TidLed);//动态创建的线程用delete删除
}
void stop_beep_thread(void) //删除beep线程命令
{
rt_thread_detach(&beepThread);//静态初始化的线程用detach删除
}
/* 导出到 msh 命令列表中*/
MSH_CMD_EXPORT(stop_led_thread, delete led thread);
MSH_CMD_EXPORT(stop_beep_thread, delete beep thread);
复制以上代码文件到applicastions目录中,构建并下载程序到开发板。
(1)系统启动后,两个LED等常亮无闪烁,终端输入ps命令查看系统线程情况,发现系统中新增了2个线程,分别使BEEP和LED,如图所示
(2)在终端中输入help命令查看系统命令支持情况,发现系统新增了程序中导出的3个命令ledmode、stop_led_thread、stop_beep_thread,如下图所示:
(3)终端输入ledmode 1、ledmode 2、ledmode 3等命令后,观察到车灯闪烁分别变为“双闪”“左灯闪”“右灯闪”。
(4)按下按键后,喇叭发出响声。
(5)松开按键后,喇叭停止发声。
(6)一直按住按键不松开,喇叭发出响声的同时,车灯继续闪烁。
(7)通过终端输入停止车灯闪烁命令stop_led_thread,观察车灯不再闪烁;通过ps命令查看系统线程情况,发现系统中不存在LED线程了
(8)通过终端输入停止喇叭响声命令stop_beep_thread,按下按键时喇叭不响;通过ps命令查看系统线程情况,发现系统中不存在BEEP线程了
本文讲了线程创建的应用