多线程-RGB_LED闪烁灯

Posted 玖道

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闪烁灯的主要内容,如果未能解决你的问题,请参考以下文章

多线程中的event,用于多线程的协调

java多线程模拟红绿灯案例

我要把斐讯k2恢复出厂设置,按了rest之后红灯灯一直在急闪,恢复出厂设置也没成功。

单片机应用,一个键多次按下,能够依次控制红黄蓝绿四个灯控制第一次按下亮红灯然后绿灯依次四下

STM32F103R6红灯闪并报警,绿灯亮不报警

模拟红绿灯交替指示编程思路