1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614
第二十三章 SysTick高精度延时实验
前面章节的实验我们使用的是HAL库里自带的API函数HAL_Delay来实现毫秒级别延时的,如果使用到更高精度的延时,例如us级别的延时,我们可以使用定时器来实现,也可以使用SysTick的时钟摘取法来实现。本节,我们介绍如何使用SysTick来实现us级别的延时。
本章分为如下几个小节:
23.1 、SysTick简介;
23.2 、HAL_Delay函数;
23.3 、SysTick高精度延时实验;
23.4 、编译和测试;
23.1 SysTick简介
23.1.1 SysTick初识
- 什么是SysTick
SysTick即系统滴答定时器(system tick timer),它被捆绑在NVIC中,属于Cortex-M内核的一部分。SysTick是一个24位的递减定时器,它是可编程的,软件上可通过对其对应的LOAD寄存器中写入一个数值(最大为224-1)来配置定时器的定时初值。当SysTick以一定的频率工作的时,每来一个脉冲,SysTick从定时初值逐1递减,当递减到0时,SysTick产生一次中断,同时从RELOAD 寄存器(值等于LOAD)中自动重装载定时初值,并重新开始新一轮的递减计数,如此反复。此过程就和我们前面学习的通用定时器以及高级定时器的递减计数功能类似,通过递减计数产生周期性的中断、延时,从而达到计时的效果。
图23.1.1. 1 SysTick是递减计数
SysTick属于Cortex-M内核的一个部分,并不是MCU片上外设,所以找遍了参考手册以及数据手册的犄角旮旯,你也看不到更多有关SysTick的详细介绍,如果想了解SysTick,应该查阅内核有关的文档,例如《ARM Cortex-M3与Cortex-M4处理器权威指南》、《Cortex-M3权威指南(中文)》以及《STM32F3与F4系列Cortex M4内核编程手册》等内核相关文件,这些资料存放在 STM32MP157开发板\开发板光盘A-基础资料\4、参考资料 下。
2. 为什么要有SysTick
SysTick设计的目的就是给操作系统(OS)提供时基,用于周期性地产生中断来定时触发OS内核,如用于任务管理和上下文切换,处理器也可以在不同时间片内处理不同的任务。若应用中存在嵌入式OS,例如TinyOS、UCOS、RTOS和FreeRTOS等操作系统,SysTick会被OS使。如果不使用OS ,SysTick可当做简单的定时器来用。
对于简单的应用程序,例如我们编写的裸机程序,都是在一个while循环里依次调用各单任务来实现的,如果要处理更多任务或者更复杂的应用,再使用裸机程序就力不从心了,而且会出现更多的问题,例如,在while循环的单任务引用中,如果某一个任务出现问题,后续的任务就被牵连,从而导致系统崩溃。为解决这个问题,操作系统就可以登场了,我们以实时操作系统(RTOS)为例子来说明。当RTOS以并行的架构处理多个任务时,SysTick的任务就是给系统的任务调度提供时钟节拍。根据这个节拍,系统的整个时间段被分成很多很多很小的时间片,而这个时间片则是RTOS实现多任务切换的最小时间单位,每个任务每次只能运行一个时间片的时长就得退出给别的任务运行,这样就可以确保任何一个任务都不会霸占整个系统。如果其中某个任务挂掉了,挂掉的任务并不一定会牵连到整个系统,系统依然可以运行,其它任务依然可以正常调度。
只要是Cortex-M内核都有SysTick,由于和MCU外设无关,这样就方便了程序在不同厂家的Coetex-M内核的MCU之间的移植,例如将RTOS移植到别的硬件平台上,由于SysTick的存在,这样就大大降低了移植的难度。
3. SysTick的时钟
SysTick是MCU内核的一个设备,其时钟来自MCU系统时钟,然后经过分频后得到其工作的时钟,分频值可以是1或者8,所以SysTick的时钟频率最大值为209MHz,可以说其时钟精度还是比较高的,我们从时钟树中就可以看出来:
图23.1.1. 2 SysTick的时钟频率最大为209MHz
4. SysTick和其它定时器的差别
SysTick属于Cortex-M内核外设,定时器和RTC属于片上外设。
SysTick一般由ARM设计,每个Cortex-M内核里的SysTick都一样,而定时器和RTC属于片上外设,每个芯片厂家设计的芯片可能不一样,所以定时器以及RTC的资源也会不一样。
SysTick的存在主要是用于操作系统中的,如果应用中不使用操作系统,那么SysTick就当做简单的递减定时器来用;RTC可以分配给MPU使用,不能给MCU使用;定时器既可以给MPU使用,也可以给MCU使用,不过在同一个时刻只能单选。
SysTick的时钟源来自Cortex-M内核时钟,RTC时钟源可以是HSE、LSE和LSI,定时器时钟源来自APB1和APB2。默认情况下,STM32CubeMX使用Systick作为时基给其它程序提供计时,例如HAL_Delay延时函数,以及串口程序中的Timeout 超时机制等等,当然也可以选择其它定时器作为时基:
图23.1.1. 3 Systick作为时基
23.1.2 SysTick 寄存器
固件包的STM32Cube_FW_MP1_V1.2.0\Drivers\CMSIS\Core\Include文件夹下是符合CMSIS标准的内核头文件和CMSIS编译器相关的文件,core开头的是和 Cortex-M 内核相关的文件, MPU开头的是和MPU相关的文件。其中Cortex-M4内核使用core_cm4.h头文件,该文件中有很多关于NVIC中断相关的函数定义和类型定义以及内核的外设相关定义,SysTick相关的寄存器就定义在该文件中:
表23.1.2. 1 SysTick寄存器列表
下面我们来介绍以上4个寄存器:
- STK_CTRL(SysTick 控制及状态寄存器)
STK_CTRL各位如下:
图23.1.2. 1 STK_CTRL寄存器
ENABLE
ENABLE是计数器使能位,用于启用计数器(也就是启用SysTick定时器)。改为置1则使能计数器,清0则关闭计数器。
当ENABLE设置为1时,SysTick定时器被使能,计数器从LOAD寄存器加载RELOAD值,然后递减计数,当递减到0时,COUNTFLAG位变为1,并根据TICKINT的值选择置位SysTick, 然后它将再次加载RELOAD值,并开始计数。
TICKINT
TICKINT是SysTick异常请求使能位,该位为0时,当计数器递减到0的时候,SysTick不产生异常请求;该位为1时,当计数器递减到0时产生异常请求。
注:软件可以使用COUNTFLAG来确定SysTick是否曾经计数为零。
CLKSOURCE
CLKSOURCE用于配置SysTick的时钟源选择,我们前面说了,SysTick的时钟来自MCU系统时钟,分频值可以是1或者8,当该位为0时为1分频,为1时为8分频。
COUNTFLAG
COUNTFLAG是标志计数器是否已经为0,当计数器递减为0时,该位为1。如果读取寄存器或者清除计数器当前值时,该位会被清0.
2. STK_LOAD(SysTick 重装载数值寄存器)
STK_LOAD的各位如下图所示:
图23.1.2. 2 STK_LOAD寄存器
RELOAD[23:0]位是计数器为0时的重装载值,值可以是0x00000001-0x00FFFFFF范围内的任何值。
3. STK_VAL(SysTick当前值寄存器)
图23.1.2. 3 STK_VAL寄存器
CURRENT[23:0]位是当前计数器的值,读取此位即可获得当前计数器的当前值。写入该位任何值均会清空此位为0,同时STK_CTRL寄存器中的COUNTFLAG位也会被清0。
4. STK_CALIB(SysTick校准寄存器)
图23.1.2. 4 STK_CALIB寄存器
TENMS[23:0]
TENMS[23:0]位是10ms校准值,该值应由芯片设计者提供,若读取该值为0,表示校准值不可用。
SKEW
SKEW位用于指示校准值(TENMS值)是否精确,读取此位:
为0,表示校准值正确;
为1,表示校准值不精确,或者校准值未知,这可能会影响SysTick作为软件实时时钟的适用性。
NOREF
NOREF用于指示是否有参考时钟提供给处理器。读取此位:
为0,表示有外部参考时钟可供使用;
为1,表示没有外部参考时钟。
23.1.3 SysTick的HAL库驱动
SysTick的HAL库驱动我们在第七章7.4.2小节 滴答定时器相关函数 部分就已经详细分析过了,大家可以参考前面章节的分析,这里就列举出一些HAL库的API函数。
- HAL_InitTick函数
HAL_InitTick用于配置SysTick的重装载数值寄存器的值,其通过层层调用HAL_SYSTICK_Config函数和SysTick_Config函数完成SysTick的配置,此函数声明如下:
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
函数描述:
用于初始化SysTick,配置SysTick的重装载数值寄存器的值。
函数形参:
形参TickPriority是SysTick的中断优先级。
函数返回值:
无。
注意事项:
1、此函数是弱(weak)定义函数,如果用户在别的地方重新定义了同名函数,该函数将被覆盖,编译器会编译用户定义的那个函数;
2、系统复位后,HAL_Init最先被执行时,或者程序由HAL_RCC_ClockConfig重新配置时钟的时候,该函数都会被调用来初始化SysTick;
3、默认情况下,SysTick是计时的时基,SysTick通过周期性的中断来计时的,如果在别的中断中调用HAL_Delay就要小心了,SysTick中断的优先级必须调用它的中断具有更高的优先级(中断优先级数字上更低),否则程序会卡死。 - HAL_GetTickPrio
HAL_GetTickPrio用于获取SysTick的中断优先级,返回值uwTickPrio是SysTick的中断优先级,函数声明如下:
uint32_t HAL_GetTickPrio(void)
3. HAL_SetTickFreq和HAL_GetTickFreq函数
HAL_SetTickFreq 函数用于重新配置SysTick的中断频率,HAL_GetTickFreq函数用于获取SysTick的中断频率。函数声明如下:
HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)
HAL_TickFreqTypeDef HAL_GetTickFreq(void)
23.2 HAL_Delay函数
23.2.1 SysTick的分频值为1
- 未执行SystemClock_Config函数之前
在前面我们有简单分析过HAL_Delay怎么实现的毫秒延时的,这里我们再次分析实现的过程。
(1)HAL_Init函数
程序进入main.c文件以后,最先执行的是HAL_Init函数,该函数用于初始化HAL库,并配置SysTick每隔1ms生成一次中断。注意的是,此时还没有配置系统时钟(未执行SystemClock_Config函数),系统还是默认使用内部高速时钟HSI,HAL库里SysTick默认采用1分频,所以SysTick时钟频率也为64MHz。
stm32mp1xx_hal.c文件代码
/**
* @brief 初始化HAL库,设置中断优先级分组为4,设置SysTick每隔1ms产生一次中断
* @note 1、此函数是在主程序中在调用任何其他HAL库的IPA函数前执行的第一个指令,
* 在这个阶段,还未配置用户指定的时钟,系统还默认使用内部HSI的64MHz运行。
* 2、调用HAL_NVIC_SetPriorityGrouping完成中断优先级分组,调用
* HAL_InitTick完成SysTick的初始化
* 3、同时,调用用户文件中(STM32CubeMX中生成的stm32mp1xx_hal_msp.c文件)的
* 回调函数HAL_MspInit进行全局底层硬件初始化 。
* @param 无
* @retval 无
*/
HAL_StatusTypeDef HAL_Init(void)
{
/* 设置中断优先级分组为4 */
#if defined (CORE_CM4)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
#endif
/* 更新SystemCoreClock全局变量 */
SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();
/* 使用滴答定时器作为时钟基准,配置1ms滴答(重置后默认的时钟源为HSI)*/
if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
return HAL_ERROR;
}
HAL_MspInit();
return HAL_OK;
}
(2)HAL_InitTick函数
HAL_Init函数调用了HAL_InitTick函数完成SysTick的初始化,我们来看看此函数:
stm32mp1xx_hal.c文件代码
/**
* @brief 初始化SysTick
* @param TickPriority :SysTick的中断优先级
* @note 1、该函数是弱定义函数,用户可以在别的地方重新定义一个同名函数,
* 编译以后,编译器只编译用户定义的函数。
* 2、该函数主要是初始化SysTick的中断优先级、配置SysTick的时钟频率为1kHz,
* 则周期为1/1000Hz=1ms
*
* @retval 无
*/
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/* 配置SysTick在1ms的时间产生一次中断 */
#if defined (CORE_CA7)
/****** 此处省略A7部分的代码 ******/
#endif /* CORE_CA7 */
#if defined (CORE_CM4)
/* uwTickFreq是个枚举类型,如果检测到uwTickFreq为零,则返回1(HAL_ERROR) */
if ((uint32_t)uwTickFreq == 0U)
{
return HAL_ERROR;
}
/* 将SysTick配置为在1ms的时间产生一次中断 ,uwTickFreq的值默认为1*/
if (HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq)) > 0U)
{
return HAL_ERROR;
}
/* 配置 SysTick的中断优先级 */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
return HAL_ERROR;
}
#endif /* CORE_CM4 */
return HAL_OK;
}
(3)HAL_SYSTICK_Config函数
从以上代码看到,通过HAL_SYSTICK_Config函数配置SysTick,我们再看看此函数的内容是什么:
stm32mp1xx_hal_cortex.c文件代码
/**
* @brief 初始化SysTick及其中断,并启动SysTick。
* @param 无
* @retval 无
*/
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
(4)SysTick_Config函数
再看看SysTick_Config文件,该函数在core_cm4.h文件中:
core_cm4.h文件代码
/**
* @brief 配置SysTick重载值,并启动SysTick。
* @param 无
* @retval 无
*/
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
/* 判断参数ticks是否符合要求,不能大于2^24-1 */
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* 无法重新加载值*/
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* 设置重载寄存器 */
/* 设置Systick中断的优先级 */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);
SysTick->VAL = 0UL; /* 清空SysTick当前值 */
/* 配置SysTick分频值、启用SysTick异常请求、启动SysTick*/
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0UL);
}
经过分析,最后HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq))里的参数传递给了LOAD,SysTick就是以此值逐渐递减1来达到计时的。这里,uwTickFreq为1,则参数为SystemCoreClock/1000。
经过前面的分析,在系统复位后,进入主函数前先执行的是HAL_Init函数,而此时系统时钟默认为64MHz,即SystemCoreClock为64MHz。而SysTick的分频值是1,所以SysTick的工作频率为64MHz,那么就可以计算出SysTick多少时间产生一次中断了:SysTick的一个节拍用的时间 * 总的节拍数。一个节拍用时,总的节拍数是,所以计算得SysTick的中断周期:*。
也就是说,系统复位后或者如果时钟树中采用默认的配置,即系统时钟和SysTick的时钟为64MHz,SysTick的分频值为1,SysTick会以每1ms产生一次中断。
(5)SysTick_Handler函数
下面我们看看SysTick的中断服务函数。在前面的实验中,在STM32CubeMX配置好以后,都会在stm32mp1xx_it.c文件中生成一个SysTick的中断服务函数:
/**
* @brief SysTick的中断服务函数
* @note 此中断服务函数的名字要和中断向量表中的对应,如果自己
* 手动编写中断服务函数的话,该函数的名字不能写错,否则
* 发生中断后,找不到中断服务函数,就会默认执行Default_Handler
* 函数,Default_Handler函数是一个死循环。
* @retval 无
*/
void SysTick_Handler(void)
{
HAL_IncTick(); /* uwTick加1 */
}
(6)HAL_IncTick函数
此中断服务函数调用HAL_IncTick函数,该函数的作用就是使全局变量uwTick每次进入中断后加1,经过前面的分析,系统复位后在未配置系统时钟之前,或者说时钟树采用默认配置时,SysTick每1ms进入中断,也就是每1ms变量 uwTick就自加1:
/**
* @brief 使全局变量uwTick加1
* @note 该函数是弱定义函数,用户可以在别的地方重新定义
* 一个同名函数,编译以后,编译器只编译用户定义的
* 函数。
* @retval 无
*/
__weak void HAL_IncTick(void)
{
uwTick += (uint32_t)uwTickFreq; /* uwTickFreq为1 */
}
(7)HAL_Delay函数
当调用HAL_Delay时,HAL_Delay怎么实现的毫秒延时的呢,我们查看HAL_Delay的代码:
1 /**
2 * @brief 此函数基于递增的变量提供准确的延迟(以毫秒为单位)
3 * @param Delay:用户设置的延时时间
4 * @note SysTick是时基的来源
5 * @retval 无
6 */
7 __weak void HAL_Delay(uint32_t Delay)
8 {
9 uint32_t tickstart = HAL_GetTick(); /* 获取uwTick的值 */
10 uint32_t wait = Delay;/* 将设置的延时时间参数赋值给wait变量 */
11 /* uwTickFreq的值为1,wait自加1,wait最大为2^24-1*/
12 if (wait < HAL_MAX_DELAY)
13 {
14 wait += (uint32_t)(uwTickFreq); /* uwTickFreq默认为0x01 */
15 }
16 while ((HAL_GetTick() - tickstart) < wait)
17 {
18 }
19 }
第9行,进入HAL_Delay函数以后,先获取当前uwTick的值,并记录在tickstart中,我们称tickstart的值就是原始uwTick的值;
第10行,将用户设置的延时毫秒数Delay赋值给变量wait;
第12~15行,如果没有改动过uwTickFreq 的值,默认uwTickFreq的值为1,这里wait加1后等于(Delay+1)。这里加1为了用于第16~18行的计算。注意,因为SysTick是24位的,所以wait的最大值只能是2^24-1。
第16~18行,通过HAL_GetTick函数获取当前最新的uwTick值,并与之前获取的uwTick值取差运算,当两者差值小于wait时,则一直执行空循环,当差值等于或者大于wait值时,则退出空循环。
假设Delay为10,wait加1后为11,tickstart为15,如果HAL_GetTick()-15<11,那么HAL_GetTick()应该为25,当HAL_GetTick()为26以后就退出空循环了,此时经过了SysTick的10个中断周期,即为10ms。
2. 执行SystemClock_Config函数之后
在没有执行SystemClock_Config函数之前,SysTick的工作频率是64MHz,分频值默认为1,SysTick以1ms产生一次中断。如果我们手动配置了时钟树,例如配置系统时钟为209MHz,SysTick还是1分频,那么SysTick还是1ms的中断周期吗?我们通过代码来分析:
在system_stm32mp1xx.c文件中有SystemCoreClockUpdate函数,SystemCoreClockUpdate函数的作用就是根据时钟寄存器的值来更新SystemCoreClock变量,用户应用程序可以使用它来设置SysTick定时器或配置其他参数。在程序执行期间,每次内核时钟改变时,都会调用SystemCoreClockUpdate函数来更新SystemCoreClock变量的值,这么做也就是为了保证SystemCoreClock的准确性。
1 uint32_t SystemCoreClock = HSI_VALUE;
2
3 void SystemCoreClockUpdate (void)
4 {
5 uint32_t pllsource, pll3m, pll3fracen;
6 float fracn1, pll3vco;
7
8 switch (RCC->MSSCKSELR & RCC_MSSCKSELR_MCUSSRC)
9 {
10 case 0x00: /* HSI used as system clock source */
11 SystemCoreClock = (HSI_VALUE >> (RCC->HSICFGR & RCC_HSICFGR_HSIDIV));
12 break;
13
14 case 0x01: /* HSE used as system clock source */
15 SystemCoreClock = HSE_VALUE;
16 break;
17
18 case 0x02: /* CSI used as system clock source */
19 SystemCoreClock = CSI_VALUE;
20 break;
21
22 case 0x03: /* PLL3_P used as system clock source */
23 /*******省略部分代码*******/
24 break;
25 }
26
27 /* Compute mcu_ck */
28 SystemCoreClock = SystemCoreClock >> (RCC->MCUDIVR & RCC_MCUDIVR_MCUDIV);
29 }
所以当我们配置好系统的时钟为209MHz时,SystemCoreClock的值也是209M,当SysTick还是1分频时,SysTick的频率也为209MHz。结合前面的分析,SysTick的一个节拍用的时间为,SysTick的总节拍数为HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq))=,那么SysTick产生中断的周期是:=1ms。
通过这里可以验证,不管配置系统时钟(MCU的时钟)为多少,只要SysTick为1分频,SysTick的中断周期都是1ms,使用HAL_Delay可以实现以1ms为单位来计时。
23.2.1 SysTick的分频值为8
当SysTick的分频值为8时,SysTick的中断周期是8ms,我们以系统时钟为209MHz为例子来计算一下。SystemCoreClock的值为209MHz,SysTick分频值为8,SysTick的频率为,SysTick的一个节拍用的时间为,SysTick的总节拍数还是不变为,那么SysTick产生中断的周期是:8ms 。
所以,当修改了SysTick的分频值为8时,执行HAL_Delay(10)函数以后不再是延时10ms,而是80ms。
23.3 SysTick高精度延时实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 16-1 SysTick。
经过前面的分析,使用HAL库自带的HAL_Delay延时函数可以实现的是毫秒级别的延时,如果需要更精确的延时,例如微妙级别的延时,使用HAL_Delay就不能够实现了。下面,我们使用SysTick来实现微妙级别的延时。
我们将编写delay_us函数实现微妙延时,基于delay_us的基础上编写delay_ms函数实现毫秒延时。编程的思路采用时钟摘取法,以delay_us为例,在进入函数的时候先计算需要延时一段时间所需要的等待的SysTick的计数节拍数,然后我们就一直统计SysTick的计数变化,当计数器的变化节拍数达到我们计算出的节拍数时,说明延时时间到了。比如delay_us(50)表示需要延时50us,假设系统时钟为209MHz,所以要延时50us的话则需要SysTick计数50*209,所以我们的程序就是通过判断是否达到了所需的节拍数来判断所需的延时时间是否已经达到了。
通过时钟摘取法只是抓取SysTick计数器的变化,不需要去修改SysTick的任何配置状态,所以完全不影响SysTick作为UCOS时钟节拍的功能,这就是实现delay和操作系统共用SysTick定时器的原理。下面我们开始介绍这几个函数。
23.3.1 无操作系统
本实验自行编写程序实现SysTick以us计时,然后使用LED0闪烁观察效果,实验中,我们配置系统时钟为209MHz,SysTick为1分频,所以SysTick的时钟频率也为209MHz。
- 新建和配置工程
本节实验可以在前面使用LED的工程中直接添加代码,为了方便,这里新建了一个工程SysTick,并按照前面实验的配置步骤使用HSE作为PLL3的时钟源,同时配置MCU的时钟频率为209MHz,SysTick为1分频:
图23.3.1. 1 SysTick时钟配置
生成工程后,因为本节实验用到LED0相关的驱动,在Src下新建BSP文件夹,然后把LED0的驱动文件拷贝过来,或者直接拷贝前面实验的BSP文件夹。在BSP文件夹下新建delay.c文件,在BSB/Include文件夹下新建delay.h文件,最后生成的工程如下:
图23.3.1. 2生成工程
2. 添加用户驱动
(1)添加delay.c文件代码
delay.c文件内容如下:
#include ./Include/delay.h
1 static uint32_t g_fac_us = 0; /* us延时倍乘数 */
2 /**
3 * @brief 初始化延迟函数
4 * @param sysclk: 系统时钟频率, 即MCU的频率,最大为209MHz
5 * @retval 无
6 */
7 void delay_init(uint16_t sysclk)
8 {
9 g_fac_us = sysclk; /* SYSTICK使用内核时钟源,同MCU同频率 */
10 }
11 /**
12 * @brief 要延时n个us
13 * @param nus: 要延时的us数
14 * @note 注意: nus的值,不要大于80274us(最大值即(2^24)/g_fac_us)
15 * g_fac_us最大为209MHz
16 * told是刚进入函数时的计数器的值(我们称为旧的节拍值)
17 * tnow是进入while循环后实时监测到的计数器的值(我们称为新的节拍值)
18 * tcnt是由旧的节拍值到新的节拍值经过了多少个节拍
19 * @retval 无
20 */
21 void delay_us(uint32_t nus)
22 {
23 uint32_t ticks;
24 uint32_t told, tnow, tcnt = 0;
25 uint32_t reload = SysTick->LOAD; /* LOAD的值 */
26 ticks = nus * g_fac_us; /* 计时nus需要的节拍数 */
27 told = SysTick->VAL; /* 刚进入函数时的计数器的值 */
28 while (1)
29 {
30 tnow = SysTick->VAL;/* 进入while循环后实时监测到的新的节拍值 */
31 if (tnow != told)
32 {
33 if (tnow < told)
34 {
35 tcnt += told - tnow; /* 计算经过了多少个节拍 */
36 }
37 else
38 {
39 tcnt += reload - tnow + told;/* 计算经过了多少个节拍 */
40 }
41 told = tnow; /* 更新told的值 */
42 if (tcnt >= ticks)
43 {
44 break; /* 时间超过或者等于要延迟的时间,则退出 */
45 }
46 }
47 }
48 }
49 /**
50 * @brief 要延时n个ms
51 * @param nms: 要延时的ms数 (0< nms <= 65535)
52 * @retval 无
53 */
54 void delay_ms(uint16_t nms)
55 {
56 uint32_t i;
57 for (i = 0; i < nms; i++)
58 {
59 delay_us(1000);/* 1ms=1000us */
60 }
61 }
以上就是时钟摘取法,此时的delay_us,可以实现最长224/209MHz,延时,大概是80274微秒。我们分析一下以上代码:
第7~10行,配置g_fac_us为系统时钟的值,调用该函数时参数需要我们设置。
第21~48行代码是重点部分,这部分代码实现用户设置的微妙延时的时间,我们来分析这段代码:
第23~27行,先定义几个变量,其中:ticks表示实现一定的延时时间SysTick需要计数多少个节拍。told是进入该函数时此时SysTick的计数器的值,我们称为旧的节拍值。tnow是进入while循环后新的节拍值。tcnt是从told值到tnow值,计数器经过了多少个节拍。
第30行,进入while循环后,先获取此时计时器的值tnow;
第31行,如果前后两次得到的计数器的值不相等,说明计数器已经计数了一段时间,可以通过计算在这段时间里计数器计数了多少个节拍来反推出这段时间多长(因为计数器的频率固定,每计数一个节拍用时也固定)。
第33~40,分情况计算出在这段时间里计数器计数了多少个节拍,注意的是SysTick是从224-1递减计数的,当从224-1递减到0时,然后返回继续从2^24-1递减开始递减。
第一种情况,当tnow<told时:tcnt=told-tnow;
第二种情况,当tnow>told时,因为计数器是向下递减计数的,所以计数器已经重新开始计数了,要不然tnow不会小于told。此时tcnt=LOAD-tnow+told。
我们以图来说明:
图23.3.1. 3 计数器计数示意图
第41行,完成一次判断后,更新told的值,然后再返回到第30行重新进行判断。
第42~45行,当tcnt的值大于或等于需要的节拍数ticks时,说明计时达到或者超过指定的时间,break出循环。
第54~61行,使用delay_us函数实验毫秒延时。
(2)delay.h文件内容
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32mp1xx.h"
#include "stm32mp1xx_hal.h"
#include "core_cm4.h"
void delay_init(uint16_t sysclk); /* 初始化延迟函数 */
void delay_ms(uint16_t nms); /* 延时nms */
void delay_us(uint32_t nus); /* 延时nus */
#endif
(3)修改main.c文件
main.c文件的部分内容如下,我们在红色字体之间手动添加代码:
1 #include "main.h"
2 #include "gpio.h"
3 /* USER CODE BEGIN Includes */
4 #include "./BSP/Include/led.h"
5 #include "./BSP/Include/delay.h"
6 /* USER CODE END Includes */
7 void SystemClock_Config(void);
8 int main(void)
9 {
10 /* 初始化HAL库和SysTick,系统时钟和SysTick的频率是64MHz,SysTick分频值为1 */
11 HAL_Init();
12 if(IS_ENGINEERING_BOOT_MODE())
13 {
14 SystemClock_Config(); /* 配置系统时钟 */
15 }
16 MX_GPIO_Init();
17 /* USER CODE BEGIN 2 */
18 delay_init(209); /* 延时初始化 */
19 /* USER CODE END 2 */
20 while (1)
21 {
22 /* USER CODE BEGIN 3 */
23 LED0_TOGGLE(); /* LED0闪烁 */
24 delay_ms(5000); /* 延时5s */
25 }
26 /* USER CODE END 3 */
27 }
以上代码,第18行,设置Sysclk使用内核时钟源209MHz。
第23和24,配置LED0每隔50ms翻转一次。
23.3.2 有操作系统
这里我们将介绍的是正点原子提供的最新版本的延时函数,该版本的延时函数支持在任意操作系统(OS)下面使用,它可以和操作系统共用SysTick定时器。这里,我们以UCOSII为例,介绍如何实现操作系统和我们的delay函数共用SysTick定时器。首先,我们简单介绍下UCOSII的时钟:ucos运行需要一个系统时钟节拍(类似 “心跳”),而这个节拍是固定的(由OS_TICKS_PER_SEC宏定义设置),比如要求5ms一次(即可设置:OS_TICKS_PER_SEC=200),在STM32上面,一般是由SysTick来提供这个节拍,也就是SysTick要设置为5ms中断一次,为ucos提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不准了)。
- delay.h文件
如果要支持操作系统,在delay.h中需要添加如下内容:
void delay_osschedlock(void);
void delay_osschedunlock(void);
void delay_ostimedly(uint32_t ticks);
void SysTick_Handler(void);
/**
* SYS_SUPPORT_OS用于定义系统文件夹是否支持OS
* 0,不支持OS
* 1,支持OS
*/
#define SYS_SUPPORT_OS 0 /* 默认不支持OS */
- delay_init函数
该函数用来初始化2个重要参数:g_fac_us以及g_fac_ms,同时把SysTick的时钟源选择为外部时钟,如果需要支持操作系统(OS),只需要在delay.h里面,设置SYS_SUPPORT_OS宏的值为1即可,然后,该函数会根据delay_ostickspersec宏的设置,来配置SysTick的中断时间,并开启SysTick中断。具体代码如下:
/**
* @brief 初始化延迟函数
* @param sysclk: 系统时钟频率, 即MCU的频率,最大为209MHz
* @retval 无
*/
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS /* 如果需要支持OS */
uint32_t reload;
#endif
/* SYSTICK使用内核时钟源,同CPU同频率 */
g_fac_us = sysclk; /* 不论是否使用OS,g_fac_us都需要使用 */
#if SYS_SUPPORT_OS /* 如果需要支持OS. */
reload = sysclk; /* 每秒钟的计数次数 单位为M */
/* 根据delay_ostickspersec设定溢出时间,reload为24位寄存器,最大值:16777216, 在209M下,约合0.08s左右 */
reload *= 1000000 / delay_ostickspersec;
g_fac_ms = 1000 / delay_ostickspersec; /* 代表OS可以延时的最少单位 */
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; /* 开启SYSTICK中断 */
SysTick->LOAD = reload; /* 每1/delay_ostickspersec秒中断一次 */
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* 开启SYSTICK */
#endif
}
可以看到,delay_init函数使用了条件编译,来选择不同的初始化过程,如果不使用OS的时候,只是设置一下SysTick的时钟源以及确定fac_us值。而如果使用OS的时候,则会进行一些不同的配置,这里的条件编译是根据SYS_SUPPORT_OS这个宏来确定的,该宏在delay.h里面定义。
在不使用OS的时候:g_fac_us,为us延时的基数,也就是延时1us,SysTick定时器需要走过的时钟周期数。当使用OS的时候,g_fac_us,还是us延时的基数,不过这个值不会被写到SysTick->LOAD寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而g_fac_ms则代表ucos自带的延时函数所能实现的最小延时时间。
3. OS相关的宏定义和函数
当需要delay_ms和delay_us支持操作系统(OS)的时候,我们需要用到3个宏定义和4个函数,宏定义及函数代码如下:
/*
- 当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和UCOSIII的支持,其他OS,请自行参考着移植
*/
/* 支持UCOSII */
#ifdef OS_CRITICAL_METHOD /* OS_CRITICAL_METHOD定义了,说明要支持UCOSII */
#define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行 */
#define delay_ostickspersec OS_TICKS_PER_SEC /* OS时钟节拍,即每秒调度次数 */
#define delay_osintnesting OSIntNesting/* 中断嵌套级别,即中断嵌套次数 */
#endif
/* 支持UCOSIII */
/* CPU_CFG_CRITICAL_METHOD定义了,说明要支持UCOSIII */
#ifdef CPU_CFG_CRITICAL_METHOD
#define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行 */
#define delay_ostickspersec OSCfg_TickRate_Hz /* OS时钟节拍,即每秒调度次数 */
#define delay_osintnesting OSIntNestingCtr/* 中断嵌套级别,即中断嵌套次数 */
#endif
/**
* @brief us级延时时,关闭任务调度(防止打断us级延迟)
* @param 无
* @retval 无
*/
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
OS_ERR err;
OSSchedLock(&err); /* UCOSIII的方式,禁止调度,防止打断us延时 */
#else /* 否则UCOSII */
OSSchedLock(); /* UCOSII的方式,禁止调度,防止打断us延时 */
#endif
}
/**
* @brief us级延时时,恢复任务调度
* @param 无
* @retval 无
*/
void delay_osschedunlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
OS_ERR err;
OSSchedUnlock(&err); /* UCOSIII的方式,恢复调度 */
#else /* 否则UCOSII */
OSSchedUnlock(); /* UCOSII的方式,恢复调度 */
#endif
}
/**
* @brief us级延时时,恢复任务调度
* @param ticks: 延时的节拍数
* @retval 无
*/
void delay_ostimedly(uint32_t ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
OS_ERR err;
OSTimeDly(ticks, OS_OPT_TIME_PERIODIC, &err); /* UCOSIII延时采用周期模式 */
#else
OSTimeDly(ticks); /* UCOSII延时 */
#endif
}
/**
* @brief systick中断服务函数,使用OS时用到
* @param ticks: 延时的节拍数
* @retval 无
*/
void SysTick_Handler(void)
{
HAL_IncTick();
if (delay_osrunning == 1) /* OS开始跑了,才执行正常的调度处理 */
{
OSIntEnter(); /* 进入中断 */
OSTimeTick(); /* 调用ucos的时钟服务程序 */
OSIntExit(); /* 触发任务切换软中断 */
}
}
#endif
以上代码,仅支持UCOSII和UCOSIII,不过,对于其他OS的支持,也只需要对以上代码进行简单修改即可实现。
支持OS需要用到的三个宏定义(以UCOSII为例)即:
#define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行 /
#define delay_ostickspersec OS_TICKS_PER_SEC / OS时钟节拍,即每秒调度次数 /
#define delay_osintnesting OSIntNesting / 中断嵌套级别,即中断嵌套次数 */
宏定义:delay_osrunning,用于标记OS是否正在运行,当OS已经开始运行时,该宏定义值为1,当OS还未运行时,该宏定义值为0。
宏定义:delay_ ostickspersec,用于表示OS的时钟节拍,即OS每秒钟任务调度次数。
宏定义:delay_ osintnesting,用于表示OS中断嵌套级别,即中断嵌套次数,每进入一个中断,该值加1,每退出一个中断,该值减1。
支持OS需要用到的4个函数,即:
1)函数:delay_osschedlock,用于delay_us延时,作用是禁止OS进行调度,以防打断us级延时,导致延时时间不准。
2)函数:delay_osschedunlock,同样用于delay_us延时,作用是在延时结束后恢复OS的调度,继续正常的OS任务调度。
3)函数:delay_ostimedly,则是调用OS自带的延时函数,实现延时。该函数的参数为时钟节拍数。
4)函数:SysTick_Handler,则是systick的中断服务函数,该函数为OS提供时钟节拍,同时可以引起任务调度。
以上就是delay_ms和delay_us支持操作系统时,需要实现的3个宏定义和4个函数。下面 我们来看看支持操作系统时delay_us函数编写:。
4. delay_us函数
使用OS的时候,delay_us函数和前面我们不使用操作系统的类似,不同的是多加了delay_osschedlock和delay_osschedunlock是OS提供的两个函数,用于调度上锁和解锁,这里为了防止OS在delay_us的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度!
/**
* @brief 延时nus
* @param nus: 要延时的us数
* @note nus取值范围: 0~ 80274us (最大值即2^24 / g_fac_us @g_fac_us = 209)
* @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; /* 需要的节拍数 */
delay_osschedlock(); /* 阻止OS调度,防止打断us延时 */
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; /* 时间超过/等于要延迟的时间,则退出 */
}
}
}
delay_osschedunlock(); /* 恢复OS调度 */
}
不同的是多加了delay_osschedlock和delay_osschedunlock是OS提供的两个函数,用于调度上锁和解锁,这里为了防止OS在delay_us的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度
5. delay_ms函数
使用OS的时候,delay_ms的实现函数如下:
/**
* @brief 延时nms
* @param nms: 要延时的ms数 (0< nms <= 65535)
* @retval 无
*/
void delay_ms(uint16_t nms)
{
/* 如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度) */
if (delay_osrunning && delay_osintnesting == 0)
{
if (nms >= g_fac_ms) /* 延时的时间大于OS的最少时间周期 */
{
delay_ostimedly(nms / g_fac_ms);/* OS延时 */
}
nms %= g_fac_ms; /* OS已经无法提供这么小的延时了,采用普通方式延时 */
}
delay_us((uint32_t)(nms * 1000)); /* 普通方式延时 */
}
该函数中,delay_osrunning是OS正在运行的标志,delay_osintnesting则是OS中断嵌套次数,必须delay_osrunning为真,且delay_osintnesting为0的时候,才可以调用OS自带的延时函数进行延时(可以进行任务调度),delay_ostimedly函数就是利用OS自带的延时函数,实现任务级延时的,其参数代表延时的时钟节拍数(假设delay_ostickspersec=200,那么delay_ostimedly (1),就代表延时5ms)。
当OS还未运行的时候,我们的delay_ms就是直接由delay_us实现的,不过由于delay_us的时候,任务调度被上锁了,所以还是建议不要用delay_us来延时很长的时间,否则影响整个系统的性能。当OS运行的时候,我们的delay_ms函数将先判断延时时长是否大于等于1个OS时钟节拍(fac_ms),当大于这个值的时候,我们就通过调用OS的延时函数来实现(此时任务可以调度),不足1个时钟节拍的时候,直接调用delay_us函数实现(此时任务无法调度)。
23.3.3 编译和测试
本实验我们的工程是无操作系统的,我们以无操作系统情况下的工程代码为例进行编译测试,编译后,测试PI0引脚的波形。PI0引脚在JP1排针处有引出:
图23.3.3. 1开发板的PI0引脚
所测试的波形如下,高电平脉冲为500ms,和我们实验的结果一致:
图23.3.3. 2测试结果