多线程-RGB_LED闪烁灯
Posted Albert Nie
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程-RGB_LED闪烁灯相关的知识,希望对你有一定的参考价值。
开始学习线程之前,你可能需要复习:
如果准备就绪,那么,进入正题!
线程
线程概念
In CMSIS-RTOS2, the basic unit of execution is a “Thread”. A Thread is very similar to a ‘C’ procedure but has some very fundamental (根本的)differences. An RTOS program is made up of a number of threads, which are controlled by the RTOS scheduler.
线程是程序执行的基本单元。我们可以将程序分解为多个功能相对独立的子任务(类似C函数模块化调用),然后为每个子任务分配一个线程,而RTOS负责子任务之间的调度,从而实现多线程的"并行",提高程序的实时性和效率。
unsigned int procedure(void){ // C function
...
return (ch);
}
void thread(void* arg){ // thread
while(1){
...
}
}
线程调度
问题来了,scheduler
是如何进行线程调度的?。其实很简单,scheduler以SysTick
产生的周期性中断作为时基,给每个线程分配一个时间片(相当于分配多少个sysTick),当某个线程的时间片用完了,就阻塞该线程,而调度另一个就绪线程执行。那SysTick
是什么?我们知道微处理器上面有很多时序电路,所以我们经常会用到石英晶振来产生稳定的时钟信号。一个时钟周期就是一个SysTick
,它一般是一个很精准的固定量,比如对于8MHz的晶振,其时钟周期是
1
/
8
M
H
z
=
0.125
u
s
1/8MHz = 0.125us
1/8MHz=0.125us,即SysTick = 0.125us
。
线程管理
When a thread is created, it is also allocated its own thread ID. This is a variable which acts as a handle for each thread and is used when we want to manage the activity of the thread.
每个线程都有一个id,我们可以通过这个id来管理线程。
osThreadId id1,id2,id3; // 线程id
线程切换
当线程切换时,kernel会将当前线程的所有变量状态保存到该线程的栈中,同时将该线程的运行信息保存到线程控制块中,然后执行另外一个线程。
线程通常有三种状态:运行态、就绪态、阻塞态。
大致了解这么多吧~,详细可以看操作系统相关的书籍。
RTX Thread API
操作系统的一大优点就是:抽象。它将底层的硬件资源抽象成一组接口,然后用户便可以直接在这些接口上进行开发。这样既提高了开发效率,也降低了开发门槛。CMSIS-RTX5 提供了线程的创建、删除等接口,主要包括如下:
osTreadId_t
:定义线程的ID
osThreadAttr_t
: 定义线程属性结构体
osThreadNew
: 创建线程
osThreadExit
:线程退出
实验:RGB灯闪烁
功能:通过三个线程分别控制RGB LED 灯的三个灯的亮灭,从而实现颜色的合成。
硬件:stm32f103zet6
软件:keil MKD5.23
,CMSIS-RTX5
准备
- (一)系统移植
- (二)修改配置
【系统移植】请参考:RTX系统移植,具体根据硬件平台,比如GPIO等。
【修改配置】配置用户线程数量,keil MDK默认1个用户线程,所以我们需要修改,笔者修改为10。另外,有需要也可以配置线程的栈空间大小。
配置线程
- 创建线程ID
- 创建线程函数和线程属性结构体
- 创建线程
【创建线程ID】
创建三个线程,分别表示红、绿、蓝三个线程ID。
osThreadId_t red_led,green_led,blue_led;
【创建线程函数和线程属性结构体】
这里以线程red_led
为例,其他两个类似。首先,我们来看线程创建函数osThreadNew
,其函数原型为:
osThreadId_t osThreadNew(osThreadFunc_t func,void* argument,const osThreadAttr_t* attr)
该函数有三个参数:
func
: 线程名字argument
: 线程函数的参数attr
: 线程的属性配置,包括线程函数名,栈大小,优先级等等。
返回值
osThreadId_t
: 表示该线程的ID号。
所以,在创建线程之前,我们需要先定义:func
和attr
// 线程函数一:红灯
void redLight(void* arg){
while(1){
LED1(ON); // 声明在bsp_led.h中
osDelay(100); // RTX 提供的延时函数,这里延时100个SysTick
LED1(OFF);
osDelay(100);
}
}
线程函数就是一般的函数形式,唯一注意的是两点:函数内包含while(1)
死循环,以及参数为void*
。重点说一下线程属性结构体attr
。
类型 | 数据成员 | 描述 |
---|---|---|
const char* | name | 线程名 |
uint32_t | attr_bits | \\ 不关心 ~ |
void* | cb_mem | 线程控制块起始地址,默认NULL 为动态分配 |
uint32_t | cb_size | 线程控制块大小,默认NULL 为0 |
void* | stack_mem | 线程栈起始地址,默认NULL 为使用定长内存池 |
uint32_t | stack_size | 线程栈大小,默认NULL 为0 |
osPriority_t | priority | 线程优先级,默认osPriorityNormal |
TZ__ModuleId_t | tz_module | 安全区标志,默认0 |
uint32_t | reserved | 保留位,必须是0 |
一共9个,好多参数啊~,其实不必担心,我们常用的可能就name
和Priority
,其余的默认就好。所以,就有如下这种简易表示法。
// 线程一属性结构体
static const osThreadAttr_t threadAttr_LED1 = {
.name = "redLight",
};
不过要使用这种方式,keil必须支持c99
,如下:
这里再谈一下app_main
,这个线程是CMSIS-RTX5
提供的,相当于一个启动线程。它的任务就是创建用户需要的线程,完成使命后就退出。
void app_main (void *arg) {
...
}
// 线程结构体参数,使用默认
static const osThreadAttr_t threadAttr_app_main = {
"app_main",
NULL,
NULL,
NULL,
NULL,
NULL,
osPriorityNormal,
NULL,
NULL
};
这里的线程属性结构体用的直接初始化的形式,主要是为了知道有这种方式,其实不用写也没关系,创建线程时直接传入NULL
,使用默认值即可。
osThreadNew(app_main, NULL, NULL); // 直接传NULL
【创建线程】
搞定了func
和attr
,就可以创建线程啦~,我们直接在app_main
中创建三个线程。
void app_main (void *arg) {
// initialize
LED_GPIO_Config();
// create three threads
red_led = osThreadNew(redLight,NULL,&threadAttr_LED1); // 也可用NULL默认属性结构体
green_led = osThreadNew(greenLight,NULL,&threadAttr_LED2);
blue_led = osThreadNew(blueLight,NULL,&threadAttr_LED3);
// complete task,exit!
osThreadExit();
}
完整代码:
main.c
/*----------------------------------------------------------------------------
* CMSIS-RTOS 'main' function template
*---------------------------------------------------------------------------*/
#include "RTE_Components.h"
#include CMSIS_device_header
#include "cmsis_os2.h"
#include "bsp_led.h"
#ifdef RTE_Compiler_EventRecorder
#include "EventRecorder.h"
#endif
// Thread Information
osThreadId_t red_led,green_led,blue_led;
/*----------------------------------------------------------------------------
* Task thread
*---------------------------------------------------------------------------*/
// 红灯
void redLight(void* arg){
while(1){
LED1(ON);
osDelay(100);
LED1(OFF);
osDelay(100);
}
}
static const osThreadAttr_t threadAttr_LED1 = {
.name = "redLight",
};
// 绿灯
void greenLight(void* arg){
while(1){
LED2(ON);
osDelay(100);
LED2(OFF);
osDelay(100);
}
}
static const osThreadAttr_t threadAttr_LED2 = {
.name = "greenLight",
};
// 蓝灯
void blueLight(void* arg){
while(1){
LED3(ON);
osDelay(100);
LED3(OFF);
osDelay(100);
}
}
static const osThreadAttr_t threadAttr_LED3 = {
.name = "blueLight",
};
/*----------------------------------------------------------------------------
* main thread
*---------------------------------------------------------------------------*/
void app_main (void *arg) {
// initialize
LED_GPIO_Config();
// create three threads
red_led = osThreadNew(redLight,NULL,&threadAttr_LED1); // 也可用NULL默认属性结构体
green_led = osThreadNew(greenLight,NULL,&threadAttr_LED2);
blue_led = osThreadNew(blueLight,NULL,&threadAttr_LED3);
// exit
osThreadExit();
}
// 线程参数,使用默认
static const osThreadAttr_t threadAttr_app_main = {
"app_main",
NULL,
NULL,
NULL,
NULL,
NULL,
osPriorityNormal,
NULL,
NULL
};
int main (void) {
// System Initialization
SystemCoreClockUpdate();
#ifdef RTE_Compiler_EventRecorder
// Initialize and start Event Recorder
EventRecorderInitialize(EventRecordError, 1U);
#endif
osKernelInitialize(); // Initialize CMSIS-RTOS
osThreadNew(app_main, NULL, &threadAttr_app_main); // Create application main thread
osKernelStart(); // Start thread execution
for (;;) {}
}
RGB_LED电路图
有些小伙伴,可能不理解RGB_LED,这里贴个图。
其实就是三个LED灯,集成在一起,管脚配置就不需要我介绍了吧,各位这么聪明~
编译运行
编译配置请看:RTX系统移植
我们来预料实现效果:我们创建了三个线程,分别点闪烁RGB_LED等的红绿蓝三个灯,由于线程是“并行”的,那么根据三原色的合成,最后RGB_LED灯应该是白光闪烁。
看看具体效果:
看见那个白闪闪的大灯了嘛,还不错哦,和预期一样。说明操作系统确实跑起来了,且三个线程正常工作~
其他
线程还有其他知识点,比如mutiple instances
和joinable threads
。
multiple instances(多个实例)
我们知道,线程创建函数为:
osThreadId_t osThreadNew(osThreadFunc_t func,void* argument,const osThreadAttr_t* attr);
多个实例的本质就是基于同一个线程函数func
来创建多个线程实例,RTX根据参数argument
来分配不同的线程ID。
比如,上面LED闪烁灯实验,三个线程函数都基本相同,只是LED号不同而已,所以我们可以将LED号传给参数argument,从而只需一个线程函数,便实现三个线程实例。
// base func
void Light(void* arg)
{
while(1){
switch((int)arg)
{
case 1:{
LED1(ON);
osDelay(100);
LED1(OFF);
osDelay(100);
break;
}
case 2:{
LED2(ON);
osDelay(100);
LED2(OFF);
osDelay(100);
break;
}
case 3:{
LED3(ON);
osDelay(100);
LED3(OFF);
osDelay(100);
break;
}
}
}
}
线程属性结构体不变,这时创建三个线程可以这样:
// 定义参数指针
#define red (void*)1
#define green (void*)2
#define blue (void*)3
// create three threads
red_led = osThreadNew(Light,red,&threadAttr_LED1); // 也可用NULL默认属性结构体
green_led = osThreadNew(Light,green,&threadAttr_LED2);
blue_led = osThreadNew(Light,blue,&threadAttr_LED3);
编译后,下载到开发板运行效果是一样的。
joinable Thread(可接合线程)
额,先不管翻译的恰当与否,最重要的是,啥是可接合线程?
a second thread can join it by calling osThreadJoin(). This will cause the second thread to deschedule and remain in a waiting state until the thread which has been joined is terminated.
比如你正在看电视(线程A),这时候你妹来了,说要用你的电脑处理一个word文档(线程B)。由于是你妹,你当然选择接受了(线程B is joinable)。当然,这时候你就不能继续看电视了(线程A等待),直到你妹处理完word(线程B)结束,然后你才可以继续看电视(线程A恢复)。这里可接合线程就是指的线程B。可接合线程的设置在线程属性结构体中设置,也就是之前我们不关心的那个属性attr_bits
。
static const osThreadAttr_t ThreadAttr_thread_joinable = {
.attr_bits = osThreadJoinable,
};
当在某个线程中调用osThreadJoin()
后,该线程就会等待,直到可接合线程退出为止。
osThreadJoin(<joinable_thread_handle>); // 函数原型
结合前面的例子,我们可以让red light 变成 joinable thread,然后在light线程中调用osThreadJoin()
。
首先,这里设置red light的属性,使其 joinable
static const osThreadAttr_t threadAttr_LED1 = {
.name = "redLight",
.attr_bits = osThreadJoinable,
};
然后还需要单独对线程函数做一下改变
// 红灯
void redLight(void* arg)
{
while(1){
LED1(ON);
osDelay(100);
LED1(OFF);
osDelay(100);
}
}
// base func
void Light(void* arg)
{
while(1){
switch((int)arg)
{
default:
red_led = osThreadNew(redLight,red,&threadAttr_LED1);
osThreadJoin(red_led); // 调用osThreadJoin()
case 2:{ // 红 + 绿
LED2(ON);
osDelay(100);
LED2(OFF);
osDelay(100);
break;
}
case 3:{ // 红 + 蓝
LED3(ON);
osDelay(100);
LED3(OFF);
osDelay(100);
break;
}
}
}
}
最后修改线程创建函数
// create three threads
green_led = osThreadNew(Light,green,&threadAttr_LED2);
blue_led = osThreadNew(Light,blue,&threadAttr_LED3);
joinable threads主要用于临时创建一个线程,用来处理一些事情,然后结束任务,主线程继续执行。
嗯大致就是这些~
小结
本文主要针对线程的概念,创建和编写做了简要介绍,最后基于一个具体的多线程RGB_LED灯的例子,让大家更直观理解多线程是如何工作和实现的。
重点
- 线程的概念
- 线程的创建,使用,和删除
- 多实例和可接合线程
希望对大家有所帮助,有不懂或者纠错的欢迎留言~,谢谢!
参考资料
以上是关于多线程-RGB_LED闪烁灯的主要内容,如果未能解决你的问题,请参考以下文章