LV16-17-SysTick定时器
本文主要是STM32开发——SysTick定时器的一些相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
- 通用
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.st.com/content/st_com/zh.html | ST官方网站,在这里我们可以找到STM32的相关文档 | |
https://www.stmcu.com.cn/ | 意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档 | |
http://elm-chan.org/fsw/ff/00index_e.html | FatFs文件系统官网 | |
教程书籍 | 《ARM Cortex-M3权威指南》 | ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩 |
《ARM Cortex-M0权威指南》 | ||
《ARM Cortex-M3与Cortex-M4权威指南》 | ||
开发论坛 | http://47.111.11.73/forum.php | 开源电子网,正点原子的资料下载及问题讨论论坛 |
https://www.firebbs.cn/forum.php | 国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT | |
https://www.amobbs.com/index.php | 阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。 | |
http://download.100ask.net/index.html | 韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。 | |
博客参考 | http://www.openedv.com/ | 开源网-原子哥个人博客 |
http://blog.chinaaet.com/jihceng0622 | 博主是原Freescale现NXP的现场应用工程师 | |
cortex-m-resources | 这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接) |
- STM32
STM32 | STM32 HAL库开发实战指南——基于F103系列开发板 | 野火STM32开发教程在线文档 |
STM32库开发实战指南——基于野火霸道开发板 | 野火STM32开发教程在线文档 |
- SD卡
SD Association | 提供了SD存储卡和SDIO卡系统规范 |
点击查看相关文件下载
STM32F103xx英文数据手册 | STM32F103xC/D/E系列的英文数据手册 |
STM32F103xx中文数据手册 | STM32F103xC/D/E系列的中文数据手册 |
STM32F10xxx英文参考手册(RM0008) | STM32F10xxx系列的英文参考手册 |
STM32F10xxx中文参考手册(RM0008) | STM32F10xxx系列的中文参考手册 |
Arm Cortex-M3 处理器技术参考手册-英文版 | Cortex-M3技术参考手册-英文版 |
STM32F10xxx Cortex-M3编程手册-英文版(PM0056) | STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版 |
SD卡相关资料——最新版本 | 有关SD卡的一些资料可以从这里下载 |
SD卡相关资料——历史版本 | 有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议 |
SD 2.0 协议标准完整版 | 这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看 |
这一部分我们可以查看[《ARM Cortex-M3权威指南(中文).pdf》](STM32开发相关资料/01ARM参考资料/ARM Cortex-M3权威指南(中文).pdf)的 第8章NVIC与中断控制 中的 SysTick定时器 小节。
一、SysTick定时器简介
SysTick定时器(又名系统滴答定时器)是存在于Cortex-M3的一个定时器,只要是ARM Cotex-M系列内核的MCU都包含这个定时器。使用内核的SysTick定时器来实现延时,可以不占用系统定时器,节约资源。由于SysTick是在CPU核内部实现的,跟MCU外设无关, 因此它的代码可以在不同厂家之间移植。
SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号: 15)。在以前,大多操作系统需要一个硬件定时器来产生操作系统需要的滴答中断,作为整个系统的时基。例如,为多个任务许以不同数目的时间片,确保没有一个任务能霸占系统;或者把每个定时器周期的某个时间范围赐予特定的任务等,还有操作系统提供的各种定时功能,都与这个滴答定时器有关,有了它,就可以根据这个中断,系统就可以实现时间片的计算从而切换进程。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
Cortex‐M3处理器内部包含了一个简单的定时器。因为所有的CM3芯片都带有这个定时器,软件在不同 CM3器件间的移植工作得以化简。该定时器的时钟源可以是内部时钟(FCLK,CM3上的自由运行时钟),或者是外部时钟( CM3处理器上的STCLK信号)。不过, STCLK的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能会大不相同,需要检视芯片的器件手册来决定选择什么作为时钟源。
SysTick定时器能产生中断, CM3为它专门开出一个异常类型,并且在向量表中有它的一席之地。它使操作系统和其它系统软件在CM3器件间的移植变得简单多了,因为在所有CM3产品间对其处理都是相同的。
二、基本原理
1. 原理介绍
SysTick定时器是一个24位递减定时器,即计数器可以从最大值2^24开始, 每个时钟周期减1,当减到0时,会产生Systick异常,同时再自动重载定时初值,开始新一轮计数。如下图递减计数器在时钟的驱动下,从重装载寄存器初值开始往下递减计数到0,产生中断和置位COUNTFLAG标志。然后又从重装载寄存器初值值开始重新递减计数,如此循环。
通过设置这个重装载寄存器初值,就可以实现得到指定时间。如下图所示:
y为定时器初值,然后随着时间增加, 值逐渐减小,直至为0,再重新加载初值,如此往复,0t1、 t1t2、 t2~t3这些时间段,就是我们需要的延时时间。
假设STM32F103工作在72MHz,即72000000Hz,意味着1s时间内,会计数72000000次。那么1ms则计数72000000/1000=72000次。这个72000就可以作为系统滴答定时器的初始值,将这个值写入系统滴答定时器,定时器在每个时钟周期减1,减到0时,就刚好是1ms,同时产生中断通知,再次加载72000到系统滴答定时器。如此反复。 HAL库提供“ HAL_SYSTICK_Config()”函数去设置这个初始值 。
2. 定时时长
(1)t:一个计数循环的时间,跟reload和CLK有关
1 | reload * (1 / clk) |
(2)CLK: 72M或者9M,由CTRL寄存器配置
1 | Clk = 72M时, reload=72:t = (72) *(1/72M) = 1US |
(3)RELOAD: 24位,用户自己配置
1 | 0~0xFFFFFF |
(4)时间换算
1 | 1s = 1000ms = 1000 000 us = 1000 000 000ns |
三、相关寄存器
对于STM32的系统滴答定时器相关的寄存器我们可以查看 STM32F10xxx_Cortex-M3编程手册 的4.5 SysTick timer (STK)这一节。
1. 控制和状态寄存器( STK_CTRL)
相关位说明如下:
位段 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
[31:17 ] | RESERVED | R | 0 | 保留位 |
[16] | COUNTFLAG | RW | 0 | 如果在上次读取本寄存器后,计数器已经计到 了 0,则该位为 1;读取该位,该位将自动清零 |
[15:3] | RESERVED | R | 0 | 保留 |
[2] | CLKSOURCE | RW | 0 | 0=AHB/8;1=AHB |
[1] | TICKINT | RW | 0 | 1=计数器计数到 0 时产生 SysTick 异常请求 0=计数器计数到 0 时不产生异常 |
[0] | ENABLE | RW | 0 | SysTick 定时器的使能位 |
重点关注Bit[0],用于使能系统滴答定时器, Bit[1]使能系统滴答定时器中断, Bit[2]系统滴答时钟的时钟来源。
2. 加载值寄存器( STK_LOAD)
位段 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
[31:24] | RESERVED | R | 0 | 保留位 |
[23:0] | RELOAD | RW | 0 | 当计数器向下计数到 0 时,下一个时钟滴答定 时器将重新装载的值 |
Bit[23:0],一共24位,用来设置系统滴答定时器的初始值,因此范围为1~ 16777216。
3. 当前值寄存器( STK_VAL)
位段 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
[31:24] | RESERVED | R | 0 | 保留位 |
[23:0] | RELOAD | RW | 0 | 读取时返回当前计数的值,写它则使之清零, 同时还会清除在 SysTick 控制及状态寄存器 中的 COUNTFLAG 标志 |
Bit[23:0],一共24位,用来获取当前系统滴答定时器的计数值。
4. 校准值寄存器( STK_CALIB)
位段 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
[31] | NOREF | R | - | 1=没有外部参考时钟( STCLK 不可用; 0=外部参考时钟可用 |
[30] | SKEW | R | - | 1=校准值不是准确的 10ms;0=校准值是准确的 10ms |
[29:24] | RESERVED | R | 0 | 保留 |
[23:0] | TENMS | RW | 0 | 10ms 校准值。芯片设计者应该通过 Cortex-M3 的输入信号提供该数值。若该值读回零,则表 示无法使用校准功能 |
这个寄存器没用到,可以不用管。 此外,当处理器在调试期间被暂停( halt )时, 系统滴答定时器也将暂停运作。
四、时钟来源
我们了解了寄存器之后其实就可以编程了,但是还有一个问题,那就是,它的时钟哪来的?我们看一下STM32的时钟树的这一部分:
我们可以看左侧图红线这一条路,AHB时钟讲过1或者8分频后送至Cortex系统时钟,系统滴答定时器使用的就是这个时钟,在这个时钟的驱动下,Cortex-M3内核中的系统滴答定时器寄存器值就开始递减:
五、中断优先级?
SysTick属于内核里面的外设,他的中断优先级跟片上的外设的中断优先级相比,哪个高?
systick中断优先级配置的是scb->shprx寄存器;而外设的中断优先级配置的是nvic->iprx,有优先级分组,有抢占优先级和子优先级的说法。STM32里面无论是内核还是外设都是使用4个二进制位来表示中断优先级。
中断优先级的分组对内核和外设同样适用。当比较的时候,只需要把内核外设的中断优先级的四个位按照外设的中断优先级来分组来解析即可,即人为的分出抢占优先级和子优先级。
六、相关HAL库函数
HAL库函数对系统滴答定时器的相关操作在这里:
1. HAL_Init()中的配置
1 | main()--->HAL_Init()--->HAL_InitTick()--->HAL_SYSTICK_Config() |
1.1 HAL_Init()
我们先来看一下这个 HAL_Init 函数:
1 | HAL_StatusTypeDef HAL_Init(void) |
首先是 HAL_NVIC_SetPriorityGrouping函数设置了NVIC中断分组为组4,这个是我们在STM32CubeMX中配置的。
然后调用了 HAL_InitTick() 函数来初始化系统滴答定时器,传入的参数为 TICK_INT_PRIORITY,它起始就是个 0 :
1 |
1.2 HAL_InitTick()
1 | __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) |
进来之后发现,这是个弱函数哎,我们打开对应的头文件会发现,声明的时候并没有带__weak符号:
1 | HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority); |
这就说明,当我们没有重写这个函数的时候,默认会调用这个弱函数。我们先看下边的 HAL_NVIC_SetPriority这一行,这里就是在设置系统滴答定时器中断的优先级,从 TICK_INT_PRIORITY 宏我们知道这里形参 TickPriority 的值就是 0,具体的就先不看了。我们再来看一下滴答定时器配置时传入的参数:
1 | HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) |
我们追踪一下这里的两个参数:
1 | uint32_t SystemCoreClock = 16000000;// 定义时的默认值,后边配置时钟的时候这个值会被修改 |
这个 HAL_TICK_FREQ_DEFAULT是什么?我们继续追踪:
1 | typedef enum |
从这里我们知道 HAL_TICK_FREQ_DEFAULT 的值为1,于是,上边传入 HAL_SYSTICK_Config()函数的参数就为:
1 | SystemCoreClock / (1000U / uwTickFreq) = 16000000 / (1000 / 1) = 16000 |
1.3 HAL_SYSTICK_Config()
1 | uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb) |
这个就简单了,直接返回了另一个函数调用后的值,新调用的函数的形参 TicksNumb 就等于 上边计算出来的 16000。
1.4 SysTick_Config
1 | __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks) |
在这个函数中对系统滴答定时器的寄存器进行配置,可以看到加载值寄存器的值配置为了 ticks -1 ,按前边的16000算的话,这里应该是15999。
1.5 问题?
上边就是从HAL_Init()函数到配置寄存器的函数调用及参数调用关系,但是吧,这个时候还没有初始化和配置RCC时钟哎,也不知道在这里进行一遍初始化的目的是什么。注释是这样写的:
1 | /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ |
,我的理解就是,可能就是简单的做一个初始化吧。在main.c调用完 HAL_Init() 函数后,立刻又调用了 SystemClock_Config()函数来配置时钟,这个系统滴答定时器又会被配置一次。
2. SystemClock_Config()中的配置
2.1 SystemClock_Config()
1 | void SystemClock_Config(void) |
经过这个函数的配置,我们得到的时钟如下:
1 | SYSCLK(系统时钟) = 72MHz |
根据前边分析的系统滴答定时器的始终来源,这里用于系统滴答定时器的时钟为 AHB时钟,也就是72M。
2.2 HAL_RCC_ClockConfig()
1 | HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency) |
我没有具体去追踪这个 SystemCoreClock 试是怎么被获取出来的,但是根据猜测,这里应该是系统时钟,我前边试配置成了72M(这是个全局变量,也可以在配置完成后,串口打印一下):
这样我们从这里得到两个参数:
1 | SystemCoreClock = 72000000 |
2.3 HAL_InitTick()
1 | __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) |
这个函数前边了解过了,这里传入的参数就变成了:
1 | HAL_SYSTICK_Config(72000000 / (1000U / 1); |
2.4 HAL_SYSTICK_Config()
1 | uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb) |
这里跟前边一样,没啥好说的,只是这里的参数会变成如下:
1 | SysTick_Config(72000) |
2.5 SysTick_Config()
1 | __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks) |
到这里,开始配置寄存器,我们前边知道AHB的时钟为72MHz,这里重载值寄存器配置为72000-1=71999,我们再看一下控制寄存器中是否对时钟分频了:
1 |
从这两个宏可知,SysTick_CTRL_CLKSOURCE_Msk 的值就是 1左移 2位,对应到 CTRL 寄存器,就是第2位了,这里将第2位置1,就表示不进行分频。
2.6 中断产生时间
上边经过分析,我们知道了系统滴答定时器的时钟为72MHz,不分频,重载值为72999,从0~72999一共是72000个数,我们来计算一下产生一次中断的时间:
1 | 滴答定时器时钟 = 72000000 |
所以默认情况下,系统滴答定时器每过1ms就会产生一次中断。
3. 中断处理函数
接下来我们看一下中断处理函数:
1 | SysTick_Handler()--->HAL_IncTick()--->HAL_IncTick() |
我们发现最终的回调函数为 HAL_IncTick :
1 | __weak void HAL_IncTick(void) |
我们来看一下里边的这句话:
1 | typedef enum |
于是就有:
1 | uwTick += uwTickFreq; |
所以默认情况下的系统滴答定时器会不停地在中断中计数,注意uwTick是一个全局变量,其他地方也都可以用。
4. HAL_Delay()
接下来我们看一下这个库中的延时函数的实现:
我们来分析一下:
(1)tickstart的值等于调用此延时函数时 全局变量 uwTick 的值;
(2)wait的值为我们要延时的ms数;
(3)判断一下wait时间是不是比最大延时时间短,若是比最大的延时时间还要短的话,就会将 wait 的值加上最小延时时间,这里是1,就表示我们延时最短是1ms。
(4)进入循环等待,当uwTick 的值减去进入函数时的值等于wait的时候,while循环条件为假,退出循环,开始执行HAL_Delay后边的代码,达到延时的效果。
有几个问题:
(1)这里我们延时的话,Delay输入了个小于等于-1的值怎么办?按-1来算,进来后,加一为0,下边循环直接就不满足了,直接退出;比-1小的话也是一样的结果。
(2)延时时间打与大于0xFFFFFFFF?我们传入参数的时候形参是个uint32_t类型,这就是一个32位数据,比它还大的话,会溢出,高位就没了,所以最大延时时间还是 0xFFFFFFFF。