目录
- 6、TIM(Timer)定时器
- 基本定时器
- 通用定时器
- 高级定时器
- 示例程序(定时器定时中断&定时器外部时钟)
- TIM输出比较
- 示例程序(PWM驱动LED呼吸灯&PWM驱动舵机&PWM驱动直流电机)
- TIM输入捕获
- 示例程序(输入捕获模式测频率&PWMI模式测频率和占空比)
- TIM编码器接口
- 示例代码(编码器接口)
6、TIM(Timer)定时器
STM32中功能最强大、结构最复杂的外设——定时器。
定时器共四个部分,分为八个小节笔记。本小节为第一部分第一节。
在第一部分,是定时器的基本定时的功能:定时中断功能、内外时钟源选择
在第二部分,是定时器的输出比较功能,最常见的用途是产生PWM波形,用于驱动电机等设备
在第三部分,是定时器的输入捕获功能和主从触发模式,来实现测量方波频率
在第四部分,是定时器的编码器接口,能够更加方便读取正交编码器的输出波形,编码电机测速
实验一:定时中断的功能,定时器使用内部时钟定了一个一秒的时间,每隔一秒申请一下中断,然后在中断函数里执行number++,最后在OLED上显示number。
实验二:定时器外部时钟,这个程序使用了外部时钟来驱动定时器,我们可以在定时器指定的外部引脚上输入一个方波信号,来提供定时器计数的时钟。现在这里我暂时用这个对射式红外传感器来手动模拟一个外部时钟。我们用挡光片依次遮挡、移开、遮挡、移开、提供一个方波,可以看到OLED上下面这个CNT就是定时器中计数器的值,每遮挡、移开一次计数器加一,然后计数器寄到九后自动清零,同时申请中断,执行number++。
使用定时器的外部时钟可以提供一个更加精确的时钟来计时,或者也可以把外部时钟当做一个计数器
,用来统计引脚上电平翻转的次数,毕竟定时器本质上就是一个计数器。
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)、16位预分频器(可以对计数器的时钟进行分频,让计数更加灵活)、16位自动重装寄存器(是计数的目标值,计多少个时钟申请中断),这几个寄存器构成了定时器最核心的部分,我们把这一块电路称为时机单元,在72MHz计数时钟下可以实现最大59.65s的定时(也就是预分频器设置最大,自动重装也设置最大)。
为什么在72MHz计数时钟下可以实现最大59.65s的定时?
72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多,大家可以自己算一下。
详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。【最大值是65536,但计数是从0~65535】
这就是最大的定时时间,应该说还是挺长的了。如果你嫌这个还不够长,STM32的定时器还支持级联的模式,也就是一个定时器的输出当做另一个定时器的输入,这样加一起最大的定时时间就是59.65秒
再乘两次6536,这个时间大概是8000多年。如果还嫌短,那就再级联一个定时器,定时时间还会再延长6536x6536倍,这个可见指数爆炸的威力,小小的STM32利用3个定时器几年就能实现丈量宇宙年龄的能力。
定时器不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。
定时器根据复杂度和应用场景分为了高级定时器、通用定时器(最常用)、基本定时器三种类型。
定时器类型
类型 | 编号 | 总线 | 功能 |
---|---|---|---|
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
除了TIM1-8,在库函数中还出现了TIM9、10、11等(这些一般都用不到,知道就好)
三种定时器所连的总线是不一样的,其中高级定时器连接的是性能更高的APB2总线,通用定时器和
基本定时器连接的是APB1中线,这个在RCC开启时钟的时候要注意一下。
最后看一下功能:
先看基本定时器,它的功能最少只有基本的定时中断功能和一个主模式触发DAC的功能,所以基本定时器还可以和DAC联合使用,这就是基本定时器,还是比较简单的。
然后是通用定时器,这就复杂起来了,它拥有基本定时器全部功能,同时它还额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能,这些功能就是我们本课程重点讲的。
最后就是高级定时器了,这就更复杂了,它拥有通用定时器全部功能,同时它还具有重复计数器、死区生成、互补输出、刹车输入等功能,这些功能主要是为了三相无刷电机的驱动设计的,我们本课程暂时不会涉及到。
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4,也就是一个高级定时器和三个通用定时器,没有基本定时器;不同的型号,定时器的数量是不同的。你在操作这个外设之前,一定要查一下它是不是有这个外设,别操作到了不存在的外设,那样是不会起作用的。
接下来,我们就依次来看一下高级定时器、通用定时器和基本定时器的结构图,看一下这三种定时器是怎么样来工作的,设计这些结构都能完成哪些任务。
基本定时器
理解时基单元的工作流程(定时器产生中断的全部流程)、主模式触发DAC的功能,如下内容:
下面这三个构成了最基本的计数计时电路,所以这一块电路就叫做时基单元
时基单元:预分配器(PSC)、自动重装载寄存器(ARR)、计数器(CNT)
时基单元之前连接的就是基准计数时钟的输入,最终来到了这个位置。由于基本定时器只能选择内部时钟,所以你可以直接认为这根线直接连到了输入端的这里,也就是内部时钟CK_INT,内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72MHz。
进入时基单元首先是预分频器(PSC),它可以对72MHz的计数时钟进行预分频(比如,预分频器写0就是不分频输出72MHz,这时候输出频率等于输入频率等于72MHz,写1是进行二分频输出36MHz,写2是三分频输出24MHz …,所以预分频的值和实际的分频系数相差1,即,实际分频系数=预分频器的值+1),预分频器是16位的,最大值可以写65535,也就是最大65536分频。
然后是计数器,对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值加1,这个计数器的值也是16位的,值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以还需一个存储目标值的寄存器,那就是自动重装载寄存器了。
自动重装寄存器也是16位的,它存的是我们写入的计数目标,在运行的过程中,计数值不断自增,自动重装载是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。在这里图上画的一个向上的折线箭头就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,叫做“更新中断”,这个更新中断之后就会通向NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应。这里向下的箭头代表的是会产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其他电路的工作。
总结定时器产生中断的全部流程:从基准时钟到预分频器再到计数器,计数器计数自增,同时不断地与自动重装寄存器进行比较,值相等时,即计时时间到,这时就会产生一个更新中断和更新事件,CPU响应更新中断,就完成了我们定时中断的任务了。
下图红圈,是一个向上的折线箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,叫做“更新中断”。
下图红圈,是一个向下的折线箭头,代表的是产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其它电路的工作。
主模式触发DAC的功能
下面,简单介绍一下(后续讲),主模式触发DAC的功能,STM32定时器的一大特色就是主从触发模式(主从触发模式能让内部的硬件在不受程序的控制下实现自动运行),如果能把主从触发模式掌握好,那在某些情景下将会极大地减轻CPU的负担。
主模式触发DAC的作用就是,在我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,这样会使主程序处于频繁被中断的状态,这会影响主程序的运行和其他中断的响应,所以定时器就设计了一个主模式,使用这个主模式可以把定时器的更新事件映射到触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上,这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接区触发DAC,整个过程不需要软件的参与,实现了硬件自动化,这就是主模式的作用,当然除了主模式外,还有更多硬件自动化的设计(后续讲)。
这个可编程定时器的主要部分是一个带有自动重装载的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重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了。
通用定时器
通用定时器结构就瞬间复杂了很多。
1.通用定时器与基本定时器异同
首先,中间最核心的部分,还是时基单元,这部分结构和工作流程和基本定时器是一样的,不过对于通用定时器而言,计数器的计数模式就不止向上计数一种了(向上自增),通用定时器和高级定时器支持向上计数模式、向下计数模式和中央对齐模式。(基本定时器仅支持向上计数模式)。最常用的还是向上计数模式。
-
向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后继续下一轮,依次循环
-
中央对齐模式就是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断,然后继续下一轮,依次循环
2.内外时钟源选择和主从触发模式
-
如下,是内外时钟源选择和主从触发模式的结构。
-
内外时钟源选择:对于基本定时器,定时只能选择内部时钟,也就是系统频率72MHz;对于通用定时器,时钟源可以选择内部时钟或者外部时钟。
外部时钟的选择有如下四种:
第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,TIMx_ETR(External)引脚的位置可以参考引脚定义表,中关于默认复用功能和重定义功能的定义,如下图所示。可以看到这里有TIM2_CH1_ETR,意思就是这个TIM2的CH1和ETR都复用在了引脚PA0上。下面还有其他定时器的引脚CH2、CH3、CH4和其他定时器的一些引脚也可以在表中找到。
这里可以在TIM2的ETR引脚也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这些电路可以对外部时钟进行一定的整形(因为是外部时钟,所以难免会有毛刺,这些电路就可以对输入的波形进行滤波),同时也可以选择一下极性和预分频器,最后滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了,在stm32中,这一路也叫做‘外部时钟模式2’(如图中红线);另一路与其他信号通过数据选择器输出TRGI(Trigger In,触发输入),从名字上来看,它主要是作为触发输入来使用的,这个触发输入可以触发定时器的从模式。关于触发输入和从模式的内容之后再涉及,本节主要把这个触发输入当作外部时钟来使用的情况,你暂且可以把这个TRGI当做外部时钟的输入来看,当这个TRGI当作外部时钟来使用时,这一路就称为外部时钟模式1(如图中黄线所示)。
那通过这一路的外部时钟都有哪些呢,往左看,第一个就是ETR引脚的信号,这里ETR引脚既可以通过上面这一路来当做时钟,又可以通过下面这一路来当做时钟,两种情况对于时钟输入而言是等价的,只不过是下面这一路输入会占用触发输入的通道而已。
- 第二个外部时钟可以是来自其他定时器的信号ITR
主模式的输出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引脚,还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了;
下面设置这么复杂的输入,不仅仅是为了扩大时钟树的范围,更多的还是为了某些特殊应用场景而设计的,比如未来定时器的级联而设计的这一部分,下面这一部分我们之后讲输入捕获和测频率时还会继续讲的。
对于时钟输入而言,最常用的还是内部的72兆赫兹的时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单最直接。
编码器模式
最后这里还有一块没有讲到,这个是定时器的一个编码器接口(红框下方),可以读取正交编码器的输出波形,这个我们后续课程也会再讲。
主模式输出
这部分电路可以把内部的一些事件映射到这个TRGO引脚上,比如我们刚才讲基本定时器分析的,将更新事件映射到TRGO,用于触发DAC。这里也是一样,它可以把定时器内部的一些事件映射到这里来,用于触发其它定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。
输出比较电路
通用定时器结构图的右下角即为定时器的输出比较功能的结构,如下图所示。有四个输出通道,分别对应CH1到CH4的引脚,可以用来输出PWM波形,驱动电机。
输入捕获电路
通用定时器的左下角即为输入捕获电路的结构图,它同输出比较功能一样有四个通道,对应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外部时钟、ITRx其他定时器、Tlx输入捕获通道,这些就是定时器的所有可选的时钟源了。最后这里,还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。
接下来右边这里,就是计时时间到,产生更新中断后的信号去向。如果是高级定时器的话还会多一个重复计数器。那这里中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。
为什么会有一个中断输出控制呢?
因为这个定时器模块有很多地方都要申请中断。比如上面这个图不仅更新要申请中断,这里触发信号也会申请中断,还有下面的输入捕获和输出比较匹配时也会申请。所以这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止。简单来说,这个中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。
时基单元运行时序举例
STM32中,关于时序运行的内容很多,具体请见手册的详细讨论,这里仅举一些时基单元的例子作简要分析。
1.预分频器时序分析
图中描述了当预分频器的分频系数从1变为2时,计数器的时序图。第一行是CK_PSC是预分频器的输入时钟,选内部时钟的话一般是72MHz,这个时钟在不断运行;下面的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。这就是一个计数周期的工作流程。
2.缓冲(影子)寄存器
然后是最下面的三行时序,描述的是预分频寄存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个:一个是倒数第三行的预分频控制寄存器,供我们读写用的,并不直接决定分频系数;另一个是倒数第二行的预分频缓冲寄存器(影子寄存器),才是真正起作用的寄存器。
比如我们在某个时刻把预分频寄存器由0改成了1,如果在此时立刻改变时钟的分频系数,那么就会导致这里在一个计数周期内,前半部分和后半部分的频率不一样,这里计数计到一半,计数频率突然就会改变了,这虽然一般并不会有什么问题,但是s t m32 的定时器比较严谨,设计了这个缓冲寄存器,这样当我在计数记到一半的时候,改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时产生的更新事件,预分频计数器的值才会被传递到缓冲寄存器里面去,才会生效,所以在这里看到,即使我在计数中途改变了预分频值,计数频率仍然会保持为原来的频率,直到本轮技术完成,在下一轮计数时改变后的分频值才会起作用。
在定时器结构图中,有些寄存器的画法采用了方框下加阴影的方式,就说明该寄存器不是只有一个寄存器,而是有两个寄存器来形成缓冲机制,下图中包括预分频器、自动重装载寄存器、捕获比较寄存器都是的。实际上,真正使时序电路状态发生更改的都是影子寄存器。
最后 ,预分频器内部实际上也是靠计数来分频的,当预分频值为零时,计数器就一直为零,直接输出原频率,当预分频值为1时,计数器就0 1 0 1 0 1 0 1,这样计数,再回到零的时候输出一个脉冲,这样输出频率就是输入频率的二分频。预分频器的值和时间的分频系数之间有一个数的偏移。
计数器计数频率: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)
CK_PSC=72 MHz
计数器溢出时间:1/计数器溢出频率,这就是我们计算定时时间(溢出时间)的式子。
- 计数器无预装时序图(缓冲机制失效 设置APRE = 0)
那刚才说了,预分频器为了防止计数中途更改数值造成错误,设计了缓冲寄存器,这个计数器也少不了这样的设计,并且这个缓冲计算器是用还是不用,是可以自己设置的。
计数器无预装时序就是没有缓冲寄存器的情况。
在计数器正在进行自增计数,突然更改了自动加载寄存器,就是自动重装载寄存器ARR,由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,才能产生更新。这里就可以看出,如果不使用缓冲机制,可能会给电路时序的工作造成一些问题。
当然如果你不介意这样的问题的话,那就不用管这些细节了,毕竟STM32设计出来要考虑到各种各样的情况。
4.RCC时钟树简介
RCC时钟树:在STM32中用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统。 时钟是所有外设运行的基础,所以时钟是最先配置的东西。在执行主程序之前还会执行一个SystemInit函数,这个函数的作用就是配置RCC时钟树。这个结构看上去挺复杂的,配置起来还是比较麻烦的,不过好在ST公司已经帮我们写好了配置这个时钟数的SystemInit函数。
RCC时钟树可以分为左右两部分:时钟产生电路(左)和时钟分配电路(右)。中间的SYSCLK就是系统时钟72MHz。
1.时钟产生电路
在时钟产生电路,有四个振荡源,分别是内部的8MHz高速RC振荡器、外部的4-16MHz高速石英晶体振荡器(也就是晶振,一般都外接8MHz)、外部的32.768kHz低速晶振振荡器(一般给RTC提供时钟)、内部的40kHz低速RC振荡器(给看门狗WDG提供时钟)。上面的两个高速晶振是用来提供系统时钟的,AHB\APB2\APB1的时钟都是来源于这两个高速晶振。内部和外部都有一个8MHz的晶振,都是可以用的,只不过外部的石英振荡器比内部的RC振荡器更加稳定,所以一般都用外部晶振。如果系统非常简单,且不需要过于精确的时钟,就可以使用内部的RC振荡器,这样可以省下外部的晶振电路。
在SystemInit函数中ST是这样来配置时钟的:首先会启动内部的8MHz高速RC振荡器产生时钟,选择该时钟为系统时钟,暂时以8MHz的内部时钟运行;然后再启动外部时钟,配置外部时钟信号流经如下图所示的电路:
外部晶振信号进入PLLMUL锁相环进行倍频,8MHz倍频9倍,得到72MHz,待锁相环输出稳定后,选择锁相环输出为系统时钟。这样就把系统时钟从8MHz切换为了72MHz,以上就是ST配置的过程,大家可以自己分析一下SystemInit函数。
这样分析之后,可以解决实际应用的一个问题:那就是如果外部晶振出问题,可能会出现程序时钟慢大概10倍的现象。如果外部时钟的硬件电路有问题(外部晶振引脚焊接短路或连接错误等),系统的时钟就无法切换到72MHz,会保持内部的8MHz运行。8M相比于72M大概就慢了10倍。
图中的CSS称为时钟安全系统,它同样负责切换时钟。CSS可以监测外部时钟的运行状态,一旦外部时钟失效,它就会自动把外部时钟切换为内部时钟,从而保证程序可以正常运行,防止程序卡死造成事故。另外在高级定时器的刹车输入功能中,也有CSS,一旦CSS检测到外部时钟失效,通过或门就会立刻反应到输出控制器,让输出控制的电机立刻停止,防止意外。(即切断输出控制引脚,切断电机输出,防止发生意外。)
2.时钟分配电路
首先系统时钟72MHz进入AHB总线,在AHB总线上有一个预分频器,在SystemInit函数配置的默认分频系数为1,所以AHB总线的时钟自然是72MHz。
之后信号进入APB1总线,APB1上同样有预分频器,这里SystemInit默认配置的分频系数为2,输出为36MHz,所以APB1总线的时钟为36MHz,但是前面定时器说过,所有定时器的时钟都是72MHz,这是为什么?通用定时器和基本定时器是接在APB1上的,但是APB1(APB2同理)连接定时器还有如图所示的以下结构:
通用定时器和基本定时器通过图中APB1下方的支路与APB1连接。由于APB1的预分频系数默认为2,则输出到定时器的时钟频率×2。APB2的预分频器的分频系数默认配置为1,其他流程与APB1同理。所以基本定时器,通用定时器,高级定时器的内部基准时钟都是72MHz,这样设计为我们使用定时器带来了方便,不用考虑不同定时器时钟不同的问题了(前提是不乱修改ST提供的SystemInit函数中的默认配置)。
在这些时钟输出这里,都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是我们在程序中写RCC_APB2/1PeriphClockCmd作用的地方,外设时钟控制作用的地方,打开时钟就是在这个位置写1,让左边的时钟能够通过与门输出给外设。
那关于时钟数的内容,就讲到这里,剩下的还有一些给ADC、SDIO等这些提供时钟的电路,大家就自己看看了。
参考手册
继续深入学习,可查阅参考手册
示例程序(定时器定时中断&定时器外部时钟)
知识点get:
前面定时器框图里出现好几个滤波器,滤波器工作原理:可以滤掉信号的抖动干扰。在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个采样值输出出去;如果这N个采样值不全都相同,那就说明信号有抖动,这时就保持上一次的输出,或者直接输出低电平也行,这样就能保证输出信号在一定程度上的滤波;这里的采样频率f和采样点数N都是滤波器的参数,频率越低,采样点数越多,滤波效果越好,不过相应的信号延迟就越大;采样频率f由内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,这个分频多少是由参数ClockDivision决定的,这个参数其实跟时基单元关系并不大,它的可选值可以选择1分频(也就是不分频),2分频和4分频,我们随便配置一个就好了,后面代码里使用1分频,也就是不分频 。
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);
定时器初始化步骤如下:
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,这些参数用一个结构体就可配置好了
第四步,配置中断输出控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能一下计数器,要不然计数器是不会运行的。当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个定时器中断函数,这样这个中断函数每隔一段时间就能自动执行一次了。
第一步,RCC开启时钟,这个基本上每个代码都是第一步。在这里打开时钟后,定时器的基准时钟和整个外设的工作时钟就都会同时打开了
第二步,选择时基单元的时钟源。对于定时中断,我们就选择内部时钟源
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);选择内部时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);选择ITR其他定时器的时钟,参数TIMx选择要配置的定时器,参数TIM_InputTriggerSource选择要接入哪个其他的定时器
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource, uint16_t TIM_ICPolarity, uint16_t ICFilter);选择TIx捕获通道的时钟,参数TIM_TIxExternalCLKSource选择TIx具体的某个引脚,还有两个参数是输入的极性和滤波器。
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);选择ETR通过外部时钟模式1输入的时钟,参数TIM_ExtTRGPrescaler外部触发预分频器,这里可以对ETR外部时钟再提前做一个分频,还有两个参数是输入的极性和滤波器。
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);选择ETR通过外部时钟模式2输入的时钟。
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);这个不是用来选择时钟的,单独用来配置ETR引脚的预分频器、极性、滤波器这些参数
注:没选择时钟,会默认内部时钟
然后最后一个函数,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; //指定时钟分频,1分频也就是不分频
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; //重复计数器的值,高级定时器里用到,这里用不到直接给0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
/*查看库函数TIM_TimeBaseInit源码的最后,发现函数内会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值
预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用
为了让写入的值立刻起作用,故在函数的最后手动生成了一个更新事件
但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题(避免出现上电后OLED从1开始计数)*/
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能中断到NVIC的通路
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;// 定时器2在NVIC里的通道
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引脚是哪个
-
在上一个定时中断实例程序基础上进行更改;基本任务仍然是定时中断,时钟部分就不使用内部时钟了
对射式红外传感器,DO数字输出接到PA0引脚,这个PA0引脚就是TIM2的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);
//外部触发滤波器,范围0X00-0X0F,这里不用滤波了
//配置时基单元
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 自动重装载记到9 这两个值改小,我们手动模拟的没那么快
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);//清除标志位
}
}
上面这里是number,下面是CNT计数器的值,我们用挡光片挡一下CNT就加一,因为现在时基单元没有预分频,所以每次遮挡CNT都会加一。如果有预分频呢,就是遮挡几次才能加一次,然后加到九后自动清零,同时申请中断number++,这就是第二个程序的现象了。
TIM输出比较
输出比较功能简介
输出比较,英文全称Output Compare,简称OC。它主要的功能是可以通过比较计数器CNT和捕获/比较寄存器(Capture/Compare Register)CCR值的关系,来输出电平进行置1、置0的翻转操作,用于输出一定频率和占空比的PWM波形。
- IC全称是Input Capture,意为输入捕获
- CC全称是Capture Compare,意为输入捕获和输出比较(捕获比较)的单元
捕获/比较寄存器是输入捕获和输出比较共用的,当使用输入捕获时,它就是捕获寄存器;当使用输出比较时,它就是比较寄存器。那在输出比较这里,这块电路会比较CNT和CCR的值,CNT计数自增,CCR是我们给定的一个值,当CNT大于CCR、小于CCR或者等于CCR时,这里输出就会对应的置1、置0、置1、置0,这样就可以输出一个电平不断跳变的PWM波形了。这就是输出比较的基本功能。
每个高级定时器和通用定时器都拥有4个输出比较的通道,可以同时输出4路PWM波形,且高级定时器的前3个通道额外拥有死区生成电路和互补输出的功能(用于驱动三相无刷电机,本课程暂未涉及,我们主要学习通用定时器的输出比较功能)。4个输出比较通道都有独立的CCR寄存器,但是它们共用同一个CNT计数器。
PWM简介
PWM(Pulse Width Modulation),即脉冲宽度调制,PWM波形是一个数字输出信号,是由高低电平组成的,是一种对模拟电平信号进行数字编码的方法。在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。也就是说,使用PWM波形,是用来等效地实现一个模拟信号的输出。(例如,led呼吸灯,电机调速,如下解释)
以LED为例:GPIO的输出信号只能是数字信号,按理说LED只能有完全亮和完全灭两种状态,如果想通过数字信号输出模拟量,可以通过以下的方法实现:让LED不断点亮、熄灭、点亮、熄灭,当点亮、熄灭的频率足够大时,由于LED的余晖和人眼的视觉暂留效应,LED就不会闪烁了,就会呈现出一个中等亮度。当调控点亮和熄灭的时间比例时就能让LED呈现出不同的亮度级别。
对于电机调速也类似:在高频率下不断让电机交替通断,由于电机断电后不会立刻停止,而是由于惯性转动后停下,电机的速度就能维持在一个中等速度。
PWM的秘诀是:天下武功,唯快不破! 只要我闪的足够快,你就发现不了我到底是闪着亮的,还是一个正常的平稳的亮度。需要注意的是:只有在具有惯性的系统中,才能用PWM对模拟信号进行编码。
从下图可以看出,高低电平跳变的数字信号可以被等效地表示为中间虚线所表示的模拟量。当上面电平时间长一点,下面电平短一点的时候,等效地模拟量就偏向于上面;当下面电平时间长一点,上面电平时间短一点的时候,等效地模拟量就偏向于下面。也就是说,占空比越大,等效的模拟量就越趋近于数字量的高电平;占空比越小,等效的模拟量就越趋近于数字量的低电平,且这个等效关系一般而言是线性一一对应的,比如高电平是5V,低电平是0V,那50%占空比就等效于中间电压就是2.5V;20%占空比就等效于1/5处的电压就是1V,这就是占空比。
使用PWM波形,就可以在数字系统等效输出模拟量,就能实现LED控制亮度、电机控速等功能了。
- PWM参数
首先,明白Ts就是下图这里,Ts代表一个高低电平变换周期的时间
在使用PWM对模拟量进行编码时,以下三个参数尤其重要:
频率 :f = 1 / Ts(周期的倒数就是频率);变换越快=频率越大(PWM的频率越快,它等效模拟的信号就越平稳,不过同时性能开销就越大;一般来说PWM的频率在几kHz到几十kHz之间。)
占空比:q=Ton/Ts( Ton是高电平的时间,Ts是一个周期的时间。q就是高电平时间相对于整个周期时间的比例);占空比决定了PWM等效出的模拟电压的大小。一般用百分比表示。
分辨率:占空比的变化步距;比如有的占空比只能是1%、2%、3%等等这样以1%的布距跳变,那它的分辨率就是1%;如果可以1.1%、1.2%、1.3%等等,这样以0.1%的布距跳变,那它的分辨率就是0.1%。所以分辨率就是占空比变化的细腻程度。分辨率的大小要看实际项目的需求定。如果既要高频率,又要高分辨率,就需要硬件电路要有足够的性能。要求不高的情况下,1%的分辨率就足够使用了。
输出比较通道(通用定时器)
那接下来我们就来具体地分析一下,定时器的输出比较模块是怎么来输出PWM波形的,我们先看一下通用定时器的这个结构。
左边是CNT和CCR比较的结果,右边就是输出比较电路,最后通过TIM_CH1输出的GPIO引脚上,然后下面还有三个同样的单元,分别输出到CH2、CH3、CH4。图的左边是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维持原状态,实际上此时REF与CNT和CCR都无关,不管谁大谁小,即CNT和CCR无效,REF直接保持为上一个状态。这有什么用呢?比如你正在输出PWM波,突然想暂停一会儿输出,就可以设置成这个模式,一但切换为冻结模式后,输出就暂停了,并且高低电平也维持为暂停时刻的状态,保持不变。这就是冻结模式的作用
-
无效/有效电平:这个有效电平和无效电平一般是高级定时器里面的一个说法,是和关断、刹车这些功能配合表述的,他说的比较严谨,所以叫有效电平和无效电平,在这里为了理解方便,你可以直接认为置有效电平就是置高电平。
-
匹配时…
这三个模式都是当CNT与CCR值相等时,执行操作。
第一个是CNT等于CCR时,REF置有效电平,也就是高电平;第二个是相等时置无效电平也就是低电平;第三个是相等时电平翻转。这些模式就可以用做波形输出了,比如相等时电平翻转这个模式,这个可以方便地输出一个频率可调,占空比始终为50%的PWM波形。比如你设置CCR为0,那CNT每次更新清0时,就会产生一次CNT=CCR的事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期,并且高电平和低电平的时间是始终相等的,也就是占空比始终为50%,当你改变定时器更新频率时,输出波形的频率也会随之改变。它俩的关系是输出波形的频率=更新频率/2,因为更新两次输出才为一个周期。这就是匹配时电平翻转模式的用途。
那上面这两个相等时置高电平和低电平,感觉用途并不是很大,因为它们都只是一次性的,置完高或低电平后,就不管事了,所以这俩模式不适合输出连续变化的波形。如果你想定时输出一个一次性的信号,那可以考虑一下下这两个模式。
- 强制为无效电平|有效电平
这两个模式是CNT与CCR无效,REF强制为无效电平或者强制为有效电平,这两个与冻结模式类似。如果你想暂停波形输出,并且在暂停期间保持低电平或者高电平,那你就可以设置这两个强制输出模式。 - PWM模式1/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的变化范围就越大,分辨率就越高。(占空比变化的越细腻越好)
输出比较通道(高级定时器)
这个电路仅作了解即可,不需掌握。
从这里画一个圈,把这个圈里面的东西去掉,那它就跟通用定时器的电路是一样的了,那这个圈里的电路到底有什么用呢,这就需要和外部电路来一起理解了,在它外面通常要接一个这样的电路,上面是正极,接着是一个大功率开关管,一般都是MOS管,就是一种大功率电子开关,然后再来一个MOS管,最后到GND,左边是控制机,比如说给高电平,右边两根线就导通,低电平就断开,下面也是一样,有一个控制极,高电平导通低电平断开,这就是一个基本的推挽电路,中间是输出,如果上管导通下管断开,那输出就是高电平,如果下管导通上管断开,那输出就是低电平,如果上下管都导通
那就是电源短路,这样是不允许的。如果上下管都断开,那输出就是高阻态,这就是推挽电路工作流程,如果有两个这样的推挽电路,那就构成了H桥电路,就可以控制直流电机正反转了,如果有三个这样的推挽电路,那就可以用于驱动三相无刷电机了,这就是这个电路的用途。
那对于这个电路来说,如果直接用单片机来控制的话,那就需要两个控制极,并且这两个控制极电平是相反的,也就是互补,因为上管导通下管就必须断开,下管导通上管就必须断开,知道了外围电路的需求,再来理解内部电路结构,那自然就好理解了。
首先这个OC1和OC1N就是两个互补的输出端口,分别控制上管和下管的导通和关闭,然后是在切换上下管导通状态时,如果在上管关断的瞬间,下管立刻就打开,那可能会因为器件的不理想,上管还没有完全关断,下管就已经导通了,出现了短暂的上下管同时导通的现象,这会导致功率损耗啊
引起器件发热,所以在这里为了避免这个问题,就有了死区生成电路,它会在上管关闭的时候延迟一小段时间再导通下管,下管关闭的时候延迟一小段时间再导通上管,这样就可以避免上下管同时导致的现象了,这就是死区生成和互补输出的用途。
舵机和直流电机
舵机
舵机是小型直流伺服电机的一种,是一种根据输入PWM信号占空比来控制舵机输出轴的角度的装置。它有三根输入线,其中两根是电源线,一根是PWM信号输入线。白色输出轴会固定在一个指定的角度不动,固定的位置是由信号线的PWM信号来决定的,这就是舵机的工作方式。
上边右图中可以看出,舵机其实并不是一种单独的电机,可以发现它是由一个直流电机、一个减速齿轮组、一个电位器(电压编码器)和一个控制板 4部分组成的整体。舵机不是一种单独的电机,它的内部是由直流电机驱动的。内部的控制电路板是一个电机的控制系统,整个舵机内部形成了一个闭环的控制系统。
PWM信号输入到控制板,给控制板一个指定的目标角度,然后这个电位器检测输出轴的当前角度,如果大于目标角度,电机就会反转,如果小于目标角度,电机就会正转,最终使输出轴固定在指定的角度,这就是舵机的内部工作流程(简而言之:输入一个PWM波形,输出轴固定在一个角度)。
“伺服”—词源于希腊语“奴隶”的意思,英文为Servo。人们想把某一个结构或系统当作一个得心应手的驯服工具,服从控制信号的要求而动作。伺服的主要任务是按照控制命令的要求,对输出信号和输出功率进行放大、变换与调控等处理,使驱动装置输出的力矩、速度和位置控制得非常灵活方便。由于它的“伺服”性能,因此而得名——伺服系统。它的优势在于:可以非常灵活地控制输出装置的力矩、速度和位置等物理参量。
交流伺服电机和直流伺服电机的共同点是:利用传感器(编码器)对转子的位置、转速、力矩、转向进行检测,斌且将得到的信号经由伺服驱动器反馈给伺服控制器,从而达到调节转子位置、转速、力矩、转向的目的; 二者的不同点在于,一般而言,交流伺服电机相较于直流伺服电机对转子有更高的控制精度。
输入信号脉冲宽度,周期是20ms,也就是一个上升沿到下一个上升沿之间的时间是20ms。
舵机对输入的PWM信号的要求如下:周期为20ms(对应50Hz),高电平宽度为0.5 ~ 2.5ms(就是占空比是这个范围,对应的输出角度如上图)。这时一个180° 的舵机,输出轴的角度是-90° 到+90° 或者你规定是0° 到180°,输入信号脉冲宽度与舵机输出轴转角的对应关系都是线性一一对应的(图中高电平的时间如果是0.5ms,对应的角度就是-90度;如果是1ms对应的角度就是-45度等),给个PWM,输出轴就会固定在一个角度。实际应用中,比如机器人、机械臂可以用舵机来控制关节,遥控车、遥控船可以用舵机来控制方向。这里的PWM波形实际上是作为一个通信协议来使用的,与用PWM波形等效出一个模拟输出的关系不大(前面讲了,等效模拟必须是以惯性连续为前提),将PWM当成一个通信协议也是一个比较常见的应用,因为很多控制器都有PWM输出的功能,而且PWM只需要一根信号线就行了,这也是一种应用形式。
接下来,看一下舵机的硬件电路,上图第一个是引脚定义图,在舵机上有三根线,分别是黑(电源负极GND)、红(电源正极+5V)、黄(PWM信号线)。上图第二个图中,在实际应用中,GND就接GND,电源+5V是电机的驱动电源(一般电机都是大功率设备,它的驱动电源也必须是一个大功率的输出设备,如果能像这样单独提供供电,那就再好不过了,如果不能,那也要注意电源的功率是不是能达标,如果单独供电的话,供电的负极要和STM32 共地,然后正极接在5伏供电引脚上,对于我们套件的话,可以直接从STLINK的5v输出脚引一根线使用USB的5V供电,也是带得动的),信号线PWM就直接接到STM32引脚上就行了,舵机内部是有驱动电路的,PWM只是一个通信线,是不需要大功率的。
直流电机及驱动
可以用PWM来控制电机的速度。直流电机是一个单独的电机,里面是没有驱动电路的,所以我们就要外挂一个驱动电路来控制。直流电机是一种能将电能转换为机械能的装置。有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。上图最左所示的电机是130直流电机(本实验)。
直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作。本课程使用TB6612电机驱动芯片来驱动电机。
电机驱动电路同样也是一个研究课题,市面上也有很多的电机驱动可供选择,常见的电机驱动芯片有TB6612、DRV8833、L9110、L298N等,另外还有用分立元件MOS管搭建的驱动电路,它可以实现更大的驱动功率。当然也可以自己用MOS管设计电路。
本课程就直接使用TB6612这款电机驱动芯片来驱动电机的,使用现成的芯片还是挺方便的,别人都把电路设计好集成在芯片里了,我们直接拿来用就行了。TB6612是一款双路H桥型的直流电机驱动芯片,其中有两个驱动电路,可以独立地驱动两个直流电机并且控制其转速和方向。如上图中间,是电机驱动板,芯片是TB6612,外围电路只需三个滤波电容就行了,可见这个芯片的集成度还是非常高的,基本上不需要我们在加什么东西。如上图右侧是H桥电路的基本结构,是由两路推挽电路组成的,比如左边上管导通,下管断开,那左边输出就是接在VM的电机电源正极;下管导通,上管断开,那就是接在PGND的电源负极;如果有两路推挽电路,中间接一个电机,左上和右下导通,电流就是从左流向右,右上和左下导通,电流的方向就反过来从右边流向左边,H桥可以控制电流流过的方向,所以它能控制电机的正反转。大概了解一下就行,应用的话不用管那么多。
TB6612电机驱动模块的连接电路图和引脚定义图如下所示:
如上图
- VM就是电机电源的正极,和舵机的电源要求是一样的,要接一个可以输出大电流的电源,电压和电机的额定电压保持一致,比如时5v的电机就接5v电压。
- VCC是逻辑电平输入端,一般和控制器的电源保持一致。比如使用STM32,是3.3v的器件,就接3.3v,如果是51单片机是5V的器件,那就接5V,这个引脚并不需要大功率,所以可以和控制器共用一个电源。
- 第三个引脚是GND,这个就接系统的负极就行了。
- 然后板子在右边还有两个GND,这个都是一样的引脚,在板子内部都是连通的,随便选一个GND用就可以。
- AO1\AO2\BO1\BO2是两路电机的输出,可以分别接两个电机。AO1和AO2就是A路的两个输出,它的控制端是上面的三个PWMA、AIN1和AIN2,这三个引脚控制下面的A路的一个电机,对应关系如上图的灰色填充,其中PWMA引脚要接PWM信号输出脚PA0,AIN1和AIN2引脚可以任意接两个普通的GPIO口,这三个引脚给一个低功率的控制信号,驱动电路就会从VM汲取电流来输出到电机,这样就能完成低功率的控制信号控制大功率设备的目的。右边的BO1及BO2这一路也是和A路的功能和操作方法是完全一样的。
- STBY引脚意为Stand By,为待机控制引脚。如果接GND,芯片就不工作,处于待机状态。如果接到逻辑高电平VCC(3.3V)芯片就正常工作。如果不需要待机模式的话可以直接接VCC3.3v,如果需要控制可以接入任意一个GPIO口,给高低电平就可以进行控制。
PWMA、AIN1和AIN2三个引脚是怎么控制电机正反转和速度?如上图下面的表,
- STBY低电平就待机,高电平就正常工作。
- 如果IN1和IN2全接高电平,两个输出O1和O2都为低电平,这样两个输出就没有电压差,电机是不会转的(制动),全高或全低,电机都不会转
- 如果IN1和IN2全接低电平,两个输出O1和O2直接关闭,电机也是不会转的
- 如果IN1给低电平、IN2接高电平,电机就是处于反转状态,电机就是处于反转状态,那转还是不转呢,要取决于PWM,如果PWM给高电平,那输出就是一低一高,有电压差了,电机可以转,这时候定义的是反转,开始转了;如果PWM给低电平,那输出两个低电平,电机还是不转(制动),这就是反转的逻辑(IN1给低IN2给高,PWM高转低不转)。如果PWM是一个不断翻转的电平信号,那电机就是快速的反转、停止、反转、停止,如果PWM频率足够快,电机就可以连续稳定的反转,并且速度取决于PWM信号的占空比,这就是反转的工作流程。在这里的PWM就是使用PWM来等效一个模拟量的功能。
- 正转和上面差不多。IN1给高IN2给低,PWM高正转、低不转。如果PWM频率足够快,电机就是连续稳定地正转了,并且速度取决于PWM信号的占空比。
参考手册
强制输出模式,就是CNT和CCR无效,REF强制为高和低的那两种模式
输出比较模式就是CNT等于CCR时,REF是冻结、置高、置低、还是翻转的那四种模式
最后PWM这个比较重要,就是CNT大于或小于CCR,REF置高或置低的那两种模式。
更多细节问题大家可以再详细研究一下手册,另外看这个手册的时候,可能他的语言会比较晦涩难懂,而且它这里面经常会像这样引用一大堆寄存器的名字,所以看的时候还要多对照寄存器介绍来理解这些寄存器的解释,也都是很重要的内容,把功能描述和寄存器描述都对照看看,这样才好懂一些。
示例程序(PWM驱动LED呼吸灯&PWM驱动舵机&PWM驱动直流电机)
PWM驱动LED呼吸灯
现象:在PA0端口接入LED,LED在不断地变换亮度,实现了一个呼吸灯的效果。
接线图如下:注意LED是正极接在PA0引脚,负极接在GND的驱动方法,这样就是高电平点亮,低电平熄灭,这是正极性的驱动方法,这样的话观察更直观一点,就是占空比越大LED越亮,占空比越小LED越暗 。
上一小节我们已经把pwm的整个通路都讲完了,现在我们只需要把这些模块都打通,那就可以输出PWM了。
pwm初始化函数基本步骤
第一步,RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
第二步,配置时基单元,包括时钟源选择和时基单元都配置好
第三步,配置输出比较单元,里面包括这个CCR的值、输出比较模式、极性选择、输出使能这些参数,这些参数在库函数里也是用结构体统一来配置的
第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,PWM和GPIO的对应关系可以参考引脚定义表
第五步,运行控制,启动计数器,这样就能输出PWM了
OC输出比较相关库函数
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函数最重要,其他的了解即可)
// 单独设置输出比较的极性(带N的是高级定时器中互补通道的配置,OC4没有互补通道,所以就没有OC4N的函数)
// 在这里可以设置输出极性,在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电平取反,或者说是有效电平为低电平
第一步,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来说,就是下图对应引脚
最终这个PWM波形肯定是要借用一下GPIO口才能输出,那这个STM32的OC1通道是借了哪个GPIO口呢,我们可以打开这个引脚定义表,默认复用功能这一列就是片上外设的端口和GPIO的连接关系,在这里可以看到有TIM2_CH1_ETR,它是在PA0这一行的,这就说明TIM2的ETR引脚和通道1的引脚,都是借用了PA0这个引脚的位置的,换句话说就是TIM2的引脚复用在了PA0引脚上,所以说如果我们要使用TIM2的OC1也就是CH1通道输出PWM,那它就只能在PA0的引脚上输出,而不能任意选择引脚输出,这个关系是定死的,不能任意更改。
你要使用哪个外设,就只能用对应的引脚,不过,但是虽然它是定死的,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)
参数说明:
重映射:
根据你所要重映射的引脚,在下图找到所需要的模式,比如:如果我们想把PA0改到PA15,就可以选择这个部分重映射方式1,或者完全重映射。
PA15在引脚定义图里没有加粗,因为它上电后已经默认复用为了调试端口JTDI,所以如果想让他作为普通的GPIO或者复用定时器的通道。那还需要先关闭调试端回的复用,也是用这个GPIO PinRemapConfig函数
如果我们需要用PA15、PB3、PB4这三个引脚当做GPIO来使用的话,那通常就是解除JTAG的复用,保留SWD的复用,那就加一下下面的第一句和第三句,先打开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
void pwm_init(void)
{
//1.打开时钟,选择内部时钟
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//引脚重映射功能,定时器通道1 PA0引脚重映射到PA15,将下面初始化结构体GPIO_InitStructure引脚改为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);//在TIM2的OC1通道上就可以输出PWM波形了,但最终要借用一下GPIO口才能输出,查看引脚定义为PA0
//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的通道1)【看数据手册,里面的引脚定义表,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度的位置,2500对应180度,1500对应90度
//建立一个舵机模块,封装函数。调用函数就能变为对应的角度,舵机设置角度,参数是0到180度
while(1)
{
keynum = key_getnum();
if(keynum == 1)
{
angle += 30;
if(angle > 180)
{
angle = 0;
}
}
serve_setangle(angle); //舵机设置角度 按键按一下,旋转30度
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同时跳变,所以它们的相位是同步的,这就是同一个定时器不同通道输出PWM的特点,如果驱动多个舵机或者直流电机,那使用一个定时器不同通道的pwm就完全可以了
/*
上面这个结构体的参数就会配置到通道2的
当然如果你通道1和通道2都想要用的话
那就直接在这里加两行代码
把通道1和通道2都初始化了
这样就能同时使用两个通道来输出两个PWM了
同理通道3和通道4也是可以使用的
*/
//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驱动直流电机
就是小时候玩的赛车里的那种电机
- 这里红色的是TB6612电机驱动模块,第一个引脚VM是电机电源,接在STLINK的5v引脚
- 第二个VCC逻辑电源,接在面包板3.3v正极
- AO1和AO2是电机输出端接电机的两根线,接线不分正反,对调两根线,电机的旋转方向就会反过来,然后右边这里是另一路的驱动,如果你需要接两个电机,那就在右边再接一个电机,如果只需要一个电机,那就随便选一路,另一路空着就行
- STBY是待机控制脚,不需要待机,直接接逻辑电源正3.3v
- 控制引脚 AIN1和AIN2是方向控制,任意接两个GPIO就可以,这里接PB4和PB5两个脚
- 控制引脚 PWMA是速度控制,需接PWM的输出脚,接PA2引脚,PA2对应的是TIM2的通道3(我们又换了一个通道)
- 这里还接了一个按键,接PB1口用于控制
电机旋转会发出蜂鸣器的声音,加大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); //使用的是地址传递
}
//设置速度的函数 -100 ~ 100
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前加负号
}
}
有很多地方可以改变极性,比如最简单的电机两根线反过来接,或者输入的AIN1和AIN2反过来,在程序里也可以,把这里的GPIO_ResetBits和GPIO_SetBits反过来,这都可以改变极性。
TIM输入捕获
输入捕获简介
输入捕获对于PID控制算法很重要,没有输入捕获就不能完成闭环控制,要做平衡车的一定要认真学
输入捕获就是这左边一部分电路,那右边就是我们之前讲过的输出比较部分,四个输入捕获和输出比较通道,共用四个CCR寄存器,另外它们的CH1到CH4,4个通道的引脚也是共用的,所以对于同一个定时器输入捕获和输出比较,只能使用其中一个不能同时使用。
输入捕获,即Input Capture,英文缩写为IC。输入捕获模式下,当通道输入引脚出现指定电平跳变的瞬间(程序里可以定义为上升沿、下降沿),当前CNT的值将被锁存到CCR中,就是把当前CNT的值读出来写入到CCR中去(检测电平跳变,然后执行动作(左边这四个是边沿信号输入引脚,一旦有边沿比如说上升沿,那这一块输入滤波和边缘检测电路就会检测到这个上升沿,让输入捕获电路产生动作,作用和外部中断差不多,只不过外部中断执行的动作是向CPU申请中断,输入捕获执行的是控制后续电路,让当前CNT的值锁存到CCR寄存器中)),可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。在这里,脉冲间隔和频率差不多、电平持续时间和占空比也是互相对应的关系。
每个高级定时器和通用定时器都拥有4个输入捕获通道,且二者没有区别。基本定时器没有输入捕获的功能。
输入捕获模块可以配置为PWMI(PWM输入模式)模式和主从触发模式。PWMI模式是PWM的输入模式,专门用来同时测量PWM波形的频率和占空比的。主从触发模式可以实现对频率或占空比的硬件的全自动测量。PWMI模式和主从触发模式设计得非常巧妙,把这两个功能结合起来,测量频率和占空比就是硬件全自动执行,软件不需进行任何干预,也不需进中断,需要测量的时候,直接读取CCR寄存器就行了,使用非常方便且极大地减轻了软件的压力。
如下图,左边为输入捕获电路,4个输入捕获和输出比较通道,共用4个CCR寄存器,另外它们的CH1到4的4个通道引脚也是共用的,所以对于同一个定时器,输入捕获和输出比较只能使用其中一个,不能同时使用。
输入捕获对比输出比较:
- 输出比较,引脚是输出端口,根据CNT和CCR的大小关系来执行输出动作
- 输入捕获,引脚是输入端口,接收到输入信号执行CNT锁存到CCR的动作
频率的测量方法
如上图是频率逐渐降低的方波波形,越往左频率越高,越往右频率越低,这里信号都是只有高低电平的数字信号,对于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
那这两种方法都有什么区别呢?
首先测频法适合测量高频信号,测周法适合测量低频信号,这个从图里也可以看出来,测频法在闸门时间内最好要多出现一些上升沿,计次数量多一些,这样有助于减小误差哈,假如你定了一秒的闸门时间,结果信号频率非常低,一秒的时间才只有寥寥无几的几个上升沿,甚至一个上升沿都没有,那你总不能认为频率是零吧,在计次N很少时,误差会非常大,所以测频法要求信号频率要稍微高一些
。那对于侧周法就要求信号频率低一些了,低频信号周期比较长,计次就会比较多,有助于减小误差
否则的话,比如标准频率fc为一兆赫兹,待测信号频率太高,比如待测信号500 千赫兹,那在一个周期内只能记一两个数啊,如果待测信号再高一些,甚至一个数也记不到,那总不能认为频率无穷大吧
所以测周法需要待测信号频率低一些,然后是测频法测量结果更新的慢一些,数值相对稳定,测周法更新的快,数据跳变也非常快。
从原理上看,测频法自带一个均值滤波的功能,如果在闸门时间 T内被测频率有变化,测频法得到的实际是这一段闸门时间内的平均频率,如果咱们时间选为一秒,那么每隔一秒才能得到一次结果,所以测频法结果更新慢,测量结果是一段时间的平均值,值比较平滑;反观测周法,它只测量一个周期就能出一次结果,所以出结果的速度取决于待测信号的频率,一般而言待测信号都是几百几千赫,所以一般情况下测周法结果更新更快,测周法只测量一个周期,故其结果会受噪声的影响,波动会比较大。
对于测频法和测周法的一个共同点是:N越大,误差就越小。在两种方法中,计次都可能会产生正负1误差。在测频法的一个闸门时间内,并不是每一个被测信号的周期都是完整的,比如在最后时间里
可能有一个周期刚出现一半,咱们时间就到了,那这只有半个周期只能舍弃掉,或者当作一整个周期来看,因为g是只有整数,不可能计次0.5个数吧,那在这个过程就会出现多记一个或者少记一个的情况,这就叫做正负1误差;测周法的标准计数信号的信号也不一定是被测信号的整数倍,所以它也不一定是每一个都完整的。对于上述的两种情况,都会出现多计一个数或者少计一个数的情况,所以会产生正负1误差。要想减小正负1误差的影响,就只能尽量多记一些数,当计值n比较大时,正负1对n的影响就会很小。
刚才我们说了,高频适合使用测评法,低频适合使用测周法,那多高算高频,多低算低频,我们有以下一个参数来考量:中界频率,测频法和测周法误差相等的频率点。由于两种方法的误差都与N的正负1误差有关,所以当两种方法计次的N相同时,两种方法的误差也就相同。消去两种方法公式中的N,将测频法和测周法的N提出来,令两个方法N相等,将fx解出来,可得:
式中,T 是测频法的闸门时间,f c是测频法的标准频率。
当待测信号频率小于中界频率时,测周法误差更小,选择测周法更合适;当待测信号频率大于中界频率时,测频法误差更小,选择测频法更合适。
2.4用STM32来实现测频法和测周法
测频法,之前学过的外设可以实现,对射式红外传感器计次、定时器外部时钟,这些代码稍加改进就是测频法,比如对射式红外传感器计次,每来一个上升沿计次+1,再用一个定时器,定一个1s的定时中断,在中断里,每隔1s取一下计次值,同时清0,为下一次做准备,这样每次读取的计次值就直接是频率;对应定时器外部时钟的代码,也是如此,每隔1s取一下计次,就能实现测频法测量频率的功能了。
本节输入捕获测频率,使用的方法是测周法。就是测量两个上升沿之间的时间,来进行频率测量。
输入捕获的各部分电路
从左向右依次进行电路分析:
- 最左边是四个通道的引脚,参考引脚定义表,就能知道引脚是复用在哪个位置
- 然后引脚进来,有一个三输入的异或门,这个异或门的输入接在了通道1、2、3端口,异或门的执行逻辑是,当三个输入引脚的任何一个有电平翻转时,输出引脚就产生一次电平翻转,3个引脚电平都相同为0,3个引脚中有高有低为1。
然后输出通过数据选择器,到达输入捕获通道1,数据选择器如果选择上面一个,那输入捕获通道1的输入就是三个引脚的异或值;若选择下面一个,异或门就没有用到,四个通道各用各的引脚。设计异或门,其实还是为了三相无刷电机服务的,无刷电机有三个霍尔传感器检测转子的位置,可以根据转子的位置来进行换相。有了这个异或门就可以在前三个通道接上无刷电机的霍尔传感器,然后这个定时器就作为无刷电机的接口定时器,去驱动换相电路工作,这个了解一下我们本节暂时不涉及。
- 输入信号来到了输入滤波器和边沿检测器(极性选择)。输入滤波器可以对信号进行滤波,避免一些高频的毛刺信号误触发;边沿检测器就是和外部中断一样,可以选择高电平触发或者低电平触发,当出现指定的电平时,边沿检测电路就会触发后续电路执行动作。这里设计了两套滤波和边沿检测电路,第一套电路经过滤波和极性选择得到TI1FP1,输入给通道1的后续电路。第二套电路,经过另一个滤波和极性选择,得到TI1FP2,输入给通道2的后续电路,同理,下面TI2信号进来,也经过两套滤波和极性选择得到TI2FP1输入通道1和TI2FP2输入通道2,其中TI2FP1输给上面,TI2FP2输入给下面,在这里两个信号进来可以选择各走各的,也可以选择进行一个交叉。可以进行交叉连接,例如CH1引脚输入给通道2,CH2引脚输入给通道1,进行交叉连接的目的是两个:1.第一个目的可以灵活切换后续捕获电路的输入,比如你一会儿想以CH1作为输入,一会儿想以CH2作为输入,这样就可以通过这个数据选择器灵活的进行选择。2.第二个目的,也是它交叉的主要目的,就是可以把一个引脚的输入同时映射到两个捕获单元,这也是PWMI模式的经典结构,等会讲PWMI模式的时候,你就知道这样设计的好处了,第一个捕获通道使用上升沿触发用来捕获周期,第二个通道使用下降沿触发用来捕获占空比,两个通道同时对一个硬件进行捕获,就可以同时测量频率和占空比,这就是PWMI模式,等会儿再来继续分析啊,那先回到这里,一个通道灵活切换两个引脚和两个通道同时捕获一个引脚,这就是这里交叉一下的作用和目的,同样下面通道三和通道四也是一样的结构,另外这里还有一个TRC信号,也可以选择作为捕获部分的输入,这个TRC信号是来源于这里的(上方),这样设计也是为了无刷电机的驱动哈,这个知道一下就行了,我们暂时不用。
- 输入信号进行滤波和极性选择后就来到了预分频器,每个通道各有一个预分频器,可以选择对前面的信号进行分频,分频之后的触发信号,就可以触发捕获电路进行工作了,每来一个触发信号,CNT的值就会向CCR转运一次,转运的同时会发生一个捕获事件,这个事件会在状态寄存器置标志位,同时也可以产生中断,如果需要在捕获的瞬间,处理一些事情的话,就可以开启这个捕获中断,比如可以配置上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为CNT计数器是由内部的标准时钟驱动的,所以CNT的数值可以用来记录两个上升沿之间的时间间隔(周期),再取倒数就是测周法测量的频率了,在每次捕获后将CNT清零(可以用主从触发模式来自动完成CNT清零),这样下次上升沿再捕获的时候,取出的CNT才是两个上升沿的时间间隔。
到这里输入捕获电路的执行流程和测频率的原理,我们已经大概了解了,接下来就是执行细节的问题
把电路执行的细节都了解清楚,这样写程序的时候才能得心应手。
输入捕获通道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的清零工作。
在这里硬件电路就可以在捕获之后自动完成CNT的清0工作,如何自动清0 CNT呢,看一下这里这个TI1FP1信号和TI1的边缘信号,都可以通向从模式控制器,比如TI1FP1信号的上升沿触发捕获,那通过这里TI1FP1还可以同时触发从模式,这个从模式里面就有电路可以自动完成CNT的清零,所以可以看出这个从模式就是完成自动化操作的利器。
输入捕获的主模式、从模式、触发源选择(简称:主从触发模式)
主从触发模式有什么用,如何来完成硬件自动化的操作,我们看一下,主从触发模式这个名字是江科大老师起的,手册里并没有这个描述。
主从触发模式,即主模式、从模式和触发源选择三个功能的简称。主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其他外设的操作;从模式可以接收其他外设或自身外设的一些信号,用于触发自己的一些操作(定时器的运行);触发源选择,即选择从模式的触发信号源功能,也可以认为它是从模式的一部分。
在从模式下,可以通过触发源选择功能选择一个信号产生TRGI信号,之后去触发从模式,从模式可以在上面列表中选择一项操作来自动执行。如果想完成我们刚才说的任务,想让TI1FP1信号自动触发CNT清零 ,那触发源选择就可以选中这里的TI1FP1,从模式执行的操作就可以选择执行reset的操作
这样TI1FP1的信号就可以自动触发从模式,从模式自动清零CNT,实现硬件全自动测量。
关于主从模式的详细说明可以参见手册:
比如你想实现定时器的级联,就可以选择一个定时器主模式输出更新信号到TRGO,另一个定时器选择上一个定时器触发从模式,从模式选择执行外部时钟模式一的操作,这样就能实现定时器的级联了
还有其他很多高级的功能都可以用主从触发模式来实现,使用非常灵活。
然后主模式还可以选择复位、使能、比较脉冲和四个OCREF信号,作为TIGO的输出,具体描述可以看看这里的文字,这些东西不用特别去记,知道有这个功能就行,需要用的时候再来看看。
然后主模式还可以选择复位、使能、比较脉冲和四个OCREF信号,作为TIGO的输出,具体描述可以看看这里的文字,这些东西不用特别去记,知道有这个功能就行,需要用的时候再来看看。
继续往下,这里有从模式触发源的可选信号,可以选择这些信号去触发从模式,触发从模式后可以执行哪些操作呢,
继续看从模式选择,可以执行这么多的操作,比如我们本节会使用到复位模式,执行的操作就是选中触发输入的上升沿重新初始化计数器,是不是就是清零CNT的意思啊,这就是从模式。
总结下来就是PPT这三个图,主模式、触发源选择、从模式,在库函数里也非常简单哈,这三块东西就对应三个函数,调用函数,给个参数就行了,这些就是主从触发模式的内容。
最后理一下思路,把之前的东西组合在一起,得到下面这两个图,这两个图也分别对应了我们演示两个代码的逻辑。
先看第一个图,输入捕获基本结构,这个结构我们只使用了一个通道,所以它目前只能测量频率,在右上角这里是时基单元,我们把时基单元配置好,启动定时器,那这个CNT就会在预分频之后的这个时钟驱动下不断自增,这个CNT就是我们测周法用来计数计时的东西,经过预分频之后,这个位置的时钟频率就是驱动CNT的标准频率fc。
这里不难看出来,标准频率等于72MHz/预分频系数,然后下面输入捕获通道一的GPIO口,输入一个图示绿色的方波信号,经过滤波器和边沿检测,选择TI1FP1为上升沿触发,之后输入选择直连的通道
分频器选择不分屏,当TI1FP1出现上升沿之后,CNT的当前记录值转运到CCR1里,同时触发源选择选中TI1FP1为触发信号,从模式选择复位操作,这样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模式使用两个通道来捕获频率和占空比的思路。另外也可以两个通道同时捕获第一个引脚的输入,这样通道二的前面这一部分就没有用到行,当然也可以配置两个通道同时捕获第二个引脚的输入,这样我们就是使用TI2FP1和TI2FP2这两个引脚了,这两个输入可以灵活切换。
手册
手册本节相应的内容,描述是寄存器的方式,不太好理解,需要结合上面笔记内容进行进一步理解,就比较容易理解了。
上图就是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
示例程序(输入捕获模式测频率&PWMI模式测频率和占空比)
目前我们这个测量信号的输入引脚,是PA6信号,信号从PA6进来,待测的PWM信号也是STM32自己生成的,输出引脚是PA0,所以接线这里直接用一根线,把PA0引到PA6就行了,如果你有信号发生器的话,也可以设置成方波信号输出,高电平3.3V,低电平0V,然后直接接到PA6。
目前我们要借用一下之前写好的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的时钟打开
第二步,GPIO初始化,把GPIO配置成输入模式(一般选择上拉输入或浮空输入模式)
第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行,和之前代码一样
第四步,配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用一个结构体就可以统一进行配置了
第五步,选择从模式的触发源,触发源选择为TI1FP1,这里调用一个库函数给一个参数就行了
第六步,选择触发之后执行的操作,执行Reset操作,这里调用一个库函数就行了
最后,当这些电路都配置好之后,调用TIM_Cmd函数,开启定时器。这样所有的电路就能配合起来了,按照我们的要求工作了。当我们需要读取最新一个周期的频率时,直接读取CCR寄存器,然后按照fc/N,计算一下就行了,这就是整个程序的思路。
第一步,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 TIM3_CH1(引脚图)
//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;//配置输入捕获的滤波器,数越大,滤波效果越好,每个数值对应的采样频率和采样次数在参考手册里有,若信号有毛刺和噪声就可以增大滤波器参数可以有效避免干扰
//注意滤波器和分频器的区别,虽然他俩都是计次的东西,但是滤波器计次,并不会改变信号的原有频率,一般滤波器的采样频率都会远高于信号频率,所以它只会滤除高频噪声,使信号更平滑
//1000Hz滤波之后仍然是1000Hz,信号频率不会变化,而分频器就只对信号本身进行计次,自然会改变频率,1000Hz二分频之后就是500Hz
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的值)
// 这里加1,否则结果就如下图所示,频率多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参数传进去,第三个参数是重装模式(还是影子寄存器、预装载这个问题,就是写入的值是立刻生效还是在更新事件生效;立刻生效可能会在值改变时产生切断波形的现象会出现不完整的周期,更新事件生效就是会有一个缓存器,延迟参数的写入时间,等一个周期结束了,在更新事件时,再统一改变参数,保证每个周期的完整)
// 这里选立刻生效
}
PWMI模式测频率占空比
在输入捕获模式测频率做修改
需要将输入捕获初始化的部分,进行一下升级,配置成两个通道同时捕获同一个引脚的模式,怎么配置呢?
两种方法:
第一种,把这个通道初始化的部分,复制一份,这个结构体定义的不要复制了。然后呢,通道1是直连输入,上升沿触发,沿用这个配置。接着下面,通道1改成通道2,直连输入,改成交叉输入,上升沿触发,改成下降沿触发,这样看一下,是不是就对应我们PPT的这个结构了。通道1,直连输入,上升沿触发;通道2,交叉输入,下降沿触发,这样就可以了。
第二种:ST公司怕我们麻烦,库里专门封装了函数,快捷完成配置。只针对于通道1和通道2
写一个获取占空比的函数,根据上一小节的分析,高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里,我们用CCR2/CCR1,就能得到占空比了
CCR总少1,应该是CCR从0开始计数的
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参数传进去,第三个参数是重装模式(还是影子寄存器、预装载这个问题,就是写入的值是立刻生效还是在更新事件生效;立刻生效可能会在值改变时产生切断波形的现象会出现不完整的周期,更新事件生效就是会有一个缓存器,延迟参数的写入时间,等一个周期结束了,在更新事件时,再统一改变参数,保证每个周期的完整)
}
最后,我们来研究一下这个测频率的性能。
首先是测频率的范围,目前我们给的标准频率是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的晶振不是那么准,在计次几百几万次之后,误差累积起来,也会造成一些影响。
当然目前我们这个现象还是自己测量自己,不存在晶振误差,所以数值还是非常稳定的,如果你要测量别的信号,那数值可能就会有些抖动了,后期可以再做一些滤波处理。
TIM编码器接口
实验现象:
现象:接了一个旋转编码器模块,这个代码和之前我们写的旋转编码器计次的代码,实现的功能基本都是一样的。目前我们这个代码,本质上也是旋转编码器计次,只不过这个代码是通过定时器的编码器接口,来自动计次。而我们之前的代码是通过触发外部中断,然后在中断函数里手动进行计次,使用编码器接口的好处就是节约软件资源。
如果使用外部中断来计次,那当电机高速旋转时,编码器每秒产生成千上万个脉冲,程序就得频繁进中断,然后进中断之后,完成的任务又只是简单的加—减一,是不是我们的软件资源就被这种简单而又低级的工作给占用了。所以,对于这种需要频繁执行,操作又比较简单的任务,一般我们都会设计一个硬件电路模块,来自动完成。那我们本节这个编码器接口,就是用来自动给编码器进行计次的电路。如果我们每隔一段时间取一下计次值,就能得到编码器旋转的速度了。
我们旋转试一下,向右慢速旋转,数值为正,计次比较小,向右快速旋转计次就会增大,然后向左慢速旋转,数值为负,计次比较小,向左快速旋转计次也是负向增大,然后停下来速度就是零,这就是编码器测速的实验现象,使用定时器的编码器接口啊,再配合编码器就可以测量旋转速度和旋转方向了哈,这里编码器测速一般应用在电机控制的项目上,使用pwm驱动电机,再使用编码器测量电机的速度,然后再用PID算法进行闭环控制,这是一个比较常见的使用场景哈,一般电机旋转速度比较高,会使用无接触式的霍尔传感器或者光栅进行测速哈,我们这里为了方便就是用这个触点式的旋钮编码器来演示,电机旋转呢就用人工旋转来模拟,当然实际使用的话,这个旋钮编码器和电机的霍尔光栅编码器都是一样的效果。
什么是正交编码器呢,等会儿我们会介绍,就是像这样输出的两个方波信号相位相差90度,超前90度或者滞后90度,分别代表正转和反转,这就是正交编码器。
最终的实验现象就是一个编码器,它有两个输出,一个是A相,一个是B相,然后接入到STM32定时器的编码器接口,编码器接口自动控制定时器时基单元中的CNT计数器进行自增或自减,比如初始化之后,CNT初始值为零,然后编码器右转CNT就++,右转产生一个脉冲,CNT就加一次,比如右转产生十个脉冲后停下来,那么这个过程CNT就由0自增到10停下来,编码器左转CNT就–,左转产生一个脉冲,CNT减一次,比如我编码器在左转产生五个脉冲,那CNT就在原来十的基础上自减五停下来
这个编码器接口啊,其实就是相当于一个带有方向控制的外部时钟,它同时控制着cnt的计数时钟和计数方向,这样的话CNT的值就表示了编码器的位置,如果我们每隔一段时间,取一次CNT的值,再把CNT清零,是不是每次取出来的值就表示了编码器的速度哈,借用一下上一小节测频法和测周法的知识点,这个编码器测速实际上就是测频法测正交脉冲的频率啊,然后每隔一段时间取一次计次,这就是测评法的思路,只不过这个编码器接口计时更高级,它能根据旋转方向,不仅能自增计次,还能自减计次,是一个带方向的测速,以上就是编码器接口的工作流程了。
每个高级定时器和通用定时器都拥有一个编码器接口,这个编码器接口的资源还是比较紧张的,如果一个定时器配置成了编码器接口模式,那它基本上就干不了其他活了,我们这个C8T6芯片只有TIM1、2、3、4四个定时器,所以最多只能接四个编码器,而且接完四个编码器就没有定时器可以用了,所以如果你编码器比较多的话,需要考虑一下这个资源够不够用,不过实在不行的话,你还是可以用外部中断来接编码器的,这样就是用软件资源来弥补硬件资源了哈,所以这里也可以看出硬件资源和软件资源是互补的,硬件资源越多,软件就会越轻松,硬件不够呢那就软件来凑,比如pwm我可以直接来个定时中断,然后在中断里手动计数手动翻转电平,比如输入捕获,我可以来个外部中断,然后在中断里手动把CNT取出来,放在变量里,比如编码器接口,我也可以来外部中断,然后在中断里手动自增或自减计数,这都可以实现功能,怎么输出比较啊,输入捕获啊,编码器接口啊都不需要
但是这样就是消耗软件资源了,所以一般有硬件资源的情况下,我们可以优先使用硬件资源,这样节约下来的软件资源可以去干更重要的事情。
编码器接口的两个输入引脚,借用了输入捕获的通道一和通道二,这个等会从结构框图就可以看出来
编码器的两个输入引脚就是每个定时器的CH1和CH2引脚,CH3和CH4不能接编码器,这些就是编码器接口的简介了。
我们接下来来看一下正交编码器
正交编码器一般可以测量位置或者带有方向的速度值哈,它一般有两个信号输出引脚,一个是A相,一个是B相,编码器的样子和结构呢,我们之前外部中断这里也介绍过哈,我们大概回顾一下,这里看
一下图片:
第一个是只有一个光栅加红外对管的编码器,这只能输出一个方波信号,并不是正交编码器啊,第二个图就是我们套件使用的编码器里面靠两个金属触点交替导通,可以输出A相和B相两个正交信号,是正交编码器啊,这里有四个硬件,上面两个是供电的正极和负极,下面两个是A相和B相的输出,第三个图是电机后面自带的一个编码器,电机旋转带动中间的磁铁旋转,两个霍尔传感器90度放置,最终输出A相和B相两个正交信号,是正交编码器,下面这里一般有六根线,最左和最右是直接接到电机的
然后是靠里一些的两根线是编码器电源,最中间的两根就是A相和B相的输出了,接着最后一个图是单独的编码器软件,一般都是正交编码器,当然也有的不是这个要看清商品的说明,接线的引脚一般有六个,两个是编码器电源,两个是A相和B相,一般还有一个编码器零位置的输出也就是Z相,零位置就是编码器每转到一个固定位置时,输出一个脉冲,一般应用于位置常量哈,校准零位置用的,最后还有一个硬件,一般是空引脚没有用到,这就是常见的编码器外观和工作原理。
回到这里,当编码器的旋转轴转起来时,A相和B相就会输出这样的方波信号,转得越快,这个方波的频率就越高,所以方波的频率就代表了速度,我们取出任意一相的信号来测频率,就能知道旋转速度了,但是只有一相的信号无法测量旋转方向,因为无论正转还是反转,它都是这样的,方波想要测量方向,还必须要有另一根线的辅助,比如我可以不要这个B相,再定义一个方向输出角,正转置高电平
反转置低电平,这是一种解决方案哈,但是这样的信号并不是正交信号,另一种解决方案就是我们本节所说的正交信号,当正转时A相提前B相90度,反转时A相滞后B相90度,当然这个正转是A相提前还是A相滞后,并不是绝对的,这只是一个极性问题,毕竟正转和反转的定义也是相对的,总之就是朝一个方向转,是A相提前,另一个方向是A相滞后。
那使用正交信号相比较单独定义一个方向引脚,有什么好处呢?
首先就是正交信号精度更高,因为A、B相都可以计次,相当于计次频率提高了一倍;其次就是正交信号可以抗噪声,因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路。如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那这时计次值是不会变化的。
看一下正交信号如何计次和区分旋转方向呢,首先我们观察一下波形的特点,在正转的时候,第一个时刻A相上升沿,对应B相此时是低电平,也就是表里的第一行,第二个时刻,B相上升沿对应A相高电平是表里的第三行,第三个时刻,A相下降沿对应B相高电平是表里的第二行,最后是B相下降沿对应A相低电平,是表里的第四行,再然后和第一个状态重复啊,所以在正转的时候,我们总结了右边这个表,当出现这些边沿时,对应另一项的状态是这四种,反转同理。
所以我们编码器接口的设计逻辑就是,首先把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,然后到底是增还是减呢,这个计数的方向由另一相的状态来确定。当出现某个边沿时,我们判断另一相的高低电平,如果对应另一相的状态出现在上面这个表里,那就是正转,计数自增;反之,另一相的状态出现在下面这个表里那就是反转,计数自减,这样就能实现编码器接口的功能了,这也是我们STM32定时器编码器接口的执行逻辑。
接下来,我们就来看一下这个定时器的框图,看一下这个编码器接口的电路是如何设计的。
编码器接口处于定时器的这个位置,高级定时器和通用定时器都是一样的,每个定时器都只有一个编码器接口,然后基本定时器是没有编码器接口的,我们来看一下,这里编码器接口有两个输入端,分别要接到编码器的A相和B相,然后这里是两个网络编号,分别写的是TI1FP1和TI2FP2,对应的就是下面TI1FP1,另一个是TI2FP2,可以看出这个编码器接口的两个引脚,借用了输入捕获单元的前两个通道,所以最终编码器的输入引脚,就是定时器的CH1和CH2这两个引脚,信号的通路是CH1通过这里通向编码器接口,CH2通过这里通向编码器接口,CH3和CH4与编码器接口无关,其中CH1和CH2的输入捕获滤波器和边缘检测编码器接口也有使用哈,但是后面的是否交叉预分频器和CCR寄存器与编码器接口无关,这就是编码器接口的输入部分,那编码器接口的输出部分啊,其实就相当于从模式控制器了,去控制cnt的计数时钟和计数方向,简单来说,这里的输出执行流程是按照我们之前总结的那个表,如果出现了边沿信号,并且对应另一相的状态为正转,则控制CNT自增,否则控制CNT自减
,注意在这里,我们之前一直在使用的72MHZ的内部时钟CK_PSC ,和我们在时机单元初始化时设置的计数方向并不会使用,因为此时计数时钟和计数方向都处于编码器接口托管的状态,计数器的自增和自减受编码器控制,这就是编码器接口的电路结构了。
然后我们看一下这里,我给出的一个编码器接口基本结构。
这个结构就比较清晰了,输入捕获的前两个通道,通过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)等等,这样就可以直接得到负数,非常方便,这就是我们读出数据得到负数的一个小技巧。
最后我们来看一些工作细节,和两个小例子。
那先看一下这里工作模式的这个表,这个表描述的就是我们刚才说的编码器接口的工作逻辑哈,这里TI1FP1和TI2FP2接的就是编码器的A、B相,在A相和B相的上升沿或者下降沿,触发计数,到底是向上计数还是向下计数,取决于边沿信号发生的这个时刻,另一项的电平状态,也就是这里的相对信号的电平,TI1FP1对应TI2FP2,TI2FP2对应TI1FP1,就是另一相电平的意思,然后在这里这个编码器还分了三种工作模式,分别是仅在TI1计数、仅在TI2计数、和T都计数,这三个模式是啥意思呢,我们回到这里看一下,我们目前总结的是这四种状态都是正转,都可以计次自增,下面这四种状态都是反转,都可以计次自减,这四种状态涉及了两个引脚,分别是A相上升沿、A相下降沿,B相上升沿、B相下降沿,如果这四种状态都执行自增和自减,就是A相和B相的边缘都记住,那就对应这里的第三种模式,TI1和TI2都计数,当然这里我们还可以忽略一些边缘啊,比如我们可以仅在A相的上升沿和下降沿自增或自减,而B相的这两个状态忽略掉,不执行计数,或者仅在B相的上升沿和下降沿计数
,A相的边缘不管它,这样是不是也可以实现功能啊,只不过是计次的精度低了些
总结一句话就是正转的计数都向上计数,反转的计数都向下计数,这是编码器接口执行的逻辑,然后上面这两种模式应该也好理解,就是指在一相的边缘技术,另一项的边沿忽略,和最下面这个模式都是一样的,一般情况下我们都会使用最下面这个模式,因为这个模式技术精度最高,上面这两个模式如果你有这个需求的话可以了解一下,这就是编码器接口的三种工作模式。
那接着我们再来看一下这个实例图,这个图里使用的就是两个引脚的边沿都计数的模式,执行的逻辑我放在这上面了,然后看一下这里是TI1和TI2的时序信号,下面是计数器值的变化情况,第一个状态
TI1上升沿TI2低电平,查表对应向上技术,所以这里计数器变高了一级,接着后面的这几个状态,大家可以查一下表,都是向上计数,这是正转没问题,接着后面这个地方展示的就是正交编码器抗噪声的原理了,在这里TI2没有变化,但是TI1却跳变了好几次,这不符合正交编码器的信号规律,正交信号两个输出交替变化,就像人走路一样,先迈左腿再迈右腿,在左腿,在右腿,这里就是右腿没动左腿连续走了好几步,显然这个左腿的动作是一个毛刺信号,而通过我们上面这个表的逻辑,就可以把这种一个腿没动,另一个腿连走好几步的噪声滤掉。
然后继续往右看,这里是反转的波形,对照上表都查一下,都是向下计数,这里计数值就下降,然后TI1不动,TI2多次跳变计数值也是来回摆动过滤噪声啊,最后是正转向上计数,这就是编码器接口的一个实例。
最后我们再来看一下这个实例,这个实例展示的是极性的变化对计数的影响,前面是两个引脚都不反向的图,后面这个是TI1反向,TI2不反向的图。
TI1反相是什么意思呢?
此时看下这个图,这里TI1和TI2进来,都会经过这个极性选择的部分。
在输入捕获模式下,这个极性选择是选择上升有效还是下降沿有效的。但是根据我们刚才的分析,编码器接口,显然始终都是上升沿和下降沿都有效的,上升沿和下降沿都需要计次,所以在编码器接口模式下,这里就不再是边沿的极性选择了,而是高低电平的极性选择。如果我们选择上升沿的参数,就是信号直通过来,高低电平极性不反转;如果选择下降沿的参数,就是信号通过一个非门过来,高低电平极性反转,所以这里就会有两个控制极性的参数,选择要不要在这里加一个非门,反转一下极性。
显然,这两个实例图的计数方向是相反的,这有什么作用呢?
比如你接一个编码器,发现它数据的加减方向反了,你想要正转的方向,结果它自减了,你想要反转的方向,结果它自增了,这时,就可以调整一下极性,把任意一个引脚反相,就能反转计数方向了。当然如果想改变计数方向的话,我们还可以直接把A、B相两个引脚换一下。
手册
示例代码(编码器接口)
这个接线和支线外部中断那一节的类似哈,我们把旋转编码器插在左边,VCC和GND接上电源正负极
下面的A相输出,我们接到PA6引脚,B相输出接到PA7引脚,这里PA6和PA7引脚可以交换一下,就是正转和反转的急性不一样而已,但是PA6和PA7这两个引脚不能随便更换,看一下引脚定义,PA6和PA7是TIM3的通道1和通道2,我们计划用TIM3接编码器,所以需要接在PA6和PA7这两个引脚。
第一步,RCC开启时钟,开启GPIO和定时器的时钟
第二步,配置GPIO,这里需要把PA6和PA7配置成输入模式
第三步,配置时基单元,这里预分频器我们一般选择不分频,自动重装一般给最大65535,只需要个CNT执行计数就行了
第四步,配置输入捕获单元。不过这里输入捕获单元只有滤波器和极性这两个参数有用,后面的参数没有用到,与编码器无关
第五步,配置编码器接口模式。这个直接调用一个库函数就可以了
最后,调用TIM_Cmd,启动定时器,就完事了,
电路初始化完成之后,CNT就会随着编码器旋转而自增自减,如果想要测量编码器的位置,那直接读出CNT的值就行了,如果想测量编码器的速度和方向,那就需要每隔一段固定的闸门时间取出一次CNT,然后再把CNT清零,这样就是测频法测量速度了。
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);//每隔一段时间读取一次;用定时中断
//Delay_ms(1000);//人手转的比较慢可以给个闸门时间1s,如果电机飞速转闸门时间就给短一点,提高速度刷新频率防止计数器溢出
//最好不要在主循环加入过长的Delay,这样会阻塞主循环的执行,最好使用下面的中断方法
}
}
//定时器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模式可以选择上拉、下拉或者浮空,上拉和下拉如何选择呢
我们一般可以看一下接在这个引脚的外部模块输出的默认电平,如果外部模块空闲默认输出高电平,我们就选择上拉输入默认输入高电平,如果外部模块默认输出低电平,我们配置下拉输入,默认输入低电平和外部模块保持默认状态一致,防止默认电瓶打架
这是上拉和下拉的选择原则,一般默认高电平是习惯的状态,所以一般上拉输入用的比较多,然后如果你不确定外部模块输出的默认状态,或者外部信号输出功率非常小,这时就尽量选择浮空输入,没有上拉电阻和下拉电阻影响外部信号
缺点就是当引脚悬空时没有默认电平了,输入就会受噪声干扰,来回不断的跳变,这就是三种输入模式的选择原则
*/
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; //电平极性为上升沿,这里的上升沿参数代表的是高低电平极性不反转;等会配置编码器接口的时候也有极性配置(下面第5步),属于重复配置,这个其实可以删掉;这里的上升沿并不代表上升沿有效,因为编码器接口始终都是上升沿、下降沿都有效
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)// 这里返回不要写成uint16_t,需要支持返回带负数的
{
//测速,在固定的匝门时间读一次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