STM32滴答定时器SysTick精准延时,兼容HAL库和标准库

543 阅读14分钟

STM32手册资料下载:STM32资料Github链接STM32资料Gitee链接

注意:Github是国外的,要翻墙,Gitee是国内的,无需翻墙。

目录

滴答定时器的功能

模块化思想

什么叫做模块化

如何利用keil实现模块化

第一步,准备工程文件

第二步,建立delay.c和delay.h文件

第三步,将sys加入工程

第四步 ,加入路径

代码

.c文件 

.h文件

HAL库

标准库

今后如何将delay模块加入其他工程

main.c调用

初始化

实现软件PWM

 Delay_us()实验

Delay_ms()实验

代码讲解

Delay_Init()

代码

滴答定时器寄存器介绍

 Delay_Init()函数介绍

 Delay_us()函数介绍

 Delay_ms()函数介绍

最后再次强调!!!


野火和正点原子的滴答定时器部分的延时函数我都看了,感觉对新手都及其不友好。所以我使用海创电子(教的是标准库的内容,但是真的真的讲得棒!)的滴答定时器部分代码作为讲解。

本次实验利用SysTick精准延时,实现软件PWM。需要准备一个LED灯(这个可能不太直观),或者一个示波器(这个精准一些)。

滴答定时器的功能

(1)滴答定时器可用于操作系统产生时基,维持操作系统的心跳。一般操作系统都需要一个时基,进行任务的调度、同步等功能实现。(这个了解即可,不知道没关系)

(2)滴答定时器常用于计数。比如进行微妙、毫秒延时。(这个才是重点)

注意:

(1)这一篇不需要使用到STM32CubeMX,我们是直接对寄存器进行操作!

(2)想了解底层实现的可以看详细说明,只想要延时函数的可以直接复制代码就走。

(3)因为我们都是裸机开发,所以关于操作系统部分的内容没有。需要操作系统部分的延时程序,可自行去野火或者正点原子的官方例程中复制。

(4)标准库和HAL库代码基本一样,就只要那个需要分频的函数部分需要更改,以及头文件需要更改。

(5)在裸机开发中,一般都使用滴答定时器作为精准延时函数。所以我就只讲精准延时部分。

模块化思想

什么叫做模块化

因为可能有人是根据我的博客来学习的,没有模块化的思想,甚至不知道什么叫做模块化。

(1)什么是模块化?

比如我们下载一个工程文件,里面会有很多.c文件(这些.c放在对应的文件夹下面了),只有一个main.c文件,如下。像key文件夹,lcd文件夹下面存储的这些.c文件就是模块。

(2)这样模块化了有什么用呢? 

我们都是从学习C语言开始的。像我们使用printf打印字符,scanf获取键盘上的字符。printf和scanf函数就是存放在stdio.h这个头文件下。

当我们有了stdio这个模块之后,我们就不需要重新写printf和scanf函数的实现了。直接引用文件,就可以使用了。

现在我们所说的模块化亦是如此。如果我们将延时函数进行模块化了,之后我们需要在其他工程使用延时函数,我们只需要复制delay这个模块化到对应工程下面即可。

如何利用keil实现模块化

很多人可能没怎么使用过keil这个编译器,不知道如何利用keil创建一个模块,现在我教学一下。

第一步,准备工程文件

如果是使用HAL库首先利用STM32CubeMX生成一个工程文件。

如果是标准库,你自己准备好一个点灯工程。第一步就不用看了

(1)配置RCC

(2)主频配置为72MHZ

 

(3)配置SYS 

(4)配置GPIO 

(5) 生成文件

 

第二步,建立delay.c和delay.h文件

(1) 在工程目录下建立一个文件夹名为User,然后再在这个文件夹里面建立一个delay文件夹。

(2)在delay文件夹下面建立两个txt文件 

(3)更改两个文件名为delay.c和delay.h。

注意:记得要勾选文件扩展名!!!不会的百度!!!

第三步,将sys加入工程

第四步 ,加入路径

(1)再点击两个OK,现在delay这个模块就加入工程了。

(2)然后双击delay.c就可以打开delay.c这个文件了。

(3)在delay.c中加入#include  "delay.h",编译之后,delay.h也加入了工程。

代码

.c文件 

