9. 信号量

同志们,回想一下,你是否在裸机编程中这样使用过一个变量:用于标记某个事件是否发生,或者标志一下某个东西是否正在被使用, 如果是被占用了的或者没发生,我们就不对它进行操作。

9.1. 信号量基本概念

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问, 常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护, 信号量功能可以为用户提供这方面的支持。

抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源), 当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数, 表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

  • 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。

  • 正值,表示有一个或多个释放信号量操作。

9.1.1. 二值信号量

二值信号量既可以用于临界资源访问也可以用于同步功能。

二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但是有一些细微差别: 互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步), 而互斥量更偏向应用于临界资源的访问。

用作同步时,信号量在创建后应被置为空,任务1获取信号量而进入阻塞,任务2在某种条件发生后,释放信号量, 于是任务1获得信号量得以进入就绪态,如果任务1的优先级是最高的,那么就会立即切换任务,从而达到了两个任务间的同步。 同样的,在中断服务函数中释放信号量,任务1也会得到信号量,从而达到任务与中断间的同步。

还记得我们经常说的中断要快进快出吗,在裸机开发中我们经常是在中断中做一个标记,然后在退出的时候进行轮询处理, 这个就是类似我们使用信号量进行同步的,当标记发生了,我们再做其他事情。在 FreeRTOS中我们用信号量用于同步, 任务与任务的同步,中断与任务的同步,可以大大提高效率。

可以将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满(因此称为二值), 我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。

9.1.2. 计数信号量

二进制信号量可以被认为是长度为1的队列,而计数信号量则可以被认为长度大于1的队列, 信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。

顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。 每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加1),当处理被事件时(一般在任务中处理), 处理任务会取走该信号量(信号量计数值减1),信号量的计数值则表示还有多少个事件没被处理。 此外,系统还有很多资源,我们也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目, 任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意, 在使用完资源的时候必须归还信号量,否则当计数值为0的时候任务就无法访问该资源了。

计数型信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一个停车场,里面只有100个车位, 那么能停的车只有100辆,也相当于我们的信号量有100个,假如一开始停车场的车位还有100个, 那么每进去一辆车就要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也需要减一, 当停车场停满了100辆车的时候,此时的停车位为0,再来的车就不能停进去了,否则将造成事故, 也相当于我们的信号量为0,后面的任务对这个停车场资源的访问也无法进行,当有车从停车场离开的时候, 车位又空余出来了,那么,后面的车就能停进去了,我们信号量的操作也是一样的,当我们释放了这个资源, 后面的任务才能对这个资源进行访问。

9.1.3. 互斥信号量

互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁, 也就是保护临界资源(什么是优先级继承在后续相信讲解)。

