STM32中的TIM定时器深度解析

6 TIM定时器

6.1 TIM简介

  • TIM(Timer)定时器;
  • 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断;
  • 这是定时器最基本的功能,就是定时触发中断;
  • 同时也可以看出,定时器就是一个计数器,当这个计数器的输入是一个准确可靠的基准时钟的时候,那定时器在对这个基准时钟进行计数的过程,实际上就是计时的过程;
  • 比如在STM32中,定时器的基准时钟一般都是主频72MHz。对72MHz计72个数,那就是1MHz,就是1us的时间;如果计72000个数,那就是1KHz,就是1ms的时间;
  • STM32的定时器拥有16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时;
  • 计数器就是用来执行计数定时的寄存器,每来一个时钟,计数器加一;
  • 预分频器可以对计数器的时钟进行分频,让这个计数更加灵活;
  • 自动重装寄存器就是计数的目标值,可以设置计多少个时钟就进行中断;
  • 以上三者构成了定时器最核心的部分,这部分称为时基单元;
  • 时基单元里的计数器、预分频器和自动重装寄存器都是16位的,216等于65536,也就是预分频器设置最大,自动重装也设置最大,那定时器的最大定时时间就是59.65s;
  • 计算由来:72M/65536/65536,得到中断频率,再取倒数即可;
  • 如果觉得定时时间短,STM32的定时器还支持级联模式,也就是一个定时器的输出,当做另一个定时器的输入,加在一起,最大定时时间就是59.65s再乘两次65536,大概是八千多年;如果觉得这个定时时间短,还可以再级联一个定时器,那么定时器的时间就再延长65536×65536倍;
  • 不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能;
  • 根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型。
  • 6.2 定时器类型

    类型 编号 总线 功能
    高级定时器 TIM1、TIM8 APB2 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能
    通用定时器 TIM2、TIM3、TIM4、TIM5 APB1 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能
    基本定时器 TIM6、TIM7 APB1 拥有定时中断、主模式触发DAC的功能
  • STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4;
  • 基本定时器:
  • 预分频器、计数器、自动重装载寄存器构成了最基本的计数计时电路,称作时基单元;
  • 预分频器之前,连接的是基准计数时钟的输入。由于基本定时器只能选择内部时钟,所以预分频器左边那根线就直接连接到了输入端处,也就是内部时钟CK_INT;
  • 内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72M;
  • 预分频器可以对72MHz的计数时钟进行预分频。比如这个寄存器写0,就是不分频,或者说是1分频,此时输出频率=输入频率=72MHz;如果预分频器写1,就是2分频,输出频率=输入频率/2=36MHz;如果写2,就是3分频,输出=输入/3,以此类推。预分频器的值与实际的分频系数相差1,即实际分频系数=预分频器的值+1。这个预分频器是16位的,所以最大值可以写65535,也就是65536分频。预分频器的作用就是对输入的基准频率提前进行一个分频的操作;
  • 计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加一。这个计数器也是16位的,所以里面的值可以从0一直加到65535。如果再加的话,计数器就会回到0重新开始,所以计数器的值在计数的过程中会不断地自增运行,当自增运行到目标值时,产生中断,就完成了定时任务,所以还需要一个存储目标值的寄存器,就是自动重装载寄存器;
  • 自动重装载寄存器也是16位的,其存储的就是我们写入的计数目标。在运行过程中,计数值不断自增,自动重装值是固定的目标。当计数值等于自动重装值时,就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时;
  • 向上的折线箭头并有一个字母UI的,表示此处会产生中断信号;
  • 像这种计数值等于自动重装值产生的中断,称作“更新中断”。这个更新中断之后就会通往NVIC,再配置好NVIC的定时器通道,那定时器的更新中断就可以得到CPU的响应了;
  • 向下的折线箭头并有一个字母U的,表示此处会产生一个事件,对应的事件称作“更新事件”。更新事件不会触发中断,但可以触发内部其它电路的工作;
  • 定时中断的过程:从基准时间,到预分频器,再到计数器,计数器计数自增,同时不断地与自动重装载寄存器进行比较,它两值相等时,即计时时间到,这是就会产生一个更新中断和更新事件。CPU响应更新中断,就完成了定时中断的任务;
  • 主模式触发DAC的功能:
  • STM32定时器的一大特色,就是主从触发模式,可以让内部的硬件在不受程序控制的情况下,实现自动运行;
  • 当我们在使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点;
  • 如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间,在中断程序中调用代码手动触发一次DAC转换,然后DAC输出。但是这样会使主程序处于频繁被中断的状态,这会影响主程序的运行,和其它中断的响应;
  • 所以定时器就设置了一个主模式,使用这个主模式可以把这个定时器的更新事件映射到触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上。这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接去触发DAC了。整个过程不需要软件的参与,实现了硬件的自动化,这就是主模式的作用;
  • 通用定时器:
  • 基本定时器支持的是向上计数模式。通用定时器和高级定时器不仅支持向上计数模式,还支持向下计数模式和中央对齐模式;
  • 向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后继续下一轮,依次循环;
  • 中央对齐计数模式就是从0开始,先向上自增,计到重装值,申请中断,再向下自减,减到0,再申请中断,然后继续下一轮,依次循环;
  • 基本定时器,定时只能选择内部时钟,也就是系统频率72MHz。通用定时器,时钟源不仅可以选择内部的72MHz时钟,还可以选择外部时钟,具体有以下两个:
  • TIMx_ETR引脚上的外部时钟。假设在TIM2的ETR引脚,也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这些电路可以对外部时钟进行一定的整型。滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,就可以选择作为时基单元的时钟了(如果想在ETR外部引脚提供时钟,或者对ETR时钟进行计数,把这个定时器当做计数器来用的话,那么就可以配置这一路的电路,在STM32中,这一路也叫做“外部时钟模式2”);另一路可以通过下面的TRGI当做时钟。两种情况对于时钟输入而言是等价的,只不过下面一路输入会占用触发输入的通道;
  • TRGI(Trigger In)。从名字上来看的话,其主要作用是用作触发输入来使用的,这个触发输入可以触发定时器的从模式。当这个TRGI当做外部时钟来使用的时候,这一路叫做“外部时钟模式1”,以下是该模式的时钟输入来源:
  • ITR信号,这一部分的时钟信号是来自其它定时器的。从右边可以看出,主模式的输出TRGO可以通向其它定时器,此时就可以接到其它定时器的ITR引脚上。ITE0~ITR3分别来自其它四个定时器的TRGO输出,通过这一路就可以实现定时器级联的功能;
  • 比如可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上。再初始化TIM2,选择TIR2,连接TIM3的TRGO,再选择外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联;
  • TI1F_ED,连接的是输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟。此处后缀加一个ED(Edge,边沿),也就是通过这一路输入的时钟,上升沿和下降沿均有效;
  • 最后,这个时钟还可以通过TI1FP1和TI2FP2获得,TI1FP1连接到了CH1引脚的时钟,TI1FP2连接到了CH2引脚的时钟;
  • 编码器接口:可以读取正交编码器的输出波形;
  • 输入部分:
  • 有两个输入端,分别接到编码器的A相和B相,左边是两个网络标号,分别写的是TI1FP1和TI2FP2,对应的就是下面的TI1FP1和TI2FP2。可以看出,编码器的两个引脚,借用了输入捕获单元的前两个通道,所以最终编码器的输入引脚,就是定时器的CH1和CH2这两个引脚,CH3和CH4与编码器接口无关;
  • CH1和CH2的输入滤波器和边沿检测器,编码器接口也有使用,但是后面的是否交叉、预分频器和CCR寄存器,与编码器接口无关;
  • 输出部分:
  • 相当于从模式控制器,去控制CNT的计数时钟和计数方向;
  • 执行流程:如果出现了边沿信号,并且对应另一相的状态为正转,则控制CNT自增,否则控制CNT自减;
  • 注意:此处不会使用72MHz内部时钟,和在时基单元初始化时设置的计数方向,并不会使用。因为此时计数时钟和计数方向都处于编码器接口托管的状态。计数器的自增和自减,受编码器控制;
  • 左下部分是输入捕获的各部分电路:
  • 从左到右,最左边是四个通道的引脚(参考引脚定义表,就可以知道这些引脚是复用在了哪些位置);
  • 往左是一个三输入的异或门,这个异或门的输入接在了通道1、2、3端口。异或门的执行逻辑是,当三个输入引脚的电平有任何一个发生翻转时,输出引脚就产生一次电平翻转。之后通过输出数据选择器,到达输入捕获通道1。数据选择器选择上面那一个,那输入捕获通道1的输入,就是3个引脚的异或值;若选择下面那个,那异或门就不起作用,4个通道各用各的引脚;
  • 设计这个异或门,其实还是为三相无刷电机服务的。无刷电机有3个霍尔传感器检测转子的位置,可以根据转子的位置进行换向。有了这个异或门,就可以在前三个通道接上无刷电机的霍尔传感器,然后这个定时器就作为无刷电机的接口定时器,去驱动换向电路工作;
  • 输入信号往左,到达输入滤波器和边沿检测器,输入滤波器可以对信号进行滤波,避免一些高频的毛刺信号误触发。边沿检测器,可以选择高电平触发或者低电平触发,当出现指定的电平时,边沿检测电路就会触发后续电路执行动作;
  • 此处其实是设计了两套滤波和边沿检测电路。第一套电路,经过滤波和极性选择,得到TI1FP1(TI1 Filter Polarity 1),输入给通道1的后续电路;第二套电路,经过另一个滤波和极性选择,得到TI1FP2(TI1 Filter Polarity 2),输入给下面通道2的后续电路。同理,下面TI2信号进来,也经过两套滤波和极性选择,得到TI2FP1和TI2FP2,其中TI2FP1输入给上面,TI2FP2输入给下面。在这部分电路中,两个信号经过输入滤波器和边沿检测器进来后,可以选择各走各的,也可以进行一个交叉,让CH2引脚输入给通道1,或者CH1引脚输入给通道2;
  • 为什么此处要进行一个交叉连接呢?
  • 目的1:可以灵活切换后续捕获电路的输入,比如一会想以CH1作为输入,一会想以CH2作为输入,这样就可以通过这个数据选择器,灵活地进行选择;
  • 目的2:可以把一个引脚的输入,同时映射到两个捕获单元,这也是PWMI的经典结构。第一个捕获通道,使用上升沿触发,用来捕获周期;第二个通道,使用下降沿触发,用来捕获占空比。两个通道同时对一个引脚进行捕获,就可以同时测量频率和占空比,这就是PWMI模式;
  • 下面的通道3和通道4也是一样的结构,可以选择各自独立连接,也可以选择进行交叉;
  • TRC信号,也可以选择作为捕获部分的输入,其是来源于该图上面的TRC信号,这样设计也是为了无刷电机的驱动;
  • 预分频器,每个通达各有一个,可以选择对前面的信号进行分频。分频之后的触发信号,就可以触发捕获电路进行工作了。每来一个触发信号,CNT的值,就会向CCR转运一次。转运的同时,就会发生一个捕获事件,这个事件会在状态寄存器置标志位,同时也可以产生中断。如果需要在捕获的瞬间,处理一些事情的话,就可以开启这个捕获中断;
  • 整个电路的工作流程:假设配置一个上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为这个CNT计数器是由内部的标准时钟驱动的,所以CNT的数值,其实就可以用来记录两个上升沿之间的时间间隔。这个时间间隔,就是周期,再取个倒数,就是测周法测量的频率了;
  • 高级定时器:
  • 在申请中断的地方,多了一个重复次数计数器,有了这个计数器后,就可以实现每隔几个计数周期,才发生一次更新中断和更新事件。
  • 6.3 定时中断基本结构

    6.4 预分频器时序

  • 计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
  • 6.5 计数器时序

  • 计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
  • 6.6 计数器无预装时序

    6.7 计数器有预装时序

    6.8 RCC时钟树

  • 整张图可以分为左右两部分,左边的是时钟的产生电路,右边的是时钟的分配电路;
  • 中间的SYSCLK,就是系统时钟72MHz;
  • 在时钟产生电路中,有4个震荡源,分别是内部的8MHz高速RC振荡器,外部的4~16MHz高速石英晶体振荡器(晶振,一般接8MHz),外部的36.768KHz低速晶振(一般给RTC提供时钟用的),内部的40KHz低速RC振荡器(给看门狗提供时钟);
  • 高速晶振,是用来提供系统时钟的;
  • 在SystemInit函数中,ST是这样子配置时钟的:
  • 先启动内部时钟,选择内部8MHz为系统时钟,暂时以内部8MHz时钟运行;
  • 再启动外部时钟,进入PLL锁相环进行倍频,8MHz倍频9倍,得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由8MHz切换为了72MHz;
  • 如果外部时钟出现问题,系统时钟就无法切换到72MHz,那它就会以内部的8MHz运行,8M相对于72M,大概慢了10倍;
  • CSS(Clock Security System),时钟安全系统,负责切换时钟,可以检测外部时钟的运行状态。一旦外部时钟失效,就会自动把外部时钟切换为内部时钟,保证系统时钟的运行,防止程序卡死;
  • 时钟分配线路中,系统时钟72MHz进入AHB总线,AHB总线有一个预分频器,在SystemInit里配置的分频系数为1,那AHB的时钟就是72MHz;
  • 进入APB1总线,这里配置的分频系数是2,所以APB1总线的时钟为72MHz/2=36MHz;
  • 通用定时器和基本定时器都是接在APB1上的,而APB1的时钟是36MHz,那么这些定时器应该都是36MHz才对,但实际上都是72MHz;
  • 原因:下面还有一条支路,即“如果APB1预分频系数=1,则频率不变,否则频率×2”,然后连接到右边,可以发现这一路是单独为定时器2~7开通的。因为此处的预分频系数是2,所以此处的频率要×2,所以通向定时器2~7的时钟,又变成了72MHz;
  • 进入APB2总线,给的分频系数为1,所以APB2的时钟和AHB一样,都是72MHz。下面也有一条支路,即“如果APB2预分频系数=1,则频率不变,否则频率×2”,此处给的分频系数就是1,所以频率不变;
  • 在时钟输出的地方,都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是在程序中写RCC_APB2/1PeriphClockCmd作用的地方。打开时钟,就是在这个位置写1,让左边的时钟可以通过与门输出给外设。
  • 6.9 定时器定时中断

  • 项目目录结构:
  • Timer.h
    #ifndef __TIMER_H
    #define __TIMER_H
    
    void Timer_Init(void);
    
    #endif
    
  • Timer.c
    #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:定时中断初始化
      * 参    数:无
      * 返 回 值:无
      */
    void Timer_Init(void)
    {
        //将 6.3 定时中断基本结构 中的线路打通即可
        
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
    	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;		//时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
    	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;	//计数器模式,选择向上计数
        //参考 6.5 计数器时序 中的公式:定时频率 = 72M/(PSC+1)/(ARR+1)
        //比如:定时1s,就是定时频率为1Hz
        //此处还要减一是因为:预分频器和计数器都有1个数的偏差,所以此处还要减一
        //注意ARR和PSC的取值,都要在65525之间,不能超范围了
        //ARR和PSC的取值不是唯一的,可以预分频(PSC)给少点,自动重装(ARR)给多点,这就是以一个比较高的频率计较多的数;反之就是以一个比较低的频率计较少的数,两种方法都可以达到目标的定时时间
        //此处是对72M进行7200分频,得到的就是10K的计数频率,在10K的频率下,计一万个数,就是1s的时间
    	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_TimeBaseInit,配置TIM2的时基单元	
    	
    	/*中断输出配置*/
    	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
    	//TIM_TimeBaseInit函数末尾,手动产生了更新事件
    	//若不清除此标志位,则开启中断后,会立刻进入一次中断,即一上电或者复位就会进入中断
    	//如果不介意此问题,则不清除此标志位也可
    	
    	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
    	
    	/*NVIC中断分组*/
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
    	//即抢占优先级范围:0~3,响应优先级范围:0~3
    	//此分组配置在整个工程中仅需调用一次
    	//若有多个中断,可以把此代码放在main函数内,while循环之前
    	//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
    	
    	/*NVIC配置*/
    	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
    	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
    	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
    }
    
    /* 定时器中断函数,可以复制到使用它的地方
    void TIM2_IRQHandler(void)
    {
    	//第二个参数表示更新中断
    	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    	{
    		//清除标志位
    		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    	}
    }
    */
    
  • main.c
    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "Timer.h"
    
    uint16_t Num;			//定义在定时器中断里自增的变量
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	Timer_Init();		//定时中断初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Num:");			//1行1列显示字符串Num:
    	
    	while (1)
    	{
    		OLED_ShowNum(1, 5, Num, 5);			//不断刷新显示Num变量
    	}
    }
    
    /**
      * 函    数:TIM2中断函数
      * 参    数:无
      * 返 回 值:无
      * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
      *           函数名为预留的指定名称,可以从启动文件复制
      *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
      */
    void TIM2_IRQHandler(void)
    {
    	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)		//判断是否是TIM2的更新事件触发的中断
    	{
    		Num ++;												//Num变量自增,用于测试定时中断
    		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);			//清除TIM2更新事件的中断标志位
    		//中断标志位必须清除,否则中断将连续不断地触发,导致主程序卡死
    	}
    }
    
  • 6.10 定时器外部时钟

  • 项目目录结构:
  • Timer.h
    #ifndef __TIMER_H
    #define __TIMER_H
    
    void Timer_Init(void);
    uint16_t Timer_GetCounter(void);
    
    #endif
    
  • Timer.c
    #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:定时中断初始化
      * 参    数:无
      * 返 回 值:无
      * 注意事项:此函数配置为外部时钟,定时器相当于计数器
      */
    void Timer_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	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);						//将PA0引脚初始化为上拉输入
    	
    	/*外部时钟配置*/
    	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
    	//选择外部时钟模式2,时钟从TIM_ETR引脚输入
    	//注意TIM2的ETR引脚固定为PA0,无法随意更改
    	//最后一个滤波器参数加到最大0x0F,可滤除时钟信号抖动
    	
    	/*时基单元初始化*/
    	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的值
    	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;				//预分频器,即PSC的值
    	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;			//重复计数器,高级定时器才会用到
    	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);				//将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元	
    	
    	/*中断输出配置*/
    	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
    	//TIM_TimeBaseInit函数末尾,手动产生了更新事件
    	//若不清除此标志位,则开启中断后,会立刻进入一次中断
    	//如果不介意此问题,则不清除此标志位也可
    																
    	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
    	
    	/*NVIC中断分组*/
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
    	//即抢占优先级范围:0~3,响应优先级范围:0~3
    	//此分组配置在整个工程中仅需调用一次
    	//若有多个中断,可以把此代码放在main函数内,while循环之前
    	//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
    	
    	/*NVIC配置*/
    	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
    	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
    	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
    }
    
    /**
      * 函    数:返回定时器CNT的值
      * 参    数:无
      * 返 回 值:定时器CNT的值,范围:0~65535
      */
    uint16_t Timer_GetCounter(void)
    {
    	return TIM_GetCounter(TIM2);	//返回定时器TIM2的CNT
    }
    
    /* 定时器中断函数,可以复制到使用它的地方
    void TIM2_IRQHandler(void)
    {
    	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    	{
    		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    	}
    }
    */
    
  • main.c
    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "Timer.h"
    
    uint16_t Num;			//定义在定时器中断里自增的变量
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	Timer_Init();		//定时中断初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Num:");			//1行1列显示字符串Num:
    	OLED_ShowString(2, 1, "CNT:");			//2行1列显示字符串CNT:
    	
    	while (1)
    	{
    		OLED_ShowNum(1, 5, Num, 5);			//不断刷新显示Num变量
    		OLED_ShowNum(2, 5, Timer_GetCounter(), 5);		//不断刷新显示CNT的值
    	}
    }
    
    /**
      * 函    数:TIM2中断函数
      * 参    数:无
      * 返 回 值:无
      * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
      *           函数名为预留的指定名称,可以从启动文件复制
      *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
      */
    void TIM2_IRQHandler(void)
    {
    	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)		//判断是否是TIM2的更新事件触发的中断
    	{
    		Num ++;												//Num变量自增,用于测试定时中断
    		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);			//清除TIM2更新事件的中断标志位
    		//中断标志位必须清除
    		//否则中断将连续不断地触发,导致主程序卡死
    	}
    }
    
  • 6.11 输出比较简介

  • OC(Output Compare)输出比较;
  • 输出比较可以通过比较CNT计数器与CCR(捕获/比较)寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形;
  • CCR(捕获/比较)寄存器是输入捕获和输出比较共用的,可以在6.2 定时器类型的通用定时器框图中找到;
  • 当使用输入捕获时,它就是捕获寄存器;
  • 当使用输出比较时,它就是比较寄存器;
  • 在输出比较时,这块电路会比较CNT和CRR的值。CNT计数自增,CCR就是我们给定的一个值。当CNT>CCR、<CCR或者等于CCR的时候,旁边的输出就会对应的置1、置0、置1、置0,这样就可以输出一个电平不断跳变的PWM波形了;
  • 每个高级定时器和通用定时器都拥有4个输出比较通道;
  • 高级定时器的前3个通道额外拥有死区生成和互补输出的功能。
  • 6.12 PWM简介

  • PWM(Pulse Width Modulation)脉冲宽度调制;
  • 在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域;
  • PWM参数:
  • 频率 = 1 / TS;
  • 占空比 = TON / TS;
  • 分辨率 = 占空比变化步距;
  • 6.13 输出比较通道

  • 通用定时器:

  • 对应的是6.2 定时器类型的通用定时器框图中CCR寄存器右边的那一部分输出比较电路,最后通过TIM_CH1输出到GPIO引脚上。下面还有三个同样的单元,分别输出到CH2、CH3、CH4;
  • 在下图中:
  • 左边是CNT计数器和CRR1第一路的捕获/比较寄存器,二者进行比较,当CNT>CRR1或者CNT=CRR1的时候,就会给输出模式控制器传递一个信号,然后输出模式控制器就会改变它输出OC1REF的高低电平;
  • REF(reference,参考信号)信号就是指这里信号的高低电平;
  • 那么什么时候给REF高电平,什么时候给低电平呢?见下面的输出模式比较;
  • 在上面还有一个ETRF输入,是定时器的一个小功能,一般不用,无需了解;
  • REF信号可以通过上面的线路前往主模式控制器,可以把RED映射到主模式的TRGO输出上去;
  • REF信号也可以到达右边的极性选择。给这个寄存器写0,就表示信号电平不反转,输入什么就输出什么;写1的话,信号会通过一个非门取反,那么输出的信号就是输入信号高低电平反转的信号;
  • 再往右是一个输出使能电路,选择要不要输出,输出的话会从OC1引脚(CH1通道的引脚)输出;
  • 高级定时器:

  • 输出模式比较:

  • 就是输出模式控制器里面的执行逻辑;
  • 该模式控制器输入的是CNT和CCR的大小关系,输出的是REF的高低电平;
  • 里面可以选择多种模式来更加灵活地控制REF输出,可以通过其下面的寄存器来配置;
  • 模式 描述
    冻结 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置无效电平
  • PWM模式2实际上就是PWM模式1输出的取反。

  • 6.14 PWM基本结构

  • 蓝色线是CNT的值;
  • 黄色线是ARR的值;
  • 红色线是CCR的值;
  • 绿色线是输出。
  • 6.15 PWM参数计算

  • PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1);
  • PWM占空比:Duty = CCR / (ARR + 1);
  • PWM分辨率:Reso = 1 / (ARR + 1);
  • 由上面公式可知,通过PSC和ARR都可以调节PWM频率。但是通过ARR调节频率,同时还会影响占空比,而通过PSC调节频率,不会影响占空比。
  • 6.16 舵机简介

  • 舵机是一种根据输入PWM信号占空比来控制输出角度的装置;
  • 输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms;
  • 硬件电路:
  • 6.17 直流电机及驱动简介

  • 直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转;
  • 直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作;
  • TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向;
  • 硬件电路:
  • 6.18 PWM驱动LED呼吸灯

  • 项目目录结构:
  • PWM.h
    #ifndef __PWM_H
    #define __PWM_H
    
    void PWM_Init(void);
    void PWM_SetCompare1(uint16_t Compare);
    
    #endif
    
  • PWM.c
    #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:PWM初始化
      * 参    数:无
      * 返 回 值:无
      */
    void PWM_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO重映射*/
        //若想让PA15、PB3、PB4这三个引脚当做GPIO来使用的话,使用下面的第一行和第三行代码。先打开AFIO时钟,再用AFIO将JTAG复用解除掉;
        //若想重映射定时器或者其它外设的复用引脚,使用下面的第一行和第二行。先打开AFIO时钟,再用AFIO重映射外设复用的引脚;
        //如果重映射的引脚又正好是调试端口,那么下面三行都要加上。打开AFIO时钟,重映射引脚、解除调试端口
    //	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);			//开启AFIO的时钟,重映射必须先开启AFIO的时钟
    //	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);			//将TIM2的引脚部分重映射,具体的映射方案需查看参考手册
    //	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);		//将JTAG引脚失能,作为普通GPIO引脚使用
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出。受外设控制的引脚,均需要配置为复用模式	
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		//若使用重映射引脚GPIO_Pin_15,需要打开上面三行的注释
        //TIM2的OC1通道中的PWM波形最后是通过一个GPIO口的引脚才可以输出,那么使用的是哪一个GPIO口,需要查询STM32的引脚定义表,默认复用功能那一列,就是片上外设端口和GPIO的连接关系。若一个GPIO引脚被多个外设端口想使用,可以查看旁边那一列的重定义功能,看看是否有其中一个外设端口被重定义到了其它GPIO引脚,如果有,那么该外设端口就可以使用重定义后的GPIO口
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA0引脚初始化为复用推挽输出
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
    	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
    	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
        //下面的值先已知频率为1KHz,占空比为50%,分辨率为1%,再代入到 6.15 PWM参数计算 中的公式计算得到
    	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_TimeBaseInit,配置TIM2的时基单元
    	
    	/*输出比较初始化*/
    	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;	//输出使能
    	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值(占空比)
    	TIM_OC1Init(TIM2, &TIM_OCInitStructure);						//将结构体变量交给TIM_OC1Init,配置TIM2的输出比较通道1
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
    }
    
    /**
      * 函    数:PWM设置CCR
      * 参    数:Compare 要写入的CCR的值,范围:0~100
      * 返 回 值:无
      * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
      *           占空比Duty = CCR / (ARR + 1)
      */
    void PWM_SetCompare1(uint16_t Compare)
    {
    	TIM_SetCompare1(TIM2, Compare);		//设置CCR1的值
    }
    
  • main.c
    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "PWM.h"
    
    uint8_t i;			//定义for循环的变量
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	PWM_Init();			//PWM初始化
    	
    	while (1)
    	{
    		for (i = 0; i <= 100; i++)
    		{
    			PWM_SetCompare1(i);			//依次将定时器的CCR寄存器设置为0~100,PWM占空比逐渐增大,LED逐渐变亮
    			Delay_ms(10);				//延时10ms
    		}
    		for (i = 0; i <= 100; i++)
    		{
    			PWM_SetCompare1(100 - i);	//依次将定时器的CCR寄存器设置为100~0,PWM占空比逐渐减小,LED逐渐变暗
    			Delay_ms(10);				//延时10ms
    		}
    	}
    }
    
  • 6.19 PWM驱动舵机

  • 项目目录结构:
  • PWM.h
  • 修改函数名PWM_SetCompare2
  • #ifndef __PWM_H
    #define __PWM_H
    
    void PWM_Init(void);
    void PWM_SetCompare2(uint16_t Compare);
    
    #endif
    
  • 修改了PWM.c
  • 修改GPIO初始化的引脚为GPIO_Pin_1;
  • 修改输出比较初始化,配置TIM2的输出比较通道2;
  • 修改函数名PWM_SetCompare2
  • #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:PWM初始化
      * 参    数:无
      * 返 回 值:无
      */
    void PWM_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;               //受外设控制的引脚,均需要配置为复用模式
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA1引脚初始化为复用推挽输出
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
    	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
    	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
        //下面的值先已知频率为50Hz(1/20ms),占空比为0.5ms~2.5ms,再代入到 6.15 PWM参数计算 中的公式计算得到
        //这么配置后,CCR设置成500,就是0.5ms;CCR设置成2500,就是2.5ms
    	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_TimeBaseInit,配置TIM2的时基单元
    	
    	/*输出比较初始化*/ 
    	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;   //输出使能
    	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
    	TIM_OC2Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC2Init,配置TIM2的输出比较通道2
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
    }
    
    /**
      * 函    数:PWM设置CCR
      * 参    数:Compare 要写入的CCR的值,范围:0~100
      * 返 回 值:无
      * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
      *           占空比Duty = CCR / (ARR + 1)
      */
    void PWM_SetCompare2(uint16_t Compare)
    {
    	TIM_SetCompare2(TIM2, Compare);		//设置CCR2的值
    }
    
  • 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();									//初始化舵机的底层PWM
    }
    
    /**
      * 函    数:舵机设置角度
      * 参    数:Angle 要设置的舵机角度,范围:0~180
      * 返 回 值:无
      */
    void Servo_SetAngle(float Angle)
    {
        //0度 -> 500
        //180度 -> 2500
    	PWM_SetCompare2(Angle / 180 * 2000 + 500);	//设置占空比。将角度线性变换,对应到舵机要求的占空比范围上
    }
    
  • main.c
    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "Servo.h"
    #include "Key.h"
    
    uint8_t KeyNum;			//定义用于接收键码的变量
    float Angle;			//定义角度变量
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	Servo_Init();		//舵机初始化
    	Key_Init();			//按键初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Angle:");	//1行1列显示字符串Angle:
    	
    	while (1)
    	{
    		KeyNum = Key_GetNum();			//获取按键键码
    		if (KeyNum == 1)				//按键1按下
    		{
    			Angle += 30;				//角度变量自增30
    			if (Angle > 180)			//角度变量超过180后
    			{
    				Angle = 0;				//角度变量归零
    			}
    		}
    		Servo_SetAngle(Angle);			//设置舵机的角度为角度变量
    		OLED_ShowNum(1, 7, Angle, 3);	//OLED显示角度变量
    	}
    }
    
  • 6.20 PWM驱动直流电机

  • 项目目录结构:

  • PWM.h

  • 修改函数名PWM_SetCompare3
  • #ifndef __PWM_H
    #define __PWM_H
    
    void PWM_Init(void);
    void PWM_SetCompare3(uint16_t Compare);
    
    #endif
    
  • PWM.c

  • 修改GPIO初始化的引脚为GPIO_Pin_2;
  • 修改输出比较初始化,配置TIM2的输出比较通道3;
  • 修改函数名PWM_SetCompare3
  • #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:PWM初始化
      * 参    数:无
      * 返 回 值:无
      */
    void PWM_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;           //受外设控制的引脚,均需要配置为复用模式
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA2引脚初始化为复用推挽输出	
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
    	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
    	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
    	TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;                 //计数周期,即ARR的值
        //因为电机里面也是线圈和磁铁,所以在PWM的驱动下,会发出蜂鸣器的声音,这是正常现象。加大PWM频率可以避免这个现象,当PWM频率足够大时,超出人耳的范围,人耳就听不到了。人耳听到声音的频率范围是20Hz~20KHz,加大频率可以通过减少预分频器来完成,这样不会影响占空比
    	TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1;               //预分频器,即PSC的值
    	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
    	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
    	
    	/*输出比较初始化*/ 
    	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;   //输出使能
    	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
    	TIM_OC3Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC3Init,配置TIM2的输出比较通道3
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
    }
    
    /**
      * 函    数:PWM设置CCR
      * 参    数:Compare 要写入的CCR的值,范围:0~100
      * 返 回 值:无
      * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
      *           占空比Duty = CCR / (ARR + 1)
      */
    void PWM_SetCompare3(uint16_t Compare)
    {
    	TIM_SetCompare3(TIM2, Compare);		//设置CCR3的值
    }
    
  • 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)
    {
    	/*开启时钟*/
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		//开启GPIOA的时钟
    	
    	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);						//将PA4和PA5引脚初始化为推挽输出	
    	
    	PWM_Init();													//初始化直流电机的底层PWM
    }
    
    /**
      * 函    数:直流电机设置速度
      * 参    数:Speed 要设置的速度,范围:-100~100
      * 返 回 值:无
      */
    void Motor_SetSpeed(int8_t Speed)
    {
    	if (Speed >= 0)							//如果设置正转的速度值
    	{
    		GPIO_SetBits(GPIOA, GPIO_Pin_4);	//PA4置高电平
    		GPIO_ResetBits(GPIOA, GPIO_Pin_5);	//PA5置低电平,设置方向为正转
    		PWM_SetCompare3(Speed);				//PWM设置为速度值
    	}
    	else									//否则,即设置反转的速度值
    	{
    		GPIO_ResetBits(GPIOA, GPIO_Pin_4);	//PA4置低电平
    		GPIO_SetBits(GPIOA, GPIO_Pin_5);	//PA5置高电平,设置方向为反转
    		PWM_SetCompare3(-Speed);			//PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
    	}
    }
    
  • main.c

    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "Motor.h"
    #include "Key.h"
    
    uint8_t KeyNum;		//定义用于接收按键键码的变量
    int8_t Speed;		//定义速度变量
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	Motor_Init();		//直流电机初始化
    	Key_Init();			//按键初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Speed:");		//1行1列显示字符串Speed:
    	
    	while (1)
    	{
    		KeyNum = Key_GetNum();				//获取按键键码
    		if (KeyNum == 1)					//按键1按下
    		{
    			Speed += 20;					//速度变量自增20
    			if (Speed > 100)				//速度变量超过100后
    			{
    				Speed = -100;				//速度变量变为-100
    											//此操作会让电机旋转方向突然改变,可能会因供电不足而导致单片机复位
    											//若出现了此现象,则应避免使用这样的操作
    			}
    		}
    		Motor_SetSpeed(Speed);				//设置直流电机的速度为速度变量
    		OLED_ShowSignedNum(1, 7, Speed, 3);	//OLED显示速度变量
    	}
    }
    
  • 6.21 输入捕获简介

  • IC(Input Capture)输入捕获;
  • 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数;
  • 每个高级定时器和通用定时器都拥有4个输入捕获通道;
  • 可配置为PWMI模式,同时测量频率和占空比;
  • 可配合主从触发模式,实现硬件全自动测量。
  • 6.22 频率测量

  • 下图中,越往左,频率越高;越往右,频率越低;
  • 测频法适合测量高频信号,测周法适合低频信号;
  • 测频法:在闸门时间内,最好要多出现一些上升沿,计次数量多一些,这样有助于减少误差。假设定了1s的闸门时间,结果信号频率非常低,1s的时间内上升沿次数非常少,甚至一个都没有,总不能认为频率是0吧,这会导致在计次N很少时,误差会非常大,所以测频法要求信号频率要高一些;
  • 测周法:低频信号,周期较长,计次就比较多,有助于减少误差;否则,比如标准频率fc为1MHz,待测信号频率太高,比如待测信号为500KHz,那在一个周期内只能计一两个数,若待测信号再高些,可能一个数都计不到,总不能认为频率无穷大吧,所以测周法要求信号频率要低一些;
  • 测频法测量结果更新的慢一些,数值相对稳定;测周法更新的快,数据跳变也非常快;
  • 测频法测量的是在闸门时间内的多个周期,所以它自带一个均值滤波,如果在闸门时间内波形频率有变化,那得到的其实是这一段时间的平均频率。如果闸门时间选为1s,那么每隔1s才能得到一次结果,所以测频法结果更新慢,测量结果是一段时间的平均值,值比较平滑;
  • 测周法只测量一个周期就能出一次结果,所以出结果的速度取决于待测信号的频率。一般而言,待测信号都是几百几千Hz,所以一般情况下,测周法更新结果更快。但是由于它只测量一个周期,所以结果值会受噪声影响,波动比较大;
  • 那如何界定高频信号与低频信号呢?这就涉及到中界频率的概念了;
  • 不管是测频法计次还是测周法计次,计次数量N都应该大一些。N越大,相对误差越小。因为在这些方法中,计次可能会存在正负1误差;
  • 在测频法中,在闸门时间内,并不是每个周期信号都是完整的。比如在最后时间里,可能有一个周期刚出现一半,闸门时间就到了,那这只有半个周期,只能舍弃掉或者当做一整个周期来看。那在这个过程,就会出现多计一个或者少计一个的情况,这就是正负1误差;
  • 在测周法中,用标准频率fc计次,在最后时刻可能一个数还没计完,计时就结束了,那么这半个数也只能舍弃或者按照一个数来算,这里也会出现正负1误差;
  • 所以,正负1误差是测频法和测周法这两种方法都固有的误差。要想减少这两种误差的影响,就只能尽量多计一些数。当计次N比较大时,正负1误差对N的影响就较小;
  • 当有一个频率,测频法和测周法计次的N相同,就说明误差相同,就是中界频率。将测频法和测周法的N都提出来,然后令他们相等,再解出fx,就得到中界频率fm的公式;
  • 对应图上,当待测信号频率<中界频率时,测周法误差更小,选择测周法更合适;
  • 当待测信号频率>中界频率时,测频法误差更小,选择测频法更合适;
  • 在STM32上的实现:
  • 之前写过的5.10 对射式红外传感器计次6.10 定时器外部时钟,稍加改进,就是测频法;
  • 比如对射式红外传感器计次,每来一个上升沿计次+1,可以再用一个定时器,顶一个1s的定时中断。在中断里,每隔1s取一下计次值,同时清0计次,为下一次做准备,这样每一次读取的计次值就是频率;
  • 定时器外部时钟的代码,每隔1s取一下计次,就能实现测频法测量频率的功能了;
  • 6.2 定时器类型中对通用定时器左下部分输入捕获的各部分电路的讲解,对应于讲解的最后对整个电路的工作流程的举例假设:上升沿用于触发输入捕获,CNT用于计数计时。每来一个上升沿,取一下CNT的值,自动存在CCR中,CCR捕获到的值,就是计数值N,CNT的驱动时钟,就是标准频率fc,fc/N就得到了待测信号的频率;
  • 注意:每次捕获之后,都要把CNT清0,这样下次上升沿再次捕获时,取出的CNT才是两个上升沿的时间间隔。这个在一次捕获后自动将CNT清零的步骤,可以用主从触发模式自动来完成;
  • 6.23 输入捕获通道

  • 下图是6.2 定时器类型中对通用定时器左下部分输入捕获的各部分电路的详细版;
  • 引脚进来,先经过一个滤波器,滤波器的输入是TI1,就是CH1的引脚,输入的TI1F计时滤波后的信号。fDTS是滤波器的采样时钟来源;
  • 下面CCMR1寄存器里的ICF位可以控制滤波器的参数;
  • 滤波后的信号经过边沿检测器,捕获上升沿或者下降沿;用CCR寄存器中的CC1P位,就可以选择极性了,最终得到TI1FP1触发信号,通过数据选择器,进入通达1后续的捕获电路;
  • 当然此处应该还有一套一样的电路,得到TI1FP2信号,连通到通道2的后续电路,这里并没有画出来。同样,通道2有TI2FP1,连通到通道1的后续;通道2也还有TI2FP2,连通到通道2的后续,总共是4种连接方式;
  • CC1S位可以对数据选择器进行选择;
  • ICPS位可以配置这里的分频器,可以选择不分频、2分频、4分频、8分频;
  • CC1E位,控制输出使能或失能;
  • 如果使能了输出,输入端产生指定边沿信号,经过层层电路,到达电路的最后,就可以让CNT(未画出)的值,转运到CCR中。每捕获依次CNT的值,都要把CNT清零,以便于下一次的捕获,在这里硬件电路可以在捕获之后自动完成CNT的清零工作;
  • 如何自动清零CNT?在框图的右上,TI1FP1信号和TI1的边沿信号(TI1F_ED),都可以通向从模式控制器。比如TI1FP1信号的上升沿触发捕获,其还可以同时出发从模式。在从模式中,就有电路,可以自动完成CNT的清零。可以看出,这个从模式就是完成自动化操作的利器;
  • 6.24 主从触发模式

  • 主从触发模式是主模式、从模式、触发源选择三个功能的简称;
  • 主模式:可以将定时器内部的信号,映射到TRGO引脚,用于触发别的外设;
  • 从模式:接收其他外设或者自身外设的一些信号,用于控制自身定时器的运行,也就是被别的信号控制;
  • 触发源模式:选择从模式的触发信号源,可以认为是从模式的一部分。触发源的选择,选择指定的一个信号,得到TRGI,TRGI去触发从模式,从模式可以从列表中,选择一项来自动执行;
  • 比如:让TI1FP1信号自动触发CNT清零。那么触发源选择,就可以选择TI1FP1,从模式执行的操作,就可以选择执行Reset操作。这样TI1FP1的信号,就可以自动触发从模式,从模式自动清零CNT,实现硬件全自动测量;
  • 6.25 输入捕获基本结构

  • 这个结构,只使用了一个通道,所以其目前只能测量频率;
  • 结构图:
  • 右上角是时基单元,将时基单元配置好,启动定时器;
  • CNT就会在预分频之后的时钟驱动下,不断自增;
  • CNT就是测周法用来计数计时的东西;
  • 经过预分频器预分频后的时钟频率,就是驱动CNT的标准频率fc
  • 标准频率 = 72M/预分频系数;
  • 在下面的输入捕获通道的GPIO口,输入一个左上角那样子的方波信号,经过滤波器和边沿检测,选择TI1FP1为上升沿触发,之后选择直连的通道,分频器选择不分频;
  • 当TI1FP1出现上升沿后,CNT的当前计数值转运到CCR1里,同时触发源选择,选中TI1FP1为触发信号,从模式选择复位操作。这样TI1FP1的上升沿,也可以通过上面那一路,去触发CNT清零;
  • 注意有个先后顺序,是先转运CNT的值到CCR中,再出发从模式给CNT清零。或者是非阻塞的同时转移,CNT的值转移到CCR,同时0转移到CNT里面去;
  • 左上角:
  • 信号出现一个上升沿,CCR1=CNT,就是把CNT的值转运到CCR1里面去,这是输入捕获自动执行的。然后CNT=0,清零计数器,这是从模式自动执行的;
  • 在一个周期内,CNT在标准时钟的驱动下,不断自增,并且由于之前清零过了,所以CNT就是从上升沿开始,从0开始计数,一直在++,直到下一次上升沿来临,然后执行相同的操作;
  • 注意:第二次捕获的时候,CNT的值就是从第一个上升沿到第二个上升沿的计数值,这个计数值就自动放在CCR1里。然后下一个周期,继续同样的过程,CNT从0开始自增,知道下一个上升沿,这是CCR1刷新为第二个周期的计数值,然后不断重复这个过程。所以,当这个电路工作时,CCR1的值,始终保持为最新一个周期的计数值,这个计数值就是计次N,然后fc/N,就是信号的频率。所以,当我们想要读取信号的频率时,只需要读取CCR1得到N,然后计算fc/N,就可以了。当我们不需要读取的时候,整个电路全自动地测量,不需要占用任何软件资源;
  • 注意:
  • CNT的值是有上限的,ARR一般设置为最大65535,那CNT最大也只能计65535个数。如果信号频率太低,CNT计数值可能会溢出;
  • 从模式的触发源选择,只有TI1FP1和TI1FP2,没有TI3和TI4的信号,所以想使用从模式自动清零CNT,就只能用通道1和通道2。对于通道3和通道4,就只能开启捕获中断,在中断里手动清零了,这样会导致程序处于频繁中断的状态,比较消耗软件资源;
  • 6.26 PWMI基本结构

  • 使用两个通道同时捕获一个引脚,可以同时测量周期和占空比;
  • 多了一个TI1FP2,配置为下降沿触发,通过交叉通道,去触发通道2的捕获单元;
  • 左上角:
  • 最开始上升沿,CCR1捕获,同时清零CNT,之后CNT一直++;
  • 在下降沿时刻,触发CCR2捕获,所以此时CCR2的值,就是CNT从上一个上升沿到下一个下降沿的计数值,就是高电平期间的计数值;
  • CCR2捕获,并不触发CNT清零,CNT继续++。直到下一次上升沿,CCR1捕获周期,CNT清零;
  • 这样CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,CCR2/CCR1就是占空比;
  • 6.27 输入捕获模式测频率

  • 项目目录结构:
  • PWM.h
    #ifndef __PWM_H
    #define __PWM_H
    
    void PWM_Init(void);
    void PWM_SetCompare1(uint16_t Compare);
    void PWM_SetPrescaler(uint16_t Prescaler);
    
    #endif
    
  • PWM.c
    #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:PWM初始化
      * 参    数:无
      * 返 回 值:无
      */
    void PWM_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO重映射*/
    //	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);			//开启AFIO的时钟,重映射必须先开启AFIO的时钟
    //	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);			//将TIM2的引脚部分重映射,具体的映射方案需查看参考手册
    //	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);		//将JTAG引脚失能,作为普通GPIO引脚使用
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //受外设控制的引脚,均需要配置为复用模式
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		//GPIO_Pin_15;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA0引脚初始化为复用推挽输出	
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
    	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
    	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
    	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_TimeBaseInit,配置TIM2的时基单元
    	
    	/*输出比较初始化*/
    	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;	//输出使能
    	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
    	TIM_OC1Init(TIM2, &TIM_OCInitStructure);						//将结构体变量交给TIM_OC1Init,配置TIM2的输出比较通道1
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
    }
    
    /**
      * 函    数:PWM设置CCR
      * 参    数:Compare 要写入的CCR的值,范围:0~100
      * 返 回 值:无
      * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
      *           占空比Duty = CCR / (ARR + 1)
      */
    void PWM_SetCompare1(uint16_t Compare)
    {
    	TIM_SetCompare1(TIM2, Compare);		//设置CCR1的值
    }
    
    /**
      * 函    数:PWM设置PSC
      * 参    数:Prescaler 要写入的PSC的值,范围:0~65535
      * 返 回 值:无
      * 注意事项:PSC和ARR共同决定频率,此函数仅设置PSC的值,并不直接是频率
      *           频率Freq = CK_PSC / (PSC + 1) / (ARR + 1)
      */
    void PWM_SetPrescaler(uint16_t Prescaler)
    {
        //TIM_PSCReloadMode_Immediate是配置重装模式
    	TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate);		//设置PSC的值
    }
    
  • 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_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);			//开启TIM3的时钟(TIM2被用于输出PWM)
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;                       //查表知:TIM3的通道1引脚对应的就是PA6
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA6引脚初始化为上拉输入
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM3);		//选择TIM3为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	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 = 72 - 1;               //预分频器,即PSC的值。该值决定了测周法的标准频率fc,72M/预分频,就是计数器自增的频率,就是计数标准频率,这个需要根据信号频率的分布范围来调整。暂时先给72-1,这样标准频率就是72M/72=1MHz,比较方便计算
    	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
    	
    	/*输入捕获初始化*/
    	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;	//输入信号交叉,选择直通,不交叉
    	TIM_ICInit(TIM3, &TIM_ICInitStructure);							//将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
    	
    	/*选择触发源及从模式*/
    	TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);					//触发源选择TI1FP1
    	TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);					//从模式选择复位,即TI1产生上升沿时,会触发CNT归零
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM3, ENABLE);			//使能TIM3,定时器开始运行
    }
    
    /**
      * 函    数:获取输入捕获的频率
      * 参    数:无
      * 返 回 值:捕获得到的频率
      */
    uint32_t IC_GetFreq(void)
    {
        //执行测周法的公式:fx = fc / N
        //fc = 72M/(PSC + 1),目前PSC是72-1,所以fc=1MHz
    	return 1000000 / (TIM_GetCapture1(TIM3) + 1); //不加一也可以
    }
    
  • main.c
    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "PWM.h"
    #include "IC.h"
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	PWM_Init();			//PWM初始化
    	IC_Init();			//输入捕获初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Freq:00000Hz");		//1行1列显示字符串Freq:00000Hz
    	
    	/*使用PWM模块提供输入捕获的测试信号*/
    	PWM_SetPrescaler(720 - 1);					//PWM频率Freq = 72M / (PSC + 1) / 100,100就是ARR+1
    	PWM_SetCompare1(50);						//PWM占空比Duty = CCR / 100
    	
    	while (1)
    	{
    		OLED_ShowNum(1, 6, IC_GetFreq(), 5);	//不断刷新显示输入捕获测得的频率
    	}
    }
    
  • 6.28 PWMI模式测频率占空比

  • 项目目录结构:

  • IC.h:输入捕获模块

  • 添加获取占空比的函数uint32_t IC_GetDuty(void);
  • #ifndef __IC_H
    #define __IC_H
    
    void IC_Init(void);
    uint32_t IC_GetFreq(void);
    uint32_t IC_GetDuty(void);
    
    #endif
    
  • IC.c

  • 添加TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);,实现PWMI模式;
  • 添加获取占空比的函数uint32_t IC_GetDuty(void);
  • #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:输入捕获初始化
      * 参    数:无
      * 返 回 值:无
      */
    void IC_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);			//开启TIM3的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	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);							//将PA6引脚初始化为上拉输入
    	
    	/*配置时钟源*/
    	TIM_InternalClockConfig(TIM3);		//选择TIM3为内部时钟,若不调用此函数,TIM默认也为内部时钟
    	
    	/*时基单元初始化*/
    	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 = 72 - 1;               //预分频器,即PSC的值
    	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
    	
    	/*PWMI模式初始化*/
    	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;	//输入信号交叉,选择直通,不交叉
    	TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);						//将结构体变量交给TIM_PWMIConfig,配置TIM3的输入捕获通道,此函数同时会把另一个通道配置为相反的配置,实现PWMI模式。比如传入的是通道1、直连、上升沿,该函数就会顺带配置一个通道2、交叉、下降沿
    
    	/*选择触发源及从模式*/
    	TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);					//触发源选择TI1FP1
    	TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);					//从模式选择复位
    																	//即TI1产生上升沿时,会触发CNT归零
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM3, ENABLE);			//使能TIM3,定时器开始运行
    }
    
    /**
      * 函    数:获取输入捕获的频率
      * 参    数:无
      * 返 回 值:捕获得到的频率
      */
    uint32_t IC_GetFreq(void)
    {
    	return 1000000 / (TIM_GetCapture1(TIM3) + 1);		//测周法得到频率fx = fc / N,这里不执行+1的操作也可
    }
    
    /**
      * 函    数:获取输入捕获的占空比
      * 参    数:无
      * 返 回 值:捕获得到的占空比
      */
    uint32_t IC_GetDuty(void)
    {
    	return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);	//占空比Duty = CCR2 / CCR1 * 100,这里不执行+1的操作也可
    }
    
  • main.c

    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "PWM.h"
    #include "IC.h"
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	PWM_Init();			//PWM初始化
    	IC_Init();			//输入捕获初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Freq:00000Hz");		//1行1列显示字符串Freq:00000Hz
    	OLED_ShowString(2, 1, "Duty:00%");			//2行1列显示字符串Duty:00%
    	
    	/*使用PWM模块提供输入捕获的测试信号*/
        //可以通过修改下面的参数来进行测试。比如720-1改成7200-1,频率就是100Hz;50改成80,占空比就是80%
    	PWM_SetPrescaler(720 - 1);					//PWM频率Freq = 72M / (PSC + 1) / 100
    	PWM_SetCompare1(50);						//PWM占空比Duty = CCR / 100
    	
    	while (1)
    	{
    		OLED_ShowNum(1, 6, IC_GetFreq(), 5);	//不断刷新显示输入捕获测得的频率
    		OLED_ShowNum(2, 6, IC_GetDuty(), 2);	//不断刷新显示输入捕获测得的占空比
    	}
    }
    
  • 测频率的范围:

  • 最低频率:目前给的标准频率是1MHz,计数器最大只能计到65535,所以所测量的最低频率是1M/65535,这个值大概估算一下是15Hz。若信号频率再低,计数器就会溢出,所以最低频率就是15Hz左右;
  • 若想再降低最低频率的限制,可以加大预分频,即增大TIM_TimeBaseInitStructure.TIM_Prescaler的值,这样标准频率就更低,所支持测量的最低频率也更低;
  • 最大频率:目前没有一个明显的界限,因为随着待测频率的增大,误差也会逐渐增大。若非要找一个频率上限,应该就是标准频率1MHz。超过1MHz,信号频率比标准频率还高,肯定测不了。但是这个1MHz的上限并没有意义,因为信号频率接近1MHz时,误差已经很大了,所以最大频率要看对误差的要求;
  • 正负1误差,计100个数,有一个误差,相对误差就是百分之一;计1000个数,有一个误差,相对误差就是千分之一,所以正负1误差可以认为是1/计数值。在这里,如果要求误差等于千分之一时频率为上限,那这个上限就是1M/1000=1KHz;如果要求误差等于百分之一时频率为上限,那这个上限就是1M/100=10KHz;
  • 若想提高频率的上限,就要降低PSC,即TIM_TimeBaseInitStructure.TIM_Prescaler。提高标准频率,上限就会提高;
  • 除此之外,若频率还要更高,就要考虑一下测评法了,因为测评法适合高频,测周法适合低频。
  • 误差分析:

  • 除了正负1误差,还有晶振误差;
  • 此处是自己测量自己,不存在晶振误差,所以数值还是非常稳定的;
  • 若要测量别的信号,那数值可能会有一些抖动,后期可以再做一些滤波处理。
  • 6.29 编码器接口简介

  • Encoder Interface 编码器接口;
  • 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度;
  • 每个高级定时器和通用定时器都拥有1个编码器接口;
  • 如果一个定时器配置成了编码器接口模式,那它基本上就干不了其它活了;
  • 这个C8T6芯片只有TIM1、2、3、4,四个定时器,所以最多只能接四个编码器。而且接完四个编码器,就没有定时器可以用了;
  • 实在不行,可以用外部中断来接编码器,这样就是用软件资源来弥补硬件资源;
  • 编解码接口的两个输入引脚(就是每个定时器的CH1和CH2引脚,CH3和CH4不能接编码器)借用了输入捕获的通道1和通道2。
  • 6.30 正交编码器

  • 正交编码器:输出的两个方波信号,相位相差90度,超前90度或者滞后90度,分别代表正转和反转;
  • 可以测量位置,或者带方向的速度值;
  • 当编码器的旋转轴转起来时,A相和B相就会输出下图所示的方波信号;
  • 转的越快,方波的频率就越高,所以方波的频率就代表了速度。取出任意一相的信号来测频率,就能知道旋转速度了;
  • 但是若只有一相的信号,是无法测量旋转方向的;
  • 编码器接口的设计逻辑:
  • 把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减;
  • 至于是增还是减,这个计数的方向由另一相的状态来确定;
  • 当出现某个边沿时,判断另一相的高低电平,如果对应另一相的状态出现在下图右上表中,那就是正转,计数自增;
  • 反之,对应另一相的状态出现在下图右下表中,那就是反转,计数自减;
  • 6.2 定时器类型中对编码器接口的电路进行了详细讲解。
  • 6.31 编码器接口基本结构

  • 6.2 定时器类型中编码器接口的电路框图详细版:
  • 输入捕获的前两个通道,通过GPIO口接入编码器的A、B相;
  • 然后通过滤波器和边沿检测、极性选择,产生TI1FP1和TI2FP2,通向编码器接口;
  • 编码器接口通过预分频器控制CNT计数器的时钟;
  • 同时,编码器接口还根据编码器的旋转方向,控制CNT的计数方向;
  • 编码器正转时,CNT自增;编码器反转时,CNT自减;
  • 此处ARR也是有效的,一般会设置为65535的最大量程,这样可以利用补码的特性,很容易得到负数;
  • 比如CNT初始为0,正转,CNT自增;
  • 如果反转,CNT自减,0的下一个数就是65535,此处会将这个16位的无符号数转换为16位的有符号数。根据补码的定义,这个65535就对应-1;
  • 6.32 工作模式

  • 假设TI1接A相,TI2接B相;
  • 仅在TI1计数,指的就是只在A相变化时计数,不用理会B相的变化,只是会降低一些计数精度;
  • 6.33 实例

  • 均不反相:
  • 正交编码器抗噪声的原理:如果出现了一个引脚不变,另一个引脚连续跳变多个多次的毛刺信号,计数器就会加、减、加、减来回摆动,最终的计数值还是原来那个数,并不受毛刺噪声的影响;
  • TI1反相:
  • TI1反相就是在6.31 编码器接口基本结构中,通过极性选择时,把TI1高低电平反转一下;
  • 在看下图中TI1的方波时,由于TI1反向,需要先将这条方波高低电平取反;
  • 6.34 编码器接口测速

  • 项目目录结构:
  • Encoder.h:编码器接口相关代码
    #ifndef __ENCODER_H
    #define __ENCODER_H
    
    void Encoder_Init(void);
    int16_t Encoder_Get(void);
    
    #endif
    
  • Encoder.c
    #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:编码器初始化
      * 参    数:无
      * 返 回 值:无
      */
    void Encoder_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);			//开启TIM3的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //使用TIM3的CH1和CH2引脚,对应的就是PA6和PA7
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA6和PA7引脚初始化为上拉输入
    	
    	/*时基单元初始化*/
    	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;                //预分频器,即PSC的值。预分频给0就是不分频,编码器的时钟直接驱动计数器
    	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
    	
    	/*输入捕获初始化*/
    	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);							//将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
    	TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;				//选择配置定时器通道2
    	TIM_ICInitStructure.TIM_ICFilter = 0xF;							//输入滤波器参数,可以过滤信号抖动
    	TIM_ICInit(TIM3, &TIM_ICInitStructure);							//将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
    	
    	/*编码器接口配置:配置编码器模式以及两个输入通道是否反相*/
        //第一个参数:要将哪个定时器配置成编码器模式
        //第二个参数:表示在TI1和TI2上都计数
        //第三和第四个参数:
        //注意此时参数的Rising和Falling已经不代表上升沿和下降沿了,而是代表是否反相
        //此函数必须在输入捕获初始化之后进行,否则输入捕获的配置会覆盖此函数的部分配置
        //若要修改极性,随便将第三或第四个参数的TIM_ICPolarity_Rising改为TIM_ICPolarity_Falling即可
    	TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
    	
    	/*TIM使能*/
    	TIM_Cmd(TIM3, ENABLE);			//使能TIM3,定时器开始运行
    }
    
    /**
      * 函    数:获取编码器的增量值
      * 参    数:无
      * 返 回 值:自上此调用此函数后,编码器的增量值
      */
    int16_t Encoder_Get(void)
    {
    	/*使用Temp变量作为中继,目的是返回CNT后将其清零*/
    	int16_t Temp;
    	Temp = TIM_GetCounter(TIM3);
    	TIM_SetCounter(TIM3, 0);
    	return Temp;
    }
    
  • main.c
    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "Timer.h"
    #include "Encoder.h"
    
    int16_t Speed;			//定义速度变量
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();		//OLED初始化
    	Timer_Init();		//定时器初始化
    	Encoder_Init();		//编码器初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "Speed:");		//1行1列显示字符串Speed:
    	
    	while (1)
    	{
    		OLED_ShowSignedNum(1, 7, Speed, 5);	//不断刷新显示编码器测得的最新速度
    	}
    }
    
    /**
      * 函    数:TIM2中断函数
      * 参    数:无
      * 返 回 值:无
      * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
      *           函数名为预留的指定名称,可以从启动文件复制
      *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
      */
    void TIM2_IRQHandler(void)
    {
    	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
    	{
    		Speed = Encoder_Get();						 //每隔固定时间段读取一次编码器计数增量值,即为速度值
    		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);	 //清除TIM2更新事件的中断标志位。中断标志位必须清除,否则中断将连续不断地触发,导致主程序卡死
    	}
    }
    
  • 作者:木木慕慕

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32中的TIM定时器深度解析

    发表回复