因为我们是直接对寄存器进行操作,所以无论你是使用的HAL库还是标准库,都不影响!!!

直接将下面代码复制到工程里面即可。

#include  "delay.h"

uint8_t fac_us=0;
uint16_t fac_ms=0;

void Delay_Init()
{
	//只可以选择不分频或者8分频,这里选择系统时钟8分频,最后频率为9MHZ
	SysTick->CTRL &= ~(1<<2);
	//SystemCoreClock为72000000,最终fac_us为9,也就是记录震动9次。因为频率为9MHZ所以为1us
	fac_us  = SystemCoreClock  / 8000000;  
	fac_ms  = fac_us*1000;  //1000us=1ms
}

/*
	CTRL     SysTick控制及状态寄存器
	LOAD     SysTick重装载数值寄存器
	VAL      SysTick当前数值寄存器
*/
void Delay_us(uint32_t nus)
{
	uint32_t temp;
	SysTick->LOAD  =nus*fac_us;   //设置加载的值,比如1us就要计数9次。nus传入1,CALIB=1*9=9,最后就是1us
	SysTick->VAL   =0x00;         //清空计数器中的值,LOAD里的值不是写入后就会加载,而是在systick使能且VAL值为0时才加载
	SysTick->CTRL  |=SysTick_CTRL_ENABLE_Msk;  //使能时钟,开始计时
	do
	{
		temp=SysTick->CTRL;   //查询是否计数完成
	}while((temp&0x01)&&!(temp&(1<<16)));   //先判断定时器是否在运行,再判断是否计数完成
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;      					 //清空计数器	 
}

void Delay_ms(uint32_t nms)
{
	uint32_t temp;
	SysTick->LOAD  =nms*fac_ms;   //设置加载的值,比如1us就要计数9次。nus传入1,CALIB=1*9=9,最后就是1us
	SysTick->VAL   =0x00;         //清空计数器中的值,LOAD里的值不是写入后就会加载,而是在systick使能且VAL值为0时才加载
	SysTick->CTRL  |=SysTick_CTRL_ENABLE_Msk;  //使能时钟,开始计时
	do
	{
		temp=SysTick->CTRL;   //查询是否计数完成
	}while((temp&0x01)&&!(temp&(1<<16)));   //先判断定时器是否在运行,再判断是否计数完成
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;      					 //清空计数器	 
}

.h文件

HAL库

因为HAL库和标准库的头文件名字不一样,所以这里还是有区分的

#ifndef     __delay_H
#define     __delay_H

#include "stm32f1xx.h"  // 相当于51单片机中的  #include <reg51.h>

void Delay_Init(void);
void Delay_us(uint32_t nus);
void Delay_ms(uint32_t nms);
#endif

标准库

#ifndef     __delay_H
#define     __delay_H

#include "stm32f10x.h"   // 相当于51单片机中的  #include <reg51.h>

void Delay_Init(void);
void Delay_us(uint32_t nus);
void Delay_ms(uint32_t nms);
#endif

今后如何将delay模块加入其他工程

我们只需要复制delay这个文件夹,到其他工程中,然后按照第三步,将sys加入工程和第四步 ,加入路径即可。

main.c调用

初始化

(1)首先我们需要在main.c最上面一行写上#include  "delay.h"

(2)然后在main函数里面需要调用Delay_Init();

注意:STM32Cube MX自动生成的  SystemClock_Config();需要放在Delay_Init();之前

 

实现软件PWM

只讲死循环部分,非死循环部分需要调整的如上。

 Delay_us()实验

  while (1)
  {
    /* USER CODE END WHILE */
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
		Delay_us(1000);
    /* USER CODE BEGIN 3 */
  }

最后生成了一个周期为2ms,占空比为50%的PWM

Delay_ms()实验

  while (1)
  {
    /* USER CODE END WHILE */
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
		Delay_ms(10);
    /* USER CODE BEGIN 3 */
  }

 最后生成了一个周期为20ms,占空比为50%的PWM.

代码讲解

这一部分给想跟深刻理解底层的人学习的,如果只是想用延时函数的人不需要看,对后续操作不影响。

Delay_Init()

代码

这个函数就是对滴答定时器进行一个8分频。然后设置两个变量。

uint8_t fac_us=0;
uint16_t fac_ms=0;

