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

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

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


本次实验利用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,那么现在可以计数,也就是最大延时0.233S。


(8)但是假如是进行了八分频,,最大延时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!


目录
相关文章
|
5月前
|
传感器
stm32f407探索者开发板(二十二)——通用定时器基本原理讲解
stm32f407探索者开发板(二十二)——通用定时器基本原理讲解
439 0
|
5月前
STM32CubeMX 定时器
STM32CubeMX 定时器
177 0
|
5月前
stm32f407探索者开发板(二十三)——定时器中断实验
stm32f407探索者开发板(二十三)——定时器中断实验
479 0
|
6月前
使用STM32F103标准库实现定时器控制LED点亮和关闭
通过这篇博客,我们学习了如何使用STM32F103标准库,通过定时器来控制LED的点亮和关闭。我们配置了定时器中断,并在中断处理函数中实现了LED状态的切换。这是一个基础且实用的例子,适合初学者了解STM32定时器和中断的使用。 希望这篇博客对你有所帮助。如果有任何问题或建议,欢迎在评论区留言。
465 2
|
5月前
stm32f407探索者开发板(十七)——串口寄存器库函数配置方法
stm32f407探索者开发板(十七)——串口寄存器库函数配置方法
756 0
|
7月前
|
传感器
STM32标准库ADC和DMA知识点总结-1
STM32标准库ADC和DMA知识点总结
|
6月前
|
IDE 开发工具
使用STM32F103标准库实现自定义键盘
通过本文,我们学习了如何使用STM32F103标准库实现一个简单的自定义键盘。我们首先初始化了GPIO引脚,然后实现了一个扫描函数来检测按键状态。这个项目不仅能够帮助我们理解STM32的GPIO配置和按键扫描原理,还可以作为进一步学习中断处理和低功耗设计的基础。希望本文对你有所帮助,祝你在嵌入式开发的道路上不断进步!
529 4
|
6月前
|
传感器
【经典案例】STM32F407使用HAL库配置I2C详解
STM32F407是一个强大的微控制器,广泛应用于嵌入式系统中。在许多应用中,我们需要使用I2C总线来与传感器、EEPROM、显示屏等外设进行通信。本文将详细介绍如何使用STM32 HAL库来配置和使用I2C接口。
762 2