用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源时, (临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空, 这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。

在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。 这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后, 再做处理,从而使得临界资源得到有效的保护。

9.1.4. 递归信号量

递归信号量,见文知义,递归嘛,就是可以重复获取调用的,本来按照信号量的特性, 每获取一次可用信号量个数就会减少一个,但是递归则不然,对于已经获取递归互斥量的任务可以重复获取该递归互斥量, 该任务拥有递归信号量的所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态, 其他任务无法获取,只有持有递归信号量的任务才能获取与释放。

9.2. 二值信号量应用场景

在嵌入式操作系统中二值信号量是任务间、任务与中断间同步的重要手段, 信号量使用最多的一般都是二值信号量与互斥信号量(互斥信号量在下一章讲解)。 为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1, 把这种只有 0和 1 两种情况的信号量称之为二值信号量。

在多任务系统中,我们经常会使用这个二值信号量,比如,某个任务需要等待一个标记, 那么任务可以在轮询中查询这个标记有没有被置位,但是这样子做,就会很消耗CPU资源并且妨碍其他任务执行, 更好的做法是任务的大部分时间处于阻塞状态(允许其他任务执行),直到某些事件发生该任务才被唤醒去执行。 可以使用二进制信号量实现这种同步,当任务取信号量时,因为此时尚未发生特定事件,信号量为空,任务会进入阻塞状态; 当事件的条件满足后,任务/中断便会释放信号量,告知任务这个事件发生了,任务取得信号量便被唤醒去执行对应的操作, 任务执行完毕并不需要归还信号量,这样子的CPU的效率可以大大提高,而且实时响应也是最快的。

再比如某个任务使用信号量在等中断的标记的发生,在这之前任务已经进入了阻塞态,在等待着中断的发生, 当在中断发生之后,释放一个信号量,也就是我们常说的标记,当它退出中断之后,操作系统会进行任务的调度, 如果这个任务能够运行,系统就会把等待这个任务运行起来,这样子就大大提高了我们的效率。

二值信号量在任务与任务中同步的应用场景:假设我们有一个温湿度的传感器,假设是1s采集一次数据, 那么我们让他在液晶屏中显示数据出来,这个周期也是要1s一次的,如果液晶屏刷新的周期是100ms更新一次, 那么此时的温湿度的数据还没更新,液晶屏根本无需刷新,只需要在1s后温湿度数据更新的时候刷新即可, 否则CPU就是白白做了多次的无效数据更新,CPU的资源就被刷新数据这个任务占用了大半,造成CPU资源浪费, 如果液晶屏刷新的周期是10s更新一次,那么温湿度的数据都变化了10次,液晶屏才来更新数据,那拿这个产品有啥用, 根本就是不准确的,所以,还是需要同步协调工作,在温湿度采集完毕之后,进行液晶屏数据的刷新,这样子, 才是最准确的,并且不会浪费CPU的资源。

同理,二值信号量在任务与中断同步的应用场景:我们在串口接收中,我们不知道啥时候有数据发送过来, 有一个任务是做接收这些数据处理,总不能在任务中每时每刻都在任务查询有没有数据到来,那样会浪费CPU资源, 所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,任务就进入阻塞态,不参与任务的调度, 等到数据到来了,释放一个二值信号量,任务就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据, 这样子系统的资源就会很好的被利用起来。

9.3. 二值信号量运作机制

创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数,二值信号量的最大可用信号量个数为1。

二值信号量获取,任何任务都可以从创建的二值信号量资源中获取一个二值信号量,获取成功则返回正确, 否则任务会根据用户指定的阻塞超时时间来等待其他任务/中断释放信号量。在等待这段时间, 系统将任务变成阻塞态,任务将被挂到该信号量的阻塞等待列表中。

在二值信号量无效的时候,假如此时有任务获取该信号量的话,那么任务将进入阻塞状态,具体见下图。

运转机制

假如某个时间中断/任务释放了信号量,其过程具体见下图,

中断、任务释放信号量

当获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态,其过程具体见下下图。

二值信号量运作机制

9.4. 计数信号量运作机制

计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但会限制任务的最大数目。 访问的任务数达到可支持的最大数目时,会阻塞其他试图获取该信号量的任务,直到有任务释放了信号量。 这就是计数型信号量的运作机制,虽然计数信号量允许多个任务访问同一个资源,但是也有限定,比如某个资源限定只能有3个任务访 问,那么第4个任务访问的时候,会因为获取不到信号量而进入阻塞,等到有任务(比如任务1)释放掉该资源的时候, 第4个任务才能获取到信号量从而进行资源的访问,其运作的机制如下。

计数信号量运作示意图

9.5. 信号量控制块

CMSIS-RTOS使用osSemaphoreAttr_t结构体来描述信号量属性,如下所示

cmsis_os2.h
1
2
3
4
5
6
typedef struct {
   const char                   *name;   //信号量名
   uint32_t                 attr_bits;   //FreeRTOS未使用到
   void                      *cb_mem;    //控制块(静态创建)地址
   uint32_t                   cb_size;   //控制块(静态创建)大小
} osSemaphoreAttr_t;

其中cb_mem、cb_size成员变量用于信号量的静态创建, 当不对这两个成员变量进行赋值时将使用动态内存分配的方式创建信号量。

9.6. 常用信号量函数接口讲解

9.6.1. 创建信号量函数osSemaphoreNew()

FreeRTOS中为创建二值信号量和计数信号量提供了不同的API参数。 CMSIS-RTOS将创建二值信号量与创建计数信号量的函数接口封装成了同一个函数接口,其函数源码如下

cmsis_os2.c
 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
61
62
63
64
65
66
67
68
69
70
71
72
73
osSemaphoreId_t osSemaphoreNew (uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr) {
   SemaphoreHandle_t hSemaphore;
   int32_t mem;
#if (configQUEUE_REGISTRY_SIZE > 0)
   const char *name;
#endif

   hSemaphore = NULL;

   //osSemaphoreNew函数不能在中断使用
   if (!IS_IRQ() && (max_count > 0U) && (initial_count <= max_count)) {
      mem = -1;

      if (attr != NULL) {
         if ((attr->cb_mem != NULL) && (attr->cb_size >= sizeof(StaticSemaphore_t))) {
            mem = 1; //静态创建信号量标志
         }
         else {
            if ((attr->cb_mem == NULL) && (attr->cb_size == 0U)) {
               mem = 0;      //动态创建信号量标志
            }
         }
      }
      else {
         mem = 0;
      }

      if (mem != -1) {
         //二值信号量的创建
         if (max_count == 1U) {
            if (mem == 1) {
               //静态内存方式创建二值信号量
               hSemaphore = xSemaphoreCreateBinaryStatic ((StaticSemaphore_t *)attr->cb_mem);
            }
            else {
               //动态内存方式创建二值信号量
               hSemaphore = xSemaphoreCreateBinary();
            }
         //创建失败
         if ((hSemaphore != NULL) && (initial_count != 0U)) {
            if (xSemaphoreGive (hSemaphore) != pdPASS) {
               vSemaphoreDelete (hSemaphore);
               hSemaphore = NULL;
            }
         }
      }
      //计数信号量创建
      else {
         //静态内存方式创建计数信号量
         if (mem == 1) {
            hSemaphore = xSemaphoreCreateCountingStatic (max_count, initial_count, (StaticSemaphore_t *)attr->cb_mem);
         }
            //动态内存方式创建计数信号量
         else {
            hSemaphore = xSemaphoreCreateCounting (max_count, initial_count);
         }
      }

#if (configQUEUE_REGISTRY_SIZE > 0)
      if (hSemaphore != NULL) {
         if (attr != NULL) {
            name = attr->name;
         } else {
               name = NULL;
         }
      vQueueAddToRegistry (hSemaphore, name);
      }
#endif
   }
}

   return ((osSemaphoreId_t)hSemaphore);
}

参数

  • max_count :创建信号量的最大值

  • initial_count :创建信号量的初始值

  • attr :描述信号量属性的结构体

返回值: 若创建成功则返回信号量ID,用于访问创建的信号量。创建失败则返回NULL

osSemaphoreNew函数根据传入的信号量的个数判断应该创建二值信号量还是计数信号量, 根据attr结构体中的cb_mem和cb_size参数判断应该使用静态内存的方式还是使用动态内存的方式创建 信号量。将四个原来FreeRTOS提供的API封装成了一个接口,可见其方便性。

9.6.2. 信号量删除函数osSemaphoreDelete()

osSemaphoreDelete()用于删除一个信号量,能够删除的对象包括了二值信号量,计数信号量,互斥量和递 归互斥量。如果有任务阻塞在该信号量上,那么不要删除该信号量。其源码如下

cmsis_os2.c
 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
osStatus_t osSemaphoreDelete (osSemaphoreId_t semaphore_id) {
   SemaphoreHandle_t hSemaphore = (SemaphoreHandle_t)semaphore_id;
   osStatus_t stat;

#ifndef USE_FreeRTOS_HEAP_1
   //不能在中断只使用删除信号量
   if (IS_IRQ()) {
      stat = osErrorISR;
   }
   else if (hSemaphore == NULL) {
      stat = osErrorParameter;
   }
   else {
   #if (configQUEUE_REGISTRY_SIZE > 0)
      vQueueUnregisterQueue (hSemaphore);
   #endif

      stat = osOK;
      vSemaphoreDelete (hSemaphore);
   }
#else
   stat = osError;
#endif

   return (stat);
}

参数

  • semaphore_id :信号量ID

返回值:当成功时返回osOK,失败时返回osError、osErrorISR或osErrorParameter。

删除信号量过程本质上就是删除消息队列过程,因为信号量其实就是消息队列,只不过 是无法存储消息的队列而已。

9.6.3. 信号量释放函数osSemaphoreRelease()

与消息队列的操作一样,信号量的释放可以在线程、中断中使用, FreeRTOS为我们提供两个不同的API函数,而CMSIS-RTOS为我们封装了统一的信号量释放函数, 使得我们能够在中断、线程中都使用同一个接口函数。

在前面的讲解中,我们知道,当信号量有效的时候,任务才能获取信号量,那么,是什么函数使得信号量变得有效? 其实有两个方式,一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值; 在二值信号量中,该初始值的范围是 0~1(旧版本的FreeRTOS 中创建二值信号量默认是有效的,而新版本则默认是无效), 假如初始值为 1 个可用的信号量的话,被申请一次就变得无效了,那就需要我们释放信号量, FreeRTOS提供了信号量释放函数,每调用一次该函数就释放一个信号量。但是有个问题,能不能一直释放?很显然, 这是不能的,无论是你的信号量是二值信号量还是计数信号量,都要注意可用信号量的范围,当用作二值信号量的时候, 必须确保其可用值在 0~1 范围内;而用作计数信号量的话,其范围是由用户在创建时指定max_count, 其最大可用信号量不允许超出max_count,这代表我们不能一直调用信号量释放函数来释放信号量, 其实一直调用也是无法释放成功的,在写代码的时候,我们要注意代码的严谨性罢了。其函数源码如下

cmsis_os2.c
 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
osStatus_t osSemaphoreAcquire (osSemaphoreId_t semaphore_id, uint32_t timeout) {
   SemaphoreHandle_t hSemaphore = (SemaphoreHandle_t)semaphore_id;
   osStatus_t stat;
   BaseType_t yield;

   stat = osOK;

   if (hSemaphore == NULL) {
      stat = osErrorParameter;
   }
   else if (IS_IRQ()) {
      //在中断中获取信号量不能使用阻塞延时参数
      if (timeout != 0U) {
         stat = osErrorParameter;
      }
      else {
         yield = pdFALSE;
         //当获取到信号量导致某个更高优先级的线程激活则调度器进行一次调度
         if (xSemaphoreTakeFromISR (hSemaphore, &yield) != pdPASS) {
            stat = osErrorResource;
         } else {
            portYIELD_FROM_ISR (yield);
         }
      }
   }
   //在线程中调用获取信号量
   else {
      if (xSemaphoreTake (hSemaphore, (TickType_t)timeout) != pdPASS) {
         if (timeout != 0U) {
            stat = osErrorTimeout;
         } else {
            stat = osErrorResource;
         }
      }
   }

   return (stat);
}

参数

  • semaphore_id :信号量ID

  • timeout :线程超时等待时间

返回值: 如果获取信号量成功则返回osOK,失败返回各种出错代码。

9.6.4. 信号量获取函数osSemaphoreAcquire()

与消息队列的操作一样,信号量的获取可以在任务、中断(中断中使用并不常见)中使用, FreeRTOS为我们提供两个不同的API函数,而CMSIS-RTOS为我们封装了统一的信号量获取函数, 使得我们能够在中断、线程中都使用同一个接口函数。

与释放信号量对应的是获取信号量,我们知道,当信号量有效的时候,任务才能获取信号量, 当任务获取了某个信号量的时候,该信号量的可用个数就减一,当它减到0的时候,任务就无法再获取了, 并且获取的任务会进入阻塞态(假如用户指定了阻塞超时时间的话)。如果某个信号量中当前拥有1个可用的信号量的话, 被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功,该任务便会进入阻塞态,阻塞时间由用户指定。 其函数源码如下所示

cmsis_os2.c
 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
osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id) {
   SemaphoreHandle_t hSemaphore = (SemaphoreHandle_t)semaphore_id;
   osStatus_t stat;
   BaseType_t yield;

   stat = osOK;

   if (hSemaphore == NULL) {
      stat = osErrorParameter;
   }
   //在中断中释放信号量
   else if (IS_IRQ()) {
      yield = pdFALSE;

      if (xSemaphoreGiveFromISR (hSemaphore, &yield) != pdTRUE) {
         stat = osErrorResource;
      } else {
         //当释放信号量导致某个更高优先级的线程激活则调度器进行一次调度
         portYIELD_FROM_ISR (yield);
      }
   }
   //在线程中释放信号量
   else {
      if (xSemaphoreGive (hSemaphore) != pdPASS) {
         stat = osErrorResource;
      }
   }

   return (stat);
}

