写在开篇前:
首先要明确的第一点:学习知识我们都会到网上查询各种资料,但是由于网上资料大多不全面,我们对于这些资料需要加以自身的理解并对其有所取舍,所以建议网上查来的资料只做引导、参考作用,最终确定对技术的应用还要以官方文档为准。而这个辨别资料可行性的过程是十分耗费时间的,对于此点笔者也实在无奈,如果哪些同学有更好的方法,请不吝赐教,以图共同进步。
由于本文将详细的分析SysTick整个实现过程,为了方便大家快速掌握该流程,先将结论总结如下:
1、systick是一个24位的定时器,故重装值最大值为2的24次方 = 16 777 215,要注意不要超出这个值。
2、systick是cortex_m3的标配,不是外设,不需要在RCC寄存器组打开它的时钟。
3、每次systick溢出后会置位计数标志位和中断标志位,计数标志位在计数器重装载后被清除,而中断标志位也会随着中断服务程序的响应被清除,所以这两个标志位都不需要手动清除。
4、采用使用库函数的方法,只能采用中断的方法响应定时器计时时间到,如要采用查询的方法,那只能采用设置systick的寄存器的方法。
目录
SysTick是一个系统时钟定时器,属于ARM Cortex-Mx内核的一个“内设”,所有基于此内核的微控制器都带SysTick。(ST的芯片中F1系列属于Cortex-M3内核,F3与F4系列属于Cortex-M4内核)。
0x01、什么是时钟
关于这个问题 ,我们需要从 CPU的时钟说起。
计算机是一个十分复杂的电子设备。它由各种集成电路和电子器件组成,每一块集成电路中都集成了数以万计的晶体管和其他电子元件。这样一个十分庞大的系统,要使它能够正常地工作,就必须有一个指挥,对各部分的工作进行协调。各个元件的动作就是
在这个指挥下按不同的先后顺序完成自己的操作的,这个先后顺序我们称为时序。
时序是计算机中一个非常重要的概念,如果时序出现错误,就会使系统发生故障,甚至造成死机。那么是谁来产生和控制这个操作时序呢?这就是“时钟”。
“时钟”可以认为是计算机的“心脏”,如同人一样,只有心脏在跳动,生命才能够继续。不要把计算机的“时钟”等同于普通的时钟,它实际上是由晶体振荡器产生的连续脉冲波,这些脉冲波的幅度和频率是不变的,这种时钟信号我们称为外部时钟。它们被送入 CPU 中,再形成CPU时钟。不同的CPU,其外部时钟和 CPU时钟的关系是不同的。
CPU 的时钟周期通常为节拍脉冲或T周期,它是处理操作的最基本的单位。
在微程序控制器中,时序信号比较简单,一般采用节拍电位——节拍脉冲二级体制。就是说它只要一个节拍电位,在节拍电位又包含若干个节拍脉冲(时钟周期)。节拍电位表示一个CPU周期的时间,而节拍脉冲把一个CPU周期划分为几个较小的时间间隔。
根据需要,这些时间间隔可以相等, 也可以不等。
指令周期是取出并执行一条指令的时间。指令周期常常有若干个CPU周期,CPU周期也称为机器周期,由于CPU访问一次内存所花费的时间较长,因此通常用内存中读取一个指令字的最短时间来规定CPU周期。这就是说,一条指令取出阶段(通常为取指)需要一个CPU周期时间。而一个CPU周期时间又包含若干个时钟周期(通常为节拍脉冲或T周期,它是处理操作的最基本的单位)。这些时钟周期的总和则规定了 一个CPU周期的时间宽度。
总结:
1、时钟是为CPU产生时序信号而采用晶体振荡器产生的相同幅度和频率的脉冲。
2、1个指令周期包含若干个机器周期,1个机器周期又包含了若干个时钟周期,指令周期 > 机器周期 > 时钟周期
0x02、STM32的时钟
明白了什么是时钟之后,我们来分析一下STM32芯片的时钟。
STM32的时钟系统设计十分复杂,总结起来有以下两个特点
1、提供多种时钟源选择
2、可以进行倍频、分频操作。
那么为什么要设计这么复杂的时钟系统呢?主要有以下两点原因
1、倍频:考虑到电磁兼容性,如stm32f103系列芯片,最高主频可达72MHZ,如果外部直接提供一个72MHz的晶振,太高的振荡频率可能会给制作电路板带来一定的难度。
2、分频:因为STM32既有高速外设又有低速外设,各种外设的工作频率不尽相同,如同PC机上的南北桥,把高速的和低速的设备分开来管理。最后,每个外设都配备了外设时钟的开关,当我们不使用某个外设时,可以把这个外设时钟关闭,从而降低STM32的整体功耗
众所周知,微控制器(处理器)的运行必须要依赖周期性的时钟脉冲来驱动——往往由一个外部晶体振荡器提供时钟输入为始,最终转换为多个外部设备的周期性运作为末,这种时钟“能量”扩散流动的路径,犹如大树的养分通过主干流向各个分支,因此常称之为“时钟树”。在一些传统的低端 8 位单片机诸如 51,AVR,PIC等单片机,其也具备自身的一个时钟树系统,但其中的绝大部分是不受用户控制的,即在单片机上电后,时钟树就固定在某种不可更改的状态(假设单片机处于正常工作的状态)。比如 51单片机使用典型的 12MHz晶振作为时钟源,则外设如IO口、定时器、串口等设备的驱动时钟速率便已经是固定的,用户无法将此时钟速率更改,除非更换晶振。这样对比起来,STM32的时钟系统设计具有明显的优势。
0x0001)、STM32的时钟源
STM32对于系统时钟提供多种选择。在STM32中,有五个时钟源,为 HSI、HSE、LSI、LSE、PLL,它们都可以作为系统时钟的来源。
系统时钟的选择是在启动时进行,复位时内部 8MHZ 的 RC 振荡器被选为默认的 CPU 时钟,随后可以选择外部的、具失效监控的 4-16MHZ 时钟;当检测到外部时钟失效时,它将被隔离,系统将自动地切换到内部的 RC 振荡器。
2.、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz
3、LSI 是低速内部时钟,RC振荡器,频率为40kHz,可以用于驱动独立看门狗和通过程序选择驱动 RTC(RTC用于从停机/待机模式下自动唤醒系统)
4、LSE是低速外部时钟,接频率为32.768kHz的石英晶体,也可以被用来驱动RTC
5、PLL 为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍, 但是其输出频率最大不得超过 72MHz
0x0002)、STM32的倍频、分频系统
我们先贴上STM32的时钟树图
从时钟树的分析,看到经过一系列的倍频、分频后得到了几个与我们开发密切相关的时钟。
SYSCLK:系统时钟,STM32大部分器件的时钟来源。主要由AHB预分频器分配到各个部件。
HCLK:由AHB预分频器直接输出得到,它是高速总线AHB的时钟信号,提供给存储器,DMA及 Cortex内核,是Cortex内核运行的时钟,CPU主频就是这个信号,它的大小与STM32运算速度,数据存取速度密切相关。
FCLK:同样由AHB预分频器输出得到,是内核的“自由运行时钟”。“自由”表现在它不来自时钟 HCLK,因此在HCLK时钟停止时FCLK也继续运行。它的存在,可以保证在处理器休眠时, 也能够采样和到中断和跟踪休眠事件,它与HCLK互相同步。
PCLK1:外设时钟,由APB1预分频器输出得到,最大频率为36MHZ,提供给挂载在APB1总线上的外设。
PCLK2:外设时钟,由APB2预分频器输出得到,最大频率可为72MHZ,提供给挂载在APB2总线上的外设。
我们举一个实例,分析一下我们是如何得到外设GPIOF的时钟(各型号芯片略有不同),根据我上图(时钟树图片)中的红色箭头走向
首先,这里我们为了得到STM32F103系列的最高主频72MHz,我们的外部时钟是8MHz,经过9倍的倍频即可得到72MHz。首先,8M的HSE经过PLLXTPRE,直接选择 HSE 为输入,得 8(MHz)。经过PLLSRC选择,还是 8(MHz)。8MHz 经过PLLMULL的 9 倍频,8*9=72(MHz)。经过SW选择PLLCLK做为SYSCLK(系统时钟)的输入时钟,那么SYSCLK等于输送过来的72MHz。外设 GPIO 的时钟来源于APB2总线,那么经过AHB 预分频器,不分频。在经过APB2 预分频器,不分频。最后APB2外设 GPIO 就可以得到72MHz的时钟源了。
0x03、SysTick
前面我们分析了STM32的整个时钟系统,那么SysTick是什么呢,具体怎么使用呢?
SysTick是一个24位向下递减的系统节拍定时器(system tick timer)也叫作系统滴答时钟,具有自动重载和溢出中断功能。每计数一次所需时间为1/SYSTICK,SYSTICK是系统定时器时钟,它可以直接取系统时钟,还可以通过系统时钟8分频后获取。当定时器计数到0时,将从LOAD寄存器中自动重装定时器初值,重新向下递减计数,如此循环往复。如果开启SysTick中断的话,当定时器计数到0,将产生一个中断信号。
SysTick定时器的操作可以分为2步:
第一步:设置STM32系统时钟源
因为系统时钟频率决定HCLK频率,HCLK频率决定SysTick定时器频率)
这一步由于基本上有系统启动时自行设定的,无需人工参与,所以很多介绍SysTick的文章中并没有介绍,但为了更好的理解SysTick的具体工作原理,我觉得有必要详细的介绍一下。
系统启动,设置系统时钟是通过如下函数调用过程完成的
IMPORT SystemInit ——> SetSysClock() ——> SetSysClockTo72()
IMPORT SystemInit在文件startup_stm32f10x_xx.s文件中,该文件为系统启动文件
SystemInit ()在文件system_stm32f10x.c文件中
SetSysClock()在文件system_stm32f10x.c文件中
SetSysClockTo72()在文件system_stm32f10x.c文件中
其中IMPORT SystemInit表示系统启动时要调用SystemInit()这个函数
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
SystemInit()又调用SetSysClock()
void SystemInit (void)
{
/* Reset the RCC clock configuration to the default reset state(for debug purpose) */
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
/* Reset PLL2ON and PLL3ON bits */
RCC->CR &= (uint32_t)0xEBFFFFFF;
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x00FF0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#else
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock();
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}
SetSysClock()通过#define SYSCLK_FREQ_72MHz 72000000 设定调用 SetSysClockTo72(),最终将系统时钟频率设置为72M
static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
SetSysClockTo72();
#endif
/* If none of the define above is enabled, the HSI is used as System clock
source (default after reset) */
}
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
#define SYSCLK_FREQ_72MHz 72000000
#endif
上面我们对于外部8MHZ晶振输入如何倍频到72MHZ做过解释,这里我们看一下软件上是怎么实现的(这个实现过程无需人为参与)
在SetSysClockTo72中,有如下一段操作,
/* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
RCC_CFGR_PLLMULL));
RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
通过上述操作,我们将STM32的系统时钟频率设置为72MHZ。
总结:系统会自动调用上述函数进行设置,不需要我们人为的参与,但是我们需要通过宏定义来确定系统的主频。
第二步:设置SysTick定时器
设置SysTick定时器有两种方法,一种是通过寄存器,一种是通过库函数,而库函数直接启动了SysTick定时器的中断,如果你不需要使用中断功能,为了避免对官方库文件的修改,我建议使用寄存器直接配置SysTick
0x0001、SysTick寄存器配置
我们先看一下SysTick的寄存器列表,这张表位于文件Cortex®-M3 programming manual,该文件的官方下载地址如下:
1)、STK_CTRL —— SysTick控制寄存器
Bit 16 :COUNTFLAG:Returns 1 if timer counted to 0 since last time this was read. 如果在上次读取本寄存器后,SysTick已经数到了0,则该位为1,。如果读取该位,该位将自动清零
Bit 2 :CLKSOURCE:Clock source selection 时钟源选择位
0: AHB/8
1: AHB
Bit 1 :TICKINT: SysTick exception request enable 中断响应位
0: Counting down to zero does not assert the SysTick exception request 计数器减到0不产生中断
1: Counting down to zero to asserts the SysTick exception request. 计数器减到0产生中断
Bit 0 : ENABLE: Counter enable 计数器使能位 启用计数器,当ENABLE设置为1时,计数器重加载寄存器,然后倒计时。当达到0时,它将COUNTFLAG设置为1,然后根据TICKINT的值选择是否产生中断。然后,它将重新加载
再次计算,然后开始计数。
0: Counter disabled 关闭计数器
1: Counter enabled 开启计数器
2)、STK_LOAD —— SysTick重载寄存器
Bit 23 ~ Bit 0:共24位,加载计数值,取值范围为0x00000001-0x00FFFFFF
3)、STK_VAL —— SysTick当前值寄存器
Bit 23 ~ Bit 0:共24位,当前计数值,取值范围为0x00000001-0x00FFFFFF,写入任何值都会将字段清除为0,并且还会清除STK_CTRL寄存器为0。
4)、STK_CALIB —— SysTick校准寄存器
Bit 31 :NOREF:NOREF 标志
0: 外部参考时钟可用
1: 外部参考时钟不可用
Bit 30 :SKEW:SKEW标志
0: 校准值是准确的10ms
1: 校准值不是准确的10ms
Bit 23 ~ Bit 0:TENMS:10ms的时间内倒计数的格数。芯片设计者应该通过Cortex-M3的输入信号提供该数值。若读取该值为0,则表示无法使用校准功能。
延时程序实例:根据上述对寄存器的描述,我们利用SysTick做一个ms延时和us延时程序
static u8 fac_us = 0; // 1us时间内SysTick计数器的计数值
static u16 fac_ms = 0; // 1ms时间内SysTick计数器的计数值
/**
*@brief 初始化SysTick定时器
*@param SYSCLK:系统时钟,系统选定为72MHz
*@return 无
*/
void systick_init (u32 sysclk)
{
SysTick->CTRL &= 0xfffffff9; // bit2清0,选择时钟为HCLK/8即9MHZ,bit1清0,禁止SysTick中断
fac_us = (sysclk/8)/1000000; // 1us计数值为72 000 000 / 1000 000
fac_ms = (sysclk/8)/1000; // 1ms计数值为72 000 000 / 1000
// fac_ms = (u16)fac_us*1000;
}
/**
*@brief 微秒延时函数
*@param time_us:要延时微秒时间数
*@return 无
*/
void delay_us(unsigned long time_us)
{
u32 temp;
SysTick->LOAD = time_us * fac_us; //时间加载
SysTick->VAL = 0x00; //清空计数器
SysTick->CTRL = 0x01 ; //开始倒数
do
{
temp = SysTick->CTRL;
}
while(temp & 0x01 && !(temp&(1<<16))); //等待时间到达
SysTick->CTRL = 0x00; //关闭计数器
SysTick->VAL = 0X00; //清空计数器
}
/**
*@brief 毫秒延时函数
*@param time_ms:要延时毫秒时间数
*@return 无
*/
void delay_ms(unsigned long time_ms)
{
u32 temp;
SysTick->LOAD = (u32)time_ms * fac_ms; //时间加载
SysTick->VAL = 0x00; //清空计数器
SysTick->CTRL = 0x01 ; //开始倒数
do
{
temp = SysTick->CTRL;
}
while(temp & 0x01 && !(temp&(1<<16))); //等待时间到达
SysTick->CTRL = 0x00; //关闭计数器
SysTick->VAL = 0X00; //清空计数器
}
0x0002、SysTick库函数配置
使用库函数设置SysTick定时器,只需调用SysTick_Config(uint32_t ticks)函数即可,函数自动完成:重装载值的装载,时钟源选择,计数寄存器复位,中断优先级的设置(最低),开中断,开始计数的工作。
如果不调整SysTick时钟,那么默认SysTick_CLKSource_HCLK(HCLK)
如果需要调整SysTick时钟,则调用void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)函数来配置,该函数位于文件misc.c中。
注意:函数调用顺序,应先调用SysTick_Config(uint32_t ticks),再调用SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)。
其中SysTick_CLKSource参数有两个选择,SysTick_CLKSource_HCLK_Div8(HCLK的8分频),SysTick_CLKSource_HCLK(HCLK时钟)
SysTick_CLKSourceConfig原函数如下:
/**
* @brief Configures the SysTick clock source.
* @param SysTick_CLKSource: specifies the SysTick clock source.
* This parameter can be one of the following values:
* @arg SysTick_CLKSource_HCLK_Div8: AHB clock divided by 8 selected as SysTick clock source.
* @arg SysTick_CLKSource_HCLK: AHB clock selected as SysTick clock source.
* @retval None
*/
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
/* Check the parameters */
assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
{
SysTick->CTRL |= SysTick_CLKSource_HCLK;
}
else
{
SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
}
}
其中assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));是对传入的参数是否有宏定义进行校验,宏定义代码如下:
#define SysTick_CLKSource_HCLK_Div8 ((uint32_t)0xFFFFFFFB)
#define SysTick_CLKSource_HCLK ((uint32_t)0x00000004)
#define IS_SYSTICK_CLK_SOURCE(SOURCE) (((SOURCE) == SysTick_CLKSource_HCLK) || \
((SOURCE) == SysTick_CLKSource_HCLK_Div8))
如果需要调整SysTick的中断优先级,则调用void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)函数来配置,该函数位于文件core_cm3.h中。
注意:函数调用顺序,应先调用SysTick_Config(uint32_t ticks),再调用NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)。
延时程序实例:我们利用库函数做一个ms级延时程序
main函数中调用SysTick_Config()配置并启动SysTick定时器
int main(void)
{
led_init();
/* 使用while (1)等待SysTick配置启动成功 */
/* 如果不更改SysTick时钟源,那么SysTick时钟为72MHz,即计数72 000次为1ms,并产生1次中断 */
if(SysTick_Config(72000))
{
/* Capture error */
while (1);
}
while(1)
{
GPIO_ResetBits(GPIOC,GPIO_Pin_10);
Delay(1000);
GPIO_SetBits(GPIOC,GPIO_Pin_10);
Delay(1000);
}
}
/**
* @brief Inserts a delay time.
* @param nTime: 延时nTime毫秒
* @retval None
*/
void Delay(__IO uint32_t nTime)
{
TimingDelay = nTime;
while(TimingDelay != 0);
}
/**
* @brief 在中断中调用该函数,每1ms中断1次,每次中断TimingDelay变量减1
* @param None
* @retval None
*/
void TimingDelay_Decrement(void)
{
if(TimingDelay != 0x00)
{
TimingDelay--;
}
}
/**
* @brief This function handles SysTick Handler.SysTick中断函数
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
TimingDelay_Decrement();
}
至此,SysTick定时器的完整的使用方式就介绍完了。
总结:SysTick定时器的使用流程如下:
设置系统时钟 ——> 设置SysTick定时器时钟 ——> 使用SysTick寄存器设置或者调用库函数设置SysTick定时器
需要注意的点:
1、如果使用查询法产生延时,只能用寄存器设置SysTick定时器,用库函数设置SysTick定时器会产生SysTick中断。
2、使用库函数更改SysTick时钟源或者中断优先级时需要注意函数调用顺序
SysTick_Config ——> SysTick_CLKSourceConfig
SysTick_Config ——> NVIC_SetPriority
0x04、源程序下载地址
下面上传一个源程序供大家参考: