目录

  • 线程
  • RTX Thread API
  • 实验:RGB灯闪烁
    • 准备
    • 配置线程
    • 编译运行
  • 其他
    • multiple instances(多个实例)
    • joinable Thread(可接合线程)
  • 小结
  • 参考资料

开始学习线程之前,你可能需要复习:

  • ☞为什么使用RTOS?

  • ☞RTX系统移植

如果准备就绪,那么,进入正题!

线程

线程概念

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){        // threadwhile(1){...  }
}

线程调度

问题来了,scheduler是如何进行线程调度的?。其实很简单,scheduler以SysTick产生的周期性中断作为时基,给每个线程分配一个时间片(相当于分配多少个sysTick),当某个线程的时间片用完了,就阻塞该线程,而调度另一个就绪线程执行。那SysTick是什么?我们知道微处理器上面有很多时序电路,所以我们经常会用到石英晶振来产生稳定的时钟信号。一个时钟周期就是一个SysTick,它一般是一个很精准的固定量,比如对于8MHz的晶振,其时钟周期是1/8MHz=0.125us1/8MHz = 0.125us1/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个SysTickLED1(OFF);osDelay(100);}}

线程函数就是一般的函数形式,唯一注意的是两点:函数内包含while(1)死循环,以及参数为void*。重点说一下线程属性结构体attr

类型 数据成员 描述
const char* name 线程名
uint32_t attr_bits \ 不关心 ~
void* cb_mem 线程控制块起始地址,默认NULL为动态分配
uint32_t cb_size 线程控制块大小,默认NULL0
void* stack_mem 线程栈起始地址,默认NULL为使用定长内存池
uint32_t stack_size 线程栈大小,默认NULL0
osPriority_t priority 线程优先级,默认osPriorityNormal
TZ__ModuleId_t tz_module 安全区标志,默认0
uint32_t reserved 保留位,必须是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) {// initializeLED_GPIO_Config();// create three threadsred_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) {// initializeLED_GPIO_Config();// create three threadsred_led = osThreadNew(redLight,NULL,&threadAttr_LED1);  // 也可用NULL默认属性结构体green_led = osThreadNew(greenLight,NULL,&threadAttr_LED2);blue_led = osThreadNew(blueLight,NULL,&threadAttr_LED3);// exitosThreadExit();
}// 线程参数,使用默认
static const osThreadAttr_t threadAttr_app_main = {  "app_main",NULL,NULL,NULL,NULL,NULL,osPriorityNormal,NULL,NULL
};int main (void) {// System InitializationSystemCoreClockUpdate();
#ifdef RTE_Compiler_EventRecorder// Initialize and start Event RecorderEventRecorderInitialize(EventRecordError, 1U);
#endifosKernelInitialize();                 // Initialize CMSIS-RTOSosThreadNew(app_main, NULL, &threadAttr_app_main);    // Create application main threadosKernelStart();                      // Start thread executionfor (;;) {}
}

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 funcvoid 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 funcvoid 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闪烁灯相关推荐

  1. STM32系统定时器SysTick(只能向下递减)延时闪烁灯

    参考:stm32 系统定时器 SysTick 作者:点灯小哥 发布时间: 2021-03-10 13:46:00 网址:https://blog.csdn.net/weixin_46016743/ar ...

  2. FPGA项目一:1位闪烁灯设计

    文章目录 项目一:1位闪烁灯设计 第一节 项目背景 第二节 设计目标 第三节 设计实现 3.1 顶层信号 3.2 信号设计 3.3 信号定义 第四节 综合和上板 4.1 新建工程 4.2 综合 4.3 ...

  3. ESP32开发二_LED闪烁灯

    LED闪烁灯 交流QQ: 1048272975             QQ交流群: 636564526 控制LED灯的亮灭是MCU开发中一个最简单的应用功能,实现这个应用功能包含了MCU开发中工程的 ...

  4. 单片机八灯交替闪烁c语言代码,单片机闪烁灯汇编语言源代码大全(四款闪烁灯的汇编语言源代码)...

    单片机闪烁灯汇编语言源代码大全(一) 1.单片机AT89C51的P2口接8个发光二极管,让这8个发光二极管显示闪烁功能,即八灯亮2S,熄灭3S,如此循环. 参考程序 ORG 0000H start: ...

  5. 最基础硬件学习 | 简单闪烁灯制作

    一.查询资料 1.学习三极管基本工作原理 重点:三极管的放大特性,Ib与Ic的关系 参考:三极管的工作原理(详细.通俗易懂.图文并茂)http://www.360doc.com/content/17/ ...

  6. 单片机c语言小灯闪烁,单片机c语言闪烁灯程序.doc

    单片机c语言闪烁灯程序 1. 闪烁灯 1. 实验任务 如图4.1.1 所示:在P1.0 端口上接一个发光二极管L1,使L1 在不停地一亮 灭,一亮一灭的时间间隔为0.2 秒. 2. 电路原理图 图4. ...

  7. 单片机实例1——闪烁灯(硬件电路图+汇编程序+C语言程序)

    1. 闪烁灯 1. 实验任务 如图4.1.1所示:在P1.0端口上接一个发光二极管L1,使L1在不停地一亮一灭,一亮一灭的时间间隔为0.2秒. 2. 电路原理图 图4.1.1 3. 系统板上硬件连线 ...

  8. 51单片机------闪烁灯(实验报告)

    实验一:LED闪烁灯 一.实验目的 掌握51单片机开发板的使用步骤: 掌握51单片机开发板所需软件的安装过程: 以LED灯闪烁为例子,掌握软件KEIL4的使用方法. 二.实验设备 实验仪器设备: 计算 ...

  9. 单片机c语言每隔1m闪烁一次,单片机c语言闪烁灯程序

    1.闪烁灯 1.实验任务 如图所示:在端口上接一个发光二极管L1,使L1 在不停地一亮 灭,一亮一灭的时间间隔为秒. 2.电路原理图 图系统板上硬件连线把"单片机系统"区域中的端口 ...

