在【STM32F103笔记】2、单片机中的HelloWorld——流水灯中我们曾写过一个简单的延时函数,利用空操作函数__nop()并大致计算延时时间,但这个函数并不精确,有兴趣的朋友可以再把那一篇中的程序运行结果和标准时钟比较一下。
这一篇中将使用Cortex-M3内核自带的系统时钟 (System Time)设计精确的延时函数。
SysTick
Cortex-M3内核自带一个24位的降序计数器,也就是SysTick,通常Systick是用于给实时操作系统提供准确的滴答时钟。
在这里将SysTick用于精确的计时,先介绍一下2个SysTick相关的寄存器:
- SysTick control and status register (STK_CTRL):控制和状态寄存器,下图是这个寄存器每一位的说明:
- Bit 16 COUNTFLAG:每次计数器向下自减到0的时候会自动置1,表示计数完成;
- Bit 2 CLKSOURCE:时钟来源选择,可以看出SysTick的时钟源是AHB,在第二篇中可知,系统初始化后AHB时钟为72MHz,通过这一位可以选择AHB时钟不分频或者8分频作为Systick的时钟;
- Bit 1 TICKINT:是否触发中断,触发中断则会进入SysTick_Handler函数(stm32f10x_it.c文件中);
- Bit 0 ENABLE:SysTick使能位,置1则使能SysTick,开始自减计数,清0则禁用SysTick。
- SysTick reload value register (STK_LOAD):计数器加载值寄存器,每次SysTick计数器重新开始计数时,将读取这个寄存器中的值,也就是说,计多少个数由这个寄存器控制。
由于SysTick的计数器自减操作不受其它干扰和影响,完全由其时钟决定,若时钟源选择AHB 72MHz,那么当计数器从72计数到0,相当于时间1us,因此可以使用SysTick来精确计算时间。
程序设计
程序设计思路是,首先初始化SysTick,设置固定的计数(比如720),并且每次计数完成后触发中断,这样每次触发中断就是固定的事件(10us)。
初始化SysTick——SysTick_Config库函数
同样,官方库中提供了用于SysTick配置初始化的函数:
/**
* @brief Initialize and start the SysTick counter and its interrupt.
*
* @param ticks number of ticks between two interrupts
* @return 1 = failed, 0 = successful
*
* Initialise the system tick timer and its interrupt and start the
* system tick timer / counter in free running mode to generate
* periodical interrupts.
*/
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 */
}
#endif
- SysTick_Config函数用于配置SysTick进行固定的计数,并在每次计数完成后触发中断;
- 函数参数ticks就是计数值,将写入SysTick的LOAD寄存器;
- 首先判断ticks的值是否超过LOAD寄存器的限制,然后将值写入LOAD寄存器;
- 配置SysTick的中断优先级;
- 将VAL寄存器(当前计数值寄存器)清0;
- 然后设置CTRL寄存器:
- 时钟源配置为AHB不分频,即72MHz;
- 允许触发中断;
- 使能SysTick。
- 若设置成功函数将返回0。
延时函数
在工程的USER文件夹下新建delay.c、delay.h两个文件用于存放延时函数,并且方便以后使用。
delay.h
这里直接先给出delay.h文件的内容:
/**
******************************************************************************
* @file delay.h
* @author TTZZWWW
* @version
* @date
* @brief Delay functions.
******************************************************************************
* @attention
* Delay functions should be consistent with initialized SysTick clock.
******************************************************************************
*/
#ifndef __DELAY_H
#define __DELAY_H
/* Includes ------------------------------------------------------------------*/
#include <stdint.h>
/* Defines -------------------------------------------------------------------*/
#define DELAYCLOCK_1us SystemCoreClock/1000000
#define DELAYCLOCK_10us SystemCoreClock/100000
#define DELAYCLOCK_100us SystemCoreClock/10000
#define DELAYCLOCK_1ms SystemCoreClock/1000
/* External variables --------------------------------------------------------*/
extern uint32_t DelayTimeCount;
/**
* @brief Initialize SysTick for delay functions
* @param Delay clock, already defined in delay.h
* @return None
*/
void DelayInitSysTick(uint32_t delayclock);
/**
* @brief delay us
* @param us
* @return None
*/
void delay_us(uint32_t us);
/**
* @brief delay ms
* @param ms
* @return None
*/
void delay_ms(uint32_t ms);
#endif /* __DELAY_H */
- 作为头文件,需要考虑在同一个工程的不同文件中都包含时应不引起重复定义的错误,即include了一次就不用再include了,因此在文件头尾加入:
#ifndef __DELAY_H
#define __DELAY_H
...
#endif /* __DELAY_H */
- 为了方便延时程序初始化SysTick的ticks计算,设置如下计数值宏定义,其中SystemCoreClock是官方库中定义的系统内核时钟频率,这里是72MHz,具体计算方法上面已经提到了,这里提供1us、10us、100us、1ms计数值,也就是每次进入SysTick_Handler中断的固定时间:
#define DELAYCLOCK_1us SystemCoreClock/1000000
#define DELAYCLOCK_10us SystemCoreClock/100000
#define DELAYCLOCK_100us SystemCoreClock/10000
#define DELAYCLOCK_1ms SystemCoreClock/1000
- DelayTimeCount在delay.c文件中定义,用于计数进入中断的次数,也就是延时了多少个固定时间;这里使用extern关键字,因为DelayTimeCount在delay.c文件中定义,而需要在其它文件中使用;
- 最后是3个函数声明:
void DelayInitSysTick(uint32_t delayclock);
void delay_us(uint32_t us);
void delay_ms(uint32_t ms);
初始化函数 DelayInitSysTick
DelayInitSysTick()函数用于初始化SysTick:
/**
* @brief Initialize SysTick for delay functions
* @param None
* @retval None
*/
void DelayInitSysTick(uint32_t delayclock)
{
if (SysTick_Config(delayclock))
while(1);
DelayClock = delayclock;
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
- 调用库函数SysTick_Config(delayclock)对SysTick进行配置,并将LOAD寄存器的值保存在uint3_t DelayClock变量中,用于后续计算延时,最后禁用SysTick,因为我们现在仅需要在进行延时的时候使用SysTick。
微秒级延时函数 delay_us
delay_us()函数用于进行微秒级的延时:
/**
* @brief delay us
* @param us
* @return None
*/
void delay_us(uint32_t us)
{
DelayTimeCount = us * 72 / DelayClock;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(DelayTimeCount);
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
- 首先根据SysTick的LOAD寄存器中的值(假设取DELAYCLOCK_10us=720)计算中断的次数,假设延时1000us,则DelayTimeCount计算结果为100,即SysTick计数720进入中断,每次进入中断合时间为10us,并在中断中将DelayTimeCount的值减1,因此进入100次中断就可以延时1000us;
- 然后使能SysTick,开始延时;
- 通过DelayTimeCount的值来判断延时是否完成;
- 最后关闭SysTick。
毫秒级延时函数 delay_ms
和delay_us相似:
/**
* @brief delay ms
* @param ms
* @return None
*/
void delay_ms(uint32_t ms)
{
DelayTimeCount = ms * 72000 / DelayClock;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(DelayTimeCount);
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
中断处理函数 SysTick_Handler
在中断处理函数SysTick_Handler中先判断DelayTimeCount是否已经为0,若否则将其减1(注意stm32f10x_it.c文件中要include “delay.h”):
#include "delay.h"
...
/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
if(DelayTimeCount != 0)
DelayTimeCount--;
}
这样就完成了延时函数的设计,在main函数中我们通过LED亮灭来验证。
验证程序
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"
#include "delay.h"
/* Private functions ---------------------------------------------------------*/
void LEDPB8Config(void);
int main(void)
{
LEDPB8Config();
DelayInitSysTick(DELAYCLOCK_1ms);
while(1)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(1-GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_8)));
delay_ms(1000);
}
}
void LEDPB8Config(void)
{
GPIO_InitTypeDef GPIOInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIOInitStruct.GPIO_Pin = GPIO_Pin_8;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIOInitStruct);
}
运行结果
可以看到LED以1s的延时进行闪烁,将其与标准时钟比较,可以发现延时还是很准的: