FreeRTOS学习笔记-第一个项目工程

本工程采用STM32F4单片机,实现的功能有:PC与HMI双串口通讯、TF卡文件系统

1 硬件介绍

本项目硬件包含如下模块:

  1. 串口,Usart1连接PC,Usart2连接HMI(陶晶驰)
  2. 5个按键作为外部中断输入,5个指示灯做输出
  3. 一个TF卡(SDIO接口)实现FATFS
  4. 一个状态灯功能为:初始化过程中指示异常,正常运行中显示CPU的闲忙指示
  5. 1个采用模拟IIC的EEPROM

2 工程框架介绍

与祼机系统不同,在OS中需要特别注意线程安全,在多串口通讯以及多任务中的串口数据处理都可能产生异常,下面介绍本工程的程序框架:

  1. 为每个串口创建两个队列(也叫邮箱),分别存放接收和要发送的数据(接收和发送都采用中断方式)。
  2. 编写一个线程安全的串口输出函数,采用互斥量做锁,可用在任意任务中,除了中断服务程序(中断中可以采用入队的方法将数据放到发送队列)。
  3. 创建一个专门处理串口接收的数据的任务,阻塞在任务通知状态,当发生串口中断,中断程序会执行入队操作,并发送任务通知,解除任务的阻塞状态,执行消息出队、解析操作。
  4. 创建一个存放log的队列。
  5. 编写一个线程安全的添加log的函数,采用互斥量做锁,可用在任何任务中,除了中断服务程序。
  6. 创建一个专门保存log的任务,优先级较低,阻塞在读log队列的状态,当队列中有数据,执行出队、保存log(如串口打印、保存到TF卡)的操作。

3 CubeMX配置

3.1 时钟配置

电路板采用外部晶振,RCC设置如下:
在这里插入图片描述
时钟配置如下:
在这里插入图片描述

3.2 GPIO配置

在这里插入图片描述
IIC的两个引脚需要设置为Open Drain模式,5个按键设置为外部中断

3.3 系统配置

在RTOS中,单片机硬件的SysTick时钟被强制用作时间片的产生时钟,因此需要另外选择一个定时器产生HAL库的时钟,否则HAL_Delay、HAL_GetTick函数都将无法使用。选择一个空闲的定时器即可
在这里插入图片描述

3.4 SDIO配置

选择4位宽总线通讯模式,提高通讯效率,其它参数保持默认即可
在这里插入图片描述
为SDIO分配DMA:
在这里插入图片描述
若硬件电路没有放置上拉电阻,可以在这里开启内部上拉:
在这里插入图片描述

3.5 FATFS配置

基本保持默认,开启必要的功能即可
在这里插入图片描述
在这里插入图片描述
选择TF卡插入检测的引脚:
在这里插入图片描述

3.6 FreeRTOS配置

基本参数如下配置
在这里插入图片描述
在这里插入图片描述
说明几个关键参数:

  1. TICK_RATE_HZ,此参数设置按时间片调度的周期,即SysTick的定时周期,设置100Hz表示时间片长度为10ms,若设置过短,会导致调度器占用较多的CPU时间,降低CPU效率,设置过长可能降低任务实时性,需要根据项目情况设置,一般20~100Hz都能满足项目要求。
  2. Memory Management scheme,选择动态内存分配的方案,FreeRTOS提供了5套动态内存管理方案,heap_4是比较好的一种
  3. USE_TRACE_FACILITY、USE_STATS_FORMATING_FUNCTION,使能这两项可以启用获取任务状态(函数vTaskList)功能,项目开发阶段非常实用,可以获取任务的状态、栈剩余等。

我们尽量不要用CubeMX创建任务、信号量、事件等,因为不方便修改,但CubeMX会强制创建一个默认的任务,为了不浪费这个任务,我们在该任务中执行初始化,动态创建项目需要各种任务、信号量等,执行完成后删除该任务以节省内存。
在这里插入图片描述
任务优先级要设置为最高,因为在该任务中会执行创建任务的程序,任务一旦创建即会加入就绪列表,会打断初始化程序。
任务的入口函数类型选择weak,这样我们只要在项目的c文件中实现“StartTask”这个函数即可,而不必修改freertos.c文件。
任务创建类型选择动态,只有动态创建的任务才可以删除。
其它标签页都不需要设置