参数

  • semaphore_id :信号量ID

返回值: 如果获取信号量成功则返回osOK,失败返回各种出错代码。

9.7. 信号量实验

STM32CubeIDE中关于FreeRTOS信号量相关配置如下所示

消息队列配置

STM32CubeIDE为我们提供了两种信号量的创建选择:二值信号量和计数信号量。 从上面介绍的函数接口可知CMSIS-RTOS统一了创建信号量的接口, 它们都是调用同一套API进行创建、删除、获取、释放操作。

在信号量实验中我们将做两个实验,一个是二值信号量同步实验,一个是计数信号量实验。

9.7.1. 二值信号量同步实验

使用STM32CubeIDE创建了两个FreeRTOS线程,一个是获取信号量线程,一个是释放信号量线程, 其线程配置如下

获取信号量线程

获取信号量线程

释放信号量线程

释放信号量线程

相信大家对于线程的配置都已经很熟悉了,这里就不展开详细说明了,请忘记的同学自行回顾前面章节。

在STM32CubeIDE中添加一个二值信号量,其配置如下

释放信号量线程

可以看到关于二值信号量的配置相关内容很少,配置项一目了然。

  • Semaphore Name :创建的二值信号量变量名

  • Allocation :创建二值信号量的方式,可选择使用静态创建还是使用动态内存分配。

  • Control Block Name 使用静态创建时二值信号量内存控制块类型变量名。

