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