3.7 中断配置

在这里插入图片描述
从这页可以看到,产生时间片的System tick timer的优先级为15最低
建议取消下面这两项的勾选,在串口的中断服务函数中将不再调用HAL的中断处理函数
在这里插入图片描述
至此,CubeMX配置完成,生成代码。

4 编写代码

代码生成后,main.c文件完全不需要修改,它会启动调度器,此时只有一个默认任务会执行,该任务的入口函数在freertos.c中,如下,前面有__weak的修饰符,表示该函数为虚函数。
在这里插入图片描述
我们不需要修改freertos.c文件,另外新建一个work.c文件,编写默认的启动任务函数:

//默认任务,仅用作初始化,优先级别最高,其中可以使用printf函数
void StartTask(void *argument)
{
	printf("System started\r\n");
	printf("Start initialization\r\n");
	
	MyMSH_ExecInit();		//执行自动初始化,执行所有已注册的初始化函数();
	
	printf("Initialization complete!");
	vTaskDelete(NULL);		//初始化完成,删除本任务
}

值得注意的的,该任务的优先级最高,相当于挂起了任务调度器,因此在该任务中还可以使用printf函数。
初始化完成后执行vTaskDelete函数,参数为NULL表示删除自身,任务被删除后,其它就绪态的任务才会开始执行。
项目工程共需建立如下几个文件:
1.work.c --项目的功能基本都在这时实现
2.work.h
3.init.c --项目的初始化程序文件,MyMSH_ExecInit()要执行的函数都在这里
4.MyPrintf.c --串口打印和添加Log的函数
5.MyPrintf.h
6.file.c --文件操作的程序文件
需要添加的标准函数库有:
1.MyMSH.lib --实现自动初始化函数MyMSH_ExecInit()和执行命令的函数MyMSH_ExecCmd
2.MyIIC.lib --模拟IIC总线的函数库
下面开始按模块编写程序

4.1 任务初始化

前面讲了,要在默认任务中动态创建所有任务、信号量、事件等,这些创建全都在init.c文件中实现,举例如下:

4.1.1 创建互斥量

//信号量、互斥量初始化
void Semaphore_Init(void)
{
	Mutex_Printf = xSemaphoreCreateMutex();
	if (Mutex_Printf == NULL)
	{
		Error_Handle(0, "Creat Mutex_Printf failed!\r\n");
	}
	printf ("Mutex, Free heap:%d\r\n", xPortGetFreeHeapSize());
}
MSH_INIT_EXPORT(1, Semaphore_Init, Creat all Semaphore);

注1:Error_Handle()函数将会执行初始化异常时的错误处理。
注2:xPortGetFreeHeapSize()函数用于获取空闲堆大小,适用于程序调试阶段。
注3:通过宏MSH_INIT_EXPORT注册过的函数,都将自动通过MyMSH_ExecInit()函数调用。

4.1.2 创建消息队列(邮箱)

