江科大STM32开发板介绍及使用指南
参考:
https://blog.csdn.net/weixin_54742551/article/details/132409170?spm=1001.2014.3001.5502
https://blog.csdn.net/m0_61712829/article/details/132434192
https://blog.csdn.net/Johnor/article/details/128539267?spm=1001.2014.3001.5502
SPI:https://blog.csdn.net/weixin_62127790/article/details/132015224?spm=1001.2014.3001.5502
目录
1、STM32简介

ARM
我们主要学习的就是STM32的外设。
系统结构
三个总线icode指令总线(加载程序指令)、dcode数据总线(加载数据,比如常量何调试数据)、system系统总线。icode与dcode总线主要用来连接flash闪存(flasd存储的是编写的程序)。
ICode((Instruction)指令总线):
程序编译后的指令存放在内部FLASH中,M3内核通过ICode总线取指,然后再执行指令。
DCode((Data)数据总线):
程序有常量和变量。const修饰的变量为常量存储在内部FLASH中,变量不管是全局变量还是局部变量都存放在SRAM中。由于数据可以被DCode和DMA总线访问,所以就需要经过总线矩阵来仲裁。
Systme(系统总线):
系统总线主要用来访问外设寄存器(即读写寄存器就是通过该总线完成)。
存储器和寄存器映射:https://blog.csdn.net/weixin_58038211/article/details/128553364;https://blog.csdn.net/zywcxz/article/details/131035001
SRAM用于存储程序运行时的变量数据
AHB(先进高性能总线)系统总线用于挂载主要的外设(挂载最基本或者性能比较高的外设,比如复位和时钟控制这些基本的电路)sdio也是挂载在ahb上的。
两个桥接,接到了APB1(先进外设总线)和APB2两个外设总线上(用来连接一般的外设)
AHB和APB的总线协议、总线速度还有数据传输格式的差异,所以中间需要加两个桥接来完成数据的转换和缓存
AHB的整体性能比apb高一些,APB2的性能比APB1高一些。
APB2一般和AHB同频率都是72MHz,APB1一般是36MHz,所以APB2连接的一般是外设中稍微重要的部分(例如GPIO端口,还有一些外设的一号选手比如USART1、SPI1、TIM1、TIM8(高级定时器)、ADC、EXTI、AFIO),Apb1连接次要一点的外设2、3、4号外设还有DAC\PWR\BKP等。
DMA是CPU的小秘书,比如一些大量的数据搬运这样简单且重复干的事情,让cpu来干会浪费时间。
DMA通过DMA总线连接到总线矩阵上,可以拥有和cpu一样的总线控制权,用于访问外设小弟,当需要DMA搬运数据时,外设就会通过请求线发送DMA请求,然后DMA就会获的总线控制权,访问并转运数据,整个过程不需要cpu的参与
引脚定义:
S代表电源、I代表输入、O代表输出、I/O代表输入输出、FT代表代表能容忍5V电压,没有FT的只能容忍3.3V电压,如果没有FT的需要接5V的电平,就需要加装电平转换电路了。
如果我们想让STM32正常工作,首先就需要把电源部分和最小系统部分的电路连接好,也就是上表中标注红色和蓝色的部分。
启动配置的作用是指定程序开始运行的位置,一般情况下,程序都是在Flash程序存储器开始执行。但是在某些情况下,我们也可以让程序在别的地方开始执行。
第二种启动模式(串口下载用的,区别于使用Jlink):系统存储器存的就是STM32中的一段BootLoader程序,BootLoader的作用就是接收串口的数据,然后刷到主闪存中
第三种启动模式:主要用来程序调试的,用的比较少。
BOOT引脚的值是在上电一瞬间有效的,之后就随便了。查看上面的引脚分布图,发现BOOT1和PB2是在同一个引脚上,也就是在上电瞬间是BOOT1功能,在第四个时钟过后,就是PB2的功能了。
供电部分电路:
在3.3V和GND之间,一般都会连接一个滤波电容,保证供电电压的稳定。
VBAT接的备用电池,用来给RTC和备份寄存器服务的。如果不用备用电池,VBAT可以直接接3.3V或者悬空。
晶振电路:
接了一个8MHz的主时钟晶振,经过内部锁相环倍频,得到72MHz的主频。晶振连接到STM32的5、6号引脚。另外还需要接两个20pF的电容,作为起振电容,电容的另一端接地即可。
如果需要RTC功能,还需要再接一个32.768KHz的晶振,电路和这个一样接到3、4号引脚。OSC32就是32.768KHz晶振的意思。为什么要用32.768KHz?因为32768是2的15次方,内部RTC电路经过2的15次方分频,就可以生成1S的时间信号了。
复位电路:
这个复位电路是一个10k的电阻和0.1uF的电容组成的,用来给单片机提供复位信号。NRST接到STM32的7号引脚,NRST是低电平复位的,当这个复位电路在上电的瞬间,电容是没有电的,电源通过电阻开始向电容充电,并且此时电容呈现的是短路状态,NRST就会产生低电平,当电容逐渐充满电时,电容就相当于短路,此时、NRST就会被R1上拉为高电平。那上电瞬间的波形就是先低电平,然后逐渐高电平,这个低电平就可以提供STM32的上电复位信号。当然电容充电还是非常快的,所以在我们看来单片机在上电的一瞬间复位了,这就是复位电路的作用。
电容左边还并联了一个按键,提供手动复位的功能。按键按下式,电容被放电,并且NRST引脚也通过按键被直接接地了,相当于手动产生了低电平复位信号。按键松手后,NRST又回归高电平,此时单片机就从复位状态转为工作状态。一般复位按键都是在一个小孔里,拿针戳一下设备就复位了。
启动配置:
跳线帽的方式。接拨码开关也可以。
2、软件安装、新建工程
库函数底层也是操作寄存器,只是封装了一下,方便我们使用。详细函数都在各个外设寄存器头文件里定义好了,可以去这些头文件里查看各接口的使用方法(入参、出参),用多了掌握套路就容易了,这样就不用查看寄存器和芯片手册了。
外设篇
片内外设、片上外设和片外外设的区别
3、GPIO
GPIO简介
GPIO(General Purpose Input Output)通用输入输出口。
可配置为8种输入输出模式。
引脚电平:0V~3.3V,部分引脚可容忍5V。(0v就是低电平是数据0,3.3v是高电平是数据1。容忍5v意思是可以在这个端口输入5v的点电压,也认为是高电平,但是对于输出而言,最大就只能输出3.3v,因为供电就只有3.3v,具体哪些端口能容忍5v,可以参考一下stm32的引脚定义,带FT的就是可以容忍5v,不带FT的就只能接入3.3v电压)
输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等。(后面文章显示的LED和蜂鸣器的程序现象,就使用到了GPIO的输出模式。另外在其他的应用场景,只要是可以用高低电平来进行控制地方都可以用GPIO来完成;如果控制的是功率比较大的设备,只需要再加入驱动电路即可;此外,还可以用GPIO来模拟通信协议,比如I2CC、spi或某个芯片特定协议,我们都可以用GPIO的输出模式来模拟其中的输出时序部分)
输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。(输入模式最常见的就是读取按键了,用来捕获我们的案件按下事件;另外,也可以读取带有数字输出的一些模块,比如,光敏电阻模块、热敏电阻模块等;如果这个模块输出的是模拟量,那GPIO还可以配置成模拟输入模式,再配合内部的ADC外设,就能读取端口的模拟电压了;除此之外,模拟通信协议时,接收线上的通信数据,也是靠GPIO的输入来完成的)
GPIO的基本结构
如下,为GPIO的整体构造,其中左边的是APB2外设总线;在stm32中所有的GPIO都是挂载在APB2外设总线上的,其中GPIO外设的名称都是按照GPIOA、GPIOB等等这样来命名的,每个GPIO外设,总共有16个引脚,编号是从0到15,GPIO的第0号引脚,我们一般把它称为PA0,接着第一号就是PA1…PA15以此来命名;
在每个GPIO模块内,组要包含了寄存器和驱动器,寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,这样就可以完成输出电平和读取电平的功能了,寄存器的每一位对应一个引脚,其中,输出寄存器写1,对应的引脚就会输出高电平,写0就会输出低电平,输入寄存器读取为1,就证明对应的端口目前是高电平,读取为0,就是低电平;
因为STM32是32位单片机,所以STM32内部的寄存器都是32位的,但这个端口只有16位,所以这个寄存器只有低16位对应的有端口,高16位是没有用的;
驱动器是用来增加信号的驱动能力,寄存器只负责存储数据,如果要进行点灯这样的操作,还是需要驱动器来负责增大驱动能力。
如下,这些就是GPIO的整体基本结构了。
驱动器是用来增加信号驱动能力的,寄存器只负责存储数据,如果进行点灯这样操作的话,需要驱动器增大驱动能力。
GPIO位结构(每一位的具体电路结构)
如下图为,stm32参考手册中的GPIO位结构的电路图。
左边三个就是寄存器,中间部分是驱动器,右边是某一个IO口的引脚。
整体结构可以分为两个部分,上面是输入部分,下面是输出部分。
一、输入部分
1.首先是这个IO引脚,这里接了两个保护二极管,这个是对输入电压进行限幅的,上面二极管接VDD,3.3V,下面二极管接VSS,0V;如果输入电压比3.3v还要高,那上方这个二极管就会导通,输入电压产生的电流就会直接流入VDD而不会流入内部电路,这样就可以避免过高的电压对内部电路产生伤害。
如果输入电压比0v还要低,这个电压是相对与VSS的电压,所以是可以有负电压的,那这时下方这个二极管就会导通,电流会从VSS直接流出来,电流会从VSS直接流出去,而不会从内部电路汲取电流,也是可以保护内部电路的。
如果输入电压在0-3.3v之间,那两个保护二极管均不会导通,这时二极管对电路没有影响,这就是保护二极管的用途
2.上拉和下拉电阻
上拉和下拉的作用:是为了给输入提供一个默认的输入电平,因为对应一个数字的端口,输入不是高电平就是低电平;如果输入引脚哈都不接,这时输入就会处于一个浮空状态,引脚的输入电平极易受外界干扰而改变;为了避免引脚悬空导致的输入数据不稳定,我们就需要在这里加上上拉或下拉电阻
上拉电阻至VDD,下拉电阻至VSS,这个开关是可以通过程序进行配置的。
上面导通、下面断开,就是上拉输入模式;上面断开、下面导通,就是下拉输入模式;上面断开、下面断开,就是浮空输入模式。
如果接入上拉电阻,当引脚悬空时,还有上拉电阻来保证引脚的高电平,所以上拉输入是默认为高电平的输入模式,下拉也是同理。
上拉电阻和下拉电阻的阻值都是比较大的,是一种弱上拉和弱下拉 ,目的是尽量不影响正常的输入操作。
3.TTL肖特基触发器
实际上这个应该是斯密特触发器(应该是一个翻译错误)。如下:
施密特触发器的作用就是对输入电压进行整形的,它的执行逻辑是,如果输入电压大于某一阈值,输出就会瞬间升为高电平,如果输入电压小于某一阈值,输出就会瞬间降为低电平,这样可以有效的避免由于信号波动造成的输出抖动现象。
例子:因为这个引脚的波形是外界输入的(IO口输入),虽然是数字信号,实际情况可能会产生各种失真,比如,如下波形夹杂了波动的高低变化的电平信号(下图,红色线),如果没有施密特触发器,那很有可能因为干扰而导致误判,如果有了施密特触发器,那比如定一个阈值上限和下限(下图中绿色线),高于上限输出高,低于下限输出低,如下图蓝色为施密特信号,图中的第一个蓝色圈虽然由于波动再次低于上限了,但是对于施密特触发器来说,只有高于上限或者低于下限,输出才会变化,所以此时低于上限的情况,输出并不会变化,而是继续维持高电平,然后直到下次低于下限时,才会转为低电平,第二个蓝色圈信号即使在下限附近来回横跳,因为没有跳到上限上面去,所以输出仍然是稳定的,直到下一次高于上限,输出才会变成高电平,如下蓝色线就是施密特触发器的输出信号了,可以看到,相比较输入信号,经过整形的信号就很完美。在这里使用了两个比较阈值来进行判断,中间留有一定的变化范围(上下绿色阈值线),可以有效的避免因信号波动造成的输出抖动现象。
施密特前(右)是模拟量,后(左)是01组成的数字量
接下来经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取输入数据寄存器对应某一位的数据,就可以知道端口的输入电平了。
最后上面这还有两路线路,这些就是连接到片上外设的一些端口,其中有模拟输入,这个是连接到ADC上的,因为ADC需要接收模拟量,所以这根线是接到施密特触发器前面的;另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的,比如串口的输入引脚等,这根线接收的是数字量,所以在施密特触发器后面。
二、输出部分
1、输出部分可以由输出数据寄存器或片上外设控制,两种控制方式通过这个数据选择器(输出控制左侧梯形)接到输出控制部分。
如果选择通过输出数据寄存器进行控制,就是普通的IO口输出,写这个输出数据寄存器的某一位就可以操作对应的某个端口了。
2、最左侧是位设置/清除寄存器:这个可以用来单独操作输出数据寄存器的某一位,而不影响其它位。因为这个输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以如果想单独控制其中某一个端口而不影响其他端口的话,就需要一些特殊的操作方式。
3.输出控制之后就接到了两个MOS管
上面是P-MOS,下面是N-MOS,这个MOS管就是一种电子开关,我们的信号来控制开关的导通和关闭,开关负责将IO口接到VDD或者VSS,这里可以选择推挽、开漏或关闭三种输出方式。
在推挽输出模式下,P-MOS和N-MOS均有效,数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,就是输出高电平,数据寄存器为0时,上管断开,下管导通,输出直接接到VSS,就是输出低电平,这种模式下,高低电平均有较强的驱动能力,所以推挽输出模式也可以叫强推输出模式。在推挽输出模式下,STM32对IO口具有绝对的控制权,高低电平都由STM32说的算。
在开漏输出模式下,这个P-MOS是无效的,只有N-MOS在工作,数据寄存器为1时,下管断开,这时输出相当于断开,也就是高阻模式;数据寄存器为0时,下管导通,输出直接接到VSS,也就是输出低电平;这种模式下,只有低电平有驱动能力,高电平是没有驱动能力的。那这个模式有什么用呢,这个开漏模式可以作为通信协议的驱动方式,比如12C通信的引脚,就是使用的开漏模式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰,另外开漏模式还可以用于输出5V的电平信号。
比如在IO口外接一个上拉电阻到5V的电源,当输出低电平时,由内部的N-MOS直接接VSS,当输出高电平时,由外部的上拉电阻拉高至5V,这样就可以输出5V的电平信号,用于兼容一些5V电平的设备,这就是开漏输出的主要用途。
开漏模式下,输出1时,两个mos管都相当于关断,左侧相当于断路(高阻模式)。外接5V的电能只能流向右侧,故输出5V。反之,输出0时,左下方mos管导通,外接5V的电能流到左下方Vss,且两者之间几乎没有电压降,可看做5V电压降在了上拉电阻上,故引脚输出0V
关闭状态输出方式
剩下的一种状态就是关闭,这个是当引脚配置为输入模式的时候,这两个MOS管都无效,也就是输出关闭,端口的电平由外部信号来控制。
GPIO8种工作模式
通过配置GPIO的端口配置寄存器,上面的位结构的电路就会根据我们的配置进行改变(比如,开关的通断、N-MOS和P-MOS是否有效、数据选择器的选择等),端口可以配置成以下8种模式:
1.首先是前三个,浮空输入、上拉输入、下拉输入
这三个模式的电路结构基本是一样的,区别就是上拉电阻和下拉电阻的连接,它们都属于数字的输入口,那特征就是,都可以读取端口的高低电平,当引脚悬空时,上拉输入默认是高电平,下拉输入默认是低电平,而浮空输入的电平是不确定的,所以在使用浮空输入时,端口—定要接上一个连续的驱动源,不能出现悬空的状态。
那我们来看一下这三种模式的电路结构,这里可以看到,在输入模式下,输出驱动器是断开的,端口只能输入而不能输出,上面这两个电阻可以选择为上拉工作、下拉工作或者都不工作,对应的就是上拉输入、下拉输入和浮空输入,然后输入通过施密特触发器进行波形整形后,连接到输入数据寄存器。
另外右边这个输入保护这里,上面写的是VDD或者VDD_FT,这就是3.3V端口和容忍5V端口的区别。这个容忍5V的引脚,它的上边保护二极管要做一下处理,要不然这里直接接VDD 3.3V的话,外部再接入5V电压就会导致上边二极管开启,并且产生比较大的电流,这个是不太妥当的。
- 模拟输入
这个模拟输入可以说是ADC模数转换器的专属配置了,特征是GPIO无效,引脚直接接入内部ADC。
这里输出是断开的,输入的施密特触发器也是关闭的无效状态,所以整个GPIO的这些都是没用的,那么只剩下从引脚直接接入片上外设,也就是ADC,所以,当我们使用ADC的时候,将引脚配置为模拟输入就行了,其他时候,一般用不到模拟输入。
3.开漏输出和推挽输出
开漏输出和推挽输出,这两个电路结构也基本一样,都是数字输出端口,可以用于输出高低电平,区别就是开漏输出的高电平呈现的是高阻态,没有驱动能力,而推挽输出的高低电平都是具有驱动能力的。
输出是由输出数据寄存器控制的,如果P-MOS无效,就是开漏输出;如果P-MOS和N-MOS都有效,就是推挽输出。另外我们还可以看到,在输出模式下,输入模式也是有效的,但是在我们刚才的电路图,在所有输入模式下,输出都是无效的,这是因为,一个端口只能有一个输出,但可以有多个输入,所以当配置成输出模式的时候,内部也可以顺便输入一下,这个也是没啥影响的。
4.复用开漏输出和复用推挽输出
最后我们再来看一下复用开漏输出和复用推挽输出,这俩模式跟普通的开漏输出和推挽输出也差不多。
可以看到通用的输出/数据寄存器没有连接的,引脚的控制权转移到了片上外设,由片上外设来控制,在输入部分,片上外设也可以读取引脚的电平,同时普通的输入也是有效的,顺便接收一下电平信号。
在GPIO的这8种模式中,除了模拟输入这个模式会关闭数字的输入功能,在其他的7个模式中,所有的输入都是有效的。
参考手册
当使用片上外设的引脚时,可以参考这个表里给的配置
首先是GPIO配置寄存器,每一个端口的模式由4位进行配置,16个端口就需要64位,所以这里的配置寄存器有两个,一个是端口配置低寄存器,一个是端口配置高寄存器,可以看介绍进行配置。
GPIO的输出速度可以限制输出引脚的最大翻转速度,这个设计出来,是为了低功耗和稳定性的,我们一般要求不高时一般配置成50MHz就可以了。
如下为,端口输入数据寄存器。
就是上面GPIO位结构的输入数据寄存器,里面的低16位对应16个引脚,高16位没有使用
如下为,端口输出数据寄存器,也就是上面GPIO位结构的输出数据寄存器,同样,低16位对应16个引脚,高16位没有使用
如下为,端口位设置/清除寄存器,也就是上面GPIO位结构的那部分寄存器,这个寄存器的低16位是进行位设置的,高16位是进行位清除的。写1就是设置或者清除,写0就是不产生影响。
如下为,端口位清除寄存器,低16位是进行位清除的。
这个是为方便操作设置的,如果只想单一的进行设置或者位清除,位设置用上面寄存器,位清除用下面这个寄存器,因为在设置和清除时,使用的都是低16位的数据,这样就方便一些;如果想对多个端口同时进行位设置和位清除,那就使用第一个寄存器就行了,这样可以保证位设置和位清除的同步性,当然你要对信号的同步性要求不高的话,先位设置再位清除也是没问题的
如下为,端口配置锁定寄存器。
这个可以对端口的配置进行锁定,防止意外更改,使用方法看介绍,这个我们暂时用的不多
目前,有关stm32内部的GPIO外设,我们就讲完了
接下来,我们看一下stm32外部的设备和电路。
LED和蜂鸣器介绍
LED电路符号如下,左边是正级,右边是负极、
如下为LED实物图,如果引脚没有剪过,长脚为正极,短脚为负极 。通过LED内部也可以看正负极,较小的一半是正极,较大的一半是负极
有源蜂鸣器内部电路如下左图,这里用了一个三极管开关进行驱动,我们将VCC和GND分别接上正负极的供电,然后中间引脚2接低电平,蜂鸣器就会响,接高电平,蜂鸣器就关闭,
LED和蜂鸣器的硬件电路
1.如下两个图是使用stm32的GPIO口驱动LED的电路。
这里的限流电阻一般都是要接的,一方面它可以防止LED因为电流过大而烧毁,另一方面它可以调整LED的亮度,如果你觉得LED太亮可以适当的增大限流电阻的阻值。
针对选择电平驱动哪个方式:就得看IO口高低电平的驱动能力如何了,上面讲到,GPIO的推挽输出模式下,高低电平均有较强的驱动能力,所以两种方式都可以;在单片机的电路里,一般倾向使用第一种,低电平驱动的方式,因为很多单片机或者芯片,都使用了高电平弱驱动,低电平强驱动的规则,这样可以一定程度上避免高低电平打架,所以使用高电平驱动能力弱那就不能使用第二种连接方式了。
下图是低电平驱动的电路,LED正极接3.3v,负极通过一个限流电阻接到PA0上,当PA0输出低电平时,LED两端就会产生电压差,就会形成正向导通的电流,这样LED就会点亮了;当PA0输出高电平时,因为LED两端都是3.3v的电压,不会形成电流,所以高电平LED就会熄灭。
下图是高电平驱动的电路。LED负极接到GND,正极通过一个限流电阻接到PA0上,这时就是高电平点亮,低电平熄灭。
2.下面为蜂鸣器电路
这里使用了三极管开关的驱动方案,三极管开关是最简单的驱动电路了,对于功率稍微大一点的 ,直接用IO口驱动会导致STM32负担过重,这时可以用一个三极管驱动电路来完成驱动任务
需要注意,PNP的三极管最好接在上边,NPN的三极管最好接到下边,这是因为三极管的通断是需要在发射极和基极产生一定的开启电压的,如果将负载接在发射极这边,可能会导致三极管不能开启。
下图为PNP三极管的驱动电路,三极管的左边是基极,带箭头的是发射极,剩下的是集电极。左边的基极给低电平,三极管就会导通,再通过3.3V和GND就可以给 蜂鸣器提供驱动电流了。基极给高电平,三极管截止,蜂鸣器没有电流。
下图为NPN三极管的驱动电路,同样,左边是基极,带箭头的是发射极,剩下的是集电极;它的驱动逻辑和上面的是相反的,基极给高电平导通,低电平断开。
面包板的使用方法
当我们把原件的引脚插到面包板的孔里时,它内部的金属爪就会抓住引脚;
金属爪的排列规律是:中间的金属爪是竖着放的,上下四排是连在一个的四个整体的金属爪。那就对应这个面包板的孔的连接关系。竖着的五个孔内部是连接在一起的,如下,这样我们元件插在一纵排的不同孔位时,内部的金素爪就实现了线路的连接。
上下四排孔整体是连在一起的,这四排是用于供电的,标有正负极;如果我们需要供电,就从上下的孔位中,用跳线印出来即可。另外,再说明一下,这个供电的引脚,有的面包板并不是一整排都是连接的(如果中间是断开的,用跳线再连接起来)
演示:若用面包板实现电源点亮一个LED等的电路
首先,把上面两排的供电引脚接上电源的正负极,然后用跳线将正极引下来到一个孔(5孔其1)里,然后在纵向下面的孔,横着插一个限流电阻到右边的孔,横着插一个LED到右边的孔,然后再用跳线把右边引到负极。这样就可以了。
面包板正面如下:
面包板背面(金属爪)
金属爪示意图
LED闪烁、LED流水灯、蜂鸣器
外设的GPIO配置查看
STM32F10xxx参考手册 P110有列出了各个外设的引脚配置,例如:
实战1: 如何进行基本的GPIO输入输出
操作STM32的GPIO总共需要3个步骤:
第一步,使用RCC开启GPIO的时钟
涉及的函数如下:
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
作用:使能(开启)或失能(关闭)APB2外设时钟
参数说明:
其它两个外设时钟函数也是大差不差的,根据不同外设选择相应的函数开启就行。
第二步,使用GPIO_Init函数初始化GPIO
涉及的函数如下:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
作用:根据GPIO_InitStruct中的指定参数初始化GPIOx外设。
参数说明:
指定要配置的GPIO引脚。
其中 GPIO InitTypeDef结构体配置信息如下:
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
参数说明:
引脚的工作模式如下:
举例:根据LED闪烁接线图设置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
第三步,使用输出或者输入的函数控制GPIO口
涉及的函数如下:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:设置所选数据端口位。对某个端口写1,也就是高电平
参数说明:
类似的还有:GPIO_ResetBits 函数,同样的用法,只不过这个函数是写0
3-1.LED闪烁
接线图:
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main (void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure); //GPIO配置初始化
//GPIO_SetBits(GPIOA,GPIO_Pin_0);
while(1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
Delay_ms(500);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
Delay_ms(500);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)0);
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)1);
Delay_ms(500);
}
}
3-2.LED流水灯
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启时钟
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
// GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
// |或运算 按键右转 前三个引脚或后为0x0111 ...
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
while(1)
{
GPIO_Write(GPIOA,~0x0001);// 0000 0000 0000 0001 取反前~
Delay_ms(500);
GPIO_Write(GPIOA,~0x0002);// 0000 0000 0000 0010
Delay_ms(500);
GPIO_Write(GPIOA,~0x0004);// 0000 0000 0000 0100
Delay_ms(500);
GPIO_Write(GPIOA,~0x0008);// 0000 0000 0000 1000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0010);// 0000 0000 0001 0000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0020);// 0000 0000 0010 0000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0040);// 0000 0000 0100 0000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0080);// 0000 0000 1000 0000
Delay_ms(500);
}
}
3-3.蜂鸣器
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
//RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_Initstruct;
GPIO_Initstruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Initstruct.GPIO_Pin = GPIO_Pin_12;
GPIO_Initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_Initstruct);
while(1)
{
// GPIO_ResetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(100);
// GPIO_SetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(100);
// GPIO_ResetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(100);
// GPIO_SetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(700);
}
}
GPIO输入模式硬件以及c语言下面知识点是库函数反复出现的东西,了解后会对库函数的执行逻辑更加清晰明了。另外还有c语言的指针,在后面文章会单独进行阐述。
按键简介
按键:常见的输入设备,按下导通,松手断开
按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动
传感器模块简介
传感器模块:传感器元件(传感器模块就是利用传感器元件,比如如下图的光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化(比如光线越强,光敏电阻的阻值就越小),通过与定值电阻进行串联分压即可得到模拟电压输出,再通过电压比较器进行二值化(二值化就是要么是高要么是低)即可得到数字电压输出
如下为传感器模块的基本电路,详细介绍。
这个N1就是传感器元件所代表的可变电阻,它的阻值可以根据环境的光线、温度等模拟两进行变化。
对于光敏电阻传感器来说,这个N1就是光敏电阻;对于热敏电阻传感器来说,这个N1就是热敏电阻;对应这个红外传感器来说,这个N1就是一个红外接收管
N1上面的R1,是和N1进行分压的定值电阻,R1和N1串联,一端接VCC一端接VSS,这就构成了基本的分压电路,AO电压就由R1和N1两个电阻的分压得到。
N1左边的C2是一个滤波电容,它是为了给中间的电压输出进行滤波的,用来滤除一些干扰,保证输出电压波形的平滑。一般我们在电路里遇到这种一端接在电路中,另一端接地的电容都可以考虑一下这个是不是滤波电容的作用,如果是滤波电容的作用,那这个电容就是用来保证电路稳定的。并不是电路的主要框架,这时候我们在分析电路的时候,就可以先把这个电容给抹掉,这样就可以使我们的电路分析更加简单。
那我们把这个电容抹掉,整个电路的主要框架就是定值电阻和传感器电阻的分压电路了。在这里可以用分压定理来分析一下传感器电阻的阻值变化对输出电压的影响,当然我们还可以用上下拉电阻的思维来分析,当这个N1阻值变小时,下拉作用就会增强,中间的AO端的电压就会拉低,极端情况下,N1阻值为0,AO输出被完全下拉,输出0V;当N1阻值变大,下拉作用就会减弱,中间的引脚由于R1的上拉作用,电压就会升高极端情况下,N1阻值无穷大,相当于断路,输出电压被R1拉高至VCC
二值化输出是通过这个LM393芯片来完成,这个LM393是一个电压比较器芯片,里面有两个独立的电压比较器电路,然后剩下的是VCC和GND供电。这个电压比较器其实就是一个运算放大器,当这个同相输入端的电压大于反相输入端的电压时,输出就会瞬间升高为最大值也就是输出接VCC,反之当同相输入端的电压小于反相输入端的电压时,输出就会瞬间降低为最小值也就是输出接GND,这样就可以对一个模拟电压进行二值化了,这里同相输入端IN+接到了AO这里,就是模拟电压端。
IN-呢,接了一个电位器,这个电位器的接法也是分压电阻的原理,拧动电位器,IN-就会生成一个可调的阈值电压,两个电压进行比较,最终输出结果就是DO,数字电压输出,DO最终就接到了引脚的输出端,这就是数字电路的由来,然后右边这里还有两个指示灯电路,左边的是电源指示灯,通电就亮;右边的是DO输出指示灯,它可以指示DO的输出电平,低电平点亮,高电平熄灭。那右边DO这里还多了个R5上拉电阻,这个是为了保证默认输出为高电平的。
可以用上下拉电阻的思维分析传感器电阻的阻值变化对输出电压的影响,如下:
AO这个输出端可以把它想象成一个水平杆子(下图红色直线),R1上拉电阻相当于拴在上方的弹簧,将杆子向上拉,N1下拉电阻相当于拴在地面的弹簧,将杆子向下拉;电阻的阻值越小,弹簧的拉力就越强,杆子的高度就相当于电路中的电压,杠子向拉力强的一端偏移(取决于两个弹簧的弹力之差);如果上下弹簧拉力一致,杆子处于居中位置也就是电路输出VCC/2的电压;如果上面的阻值小,拉力强,输出电压就会变高;反之下面的阻值小,输出电压就会变低 ;如果上下拉电阻的阻值都为0,就是两个无穷大的力在对抗,在电路中呈现的就是电源短路(应该避免)。单片机电路中会常出现这种上拉下拉电阻,比如弱上拉,强上拉等(强和弱就是指电阻阻值的大小,也就是这个弹簧拉力大小) ,最终输出电压就是在弹簧拉扯下最终杆子的高低。
按键和传感器硬件电路
下接按键的方式如下,一般来说我们用下接按键的方式,这个原因和LED的接法类似,是电路设计习惯和规范;下左图中,按键按下时,PA0直接下拉到GND,此时读取PA0口的电压就是低电平,在这种接法下,必须要求PA0是上拉输入模式,使按键松下,还是高电平。下右图,外部接了一个上拉电阻,当按键松手时,引脚由于上拉作用,保持为高电平,此时PA0引脚就可以配置为浮空输入或者上拉输入。
上接按键的方式(仅了解)如下,左图1中,要求将PA0必须配置成下拉输入模式,松手时,引脚会回到默认值低电平。
传感器模块电路如下,DO是数字输出端口,PA0用于读取数字量。
按键控制LED&光敏传感器控制蜂鸣器
知识点:
上拉输入:若GPIO引脚配置为上拉输入模式,在默认情况下(GPIO引脚无输入),读取的GPIO引脚数据为1,即高电平。
下拉输入:若GPIO引脚配置为下拉输入模式,在默认情况下(GPIO引脚无输入),读取的GPIO引脚数据为0,即低电平。
按键控制LED
驱动.c文件用来存放驱动程序的主体代码;驱动.h用来存放驱动程序可以对外提供的函数或变量声明。
.h文件要添加一个防止头文件重复包含的代码,格式固定,如下
#ifndef __LED_H //如果没有定义LED这个字符串
#define __LED_H //那么就定义这个字符串
//函数和变量声明放在这里
#endif //是和ifndef组成的括号
//空行结尾
//如下函数用于读取输入模式某个位,返回值就是输入数据寄存器的某一位的值
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
//如下函数用于读取输入模式整个输入数据寄存器的值
uint8_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
//如下函数用于输出模式下,用来看一下自己输出的是什么
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
//如下函数,少了个bit,用来读取整个输出寄存器
uint8_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
程序代码如下:
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h" //包含led的头文件
#include "key.h"
uint8_t keynum; //全局变量,用来存键码的返回值,与局部变量作用域不同
//局部变量只能在本函数使用,全局变量每个函数都可使用
//在函数里优先使用自己的局部变量,如果没有才会使用外部的全局变量
int main(void)
{
led_init(); //完成led的初始化,默认低电平
key_init(); //初始化按键
while(1)
{
keynum = key_getnum(); //不断读取键码值,放在keynum变量里
if(keynum == 1) //按键1按下
{
led1_turn(); //电平翻转,led状态取反,需用到GPIO_readoutput函数
}
if(keynum == 2)
{
led2_turn();
}
// led1_on();
// led2_off();
// Delay_ms(500);
// led1_off();
// led2_on();
// Delay_ms(500);
}
}
lcd.c
#include "stm32f10x.h" // Device header
void led_init(void)//初始化led
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//赋值结构体
GPIO_InitTypeDef GPIO_InitStructA; //结构体变量名GPIO_InitStructA
GPIO_InitStructA.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructA.GPIO_Pin = GPIO_Pin_1 |GPIO_Pin_2; //按位或来选择多个引脚
GPIO_InitStructA.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructA); //使用的是地址传递,将指定的GPIO外设初始化好
GPIO_SetBits(GPIOA,GPIO_Pin_1 | GPIO_Pin_2); //这样后,初始化led是高电平是熄灭的
}
void led1_on(void) //点亮led1,就是pa1口
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1); //低电平点亮
}
void led1_off(void) //熄灭led1,就是pa1口
{
GPIO_SetBits(GPIOA,GPIO_Pin_1); //高电平熄灭
}
void led1_turn(void) //led1状态取反,电平翻转
//GPIO_ReadOutputDataBit这个函数,来读取端口输出的是什么
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_1); //状态取反,0变1
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);//状态取反,1变0
}
}
//下方雷同
void led2_on(void)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
void led2_off(void)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
void led2_turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
}
lcd.h
#ifndef __LED_H
#define __LED_H
void led_init(void); //对模块外部声明,这个函数是可以被外部调用的函数
void led1_on(void);
void led1_off(void);
void led2_on(void);
void led2_off(void);
void led1_turn(void);
void led2_turn(void);
#endif
key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
void key_init(void) //按键初始化函数
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟
//配置端口模式
GPIO_InitTypeDef GPIO_InitStructB; //结构体变量名GPIO_InitStructB
GPIO_InitStructB.GPIO_Mode = GPIO_Mode_IPU; //需要上拉输入,按键未按时默认高电平
GPIO_InitStructB.GPIO_Pin = GPIO_Pin_1 |GPIO_Pin_11;
GPIO_InitStructB.GPIO_Speed = GPIO_Speed_50MHz;//在输入模式下,这个参数其实无用,无影响
GPIO_Init(GPIOB,&GPIO_InitStructB);
}
uint8_t key_getnum(void) //读取按键值的函数
{
uint8_t keynum = 0; //按键键码,没有按下,就返回0,局部变量
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0) //读取GPIO端口,返回值就是输入数据寄存器的某一位值,等于0代表按键按下
{
Delay_ms(20); //按键按下消抖20ms
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0); // 松手后动作,直到松手
Delay_ms(20); //按键松手消抖
keynum = 1; //键码为1.传递出去
}
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0)
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0);
Delay_ms(20);
keynum = 2;
}
return keynum;
}
key.h
#ifndef __KEY_H
#define __KEY_H
void key_init(void);
uint8_t key_getnum(void);
#endif
光敏传感器控制蜂鸣器
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "buzzer.h"
#include "lightsenoer.h"
int main(void)
{
buzzer_init(); //初始化蜂鸣器
lightsenoer_init(); //初始化光敏传感器
while(1)
{
if(lightsenoer_get() == 1) //光线暗,模块本身不亮指示灯
{
buzzer_off(); //关闭蜂鸣器
}
else
{
buzzer_on(); //否者,打开蜂鸣器
}
}
}
buzzer.c
#include "stm32f10x.h" // Device header
void buzzer_init(void) //蜂鸣器初始化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStruct; //定义结构体变量
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12; //pa12端口
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);//传递地址
GPIO_SetBits(GPIOB,GPIO_Pin_12);//初始化为高电平,蜂鸣器不响
}
void buzzer_on(void)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
void buzzer_off(void)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
void buzzer_turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_12) == 0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
else
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
}
buzzer.h
#ifndef __BUZZER_H
#define __BUZZER_H
void buzzer_init(void);
void buzzer_on(void);
void buzzer_off(void);
void buzzer_turn(void);
#endif
lightsenoer.c
#include "stm32f10x.h" // Device header
void lightsenoer_init(void) //光敏传感器初始化函数
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //上拉输入,默认高电平状态;若始终接在端口上,也可以选择浮空输入;只要保证引脚不会悬空即可
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; //pb13端口
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
}
uint8_t lightsenoer_get(void) //返回端口值函数
{
return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}
lightsenoer.h
#ifndef __LIGHTSENOER_H
#define __LIGHTSENOER_H
void lightsenoer_init(void);
uint8_t lightsenoer_get(void);
#endif
4、OLED显示屏及调试
调试方式:
总之,测试程序的基本思想就是:缩小范围、控制变量、对比测试等
OLED简介
OLED(Organic Light Emitting Diode):有机发光二极管(每一个像素都是一个单独的发光二极管,每一个像素都可以自发光,不像LCD需要有背光)
OLED显示屏:性能优异的新型显示屏,具有功耗低、相应速度快、宽视角、轻薄柔韧等特点
0.96寸OLED模块:小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块
供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64
规格:4针脚,像素为白色,一般I2C通信
规格:7针脚,占用IO口多一些,一般SPI通信
蓝色像素版本
黄蓝双色版本,上面1/4像素固定为黄色,其余固定为蓝色,适和做需要显示标题行的界面。无论哪个规格版本,驱动方式都是一样的。
硬件电路
4针脚版本:SCL和SDA是I2C的通信引脚,需要接在I2C通信的引脚上;(当驱动函数模块用GPIO口的模拟的I2C通信时,这两个端口可以接在任意的GPIO口上)
7针脚版本:除GND和VCC外的引脚是SPI通信协议的引脚,(如果是GPIO口模拟的通信协议,也是可以任意接GPIO口)
OLED驱动函数模块
OLED实物图及对应的屏幕坐标图如下:将OLED分割成了4行16列的小区块
知识点get:
1.STM32的引脚上电后,如果不初始化,默认是浮空输入模式,在这个模式下,引脚不会输出电平,所以不会有什么影响;做实践项目时,最好还是给OLED用电源供电,不用GPIO口供电
2.字符需要单引号括起来。字符串用双引号括起来
3.c语言不能直接写二进制的数,只能用十六进制来代替。
示例程序(OLED驱动函数)
改引脚配置和端口初始化,就可以直接使用OLED驱动函数了
比如我这里SCL接在了PB8,那这个地方就是GPIOB,GPIO_Pin_8,如果你换个端口,比如接在PA6上,那这个地方就要改成GPIOA,GPIO_Pin_6;下面这个SDA的引脚配置也是一样,SDA接在了哪个位置,就改成GPIO啥,GPIO_Pin_啥。
具体更改就是,使用到的GPIO外设都先用RCC开启一下时钟,然后下面初始化GPIOB的Pin8,再初始化GPIOB的Pin9。
驱动函数见参考博文。。
keil的调试模式
。。
5、EXTI外部中断
中断系统是管理和执行中断的逻辑结构,外部中断是众多能产生中断的外设之一,所以本节我们就借助外部中断来学习一下中断系统。在以后学习其它外设的时候,也是会经常和中断打交道的。
中断系统
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源。比如对于外部中断来说,可以是引脚发生了电平跳变;对于定时器来说,可以是定时的时间到了;对于串口通信来说,可以是接收到了数据),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。(就好比晚上睡觉前定了个闹钟,时间到了提醒你,不管时间到不到你可以安心睡觉)。
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。(这个中断优先级是我们根据程序设计的需求,自己设置的)。
中断嵌套:(中断程序再次中断,二次中断现象)当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。(也是为了照顾非常紧急的中断)。
中断执行流程
中断程序的执行流程如下,当它执行到某个地方时,外设的中断条件满足了,那这时,无论主程序是在干什么事情(比如OLED显示程序才执行一半,Delay函数还在等待等)中断来了,主程序都得立即暂停,程序由硬件电路自动跳转到中断程序中,当中断程序执行完之后,程序再返回被暂停的地方继续运行(这个暂停的地方,叫做断点)。为了程序能在中断返回后继续原来的工作,在中断执行前,会对程序的现场进行保护,中断执行后,会再返回现场,这样保证主程序被中断了,回来之后也能继续执行。
中断嵌套的执行流程如下。当一个中断正在执行时,又有新的优先级更高的中断来,那个旧中断会被打断,执行新的中断,新的中断结束,再继续执行原来的中断,原来的中断结束,再继续主程序,这就是中断嵌套的执行流程。
c语言中,中断的执行流程如下。上面是主函数,while(1)死循环里就是主程序,正常情况下,程序就是在主程序中不断循环执行,当中断条件满足时,主程序就会暂停,然后自动跳转到中断程序里运行,中断程序执行完之后,再返回主程序执行。一般中断程序都是在一个子函数里,这个函数不需要我们调用,当中断来临时,由硬件自动调用这个函数,这就是在c语言中,中断的执行流程。
STM32中断
多个可屏蔽中断通道(中断源),包含EXTI(外部中断)、TIM、ADC(模数转换器)、USART(串口)、SPI、I2C、RTC(实时时钟)等多个外设。(几乎所有模块都能申请中断)
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。
NVIC就是STM32中用来管理中断、分配优先级的,NVIC的中断优先级共有16个等级。
EXTIx是外部中断对应的中断资源。
下图为stm32的中断资源,上面灰色的是内核中断(我们一般不用,了解即可),下面不是灰色的部分就是stm32外设的中断了,外设电路检测到有什么异常或事件,需要提示一下CPU的时候,它就可以申请中断,让程序调到对应的中断函数里运行一次,用来处理这个异常或事件。图中最右边是中断的地址,因为程序中的中断函数,它的地址是由编译器来分配的,是不固定的,但是我们的中断跳转,由于硬件的限制,只能跳到固定的地址执行程序,所以为了硬件能够跳转到一个不固定的中断函数里,这里就需要在内存中定义一个地址的列表,这个列表的地址是固定的,中断发生后,就跳到这个固定位置,然后在这个固定位置,由编译器,再加上一个跳转到中断函数的代码,这样中断跳转就可以跳转到任意位置了,这个中断地址的列表,就叫中断向量表,相当于中断跳转的一个跳板,不过我们用c编程,是不需要管这个中断向量表的,因为编译器都帮我们做好了。
NVIC基本结构
NVIC(嵌套中断向量控制器),在stm32中,它是用来统一分配中断优先级和管理中断的,NVIC是一个内核外设,是CPU的小助手(如果把中断全接到cpu上,会很麻烦,毕竟CPU主要是用来运算的),NVIC有很多输入口,下图中线上划了个斜杠上面写了n(这个意思是:一个外设可能会同时占用多个中断通道,所以这里有n条线),然后NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的先后顺序,之后通过右边这一输出口就告诉CPU该处理哪个中断,对于中断先后顺序分配的任务,CPU不需要知道
举个例子:比如CPU是医生,如果医院只有一个医生时,当看病人很多时,医生就得先安排一下先看谁后看谁,如果有紧急的病人,那还得让紧急的病人最先来,这个安排先后顺序的任务很繁琐会影响医生看病的效率,所以医院就安排了一个叫号系统(NVIC),来病人了统一取号并且根据病人的等级,分配一个优先级,然后叫号系统看一下现在在排队的病人,优先叫号紧急的病人,最后叫号系统给医生输出的就是一个一个排好队的病人,医生就可以专心看病了。(EXTI、TIM、ADC等就是病人)
NVIC优先级分组
为了处理不同形式的优先级,STM32的NVIC可以对优先级进行分组,分为抢占优先级和响应优先级。
抢占优先级和响应优先级的区别,例子理解:还想一下病人叫号的例子,对于紧急的病人,其实有两种形式的优先。一种是,上一个病人1在看病,外面排队了很多病人,当病人1看完后,外面排队中的紧急病人最先进去看病即使这个紧急病人是最后来的,这种在排队中的插队的就叫响应优先级,响应优先级高的可以插队提前看病。另一种是,上一个病人1在看病,外面排队中的病人2比病人1更加紧急,病人2可以不等病人1看完直接冲到医生的屋里,让病人1先靠边站,先给病人2看病,病人2看完病接着病人1看病,然后外面排队的病人再进来,这种形式的优先级就是中断嵌套,这种决定是不是可以中断嵌套的优先级,就叫抢占优先级,抢占优先级高的,可以进行中断嵌套。
为了将优先级区分为抢占优先级和响应优先级,就需要对这16个优先级优先级进行分组,NVIC的中断优先级由优先级寄存器的4位(0~15,4位二进制,对应16个优先级)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
优先级的数值越小,优先级越高,0就是最高优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队(中断号是中断表的左边数字,数值小的优先响应),所以stm32的中断不存在先来后到的排队方式,在任何时候都是优先级高的先响应。
下表,因为优先级总共是4位,所以就有(0,4)、(1.3)、(2,2)、(3,1)、(4、0)这五种分组方式,分组0,就是0位的抢占等级,取值为0,4位的响应等级,取值为0~15,分组1234雷同。这个分组方式是我们在程序中自己进行选择的,选好分组方式后,就要注意抢占优先级和响应优先级的取值范围了,不要超出这个表里规定的取值范围。
分组方式 | 抢占优先级 | 响应优先级 |
---|---|---|
分组0 | 0位,取值为0 | 4位,取值为0~15 |
分组1 | 1位,取值为0~1 | 3位,取值为0~7 |
分组2 | 2位,取值为0~3 | 2位,取值为0~3 |
分组3 | 3位,取值为0~7 | 1位,取值为0~1 |
分组4 | 4位,取值为0~15 | 0位,取值为0 |
EXTI简介
EXTI(Extern Interrupt)外部中断
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序。(简单说:引脚电平变化,申请中断)
支持的触发方式(引脚电平的变化类型):上升沿(电平从低电平变到高电平的瞬间触发中断)/下降沿(电平从高电平变到低电平的瞬间触发中断)/双边沿(上升沿和下降沿都可以触发中断)/软件触发(程序执行代码就能触发中断)
支持的GPIO口(外部中断引脚):所有GPIO口都能触发中断,但相同的Pin不能同时触发中断(比如PA0和PB0不能同时使用,智能选一个作为中断引脚;所以如果有多个中断引脚要选择不同的pin引脚,比如PA0和PA1、PB3就可以)
通道数:总共有20个中断线路。16个GPIO_Pin(对应GPIO_pin0到15,是外部中断的主要功能),外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒(这4个中断线路,是因为外部中断有个功能是从低功耗模式的停止模式下唤醒STM32那对于PVD电源电压检测,当从电源从电池过压恢复时就需要PVD借助一下外部中断的退出停止模式;对于RTC闹钟来说,有时候为了省电,RTC定一个闹钟之后,STM32回进入停止模式,等到闹钟响的时候再唤醒,这叶需要借助外部中断,剩余USB唤醒、以太网唤醒也是类似的作用)
触发响应方式:中断响应(引脚电平触发中断,申请中断,让CPU执行中断函数)/事件响应(不会触发中断,而是触发别的外设操作,属于外设之间的联合工作。外部中断的信号不会通向CPU而是通向其它外设,用来触发其它外设的操作,比如触发ADC转换、触发DMA等)
EXTI基本结构
外部中断的整体结构图如下:
首先,最左边是GPIO口的外设,每个GPIO外设有16个引脚,所以进来16根线;如果每个引脚占用一个通道,那EXTI的16个通道是不够用的,所以在这里会有一个AFIO中断引脚选择的电路模块,这个AFIO就是一个数据选择器(可以将图中前面的3个GPIO外设的16个引脚中的其中一个连接到后面的EXTI通道(16个GPIO通道),所以对于PA0\PB0\PC0这些,通过AFIO选择之后只有其中一个能接到EXTI的通道0上),然后通过AFIO选择后的16个通道,就能接到了EXTI边沿检测及控制电路上,同时下面这4个蹭网的外设(PVD\PTC\USB\ETH)也是并列接进来的,这些加起来就组成了EXTI的20个输入信号,然后经过EXTI电路之后,分为了两种输出,也就是中断响应和事件响应(上面接到了NVIC用来触发中断,下面有20条输出线路到了其它外设,也就是事件响应)
注意点:EXTI9_5是外部中断的5,6,7,8,9分到了一个通道里,EXTI15_10也是一样;也就是说外部中断的9到5会触发同一个中断函数,15到10也会触发同一个中断函数;在编程的时候,我们在这两个中断函数里,需要再根据标志位区分到底是哪个中断进来的。(本来20路输入,应该有20路中断的输出,可能ST公司觉得20个输出太多了比较占用NVIC的通道资源,所以就把其中的外部中断95,1510,给分到了一个通道)
AFIO复用IO口内部电路
内部电路就是一系列的数据选择器,如下图的最上面输入是PA0\PB0\PC0等尾号都是0,然后通过数据选择器最终选择一个,连接到EXTI0上,上面写的文字是说配置这个寄存器的哪一个位就可以决定选择哪一个输入,图中后面部分内容都雷同。
AFIO主要用于引脚复用功能的选择和重定义(也就是数据选择器的作用)。
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射(就是最开始提到的引脚定义表,当想把默认复用功能换到重定义功能时,就是用AFIO来完成的,这也是AFIO的一大主要功能)、中断引脚选择。
EXTI内部电路框图
EXTI的右边就是20根输入线,然后输入线首先进入边沿检测电路,在上面的上升沿寄存器和下降沿寄存器可以选择是上升沿触发还是下降沿触发或者两个都触发,接着硬件触发信号和软件中断寄存器的值就进入到这个或门的输入端(也就是任意一个为1,或门就可以输出1),然后触发信号通过这个或门后就兵分两路,上一路是触发中断的,下一路是触发事件的:触发中断首先会置一个挂起寄存器(挂起寄存器相当于一个中断标志位,可以读取这个寄存器判断是哪个通道触发的中断,如果挂起寄存器置1,它就会继续向左走和中断屏蔽寄存器共同进入一个与门(与门实际上就是开关控制作用,中断屏蔽寄存器给1那另一个输入就是输出,也就是允许中断;中断屏蔽寄存器给0,那另一个输入无论是什么,输出都是0,相当于屏蔽了这个中断),然后是NVIC中断控制器)。接着就是下一路的选择是触发事件,首先也是一个事件屏蔽寄存器进行开关控制,最后通过一个吗,脉冲发生器到其它外设(脉冲发生器就是给一个电平脉冲,用来触发其它外设的动作)
补充:框图最上面两个就是外设接口和APB总线,我们可以通过总线访问这些寄存器。
或门(无直边)。它可以有多个输入,但只能有一个输出。执行的是或的逻辑,在输入端(曲边),只要有一个高电平1,输出的高电平就为1;只有全部输入低电平0,输出才为0。(尖头为输出)。(或1为1,全0则0)
与门(直边)。它可以有多个输入,但只能有一个输出。执行的是与的逻辑,在输入端(直边),只要有一个是低电平0,输出就是0;只有全部输入1,输出才为1。(与0为0,全1则1)
非门(三角号加个圈)。它只有一个输入,一个输出;输入1就输出0,输入0就输出1,执行的是非得逻辑(圈为输出,取反)
数据选择器(一个梯形)。有多个输入,一个输出,在侧面有选择控制端,根据控制端的数据,从输入选择一个接到输出。
表示20根线,代表20个通道
EXTI外部中断的特性和使用场景
外部中断的使用场景:
什么样的设备需要用到外部中断,使用外部中断有什么好处呢?大概总结了使用外部中断模块的特性:就是对于stm32来说,想要获取的信号是外部驱动的很快的突发信号。
比如旋转编码器的输出信号,你可能很久都不会拧它,这时不需要STM32做任何事,但是我一拧它,就会有很多脉冲波形需要STM32接收。这个信号是突发的,STM32不知道什么时候会来,同时它是外部驱动的,STM32只能被动读取,最后这个信号非常快,STM32稍微晚一点来读取,就会错过很多波形。那对于这种情况来说,就可以考虑使用STM32的外部中断了。有脉冲过来,STM32立即进入中断函数处理,没有脉冲的时候,STM32就专心做其它事情。
另外还有,比如红外遥控接收头的输出,接收到遥控数据之后,它会输出一段波形,这个波形转瞬即逝,并且不会等你,所以就需要我们用外部中断来读取。
最后还有按键,虽然它的动作也是外部驱动的突发事件,但我并不推荐用外部中断来读取按键。因为用外部中断不好处理按键抖动和松手检测的问题,对于按键来说,它的输出波形也不是转瞬即逝的。所以要求不高的话可以在主程序中循环读取,如果不想用主循环读取的话,可以考虑一下定时器中断读取的方式。这样既可以做到后台读取按键值、不阻塞主程序,也可以很好地处理按键抖动和松手检测的问题。
手册
大概看一下每个外设在手册的介绍
NVIC是内核外设,在这个内核cortex-m3编程手册中查看,这个cortex-m3编程手册就是内核和内核外设的详细介绍,想研究一下内核的运转,可以看一下这个手册
NVIC的一些寄存器
这个中断优先级寄存器就是用来设置每个中断的优先级的,用库函数直接给结构体赋值就行了,要知道库函数要最终落实到寄存器上来的
中断分组配置寄存器被分配到了这个SCB里面
这三位就是用来配置中断分组的
中断和外部中断的介绍在参考手册中
AFIO介绍
EXTI中断示例程序(对射式红外传感器&旋转编码器计次)
本节先主要学习外部中断读取编码器计次数据的用法,后面学了定时器,还会再来看一下编码器测速的用途。
旋转编码器简介
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
类型:机械触点式/霍尔传感器式/光栅式
1.下面的是一种最简单的编码器样式,这里使用的也是对射式红外传感器来测速的,为了测速还需配合一个光栅编码盘(银色圆圈),当这个编码盘转动时,红外传感器的红外光就会出现遮挡、透过、遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波,方波的个数代表了转过的角度,方波的频率表示转速,我们就可以用外部中断来捕获这个方波的边沿,以此来判断位置和速度,不过这个模块只有一路输出,正转反转输出波形没法区分,所以这种测试方法只能测位置和速度,不能测量旋转方向,为了进一步测量方向,我们就可以用后面的几种编码器。
2.如下是我们接下来将要用过的旋转编码器,左边是外观,右边是内部拆解的结构;可以看到内部是用金属触电进行通断的,所以它是一种机械触电式编码器,左右是两部分开关触电;中间银色圆形金素片为一个按键,这个旋转编码器的轴是可以按下去的,这种编码器一般是用来进行调节的,比如音响调节音量,因为它是触电接触的形式,所以不适合电机这种高速旋转的地方,另外三种都是非接触的形式,可以用于电机测速(电机测速在电机驱动的应用中还是很常见的)
下面为详细讲解旋转编码器的硬件部分:
金属触电
内侧的两根细的触电都是和中间的引脚c连接的,外侧触电一个连接A,一个连接B。
圆形金属片(按键)的两根线,就在上面引出来了;按键的轴按下,上面两根线短路,松手,上面两根线断开,就是个普通的按键
这个旋转编码器的轴是可以按下去的;轴的外侧是白色的编码盘,它也是一系列光栅一样的东西,只不过这是金属触电,在旋转时,依次接通和断开两边的触电;这个金属盘的位置是经过设计的,它能让两侧触电的通断产生一个90度的相位差,最终配合一下外部电路,这个编码器的两个输出就会输出如下这样的正交波形,带正交波形输出的编码器是可以用来测方向的(这就是单相输出和两相正交输出的区别),当然还有的编码器不是输出正交波形,而是一个引脚输出方波信号代表转速,另一个输出高低电平代表旋转方向,这种不是正交输出的编码器也是可以测方向的。
当正转时,A相引脚输出一个方波波形,B相引脚输出一个和它相位相差90的波形(正交波形),如下。
当反向旋转时,A相引脚还是方波信号,B相引脚会提前90度,如下。
3.霍尔传感器形式编码器,这种是直接附在电机后面的编码器,中间是一个圆形磁铁,边上有两个位置错开的活儿传感器,当磁铁旋转时,通过霍尔传感器就可以输出正交的方波信号,如下。
4.这是独立的编码器元件,它的输入轴转动时,输出就会波形,这个也是可以测速和测方向的,具体用法再看相应的手册。如下。
旋转编码器的硬件电路
模块的电路图如下,图中正方形区域就是旋转编码器,上面按键的两根线这个模块没有使用,是悬空的
下面为模块电路细节介绍:
这里是编码器内部的两个触电,旋转轴旋转时,这两个触电以相位相差90度的方式交替导通,因为这只是个开关信号,所以要配合外围电路才能输出高低电平
左边接了一个10k的上拉电阻,默认没旋转的情况下,这个点被上拉为高电平,再通过R3这个电阻输出到A端口的就也是高电平,当旋转时,内部触电导通,那C端口处就直接被拉低到GND,再通过R3输出,A端口就是低电平了,之后这个R3是一个输出限流电阻(是为了防止模块引脚电流过大的);C1是输出滤波电容,可以防止一些输出信号抖动。剩下的右边电路和左边是雷同的。
使用这个模块时的接线如下,下面的A相输出和B相输出接到STM32的两个引脚上(主要引脚的尾数不能一样),中间的C引脚就是GND,我们暂时不用
接线图
对射式红外传感器
旋转编码器
当挡光片在对射式红外传感器中间经过时,DO输出电平跳变信号,触发PB14号口的中断,在中断断数执行Num++
第一步,配置RCC,将程序涉及外设的时钟都打开
提示:有GPIOB和AFIO
第二步,配置GPIO,选择端口为输入模式
第三步,配置AFIO,选择硬件所用用的那一路GPIO,连接到后面的EXTI
涉及函数如下:
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
作用:选择用作EXTI线的GPIO引脚。
参数说明:
第四步,配置EXTI,选择边沿触发方式,比如上升沿、下降沿或者双边沿,还有选择触发响应方式,可以选择中断响应和事件响应
涉及函数如下:
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)
作用:根据EXTI InitStruct中的指定参数初始化EXTI外设。
参数说明:
EXTI InitTypeDef结构体说明:
typedef struct
{
uint32_t EXTI_Line;
EXTIMode_TypeDef EXTI_Mode;
EXTITrigger_TypeDef EXTI_Trigger;
FunctionalState EXTI_LineCmd;
}EXTI_InitTypeDef;
参数说明以及举例
举例:
/* Enables external lines 12 and 14 interrupt generation on falling
edge */
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line12 | EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
第五步,配置NVIC,给我们这个中断选择一个合适的优先级
涉及函数如下:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
作用:配置优先级分组:抢占优先级和子优先级。
参数说明:
取值范围:
例如:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
最后,通过NVIC,外部中断信号就能进入CPU了,这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序
涉及函数如下:
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
作用:根据NVIC InitStruct中指定的参数初始化NVIC外设。
参数说明:
举例:
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
中断函数的格式:
根据中断向量表,找到所需中断函数,这里面以IRQHandler结尾的字符串就是中断函数的名字,再根据名字写中断函数。
例如:void EXTI15_10_IRQHandler(void){ }
这就是中断函数的格式,中断函数都是无参无返回值的,中断函数的名字不要写错了,写错了就进不了中断了,最好是直接从启动文件复制过来,这样就不会有问题了。
注:启动文件为
然后在中断函数里,一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的这个函数,因为这个函数EXTI10到EXTI15都能进来,所以要先判断一下是不是我们想要的EXTI14进来的。所用函数:EXTI_GetITStatus(uint32_t EXTI_Line)
最后,中断程序结束后,一定要再调用一下清除中断标志位的函数,因为只有中断标志位置1了,程序就会跳转到中断函数。如果你不清除中断标志位,那它就会一直申请中断,这样程序就会不断响应中断,执行中断函数,那程序就卡死在中断函数里了。所用函数:EXTI_ClearITPendingBit(uint32_t EXTI_Line)
中断函数就不用声明了,因为中断函数不需要调用,它是自动执行的。
其它涉及函数:
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)
作用:检查指定的 EXTI 线路触发请求发生与否(是不是我们想要的中断触发源)
返回值:(SET或RESET)
参数说明:
void GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:读取指定端口管脚的输入
参数说明:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line)
作用:清除EXTI线路挂起位
参数说明:
EXTI和NVIC两个外设,这两个外设的时钟是一直都打开着的,不需要我们再开启时钟了。EXIT模块是由NVIC模块直接控制的,并不需要单独的外设时钟。NVIC也不需要开启时钟,是因为NVIC是内核的外设,内核的外设都是不需要开启时钟的。
代码如下:
蓝线部分是我自己需要注意的地方
当我们的挡光片或者编码盘在这个对射式红外传感器中间经过时,这个DO就会输出电平跳变的信号,然后这个电平跳变的信号触发STM32 PB14号口的中断,我们在中断函数里,执行变量++的程序,然后主循环里用OLED显示这个变量,这样第一个程序就完成了。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "countsensor.h"
int main(void)
{
countsensor_init();//初始化红外对射式模块计次
OLED_Init(); //初始化OLED
OLED_ShowString(1,1,"Count:");//第一行第三列开始显示字符串
while(1)
{
OLED_ShowNum(1,7,countsersor_get(),5);//显示countsersor_get的返回值,长度为5
}
}
countsensor.c
#include "stm32f10x.h" // Device header
uint16_t countsensor_count; //这个数字来统计中断触发的次数
//初始化函数,将模块要用的资源配置好
void countsensor_init(void)
{
//第一步,时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//EXTI和NVIC两个外设的时钟是一直开的 ,NVIC内核外设都是不需要开启时钟
//第二步,配置GPIO
//首先定义结构体
GPIO_InitTypeDef GPIO_initstruct; //结构体名字GPIO_initstruct
//将结构体成员引出来
//对于EXTI来说,模式为浮空输入|上拉输入|下拉输入;不知该写什么模式,可以看参考手册中的外设GPIO配置
GPIO_initstruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_initstruct.GPIO_Pin = GPIO_Pin_14;
GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
//最后初始化GPIO
GPIO_Init(GPIOB,&GPIO_initstruct); //传地址
//第三步,配置AFIO外设中断引脚选择
//AFIO的库函数是和GPIO在一个文件里,可以查看Library文件中的gpio.h查看函数
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);//代表连接PB14号口的第14个中断线路
//第四步,配置EXTI,这样PB14的电平信号就能够通过EXTI通向下一级的NVIC了
EXTI_InitTypeDef EXTI_InitStructure;//结构体类型名EXTI_InitTypeDef,变量名EXTI_InitStructure
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//因为上面是GPIO_Mode_IPU设置为高电平,所以触发中断是下降
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//第五步,配置NVIC,NVIC是内核外设,所以它的库函数在misc.h
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组方式,整个芯片只能用一种。如放在模块中进行分组,要确保每个模块分组都选的是同一个;或者将这个代码放在主函数的最开始
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//因为我们这个程序只有一个,所以中断优先级的配置也是非常随意的
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
uint16_t countsersor_get(void)
{
return countsensor_count;
}
//中断函数,都是无参无返回值的,名字固定在startup文件向量表中
//中断函数不需声明,它是自动执行的
void EXTI15_10_IRQHandler(void)
{
/*一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的函数,因为这个函数EXTI10到
EXTI15都能进来,所以要先判断一下是不是我们想要的EXTI14进来的*/
if(EXTI_GetFlagStatus(EXTI_Line14) == SET)
{
countsensor_count++;
//每次中断函数结束后,都应该清除一下中断标志位
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
//写完模块之后最好编译一下,要不然代码提示可能显示不出我们新写的函数
countsensor.h
#ifndef __COUNTSENSOR_H
#define __COUNTSENSOR_H
void countsensor_init(void);
uint16_t countsersor_get(void);
#endif
5-2 旋转编码器计次37:30
在写中断函数的核心思想:
只有在B相下降沿和A相低电平时,才判断为正转
在A相下降沿和B相低电平时,才判断为反转
代码如下:
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "encoder.h"
int16_t num;
int main(void)
{
OLED_Init(); //初始化OLED
encoder_init();
OLED_ShowString(1,1,"num:");//第一行第三列开始显示字符串hello word!
while(1)
{
num += encoder_get();
OLED_ShowSignedNum(1,5,num,5);
}
}
encoder.c
#include "stm32f10x.h" // Device header
int16_t encoder_count;
void encoder_init(void)
{
//第一步,时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//EXTI和NVIC两个外设的时钟是一直开的 ,NVIC内核外设都是不需要开启时钟
//第二步,配置GPIO
//首先定义结构体
GPIO_InitTypeDef GPIO_initstruct; //结构体名字GPIO_initstruct
//将结构体成员引出来
//对于EXTI来说,模式为浮空输入|上拉输入|下拉输入;不知该写什么模式,可以看参考手册中的外设GPIO配置
GPIO_initstruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_initstruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
//最后初始化GPIO
GPIO_Init(GPIOB,&GPIO_initstruct); //传地址
//第三步,配置AFIO外设中断引脚选择
//AFIO的库函数是和GPIO在一个文件里,可以查看Library文件中的gpio.h查看函数
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
//第四步,配置EXTI,这样PB14的电平信号就能够通过EXTI通向下一级的NVIC了
EXTI_InitTypeDef EXTI_InitStructure;//结构体类型名EXTI_InitTypeDef,变量名EXTI_InitStructure
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//因为上面是GPIO_Mode_IPU设置为高电平,所以触发中断是下降
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//第五步,配置NVIC,NVIC是内核外设,所以它的库函数在misc.h
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组方式,整个芯片只能用一种。如放在模块中进行分组,要确保每个模块分组都选的是同一个;或者将这个代码放在主函数的最开始
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
int16_t encoder_get(void)
{
int16_t temp;
temp = encoder_count;
encoder_count = 0;
return temp;
}
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)
{
encoder_count--;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line1) == SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0)
{
encoder_count++;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void encoder_init(void);
int16_t encoder_get(void);
#endif
6、TIM(Timer)定时器
定时器共四个部分,分为八个小节笔记。本小节为第一部分第一节。
在第一部分,是定时器的基本定时的功能:定时中断功能、内外时钟源选择
在第二部分,是定时器的输出比较功能,最常见的用途是产生PWM波形,用于驱动电机等设备
在第三部分,是定时器的输入捕获功能和主从触发模式,来实现测量方波频率
在第四部分,是定时器的编码器接口,能够更加方便读取正交编码器的输出波形,编码电机测速
TIM简介
TIM(Timer)定时器,定时触发中断
定时器本质上就是一个计数器
定时器可以对输入的时钟进行计数(在stm32中定时器的基准时钟一般是主频72MHz,如果对72MHz记72个数,那就是1MHz也就是1us的时间(72MHz就是1秒记72M个数,可以理解为对72个数计数1M次,记72个数的频率就是1MHz,用时1us)),如果记72000个数,那就是1KHz也就是1ms的时间,并在计数值达到设定值时触发中断
stm32的定时器拥有16位(2的16次方是65536)的计数器(计数器就是用来执行计数定时的寄存器,每来一个时钟,计数器加1)、预分频器(可以对计数器的时钟进行分频,让计数更加灵活)、自动重装寄存器(是计数的目标值,计多少个时钟申请中断)的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时。
预分频值(PSC)、自动重装载值(ARR)。
定时器的计数频率 = 时钟频率 / (PSC + 1)
最大定时时间 = (ARR + 1)/ 定时器的计数频率
即,最大定时时间 = (ARR + 1) * (PSC + 1) / 时钟频率
不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型
为什么在72MHz计数时钟下可以实现最大59.65s的定时?
72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多,大家可以自己算一下。
详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。【最大值是65536,但计数是从0~65535】
定时器类型
类型 | 编号 | 总线 | 功能 |
---|---|---|---|
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
除了TIM1-8,在库函数中还出现了TIM9、10、11等(这些一般都用不到)
高级定时器额外具有的重复计数器、死区生成、互补输出、刹车输入等,这些功能主要是为了三相无刷电机的驱动设计的
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4;不同的型号,定时器的数量是不同的
接下来,我们就依次来看一下高级定时器、通用定时器和基本定时器的结构图,看一下这三种定时器是怎么样来工作的,设计这些结构都能完成哪些任务。
基本定时器
理解时基单元的工作流程(定时器产生中断的全部流程)、主模式触发DAC的功能,如下内容:
1.基本定时器时基单元
下面这三个构成了最基本的计数计时电路,所以这一块电路就叫做时基单元
时基单元:预分配器(PSC)、自动重装载寄存器(ARR)、计数器(CNT)
2.时基单元的工作流程
内部时钟的来源是RCC_TIMxCLK,频率值是系统的主频72MHz,所以通向时基单元的计数基准频率就是72MHz
进入时基单元首先是预分频器(PSC),它可以对72MHz的计数时钟进行预分频(比如,预分频器写0就是不分频输出72MHz,写1是进行二分频输出36MHz,写2是三分频输出24MHz …,所以预分频的值和实际的分频系数相差1,即,实际分频系数=预分频器的值+1),预分频器是16位的,最大值可以写65535,也就是最大65536分频。
然后是计数器,对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值加1,这个计数器的值也是16位的,值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以还需一个存储目标值的寄存器,那就是自动重装载寄存器了
自动重装寄存器也是16位的,它存的是我们写入的计数目标,在运行的过程中,计数值不断自增,自动重装载是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。像这种计数值等于自动重装值产生的中断,叫做“更新中断”,这个更新中断之后就会通向NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应。
总结定时器产生中断的全部流程:从基准时钟到预分频器再到计数器,计数器计数自增,同时不断地与自动重装寄存器进行比较,值相等时,即计时时间到,这时就会产生一个更新中断和更新事件,CPU响应更新中断,就完成了我们定时中断的任务了。
下图红圈,是一个向上的折线箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,叫做“更新中断”。
下图红圈,是一个向下的折线箭头,代表的是产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其它电路的工作。
这个可编程定时器的主要部分是一个带有自动重装载的16位累加计数器,计数器的时钟通过一个预分频器得到。
软件可以读写计数器、自动重装载寄存器和预分频寄存器,即使计数器运行时也可以操作。时基单元包含:
预分频寄存器(TIMx_PSC)
预分频器
预分频可以以系数介于1至65536之间的任意数值对计数器时钟分频,就是对输入的基淮频率提前进行一个分频的操作。它是通过一个16位寄存器(TIMx-PSC)的计数实现分频。因为TIMx-PSC控制寄存器具有缓冲,可以在运行过程中改变它的数值,新的预分频数值将在下一个更新事件时起作用。
假设这个寄存器写0,就是不分频,或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz,所以预分频器的值和实际的分频系数相差了1,即实际分频系数=预分频器的值+1。
时序图讲解32:34
注意:实际的设置计数器使能信号CNT_EN相对于CEN滞后一个时钟周期。
计数器寄存器(TIMx_CNT)
计数器由预分频输出CK_CNT驱动,设置TIMx_CR1寄存器中的计数器使能位(CEN)使能计数器计数。这个计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升滑,计数器的值就加1,由于这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了。
时序图讲解
自动重裝载寄存器(TIMx_ARR)
自动重装载寄存器是预加载的,每次读写自动重装载寄存器时,实际上是通过读写预加载寄存器实现。根据TIMx CR1寄存器中的自动重装载预加载使能位(ARPE),写入预加载寄存器的内容能够立即或在每次更新事件时,传送到它的影子寄存器。当TIMx CR1寄存器的UDIS位为’0’,则每当计数器达到溢出值时,硬件发出更新事件;软件也可以产生更新事件;关于更新事件的产生,随后会有详细的介绍。
38:47讲解
39:27讲解
3.主模式触发DAC的功能
下面,简单介绍一下(后续讲),主模式触发DAC的功能,stm32定时器的一大特色就是主从触发模式(主从触发模式能让内部的硬件在不受程序的控制下实现自动运行),如果能把主从触发模式掌握好,那在某些情景下将会极大地减轻CPU的负担。
主模式触发DAC的作用就是,在我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,这样会使主程序处于频繁被中断的状态,这会影响主程序的运行和其他中断的响应,所以定时器就设计了一个主模式,使用这个主模式可以把定时器的更新事件映射到触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上,这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接区触发DAC,整个过程不需要软件的参与,实现了硬件自动化,这就是主模式的作用,当然除了主模式外,还有更多硬件自动化的设计(后续讲)
通用定时器
1.通用定时器与基本定时器异同
首先,中间最核心的部分,还是时基单元,如下,这部分结构和工作流程和基本定时器是一样的,不过对于通用定时器而言,计数器的计数模式就不止向上计数一种了(向上自增),通用定时器和高级定时器支持向上计数模式、向下计数模式和中央对齐模式。(基本定时器仅支持向上计数模式)。最常用的还是向上计数模式。
向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后继续下一轮,依次循环
中央对齐模式就是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断,然后继续下一轮,依次循环
2.内外时钟源选择功能
如下,是内外时钟源选择和主从触发模式的结构。
内外时钟源选择:对于基本定时器,定时只能选择内部时钟,也就是系统频率72MHz;对于通用定时器,时钟源可以选择内部时钟或者外部时钟
外部时钟的选择有如下四种:

TIMx_ETR引脚的位置可以参考引脚定义表中关于默认复用功能和重定义功能的定义,如下图所示。可以看到TIM2的CH1和ETR都复用在了引脚PA0上。其他定时器的引脚也可以在表中找到。
主模式的输出TRGO可以通向其他定时器,实际上通向的就是ITR引脚,通过这一路就可以实现定时器级联的功能。如上如黄线所示,ITR0到ITR3分别来自其他4个定时器的TRGO输出,具体的连接方式如下表所示,这就是ITR和定时器的连接关系。实现定时器级联功能例如,可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,选择ITR2对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就是实现了定时器的级联
第三个外部时钟可来自TIMx_CH1的TI1_ED,CH1引脚的边沿,即从CH1引脚连接的输入捕获模块获得时钟,ED意为Edge,意为通过这一路的时钟,上升沿和下降沿均有效。
第四个外部时钟可来自TIMx_CH1的TI1FP1和来自TIMx_CH2的TI2FP2
总结一下,外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚;外部时钟模式2的输入只能是ETR引脚。
如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单最直接
以上就是有关时钟输入的部分
通用计时器库函数
关于图中引脚对应可以参考引脚定义图
红框所标出来的意思:这个TIM2的CH1和ETR脚都复用在PA0引脚,下面还有CH2、CH3、CH4(CH是通道)和其他定时器的一些引脚,也都可以在这里找到。
中间由红框标出来的寄存器是捕获/比较寄存器,是输入捕获和输出比较电路共用的,因为输入捕获和输出比较不能同时使用,所以这里的寄存器是共用的,引脚也是共用的。
)
1.2_1_ 计数器模式
像这样带一个黑色阴影的寄存器,都是有影子寄存器这样的的缓冲机制的,包括预分频器,自动重装寄存器和下面的捕获比较寄存器,所以计数的这个ARR自动重装寄存器,也是有一个缓冲寄存器的,并且这个缓冲寄存器是用还是不用,是可以自己设置的
38:45计数器有无缓冲寄存器的情况
1.2_2_ 时钟选择(电路讲解)
时钟源的输入——时钟源
预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以你可以直接认为时基单元直接连到了输入端,也就是内部时钟CK_INT。内部时钟的来源是RCC_TIMXCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72M。
计数器的时钟由内部时钟(CK_INT)提供。TIMx CR1寄存器的CEN位和TIMx EGR寄存器的UG位是实际的控制位, (除了UG位被自动清除外)只能通过软件改变它们。一旦置CEN位为’1’,内部时钟即向预分频器提供时钟。下图示出控制电路和向上计数器在普通模式下,没有预分频器时的操作。
计数器时钟可由下列时钟源提供:
内部时钟(CK_INT)
外部时钟模式1:外部输入脚(TIx)
外部时钟模式2:外部触发输入(ETR)
内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器。
【注:编码器接口可以读取编码器的输出波形】
内部时钟(CK_INT)
外部时钟模式1:外部输入脚(TIx)
当TIMx_SMCR寄存器的SMS=111时,此模式被选中。计数器可以在选定输入端的每个上升沿或下降沿计数。
当这个TRGI当做外部时钟来使用的时候,这一路就叫做“外部时钟模式1”,那通过这一路的外部时钟都有哪些呢?
第一个,就是ETR引脚的信号
然后第二个,就是ITR信号,这一部分的时钟信号是来自其他定时器,从右边可以看出,这个主模式的输出TRGO可以通向其他定时器,那通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了。
这个ITRO到ITR3分别来自其他4个定时器的TRGO输出,至于具体的连接方式是怎么的,手册的这个位置有一张表。这里可以看到,TIM2的ITRO是接在了TIM1的TRGO上,ITR1接在了TIM8,ITR2接在了TIM3,ITR3接在了TIM4,其他定时器也都可以参照一下这个表,这就是TR和定时器的连接关系。通过这一路我们就可以实现定时器级联的功能.比如我可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联.
这里还可以选择TI1F_ED,这里连接的是这里输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思,也就是通过这一路输入的时钟,上升沿和下降沿均有效
最后,这个时钟还能通过TI1FP1和TI2FP2获得
总结一下就是,外部时钟模式1的输入可以是ETR引脚、其他定时器,CH1引脚的边沿、CH1引脚和CH2引脚,这还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了。上面设置这么复杂的输入,不仅仅是为了扩大时钟输入的范围,更多的还是为了某些特殊应用场景而设计的,比如为了定时器的级联而设计的ITRx引脚,最后的一部分,我们之后讲输入捕获和测频率时,还会继续讲到。
注:对于时钟输入而言,最常用的还是内部的72MHz的时钟,如果要使用外部时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单、最直接。
外部时钟模式2:外部触发输入(ETR),
计数器能够在外部触发ETR的每一个上升沿或下降沿计数。
这个ETR(External)引脚的位置,可以参考一下引脚定义表。
可以看到这里有TIM2_CH1_ETR,意思就是这个TIM2的CH1和ETR都是复用在了这个位置,也就是PA0引脚,下面还有CH2,CH3,CH4和其他定时器的一些引脚,也都可以在这里找到。
那这里我们可以在这个TIM2的ETR引脚,也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这两块电路可以对外部时钟进行一定的整形。因为是外部引脚的时钟,所以难免会有的毛刺,那这些电路就可以对输入的波形进行滤波,同时也可以选择一下极性和预分频器。最后,滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了。
如果你想在ETR外部引脚提供时钟或者想对ETR时钟进行计数,把这个定时器当做计数器来用的话,那就可以配置这一路的电路,在STM32中,这一路也叫做“外部时钟模式2“。
例如,要配置在ETR下每2个上升沿计数一次的向上计数器,使用下列步骤:
1,本例中不需要滤波器,置TIMx_SMCR寄存器中的ETF(握)= 0000
2,设置预分频器,置TIMx_SMCR寄存器中的早期胸腺祖细胞(1:0)= 1
3.设置在ETR的上升沿检测,置TIMx_SMCR寄存器中的ETP=0
4,开启外部时钟模式2,置TIMx_SMCR寄存器中的ECE=1
5.,启动计数器,置TIMx_CR1寄存器中的CEN=1
计数器在每2个ETR上升沿计数一次。
在ETR的上升沿和计数器实际时钟之间的延时取决于在ETRP信号端的重新同步电路。
内部触发输入(ITRx)(定时器同步)
所有TIMx定时器在内部相连,用于定时器同步或链接。当一个定时器处于主模式时,它可以对另一个处于从模式的定时器的计数器进行复位、启动、停止或提供时钟等操作。
配置定时器1为主模式,它可以在每一个更新事件UEV时输出一个周期性的触发信号。在TIM1_CR2寄存器的MMS='010’时,每当产生一个更新事件时在TRGO1上输出一个上升沿信号。
连接定时器1的TRGO1输出至定时器2,设置TIM2_SMCR寄存器的TS =‘000’,配置定时器2为使用ITR1作为内部触发的从模式。(为什么是‘000’,硬件底层已经根据不同选择定义好了)
然后把从模式控制器置于外部时钟模式1(TIM2 SMCR寄存器的SMS-111):这样定时器2即可由定时器1周期性的上升沿(即定时器1的计数器溢出)信号驱动。
最后,必须设置相应(TIMx_CR1寄存器)的CEN位分别启动两个定时器。如果OCx已被选中为定时器1的触发输出(MMS=1xx),它的上升沿用于驱动定时器2的计数器。
注:如果OCx已被选中为定时器1的触发输出(MMS=1xx),它的上升沿用于驱动定时器2的计数器。
这一段内容是涉及参考手册14.3.15的内容,关于这个模式还有更多功能,比如:使用一个定时器使能另一个定时器;使用一个定时器去启动另一个定时器;使用一个定时器作为另一个的预分频器;使用一个外部触发同步地启动2个定时器,感兴趣的可以自己去了解
编码器模式
最后这里还有一块没有讲到,这个是定时器的一个编码器接口(红框下方),可以读取正交编码器的输出波形,这个我们后续课程也会再讲。
4.主从触发模式功能
图中的TRGO与基本定时器类似,它可以将定时器内部的一些事件映射到其他电路,从而完成其他电路的功能。
这部分电路可以把内部的一些事件映射到这个TRGO引脚上,比如我们刚才讲基本定时器分析的,将更新事件映射到TRGO,用于触发DAC。这里也是一样,它可以把定时器内部的一些事件映射到这里来,用于触发其它定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。
5.输出比较功能
通用定时器结构图的右下角即为定时器的输出比较功能的结构,如下图所示。有四个输出通道,分别对应CH1到CH4的引脚,可以用来输出PWM波形,驱动电机。
6.输入捕获电路
通用定时器的左下角即为输入捕获电路的结构图,它同输出比较功能一样有四个通道,对应CH1到CH4。可以用于测量输入方波的频率。因为输入捕获和输出比较不能同时使用,故中间的捕获/比较寄存器是输入捕获和输出比较电路共用的,CH1到CH4的引脚也是共用的。
那有关输入捕获和输出比较这部分电路,在之后具体分析
了解:通用定时器中异或门的作用
高级定时器
高级定时器拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能。高级定时器结构框图如下图所示:
高级定时器的大部分结构和通用定时器相同,只在部分作了功能拓展。相比于通用定时器,拓展了框图右边红圈的内容。
1.重复次数计数器
在申请中断的的信号输出处,增加了一个重复次数计数器,它的作用是:可以实现每隔几个计数周期,才发生一次更新事件和中断。原来的结构是每个计数周期完成后就都会发生更新,现在这个计数器实现每隔几个周期再更新一次,相当于对输出的更新信号又作了一次分频。(对于高级定时器,我们之前计算的最大定时时间59秒多,在这里就还需要再乘一个65536,也就是提升了很多的定时时间)
下面部分,是高级定时器对输出比较模块的升级了,暂时了解即可
2.死区生成电路与三相无刷电机
图中的DTG和DTG寄存器组成死区生成电路,右侧的引脚TIMx_CH1/CH2/CH3由原来的每路一个变成了两个互补的输出引脚(TIMx_CH1/CH2/CH3和TIMx_CH1N/CH2N/CH3N),可以输出一对互补的PWM波。这些电路是为了驱动三相无刷电机设计的。在四轴飞行器、电动车后轮、电钻中都可以发现三相无刷电机。三相无刷电机的驱动电路需要三个桥臂,每个桥臂需要2个大功率开关管来控制,总共需要6个大功率开关管控制。所以输出的PWM引脚的前三路就变为了互补的输出引脚,而第四路TIMx_CH4没有变化。三相电机只需要三路。
为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,故添加了死区生成电路。在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全部关断,防止出现直通现象。
3.刹车输入
刹车输入的主要作用是给电机驱动提供安全保障。如果外部引脚BKIN(Break In)产生了刹车信号,或者内部时钟失效,产生了故障,控制电路就会自动切断电机的输出,防止意外的发生。
定时中断基本结构
定时中断的基本结构如下图所示:
首先中间最重要的还是PSC(Prescaler)预分频器、CNT (Counter)计数器、ARR (AutoReloadRegister)自动重装器这三个寄存器构成的时基单元。下面这里是运行控制,就是控制寄存器的一些位,比如启动停止、向上或向下计数等等,我们操作这些寄存器就能控制时基单元的运行了。
左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2。在本小节示例程序里,第一个定时器定时中断就是用的内部时钟这一路,第二个定时器外部时钟就是用的外部时钟模式2这一路。当然还可以选择这里的触发输入当做外部时钟,即外部时钟模式1,对应的有ETR外部时钟、TTRX其他定时器、TlX输入捕获通道,这些就是定时器的所有可选的时钟源了。最后这里,还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。
接下来右边这里,就是计时时间到,产生更新中断后的信号去向。那这里中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。
为什么会有一个中断输出控制呢?
因为这个定时器模块有很多地方都要申请中断。比如上面这个图不仅更新要申请中断,这里触发信号也会申请中断,还有下面的输入捕获和输出比较匹配时也会申请。所以这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止。简单来说,这个中断输出控制就是一个中断输出的允许位。
时基单元运行时序举例
STM32中,关于时序运行的内容很多,具体请见手册的详细讨论,这里仅举一些时基单元的例子作简要分析。
1.缓冲(影子)寄存器
结构图中如下红圈中,带黑色阴影的寄存器,都是有影子寄存器这样的缓冲机制,包括预分频器,自动重装载寄存器和捕获比较寄存器;这个缓冲寄存器是用还是不用,是可以自己设置的。
STM32在设计之初,为了保证能适用于多种多样的情况,故对时序运行过程中突然手动更改寄存器对时序的影响作了严谨的设计。这里引入缓冲(影子)寄存器,主要目的就是同步,即可以让寄存器设定的某些目标值的变化和更新事件同时发生,防止在运行途中更改造成错误。在定时器结构图中,有些寄存器的画法采用了方框下加阴影的方式,就说明该寄存器不是只有一个寄存器,而是有两个寄存器来形成缓冲机制。实际上,真正使时序电路状态发生更改的都是影子寄存器。
2.预分频器时序分析
下图描述了当预分频器的分频系数从1变为2时,计数器的时序图。第一行是CK_PSC是预分频器的输入时钟,这个时钟在不断运行;下面的CNT_EN是计数器使能,高电平计数器正常运行,低电平计数器停止,再下面是CK_CNT是计数器时钟既是预分频器的时钟输出也是计数器的时钟输入。开始时,计数器未使能,计数器时钟不运行;然后使能后,前半段,当计数器使能信号CNT_EN变为高电平后的下一个CK_PSC的高电平,定时器时钟CK_CNT接收CK_PSC。且此时预分频器的分频系数为1,PSC = 0,预分频器完成一分频,计数器时钟等于预分频前的时钟,即,CK_PSC = CK_CNT;后半段,预分频系数变为2,计数器时钟变为预分频前时钟的一半。
在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增;当计数器寄存器的值依次递增达到0xFC后立即跳变为0x00,说明重装载寄存器ARR设计的目标计数值就是0xFC,此时电路产生一个更新事件脉冲信号UEV,并产生中断信号,计数值清0。这就是一个计数周期的工作流程。
然后是最下面的三行时序,描述的是预分频寄存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个:一个是倒数第三行的预分频控制寄存器,供读写用并不直接决定分频系数;另一个是倒数第二行的预分频缓冲寄存器(影子寄存器),才是真正起作用的寄存器,
在更新事件信号之前在TIMx_PSC中写入新数值,将预分频器的分频系数从1改为2,但是由于缓冲寄存器的存在,CK_CNT不会立即变为CK_PSC / 2,而是在下一次更新中断产生的同时,由预分频缓冲器(影子寄存器)修改分频系数为2,PSC = 1。
由预分频计数器时序可以看到,预分频的分频功能实际上也是通过计数器来实现的。当分频系数变为2后,预分频计数器按0、1、0、1依次计数,每当预分频计数器回到0时,预分频器输出信号,CN_CNT输出一个脉冲。
计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
3.计数器时序分析
1.计数器工作时序图如下。
内部分频因子为2,就是分频系数为2。第一行是内部时钟72MHz,第二行是时钟使能,高电平启动,第三行是计数器时钟,因为分频系数为2,所以这个频率是上面CK_INT除2,然后计数器在这个时钟每个上升沿自增,当增到0036时发生溢出,之后再来一个上升沿,计数器清零,产生一个更新事件脉冲,另外还会置一个更新中断标志位UIF,标志位UIF只要置1就会去申请中断,然后中断响应后,需要在中断程序中手动清零,以上就是计数器的工作流程。
计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
计数器溢出时间:1/计数器溢出频率
- 计数器无预装时序图(缓冲机制失效 APRE = 0)
计数器无预装时序就是没有缓冲寄存器的情况。
在计数器正在进行自增计数,突然更改了自动加载寄存器,就是自动重装载寄存器由FF改成了36,即计数值的目标值就由FF变成了36,所以计数器寄存器计到36之后,就直接更新,开始下一轮计数。
3.计数器有预装时序(缓冲机制有效 APRE = 1)
计数器有预装时序就是有缓冲寄存器的情况。
通过设置ARPE位,就可以选择是否使用预装功能。
在有预装的情况下,在计数中途,若突然将自动加载寄存器计数目标由F5改成了36,下面影子寄存器才是真正起作用的,它还是F5,所以现在计数的目标还是计到F5,产生更新事件,同时,要更改的36才被传递到影子寄存器,在下一个计数周期这个更改的36才有效(类似10086,本月更改,下月生效),所以引入影子寄存器的目的实际上是为了同步,就是让值的变化和更新事件同步发生,防止在运行途中更改造成错误。
在上面这个例子中,若不用影子寄存器的话,更改TIMx_ARR寄存器的值有一种不严谨情况:当F5改到36立即生效,但此时计数器已经到了F1,已经超过36了,F1只能增加,但它的目标值却是36比F1小,此时计数器寄存器的值只能递增,故该寄存器会一直递增到最大值0xFFFF之后回到0x0000,再依次递增,再加到36,才能产生更新。这里就可以看出,如果不使用缓冲机制,可能会给电路时序的工作造成一些问题。
4.RCC时钟树简介
RCC时钟树:在STM32中用来产生和配置时钟,并且把配置好的各个外设都发射到各个外设的系统。 时钟是所有外设运行的基础,所以时钟是最先配置的东西。在程序执行时,在执行主程序之前还会执行一个SystemInit函数,这个函数的作用就是配置RCC时钟树。
RCC时钟树可以分为左右两部分:时钟产生电路(左)和时钟分配电路(右)。中间的SYSCLK就是系统时钟72MHz。
1.时钟产生电路
在时钟产生电路,有四个振荡源,分别是内部的8MHz高速RC振荡器、外部的4-16MHz高速晶振振荡器(也就是晶振,一般都外接8MHz)、外部的32.768kHz低速晶振振荡器(一般给RTC提供时钟)、内部的40kHz低速RC振荡器(给看门狗WDG提供时钟)。上面的连个该高速晶振是用来提供系统时钟的,AHB\APB2\APB1的时钟都是来源于这两个高速晶振。内部和外部都有一个8MHz的晶振,只不过外部的石英振荡器比内部的RC振荡器更加稳定,所以一般都用外部晶振。如果系统非常简单,且不需要过于精确的时钟,就可以使用内部的RC振荡器,这样可以省下外部的晶振电路。
在SystemInit函数中是这样来配置时钟的:首先会启动内部的8MHz高速RC振荡器产生时钟,选择该时钟为系统时钟,暂时以8MHz的内部时钟运行;然后再启动外部时钟,配置外部时钟信号流经如下图所示的电路:
外部晶振信号进入PLLMUL锁相环进行倍频,8MHz倍频9倍,得到72MHz,待锁相环输出稳定后,选择锁相环输出为系统时钟。这样就把系统时钟从8MHz切换为了72MHz,以上就是配置的过程。
这样分析可以解决一个问题:如果外部晶振出问题,可能会出现程序时钟慢大概10倍的现象。如果外部时钟的硬件电路有问题(晶振短路或连接错误等),系统的时钟就无法切换到72MHz,会保持内部的8MHz运行。8M相比于72M大概就慢了10倍。
图中的CSS称为时钟安全系统,它同样负责切换时钟。CSS可以检测时钟的运行状态,一旦外部时钟失效,它就会自动把外部时钟切换为内部时钟,从而保证程序可以正常运行,不会卡死造成事故。另外在高级定时器的刹车输入功能中,一旦CSS检测到外部时钟失效,通过或门就会立刻反应到输出控制器,让输出控制的电机立刻停止,防止意外。(即切断输出控制引脚,切断电机输出,防止发生意外。)
2.时钟分配电路
首先系统时钟72MHz进入AHB总线,在AHB总线上有一个预分频器,在SystemInit函数配置的默认分频系数为1,所以AHB总线的时钟自然是72MHz。
之后信号进入APB1总线,APB1上同样有预分频器,这里SystemInit默认配置的分频系数为2,输出为36MHz,所以APB1总线的时钟为36MHz。通用定时器和基本定时器是接在APB1上的,但是APB1(APB2同理)连接定时器还有如图所示的以下结构:
通用定时器和基本定时器通过图中APB1下方的支路与APB1连接。由于APB1的预分频系数默认为2,则输出到定时器的时钟频率×2。APB2的预分频器的分频系数默认配置为1,其他流程与APB1同理。所以基本定时器,通用定时器,高级定时器的内部基准时钟都是72MHz,这样设计为我们使用定时器带来了方便,不用考虑不同定时器时钟不同的问题了(前提是不乱修改SystemInit函数中的默认配置)。
在时钟输出端口,都有一个与门进行时钟输出控制。控制端外部时钟使能就是程序中 RCC_APB2/1 PeriphClockCmd 函数作用的地方。打开时钟就是写1进行按位与。
注意:(实际)预分频系数=预分频器的值+1
参考手册
继续深入学习,可查阅参考手册
示例程序(定时器定时中断&定时器外部时钟)
知识点get:
滤波器工作原理:可以滤掉信号的抖动干扰。在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个采样值输出出去;如果这N个采样值不全都相同,那就说明信号有抖动,这时就保持上一次的输出,或者直接输出低电平也行,这样就能保证输出信号在一定程度上的滤波;这里的采样频率f和采样点数N都是滤波器的参数,频率越低,采样点数越多,滤波效果越好,不过相应的信号延迟就越大;采样频率f由内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,这个分频多少是由参数ClockDivision决定的,这个参数其实跟时基单元关系并不大,它的可选值可以选择1分频(也就是不分频),2分频和4分频。
定时器相关的库函数非常多,本节仅对将要使用的库函数和 亿些使用细节 进行说明(即使这样也还是很多)。
定时中断基本结构如下,便于理解下面的库函数及程序流程。
定时器初始化步骤如下,对应定时中断结构图
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,参数用结构体配置
第四步,配置输出中断控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能计数器,当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个中断函数,中断函数每隔一段时间就能自动执行一次
第一步,RCC开启时钟,这个基本上每个代码都是第一步。在这里打开时钟后,定时器的基准时钟和整个外设的工作时钟就都会同时打开了
第二步,选择时基单元的时钟源。对于定时中断,我们就选择内部时钟源
注:没选择时钟,会默认内部时钟
然后最后一个函数,TIM_ETRConfig,这个不是用来选择时钟的,就是单独用来配置ETR引脚的预分频器、极性、滤波器这些参数的
涉及函数如下:
void TIM_InternalClockConfig(TIM_TypeDef* TIMx)
作用:配置TIMx内部时钟
参数说明:
第三步,配置时基单元。包括这里的预分频器、自动重装器、计数模式等等,这些参数用一个结构体就可以配置好了。
涉及函数如下:
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
作用:根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx时基单元外设。
参数说明:
18:49~20:17
如何确定时间参数讲解
假设定时1s,也就是定时频率为1Hz,那我们就可以PSC给一个7200,ARR给一个10000,然后两个参数都再减一个1,因为预分频器和计数器都有1个数的偏差,所以这里要再减个1。然后注意这个PSC和ARR的取值都要在0~65535之间,不要超范围了
第四步,配置输出中断控制,允许更新中断输出到NVIC(开启更新中断到NVIC的通路)
涉及函数如下:
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)
作用:启用或禁用指定的TIM中断。
参数说明:
注:TIM_IT_Update 更新中断
在STM32库里还提及其它中断源
第五步,配置NVIC,在NMC中打开定时器中断的通道,并分配一个优先级。这部分在上节我们也用过,流程基本是一样的
涉及函数:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
第六步,就是运行控制了。整个模块配置完成后,我们还需要使能一下计数器。要不然计数器是不会运行的。当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断。
涉及函数如下:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
作用:启用或禁用指定的TIM外设。
参数说明:
这样初始化基本上就OK了,接下来,我们再看几个函数,因为在初始化结构体里有很多关键的参数,比如自动重装值和预分频值等等,这些参数可能会在初始化之后还需要更改,如果为了改某个参数还要再调用一次初始化函数,那太麻烦了。所所以这里有一些单独的函数,可以方便地更改这些关键参数。
比如这里的TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode),就是用来单独写预分频值的,看一下参数,Prescaler,就是要写入的预分频值;后面还有个参数,PSCReloadMode,写入的模式。我们上一小节说了,预分频器有一个缓冲器,写入的值是在更新事件发生后才有效的,所以这里有个写入的模式,可以选择是听从安排,在更新事件生效,或者是,在写入后,手动产生一个更新事件,让这个值立刻生效。
TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);,用来改变计数器的计数模式,参数CounterMode,选择新的计数器模式。
TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);,自动重装器预装功能配置。
TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);,给计数器写入一个值。如果你想手动给一个计数值,就可以用这个函数
TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);给自动重装器写入一个值,如果你想手动给一个自动重装值,就可以用这个函数
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);获取当前计数器的值,如果你想看当前计数器计到哪里了,就可以调用一下这个函数,返回值就是当前的计数器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);获取当前的预分频器的值
最后我们再写一个定时器的中断函数。这样这个中断函数每隔一段时间就能自动执行一次了。
本次实验要完成的现象是:定义一个 uint16_t 的 Num 变量,使其每秒+1。
Timer.c
#include "stm32f10x.h" // Device header
/*
定时器初始化
对应定时中断结构图
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,参数用结构体配置
第四步,配置输出中断控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能计数器,当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个中断函数,中断函数每隔一段时间就能自动执行一次
*/
void Timer_Init(void) //定时中断初始化代码
{
//初始化tim2,也就是通用定时器
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
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; //计数器模式
/*定时1s也就是定时频率为1Hz,定时频率=72M/ (PSC + 1) / (ARR + 1) = 1s =1Hz,
那就可以PSC给7200,ARR给10000(1MHz等于10^6Hz),然后两个参数再减1
在这里预分频是对72M进行7200分频,得到的就是10k的计数频率,
在10k的频率下,计10000个数,就是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函数的最后,会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值
预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用
为了让写入的值立刻起作用,故在函数的最后手动生成了一个更新事件
但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题*/
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启更新中断到NVIC的通道
//NVIC中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//NVIC优先级分组
NVIC_InitTypeDef NVIC_InitTyStructure;
NVIC_InitTyStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitTyStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitTyStructure.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
NVIC_InitTyStructure.NVIC_IRQChannelSubPriority = 1;//响应优先级
NVIC_Init(&NVIC_InitTyStructure);
//启动定时器
TIM_Cmd(TIM2,ENABLE);//当产生更新时,就会触发中断
}
/*
中断函数模版
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
//检查中断标志位
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
//执行相应的用户代码
Num ++;
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:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);//CNT计数器值的变化情况(变化范围是ARR从0一直到自动重装值(10000-1))
}
}
//定时器2中断函数放在使用中断的main.c文件中;在startup文件中
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
//检查中断标志位
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
//执行相应的用户代码
Num ++;//定时器每秒自动加一个Num全局变量
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除标志位
}
}
定时器外部时钟选择
可以在引脚定义图里找TIMx的ETR引脚是哪个
在上一个定时中断实例程序基础上进行更改;基本任务仍然是定时中断,时钟部分就不使用内部时钟了
本次实验要完成的现象是:用光敏传感器手动模拟一个外部时钟,定义一个 uint16_t 的 Num 变量,当外部时钟触发10次(预分频之后的脉冲)后Num + 1。器件连接图和程序源码如下所示:
提示:
这里推荐配置是浮空是输入,但是我一般不太喜欢浮空输入平因为一旦悬空,电平就会跳个没完,所以我准备给上拉输入,这也是可以的。
那什么时候需要用浮空输入呢?就是如果你外部的输入信号功率很小,内部的这个上拉电阻可能会影响到这个输入信号,这时就可以用一下浮空输入,防止影响外部输入的电平。
在6-1的基础上更改,尤其注意在第二步更改时基单元的时钟源,通过ETR引脚的外部时钟模式2配置。
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter)
作用:配置TIMx外部时钟模式2
参数说明:
Timer.c
#include "stm32f10x.h" // Device header
/*
定时器初始化
对应定时中断结构图
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,参数用结构体配置
第四步,配置输出中断控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能计数器,当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个中断函数,中断函数每隔一段时间就能自动执行一次
*/
void Timer_Init(void) //定时中断初始化代码
{
//初始化tim2,也就是通用定时器
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
//外部模式2需要用到gpio,进行GPIOA的时钟配置
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);
//通过ETR引脚的外部时钟模式2配置
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);
//配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
/*定时1s也就是定时频率为1Hz,定时频率=72M/ (PSC + 1) / (ARR + 1) = 1s =1Hz,
那就可以PSC给7200,ARR给10000(1MHz等于10^6Hz),然后两个参数再减1
在这里预分频是对72M进行7200分频,得到的就是10k的计数频率,
在10k的频率下,计10000个数,就是1s的时间*/
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函数的最后,会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值
预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用
为了让写入的值立刻起作用,故在函数的最后手动生成了一个更新事件
但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题*/
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启更新中断到NVIC的通道
//NVIC中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//NVIC优先级分组
NVIC_InitTypeDef NVIC_InitTyStructure;
NVIC_InitTyStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitTyStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitTyStructure.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
NVIC_InitTyStructure.NVIC_IRQChannelSubPriority = 1;//响应优先级
NVIC_Init(&NVIC_InitTyStructure);
//启动定时器
TIM_Cmd(TIM2,ENABLE);//当产生更新时,就会触发中断
}
//函数封装
uint16_t Timer_getcounter(void)
{
return TIM_GetCounter(TIM2);
}
/*
中断函数模版
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
//检查中断标志位
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
//执行相应的用户代码
Num ++;
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:");
OLED_ShowString(2,1,"CNT:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);//CNT计数器值的变化情况(变化范围是ARR从0一直到自动重装值(10000-1))
}
}
//定时器2中断函数放在使用中断的main.c文件中;在startup文件中
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
//检查中断标志位
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
//执行相应的用户代码
Num ++;//定时器每秒自动加一个Num全局变量
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除标志位
}
}
TIM输出比较
输出比较功能简介
输出比较,英文全称Output Compare,简称OC。它最主要的功能是 可以通过比较计数器CNT和捕获/比较寄存器(Capture/Compare Register)CCR值的关系,来输出电平进行置1、置0的翻转操作,用于输出一定频率和占空比的PWM波形。
每个高级定时器和通用定时器都拥有4个输出比较的通道,可以同时输出4路PWM波形,且高级定时器的前3个通道额外拥有死区生成电路和互补输出的功能(用于驱动三相无刷电机)。4个输出比较通道都有独立的CCR寄存器,但是它们共用同一个CNT计数器。
捕获/比较寄存器是输入捕获和输出比较共用的,当使用输入捕获时,它就是捕获寄存器;当使用输出比较时,它就是比较寄存器。那在输出比较这里,这块电路会比较CNT和CCR的值,CNT计数自增,CCR是我们给定的一个值,当CNT大于CCR、小于CCR或者等于CCR时,这里输出就会对应的置1、置0、置1、置0,这样就可以输出一个电平不断跳变的PWM波形了。这就是输出比较的基本功能。
PWM简介
PWM(Pulse Width Modulation),即脉冲宽度调制,PWM波形是一个数字输出信号,是由高低电平组成的,是一种对模拟电平信号进行数字编码的方法。在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。也就是说,使用PWM波形,是用来等效地实现一个模拟信号的输出。(例如,led呼吸灯,电机调速,如下解释)
以LED为例:GPIO的输出信号只能是数字信号,如果想通过数字信号输出模拟量,可以通过以下的方法实现:让LED不断点亮、熄灭、点亮、熄灭,当点亮、熄灭的频率足够大时,由于LED的余晖和人眼的视觉暂留效应,LED就会呈现出一个中等亮度。当调控点亮和熄灭的时间比例时就能让LED呈现出不同的亮度级别。
对于电机调速也类似:在高频率下不断让电机交替通断,由于电机断电后不会立刻停止,而是由于惯性转动后停下,电机的速度就能维持在一个中等速度。
PWM的秘诀是:天下武功,唯快不破! 需要注意的是:只有在具有惯性的系统中,才能用PWM对模拟信号进行编码。
从下图可以看出,高低电平跳变的数字信号可以被等效地表示为中间虚线所表示的模拟量。当上面电平时间长一点,下面电平短一点的时候,等效地模拟量就偏向于上面;当下面电平时间长一点,上面电平时间短一点的时候,等效地模拟量就偏向于下面。也就是说,占空比越大,等效的模拟量就越趋近于数字量的高电平;占空比越小,等效的模拟量就越趋近于数字量的低电平,且这个等效关系一般而言是线性一一对应的。
使用PWM波形,就可以在数字系统等效输出模拟量,就能实现LED控制亮度、电机控速等功能了。
首先,明白Ts就是下图这里,Ts代表一个高低电平变换周期的时间
在使用PWM对模拟量进行编码时,以下三个参数尤其重要:
频率 :f = 1 / Ts(周期的倒数就是频率);变换越快=频率越大(PWM的频率越快,它等效模拟的信号就越平稳,不过同时性能开销就越大;一般来说PWM的频率在几kHz到几十kHz之间。)
占空比:q=Ton/Ts( Ton是高电平的时间,Ts是一个周期的时间。q就是高电平时间相对于整个周期时间的比例);占空比决定了PWM等效出的模拟电压的大小。一般用百分比表示。
分辨率:占空比的变化步距;分辨率就是占空比变化的精细程度。即,占空比最小能以百分之多少的精度变化,它的值可以是1%、0.1%。分辨率的大小要看实际项目的需求定。如果既要高频率,又要高分辨率,就需要硬件电路要有足够的性能。要求不高的情况下,1%的分辨率就足够使用了。
使用这个PWM波形,是用来等效地实现一个模拟信号的输出
问题:数字输出端口控制LED,按理说LED只能有完全亮和完全灭两种状态,怎么能实现控制亮度大小呢?
通过PWM就可以实现,我们让LED不断点亮、熄灭、点亮、熄灭,当这个点亮、熄灭的频率足够大时,LED就不会闪烁了,而是呈现出一个中等亮度。当我们调控这个点亮和熄灭的时间比例时,就能让LED呈现出不同的亮度级别。对于电机调速也是一样。
当然,PWM的应用场景必须要是一个惯性系统,就是说LED在熄灭的时候,由于余晖和人眼视觉暂留现象,LED不会立马熄灭,而是有一定的惯性,过一小段时间才会熄灭。电机也是,当电机断电时,电机的转动不会立马停止,而是有一定的惯性,过一会才停。
输出比较通道(通用定时器)
那接下来我们就来具体地分析一下,定时器的输出比较模块是怎么来输出PWM波形的,我们先看一下通用定时器的这个结构。
图的左边是CNT计数器和CCR1第一路的捕获/比较寄存器,它俩进行比较,当CNT = CCR1或者CNT > CCR1时,输出模式控制器就会收到一个信号,输出模式控制器就会改变它输出的OC1REF的高低电平。REF是Reference的缩写,意为参考信号。上面有个ETRF输入(是定时器的一个小功能,一般不用,不需要了解)
接下来OC1REF信号兵分两路:一路以将REF信号映射到主模式控制器的TRGO上,去触发其他外设的功能;不过REF的主要去向还是下面这一路,通往一个极性选择电路,通过控制TIMx_CCER寄存器的值(0或1),可以选择是否将REF信号翻转(写0信号就会往上走,就是信号电平不翻转,进来哈样出去还是哈样;写1信号就会往下走,就是信号通过一个非门取反,输出的信号就是输入信号高低电平反转的信号,这就是极性选择,就是选择是不是要把高低电平反转一下),之后通往输出使能电路,可以控制是否输出,最后通往OC1引脚,即TIMx_CH1通道的引脚(在引脚定义表中即可找到具体的GPIO口)。
补充:
极性选择电路
非门取反
输出模式控制器的执行逻辑(工作流程)
接下来我们还需要看一下这个输出模式控制器,它具体是怎么工作的。什么时候给REF高电平,什么时候给REF低电平。我们看一下下面的这个表,这就是输出比较的8种模式,也就是这个输出模式控制器里面的执行逻辑。这个模式控制器的输入是CNT和CCR的大小关系,输出是REF的高低电平,里面可以选择多种模式来更加灵活地控制REF输出。这个模式可以通过寄存器来进行配置,具体操作看下面的表
冻结:CNT = CCR时维持原状态,实际上此时REF与CNT和CCR都无关,即CNT和CCR无效,REF保持为上一个状态。这有什么用呢?比如你正在输出PWM波,突然想暂停一会儿输出,就可以设置成这个模式,一但切换为冻结模式后,输出就暂停了,并且高低电平也维持为暂停时刻的状态,保持不变。这就是冻结模式的作用
无效/有效电平:CNT = CCR时REF置无效/有效电平。这两个模式不适合输出连续变化的波形。如果想定时输出一个“一次性”的信号,则可以考虑这两个模式。有效电平和无效电平是高级定时器中的表述,与关断、刹车等功能配合表述的,这里表述的比较严谨。在这里为了理解方便,可以直接认为有效电平就是高电平,无效电平就是低电平。
匹配时…
这三个模式都是当CNT与CCR值相等时,执行操作。
这些模式就可以用做波形输出了,比如相等时电平翻转这个模式,这个可以方便地输出一个频率可调,占空比始终为50%的PWM波形。比如你设置CCR为0,那CNT每次更新清0时,就会产生一次CNT=CCR的事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期,并且高电平和低电平的时间是始终相等的,也就是占空比始终为50%,当你改变定时器更新频率时,输出波形的频率也会随之改变。它俩的关系是输出波形的频率=更新频率/2,因为更新两次输出才为一个周期。这就是匹配时电平翻转模式的用途。
那上面这两个相等时置高电平和低电平,感觉用途并不是很大,因为它们都只是一次性的,置完高或低电平后,就不管事了,所以这俩模式不适合输出连续变化的波形。如果你想定时输出一个一次性的信号,那可以考虑一下下这两个模式。
与冻结模式类似。如果你想暂停波形输出,并且在暂停期间保持低电平或者高电平,那你就可以设置这两个强制输出模式。
它们可以用于输出频率和占空比都可调的PWM波形,也是我们主要使用的模式。这个情况比较多,一般我们都只使用向上计数,向上/向下计数之间也只有大小关系、极性不用,基本思想都是一样的。PWM模式1/2的向上计数区别就是输出的高低电平反过来了,PWM模式2实际上就是PWM模式1输出的取反(改变PWM模式1和PWM模式2,就只是改变了REF电平的极性而已),是因为REF输出之后还有一个极性的配置,所以使用PWM模式1的正极性和PWM模式2的反极性最终的输出是一样的。所以使用的话,我们可以只使用PWM模式1,并且是向上计数,这一种模式就行了。
输出PWM波形及参数计算
那PWM模式1向上计数是怎么输出频率和占空比都可调的PWM波形的呢?
以PWM模式1、向上计数模式为例,PWM波形产生原理(输出PWM的基本结构)如下图所示:
在上图中,首先左上角是时间单元和运行控制部分,再左边是时钟源选择(省略上一小节内容),在这里我们不需要使用更新事件的中断申请(输出PWM暂时还不需要中断)这就是时基单元的部分。配置好了时基单元,这里的CNT就可以开始不断地自增运行了。然后,下面粉红区域就是输出比较单元了,总共有四路,输出比较单元的最开始是CCR捕获/比较寄存器,CCR是我们自己设定的,CNT不断自增运行,同时它俩还在不断进行比较;CCR捕获/比较寄存器后面是输出模式控制器,在这里以PWM模式1为例,是PWM模式1的执行逻辑,那它是怎么输出PWM波形的,解释如下,右上角图中,蓝色线是CNT的值,黄色线是ARR的值,蓝色线从0开始自增,一直增到ARR也就是99,之后清0继续自增,在这个过程中红色线是CCR,比如设置CCR为30,执行输出模式控制器里的逻辑,下面的绿色线就是输出,可以看到CNT<CCR时置高电平,之后CNT>=CCR就变为低电平,当CNT溢出清0后,CNT又小于CCR所以置高电平…这样一直持续下去,REF的电平就会不断变化,并且它的占空比是受CCR的值的调控的,如果CCR的值设置的高一些,输出的占空比就会变大,CCR设置的低一点,输出的占空比就会变小,以上就是PWM的工作流程。(这里REF就是一个频率可调,占空比也可调的PWM波形),最终再经过极性选择,输出使能,最终通向GPIO口,这样就能完成PWM波形的输出了。需要注意的是: 设置的CCR值越接近ARR,输出的PWM波形的占空比就越大。
我们就再来看一下PWM的参数是如何计算的
PWM的一个周期如上图中的下面绿色区段的红线区间,可以看出它始终对应着计数器的一个溢出更新周期,所以PWM的频率就等于计数器的更新频率
当CNT = CCR时电路已经置为低电平,故REF为高电平的时间为CNT从0变到29(30个数)的时间。
CCR的值应设置在0到ARR+1的范围里,CCR=ARR+1时占空比是100%,ARR越大,CCR的范围就越大,对应的分辨率就越大
参数计算公式如下所示:
PWM频率:即计数器的更新频率 Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:即占空比变化的步距 Reso = 1 / (ARR + 1),以上定义的分辨率是占空比最小的变化步距。ARR越大,CCR的变化范围就越大,分辨率就越高。(占空比变化的越细腻越好)
输出比较通道(高级定时器)
这个电路仅作了解即可,不需掌握。
舵机和直流电机
舵机
舵机是小型直流伺服电机的一种,是一种根据输入PWM信号占空比来控制舵机输出轴的角度的装置。它有三根输入线,其中两根是电源线,一根是PWM信号输入线。白色输出轴会固定在一个指定的角度不动,固定的位置是由信号线的PWM信号来决定的,这就是舵机的工作方式。
上边右图中可以看出,舵机其实并不是一种单独的电机,可以发现它是由一个直流电机、一个减速齿轮组、一个电位器(电压编码器)和一个控制板 4部分组成的整体。舵机不是一种单独的电机,它的内部是由直流电机驱动的。内部的控制电路板是一个电机的控制系统,整个舵机内部形成了一个闭环的控制系统。
PWM信号输入到控制板,给控制板一个指定的目标角度,然后这个电位器检测输出轴的当前角度,如果大于目标角度,电机就会反转,如果小于目标角度,电机就会正转,最终使输出轴固定在指定的角度,这就是舵机的内部工作流程(简而言之:输入一个PWM波形,输出轴固定在一个角度)。
“伺服”—词源于希腊语“奴隶”的意思,英文为Servo。人们想把某一个结构或系统当作一个得心应手的驯服工具,服从控制信号的要求而动作。伺服的主要任务是按照控制命令的要求,对输出信号和输出功率进行放大、变换与调控等处理,使驱动装置输出的力矩、速度和位置控制得非常灵活方便。由于它的“伺服”性能,因此而得名——伺服系统。它的优势在于:可以非常灵活地控制输出装置的力矩、速度和位置等物理参量。
交流伺服电机和直流伺服电机的共同点是:利用传感器(编码器)对转子的位置、转速、力矩、转向进行检测,斌且将得到的信号经由伺服驱动器反馈给伺服控制器,从而达到调节转子位置、转速、力矩、转向的目的; 二者的不同点在于,一般而言,交流伺服电机相较于直流伺服电机对转子有更高的控制精度。
输入信号脉冲宽度,周期是20ms,也就是一个上升沿到下一个上升沿之间的时间是20ms。
舵机对输入的PWM信号的要求如下:周期为20ms(对应50Hz),高电平宽度为0.5 ~ 2.5ms(就是占空比是这个范围,对应的输出角度如上图)。这时一个180° 的舵机,输出轴的角度是-90° 到+90° 或者你规定是0° 到180°,输入信号脉冲宽度与舵机输出轴转角的对应关系都是线性一一对应的,给个PWM,输出轴就会固定在一个角度。实际应用中,比如机器人、机械臂可以用舵机来控制关节,遥控车、遥控船可以用舵机来控制方向。这里的PWM波形实际上是作为一个通信协议来使用的,与用PWM波形等效出一个模拟输出的关系不大,将PWM当成一个通信协议也是一个比较常见的应用,因为很多控制器都有PWM输出的功能,而且PWM只需要一根信号线就行了,这也是一种应用形式。
接下来,看一下舵机的硬件电路,上图第一个是引脚定义图,在舵机上有三根线,分别是黑(电源负极GND)、红(电源正极+5V)、黄(PWM信号线)。上图第二个图中,在实际应用中,GND就接GND,电源+5V是电机的驱动电源(一般电机都是大功率设备,驱动电源也必须是大功率的输出设备,对于套件中,可以直接从STLINK的5v输出脚引一根线使用USB的5V供电),信号线PWM就直接接到STM32引脚上就行了,PWM只是一个通信线,是不需要大功率的。
直流电机及驱动
可以用PWM来控制电机的速度。直流电机是一个单独的电机,里面是没有驱动电路的,所以我们就要外挂一个驱动电路来控制。直流电机是一种能将电能转换为机械能的装置,有两个电极。有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。上图所示的电机是130直流电机。直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作。本课程使用TB6612电机驱动芯片来驱动电机。
TB6612是一款双路H桥型的直流电机驱动芯片,其中有两个驱动电路,可以独立地驱动两个直流电机并且控制其转速和方向。如上左图,是电机驱动板,芯片是TB6612,外围电路只需三个滤波电容就行了。如上右图是H桥电路的基本结构,是由两路推挽电路组成的,比如左边上管导通,下管断开,那左边输出就是接在VM的电机电源正极;下管导通,上管断开,那就是接在PGND的电源负极;如果有两路推挽电路,中间接一个电机,左上和右下导通,电流就是从左流向右,右上和左下导通,电流的方向就反过来从右边流向左边,H桥可以控制电流流过的方向,所以它能控制电机的正反转。
电机驱动电路同样也是一个研究课题,市面上也有很多的电机驱动可供选择,常见的电机驱动芯片有TB6612、DRV8833、L9110、L298N等,另外还有用分立元件MOS管搭建的驱动电路,它可以实现更大的驱动功率。当然也可以自己用MOS管设计电路。
TB6612电机驱动模块的连接电路图和引脚定义图如下所示:
如上图
PWMA、AIN1和AIN2三个引脚控制电机正反转和速度:如上图下面的表,
参考手册
TIM输出比较示例程序(PWM驱动LED呼吸灯&PWM驱动舵机&PWM驱动直流电机)
输出比较相关库函数
1.OC初始化(掌握)
// 配置输出比较模块,输出比较单元有四个,对应也有四个函数
// 第二个参数是结构体,就是输出比较的一些参数
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
// 给输出比较结构体赋一个默认值(防止结构体的值不确定导致一些奇怪的问题)
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
2.OC参数更改(TIM_SetComparex函数最重要,其他的了解即可)
// 使用高级定时器输出PWM波形时使能主输出,否则PWM波形不能正常输出
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
// 单独设置输出比较的输出极性(带N的是高级定时器中互补通道的配置)
// 在这里可以设置输出极性,在OC初始化函数中也可以用结构体设置输出极性,这里相当于将单独修改结构体中的某一参数封装到一个函数中
//在结构体初始化的那个函数里也可以设置极性,这两个地方设置极性的作用是一样的,只不过是用结构体是一起初始化的,在这里是单独函数进行修改的
//一般来说,结构体里的参数都会有一个单独的函数可进行更改
void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC1NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC2PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC2NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC3PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC3NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
// 单独修改输出使能参数
void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx);
void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);
// 单独更改输出比较模式的函数
void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
// 单独更改CCR寄存器值的函数
//在运行时,更改占空比,就需要用到这四个函数
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);
3.OC输出比较的一些小功能(不常用,了解即可)
// 配置强制输出模式(运行中暂停输出波形且强制输出高/低电平)
// 强制输出高电平和设置100%占空比等效,强制输出低电平和设置0%占空比等效
void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC2Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC3Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC4Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
// 配置CCR寄存器的预装功能(影子寄存器,就是写入的值不会立即生效而是在更新事件才会生效,可以避免一些小问题)
void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
// 配置快速使能(手册中“单脉冲模式”一节有介绍)
void TIM_OC1FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC2FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC3FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC4FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
// 清除REF信号(手册中在“外部事件时清除REF信号”一节有介绍)
void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC2Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC3Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC4Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
4.补充
//仅高级定时器使用
//在使用高级定时器输出PWM时。需要调用这个函数,使能输出。否则PWM将不能正常输出
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
5.补充
//TIM_OCMode 输出比较模式中的选择
TIM_OCMode_Timing//冻结模式
TIM_OCMode_Active//相等时置有效电平
TIM_OCMode_Inactive//相等时置无效电平
TIM_OCMode_Toggle//相等时电平翻转
TIM_OCMode_PWM1//PWM模式1,主要用
TIM_OCMode_PWM2//PWM模式2
TIM_ForcedAction_Active//强制输出模式,初始化时不使用
TIM_ForcedAction_InActive
TIM_Output_Compare_Polarity 输出比较的极性选择
TIM_OCPolarity_High //高极性,就是极性不翻转,REF波形直接输出,或者说是有效电平是高电平,REF有效时,输出高电平
TIM_OCPolarity_Low //低极性,就是REF电平取反,或者说是有效电平为低电平
PWM驱动LED呼吸灯
接线图如下:注意LED是正极接在PA0引脚,负极接在GND的驱动方法,这样就是高电平点亮,低电平熄灭,这是正极性的驱动方法,这样的话观察更直观一点,就是占空比越大LED越亮,占空比越小LED越暗 。
现象:在PA0端口接入LED,LED在不断地变换亮度,实现了一个呼吸灯的效果
第一步,RCC开启时钟,把我们要用的TIM外设和GPIO外设的时钟打开
第二步,配置时基单元
第三步,配置输出比较单元,里面包括这个CCR的值、输出比较模式、极性选择、输出使能这些参数
涉及函数:
void TIM_OCXInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct)其中TIM_OCXInit的X为1、2、3、4,对应4个输出比较单元,或者说输出比较通道。你需要初始化哪个通道,就调用哪个函数。不同的通道对应的GPIO口也是不一样的,所以这里要按照你GPIO口的需求来。这里使用的是PAO口,对应的就是第一个输出比较通道。对于TIM2来说,就是下图对应引脚
你要使用哪个外设,就只能用对应的引脚,不过,但是虽然它是定死的,STM32还是给了我们一次更改的机会的,这就是重定义,或者叫重映射。比如如果你既要用USART2的TX引脚,又要用TIM2的CH3通道,它俩冲突成,没办法同时用,那我们就可以在这个重映射的列表里找一下,比如这里我们找到了TIM2的CH3,那TIM2的CH3就可以从原来的引脚,换到这里的引脚,这样就避免了两个外设引脚的冲突。如果这个重映射的列表里找不到,那外设复用的GPIO就不能挪位置.这就是重映射的功能,配置重映射是用AFIO来完成的,重映射在最后会讲
作用:根据TIM_OCInitStruct中指定的参数初始化TIMx channel。
参数说明:
TIM_OCInitTypeDef structure结构体说明:
实际上通用计时器只用到了这些结构体成员,但结构体里面还有些成员是面向高级定时器,比如:
但是如果当你中途想把高级定时器当做通用定时器输出PWM时,那你自然就会把TIM_OCXInit的TIM2改成TIM1。这样的话,这个结构体原本没有用到的成员,现在需要使用,但是对于那些成员并没有赋值,那就会导致高级定时器输出PWM出现一些奇怪的问题最终找到的原因,就是因为这里结构体成员没有配置完整。所以为了避免程序中出现不确定的因素,把结构体所有的成员都配置完整;需要么就先给结构体成员都赋一个初始值,再修改部分的结构体成员,
所以void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct)有了用武之地。
作用:TIM_OCInitStruct 中的每一个参数按缺省值填入
参数说明:
第四步,配置GPIO.把PWM对应的GPIO口,初始化为复用推挽输出的配置。为什么选择这个模式呢?对于普通的开漏/推挽输出,引脚的控制权是来自于输出数据寄存器的
那通过刚才看到引脚定义表,我们就知道了,这里片上外设引脚连接的就是TIM2的CH1通道。所以,只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能通过引脚输出。
那最后,第五步,就是运行控制了.启动计数器,这样就能输出PWM了
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1)(通道1 )
作用:设置TIMx捕获比较寄存器值(CCR)
参数说明:
重映射:
根据你所要重映射的引脚,在下图找到所需要的模式,比如:如果我们想把PAO改到PA15,就可以选择这个部分重映射方式1,或者完全重映射。
在但是PA15在引脚定义图里没有加粗,因为它上电后已经默认复用为了调试端口JTDI,所以如果想让他作为普通的GPIO或者复用定时器的通道。那还需要先关闭调试端回的复用,也是用这个GPIO PinRemapConfig函数
如果你想让PA15、PB3、PB4这三个引脚当做GPIO来使用的话,那就加一下这里的第一句和第三句,先打开AFIO时钟,再用AFIO将JTAG复用解除掉,这样就行了;
如果你想重映射定时器或者其他外设的复用引脚,那就加一下这里的第一句和第二句,先打开AFIO时钟,再用AFIO重映射外设复用的引脚,这样就行了;
如果你重映射的引脚又正好是调试端口,那这三句就都得加上,打开AFIO时钟,重映射引脚,解除调试端口,这样才行。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM_LED.h"
uint8_t i;
int main(void)
{
OLED_Init(); //初始化OLED
pwm_init();
while(1)
{
//不断调用PWM_SetCompare1函数,更改CCR的值,实现LED呼吸灯的效果
for(i=0;i<=100;i++)
{
PWM_SetCompare1(i);//设置CCR寄存器的值
Delay_ms(10);
}
for(i=0;i<=100;i++)
{
PWM_SetCompare1(100-i);
Delay_ms(10);
}
}
}
PWM_LED.c
#include "stm32f10x.h" // Device header
/*
pwm初始化函数基本步骤(参考笔记PWM基本结构图)
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置单元,包括时钟源选择和时基单元都配置好
第三步,配置输出比较单元,包括CCR值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一来配置
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,Pwm和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器,这样就能输出PWM了
*/
void pwm_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//引脚重映射内容,将PA0引脚重映射到PA15,将下面GPIO改为PA15其它不动
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//引脚重映射;引脚重映射(TIM2的CH1本来是挂载在PA0引脚的,现在我想在其他引脚使用TIM2的CH1通道
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);//参考手册AFIO。将PA0引脚重映射到PA15,第一个参数可以是GPIO_PartialRemap1_TIM2或GPIO_FullRemap_TIM2
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//取消调试端口复用JTAG,PA15端口默认使用JTAG调试端口,需要关闭;SWJ就是SWD和JTAG两种调试方式;若想用PA15\PB3\PB4三个引脚做GPIO使用,先打开AFIO再将JTAG复用解除
//2.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
/*
公式:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
若PWM波形为频率为1KHz,占空比为50%,分辨率为1%
CK_PSC=72MHz
代入公式:
Freq =1000Hz=72MHz / 720 / 100
Duty = 50% = 50 / 100
Reso = 1% = 1 / 100
因此:PSC=719,ARR=99,ARR=50
*/
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_ClearFlag(TIM2,TIM_FLAG_Update);
//4.初始化输出比较单元(通道)
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//给结构体赋初始值;若不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改你想改的值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较的模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出比较的极性
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//设置输出使能(输出状态)
TIM_OCInitStructure.TIM_Pulse = 50;//设置CCR,Pulse直译是脉冲
TIM_OC1Init(TIM2, &TIM_OCInitStructure);//使用PA0口对应是第一个输出比较通道;在TIM2的OC1通道上就可以输出PWM波形了
//5.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure; //结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出;PWM波形通过引脚输出,使用定时器来控制引脚,输出数据寄存器将被断开,输出控制权将转移给片上外设(这里片上外设引脚连接的就是TIM2的CH1通道)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure); //使用的是地址传递
//6.启动定时器
TIM_Cmd(TIM2,ENABLE);//PWM波形就能通过PA0输出了
}
//让LED呈现呼吸灯的效果,那就是不断更改CCR的值就行了
//在运行过程更改CCR,使用函数TIM_SetCompare1封装用来单独更改通道1的CCR值
//TIM_SetCompare1封装
void PWM_SetCompare1(uint16_t Compare1)
{
TIM_SetCompare1(TIM2,Compare1);
}
PWM驱动舵机
!!这里一定要注意正负极!!接错可能会烧坏电脑!!
SG90舵机,它有三根线,第一个GND,就是棕色线,接在面包板的GND;第二个5V正极,就是红色线,这里要接5V的电机电源,大家不要把它接在面包板的正极了,这个STM32芯片正极只有3.3V的电压,而且输出功率不太,带不动电机的,所以我们需要把它接在STLINK的5V输出引脚;然后看第三个引脚,PWM信号,就是橙色线,接在PA1引脚上(这里用的是PA1的通道2)【看数据手册,里面的引脚定义表,PA0的复用功能是TIM2_CH1(通道一),PA1的复用功能是TIM2_CH2(通道2)】
那最后,再在PB1接一个按键,用来控制舵机,这样这个电路就完成了。
驱动舵机的关键就是输出一个下面一样的PWM波形,只要波形能够按照如下规定,准确的输出,那驱动舵机就非常简单了。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "serve.h"
#include "key.h"
uint8_t keynum; //按键键码
float angle;//角度变量
int main(void)
{
OLED_Init(); //初始化OLED
serve_init();
key_init();
OLED_ShowString(1, 1 ,"angle:");
//serve_setangle(120); //舵机设置角度
//PWM_SetCompare2(500); //对应舵机0度的位置
//建立一个舵机模块,封装函数。调用函数就能变为对应的角度,舵机设置角度,参数是0到180度
while(1)
{
keynum = key_getnum();
if(keynum == 1)
{
angle += 30;
if(angle > 180)
{
angle = 0;
}
}
serve_setangle(angle); //舵机设置角度
OLED_ShowNum(1,7,angle,3);//一行七列显示angle变量长度为3
}
}
pwm_led.c
#include "stm32f10x.h" // Device header
/*
pwm初始化函数基本步骤(参考笔记PWM基本结构图)
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置单元,包括时钟源选择和时基单元都配置好
第三步,配置输出比较单元,包括CCR值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一来配置
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,Pwm和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器,这样就能输出PWM了
*/
//驱动舵机用的是PA1口的通道2
void pwm_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//2.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元
/*
**********************************************************
公式:
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
************************************************************
若PWM波形为频率为1KHz,占空比为50%,分辨率为1%
舵机要求的周期是20ms,频率就是1/20ms=50hz;舵机要求高电平时间是0.5ms-2.5ms,也就是占空比
ARR设置为20k对应20ms(计数器加一次就是1us)
CCR设置500就是0.5ms,设置2500就是2.5ms
*/
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_ClearFlag(TIM2,TIM_FLAG_Update);
//4.初始化输出比较单元(通道)
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//给结构体赋初始值;若不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改你想改的值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较的模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出比较的极性
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//设置输出使能(输出状态)
TIM_OCInitStructure.TIM_Pulse = 50;//设置CCR,Pulse直译是脉冲
TIM_OC2Init(TIM2, &TIM_OCInitStructure);//OC2是通道2;通道和引脚是对应的;对于同一个定时器的不同通道输出的PWM的特点如后:因为不同通道共用一个计数器,所以它们的频率必须是一样的,它们的占空比由各自的CCR决定的;由于计数器的更新,所有PWM同时跳变,所以它们的相位是同步的
//5.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure; //结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出;PWM波形通过引脚输出,使用定时器来控制引脚,输出数据寄存器将被断开,输出控制权将转移给片上外设(这里片上外设引脚连接的就是TIM2的CH1通道)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure); //使用的是地址传递
//6.启动定时器
TIM_Cmd(TIM2,ENABLE);//PWM波形就能通过PA0输出了
}
//TIM_SetCompare2封装,使用通道2
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2,Compare);
}
serve.c
#include "stm32f10x.h" // Device header
#include "PWM_LED.h" //继承pwm的功能
//舵机初始化函数
void serve_init(void)
{
pwm_init();//将pwm底层初始化
}
/*
0度 对应 CCR 500
180 2500
对angle进行缩放。0-180是180范围,500-2500是2000范围,所以angle / 180*2000 + 500偏移,就得到目标比例了完成0-180到500-2500的映射了
*/
void serve_setangle(float angle) //舵机设置角度
{
PWM_SetCompare2(angle / 180 * 2000 + 500);//线性映射
}
PWM驱动直流电机
加大PWM频率,当PWM频率足够大时,超出人耳的范围,人耳就听不到了,人耳听到的范围是20Hz到20KHz。可以减小PSC来加大频率且不会影响占空比
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "moter.h"
#include "key.h"
uint8_t keynum;//按键键码
int8_t speed;//有符号的速度变量
int main(void)
{
OLED_Init(); //初始化OLED
moter_init();
key_init();
OLED_ShowString(1,1,"speed:");
while(1)
{
keynum = key_getnum();
if(keynum == 1)
{
speed += 20;
if(speed > 100)
{
speed = -100;//speed从-100到100变化
}
}
moter_setspeed(speed);//实现按键控制速度
OLED_ShowSignedNum(1,7,speed,3);
}
}
PWM_LED.c
#include "stm32f10x.h" // Device header
/*
pwm初始化函数基本步骤(参考笔记PWM基本结构图)
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置单元,包括时钟源选择和时基单元都配置好
第三步,配置输出比较单元,包括CCR值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一来配置
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,Pwm和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器,这样就能输出PWM了
*/
//1.电机接在TIM2的通道3上。修改:GPIO_Pin_2。TIM_OC3Init。PWM_SetCompare3
//2.对于直流电机也建立一个hardware模块
void pwm_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//2.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
/*
公式:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
若PWM波形为频率为1KHz,占空比为50%,分辨率为1%
CK_PSC=72MHz
代入公式:
Freq =1000Hz=72MHz / 720 / 100
Duty = 50% = 50 / 100
Reso = 1% = 1 / 100
因此:PSC=719,ARR=99,ARR=50
*/
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR 周期
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //PSC 预分频器,现在为20KHz
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//4.初始化输出比较单元(通道)
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//给结构体赋初始值;若不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改你想改的值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较的模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出比较的极性
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//设置输出使能(输出状态)
TIM_OCInitStructure.TIM_Pulse = 50;//设置CCR,Pulse直译是脉冲
TIM_OC3Init(TIM2, &TIM_OCInitStructure);//TIM2通道3
//5.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure; //结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出;PWM波形通过引脚输出,使用定时器来控制引脚,输出数据寄存器将被断开,输出控制权将转移给片上外设(这里片上外设引脚连接的就是TIM2的CH1通道)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure); //使用的是地址传递
//6.启动定时器
TIM_Cmd(TIM2,ENABLE);//PWM波形就能通过PA0输出了
}
//TIM_SetCompare1封装
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM2,Compare);
}
moter.c
#include "stm32f10x.h" // Device header
#include "PWM_LED.h" //继承PWM模块
void moter_init(void) //初始化函数
{
pwm_init();//调用底层的PWM_init,初始化pwm
//需要额外初始化方向控制的两个脚,即初始化GPIO引脚
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//配置端口模式
GPIO_InitTypeDef GPIO_InitStructA; //结构体变量名GPIO_InitStructA
GPIO_InitStructA.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructA.GPIO_Pin = GPIO_Pin_4 |GPIO_Pin_5; //或运算,选择两个引脚
GPIO_InitStructA.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructA); //使用的是地址传递
}
//设置速度的函数
void moter_setspeed(int8_t speed)
{
//针对正转和翻转,用if来分别处理
if(speed >= 0)//正转的逻辑
{
//首先将方向控制脚设置为一个高电平,一个低电平.哪个为高哪个为底无所谓
GPIO_SetBits(GPIOA,GPIO_Pin_4);
GPIO_ResetBits(GPIOA,GPIO_Pin_5);
//速度
PWM_SetCompare3(speed);
}
else//speed就是负数,代表反转
{
//首先是正反转,将set和reset反过来就能反转了
GPIO_ResetBits(GPIOA,GPIO_Pin_4);
GPIO_SetBits(GPIOA,GPIO_Pin_5);
PWM_SetCompare3(-speed);//此时speed为负数,必须为正数,在speed前加负号
}
}
TIM输入捕获
输入捕获简介
输入捕获对于PID控制算法很重要,没有输入捕获就不能完成闭环控制,要做平衡车的一定要认真学
输入捕获,即Input Capture,英文缩写为IC。输入捕获模式下,当通道输入引脚出现指定电平跳变瞬间(可以定义为上升沿、下降沿),当前CNT的值将被锁存到CCR中(检测电平跳变,然后执行动作(作用和外部中断差不多,只不过外部中断执行的动作是向CPU申请中断,输入捕获执行的是控制后续电路)),可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。在这里,脉冲间隔和频率差不多、电平持续时间和占空比也是互相对应的关系。
每个高级定时器和通用定时器都拥有4各输入捕获通道,且二者没有区别。基本定时器没有输入捕获的功能。
输入捕获模块可以配置为PWMI(PWM输入)模式和主从触发模式。PWMI模式是PWM的输入模式,专门用来同时测量PWM波形的频率和占空比的。主从触发模式可以实现对频率或占空比的硬件的全自动测量。把这两个功能结合起来,测量频率和占空比就是硬件全自动执行,软件不需进行任何干预,也不需进中断,需要测量的时候,直接读取CCR寄存器就行了,使用非常方便且极大地减轻了软件的压力。
如下图,左边为输入捕获电路,4个输入捕获和输出比较通道,共用4个CCR寄存器,另外它们的CH1到4的4个通道引脚也是共用的,所以对于同一个定时器,输入捕获和输出比较只能使用其中一个,不能同时使用。
输入捕获对比输出比较:
输出比较,引脚是输出端口,根据CNT和CCR的大小关系来执行输出动作 输入捕获,引脚是输入端口,接收到输入信号执行CNT锁存到CCR的动作
输入捕获的各部分电路
从左向右依次进行电路分析:

然后输出通过数据选择器,到达输入捕获通道1,数据选择器如果选择上面一个,那输入捕获通道1的输入就是三个引脚的异或值;若选择下面一个,异或门就没有用。设计异或门,其实还是为了三相无刷电机服务的,无刷电机有三个霍尔传感器检测转子的位置,可以根据转子的位置来进行换相。



输入捕获通道1的详细框图如下所示:(如下是上面框图的一个细化结构,基本功能都是一样的)
如上图,电路细节内容如下:
引脚进来,先经过一个滤波器,滤波器的输入是TI1就是CH1的引脚,输出的TI1F就是滤波后的信号
FDTS是滤波器的采样时钟来源
CCMR1寄存器里的ICF位可以控制滤波器的参数
滤波器的工作原理就是:以采样频率对输入信号进行采样,当连续N个值都为高电平,输出才为高电平,当连续N个值都为低电平,输出才为低电平,如果信号出现高频抖动,导致连续采样N个值不全都一样,那输出就不会变化,这样就可以达到滤波的效果。采样频率越低,采样个数N越大,滤波效果就越好。在实际应用中,如果波形噪声比较大,就可以把IC1F位参数设置大一点来过滤噪声。
滤波之后的信号通过边沿检测器。捕获上升沿或者下降沿
CCER寄存器里的CC1P位可以进行极性选择
最终得到TI1FP1触发信号
通过数据选择器,进入通道1后续的捕获电路。
当然还有一套一样的电路得到TI1FP2触发信号,连通到通道2的后续电路,上图并没有画出来,同样,通道2有TI2FP1连通到通道1的后续,通道2也有TI2FP2连通到通道2的后续,总共有四种连接方式,然后经过数据选择器,进入后续捕获部分电路
CCMR寄存器的CC1S位可以对数据选择器进行选择
之后,CCMR寄存器的ICPS位可以配置分频器,可以选择不分频、2分频、4分频、8分频
CCMR寄存器的CC1E位,控制输出使能或失能。如果使能了输出,输入端产生指定边沿信号,经过层层电路,就可以最后将CNT的值转运到CCR里来,每捕获一次CNT的值,都要把CNT清0一下,以便于下一次的捕获,从模式控制器就可以在捕获之后自动完成CNT的清零工作
TI1FP1信号和TI1F_ED边沿信号,都可以通向从模式控制器,比如TI1FP1信号的上升沿触发捕获,还可以同时触发从模式,这个从模式里就有电路,可以自动完成CNT的清零。从模式就是完成自动化操作的利器
输入捕获的主模式、从模式、触发源选择(简称:主从触发模式)
CCR对CNT进行捕获之后,需要对CNT进行一次清0操作,这样每次捕获得到的值才是测周法,两个上升沿(下降沿)之间的时间间隔。这个清0操作,就需要用到主从触发模式来自动完成。由输入捕获通道1的详细框图可得:经过滤波和极性选择的TI1FP1信号和经过滤波的边沿信号TI1F_ED都可以通向从模式控制器,之后便可以通过硬件电路自动完成CNT的清0操作。
主从触发模式,即主模式、从模式和触发源选择三个功能的简称。主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其他外设的操作;从模式可以接收其他外设或自身外设的一些信号,用于触发自己的一些操作(定时器的运行);触发源选择,即选择从模式的触发信号源功能,也可以认为它是从模式的一部分。
在从模式下,可以通过触发源选择功能选择一个信号产生TRGI信号,之后去触发从模式,从模式可以在上面列表中选择一项操作来自动执行。关于主从模式的详细说明可以参见手册:
输入捕获和PWMI基本结构
1.4.1输入捕获基本结构
下图是输出捕获模式测频率的基本结构图。
上图清晰地展示了输入捕获模式测量频率的过程,同时也是编程的逻辑基础。在这里我们只使用了一个通道,所以它只能测量频率。
首先,配置时基单元,启动寄存器,则CNT就会在预分频之后的时钟驱动下不断自增。测周法用CNT来计数,间接实现计时的功能。经过预分频后的时钟频率,就是测周法的标准频率fc。之后,GPIO输入一个待测的方波信号,经过经过滤波器和边沿检测选择TI1FP1为上升沿触发,之后数据选择器选择直连通道,分频器选择不分频。当TI1FP1出现上升沿之后,CNT的值就会被CCR1转运捕获;同时触发源选择模块选择TI1FP1为触发信号,从模式选择复位操作,触发CNT清零(先后顺序是:先转运CNT的值到CCR,再触发从模式给CNT清零。或者是非阻塞的同时转移:CNT的值转移到CCR,同时0转移到CNT里面去,总之是先捕获,再清零)。当电路不断工作时,CCR1中的值始终是最新一个周期的计数值,即测周法的计次数 N。所以,当我们想读取信号的频率时,只需要读取CCR1得到N,再计算fc/N就得到频率了。当不需要读取时,整个电路全自动的测量,不需要占用任何软件资源。
这里需要注意以下两点:
CNT的计数值是有上限的。由于ARR最大为65535,故CNT最大也只能计65535个数。如果信号频率太低,CNT的计数值可能会溢出。 从模式的触发源选择中有TI1FP1和TI2FP2,但是没有TI3和TI4的信号。所以如果要使用从模式自动清零CNT,就必须使用通道1或通道2作为输入。对于通道3和通道4,就只能开启捕获中断,在中断中手动清0了(程序会处于频繁中断的状态,比较占用软件资源)。
PWMI基本结构
PWMI模式使用两个通道同时捕获一个引脚,可以同时测量周期和占空比,相比前面输入捕获,下面多了一个TI1FP2的通道。
首先TI1FP1配置上升沿触发,触发捕获和清零CNT,正常的捕获周期,再来一个TI1FP2,配置为下降沿触发,通过交叉通道去触发通道2的捕获单元(最开始上升沿CCR1捕获同时清零CNT,之后CNT一直加,然后在下降沿时刻触发CCR2捕获,这时CCR2的值就是CNT从上升沿到下降沿的计数值也就是高电平期间的计数值,CCR2捕获并不触发CNT清零,所以CNT继续加,直到下一次上升沿,CCR1捕获周期并CNT清零,这样执行之后CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,用CCR2/CCR1就是占空比,以上就是PWMI模式使用两个通道来捕获频率和占空比的思路。另外也可以两个通道同时捕获第一个引脚的输入)
频率的测量方法
如上图是频率逐渐降低的方波波形,越往左频率越高,越往右频率越低,这里信号都是只有高低电平的数字信号,对于STM32测频率而言,它也是只能测量数字信号的。如果需要测量一个正弦波则需要搭建一个信号预处理电路,最简单的就是用运放搭建一个比较器,把正弦波转换为数字信号再输入给STM32就行了;如果你测量的信号电压非常高,那还要考虑隔离的问题,比如使用隔离放大器、电压互感器等元件,隔离高压端和低压端,保证电路的安全。总之,经过处理最终输入给STM32的信号是如上图的高低电平信号,高电平3.3v,低电平0v。
为了测量频率,有两种方法可以选择:测频法、测周法
测频法:定时器中断,并记录捕获次数;测周法:捕获中断,并记录定时器次数。
测频法
测频法的测试方法(直接按频率定义来进行测量的方法)是:在闸门时间T内,对上升沿(也可以是下降沿)计次,得到N,则待测信号频率𝑓𝑥 为:
𝑓𝑥 = 𝑁 / 𝑇
例如,可以定义闸门时间闸门时间T=1s (砸门时间不是必须为1s),则在一秒中得到的上升沿的个数(每来一个上升沿就是完整的一个周期的信号个数)就是频率
频率的定义就是,1s内出现了多少个重复的周期,那频率就是多少Hz
测周法
测周法的测试方法是:两个上升沿内,以标准频率fc计次,fc=72M/(psc+1),得到N(就是读取CCR的值),则测量频率𝑓𝑥为:
𝑓𝑥 = 𝑓𝑐 / 𝑁
测周法的基本思想是:周期的倒数就是频率。如果我们能用定时器测量出一个周期的时间(相邻上升沿或相邻下降沿的间隔时间)取倒数即得到测量频率。
捕获信号的两个上升沿,然后测量一下两个上升沿之间持续的时间,但是实际上,并没有一个精度无穷大的秒表来测量时间,测量时间的方法,实际上也是定时器计次,我们使用一个已知的标准频率fc的计次时钟来驱动计数器,从一个上升沿开始计数器从0开始一直计到下一个上升沿停止,计一个数的时间是1/fc,计N个数时间就是N/fc也就是周期,再取倒数就得到了频率fx
输入捕获模块采用测周法进行测量。
测频法和测周法的误差分析
测频法适用于测量高频信号,测周法适用于测量低频信号。
根据上图可以清晰地看出,测频法在闸门时间内,最好要多出现一些上升沿,计次数量多一些有助于减小误差,测频法要求信号频率要稍微高一些,测频法的测量结果更新慢一些,测量结果是一段时间的平均值,值比较平滑,数值相对稳定;对于测周法,就要求信号频率低一些,低频信号,周期比较长,计次就会比较多,有助于减小误差,测周法的测量结果更新的快,只测量一个周期,就能出一次结果,数据跳变也非常快,所以出结果的速度取决于待测信号的频率,一般而言,待测信号都是几百hz几千hz,所以一般情况下,测周法结果更新更快,但是由于它只测量一个周期,所以结果值会受噪声的影响,波动比较大,这就是这两种方法的基本特征对比。
测频法计次和测周法计次,这个计次数量N尽量要大一些,N越大,相对误差越小,因为在这些方法中,计次可能存在正负1误差,要想减小正负1误差,就尽量多记一些数,当计次N比较大时,正负1对N的影响就会很小
由于测量原理的差异,一般而言,测频法的结果更新频率会比较慢,但是数值较为稳定;测周法的结果更新频率较快,数据跳变也比较灵敏。从原理上看,测频法自带一个均值滤波的功能,如果在闸门时间 T内被测频率有变化,测频法得到的实际是这一段闸门时间内的平均频率;而测周法只测量一个周期,故其结果会受噪声的影响,波动会比较大。所以,对于测频法和测周法的一个共同点是:N越大,误差就越小。在两种方法中,计次都可能会产生正负1误差。在测频法的一个闸门时间内,并不是每一个被测信号的周期都是完整的;测周法的标准计数信号的信号也不一定是被测信号的整数倍,所以它也不一定是每一个都完整的。对于上述的两种情况,都会出现多计一个数或者少计一个数的情况,所以会产生正负1误差。
如何在不同情况下正确选择测频法和测周法呢?所以多高算高频,多低算低频,我们有以下一个参数来考量:中界频率,测频法和测周法误差相等的频率点。由于两种方法的误差都与N的正负1误差有关,所以当两种方法计次的N相同时,两种方法的误差也就相同。消去两种方法公式中的N,将测频法和测周法的N提出来,令两个方法N相等,将fx解出来,可得:
式中,T 是测频法的闸门时间,f c是测频法的标准频率。
当待测信号频率小于中界频率时,测周法误差更小,选择测周法更合适;当待测信号频率大于中界频率时,测频法误差更小,选择测频法更合适。
2.4用STM32来实现测频法和测周法
测频法,之前学过的外设可以实现,对射式红外传感器计次、定时器外部时钟,这些代码稍加改进就是测频法,比如 对射式红外传感器计次,每来一个上升沿计次+1,再用一个定时器,定一个1s的定时中断,在中断里,每隔1s取一下计次值,同时清0,为下一次做准备,这样每次读取的计次值就直接是频率;对应定时器外部时钟的代码,也是如此,每隔1s取一下计次,就能实现测频法测量频率的功能了。
本节输入捕获测频率,使用的方法是测周法。就是测量两个上升沿之间的时间,来进行频率测量。
手册
手册本节相应的内容,描述是寄存器的方式,结合上面笔记内容进行进一步理解
手册中, 脉冲宽度测量就是占空比, 周期测量就是频率
主要理解,从模式配合输入捕获完成硬件自动化
PWM输入捕获示例程序(输入捕获模式测频率&PWMI模式测频率和占空比)
输入捕获电路的工作流程
由四个问题来深入输入捕获的工作流程
输入捕获和输出比较的区别?
为什么要进行一个交叉连接呢?
滤波器具体是怎么工作的呢?
如何自动清零CNT呢?
输入捕获和输出比较的区别?
对比一下输出比较,就是:
输出比较,引脚是输出端口;输入捕获,引脚是输入端口;
输出比较,是根据CNT和CCR的大小关系来执行输出动作;输入捕获,是接收到输入信号,执行CNT锁存到CCR的动作。
交叉连接的目的:
为什么要进行一个交叉连接呢?
这样做的目的,个人认为主要有两个,第一个目的,可以灵活切换后续捕获电路的输入;第二个目的,也是它交叉的主要目的,就是可以把一个引脚的输入,同时映射到两个捕获单元,这也是PWMI模式的经典结构。第一个捕获通道,使用上升沿触发,用来捕获周期,第二个通道,使用下降沿触发,用来捕获占空比。两个通道同时对一个引脚进行捕获,就可以同时测量频率和占空比,这就是PWMI模式,等会儿再来继续分析。一个通道灵活切换两个引脚,和两个通道同时捕获一个引脚,这就是这里交叉一下的作用和目的。同样,下面通道3和通道4,也是一样的结构,可以选择各自独立连接,也可以选择进行交叉。另外,这里还有一个TRC信号,也可以选择作为捕获部分的输入,这样设计,也是为了无刷电机的驱动。
到这里,电路的整个工作流程讲完了。比如我们可以配置上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为这个CNT计数器是由内部的标准时钟驱动的,所以CNT的数值,其实就可以用来记录两个上升沿之间的时间间隔,这个时间间隔,就是周期,再取个倒数,就是测周法测量的频率了。另外这里还有个细节问题,就是每次捕获之后,我们都要把CNT清0一下,这样下次上升沿再捕获的时候,取出的CNT才是两个上升沿的时间间隔,这个在一次捕获后自动将CNT清零的步骤,我们可以用主从触发模式,自动来完成。
接下来就是执行细节的问题,把电路执行的细节都了解清楚,这样写程序的时候才能得心应手。好,那接着看一下这里,这是输入捕获通道1的一个更详细的框图,基本功能都是一样的。
滤波器具体是怎么工作的呢?
可以看一下手册,在CCMR1寄存器这里有IC1F位,根据它的描述简单理解,这个滤波器工作原理就是:以采样频率对输入信号进行采样,当连续N个值都为高电平,输出才为高电平,连续N个值都为低电平,输出才为低电平。如果你信号出现高频抖动,导致连续采样N个值不全都一样,那输出就不会变化,这样就可以达到滤波的效果。采样频率越低,采样个数N越大说滤波效果就越好,那下面这些描述,就是每个参数对应的采样频率和采样个数。在实际应用中,如果波形噪声比较大入100,就可以把这个参数设置大一些,这样就可以过滤噪声了。
如何自动清零CNT呢?
看一下这里,这个TI1FP1信号和TI1的边沿信号,都可以通向从模式控制器,比如TI1FP1信号的上升沿触发捕获,那通过这里,TI1FP1还可以同时触发从模式,这个从模式里面,就有电路,可以自动完成CNT的清零。所以可以看出,这个从模式就是完成自动化操作的利器。
那接下来我们就来研究一下这个主从触发模式。主从触发模式有什么用,如何来完成硬件自动化的操作。
主从触发模式,就是主模式、从模式。
如果想完成我们刚才说的任务,想让TI1FP1信号自动触发CNT清零,那触发源选择,就可以选中这里的TI1FP1,从模式执行的操作,就可以选择执行Reset的操作。这样TI1FP1的信号就可以自动触发从模式,从模式自动清零CNT,实现硬件全自动测量,这就是主从触发模式的用途。
那有关这些信号的具体解释,可以看手册
那回到PPT,总结下来就是这三个图,主模式,触发源选择,从模式,在库函数里也非常简单。这三块东西,就对应三个函数,调用函数,给个参数,就行了,这些就是主从触发模式的内容。接下来,我们就来最后理一下思路,把之前的东西组合在一起,得到这两个图。这两个图也分别对应了我们演示两个代码的逻辑,先看第一个,输入捕获基本结构:
然后还有几个注意事项说明一下,首先是这里CNT的值是有上限的,ARR—般设置为最大65535,那CNT最大也只能计65535个数。如果信号频率太低,CNT计数值可能会溢出(因为CNT计数的快慢是根据时基单元的时钟频率而变化的,如果时钟频率很高,CNT增长非常快,如果被测信号频率太低,完全有可能CNT计满65536都不到被测信号的一个周期)。另外还有就是,这个从模式的触发源选择,在这里看到,只有TI1FP1和TI2FP2,没有TI3和TI4的信号,所以这里如果想使用从模式自动清零CNT,就只能用通道1和通道2。对于通道3和通道4,就只能开启捕获中断,在中断里手动清零了,不过这样,程序就会处于频繁中断的状态,比较消耗软件资源,这个注意一下。
好,接下来我们继续来看最后一个PPT,这里展示的是PWMI基本结构。
这个PWMI模式,使用了两个通道同时捕获一个引脚,可以同时测量周期和占空比。
我们来看一下,上面这部分结构,和刚才演示的一样,下面这里多了一个通道。
首先,TI1FP1配置上升沿触发,触发捕获和清零CNT,正常地捕获周期,这时我们再来一个TI1FP2,配置为下降沿触发,通过交叉通道,去触发通道2的捕获单元,这时会发生什么呢?
我们看一下左上角的这个图,最开始上升沿,CCR1捕获,同时清零CNT,之后CNT一直++,然后,在下降沿这个时刻,触发CCR2捕获,所以这时CCR2的值,就是CNT从这里到这里的计数值,就是高电平期间的计数值,CCR2捕获,并不触发CNT清零,所以CNT继续++。
直到下一次上升沿,CCR1捕获周期,CNT清零,这样执行之后CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,我们用CCR2/CCR1,是不是就是占空比了。这就是PWMI模式,使用两个通道来捕获频率和占空比的思路。
另外这里,你可以两个通道同时捕获第一个引脚的输入,这样通道2的前面这一部分就没有用到。
当然也可以配置两个通道同时捕获第二个引脚的输入,这样我们就是使用TI2FP1和TI2FP2这两个引脚了,这两个输入可以灵活切换。
好,到这里,我们本小节的内容差不多就结束了,最后大致看一下手册37:28
所以步骤就是:
第一步,RCC开启时钟,把GPIO和TIM的时钟打开
第二步,GPIO初始化,把GPIO配置成输入模式(一般选择上拉输入或浮空输入模式)
第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行,和之前代码一样
第四步,配置输入捕获单元,包括滤波器、极性、直连通道、交叉通道、分频器这些参数,用一个结构体就可以统一进行配置了
第五步,选择从模式的触发源,触发源选择为TI1FP1,这里调用一个库函数给一个参数就行了
第六步,选择触发之后执行的操作,执行Reset操作,这里调用一个库函数就行了
最后,当这些电路都配置好之后,调用TIM_Cmd函数,开启定时器。这样所有的电路就能配合起来了,按照我们的要求工作了。当我们需要读取最新一个周期的频率时,直接读取CCR寄存器,然后按照fc/N,计算一下就行了,这就是整个程序的思路
输入捕获模式测频率
现象:在这里,为了测量外部信号的频率,我们先得有个信号源,产生一个频率和占空比可调的波形,但是考虑到大家可能没有信号发生器,所以我这里就借用了一下上一小节的代码。先用PWM模块,在PAO端口输出一个频率和占空比可调的波形,然后我们本节的代码,测量波形的输入口是PA6,所以我们直接用一根线,把PAO和PA6连在一起,这样就能测量自己PWM模块产生波形的频率了。
目前这个程序只能测频率,还不能测量占空比,如果想同时测量频率和占空比,STM32的输入捕获还设计了一个PWM模式,即PWM输入模式。
在6-3 PWM驱动LED呼吸灯的工程基础上写
前置操作:
PWM模块这里,我们还要再进行一些改进。目前这个代码的逻辑是初始化TIM2的通道1,产生一个PWM波形,输出引脚是PA0。然后通过SetCompare1函数,可以调节CCR1寄存器的值,从而控制PWM的占空比。但是目前PWM的频率,是在初始化里写好了的,是固定的,运行的时候调节不太方便,所以我们在最后再加一个函数,用来便捷地调节PWM频率。
如何调节PWM频率呢?
通过公式,我们知道PWM频率=更新频率=72M/(PSC+1/(ARR+1),所以PSC和ARR都可以调节频率,但是占空比=CCR/(ARR+1),所以通过ARR调节频率,还同时会影响到占空比,而通过PSC调节频率,不会影响占空比,显然比较方便。所以我们的计划是,固定ARR为100-1,通过调节PSC来改变PWM频率,另外ARR为100-1,CCR的数值直接就是占空比,用起来比较直观。
当然实际使用也是有技巧的,一般我们可以根据分辨率的要求,先确定好ARR,比如分辨率,1%就足够了;那ARR给100-1,这样PSC决定频率,CCR决定占空比。如果我想要更高的分辨率,比如0.1%,那ARR就先固定1000-1,这样频率就是72M/预分频/1000,占空比就是CCR/1000,这样也好算。
在这里,目前ARR我们固定给100-1,初始化操作的PSC就先不管,我们后面再写一个函数,在初始化之后单独修改PSC。
例如:定义一个void PWM_SetPrescaler(uint16_t Prescaler)函数,在自定义函数里面,我们就要调用库函数里单独写入PSC的函数了,TIM_PrescalerConfig,就是单独写入PSC的函数。因为这个函数还有一个重装模式的参数,所以它并不叫SetPrescaler,而叫PrescalerConfig。这是这个库的命名规范。
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode)
可能是因为手册版本太低了,并没有提到中间参数,那我们就看库里面的注释
参数Prescaler:要写入PSC的值。
接下来就可以写输入捕获的代码
第一步,RCC开启时钟,把GPIO和TIM的时钟打开
注意:我们这个代码还需要TIM2输出PWM,所以输入捕获的定时器要换一个,我们就换到TIM3(这里在组建IC捕获模块,TIM2是PWM已经定义好的,捕获模块要重新定义一个)。其次我们这里用到的是TIM3通道1,查引脚定义表,你就知道为什么连PA6。
第二步,GPIO初始化,把GPIO配置成输入模式,一般选择上拉输入或者浮空输入模式
第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行,这一步和之前的代码是一样的
ARR自动重装值,根据之前的分析,arr越大,输入捕获越能更精准地测更小的频率,其次防止计数溢出。
72M/预分频,就是计数器自增的频率,就是计数标准频率。这个需要根据你信号频率的分布范围来调整,我暂时先给72-1,这样标准频率就是72M/72=1MHz。
第四步,配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用一个结构体就可以统一进行配置了
第五步,选择从模式的触发源。触发源选择为TI1FP1,这里调用一个库函数,给一个参数就行了
第六步,选择触发之后执行的操作。执行Reset操作,这里也是调用一个库函数就行了
最后,当这些电路都配置好之后,调用TIM_Cmd函数,开启定时器,这样所有的电路就能配合起来,按照我们的要求工作了。直接读取CCR寄存器,然后按照fc/N,(N是读取CCR的值)计算一下就行了。这就是整个程序的思路
fc=72M/(PSC+1)
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM_LED.h"
#include "IC.h"
int main(void)
{
OLED_Init(); //初始化OLED
pwm_init();
IC_init();//初始化整个电路
OLED_ShowString(1,1,"Freq:00000Hz");
//PA0口输出1khz频率,50%占空比的待测信号;PWM模块将待测信号输出给PA0,PA0然后通过导线输入到PA6(PA6是TIM3的通道1,通道1通过输入捕获模块测量得到频率,然后在主循环里不断刷新显示频率)
PWM_setPSC(720-1); //频率=72M/(psc+1)/(arr+1) //频率=72M/720/100 =1khz
PWM_SetCompare1(50);//占空比=ccr/(ARR+1) //占空比=50/100 = 50%
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);//不断刷新显示频率
}
}
IC.c
#include "stm32f10x.h" // Device header
void IC_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM3是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//打开时钟,PA6的通道1
//2.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure;//结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure);//使用的是地址传递
//3.1.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM3);
//3.2.配置时基单元参数
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,向上计数
/*公式:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1) */
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR周期,最好要设置大一些防止计数溢出,16位的计数器可以满量程计数是65535
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器,标准频率就是72M/72=1MHz;这个值决定了测周法的标准频率fc,72M/预分频就是计数器自增的频率就是计数标准频率;需要根据信号频率的分步范围来调整
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
//4.初始化输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道,使用TIM3的通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF;//配置输入捕获的滤波器,数越大,滤波效果越好,每个数值对应的采样频率和采样次数在参考手册里有,若信号有毛刺和噪声就可以增大滤波器参数可以有效避免干扰
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//对应边沿检测、极性选择部分,可以选择上升沿触发/下降沿触发/上升沿和下降沿都触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//分频器,触发信号分频器,不分频就是每次触发都有效,2分频就是每隔一次有效一次,以此类推
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入,对应配置数据选择器的。可以选择直连通道/交叉通道/TRC引脚
TIM_ICInit(TIM3,&TIM_ICInitStructure);
//5.配置触发源选择,配置TRGI的触发源为TI1FP1
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);//触发源选择TI1FP1
//6.配置从模式,为Reset
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);//从模式选择Reset
//7.启动定时器,调用TIM_Cmd
TIM_Cmd(TIM3,ENABLE);//CNT就会在内部时钟的驱动下不断自增,即使没有信号过来,它也会不断自增;有信号来的时候,CNT就会在从模式的作用下自动清零并不会影响测量
/*
初始化之后,整个电路就能全自动测量了,当我们想查看频率时,需要读取CCR进行计算,所以需要在下面写一个函数
*/
}
uint32_t IC_GetFreq(void)
{
//使用测周法的公式,fc=72M/(psc+1),目前psc=72-1,所以fc=1MHz
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //返回的是最新一个周期的频率值(单位是HZ) = 1MHz(1000000) / N(就是读取CCR的值)
}
PWM.c
#include "stm32f10x.h" // Device header
/*
pwm初始化函数基本步骤(参考笔记PWM基本结构图)
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置单元,包括时钟源选择和时基单元都配置好
第三步,配置输出比较单元,包括CCR值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一来配置
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,Pwm和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器,这样就能输出PWM了
*/
//PWM控制呼吸灯的代码逻辑是初始化TIM2的通道1,产生一个PWM波形,输出引脚是PA0,然后通过PWM_SetCompare1可以调节CCR1寄存器的值从而控制PWM的占空比,PWM的频率是固定写好在初始化程序里了,运行时候调节不太方便
//在最后再加一个函数,用来便捷地调节PWM频率,PSC和ARR都可以调节频率,但是调节ARR会影响占空比,通过PSC调节频率不会影响占空比,所以计划是固定ARR值,通过调节PSC来改变PWM频率
//一般可以根据分辨率的要求先确定ARR,PSC决定频率,CCR决定占空比
void pwm_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//2.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
/*
公式:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
*/
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_ClearFlag(TIM2,TIM_FLAG_Update);
//4.初始化输出比较单元(通道)
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//给结构体赋初始值;若不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改你想改的值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较的模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出比较的极性
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//设置输出使能(输出状态)
TIM_OCInitStructure.TIM_Pulse = 50;//设置CCR,Pulse直译是脉冲
TIM_OC1Init(TIM2, &TIM_OCInitStructure);//使用PA0口对应是第一个输出比较通道;在TIM2的OC1通道上就可以输出PWM波形了
//5.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure; //结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出;PWM波形通过引脚输出,使用定时器来控制引脚,输出数据寄存器将被断开,输出控制权将转移给片上外设(这里片上外设引脚连接的就是TIM2的CH1通道)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure); //使用的是地址传递
//6.启动定时器
TIM_Cmd(TIM2,ENABLE);//PWM波形就能通过PA0输出了
}
//在运行过程更改CCR,使用函数TIM_SetCompare1封装用来单独更改通道1的CCR值,进而改变占空比
void PWM_SetCompare1(uint16_t Compare1)//TIM_SetCompare1封装
{
TIM_SetCompare1(TIM2,Compare1);
}
//封装此函数,在初始化之后单独修改PSC,进而改变频率
void PWM_setPSC(uint16_t prescaler)
{
//调用库函数里单独写入PSC的函数,在tim.h中找,这个函数还有一个重装模式的参数所以叫TIM_PrescalerConfig
TIM_PrescalerConfig(TIM2,prescaler,TIM_PSCReloadMode_Immediate);//写入PSC,第二个参数是写入PSC的值,直接将外层函数的prescaler参数传进去,第三个参数是重装模式(还是影子寄存器、预装载这个问题,就是写入的值是立刻生效还是在更新事件生效;立刻生效可能会在值改变时产生切断波形的现象会出现不完整的周期,更新事件生效就是会有一个缓存器,延迟参数的写入时间,等一个周期结束了,在更新事件时,再统一改变参数,保证每个周期的完整)
}
6-7 PWMI模式测频率占空比
在6-6 输入捕获模式测频率做修改
需要将输入捕获初始化的部分,需要进行一下升级,配置成两个通道同时捕获同一个引脚的模式,怎么配置呢?
两种方法:
第一种,把这个通道初始化的部分,复制一份,这个结构体定义的不要复制了。然后呢,通道1是直连输入,上升沿触发,沿用这个配置。接着下面,通道1改成通道2,直连输入,改成这个交叉输入,上升沿触发,改成下降沿触发,这样看一下,是不是就对应我们PPT的这个结构了。通道1,直连输入,上升沿触发;通道2,交叉输入,下降沿触发,这样就可以了。
第二种:库里有专门的封装函数。只针对于通道1和通道2
写一个获取占空比的函数,根据上一小节的分析,高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里,我们用CCR2/CCR1,就能得到占空比了
CCR总少1,应该是CCR从0开始计数的
测频率的性能讲解视频片段
最后,我们来研究一下这个测频率的性能。
首先是测频率的范围,目前我们给的标准频率是1MHz,计数器最大只能计到65535。所以所测量的最低频率是1M/65535,这个值算一下大概是15Hz。如果信号频率再低,计数器就要溢出了,所以最低频率就是15Hz左右。那如果想要再降低一些最低频率的限制,我们可以把这个预分频再加大点,这样标准频率就更低,所支持测量的最低频率也就更低。这是测量频率的下限。
测得的频率等于fc/N,这里的N值就是CNT里面过去的,当N越大,频率越小,但是CNT最大不能超过ARR的值(最大为65535)所以测量的最小频率大概是15Hz
然后是测量的上限,就是支持的最大频率。这个最大频率,并没有一个明显的界限,因为随着待测频率的增大,误差也会逐渐增大,如果非要找个频率上限,那应该就是标准频率1MHZ,超过1MHz,信号频率比标准频率还高,那肯定测不了了。但是这个1MHz的上限并没有意义,因为信号频率接近1MHz时,误差已经非常大了,所以最大频率要看你对误差的要求。上一小节我们说到了正负1误差,计100个数,误差1个,相对误差就是百分之一;计1000个数,误差1个,相对误差就是千分之一,所以正负1误差可以认为是1/计数值。在这里,如果要求误差等于千分之一时,频率为上限那这个上限就是1M/1000=1KHz;如果要求误差可以到百分之一,那频率上限就是1M/100=10KHz,这就是频率的上限.如果想提高频率的上限,那我们在这里(时基单元初始化时),就要把PSC给降低一点.,提高标准频率,上限就会提高。除此之外,如果频率还要更高,那我们就要考虑一下测频法了。测频法适合高频,测周法适合低频,我们这里是测周法,所以对于非常高的频率,还是交给测频法来解决吧。
然后呢,还有一个就是误差分析。除了我们之前说的正负1误差外,在实际测量的时候,还会有晶振误差。比如我们STM32的晶振不是那么准,在计次几百几万次之后,误差累积起来,也会造成一些影响
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM_LED.h"
#include "IC.h"
int main(void)
{
OLED_Init(); //初始化OLED
pwm_init();
IC_init();//初始化整个电路
OLED_ShowString(1,1,"Freq:00000Hz");
OLED_ShowString(2,1,"Duty:00%");
//PA0口输出1khz频率,50%占空比的待测信号;PWM模块将待测信号输出给PA0,PA0然后通过导线输入到PA6(PA6是TIM3的通道1,通道1通过输入捕获模块测量得到频率,然后在主循环里不断刷新显示频率)
PWM_setPSC(7200-1); //频率=72M/(psc+1)/(arr+1) //频率=72M/720/100 =1khz
PWM_SetCompare1(80);//占空比=ccr/(ARR+1) //占空比=50/100 = 50%
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);//不断刷新显示频率
OLED_ShowNum(2,6,IC_GetDuty(),2);//不断刷新显示占空比
}
}
IC.c
#include "stm32f10x.h" // Device header
//PWMI模式,方法1:修改上一个程序的4.初始化输入捕获单元
//方法2:使用TIM_PWMIConfig函数
void IC_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM3是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//打开时钟,PA6的通道1
//2.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure;//结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure);//使用的是地址传递
//3.1.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM3);
//3.2.配置时基单元参数
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,向上计数
/*公式:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
目前我们给的标准频率时1mhz,计数器最大只能计到65535,所以所测量的最低频率是1m/65535=15hz,如果信号频率再低,计数器就要溢出了所以最低频率就是15hz左右,如果想再降低一些最低频率的限制可以把psc再加大点这样标准频率就更低所支持测量的最低频率也就更低;最大频率没有界限,1MHZ,信号频率接近1mhz时误差已经非常大了,
*/
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR周期,最好要设置大一些防止计数溢出,16位的计数器可以满量程计数是65535
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器,标准频率就是72M/72=1MHz;这个值决定了测周法的标准频率fc,72M/预分频就是计数器自增的频率就是计数标准频率;需要根据信号频率的分步范围来调整
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
//4.初始化输入捕获单元,PWMI模式需配置成两个通道同时捕获同一个引脚的模式(一个简单的想法是:将通道初始化部分复制一份,结构体定义不需复制,通道1是直连模式上升沿触发通道2也延用这个配置)
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道,使用TIM3的通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF;//配置输入捕获的滤波器,数越大,滤波效果越好,每个数值对应的采样频率和采样次数在参考手册里有,若信号有毛刺和噪声就可以增大滤波器参数可以有效避免干扰
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//对应边沿检测、极性选择部分,可以选择上升沿触发/下降沿触发/上升沿和下降沿都触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//分频器,触发信号分频器,不分频就是每次触发都有效,2分频就是每隔一次有效一次,以此类推
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入,对应配置数据选择器的。可以选择直连通道/交叉通道/TRC引脚
TIM_ICInit(TIM3,&TIM_ICInitStructure);
//方法1,:将通道初始化部分复制一份,结构体定义不需复制
// 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;//交叉输入
// TIM_ICInit(TIM3,&TIM_ICInitStructure);
//方法2:使用TIM_PWMIConfig函数,可快捷地把电路配置成PWMI模式的标准结构,这个函数只支持通道1和2不支持通道3和4,和方法1的效果是一样的,只需传入一个通道的参数就行了,在函数里会自动把剩下的一个通道初始化成相反的配置(比如已经传入了通道1、直连、上升沿,那函数里就会顺带配置通道2、交叉、下降沿)
TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);
//5.配置触发源选择,配置TRGI的触发源为TI1FP1
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);//触发源选择TI1FP1
//6.配置从模式,为Reset
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);//从模式选择Reset
//7.启动定时器,调用TIM_Cmd
TIM_Cmd(TIM3,ENABLE);//CNT就会在内部时钟的驱动下不断自增,即使没有信号过来,它也会不断自增;有信号来的时候,CNT就会在从模式的作用下自动清零并不会影响测量
/*
初始化之后,整个电路就能全自动测量了,当我们想查看频率时,需要读取CCR进行计算,所以需要在下面写一个函数
*/
}
//获取频率的函数
uint32_t IC_GetFreq(void)
{
//使用测周法的公式,fc=72M/(psc+1),目前psc=72-1,所以fc=1MHz
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //返回的是最新一个周期的频率值(单位是HZ) = 1MHz(1000000) / N(就是读取CCR的值)
}
//获取占空比的函数
uint32_t IC_GetDuty(void)
{
//高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里,用CCR2/CCR1就能得到占空比了
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);//显示整数的话,给它乘100,这样返回值的范围就是0-100,对应占空比0%-100%
//经过实测,CCR总会少一个数,所以需要各加一个1补回来
}
pwm.c
#include "stm32f10x.h" // Device header
/*
pwm初始化函数基本步骤(参考笔记PWM基本结构图)
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置单元,包括时钟源选择和时基单元都配置好
第三步,配置输出比较单元,包括CCR值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一来配置
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,Pwm和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器,这样就能输出PWM了
*/
//PWM控制呼吸灯的代码逻辑是初始化TIM2的通道1,产生一个PWM波形,输出引脚是PA0,然后通过PWM_SetCompare1可以调节CCR1寄存器的值从而控制PWM的占空比,PWM的频率是固定写好在初始化程序里了,运行时候调节不太方便
//在最后再加一个函数,用来便捷地调节PWM频率,PSC和ARR都可以调节频率,但是调节ARR会影响占空比,通过PSC调节频率不会影响占空比,所以计划是固定ARR值,通过调节PSC来改变PWM频率
//一般可以根据分辨率的要求先确定ARR,PSC决定频率,CCR决定占空比
void pwm_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//2.初始化时基单元
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式
/*
公式:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
*/
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_ClearFlag(TIM2,TIM_FLAG_Update);
//4.初始化输出比较单元(通道)
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//给结构体赋初始值;若不想把所有成员都列一遍赋值,就可以先用这个函数赋一个初始值,再更改你想改的值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较的模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出比较的极性
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//设置输出使能(输出状态)
TIM_OCInitStructure.TIM_Pulse = 50;//设置CCR,Pulse直译是脉冲
TIM_OC1Init(TIM2, &TIM_OCInitStructure);//使用PA0口对应是第一个输出比较通道;在TIM2的OC1通道上就可以输出PWM波形了
//5.初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure; //结构体变量名GPIO_InitStructure
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出;PWM波形通过引脚输出,使用定时器来控制引脚,输出数据寄存器将被断开,输出控制权将转移给片上外设(这里片上外设引脚连接的就是TIM2的CH1通道)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructure); //使用的是地址传递
//6.启动定时器
TIM_Cmd(TIM2,ENABLE);//PWM波形就能通过PA0输出了
}
//在运行过程更改CCR,使用函数TIM_SetCompare1封装用来单独更改通道1的CCR值,进而改变占空比
void PWM_SetCompare1(uint16_t Compare1)//TIM_SetCompare1封装
{
TIM_SetCompare1(TIM2,Compare1);
}
//封装此函数,在初始化之后单独修改PSC,进而改变频率
void PWM_setPSC(uint16_t prescaler)
{
//调用库函数里单独写入PSC的函数,在tim.h中找,这个函数还有一个重装模式的参数所以叫TIM_PrescalerConfig
TIM_PrescalerConfig(TIM2,prescaler,TIM_PSCReloadMode_Immediate);//写入PSC,第二个参数是写入PSC的值,直接将外层函数的prescaler参数传进去,第三个参数是重装模式(还是影子寄存器、预装载这个问题,就是写入的值是立刻生效还是在更新事件生效;立刻生效可能会在值改变时产生切断波形的现象会出现不完整的周期,更新事件生效就是会有一个缓存器,延迟参数的写入时间,等一个周期结束了,在更新事件时,再统一改变参数,保证每个周期的完整)
}
示例程序(编码器接口测速)
那使用正交信号相比较单独定义一个方向引脚,有什么好处呢?
首先就是正交信号精度更高,因为A、B相都可以计次,相当于计次频率提高了一倍;其次就是正交信号可以抗噪声,因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路。如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那这时计次值是不会变化的。
所以我们编码器接口的设计逻辑就是,首先把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,然后到底是增还是减呢,这个计数的方向由另一相的状态来确定。当出现某个边沿时,我们判断另一相的高低电平,如果对应另一相的状态出现在上面这个表里,那就是正转,计数自增;反之,另一相的状态出现在下面这个表里那就是反转,计数自减,这样就能实现编码器接口的功能了,这也是我们STM32定时器编码器接口的执行逻辑。
接下来,我们就来看一下这个定时器的框图,看一下这个编码器接口的电路是如何设计的。
注意使用编码器模式的时候,我们之前一直在使用的72MHz内部时钟,和我们在时基单元初始化时设置的计数方向,并不会使用。因为此时计数时钟和计数方向都处于编码器接口托管的状态,计数器的自增和自减,受编码器控制.
然后我们看一下这里,我给出的一个编码器接口基本结构。
输入捕获的前两个通道,通过GPIO口接入编码器的A、B相,然后通过滤波器和边沿检测极性选择 ,产生TI1FP1和TI2FP2,通向编码器接口。编码器接口通过预分频器控制CNT计数器的时钟,同时,编码器接口还根据编码器的旋转方向,控制CNT的计数方向,编码器正转时,CNT自增,编码器反转时,CNT自减。
另外这里ARR也是有效的,一般我们会设置ARR为65535,最大量程,这样的话,利用补码的特性,很容易得到负数。比如CNT初始为0,我正转,CNT自增,0、 1、2、3、4、5、6、7等等,显示都没问题,但是我反转呢,CNT自减,0下一个数就是65535,接着是65534、65533等等这里负数不应该是-1、-2吗,65535是不是就出问题了。但是没关系,直接把这个16位的无符号数转换为16位的有符号数。根据补码的定义,这个65535就对应-1,65534就对应-2(有符号编码时负数按补码计算,2^16 的补码= -1)等等,这样就可以直接得到负数,非常方便,这就是我们读出数据得到负数的一个小技巧。
最后我们来看一些工作细节,和两个小例子。
这个工作描述的表,描述的就是我们刚才说什么时候正转、反转的,编码器接口的工作逻辑
这个实例展示的是极性的变化对计数的影响。
TI1反相是什么意思呢?
此时看下这个图,这里TI1和TI2进来,都会经过这个极性选择的部分。
在输入捕获模式下,这个极性选择是选择上升没有效还是下降沿有效的。但是根据我们刚才的分析,编码器接口,显然始终都是上升沿和下降沿都有效的,上升沿和下降沿都需要计次,所以在编码器接口模式下,这里就不再是边沿的极性选择了而是高低电平的极性选择。如果我们选择上升沿的参数,就是信号直通过来,高低电平极性不反转;如果选择下降沿的参数,就是信号通过一个非门过来,高低电平极性反转,所以这里就会有两个控制极性的参数,选择要不要在这里加一个非门,反转一下极性。
显然,这两个实例图的计数方向是相反的,这有什么作用呢?
比如你接一个编码器,发现它数据的加减方向反了,你想要正转的方向,结果它自减了,你想要反转的方向,结果它自增了,这时,就可以调整一下极性,把任意一个引脚反相,就能反转计数方向了。当然如果想改变计数方向的话,我们还可以直接把A、B相两个引脚换一下。
我们本节的内容(4.编码器接口),对应手册这里的14.3.12 编码器接口模式
代码实战
这里编码器测速一般应用在电机控制的项目上,使用PWM驱动电机,再使用编码器测量电机的速度,然后再用PID算法进行闭环控制。
现象:接了一个旋转编码器模块,这个代码和之前我们写的旋转编码器计次的代码,实现的功能基本都是一样的。目前我们这个代码,本质上也是旋转编码器计次,只不过这个代码是通过定时器的编码器接口,来自动计次。而我们之前的代码是通过触发外部中断,然后在中断函数里手动进行计次,使用编码器接口的好处就是节约软件资源,
如果使用外部中断来计次,那当电机高速旋转时,编码器每秒产生成千上万个脉冲,程序就得频繁进中断,然后进中断之后,完成的任务又只是简单的加—减一,是不是我们的软件资源就被这种简单而又低级的工作给占用了。所以,对于这种需要频繁执行,操作又比较简单的任务,一般我们都会设计一个硬件电路模块,来自动完成。那我们本节这个编码器接口,就是用来自动给编码器进行计次的电路。如果我们每隔一段时间取一下计次值,就能得到编码器旋转的速度了。
第一步,RCC开启时钟,开启GPIO和定时器的时钟
第二步,配置GPIO,这里需要把PA6和PA7配置成输入模式
第三步,配置时基单元,这里预分频器我们一般选择不分频
第四步,配置输入捕获单元。不过这里输入捕获单元只有滤波器和极性这两个参数有用,后面的参数没有用到,与编码器无关
第五步,配置编码器接口模式。这个直接调用一个库函数就可以了
最后,调用TIM_Cmd,启动定时器,就完事了
main.c
这段代码主要作用是通过定时器定时执行 TIM2_IRQHandler
中断服务程序,在定时器中断处理函数中读取编码器的值,并将其存储在 speed
变量中,然后在主循环中利用 OLED 显示器显示速度值。
值得注意的是,在 main
函数中需要调用 OLED_Init()
进行 OLED 显示器的初始化,并在使用 OLED 显示的地方调用 OLED_ShowString
和 OLED_ShowSignedNum
函数进行显示。
除了这些代码片段外,程序的其他部分,例如 OLED_Init
、Timer_Init
、Encoder_init
等函数的定义以及头文件的包含内容,以及关于定时器和编码器的配置,也是非常重要的。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
uint16_t speed;
int main(void)
{
OLED_Init(); //初始化OLED
Timer_Init(); //初始化定时器
Encoder_init();
OLED_ShowString(1,1,"speed:");
while(1)
{
OLED_ShowSignedNum(1,7,speed,5);//每隔一段时间读取一次;用定时中断
}
}
//定时器2中断函数放在使用中断的main.c文件中;在startup文件中;定时中断每隔1s执行一次
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
//检查中断标志位
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
//执行相应的用户代码
speed = Encoder_Get(); //定时器每隔1s读取一下速度,存在speed变量里
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除标志位
}
}
Encoder.c
这段代码是针对 STM32 微控制器的编码器初始化和测速函数的实现,用于读取编码器的旋转位置和速度。以下是对代码的简要说明:
-
Encoder_init
函数是编码器的初始化函数,主要包括了以下几个步骤: - 开启时钟,选择内部时钟,以及初始化 GPIO 接口。
- 配置时基单元 TIM3,设置预分频器、计数器模式和周期等参数。
- 配置输入捕获单元(通道)TIM_ICInit,设置滤波器和极性。
- 最后调用
TIM_EncoderInterfaceConfig
函数配置编码器接口。 - 启动定时器 TIM3。
-
Encoder_Get
函数是用于测速的函数,主要实现了在固定的时间间隔内读取一次计数器的值,并清零计数器。函数返回了测量得到的编码器的变化值,即速度信息。
#include "stm32f10x.h" // Device header
//编码器旋转控制CNT自增自减
//编码器初始化函数,编码器电路初始化后,CNT就会随着编码器旋转而自增自减;直接读出CNT值就能测量编码器的位置;测量编码器的速度和方向就需要每隔一段固定的闸门时间取出一次CNT然后再把CNT清零这就是测频法测量速度了
/*
第一步,RCC开启时钟,开启GPIO和定时器的时钟
第二步,配置GPIO,需将PA6和PA7配置成输入模式
第三步,配置时基单元,预分频器一般选择不分频,ARR一般给最大值655535,只需要CNT执行计数就行了
第四步,配置输入捕获单元,这里只有滤波器和极性两个参数有用,后面的参数没有用到,与编码器无关
第五步,配置编码器接口模式,直接调用一个库函数
最后,调用TIM_Cmd,启动定时器
*/
void Encoder_init(void)
{
//1.打开时钟,选择内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//2.初始化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);
//不需要初始化时基单元下面这个内部时钟函数,因为编码器接口会托管时钟,编码器接口就是一个带方向控制的外部时钟,所以内部时钟就不用了
//TIM_InternalClockConfig(TIM3);
//3.配置时基单元
/*
公式:
PWM 频 率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
*/
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); //初始化TIM3
//4.配置输入捕获单元(通道),编码器接口只使用了通道1和2的滤波器和极性选择
//首先定义结构体变量,然后StructInit给结构体赋一个初始值,再部分修改我们想要的参数,调用ICInit配置一遍电路,结构体变量的配置在调用ICInit函数之后就写入到硬件的寄存器了,所以ICInit之后这个结构体我们可以换个值继续使用、不需要重新定义新的结构体
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);//结构体初始化,防止结构体中出现不确定值可能造成问题,最好用StructInit给结构体赋一个初始值
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器为0xF
//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //电平极性为上升沿,这里的上升沿参数代表的是高低电平极性不反转;等会配置编码器接口的时候也有极性配置,属于重复配置,这个其实可以删掉;这里的上升沿并不代表上升沿有效,因为编码器接口始终都是上升沿、下降沿都有效
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器为0xF
//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //电平极性为上升沿,这里的上升沿参数代表的是高低电平极性不反转;等会配置编码器接口的时候也有极性配置,属于重复配置,这个其实可以删掉;这里的上升沿并不代表上升沿有效,因为编码器接口始终都是上升沿、下降沿都有效
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//5.配置编码器接口,只需调用一个函数就行了;;需保证TIM_EncoderInterfaceConfig在TIM_ICInit函数之后,否则TIM_ICInit覆盖TIM_EncoderInterfaceConfig函数的配置
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//选择Rising是通道不反相,选择Falling是通道反相;重复配置TIM_ICPolarity_Rising,后面的参数会覆盖前面的参数配置
//6.启动定时器
TIM_Cmd(TIM3,ENABLE);
}
int16_t Encoder_Get(void)
{
//测速,在固定的匝门时间读一次CNT然后把CNT清零
int16_t temp;//因为要先读取CNT再清零,所以要用temp缓存一下
temp = TIM_GetCounter(TIM3);//读取CNT
TIM_SetCounter(TIM3,0);//CNT清零
return temp;
}
Encoder.h
这段代码是编码器模块的头文件 “Encoder.h” 的内容。它使用了条件编译指令,防止头文件的重复包含。
在头文件中,声明了两个函数的原型:
Encoder_init
:编码器的初始化函数。Encoder_Get
:获取编码器的值(位置或速度)的函数。同时,使用了预处理指令 #ifndef
、#define
、#endif
,确保头文件只包含一次,以避免重复定义的错误。
通过包含这个头文件,其他源文件就可以使用 Encoder_init
和 Encoder_Get
函数进行编码器的初始化和获取编码器的值。
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_init(void);
int16_t Encoder_Get(void);
#endif