0%

第二章——变量和基本类型

说在前面

C++这个板块是我在学习了C之后才学的,因此我只会记录一些重要的或者在C中没有的东西

C++板块是基于C++primer的方案学习的

1、基本内置类型

  • bool类型取值只为真(1)和假(0)

  • 当一个算术表达式中既有无符号数(unsigned)又有int型时,int型的值会转换成无符号数。且值大于0时会正常显示,小于零时会异常显示(当int为4字节时,值为2^32 + 计算结果(负数))也就是4294967295加上

    (-n)。

指定字面值类型:

  • 字符和字符串字面值
前缀 含义 代表类型
u Unicode16字符 char16_t
U Unicode32字符 char32_t
L 宽字符(2字节) wchar_t
u8 Unicode8字符 char
  • 整型字面值
后缀 最小匹配类型
u or U unsigned
l or L long
ll or LL long long
  • 浮点型
后缀 代表的类型
f or F float
l or L long double

2、变量

  • 一种初始化字符串的方法

    1
    std::string a("test");

    字符串变量名a,内容为test

3、对象

通常情况下,对象是指一块能存储数据并具有某种类型的内存空间

关于初始化的问题

在学C的时候我自己也是一直把初始化理解成赋值,但是也一直感觉怪怪的,特别是对于指针变量的初始化,感觉明显是不符合“赋值”这一操作的,今天在阅读书籍时书中也指出了初始化≠赋值,但是具体的区别目前我也不能讲的很明白,学到后面如果搞懂了再说吧。

  • 定义在函数体内部的变量将不被初始化。

列表初始化

用花括号来初始化变量,在C++中以下四种初始化都是正确的:

1
2
3
4
int a =1;
int a ={1};
int a(1);
int a{1};

4、标识符

虽然感觉很怪异,但是int _=1;是合法的。

5、复合类型变量

引用

为对象起另外一个名字,同时引用必须被初始化。

1
2
3
int a(1);
int &b=a;
b=2;//等同于 a=2;
  • 引用并非对象,可以只是理解为一个已存在变量的别名。
  • 一旦使用引用,引用对象将和引用的初始化变量绑定,无法更改,因此引用必须初始化。
  • 引用只能绑定在对象上,而不能与某个字面值或某个计算结果绑定

指向指针的引用:

1
2
3
4
5
int i=1;
int *p;
int *&r=p; //注意这里是*&r
r=&i;
*r=0;

指针

规则与C基本一样,不再赘述。