最新文章

  1. Ubuntu14搭建配置青岛大学OJ系统
  2. 西安交大计算机考研分数线2020院线,西安交通大学2020研究生复试分数线预计4月中旬左右公布...
  3. SQL Server数据库大型应用解决方案总结【转】
  4. lamp里php 的升级,lamp架构之升级php版本
  5. iPhone SE 3 5G版发布在即 旧款将降价到千元出头
  6. 游标中的static参数
  7. 洛谷 P1631 序列合并
  8. linux去除内容重复行,实例详细说明linux下去除重复行命令uniq
  9. LintCode 1350: Excel Sheet Column Title
  10. ubuntu 下的限速软件 wondershaper 以及 命令行测试网速
  11. 《Xenogears》(异度装甲)隐含的原型与密码
  12. 给控件做数字签名——摘录自阿泰博客
  13. 程序在单片机里是如何运行的?
  14. Redis源码学习(13),t_set.c 学习(一),sadd,srem 命令学习
  15. 打开fiddler 电脑无法上网问题
  16. 彻底理解numpy中的axis
  17. Linux内核编程接口函数
  18. 使用MySQL的聊天室_聊天室phpmysql(一)
  19. 图书管理系统(C文件读、存)
  20. 如何用python爬取e-hentai的图片

热门文章

  1. cad两直线相交画圆弧,CAD 两直线,怎么用圆弧连接?
  2. 大数据分析师·人才培养·高薪起航
  3. 《Unity Shader入门精要》彩图版免费分享~~~~~
  4. OpenVINO之链接库
  5. 怎样关闭qq位置定位服务器,手机qq怎么关闭定位
  6. MAC常用进入文件夹剪贴粘贴打开文件夹快捷键
  7. [竞赛01]2021CCF BDCI新闻摘要自动生成Baseline-T5模型
  8. 南京工资个税计算机,南京个税计算器_南京税后月薪|工资计算器_南京个人所得税查询 - Tax518...
  9. 好斗or炒作?甲骨文“撕咬”过的那些对手 - 爱上英语题库系统|郭雄飞
  10. 微信小程序有哪些优点和价值