重要的内容写在前面:
- 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
- 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
- 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
- 如有错漏欢迎指出。
视频链接:[6-1] TIM定时中断_哔哩哔哩_bilibili
一、STM32中的定时器
1、定时器TIM概述
(1)TIM(Timer)定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断。它由16位计数器、预分频器、自动重装寄存器的时基单元组成,在72MHz计数时钟下可以实现最大59.65s的定时。
(2)TIM不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。
(3)根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型。
类型 | 编号 | 总线 | 功能 |
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能(内部硬件在不受程序的控制下实现自动运行) |
(4)STM32F103C8T6提供的定时器资源:TIM1、TIM2、TIM3、TIM4。
2、三种定时器框图
(1)基本定时器框图:
①计数器、预分频器和自动重装载寄存器构成最基本的计数计时电路(时基单元)。
②来自RCC的TIMxCLK提供时钟(内部时钟),频率值一般都是系统的主频72MHz,所以通向时基单元的技术基准频率就是72MHz。
③预分频器可以对72MHz的计数时钟进行预分频,如果该寄存器写0就表示不分频(也叫一分频),这时输出频率等于输入频率(72MHz),写1就表示二分频,输出频率等于输入频率的二分之一(36MHz),写2就表示三分频,以此类推。预分频器共16位,所以最大值可以写65535。
④计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值+1,计数器也有16位,计数最大值为65535,计数超过最大值后计数器的值归零并产生中断。
⑤自动重装载寄存器也是16位,它存储的是用户写入的计数目标值(自动重装值),在定时器运行的过程中,计数器中的值不断自增,而自动重装值是固定的目标值,当计数器中的值等于自动重装值时,定时器产生中断信号,并且计数器中的值清零,同时开始下一次的计数计时。
⑥向上的折线箭头代表产生中断信号,对应的中断称为“更新中断”,计数值等于自动重装值时,更新中断会通往NVIC,这时需要配置好NVIC的定时器通道,那么定时器的更新中断就能够得到CPU的响应。
⑦向下的折线箭头代表产生一个事件,对应的事件称为“更新事件”,更新事件不会触发中断,但可以触发内部其它电路的工作。
⑧主模式触发DAC的功能:在使用DAC时可能需要用DAC会输出一段波形,这时需要每隔一段时间触发一次DAC,让它输出下一个电压点,其实这个功能可以由中断实现,但是会增大CPU的负担,为此定时器设计了一个主模式,使用主模式可以将定时器的更新事件映射到触发输出TRGO的位置,而TRGO直接连接DAC的触发转换引脚,这样就不需要通过中断触发DAC转换,实现硬件自动化。
(2)通用定时器框图:
①计数器、预分频器和自动重装载寄存器构成最基本的计数计时电路(时基单元),它们的工作流程和基本定时器基本一样。
②对于通用定时器和高级定时器而言,计数器的计数模式不止向上计数(从0自增到自动重装值)一种,还有向下计数模式和中央对齐计数模式。向下计数模式就是从自动重装值开始计数器的值向下自减,直到计数为0后计数器的值回到重装值,同时申请中断;中央对齐计数模式就是从0开始计数器的值向上自增,待达到自动重装值后申请中断,然后计数器的值开始向下自减,直到计数为0后再次申请中断,以此往复。(最常用的还是向上计数模式)
③时基单元上面的一部分是内外时钟源选择和主从触发模式的结构,对于基本定时器而言,定时只能选择内部时钟(也就是系统频率为72MHz),而通用定时器的时钟源不仅可以选择内部的72MHz时钟,还可以选择外部时钟。
[1]一个是来自TIMx_ETR引脚上的外部时钟,对于TIM2,TIM2_ETR引脚和PA0在一起复用(PA0的波形就是TIM2_ETR的时钟),可以在PA0上接一个外部方波时钟,然后配置内部的极性选择、边沿检测和预分频器电路,再配置输入滤波电路。经过滤波的信号有两个去向,第一个去向是ETRF,经过触发控制器的选择后进入时基单元;第二个去向是TRGI,占用触发输入通道经过触发控制器的选择后进入时基单元。
[2]第二个外部时钟是ETR下面的ITR信号,它是由来自其它4个定时器的时钟信号(由其它定时器的TRGO输出)经过4选1得到的。通过TRGO和ITR可以实现定时器的级联(主定时器产生一次更新事件,其TRGO输出一个时钟脉冲到从定时器的ITR,从定时器收到一个时钟脉冲,计数器值+1)。
[3]第三个外部时钟是ITR下面的TI1F_ED,TI1F_ED连接输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,通过这一路输入的时钟,上升沿和下降沿均有效。
[4]触发选择器还能选择TI1FP1和TI2FP2提供的时钟,其中TI1FP1就是CH1引脚的时钟,TI2FP2就是CH2引脚的时钟。
④触发控制器下的编码器接口可以读取正交编码器的输出波形,后续会有这方面的介绍。
⑤时基单元下面的部分包含两块电路(两块电路同时包含捕获/比较寄存器),右边一块是输出比较电路,总共有4个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形;左边一块是输入捕获电路,总共有四个通道,也分别对应CH1到CH4的引脚,可以用于测量输入方波的频率。对于同一个定时器,输入捕获和输出比较不能同时使用。
(3)高级定时器框图:
相比于通用定时器,高级定时器在申请中断的地方增加了一个重复次数计数器,它可以实现每隔几个计数周期才发生一次更新事件和更新中断(相当于对输出的更新信号又做了一次分频),对于高级定时器,最大定时时间可以很大很大。
3、定时中断基本结构
4、预分频器时序
(1)CK_PSC是输入预分频器的时钟信号。
(2)CNT_EN是计数器使能,高电平时计数器能正常运行,低电平时计数器停止。
(3)CK_CNT是计数器时钟(输入计数器的时钟,由预分频器输出),CNT_EN未使能时时,CK_CNT不会产生计数脉冲,CNT_EN使能后,时序图前半段预分频器的参数为1,计数器的时钟频率CK_CNT等于输入预分频器的时钟CK_PSC,时序图后半段预分频器的参数为2,计数器的时钟频率CK_CNT等于输入预分频器的时钟CK_PSC二分之一。
(4)在计数器时钟CK_CNT的驱动下,计数器寄存器的计数跟随CK_CNT的上升沿不断自增。(重装值为FC,当计数值达到重装值,且下一个CK_CNT上升沿来临时,计数值回到0,同时产生一个更新事件脉冲)
(5)预分频控制寄存器共用户读写使用,当用户改变预分频控制寄存器的值时,预分频器的参数不会马上改变,待一个计数周期结束后,缓冲寄存器(又称为影子寄存器)会将预分频控制寄存器的值读入,这时预分频器的参数才会真正被改变。
(6)预分频器的内部实际上是靠计数进行分频的,当参数为1时,预分频器不分频,当参数为2时,预分频计数器会进行计数,每2个计数输出一个计数脉冲给计数器,结果就是输出频率等于输入频率的二分之一。(计数器计数频率:CK_CNT = CK_PSC / (PSC + 1))
5、计数器时序
(1)CK_INT是内部时钟,频率为72MHz。
(2)CNT_EN是时钟使能,高电平时计数器时钟CK_CNT能正常运行,低电平时计数器时钟CK_CNT关闭。
(3)由于分频因子为2,所以CK_CNT的频率是CK_INT的二分之一。
(4)CK_CNT产生上升沿时,计数器寄存器的计数值+1,当增加到自动重装值(ARR)时发生溢出且下一个CK_CNT上升沿来临时,产生一个更新事件脉冲,同时将更新中断标志位UIF置为1(UIF置为1,会向CPU申请中断,中断响应后中断程序需要将该标志位清零),并且计数器中的值清零,同时开始下一次的计数计时。
(5)计数器溢出频率(中断产生频率):
CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
(6)计数器的ARR寄存器(自动加载寄存器)也设计了缓冲寄存器,是否使用该寄存器可以由用户自行选择:
①计数器无预装时序:
②计数器有预装时序:
6、RCC时钟树
(1)图中左边一部分是时钟的产生电路,右边一部分(从AHB预分频器开始)是时钟的分配电路。
(2)在时钟产生电路有4个震荡源,分别是内部的8MHz高速RC振荡器(8 MHz HSI RC)、外部的4-16MHz高速石英晶体振荡器(4-16 MHz HSE OSC,也就是晶振,一般接8MHz,负责提供系统时钟)、外部的32.768KHz低速晶振(LSE OSC 32.768 kHz,一般负责给RTC提供时钟)和内部的40kHz低速RC振荡器(LSI RC 40kHz,给看门狗提供时钟),其中前两个高速振荡器负责提供系统时钟(AHB、APB2、APB1的时钟都来源于这两个高速晶振)。
(3)在SystemInit函数中,ST是这样来配置时钟的,首先它会启动内部时钟,选择内部的8MHz高速RC振荡器提供系统时钟,暂时以内部8MHz的时钟运行,接着再启动外部时钟(4-16MHz高速石英晶体振荡器),配置外部时钟进入PLL锁相环进行倍频,8MHz倍频9倍得到72MHz的时钟,待锁相环输出稳定后,选择锁相环输出为系统时钟SYSCLK,这样就把系统时钟的频率由8MHz切换为72MHz。
(4)72MHz的系统时钟进入AHB总线,AHB总线有一个预分频器,在SystemInit中配置的分配系数为1,那么AHB的时钟频率就是72MHz。
①AHB时钟进入APB1总线,APB1总线的预分频器系数被配置为2,所以APB1总线的时钟频率为36MHz,不过APB1总线的预分频器后面除了直接通向APB1外设的线路外,还有一条通向定时器的线路,因为APB1总线的预分频器系数为2,所以这里时钟的频率倍频(系数不为1就倍频),那么传到定时器的时钟频率就是72MHz。
②AHB时钟进入APB2总线,APB2总线的预分频器系数被配置为1,所以APB2总线的时钟频率为72MHz,APB2总线的预分频器后面除了直接通向APB2外设的线路外,也有一条通向定时器的线路,因为APB2总线的预分频器系数为1,所以这里时钟的频率不倍频,那么传到定时器的时钟频率就是72MHz。
③无论是高级定时器、通用定时器还是基本定时器,它们的内部基准时钟频率都被保证是72MHz,前提是不要随便修改SystemInit中的默认配置。
④在时钟进入外设前都必须经过一个与门,这时时钟是否能进入外设就由使能端控制,是否使能时钟,由程序员在代码中决定(这是为了节能,不需要工作的外设自然就不需要时钟)。
7、定时器定时中断
(1)按照下图所示接好线路,并将使用OLED屏进行显示的工程文件夹作为模板复制一份使用。
(2)在项目的System组中添加Timer.h文件和Timer.c文件用于封装定时器模块的代码,因为定时器模块不涉及外部硬件,所以不建议放在Hardware(为了便于管理)。
①Timer.h文件:
#ifndef __Timer_H
#define __Timer_H
void Timer_Init(void);
#endif
②Timer.c文件:
#include "stm32f10x.h" // Device header
extern uint16_t Num; //声明外部变量,允许该文件使用/更改main.c中的Num
void Timer_Init(void)
{
//开启TIM2的RCC内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//选择时基单元的时钟源(对于定时中断,选择内部时钟源)
TIM_InternalClockConfig(TIM2);
//配置时基单元(预分频器参数、自动重装值、计数模式等)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//定时频率 = 72MHz / (PSC + 1) / (ARR + 1) = 1Hz(定时1秒)=> PSC = 7199,ARR = 9999
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_IT_Update);
//TIM_TimeBaseInit会将中断标志位置为1,这里需要清除一下
//否则定时器2初始化完成时会立马发生一次中断(导致计数会从1而不是0开始)
//配置输出中断控制,允许更新中断输出到NVIC
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//配置NVIC,在NVIC中打开定时器2中断的通道并分配优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void TIM2_IRQHandler(void) //中断函数可以写在main.c中
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断这个中断是不是由定时器2触发
{
Num++; //计数值+1
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志位
}
}
(3)定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,下面先介绍比较常用的几个(仅简单介绍,需要使用时转去看函数上方的注释即可)。
[1]TIM_DeInit函数:将指定定时器的配置重设为缺省配置。
[2]TIM_TimeBaseInit函数:时基单元初始化。
[3]TIM_TimeBaseStructInit函数:给结构体变量赋一个默认值。(有时候结构体中有非常多的参数,但是在使用结构体时并不一定需要配置好所有参数,为了防止未配置的参数引发不可预知的问题,可以使用该函数给结构体赋一个默认值)
[4]TIM_Cmd函数:使能计数器。
[5]TIM_ITConfig函数:使能中断输出信号
[6]TIM_InternalClockConfig函数:选择内部时钟
[7]TIM_ITRxExternalClockConfig函数:选择ITRx(级联其它定时器)的时钟。
[8]TIM_TIxExternalClockConfig函数:选择TIx捕获通道的时钟。
[9]TIM_ETRClockMode1Config函数:选择ETR通过外部时钟模式1输入的时钟。
[10]TIM_ETRClockMode2Config函数:选择ETR通过外部时钟模式2输入的时钟。
[11]TIM_ETRConfig函数:配置ETR引脚的预分频器、极性、滤波器相关的参数。
[12]TIM_PrescalerConfig函数:单独写预分频值。
[13]TIM_CounterModeConfig函数:改变计数器的计数模式。
[14]TIM_ARRPreloadConfig函数:自动重装器预装功能配置。
[15]TIM_SetCount函数:给计数器写入一个值。
[16]TIM_SetAutoreload函数:给自动重装器写入一个值。
[17]TIM_GetCounter函数:获取当前计数器的值。
[18]TIM_GetPrescaler函数:获取当前预分频器的值。
[19]TIM_GetFlagStatus函数:在主程序中获取标志位是否被置1。
[20]TIM_ClearFlag函数:在主程序中对标志位进行清除(置0)。
[21]TIM_GetITStatus函数:在中断函数中获取标志位是否被置1。
[22]TIM_ClearITPendingBit函数:在中断函数中对标志位进行清除(置0)。
(4)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,OLED屏显示的计数应该是从0开始逐秒+1。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
}
}
8、定时器外部时钟
(1)按照下图所示接好线路,并将定时器定时中断的工程文件夹作为模板复制一份使用。
(2)修改项目的System组中添加Timer.h文件和Timer.c文件。
①Timer.h文件:
#ifndef __Timer_H
#define __Timer_H
void Timer_Init(void);
uint16_t Timer_GetCount(void);
#endif
②Timer.c文件:
#include "stm32f10x.h" // Device header
extern uint16_t Num; //声明外部变量,允许该文件使用/更改main.c中的Num
void Timer_Init(void)
{
//开启TIM2的RCC内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//选择时基单元的时钟源(选择通过ETR的外部时钟源)
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);
//TIM_ExtTRGPSC_OFF:不需要分频
//TIM_ExtTRGPolarity_NonInverted:高电平/上升沿有效
//0x00:不使用滤波器(采样点个数为0)
//TIM2_ETR引脚和PA0在一起复用,需要配置GPIOA(PA0的波形就是输入TIM2的时钟)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元(预分频器参数、自动重装值、计数模式等)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; //ARR自动重装器的值(10个脉冲产生一次中断,计数值会归零)
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //PSC预分频器的值(不分频)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_IT_Update);
//TIM_TimeBaseInit会将中断标志位置为1,这里需要清除一下
//否则定时器2初始化完成时会立马发生一次中断(计数会从1而不是0开始)
//配置输出中断控制,允许更新中断输出到NVIC
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//配置NVIC,在NVIC中打开定时器2中断的通道并分配优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
uint16_t Timer_GetCount(void)
{
return TIM_GetCounter(TIM2); //返回定时器2计数器的值
}
void TIM2_IRQHandler(void) //中断函数可以写在main.c中
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断这个中断是不是由定时器2触发
{
Num++; //计数值+1
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志位
}
}
(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,按照主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
OLED_ShowString(2,1,"CNT:");
while(1)
{
OLED_ShowNum(1,5,Num,5); //TIM2每产生一次中断(遮挡传感器10次),Num值+1
OLED_ShowNum(2,5,Timer_GetCount(),5); //显示当前遮挡次数(达到10后会清零)
}
}
二、使用定时器输出PWM信号
1、定时器提供的相关硬件结构
(1)OC(Output Compare,输出比较)可以通过比较CNT(时基单元中的计数器)与CCR寄存器(捕获/比较寄存器)值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形,每个高级定时器和通用定时器都拥有4个输出比较通道,高级定时器的前3个通道额外拥有死区生成和互补输出的功能。
(2)PWM:PWM(Pulse Width Modulation)即脉冲宽度调制,在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速、开关电源等领域。
(3)通用定时器的输出部分:
①CNT是计数器的值(4个通道共用一个CNT,所以4个通道的频率相同),CCR1是第一路的捕获/比较寄存器的值,两个值进行比较,当CNT≥CCR1时,输出模式控制器收到一个信号,oc1ref输出高低电平(具体输出高电平还是低电平取决于输出比较模式,由OC1M选择)。
②ref有两个去向,第一个去向是前往主模式控制器,可以将ref映射到主模式的TRGO输出上;第二个去向是前往右侧的极性选择,给CC1P写0,ref信号的电平不翻转,给CC1P写1,ref信号的电平翻转。
③输出使能电路决定经过电平翻转器的电流能不能输出,最后达到OC1引脚(CH1通道的引脚)。
④PWM模式的参数计算:
[1]PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
[2]PWM占空比: Duty = CCR / (ARR + 1)
[3]PWM分辨率: Reso = 1 / (ARR + 1)(比如分辨率为1%,那么占空比的可调单位就为1%)
输出比较的模式 | 描述 |
冻结 | CNT=CCR时,REF保持为原状态 |
匹配时置有效电平 | CNT=CCR时,REF置有效电平(高电平) |
匹配时置无效电平 | CNT=CCR时,REF置无效电平(低电平) |
匹配时电平翻转 | CNT=CCR时,REF电平翻转 |
强制为无效电平 | CNT与CCR无效,REF强制为无效电平 |
强制为有效电平 | CNT与CCR无效,REF强制为有效电平 |
PWM模式1 | 向上计数:CNT<CCR时,REF置有效电平,CNT≥CCR时,REF置无效电平(如下图所示) 向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平 |
PWM模式2 | 向上计数:CNT<CCR时,REF置无效电平,CNT≥CCR时,REF置有效电平 向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平 |
(4)高级定时器前三个通道的输出部分:
2、LED呼吸灯
(1)理论上给LED灯一个模拟信号(如上面那个紫色虚线的波形),LED灯的亮度可以从低慢慢变高,再从高慢慢变低,但是这在单片机中难以实现(因为软件中只能写出数字信号),不过可以给LED灯一个占空比不断变化的方波脉冲(可能不是专业名词,能理解就行),因为有PWM调制,就相当于给了LED一个模拟信号量。
(2)按下图所示接好电路,并将使用OLED屏进行显示的工程文件夹作为模板复制一份使用。(本次LED灯的正极连接PA0,负极连接GND,使用的是高电平驱动方法)
(3)在项目的Hardware组中添加PWM.h文件和PWM.c文件用于PWM模式相关的代码。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere1(uint16_t Compare);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH1_ETR与PA0复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //需要选择复用推挽输出,具体可看GPIO的位结构图
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 99
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 719
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)(占空比越大LED灯越亮)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的值
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare); //修改CCR的值(修改占空比)
}
(4)定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,这里再介绍几个(仅简单介绍,需要使用时转去看函数上方的注释即可)。
[1]TIM_OCxInit函数:x取1~4,分别配置4个输出比较单元。
[2]TIM_ForcedOCxConfig函数:x取1~4,分别配置4个输出比较单元为强制输出模式(暂停输出波形且强制输出高电平或低电平)。
[3]TIM_OCxPreloadConfig函数:x取1~4,配置CCR寄存器的预装功能(影子寄存器)。
[4]TIM_OCxFastConfig函数:x取1~4,配置快速使能。
[5]TIM_ClearOCxRef函数:x取1~4,产生外部事件时清除REF信号。
[6]TIM_OCxPolarityConfig函数:x取1~4,设置输出比较单元的极性选择。
[7]TIM_OCxNPolarityConfig函数:x取1~3,设置输出比较单元的极性选择(高级定时器中互补通道的配置)。
[8]TIM_CCxCmd函数:单独修改输出使能参数。(x是函数名的一部分)
[9]TIM_CCxNCmd函数:单独修改输出使能参数。(x是函数名的一部分)
[10]TIM_SelectOCxM函数:选择输出比较模式。(x是函数名的一部分)
[11]TIM_SetComparex函数:x取1~4,单独修改CCR寄存器的值。
[12]TIM_CtrlPWMOutputs函数:在使用高级定时器输出PWM时需要调用这个函数使能主输出。
[13]TIM_OCStructInit函数:给结构体赋一个初始值。(有时候结构体中有非常多的参数,但是在使用结构体时并不一定需要配置好所有参数,为了防止未配置的参数引发不可预知的问题,可以使用该函数给结构体赋一个默认值)
(5)TIM2_CH1_ETR与PA0复用(TIM2的比较输出信号会来到PA0),在配置GPIOA时需要选择复用推挽输出而不能选择普通推挽输出,具体可看下图,PA0的信号来源是TIM2而不是输出数据寄存器(梯形代表选择)。
(6)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,可以看到LED灯有呼吸灯的现象。
(7)引脚重映射:
①首先必须打开AFIO的时钟(RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); )。
②如果想让PA15、PB3、PB4三个引脚(调试端口)当作普通GPIO口引脚使用,需要将JTAG复用解除,使用“GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);”语句即可。
③如果想重映射定时器或其它外设的复用引脚,比如将TIM2_CH1_ETR映射到PA15上,使用“GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);”语句即可。
④如果重映射的引脚正好是调试端口,那么需要将JTAG复用解除,然后进行映射。
“将TIM2_CH1_ETR映射到PA15”在手册上有体现:
3、驱动舵机
(1)舵机概述:
①舵机是一种根据输入PWM信号占空比来控制输出角度的装置。
②输入PWM信号要求:周期为20ms(频率为50Hz),高电平宽度为0.5ms~2.5ms。
(2)按下图所示接好电路,并将PWM驱动LED呼吸灯的工程文件夹作为模板复制一份使用。(这次使用的是TIM2的通道2,舵机的输入信号连接PA1)
(3)修改PWM.h文件和PWM.c文件。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere2(uint16_t Compare);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH2与PA1复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //需要选择复用推挽输出,具体可看GPIO的位结构图
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 19999
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 50Hz => PSC = 71
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)(ARR取值范围500~2500)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的初始值
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare); //修改CCR的值(修改占空比)
}
(4)在项目的Hardware组中添加Servo.h文件和Servo.c文件用于封装舵机模块的代码。
①Servo.h文件:
#ifndef __Servo_H
#define __Servo_H
void Servo_Init(void);
void Servo_SetAngle(float Angle);
#endif
②Servo.c文件:
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Servo_Init(void)
{
PWM_Init();
}
void Servo_SetAngle(float Angle)
{
PWM_SetCompere2(Angle /180 *2000 + 500);
//Angle为旋转角度,这里采取的角度范围是0°~180°
//当Angle为90°时,输入信号中的一个脉冲要有1.5ms处于高电平
//PWM_SetCompere2会将CCR的值修改为1500
//占空比Duty = CCR / (ARR + 1) = 1500 / 20000(也就是1.5ms/20ms)
}
(5)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,根据主函数中的注释进行调试。(按键模块的代码复用旧例的即可)
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Key.h"
#include "Servo.h"
uint8_t KeyNum = 0;
float Angle;
int main()
{
OLED_Init();
OLED_ShowString(1,1,"Angle:");
Servo_Init(); //初始化舵机(舵机包含PWM模块,PWM也会在其中初始化)
Key_Init();
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1) //按下按键1,舵机旋转30°
{
Angle += 30;
if(Angle > 180) //如果已经旋转到最大值,回到初始状态0°
Angle = 0;
}
Servo_SetAngle(Angle); //更改当前旋转角度
OLED_ShowNum(1,7,Angle,3);
}
}
4、驱动直流电机
(1)直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作,TB6612(左图)是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。
(2)当计数器的值低于比较值CCR时输出高电平、高于比较值CCR时输出低电平,通过更改比较值可以产生占空比不同的脉冲,以此达到调节直流电机转速的目的。(直流电机由方波信号驱动,高电平时直流电机工作,低电平时会由于惯性继续运动,不过运动速度逐渐下降)
(3)按下图所示接好电路,并将PWM驱动LED呼吸灯的工程文件夹作为模板复制一份使用。(这次使用的是TIM2的通道3,直流电机的输入信号连接PA2)
(4)修改PWM.h文件和PWM.c文件。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere3(uint16_t Compare);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH3与PA2复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //需要选择复用推挽输出,具体可看GPIO的位结构图
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 99
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 719
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)(占空比越大电机速度越快)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的值
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare); //修改CCR的值(修改占空比)
}
(5)在项目的Hardware组中添加Motor.h文件和Motor.c文件用于封装舵机模块的代码。
①Motor.h文件:
#ifndef __Motor_H
#define __Motor_H
void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
②Motor.c文件:
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Motor_Init(void)
{
PWM_Init();
//使能GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置端口模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; //电机控制模式输入端
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
void Motor_SetSpeed(int8_t Speed)
{
if(Speed >= 0) //正转
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompere3(Speed); //修改速度(修改占空比)
}
else //反转
{
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
PWM_SetCompere3(Speed); //修改速度(修改占空比)
}
}
(6)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,根据主函数中的注释进行调试。(按键模块的代码复用旧例的即可)
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Motor.h"
#include "Key.h"
uint8_t KeyNum;
int8_t Speed;
int main()
{
OLED_Init();
Motor_Init();
Key_Init();
OLED_ShowString(1,1,"Speed:");
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1) //按下按键1,电机转速+20
{
Speed += 20;
if(Speed > 100) //电机转速上限100(占空比最大100%)
Speed = -100; //切换为反向满速转动
}
Motor_SetSpeed(Speed); //修改当前转速
OLED_ShowNum(1,7,Speed,3);
}
}
三、使用定时器捕获输入信号
1、定时器提供的相关硬件结构
(1)IC(Input Capture)输入捕获:输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。
①每个高级定时器和通用定时器都拥有4个输入捕获通道。
②可配置为PWMI模式,同时测量频率和占空比。
③可配合主从触发模式,实现硬件全自动测量。
(2)频率测量:测频法适合测量高频信号,测周法适合测量低频信号。(N越大,误差越小)
(3)输入捕获通道:
①输入信号从TIMx_CH1进入定时器的输入通道1,首先经过滤波器过滤信号(CCMR1寄存器的ICF位可以控制滤波器的参数,参数越大,过滤效果越好),接着信号进入边沿检测器,边沿检测器可以选择上升沿出发或下降沿触发,当出现指定的电平跳变时边沿检测电路就会触发后续电路执行动作。
②边沿检测器的输出信号经过极性选择(CCER寄存器的CC1P位决定)后来到数据选择器,该选择器选择TIMx_CH1、TIMx_CH2和TRC中的一个信号(通道3和通道4也是这样的结构),这样做有两个目的:
[1]为了可以灵活切换后续捕获电路的输入。
[2]把一个引脚的输入同时映射到两个捕获单元。
③经过数据选择器(CCMR1寄存器的CC1S位决定)后信号进入(预)分频器(分频参数由CCMR1寄存器的ICPS位决定),经过分频后的触发信号可以触发捕获电路进行工作,每有一个触发信号,CNT的值就会向CCR转运一次(CNT记录两个上升沿的时间间隔,CNT由定时器内部时钟驱动,相当于计时器,每次捕获后需要将CNT清零,这一步可以使用主从触发模式完成),转运的同时会发生一个捕获事件,这个事件会在状态寄存器置标志位,同时也可以产生中断(捕获中断)。
④TI1FP1除了前往数据选择器,同时还可以触发从模式,从模式控制器可以自动完成CNT的清零。
(4)主从触发模式:
①主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其它外设。
②从模式可以接收其它外设或者自身外设的一些信号用于控制自身定时器的运行,需要选择指定的一个信号得到TRGI,TRGI触发从模式,从模式可以选择一项操作自动执行。(如果想让TI1FP1信号自动触发CNT清零,那么从模式的触发源就选择TI1FP1,从模式执行Reset)
2、输入捕获模式测频率
(1)输入捕获基本结构:两个上升沿的时间间隔正好为一个周期,CNT负责计时,每有一个上升沿,CNT的值就会向CCR1转运一次并且自动清零,CCR1接收到的就是测得的周期值。
(2)按照下图所示接好线路,并将PWM驱动LED呼吸灯的工程文件夹作为模板复制一份使用。(由定时器自己产生信号,再让另一个定时器捕获,进行测量)
(3)修改PWM.h文件和PWM.c文件。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere1(uint16_t Compare);
void PWM_SetPrescaler(uint16_t Prescaler);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH1_ETR与PA0复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //需要选择复用推挽输出,具体可看GPIO的位结构图
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 99
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 719
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的值
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare); //修改CCR的值(修改占空比)
}
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate); //修改PSC的值(修改频率)
//TIM_PSCReloadMode_Immediate:修改参数后立刻重装,忽视影子寄存器
}
(4)在项目的Hardware组中添加IC.h文件和IC.c文件用于封装输入捕获相关的代码。
①IC.h文件:
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
#endif
②IC.c文件:
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
//RCC开启时钟,将GPIO和TIM3的时钟打开
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//GPIO初始化,将GPIO配置成输入模式(TIM3_CH1与PA6复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR自动重装器的值(填最大值防止溢出)
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 71
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器的值(设置测周法标准频率为1MHz)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //直连通道输入(通道1输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//选择从模式触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
//选择从模式触发执行的操作
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
//开启定时器
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //读取CCR的值并用它计算TIM2产生信号的频率(单位Hz)
}
(5)定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,这里再介绍几个(仅简单介绍,需要使用时转去看函数上方的注释即可)。
[1]TIM_ICInit函数:使用结构体配置输入捕获单元。(4个通道共用一个函数)
[2]TIM_PWMIConfig函数:使用结构体配置两个通道的输入捕获单元。
[3]TIM_ICStructInit函数:给结构体赋一个初始值。(有时候结构体中有非常多的参数,但是在使用结构体时并不一定需要配置好所有参数,为了防止未配置的参数引发不可预知的问题,可以使用该函数给结构体赋一个默认值)
[4]TIM_SelectInputTrigger函数:选择从模式输入触发源TRGI。
[5]TIM_SelectOutputTrigger函数:选择主模式输出触发源。
[6]TIM_SelectSlaveMode函数:选择从模式触发后执行的操作。
[7]TIM_SetICxPrescaler函数:x取1~4,分别单独配置通道1~4的分频器。
[8]TIM_GetCapturex函数:x取1~4,分别读取4个通道的CCR寄存器。
(6)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,OLED屏应显示测得TIM2产生的信号频率为1000Hz。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main()
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
//TIM2比较输出信号的参数设置
PWM_SetPrescaler(720 - 1); //频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz
PWM_SetCompere1(50); //占空比Duty = CCR / (ARR + 1)
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
}
}
3、PWMI模式测频率和占空比
(1)PWMI基本结构:
①TL1FP1负责捕获周期(用于计算频率,频率=1/周期),两个上升沿的时间间隔正好为一个周期,CNT负责计时,每有一个上升沿,CNT的值就会向CCR1转运一次并且自动清零(将从模式触发源配置为TL1FP1,从模式执行Reset,也就是CNT清零操作)。
②TL1FP2负责捕获脉宽(用于计算占空比,占空比=脉宽/周期×100%),在一个周期开始时上升沿将CNT清零,CNT开始计时,当来到下降沿时,CNT的值就是脉宽,CNT的值向CCR2转运,但是不清零。
(2)按照下图所示接好线路,并将输入捕获模式测频率的工程文件夹作为模板复制一份使用。(由定时器自己产生信号,再让另一个定时器捕获,进行测量)
(3)修改IC.h文件和IC.c文件:
①IC.h文件:
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GerDuty(void);
#endif
②IC.c文件:
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
//RCC开启时钟,将GPIO和TIM3的时钟打开
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//GPIO初始化,将GPIO配置成输入模式(TIM3_CH1与PA6复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR自动重装器的值(填最大值防止溢出)
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 71
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器的值(设置测周法标准频率为1MHz)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //直连通道输入(通道1输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure); //仅支持通道1和通道2
/*配置输入捕获单元等效代码
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //直连通道输入(通道1输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //选择通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; //下降沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI; //交叉通道输入(通道2输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
*/
//选择从模式触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
//选择从模式触发执行的操作
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
//开启定时器
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //读取CCR的值并用它计算TIM2产生信号的频率(单位Hz)
}
uint32_t IC_GerDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1); //计算占空比
}
(4)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,OLED屏应显示测得TIM2产生的信号频率为1000Hz、占空比为50%。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main()
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
OLED_ShowString(2,1,"Duty:000%");
//TIM2比较输出信号的参数设置
PWM_SetPrescaler(720 - 1); //频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz
PWM_SetCompere1(50); //占空比Duty = CCR / (ARR + 1)
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
OLED_ShowNum(2,6,IC_GerDuty(),3);
}
}
4、编码器接口测速
(1)Encoder Interface 编码器接口:
①编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度。
②每个高级定时器和通用定时器都拥有1个编码器接口。
③两个输入引脚借用了输入捕获的通道1和通道2。
(2)编码器接口基本结构:
ARR中的值应为最大值65535,CNT从0开始计数,当编码器正向旋转时,CNT的值递增,当编码器反向旋转时,CNT的值递减,如果这时CNT的值为0,那么CNT再往下递减就会从65535开始递减,如果认为计数器中的值是以补码形式存储,那么这时读取CNT得到的恰好是-1,以此类推。
(3)计数方向与编码器信号的关系:
(4)两个实例:(在TI1和TI2上计数)
①TI1和TI2均不反相:
②TI1反相:
(5)编码器接口测速示例程序:
①按照下图所示接好线路,并将定时器定时中断的工程文件夹作为模板复制一份使用。(定时器的编码器一般给电机测速,本例用旋转编码器模拟电机旋转)
②项目的Hardware组中添加Encoder.h文件和Encoder.c文件用于封装旋转编码器的代码。
[1]Encoder.h文件:
#ifndef __Encoder_H
#define __Encoder_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
[2]Encoder.c文件:
#include "stm32f10x.h" // Device header
void Encoder_Init(void)
{
//开启GPIO和定时器的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//配置GPIO(TIM3通道1和通道2的输入引脚分别为PA6和PA7)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR自动重装器的值(填最大值)
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //不分频,编码器的时钟直接驱动计数器
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure); //给结构体中的所有参数赋一个默认值
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInit(TIM3, &TIM_ICInitStructure); //配置通道1
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInit(TIM3, &TIM_ICInitStructure); //配置通道2
//配置编码器接口模式
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//TIM_EncoderMode_TI12:在TI1和TI2上计数
//TIM_ICPolarity_Rising:通道不反向(第三个参数配置通道1,第四个参数配置通道2)
//启动定时器
TIM_Cmd(TIM3, ENABLE);
}
int16_t Encoder_Get(void)
{
int16_t Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0); //每返回一次速度值(在TIM2定时时间内旋转多少次)CNT就清零,为下次测速做准备
return Temp; //获取TIM3的计数器值(并将其中的二进制数强制转换为有符号数)
}
③定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,这里再介绍一个(仅简单介绍,需要使用时转去看函数上方的注释即可)。
TIM_EncoderInterfaceConfig函数:用于配置定时器编码器接口。
④在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,根据主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Encoder.h"
#include "Delay.h"
#include "Timer.h"
int16_t Speed;
int main()
{
OLED_Init();
Encoder_Init();
Timer_Init();
OLED_ShowString(1,1,"Speed:");
while(1)
{
OLED_ShowSignedNum(1,7,Speed,5); //更新显示当前旋转速度(有符号数)
//Delay_ms(1000); Delay会阻塞主程序运行,不建议使用
}
}
void TIM2_IRQHandler(void) //使用TIM2的更新中断读取当前旋转速度
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断这个中断是不是由定时器2触发
{
Speed = Encoder_Get(); //每隔1秒(TIM2的定时时间)读取一次旋转速度
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志位
}
}