//队列初始化,创建要使用的所有队列
void Queue_Init(void)
{
	Queue_PC_RX = xQueueCreate(200, 1);											//创建存放从PC接收到的数据的消息队列
	if (Queue_PC_RX == NULL)
	{
		Error_Handle(0, "Creat PC receive queue failed!\r\n");
	}
	printf ("Queue, Free heap:%d\r\n", xPortGetFreeHeapSize());
	__HAL_UART_ENABLE_IT(&UART_PC, UART_IT_RXNE);								//创建成功后,开启接收中断
	
	Queue_PC_TX = xQueueCreate(200, 1);											//创建存放要发送到PC的数据的消息队列
	if (Queue_PC_TX == NULL)
	{
		Error_Handle(0, "Creat PC send queue failed!\r\n");
	}
	printf ("Queue, Free heap:%d\r\n", xPortGetFreeHeapSize());
	
	Queue_HMI_RX = xQueueCreate(200, 1);										//创建存放从HMI接收到的数据的消息队列
	if (Queue_HMI_RX == NULL)
	{
		Error_Handle(0, "Creat HMI receive queue failed!\r\n");
	}
	printf ("Queue, Free heap:%d\r\n", xPortGetFreeHeapSize());
	__HAL_UART_ENABLE_IT(&UART_HMI, UART_IT_RXNE);								//创建成功后,开启接收中断
	
	Queue_HMI_TX = xQueueCreate(200, 1);										//创建存放要发送到HMI的数据的消息队列
	if (Queue_HMI_TX == NULL)
	{
		Error_Handle(0, "Creat HMI send queue failed!\r\n");
	}
	printf ("Queue, Free heap:%d\r\n", xPortGetFreeHeapSize());
	
	Queue_Log = xQueueCreate(200, 4);											//创建存放log的消息队列
	if (Queue_Log == NULL)
	{
		Error_Handle(0, "Creat log queue failed!");
	}
	printf ("Queue, Free heap:%d\r\n", xPortGetFreeHeapSize());
}
MSH_INIT_EXPORT(1, Queue_Init, Fifo for PC and HMI init);

创建4个存放串口接收和发送数据的消息队列,本程序中在创建完队列后才开启串口接收中断,__HAL_UART_ENABLE_IT(&UART_HMI, UART_IT_RXNE),也可以放在其它地方开启。

4.1.3 创建事件组

//事件初始化
void Even_Init(void)
{
	Events = xEventGroupCreate();
 
	if (Events == NULL) 
	{
		Error_Handle(0, "Start timer failed!");
	}
	printf ("Events, Free heap:%d\r\n", xPortGetFreeHeapSize());
}
MSH_INIT_EXPORT(1, Even_Init, Creat all events);

4.1.4 创建任务

//任务创建函数
void Task_Init(void)
{
	BaseType_t xReturn;
	extern void StartTaskMain(void *argument);
	xReturn = xTaskCreate(
		StartTaskMain,
		"Main task",
		128,
		NULL,
		50,
		&Task_Main);
	if (xReturn != pdPASS)
	{
		Error_Handle(0, "Creat Task_Main failed!");
	}
	printf ("Task, Free heap:%d\r\n", xPortGetFreeHeapSize());
	
	extern void StartTaskUart(void *argument);
	xReturn = xTaskCreate(
		StartTaskUart,
		"Uart task",
		384,
		NULL,
		32,
		&Task_Uart);
	if (xReturn != pdPASS)
	{
		Error_Handle(0, "Creat Task_Uart failed!");
	}
	printf ("Task, Free heap:%d\r\n", xPortGetFreeHeapSize());
	
	extern void StartTaskLog(void *argument);
	xReturn = xTaskCreate(
		StartTaskLog,
		"Log task",
		200,
		NULL,
		10,
		&Task_Log);
	if (xReturn != pdPASS)
	{
		Error_Handle(0, "Creat Task_Log failed!");
	}
	printf ("Task, Free heap:%d\r\n", xPortGetFreeHeapSize());
}
MSH_INIT_EXPORT(4, Task_Init, Creat all tasks);

这里创建了三个任务:Task_Main、Task_Uart、Task_Log,注意优先级,数字越大,优先级越高。
其它如IIC、文件系统的初始化不一一列举。

4.2 串口数据接收和发送

4.2.1 串口接收