使用STM32CubeIDE配置完FreeRTOS之后便可生成工程代码。 生成与FreeRTOS相关的代码在app_freertos.c文件中。

9.7.2. app_freertos.c

本小节只讲解重点部分代码,完整代码请打开工程查看。

9.7.2.1. MX_FREERTOS_Init函数

MX_FREERTOS_Init函数主要用于创建二值信号量和线程,这部分的代码由STM32CubeIDE生成,代码如下所示

app_freertos.c
1
2
3
4
5
6
7
8
9
void MX_FREERTOS_Init(void) {

   BinarySem01Handle = osSemaphoreNew(1, 1, &BinarySem01_attributes);

   ReceiveTaskHandle = osThreadNew(Receive_Task, NULL, &ReceiveTask_attributes);

   SendTaskHandle = osThreadNew(Send_Task, NULL, &SendTask_attributes);

}

在MX_FREERTOS_Init函数中调用osSemaphoreNew函数进行创建信号量,根据之前osSemaphoreNew函数的源码 可知当传入的max_count=1时,将调用FreeRTOS函数提供的xSemaphoreCreateBinary函数进行创建二值信号量。

9.7.2.2. Receive_Task

获取信号量任务是一直在等待信号量,等到获取到信号量之后,任务开始执行任务代码, 如此反复等待另外任务释放的信号量。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void Receive_Task(void *argument)
{
   osStatus_t xReturn = osOK ;

   for(;;)
   {
      //获取二值信号量 xSemaphore,没获取到则一直等待
      xReturn = osSemaphoreAcquire(BinarySem01Handle,/* 二值信号量句柄 */
                        osWaitForever); /* 等待时间 */
      if(osOK == xReturn)
         printf("BinarySem_Handle二值信号量获取成功!\n");

   }

}

