一、完成系统移植
系统移植上官网寻找合适的系统包,下载后将文件移植入工程文件
二、创建任务句柄、内核对象句柄(信号量,消息队列,事件标志组,软件定时器)、声明全局变量、声明函数
三、创建主函数,用于存放硬件初始化(bsp_init())、创建主任务任务(用于创建多任务)( xTaskCreate())、启动任务调度( vTaskStartScheduler())
int bsp_init()
{
/*
主要实现如
gpio引脚:
gpio组时钟使能(RCC)、gpio初始化(GPIO_INIT())、gpio输入输出cpu
UART:
gpio组时钟使能(RCC)、gpio初始化(GPIO_INIT())、gpio复用(GPIO_PINGAFCONFIG())、uart结构体定义、uart初始化(uart_init())、中断配置(uart_itconfig())、NVIC控制器(nvic_init())、uart使能(uart_cmd())、收发数据
IIC:
SPI:
CAN:
gpio组时钟使能(RCC)、gpio初始化(GPIO_INIT())、gpio复用(GPIO_PINGAFCONFIG())、can结构体定义、can初始化(can_init())、筛选器配置(CAN_FilterInit())、can使能(can_cmd())、收发数据
ETH:
gpio组时钟使能(RCC)、gpio初始化(GPIO_INIT())、gpio复用(GPIO_PINGAFCONFIG())、eth结构体定义、eth初始化(HAL_ETH_Init())、配置发送接收描述符(HAL_ETH_DMATxDescListInit()、HAL_ETH_DMARxDescListInit())、启动ETH通信(HAL_ETH_Start())、收发数据
}
xTaskCreate()
/* 创建主任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
四、创建主任务函数AppTaskCreate()
static void AppTaskCreate(void)
{
taskENTER_CRITICAL(); //进入临界区
/* 创建xxx_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )xxx_Task, /* 任务入口函数 */
(const char* )"LED_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED_Task_Handle);/* 任务控制块指针 */
/* 创建xxx2_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )xxx2_Task, /* 任务入口函数 */
(const char* )"KEY_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&KEY_Task_Handle);/* 任务控制块指针 */
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
五、创建xxx_Task、xxx2_Task任务函数(实现具体的功能)(static void xxx_Task())
static void xxx_Task(void* parameter)
{
while (1)
{
//使用BSP_init()中封装的内容实现具体的功能,如收发数据
}
}
六、根据需要,实现任务的抢占
FREERTOS是一个抢占式实时任务系统,多任务情况下涉及一个任务的状态迁移,一般来说任务抢占,会在任务函数中去实现,完成挂起,延时、删除、恢复的操作。
1.任务挂起函数 vTaskSuspend()
挂起指定任务。被挂起的任务绝不会得到 CPU 的使用权,不管该任务具有什么优先级。 任务可以通过调用 vTaskSuspend()函数都可以将处于任何状态的任务挂起,被挂起的 任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从 挂起态中解除。
2.vTaskSuspendAll()
这个函数就是比较有意思的,将所有的任务都挂起,其实源码很简单,也很有意思, 不管三七二十一将调度器锁定,并且这个函数是可以进行嵌套的,说白了挂起所有任务就 是挂起任务调度器。调度器被挂起后则不能进行上下文切换,但是中断还是使能的。 当调 度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个任务将会被挂起,在调度 器恢复之后才执行切换任务。
3.任务恢复函数 vTaskResume()
既然有任务的挂起,那么当然一样有恢复,不然任务怎么恢复呢,任务恢复就是让挂 起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂 起时的状态继续运行。如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一 位,那么系统将进行任务上下文的切换。
4.任务删除函数 vTaskDelete()
用于删除一个任务。当一个任务删除另外一个任务时,形参为要删除任 务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。 要想使用该函数必须在 FreeRTOSConfig.h 中把 INCLUDE_vTaskDelete 定义为 1,删除的任务将从所有就绪,阻塞, 挂起和事件列表中删除。
5.任务延时函数 vTaskDelay()
vTaskDelay()在我们任务中用得非常之多,每个任务都必须是死循环,并且是必须要有 阻塞的情况,否则低优先级的任务就无法被运行了。要想使用 FreeRTOS 中的 vTaskDelay() 函数必须在 FreeRTOSConfig.h 中把 INCLUDE_vTaskDelay 定义为 1 来使能。
6.vTaskDelayUntil()
在 FreeRTOS 中,除了相对延时函数,还有绝对延时函数 vTaskDelayUntil(),这个绝 对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行, 而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不 是相对的。
七、创建、写、读、删除消息队列
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之 间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的 长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以 也称为消息队列。
1、数据存储
通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是也可以使用 LIFO 的存储缓冲,也就是后进先出,FreeRTOS 中的队列也提供了 LIFO 的存储缓冲机制。 数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在 队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。
2、多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息
3、出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。出队就是就从队列中读取消息,出队阻塞是 针对从队列中读取消息的任务而言的
4、入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队 列发送消息的话也可以设置阻塞时间
5、消息队列在任务中实现
一般来说,消息队列的创建、读取、写入、删除都在任务函数中实现
1. 消息队列创建函数 xQueueCreate()
2 消息队列静态创建函数 xQueueCreateStatic()
3.读队列 xQueueReceive()
4.写队列 xQueueSend()
5.消息队列删除函数 vQueueDelete()
6.复位 xQueueReset()
7.查询 uxQueueMessagesWaiting()(列中可用数据的个数)、uxQueueSpacesAvailable()(队列中可用空间的个数)
在主任务创建消息队列,在任务中读、写、删除、复位、查询队列
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Test_Queue */
Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */
(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */
if(NULL != Test_Queue)
printf("创建Test_Queue消息队列成功!\r\n");
/* 创建Receive_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive_Task, /* 任务入口函数 */
(const char* )"Receive_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Receive_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive_Task任务成功!\r\n");
/* 创建Send_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Send_Task, /* 任务入口函数 */
(const char* )"Send_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Send_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Send_Task任务成功!\n\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void Receive_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdTRUE */
uint32_t r_queue; /* 定义一个接收消息的变量 */
while (1)
{
xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if(pdTRUE == xReturn)
printf("本次接收到的数据是%d\n\n",r_queue);
else
printf("数据接收出错,错误代码0x%lx\n",xReturn);
}
}
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint32_t send_data1 = 1;
uint32_t send_data2 = 2;
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{/* K1 被按下 */
printf("发送消息send_data1!\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
&send_data1,/* 发送的消息内容 */
0 ); /* 等待时间 0 */
if(pdPASS == xReturn)
printf("消息send_data1发送成功!\n\n");
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{/* K2 被按下 */
printf("发送消息send_data2!\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
&send_data2,/* 发送的消息内容 */
0 ); /* 等待时间 0 */
if(pdPASS == xReturn)
printf("消息send_data2发送成功!\n\n");
}
vTaskDelay(20);/* 延时20个tick */
}
}
八、创建信号量
信号量常常用于控制对共享资源的访问和任务同步
1.二值信号量(只有0、1,用于临界资源的保护,作用约等于上锁解锁)
二值信号量既可以用于临界资源访问也可以用于同步功能。 二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但是有一些细 微差别:互斥量有优先级继承机制,二值信号量则没有这个机制
2.计数信号量(0~N的计数,用于事件计数或者某个资源的管理计数)
计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一个信号量(信号量 计数值加 1),当处理被事件时(一般在任务中处理),处理任务会取走该信号量(信号 量计数值减 1),信号量的计数值则表示还有多少个事件没被处理。此外,系统还有很多资源,我们也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统 没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0 的时候任务就无法访问该资源了。 计数型信号量允许多个任务对其进行操作,但限制了任务的数量
3.互斥信号量(和二值信号量相似,多出一个优先级继承机制(暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该 资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该 资源时,优先级重新回到初始设定值))
互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源。
用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源时, (临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空, 这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界 资源的安全。
在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个 资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得 到有效的保护
4.递归信号量(可以重复调用信号量,在信号量递归调用未完全归还前,其他任务无法获取信号量)
信号量是可以重复获取调用的, 对于已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。任务成功获取几次递 归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态,其他任务无法获取,只 有持有递归信号量的任务才能获取与释放
5.信号量在任务、中断中实现(创建、获取、删除、释放)
互斥信号量只能在任务中实现,无法在中断使用
1.创建二值信号量 xSemaphoreCreateBinary()
2. 创建计数信号量 xSemaphoreCreateCounting()
3.创建互斥信号量 xSemaphoreCreateMutex()
4..创建递归互斥信号量xSemaphoreCreateRecursiveMutex()
5.信号量删除函数 vSemaphoreDelete()
6.信号量释放函数 xSemaphoreGive()(任务)、xSemaphoreGiveFromISR()(中断)
7.递归互斥量释放函数 xSemaphoreGiveRecursive()
8.信号量获取函数xSemaphoreTake()(任务)、xSemaphoreTakeFromISR()(中断)
9.递归互斥量获取函数 xSemaphoreTakeRecursive()
6.二值、计数信号量在任务中实现
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary(); /*xSemaphoreCreateBinary 创建二值信号量*/
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功!\r\n");
/* 创建Test_Queue */
CountSem_Handle = xSemaphoreCreateCounting(10,10); /*xSemaphoreCreateCounting 创建计数信号量*/
if(NULL != CountSem_Handle)
printf("CountSem_Handle计数信号量创建成功!\r\n");
/* 创建Receive_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive_Task, /* 任务入口函数 */
(const char* )"Receive_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Receive_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive_Task任务成功!\r\n");
/* 创建Send_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Send_Task, /* 任务入口函数 */
(const char* )"Send_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Send_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Send_Task任务成功!\n\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void Receive_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 xSemaphoreTake获取一个信号量,可以是二值信号量、计数信号量、互斥量。*/
portMAX_DELAY); /* 等待时间 */
if(pdTRUE == xReturn)
printf("BinarySem_Handle二值信号量获取成功!\n\n");
/* 获取一个计数信号量 */
xReturn = xSemaphoreTake(CountSem_Handle, /* 计数信号量句柄 */
0); /* 等待时间:0 */
if ( pdTRUE == xReturn )
printf( "KEY1被按下,成功申请到停车位。\n" );
else
printf( "KEY1被按下,不好意思,现在停车场已满!\n" );
}
}
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
/* K1 被按下 */
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量 xSemaphoreGive 释放信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle二值信号量释放成功!\r\n");
else
printf("BinarySem_Handle二值信号量释放失败!\r\n");
}
/* K2 被按下 */
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle二值信号量释放成功!\r\n");
else
printf("BinarySem_Handle二值信号量释放失败!\r\n");
/* 获取一个计数信号量 */
xReturn = xSemaphoreGive(CountSem_Handle);//给出计数信号量
if ( pdTRUE == xReturn )
printf( "KEY2被按下,释放1个停车位。\n" );
else
printf( "KEY2被按下,但已无车位可以释放!\n" );
}
vTaskDelay(20);
}
}
7.互斥信号量在任务中实现
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建MuxSem */
MuxSem_Handle = xSemaphoreCreateMutex();//创建互斥量
if(NULL != MuxSem_Handle)
printf("MuxSem_Handle互斥量创建成功!\r\n");
xReturn = xSemaphoreGive( MuxSem_Handle );//释放互斥量
// if( xReturn == pdTRUE )
// printf("释放信号量!\r\n");
/* 创建LowPriority_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LowPriority_Task, /* 任务入口函数 */
(const char* )"LowPriority_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LowPriority_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LowPriority_Task任务成功!\r\n");
/* 创建MidPriority_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )MidPriority_Task, /* 任务入口函数 */
(const char* )"MidPriority_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&MidPriority_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建MidPriority_Task任务成功!\n");
/* 创建HighPriority_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )HighPriority_Task, /* 任务入口函数 */
(const char* )"HighPriority_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )4, /* 任务的优先级 */
(TaskHandle_t* )&HighPriority_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建HighPriority_Task任务成功!\n\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void LowPriority_Task(void* parameter)
{
static uint32_t i;
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
printf("LowPriority_Task 获取互斥量\n");
//获取互斥量 MuxSem,没获取到则一直等待
xReturn = xSemaphoreTake(MuxSem_Handle,/* 互斥量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdTRUE == xReturn)
printf("LowPriority_Task Runing\n\n");
for(i=0;i<2000000;i++)//模拟低优先级任务占用互斥量
{
taskYIELD();//发起任务调度
}
printf("LowPriority_Task 释放互斥量!\r\n");
xReturn = xSemaphoreGive( MuxSem_Handle );//释放互斥量
LED1_TOGGLE;
vTaskDelay(1000);
}
}
static void MidPriority_Task(void* parameter)
{
while (1)
{
printf("MidPriority_Task Runing\n");
vTaskDelay(1000);
}
}
static void HighPriority_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
printf("HighPriority_Task 获取互斥量\n");
//获取互斥量 MuxSem,没获取到则一直等待
xReturn = xSemaphoreTake(MuxSem_Handle,/* 互斥量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdTRUE == xReturn)
printf("HighPriority_Task Runing\n");
LED1_TOGGLE;
printf("HighPriority_Task 释放互斥量!\r\n");
xReturn = xSemaphoreGive( MuxSem_Handle );//给出互斥量
vTaskDelay(1000);
}
}
九、事件组
1.事件位(事件标志)
事件位用来表明某个事件是否发生,事件位通常用作事件标志
当收到一条消息并且把这条消息处理掉以后就可以将某个位(标志)置 1,当队列中没有消息需要处理的时候就可以将这个位(标志)置 0
当把队列中的消息通过网络发送输出以后就可以将某个位(标志)置 1,当没有数据需要从网络发送出去的话就将这个位(标志)置0
当需要向网络中发送一个心跳信息,将某个位(标志)置 1,当不需要向网络中发送心跳信息,这个位(标志)置0
2.事件组
事件组的数据类型为 EventGroupHandle_t,当configUSE_16_BIT_TICKS 为 1 的时候 事件标志组可以存储 8 个事件位,当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24 个事件位
一个事件组就是一组的事件位,事件组中的事件位通过位编号来访问
事件标志组的 bit0 表示队列中的消息是否处理掉
事件标志组的 bit1 表示是否有消息需要从网络中发送出去
事件标志组的 bit2 表示现在是否需要向网络发送心跳信息
3.事件应用场景
用事件来做标志位,判断某些事件是否发生了,然后根据结果做处理(事件可使用于多种场合,能够在一定程度上替代信号量,用于任务与任务间,中断与任务间的同步)
接收事件时,可以根据感兴趣的参事件类型接收事件的单个或者多个事件类型。事件接收成功后,必须使用 xClearOnExit 选项来清除已接收到的事件类型,否则不会清除已接收 到的 事件 ,这样就需要用户显式清除事件位。用户可以自定义通过传入参数xWaitForAllBits 选择读取模式,是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。
设置事件时,对指定事件写入指定的事件类型,设置事件集合的对应事件位为1,可 以一次同时写多个事件类型,设置事件成功可能会触发任务调度。
清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清0操作。 事件不与任务相关联,事件相互独立,一个 32位的变量(事件集合,实际用于表示事 件的只有 24 位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表 示该事件类型未发生、1表示该事件类型已经发生)
4.事件控制块
事件标志组存储在一个 EventBits_t 类型的变量中,该变量在事件组结构体中定义,如果宏 configUSE_16_BIT_TICKS 定义为 1,那么变量 uxEventBits 就 是 16 位 的 , 其 中 有 8 个 位 用来存储 事 件 组 , 如 果 宏 configUSE_16_BIT_TICKS 定义为 0,那么变量 uxEventBits 就是 32 位的,其中有 24 个位 用来存储事件组,每一位代表一个事件的发生与否,利用逻辑或、逻辑与等实现不同事件的不同唤醒处理
除了事件标志组变量之外,FreeRTOS 还使用了一个链表来记录等待事件的任务,所有 在等待此事件的任务均会被挂载在等待事件列表 xTasksWaitingForBits
typedef struct xEventGroupDefinition {
EventBits_t uxEventBits;
List_t xTasksWaitingForBits;
#if( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxEventGroupNumber;
#endif
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) \
&& ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;
#endif
} EventGroup_t;
5.事件组实现(创建、置位、等待、清除位、删除)
1.事件创建函数 xEventGroupCreate()
2.事件删除函数 vEventGroupDelete()
3.事件组置位函数 xEventGroupSetBits()(任务)
4.等待事件函数 xEventGroupWaitBits()
5. 事件组清除函数位 xEventGroupClearBits()(任务)与 xEventGroupClearBitsFromISR()(中断)
6.事件组在任务中实现
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建 Event_Handle */
Event_Handle = xEventGroupCreate();
if(NULL != Event_Handle)
printf("Event_Handle 事件创建成功!\r\n");
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
(const char* )"LED_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED_Task任务成功!\r\n");
/* 创建KEY_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )KEY_Task, /* 任务入口函数 */
(const char* )"KEY_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&KEY_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建KEY_Task任务成功!\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void LED_Task(void* parameter)
{
EventBits_t r_event; /* 定义一个事件接收变量 */
/* 任务都是一个无限循环,不能返回 */
while (1)
{
/*******************************************************************
* 等待接收事件标志
*
* 如果xClearOnExit设置为pdTRUE,那么在xEventGroupWaitBits()返回之前,
* 如果满足等待条件(如果函数返回的原因不是超时),那么在事件组中设置
* 的uxBitsToWaitFor中的任何位都将被清除。
* 如果xClearOnExit设置为pdFALSE,
* 则在调用xEventGroupWaitBits()时,不会更改事件组中设置的位。
*
* xWaitForAllBits如果xWaitForAllBits设置为pdTRUE,则当uxBitsToWaitFor中
* 的所有位都设置或指定的块时间到期时,xEventGroupWaitBits()才返回。
* 如果xWaitForAllBits设置为pdFALSE,则当设置uxBitsToWaitFor中设置的任何
* 一个位置1 或指定的块时间到期时,xEventGroupWaitBits()都会返回。
* 阻塞时间由xTicksToWait参数指定。
*********************************************************/
r_event = xEventGroupWaitBits(Event_Handle, /* 事件对象句柄 */
KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */
pdTRUE, /* 退出时清除事件位 */
pdTRUE, /* 满足感兴趣的所有事件 */
portMAX_DELAY);/* 指定超时事件,一直等 */
if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT))
{
/* 如果接收完成并且正确 */
printf ( "KEY1与KEY2都按下\n");
LED1_TOGGLE; //LED1 反转
}
else
printf ( "事件错误!\n");
}
}
static void KEY_Task(void* parameter)
{
/* 任务都是一个无限循环,不能返回 */
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) //如果KEY2被单击
{
printf ( "KEY1被按下\n" );
/* 触发一个事件1 */
xEventGroupSetBits(Event_Handle,KEY1_EVENT);//事件组置位
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) //如果KEY2被单击
{
printf ( "KEY2被按下\n" );
/* 触发一个事件2 */
xEventGroupSetBits(Event_Handle,KEY2_EVENT);//事件组置位
}
vTaskDelay(20); //每20ms扫描一次
}
}
十、任务通知
FreeRTOS 从V8.2.0版本开始提供任务通知这个功能,每个任务都有一个32位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为1的消息队列(可以保存一个 32位整数或指针值)。相对于以前使用 FreeRTOS 内核通信的资源,必须创建队列、二进制信号量、计数信 号量或事件组的情况,使用任务通知显然更灵活
任务通知的使用无需创建队列,想要使用任务通知, 必须将 FreeRTOSConfig.h 中的宏定义 configUSE_TASK_NOTIFICATIONS 设置为 1,其实 FreeRTOS 默认是为 1 的,所以任务通知是默认使能的
FreeRTOS 提供以下几种方式发送通知给任务 :
发送通知给任务, 如果有通知未读,不覆盖通知值
发送通知给任务,直接覆盖通知值
发送通知给任务,设置通知值的一个或者多个位,可以当做事件组来使用
发送通知给任务,递增通知值,可以当做计数信号量使用。 通过对以上任务通知方式的合理使用,可以在一定场合下替代 FreeRTOS 的信号量, 队列、事件组等。
有以下限制 :
只能有一个任务接收通知消息,因为必须指定接收通知的任务
只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态
1.任务通知实现
1.指定任务通知函数 xTaskNotifyGive()(任务)vTaskNotifyGiveFromISR()(中断)
2.发送任务通知函数 xTaskNotify()(任务)xTaskNotifyFromISR()(中断)
3.发送任务通知并返回上一个任务通知值函数 xTaskNotifyAndQuery()(任务)xTaskNotifyAndQueryFromISR()(中断)
4.获取任务通知函数(二值、计数信号量) ulTaskNotifyTake()
5.等待任务通知函数 xTaskNotifyWait()
2.任务通知在任务中实现
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建Receive1_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive1_Task, /* 任务入口函数 */
(const char* )"Receive1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&Receive1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive1_Task任务成功!\r\n");
/* 创建Receive2_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Receive2_Task, /* 任务入口函数 */
(const char* )"Receive2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Receive2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Receive2_Task任务成功!\r\n");
/* 创建Send_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Send_Task, /* 任务入口函数 */
(const char* )"Send_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )4, /* 任务的优先级 */
(TaskHandle_t* )&Send_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Send_Task任务成功!\n\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void Receive1_Task(void* parameter)
{
while (1)
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);//获取任务通知 ,没获取到则一直等待
printf("Receive1_Task 任务通知获取成功!\n\n");
LED1_TOGGLE;
}
}
static void Receive2_Task(void* parameter)
{
while (1)
{
/* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
* xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
* pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
*/
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);//获取任务通知 ,没获取到则一直等待
printf("Receive2_Task 任务通知获取成功!\n\n");
LED2_TOGGLE;
}
}
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
/* KEY1 被按下 */
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
xReturn = xTaskNotifyGive(Receive1_Task_Handle);//指定任务通知函数
/* 此函数只会返回pdPASS */
if( xReturn == pdTRUE )
printf("Receive1_Task_Handle 任务通知发送成功!\r\n");
}
/* KEY2 被按下 */
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
xReturn = xTaskNotifyGive(Receive2_Task_Handle);//指定任务通知函数
/* 此函数只会返回pdPASS */
if( xReturn == pdPASS )
printf("Receive2_Task_Handle 任务通知发送成功!\r\n");
}
vTaskDelay(20);
}
}
十一、软件定时器
软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的,使用软件定时器时,需要我们在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数(任务)),在回调函数中处理信息
FreeRTOS 软件定时器功能上支持:
裁剪:能通过宏关闭软件定时器功能。
软件定时器创建。
软件定时器启动。
软件定时器停止。
软件定时器复位。
软件定时器删除。
FreeRTOS 提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时 间到之后都会调用软件定时器的回调函数,用户可以在回调函数中加入要执行的工程代码。
单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器进入休眠状态,不再重新执行。
周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除。
FreeRTOS通过一个 prvTimerTask 任务(也叫守护任务 Daemon)管理软定时器,它是在启动调度器时自动创建的,为了满足用户定时需求,prvTimerTask 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。只有设置 FreeRTOSConfig.h 中的宏定义 configUSE_TIMERS 设置为 1 ,将相关代码编译进来,才能正常使用软件定时器相关功能,软件定时器以系统节拍周期为计时单位。系统节拍是系统的心跳节拍,表示系统时钟的频率,就类似人的心跳,1s 能跳动多少下,系统节拍配置为 configTICK_RATE_HZ,该宏在 FreeRTOSConfig.h 中有定义,默认是 1000。那么系统的时 钟节拍周期就为 1ms(1s跳动 1000 下,每一下就为 1ms)
软件定时器是可选的系统资源,在创建定时器的时候会分配一块内存空间。当用户创建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表,FreeRTOS 中采用两个定时器列 表维护软件定时器,pxCurrentTimerList 与 pxOverflowTimerList 是列表指针,在初始化的时 候分别指向 xActiveTimerList1 与 xActiveTimerList2
软件定时器用到的列表:
PRIVILEGED_DATA static List_t xActiveTimerList1;
PRIVILEGED_DATA static List_t xActiveTimerList2;
PRIVILEGED_DATA static List_t *pxCurrentTimerList;
PRIVILEGED_DATA static List_t *pxOverflowTimerList;
xCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到 pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时 器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起, 因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到
pxOverflowTimerList :是在软件定时器溢出的时候使用,作用与 pxCurrentTimerList 一致
1.系统时钟运行
系统在不断运行,而 xTimeNow(xTickCount) 随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,xTimeNow 变量会加 1), 在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间 xTimeNow 是否大于或等于下一个定时器唤醒时间 xTicksToWait,若大于则表示已经超时, 定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息
使用软件定时器时候要注意以下几点:
软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的API接口,在回调函数中也绝对不允许出现死循环。
软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为configTIMER_TASK_PRIORITY,为了更好响应,该优先级应设置为所有任 务中最高的优先级,创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件 定时器,并回收资源
定时器任务的堆栈大小默认为 configTIMER_TASK_STACK_DEPTH 个字节。
2.软件定时器实现(创建、启动、停止、任务、删除)
1.软件定时器创建函数 xTimerCreate()
2.软件定时器启动函数 xTimerStart()
3.软件定时器停止函数 xTimerStop()
4. 软件定时器任务 xTimerCreateTimerTask()
5.软件定时器删除函数 xTimerDelete()
3.软件定时器在任务中实现
static TimerHandle_t Swtmr1_Handle =NULL; /* 软件定时器句柄 */
static TimerHandle_t Swtmr2_Handle =NULL; /* 软件定时器句柄 */
static uint32_t TmrCb_Count1 = 0; /* 记录软件定时器1回调函数执行次数 */
static uint32_t TmrCb_Count2 = 0; /* 记录软件定时器2回调函数执行次数 */
static void AppTaskCreate(void)
{
taskENTER_CRITICAL(); //进入临界区
/************************************************************************************
* 创建软件周期定时器
* 函数原型
* TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )
* @uxAutoReload : pdTRUE为周期模式,pdFALS为单次模式
* 单次定时器,周期(1000个时钟节拍),周期模式
*************************************************************************************/
Swtmr1_Handle=xTimerCreate((const char* )"AutoReloadTimer",
(TickType_t )1000,/* 定时器周期 1000(tick) */
(UBaseType_t )pdTRUE,/* 周期模式 */
(void* )1,/* 为每个计时器分配一个索引的唯一ID */
(TimerCallbackFunction_t)Swtmr1_Callback); //创建软件定时器
if(Swtmr1_Handle != NULL)
{
/***********************************************************************************
* xTicksToWait:如果在调用xTimerStart()时队列已满,则以tick为单位指定调用任务应保持
* 在Blocked(阻塞)状态以等待start命令成功发送到timer命令队列的时间。
* 如果在启动调度程序之前调用xTimerStart(),则忽略xTicksToWait。在这里设置等待时间为0.
**********************************************************************************/
xTimerStart(Swtmr1_Handle,0); //开启周期定时器
}
/************************************************************************************
* 创建软件周期定时器
* 函数原型
* TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )
* @uxAutoReload : pdTRUE为周期模式,pdFALS为单次模式
* 单次定时器,周期(5000个时钟节拍),单次模式
*************************************************************************************/
Swtmr2_Handle=xTimerCreate((const char* )"OneShotTimer",
(TickType_t )5000,/* 定时器周期 5000(tick) */
(UBaseType_t )pdFALSE,/* 单次模式 */
(void* )2,/* 为每个计时器分配一个索引的唯一ID */
(TimerCallbackFunction_t)Swtmr2_Callback); //创建软件定时器
if(Swtmr2_Handle != NULL)
{
/***********************************************************************************
* xTicksToWait:如果在调用xTimerStart()时队列已满,则以tick为单位指定调用任务应保持
* 在Blocked(阻塞)状态以等待start命令成功发送到timer命令队列的时间。
* 如果在启动调度程序之前调用xTimerStart(),则忽略xTicksToWait。在这里设置等待时间为0.
**********************************************************************************/
xTimerStart(Swtmr2_Handle,0); //开启周期定时器
}
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
static void Swtmr1_Callback(void* parameter)
{
TickType_t tick_num1;
TmrCb_Count1++; /* 每回调一次加一 */
tick_num1 = xTaskGetTickCount(); /* 获取滴答定时器的计数值 */
LED1_TOGGLE;
printf("Swtmr1_Callback函数执行 %d 次\n", TmrCb_Count1);
printf("滴答定时器数值=%d\n", tick_num1);
}
static void Swtmr2_Callback(void* parameter)
{
TickType_t tick_num2;
TmrCb_Count2++; /* 每回调一次加一 */
tick_num2 = xTaskGetTickCount(); /* 获取滴答定时器的计数值 */
printf("Swtmr2_Callback函数执行 %d 次\n", TmrCb_Count2);
printf("滴答定时器数值=%d\n", tick_num2);
}
十二、内存管理(静态内存管理、动态内存管理)
FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略
嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法
可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反
FreeRTOS 内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一。主 要包括内存的初始化、分配以及释放
为什么不直接使用 C 标准库中的内存管理函数呢?在电脑中我们可以用 malloc()和 free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,原因有以下几点:
1.这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。
2.它们的实现可能非常的大,占据了相当大的一块代码空间。
3.他们几乎都不是安全的。
4.它们并不是确定的,每次调用这些函数执行的时间可能都不一样。
5. 它们有可能产生碎片。
6. 这两个函数会使得链接器配置得复杂。
7.如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为debug的灾难。
嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间 必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与