1. 时钟
什么是时钟呢?
一个可以产生周期性信号的设备
什么是周期性信号?
1 ----- ----- -----0 ----- ----- -----
周期:一个上升沿到另一个上升沿,或者一个下降沿到另一个下降沿就是一个时钟周期
所以时钟信号就是周期性变化的信号
时钟是由电路产生的具有周期性的脉冲信号,相当于单片机的心脏,给单片机提供一个统一的信号,要想使用单片机的外设必须开启相应的时钟
关于时钟我们有两个比较重要的概念需要理解:
T:时钟周期,最小重复的信号单元的时间长度,基本单位为s(秒)F:时钟频率,1s内有多少个重复的信号单元(1s振动多少次),单位Hz
---> T * F == 1s
例子:
F = 200Hz 意味着 1s 振动 200次
---> 每次振动的时间是 1 / 200 s == 5ms
2. 为什么需要时钟
时钟最主要的作用是用来同步信号用的
什么是同步呢?
就比如我们的左腿和右腿实际上就需要进行同步,两腿需要协同工作,左腿迈一步然后右腿迈一步,依次重复,人就能正常前行。对应在我们的机器上也是一样的,机器的运行是由很多器件协同工作完成的,当一个器件完成分给它的工作时,理应通知别的器件它完成了,轮到你干活了,等你干完我再继续干。类似于左腿迈完步子了,要等右腿迈完左腿再迈。比如:在A和B进行数据的收发时,A发完数据后应该等待B去接收,等B接收完了之后A再继续发,这种就是同步通信
而我们的M4中大部分时序逻辑电路需要同步,那设备之间怎么实现同步?
这就需要用到我们的时钟信号了
设备是怎么根据时钟信号实现同步的呢?来看电路以及时序图:
如上电路图中假设A端和B端的时序变化如下:
理论上A&B端的电平变化应该如上图所示,输出端(A&B)的电平变换应该要与B端的一致,但是实际上却是下面这样:
为什么会有这样的现象呢?主要是因为A和B输入到&门时,输出需要经过一段的反应时间,虽然这个时间比较短。那么在B端电平变化后,不能立马去读取输出端的电平,应该要等待一段时间后再去读取,那么应该要等待多长时间?此时间段实际上是可以获取到的,我们只需要对电路进行如下改动:
电路修改后, C端的电平的变化如下:
很明显,C端的高电平时期就是&门处理数据的时间(记为de_t),这段时间是不正常的,正常情况下C端应该一直是低电平(当&门处理数据没有延时的时候),我们将C端时序中凸起的地方就称之为“毛刺”。“毛刺”时期是不正常的,我们应该要略过它,略过的意思是指当B的信号发生改变时我们不应该立马去读输出值,而是应该等待一段时间,等电路(如:&门)将数据处理完毕后,再去读取输出值此时才是准确的怎么略过?此时就需要用到我们的时钟信号了
当我们在电路中加上REG后,在REG内部的触发器(假设触发器上升沿触发)的作用下,D端的电平变化如图:
通过上面的时序图可以得知,D此时输出的是一条干净的,没有“毛刺”的信号,那么通过上面这种现象我们可以总结出电路中处理“毛刺”的解决方案:
- 输入信号只能在Clock低跳变(下降沿)时改变,在Clock为高电平时保持不变
- T > 2 * de_t (周期必须大于2倍时延)
3. 时钟信号是怎么产生的呢?
在自然界中有一些物体天生就会产生摆动(振动) ---> 石英晶体
如果想利用石英晶体规则地、周期性的产生方波信号,需要一些电路来保证:
晶振电路:频率一般会比较小,如:12M、8M...
但是石英晶振难以满足现代计算机的高频需要,如:CPU它的频率会很高
那么我们就有“分频 / 倍频”电路:
分频:把输入频率变小
倍频:把输入频率变大
比如:在32中接触的比较多的有PLL:锁相环电路
4. STM32F4xx 时钟树(时钟系统)
查看<STM32F4xx中文参考手册.pdf>第六章(复位和时钟控制)第二节(时钟)第107页可以得到M4的时钟树(见图<时钟树.png>)
时钟树中有几个关键名词:LSI: Low Speed Internal 内部低速时钟 (32 kHz RC振荡器)
LSE: Low Speed External 外部低速时钟 (32.768KHz)
HSI: High Speed Internal 内部高速时钟 (16 MHz RC振荡器)
HSE: High Speed External 外部高速时钟 (M4原理图 ---> Y4可知:8MHz)
内部时钟: 由内部集成的RC震荡电路产生
外部时钟: 由晶振产生通过M4原理图 ---> Y4可知:
HSE_VALUE = 8M(外部高速时钟8M,取决于所接晶振大小)
从HSE出发沿着线路往右边走,会来到SW选择器,选择器一共有三路输入,分别为HSE / HSI / PLLCLK(锁相环时钟),选择这三者之一作为系统时钟来使用,我们的统系时钟最高可以达到168Mhz。很明显HSE / HSI 提供不了168Mhz的频率,那么则由锁相环提供
而锁相环的输入是由HSI和HSE二选一之后进行M分频后得到的。在我们M4中我们选择的是HSE,也就是8M进行M分频后输入锁相环
即 SYSCLK = PLLCLK <--- 168M
= (HSE / M) * N / P
= (8Mhz / M ) * N / P
所以PLLCLK是由M和N以及P决定,实际上这三者的值可以在代码中指定:
M ---> 代码中用PLL_M表示 ---> 8分频(根据HSE而来,目的将HSE分频为1M)
N ---> 代码中用PLL_N表示 ---> 336倍频(336M)
P ---> 代码中用PLL_P表示 ---> 2分频
选择器选择HSE / HSI / PLLCLK三者之一作为SYSCLK系统时钟后,接着就可以来到AHB总线,可以通过多个预分频器配置AHB频率、高速APB(APB2)和低速APB(APB1)频率
不同的时钟总线上挂载着不同的 外设控制器
AHB BUS = PLLCLK / (AHB Prescaler) // AHB Prescaler AHB总线的预分频
APBx BUS = AHB BUS CLK / (APBx Prescaler) // APBx Prescaler APBx总线的预分频
AHB总线时钟最大值为168M;
低速APB(APB1)最大值为42M,高速APB(APB2)最大值为84M
来到APBx总线上后,我们的定时器就挂载在APBx总线上,那我们的定时器的时钟频率是多少呢?
通过时钟树我们可以得知定时器的时钟频率分为两种:
- if (APBx presc == 1) 的意思是如果APB预分频值为1,那么定时器时钟频率等于APB的时钟频率;
- else 的意思就是如果APB预分频器值不为1,那么定时器时钟频率等于两倍的APB的时钟频率
APB1 CLK = 42M = AHB BUS CLK / (APB1 Prescaler)
APB1 Prescaler = 168 / 42 = 4
则位于APB1总线上的定时器时钟频率为 42M*2 = 84M
APB2 CLK = 84M = AHB BUS CLK / (APB2 Prescaler)
APB2 Prescaler = 168 / 84 = 2
则位于APB2总线上的定时器时钟频率为 84M*2 = 168M
5. 修改固件库时钟相关代码
因为ST公司提供固件库的时候,不知道其他公司设计的板子会采用多少频率的HSE晶振,因此只提供了最大值配置
M4采用的是8M HSE,所以需要修改
1)修改 HSE_VALUE 为 8M
stm32f4xx.h --> 144行
2)修改 PLL 相关
PLL_M 8 (371行)
PLL_N 336 (不需要改)
PLL_P 2 (不需要改)
system_stm32f4xx.c
上述修改需根据硬件电路的设计而来!!!
6. 定时器
timer:定时器就是用来定时的器件
通过使用计数器计算达到的时钟脉冲周期个数,当计数的时钟周期个数达到设定值并计数溢出时可以产生定时器中断,从而使内核进入中断模式
在STM32上,一般来说,定时器由三部分组成:
时基单元、输入捕获单元、输出比较单元
1. 时基单元:Time Bese Unit
定时器的基本单元,所有定时器都具备的单元
时基单元 = 计数器 + 重载计数值寄存器 + 定时器预分频器 组成
时基单元工作原理:
将计数器设置为一个值按照一定的时钟频率递减到0,或按照一定的时钟频率从0递增到某个值,当计数器溢出后,可以产生一个溢出事件/中断以此来达到定时的功能
(依据于<STM32F4xx中文参考手册>第17章<基本定时器>中图188<基本定时器框图>):
组成时基单元的三个器件的作用:1)定时器预分频器(TIMx_PSC)Prescaler
用来将定时器的总线时钟进行分频,提供一个合适的频率,给计数器去计数。它是一个16位的寄存器,可设置的值为 0—65535,为了防止分母为0,最终得到的分频值 = 设置的值+1,即分频系数介于1到65536之间
计算器时钟频率 = Fin / (TIMx_PSC + 1)
2)重载计数值寄存器(TIMx_ARR)Auto Reload Register
用来设定计数值的最大N。如果设定自动重装载数值N为0,则计数器停止工作
具体是多少位的,需要参考手册,每个定时器都不一样(Tim1—Tim14,共14个定时器)
3)计数器(TIMx_CNT) Counter
按照预分频器得到的频率,从0递增到N,或者从N递减到0,并且可以在溢出后,产生定时器中断/事件
定时器能不能 递增、递减 需要参考手册,每个定时器都不一样
溢出的含义为:
如果为递增计数,当计数值达到N+1时产生溢出
如果为递减计数,当计数值达到-1时产生溢出
比如:如果为递增计数,从0开始在一定的时钟频率下开始加1,一直加,加到N+1时,此时完成计数,就会溢出,产生定时器中断/事件
那么我们如何知道计数器溢出产生中断时,到底花费了多少时间呢?
我们知道计数器每做一次运算(+1运算)是需要花费时间的,那么我们只需要将计数器每+1所花费的时间求出来t,那么产生中断的时间应该就为:
(N+1) * t // N+1指做了N+1次运算
// 从0递增到N或者从N递减到0是N次运算
// 产生溢出又是一次运算,所以一共是 N+1 次运算
计数器每+1花费的时间到底是多少?
计数器每+1所花费的时间是跟输入计数器的时钟频率有关系的
Vt = (N + 1) * Tcnt = (N + 1) * (TIMEx_PSC + 1) / Fin
举个例子:
假设TIMEx是位于APB1总线上,那么Fin = 84M hz
为了方便计算一般情况下 TIMEx_PSC 设置为83
则计数器时钟频率为 Fcnt = Fin / (TIMEx_PSC + 1) = 84M hz / 84 = 1M hz = 10000000 hz
此时计数器的时钟周期Tcnt = 1 / Fcnt = 1 / 1M s = 1us 1s = 1000ms = 1000000us
此时即意味着计数器每过1us加1
所以产生定时器中断的时间为 Vt = (N + 1)* Tcnt = (N + 1)us
假设 TIMx 是位于APB上,想设定 定时器 定时的时间为 50ms请问Timx_PSC 和 N值可以设定为多少? 答案不唯一
Timx_PSC:83999
N:49
2. 输入捕获单元
可以对一个或多个输入信号进行处理
有些定时器不具备输入捕获单元
具体可以捕捉多少个输入信号需要看你的定时器有几个输入通道 Channel
有什么用呢?
比如: 可以计算输入信号的频率
输入信号经过"输入捕获阶段"(数字滤波,多路复用,预分频,去噪等等)到信号检测,当检测到需要的信号状态(上升沿变化 / 下降沿变化)变化时,就会把定时器时基单元中的TIMx_CNT 计数器值,锁定到"输入捕获寄存器"中
这样就可以根据预先设定的定时器参数(时钟频率,N值等等),就可以计算出从开始捕获到锁定这个信号所花费的时间了(一个周期) ===> t s
那么进而我们就可以求出输入信号的频率 = 1/t Hz
3. 输出比较单元
可以输出一个或多个信号
有些定时器没有输出比较单元
具体可以输出多少个信号,就需要看你的定时器有几个通道 Channel
输出比较:
定时器可以向对应的GPIO引脚(复用功能)输出一个电平状态
并且可以根据"输出比较寄存器(TIMx_CCR)"中的值,来翻转输出的电平状态
比如:
TIMx_CCR > TIMx_CNT 向GPIO引脚输出一个低电平
TIMx_CCR <= TIMx_CNT 向GPIO引脚输出一个高电平
至于 TIMx_CCR > TIMx_CNT 是输出低电平还是高电平,由自己定
典型应用: 输出PWM波
注意:
输入捕获和输出比较共用一个寄存器
因此同一个定时器的输入捕获和输出比较功能 ---> 不能同时使用
7. STM32F4xx 定时器概述
1)SysTick
时钟滴答定时器 ===> 系统定时器
在所有M3/M4中都会内置于NVIC中一个SysTick定时器
这个定时器被称为系统定时器,只有时基单元
并且在溢出时,会产生SysTick中断,执行中断处理函数 SysTick_Handler为什么需要这样一个定时器呢?
因为大多数操作系统,都需要一个硬件定时器来产生操作系统需要的时钟嘀嗒中断,主要用于操作系统计时,比如时间片的产生SysTick定时器作为整个系统的基本的时间单元
不跑操作系统,这个定时器有用吗?
有,延时函数就可以用SysTick来实现
SysTick被集成在NVIC中,它可以产生SysTick中断,其实就是一个简单的 24bits(N值)递减定时器,它可以运行在系统的主频上 168M Hz,也可以运行在系统主频的8分频上 21M hz
SysTick定时器一般可以通过图示的寄存器来操作:(参考<Cortex M3与M4权威指南.pdf>第351页)
完成配置后,每过一段设置好的时间就会产生一次SysTick中断
a. Disable SysTick(禁用SysTick) 将Control and Status控制和状态寄存器中的bit0置为0 b. 选择时钟源 将Control and Status控制和状态寄存器中的bit2: 1:系统内核(系统上的主频) 168Mhz 0:系统主频上的8分频 21Mhz c. 使能中断 将Control and Status控制和状态寄存器中的bit1: 1:SysTick中断使能 0:SysTick中断禁止 d. 设定 N值(24位:0——16777215) 计数初值(24位,给 0 就行,因为每次都会将N值复制到这里进行递减) e. Enable SysTick(使能SysTick)完成配置后,每过一段设置好的时间就会产生一次SysTick中断
注意:
系统已经在 stm32f4xx_it.c 写好了一个中断函数,可以直接去修改里面的值,但是必须在 stm32f4xx_it.c 声明外部变量(extern int delay_time;)
如果想要写在自己的函数里面,就需要把系统里面的中断函数注释掉,此时就不需要再声明外部变量了
定时器每次+1,是不需要CPU执行的,如果使能中断,触发了中断,CPU就去执行中断
练习:利用SysTick实现ms级延时 需要将SysTick设置为每隔1ms产生一个中断 思路: 选择SysTick的时钟源为内核时钟:168M 计数值减1所需要的时间为:(1/168M)s 让SysTick产生中断的时间为: (N + 1) * (1/168M)s == 1ms ---> N = 167999 ------------------------------------------------------------------------ // systick.h #ifndef __SYSTICK_H__ #define __SYSTICK_H__ #include "stm32f4xx.h" // SysTick定时器中 Control and Status 控制和状态寄存器 #define rSysTickCTL *((volatile unsigned long *)0xE000E010) // SysTick定时器中 Reload Value 重载计数值寄存器 #define rSysTickRELVAL *((volatile unsigned long *)0xE000E014) // SysTick定时器中 Current Value 计数值寄存器 #define rSysTickCURVAL *((volatile unsigned long *)0xE000E018) /* 利用固件库控制SysTick定时器实现毫秒级别的延时函数 */ void delay_ms(unsigned int d); /* 利用固件库控制SysTick定时器实现微秒级别的延时函数 */ void delay_us(unsigned int d); // 不使用 中断 延时 // 注意:N值(24位:0——16777215),延时时长有限制 (16777215+1)/168000 = 99.86... void No_Interrupt_delay_ms(unsigned int d); #endif ------------------------------------------------------------------------- // systick.c #include "systick.h" static int delay_time; /* SysTick中断处理函数 */ void SysTick_Handler(void) { if (delay_time > 0) { delay_time--; } } /* 利用固件库控制SysTick定时器实现毫秒级别的延时函数 */ void delay_ms(unsigned int d) { // 1. 配置SysTick定时器 rSysTickCTL &= ~(1 << 0); // 禁止SysTick rSysTickCTL |= 1 << 2; // 选择内核时钟,168M hz rSysTickCTL |= 1 << 1; // 使能SysTick中断 rSysTickCURVAL = 0; // 当前计数值为0 rSysTickRELVAL = 168000 - 1; // N值,将你要计数的值-1赋值给N rSysTickCTL |= 1 << 0; // 使能SysTick // 2. 设置好后,此时SysTick每一毫秒产生一次中断 delay_time = d; // 3. 等 while (delay_time); rSysTickCTL &= ~(1 << 0); // 禁止SysTick,节省CPU } /* 利用固件库控制SysTick定时器实现微秒级别的延时函数 */ void delay_us(unsigned int d) { // 1. 配置SysTick定时器 rSysTickCTL &= ~(1 << 0); // 禁止SysTick rSysTickCTL |= 1 << 2; // 选择内核时钟,168M hz rSysTickCTL |= 1 << 1; // 使能SysTick中断 rSysTickCURVAL = 0; // 当前计数值为0 rSysTickRELVAL = 168 - 1; // N值,将你要计数的值-1赋值给N rSysTickCTL |= 1 << 0; // 使能SysTick // 2. 设置好后,此时SysTick每一毫秒产生一次中断 delay_time = d; // 3. 等 while (delay_time); rSysTickCTL &= ~(1 << 0); // 禁止SysTick,节省CPU } // 不使用 中断 延时 // 注意:N值(24位:0——16777215),延时时长有限制 (16777215+1)/168000 = 99.86... void No_Interrupt_delay_ms(unsigned int d) { // 1. 配置SysTick定时器 rSysTickCTL &= ~(1 << 0); // 禁止SysTick rSysTickCTL |= 1 << 2; // 选择内核时钟,168M hz rSysTickCTL &= ~(1 << 1); // 禁止SysTick中断 rSysTickCURVAL = 0; // 当前计数值为0 rSysTickRELVAL = 168000 * d - 1; // N值,将你要计数的值-1赋值给N rSysTickCTL |= 1 << 0; // 使能SysTick u32 temp; do { temp = rSysTickCTL; } while ((temp & 0x01) && !(temp & (1 << 16))); // 等待时间到达 rSysTickCTL &= ~(1 << 0); // 禁止SysTick,节省CPU }在STM32固件库中提供了一个配置 SysTick 定时器的函数:
uint32_t SysTick_Config(uint32_t ticks); 此函数默认采用内核时钟频率 bit2 -> 1 168M hz 使能中断 bit1 -> 1 参数ticks就是要计数的次数 = 赋值给自动加载寄存器的值 + 1 同时,在固件库中有一个全局变量 SystemCoreClock 表示系统内核时钟(168M) 如: SysTick_Config(168000); <===> SysTick_Config(SystemCoreClock / 1000); ---> 1ms产生一个中断 SysTick_Config(168); <===> SysTick_Config(SystemCoreClock / 1000000); ---> 1us产生一个中断 ticks * (1 / 168M)s = 1ms ===> ticks = 168M / 1000 配置为1ms产生一个中断: ticks = 内核时钟 / 1000 配置为1us产生一个中断: ticks = 内核时钟 / 1000000需要注意的是:
a. 固件库中 SysTick 中断处理函数已经定义了 ----> stm32f4xx_it.c 142行左右
所以如果自己想设计延时函数,请修改固件库自带的SysTick中断处理函数
方法一:找到固件库定义的SysTick_Handler函数,将函数内的代码替换成自己的
方法二:修改启动文件中,SysTick中断对应中断函数的函数名,以该函数名,重写中断处理函数
b. 最好将中断SysTick_IRQn(SysTick中断编号)的优先级配置为0
主要是为了能在其他中断中使用我们的 delay_ms / delay_us函数
我们需要把SysTick中断优先级配置成一个比较高的优先级
NVIC_SetPriority(SysTick_IRQn, 0);
原因:假设一个按键中断的优先级2,如果你的SysTick中断的优先级比2低,那么你在按键中断中调用mdelay就会死循环 ====> 死机
systick.h#ifndef __SYSTICK_H__ #define __SYSTICK_H__ #include "stm32f4xx.h" // SysTick定时器中 Control and Status 控制和状态寄存器 #define rSysTickCTL *((volatile unsigned long *)0xE000E010) // SysTick定时器中 Reload Value 重载计数值寄存器 #define rSysTickRELVAL *((volatile unsigned long *)0xE000E014) // SysTick定时器中 Current Value 计数值寄存器 #define rSysTickCURVAL *((volatile unsigned long *)0xE000E018) /* 利用固件库控制SysTick定时器实现毫秒级别的延时函数 */ void delay_ms(unsigned int d); /* 利用固件库控制SysTick定时器实现微秒级别的延时函数 */ void delay_us(unsigned int d); // 不使用 中断 延时 // 注意:N值(24位:0——16777215),延时时长有限制 (16777215+1)/168000 = 99.86... void No_Interrupt_delay_ms(unsigned int d); #endifsystick.c
#include "systick.h" static int delay_time; /* SysTick中断处理函数 */ void SysTick_Handler(void) { if (delay_time > 0) { delay_time--; } } /* 利用固件库控制SysTick定时器实现毫秒级别的延时函数 */ void delay_ms(unsigned int d) { // 1. 配置SysTick定时器 // rSysTickCTL &= ~(1 << 0); // 禁止SysTick // rSysTickCTL |= 1 << 2; // 选择内核时钟,168M hz // rSysTickCTL |= 1 << 1; // 使能SysTick中断 // rSysTickCURVAL = 0; // 当前计数值为0 // rSysTickRELVAL = 168000 - 1; // N值,将你要计数的值-1赋值给N // rSysTickCTL |= 1 << 0; // 使能SysTick // SystemCoreClock 内核时钟频率 SysTick_Config(SystemCoreClock / 1000); // 让SysTick中断优先级为0(最高级) NVIC_SetPriority(SysTick_IRQn, 0); // 2. 设置好后,此时SysTick每一毫秒产生一次中断 delay_time = d; // 3. 等 while (delay_time); rSysTickCTL &= ~(1 << 0); // 禁止SysTick,节省CPU } /* 利用固件库控制SysTick定时器实现微秒级别的延时函数 */ void delay_us(unsigned int d) { // 1. 配置SysTick定时器 // rSysTickCTL &= ~(1 << 0); // 禁止SysTick // rSysTickCTL |= 1 << 2; // 选择内核时钟,168M hz // rSysTickCTL |= 1 << 1; // 使能SysTick中断 // rSysTickCURVAL = 0; // 当前计数值为0 // rSysTickRELVAL = 168 - 1; // N值,将你要计数的值-1赋值给N // rSysTickCTL |= 1 << 0; // 使能SysTick // SystemCoreClock 内核时钟频率 SysTick_Config(SystemCoreClock / 1000000); // 让SysTick中断优先级为0(最高级) NVIC_SetPriority(SysTick_IRQn, 0); // 2. 设置好后,此时SysTick每一毫秒产生一次中断 delay_time = d; // 3. 等 while (delay_time); rSysTickCTL &= ~(1 << 0); // 禁止SysTick,节省CPU } // 不使用 中断 延时 // 注意:N值(24位:0——16777215),延时时长有限制 (16777215+1)/168000 = 99.86... void No_Interrupt_delay_ms(unsigned int d) { // 1. 配置SysTick定时器 rSysTickCTL &= ~(1 << 0); // 禁止SysTick rSysTickCTL |= 1 << 2; // 选择内核时钟,168M hz rSysTickCTL &= ~(1 << 1); // 禁止SysTick中断 rSysTickCURVAL = 0; // 当前计数值为0 rSysTickRELVAL = 168000 * d - 1; // N值,将你要计数的值-1赋值给N rSysTickCTL |= 1 << 0; // 使能SysTick u32 temp; do { temp = rSysTickCTL; } while ((temp & 0x01) && !(temp & (1 << 16))); // 等待时间到达 rSysTickCTL &= ~(1 << 0); // 禁止SysTick,节省CPU }2)基本定时器(TIM6,TIM7)
只有时基单元,没有输入捕获 / 输出比较单元(无输入输出引脚)
16位自动重载递增计数器 ===> 计数值上限65535,只能递增计数
用途:只用作定时
3)通用定时器(Tim2 ~ Tim5,Tim9 ~ Tim14)●TIM2 ~ TIM5
TIME3 / TIME4(16bit)、TIME2 / TIME5(32bit)计数器
计数模式可以由软件(代码)配置为:
递增,递减,先递增再递减
多达4个独立的通道(可以有4个GPIO引脚复用),可以软件配置为:
输入捕获、输出比较、PWM生成、单脉冲模式
● TIM9~TIM14
16bits计数器,只能递增计数
2个独立的通道,可以配置为如下功能:
输入捕获、输出比较、PWM生成、单脉冲模式
● 通用定时器的用途
a) 作为基本定时器使用(只用作定时器中断) ===> 同基本定时器
b) 捕获输入信号 ===> 配置 时基单元 + 输入捕获单元
c) 输出特定信号(PWM / 单脉冲) ===> 配置 时基单元 + 输出比较单元
4)高级定时器(TIM1,TIM8)16bits计数器,计数模式:递增、递减、先递增再递减
4个独立的通道,可以配置为如下功能:
输入捕获、输出比较、PWM生成、单脉冲模式
● 高级
重复计数器(TIMx_RCR):Repeation Counter Register
如果使用了重复计数,则当计数器重复溢出次数达到了设定的重复计数器的值+1后,才会产生定时器溢出中断/事件
如果不用重复计数器,在每次计数器溢出时都会产生事件/中断。这个时候,和通用定时器没有区别
......
5)看门狗 Watch Dog● 为什么要看门狗?
系统 / 程序可能存在一些致命问题,会导致系统"卡死"
看门狗的作用就是当系统卡死(跑飞了)之后,会产生复位中断(Reset_Handler)
跟你按复位键效果一样
但是看门狗只不过相当于忽略了卡死的现象,它并不能帮你解决卡死的问题
● 看门狗是怎么做到的呢?
看门狗的原理其实就是一个递减的定时器,当定时器的计数溢出后,就产生溢出中断,这个中断产生后,就会去执行RESET复位中断对应的中断服务函数
● 看门狗的实现
1. 初始化配置看门狗,比如设置初始值为N
配置好之后,就会从N开始减,当减到-1之后,就会CPU复位。所以在正常情况下(没有死机或者说程序没有跑飞的情况下),不能让看门狗减到-1。那么为了避免在正常情况下看门狗减到-1,需要“喂狗”
2. 周期性的”喂狗”(重置定时器计数值)
比如:
假如看门狗是 50ms 产生复位中断
你必须每隔 < 50ms "喂狗"一次,以避免它产生复位中断
伪代码: int main(void) { Init_Watch_Dog(50ms); // 配置50ms看门狗 ..... Wei_Dog(); // 50ms内必须喂狗让看门狗重新计时 ..... Wei_Dog(); // 50ms内必须喂狗 ..... Wei_Dog(); // 50ms内必须喂狗 }● 注意
问题:看门狗设置为50ms,用另外一个定时器设置为30ms,时间一到就去 "喂狗",这样的行为可以吗?
不可以,"喂狗"不能采用定时器中断来喂狗
因为定时器中断是无论CPU是否跑飞,都会在溢出后产生中断,用定时器喂狗,会导致看门狗失去效果
"喂狗"的正确操作是:每隔一段时间,就喂狗一次,每写一段代码,就加一行喂狗代码
8. STM32F4xx 固件库中定时器相关函数
(1) 配置定时器时基单元(Time Base Unit)
a) 使能定时器时钟总线(定时器都处于APB1或APB2上,具体需要自己查询手册) RCC_APB1PeriphClockCmd(); or RCC_APB2PeriphClockCmd(); b) 初始化定时器的时基单元 ---> stm32f4xx_tim.h TIM_TimeBaseInit:用来初始化配置定时器时基单元 void TIM_TimeBaseInit(TIM_TypeDef *TIMx, TIM_TimeBaseInitTypeDef *TIM_TimeBaseInitStruct); @TIMx:指定要初始化的定时器编号 如:TIM1,TIM2......Tim14 @TIM_TimeBaseInitStruct:指向定时器时基单元初始化信息结构体 typedef struct { uint16_t TIM_Prescaler; 指定定时器预分频值,是一个无符号的16bit的整数 范围为:【0——65535】 定时器计数频率:TIM_CLK = Fin(定时器的输入时钟频率) / (TIM_Prescaler + 1) 计数器每 +1/-1 就需要 1/TIM_CLK s if APBx prescaler == 1 则: Fin = APBx BUS clock else Fin = APBx Bus clock * 2 TIM13 在总线APB1上,APB1 prescaler == 4 APB1 BUS clock = 168M/4 = 42M 因此,Fin = 42M*2 = 84M uint16_t TIM_CounterMode; 指定定时器计数模式(递增,递减,先递增后递减 ....) 有些定时器的模式是固定的,必须按手册来,比如:TIM9~14只能是递增计数 TIM_CounterMode_Up 递增计数 TIM_CounterMode_Down 递减计数 中心对齐:计数器是先递增再递减 TIM_CounterMode_CenterAligned1 中心对齐计数模式1 只有在向下计数到最低点产生中断 TIM_CounterMode_CenterAligned2 中心对齐计数模式2 只有在向上计数到最高点产生中断 TIM_CounterMode_CenterAligned3 中心对齐计数模式3 在最高点和最低点都会产生中断 详情请见<STM32F4xx中文参考手册.pdf>332页<14.3.2 计数器模式> uint32_t TIM_Period; 指定自动加载寄存器的值===> N值,整数 【每个定时器占多少位是不一样的,具体占多少位需要根据定时器查找手册】 定时器定时时间 = (N + 1) * (1/TIM_CLK) uint16_t TIM_ClockDivision; 用于输入捕获功能 指定"输入捕获阶段"中数字滤波的采集频率分频值 TIM_CKD_DIV1 数字滤波采集频率 = 计数器频率TIM_cLK * 1 TIM_CKD_DIV2 数字滤波采集频率 = 计数器频率TIM_cLK * 2 TIM_CKD_DIV4 数字滤波采集频率 = 计数器频率TIM_cLK * 4 uint8_t TIM_RepetitionCounter; 重复计数器的值(计数器溢出重复次数),只有高级定时器TIM1和TIM8才有 计算器的溢出次数应该是重复计数器的值+1 才会产生中断 } TIM_TimeBaseInitTypeDef;(2) 定时器中断配置 ---> stm32f4xx_tim.h
当要使用定时器中断时,则需要配置由哪种方式触发定时器中断 ---> 溢出之后产生中断 --->更新Update TIM_ITConfig:用来指定触发定时器中断的方式 void TIM_ITConfig(TIM_TypeDef *TIMx, uint16_t TIM_IT, FunctionalState NewState); @TIMx:指定定时器编号,一共有十四个定时器 @TIM_IT:指定定时器中断方式 TIM_IT_Update 定时器更新中断(在计数器溢出时产生的中断) ... @NewState:使能或禁止中断 ENABLE 使能 DISABLE 禁止,就算计数器溢出也不会向NVIC报告中断(3) 初始化配置 NVIC 中断控制器 ---> NVIC 管理所有的中断 ---> misc.c
NVIC_Init:用来初始化配置NVIC中断控制器 void NVIC_Init(NVIC_InitTypeDef *NVIC_InitStruct) @NVIC_InitStruct: 指向配置信息结构体 typedef struct { uint8_t NVIC_IRQChannel; 指定中断输入通道(不可以位或多个) STM32固件库中,给每一个中断一个唯一的编号,并且这个编号是用枚举来实现的 所有中断输入通道都定义在 stm32f4xx.h 去这个文件查找你所需要的中断输入通道 如: 外部中断0 ---> EXTI0_IRQn 1 ---> EXTI1_TRQn 2 ---> EXTI2_IRQn 3 ---> EXTI3_IRQn 4 ---> EXTI4_IRQn 5-9 ---> EXTI9_5_IRQn 10-15 ---> EXTI15_10_IRQn 串口中断1 ---> USART1_IRQn ...... uint8_t NVIC_IRQChannelPreemptionPriority; 指定抢占优先级,数字越小优先级越高,根据所占 bits 给值 如果抢占优先级占 2 bits,则可以给的值范围为:0-3 如果抢占优先级占 3 bits,则可以给的值范围为:0-7 uint8_t NVIC_IRQChannelSubPriority; 指定子优先级(响应优先级),数字越小优先级越高,根据所占 bits 给值 如果子优先级占 2 bits,则可以给的值范围为:0-3 如果子占优先级占 3 bits,则可以给的值范围为:0-7 NVIC允许用户给每一个中断通道分配一个优先级,中断通道的优先级占4bits 优先级4bits分为两类: 抢占优先级占 x bits 子优先级占 4-x bits 抢占优先级: 当中断A产生并且在处理中断A时,如果此时产生了中断B, 并且中断B的抢占优先级高于中断A,那么系统会打断中断A的处理,转去处理中断B, B处理完后,再继续处理A,A处理完后,才回到CPU正常执行 响应优先级(子优先级): 当两个或两个以上中断同时产生时,CPU预先处理谁 如果中断A子优先级高于中断B,那么CPU先处理中断A x具体占多少,由用户调用函数配置 void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); @NVIC_PriorityGroup: 指定抢占优先级占多少bits 0~4 NVIC_PriorityGroup_0 抢占优先级0bits子优先级4bits NVIC_PriorityGroup_1 抢占优先级1bits子优先级3bits NVIC_PriorityGroup_2 抢占优先级2bits子优先级2bits NVIC_PriorityGroup_3 抢占优先级3bits子优先级1bits NVIC_PriorityGroup_4 抢占优先级4bits子优先级0bits 这个函数一般在main里面调用一次就可以了 (防止后面分配的优先级超过所占bits范围大小) 1bit ---> 0-1 2bits ---> 0-3 3bits ---> 0-7 FunctionalState NVIC_IRQChannelCmd; 使能/禁止相应中断通道 ENABLE DISABLE NVIC不会向CPU报告该中断 } NVIC_InitTypeDef; ---------------------------------------------------------------------------- 比如:配置KEY0中断使能 NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct);(4) 使能定时器
void TIM_Cmd(TIM_TypeDef *TIMx, FunctionalState NewState) @TIMx:指定定时器编号 @NewState:是否开启定时器 Enable 开启定时器 Disable 关闭定时器同时写一个中断处理函数:假设是TIM13的更新中断 定时器也有自己的检测标志位以及清除标志位的函数 // 获取中断状态标志 ITStatus TIM_GetITStatus(TIM_TypeDef *TIMx, uint16_t TIM_IT); // 清除中断状态标志 void TIM_ClearITPendingBit(TIM_TypeDef *TIMx, uint16_t TIM_IT); // 去 startup_stm32f40xx.s 查找中断函数名 void TIM8_UP_TIM13_IRQHandler(void) { if (TIM_GetITStatus(TIM13, TIM_IT_Update) == SET) { ....执行的代码 TIM_ClearITPendingBit(TIM13, TIM_IT_Update); } }所以定时器中断的实现步骤:
(1) 配置时基单元 a) 使能定时器时钟总线 b) 初始化定时器的时基单元 (2) 定时器中断使能 (3) VNIC 配置 同时写一个定时器中断处理函数就可以了 (4) 使能定时器练习:
利用 定时器13 定时 1s 去翻转 BEEP蜂鸣器 ===> 查资料可知,Tim13 只能递增计数,挂载在 APB1 总线上,16 位自动重载计数器
// tim.h #ifndef __TIM_H__ #define __TIM_H__ #include "stm32f4xx.h" // 定时器13的初始化 1s产生一次定时器中断 void Tim13_delay_s_Init(void); #endif// tim.c #include "tim.h" #include "beep.h" // 定时器13的初始化 1s产生一次定时器中断 void Tim13_delay_s_Init(void) { // (1) 配置时基单元 // a) 使能定时器时钟总线 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM13, ENABLE); // b) 初始化定时器的时基单元 TIM_TimeBaseInitTypeDef t; t.TIM_Prescaler = 8399; // 1/10000 t.TIM_CounterMode = TIM_CounterMode_Up; t.TIM_Period = 9999; // 10000 * 1/10000 = 1s TIM_TimeBaseInit(TIM13, &t); // (2) 定时器中断配置 TIM_ITConfig(TIM13, TIM_IT_Update, ENABLE); // (3) 初始化配置 NVIC 中断控制器 NVIC_InitTypeDef n; n.NVIC_IRQChannel = TIM8_UP_TIM13_IRQn; n.NVIC_IRQChannelPreemptionPriority = 2; n.NVIC_IRQChannelSubPriority = 2; n.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&n); // (4) 使能定时器 TIM_Cmd(TIM13, ENABLE); } void TIM8_UP_TIM13_IRQHandler(void) { if (TIM_GetITStatus(TIM13, TIM_IT_Update) == SET) { // 翻转BEEP BEEP_reversal_status(); TIM_ClearITPendingBit(TIM13, TIM_IT_Update); } }(5) 输出比较
不同的定时器有2~4个独立的通道,每个通道都是由GPIO复用功能而来
因此,如果需要输出比较(输入捕获),都需要事先配置通道对应的GPIO控制(AF模式)
输出比较的实现步骤:(1) 初始化GPIO控制器 需要根据原理图找到定时器的通道与引脚的对应关系 比如 PF8/TIM13_CH1 意味着PF8可以功能复用为TIM13的通道1 1.1 使能GPIO分组时钟 RCC_AHB1PeriphClockCmd(); 如:RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); 1.2 初始化GPIO GPIO_Init(); --->配置GPIO引脚的功能 AF: Alternate Function 复用功能 1.3 配置GPIO复用功能 GPIO_PinAFConfig void GPIO_PinAFConfig(GPIO_TypeDef *GPIOx, uint16_t GPIO_PinSource, uint8_t GPIO_AF); @GPIOx:指定具体的GPIO分组 @GPIO_PinSource:指定具体的GPIO引脚编号 @GPIO_AF:指定具体的复用功能 比如:PF8/TIM13_CH1 GPIO_PinAFConfig(GPIOF, GPIO_PinSource8, GPIO_AF_TIM13); (2) 配置定时器时基单元 2.1 使能定时器时钟 RCC_APB1PeriphClockCmd(); or RCC_APB2PeriphClockCmd(); 2.2 初始化定时器时基单元 TIM_TimeBaseInit(); (3) 配置输出比较器(各个通道是独立的) void TIM_OC1Init(TIM_TypeDef *TIMx, TIM_OCInitTypeDef *TIM_OCInitStruct) void TIM_OC2Init(TIM_TypeDef *TIMx, TIM_OCInitTypeDef *TIM_OCInitStruct) void TIM_OC3Init(TIM_TypeDef *TIMx, TIM_OCInitTypeDef *TIM_OCInitStruct) void TIM_OC4Init(TIM_TypeDef *TIMx, TIM_OCInitTypeDef *TIM_OCInitStruct) TIM_OCxInit:x=1,2,3,4 用来配置通道1,2,3,4的输出比较器 @TIMx:指定定时器编号,如: TIM1, TIM2, ... @TIM_OCInitStruct:指向输出比较器初始化信息结构体 typedef struct { 【uint16_t TIM_OCMode;】 // 指定输出通道模式,有以下几种: TIM_OCMode_Timing: 输出比较寄存器(TIMx_CCRn)与计数器(TIIMx_CNT)的比较不会影响输出 TIM_OCMode_Active: 当TIMx_CCRn == TIIMx_CNT 输出高电平 TIM_OCMode_Inactive: 当TIMx_CCRn == TIIMx_CNT 输出低电平 TIM_OCMode_Toggle: 当TIMx_CCRn == TIIMx_CNT 翻转输出电平(0->1 1->0) TIM_OCMode_PWM1: 当TIIMx_CNT < TIMx_CCRn 输出有效电平(根据极性来) 当TIIMx_CNT >=TIMx_CCRn 输出无效电平 TIM_OCMode_PWM2: 当TIIMx_CNT < TIMx_CCRn 输出无效电平(根据极性来) 当TIIMx_CNT >=TIMx_CCRn 输出有效电平 【uint16_t TIM_OutputState;】 // 指定输出信号状态(enable/disable) TIM_OutputState_Disable 不输出信号 TIM_OutputState_Enable 输出信号 uint16_t TIM_OutputNState; // 指定互补输出状态(针对高级定时器) TIM_OutputNState_Disable 不输出互补信号 TIM_OutputNState_Enable 输出互补信号 【uint32_t TIM_Pulse;】 // 指定比较寄存器的值TIMx_CCRn,整数 【uint16_t TIM_OCPolarity;】 // 指定输出极性 TIM_OCPolarity_High 高电平为有效电平 TIM_OCPolarity_Low 低电平为有效电平 uint16_t TIM_OCNPolarity; // 指定互补输出信号的极性(针对高级定时器) TIM_OCNPolarity_High TIM_OCNPolarity_Low uint16_t TIM_OCIdleState; // 指定输出引脚在idle(空闲)状态时的输出(针对高级定时器) TIM_OCIdleState_Set 引脚在空闲的时候为高电平 TIM_OCIdleState_Reset 引脚在空闲的时候为低电平 uint16_t TIM_OCNIdleState; // 指定互补输出引脚在idle(空闲)状态时的输出(针对高级定时器) TIM_OCNIdleState_Set 引脚在空闲的时候为高电平 TIM_OCNIdleState_Reset 引脚在空闲的时候为低电平 } TIM_OCInitTypeDef; (4) 使能定时器,开始运行 void TIM_Cmd(TIM_TypeDef *TIMx, FunctionalState NewState); ---------------------------------------------------------------------------- 辅助函数:可以随时把新的比较值CCR写进寄存器 void TIM_SetCompare1(TIM_TypeDef *TIMx, uint32_t Compare1); @TIMx:定时器的编号 @Compare1:新的比较器的值
9. 输出PWM波练习
(1) 使用定时器输出比较功能输出一个PWM方波,驱动蜂鸣器的声音时大时小(类似警车声)
想听到完整的现象:
1. 确保声音上升和下降完整时间不超过3s
2. 定时器的定时时间小于等于延时间隔时间
pwm.h
#ifndef __PWM_H__ #define __PWM_H__ #include "stm32f4xx.h" /* 配置Time13通道1输出PWM波至BEEP PF8/TIM13_CH1 @arr_value:自动加载寄存器的值 ===> N + 1 @ccr_value:比较寄存器的值 */ void Tim13_PWM_Init(int arr_value, int ccr_value); // 把新的比较值CCR写进寄存器 void Tim13_PWM_SetCompare1(TIM_TypeDef *TIMx, uint32_t Compare1); #endifpwm.c
#include "pwm.h" /* 配置Time13通道1输出PWM波至BEEP PF8/TIM13_CH1 @arr_value:自动加载寄存器的值 ===> N + 1 @ccr_value:比较寄存器的值 */ void Tim13_PWM_Init(int arr_value, int ccr_value) { // (1) 初始化GPIO控制器 // 1.1 使能GPIO分组时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); // 1.2 初始化GPIO GPIO_InitTypeDef g; g.GPIO_Pin = GPIO_Pin_8; // BEEP->PF8 g.GPIO_Mode = GPIO_Mode_AF; // 复用模式 g.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz g.GPIO_OType = GPIO_OType_PP; // 输出推挽 g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上拉也无下拉 GPIO_Init(GPIOF, &g); // 1.3 配置GPIO复用功能 GPIO_PinAFConfig(GPIOF, GPIO_PinSource8, GPIO_AF_TIM13); // (2) 配置定时器时基单元 // 2.1 使能定时器时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM13, ENABLE); // 2.2 初始化定时器时基单元 TIM_TimeBaseInitTypeDef t; // 指定定时器预分频值 t.TIM_Prescaler = 84 - 1; // 1/1000 000 t.TIM_CounterMode = TIM_CounterMode_Up; // 指定自动加载寄存器的值===> N值,整数 t.TIM_Period = arr_value - 1; TIM_TimeBaseInit(TIM13, &t); // (3) 配置输出比较器(各个通道是独立的) TIM_OCInitTypeDef TIM_OCInitStruct; // 当TIIMx_CNT < TIMx_CCRn 输出有效电平(根据极性来) TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 指定比较寄存器的值TIMx_CCRn,整数 TIM_OCInitStruct.TIM_Pulse = ccr_value; // 指定输出极性 高电平为有效电平 TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM13, &TIM_OCInitStruct); // (4) 使能定时器,开始运行 TIM_Cmd(TIM13, ENABLE); } // 把新的比较值CCR写进寄存器 void Tim13_PWM_SetCompare1(TIM_TypeDef *TIMx, uint32_t Compare1) { TIM_SetCompare1(TIMx, Compare1); }main.c
#include "stm32f4xx.h" #include "systick.h" #include "beep.h" #include "pwm.h" int main(void) { Tim13_PWM_Init(1000, 0); while (1) { for (int i = 0; i <= 999; i++) { Tim13_PWM_SetCompare1(TIM13, i); delay_ms(1); } for (int i = 0; i <= 999; i++) { Tim13_PWM_SetCompare1(TIM13, 999 - i); delay_ms(1); } } // u8 mode = 1; // 1代表上升模式 0代表下降模式 // u32 new_compare = 0; // while (1) { // delay_ms(1); // if (mode == 1) { // new_compare++; // if (new_compare == 999) { // mode = 0; // } // } else if (mode == 0) { // new_compare--; // if (new_compare == 0) { // mode = 1; // } // } // Tim13_PWM_SetCompare1(TIM13, new_compare); // } }(2) 呼吸灯














3803

被折叠的 条评论
为什么被折叠?