6、const 限定符

  • 作用:定义一个变量后,它的值不能再被改变

    因此,const的对象必须初始化。

  • 当多个文件出现了同名的const变量时,视作分别定义了独立的变量,也就是说默认情况下const对象只在本文件内有效。(当然也可以使用extern关键字,具体可见一些关键字 | 小董的BLOG (gitee.io)

    对于使用extern,如果要在多个文件之间共享const对象,则在定义该对象时也必须添加extern

对const对象的引用

1
2
3
const int a=10;
const int &b1=a;//对
int &b2=a;//错,引用的类型必须与引用对象类型一致

假设第三行操作合法,则可以通过b2改变a的值,这显然是不正确的

  • 若初始化引用的对象为表达式,只要该表达式的结果能转化成引用的类型即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int a=10;
    const int &b=a;
    //允许将const int型引用绑定到普通的int对象上(其他普通变量也可以视情况转化)

    //因此,可以有以下操作
    int a=10;
    int &b1=a;
    const int &b2=a;
    //可以通过b1改变a的值,但不允许通过b2改变a的值

指向const对象的指针和const指针

1
2
3
4
5
//指向const对象的指针
const int a=10,b=20;
int *p=&a;
*p=b; //错误,不可修改p指向的对象的值
p=&b; //合法,并没有更改a的值,只是更改了一般指针p的值

说明一下,上述中的int *p=&a;也可以改为const int *p=&a;且二者完全一致,原因与后文中的顶/底层const有关

1
2
3
4
5
//const指针
int a=10,b=20;
int *const p=&a;
p=&b; //错误,const指针一旦初始化便会一直指向初始化时的对象
*p=b; //正确,因为指针指向的值为普通int型,可以改变

将指针本身定义为const型(不太好用语言描述感觉),即一旦初始化完成,该指针便无法再指向其他对象。

下面还是一个const指针的说明:

1
2
3
4
//const指针
const int a=10,b=20;
const int *const p=&a;
//此时p和*p都是无法改变的

当使用const指针指向的值也是const类型时,const指针本身必须再次使用一次const,因为要保持类型的一致

顶/底层const

对于指针,指针本身是一个对象,而它也指向了另一个对象,因此用顶层const表示指针本身是个const类型,用底层const表示指针所指的对象时一个const型。一般来说,顶层const可以表示任意对象。

  • 当const + 类型 时,意为使这个类型的值为常量,如

    1
    const int *p;

    定义一个指针变量,该变量所指的值为int型,因此该值(*p)不可被修改,但是p可以被修改。

  • 当const + 变量 时,意为使该变量为常量,如:

  • ```c++
    int* const p;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    指针变量p为常量,因此其指向的地址无法被改变,但是地址中的值(*p)可以被改变。

    ## 7、constexpr类型

    * 常量表达式:指值不会改变**并且在编译过程就能得到结果的表达式**;字面值、用常量表达式初始化的const对象也是常量表达式。

    ```c
    const int i = 10; //字面值是常量表达式
    const int j = i+1; //j是常量表达式
    int k = i; //k不是const对象,所以k不是是常量表达式
    const int m = get_val(); //m的值直到运行时才能获得,所以m不是常量表达是

    从上面我们可以看出,使用const进行声明时,我们需要人为的验证赋给const对象的初始值是不是常量表达是,在复杂系统中,有时候很难分辨,而使用constexpr进行声明时,可以由编译器来验证变量是不是常量表达式

1
2
3
4
5
6
7
8
9
10
11
12
constexpr int func(int n) {
return n;
}
int main()
{
int n=10;
const int m=10;
constexpr int i = func1(10);//正确,i是一个常量表达式
constexpr int j = func1(n);//错误,n为变量,不是字面值
constexpr int k = func1(m+1);//正确,k是一个常量表达式
return 0;
}

const和constexpr的区别

  • 对于修饰对象来说,const并未区分出编译期常量和运行期常量,constexpr限定在了编译期常量
  • 在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
  • constexpr将它所定义的对象置为顶层const
1
2
3
4
const int*p1=nullptr;  			//p1是一个指向常量的指针
constexpr int*p2=nullptr; //p2是一个指向整数的常量指针
constexpr const int*p3=nullptr;
//p3是一个指向常量的常量指针,由于constexpr只对指针有效,需要再加一个底层const才可以限制指针指向的对象

8、处理类型

(1)类型别名

类型别名是一个名字是某种类型的同义词。有两种办法可用于定义类型别名:

  • 使用关键字typedef,详见一些关键字 | 小董的BLOG

  • 使用别名声明:

    1
    using INT =int;

类型别名与指针、常量

有以下代码:

1
2
3
4
5
using abc=char*;
char a=10;
const abc p=&a;//此处等同于char *const p=&a;而不是 const char *p=&a;
//也就是说此处是常量指针,而不是指向常量的指针,因此:
*p=20;//合法

感觉可以直接理解为这里的const为顶层const型

(2)auto类型说明符

这里auto和C中用法有所出入,在C++11新标准中,auto用于让编译器通过初始值来推算变量的类型

1
2
int a=1,b=2;
auto c=a+b;

此时c的类型就为int

  • auto定义的变量必须有初始值
  • 参与初始化的各对象类型必须相同,否则会报错

在复合类型与常量参与的情况下:

1
2
3
4
5
const int a=1; &a1=a;
auto b=a; //b为int型
auto c=a1;//c为int型
auto d=&a;//d为指向常量的指针
const auto e=a; //顶层const需专门声明
  • auto一般会忽略顶层const

  • 对一个常量对象取地址是一种底层const

(3)decltype类型指示符

当我们定义一个变量时希望用一个表达式返回的类型定义该变量,但是又不想用该表达式的结果的值初始化变量,为满足这一要求,C++11新标准引入了decltype,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式的类型,却不实际计算它的值。

1
2
int a=1;
decltype(a) b=2;

decltype()内的参数可以为变量、函数、甚至引用

decltype和引用:

1
2
3
4
5
int a=1,&a1=a,*p=&a;
int b=2;
decltype(a1) c=b;//c为int&,也就是引用类型
decltype(a1+0) d=1; //d为int型,原因是a1+0的结果为int型
decltype(*p) e=b; //*p的值按道理来说是1,书上讲这里讲为int&型,也就是与c一样

当decltype传入的参数添加上一对以上的括号后,例如decltype((a)),结果永远为引用

输入与输出

  • iostream库

    在C++中没有专门定义的输入输出(IO)语句,而是使用了一个全面的标准库来提供IO机制等功能,也就是iostream库。而在iostream库中包含了两个基础类型

    • istream(in stream):输入流
    • ostream(out stream):输出流
  • 流的

  • IO对象

    在标准库中定义了4个IO对象,分别为1个istream型,3个ostream型:

    • instream型:
      • cin(发音see in):标准输入对象
    • ostream型:
      • cout:标准输出对象
      • cerr:输出警告和错误信息
      • clog:输出程序运行时的一般性消息

输入输出例程:

1
2
3
4
5
6
7
8
#include "iostream"
int main()
{
int a,b;
std::cin>>a>>b;//输入
std::cout<<a+b<<std::endl;//输出
return 0;
}

下面针对这个例程讲解:

  • 前缀std::

    这个前缀指出cin、cout、endl都是定义在名为std的命名空间中的。使用命名空间可以避免与用户定义的名字冲突。且标准库的所有名字都是定义在std中的。

  • 从输入流读取数据:

    1
    2
    int a,b;
    std::cin>>a>>b;//输入
    • 输入运算符(>>)

      该运算符接收两个运算对象:

      • 左侧:必须为一个ostream型的对象

      • 右侧:存放输入的值的变量

      返回值(计算结果): ostream型的对象(左侧运算对象)

      也就是说下面几种写法是一个意思:

      1
      2
      3
      4
      5
      6
      std::cin>>a>>b;
      /****************************/
      (std::cin>>a)>>b;
      /****************************/
      std:cin>>a;
      std:cin>>b;
  • 向输出流写入数据:

    1
    std::cout<<a+b<<std::endl;//输出
    • 输出运算符(<<)

      该运算符接收两个运算对象:

      • 左侧:必须为一个istream型的对象
      • 右侧:要打印的值

      返回值(计算结果): 写入给定值的ostream型的对象

      也就是说下面几种写法是一个意思:

      1
      2
      3
      4
      5
      6
      std::cout<<a+b<<std::endl;
      /****************************/
      (std::cout<<a+b)<<std::endl;
      /****************************/
      std::cout<<a+b;
      std::cout<<std::endl;
    • endl操纵符 (小写L,不是1)

      endl也是被定义在std中的一个特殊值,写入endl的效果是结束当前行(类似c中的\n),并将与设备相关联的缓冲区中的内容刷到设备中。

      缓冲刷新操作可以保证目前为止程序中所产生的所有输出都真正的写入到了输出流中,而不仅停留在内存中等待写入流。

    • 关于数据缓冲区

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      #include "iostream"
      int main()
      {
      int a,b=1,c;
      while(std::cin>>a)
      {
      //while (getchar()!= '\n');
      std::cout<<"times:"<<b++<<std::endl;
      }
      return 0;
      }

      当使用一个istream对象为条件时,其效果就是检测输入流当前的状态,如果流是有效的,此时istream对象使条件为真,当遇到以下情况时为假:

      • 无效输入:例如上方输入不为int型(与a类型不符合)
      • 当输入流遇到文件结束符,对于window系统,输入文件结束符的方法是ctrl+z+\n

      在上方程序中(当注释掉第七行时),当输入4 ‘\0’ 5时,time也会对应输出2次,即1 ‘\n’ 2 。这是因为当输入一次性输入两个数时,第一个数存进a,第二个数会被存在数据缓冲区中等待下一次读取输入流的操作;若想清除缓冲区(或者说是清零),只需添加第7行即可。

说在前面

本来是打算在FreeRTOS在CubeMX中的使用就把这些内容给讲了的,但是学习的过程中发现网络上很多教程都存在着一些差异,所以为了更系统地学习,同时也方便日后查阅,就单独开一篇文章总结一下相关内容

注:本文所有例程均经过测试且可以正常运行

1、队列

FreeRTOS中的队列是一种用于实现【任务与任务】,【任务与中断】以及【中断与任务】之间的通信机制。此外,任务从队列读数据或者写入数据到队列时,都可能被阻塞。这个特性使得任务可以被设计成基于事件驱动的运行模式,大大提高了CPU的执行效率。队列是实现FreeRTOS中其他特性的基础组件,像软件定时器,信号量,互斥锁都是基于队列而实现的。

在操作系统里面,直接使用全局变量传输数据十分危险,看似正常运行,但不知道啥时候就会因为寄存器或者内存等等原因引起崩溃,所以引入消息,队列的概念。

队列的基本特性

队列是一种FIFO操作的数据结构,入队操作就是把一个新的元素放进队尾(tail),出队操作就是从队头(front)取出一个元素。FreeRTOS中也支持把一个元素放到队头的操作,这个操作会覆盖之前队头的元素。

入队策略

队列在设计的时候,主要有两种元素入队存储策略:Queue by copy 和 Queue by reference。FreeRTOS的队列使用的是Queue by copy存储策略,考虑到这种策略实现起来更加简单,灵活,且安全。

  • Queue by copy:数据入队的时候,队列中存储的是此数据的一份拷贝
  • Queue by reference:数据队列的时候,队列中存储的是数据的指针,而非数据本身

入队堵塞

一个任务在尝试写入数据到队列时,可以指定一个阻塞时间,即任务在等待队列有空余空间(非满)可写前最长等待阻塞时间。当队列中有空间可写入时,任务会自动从阻塞态转换为就绪态。如果在等待时间内队列一直是满的,则等待时间到期后,任务也会自动从阻塞态转换为就绪态,但是它会返回一个写入失败的结果。

队列可能有多个writerTask,所以在等待队列有空闲空间时,可能会有多个任务阻塞。当队列有空闲空间可写时,调度器会从所有阻塞的任务中选取优先级最高的那个任务让其进入到就绪态如果最高优先级的不止一个,则让等待最久的那个进入到就绪态。

出队堵塞

一个任务在尝试从队列中读取数据时,可以指定一个阻塞时间,即任务在等待队列有元素可读前最长等待阻塞时间。当队列中有元素可读时,任务会自动从阻塞态转换为就绪态。如果在等待时间内队列一直没有数据可读,则等待时间到期后,任务也会自动从阻塞态转换为就绪态,但是它会返回一个读取失败的结果

队列可能有多个reader Task,所以在等待队列有数据可读时,可能会有多个任务阻塞。当队列有数据可读时,调度器会从所有阻塞的任务中选取优先级最高的那个任务让其进入到就绪态。如果最高优先级的不止一个,则让等待最久的那个进入到就绪态。

相关API函数

  • 队列创建(若使用cubemx则只用点点点,不用再用函数创建,所有创建类函数同理)

    使用xQueueCreate()内核函数来创建一个队列。队列的1存储空间从FreeRTOS heap中分配。在使用xQueueCreate()创建队列时,如果FreeRTOS heap中没有足够的存储空间分配给当前队列,则函数返回NULL。如果创建成功,则返回队列的句柄(QueueHandle_t类型)

函数 QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 uxQueueLength:指定队列的长度,即最多可以存放的元素个数
uxItemSize:队列中存储的元素的大小(占用的字节数)
返回值 返回NULL代表创建失败,没有足够的堆空间来创建当前队列;创建成功则返回队列的句柄
  • 元素入队

    使用xQueueSendToBack()函数来向队尾存放一个元素,使用xQueueSend()函数来向队首存放一个元素,两个函数用法一样

函数 BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
参数 xQueue:目标队列的句柄
pvItemToQueue:入队元素的指针。队列将存储此指针指向的数据的备份
xTicksToWait:指定等待队列有空间可以容纳新元素入队的最长等待(阻塞)时间
返回值 当元素成功入队时返回pdPASS,因为队列满而无法入队时返回errQUEUE_FULL(超时后)
  • 查询队列元素个数

    使用内核函数uxQueueMessagesWaiting()来获取队列中有多少个元素

函数 UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
参数 xQueue:队列的句柄
返回值 队列中的元素个数,返回0代表队列为空

按键例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void task1(void const * argument) //低优先级任务
{
BaseType_t result;
uint8_t dat[]="666\r\n";
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(key_1_GPIO_Port,key_1_Pin == GPIO_PIN_SET)) //检测按键
{
result= xQueueSendToBack(myQueue01Handle,dat,portMAX_DELAY); //将元素dat放至队尾
if(result==pdPASS) //放置成功返回pdPASS
{
HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET);//点亮绿灯
}
}
osDelay(5);
}
}