串口1和串口2的数据接收都在中断中完成,打开 stm32f4xx_it.c 文件,找到 USART1_IRQHandler 函数,在其中编写数据接收代码:

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	uint8_t temp;
	uint32_t state = huart1.Instance->SR;
	BaseType_t TaskWoken = pdFALSE;												//使用之前必须初始化为False
	if (state & USART_SR_RXNE)													//发生了接收中断
	{																			//接收中断标志在读一次SR和DR后自动清除
		temp = (uint8_t)huart1.Instance->DR;									//取出串口收到的数据
		
		if (Queue_PC_RX == NULL) return;
		xQueueSendFromISR(Queue_PC_RX, &temp, NULL);							//消息入队
		xTaskNotifyFromISR(Task_Uart, EVENT_PC_RECV, eSetBits, &TaskWoken);		//发送任务通知
		portYIELD_FROM_ISR(TaskWoken);											//如果TaskWoken为true,进行一次上下文切换
	}
  /* USER CODE END USART1_IRQn 0 */
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}
  1. xQueueSendFromISR,用于在中断服务程序中向队列添加一个元素。
  2. xTaskNotifyFromISR,用于在中断服务程序中发送任务通知,通知Task_Uart任务串口已收到数据,当Task_Uart任务得到执行机会时会执行出队和解析操作。
  3. portYIELD_FROM_ISR函数用于执行任务切换,由于usart1的中断打断了当前正在执行的任务,比如taskA,xTaskNotifyFromISR函数使得Task_Uart解除阻塞态,若Task_Uart的优先级比taskA高,则中断退出后程序仍然会继续执行taskA,直到再次触发任务切换,这就导致了Task_Uart没有及时得到执行的机会,为了解决该问题,FreeRTOS在xTaskNotifyFromISR函数中增加了一个参数用于记录函数执行过程中是否触发了将更高优先级由阻塞态恢复到就绪态的操作,若发生了,则将TaskWoken变量设置为pdTRUE,这样在中断退出前执行一次任务切换即可使高优先级任务得到执行的机会。
    另外,执行portYIELD_FROM_ISR函数并不会立即触发任务切换,而仅是标记一次pendSV中断标志,而pendSV的中断优先级同SystemTick一样,都是最低,因此退出当前中断服务程序后PendSV中断程序才会执行。
    同样的方法,编写串口2的接收程序:
void USART2_IRQHandler(void)
{
  /* USER CODE BEGIN USART2_IRQn 0 */
	uint8_t temp;
	uint32_t state = huart2.Instance->SR;
	BaseType_t TaskWoken = pdFALSE;												//使用之前必须初始化为False
	if (state & USART_SR_RXNE)													//发生了接收中断
	{																			//接收中断标志在读一次SR和DR后自动清除
		temp = (uint8_t)huart2.Instance->DR;									//取出串口收到的数据
		
		if (Queue_HMI_RX == NULL) return;
		xQueueSendFromISR(Queue_HMI_RX, &temp, NULL);							//消息入队
		xTaskNotifyFromISR(Task_Uart, EVENT_HMI_RECV, eSetBits, &TaskWoken);	//发送任务通知
		portYIELD_FROM_ISR(TaskWoken);											//如果TaskWoken为true,进行一次上下文切换
	}
  /* USER CODE END USART2_IRQn 0 */
  /* USER CODE BEGIN USART2_IRQn 1 */

  /* USER CODE END USART2_IRQn 1 */
}

4.2.2 串口发送

在多线程操作系统中,要避免使用阻塞式的串口数据发送方法,应采用中断方式发送,串口的接收和发送中断入口地址相同,因此还要在上面两个中断服务程序中增加发送的部分代码,下面仅展示Usart1的发送代码:

	if (state & USART_SR_TXE)													//发生了发送中断
	{																			//发送中断标志在读一次SR和写一次DR后自动清除
		if (Queue_PC_TX == NULL) return;
		if (xQueueReceiveFromISR(Queue_PC_TX, &temp, NULL) == pdTRUE)			//从发送队列中取数据
		{
			huart1.Instance->DR = temp;
		}
		else
		{
			__HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);
		}
	}

将此段代码续写在 if (state & USART_SR_RXNE) 函数体后面即可。

4.3 重写printf函数

4.3.1 线程安全的printf函数

sdtio标准库中的printf函数是非线程安全的,包含sprintf函数也是如此,当多个任务都会调用printf函数时,可能会使输出的数据出现错乱,因此需要重写一个线程安全的printf函数。
打开 MyPrintf.c 文件,编写如下代码:

