上次我们认识了队列,这次实验我们使用两个实验来分别认识二值信号量和计数信号量。本次实验采用STM32F103ZET6主芯片的开发板,使用HAL库开发。
信号量可以用于进程间的通信,它和互斥量都是基于队列的基本数据结构,信号量又分为二值信号量和计数信号量。
二值信号量
二值信号量就是只有一个项的队列,这个队列要么是空的,要么是满的,所以可以相当于0和1两种信号。二值信号量就像一个标志,适合用于进程间的同步通信。
注意:二值信号量通信时一方只能发送,另一方只能接收信息,不能修改。
二值信号量的使用示例
认识二值信号的各种函数
- 创建二值信号:osSemaphoreNew();这是一个宏函数,这个函数用于创建一个二值信号,我们可以不用具体认识这个函数,因为我们是在CobeMX中可视化地创建二值信号的,当我们创建好后,会自动调用这个函数。
- 释放二值信号量:xSemaphoreGive();在二值函数被创建后是无法直接使用的,需要先将这个二值信号量释放,相当于刚创建的二值信号量是0,释放就相当于使其有效,也就是将其置为1。这个函数也可以用于释放计数信号量和互斥量。这个函数有4个参数:第一个参数是填入想要释放的句柄;第二个是需要向队列写入的数据,在二值信号这里配置为NULL;第三个是等待的节拍数,在二值信号这里不用等待,所以是宏定义常量semGIVE_BLOCK_TIME,即为0;第四个是表示写入队列的方向,这里函数直接定义为宏常量:queueSEND_TO_BACK,。如果函数返回pdTRUE表示释放成功,反之返回pdFALSE。
- 释放二值信号量的ISR版本为:xSemaphoreGiveFromISR();这个函数一共有两个参数,一个参数:xQueue表示传入参量的句柄;另一个参数:pxHigherPriorityTaskWoken是返回数据的指针,如果释放信号导致一个高等级任务解锁,返回pdTRUE,反之返回pdFALSE。如果返回pdTRUE,我们应该重新进行任务调度,使高等级的任务优先执行,这就需要在退出ISR之前调用portYIELD_FROM_ISR()函数使任务重新调用。
- 获取二值信号:xSemaphoreTake();这个函数的两个参数分别是:xSemaphore:用于表示信号的句柄(这个函数还可以用于计数信号量和互斥量);xBlockTime:等待时间。
认识这些基本函数后,我们开始进行第一个关于二值信号量的实验。
实验一:认识二值信号量
实验过程:创建一个二值信号量,使用ADC触发一个500ms的数据采集,然后释放信号,用一个任务接收这个信号,然后在LCD上显示。
CobeMX设置
这里我们使用到LCD与上次列表的相同,可以直接查看:https://mp.csdn.net/mp_blog/creation/editor/130230340
这里再配置定时器,使用定时器3定时500ms作为ADC的外部触发信号,具体设置如图:
图 1 TIM3设置
然后使用ADC1的通道5作为输入通道(PA5),使用右对齐,外部触发源选择Timer 3 Trigger Out event。要在中断模式下进行ADC数据采集还需要打开ADC1中断.。具体配置如图:
图 2 ADC1设置
完成设置后再设置FreeRTOS,这里启动FreeRTOS,接口选择CMSIS_V2,其他参数都保持默认,在Tasks and Queues页面设置任务(将默认任务修改):
图 3 FreeRTOS任务设置
在Timer and Semaphores页面设置一个二值信号,如图所示:
图 4 二值信号量设置
这些设置完成后再开启ADC1的中断,将其抢占优先级配置为4(只要小于5都可以)。
代码实现
这里的LCD部分和之前设置一样,可以看列表那的操作。
在主函数中我们需要开启ADC1的中断和TIM3的时钟,这里附上主函数的代码:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FSMC_Init();
MX_ADC1_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
LCD_Init();
LCD_ShowString(10, 0, 280, 16, 16, (uint8_t *)"this is the damo five!");
HAL_ADC_Start_IT(&hadc1); // 打开ADC中断
HAL_TIM_Base_Start(&htim3); // 打开定时器3
/* USER CODE END 2 */
osKernelInitialize(); /* Call init function for freertos objects (in freertos.c) */
MX_FREERTOS_Init();
osKernelStart();
while (1)
{
}
}
完成这些配置后,我们进入freertos.c文件实现函数主功能:
先在头文件处添加我们会用到几个头文件:
#include "lcd.h"
#include "semphr.h"
实现ADC的中断回调函数:HAL_ADC_ConvCpltCallback();在这个函数中我们要将ADC采集数据的获取和释放信号量以便于任务Task_Show()读取数据。具体代码如下:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance == ADC1)
{
adc_value = HAL_ADC_GetValue(hadc); // 获取ADC的电压值
BaseType_t HighTaskWoken= pdFALSE;
if(myBinSem_DateReadyHandle != NULL)
{
xSemaphoreGiveFromISR(myBinSem_DateReadyHandle,&HighTaskWoken);
portYIELD_FROM_ISR(HighTaskWoken); // 进行任务调度
}
}
}
完成后我们实现任务Task_Show()对数据的读取和显示,xSemaphoreTake()这个函数用于读取数据,然后实现数据转化为电压值(单位为mv)。这部分代码较简单,这里直接附上代码:
void Task_Show(void *argument)
{
/* USER CODE BEGIN Task_Show */
/* Infinite loop */
for(;;)
{
// 获取ADC的值
if(xSemaphoreTake(myBinSem_DateReadyHandle,portMAX_DELAY) == pdTRUE)
{
uint32_t tempValue = adc_value;
uint32_t valt = 3300 * tempValue; // 将这个值转换为电压值
valt = valt>>12;
LCD_ShowxNum(0, 60, valt, 5, 16, 0);
}
}
/* USER CODE END Task_Show */
}
这样就完成了整个代码的书写,烧录进开发板后可以看到电压值的变化,将其分别接到3.3v和0v,可以看到电压值的显示基本准确:
计数信号量
计数信号量就像一个餐厅一样,它可以同时容纳多个数据,计数信号量被创建时可以设置初值和最大值,一般设置这两个相等。最大值就是餐厅最多容纳的客人量,当餐厅坐满人后,剩下的数据就没办法存入了,必须等有客人离开才可以。
这里的客人其实就是访问的ISR或任务。
当有客人进店,其实就是获取信号量。
在有任务申请信号量时,可以设置等待时间,在等待时,任务进入阻塞状态。
当有客人离开,其实就是释放信号量。
二值信号量的使用示例
认识二值信号的各种函数
- 创建计量信号:osSemaphoreNew();这是一个宏函数,这个函数和创建二值信号的一样。
- 释放计量信号量:xSemaphoreGive();这个函数和释放二值信号的一样。释放信号就是释放资源,计数信号量的计数值加一,表示可用资源加一。
- 获取计量信号:xSemaphoreTake();这个函数和释放二值信号的一样。释放信号就是释放资源,计数信号量的计数值减一,表示可用资源减一。
认识这些基本函数后,我们开始进行第二个关于计数信号量的实验。
实验二:认识计数信号量
实验过程:创建一个计数信号量(初值为5),使用TIM的计时功能,当计时达到3s时,将信号量释放一个。当按下KEY1时,创建一个计量信号;利用LCD展示当前的剩余信号量和能否写入信号量。
CobeMX设置
这个实验和前一个实验基本配置都是LCD,然后打开TIM7,设置计数3s,具体配置如图:
图 5 TIM7设置
然后在NVIC中打开TIM7的中断,删除上一个实验使用的TIM3和ADC1。
加入一个按键KEY1,设置为GPIO_OutPut模式。
在freertos中Tasks and Queues页面设置任务将默认任务改成myTask_Checkln,具体配置如图:
图 6 freertos任务设置
在Timer and Semaphores页面设置一个计数信号量,如图所示:
图 7 freertos计数信号量设置
这些设置完成后就可以生成代码了。
代码实现
这里的LCD部分和之前设置一样,可以使用之前操作。
先在主函数中打开LCD的初始化和TIM7的中断,再测试LCD显示,主函数代码如下:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FSMC_Init();
MX_TIM7_Init();
LCD_Init();
LCD_ShowString(10, 0, 280, 16, 16, (uint8_t *)"this is the damo five!");
HAL_TIM_Base_Start_IT(&htim7); // 打开定时器7
osKernelInitialize(); /* Call init function for freertos objects (in freertos.c) */
MX_FREERTOS_Init();
osKernelStart();
while (1)
{
}
}
完成后进入freertos.c
在其中先实现TIM中断回调函数,在这个函数中主要完成对信号量的释放,具体代码如下:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */
/* USER CODE END Callback 0 */
if (htim->Instance == TIM6) {
HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */
if (htim->Instance == TIM7)
{
if(Sem_TablesHandle != NULL)
{
BaseType_t HighTaskWoken= pdFALSE;
xSemaphoreGiveFromISR(Sem_TablesHandle,&HighTaskWoken); // 释放信号量
portYIELD_FROM_ISR(HighTaskWoken); // 进行任务调度
}
}
/* USER CODE END Callback 1 */
}
注意:直接使用这个代码时会报错,提示在main函数和freertos里面都定义了一次,我们只需要将main函数中的这个回调函数注释掉,main函数中的回调函数就是开启TIM6作为系统时钟,我在上面代码处已经将其添加,所以直接注释就好。
接下来我们在任务中实现按下KEY1获取一个信号量并将信号量的个数实时显示在LCD上。具体代码如下:
void Task_Checkln(void *argument)
{
/* USER CODE BEGIN Task_Checkln */
UBaseType_t freeSpace = 0;
// 获取计量信号量初始剩余空间
UBaseType_t qSpaces = uxQueueSpacesAvailable(Sem_TablesHandle);
LCD_ShowString(10, 20, 280, 16, 16, (uint8_t *)"QueueSpaces = ");
LCD_ShowxNum(120, 20, qSpaces, 2, 16, 0);
/* Infinite loop */
for(;;)
{
// 读取是否按下KEY1,如果按下,使当前读取的桌子数减一
GPIO_PinState key_State= HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);
if(key_State == GPIO_PIN_RESET)
{
BaseType_t result = xSemaphoreTake(Sem_TablesHandle,100); // 设置等待时间为100ms
if(result == pdTRUE)
{
LCD_ShowString(10, 40, 280, 16, 16, (uint8_t *)"Check in OK ");
}
else
{
LCD_ShowString(10, 40, 280, 16, 16, (uint8_t *)"Check in fail ");
}
vTaskDelay(200); // 延时加消除抖动
}
// 当前餐桌数,等待读取的消息数
freeSpace = uxSemaphoreGetCount(Sem_TablesHandle);
LCD_ShowString(10, 60, 280, 16, 16, (uint8_t *)"freeSpace = ");
LCD_ShowxNum(100, 60, freeSpace, 2, 16, 0);
}
/* USER CODE END Task_Checkln */
}
将其烧录进开发板,我们可以看到当成功获取一个信号量,LCD上显示Check in OK,如果其达到最大信号值,就会显示Check in fail。当等待3s后就会释放一个信号量。