声明:本人跟随b站江科大学习,本文章是观看完视频后的一些个人总结和经验分享,建议配合原视频使用。(仅仅是个人的理解和看法,可能有误,也希望大家能帮忙纠正,谢谢大家❤️)
本章节非常长,希望大家能够收藏分几部分慢慢观看,喜欢的可以订阅该栏目后续会持续更新,相信大家看完能够有所收益
理论部分
一、TIM
1.TIM介绍
- 时基单元:由三部分组成,即计数器,预分频器,自动重装载寄存器
- 自动重装载寄存器:程序员设定初值,计数到这个初值就触发中断,也是16位寄存器
- 72MHZ时钟下最大可以实现59.65s的计时,72MHZ就是一微秒可以计72个数,我们的计数器是16位的,可以存储2^16=65536个数,而且分频器也是16位的,最大可以实现65536分频,所以72MHZ除以65536就是分频后1us能够计的个数,再用65536/(72/65536)就是最大计时的微秒数,换成秒数就约等于59.65s。
- 级联:把一个定时器的输出当作另一个定时器的输入,可以实现更长久的计时,两个就是59.65* 65536 *65536s,以此类推
2.定时器电路
基础定时器只有向上计数,通用和高级都有
基本定时器
- 更新中断:即自动重装载值=计数值引起的中断响应,中断之后会进入NVIC,配置好NVIC就可以使,CPU处理该中断
- 更新事件:即自动重装载值=计数值引起的事件响应,没有进入中断函数而是去操作其他的电路外设,如DAC
- 主模式:当我们要用DAC数模转换输出一段波形时,像PWM,由于输出PWM波形需要不定时迅速输出高低电平,会频繁进入中断,那我们就可以使用主模式,将更新事件映射到TRGO(Trigger out)触发输出这个地方,TRGO直接连接在DAC这个引脚上,不需要用中断来触发DAC转换了。这样就不会为了输出波形而去频繁中断,从而影响主函数的运行了,这样就无需软件的参与,实现了硬件自动化
通用定时器
红色:时基单元
浅蓝色:时钟输入部分,分为内部时钟输入和外部时钟输入,其中TIMx_ETR可以是任意通用定时器的一个引脚,具体引脚看下图,
这里是的ETR和CH1复用都是TIM2的PA0。在PA0上面接一个外部方波就可以实现外部时钟输入,ETR后面的极性选择和滤波两部分电路都是为了对外部时钟进行整形(外部时钟难免会有点瑕疵)。滤波之后的信号有两种方式出去,一个是从上面ETRF到触发控制器直接到时基单元(外部时钟模式二),一个是从下面的TRGI(Trigger in)触发输入进入从模式再到时基单元(外部时钟模式一)。两种模式在做时钟信号上没有太大区别,就是在模式一下,输入会占用TRGI的通道而已。
紫色:ITR信号这部分信号来自于其他定时器,我们可以看到TRGO连接着其他的定时器和电路,其实就是连接着下一个定时器的ITR引脚,达到定时器级联的功能(第一个定时器的更新事件驱动第二个定时器的时基单元)。黄色部分有级联实现的方法:初始化第一个定时器并使用主模式将其更新事件映射到TRGO上,再初始化第二个定时器,并根据表找到第一个定时器TRGO对应第二个定时器的哪一个ITR,最后选择时钟为外部时钟模式一,具体连接规则可看下表:
深蓝色:输入捕获电路,可以通过TI1F_ED(即上升沿下降沿都可以触发)和TI1FP1(CH1通道的滤波极性选择),TI2FP2(CH2通道的滤波极性选择)输入外部信号,四通道分别对应CH1~4,用于测量输入方波的频率
橙色:输出比较电路,四个通道分别对应CH1~4,可以用来输出PWM波形和电机驱动
灰色:捕获比较寄存器输入捕获电路和输出比较电路共用的寄存器,输入捕获和输出比较不能同时使用,所以寄存器和引脚都共用
粉色:编码器接口,用于测量电机或者编码器旋转速度
高级定时器
图中红色区域是与通用定时器不同的地方
重复计次计数器:达到几次中断标准(达到自动重装载值或者到0,视计数模式而改变)才会进入一次中断,相当于又进行一次分频,最大计数时间变成59.65s的65536倍。
DTG寄存器:死区生成电路的寄存器,输出控制由通用定时器的一个变成的两个互补的输出,可以输出一对互补的PWM波,可以看到只改变了三个输出控制,是因为这些电路一般是用来驱动三相无刷电机,而且三相无刷电机的驱动电路需要三个桥臂,每个桥臂需要两个大功率开关控制,所以输出PWM波引脚的前三路就变成了互补输出。为了防止互补输出的PWM波驱动桥臂时,在开关切换的瞬间由于器件的不理想造成短暂的直通现象(短路),所以就设置了一个死区生成电路,在开关切换的瞬间产生一定时长的死区,让桥臂的上下管全部关闭,防止直通现象的产生。
TIMx_BKIN(Break in):刹车输入功能,目的是给电机引脚提供输出保障,如果外部引脚BKIN产生了刹车信号,或者内部时钟失效故障,那么控制电路就会自动切断电机的输出,防止意外发生。
3.定时器中断的基本结构
4.定时器时序
预分频器时序
- 计数器寄存器:16位用于计数,最大计数65535,可以看见图中在蓝线位置之后清零重新计数,说明FC是这个位置的自动重装载值,同时产生一个更新事件
- 预分频值(PSC)与分频系数:分频系数等于预分频值+1,也就是图中公式里面的PSC+1
- 预分频控制寄存器:用于改变预分频值,看图发现绿色位置改变了预分频值但是定时器时钟脉冲波形并没有立即改变,那是因为实际起作用的是我们的预分频缓冲器,因为定时器时钟输出脉冲信号到一半时,突然改变了预分频值,会导致脉冲信号也突然改变,容易产生错误,所以这里就有一个预分频缓冲器用于缓冲,使预分频值的改变到脉冲输出一个周期结束后才会起作用,所以图中能看到预分频缓存器改变预分频值时,定时器时钟脉冲信号才会改变。
- 预分频计数器:原本定时器时钟脉冲没改变时,一直为0;改变之后,开始计数,从0开始计数到1又清零,清零的同时才会发送一个定时器时钟的脉冲
计数器时序
通过ARPE来设置有无影子寄存器,这里的影子寄存器相当于上面的预分频缓冲器,自动加载寄存器就是自动重装载值寄存器。第一个图没有影子寄存器,看图知道当自动重装载值从FF变成36之后这个计数周期还未结束,计数抵达36时候马上就产生更新事件了;第二个图有影子寄存器,看图可以发现尽管自动重装载值从F5变了36,但是由于起作用的是影子寄存器,影子寄存器的载值还没有改变,直到计数周期结束产生更新事件时,载值才会改变。影子寄存器的作用:让载值和更新事件同步变化,防止在运行途中更改产生错误
5.RCC时钟树
红线的左边是时钟产生电路,右边是时钟分配电路,RCC时钟树的配置都由ST公司在SystemInit里面配置好了
时钟产生电路(看上图)
- 蓝色部分都是时钟振荡器,用于产生时钟脉冲,ST配置时钟的方法:首先启动内部时钟,让8MHZ高速RC时钟当作系统时钟维持运行,然后启动外部时钟,让8MHZ晶振进入PLL锁相环进行9倍的倍频得到72MHZ的时钟,等锁相环输出稳定后,使用锁相环输出代替高速RC时钟成为系统时钟。
- 当外部时钟出现问题时,CSS(Clock Security System)为了保障系统不死机,会自动将系统时钟变为内部时钟8MHZ,保障系统时钟正常运行。
时钟分配电路(看上图)
- 绿色数字是SystemInit对预分频器分配系数的配置,看图知道AHB的时钟为72MHZ,泽分频系数为2的APB1的时钟为36MHZ,但是可以看到下面绿色框框里面APB1分频系数为2,所以时钟频率*2为72MHZ输出给TIM2-7。下面APB2的分频系数为1,则时钟就为72MHZ,输出给TIM1和8,所以我们一般说无论是高级,通用还是基础定时器的时钟都为72MHZ。
6.补充(滤波器原理)
- 一般来说定时器外部信号输入引脚都会有一个滤波器,用来过滤掉信号的抖动干扰。
工作原理如图:
即在f的时钟频率下,读取每一个采样点的信号,如果连续N个采样点电平都相同了就说明输出稳定了,然后输出这个采样值,如果这N个采样值不全都相同,则说明信号还在抖动,这个时候就保持上一次的输出或者直接输出低电平,保证输出信号在一定程度上的滤波,f和N都是滤波器的参数,f越小N越大滤波效果越好,但是信号延迟越大,这个f来自于内部时钟或者是内部时钟加一个时钟分频,分频多少取决于TIM库函数里面的clock division的配置
- 定时器一上电就进入中断的原因:
这个注释的意思是,生成一个更新事件来重新装载定时器和重复计数器的值,立刻。我们在写入预分频值时,由于预分频缓冲器我们输入的值不会在第一时间起作用,只有在产生更新事件的时候才会起作用,这里为了使我们输入的1分频立刻起作用,就手动加入了一个更新事件,使其进入中断。但是由于更新中断和更新事件同时发生了,而且更新中断会产生中断标志位,当我们初始化完毕之后,标志位没有手动清除,所以一上电就会马上进入中断。解决方法:在时基单元初始化之后,在中断开启(NVIC初始化)之前手动清除中断标志位即可 - 什么时候需要使用浮空输入?
看这个图,使用TIM2配置为外部触发时钟时,ST公司推荐的GPIO使用浮空输出,但是浮空输出电平不稳定,我们可以使用上拉输出好一点。当外部输入信号功率很小,内部的上拉电阻会影响到这个输入信号,这个时候就可以使用浮空输入,避免上拉电阻的影响。
在外部输入信号功率小的情况下使用浮空输入主要有以下几个原因:
1.减少干扰,浮空输入引脚不与特定的电源或地相连,这使得它受外部电源波动和地噪声的干扰相对较小。当外部信号功率很小时,微弱的有用信号很容易被其他电气噪声淹没。例如,在一个有多个大功率设备的复杂电路环境中,接地线上可能会出现各种干扰电流。如果采用非浮空输入(比如直接接地的输入方式),这些干扰可能会通过公共地路径耦合到小信号电路中,影响信号的真实性。浮空输入就像在信号接收端建立一个相对独立的“小岛”,使小信号能够在较少受干扰的情况下被处理。
2.提高输入阻抗,浮空输入一般具有很高的输入阻抗。对于小功率信号源来说,其输出信号的驱动能力往往较弱。高输入阻抗可以最大限度地减小信号源的消耗。比如,把信号源想象成一个小水库,它的水(信号)本来就不多。如果连接的负载(输入电路)对它索取太多(输入阻抗低),水库很快就干涸了(信号失真)。而浮空输入的高阻抗就像是一个只需要很少水量(信号电流)就能检测到水位(信号电压)的高精度传感器,它能够在几乎不消耗信号源能量的情况下获取信号,确保小功率信号能完整地进入后续电路进行处理。
二、TIM输出比较
1.OC(输出比较)介绍
2.PWM介绍
原理:一个物体施加力会动,停止施加力物体并不会马上停止,还未停止时再次施加力物体加速,总时间相等时,以很快的频率改变施加力和停止施加力的时间比例,物体便会以不同速度运动
注意:
- 惯性系统才能用PWM(惯性系统就是去掉驱动力之后不会马上停止运行,由于惯性会继续运行的系统)
- 频率:一个上升沿到下一个上升沿的周期的倒数,实际上就是定时器时钟的频率
- 占空比:给高电平开启的时间和一个周期的时间的比(占空比越大,电机速度越快)
- 分辨率:占空比步距变化的精度(1%->2%的变化精度是1%,1.1%->1.2%的变化精度是0.1%,精度越小分辨率越高)
- TS是定值
黄线:ARR寄存器的值
蓝线:CNT寄存器的计数值变化
红线:CCR寄存器的值比较值
绿线:PWM波形高低电平的变化
读图:当蓝线抵达ARR值时就会清零,蓝线与红线的交点处绿线就会发生改变,即ARR值与CRR值的不断比较引起PWM波高低电平的不断变化,从而影响电机的运行速率
分辨率由ARR值决定,ARR一旦设置后不能轻易改变是个定值相当于TS,若想修改频率又不修改占空比,可以修改时基单元的PSC预分频
3.输出比较通道
通用定时器
极性选择:通过非门和引脚选择器进行电平反转来实现,给TIMx_CCER寄存器写0则没有通过非门,电平不反转,写1则通过非门电平反转
输出模式控制器可以使用的模式,通过配置TIMx_CCMR1寄存器来改变模式
冻结:一般用于想停止输出PWM波,并使高低电平维持在暂停时候不变
匹配时置有效电平:CNT=CCR时,REF相当于输出高电平(说是高电平并不严谨,建议查手册配合高级定时器里的刹车来理解),不适合输出连续变化波形,一般用于输出一次性信号
匹配时置无效电平:CNT=CCR时,REF相当于输出低电平(说是低电平并不严谨,建议查手册配合高级定时器里的刹车来理解),不适合输出连续变化波形,一般用于输出一次性信号
匹配时电平翻转:一般用于输出频率可调,占空比位为50%的PWM波
强制为无效电平和强制为有效电平:和冻结类似,可以暂停波形输出,并在暂停期间保持低电平或者高电平
PWM模式1(2):用于输出频率和占空比都可调的波形,PWM模式2较模式1来说就是REF输出取反而已
高级定时器
红色部分为外部电路(推挽电路),由两个MOS管组成,上管导通下管断开则输出端为1高电平,上管断开下管导通则输出端为0低电平。两个这样的推挽电路可以形成H桥驱动电路(控制直流电机正反转),三个则能用于驱动三相无刷电机。
OC1和OC1N为两个互补的输出端口分别控制上下管的导通和关闭,由于上管导通下管就要断开,下管导通上管就要关闭所以需要是互补的输出端口。如果在上管还未关闭时下管已经导通了,那么就会形成短暂的直通现象,使功率损耗,器件发热,所以就有了死区生成电路使上管关闭时延迟一小段时间再导通下管,防止直通现象产生。
4.输出PWM的基本结构
这里以PWM模式1向上计数为例
写代码思路还是看这个图
同一个定时器的不同通道输出PWM,由于使用同一个定时器计数器是一样的,所以频率必须是一样的,ARR值也相同,所以分辨率也是一样的,占空比则可以以通过初始化每个通道改变其对应CCR值来改变,从而输出频率相同占空比可变的PWM波,想要输出不同频率不同的PWM波只能选择不同的定时器分别输出
5.STM32外部设备
舵机
输入PWM的信号要求:周期20ms即频率50HZ,高电平时间长短决定了舵机转动角度(也就是占空比决定了转动角度) 内置驱动板可直接用IO口驱动
注意:
接线的颜色正负极不要接反了,正极是5V供电,可以直接使用STLINK5V引脚供电,或者使用电源单独供电(要将供电电源的负极与STM32一起接地)。PWM信号线要接在指定GPIO信号线引脚上,这里是PA0
直流电机
由于GPIO口无法直接驱动大功率器件,所以使用了TB66612的H桥驱动电路控制电机正反转和速度
VM:一般和电机额定电压保持一致
VCC:和控制器电压保持一致(使用的STM32是3.3V供电,这里就接3.3V),功率不大可以和控制器共电源
GND:图中三个GND都是一样的,随便一个接地都行
STBY:待机控制引脚,接VCC则正常工作,接GND则处于待机状态不工作
其中PWMA、AIN1、AIN2控制AO1、AO2,PWMB、BIN1、BIN2控制BO1、BO2,AIN1和2随便接一个GPIO口就行,PWMA要接PWM信号输出端
AIN1和2的输出相当于H桥电路中的上下管的导通和关闭,可以决定电机旋转方向,也可以控制电机转或不转,因为一旦两个输出端输出相同的电压,就不会存在电压差就没有电流流过电机,电机自然不会转,而PWMA输出PWM信号,PWM输出高电平电机就转,低电平就不转,相当于一个可变化的使能引脚。(具体图片右下角的小图,L为低电平,H为高电平)
6.补充
- 定时器输出比较通道需要和GPIO口一一对应,对应关系见下表:
GPIO要使用复用模式,否则引脚控制权就不在TIM2手上,输出的PWM进不去引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //让片上外设TIM2来控制引脚,所以使用复用模式
- AFIO重映射功能
CH1的默认复用引脚对应PA0,重映射的是PA15,调用AFIO库函数可以实现CH1对应引脚改成PA15,这样PWM信号线就要接到PA15了,还要关闭PA15原本的复用引脚作用(谨慎使用,关闭之后就无法使用STLINK下载程序了,只能使用串口调试来重新改回来) - PWM波若输出在1KHZ时会听见像蜂鸣器一样的叫声,可以加大频率到20KHZ人耳就听不到了
三、TIM输入捕获
1.IC(输入捕获)介绍
- 指定电平跳变:程序手动设置上升沿还是下降沿触发
- 与OC区别:OC是比较CCR与CNT的值来输出PWM波,IC则是类似于中断一样,出现电平跳变这样的信号就会使CCR=CNT,CNT继续计数,直到下一次电平跳变信号出现,再次使CCR=CNT,使原来的CCR被新的CCR值覆盖,然后对多次不同的CCR处理来测频率,占空比等等数值
2.频率测量方法
测频法:由于每来一个上升沿就是一个周期,那么在一个固定周期T(闸门时间T)内,对上升沿个数计次,此时如果T=1s,那么测出来的就直接是频率,如果T不是1s,那么就把最后测出来的结果除以T就是频率了。
实现方法:令T=1s,使用输入捕获读取每一次CNT的值存入CCR,并清零CNT为下次计数做准备,CCR值就时频率
误差来源:想象一下如果频率无限低,那么周期无限大,也就是图中两个上升沿之间相隔无穷远,那么单位时间T内可能就只有一个甚至没有上升沿,这时候计次就为零,那么这个时候误差就很大,因为实际情况计次为0,不代表频率就一定为0
适用范围:
适用于高频信号,看图可知,单位时间T内,计次数量越多误差越小;
测频法结果更新慢(周期是固定的,一个周期过去频率才能测出来)
测频法数值稳定(可以看出它和滤波器原理相似,测出的频率是平均频率,所以数值平滑稳定)
测周法:在两个上升沿之间就是一个周期,周期的倒数就是频率,采用定时器计数的方式来计算一个周期的时间(比如定时器1s计一个数),定时器计数频率为fc,那么计一个数的时间就是1/fc,计N个数的时间就是N/fc,取倒数fc/N就是频率
实现方法:配置上升沿或者下降沿触发捕获,使CNT值锁存到CCR,然后清零CNT,为下一次计数做准备,由于计时器周期是已知的,所以这个时候CCR的值就是一个周期的时间,取倒数就是频率
误差来源:如果一个周期内频率无穷大,周期无穷小,那么两个上升沿之间的距离就无穷小,你可能一个数都没计到,一个周期就结束了,那么N就为零,在实际情况中尽管频率很大,N可能会为0,但是你不能够认为频率无穷大
适用范围:
适用于低频信号,看图可知,两个上升沿之间,计次数量越多误差越小;
测周法更新快(一个周期便可以得到结果,通常这个周期时间很短)
测周法数据跳变快(由于只测量了一个周期,外部信号干扰会对结果有较大影响)
正负一误差:两种方法都存在的固有误差。测频法如果计数到一半,但是刚好闸门时间T 结束了,由于计数N必须为整数,所以就要舍弃或者多进一位。测周法如果计数到一半,但是刚好到了下一个上升沿,这个时候也要保留或者舍弃这半个计数值。所以为了减小这种情况的误差,就应该多计一点数减小误差,所以可以说计数个数相同时误差也相同
中界频率:用于判断使用测周法,测频法哪种方法误差更小。由于两种方法都存在正负一误差,那么当存在一个频率使得两种方法误差计数值相等,也就是误差相等时,这个频率我们就称之为中界频率,大于中界频率使用测周法误差更小,小于中界频率使用测频法误差更小,所以我们令图中测频法和测周法中N相等,解方程解出fx得到的就是fm中界频率
3.输入捕获电路
- 异或门的作用:GPIO连接输入引脚,异或门连接输入引脚1,2,3,如果数据选择器选择上面从异或门出来的通道,那么输出值就是三个输入引脚异或后的值,如果选择下面,那么异或门就不起作用,三个输入引脚输入什么就输出什么。这样设计是为了驱动三相无刷电机,前三路输入引脚接在无刷电机三个霍尔传感器上面来检测转子位置,用于驱动换相电路的工作。
- CH1通道的信号在经过输入滤波器和边沿检测器后可以从TI1FP1(Timer Input 1 Filtered and Polarity Detected 1,即定时器输入1滤波及极性检测输出1)走IC1的通道,也可以通过TI1FP2走IC2的通道,其他CHx通道也都同理,这样设计的目的有两个:第一个是为了方便实现IC1的输入通道CH1和CH2的自由切换;第二个是为了实现PWMI模式(一个输入引脚有两路输出通道可以同时测PWM波的频率和占空比)
ICF[3:0]:用于配置滤波器
CC1S[1:0]:对数据选择器进行选择
ICPS[1:0]:对预分频进行配置(0,2,4,8分频)
CC1E:控制输出使能或者使能
4.主从触发模式
主模式:将定时器内部信号(OC1,OC1REF等等)映射到TRGO上用于控制其他外设
从模式:接收其他外设(ITR0,ITR1等等)或者自身外设(TI1FP1等等)的信号,用于控制自身定时器的运行
触发源选择:其他外设(ITR0,ITR1等等)或者自身外设(TI1FP1等等),用于触发从模式
5.输入捕获和PWMI基本结构
TI1FP1选择上升沿触发模式,对应左上角的图,TI1FP1从左边走是输入捕获使CCR1=CNT,从上面走是触发从模式使CNT=0,先CCR1=CNT,再CNT=0;左上图中绿色代表GPIO口电平翻转,不断执行CNT自增,CCR1=CNT,CNT=0这三个操作,看图可知代码思路大概就是配置时基单元,配置通道对应的GPIO口,配置TI1PF1为触发源还有其作为输入捕获的滤波和极性
这里有两个输出通道分别测频率和占空比,与普通输入捕获的区别在于需要将TI1FP1和TI1FP2分别设置为上升沿触发和下降沿触发,看左上角的图可知,上升沿时执行CCR1=CNT,CNT=0,下降沿时执行CCR2=CNT,两个上升沿之间CNT不断自增,所以CCR1里面存的是一个完整周期的计数值,CCR2里面只有高电平期间的计数值,计数值表示周期,所以CCR1取倒数就是频率,CCR2/CCR1就是高电平时间占总时间的比例,也就是占空比。
6.补充
PWM的ARR和CCR值决定的是占空比,PSC是为了改变PWM波的频率;由于使用的是测周法,所以IC的CCR决定了频率,ARR值往大的设置是为了防止其频率太小,周期太大,计数溢出。
四、TIM编码器接口
1.编码器的介绍
可以看一下STM3——EXTI我的这篇文章,里面有介绍
注意:
- 编码器接口只有输入捕获输出比较通道CH1和CH2拥有,与CH3和CH4无关
- 由于使用两相且两相交叉间隔90°,那么频率就相当于只使用一相的两倍,计数更加准确
2.编码器接口基本机构
基本思路:两个GPIO口分别对应AB两相,一个接在CH1的TI1FP1,一个接在CH2的TI2FP2,连在编码器接口上,编码器接口既要判断是CNT向上计数还是向下计数(编码器接口可托管时基单元的时钟输入信号,也就是说时基单元的时钟不再是72MHZ,而是从CH1和CH2输入信号的频率,类似于带有方向选择的外部时钟信号),还要控制外部时钟信号的输入,也就是以多大的频率计数
这里的极性选择可以改变计数方向,边沿检测要设置为上下沿都计数,若想要测速度则直接读取CNT的值就可以了,如果想要测方向,用测频法,每隔一段固定时间读取两相的状态,判断上升沿还是下降沿就行,再使CNT自增或者自减,然后清零CNT等下次测量
3.工作模式
这里TIFP1信号和TI2FP2信号表示两相输入的信号,TI1FP1对应的TI2表示连接TI1FP1的相的另一相的电平变化。如果是第3个模式就是,两个信号决定了CNT是向上计数还是向下计数。第1个模式就是只有TI1FP1的信号产生作用使CNT计数,TI2FP2无论是什么沿都不会计数。第2个模式也就是只有TI2FP2的信号产生作用了。(由于使用第三个模式,两相都起作用,也就是每隔1/4个周期就使CNT变化,而使用模式1或2都只有一相起作用,也就是每隔1/2个周期,才会使CNT变化,所以使用模式3,CNT变化频率也就更快,计数也就会更加准确 )
这里假设TI1FP1信号连接A相,TI2FP2信号连接B相,以在TI1和TI2上计数这个模式为例,对照上面表示正反转的表格,所以蓝色框框里面就是反转,红色的是正转,也就得到规律:正转CNT自加,反转CNT自减
4.补充
正交编码器抗噪声原理:
看图的毛刺部分可知当TI1信号连续变化时,TI2没有变化,这个时候根据正反转CNT变化规律,发现CNT反复加一减一最后CNT数值保持不变,过滤掉了噪声
代码部分
主要学习目的:熟练使用库函数,读懂代码,了解代码思路(声明:注释是up主写的,本人补充了一小部分)
1.定时器中断
思路可以照着定时器中断的基本结构看
定时器定时中断
特点:使用内部时钟自己计时计数
#include "stm32f10x.h" // Device header
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
*/
void Timer_Init(void)
{
/*开启时钟*/
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; //计数器模式,选择向上计数
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);
}
}
*/
定时器外部中断
特点:其他外设连接着GPIO,外设引起的GPIO引脚高低电平变化,相当于定时器时钟的频率变化,这个频率变化充当外部定时器给时基单元提供时钟,所以电平变化就会让计数器加1,直到到达自动重装载值产生中断,极性选择的作用:决定上升沿还是下降沿使计数器加一
#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);
//1.选择外部时钟模式2,时钟从TIM_ETR引脚输入
//注意TIM2的ETR引脚固定为PA0,无法随意更改
//2.关闭预分频
//3.极性不反转
//4.滤波器参数加到最大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);
}
}
*/
2.PWM驱动
PWM不需要定时器的中断,但是需要定时器计数的功能让ARR不断变化,再让ARR与CCR的值不断比较输出高低电平形成PWM波,再通过GPIO把波形输出给硬件的PWM信号线驱动器件运行
驱动LED呼吸灯
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; //让片上外设TIM2来控制引脚,所以使用复用模式
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.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
#endif
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
}
}
}
驱动舵机
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初始化*/
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; //计数器模式,选择向上计数
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的值
}
PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare2(uint16_t Compare);
#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)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500); //设置占空比
//将角度线性变换,对应到舵机要求的占空比范围上
}
Servo.h
#ifndef __SERVO_H
#define __SERVO_H
void Servo_Init(void);
void Servo_SetAngle(float Angle);
#endif
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显示角度变量
}
}
驱动直流电机
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初始化*/
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的值
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的值
}
PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare3(uint16_t Compare);
#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只能给正数
}
}
Motor.h
#ifndef __MOTOR_H
#define __MOTOR_H
void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
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显示速度变量
}
}
3.输入捕获模式&PWMI模式
整体思路并不复杂,一个GPIO口用定时器2输出比较输出PWM波,一个GPIO口用定时器3输入捕获测频率和占空比,我们就分别配置对应定时器模式就够了,基本的思路就是打开时钟,初始化GPIO,配置定时器(包括使用哪一个定时器,使用内部时钟还是外部时钟,时基单元的配置,输入捕获或者输出比较的配置,是否要用主从触发模式,最后使能时钟)。
输入捕获模式测频率
IC.c
#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的时基单元
/*输入捕获初始化*/
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)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //测周法得到频率fx = fc / N,这里不执行+1的操作也可
}
IC.h
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
#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_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate); //设置PSC的值
}
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
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
PWM_SetCompare1(50); //PWM占空比Duty = CCR / 100
while (1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5); //不断刷新显示输入捕获测得的频率
}
}
PWMI模式测频率占空比
IC.c
#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模式
/*选择触发源及从模式*/
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的操作也可
}
IC.h
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GetDuty(void);
#endif
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模块提供输入捕获的测试信号*/
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); //不断刷新显示输入捕获测得的占空比
}
}
4.编码器接口测速
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;
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的值
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_ICInitStructure.TIM_ICPolarity =TIM_ICPolarity_Rising; //表示高低电平不反转而不是高电平有效
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的输入捕获通道
/*编码器接口配置*/
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//配置编码器模式以及两个输入通道是否反相
//注意此时参数的Rising和Falling已经不代表上升沿和下降沿了,而是代表是否反相
//此函数必须在输入捕获初始化之后进行,否则输入捕获的配置会覆盖此函数的部分配置
/*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;
}
Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
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更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
一些小技巧
- 若想修改PWM波的频率又不修改占空比,可以修改时基单元的PSC预分频