//线程安全的串口打印函数,一次最多200个字符
HAL_StatusTypeDef Safe_Printf(UART_HandleTypeDef * UART_Handle, char * format, ...)
{
	char buffer[200];
	BaseType_t Ret;
	Ret = xSemaphoreTake(Mutex_Printf, portMAX_DELAY);							//获取互斥锁
	if (Ret != pdTRUE) return HAL_TIMEOUT;										//互斥量获取失败,返回超时
	
	va_list ap;
	va_start(ap, format);
	vsprintf(buffer, format, ap);
	va_end(ap);
	
	for (uint8_t i = 0; i < 200; i++)											//将消息内容全部加入消息队列
	{
		if (buffer[i] == '\0') break;											//读取结束
		
		if (UART_Handle == &UART_PC)
		{
			Ret = xQueueSend(Queue_PC_TX, &buffer[i], 100);
		}
		else if (UART_Handle == &UART_HMI)
		{
			Ret = xQueueSend(Queue_HMI_TX, &buffer[i], 100);
		}
		
		if (Ret != pdPASS) break;												//入队失败,退出
	}
	__HAL_UART_ENABLE_IT(UART_Handle, UART_IT_TXE);								//使能发送中断,即置位TXEIE

	xSemaphoreGive(Mutex_Printf);												//释放互斥锁
	
	return Ret == pdPASS ? HAL_OK : HAL_ERROR;
}

程序内容不作细讲,都比较容易理解,注意下此处使用的互斥量:Mutex_Printf。
实现了该函数,我们还可以写一些宏也简化项目程序,打开 work.h ,编写如下宏:

extern  UART_HandleTypeDef 			huart1;
extern  UART_HandleTypeDef 			huart2;
#define UART_PC						huart1								//指定PC使用的串口
#define UART_HMI					huart2								//指定HMI使用的串口

//打印信息到PC
#define Printf_To_PC(format,...)	Safe_Printf(&UART_PC, format, ##__VA_ARGS__)
//打印信息到HMI
#define Printf_To_HMI(format,...)	Safe_Printf(&UART_HMI, format, ##__VA_ARGS__)

由于采用了中断式发送,Safe_Printf 函数将要发送的数据放入发送队列中,在串口的发送中断服务程序中取队列发送,因此不依赖标准库的 fputc 函数,但我们在写初始化程序时,需要打印一些信息,而此时由于调度器挂起或创建队列失败等,而无法使用 Safe_Printf 函数,因此仍然使用了printf函数,因此我们仍然需要重写 fputc 函数:

//printf重定向, 只有printf函数会调用该函数,其它场合都用中断式发送
int fputc(int ch, FILE *f)
{
	uint8_t temp = (uint8_t)ch;
	HAL_UART_Transmit(&UART_PC, &temp, 1, 10);
    return ch;
}

4.3.2 添加Log函数

系统log一般都是一行一行保存,每行长度不一,且也会添加一些前缀如日期、时间、log类型等,前面在创建任务的时候,Task_Log任务的优先级很低,这意味着,若要将每条log都存入队列,系统的开销将会很高,因此我采用的方案是,要添加log时申请一块内存保存log,将这块内存的地址添加到队列中,在Task_Log任务中,每取出一个队列成员,即log地址,保存或输出一条,然后释放内存,这种机制的效率比较高。
打开 MyPrintf.h 文件,编写下面一个log类型,一个log结构体:

typedef enum								//定义5种log类型
{
	LogType_Debug,
	LogType_Info,
	LogType_Warn,
	LogType_Error,
	LogType_Fatal
}LogType_t;

typedef struct
{
	const char * 	LogType;
	uint32_t 		TimeTick;
	const char *	TaskName;
	char 			LogMsg[128];
}MyLog_t;

每条log的消息长度最多128个字符,包含结束符,实际有效字符长度127字节。
打开 MyPrintf.c 文件,编写添加log的代码:

const char *LogType[5] = {"Debug", "Info", "Warning", "Error", "Fault"};

