FreeRTOS_信号量之二值信号量

目录

[1. 信号量简介](#1. 信号量简介)

[2. 二值信号量](#2. 二值信号量)

[2.1 二值信号量简介](#2.1 二值信号量简介)

[2.1.1 二值信号量无效](#2.1.1 二值信号量无效)

[2.1.2 中断释放信号量](#2.1.2 中断释放信号量)

[2.1.3 任务获取信号量成功](#2.1.3 任务获取信号量成功)

[2.1.4 任务再次进入阻塞态](#2.1.4 任务再次进入阻塞态)

[2.2 创建二值信号量](#2.2 创建二值信号量)

[2.2.1 vSemaphoreCreateBinary()](#2.2.1 vSemaphoreCreateBinary())

[2.2.2 xSemaphoreCreateBinary()](#2.2.2 xSemaphoreCreateBinary())

[2.2.3 xSemaphoreCreateBinaryStatic()](#2.2.3 xSemaphoreCreateBinaryStatic())

[2.3 二值信号量创建过程分析](#2.3 二值信号量创建过程分析)

[2.4 释放信号量](#2.4 释放信号量)

[2.4.1 函数 xSemaphoreGive()](#2.4.1 函数 xSemaphoreGive())

[2.4.2 函数 xSemaphoreGiveFromISR()](#2.4.2 函数 xSemaphoreGiveFromISR())

[2.5 获取信号量](#2.5 获取信号量)

[2.5.1 函数 xSemaphoreTake()](#2.5.1 函数 xSemaphoreTake())

[2.5.2 函数 xSemaphoreTakeFromISR()](#2.5.2 函数 xSemaphoreTakeFromISR())

[3. 二值信号量操作实验](#3. 二值信号量操作实验)

[3.1 实验目的](#3.1 实验目的)

[3.2 实验设计](#3.2 实验设计)

[3.3 实验程序](#3.3 实验程序)

[3.3.1 USART.c](#3.3.1 USART.c)

[3.3.2 main.c](#3.3.2 main.c)


信号量是操作系统中重要的一部分,信号量一般用于进行资源管理和任务同步,在 FreeRTOS 中,信号量可以分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其应用场景不同,但是有些应用场景是可以互换着使用的,从这里开始,我们进行信号量的学习!!!

1. 信号量简介

信号量常常用于控制对共享资源的访问和任务同步。举个很常见的例子,某个停车场有 100 个停车位,这 100 个停车位大家都可以使用,对于大家来说,这 100 个停车位就是共享资源。现在假设这个停车场正常运行,我们要把车停到这个停车场首先肯定要先看一下现在已经停了多少车?还有没有停车位?当前的停车数量就是一个信号量,具体的停车数量就是这个信号量值,当这个值到达 100 的时候就说明停车场满了。停车场满的时候我们可以等一会看看有没有其他的车开出停车场,当有车开出停车场的时候停车数量就会加一,也就是信号量加一。

这是一个典型的使用信号量进行共享资源管理的案例,在这个案例中使用的就是计数型信号量。我们再看另外一个,使用公共电话,我们知道一次只能一个人使用电话,这个时候公共电话就只可能有两个状态:使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。

信号量的另一个重要的应用场合是任务同步,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函数里面不能放太多的代码,否则的话就会影响中断的实时性。裸机编写中断服务函数的时候一般都只是在中断服务函数中打个标记,然后在其他地方根据标记来做具体的处理过程。在使用 RTOS 系统的时候我们就可以借助信号量完成此功能,当中断发生的时候就释放信号量,中断服务函数不作具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获取到信号量就说明中断发生了,那么就开始完成相应的处理,这么做的好处就是中断执行的时间非常短。

这里区别信号量和队列?

队列 是用来传输数据的,而信号量是用来传输状态的;

信号量表示一种状态:

如果信号量为 0 和 1,那么此时称为二值信号量;

如果信号量为 1 2 3 4 5 6 7 8 9 10,那么此时称为计数型信号量;

队列:

可以容纳多个数据;

创建队列有两部分内存:队列结构体 + 队列项存储空间;

写入队列:当队列满时,可阻塞;

读取队列:当队列为空时,可阻塞;
信号量:

仅存放计数值,无法存放其他数据;

创建信号量,只需分配信号量结构体;

释放信号量:不可阻塞,计数值++;当计数值为最大值时,返回失败;

获取信号量:计数值--;当没有资源时,可阻塞;
信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问?

在计算机中,为了合理的进行内存管理,引入共享资源的概念。同步问题就是说,比如我让计算机去算 A + B*C,我们肯定知道要先算 B*C,然后再去算 A + B*C 的结果,但是如果我们不加以限制,计算机不会这样,计算机会先去计算 A + B,然后再去计算乘法,因此,同步就是为了保证任务与任务之间的运行次序,比如,我先给乘法运行的任务一个信号量,乘法运算的任务收到这个信号量,开始在任务中执行乘法运算;然后同样的过程进行加法运算,这样就可以保证乘法运算是在加法运算之前进行的;共享资源就是计算机中的所有任务都可以使用的资源,但是同一时刻只能有一个任务进行使用,你能想象打印机第一页打印 A 文档,第二页打印 B 文档吗?所以同步机制对于操作系统而言是至关重要的!

2. 二值信号量

2.1 二值信号量简介

二值信号量通常用于互斥访问或同步。二值信号量和互斥信号量非常类似,但是也是存在一些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号量更加适用于同步(任务与任务或任务与中断的同步),而互斥信号量适合于简单的互斥访问。

和队列一样,信号量 API 函数允许设置一个阻塞时间。阻塞时间是当任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一个信号量上的话,那么优先级最高的那个任务优先获得信号量,这样当信号量有效的时候高优先级的任务就会解除阻塞状态。

二值信号量其实就是一个只有一个队列项的队列(队列长度为 1,该队列就只有空 0 和满 1 两种情况,这就是二值;就像硬币的正反面一样)。这个特殊的队列要么是满的,要么是空的,正如其名,二值?任务与中断使用这个特殊的队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的即可。可以利用这个机制完成任务与中断之间的同步。

在实际应用中通常会使用一个任务来处理 MCU 的某个外设,比如网络应用中,一般最简单的方法就是使用一个任务去轮询的查询 MCU 的 ETH(网络相关外设,如 STM32 的以太网 MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样的使用轮询的方式是很浪费 CPU 资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任务就进入阻塞态,把 CPU 让给其他任务,当有数据的时候网络任务才去执行。现在使用二值信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入阻塞态,而网络中断服务函数(大多数的网络外设都有中断功能,比如 STM32 的 MAC 专用 DMA 中断,通过中断可以判断是否接收到了数据)通过释放信号量来通知任务以太网外设接收到了网络数据,网络任务可以去提取处理了。网络任务只是在一直的获取二值信号量,它不会释放信号量,而中断服务函数是一直在释放信号量,它不会获取信号量。在中断服务函数中发送信号量可以使用 xSemaphoreGiveFromISR(),也可以使用任务通知功能来代替二值信号量,而且使用任务通知的话速度更快,代码量更少。

使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及时的处理,这样做相当于推迟了中断处理过程。也可以使用队列来代替二值信号量,在外设事件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。

2.1.1 二值信号量无效

上图中任务 Task 通过函数 xSemaphoreTake() 获取信号量,但是此时二值信号量无效,所以任务 Task 进入阻塞态。

2.1.2 中断释放信号量

此时中断发生了,在中断服务函数中通过函数 xSemaphoreGiveFromISR() 释放信号量,因此信号量变为有效。

2.1.3 任务获取信号量成功

由于信号量已经有效了,所以任务 Task 获取信号量成功,任务从阻塞态解除,开始执行相关的处理过程。

2.1.4 任务再次进入阻塞态

由于任务函数一般都是一个大循环,所以在任务做完相关的处理以后就会再次调用函数 xSemaphoreTake() 获取信号量。在执行完第三步以后二值信号量就已经变为无效的了,所以任务将再次进入阻塞态,和第一步一样,直至中断再次发生并且调用函数 xSemaphoreGiveFromISR() 释放信号量。

2.2 创建二值信号量

和队列一样,要想使用二值信号量就必须先创建二值信号量,二值信号量创建函数如下表所示:

vSemaphoreCreateBinary() 动态创建二值信号量(老版本的 FreeRTOS 创建信号量的 API 函数)

xSemaphoreCreateBinary() 动态创建二值信号量(新版本的 FreeRTOS 创建信号量的 API 函数)

xSemaphoreCreateBinaryStatic() 静态创建二值信号量

2.2.1 vSemaphoreCreateBinary()

此函数是老版本的 FreeRTOS 中的创建二值信号量的函数,新版本已经不再使用了,这里还保留这个函数是为了兼容那些基于老版本 FreeRTOS 而做的应用层代码。

此函数是一个宏,具体创建过程由函数 xQueueGenericCreate() 来完成;

复制代码
void vSemaphoreCreateBinary(SemaphoreHandle_t xSemaphore)

参数:

xSemaphore:保存创建成功的二值信号量句柄

返回值:

NULL:二值信号量创建失败。

其他值:二值信号量创建成功。

复制代码
#define xSemaphoreCreateBinary()
    xQueueGenericCreate(1,semSEMAPHORE_QUEUE_ITEM_LENGTH,queueQUEUE_TYPE_BINARY_SEMAPHORE)
#define semSEMAPHORE_QUEUE_ITEM_LENGTH    ((uint8_t)0U)

宏定义为 semSEMAPHORE_QUEUE_ITEM_LENGTH 为 0,所以创建的二值信号量队列长度为 0,队列项 1 个

2.2.2 xSemaphoreCreateBinary()

此函数是创建二值信号量的新版本函数,使用此函数来创建二值信号量的话信号量所需的 RAM 是由 FreeRTOS 的内存管理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号量使用函数 xSemaphoreTake() 是获取不到的(因为获取二值信号量必须保证整个队列不为空),此函数也是一个宏,具体创建过程是由函数 xQueueGenericCreate() 来完成的,函数原型如下:

复制代码
SemaphoreHandle_t xSemaphoreCreateBinary(void)

参数:

返回值:

NULL:二值信号量创建失败。

其他值:创建成功的二值信号量的句柄。

2.2.3 xSemaphoreCreateBinaryStatic()

此函数也是用来创建二值信号量的,只不过使用此函数创建二值信号量的话信号量所需要的 RAM 需要由用户来分配,此函数也是一个宏,具体创建过程是通过函数 xQueueGenericCreateStatic() 来完成的,函数原型如下:

复制代码
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffe)

参数:

pxSemaphoreBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

返回值:

NULL:二值信号量创建失败。

其他值:创建成功的二值信号量句柄。

2.3 二值信号量创建过程分析

上面我们学习二值信号量的创建函数,两个动态创建函数和一个静态创建函数。

这里我们着重的看两个动态的创建函数:

复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 
    #define vSemaphoreCreateBinary( xSemaphore )                      \ 
        {                                                             \ 
            ( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1,  \ (1) 
            semSEMAPHORE_QUEUE_ITEM_LENGTH,                           \ 
            queueQUEUE_TYPE_BINARY_SEMAPHORE );                       \ 
            if( ( xSemaphore ) != NULL )                              \ 
            {                                                         \ 
                ( void ) xSemaphoreGive( ( xSemaphore ) );            \ (2) 
            }                                                         \ 
        } 
#endif 

(1)在上面我们提到了二值信号量是在队列的基础上实现的,所以创建二值信号量其实就是创建队列的过程。这里使用函数 xQueueGenericCreate() 创建了一个队列,队列长度为 1 ,队列项长度为 0,队列类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE,也就是二值信号量。

(2)当二值信号量创建成功以后立即调用函数 xSemaphoreGive() 释放二值信号量,此时新创建的二值信号量有效。

紧接着, 我们来看新版本的二值信号量创建函数 xSemaphoreCreateBinary(),函数代码如下:

复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 
    #define xSemaphoreCreateBinary()                 \ 
    xQueueGenericCreate( ( UBaseType_t ) 1,          \ 
    semSEMAPHORE_QUEUE_ITEM_LENGTH,                  \ 
    queueQUEUE_TYPE_BINARY_SEMAPHORE )               \ 
#endif

通过观察可以发现,新版本和旧版本的创建二值信号量的函数具有相同点,也具有不同点:新版本的二值信号量创建函数也是使用函数 xQueueGenericCreate() 来创建一个类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为 1 、队列项长度为 0 的队列。这一步和老版本的创建二值信号量的函数一样,唯一不同的是新版本的函数在成功创建二值信号量以后不会立即调用函数 xSemaphoreGive() 释放二值信号量。也就是说新版本函数创建二值信号量默认是无效的,而老版本默认是有效的。

注意看,创建的队列的队列项长度为 0 ,也就是说创建的队列是个没有存储区的队列,前面说了使用队列是否为空来表示二值信号量,而队列是否为空可以通过队列结构体的成员变量 uxMessagesWaiting 来判断。

2.4 释放信号量

释放信号量的函数有两个:

xSemaphoreGive() 任务级信号量释放函数

xSemaphoreGiveFromISR() 中断级信号量释放函数

和队列一样!释放信号量也分为任务级和中断级

注意:

不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上述的函数释放信号量,但是递归互斥信号量有专用的释放函数!

2.4.1 函数 xSemaphoreGive()

此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信号量的过程是由函数 xQueueGenericSend() 来完成的,函数原型如下:

复制代码
BaseType_t xSemaphoreGive(xSemaphore)

参数:

xSemaphore 要释放的任务量句柄。

返回值:

pdPASS 释放信号量成功。

errQUEUE_FULL 释放信号量失败。

紧接着,我们来看一下函数 xSemaphoreGive() 的具体内容,此函数在文件 semaphr.h 中有如下定义:

复制代码
#define xSemaphoreGive( xSemaphore )                             \ 
    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),         \ 
    NULL,                                                        \ 
    semGIVE_BLOCK_TIME,                                          \ 
    queueSEND_TO_BACK )                                          \ 

//该函数总共4个参数
//第一个参数信号量句柄
//第二个参数发送的消息,在信号量中都是针对的计数值,所以为NULL
//第三个参数是阻塞时间,这里设置为0,也就是释放信号量计数值++,永远不会阻塞
//第四个参数表示队列的入队方式,这里是后向入队

可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息,阻塞时间为 0 (宏 semGIVE_BLOCK_TIME),入队方式采用的后向入队。入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了,当 uxMessagesWaiting 为 1 的话说明二值信号量有效,为 0 就无效。如果队列满的话就返回错误值 errQUEUE_FULL,提示队列满,入队失败!

2.4.2 函数 xSemaphoreGiveFromISR()

此函数用于在中断中释放信号量,此函数只能用来释放二值信号量和计数型信号量,绝对不能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数 xQueueGiveFromISR() ,此函数原型如下:

复制代码
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t    xSemaphore,
                                 BaseType_t*          pxHigherPriorityTaskWoken)

参数:

xSemaphore 要释放的信号量句柄

pxHigherPriorityTaskWoken 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置,用户不用进

行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退

出中断服务函数之前一定要进行一次任务切换。

返回值:

pdPASS 释放信号量成功。

errQUEUE_FULL 释放信号量失败。

在中断中释放信号量真正使用的是函数 xQueueGiveFromISR(),此函数和中断级通用入队函数 xQueueGenericSendFromISR() 极其类似!只是针对信号量做了微小的改动。函数 xSemaphoreGiveFromISR() 不能用于中断中释放互斥信号量,因为互斥信号量涉及到优先级继承的问题,而中断不属于任务,没法处理中断优先级继承。

2.5 获取信号量

获取信号量也有两个函数,如下表所示:

xSemaphoreTake() 任务级获取信号量函数

xSemaphoreTakeFromISR() 中断级获取信号量函数

同释放信号量的 API 函数一样,不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上述的函数获取信号量。

2.5.1 函数 xSemaphoreTake()

此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信号量的过程是由函数 xQueueGenericReceive() 来完成的,函数原型如下:

复制代码
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
                           TickType_t        xBlockTime)

参数:

xSemaphore:要获取的信号量句柄

xBlockTime:阻塞时间。

返回值:

pdTRUE:获取信号量成功。

pdFALSE:超时,获取信号量失败。

紧接着,来看函数 xSemaphoreTake() 的具体内容,此函数在文件 semaphr.h 中如下定义:

复制代码
#define xSemaphoreTake( xSemaphore, xBlockTime )                     \ 
    xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ),          \ 
    NULL,                                                            \ 
    ( xBlockTime ),                                                  \ 
    pdFALSE )                                                        \

获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。在对队列学习时,我们知道函数 xQueueGenericReceive() 的时候说过如果队列为空并且阻塞时间为 0 的话就立即返回 errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务添加到延时列表中。如果队列不为空的话就从队列中读取数据,数据读取完成以后还需将队列结构体成员变量 uxMessagesWaiting 减一,然后解除某些因为入队而阻塞的任务,最后返回 pdPASS 表示出队成功。

2.5.2 函数 xSemaphoreTakeFromISR()

此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量,绝对不能使用此函数来获取互斥信号量!此函数是一个宏,真正执行的是函数 xQueueReceiveFromISR(),此函数原型如下:

复制代码
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t  xSemaphore,
                                 BaseType_t*        pxHigherPriorityTaskWoken)

参数:

xSemaphore:要获取的信号量句柄。

pxHigherPriorityTaskWoken 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置,用户不用进

行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退

出中断服务函数之前一定要进行一次任务切换。

返回值:

pdPASS:获取信号量成功。

pdFALSE:获取信号量失败。

在中断中获取信号量真正使用的是函数 xQueueReceiveFromISR(),这个函数就是中断级出队函数!当队列不为空的时候就拷贝队列中的数据,然后将队列结构体中的成员变量 uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数 pxHigherPriorityTaskWoken 设置为 pdTRUE,最后返回 pdPASS 表示出队成功。如果队列为空的话就直接返回 pdFAIL 表示出队失败!

3. 二值信号量操作实验

3.1 实验目的

二值信号量的使命就是同步,完成任务与任务或中断与任务之间的同步。大多数情况下都是中断与任务之间的同步。

3.2 实验设计

这里我们设置一个通过串口发送指定的指令来控制开发板上的的 LED1 和 BEEP 开关的实验,指令如下(不区分大小写):

LED1ON:打开 LED1。

LED1OFF:关闭 LED1。

BEEPON:打开蜂鸣器。

BEEPOFF:关闭蜂鸣器。

这些指令通过串口发送给开发板,指令是不区分大小写的!开发板使用中断接收,当接收到数据以后就释放二值信号量。

任务 DataProcess_task() 用于处理这些指令,任务会一直尝试获取二值信号量,当获取到信号量就会从串口接收缓冲区中提取这些指令,然后根据指令控制相应的外设。(简单来说就是:串口将指令发送给开发板,开发板通过中断来接收,在中断中释放二值信号量;然后任务 DataProcess_task 处理这些指令,并且一直获取中断释放的二值信号量,一旦获取到信号量就从串口接收缓冲区中提取这些指令,然后根据指令控制相关的外设)

本实验设计三个任务:start_task、task1_task、DataProcess_task 这三个任务的任务功能如下:

**start_task:**用于创建其他两个任务。

**task1_task:**控制 LED0 闪烁,提示系统正在运行。

**DataProcess_task:**指令处理任务,根据接收到的指令来控制不同的外设。

实验中还创建了一个二值信号量 BinarySemaphore 用于完成串口中断和任务 DataProcess_task 之间的同步。

3.3 实验程序

3.3.1 USART.c

复制代码
extern SemaphoreHandle_t BinarySemaphore;  //信息队列句柄 
//QueueHandle_t queue.h 中定义

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
	u8 Res;
    //xHigherPriorityTaskWoken:用来标记退出此函数以后是否进行任务切换,这个变量的值由三个函数来设置,用户不再进行设置
                                 //用户只需要提供一个变量来保存这个值就可以了。
                                 //但是切记要注意:当此值为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换。
    
    BaseType_t xHigherPriorityTaskWoken;  //BaseType_t 也在 queue.h 中定义
    
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
		Res =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
		{
			if(USART_RX_STA&0x4000)//接收到了0x0d
			{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了 
			}
			else //还没收到0X0D
			{	
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
				{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收	  
				}		 
			}
		} 
    }
    //释放二值信号量
    //指令通过串口发送给开发板,串口中断用来释放二值信号量,任务用来不断获取信号量
    //任务一旦获取到信号量,就会从串口接收缓冲区中提取这些指令,然后根据这些指令控制相应的外设
    if((USART_RX_STA&0x8000)&&(BinarySemaphore!=NULL)) //串口接收到数据,并且二值信号量不为空,也就表示二值信号量是有效的
    {
        xSemaphoreGiveFromISR(BinarySemaphore,&xHigherPriorityTaskWoken);  //调用在中断中释放二值信号量函数
        //函数第一个参数:释放二值信号量句柄
        //函数第二个参数:标记是否需要进行任务切换
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken); //如果需要的话进行一次任务切换
    }   //二值信号量用来实现同步的意思就是说:保证中断先释放信号量,然后任务在获取信号量;
}

3.3.2 main.c

复制代码
#include "stm32f4xx.h"  
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h"     //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"
#include "semphr.h"


//任务优先级
#define START_TASK_PRIO     1     //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE      256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);

//任务优先级
#define TASK1_TASK_PRIO     2   //控制 LED0 闪烁,提示系统正在运行
//任务堆栈大小
#define TASK1_STK_SIZE      256
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);

//任务优先级
#define DATAPROCESS_TASK_PRIO 3  //指令处理函数
//任务堆栈大小	
#define DATAPROCESS_STK_SIZE  256 
//任务句柄
TaskHandle_t DataProcess_Handler;
//任务函数
void DataProcess_task(void *pvParameters);

//二值信号量句柄
SemaphoreHandle_t BinarySemaphore;    //二值信号量句柄

//用于命令解析用的命令值
#define LED1ON  1
#define LED1OFF 2
#define BEEPON  3
#define BEEPOFF 4
#define COMMANDERR  0xFF

//函数 LowerToCap 用于将串口发送过来的命令中的小写字母统一转换成大写字母,
//这样就可以在发送命令的时候不用区分大小写,因为开发板会统一转换成大写。
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{
    u8 i;
    for(i=0;i<len;i++)
    {
        //判断字符串的ASCII码是否位于96到123之间
        if((96<str[i])&&(str[i]<123))  //小写字母
        {
            //ASCII码是一种用于表示字符的编码系统。在ASCII码中,每个字符都被赋予一个唯一的整数值。
            //大写字母的ASCII码值是65到90
            //小写字母的ASCII码值是97到122   所以一旦确定ASCII码值位于小写字母的范畴内,只需要将ASCII码值减去32即可转换为大写
            str[i] = str[i] - 32;  //转换为大写
        }
    }
}

//函数 CommandProcess 用于将接收到的命令字符串转换成命令值,比如说命令"LED1ON"转换成命令值就是 0(宏LED1ON为 0)
//命令处理函数,将字符串命令转换成命令值
//str:命令
//返回值:0xFF,命令错误;其他值,命令值
u8 CommandProcess(u8 *str)
{
    u8 CommandValue = COMMANDERR;
    if(strcmp((char*)str,"LED1ON")==0) //strcmp 字符串比较函数
        //这个函数会比较两个参数;比较时,会以字符的ASCII值进行比较
        //如果str1的ASCII码值小于str2,返回一个负数;反之,返回一个正数;
        //如果str1的ASCII码值等于str2,返回 0,此时,if判断语句成立
        CommandValue = LED1ON; //设置的LED1ON的宏为1,也就是在串口输入1,if判断语句成立
    else if(strcmp((char*)str,"LED1OFF")==0)
        CommandValue = LED1OFF; //在串口输入2,if判断语句成立
    else if(strcmp((char*)str,"BEEPON")==0)
        CommandValue = BEEPON; //在串口输入3,if判断语句成立
    else if(strcmp((char*)str,"BEEPOFF")==0)
        CommandValue = BEEPOFF; //在串口输入4,if判断语句成立
    return CommandValue;
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级
    delay_init(168);
    uart_init(115200);
    LED_Init();
    KEY_Init();
    BEEP_Init();
    LCD_Init();
    my_mem_init(SRAMIN);    //初始化内部内存池
    
    POINT_COLOR=RED;
    LCD_ShowString(10,10,200,16,16,"ATK STM32F407");
    LCD_ShowString(10,30,200,16,16,"FreeRTOS Example");
    LCD_ShowString(10,50,200,16,16,"Binary Semaphore");
    LCD_ShowString(10,70,200,16,16,"Command Data:");
    
    //创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    vTaskStartScheduler();          //开启任务调度
}

//开始任务任务函数
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();    //进入临界区
    
    //创建二值信号量,也就是创建一个长度为1的队列
    BinarySemaphore = xSemaphoreCreateBinary();   //xSemaphoreCreateBinary函数为动态创建二值信号量函数
    //返回 NULL,二值信号量创建失败;返回其他值,表示创建成功的二值信号量的句柄;
    //所以BinarySemaphore表示创建成功的二值信号量的句柄;
    
    //创建Task1任务
    xTaskCreate((TaskFunction_t )task1_task,            //任务函数
                (const char*    )"task1_task",          //任务名称
                (uint16_t       )TASK1_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )TASK1_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&Task1Task_Handler);   //任务句柄  
    //创建Task2任务
    xTaskCreate((TaskFunction_t )DataProcess_task,            //任务函数
                (const char*    )"DataProcess_task",          //任务名称
                (uint16_t       )DATAPROCESS_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )DATAPROCESS_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&DataProcess_Handler);   //任务句柄  
                                  
    vTaskDelete(StartTask_Handler);  //删除开始任务
    taskEXIT_CRITICAL();          //退出临界区
}

//Task1任务
//控制 LED0 闪烁,提示系统正在运行
void task1_task(void *pvParameters)
{
    while(1)
    {
        LED0=!LED0;
        vTaskDelay(500);        //延时500ms,也就是500个时钟节拍
    }
}

//DataProcess_task函数
//指令处理任务,根据接收到的指令来控制不同的外设
void DataProcess_task(void *pvParameters)
{
    u8 len=0;
    u8 CommandValue=COMMANDERR;
    BaseType_t err=pdFALSE;
    
    u8 *CommandStr;
    POINT_COLOR=BLUE;
    while(1)
    {
        if(BinarySemaphore!=NULL) //二值信号量不为空,表明接收到的数据是有效的
        {
            err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取信号量函数;返回值pdTURE,获取信号量成功;pdFALSE,获取信号量失败;
            //第一个参数,要获取的信号量句柄
            //第二个参数,阻塞时间,这里设置为portMAX_DEALY,译为无限等待,直至获得信号量
            if(err==pdTRUE) //获取信号量成功
            {
                len=USART_RX_STA&0x3fff;  //得到此次接收到的数据长度
                //接收状态
                //bit15,	接收完成标志
                //bit14,	接收到0x0d
                //bit13~0,	接收到的有效字节数目
                CommandStr=mymalloc(SRAMIN,len+1);  //申请内存 指针指向申请内存的首地址
                sprintf((char*)CommandStr,"%s",USART_RX_BUF);  //打印接收缓存区,把接收缓存区的数据保存到CommandStr中
                CommandStr[len]='\0';   //加上字符串结尾符号
                //CommandStr 是个指针,长度为len,数组是从下角标 0 开始的,所以len就表示数组的最后一个
                LowerToCap(CommandStr,len);  //将字符串转换成大写
                CommandValue=CommandProcess(CommandStr);  //命令解析,也就是获取上面定义的宏 1 2 3 4
                if(CommandValue!=COMMANDERR)//if判断语句成立,表示CommandValue不等于0xFF,那也就是 LED1ON、LED1OFF、BEEPON、BEEPOFF 其中一个指令
                {
                    LCD_Fill(10,90,210,110,WHITE);  //清除显示区域
                    LCD_ShowString(10,90,200,16,16,CommandStr);  //在LCD上显示命令
                    printf("命令为:%s\r\n",CommandStr);   
                    switch(CommandValue)
                    {
                        case LED1ON:
                            LED1=0;
                            break;
                        case LED1OFF:
                            LED1=1;
                            break;
                        case BEEPON:
                            BEEP=1;
                            break;
                        case BEEPOFF:
                            BEEP=0;
                            break;
                    }
                }
                else
                {//当命令错误的时候开发板会向串口调试助手发送命令错误的提示信息
                    //比如我们发送 LED1_off 这个命令,串口助手会显示:无效的命令,请重新输入!!
                    printf("无效的命令,请重新输入!!\r\n");
                }
                USART_RX_STA = 0;
                memset(USART_RX_BUF,0,USART_REC_LEN);  //串口接收缓冲区清零
                myfree(SRAMIN,CommandStr);             //释放内存
            }
        }
        else if(err==pdFALSE)
        {
            vTaskDelay(10);   //延时10ms,也就是10个时钟节拍
        }
    }
}
相关推荐
Meraki.Zhang4 分钟前
【STM32实践篇】:最小系统组成
stm32·单片机·嵌入式硬件·最小系统
Do vis8241 小时前
STM32第二十一天定时器TIM
stm32·单片机·嵌入式硬件
丁满与彭彭2 小时前
嵌入式学习笔记--MCU阶段--day03中断
笔记·单片机·学习
森焱森3 小时前
一文理解锂电池充电、过放修复与电量测量:从原理到实战
c语言·单片机·架构
艾莉丝努力练剑3 小时前
【C语言】学习过程教训与经验杂谈:思想准备、知识回顾(五)
c语言·开发语言·数据结构·学习·算法
逼子格3 小时前
滤波电路Multisim电路仿真实验汇总——硬件工程师笔记
笔记·嵌入式硬件·硬件工程·硬件工程师·硬件工程师真题·multisim电路仿真·滤波电路
xienda3 小时前
冒泡、选择、插入排序:三大基础排序算法深度解析(C语言实现)
数据结构·算法·排序算法
future14123 小时前
游戏开发日记7.12
数据结构·学习·c#·游戏开发
皮卡蛋炒饭.4 小时前
数据结构——堆
数据结构·算法
JuneXcy4 小时前
第七章应用题
数据结构