void delay(unsigned int i)
{
while(i--);
}
上面是我们最初写的延时函数,比较简单,就是循环,但是无法实现精确延时,或者说想要实现精确延时很复杂,需要设置断点,一次次去碰运气,而且还占用CPU,就是CPU必须执行完delay延时,才可以执行其他程序(按顺序执行),而今天使用的SysTick不占用CPU,使用的时候,CPU还可以干其他的事。
SysTick 属于单片机内部的外设,不需要额外的硬件电路。
1、先介绍下SysTick:系统滴答计时器,虽然存放在了NVIC中,但是NVIC无法控制SysTick。
系统定时器是一个24位的向下递减的计数器,也就是计数值最大可以达到2^24-1,存放在重装载数值寄存器中,当重装载数值寄存器的值递减到0 的时候,系统定时器就产生一次中断,执行中断服务函数中的程序,以此循环往复。
计数器每计数一次的时间为1/SYSCLK,我们使用的是STM32F103VET6芯片,所以系统时钟SYSCLK等于72M。
为什么最大计数值会减1?
因为最大计数值再计以为就溢出归0。
系统定时器工作原理:
系统定时器的作用:
由于小白杨,还未学到操作系统(ucos2 ucos3 freertos…),所有也不知道在操作系统中如何使用系统定时器,以后学到了,会再补充的,这里就先知道如何实现精确延时就够了。
系统定时器的构成:包括4个寄存器,存放在CMSIS系统文件中
系统定时器的校准数值寄存器在定时实验中用不到,只使用前3个寄存器就可以
系统定时器的配置,通过代码进行讲解:
实验:利用SysTick 产生1s 的延时中断,使LED灯以1s 的频率进行闪烁。
先介绍一下系统时钟源,这是延时的根本来源:
STM32时钟树:
这里看图理解会有一个误区,以为滴答定时器是系统时钟的1/8,其实不是,滴答定时器的时钟既可以是HCLK/8,也可以是HCLK,这个是通过CTRL寄存器进行设定的。
计数时间计算:
t=reload*(1/SystemCoreClock)
当reload=72,t=72*(1/72M)=1us;
当reloa=72000,t=72000*(1/72M)=1ms。
这里介绍一下
static __INLINE uint32_t SysTick_Config(uint32_t ticks)函数,定义于core_cm3.h文件中,具体内容我已在下方代码中写了注释
/**
* @brief Initialize and start the SysTick counter and its interrupt.
*初始化并开启定时计数器和中断
* @param ticks number of ticks between two interrupts
* 参数:计数值
* @return 1 = failed, 0 = successful
* 返回值 1=不符合规定 0=符合
*
* Initialise the system tick timer and its interrupt and start the
* system tick timer / counter in free running mode to generate
* periodical interrupts. 触发定期中断
*/
//总结:定义一个内联函数,检查计数值ticks是否小于最大值,防止出错,且这个函数只可以在这个文件中使用
#if 0
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); //如果不符合规则,返回1
//符合规定,执行下面程序
//将计数值保存到LOAD中,
//ticks和SysTick_LOAD_RELOAD_Msk(值为0xFF FFFF)位与 ,主要是为了屏蔽ticks中24bit~31bit的值(理论上会是0),作为一种冗余的措施,所以最后的结果还是ticks的值,其实目的还是防止ticks超过最大值
//-1是因为每次每数的时候都是从0开始的,比如:SysTick定时器的初值是0(“SysTick->VAL = 0"),假设不减1,ticks=3,计数过程为:0->3->2->1->0,总共用经历了4个时钟周期,不符合规则,所以要减1,可以等到3次计数,0->2->1->0
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;
//配置中断优先级
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
//使寄存器VAL重新赋值为0
SysTick->VAL = 0;
//选择时钟源,72MHz
//开启Systick中断
// 使能Systick定时器
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0);//返回0函数成功配置
}
#endif
这里再解释一下返回值返回到哪里,它返回到 #if 0 那里 ,所以如果计数值正常,则不会执行这段程序。
#if 0 //为1才执行
code //屏蔽代码
#endif
如果需要使用HCLK/8,可以直接调用SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)函数:
最后我把这些代码都汇总到一起,方便阅读:
led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"
//定义引脚
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED1_GPIO_PORT GPIOB
#define LED1_GPIO_PIN GPIO_Pin_5
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED2_GPIO_PORT GPIOB
#define LED2_GPIO_PIN GPIO_Pin_0
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_PIN GPIO_Pin_1
#define ON 0
#define OFF 1
//库函数操作I/O口,带参宏,可以像内联函数一样使用
#define LED1(A) if (A) \
GPIO_SetBits(LED1_GPIO_PORT,LED1_GPIO_PIN);\
else \
GPIO_ResetBits(LED1_GPIO_PORT,LED1_GPIO_PIN);
#define LED2(A) if (A) \
GPIO_SetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);\
else \
GPIO_ResetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);
#define LED3(A) if (A) \
GPIO_SetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);\
else \
GPIO_ResetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);
//声明LED点亮函数,在主代码中直接用led.h文件就可以
void LED_GPIO_Config(void);
#endif
\:这是续行符,用来表示下一行与上一行是同一行,这样比较有观赏性,否则,所有代码都放在一行,就不太好看,“\”后面不能有任何字符(包括注释、空格),只能直接回车。
led.c
#include "led.h"
void LED_GPIO_Config(void)
{
//引入结构体,声明一个名字叫做GPIO_InitStructure结构体,原型是GPIO_InitTypeDef(管方库),作用类似于赋值重取名
GPIO_InitTypeDef GPIO_InitStructure;
//2.打开时钟
RCC_APB2PeriphClockCmd(LED1_GPIO_CLK|LED2_GPIO_CLK|LED3_GPIO_CLK,ENABLE);
//3.选择引脚
GPIO_InitStructure.GPIO_Pin=LED1_GPIO_PIN|LED2_GPIO_PIN|LED3_GPIO_PIN;
//4.引脚模式
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
//5.引脚速度
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//6.调用库函数,初始化GPIOB,作用就是将GPIO_InitTypeDef地址给GPIOB
GPIO_Init(GPIOB,&GPIO_InitStructure);
//7.关闭所有LED,从最初状态开始
GPIO_SetBits(GPIOB,GPIO_Pin_All);
}
delay.h
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f10x.h"
#include "core_cm3.h"
void delay_us(uint32_t us);//微秒级别
void delay_ms(uint32_t ms);//毫秒级别
#endif
注意:如果下方两个头文件的顺序相反,会出现很多errors,因为我们引用了中断,但是中断现在stm32f10x.h这个文件中先定义的,使用时必须放在前面,类似于先声明。
delay.c
#include "delay.h"
#if 0
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
// 判断 tick 的值是否大于 2^24,如果大于,则不符合规则
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
// 初始化reload寄存器的值
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;
// 配置中断优先级,配置为15,默认为最低的优先级
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
// 初始化counter的值为0
SysTick->VAL = 0;
// 配置 systick 的时钟为 72M
// 使能中断
// 使能systick
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0);
}
#endif
void delay_us(uint32_t us)
{
uint32_t i; //定义一个变量
SysTick_Config(72);//直接写ticks,这样比较好理解 72*(1/72M)=1us
for(i=0; i<us; i++) //循环,当us=1000时,则执行1000次循环
{
while( !((SysTick->CTRL) & (1<<16)) ); //位与,当递减到0,置1,则!1=0
}
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;//关闭定时器
}
void delay_ms(uint32_t ms)
{
uint32_t i;
SysTick_Config(72000);
for(i=0; i<ms; i++)
{
while( !((SysTick->CTRL) & (1<<16)) );
}
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
}
stm32f10x_it.h
#ifndef __STM32F10x_IT_H//Define to prevent recursive inclusion
#define __STM32F10x_IT_H
#endif //暂时用不到
stm32f10x_it.c
#include "stm32f10x_it.h"
/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
} //中断服务函数,可以为空,但必须使用
main.c
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
int main(void)
{
//LED初始化
LED_GPIO_Config();
while(1)
{
LED1(ON);
delay_ms(1000);//延时1s
LED1(OFF);
delay_ms(1000);
LED2(ON);
delay_ms(1000);
LED2(OFF);
delay_ms(1000);
LED3(ON);
delay_ms(1000);
LED3(OFF);
delay_ms(1000);
}
}
最后再补充一点C语言知识:
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
static:用于声明内部函数
inline 关键字可以把函数指定为内联函数,但是关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前是不起任何作用的。
其实这篇文章中的精确延时方法还有很多需要优化的地方,先暂时不纠结深入了,以后懂了再去修改。
作者能力水平有限,文章难免存在错误和纰漏,请大佬不吝赐教,非常欢迎大家与小白杨进行技术交流,希望在此能遇到志同道合的朋友,一起学习技术。