在学习STM32标准库的基本操作之后,为了后续Linux的深入学习,我选择先从FreeRTOS入手,学习简单的操作系统原理。
下面是我选择观看的视频教程(该文章大量引用了此视频的内容),该教程使用的是STM32cubeMX辅助开发FreeRTOS。我的计划是先学会FreeRTOS的应用,再去学习FreeRTOS的源码
FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS(FreeRTOS教程 基于STM32,以实际项目为导向)_哔哩哔哩_bilibili
声明:本视频可能存在许多错误的地方,欢迎大家指正
一、学习CubeMX的简单使用
CubeMX为stm32的开发提供了很多的便利,但这个软件用的是HAL库开发,与我之前用的标准库开发有一些不同。在阅览一段时间的HAL代码之后,我们还是很容易发现其与标准库的许多相同之处。总的来说,只要学习过标准库,我们再花费一点时间,还是容易读懂HAL库的
我下面举个例子,大家简单的对比一下就会明白
1.HAL库与标准库
/*标准库初始化 PA1 PA2 */
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启RCC时钟
GPIO_InitTypeDef GPIO_InitStructure;//结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//模式选择
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;//引脚选择
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度选择
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化引脚
GPIO_SetBits(GPIOA,GPIO_Pin_1 | GPIO_Pin_2);//引脚置低电平
}
/*HAL库初始化 */
int Led_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};//结构体
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();//RCC时钟配置
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);//引脚置低电平
/*Configure GPIO pin : PC13 */
GPIO_InitStruct.Pin = GPIO_PIN_13;//引脚选择
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;//引脚模式
GPIO_InitStruct.Pull = GPIO_NOPULL;//是否上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//速度选择
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);//初始化引脚
return 0;
}
2.CubeMX配置
a.创建新工程
b.芯片选择
c.初始化系统时钟(该教程用的TIM4),RCC时钟(内部高速晶振),时钟树
d.FREERTOS配置
e.工程配置(名称,路径,编译软件)
f.简单认识一下GPIO初始化(熟悉软件的使用)
对于这部分配置的话我推荐大家回去复习一下STM32的时钟树
【STM32】超清晰STM32时钟树动画讲解_哔哩哔哩_bilibili
二、FreeRTOS可以干什么
接下来我们先了解一下FreeRTOS可以干什么(最简单的应用)
其实就是一件事多任务管理与运行(多线程)
我们知道MCU都是单核的,在我们之前学习的过程中,它一次只能运行一个程序,要想让他执行几个程序一般就是加个外部中断或者定时中断,但这并不是真正的多线程运行。这样子看起来FreeRTOS的牛逼之处就体现出来了,比如FreeRTOS能够同时运行两个while函数,让两个LED灯以不同的频率同时闪烁(虽然我们也可以直接修改代码,但是那样子也太麻烦了,且可读性不强)
这里再推荐大家观看一个视频
单片机也能跑多线程?5分钟带你入门FreeRTOS_哔哩哔哩_bilibili
三、任务的创建与删除
CubeMX生成出来的程序我们用Keil将其打开可以发现里面有个freertos.c的文件,这就是Freertos执行的代码,而main函数在这里只执行一些初始化操作
我们可以看一下main
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Init scheduler */
osKernelInitialize(); /* Call init function for freertos objects (in freertos.c) */
MX_FREERTOS_Init();
/* Start scheduler */
osKernelStart();
/* We should never get here as control is now taken by the scheduler */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
那在FreeRTOS中如何让程序跑起来呢?这里我们要引入一个新概念“任务”,任务其实就是一个函数程序。
比如初始化时,软件就帮我们生成的一个默认任务程序,我们在里面写一些代码就能让Freertos跑起来
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
}
/* USER CODE END StartDefaultTask */
}
既然知道了任务就是一个函数,那么我们创建一个函数之后怎么才能使其运行呢
FreeRTOS提供了两个创建任务的函数(里面涉及到堆和栈,建议大家看视频学习一下,另外的还需要学习链表的知识)
1.基本方法
一个是动态内存分配:
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
另一个是静态内存分配:
TaskHandle_t xTaskCreateStatic (
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
例如:
TaskHandle_t xInLEDHandle;
BaseType_t ret;
static StackType_t g_pucStackExternLEDtask[128];
static StaticTask_t g_TCBExternLED;
static TaskHandle_t xExternLEDHandle;
void MX_FREERTOS_Init(void) {
ret=xTaskCreate(Led_Test,"LEDtask",128,NULL,osPriorityNormal,&xInLEDHandle);/*函数名,任务名,栈大小,函数变量的参数,优先级,任务句柄*/
xExternLEDHandle=xTaskCreateStatic(Led_Extern_Test,"InLEDtask",128,NULL,osPriorityNormal,g_pucStackExternLEDtask,&g_TCBExternLED);
}
其中Led_Test和Led_Extern_Test是已经写好的函数
2.创建结构体,以快速配置(多个任务可调用同个函数)
static StackType_t g_pucStackOLEDtask[128];
static StaticTask_t g_TCBOLED;
static TaskHandle_t xOLEDHandle;
struct TaskPrintInfo
{
uint8_t x;
uint8_t y;
char name[16];
};
static struct TaskPrintInfo g_Task1Info={0,0,"Task1"};
static struct TaskPrintInfo g_Task2Info={0,3,"Task2"};
static struct TaskPrintInfo g_Task3Info={0,6,"Task3"};
static int g_LCDCanUse = 1; //用于保护lcd的全局变量
void LcdPrintTask(void *params)
{
struct TaskPrintInfo *pInfo = params;
uint32_t count=0;
int len;
while(1)
{
if(g_LCDCanUse)
{
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
len += LCD_PrintString(len,pInfo->y,":");
LCD_PrintSignedVal(len,pInfo->y,count++);
g_LCDCanUse = 1;
}
mdelay(500);
}
}
void MX_FREERTOS_Init(void) {
/*使用同个函数创建不同的任务*/
xTaskCreate(LcdPrintTask,"task1",128,&g_Task1Info,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"task2",128,&g_Task2Info,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"task3",128,&g_Task3Info,osPriorityNormal,NULL);
}
FreeRTOS执行三个任务
但是多个任务同时调度一个函数是可能发生冲突的,这个冲突的解决方法等以后再说
3.删除任务
任务删除的函数只有一个
void vTaskDelete( TaskHandle_t xTaskToDelete )
任务删除的方式正常有两种,一种是自杀(本任务删除本任务,Handle写NULL),一种是他杀(一个任务被另一个任务删除Handle写要删除的任务的Handle)
他杀(其中xHandleOLED是另一个任务的Handle):
void LED()
{
vTaskDelete(xHandleOLED);
}
自杀:
void LED()
{
vTaskDelete(NULL);
}
四、汇编基础
因为FreeRTOS的内部机制会涉及到少数汇编知识,所以在学习FreeRTOS最好补一下汇编的几个基础代码,了解C语言的本质
五、ARM架构基础
主要是了解一下芯片是如何进行程序运算的,一句简单的C语言代码实际上是由几句汇编代码组成的,而汇编再转换为机器语言(二进制)即可实现数据运算
六、优先级与tick
1.优先级
我们可以在上面的创建任务里发现有一个优先级的参数,这个其实和NVIC的优先级配置差不多,就是当优先级高和优先级低的任务冲突的时候,高优先级的任务先执行,低优先级的任务后执行,同优先级的任务轮流执行(可以看一下上面的视频“FreeRTOS执行三个任务”)
但是在我们使用优先级时就会发现一个问题(使用代码如下)我们会发现执行仅停滞于优先级最高的任务
static StackType_t g_pucStackOLEDtask[128];
static StaticTask_t g_TCBOLED;
static TaskHandle_t xOLEDHandle;
struct TaskPrintInfo
{
uint8_t x;
uint8_t y;
char name[16];
};
static struct TaskPrintInfo g_Task1Info={0,0,"Task1"};
static struct TaskPrintInfo g_Task2Info={0,3,"Task2"};
static struct TaskPrintInfo g_Task3Info={0,6,"Task3"};
static int g_LCDCanUse = 1; //用于保护lcd的全局变量
void LcdPrintTask(void *params)
{
struct TaskPrintInfo *pInfo = params;
uint32_t count=0;
int len;
while(1)
{
if(g_LCDCanUse)
{
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
len += LCD_PrintString(len,pInfo->y,":");
LCD_PrintSignedVal(len,pInfo->y,count++);
g_LCDCanUse = 1;
}
mdelay(500);
}
}
void MX_FREERTOS_Init(void) {
/*使用同个函数创建不同的任务*/
xTaskCreate(LcdPrintTask,"task1",128,&g_Task1Info,osPriorityNormal+1,NULL);
xTaskCreate(LcdPrintTask,"task2",128,&g_Task2Info,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"task3",128,&g_Task3Info,osPriorityNormal,NULL);
}
FreeRTOS任务停滞
那这个问题的本质就是CPU被占用导致任务无法被切换(最主要的就是Delay对CPU资源的占用),所以到这里我们就要引入FreeRTOS自带的Delay函数(Untial函数暂不赘述),该函数在执行时可以跳出本任务,去执行另外的任务
void vTaskDelay( const TickType_t xTicksToDelay );
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );
只要我们把之前代码的delay函数修改为vTaskDelay就可以多任务运行,只是“task1”会优先运行
if(g_LCDCanUse)
{
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
len += LCD_PrintString(len,pInfo->y,":");
LCD_PrintSignedVal(len,pInfo->y,count++);
g_LCDCanUse = 1;
}
vTaskDelay(500);
2.Tick
我们知道了任务会按优先级切换执行,那任务会在什么时候进行切换,运行多长时间就会切换呢?
那就需要引入FreeRTOS里面Tick的概念了,这其实和我们之前学习的Tick很像,也是用定时器产生中断,每次中断就会产生任务切换,该切换周期由configTICK_RATE_HZ 决定,如果configTICK_RATE_HZ为100,那么任务切换时间就是10ms(我们也可以在CubeMX发现这一配置)
上面提到的vTaskDelay其实就是等待多少个Tick,在等待过程中,可以执行其他任务
那vTaskDelayUntil其实就是把任务执行时间设置为一个更精确的时间(但是我们要注意,vTaskDelayUntil和vTaskDelay都会产生一点小误差,这是无法避免的)
七、状态机
FreeRTOS中的状态机和江科大教的串口通讯状态机有些类似
我们可以简单对比一下
FreeRTOS任务创建之后就会处于就绪态,调度的时候会处于运行态,在我们使用vTaskDelay之后任务就会处于阻塞态,此时其他任务可以运行,或者我们直接将任务设置在暂停(挂起)态(可以自己进入暂停态,但是需要别的任务将其唤醒,本任务无法唤醒本任务)。学会暂停时候我们就可以不再一味的删除、创建任务,并且可以保留任务的运行状态,也可给别的任务提供更充裕的运行时间
具体的函数可以参考图片或者韦东山老师的视频教程