上一章中我们初步的了解了一下 FreeRTOS,本章就正式踏上 FreeRTOS 的学习之路, 首先
肯定是把 FreeRTOS 移植到我们所使用的平台上, 这里以 ALIENTEK 的 STM32F103 开发板为
例,本章分为如下几部分:
2.1 准备工作
2.2 FreeRTOS 移植
2.3 移植验证实验
要移植 FreeRTOS,肯定需要一个基础工程, 基础工程越简单越好, 这里我们就用基础例程
中的跑马灯实验来作为基础工程。
FreeRTOS 系统源码在上一章已经详细的讲解过如何获取了, 这里我们会将 FreeRTOS 的系
统源码放到开发板光盘中去,路径为: 6, 软件资料->14, FreeRTOS 学习资料->FreeRTOS 源码。
1、 添加 FreeRTOS 源码
在基础工程中新建一个名为 FreeRTOS 的文件夹, 如图 2.2.1.1 所示:
图 2.2.1.1 新建 FreeRTOS 文件夹
创建 FreeRTOS 文件夹以后就可以将 FreeRTOS 的源码添加到这个文件夹中, 添加完以后
如图 2.2.1.2 所示:
图 2.2.1.2 添加 FreeRTOS 源码
在 1.3.2 小节中详细的讲解过 portable 文件夹,我们只需要留下 keil、 MemMang 和 RVDS
这三个文件夹, 其他的都可以删除掉, 完成以后如图 2.2.1.3 所示:
图 2.2.1.3 portable 文件夹
2、向工程分组中添加文件
打开基础工程,新建分组 FreeRTOS_CORE 和 FreeRTOS_PORTABLE,然后向这两个分组
中添加文件,如图 2.2.1.4 所示:
图 2.2.1.4 添加文件
分组 FreeRTOS_CORE 中的文件在什么地方就不说了,打开 FreeRTOS 源码一目了然。 重
点来说说 FreeRTOS_PORTABLE 分组中的 port.c 和 heap_4.c 是怎么来的, port.c 是 RVDS 文件夹下的 ARM_CM3 中的文件, 因为 STM32F103 是 Cortex-M3 内核的, 因此要选择 ARM_CM3中的 port.c 文件。 heap_4.c 是 MemMang 文件夹中的,前面说了 MemMang 是跟内存管理相关的,里面有 5 个 c 文件: heap_1.c、 heap_2.c、 heap_3.c、 heap_4.c 和 heap_5.c。
这 5 个 c 文件是五种不同的内存管理方法,就像从北京到上海你可以坐火车、坐飞机, 如果心情好的话也可以走路,反正有很多种方法, 只要能到上海就行。这里也一样的,这 5 个文件都可以用来作为FreeRTOS 的内存管理文件, 只是它们的实现原理不同, 各有利弊。 这里我们选择 heap_4.c,至于原因, 后面会有一章节专门来讲解 FreeRTOS 的内存管理,到时候大家就知道原因了。 这里就先选择 heap_4.c,毕竟本章的重点是 FreeRTOS 的移植。
3、 添加相应的头文件路径
添加完 FreeRTOS 源码中的 C 文件以后还要添加 FreeRTOS 源码的头文件路径,头文件路
径如图 2.2.1.5 所示:
图 2.2.1.5 FreeRTOS 头文件路径
头文件路径添加完成以后编译一下,看看有没有什么错误, 结果会发现提示打不开
“FreeRTOSConfig.h”这个文件, 如图 2.2.1.6 所示:
图 2.2.1.6 错误提示
这是因为缺少 FreeRTOSConfig.h 文件,这个文件在哪里找呢? 你可以自己创建,显然这不
是一个明智的做法。 我们可以找找 FreeRTOS 的官方移植工程中会不会有这个文件,打开
FreeRTOS 针对 STM32F103 的移植工程文件,文件夹是 CORTEX_STM32F103_Keil,打开以后
如图 2.2.1.7 所示:
图 2.2.1.7 STM32F103 移植工程
果然! 官方的移植工程中有这个文件, 我们可以使用这个文件, 但是建议大家使用我们例
程中的 FreeRTOSConf.h 文件,这个文件是 FreeRTOS 的系统配置文件,不同的平台其配置不同,
但是我们提供的例程中的这个文件肯定是针对 ALIENTEK 开发板配置正确的。 这个文件复制到什么地方大家可以自行决定,这里我为了方便放到了 FreeRTOS 源码中的 include 文件夹下。
FreeRTOSConfig.h 是何方神圣?看名字就知道, 他是 FreeRTOS 的配置文件,一般的操作系统都有裁剪、 配置功能, 而这些裁剪及配置都是通过一个文件来完成的, 基本都是通过宏定义来完成对系统的配置和裁剪的, 关于 FreeRTOS 的配置文件 FreeRTOSConfig.h 后面也会有一章节来详细的讲解。到这里我们再编译一次, 没有错误! 如图 2.2.1.8 所示:
图 2.2.1.8 编译结果
如果还有错误的话大家自行根据错误类型查找和修改错误!
SYSTEM 文件夹里面的文件一开始是针对 UCOS 而编写的,所以如果使用 FreeRTOS 的话
就需要做相应的修改。本来打算让 SYSTEM 文件夹也支持 FreeRTOS, 但是这样的话会导致
SYSTEM 里面的文件太过于复杂, 这样非常不利于初学者学习, 所以这里就专门针对 FreeRTOS
修改了 SYSTEM 里面的文件。
1、 修改 sys.h 文件
sys.h 文件修改很简单,在 sys.h 文件里面用宏 SYSTEM_SUPPORT_OS 来定义是否使用 OS,我们使用了 FreeRTOS, 所以应该将宏 SYSTEM_SUPPORT_OS 改为 1。
//0,不支持 os
//1,支持 os
#define SYSTEM_SUPPORT_OS 1 //定义系统文件夹是否支持 OS
2、 修改 usart.c 文件
usart.c 文件修改也很简单, usart.c 文件有两部分要修改,一个是添加 FreeRTOS.h 头文件,默认是添加的 UCOS 中的 includes.h 头文件,修改以后如下:
//如果使用 os,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "FreeRTOS.h" //os 使用
#endif
另外一个就是 USART1 的中断服务函数, 在使用 UCOS 的时候进出中断的时候需要添加
OSIntEnter()和 OSIntExit(), 使用 FreeRTOS 的话就不需要了,所以将这两行代码删除掉,修改
以后如下:
void USART1_IRQHandler(void) //串口 1 中断服务程序
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
if((USART_RX_STA&0x8000)==0) //接收未完成
{
if(USART_RX_STA&0x4000) //接收到了 0x0d
{
if(Res!=0x0a)USART_RX_STA=0; //接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到 0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
}
}
}
}
}
delay.c 文件修改的就比较大了,因为涉及到 FreeRTOS 的系统时钟, delay.c 文件里面有 4
个函数,先来看一下函数 SysTick_Handler(),此函数是滴答定时器的中断服务函数, 代码如下:
extern void xPortSysTickHandler(void);
//systick 中断服务函数,使用 OS 时用到
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
xPortSysTickHandler();
}
}
FreeRTOS 的心跳就是由滴答定时器产生的,根据 FreeRTOS 的系统时钟节拍设置好滴答定
时器的周期,这样就会周期触发滴答定时器中断了。在滴答定时器中断服务函数中调用
FreeRTOS 的 API 函数 xPortSysTickHandler()。
delay_init()是用来初始化滴答定时器和延时函数,代码如下:
//初始化延迟函数
//SYSTICK 的时钟固定为 AHB 时钟,基础例程里面 SYSTICK 时钟频率为 AHB/8
//这里为了兼容 FreeRTOS,所以将 SYSTICK 的时钟频率改为 AHB 的频率!
//SYSCLK:系统时钟频率
void delay_init()
{
u32 reload;
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择外部时钟 HCLK
fac_us=SystemCoreClock/1000000; //不论是否使用 OS,fac_us 都需要使用
reload=SystemCoreClock/1000000; //每秒钟的计数次数 单位为 M
reload*=1000000/configTICK_RATE_HZ; //根据 configTICK_RATE_HZ 设定溢出
//时间 reload 为 24 位寄存器,最大值;16777216,在 72M 下,约合 0.233s 左右
fac_ms=1000/configTICK_RATE_HZ; //代表 OS 可以延时的最少单位
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启 SYSTICK 中断
SysTick->LOAD=reload; //每 1/configTICK_RATE_HZ 秒中断一次
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK
}
前面我们说了 FreeRTOS 的系统时钟是由滴答定时器提供的,那么肯定要根据 FreeRTOS 的
系统时钟节拍来初始化滴答定时器了, delay_init()就是来完成这个功能的。 FreeRTOS 的系统时
钟节拍由宏 configTICK_RATE_HZ 来设置,这个值我们可以自由设置,但是一旦设置好以后我
们就要根据这个值来初始化滴答定时器,其实就是设置滴答定时器的中断周期。 在基础例程中
滴答定时器的时钟频率设置的是 AHB 的 1/8,这里为了兼容 FreeRTOS 将滴答定时器的时钟频
率改为了 AHB,也就是 72MHz!这一点一定要注意!
接下来的三个函数都是延时的,代码如下:
//延时 nus
//nus:要延时的 us 数.
//nus:0~204522252(最大值即 2^32/fac_us@fac_us=168)
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD 的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
//这里注意一下 SYSTICK 是一个递减的计数器就可以了.
if(tnow=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
//延时 nms,会引起任务调度
//nms:要延时的 ms 数
//nms:0~65535
void delay_ms(u32 nms)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
if(nms>=fac_ms) //延时的时间大于 OS 的最少时间周期
{
vTaskDelay(nms/fac_ms); //FreeRTOS 延时
}
nms%=fac_ms; //OS 已经无法提供这么小的延时了,
//采用普通方式延时
}
delay_us((u32)(nms*1000)); //普通方式延时
}
//延时 nms,不会引起任务调度
//nms:要延时的 ms 数
void delay_xms(u32 nms)
{
u32 i;
for(i=0;i
delay_us()是 us 级延时函数, delay_ms 和 delay_xms()都是 ms 级的延时函数, delay_us()和delay_xms()不会导致任务切换。 delay_ms()其实就是对 FreeRTOS 中的延时函数 vTaskDelay()的简单封装,所以在使用 delay_ms()的时候就会导致任务切换。
delay.c 修改完成以后编译一下,会提示如图 2.2.2.1 所示错误:
图 2.2.2.1 错误提示
图 2.2.2.1 的错误提示表示在 port.c、 delay.c 和 stm32f10x_it.c 中三个重复定义的函数:
SysTick_Handler()、 SVC_Handler()和 PendSV_Handler(),这三个函数分别为滴答定时器中断服
务函数、 SVC 中断服务函数和 PendSV 中断服务函数,将 stm32f10x_it.c 中的三个函数屏蔽掉,
如图 2.2.2.2 所示:
图 2.2.2.2 屏蔽函数
再次编译代码,应该没有错误了,如果还是错误的话自行根据错误类型修改!至此, SYSTEM
文件夹就修改完成了。
1、实验目的
编写简单的 FreeRTOS 应用代码,测试 FreeRTOS 的移植是否成功。鉴于大家还没正式学习
FreeRTOS, 可以直接将本实验代码复制粘贴到自己的移植工程中。
2、实验设计
本实验设计四个任务: start_task()、 led0_task ()、 led1_task ()和 float_task(),这四个任务的任务功能如下:
start_task():用来创建其他三个任务。
led0_task ():控制 LED0 的闪烁,提示系统正在运行。
led1_task ():控制 LED1 的闪烁。
float_task():简单的浮点测试任务,用于测试 STM32F4 的 FPU 是否工作正常。
3、实验工程
FreeRTOS 实验 2-1 FreeRTOS 移植实验。
4、实验程序与分析
●任务设置
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define LED0_TASK_PRIO 2 //任务优先级
#define LED0_STK_SIZE 50 //任务堆栈大小
TaskHandle_t LED0Task_Handler; //任务句柄
void led0_task(void *p_arg); //任务函数
#define LED1_TASK_PRIO 3 //任务优先级
#define LED1_STK_SIZE 50 //任务堆栈大小
TaskHandle_t LED1Task_Handler; //任务句柄
void led1_task(void *p_arg); //任务函数
● main()函数 |
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组 4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化 LED
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
● 任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建 LED0 任务
xTaskCreate((TaskFunction_t )led0_task,
(const char* )"led0_task",
(uint16_t )LED0_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED0_TASK_PRIO,
(TaskHandle_t* )&LED0Task_Handler);
//创建 LED1 任务
xTaskCreate((TaskFunction_t )led1_task,
(const char* )"led1_task",
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//LED0 任务函数
void led0_task(void *pvParameters)
{
while(1)
{
LED0=~LED0;
vTaskDelay(500);
}
}
//LED1 任务函数
void led1_task(void *pvParameters)
{
while(1)
{
LED1=0;
vTaskDelay(200);
LED1=1;
vTaskDelay(800);
}
}
测试代码中创建了 3 个任务: LED0 测试任务、 LED1 测试任务和浮点测试任务,它们的任务函数分别为: led0_task()、 led1_task()。 led0_task()和 led1_task()任务很简单,就是让 LED0 和LED1 周期性闪烁。
编译并下载代码到 STM32F103 开发板中,下载进去以后会看到 LED0 和 LED1 开始闪烁,
LED0 均匀闪烁,那是因为我们在 LED0 的任务代码中设置好的 LED0 亮 500ms,灭 500ms。
LED1 亮的时间短,灭的时间长,这是因为在 LED1 的任务代码中设置好的亮 200ms,灭 800ms。