多线程-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号。

所以,在创建线程之前,我们需要先定义:funcattr

 // 线程函数一:红灯
 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_tattr_bits\\ 不关心 ~
void*cb_mem线程控制块起始地址,默认NULL为动态分配
uint32_tcb_size线程控制块大小,默认NULL0
void*stack_mem线程栈起始地址,默认NULL为使用定长内存池
uint32_tstack_size线程栈大小,默认NULL0
osPriority_tpriority线程优先级,默认osPriorityNormal
TZ__ModuleId_ttz_module安全区标志,默认0
uint32_treserved保留位,必须是0

一共9个,好多参数啊~,其实不必担心,我们常用的可能就namePriority,其余的默认就好。所以,就有如下这种简易表示法。

// 线程一属性结构体
 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

【创建线程】

搞定了funcattr,就可以创建线程啦~,我们直接在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 instancesjoinable 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灯的例子,让大家更直观理解多线程是如何工作和实现的。

重点

  • 线程的概念
  • 线程的创建,使用,和删除
  • 多实例和可接合线程

希望对大家有所帮助,有不懂或者纠错的欢迎留言~,谢谢!

参考资料

☞官方tutorial

以上是关于多线程-RGB_LED闪烁灯的主要内容,如果未能解决你的问题,请参考以下文章

传感网灯闪烁代码

arduino两个led灯交替闪烁

FPGA -- 实验一:闪烁灯

单片机c51,9个灯同时闪烁

第三节:累计主循环次数使LED灯闪烁。

编制一个循环闪烁灯的程序,每次其中某个灯闪烁10次后,转到下一个闪烁10次,循环不止(用汇编语言编制)