9.7.2.3. Send_Task

释放信号量任务在检测按键是否按下,如果按下则释放信号量,此时释放信号量会唤醒获取任务, 获取任务开始运行,然后形成两个任务间的同步,因为如果没按下按键,那么信号量就不会释放,只有当信号量释放的时候, 获取信号量的任务才会被唤醒,如此一来就达到任务与任务的同步,同时程序的运行会在串口打印出相关信息,

app_freertos.c
 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
void Send_Task(void *argument)
{
   osStatus_t xReturn = osOK ;

   for(;;)
   {
      /* K1 被按下 */
      if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
      {
         xReturn = osSemaphoreRelease( BinarySem01Handle );//给出二值信号量
         if( xReturn == osOK )
            printf("BinarySem_Handle二值信号量释放成功!\r\n\n");
         else
            printf("BinarySem_Handle二值信号量释放失败!\r\n\n");
      }
      /* K2 被按下 */
      if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
      {
         xReturn = osSemaphoreRelease( BinarySem01Handle );//给出二值信号量
         if( xReturn == osOK )
            printf("BinarySem_Handle二值信号量释放成功!\r\n\n");
         else
            printf("BinarySem_Handle二值信号量释放失败!\r\n\n");
      }
      osDelay(20);
      }
}

9.7.2.4. main函数