void task2(void const * argument) //高优先级任务
{
BaseType_t result;
uint8_t datbuf[10]; //随便定义个缓冲区就可以,用于存放取出的元素
/* Infinite loop */
for(;;)
{
result=xQueueReceive(myQueue01Handle,datbuf,portMAX_DELAY);//取出元素放入datbuf中
if(result==pdPASS)//读取成功返回pdPASS
{
HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_RESET);//点亮红灯
}
osDelay(10);
}
}

2、任务通知

在网上找了很多博客对于任务通知介绍的函数都不一样,我自己也实验了很多,就把我试验过的可行的函数都总结一遍,可以根据具体的需要选择相关函数(其实函数功能基本差不多,但是原理不一样)

每个RTOS任务都有一个32位的通知值,任务创建时,这个值被初始化为0。RTOS任务通知相当于直接向任务发送一个事件,接收到通知的任务可以解除阻塞状态,前提是这个阻塞事件是因等待通知而引起的。发送通知的同时,也可以可选的改变接收任务的通知值。

相对于用前必须分别创建队列、二进制信号量、计数信号量或事件组的情况,使用任务通知显然更灵活。更好的是,相比于使用信号量解除任务阻塞,使用任务通知可以快45%、使用更少的RAM。

下面介绍的函数大类都可以实现任务通知的功能,可以根据需要选择