//生成一条Log,添加到log队列中,每条Log的消息长度最多128字符
HAL_StatusTypeDef AddLog(LogType_t type, char * format, ...)
{
	BaseType_t Ret;
	Ret = xSemaphoreTake(Mutex_Printf, 100);									//获取互斥锁
	if (Ret != pdTRUE) return HAL_TIMEOUT;										//互斥量获取失败,返回超时
	
	MyLog_t *Log = pvPortMalloc(sizeof(MyLog_t));								//申请内存,创建一条新Log
	if (Log == NULL)															//内存申请失败,退出
	{
		xSemaphoreGive(Mutex_Printf);
		return HAL_ERROR;
	}
	
	Log->LogType = LogType[type];												//设置Log类型
	Log->TimeTick = HAL_GetTick();												//获取时间戳
	Log->TaskName = pcTaskGetName(NULL);										//获取当前运行的任务的任务名
	
	va_list ap;
	va_start(ap, format);
	vsprintf(Log->LogMsg, format, ap);											//生成Log消息内容
	va_end(ap);
	
	Ret = xQueueSend(Queue_Log, &Log, pdMS_TO_TICKS(100));										//Log入队,等待保存Log的任务取队列

	xSemaphoreGive(Mutex_Printf);												//释放互斥锁
	
	return Ret == pdPASS ? HAL_OK : HAL_ERROR;
}

AddLog与Safe_Printf函数都不可重入,因此需要用互斥锁保证不可重入,并且其中都调用了不可重入的vsprintf函数,因此两个函数都用同一个互斥量当作锁。

4.4 编写任务函数

4.4.1 串口数据处理

打开 work.c 文件,编写串口数据处理任务:

//线程:串口数据处理线程
void StartTaskUart(void *argument)
{
	char cmd_PC[50], cmd_HMI[50];												//存放接收到的命令
	uint8_t cmd_PC_Len = 0, cmd_HMI_Len = 0, temp;
	EventBits_t EventBits;
	while (1)
	{		
		xTaskNotifyWait(														//等待有串口数据到达
			0x00,																//进入时不清除任何标志位
			EVENT_PC_RECV | EVENT_HMI_RECV,										//退出时清除相关标志位
			&EventBits,
			portMAX_DELAY);

		if (EventBits & EVENT_PC_RECV)
		{
			Sender = &UART_PC;
			while (xQueueReceive(Queue_PC_RX, &temp, 0) == pdTRUE)				//从接收队列中连续取所有数据
			{
				if (temp == 0x0D)												//接收到\r表示回车
				{
					cmd_PC[cmd_PC_Len] = '\0';									//因为buffer没有清理过,所以需要强制添加结束符
					if (MyMSH_ExecCmd(cmd_PC) == 0)								//执行命令,若是未识别的命令返回0
					{
						Printf_To_PC("NG,%s,Undefined command!\r\n@_@", cmd_PC);
					}
					cmd_PC_Len = 0;
					
				}
				else if (temp == 0x0A)											//收到了\n,不做任何处理
				{
				}
				else 															//接收到有效数据,存入数组,长度加1
				{
					cmd_PC[cmd_PC_Len++] = (char)temp;
					if (cmd_PC_Len >= sizeof(cmd_PC) - 1) cmd_PC_Len = 0;
				}
			}
		}
		if (EventBits & EVENT_HMI_RECV)											//如果有HMI接收完成的事件
		{
			Sender = &UART_HMI;													//
			while (xQueueReceive(Queue_HMI_RX, &temp, 0) == pdTRUE)				//从接收队列中连续取所有数据
			{
				if (temp >= 0x7F)												//非法字符不做处理
				{
				}
				else if (temp == 0x0D)											//接收到\r, 表示接收完成
				{
					cmd_HMI[cmd_HMI_Len] = '\0';								//因为cmd_HMI有清理过,所以需要强制添加结束符
					if (MyMSH_ExecCmd(cmd_HMI) == 0)							//执行命令,若是未识别的命令返回0
					{
						Printf_To_HMI("%s=\"Undefined command!\"%s", HMI_Ctr_UartRcv, HMI_End);
					}
					cmd_HMI_Len = 0;
				}
				else if (temp == 0x0A)											//收到了\n,不做任何处理
				{
				}
				else 															//接收到有效数据,存入数组,长度加1
				{
					cmd_HMI[cmd_HMI_Len++] = (char)temp;
					if (cmd_HMI_Len >= sizeof(cmd_HMI) - 1) cmd_HMI_Len = 0;
				}
			}
		}
	}
}

