移植正点原子HAL库延时函数
相关文章:
正点原子延时函数为什么是死等
STM32HAL库初始化配置-CubeMX生成的系统初始化内容写哪去了
STM32HAL库滴答定时器(SysTick)实现1ms中断的机制详解
文章目录
- 移植正点原子HAL库延时函数
- 一、裸机移植
- delay.c
- delay.h
- 二、FreeRTOS移植
- 方法1
- 方法2
- 方法3
最近使用cubemx移植原子哥的stm32姿态解算mpu6050到HAL库时发现缺少us级延时函数,这在很多iic例程中都是必不可少的,如果能移植一个靠谱的版本,以后的开发会省很多事,于是···
一、裸机移植
我看了几篇博客,里面的提到的方法都比较简单粗暴,要么是使用空指令(适应性不强),要么是额外使用一个定时器(硬件开销太大),还有大部分是使用滴答定时器重装载(老一代延时方法),都不是我想要的。不过在看原子哥的FreeRTOS教程的时候,他的死延时也是使用了滴答定时器,但是不会影响系统任务调度,于是···
我翻看了原子哥很多个版本的STM32例程,发现里面的延时函数也是龙生九子-各有不同,不过我在新版HAL库例程中找到了一版比较适用的,具体版本说明如下:
* 修改说明* V1.0 20230206* 第一次发布* V1.1 20230225* 修改SYS_SUPPORT_OS部分代码, 默认仅支持UCOSII 2.93.01版本, 其他OS请参考实现* 修改delay_init不再使用8分频,全部统一使用MCU时钟* 修改delay_us使用时钟摘取法延时, 兼容OS* 修改delay_ms直接使用delay_us延时实现.
这里面最重要的一点是时钟摘取法
,只通过不断读取systick计数值来判断延时是否到达,不会改变systick的配置,而老一代延时方法是直接设定systick的重装载值为延时时间。这也是能不能直接移植到HAL库而不额外占用硬件资源的关键,因为标准库裸机一般用不到滴答定时器,所以可以拿来专门作延时,而HAL库裸机默认使用滴答定时器作为自己的时基,配置为1ms中断,实现ms级延时,并为一些超时机制提供时基。而这种只读的方式恰好不会影响原来的滴答定时器正常运行,可以直接拿来用。使用时需要把sys.h里的一个宏拿过来,放到delay.h,意思是不支持OS,使OS相关的代码条件编译失效 ,或者可以直接把这些条件编译部分的代码删掉
,使用前记得初始化一下,参数是系统时钟频率(MHz)
如delay_init(72);
,要放在时钟配置之后:
HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit */delay_init(72);/* USER CODE END SysInit */
/*** SYS_SUPPORT_OS用于定义系统文件夹是否支持OS* 0,不支持OS* 1,支持OS*/
#define SYS_SUPPORT_OS 0
原版代码:
delay.c
/******************************************************************************************************* @file delay.c* @author 正点原子团队(ALIENTEK)* @version V1.1* @date 2023-02-25* @brief 使用SysTick的普通计数模式对延迟进行管理(支持ucosii)* 提供delay_init初始化函数, delay_us和delay_ms等延时函数* @license Copyright (c) 2022-2032, 广州市星翼电子科技有限公司***************************************************************************************************** @attention** 实验平台:正点原子 STM32F103开发板* 在线视频:www.yuanzige.com* 技术论坛:www.openedv.com* 公司网址:www.alientek.com* 购买地址:openedv.taobao.com** 修改说明* V1.0 20230206* 第一次发布* V1.1 20230225* 修改SYS_SUPPORT_OS部分代码, 默认仅支持UCOSII 2.93.01版本, 其他OS请参考实现* 修改delay_init不再使用8分频,全部统一使用MCU时钟* 修改delay_us使用时钟摘取法延时, 兼容OS* 修改delay_ms直接使用delay_us延时实现.******************************************************************************************************/#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"static uint32_t g_fac_us = 0; /* us延时倍乘数 *//* 如果SYS_SUPPORT_OS定义了,说明要支持OS了(不限于UCOS) */
#if SYS_SUPPORT_OS/* 添加公共头文件 ( ucos需要用到) */
#include "os.h"/* 定义g_fac_ms变量, 表示ms延时的倍乘数, 代表每个节拍的ms数, (仅在使能os的时候,需要用到) */
static uint16_t g_fac_ms = 0;/** 当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持* 首先是3个宏定义:* delay_osrunning :用于表示OS当前是否正在运行,以决定是否可以使用相关函数* delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init将根据这个参数来初始化systick* delay_osintnesting :用于表示OS中断嵌套级别,因为中断里面不可以调度,delay_ms使用该参数来决定如何运行* 然后是3个函数:* delay_osschedlock :用于锁定OS任务调度,禁止调度* delay_osschedunlock:用于解锁OS任务调度,重新开启调度* delay_ostimedly :用于OS延时,可以引起任务调度.** 本例程仅作UCOSII的支持,其他OS,请自行参考着移植*//* 支持UCOSII */
#define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行 */
#define delay_ostickspersec OS_TICKS_PER_SEC /* OS时钟节拍,即每秒调度次数 */
#define delay_osintnesting OSIntNesting /* 中断嵌套级别,即中断嵌套次数 *//*** @brief us级延时时,关闭任务调度(防止打断us级延迟)* @param 无* @retval 无*/
void delay_osschedlock(void)
{OSSchedLock(); /* UCOSII的方式,禁止调度,防止打断us延时 */
}/*** @brief us级延时时,恢复任务调度* @param 无* @retval 无*/
void delay_osschedunlock(void)
{OSSchedUnlock(); /* UCOSII的方式,恢复调度 */
}/*** @brief us级延时时,恢复任务调度* @param ticks: 延时的节拍数* @retval 无*/
void delay_ostimedly(uint32_t ticks)
{OSTimeDly(ticks); /* UCOSII延时 */
}/*** @brief systick中断服务函数,使用OS时用到* @param ticks : 延时的节拍数 * @retval 无*/
void SysTick_Handler(void)
{/* OS 开始跑了,才执行正常的调度处理 */if (delay_osrunning == OS_TRUE){/* 调用 uC/OS-II 的 SysTick 中断服务函数 */OS_CPU_SysTickHandler();}HAL_IncTick();
}
#endif/*** @brief 初始化延迟函数* @param sysclk: 系统时钟频率, 即CPU频率(rcc_c_ck), 72MHz* @retval 无*/
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS /* 如果需要支持OS */uint32_t reload;
#endifg_fac_us = sysclk; /* 由于在HAL_Init中已对systick做了配置,所以这里无需重新配置 */
#if SYS_SUPPORT_OS /* 如果需要支持OS. */reload = sysclk; /* 每秒钟的计数次数 单位为M */reload *= 1000000 / delay_ostickspersec; /* 根据delay_ostickspersec设定溢出时间,reload为24位* 寄存器,最大值:16777216,在72M下,约合0.233s左右*/g_fac_ms = 1000 / delay_ostickspersec; /* 代表OS可以延时的最少单位 */SysTick->CTRL |= 1 << 1; /* 开启SYSTICK中断 */SysTick->LOAD = reload; /* 每1/delay_ostickspersec秒中断一次 */SysTick->CTRL |= 1 << 0; /* 开启SYSTICK */
#endif
}/*** @brief 延时nus* @note 无论是否使用OS, 都是用时钟摘取法来做us延时* @param nus: 要延时的us数* @note nus取值范围: 0 ~ (2^32 / fac_us) (fac_us一般等于系统主频, 自行套入计算)* @retval 无*/
void delay_us(uint32_t nus)
{uint32_t ticks;uint32_t told, tnow, tcnt = 0;uint32_t reload = SysTick->LOAD; /* LOAD的值 */ticks = nus * g_fac_us; /* 需要的节拍数 */#if SYS_SUPPORT_OS /* 如果需要支持OS */delay_osschedlock(); /* 锁定 OS 的任务调度器 */
#endiftold = SysTick->VAL; /* 刚进入时的计数器值 */while (1){tnow = SysTick->VAL;if (tnow != told){if (tnow < told){tcnt += told - tnow; /* 这里注意一下SYSTICK是一个递减的计数器就可以了 */}else{tcnt += reload - tnow + told;}told = tnow;if (tcnt >= ticks) {break; /* 时间超过/等于要延迟的时间,则退出 */}}}#if SYS_SUPPORT_OS /* 如果需要支持OS */delay_osschedunlock(); /* 恢复 OS 的任务调度器 */
#endif }/*** @brief 延时nms* @param nms: 要延时的ms数 (0< nms <= (2^32 / fac_us / 1000))(fac_us一般等于系统主频, 自行套入计算)* @retval 无*/
void delay_ms(uint16_t nms)
{#if SYS_SUPPORT_OS /* 如果需要支持OS, 则根据情况调用os延时以释放CPU */if (delay_osrunning && delay_osintnesting == 0) /* 如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度) */{if (nms >= g_fac_ms) /* 延时的时间大于OS的最少时间周期 */{delay_ostimedly(nms / g_fac_ms); /* OS延时 */}nms %= g_fac_ms; /* OS已经无法提供这么小的延时了,采用普通方式延时 */}
#endifdelay_us((uint32_t)(nms * 1000)); /* 普通方式延时 */
}/*** @brief HAL库内部函数用到的延时* @note HAL库的延时默认用Systick,如果我们没有开Systick的中断会导致调用这个延时后无法退出* @param Delay : 要延时的毫秒数* @retval None*/
void HAL_Delay(uint32_t Delay)
{delay_ms(Delay);
}
delay.h
/******************************************************************************************************* @file delay.h* @author 正点原子团队(ALIENTEK)* @version V1.0* @date 2020-04-17* @brief 使用SysTick的普通计数模式对延迟进行管理(支持ucosii)* 提供delay_init初始化函数, delay_us和delay_ms等延时函数* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司***************************************************************************************************** @attention** 实验平台:正点原子 STM32F103开发板* 在线视频:www.yuanzige.com* 技术论坛:www.openedv.com* 公司网址:www.alientek.com* 购买地址:openedv.taobao.com** 修改说明* V1.0 20211103* 第一次发布******************************************************************************************************/#ifndef __DELAY_H
#define __DELAY_H#include "./SYSTEM/sys/sys.h"#define SYS_SUPPORT_OS 0void delay_init(uint16_t sysclk); /* 初始化延迟函数 */
void delay_ms(uint16_t nms); /* 延时nms */
void delay_us(uint32_t nus); /* 延时nus */#if (!SYS_SUPPORT_OS) /* 如果不支持OS */void HAL_Delay(uint32_t Delay); /* HAL库的延时函数,HAL库内部用到 */
#endif#endif
二、FreeRTOS移植
使用操作系统时,滴答定时器会被操作系统占用,使用上述方法虽然仍然行得通,但是有一些弊端。如果是像正点原子的教程一样,手动写初始化代码,区别倒是不大,但是如果你是使用cubemx自动生成,就不一样了。主要原因是二者的HAL库时基不一样,在手动写时,你可以把HAL库时基和FreeRTOS同时使用滴答定时器,但是如果是cubemx自动生成,他会让你重新选一个定时器作为HAL库时基,也就是这步:
在代码的体现就是HAL_Init()->HAL_InitTick(TICK_INT_PRIORITY);
里面的内容,可以看到,默认的和生成的是不一样的:
默认的(裸机):
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{/* Configure the SysTick to have interrupt in 1ms time basis*/if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U){return HAL_ERROR;}/* Configure the SysTick IRQ priority */if (TickPriority < (1UL << __NVIC_PRIO_BITS)){HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);uwTickPrio = TickPriority;}else{return HAL_ERROR;}/* Return function status */return HAL_OK;
}
使用cubemx生成FreeRTOS时,会重写这个函数并额外生成一个timebase.c文件存放:
HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{RCC_ClkInitTypeDef clkconfig;uint32_t uwTimclock, uwAPB1Prescaler = 0U;uint32_t uwPrescalerValue = 0U;uint32_t pFLatency;HAL_StatusTypeDef status = HAL_OK;/* Enable TIM3 clock */__HAL_RCC_TIM3_CLK_ENABLE();/* Get clock configuration */HAL_RCC_GetClockConfig(&clkconfig, &pFLatency);/* Get APB1 prescaler */uwAPB1Prescaler = clkconfig.APB1CLKDivider;/* Compute TIM3 clock */if (uwAPB1Prescaler == RCC_HCLK_DIV1){uwTimclock = HAL_RCC_GetPCLK1Freq();}else{uwTimclock = 2UL * HAL_RCC_GetPCLK1Freq();}/* Compute the prescaler value to have TIM3 counter clock equal to 1MHz */uwPrescalerValue = (uint32_t) ((uwTimclock / 1000000U) - 1U);/* Initialize TIM3 */htim3.Instance = TIM3;/* Initialize TIMx peripheral as follow:+ Period = [(TIM3CLK/1000) - 1]. to have a (1/1000) s time base.+ Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock.+ ClockDivision = 0+ Counter direction = Up*/htim3.Init.Period = (1000000U / 1000U) - 1U;htim3.Init.Prescaler = uwPrescalerValue;htim3.Init.ClockDivision = 0;htim3.Init.CounterMode = TIM_COUNTERMODE_UP;htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;status = HAL_TIM_Base_Init(&htim3);if (status == HAL_OK){/* Start the TIM time Base generation in interrupt mode */status = HAL_TIM_Base_Start_IT(&htim3);if (status == HAL_OK){/* Enable the TIM3 global Interrupt */HAL_NVIC_EnableIRQ(TIM3_IRQn);/* Configure the SysTick IRQ priority */if (TickPriority < (1UL << __NVIC_PRIO_BITS)){/* Configure the TIM IRQ priority */HAL_NVIC_SetPriority(TIM3_IRQn, TickPriority, 0U);uwTickPrio = TickPriority;}else{status = HAL_ERROR;}}}/* Return function status */return status;
}
并且在main函数下面会有回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{/* USER CODE BEGIN Callback 0 *//* USER CODE END Callback 0 */if (htim->Instance == TIM3) {HAL_IncTick();}/* USER CODE BEGIN Callback 1 *//* USER CODE END Callback 1 */
}
正点原子是手写的,HAL库和FreeRTOS都是使用的滴答定时器,所以他可以直接使用原来的延时函数,而cubemx生成时,由于滴答定时器只有在任务启动调度后才会配置,所以在这之前滴答定时器是不工作的,这个时候是无法使用延时的。可是一些外设初始化要依赖于延时,要想使用,怎么办?
方法1
必须在启动任务调度器osKernelStart();
之后使用,因为这一步才会配置滴答定时器
,这之后的内容就是运行任务了,滴答定时器正常启动了,可以使用延时了:
/* USER CODE END 2 *//* Init scheduler */osKernelInitialize();/* Call init function for freertos objects (in cmsis_os2.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)
这样可以把每个任务需要用的外设初始化放在自己的任务while(1)之前,但是这样不太利于管理,并且有些任务之间联系比较紧密,可能需要提前初始化好才能正常通信,分开初始化的话可能由于调度顺序会发生一些错误,这就需要单独创建一个任务专门进行统一的初始化,并在里面创建其他任务,完成后删除自己,具体流程如下:
void start_task(void *pvParameters)
{/* 初始化外设 */xxx_Init();······/* 进入临界区 */taskENTER_CRITICAL();/* 创建其他任务 */xTaskCreate();······/* 删除开始任务 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();}
方法2
在任务调度之前先初始化滴答定时器用一会儿
,反正启动任务调度器后会覆盖滴答定时器的配置,那我在你还没用之前先用一会,完成自己的外设初始化,到时候还给你不就行了,你启动了之后我反正就正常用了。具体操作可以打开delay_init()里的条件编译(就是初始化滴答定时器的内容),注意不要直接把#define SYS_SUPPORT_OS 0
这个宏打开,因为这里面有一些代码是用不到的,可能和cubemx生成的有冲突,可以把其他的都删了。具体内容,让这里面的条件编译生效:
/*** @brief 初始化延迟函数* @param sysclk: 系统时钟频率, 即CPU频率(rcc_c_ck), 168Mhz* @retval 无*/
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS /* 如果需要支持OS */uint32_t reload;
#endifHAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);/* SYSTICK使用内核时钟源,同CPU同频率 */g_fac_us = sysclk; /* 不论是否使用OS,g_fac_us都需要使用 */
#if SYS_SUPPORT_OS /* 如果需要支持OS. */reload = sysclk; /* 每秒钟的计数次数 单位为M */reload *= 1000000 / configTICK_RATE_HZ; /* 根据delay_ostickspersec设定溢出时间,reload为24位寄存器,最大值:16777216,在168M下,约合0.099s左右 */SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; /* 开启SYSTICK中断 */SysTick->LOAD = reload; /* 每1/delay_ostickspersec秒中断一次 */SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* 开启SYSTICK */
#endif
}
方法3
既然cubemx生成代码时为HAL库重新选了一个定时器,那为何不直接用这个呢
。而且这个定时器在hal_init()时就配置好了,并且优先级还是可以手动调的,完全不用看滴答定时器的脸色了,不用等启动调度器后就可以使用。这个定时器替代了裸机下滴答定时器的作用,为HAL库提供ms级延时,原理类似,我们可以在原来的延时函数基础上稍作改动,仍然使用时钟摘取法来实现延时。这里参考韦东山老师的例程进行修改,有一个踩坑点需要注意:原来的滴答定时器是一个递减的定时器,而现在的普通定时器是递增的,所以原来的程序told和tnow需要交换一下;并且这里的定时器时钟不是72MHz,而是1MHz,所以我们我们需要重新调整一下节拍数
:
这一点可以在它的初始化里有提到:
/* Compute the prescaler value to have TIM3 counter clock equal to 1MHz */uwPrescalerValue = (uint32_t) ((uwTimclock / 1000000U) - 1U);/* Initialize TIM3 */htim3.Instance = TIM3;/* Initialize TIMx peripheral as follow:+ Period = [(TIM3CLK/1000) - 1]. to have a (1/1000) s time base.+ Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock.+ ClockDivision = 0+ Counter direction = Up*/htim3.Init.Period = (1000000U / 1000U) - 1U;htim3.Init.Prescaler = uwPrescalerValue;htim3.Init.ClockDivision = 0;htim3.Init.CounterMode = TIM_COUNTERMODE_UP;htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
代码:
/*** @brief 延时nus* @note 无论是否使用OS, 都是用时钟摘取法来做us延时* @param nus: 要延时的us数* @note nus取值范围: 0 ~ (2^32 / fac_us) (fac_us一般等于系统主频, 自行套入计算)* @retval 无*/
void delay_us(uint32_t nus)
{extern TIM_HandleTypeDef htim3; //这里换成cubemx中为HAL库选的时基定时器TIM_HandleTypeDef *timebase = &htim3;uint32_t ticks;uint32_t told, tnow, tcnt = 0;uint32_t reload = __HAL_TIM_GET_AUTORELOAD(timebase); /* LOAD的值 */ticks = nus * reload / (1000); /* 需要的节拍数 */told = __HAL_TIM_GET_COUNTER(timebase); /* 刚进入时的计数器值 */while (1){tnow = __HAL_TIM_GET_COUNTER(timebase);if (tnow != told){if (tnow > told){tcnt += tnow - told; /* 这里注意一下SYSTICK是一个递减的计数器就可以了,注意普通定时器是递增的,所以要交换told,tnow */}else{tcnt += reload - told + tnow; //注意普通定时器是递增的,所以要交换told,tnow}told = tnow;if (tcnt >= ticks) {break; /* 时间超过/等于要延迟的时间,则退出 */}}}
}/*** @brief 延时nms* @param nms: 要延时的ms数 (0< nms <= (2^32 / fac_us / 1000))(fac_us一般等于系统主频, 自行套入计算)* @retval 无*/
void delay_ms(uint16_t nms)
{for (int i = 0; i < nms; i++)delay_us(1000);
}