一、xTask函数

发送函数(1)

函数 BaseType_t xTaskNotify( TaskHandle_txTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction);
参数 xTaskToNotify:被通知的任务句柄
ulValue: 通知更新值
eAction:枚举类型,指明更新通知值的方法
返回值 成功则返回pdPASS

eAction具体说明:

枚举成员 作用
eNoAction 发送通知但不更新通知值,意味着ulValue未使用
eSetBits 被通知任务值赋值按位或赋值ulValue
elncrement 被通知任务的通知值++
eSetValueWithOverwrite 被通知任务值赋值为ulValue
eSetValueWithoutOverwrite 如果被通知的任务还没取走上一个通知的情况下,又向被通知任务发送一个新的通知,则新通知值被丢弃,且xTaskNotify()会返回pdFALSE

发送函数(2)

函数 BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify );
参数 xTaskToNotify:被通知的任务句柄
返回值 成功则返回pdPASS

其实这是一个宏,本质上相当于xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement )在这种情况下,应该使用API函数ulTaskNotifyTake()来等待通知,而不应该使用API函数xTaskNotifyWait()

此函数不可以在中断服务例程中调用,中断保护等价函数为vTaskNotifyGiveFromISR()

接收函数(1)

注意,此处讲到的接收函数应与上面讲到的发送函数配对,例如:使用发送函数(1),接收通知也要使用接收函数(1)