若串口没有接收到数据,该任务将阻塞在等待任务通知的状态中,持续挂起,不会得到执行的机会。
当串口收到数据,串口中断会发送任务通知,解除该任务的阻塞态,然后判断是收到了哪个串口的数据,进而进行解析和执行,解析的操作都由MyMSH_ExecCmd函数完成,MyMSH_ExecCmd函数会自动寻找命令对应的函数并执行,若未找到,刚返回0。

4.4.2 Log处理任务

当Queue_Log队列中无消息时,Task_Log任务会持续阻塞在等待消息队列的状态中,不会得到执行的机会,当队列中有数据时,会解除阻塞态,然后开始执行出队列、保存log的操作。

//线程:Log保存
//从Log队列中取出消息,发送或保存,并释放内存
void StartTaskLog(void *argument)
{
	MyLog_t *Log;
	uint8_t days, hours, mins, sends;
	uint16_t msends;
	BaseType_t Ret;
	while (1)
	{
		Ret = xQueueReceive(Queue_Log, &Log, portMAX_DELAY);					//线程阻塞在读取Log队列的状态中
		if (Ret == pdTRUE)														//读队列成功
		{
			days = (uint8_t)(Log->TimeTick / 86400000);
			Log->TimeTick %= 86400000;
			hours = (uint8_t)(Log->TimeTick / 3600000);
			Log->TimeTick %= 3600000;
			mins = (uint8_t)(Log->TimeTick / 60000);
			Log->TimeTick %= 60000;
			sends = (uint8_t)(Log->TimeTick / 1000);
			Log->TimeTick %= 1000;
			msends = (uint16_t)Log->TimeTick;
			
			Printf_To_PC("%2hhu %02hhu:%02hhu:%02hhu:%03hu  [%s] [%s]: %s\r\n", 
				days, hours, mins, sends, msends,
				Log->LogType, 
				Log->TaskName, 
				Log->LogMsg);
			
			vPortFree(Log);														//释放内存
		}
	}
}

执行完之后切记释放内存。

4.4.3 主任务

目前主任务仅用于显示5个按键的状态,当按键按下时发送“Key Down”,按键松开时发送“Key Up”,5个按键随意操作,系统不会遗漏任何一次事件。
要实现该功能,必然要采用外部中断,在work.c中编写外部中断的回调函数:

//外部中断的回调函数,快进快出,不能阻塞
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	BaseType_t TaskWoken;
	uint32_t EventBits = GPIO_Pin;												//GPIO的值与各个按键的事件值相同
	if (HAL_GPIO_ReadPin(GPIOF, GPIO_Pin) == GPIO_PIN_SET)						//如果是UP事件,事件值需左移5位
	{
		EventBits <<= 5;
	}
	TaskWoken = pdFALSE;														//使用之前必须初始化为False
	xEventGroupSetBitsFromISR(Events, EventBits, &TaskWoken);					//置位事件
	portYIELD_FROM_ISR(TaskWoken);												//如果TaskWoken为true,进行一次上下文切换
}

无论哪个按键按下,中断服务程序都会调用该函数,传入的Pin参数都是2的冥次,正好符合事件组的事件标志位的特点,因此直接将GPIO_Pin作为事件标志即可,若是抬起事件,则将整个值左移5位。
然后编写主任务:

