移植正点原子HAL库延时函数
相关文章:
正点原子延时函数为什么是死等
STM32HAL库初始化配置-CubeMX生成的系统初始化内容写哪去了
STM32HAL库滴答定时器(SysTick)实现1ms中断的机制详解
最近使用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;
#endif
g_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 的任务调度器 */
#endif
told = 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已经无法提供这么小的延时了,采用普通方式延时 */
}
#endif
delay_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 0
void 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;
#endif
HAL_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);
}