最近在学习基于STM32的FreeRTOS的知识,学习了大概有2周,听课一周,看书一周,学完之后,根据教程做了一个稍微综合一点的实验,发文来记录一下。文章中多有疏漏,念在初学者和第一次发文的份上,请见谅并指出其中有误之处。
一、期望实现的目标
利用手头上的野火F103指南者开发板、BH1750光照强度传感器和0.96OLED显示屏,实现:
1. 读取BH1750的数据,在OLED屏幕上显示出来并在串口助手打印;
2. 往串口助手发送数据,根据发送的数据,点亮响应的LED灯,并将接收的数据再打印出来;
3. 根据按键K1和K2的切换,点亮/熄灭LED灯。
4. 统计CPU(MCU)的利用率。
二、目标分析
1.BH1750+OLED验证
BH1750传感器数据采集和IIC OLED显示屏的显示可以利用裸机来测试程序的正确性。
2.数据接收和LED灯的点亮
数据的接收和对应LED灯的点亮,可以使用串口中断进行数据的接收,我用的是串口DMA+空闲中断,前两天刚刚学了DMA,因此又来复习了一遍。在FreeRTOS里,通过在中断函数里,发出二值信号量,通知任务函数有数据的到来,解除阻塞状态,进行数据的处理。
3.按键的切换
按键状态的切换利用外部中断EXTI进行处理,在中断处理函数里,调用消息队列,向任务函数发送不定长的消息,进而进行按键状态的判断。
4. CPU利用率统计
CPU利用率的统计是通过消耗一个高精度的定时器测量每个任务占用 CPU 的时间,这个定时器定时的精度是系统时钟节拍的 10-20 倍,比如当前系统时钟节拍是 1000HZ,那么定时器的计数节拍就要是 10000-20000HZ。
三、开始实验
1. BH1750光照传感器和IIC OLED显示屏的裸机测试
这部分就不多说了,程序可供参考。
链接: https://pan.baidu.com/s/1DBQh68g4qolam-Vx-Ihezw 提取码: 3q4g 复制这段内容后打开百度网盘手机App,操作更方便哦
2. FreeRTOS的移植
野火的移植模板
链接: https://pan.baidu.com/s/1fxZcAJnJJDtTnNpba_86nQ 提取码: x6cp 复制这段内容后打开百度网盘手机App,操作更方便哦
注意事项:
-
需要包含的文件
将FreeRTOSConfig.h添加到USER分组
添加FreeRTOS/src和FreeRTOS/port两个文件夹,将对应的文件添加进去,在魔术棒选项卡添加文件路径。
如下图所示。
-
修改FreeRTOSConfig.h
太多了,直接看上面链接里的文件。 -
修改 stm32f10x_it.c
-
包含头文件
#include "stm32f10x_it.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
/* 开发板硬件bsp头文件 */
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.h"
#include "bsp_exti.h"
#include "base_tim.h"
- 去掉原本的SysTick定时器的延时实现
FreeRTOS 帮我们实现了 SysTick 的启动的配置:在 port.c 文件中已经实现 vPortSetupTimerInterrupt()函数,并且 FreeRTOS 通用的 SysTick 中断服务函数也实现了:在 port.c 文件中已经实现 xPortSysTickHandler()函数,所以移植的时候只需要我们在 stm32f10x_it.c 文件中实现我们对应(STM32)平台上的 SysTick_Handler()函数即可。
extern void xPortSysTickHandler(void);
//systick中断服务函数
void SysTick_Handler(void)
{
#if (INCLUDE_xTaskGetSchedulerState == 1 )
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
#endif /* INCLUDE_xTaskGetSchedulerState */
xPortSysTickHandler();
#if (INCLUDE_xTaskGetSchedulerState == 1 )
}
#endif /* INCLUDE_xTaskGetSchedulerState */
}
- 注 释 掉PendSV_Handler()与 SVC_Handler()这两个函数
FreeRTOS 在 port.c 文件中已经实现 xPortPendSVHandler()与 vPortSVCHandler()函 数 。
3. 裸机程序的一些修改
BH1750和OLED都是采用的IIC协议,因此,在读写时序里,需要延时的实现,而原本的裸机中使用的SysTick延时已经被去掉,因此不能按照之前的方式调用延时。
这里提供一种延时方法,相对精确一些。
static void i2c_Delay(void)
{
uint8_t i;
/*
下面的时间是通过逻辑分析仪测试得到的。
工作条件:CPU主频72MHz ,MDK编译环境,1级优化
循环次数为10时,SCL频率 = 205KHz
循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us
循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us
*/
for (i = 0; i < 10; i++);
}
可以再调用一次循环,实现相应的延时。
4. 正式进入FreeRTOS
- 头文件的包含
//硬件头文件
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_bh1750.h"
#include "oled.h"
#include "bsp_usart.h"
#include "bsp_key.h"
#include "bsp_exti.h"
#include "base_tim.h"
//FreeRTOS 头文件
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
//标准库头文件
#include <string.h>
- 将硬件的初始化放在一个函数里进行封装
void Hardware_Init(void)
{
SystemInit();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
USART_Config();
LED_GPIO_Config();
Key_GPIO_Config();
BASIC_TIM_Init();
EXTI_Key_Config();
USARTx_DMA_Config();
OLED_Init();
BH1750_Init();
OLED_ColorTurn(0);//0正常显示,1 反色显示
OLED_DisplayTurn(0);//0正常显示 1 屏幕翻转显示
OLED_Refresh();
}
- 根据前面的分析创建任务
3.1首先创建任务句柄
任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么这个句柄可以为NULL。
static TaskHandle_t AppTaskCreate_Handle = NULL;//创建任务句柄
static TaskHandle_t KEY_Task_Handle = NULL;//KEY 任务句柄
static TaskHandle_t OLED_Show_Task_Handle = NULL ; //OLED 显示任务句柄
static TaskHandle_t LED_Task_Handle = NULL;//LED显示任务句柄
static TaskHandle_t CPU_Task_Handle = NULL;//CPU利用率统计任务句柄
3.2 信号量和消息队列的创建
二值信号量是任务间、 任务与中断间同步的重要手段。通过二值信号量来将中断发生的事件通知给任务,解除阻塞态,执行相应的操作。
下面是野火教程里的原话。
在多任务系统中,我们经常会使用这个二值信号量,比如,某个任务需要等待一个标
记,那么任务可以在轮询中查询这个标记有没有被置位, 但是这样子做,就会很消耗 CPU
资源并且妨碍其它任务执行, 更好的做法是任务的大部分时间处于阻塞状态(允许其它任
务执行),直到某些事件发生该任务才被唤醒去执行。可以使用二进制信号量实现这种同
步, 当任务取信号量时,因为此时尚未发生特定事件,信号量为空,任务会进入阻塞状态;
当事件的条件满足后,任务/中断便会释放信号量, 告知任务这个事件发生了, 任务取得信
号量便被唤醒去执行对应的操作,任务执行完毕并不需要归还信号量, 这样子的 CPU 的效
率可以大大提高, 而且实时响应也是最快的。
消息队列
我的理解是,消息队列比信号量更高级一点,在同步数据的同时可以收发数据。
队列又称消息队列,是一种常用于任务间通信的数据结构, 队列可以在任务与任务间、
中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务
能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可
以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞
状态以等待队列数据有效。 当队列中有新消息时, 被阻塞的任务会被唤醒并处理新消息;
当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态
转为就绪态。 消息队列是一种异步的通信方式。
SemaphoreHandle_t BinarySem_Handle =NULL; //二值信号量句柄
QueueHandle_t LED_Queue = NULL;//消息队列句柄
3.3 全局变量的声明
char buffertemp[100] = {0};//BH1750数据的缓存
extern char Usart_Rx_Buf[USART_RBUFF_SIZE];//串口接收数据缓存
3.4 main函数
在main()函数里,进行硬件的初始化,创建一个启动任务,启动调度器,然后在启动任务里面创建各种应
用任务,当所有任务都创建成功后,启动任务把自己删除。
int main(void)
{
BaseType_t xReturn = pdPASS;
Hardware_Init();
printf("这是一个FreeRTOS测试工程\r\n");
xReturn = xTaskCreate((TaskFunction_t) AppTaskCreate,
(const char * ) "AppTaskCreate",
(u16 )512, //任务栈大小
(void * )NULL,
(UBaseType_t )1,
(TaskHandle_t *)&AppTaskCreate_Handle);
if(pdPASS == xReturn)
vTaskStartScheduler();//启动任务,开启调度
else
return -1;
while(1);
}
3.5 启动任务
用启动任务来管理其他创建的任务。
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;
taskENTER_CRITICAL();//进入临界区
BinarySem_Handle = xSemaphoreCreateBinary();//创建二值信号量
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功\r\n");
LED_Queue = xQueueCreate((UBaseType_t )QUEUE_LEN,
(UBaseType_t )QUEUE_SIZE );
if(NULL != LED_Queue)
printf("LED_Queue消息队列创建成功\r\n");
xReturn = xTaskCreate((TaskFunction_t )KEY_Task,
(const char* )"KEY_Task",
(u16 )512,
(void* )NULL,
(UBaseType_t )3,
(TaskHandle_t )&KEY_Task_Handle);
if(pdPASS == xReturn)
printf("创建KEY_Task任务成功\r\n");
xReturn = xTaskCreate((TaskFunction_t )OLED_Show_Task,
(const char* )"OLED_Show_Task",
(u16 )512,
(void* )NULL,
(UBaseType_t )4,
(TaskHandle_t )&OLED_Show_Task_Handle);
if(pdPASS == xReturn)
printf("创建OLED_Show_Task任务成功\r\n");
xReturn = xTaskCreate((TaskFunction_t )LED_Task,
(const char* )"LED_Task",
(u16 )512,
(void * )NULL,
(UBaseType_t )5,
(TaskHandle_t )&LED_Task_Handle);
if(pdPASS == xReturn)
printf("创建LED_Task任务成功\r\n");
xReturn = xTaskCreate((TaskFunction_t )CPU_Task,
(const char* )"CPU_TASK",
(uint16_t )512 ,
(void* )NULL ,
(UBaseType_t )6 ,
(TaskHandle_t )&CPU_Task_Handle);
if(pdPASS == xReturn)
printf("创建CPU_Task任务成功\r\n");
vTaskDelete(AppTaskCreate_Handle);//所有任务都创建成果,将自身任务删除
taskEXIT_CRITICAL();//退出临界区
}
1. LED任务
根据串口中断释放出的二值信号量,来解除任务的阻塞,进行相应的处理。
通过xSemaphoreTake()函数获取二值信号量的值,如果函数的返回值是pdPASS ,说明发生了中断,获取到了数据根据Usart_Rx_Buf[]的值进行相应的处理,否则一直在等待,处于阻塞状态。
static void LED_Task(void *parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdPASS == xReturn)
{
printf("收到数据:%s\r\n",Usart_Rx_Buf);
switch(Usart_Rx_Buf[0])
{
case '1':
LED_RED;
break;
case '2':
LED_GREEN;
break;
case '3':
LED_BLUE;
break;
case '4':
LED_YELLOW;
break;
case '5':
LED_PURPLE;
break;
case '6':
LED_CYAN;
break;
case '7':
LED_WHITE;
break;
case '8':
LED_RGBOFF;
break;
}
memset(Usart_Rx_Buf,0,USART_RBUFF_SIZE);/* 清零 */
}
}
}
串口DMA 配置和普通的串口DMA配置相同。
串口1中断处理函数:
void USART1_IRQHandler(void)
{
uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)!=RESET)
{
Uart_DMA_Rx_Data(); /* 释放一个信号量,表示数据已接收 */
USART_ReceiveData(DEBUG_USARTx); /* 清除标志位 */
}
/* 退出临界段 */
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}
extern SemaphoreHandle_t BinarySem_Handle;
void Uart_DMA_Rx_Data(void)
{
BaseType_t pxHigherPriorityTaskWoken;
DMA_Cmd(DMA1_Channel5,DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC5);
DMA1_Channel5->CNDTR = USART_RBUFF_SIZE;
DMA_Cmd(DMA1_Channel5,ENABLE);
//给出二值信号量 ,发送接收到新数据标志,供前台程序查询
xSemaphoreGiveFromISR(BinarySem_Handle,&pxHigherPriorityTaskWoken); //释放二值信号量
//如果需要的话进行一次任务切换,系统会判断是否需要进行切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
2. OLED显示数据任务
在这个任务中,调用BH1750数据的读取函数读取数据,并调用OLED的显示函数,进行数据的显示。关键的是注意最后调用阻塞延时函数,让其他任务有时间来处理。
static void OLED_Show_Task(void *parameter)
{
printf("OLEDshowtask\r\n");
float bh = 0 ;
while(1)
{
bh = read_BH1750();
printf("bh1750 = %.2f\r\n",bh);
//LED1_ON;
sprintf(buffertemp,"bh1750 : %.2f",bh);
OLED_ShowString(2,16,buffertemp,16);
OLED_Refresh();
vTaskDelay(1000);
}
}
3. 按键任务
按键任务利用消息队列,在按键被按下的时候,发出消息,解除按键任务的阻塞,进行LED灯的切换。
static void KEY_Task(void *parameter)
{
BaseType_t xReturn = pdPASS;
u32 r_queue;
while(1)
{
xReturn = xQueueReceive(LED_Queue,
&r_queue,
portMAX_DELAY);
if(pdPASS == xReturn)
{
printf("触发中断的是 KEY%d !\n",r_queue);
if(r_queue==1)
{
LED3_ON;
}
else
{
LED3_OFF;
}
}
else
{
printf("数据接收出错\r\n");
}
}
}
按键的中断处理函数:
声明需要发送到消息队列的数据:
static uint32_t send_data1 = 1;
static uint32_t send_data2 = 2;
中断处理函数:
//KEY1
void EXTI0_IRQHandler(void)
{
BaseType_t pxHigherPriorityTaskWoken;
u32 ulReturn;
ulReturn = taskENTER_CRITICAL_FROM_ISR();
if(EXTI_GetITStatus(EXTI_Line0)!=RESET)
{
xQueueSendFromISR(LED_Queue,
&send_data1,
&pxHigherPriorityTaskWoken);
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line0);
}
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}
//KEY2
void EXTI15_10_IRQHandler(void)
{
BaseType_t pxHigherPriorityTaskWoken;
uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();
//确保是否产生了EXTI Line中断
if(EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET)
{
/* 将数据写入(发送)到队列中,等待时间为 0 */
xQueueSendFromISR(LED_Queue, /* 消息队列的句柄 */
&send_data2,/* 发送的消息内容 */
&pxHigherPriorityTaskWoken);
//如果需要的话进行一次任务切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
//清除中断标志位
EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);
}
/* 退出临界段 */
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}
4.CPU统计任务
通过定时器的计时作用来进行利用率的统计。初始化一个普通定时器,仅用来定时,和普通的定时器初始化函数相同。
任务函数
static void CPU_Task(void* parameter)
{
uint8_t CPU_RunInfo[400]; //保存任务运行时间信息
while (1)
{
memset(CPU_RunInfo,0,400); //信息缓冲区清零
vTaskList((char *)&CPU_RunInfo); //获取任务运行时间信息
printf("---------------------------------------------\r\n");
printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n");
printf("%s", CPU_RunInfo);
printf("---------------------------------------------\r\n");
memset(CPU_RunInfo,0,400); //信息缓冲区清零
vTaskGetRunTimeStats((char *)&CPU_RunInfo);
printf("任务名 运行计数 利用率\r\n");
printf("%s", CPU_RunInfo);
printf("---------------------------------------------\r\n\n");
vTaskDelay(2000); /* 延时2000个tick */
}
}
TIM6基本定时器的中断处理函数。
volatile uint32_t CPU_RunTime = 0UL;
void TIM6_IRQHandler(void)
{
if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET )
{
CPU_RunTime++;
TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
}
}
至此,任务创建完成,系统初始化完成,下载程序观看实验现象。
四、实验结果
下载程序,打开串口调试助手。
可以看到,各类任务创建成功,BH1750数据开始刷新,CPU利用率统计任务开始显示数据。
1.LED任务 BH1750,OLED 任务的验证
手动发送数据 :2
串口显示接收到的数据,绿灯亮起。其余类似。同时可以看到OLED显示的数据和串口打印的数据相同。
手动发送数据 :8
串口显示接收到的数据,灯熄灭,同时可以看到BH1750数据随之变化。
2. 按键任务的验证
按下按键KEY1,LED灯变成蓝色(在这之前将LED灯熄灭,如果不熄灭的话,显示的应该是蓝色加串口接收到2显示的绿色,是浅蓝色吧),串口打印中断源,显示消息队列里发出的数据。
按下按键KEY2,LED熄灭,串口打印出中断源。
实验验证成功。