void Delay_Init()
{
	//只可以选择不分频或者8分频,这里选择系统时钟8分频,最后频率为9MHZ
	SysTick->CTRL &= ~(1<<2);
	//SystemCoreClock为72000000,最终fac_us为9,也就是记录震动9次。因为频率为9MHZ所以为1us
	fac_us  = SystemCoreClock  / 8000000;  
	fac_ms  = fac_us*1000;  //1000us=1ms
}

滴答定时器寄存器介绍

因为滴答定时器属于CM3内核相关的内容,所以我们需要查看CM3的手册。在我上面的链接里面,有很多STM32F103的资料,可以看野火,正点原子的SysTick部分的资料,或者直接看CM3权威指南。

首先解释Sys Tick定时器相关的寄存器

(1)CTRL控制及状态寄存器

CTRL只有0,1,2,16这四个位有用,其他的都没有使用。

 (2)LOAD重装载数值寄存器

1,我们知道,Sys Tick滴答定时器是一个24位定时器,所以他的重装载数值寄存器是一个24bit的寄存器。

2,重装载可能有些人无法理解。因为Sys Tick滴答定时器是一个向下计数的寄存器。比如滴答定时器现在的值为100,那么他从100一直自减到0的时候,LOAD会自动将100存入滴答定时器。

(3)VAL 当前数值寄存器

这个负责记录当前滴答定时器的值。加入这个值为0了,那么重装载寄存器里面的值将会自动存入VAL。(注意,重装载寄存器中的值并没有减少!相当于将重装载寄存器中的值复制,然后粘贴给VAL )

 (4)CALIB 校准数值寄存器

这个不知道什么用,正点原子手册里面没讲解,野火的手册里面说他们也是懵逼的。所以我也不明白,但是我猜测是进行校准滴答定时器的值,如果里面的值出问题了会又一些操作进行校正。用不到,不用纠结

 

 Delay_Init()函数介绍

下面为Delay_Init()这个函数的全部部分。uint8_t fac_us=0;和uint16_t fac_ms=0;也要包含!

uint8_t fac_us=0;
uint16_t fac_ms=0;

void Delay_Init()
{
	//只可以选择不分频或者8分频,这里选择系统时钟8分频,最后频率为9MHZ
	SysTick->CTRL &= ~(1<<2);
	//SystemCoreClock为72000000,最终fac_us为9,也就是记录震动9次。因为频率为9MHZ所以为1us
	fac_us  = SystemCoreClock  / 8000000;  
	fac_ms  = fac_us*1000;  //1000us=1ms
}

(1)我们看第一行代码,很简单,就是对滴答定时器进行一次八分频。CTRL是控制状态寄存器,当我们对他的bit2进行操作的时候,就是在选择SysTick的时钟源。这也是为什么我上面强调SystemClock_Config();需要放在Delay_Init();之前的原因了。

(2)因为我们CubeMX生成的滴答定时器是72MHZ,没有进行8分频。(不过你可以在CubeMX设置让他8分频)如果SystemClock_Config() 放在Delay_Init() 前面,那么滴答定时器本来在Delay_Init() 进行了八分频,现在你又用SystemClock_Config() 把滴答定时器变成没有分频。会导致延时出现问题。

(3)这个时候有人会问了,为什么滴答定时器要进行八分频呢?可以不八分频吗?

(4)答案显然是可以的,但是如果看过我之前的博客就知道,如果频率越高,功耗越大,响应速度也快。上面说了,滴答定时器就延时给操作系统提供时基的,我们不用操作系统,那么不需要考虑响应速度的问题。

(5)那么现在就考虑延时的问题,先说结论,如果频率越高,滴答定时器最大延时越短。什么意思呢?

(6)我们先又一个概念,1MHZ表示1S跳变1,000,000次。跳变一次,滴答定时器的VAL( 当前数值寄存器)中的数据就会减1。滴答定时器是24位定时器,2^24=16,777,215。可以跳变16,777,215次

(7)假设不分频,72MHZ,那么现在可以计数1/(72,000,000)*16,777,215\approx 0.233,也就是最大延时0.233S

(8)但是假如是进行了八分频,1/(72,000,000/8)*16,777,215\approx 1.864最大延时1.864S所以,为了更长的延时时间,我们选择了八分频

(9)fac_us 与fac_ms 作用又是什么呢?

