STM32 芯片为了实现低功耗,设计了一个功能完善但却非常复杂的时钟系统。普通的MCU 一般只要配置好 GPIO 的寄存器就可以使用了,但 STM32 还有一个步骤,就是开启外设时钟。
在 STM32 中,有五个时钟源,为 HSI、 HSE、 LSI、 LSE、 PLL。 从时钟频率来分可以分为高速时钟源和低速时钟源,在这 5 个中 HIS, HSE 以及 PLL 是高速时钟, LSI 和 LSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的 5 个时钟源,我们讲解顺序是按图中红圈标示的顺序:
①HSI 是高速内部时钟, RC 振荡器,频率为 8MHz。
②HSE 是高速外部时钟,可接石英 /陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。 我们的开发板接的是 8M 的晶振。
③LSI 是低速内部时钟,RC 振荡器,频率为 40kHz。独立看门狗的时钟源只能是 LSI,同时 LSI 还可以作为 RTC 的时钟源。
④LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。
⑤PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、HSE 或者 HSE/2。倍频可选择为2~16 倍,但是其输出频率最大不得超过 72MHz。
图中我们用 A~E 标示我们要讲解的地方。
A. MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出, 可以选择为 PLL 输出的 2 分频、 HSI、 HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。
B. 这里是 RTC 时钟源,从图上可以看出, RTC 的时钟源可以选择 LSI, LSE,以及HSE 的 128 分频。
C. 从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。 STM32 中有一个全速功能的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB模块时, PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。
D. D 处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时钟源。系统时钟可选择为 PLL 输出、 HSI 或者 HSE。系统时钟最大频率为 72MHz,当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
E. 这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最终来源都是 SYSCLK。 SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些模块包括:
①AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。
②通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。
③直接送给 Cortex 的空闲运行时钟 FCLK。
④送给 APB1 分频器。 APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大频率 36MHz),另一路送给定时器(Timer)2、 3、 4 倍频器使用。
⑤送给 APB2 分频器。 APB2 分频器分频输出一路供 APB2 外设使用(PCLK2,最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。
其中需要理解的是 APB1 和 APB2 的区别, APB1 上面连接的是低速外设,包括电源接口、备份接口、 CAN、 USB、 I2C1、 I2C2、 UART2、 UART3 等等, APB2 上面连接的是高速外设包括 UART1、 SPI1、 Timer1、 ADC1、 ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。
SystemInit()函数中设置的系统时钟大小:
SYSCLK(系统时钟) =72MHz
AHB 总线时钟(使用 SYSCLK) =72MHz
APB1 总线时钟(PCLK1) =36MHz
APB2 总线时钟(PCLK2) =72MHz
PLL 时钟 =72MHz
具体代码清读者查看工程文件的system_stm32f10x.c文件。
SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号 :15)。在以前,操作系统和所有使用了时基的系统都必须有一个硬件定时器来产生需要的“滴答”中断,作为整个系统的时基。滴答中断对操作系统尤其重要。例如,操作系统可以为多个任务分配不同数目的时间片,确保没有一个任务能霸占系统 ;或者将每个定时器周期的某个时间范围赐予特定的任务等,操作系统提供的各种定时功能都与这个滴答定时器有关。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
Cortex-M3 在内核部分包含了一个简单的定时器——SysTick。因为所有的 CM3 芯片都带有这个定时器,软件在不同芯片生产厂商的 CM3 器件间的移植工作就得以简化。该定时器的时钟源可以是内部时钟(FCLK,CM3 上的自由运行时钟),或者是外部时钟( CM3 处理器上的 STCLK 信号)。不过,STCLK 的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能大不相同。因此,需要阅读芯片的使用手册来确定选择什么作为时钟源。在 STM32 中 SysTick 以 HCLK(AHB 时钟)或 HCLK/8 作为运行时钟,见图 1- 1。
SysTick 定时器能产生中断,CM3 为它专门开出一个异常类型,并且在向量表中有它的一席之地。它使操作系统和其他系统软件在 CM3 器件间的移植变得简单多了,因为在所有 CM3 产品间,SysTick 的处理方式都是相同的。SysTick 定时器除了能服务于操作系统之外,还能用于其他目的,如作为一个闹铃、用于测量时间等。Systick 定时器属于Cortex 内核部件,可以参考《ARM Cortex-M3 权威指南》((英)JosephYiu 著,宋岩译,北京航空航天大学出版社出版)或“STM32xxx-Cortex-M3programmingmanual”(这是 ST 官方提供的电子版编程手册,可以在 ST 官网下载)来了解。
在传统的嵌入式系统软件按中通常实现 Delay(N) 函数的方法为:
for(i = 0; i <= x; i ++);
x --- ;
对于STM32系列微处理器来说,执行一条指令只有几十个 ns,进行 for 循环时,要实现 N 毫秒的 x 值非常大,而且由于系统频率的宽广,很难计算出延时 N 毫秒的精确值。针对 STM32 微处理器,需要重新设计一个新的方法去实现该功能,以实现在程序中使用 Delay(N)。
Cortex-M3 的内核中包含一个 SysTick 时钟。SysTick 为一个 24 位递减计数器,SysTick 设定初值并使能后,每经过 1 个系统时钟周期,计数值就减 1。计数到 0 时,SysTick 计数器自动重装初值并继续计数,同时内部的 COUNTFLAG 标志会置位,触发中断 (如果中断使能情况下)。
在 STM32 的应用中,使用 Cortex-M3 内核的 SysTick 作为定时时钟,设定每一毫秒产生一次中断,在中断处理函数里对 N 减一,在Delay(N) 函数中循环检测 N 是否为 0,不为 0 则进行循环等待;若为 0 则关闭 SysTick 时钟,退出函数。
注: 全局变量 TimingDelay , 必须定义为 volatile 类型 , 延迟时间将不随系统时钟频率改变。
STM32中的Systick 部分内容属于NVIC控制部分,一共有4个寄存器,名称和地址分别是:
STK_CTRL, 0xE000E010 – 控制寄存器
第0位:ENABLE,Systick 使能位
(0:关闭Systick功能;1:开启Systick功能)
第1位:TICKINT,Systick 中断使能位
(0:关闭Systick中断;1:开启Systick中断)
第2位:CLKSOURCE,Systick时钟源选择
(0:使用HCLK/8 作为Systick时钟;1:使用HCLK作为Systick时钟)
第16位:COUNTFLAG,Systick计数比较标志,如果在上次读取本寄存器后,SysTick 已经数到了0,则该位为1。如果读取该位,该位将自动清零
STK_LOAD, 0xE000E014 – 重载寄存器
Systick是一个递减的定时器,当定时器递减至0时,重载寄存器中的值就会被重装载,继续开始递减。STK_LOAD 重载寄存器是个24位的寄存器最大计数0xFFFFFF。
STK_VAL, 0xE000E018 – 当前值寄存器
也是个24位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick 控制及状态寄存器中的COUNTFLAG 标志。
STK_CALRB, 0xE000E01C – 校准值寄存器
校准值寄存器提供了这样一个解决方案:它使系统即使在不同的CM3产品上运行,也能产生恒定的SysTick中断频率。最简单的作法就是:直接把TENMS的值写入重装载寄存器,这样一来,只要没突破系统极限,就能做到每10ms来一次 SysTick异常。如果需要其它的SysTick异常周期,则可以根据TENMS的值加以比例计算。只不过,在少数情况下, CM3芯片可能无法准确地提供TENMS的值(如, CM3的校准输入信号被拉低),所以为保险起见,最好在使用TENMS前检查器件的参考手册。
SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。要注意的是,当处理器在调试期间被喊停( halt)时,则SysTick定时器亦将暂停运作。
SysTick 库函数
SysTick_CLKSourceConfig 设置 SysTick 时钟源;
SysTick_SetReload 设置 SysTick 重装载值;
SysTick_CounterCmd 使能或者失能 SysTick 计数器;
SysTick_ITConfig 使能或者失能 SysTick 中断;
SysTick_GetCounter 获取 SysTick 计数器的值;
SysTick_GetFlagStatus 检查指定的 SysTick 标志位设置与否。
int main(void)
{
/* LED 端口初始化 */
LED_GPIO_Config();
/* 配置SysTick 为10us中断一次 */
SysTick_Init();
for(;;)
{
LED1( ON );
Delay_us(10000); // 10000 * 10us = 100ms
//Delay_ms(100);
LED1( OFF );
LED2( ON );
Delay_us(10000); // 10000 * 10us = 100ms
//Delay_ms(100);
LED2( OFF );
LED3( ON );
Delay_us(10000); // 10000 * 10us = 100ms
//Delay_ms(100);
LED3( OFF );
}
}
在 main 函数中,SysTick_Init() 和 Delay_us() 这两个函数比较陌生,它们的功能分别是配置好 SysTick 定时器和进行精确延时。整个 main 函数的流程就是初始化 LED 及SysTick 定时器之后,就进入死循环,轮流点亮 LED1、LED2、LED3,点亮的时间为精确的 100 ms。
配置并启动 SysTick
我们看一下 SysTick_Init() 这个函数,其功能是启动系统滴答定时器 SysTick ,并将 SysTick 配置为 10 μs 中断一次。
void SysTick_Init(void)
{
/* SystemFrequency / 100000 10us中断一次
* SystemFrequency / 1000000 1us中断一次*/
// if (SysTick_Config(SystemFrequency / 100000)) // ST3.0.0库版本
if (SysTick_Config(SystemCoreClock / 100000)) // ST3.5.0库版本
{
/* Capture error */
while (1);
}
// 关闭滴答定时器
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
}
本函数实际上只是调用了 SysTick_Confi g() 函数,它是属于内核层的 Cortex-M3 通用函数,位于 core_cm3.h 文件中。若调用 SysTick_Confi g() 配置 SysTick 不成功,则进入死循环,初始化 SysTick 成功后,先关闭定时器,在需要的时候再开启。SysTick_Confi g() 函数无法在STM32 外设固件库文件中找到其使用方法。所以我们在 Keil 环境下直接跟踪这个函数到 core_cm3.h 文件,查看函数的定义。
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
/* Reload value impossible */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
/* set Priority for Cortex-M0 System Interrupts */
SysTick->VAL = 0;
/* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
/* Enable SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
在这个函数定义的前面有关于它的注释,如果我们不想去研究它的具体实现,可以根据这段注释了解函数的功能 :这个函数启动了 SysTick ;并把它配置为计数至 0 时引起中断 ;输入的参数 ticks 为两个中断之间的脉冲数,即相隔 ticks 个时钟周期会引起一次中断 ;配置 SysTick 成功时返回 0,出错时返回 1。但是,这段注释并没有告诉我们它把 SysTick 的时钟设置为 AHB 时钟还是 AHB/8,这是一个十分关键的问题,于是,我们将对这个函数的具体实现进行分析,与大家再分享一下如何分析底层库函数。分析底层库函数,要有 SysTick 定时器工作分析的知识准备。
检查输入参数
SysTick_Confi g() 第 3 行代码是检查输入参数 ticks,因为 ticks 是脉冲计数值,要被保存到重载寄存器 STK_LOAD 寄存器中,再由硬件把 STK_LOAD 值加载到当前计数值寄存器 STK_VAL 中使用,STK_LOAD 和 STK_VAL 都是 24 位的,所以当输入参数 ticks 大于其可存储的最大值时,将由这行代码检查出错误并返回。
位指示宏及位屏蔽宏
检查 ticks 参数没有错误后,就稍稍处理一下把 ticks-1 赋值给 STK_LOAD 寄存器,要注意的是减 1,若 STK_VAL 从 ticks−1 向下计数至 0,实际上就经过了 ticks 个脉冲。这句赋值代码使用了宏 SysTick_LOAD_RELOAD_Msk,与其他库函数类似,这个宏是用来指示寄存器的特定位置或进行位屏蔽的。
/* SysTick Control / Status Register Definitions */
#define SysTick_CTRL_COUNTFLAG_Pos 16 /*!< SysTick CTRL: COUNTFLAG Position */
#define SysTick_CTRL_COUNTFLAG_Msk (1ul << SysTick_CTRL_COUNTFLAG_Pos) /*!< SysTick CTRL: COUNTFLAG Mask */
#define SysTick_CTRL_CLKSOURCE_Pos 2 /*!< SysTick CTRL: CLKSOURCE Position */
#define SysTick_CTRL_CLKSOURCE_Msk (1ul << SysTick_CTRL_CLKSOURCE_Pos) /*!< SysTick CTRL: CLKSOURCE Mask */
#define SysTick_CTRL_TICKINT_Pos 1 /*!< SysTick CTRL: TICKINT Position */
#define SysTick_CTRL_TICKINT_Msk (1ul << SysTick_CTRL_TICKINT_Pos) /*!< SysTick CTRL: TICKINT Mask */
#define SysTick_CTRL_ENABLE_Pos 0 /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk (1ul << SysTick_CTRL_ENABLE_Pos) /*!< SysTick CTRL: ENABLE Mask */
/* SysTick Reload Register Definitions */
#define SysTick_LOAD_RELOAD_Pos 0 /*!< SysTick LOAD: RELOAD Position */
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFul << SysTick_LOAD_RELOAD_Pos) /*!< SysTick LOAD: RELOAD Mask */
/* SysTick Current Register Definitions */
#define SysTick_VAL_CURRENT_Pos 0 /*!< SysTick VAL: CURRENT Position */
#define SysTick_VAL_CURRENT_Msk (0xFFFFFFul << SysTick_VAL_CURRENT_Pos) /*!< SysTick VAL: CURRENT Mask */
/* SysTick Calibration Register Definitions */
#define SysTick_CALIB_NOREF_Pos 31 /*!< SysTick CALIB: NOREF Position */
#define SysTick_CALIB_NOREF_Msk (1ul << SysTick_CALIB_NOREF_Pos) /*!< SysTick CALIB: NOREF Mask */
#define SysTick_CALIB_SKEW_Pos 30 /*!< SysTick CALIB: SKEW Position */
#define SysTick_CALIB_SKEW_Msk (1ul << SysTick_CALIB_SKEW_Pos) /*!< SysTick CALIB: SKEW Mask */
#define SysTick_CALIB_TENMS_Pos 0 /*!< SysTick CALIB: TENMS Position */
#define SysTick_CALIB_TENMS_Msk (0xFFFFFFul << SysTick_VAL_CURRENT_Pos) /*!< SysTick CALIB: TENMS Mask */
/*@}*/ /* end of group CMSIS_CM3_SysTick */
其中寄存器位指示宏 :SysTick_xxx_Pos ,宏展开后即为 xxx 在相应寄存器中的位置,如控制 SysTick 时钟源的 SysTick_CTRL_CLKSOURCE_Pos ,宏展开为 2,这个寄存器位正是寄存器 STK_CTRL 中的 Bit2。
而寄存器位屏蔽宏 :SysTick_xxx_Msk,宏展开是 xxx 的位全部置 1 后,左移SysTick_xxx_Pos 位。如控制 SysTick 时钟源的 SysTick_CTRL_CLKSOURCE_Msk,宏展开为“1ul << SysTick_CTRL_CLKSOURCE_Pos”, 把 无 符 号 长 整 型 数 值(ul) 1 左移 2 位, 得 到 了 一 个 只 有 Bit2 :CLKSOURCE 位被置 1,其他位为 0 的数值,这样的数值配合位操作 &(按位与)、| (按位或)可以很方便地修改寄存器的某些位。假如控制 CLKSOURCE 需 要 4 个 寄 存 器 位 , 这 个 宏 就 应 该 被 改 为 ( 0xf ul < 配置中断向量及重置 STK_VAL 寄存器 配置 SysTick 时钟为 AHB 使能、关闭定时器 定时时间的计算 根据前面对 SysTick_Confi g() 函数的介绍,它的输入参数为 SysTick 将要计时的脉冲数,经过 ticks 个脉冲(经过 ticks 个时钟周期)后将触发中断,触发中断后又重新开始计数。由此我们可以算出定时的时间,下面为计算公式 : 例如 :本实验例子中,使用时钟源为 AHB 时钟,其频率被配置为 72 MHz。调用函数时,把 ticks 赋值为 ticks=SystemFrequency / 10 000 =720,表示 720 个时钟周期中断一次 ;1/f 是时钟周期的时间,此时(1/f =1/72 μs),所以最终定时总时间 T=720×(1/72),为720 个时钟周期,正好是 10 μs。 编写中断服务函数 使能了 SysTick 之后,就使用 while(TimingDelay != 0)语句等待 TimingDelay 变量变为 0,这个变量是在中断服务函数中被修改的。因此,我们需要编写相应的中断服务程序,在本实验室中我们配置为 10μs 中断一次,每次中断把 TimingDelay 减 1。中断程序在 stm32f10x_it.c 中实现。 SysTick中断属于系统异常向量,在stm32f10x_it.c文件中已经默认有了它的中断服务函数SysTick_Handler(),但内容为空。我们找到这个函数,其调用了用户函数TimingDelay_Decrement()。后者是由用户编写的一个应用程序。 每次进入 SysTick 中断就调用一次 TimingDelay_Decrement()函数,使全局变量TimingDelay 自减一次。用户函数 Delay_us ()在TimingDelay 被减至0时,才退出延时循环,即我们对 TimingDelay 赋的值为要中断的次数。所以总的延时时间 : IAR版本完整代码
回到 SysTick_Confi g() 函数,接下来调用了 NVIC_SetPriority () 函数并配置了 SysTick中断,这就是为什么我们在外部没有再使用 NVIC 配置 SysTick 中断的原因。配置好SysTick 中断后把 STK_VAL 寄存器重新赋值为 0(在使能 SysTick 时,硬件会把存储在STK_LOAD 寄存器 中的 ticks 值加载给它)。
在这段代码最后,向 STK_CTRL 寄存器写入了 SysTick 的控制参数,配置为使用AHB 时钟,使能计数至 0 时引起中断,使能 SysTick。执行了这行代码,SysTick 就开始运行并进行脉冲计数了。
若读者想要使用 AHB/8 作为时钟,可以调用库函数 SysTick_CLKSourceConfi g() 进行修改,也可以直接对 SysTick_Confi g() 函数的代码进行修改。
由于调用 SysTick_Confi g() 函数之后,SysTick 定时器就被开启了,但我们在初始化的时候并不希望这样,而是根据需要再开启。所以在 SysTick_Init() 函数中,调用完SysTick_Confi g() 并配置好后,应先把定时器关闭了。SysTick 的开启和关闭由寄存器STK_CTRL 的 Bit0 :ENABLE 位来控制,使用位屏蔽宏以操作寄存器的方式实现。SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 使能滴答定时器
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk; // 关闭滴答定时器
在调用SysTick_Config()函数时,向它输入的参数为SystemCoreClock / 100000,SystemCoreClock为定义了系统时钟(SYSCLK)频率的 宏,即等于 AHB的时钟频率。在本书的所有例程中AHB 都是被配置为 72 MHz 的,也就是这个 SystemCoreClock 宏展开为数值 7200 0000。
T=ticks×(1/f)
其中,T 为要定时的总时间 ;ticks 为 SysTick_Confi g() 的输入参数 ;1/ f 即为SysTick 使用的时钟源的时钟周期,f 为该时钟源的时钟频率,当时钟源确定后为常数。
SysTick 定时器的定时时间(配置为触发中断,即为中断周期)由 ticks 参数决定,最大定时周期不能超过 224 个。
一旦我们调用了 Delay_us() 函数,SysTick 定时器就被开启,按照设定好的定时周期递减计数,当 SysTick 的计数寄存器的值减为 0 时,就进入中断函数,当中断函数执行完毕之后重新计时,如此循环,除非它被关闭。void Delay_us(__IO u32 nTime)
{
TimingDelay = nTime;
// 使能滴答定时器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
void SysTick_Handler(void)
{
TimingDelay_Decrement();
}
void TimingDelay_Decrement(void)
{
if (TimingDelay != 0x00)
{
TimingDelay--;
}
}
T 延时 = T 中断周期 ×TimingDelay
至此,SysTick 的精确延时功能讲解完毕。本章参考代码
KEIL版本完整代码