文章摘要
本文介绍了使用查询法的方式来学习F4系列系统滴答定时器,能够使用系统滴答定时器进行特定时间的延时。本文从时钟系统的配置再到系统滴答定时器的设置由浅入深依次对各细节进行解析和介绍,如有不足之处,还请在评论区中留下宝贵的意见,感谢大家。
系统滴答定时器的时钟来源
上图是一个STM32CubeMX配置系统时钟的的界面,让我们跟随红色箭头来分析系统滴答定时器的时钟源是如何从AHB总线时钟、SysClock时钟、PLL倍频器、振荡器演变过来的。
1.此图展示了系统滴答定时器的时钟源来源为AHB时钟
2.此图展示了AHB时钟来源于系统时钟
3.此图展现了SYSCLK系统时钟的来源为PLL
4.此图展现了输入至锁相环倍频器的振荡器为HSE(High Speed External),而HSE的取值为8M,实际要根据原理图上所使用的值来选定,大多数情况下都会使用8M的HSE振荡器。
那么由此可以知道,系统滴答定时器的时钟源路径为:
HSE振荡器->>PLL->>SYSCLK->>AHB-->>SysTick_Clock
再仔细看下图,我们发现当选择了HSE作为PLL的输入源之后,经过了多次分频和一次倍频最终到达SYSCLK,当HSE的值作为8MHz来到PLL时,我们先是进行了M分频,图M取值为8,则HSE经过8分频后值为1M,当HSE的值变成1M后又经过了N倍频,图中N值为336,此时值从1M变成了336M,当HSE的值变成336M之后会出现两个分频路径,一个是P分频一个是Q分频,经过P分频后最终到达系统时钟,图中P值为2,则最终系统时钟为366/2 = 168M,而经过Q分频则是通往一个全速USB外设,本文不讨论。
最后,系统时钟以168MHz经过1分频后使得HCLK时钟(AHB总线时钟)等于168Mhz
而HCLK时钟经过8分频最终成为了系统滴答定时器的时钟,也就是说下图配置的系统滴答定时器的时钟为168/8 = 21Mhz,也就是说系统滴答定时器1秒钟计数值为21000000次(后面详细讨论)
上述这一些图片在芯片中所做的主要的工作就是配置系统时钟,只有配置好了系统时钟才能够准确的知道我们系统滴答定时器的时钟值,才能够根据滴答定时器的时钟值来进行微妙、毫秒等延时系数的配置。上述配置系统滴答定时器的步骤可以通过STM32CubeMX直接生成配置代码,也可以通过手动配置HAL库来实现,如下图所示。
系统滴答定时器延时函数解析
一、HAL库手动系统时钟配置
二、使用STM32CubeMX配置系统时钟(自动生成代码)
在配置好了系统时钟之后,我们现在可以开始进行系统滴答定时器的使用了。
使用系统滴答定时器进行延时
关于系统滴答定时器的延时函数,网上有许多种版本,本文使用的是阻塞式查询法。
在配置好系统时钟后我们现在可以开始进行系统滴答定时器的延时函数编写了。
下图展示了系统滴答定时器的各寄存器组成(图片内容源自野火F4开发手册)
网址:19. SysTick—系统定时器 — [野火]STM32库开发实战指南——基于野火霸天虎开发板 文档 (embedfire.com)
系统滴答定时器一共有四个寄存器,上面列出了常用的三个寄存器,各寄存器功能如图所示。
CTRL:(控制寄存器)
VAL:(计数器计数值寄存器)
LOAD:(重装载寄存器)
回到源码,要使用系统滴答定时器需要对系统滴答定时器进行初始化,以下为初始化函数。
可以看到第一行是将CTRL寄存器清零,其原因根据HAL库的规则,HAL_Init()总是主程序执行的第一个指令,如下图所示:
个人猜测(不确定)可能是下图标红的函数会对系统滴答定时器造成影响。
因此我们在进行系统滴答定时器的初始化时,第一行则是对CTRL寄存器进行清零
(借鉴来源:正点原子教学视频)
好了,让我们继续回到系统滴答定时器初始化函数中,如下图所示:
第二句话则是对系统滴答定时器的时钟源进行选择,此处选择以AHB总线时钟的八分频作为系统滴答定时器的时钟,即:168/8=21Mhz。因此我们的滴答定时器的频率为21Mhz,也就是说一秒钟可以计数21000000次,那么一毫秒计数21000次,一微秒计数21次。
第三行:us_count = SystemFrequence / 8;的意思是,获取计数器一微秒能够计数的次数,赋值给全局变量us_count。
显然u_count的值为21,SystemFrequence的值我们一般在使用的时候会填上自己系统时钟的值,F4为168Mhz,因此SystemFrequence的值为168(单位:M),那么168/8=21,因此us_count值为21。
同理,因为1毫秒 = 1000* 1微妙,那么我们一毫秒所需要计数的个数则是21*1000=21000个。
也就是说,一微秒计数器向下递减计数21次,一毫秒递减计数21000次,计数器的最大值为
2^24,取值范围为:0~16777215 共计16777216次。
以上是系统滴答定时器的时钟为21M所获取的1微秒、1毫秒计数器计数个数。
那么我们有了1微妙、1毫秒的计数个数之后,就可以在不超过最大计数值的情况下进行我们任意时间的延时了。下图为微秒延时函数
变量register_value用于查询CTRL定时器当前的值,形式参数为us用于获取用户想要进行的延时时间(单位:微秒),整个函数的流程则是,定义临时变量用于获取CTRL寄存器的值(查询法的意义在于此)->>将用户想要进行延时对应的计数个数载入重装载值->>清空当前计数值->>启动滴答定时器,让滴答定时器从用户载入的重装载值开始向下递减->>一旦查询到计数器递减到0则关闭滴答定时器而后清空(如果先清空,滴答定时器此时还在运行,清空后还是会有数,因此先关闭不让计数器计数再清空VAL值)
关于:
do
{
register_value = SysTick->CTRL;
}while((register_value & 0x01) && !(register_value & (1<<16)));
这句话我来解析一下:
先是把寄存器CTRL的值赋值给临时变量,然后在while中进行条件判断。
"&&"这个操作符的用法是: 左右两边必须都满足为真才会使得整个表达式为真。
比如: A && B ,如果A 为假,那么 (A && B)整个直接就为0,B都不需要去是否为真。
A && B , 如果A 为真,那么再看B,如果A为真B为假,那么(A && B)整个表达式还是为假。
只有 A 真 并且在A真的前提下B为真,整个表达式(A && B) 才为真。
有了上面的解释就好理解了,while((register_value & 0x01) && !(register_value & (1<<16)));
register_value & 0x01 就是获取CTRL寄存器的位0,因为1和任何一个位进行与运算都是任何数本身,比如1 & 0 = 0 ,1 & 1 = 1。
register_value & (1<<16),这句话就是获取CTRL位16了。
那么综上所述,根据CTRL寄存器中,位16的描述,我们可以理解整个while循环中的这句话的意思是:
当系统滴答定时器处于开启的状态下,一旦检测到计数器递减到0,则退出while循环,否则一直回到do中进行CTRL寄存器的查询(此为阻塞时查询法)
系统滴答定时去处于开启状态下:表达式(register_value & 0x01 )的值是1,为真
当检测到计数器的值递减到0了: 表达式(register_value * (1 << 16) )的值是1,为真,但是括号外面有个逻辑取反,那么 !(register_value & (1<<16))的值是0,为假。
(用A代替上面的 register_value & 0x01)
(用B代替上面的 !register_value * (1 << 16)
那么整个表达式 A && B 就为假,那么whlie(A && B)就相当于 while(0),不满足条件了,则退出循环
那么退出循环后则关闭滴答定时器并且进行清零操作。
同样的,毫秒级别的延时函数原理也是如此,只需要修改一下 延时系数,如图所示:
那么最终的延时结果是否准确呢?以LED等每500ms亮灭一次(1s的周期\频率)
代码如下:
最终用示波器和正点原子两种方法检测了一下配置的延时函数是否准确:
示波器:
正点原子keil仿真查询法:
误差为:0.500000036-0.500000000 = 0.00000036秒,放大1000000倍,
0.00000036秒= 0.36微秒,还是有0.36微秒的误差的,但是可能是元器件的原因导致的吧,如果有知道的欢迎评论区留言,
好了,本期阻塞式查询法系统滴答定时器延时就介绍完毕了,感谢大家的支持。