现在我们的滴答定时器是9MHZ(72MHZ/8=9MHZ),所以说,当VAL( 当前数值寄存器)每减一,那么就表示过了1/9,000,000S。那么1us(1us=1/1,000,000S)就是VAL的值减9次。1ms就是VAL减9,000次。

(10)如果我们硬是要72MHZ的滴答定时器怎么办呢?

fac_us  = SystemCoreClock  / 8000000;  ——>fac_us  = SystemCoreClock  / 1000000;即可。  

 Delay_us()函数介绍

void Delay_us(uint32_t nus)
{
	uint32_t temp;
	SysTick->LOAD  =nus*fac_us;   //设置加载的值,比如1us就要计数9次。nus传入1,CALIB=1*9=9,最后就是1us
	SysTick->VAL   =0x00;         //清空计数器中的值,LOAD里的值不是写入后就会加载,而是在systick使能且VAL值为0时才加载
	SysTick->CTRL  |=SysTick_CTRL_ENABLE_Msk;  //使能时钟,开始计时
	do
	{
		temp=SysTick->CTRL;   //查询是否计数完成
	}while((temp&0x01)&&!(temp&(1<<16)));   //先判断定时器是否在运行,再判断是否计数完成
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;      					 //清空计数器	 
}

(1)我们知道了fac_us可以表示1us,那么我们传入参数nus*fac_us就可以表示为延时了多少us。现在将要延时的时间存入LOAD(自动重装载寄存器),然后清空VAL( 当前数值寄存器)的值,在我们开启滴答定时器的瞬间,LOAD会将数据存入VAL。因为当VAL寄存器为0的时候,LOAD会将值传给VAL。

(2)现在我们进行轮询法,不断查询CTRL(控制及状态寄存器)的bit16,因为CTRL的bit16在VAL为0的时候会为1。

(3)这个时候有人会问了呀,我们一开始就置零了VAL,那么现在CTRL的bit16不就已经是1了吗?所以我们要实现清空CTRL的bit16位。

(4)这个时候我们就需要自行看手册说明了。如果在上次读取本寄存器(也就是CTRL)后,SysTick 已经计到了 0,则该位为 1。

所以流程是,读取CTRL寄存器——>LOAD为0——>CTRL的bit16置1。我们在上面清零LOAD的时候,并没有读取CTRL,当我们在进行轮询的时候,LOAD的值已经被重装载了。

(5)一直轮询,直到VAL为0,那么现在延时结束。关闭滴答定时器,清空VAL的值即可。

(6)能够看到这里,说明你有一定基础。但是可能还是有一些新手像了解底层,坚持到了这里,看到SysTick_CTRL_ENABLE_Msk这个东西很奇怪,不知道这个是啥。这个时候我们需要将鼠标点击到SysTick_CTRL_ENABLE_Msk,按F12可以跳转到他的定义。

我们看发现SysTick_CTRL_ENABLE_Msk其实就是无符号的数字1。开关Sys Tick依靠CTRL的bit0。

#define SysTick_CTRL_ENABLE_Msk            (1UL /*<< SysTick_CTRL_ENABLE_Pos*/)           /*!< SysTick CTRL: ENABLE Mask */

 Delay_ms()函数介绍

void Delay_ms(uint32_t nms)
{
	uint32_t temp;
	SysTick->LOAD  =nms*fac_ms;   //设置加载的值,比如1us就要计数9次。nus传入1,CALIB=1*9=9,最后就是1us
	SysTick->VAL   =0x00;         //清空计数器中的值,LOAD里的值不是写入后就会加载,而是在systick使能且VAL值为0时才加载
	SysTick->CTRL  |=SysTick_CTRL_ENABLE_Msk;  //使能时钟,开始计时
	do
	{
		temp=SysTick->CTRL;   //查询是否计数完成
	}while((temp&0x01)&&!(temp&(1<<16)));   //先判断定时器是否在运行,再判断是否计数完成
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;      					 //清空计数器	 
}

Delay_ms()与Delay_us()操作步骤一样的,就只是把    SysTick->LOAD  =nus*fac_us; 改为    SysTick->LOAD  =nms*fac_ms; 。

最后再次强调!!!

为了更长的延时时间,我们选择了八分频。但是八分频之后,依旧只能最大延时1.864S!