函数 BaseType_t xTaskNotifyWait( uint32_tulBits ToClearOnEntry,
uint32_tulBits ToClearOnExit,
uint32_t* pulNotificationValue,
TickType_t xTicksToWait );
参数 见下方
返回值 成功则返回pdPASS

关于参数:

  • ulBitsToClearOnEntry:在使用通知之前,先将任务的通知值与参数ulBitsToClearOnEntry的按位取反值按位与操作。设置参数ulBitsToClearOnEntry为0xFFFFFFFF(ULONG_MAX),表示清零任务通知值。
    • 但是在实际调试过程中感觉给0x00和0xFFFFFFFF在使用上没有区别,所以就给0xFFFFFFFF就可以
  • *ulBitsToClearOnExit:在函数xTaskNotifyWait()退出前,将任务的通知值与参数ulBitsToClearOnExit的按位取反值按位与操作。设置参数ulBitsToClearOnExit为0xFFFFFFFF(ULONG_MAX),表示清零任务通知值。
    • 使用同上,默认0xFFFFFFFF就可以
  • pulNotificationValue:用于向外回传任务的通知值。这个通知值在参数ulBitsToClearOnExit起作用前将通知值拷贝到pulNotificationValue中(*需额外定义一个缓冲区)。如果不需要返回任务的通知值,这里设置成NULL。
  • xTicksToWait:因等待通知而进入阻塞状态的最大时间。时间单位为系统节拍周期。宏pdMS_TO_TICKS用于将指定的毫秒时间转化为相应的系统节拍数。

接收函数(2)

函数 uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );
参数 xClearCountOnExit:如果该参数为pdFALSE,则API函数xTaskNotifyTake()退出前,将任务的通知值减1;如果该参数设置为pdTRUE,则API函数xTaskNotifyTake()退出前,将任务通知值清零。
xTicksToWait:因等待通知而进入阻塞状态的最大时间
返回值 成功则返回pdPASS