main.c
 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
int main(void)
{

   HAL_Init();               //初始化时钟

   if(IS_ENGINEERING_BOOT_MODE())
   {
      /* Configure the system clock */
      SystemClock_Config();  //时钟配置
   }


   MX_GPIO_Init();            //初始化GPIO
   MX_USART3_UART_Init(); //初始化串口

   osKernelInitialize();  //初始化内核状态
   MX_FREERTOS_Init();        //创建任务

   osKernelStart();          //启动任务调度器,

   //当成功启动任务调度器之后以下代码将不会被执行
   while (1)
   {

   }

}

9.7.2.5. 二值信号量实验现象

二值信号量实验现象

当程序开始运行时,会先获取信号量。之后若有按键按下,此时释放信号量会唤醒获取任务, 获取任务开始运行,形成两个任务间的同步。

9.7.3. 计数信号量实验

计数型信号量实验是模拟停车场工作运行。在创建信号量的时候初始化5个可用的信号量,

获取信号量任务是通过按下KEY1按键进行信号量的获取,模拟停车场停车操作,其等待时间是0, 在串口调试助手输出相应信息。

释放信号量任务则是信号量的释放,释放信号量任务也是通过按下KEY2按键进行信号量的释放, 模拟停车场取车操作,在串口调试助手输出相应信息。

本次实验中创建的线程名为TakeTask以及GiveTask,其他参数与二值信号量配置相同,不再赘述。 在STM32CubeIDE中添加一个计数信号量,其配置如下

计数信号量

计数信号量的配置选择也是相对简单,只比二值信号量多了信号量计数配置。

  • Semaphore Name :创建的二值信号量变量名

  • Count :可用的最大信号量计数。

  • Allocation :创建二值信号量的方式,可选择使用静态创建还是使用动态内存分配。

  • Control Block Name 二值信号量内存控制块类型变量名。

使用STM32CubeIDE配置完FreeRTOS之后便可生成工程代码。 生成与FreeRTOS相关的代码在app_freertos.c文件中。

9.7.4. app_freertos.c

本小节只讲解重点部分代码,完整代码请打开工程查看。

9.7.4.1. MX_FREERTOS_Init函数

MX_FREERTOS_Init函数主要用于创建计数信号量和线程,这部分的代码由STM32CubeIDE生成,代码如下所示

app_freertos.c
1
2
3
4
5
6
7
8
9
void MX_FREERTOS_Init(void) {

   CountingSemHandle = osSemaphoreNew(5, 5, &CountingSem_attributes);

   TakeTaskHandle = osThreadNew(Take_Task, NULL, &TakeTask_attributes);

   GiveTaskHandle = osThreadNew(Give_Task, NULL, &GiveTask_attributes);

}

可计数信号和二值信号量最大的区别在于传给osSemaphoreNew函数的参数不相同。

9.7.4.2. Take_Task

当按键KEY1按下时获取信号量。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Take_Task(void *argument)
{
   osStatus_t xReturn = osOK ;

   for(;;)
   {
      if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
      {
         xReturn = osSemaphoreAcquire(CountingSemHandle,/* 信号量句柄 */
                           0); /* 等待时间 */
         if(osOK == xReturn)
         {
            printf("Conuting_Handle信号量获取成功!\n");
         }
         else
         {
            printf("Conuting_Handle信号量获取失败!\n");
         }

      }
      osDelay(20);
   }

}

9.7.4.3. Give_Task

当按键KEY2按下时释放信号量。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
     void Give_Task(void *argument)
     {
             osStatus_t xReturn = osOK ;
             for(;;)
             {
                     /* K2 被按下 */
                     if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
                     {
                             xReturn = osSemaphoreRelease( CountingSemHandle );//给出信号量
                             if( xReturn == osOK )
                             {
                                     printf("Conuting_Handle信号量释放成功!\r\n");
                             }
                             else
                             {
                                     printf("Conuting_Handle 信号量释放失败!\r\n");
                             }

                     }
                     osDelay(20);
             }
     }

9.7.4.4. 计数信号量实验现象

按下开发版的 KEY1 按键获取信号量模拟停车,按下 KEY2 按键释放信号量模拟取车; 实验现象如下图所示,请读者自行体验

计数信号量