(1)FreeRTOS是我一天过完的,由此回忆并且记录一下。个人认为,如果只是入门,利用STM32CubeMX是一个非常好的选择。学习完本系列课程之后,再去学习网上的一些其他课程也许会简单很多。
(2)本系列课程是使用的keil软件仿真平台,所以对于没有开发板的同学也可也进行学习。
(3)叠甲,再次强调,本系列课程仅仅用于入门。学习完之后建议还要再去寻找其他课程加深理解。
(4)本系列博客对应代码仓库:
(1)将1.1章节博客最终的代码复制一份。
(2)首先,我们调整高其中一个任务的优先级(按
Ctrl+F
搜索add threads
即可找到如下部分,并进行补充)
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
keilTaskHandle = xTaskCreateStatic(StartKeilTask,"KeilTask", 128, NULL, osPriorityLow2, g_pucStackKeilTaskBuff,&g_TCBKeilTask);
if(keilTaskHandle == NULL)
{
printf("KeilTask creation failed\r\n");
}
(3)填充
StartCubemxTask()
函数。(按Ctrl+F
搜索StartCubemxTask
即可找到任务函数)
/* USER CODE END Header_StartCubemxTask */
void StartCubemxTask(void *argument)
{
/* USER CODE BEGIN StartCubemxTask */
char *CubemxTaskPrintf = (char *)argument;
/* Infinite loop */
for(;;)
{
printf(CubemxTaskPrintf);
HAL_Delay(100);
}
/* USER CODE END StartCubemxTask */
}
(1)这是一个阻塞式延时函数,会占用CPU的资源,不会切换到其他任务。
(2)前面章节,我们讲解了FreeRTOS的4种任务状态。在多任务中,使用阻塞延时函数,其实就是一直在Running
(运行态)死等延时结束,这样是非常占用CPU
资源的,用粗俗一点的话来说,就是占着茅坑不拉*。所以说,对于RTOS
而言,是不建议使用HAL_Delay()
这种阻塞延时函数的。
(3)其实在裸机中,也不建议使用HAL_Delay()
这种阻塞式延时函数。
(4)HAL_Delay()
函数声明如下,单位是1ms
,传入的值要求是0~65535之间。
/**
* @brief 阻塞式延时,单位1ms
*
* @param 要延时的时间,单位ms,取值范围是0~65535
*
* @return 无
*/
__weak void HAL_Delay(uint32_t Delay);
(1)这里不需要进行任何调整,代码如下。(按
Ctrl+F
搜索StartKeilTask
即可找到任务函数)
void StartKeilTask(void *argument)
{
while(1)
{
//实验1,阻塞式延时
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(100);
}
}
(2)开始调试,会发现,串口没有数据打印。这个是为什么呢?原因很简单,因为波形变化的任务
StartKeilTask()
优先级高于串口任务StartCubemxTask()
,而又因为FreeRTOS
作为抢占式操作系统,高优先级任务不停止,低优先级任务无法执行。因此,串口将永远无法出现数据打印信息。
(1)
vTaskDelay()
函数的实现,其实就是类似一次任务挂起,等延时时间到,恢复任务。因此vTaskDelay()
函数不会造成阻塞情况,当调用vTaskDelay()
函数之后,任务进入阻塞态,运行态执行其他任务。这样做能够提高CPU
的使用率,同时给低优先级的任务执行空间。
(2)vTaskDelay()
函数的声明如下,需要注意的是,vTaskDelay()
函数的单位并一定是1ms
。vTaskDelay()
函数是以时钟节拍(tick
)为单位的时间,我们可以根据portTICK_PERIOD_MS
这宏知道时钟节拍是多少。
(3)我们要进行延时101ms,就传入101/portTICK_PERIOD_MS
。需要各位了解的是,如果时钟节拍(tick
)是1ms,那么最终延时的确实是101ms。但是如果时钟节拍(tick
)是10ms,那么最终延时的时间是100ms。 因为我们的单位是时钟节拍(tick
),最终的延时时间要是时钟节拍(tick
)的整数倍。
/**
* @brief 非阻塞式相对延时
*
* @param 要延时的时间,单位tick
*
* @return 无
*/
void vTaskDelay( const TickType_t xTicksToDelay );
(4)如果要使用
vTaskDelay()
函数,需要确认在FreeRTOSConfig.h
文件中将INCLUDE_vTaskDelay
这个宏定义置1。
(5)
vTaskDelay()
函数在使用过程中,需要注意以下两点。
<1>临界区中不能使用。(临界区的知识将会在后面的同步与互斥章节讲解)
<2>不能在中断服务中使用。
// 进入临界区
pthread_mutex_lock(&mutex);
// 访问共享资源,这里不能使用这里不能使用 vTaskDelay()
sharedVariable++;
// 离开临界区
pthread_mutex_unlock(&mutex);
(1)按
Ctrl+F
搜索StartKeilTask
即可找到任务函数,并且进行如下补充。
void StartKeilTask(void *argument)
{
while(1)
{
//实验2,非阻塞式相对延时
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
vTaskDelay(100/portTICK_PERIOD_MS);
}
}
(2)开始仿真此时我们可以看到,虚拟逻辑分析仪上有电平变化,并且虚拟串口有数据打印了。对照这两张图的结果,就很好的能够理解和区分
vTaskDelay()
和HAL_Delay()
这两个函数的阻塞与非阻塞的意思。
(1)
vTaskDelay()
延时函数很好理解,即相对于vTaskDelay()
被调用的时刻开始进行相对的时间延时。
(2)但是,假如我想像裸机定时器那样,进行周期性的绝对延时,应当如何处理呢?为了解决这个问题,FreeRTOS
提供了vTaskDelayUntil()
。
(3)vTaskDelayUntil()
函数每次延时是根据调用xTaskGetTickCount()
的时刻开始,然后间隔你设置的延时时间进行延时。xTaskGetTickCount()
被调用的时刻,你可以理解为裸机定时器的定时器计数开始时刻,传入的xTimeIncrement
就是裸机定时的周期定时时间,单位是tick
。
(4)需要注意的是,这里的vTaskDelayUntil()
提供了裸机定时器的效果,但是却有些许不同,裸机的定时器只要配置好之后,就会以绝对时间周期性调用,之后就不再需要理会。这里不同,你每次被唤醒之后,还想以绝对时间周期性延时,那么就需要再调用一次vTaskDelayUntil()
重新装载延时时间。否则将会停止延时。
/**
* @brief 非阻塞式相对延时
*
* @param 无
*
* @return 系统时钟节拍数
*/
TickType_t xTaskGetTickCount( void );
/**
* @brief 非阻塞式相对延时
*
* @param -pxPreviousWakeTime 存储任务上一次唤醒的时间,如果第一次使用,调用xTaskGetTickCount()进行初始化
* -xTimeIncrement 要延时的时间,单位tick
*
* @return 无
*/
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );
(1)按
Ctrl+F
搜索StartKeilTask
即可找到任务函数,并且进行如下补充。
void StartKeilTask(void *argument)
{
//实验3,周期性非阻塞式延时
TickType_t preTime = xTaskGetTickCount();
int i = 1;
while(1)
{
//实验3,非阻塞式绝对延时,了解绝对延时和相对延时区别
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
if(i < 4)
{
i++;
HAL_Delay(i*20);
}
else
{
i = 1;
HAL_Delay(i*20);
}
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
//vTaskDelay(100/portTICK_PERIOD_MS);
vTaskDelayUntil(&preTime, 100/portTICK_PERIOD_MS);
}
}
(2)我们对比
vTaskDelay(100/portTICK_PERIOD_MS)
和vTaskDelayUntil(&preTime, 100/portTICK_PERIOD_MS)
看一下结果。
<1>在vTaskDelay(100/portTICK_PERIOD_MS)
的测试结果中,我们发现,低电平的持续时间永远是0.1s。因为他的延时是基于vTaskDelay(100/portTICK_PERIOD_MS)
被调用时刻开启的相对延时。
<2>在
vTaskDelayUntil(&preTime, 100/portTICK_PERIOD_MS)
的测试结果中,我们发现,两次上升沿的间隔时间永远是0.1s。因为这个的延时是基于pxPreviousWakeTime
这个参数为基准的绝对延时,而这个参数的值一般都是xTaskGetTickCount()
函数提供。
(1)因为,上面我说了
vTaskDelayUntil()
提供了裸机定时器的效果,但是却有些许不同。这里我们将会展示,如果只调用vTaskDelayUntil()
函数10次,10次之后,是否还会存在低电平。(注意,如果vTaskDelayUntil()
不再具备延时效果之后,将不会看到低电平。因为引脚置低之后,将会马上置高。)
void StartKeilTask(void *argument)
{
//实验3,周期性非阻塞式延时
TickType_t preTime = xTaskGetTickCount();
int i = 1,j = 0;
while(1)
{
//实验4,了解vTaskDelayUntil()的延时特性
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
if(i < 4)
{
i++;
HAL_Delay(i*20);
}
else
{
i = 1;
HAL_Delay(i*20);
}
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
if(j<10)
{
j++;
vTaskDelayUntil(&preTime, 100/portTICK_PERIOD_MS);
}
}
}
(2)测试后发现,这个延时函数,每次绝对延时,都需要被调用不能够像裸机定时器那样自动延时。
(1)这两个函数分别是
vTaskDelay()
和vTaskDelayUntil()
的RTOS
抽象层。使用方法和上述一样,不再赘述。
(1)C站:freertos的vTaskDelay使用禁忌
(2)C站:FreeRTOS一天一个小知识之任务延时函数vTaskDelay
(3)FreeRTOS官方文档:vTaskDelay函数介绍
(4)FreeRTOS官方文档:vTaskDelayUntil函数介绍