上例程!老规矩按键+led

发送函数(1)+接收函数(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#define event1 0x0001<<1
#define event2 0x0001<<2
void task1(void const * argument) //低优先级
{
BaseType_t result;
for(;;)
{
if(HAL_GPIO_ReadPin(key_1_GPIO_Port,key_1_Pin) == GPIO_PIN_SET)//按键检测
{
result= xTaskNotify(task2Handle,event1,4);//直接数字代替枚举成员
if(result==pdPASS)
{
HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET);//绿灯
}
}
if(HAL_GPIO_ReadPin(key_2_GPIO_Port,key_2_Pin) == GPIO_PIN_SET)//按键检测
{
result= xTaskNotify(task2Handle,event2,4);//直接数字代替枚举成员
if(result==pdPASS)
{
HAL_GPIO_WritePin(led_b_GPIO_Port,led_b_Pin,GPIO_PIN_RESET);//蓝灯
}
}
osDelay(100);
}
}

void task2(void const * argument)//高优先级
{
BaseType_t result;
uint32_t ulNotifiedValue; //存放任务通知值的缓冲区
for(;;)
{
result=xTaskNotifyWait(0xFFFFFFFF, 0xFFFFFFFF,&ulNotifiedValue, portMAX_DELAY);
if(result==pdPASS)
{
HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_RESET);//红灯
}
if(ulNotifiedValue&event1)
{
HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(led_b_GPIO_Port,led_b_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_SET);//全灭
}
if(ulNotifiedValue&event2)
{
HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_RESET);
HAL_GPIO_WritePin(led_b_GPIO_Port,led_b_Pin,GPIO_PIN_RESET);
HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET);//全亮
}
/*
整体逻辑:
按键1按下->绿灯亮->红灯亮->全灭
按键2按下->蓝灯亮->红灯亮->全灭

这里对于通知值的判断可以让用户知道是哪个任务通知函数触发了这次任务接收,感觉非常好用!!!
*/
osDelay(10);
}
}

总结:

发送函数(2)+接收函数(2)就不单独写了,基本同理,只是接收函数(2)无法对通知值进行判断,但是整体更方便快捷

二、os函数

向指定任务发送任务通知

函数 int32_t osSignalSet (osThreadId thread_id, int32_t signal)
参数 thread_id: 接收通知的任务ID
signal:任务通知值(按位操作数字)
返回值 错误码

等待任务通知

函数 osEvent osSignalWait (int32_t signals, uint32_t millisec)
参数 signals: 接收完成后等待被清零的数据位(0x0001\ 0x0002=0x003)
millisec: 等待超时时间,单位为系统节拍周期
返回值 错误码

例程,按键+led

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define event1 0x0001
void task1(void const * argument) //低优先级任务
{
/* USER CODE BEGIN led_G */

/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(key_1_GPIO_Port,key_1_Pin == GPIO_PIN_SET))
{
osSignalSet(task2Handle,event1);
}
osDelay(10);
}
/* USER CODE END led_G */
}

void task2(void const * argument) //高优先级任务
{
/* USER CODE BEGIN led_B */
osEvent event;
/* Infinite loop */
for(;;)
{
event = osSignalWait(event1,osWaitForever);
if(event.status == osEventSignal)
{
HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET);
}
}
}

这里task2可以不用额外在循环中添加堵塞,因为wait函数本身自带堵塞,其堵塞的解除是在osSignalSet函数的作用之后的瞬间,也就是说在执行完osSignalSet(task2Handle,event1)后task2会直接抢占task1

而xTask函数不同于os函数,xTask函数是在task1执行完后才会进入task2,这点请务必记住,二者通知原理不同,所以根据使用情况选择使用xTask函数还是os函数

3、信号量

信号量同样是一种任务之间交互的函数,但是就我按照网上教程使用过后感觉。。。不如队列和任务通知,可能是我目前的一些工程量还没到使用到信号量的程度?而且在使用过程中也遇到了一些迷惑的地方,所以目前不准备深究,等到实在要用的时候再说吧😋,感兴趣的同学可以去(19条消息) FreeRTOS系列第19篇—-FreeRTOS信号量_研究是为了理解的博客-CSDN博客_freertos 信号看看,讲的很详细了可以说,我大部分内容也是跟着这个学的。