小程序开发者工具下载,网站推广与搜索引擎优化,制作网站的收获体会,做实体店优惠券的网站硬件#xff1a;STM32F103ZET6、ST-LINK、usb转串口工具、4个LED灯、1个蜂鸣器、4个1k电阻、2个按键、面包板、杜邦线 文章目录 前言一、线程管理接口介绍二、任务#xff1a;使用多线程的方式同时实现led闪烁和按键控制喇叭#xff08;扫描法#xff09;1. RT-Thread相关接… 硬件STM32F103ZET6、ST-LINK、usb转串口工具、4个LED灯、1个蜂鸣器、4个1k电阻、2个按键、面包板、杜邦线 文章目录 前言一、线程管理接口介绍二、任务使用多线程的方式同时实现led闪烁和按键控制喇叭扫描法1. RT-Thread相关接口函数1创建和删除线程2初始化和脱离线程3启动线程4线程睡眠 2. 代码实现1led灯闪烁功能模块实现2按键控制喇叭功能模块实现3main()程序设计 3. 程序测试 总结 前言
本章主要讲线程的工作机制和管理方法通过实例讲解如何使用多线程完成多任务开发。 一、线程管理接口介绍
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; /* 用户数据 */
};
init_priority 是线程创建时指定的线程优先级在线程运行过程当中是不会被改变的除非用户执行线程控制函数进行手动调整线程优先级。cleanup 会在线程退出时被空闲线程回调一次以执行用户设置的清理现场等工作。user_data 可由用户挂接一些数据信息到线程控制块中以提供一种类似线程私有数据的实现方式。
线程的创建、启动、删除等操作都与线程控制块相关RT-Thread提供线程管理和控制的一些函数具体如下
rt_thread_create() 创建线程rt_thread_delete() 删除线程rt_thread_init() 初始化线程rt_thread_detach() 脱离线程rt_thread_startup() 启动线程rt_thread_self() 活得当前正在运行的线程返回值位rt_thread_t类型线程的句柄rt_thread_yield() 使线程让出处理器资源rt_thread_sleep() 使线程睡眠指定tick调用该函数线程会被挂起rt_thread_delay() 使线程延迟指定tick调用该函数线程会被挂起rt_thread_mdelay() 使线程睡眠指定毫秒调用该函数线程会被挂起rt_thread_suspend() 挂起线程rt_thread_resume() 恢复线程rt_thread_control() 控制线程
二、任务使用多线程的方式同时实现led闪烁和按键控制喇叭扫描法
任务描述在main线程中实现LED灯闪烁的功能同时创建一个线程单独实现按键控制喇叭的功能按键的识别使用扫描法。另外要求可以通过命令控制灯的闪烁模式。硬件电路如下
1. RT-Thread相关接口函数
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 方式对齐。
线程创建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 宏定义已经定义了。
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);静态线程的线程句柄或者说线程控制块指针、线程栈由用户提供。
静态线程是指线程控制块、线程运行栈一般都设置为全局变量在编译时就被确定、被分配处理内核不负责动态分配内存空间。
需要注意的是用户提供的栈首地址需做系统对齐例如 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()初始化的线程控制块。**同样线程本身不应调用这个接口脱离线程本身。
3启动线程
创建初始化的线程状态处于初始状态并未进入就绪线程的调度队列我们可以在线程初始化 / 创建成功后调用下面的函数接口让该线程进入就绪态
rt_err_t rt_thread_startup(rt_thread_t thread);当调用这个函数时将把线程的状态更改为就绪状态并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高将立刻切换到这个线程。线程启动接口 rt_thread_startup() 的参数和返回值见下表
参数描述thread线程句柄返回RT_EOK线程启动成功-RT_ERROR 线程启动失败
4线程睡眠
在实际应用中我们有时需要让运行的当前线程延迟一段时间在指定的时间到达后重新运行这就叫做 “线程睡眠”。线程睡眠可使用以下三个函数接口
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 操作成功
2. 代码实现
本任务主要实现两个功能灯闪烁、按键控制喇叭。这个两个功能都可以采用循环结构来设计但是在第3章的任务中发现单线程中采用循环结构同时实现这两个功能在代码实现上比较困难所以本任务分两个线程来实现一个线程实现灯闪烁另一个线程实现按键扫描并控制喇叭开关。 构建两个小功能模块分别是灯闪烁和按键控制喇叭。
1led灯闪烁功能模块实现
car_led.h文件主要用于实现变量类型的定义和函数的声明代码如下
#ifndef APPLICATIONS_CAR_LED_H_
#define APPLICATIONS_CAR_LED_H_/* 枚举方式定义车灯闪烁模式 */
enum led_mode {LED_MODE_STOP0, //停止闪烁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 rtthread.h
#include rtdevice.h
#include drv_common.h#define DBG_TAG LED //定义日志打印标签
#define DBG_LVL DBG_LOG //定义日志打印级别
/* 包含日志打印头文件。注意以上2个宏定义一定要在这个头文件前定义才有效*/
#include rtdbg.h
#include stdlib.h //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 LedModLED_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(argn2){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);
2按键控制喇叭功能模块实现
扫描法进行按键扫描。 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 rtthread.h
#include rtdevice.h
#include drv_common.h
#define DBG_TAG BEEP //定义日志打印标签
#define DBG_LVL DBG_LOG //定义日志打印级别
#include rtdbg.h //以上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_LOWrt_pin_read(KeyBeep)){//按键按下rt_thread_mdelay(20);//延时去抖if(PIN_LOWrt_pin_read(KeyBeep))rt_pin_write(Beep, PIN_LOW);//蜂鸣器响}elsert_pin_write(Beep, PIN_HIGH);//否则蜂鸣器不响rt_thread_mdelay(100);//每0.1秒进行一次按键扫描线程要有让出CPU的时候}return RT_EOK;
}注本任务两个线程都采用了无线循环模式为了不让线程陷入死循环操作两个线程都在每次循环后通过调用延时函数来让出CPU使用权。但这种让出方式还不是最好的方式通常可以通过让线程灯带信号量的方式使线程挂起从而达到在线程空闲时让出CPU的效果具体参考后面线程同步相关章节。
3main()程序设计
main()程序主要实现线程的创建在本任务中我们分别采用动态方法和静态方法来创建线程主要是方便了解两种方法的使用具体代码如下
#include rtthread.h
#define DBG_TAG main
#define DBG_LVL DBG_LOG
#include rtdbg.h
#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目录中构建并下载程序到开发板。
3. 程序测试
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线程了 总结
本文讲了线程创建的应用