//线程:主任务
void StartTaskMain(void *argument)
{
	EventBits_t EventBits;
	while (1)
	{
		EventBits = xEventGroupWaitBits(Events, 								//等待按键事件
			EVENT_ALL_KEY, 														//任意一个事件触发都有效
			pdTRUE,																//读取事件后清除该事件
			pdFALSE,															//任一事件满足
			portMAX_DELAY);														//等待时间,多少个系统Tick
		
		if (EventBits & EVENT_KEY1_DOWN)										//处理Key1的按下事件
		{
			AddLog(LogType_Debug, "Key1 Down");
			HAL_Delay(100);
		}
		if (EventBits & EVENT_KEY1_UP)											//处理Key1的松开事件
		{
			AddLog(LogType_Debug, "Key1 UP");
			HAL_Delay(200);
		}
		if (EventBits & EVENT_KEY2_DOWN)										//处理Key2的按下事件
		{
			AddLog(LogType_Debug, "Key2 Down");
			HAL_Delay(100);
		}
		if (EventBits & EVENT_KEY2_UP)											//处理Key2的松开事件
		{
			AddLog(LogType_Debug, "Key2 UP");
			HAL_Delay(200);
		}
		if (EventBits & EVENT_KEY3_DOWN)										//处理Key3的按下事件
		{
			AddLog(LogType_Debug, "Key3 Down");
			HAL_Delay(100);
		}
		if (EventBits & EVENT_KEY3_UP)											//处理Key3的松开事件
		{
			AddLog(LogType_Debug, "Key3 UP");
			HAL_Delay(200);
		}
		if (EventBits & EVENT_KEY4_DOWN)										//处理Key4的按下事件
		{
			AddLog(LogType_Debug, "Key4 Down");
			HAL_Delay(100);
		}
		if (EventBits & EVENT_KEY4_UP)											//处理Key4的松开事件
		{
			AddLog(LogType_Debug, "Key4 UP");
			HAL_Delay(200);
		}
		if (EventBits & EVENT_KEY5_DOWN)										//处理Key5的按下事件
		{
			AddLog(LogType_Debug, "Key5 Down");
			HAL_Delay(100);
		}
		if (EventBits & EVENT_KEY5_UP)											//处理Key5的松开事件
		{
			AddLog(LogType_Debug, "Key5 UP");
			HAL_Delay(200);
		}
	}
}

在任务中模拟了按键事件的处理时间,若是按下,延时0.1秒,抬起则延时0.2秒,用于检验系统是否可靠。

4.4.4 编写状态灯控制程序

本系统状态灯实现为当CPU空闲时(处理空闲任务)灯灭,CPU忙时(处理其它任务)灯亮,这样从状态灯即可判断CPU利用率。
要实现该功能,有多种方法,如开启一个ms级定时器,在定时中断中判断当前任务是否是空闲任务,采用这种方法的准确性不好,因为若定时周期长则准确性差,定时周期短则系统开销大,这里找了一个方法,将状态灯的控制放在任务切换函数中,首先编写下面状态灯控制函数:

void IdleOrBusyLED(uint32_t IsIdle)
{
	LED_STATE = IsIdle;
}

然后打开 tasks.c 文件,找到 vTaskSwitchContext 函数,在函数下面添加如下代码:

extern void IdleOrBusyLED(uint32_t IsIdle);
IdleOrBusyLED(pxCurrentTCB->uxPriority == 0);

如下图:
在这里插入图片描述

5 测试

至此,程序框架性工作基本都已完成,可以在work.c 中编写下面的函数测试:

//命令,获取所有任务清单
void Task_State(char * Payload)
{
	char Info[256];
	vTaskList(Info);
	Printf_To_PC("  任务名    任务状态  优先级  剩余栈 任务序号\r\n");
	Printf_To_PC("%s", Info);
}
MSH_CMD_EXPORT(Task_State, Get state of all tasks);

使用MSH_CMD_EXPORT注册的函数,都会被 MyMSH_ExecCmd 函数找到并执行。

//命令,读设备ID
void Get_ID(char * Payload)
{
	if (DevInfo.ID[0] != 0)														//检查是否已设置了ID
	{
		Printf_If_PC("OK,%s,%s\r\n@_@", __func__, DevInfo.ID);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_Value, DevInfo.ID, HMI_End);
	}
	else
	{
		Printf_If_PC("NG,%s,No ID\r\n@_@", __func__);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_Value, "No ID", HMI_End);
	}
}
MSH_CMD_EXPORT(Get_ID, Get fixture ID);

其它命令不一一展示,最后再展示下串口助手的显示结果:
在这里插入图片描述
可以看出一切正常,且HMI的操作也能成功。

本工程主要为了讲解程序框架,细节之处并未过多体现,见谅!
后续有空会继续添加其它模块的程序,比如IIC、FATFS。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值