第05章 TIM定时器
5.1 TIM定时中断
定时器的内容很多,本章共分为4个部分:第一部分主要讲定时器定时功能,也就是定一个时间、然后让定时器每隔这个时间产生一个中断,来实现每隔一个固定时间执行一段程序的目的,比如要做个时钟、秒表,或者使用一些程序算法的时候,都需要用到定时中断的这个功能。第二部分主要讲的是定时器输出比较的功能,输出比较这个模块最常见的用途就是产生PWM波形、用于驱动电机等设备,在这个部分,我们将会学习到,使用STM32输出的PWM波形,来驱动舵机和直流电机的例子。第三部分主要讲定时器输入捕获的功能,在这部分,我们将会学习到使用输入捕获这个模块来实现测量方波频率的例子。第四部分学习定时器编码接口,使用这个编码器接口,能够更加方便地读取正交编码器地输出波形,在编码电机测速中,应用也是非常广泛的。
5.1.1 TIM简介
(1)TIM(Timer)定时器。
(2)定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断。
定时器最基本的功能就是在定时触发中断,同时也可以看出来,定时器就是一个计数器,当这个计数器的输入是一个准确可靠的基准时钟的时候,那它在对这个基准时钟进行计数的过程,实际上就是计时的过程,比如在STM32中,定时器的基准时钟一般都是主频72MHz,如果对72MHz计72个数,那么1MHz就是1us的时间,如果计72000个数,那就是1KHz,也就是1ms的时间。
(3)16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时。
这里计数器就是用来执行计数定时的一个寄存器,每来一个时钟,计数器加1;预分频器可以对计数器的时钟进行分频,让这个计数更加灵活;自动重装寄存器就是计数的目标值,就是我们想要计多少个时钟申请中断。这些寄存器构成了定时器最核心的部分,我们把这一块电路称为时基单元。时基单元里面的计数器、预分频器、自动重装寄存器都是16位的。2的16次方是65536,也就是如果预分频器设置最大、自动重装也设置最大,那定时器的最大定m时时间就是59.65s,接近1分钟。这个怎么算的呢?就是72MHz/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多。如果觉得这个时间还不够长,STM32的定时器还支持级联的模式,也就是一个定时器的输出当作顶一个定时器的输入,这样加一起,最大定时时间就是59.65s再乘2次65536,这个时间就是八千多年,如果还觉得短,那就再级联一个定时器,定时时间再延长65536×65536倍,这个时间大概是34万亿年,可见指数爆炸的威力。
(4)不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。
定时器的这个基本结构是非常通用的,很多模块电路都能用到,所以STM32的定时器上扩展了非常多的功能。第一部分讲的就是定时中断和内外时钟源选择的功能;第二部分讲输出比较;第三部分讲输入捕获和主从触发模式;第四部分讲编码器接口。
(5)根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型。
5.1.2 定时器类型
类型 | 编号 | 总线 | 功能 |
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
除了TIM1到8,在库函数中还出现了TIM9、10、11等等,这些一般用不到。
基本定时器还可以和DAC联合使用。
这三种定时器是由高级到低级向下兼容的,高级定时器拥有通用计时器的全部功能,通用计时器又有基本定时器的全部功能。
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4 。
不同的型号,定时器的数量是不同的,在使用这个外设之前,一定要查一下它是不是这个外设,如果操作了不存在的外设是不起作用的。
5.1.3 高级定时器、通用定时器、基本定时器结构图
5.1.3.1 基本定时器
首先是3个最重要的寄存器,分别是预分频器、计数器及自动重装寄存器,它们构成了最基本的计数计时电路,所以这一块电路就叫做时基单元。预分频器之前,连接的就是基准计数时钟的输入,最终来到控制器,由于基本定时器只能选择内部时钟,,所以可直接认为预分频器连接到了输入端的内部时钟CK_INT,内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz。所以通向时基单元的计数基准频率就是72MHz。再来看时基单元,首先是预分频器,它可以对72MHz的计数时钟进行预分频,比如这个寄存器写0,那就是不分频、或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz;如果写2,就是3分频,输出=输入/3,依次类推。所以预分频器的值和实际的分频系数相差了1,即实际分频系数=预分频器的值+1。这个预分频器是16位的,所以最大值可以写65535,也就是65536分频。这就是预分频器,就是对输入的基准频率提前进行一个分频的操作。然后是计数器,这个计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加1,这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值再计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时地任务。所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了。自动重装寄存器也是16位的,它存的就是我们写入的计数目标。在运行的过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了。那它就会产生一个中断信号,并且清零计数器,计数器开始下一次的计数计时。图上画了一个向上的折线箭头(UI处),就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,我们一般把它叫做“更新中断”。这个更新中断之后就会通过NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应了。向下的箭头代表的是会产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断、但可以触发内部其他电路的工作。以上就是定时器定时中断的全部流程了。
从基准时钟到预分频器再到计数器,计数器计数自增,同时不断地与自动重装寄存器进行比较。它两值相等时,即计时时间到,这时会产生一个更新中断和更新事件。CPU响应更新中断,就完成了定时中断的任务了。
STM32的一大特色,就是主从触发模式,它能让内部的硬件在不受程序的控制下实现自动运行,如果能把这个主从触发模式掌握好,那在某些情况下将会极大地减轻CPU的负担。主模式触发DAC的用途就是在我们使用DAC的时候,可能会用DAC输出一段波形。那就需要每隔一段事件来触发DAC,让它输出下一个电压点。如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,这样也是没问题的,但事实这样会使主程序处于频繁被中断的状态,这会影响主程序的运行和其他中断的响应。所以定时器设置了一个主模式,使用这个主模式就可以把这个定时器的更新事件,映射到触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上,这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接去触发DAC了。整个过程不需要软件的参与,实现了硬件的自动化,这就是主模式的作用。当然除了这个主模式外,还有更多硬件自动化的设计。
5.1.3.2 通用定时器
首先最核心的部分还是中间的时基单元, 这部分结构和基本定时器是一样的,由预分频器、计数器、自动重装寄存器构成,每部分的工作流程和基本定时器是一样的。预分频器对时钟进行分频,计数器自增计数,当计数值达到自动重装值时,计数值清零同时产生更新中断和更新事件。不过对于通用定时器而言,这个计数器的计数模式就不止向上计数这一种了。除了向上计数外,通用定时器和高级计时器还支持向下计数模式和中央对齐模式。向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后继续下一轮,一次循环,这就是向下计数。还有中央对齐的计数模式,就是从0开始,先向上自增,计到重装值,然后再向下自减,减到0,再申请中断。然后继续下一轮,依次循环,这就是中央对齐模式。总结一下就是,基本定时器仅支持向上计数这一种模式,通用定时器和高级定时器支持向上计数、向下计数、中央对齐这三种模式。这个模式比较多,不过我们最常用的还是向上计数模式。
上面部分就是内外时钟源选择和主从触发模式的结构了。
先看一下内外时钟源选择,对于基本定时器而言,定时只能选择内部时钟,也就是系统频率72MHz,到了通用定时器这里,时钟源不仅可以选择内部的72MHz时钟,还可以选择外部时钟,具体有:
第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,这个ETR(External)引脚的位置,可以参考一下引脚定义表。例如,我们可以再TIM2的ETR引脚,也就是PA0上接一个外部方波时钟,然后配置一下外部的极性选择,边沿检测和预分频器电路,再配置一下输入滤波电路。这两块电路可以对外部时钟进行一定的整形,因为是外部引脚的时钟,所以难免会有毛刺,那这些电路就可以对输入的波形进行滤波,同时也可以选择一下极性和预分频器。最后,滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了。如果想在ETR外部引脚提供时钟,或者想对ETR时钟进行计数,把这个定时器当作计数器来用的话,那就可以配置这一路的电路,在STM32中,这一路也叫做“外部时钟模式2”。
除了外部ETR引脚可以提供时钟外,下面还有一路可以提供时钟,就是TRGI(Trigger In),这一路从名字上看的话,它主要是用作触发输入来使用的,这个触发输入可以触发定时器的从模式。这个触发输入作为外部使用的情况下,暂且可以把这个TRGI当做外部时钟的输入来看,当这个TRGI当做外部时钟来使用的时候,这一路就叫做“外部时钟模式1”。通过这一路的外部时钟都有哪些呢?第一个就是ETR引脚的信号,这里ETR引脚既可以通过上面那一路当作时钟,又可以通过下面这一路进来当作时钟,两种情况对于时钟输入而言是等价的,只不过下面这一路输入会占用触发输入的通道而已。然后第二个就是ITR信号,这一部分的时钟信号是来自其他定时器的。从右边可以看出,这个主模式的输出TRGO可以通向其他定时器,那通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了。这个ITR0到ITR3分别来自其他4个定时器的TRGO输出,至于具体的连接方式是怎么样的:
TIM2的ITR0是接在了TIM1的TRGO上,ITR1接在TIM8,ITR2接在TIM3,ITR3接在TIM4,其它定时器也都可以参照上表,这就是ITR和定时器的连接关系。通过这一路我们就可以实现定时器级联的功能,比如我们可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着在初始化ITM2,这里选择ITR2,对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联。
还可以选择TI1F_ED,这里连接的是输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思。也就是通过这一路输入的时钟,上升沿和下降沿均有效,最后这个时钟还能通过TI1FP1和TI2FP2获得,其中TI1FP1连接到了CH1引脚的时钟,TI2FP2连接到了CH2引脚的时钟,到这里外部时钟模式1的输入就介绍完了。
总结一下就是,外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚。这还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了。下面设置这么复杂地输入,不仅仅是为了扩大时钟输入的范围,更多的还是为了某些特殊应用场景而设计的。比如为了定时器的级联而设计的ITR路线。对于时钟输入而言,最常用的还是内部的72MHz的时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单、最直接。有关时钟输入的部分就结束了。
最后还有一块编码器接口,这个是定时器的一个编码器接口,可以读取正交编码器的输出波形。TRGO输出那里就是主模式的输出了,编码器接口这部分顶电路可以把内部的一些事件映射到这个TRGO引脚上。比如基本定时器分析时,将更新事件映射到TRGO,用于触发ADC,这里也是一样,我们可以把定时器内部的一些事件映射到这里来,用于触发其它定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。
然后是最下边部分:右边这一块是输出比较电路,总共有4个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形,驱动电机。左边这一块是输入捕获电路,也是有4个通道,对应的也是CH1到CH4的引脚,可以用于测量输入方波的频率等。中间这个寄存器是捕获/比较寄存器,是输入捕获和输出比较电路共用的,因为输入捕获和输出比较不能同时使用,所以这里的寄存器是共用的,引脚也是共用的。
5.1.3.3 高级定时器
对比通用定时器的结构,高级定时器这里,左上的这一大部分都没有变化, 主要改动的是右边和下边。第一个申请中断的地方,增加了重复次数计数器,有了这个计数器之后,就可以实现每隔几个计数周期,才发生一次更新事件和更新中断。原来的结构是每个计数周期完成后都会发生更新,现在有个计数器在这里,可以实现每隔几个周期再更新一次,这个就相当于对输出的更新信号又做了一次分频。对于高级定时器的话,之前计算的最大定时时间是59秒多,在这里就还需要再乘以一个65536,这就又提升了很多定时时间,这就是重复计数器的功能。
然后剩下新增部分就是高级定时器对输出比较模块的升级。这个DTG(Dead Time Generate)是死区生成电路,右边这里的输出引脚由原来的一个变为了两个互补的输出,可以输出一对互补的PAM波,这些电路是为了驱动三相无刷电机的,三相无刷电机还是比较常用的,比如四轴飞行器、电动车的后轮、电钻等,里面都可能是这个三相无刷电机。因为三相无刷电机一般需要三个桥臂,每个桥臂2个大功率开关管来控制,总共需要6个大功率开关管来控制,所以这里的输出PWM引脚的前三路就变为了互补的输出,而第四路却没什么变化,因为三相电机只需要三路就行了。另外,为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,所以前面就加上了死区生成电路,在开关切换的瞬间,产生一定时长的死区。让桥臂的上下管全部关断,防止直通现象。
最后一部分就是刹车输入的功能了,这个是为了给电机驱动提供安全保障的。如果外部引脚BKIN(Break IN)产生了刹车信号,或者内部时钟失效,产生了故障,那么控制电路就会自动切断电机的输出,防止意外的发生,这就是刹车输入的功能。
5.1.4 定时中断基本结构
首先中间最重要的还是PSC(Prescaler)预分频器、CNT(Counter)计数器、 ARR(AutoReloadRegister)自动重装器这三个寄存器构成的时基单元。下面是运行控制,就是控制寄存器的一些位,比如启动停止、向上或向下计数等等。我们操作这些寄存器就能控制时基单元的运行了。左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2,在本小节示例程序里目的一个定时器定时中断就是用的内部时钟这一路,第二个定时器外部时钟就是用的外部时钟模式2这一路,当然还可以选择这里的触发输入当作外部时钟,即外部时钟模式1,对应的有ETR外部时钟、ITRx其它定时器、TIx输入捕获通道,这些就是定时器的所有可选择时钟源了。 最后还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。接下来右边这里,就是计时时间到,产生更新中断后的信号去向,在这里如果是高级定时器的话,还会多一个重复计数器。中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。为什么会有一个中断输出控制呢,因为这个定时器模块有很多地方都要申请中断,不仅更新要申请中断,这里触发信号也会申请中断,还有下面的输入捕获和输出比较匹配时也会申请,所以这些中断都要经过输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止,简单来说,这个中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。
5.1.5 时序图
了解一下时基单元运行的细节问题。
5.1.5.1 预分频器时序图
CK_PSC:预分频器的输入时钟,选内部时钟的话一般是72MHz,这个时钟在不断地运行。
CNT_EN:计数器使能,高电平计数器正常运行,低电平计数器停止。
CK_CNT:计数器时钟,即是预分频器的时钟输出,也是计数器的时钟输入。开始时,计数器未使能,计数器时钟不运行,使能后,前半段预分频器系数为1,计数器的时钟等于预分频器前的时钟,后半段预分频器的系数变为2,计数器的时钟也就变为预分频器前时钟的一半了。
计数器寄存器:在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增,在中间位置FC之后,计数值变为0了,这里虽然没写,但是可以推断出ARR自动重装值就是FC。当计数值计到和重装值相等,并且下一个时钟来临时,计数值才清零。同时下面产生一个更新事件。这就是一个计数周期的工作流程。
下面还有三行时序,这三行时序描述的意思是:这个预分频寄存器的一种缓冲机制,也就是预分频寄存器实际上是有两个,一个是预分频控制寄存器,供我们读写用的,它并不直接决定分频系数。另外还有一个缓冲寄存器或者说是影子寄存器,缓冲寄存器和影子寄存器,这两个说法其实是一个意思。这个缓冲寄存器才是真正起作用的寄存器,比如我们在某个时刻,把预分频寄存器由0改成了1。如果在此时立刻改变时钟的分频系数,那么就会导致在一个计数周期内,前半部分和后半部分的频率不一样,这里计数计到一半,计数频率突然就会改变了,这虽然一般并不会有什么问题,但是STM32的定时器比较严谨,设计了这个缓冲寄存器,这样,当我们在计数计到一半的时候改变了分频值,这个变化不会立刻生效,而是会等到本次技术周期结束时,产生了更新事件,预分频器的值才会被传递到缓冲寄存器里面去,才会生效。所以在这里看到,即使我在技术中途改变了预分频器值,技术频率仍然会保持原来的频率,直到本轮计数完成,在下一轮计数时,改变后的分频值才会起作用。预分频器内部实际上也是靠计数来分频的,当预分频值为0时,计数器就一直为0,直接输出原频率,当预分频器为1时,计数器就0、1、0、1、0、1、0、1这样计数。在回到0的时候,输出一个脉冲,这样输出频率就是输入频率的2分频。预分频器的系数和实际的分频系数之间有一个数的偏移。
计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
5.1.5.2 计数器时序
CK_INT:内部时钟72MHz;
CNT_EN:时钟使能,高电平启动;
CK_CNT:计数器时钟,因为分频系数为2,所以这个频率是CK_INT/2
计数器在每个时钟上升沿自增,当增到0036的时候发生溢出,那计到36之后,再来一个上升沿,计数器清零,计数器溢出,产生一个更新事件脉冲,另外还会置一个更新中断标志位UIF,这个标志位只要置1了,就回去申请中断,然后中断响应后,需要在中断程序中手动清零,这就是计数器的工作流程。
计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
这就是计算定时时间的一个式子,用72MHz/(PSC + 1) / (ARR + 1)就能得到溢出频率,如果想算溢出时间,那就只需要再取倒数就行了。
预分频器为了防止计数中途更改数值造成错误,设计了缓冲寄存器,这个计数器也少不了这样的设计。像下图所示框内带有黑色阴影的寄存器都是有这样的缓冲机制的,包括预分频器,自动重装寄存器和下面的捕获比较寄存器。所以计数器的ARR自动重装寄存器,也是有一个缓冲寄存器的,并且这个缓冲寄存器是用还是不用,是可以自己设置的。
通过设置ARPE位可以选择是否使用预装功能。
5.1.5.2.1 计数器无预装时序
没有缓冲寄存器的情况。在这里,计数器正在自增计数,突然更改了自动加载寄存器,就是自动重装寄存器,由FF改成了36,那计数值的目标值就由FF改成了36,所以这里计到36之后,就直接更新,开始下一轮计数。
5.1.5.2.2 计数器有预装时序
有缓冲寄存器的情况。在计数的中途,突然把计数目标由F5改成了36,可以看到下面有一个影子寄存器,这个影子寄存器才是真正起作用的,它还是F5,所以现在计数的目标还是计到F5,产生更新事件,同时,要更改的36才被传递到影子寄存器。在下一个计数周期这个更改的36才有效。所以可以看出,引入这个影子寄存器的目的,实际上是为了同步,就是让值得变化和更新事件同步发生,防止在运行途中更改造成错误。从这个图也可以看出,如果不使用影子寄存器得话,F5改成36立刻生效,但此时计数值已经到了F1,已经超过36了,F1只能增加,但它得目标确是36,比它还小,这样F1就只能一直加到FFFF,再回到0,再加到36,才能产生更新,这样就造成一些小问题。
5.1.6 RCC时钟树结构图
这个时钟树,就是STM32用来产生和配置时钟, 并且把配置好的时钟发送到各个外设的系统。时钟是所有外设运行的基础,所以时钟也是最先需要配置的东西。程序中,主函数之前还会执行一个SystemInit的函数,这个函数就是用来配置这个时钟树的,这个结构看上去比较复杂,配置起来也比较麻烦,不过ST公司已经写好了配置这个时钟树的SystemInit函数。
这个图左边都是时钟的产生电路,右边的都是时钟的分配电路。 中间的SYSCLK就是系统时钟72MHz。在时钟产生电路,有4个振荡源,分别是内部的8MHz高速RC振荡器、外部的4~16MHz高速石英晶体振荡器,也就是晶振,一般都是接8MHz、外部的32.768KHz低速晶振,这个一般是给RTC提供时钟的,最后是内部的40KHz低速RC振荡器,这个可以给看门狗提供时钟。上面这两个高速晶振,是用来提供系统时钟的。AHB、APB2、APB1的时钟都是来源于这两个高速晶振。这里内部和外部都有一个8MHz的晶振,都是可以用的。只不过是外部的石英振荡器比内部的RC振荡器更加稳定,所以我们一般用外部晶振。但是如果系统简单,而且不需要那么精确的时钟,那也是可以使用内部的RC振荡器的,这样就可以省下外部晶振的电路了。
在SystemInit函数里,ST是这样配置时钟的:首先启动内部时钟,选择8MHz为系统时钟,暂时以内部8MHz的时钟运行,然后再启动外部时钟,进入PLL锁相环进行倍频,8MHz倍频9倍,得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由8MHz切换为了72MHz。这样分析之后,可以解决实际应用的一个问题,那就是如果外部晶振出问题了,可能会导致一个现象,程序的时钟会慢了大概10倍,比如用定时器定了个1s的时间,结果过了大概10s才进中断。问题就出在这里,如果外部晶振出问题了,系统时钟就无法切换到72MHz,就会以内部的8MHz进行运行,8MHz相较于72MHz,慢了大概10倍。
另外还有一个CSS(Clock Security System),这个是时钟安全系统,也是负责切换时钟的,它可以监测外部时钟的运行状态,一但外部时钟失效,它就会自动把外部时钟切换为内部时钟,保证系统时钟的运行,防止程序卡死造成事故。
在高级定时器里,也有这个CSS的身影。在刹车输入那里,一旦CSS检测到外部时钟失效,通过或门,就会立刻反应到输出比较那里,让输出控制的电机立即停止,防止意外,这就是STM32里的一些安全保障措施。
右边的时钟分配电路:首先系统时钟72MHz进入AHB总线,AHB总线有个预分频器,在SystemInit里配置的分配系数为1,那AHB的时钟就是72MHz,然后进图APB1总线,这里配置的分配系数是2,所以APB1总线的时钟为72MHz/2=36MHz。
现在可能会有个疑问,就是通用定时器和基本定时器是接在APB1上的,而APB1的时钟是36MHz,按理说它们的时钟应该是36MHz,但是在讲定时器的时候,一直都是说所有的定时器的时钟都是72MHz。这是因为预分频器出来后,下面还有一条支路,上面写的是如果APB1预分频系数=1,则频率不变,否则频率×2”,然后再看右边,这一路是单独为定时器2~7开通的,那因为这里我们给的预分频器系数是2,所以这里频率要再乘2,所以通向2~7的时钟,就又回到了72MHz。所以这里有个结论,无论是高级定时器、还是通用定时器、基本定时器,它们的内部基准时钟都是72MHz。
APB2的时钟给的分频器系数为1,和AHB一样,都是72MHz,这里接在APB2上的高级定时器也单开了一路,上面写的也是“如果APB2预分频系数=1,则频率不变,否则频率×2”,但是这里APB2的预分频系数就是1,所以频率不变,定时器1和8的时钟就是72MHz。
在时钟输出这里,都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是我们在程序中写RCC_APB2/1PeriphClockCmd作用的地方,打开时钟,就是在这个位置写1,让左边的时钟能够通过与门输出给外设。
5.2 定时器定时中断
5.2.1 硬件电路
5.2.2 软件部分
(1)复制《OLED显示屏代码 》并改名为《定时器定时中断》。
(2)添加定时器驱动文件
因为定时器不涉及外部硬件,所以放在“system”文件夹下。
/*初始化定时器函数
第一步:RCC开启时钟,打开始终后,定时器的基准时钟和整个外设的工作时钟就会同步打开了;
第二步:选择时基单元的时钟源,对于定时中断,我们选择内部时钟源;
第三步:配置时基单元,包括预分频器、自动重装器、计数模式等,用一个结构体就可以配置好了;
第四步:配置输出中断控制,允许更新中断输出到NVIC;
第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级;
第六步:运行控制,整个模块配置完成后,还要使能一下计数器,要不然计数器是不会运行的,当计时器
使能后,计数器就会开始计数了,当计数器更新时,触发中断;
最后再学习一个定时器的中断函数,这样中断函数每隔一段时间就能自动执行一次了*/
(3)定时器库函数
void TIM_DeInit(TIM_TypeDef* TIMx); // 恢复缺省配置
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//时基单元初始化,用来配置时基单元,第一个参数用来选择某个定时器,第二个是结构体,里面包含了配置时基单元的一些参数
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_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_BDTRConfig(TIM_TypeDef* TIMx, TIM_BDTRInitTypeDef *TIM_BDTRInitStruct);
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_BDTRStructInit(TIM_BDTRInitTypeDef* TIM_BDTRInitStruct);
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState); // 用来使能计数器,对应运行控制,第一个参数选择定时器,第二个参数选择使能还是失能
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
// 用来使能中断输出信号,对应中断输出控制,第一个参数选择定时器,第二个参数选择要配置哪个中断输出,第三个参数选择
void TIM_GenerateEvent(TIM_TypeDef* TIMx, uint16_t TIM_EventSource);
void TIM_DMAConfig(TIM_TypeDef* TIMx, uint16_t TIM_DMABase, uint16_t TIM_DMABurstLength);
void TIM_DMACmd(TIM_TypeDef* TIMx, uint16_t TIM_DMASource, FunctionalState NewState);
//下面6个函数对应时基单元的时钟选择部分,可以选择RCC内部时钟、ETR外部时钟、ITRx其它定时器、TIx捕获通道等等
void TIM_InternalClockConfig(TIM_TypeDef* TIMx); //选择内部时钟,
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
//选择ITRx其它定时器的时钟,参数是ITMx,选择要配置的定时器,TIM_InputTriggerSource选择要接入哪个其它的定时器
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
/*选择TIx捕获通道的时钟,第二个TIM_TIxExternalCLKSource选择TIx具体的某个引脚,TIM_ICPolarity和ICFilter:输入的极性和滤波器
对于外部引脚的波形,一般都会由极性选择和滤波器,这样更灵活一些*/
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
/*选择ETR通过外部时钟模式1输入的时钟,TIM_ExtTRGPrescaler:外部触发预分频器,这里可以对ETR的外部时钟再提前做一个分频,
TIM_ExtTRGPolarity和ExtTRGFilter:极性选择和过滤器*/
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
/*选择ETR通过外部时钟模式2输入的时钟,TIM_ExtTRGPrescaler:外部触发预分频器,这里可以对ETR的外部时钟再提前做一个分频,
TIM_ExtTRGPolarity和ExtTRGFilter:极性选择和过滤器*/
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
/*单独用来配置ETR引脚的预分频器、极性、滤波器这些参数的*/
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
/*单独写预分频值,Prescaler就是要写入的预分频值,TIM_PSCReloadMode写入的模式(预分频器有一个缓冲器,写入的值实在更新事件发生后才有效的
所以这里有一个写入的模式,可以选择是听从安排,在更新事件生效或者是在写入后,手动产生一个更新事件,让这个值立刻生效)*/
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
/*改变计数器的计数模式,TIM_CounterMode选择新的计数器模式*/
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
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);
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
/*自动重装器预装功能配置*/
void TIM_SelectCOM(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCCDMA(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CCPreloadControl(TIM_TypeDef* TIMx, FunctionalState NewState);
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);
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);
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);
void TIM_UpdateDisableConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_UpdateRequestConfig(TIM_TypeDef* TIMx, uint16_t TIM_UpdateSource);
void TIM_SelectHallSensor(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectOnePulseMode(TIM_TypeDef* TIMx, uint16_t TIM_OPMode);
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
/*给计数器写入一个值,如果想手动给一个计数值,就可以用这个函数*/
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
/*给自动重装器写入一个值,如果想手动给一个自动重装值,就可以用这个函数*/
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);
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetClockDivision(TIM_TypeDef* TIMx, uint16_t TIM_CKD);
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
/*获取当前计数器的值,如果想看当前计数器计到哪里,调用这个函数,返回值就是当前计数器的值*/
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
/*获取当前预分频器的值,如果想看预分频器值,调用这个函数*/
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
/*上面四个函数就是用来获取和清除标志位的*/
在定时器的外部信号输入引脚,一般都会有一个滤波器,这个滤波器可以滤掉信号的抖动干扰。工作方式是:
在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个采样值输出出去,如果这N个采样值不全都相同,那就说明信号有抖动,这时就保持上一次的输出,或者直接输出低电平也行,这样就能保证输出信号在一定程度上的滤波,这里的采样频率f和采样点数N都是滤波器的参数,频率越低,采样点数越多,那滤波效果就越好,不过相应的信号延迟就越大,这就是滤波器的工作原理。
采样频率f可以由内部时钟直接而来,也可以由内部时钟加一个分频而来,分频多少就是由
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
这个结构体初始化时候的
TIM_TimeBaseInitStructure.TIM_ClockDivision = ;
决定的。
跨文件使用变量:
extern uint16_t Num;
extern声明变量以后,就是告诉编译器,有一个Num变量,它在别的文件里定义了,让编译器自己去找。
(4)Timer.c
#include "stm32f10x.h" // Device header
/*初始化定时器函数
第一步:RCC开启时钟,打开始终后,定时器的基准时钟和整个外设的工作时钟就会同步打开了;
第二步:选择时基单元的时钟源,对于定时中断,我们选择内部时钟源;
第三步:配置时基单元,包括预分频器、自动重装器、计数模式等,用一个结构体就可以配置好了;
第四步:配置输出中断控制,允许更新中断输出到NVIC;
第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级;
第六步:运行控制,启动定时器,整个模块配置完成后,还要使能一下计数器,要不然计数器是不会运行的,当计时器
使能后,计数器就会开始计数了,当计数器更新时,触发中断;
最后再学习一个定时器的中断函数,这样中断函数每隔一段时间就能自动执行一次了*/
void Timer_Init(void)
{
/*第一步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启挂载在APB1上的TIM2时钟
/*第一步:选择时基单元的时钟源*/
TIM_InternalClockConfig(TIM2); //TIM2的时基单元由内部时钟来驱动
/*第三步:配置时基单元*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000-1; //指定ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200-1; //指定预分频器的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update); //手动清除一下更新中断标志位,这样计数就从0开始,否则是从1开始(刚初始化就进中断)
/*第四步:使能更新中断*/
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //TIM_IT_Update更新中断
/*第五步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//要对两个中断分别设置优先级
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //将抢占优先级设置为2,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
/*第六步:运行控制,启动定时器*/
TIM_Cmd(TIM2, ENABLE);
}
/*TIM2的中断函数*/
/*void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) //判断一下中断标志位状态
{
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除标志位
}
}
*/
(5)Timer.h
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
(6)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Timer_Init(); // 初始化定时器
OLED_ShowString(1,1,"Num:"); // 在1行3列显示字符串
while(1)
{
OLED_ShowNum(1,5,Num,5);
OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5); //显示计数器值
}
}
/*TIM2的中断函数*/
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) //判断一下中断标志位状态
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除标志位
}
}
5.3 定时器外部时钟
5.3.1 硬件电路
PA0引脚就是TIM2的ETR引脚,在这个引脚输入一个外部时钟。
5.3.2 软件部分
(1)复制《定时器定时中断》工程改名为《定时器外部时钟》。
(2)Timer.c
#include "stm32f10x.h" // Device header
/*初始化定时器函数
第一步:RCC开启时钟,打开始终后,定时器的基准时钟和整个外设的工作时钟就会同步打开了;
第二步:选择时基单元的时钟源,对于定时中断,我们选择内部时钟源;
第三步:配置时基单元,包括预分频器、自动重装器、计数模式等,用一个结构体就可以配置好了;
第四步:配置输出中断控制,允许更新中断输出到NVIC;
第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级;
第六步:运行控制,启动定时器,整个模块配置完成后,还要使能一下计数器,要不然计数器是不会运行的,当计时器
使能后,计数器就会开始计数了,当计数器更新时,触发中断;
最后再学习一个定时器的中断函数,这样中断函数每隔一段时间就能自动执行一次了
第七步:配置GPIO,要用到GPIO*/
void Timer_Init(void)
{
/*第一步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启挂载在APB1上的TIM2时钟
/*第二步:选择时基单元的时钟源*/
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted, 0x0F);
//通过ETR引脚的外部时钟模式2配置,TIM_ExtTRGPSC_OFF预分频器不需要分频,TIM_ExtTRGPolarity_NonInverted外部触发极性设置为不反向(高电平或上升沿有效),0x0F用滤波器防止干扰
/*第三步:配置时基单元*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10-1; //指定ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1-1; //指定预分频器的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update); //手动清除一下更新中断标志位,这样计数就从0开始,否则是从1开始(刚初始化就进中断)
/*第四步:使能更新中断*/
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //TIM_IT_Update更新中断
/*第五步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//要对两个中断分别设置优先级
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //将抢占优先级设置为2,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
/*第六步:运行控制,启动定时器*/
TIM_Cmd(TIM2, ENABLE);
/*第七步:配置GPIO*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
}
/*获取计数器的值*/
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
/*TIM2的中断函数*/
/*void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) //判断一下中断标志位状态
{
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除标志位
}
}
*/
(3)Timer.h
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
uint16_t Timer_GetCounter(void);
#endif
(4)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,Timer_GetCounter(),5); //显示计数器值
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) //判断一下中断标志位状态
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除标志位
}
}
5.4 TIM输出比较
5.4.1 输出比较简介
这个输出比较功能是非常重要的,主要用来输出PWM波形的,PWM波形又是驱动电机的必要条件,所以如果想用STM32做一些有电机的项目,比如智能车、机器人等。
(1)OC(Output Compare)输出比较;
(2)输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形;
CCR就是捕获比较寄存器,CC是捕获/比较的意思 ,R是Register,寄存器的意思。这个捕获/比较寄存器是输入捕获/输出比较共用的,当使用输入捕获时,它就是捕获寄存器;当使用输出比较时,它就是比较寄存器。在输出比较这里,这块电路会比较CNT和CCR的值,CNT计数自增,CCR是我们给定的一个值,当CNT大于CCR、小于CCR或者等于CCR时,这里输出就会对应的置1、置0、置1、置0,这样就可以输出一个电平不断跳变的PWM波形了。
(3)每个高级定时器和通用定时器都拥有4个输出比较通道;
(4)高级定时器的前3个通道额外拥有死区生成和互补输出的功能。
基本定时器没有输入捕获和输出比较的功能,所以基本定时器不能使用。通用定时器和高级定时器都有四个输出比较的通道,可以同时输出4路PWM波形,这四个通道上有各自的CCR寄存器,但是它们是共用一个CNT计数器的。高级定时器前三个输出比较通道还额外有死区生成和互补输出的功能,这个是用来驱动三相无刷电机的。
5.4.2 PWM简介
PWM(Pulse Width Modulation)脉冲宽度调制;
PWM波形是一个数字输出信号,也是由高低电平组成的。
在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
PWM参数:
频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距
占空比决定了PWM等效出来地模拟电压的大小, 占空比越大,那等效的模拟电压就趋近于高电平,占空比越小,那等效的模拟电压就越趋近于低电平,这个等效关系一般来说是线性的。比如高电平是5V,低电平是0V,那50%占空比就等效于中间电压,就是2.5V。20%占空比就等效于,1/5处的电压,就是1V。
分辨率:比如有的占空比只能是1%、2%、3%等等这样的以1%的步距跳变,那它的分辨率就是1%,如果可以以1.1%、1.2%、1.3%等等这样以0.1%的步距跳变,那它的分辨率就是0.1%。所以这个分辨率就是占空比变化的精细程度。这个分辨率需要多高,得看项目时基需求。如果既要高频率,又要高分辨率,这时对硬件电路要求就比较高了。不过一般要求比较高得话,1%的分辨率也就已经足够使用了。
使用PWM波形,就可以在数字系统等效输出模拟量,就能实现LED控制亮度、电机控速等功能了。
5.4.3 输出比较通道(通用)
上图是通用定时器的输出比较部分电路,包括高级定时器的第四个通道和这个机构也基本是一样的。对应下图标注部分电路。
左边是CNT和CCR比较的结果,右边就是输出比较电路,最后通过TIM_CH1输出到GPIO引脚上。 下面还有三个同样的单元,分别是输出到CH2、CH3、CH4。
在这个图里,左边就是CNT计数器和CCR1第一路的捕获/比较寄存器。 它两进行比较,当CNT>CCR1或者CNT=CCR1时,就会给输出模式控制器传一个信号,然后输出模式控制器就会改变它输出OC1REF的高低电平。REF信号实际上就是指信号的高低电平,这个REF是reference的缩写,意思是参考信号。上面还有一个ETRF输入,这个是定时器的一个小功能,一般不用。接着REF信号可以前往主模式控制器,可以把这个REF信号映射到主模式的TRGO输出上去,不过REF的主要去向还是下面这一路,通过这一路到达TIMx_CCER,给这个寄存器写0,信号就会往上走,就是信号电平不翻转,进来啥样,出去啥样;写1的话,信号会往下走,就是信号通过一个非门取反,那输出的信号就是输入信号高低电平反转的信号;这就是极性选择,就是选择是不是要把高低电平反转一下。接着就是输出使能电路了,选择要不要输出,这个引脚就是CH1通道的引脚,最后就是OC1引脚,这个引脚就是CH1通道的引脚,在引脚定义表里就可以直到具体是哪个GPIO口了。
输出模式控制器是如何工作的:什么时候给REF低电平,什么时候给高电平。输出模式控制器里面执行逻辑入下表所示。
模式 | 描述 |
冻结 | CNT=CCR时,REF保持为原状态 |
匹配时置有效电平 | CNT=CCR时,REF置有效电平 |
匹配时置无效电平 | CNT=CCR时,REF置无效电平 |
匹配时电平翻转 | CNT=CCR时,REF电平翻转 |
强制为无效电平 | CNT与CCR无效,REF强制为无效电平 |
强制为有效电平 | CNT与CCR无效,REF强制为有效电平 |
PWM模式1 | 向上计数:CNT<CCR时,REF置有效电平,CNT≥CCR时,REF置无效电平 向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平 |
PWM模式2 | 向上计数:CNT<CCR时,REF置无效电平,CNT≥CCR时,REF置有效电平 向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平 |
冻结模式:CNT和CCR没有用,处于无效状态,REF保持为原状态,这都是一样的效果。这个模式也比较简单,它根部不管CNT和CCR谁大谁小,直接REF保持不变,维持上一个状态就可以了。用处:比如正在输出PWM波、突然想暂停一会儿输出、就可以设置成这个模式。一旦切换为冻结模式后,输出就暂停了。并且高低电平也维持为暂停时刻的状态,保持不变,这就是冻结模式的作用。
匹配时置有效电平/匹配时置无效电平/匹配时电平翻转:这个有效电平和无效电平,一般是高级定时器里面的一个说法,是和关断、刹车这些功能配合表述的,它说的比较严谨,所以叫有效电平和无效电平。所以可以直接理解为置有效电平就是置高电平,置无效电平就是置低电平。这些模式就可以用作波形输出了。比如相等时电平翻转这个模式,这个可以方便地输出一个频率可调,占空比始终为50%的PWM波形,比如设置CCR为0,那CNT每次更新清0时,就会产生一次CNT=CCR的事件,这就会导致输出电平翻转一次,每更新两次、输出为一个周期,并且高电平和低电平的时间是始终相等的,也就是占空比时钟为50%。当改变定时器更新频率时,输出波形的频率也会随之改变,它两的关系是输出波形的频率=更新频率/2,因为更新两次输出才为一个周期,这就是匹配时电平翻转模式的用途。匹配时置有效电平/匹配时置无效电平都是一次性的,置完高电平/低电平后,就不管事了,所以这两个模式不适合输出连续变化的波形,如果想定时输出一次性的信号,可以考虑一下这两个模式。
强制为无效电平/强制为有效电平:这两个模式是CNT与CCR无效,REF强制为无效电平或者强制为有效电平。这两个模式和冻结模式也差不多。如果想暂停波形输出,并且在暂停期间保持低电平或者保持高电平,就可以设置这两个强制输出模式。
PWM模式1/PWM模式2:它们可以用于输出频率和占空比都可调的PWM波形,也是我们主要使用的模式。
5.4.4 输出比较通道(高级)
上图是高级定时器前三个通道的输出比较部分电路。在它外面,通常要接这样一个外部电路,如下图所示, 上面是正极,接的是一个大功率开发管,一般都是MOS管,就是一种大功率电子开关,然后再来一个MOS管,最后到GND。MOS管左边是控制极、比如说给高电平右边两根线就导通,低电平就断开;下面也是一样,高电平导通,低电平断开。这就是一个最基本的推挽电路,中间是输出;如果上管导通,下管断开,那输出就是高电平;如果下管导通,上管断开,输出就是低电平;如果上下管都导通,那就是电源短路,这样是不允许的;如果上下管都断开,那输出就是高阻态;这就是推挽电路的工作流程。如果有两个这样的推挽电路,那就构成了H桥电路,就可以控制直流电机正反转了。如果有三个这样的推挽电路,就可以用于驱动三相无刷电机了。这就是这个电路的用途。对于这个电路来说,如果直接用单片机来控制的话,那就需要两个控制极,并且这两个控制极电平是相反的,也就是互补,因为上管导通,下管就必须断开,下管导通,上管就必须断开。知道了外部电路的需求,再来理解内部电路结构,那自热就好理解了。首先这个OC1和OC1N就是两个互补的输出端口,分别控制上口和下口的导通和关闭,然后是在切换上下管导通状态时,如果在上管关断的瞬间,下管就会立刻打开,那可能会因为器件的不理想,上管还没有完全关断,下管就已经导通了,出现了短暂的上下管同时导通的现象,这会导致功率损耗,引起器件发热,所以在这里为了避免这个问题,就有了死区生成电路,它会在上管关闭的手,延迟一小段时间,再导通下管,下管关闭的时候,延时一小段时间,再导通上管,这样就可以避免上下管同时导通的现象了,这就是死区生成和互补输出的用途。
5.4.5 PWM基本结构
左上角是时基单元和运行控制部分,最左边是时钟源选择,这里省略了,输出PWM不需要中断。配置好了时基单元,CNT就可以开始不断地自增运行了。
下面就是输出比较单元,总共有4路。输出比较单元地最开始,是CCR捕获/比较寄存器,CCR是我们自己设定地,CNT不断自增运行,同时它两还在不断运行比较,后面就是输出模式控制器。以PWM模式1为例:CNT<CCR时,REF置有效电平、CNT≥CCR时,REF置无效电平。如何输出PWM波形如右上图所示:蓝色线是CNT的值,黄色线是ARR的值。蓝色线从0开始自增,一直增到ARR,也就是99,之后清零继续自增。在这个过程中,再设置一条红色线,这条红色线就是CCR,比如我们设置CCR为30。之后再执行PWM模式1逻辑,下面绿色线就是输出。并且占空比是受CCR的值调控的,如果CCR设置高一些,输出的占空比就变大。这里REF就是一个频率可调,占空比也可调的PWM波形,最终再经过极性选择,输出使能,最终通向GPIO口,这样就能完成PWM波形的输出了。
5.4.6 PWM参数计算
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比: Duty = CCR / (ARR + 1)
PWM分辨率: Reso = 1 / (ARR + 1)
5.4.7 舵机简介
舵机是一种根据输入PWM信号占空比来控制输出角度的装置。
图示舵机型号为SG90,它有三根输入线,两根电源线,一根是信号线,PWM就是输入到这个信号线,来控制舵机的。有一个白色的输出轴,它的轴会固定再一个指定的角度不动,至于固定在哪个位置,是由信号线的PWM信号来决定的。这就是舵机的工作方式。PWM输出到控制板,给控制板一个指定的目标角度,然后这个电位器检测输出轴的当前角度,如果大于目标角度,电机就会反转,如果小于目标角度,电机就会正转。最终使输出轴固定在指定角度,这就是舵机的内部工作流程。不管怎样,输入一个PWM波形,输出轴固定在一个角度就行了。
输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms。
实际应用的时候,比如机器人、机械臂,可以用舵机来控制关节,遥控车、遥控船可以用舵机来控制方向。还有一些其它的机器结构,都可以考虑用这个舵机。这里的PWM波形其实是当作一个协议来使用的。
5.4.7.1 舵机硬件电路
舵机引脚有以下几种区分:
颜色 | 信号 |
黑 | GND |
红 | VCC(+5V) |
黄 | PWM |
颜色 | 信号 |
棕 | GND |
红 | VCC(+5V) |
橙 | PWM |
颜色 | 信号 |
黑 | GND |
红 | VCC(+5V) |
白 | PWM |
实际应用的时候,电源正的5V,一般电机都是大功率设备,它的驱动电源也必须是一个大功率的输出设备,如果能单独供电最好,如果不能也要注意电源功率是否能达标。单独供电的话,供电的负极要和STM32共地,然后正极接在5V供电引脚上。对于套件,可以直接从STLINK的5V输出角,引一根线,接到这里,这样就是使用USB的5V供电。
然后是PWM,因为舵机内部是有驱动电路的,所以单片机的引脚可以直接接上去,PWM只是一根通信线,是不需要大功率的。
5.4.8 直流电机及驱动简介
直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。
套件里的是130直流电机,一边接正,一边接负、电机就朝一个方向转,如果把正负极对调,那电机就朝另一个方向转。
直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作。
电机这类器件都属于大功率驱动设备,必须要加驱动电路才能控制。市面上有很多驱动电路可以选择,比如TB6612、DRV8833、L9110、L298N等等,这些都是比较常见的电机驱动芯片。另外还有一些用分立元件MOS管搭建的电路,这个功率可以做的更大一些,当然也可以自己用MOS管来设计电路。
TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。
这个芯片是有两路驱动电路的,可以独立地控制两个电机,又因为它是H桥型地驱动电路,里面一路有4个开关管,所以可以控制正反转。像有些芯片,比如ULN2003,它里面只有一路就只有一个开关管,所以就只能控制电机在一个方向转。
5.4.8.1 硬件电路
STBY(Stand By)引脚:这个是待机控制脚,如果接GND,芯片就不工作,处于待机状态,如果接逻辑电源VCC,芯片就正常工作。这个引脚如果不需要待机的话,可以直接接VCC,3.3V。如果需要的话,可以接任意一个GPIO,给高低电平就可以控制了。这就是电机驱动板的硬件电路。
5.5 PWM驱动LED呼吸灯
5.5.1 硬件电路
5.5.2 软件部分
(1)复制《OLED显示屏》工程修改为《PWM驱动LED呼吸灯》
(2)添加PWM驱动文件
PWM初始化就是打通下图所示结构:
(3)相关库函数功能
void TIM_DeInit(TIM_TypeDef* TIMx);
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
/*下面4个函数就是用来配置输出比较模块的,OC就是Output Compare,输出比较*/
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_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_BDTRConfig(TIM_TypeDef* TIMx, TIM_BDTRInitTypeDef *TIM_BDTRInitStruct);
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
/*给输出比较结构体赋一个默认值*/
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_BDTRStructInit(TIM_BDTRInitTypeDef* TIM_BDTRInitStruct);
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
/*上面这个函数仅高级定时器使用,在高级定时器PWM时需要调用这个函数,使能输出,否则PWM将不能正常输出*/
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
void TIM_GenerateEvent(TIM_TypeDef* TIMx, uint16_t TIM_EventSource);
void TIM_DMAConfig(TIM_TypeDef* TIMx, uint16_t TIM_DMABase, uint16_t TIM_DMABurstLength);
void TIM_DMACmd(TIM_TypeDef* TIMx, uint16_t TIM_DMASource, FunctionalState NewState);
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
/*下面4个函数是用来配置强制输出模式的,如果在运行中想要暂停输出波形并且强制输出高低电平,可以使用这几个函数,不过一般用的不多
因为强制输出高电平和设置占空比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);
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCOM(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCCDMA(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CCPreloadControl(TIM_TypeDef* TIMx, FunctionalState NewState);
/*下面4个函数用来配置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);
/*下面4个函数用来配置快速使能*/
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信号那一节有介绍*/
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);
/*下面7个就是用来单独设置输出比较的极性的,带N的就是高级定时器里互补通道的配置,OC4没有互补通道。
这里有函数可以设置极性,在结构体初始化那个函数里也可以设置极性,这两个地方设置极性的作用是一样的,
只不过用结构体是一起初始化的,在这里是一个单独的函数进行修改的,一般来说,结构体里的参数,
都会有一个单独的函数可以进行更改,这里的函数就是用来单独更改输出极性的*/
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);/*选择输出比较模式,这个是用来单独更改输出比较模式的函数*/
void TIM_UpdateDisableConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_UpdateRequestConfig(TIM_TypeDef* TIMx, uint16_t TIM_UpdateSource);
void TIM_SelectHallSensor(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectOnePulseMode(TIM_TypeDef* TIMx, uint16_t TIM_OPMode);
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
/*下面四个函数是用来单独更改CCR寄存器值的函数,这4个函数比较重要,在运行的时候,更改占空比,就需要用到这4个函数*/
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);
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetClockDivision(TIM_TypeDef* TIMx, uint16_t TIM_CKD);
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
如果使用TIM2的OC1也就是CH1通道输出PWM,那就只能在PA0输出;如果选择TIM2的CH3,就只能选择PA2。其它外设也是同理,这个关系是定死的,不能更改,不过STM32还是提供了一次更改的机会,就是重定义或者叫重映射功能。比如既要用USTART2的TX引脚,又要用TIM2的CH3通道,它两冲突了,没办法同时用,那我们就可以在重映射的列表里找一下,发现TIM2的CH3通道可以重映射到PB10引脚;如果重映射列表里找不到,那外设复用的GPIO就不能挪位置,这就是重映射的功能,配置重映射是用AFIO来完成的。
(4)PWM.c
#include "stm32f10x.h" // Device header
/*PWM初始化函数:
第1步:RCC开启时钟,把我们需要的TIM外设和GPIO外设的时钟打开
第2步:配置时基单元,包括前面的时钟源选择和这里的时基单元,都配置好
第3步:配置输出比较单元,包括CCR的值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一配置的
第4步:配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,这个PWM和GPIO的对应关系是怎样的?可以参考引脚定义表
第5步:运行控制,启动计数器,这样就能输出PWM了
*/
void PWM_Init(void)
{
/*第1步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启挂载在APB1上的TIM2时钟
/*第2步:选择时基单元的时钟源*/
TIM_InternalClockConfig(TIM2); //TIM2的时基单元由内部时钟来驱动
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100-1; //指定ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720-1; //指定预分频器PCS的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
/*第3步:配置输出比较单元*/
TIM_OCInitTypeDef TIM_OCInitStruct;
/*对于这个结构体变量来说,它现在是一个局部变量,如果不给它的成员赋初始值,它成员的值就是不确定的,可能回导致一些问题,
比如当要把高级定时器当作通用定时器输出PWM时,自然会把TIM2改为TIM1,这个结构体原来不用的成员现在又需要用了,而这些成员又
没有赋值,就会导致高级定时器输出PWM出现一些奇怪的问题*/
TIM_OCStructInit(&TIM_OCInitStruct); //为避免上述提到的问题,给这个结构体赋初始值
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式设置为PWM1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; //极性选择设置为高极性,有效电平是高电平,REF有效时,输出高电平
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; //输出使能
TIM_OCInitStruct.TIM_Pulse = 0; //设置CCR的值
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
/*第4步:配置GPIO*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
/*对于普通的开漏/推挽输出,引脚的控制全是来自输出数据寄存器的,如果想用定时器来控制引脚,那就需要使用复用开漏/推挽输出的模式
这里输出数据寄存器将被断开,输出控制全将转移给片上外设,只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能
通过引脚输出*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
/*第六步:运行控制,启动定时器*/
TIM_Cmd(TIM2, ENABLE);
}
/*更改占空比*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
(5)PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
#endif
(6)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "PWM.h"
uint8_t i;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
PWM_Init();
while(1)
{
for(i=0;i<=100;i++)
{
PWM_SetCompare1(i); //更改占空比
Delay_ms(10);
}
for(i=0;i<=100;i++)
{
PWM_SetCompare1(100-i); //更改占空比
Delay_ms(10);
}
}
}
(7)使用复用功能(引脚重映射)实现
PWM.c如下
#include "stm32f10x.h" // Device header
/*PWM初始化函数:
第1步:RCC开启时钟,把我们需要的TIM外设和GPIO外设的时钟打开
第2步:配置时基单元,包括前面的时钟源选择和这里的时基单元,都配置好
第3步:配置输出比较单元,包括CCR的值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一配置的
第4步:配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,这个PWM和GPIO的对应关系是怎样的?可以参考引脚定义表
第5步:运行控制,启动计数器,这样就能输出PWM了
*/
void PWM_Init(void)
{
/*第1步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启挂载在APB1上的TIM2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启挂载在APB2上的AFIO时钟
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); //将TIM2的CH1通道重映射到PA15
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
/*PA15上电后默认复用为了调试端口JTDI,所以想让它作为普通GPIO或者复用定时器通道,要先关闭调试端口的复用*/
/*第2步:选择时基单元的时钟源*/
TIM_InternalClockConfig(TIM2); //TIM2的时基单元由内部时钟来驱动
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100-1; //指定ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720-1; //指定预分频器PCS的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
/*第3步:配置输出比较单元*/
TIM_OCInitTypeDef TIM_OCInitStruct;
/*对于这个结构体变量来说,它现在是一个局部变量,如果不给它的成员赋初始值,它成员的值就是不确定的,可能回导致一些问题,
比如当要把高级定时器当作通用定时器输出PWM时,自然会把TIM2改为TIM1,这个结构体原来不用的成员现在又需要用了,而这些成员又
没有赋值,就会导致高级定时器输出PWM出现一些奇怪的问题*/
TIM_OCStructInit(&TIM_OCInitStruct); //为避免上述提到的问题,给这个结构体赋初始值
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式设置为PWM1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; //极性选择设置为高极性,有效电平是高电平,REF有效时,输出高电平
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; //输出使能
TIM_OCInitStruct.TIM_Pulse = 0; //设置CCR的值
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
/*第4步:配置GPIO*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
/*对于普通的开漏/推挽输出,引脚的控制全是来自输出数据寄存器的,如果想用定时器来控制引脚,那就需要使用复用开漏/推挽输出的模式
这里输出数据寄存器将被断开,输出控制全将转移给片上外设,只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能
通过引脚输出*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_15);
/*第六步:运行控制,启动定时器*/
TIM_Cmd(TIM2, ENABLE);
}
/*更改占空比*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
5.6 PWM驱动舵机
5.6.1 硬件电路
5.6.2 软件部分
(1)复制《PWM驱动LED呼吸灯》工程改为《PWM驱动舵机》
(2)添加舵机驱动文件
(3)SERVO.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/*舵机初始化函数*/
void Servo_Init(void)
{
PWM_Init(); //初始化一下PWM底层
}
void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle/180*2000+500); //角度到CCR值的映射(0对应500,180对应2500)
}
(4)SERVO.h
#ifndef __SERVO_H
#define __SERVO_H
void Servo_Init(void);
void Servo_SetAngle(float Angle);
#endif
(5)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "SERVO.h"
#include "Key.h"
uint8_t KeyNum;
float Angle;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Servo_Init();
OLED_ShowString(1,1,"Angle:");
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
Angle += 30;
if(Angle>180)
{
Angle = 0;
}
Servo_SetAngle(Angle);
OLED_ShowNum(1,7,Angle,3);
}
}
}
5.7 PWM驱动直流电机
5.7.1 硬件电路
5.7.2 软件部分
(1)复制《PWM驱动LED呼吸灯》工程改为《PWM驱动直流电机》
(2)新建驱动文件
(3)Motor.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/*直流电机初始化函数*/
void Motor_Init(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_4|GPIO_Pin_5;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
PWM_Init();
}
/*直流电机速度函数*/
void Motor_SetSpeed(int8_t Speed)
{
if(Speed >= 0) //正转
{
GPIO_SetBits(GPIOA,GPIO_Pin_4);
GPIO_ResetBits(GPIOA,GPIO_Pin_5);
PWM_SetCompare3(Speed);
}
else //反转
{
GPIO_SetBits(GPIOA,GPIO_Pin_5);
GPIO_ResetBits(GPIOA,GPIO_Pin_4);
PWM_SetCompare3(-Speed);
}
}
(4)Motor.h
#ifndef __MOTOR_H
#define __MOTOR_H
void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
(5)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "Key.h"
#include "OLED.h"
#include "Motor.h"
uint8_t KeyNum;
int8_t Speed;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Motor_Init();
Key_Init();
OLED_ShowString(1,1,"Speed:");
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
Speed += 20;
if(Speed>100)
{
Speed = -100;
}
}
Motor_SetSpeed(Speed);
OLED_ShowSignedNum(1,7,Speed,3);
}
}
5.8 TIM输入捕获
5.8.1 输入捕获简介
(1)IC(Input Capture)输入捕获。
左圈就是输入捕获、右边就是输出比较部分。4个输入捕获和输出比较通道,共用4个CCR寄存器,另外它们的CH1到CH4,4个通道的引脚,也是共用的。 所以对于同一个定时器,输入捕获和输出比较,只能使用其中一个,不能同时使用。
(2)输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。
指定电平跳变就是指上升沿或者下降沿,可以通过程序配置。发生电平跳变的瞬间,输入捕获电路会干些啥呢?会让当前CNT的值锁存到CCR中,锁存到CCR的意思就是,把当前CNT的值读出来,写入到CCR中去,这就是功能描述。
左边的4个边沿信号输入引脚,一旦有边沿,比如说上升沿,那输入滤波和边沿检测电路就会检测到这个上升沿,让输入捕获电路产生动作。所以输入滤波和边沿检测电这一块的作用和外部中断差不多,都是检测电平跳变,然后执行动作,只不过外部中断执行的动作是向CPU申请中断,而这里电路执行的动作就是控制后续电路,让当前CNT的值,锁存到CCR寄存器中。对比一下输出比较,输出比较引脚是输出端口;输入捕获,引脚是输入端口;输出比较,是根据CNT和CCR的大小关系来执行输出动作;输入捕获是接收到输入信号,执行CNT锁存到CCR的动作,这就是输入捕获的执行流程与输出比较的区别。
脉冲间隔实际上是和频率差不多的意思,电平持续时间和占空比差不多。
(3)每个高级定时器和通用定时器都拥有4个输入捕获通道。
输入捕获电路,通用定时器和高级定时器没有区别,都是一样的,然后基本定时器,没有输入捕获的功能。
(4)可配置为PWMI模式,同时测量频率和占空比。
PWMI模式就是PWM的输入模式,是专门为测量PWM频率和占空比设计的。
(5)可配合主从触发模式,实现硬件全自动测量。
PWMI模式和主从才触发模式,设计的非常巧妙,把这两个功能结合起来,测量频率占空比就是硬件全自动执行,软件不需要进行任何干预、也不需要中断。需要测量的时候,直接读取CCR寄存就行了,使用非常方便,极大减轻了软件的压力。
5.8.2 频率测量
测频法:在闸门时间T内,对上升沿计次,得到N,则频率
测周法:两个上升沿内,以标准频率fc计次,得到N ,则频率
中界频率:测频法与测周法误差相等的频率点
对于STM32测频率而言,它也是只能测量数字信号的,如果需要测量一个正弦波,那还需要搭建一个信号预处理电路,最简单的就是用运放搭一个比较器,把正弦波转换为数字信号,再输入给STM32就行了。如果测量的信号电压非常高,还要考虑一下隔离的问题,比如用一些隔离放大器、电压互感器等元件,隔离高压端和低压端,保证电路的安全,总之,经过处理,最终输入给STM32的信号,要是上图所示的高电平信号,高电平3.3V,低电平0V。
测量上图所示这样一个信号的方法:
首先为了测量频率,有两种方法可以选择,第一种是测频法,执行流程是:在闸门时间T内,对上升沿计次(计次下降沿也是可以,只是极性不同,都是一样的,之后为了方便,我们统一以上升沿为一个周期的开始进行描述),得到N,则频率。、
5.8.2.1 测评法
比如我们要测量上图框内所示部分的频率,就可以自定义一个闸门时间T,通常设置为1s,在1s时间内,对信号上升沿计次,从0开始计,每来一个上升沿,计次+1,每来一个上升沿,其实就是来了一个周期的信号,所以在1s时间内,来了多少个周期,那它的频率就是多少Hz。闸门时间也可以是2s,然后除以2;也可以是0.5秒,然后乘以2就是频率。
5.8.2.2 测周法
我们捕获信号的两个上升沿, 然后测量一下之间持续的时间就可以了。但实际上,我们并没有一个精度无穷大的秒表来测量时间,测量时间的方法,实际上也是定时器计次。我们使用一个已知的标准频率fc的计次时钟,来驱动计数器。从一个上升沿开始计,计数器从0开始,一直计到下一个上升沿,停止。计一个数的时间是,计N个数,时间就是
,
就是周期,再取个倒数,就得到了公式,
。每次捕获之后,我们都要把CNT清零一下,这样下次上升沿再捕获的时候,取出的CNT才是两个上升沿的时间间隔;这个在一次捕获后自动将NCT清零的步骤,可以用主从触发模式,自动来完成。
5.8.2.3 测频法和测周法的区别
(1)测频法适合测量高频信号,测周法适合测量低频信号。
测频法在闸门时间内最好要多出现一些上升沿,计次数量多一些,这样有助于减小误差,加入定了1s的闸门时间,结果信号频率非常低,1s时间只有寥寥无几的几个上升沿,甚至一个上升沿都没有,总不能认为频率是0,在计次N很少时,误差会非常大,所以测频法要求,信号频率要稍微高一些。
对于测周法,就要求信号频率低一些,低频信号,周期比较长,计次就会比较多,有助于减小误差;否则的话,比如标准频率为1MHz,待测信号频率太高,比如待测信号是500KHz,那在一个周期内只能计一两个数,甚至一个数也记不到,总不能认为频率无穷大,所以测周法需要待测信号频率低一些。
(2)测评法测量结果更新慢一些,数值相对稳定;测周法更新的快,数据跳变也非常快。
测频法测量的是在闸门时间内的多个周期,所以它自带一个均值滤波,如果在闸门时间内波形频率有变化,那得到的其实是在这一段时间内的平均频率,如果闸门时间选为1s,那么每隔1s才能得到一次结果,所以测评法结果更新慢,测量结果是一段时间的平均值,值比较平滑。
反观测周法,只测量一个周期,就能出一次结果,所以出结果的速度取决于待测信号的频率,一般而言,待测信号都是几百几千Hz,所以一般情况下,测周法结果更新更快;但是由于它只测量一个周期,所以结果值会受到噪声的影响,波动比较大。这就是这两种方法的基本特征对比。
高频适合测评法,低频适合测周法,那多高算高,多低算低呢?
5.8.2.4 中界频率
中界频率是测频法与测周法误差相等的频率点。
测频法计次要求计次数量N尽量要大一些,N越大,相对误差越小;因为在测频法中,计次可能会存在正负1误差,比如测评法,在闸门时间内,并不是每个周期信号都是完整的,比如在最后时间里,可能有一个周期刚出现一半,闸门时间就到了,那这就只有半个周期,只能舍弃或者当作一整个周期来看,因为计次只有整数,不可能计次0.5个数,就会出现多计一个或者少计一个,这就叫做正负1误差。
另外在测周法中,标准频率fc计次,在最后时刻,有可能一个数刚数到一半,计时就结束了,那这半个数也只能舍弃或者按一整个数来算,这里也会出现正负1误差,正负1误差是这两种方法都固有的误差。要想减小正负1误差的影响,就只能尽量多计一些数。
如果有一个频率,测频法和测周法计次的N相同,就说明误差相同,这就是中界频率了。当待测信号频率小于中界频率时,测周法误差更小,选用测周法更合适;当待测信号频率大于中界频率时,测频法误差更小,选用测频法更合适。
5.8.2.5 使用STM32实现测频
对射式红外传感器计次、定时器外部时钟,这些代码稍加改进,就是测频法;比如对射式红外传感器计次,每来一个上升沿,计次+1;那我们再用一个定时器,订一个1s的中断,在中断里,每隔1s取一下计次值,同时清0计次,为下一次做准备,这样每次读取的计次值就直接是频率。对于外部定时器时钟的代码也是如此,每隔1s取一下计次,就能实现测频法测量频率的功能了。本节使用测周法。
从左往右看,最左边是4个通道的引脚, 参考引脚定义表,就能知道这个引脚是复用在了哪个位置。然后引脚进来,有一个三输入的异或门,这个异或门的输入接在了通道1、2、3端口,异或门的执行逻辑是,当三个输入引脚的任何一个有电平翻转时,输出引脚就产生一次电平翻转,之后输出通道数据选择器,到达输入捕获通道1,数据选择器如果选择上面一个,那输入捕获通道1的输入,就是3个引脚的异或值,如果选择下面一路,异或门就没有用,4个通道各用各的引脚。设计在这个异或门,其实还是为三相无刷电机服务的,无刷电机有三个霍尔传感器检测转子的位置,可以根据转子的位置进行换相,有了这个异或门,就可以在前三个通道接上无刷电机的霍尔传感器,然后在这个定时器就作为无刷电机的接口定时器,去驱动换相电路工作。输入信号过来以后,连在了输入滤波器和边沿检测器,输入滤波器可以对信号进行滤波,避免一些高频的毛刺信号误触发;然后边沿检测器,这就和外部中断那里是一样的了,可以选择高电平触发或者电平触发,当出现指定的电平时,边沿检测电路就会触发后续电路执行动作;另外这里,它其实是设计了两套滤波和边沿检测电路,第一套电路经过滤波和极性选择得到TI1FP1(TI1 Filter Polarity 1),输入给通道1的后续电路;第二套电路,经过另一个滤波和极性选择得到TI1FP2(TI1 Filter Polarity 2),输入给通道2的后续电路。同理,下面TI2信号进来,也经过两套滤波和极性选择,得到TI2FP1和TI2FP2,TI2FP1输入给上面,TI2FP2输入给下面。在这里两个信号进来,可以选择各走各的,也可以选择进行一个交叉,让CH2引脚输入给通道1,或者CH1引脚输入给通道2.这里为什么要进行交叉连接呢,这样做的目的主要有两个,第一个,可以灵活切换后续捕获电路的输入,比如一会儿想以CH1作为输入,一会儿想以CH2作为输入,这样就可以通过这个数据选择器,灵活地进行选择。第二个目的,也是它交叉的主要目的,就是可以把一个引脚的输入,同时映射到两个捕获单元,这也是PWMI模式的经典结构。第一个捕获通道使用上升沿触发,用来捕获周期,第二个通道使用下降沿触发,用来捕获占空比。两个通道同时对1个引脚进行捕获,就可以同时测量频率和占空比,这就是PWMI模式。通道3和通道4也是一样的结构,可以选择各自独立连接,也可以选择进行交叉。另外还有一个TRC信号,也可以选择作为捕获部分的输入。输入信号经过滤波和极性选择后,就来到了预分频器,每个通道各有一个,可以选择对前面的信号进行分频,分频之后的触发信号,就可以触发捕获电路进行工作了,每来一个触发信号,CNT的值,就会向CCR转运一次,转运的同时,会发生一个捕获事件,这个事件会在状态寄存器置标志位,同时也可以产生中断。若果需要在捕获的瞬间,处理一些事情的话,就可以开启这个捕获中断。这就是整个电路的工作流程,比如我们可以配置上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为这个CNT计数器是由内部的标准时钟驱动的,所以CNT的数值,其实就可以用来记录两个上升沿之间的时间间隔,这个时间间隔,就是周期,再取个倒数,就是测周法测量的频率了。
5.8.3 输入捕获通道1框图
这个框图是上一个框图的细化结构,基本功能都是一样的。引脚进来还是先经过一个滤波器,滤波器的输入是TI1,就是CH1的引脚。输出的TI1F就是滤波后的信号,fDTS是滤波器的采样时钟来源,下面CCM1寄存器里的ICF位可以控制滤波器的参数,那这个滤波器具体怎么工作呢?
简单理解,这个滤波器工作原理就是: 以采样频率对输入信号进行采样,当连续N个值都为高电平,输出才为高电平;连续N个值都为低电平,输出才为低电平。如果信号出现高频抖动,导致连续采样N个值不全都一样,那输出就不会变化,这样就可以达到滤波的效果。采样频率越低,采样个数N越大,滤波效果越好。在实际应用中,如果波形噪声比较大,就可以把这个参数设置大一些,这样就可以过滤噪声了。
滤波之后的信号通过边沿检测器,捕获上升沿或者下降沿,用这个CCER寄存器里的CC1P位,就可以选择极性了,最终得到TI1FP1触发信号,通过数据选择器,进入通道1后续的捕获电路;实际应该还有一套一样的电路,得到TI1FP2触发信号,连通到通道2的后续电路,这里并没有画出来,同理,通道2有TI2FP1连通到通道1的后续,通道2也还有TI2,连通到通道2的后续。总共是4总连接方式,然后经过数据选择器,进入后续捕获电路部分,CC1S位可以对数据选择器进行选择,之后ICPS位,可以配置这里的分频器,可以选择不分频、2分频、4分频、8分频,最后CC1E位,控制输出使能或失能。如果使能了输出,输入端产生指定边沿信号,经过层层电路,到达ICPS,就可以让这里CNT的值,转运到CCR里。另外,每捕获一次,都要把CNT的清零一下,以便于下一次的捕获,在这里硬件电路就可以在捕获之后自动完成CNT的清零工作。如何自动清零呢?这个TI1FP1信号和TI1的边沿信号,都可以通向从模式控制器,比如TI1FP1信号的上升沿出发捕获,那通过这里,TI1FP1还可以同时触发从模式,这个从模式里就有电路可以自动完成CNT的清零。所以这个从模式就是完成自动化操作的利器。
5.8.4 主从触发模式
主从触发模式就是 主模式、从模式、触发源选择这三个功能的简称。其中主模式可以将定时器内部的信号,映射到TRGO引脚,用于触发别的外设,所以这部分叫做主模式。从模式就是接收其它外设或者自身外设的一些信号。用于控制自身定时器的运行,也就是被别的信号控制,所以这部分叫从模式。触发源选择就是选择从模式的触发信号源的,可以认为它是从模式的一部分,触发源选择,选择指定的一个信号,得到TRGI,TRGI去触发从模式,从模式可以在这个列表里,选择一项操作来自动执行,若果想要完成TI1FP1信号自动触发CNT清零,那触发源选择,就可以选中这里的TI1FP1,从模式执行的操作,就可以选择执行Reset的操作。这样TI1FP1的信号就可以自动触发从模式,从模式清零CNT,实现硬件全自动测量。
假如想实现定时器的级联,就可以选择一个定时器主模式输出更新信号到TRGO ,另一个定时器选择上一个定时触发从模式,从模式选择执行外部时钟模式1的操作。这样就能实现定时器的级联了。还有很多其它高级功能,都可以用主从触发模式来实现。主模式还可以选则复位、使能、比较脉冲和4个OCREF信号,作为TRGO的输出。
上图是从模式触发源的可选信号。
库函数里, 上图对应三个函数,调用函数,给个参数就行了。
5.8.5 输入捕获基本结构
上图所示结构只是用了一个通道,所以它目前只能测量频率。右上角是时基单元,我们把时基单元配置好,启动定时器, 那这个CNT就会在预分频之后的这个时钟驱动下,不断自增,这个CNT就是我们测周法用来计数计时的东西,经过预分频之后这个位置的时钟频率,就是驱动CNT的标准频率fc,这里不难看出,标准频率=72M/预分频系数。然后下面输入捕获通过通道1的GPIO口,输入一个方波信号,经过滤波器和边沿检测,选择TI1FP1为上升沿触发,之后输如选则直连的通道,分频器选择不分频,当TI1FP1出现上升沿之后,CNT的当前计数值转运到CCR1里,同时触发源选择,选中TI1FP1为触发信号。从模式选择复位操作,这样TI1FP1的上升沿,也会通过上面这一路,去触发CNT清零,当然这里会有个先后顺序,肯定得是先转CNT的值到CCR里去,再触发从模式给CNT清零,或者是非阻塞的同时转移。CNT的值转移到CCR,同时0转移到CNT里面去。总之肯定不会是先清零,再捕获,要不然捕获值肯定都是0了。这是这两条路二点执行逻辑。左上角的方波信号,在这里,信号出现一个上升沿,CCR1=CNT,就是把CNT的值转运到CCR1里面去,这是输入捕获自动执行的,然后CNT=0,清零计数器,这是从模式自动执行的,然后在一个周期内,CNT在标准时钟驱动下,不断自增,并且由于之前清零过了,所以CNT就是从上升沿开始,,从0开始计数,一直++,直到下一次上升沿来临。然后执行相同的操作,CCR1 = CNT,CNT = 0。CNT的值是有上限的,ARR一般设置为最大65535,那CNT最大也只能计65535个数,如果信号频率太低,CNT计数值可能会溢出;另外还有就是,从模式的触发源选择,只有TI1FP1和TI2FP2,没有TI3和TI4的信号,所以这里如果想使用从模式自动清零,就只能用通道1和通道2.对于通道3和通道4,就只能开启捕获中断,在中断里手动清零了。不过这样,程序就会处于频繁中断的状态,比较消耗软件资源。
5.8.6 PWMI基本结构
PWMI模式使用了两个通道同时捕获一个引脚,可以同时测量周期和占空比,上面这部分结构,和上面演示的一样,下面多了一个通道,首先TI1FP1配置上升沿触发,触发捕获和清零CNT,正常地捕获周期,这时我们再来一个TI1FP2,配置为下降沿触发,通过交叉通道去触发通道2的捕获单元。这是会发生什么呢?由左上图可知,最开始上升沿,CCR1捕获,同时清零CNT,之后CNT一直++,然后在下降沿这个时刻,触发CCR2捕获,所以这是CCR2的值,就是CNT从上升沿到下降沿的计数值,就是高电平期间的计数值;CCR2捕获,并不触发CNT清零,所以CNT继续++,直到下一次上升沿,CCR1捕获周期,CNT清零,这样执行之后,CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,用CCR2/CCR1就是占空比了。另外这里,可以用两个通道同时捕获第一个引脚的输入,这样通道2的前面部分就没有用到了;当然也可以配置两个通道同时捕获第二个引脚的输入,这样我们就是使用TI2FP1和TI2FP2这两个引脚了,这两个输入可以去灵活切换。
5.9 输入捕获模式测频率
5.9.1 硬件电路
PA0产生信号给PA6, 测量PA6的信号频率。如果由信号发生器的话,也可以设置成方波信号输出,高电平3.3V,低电平0V,然后直接接到PA6,当然别忘了共地。
5.9.2 软件部分
(1)复制《PWM驱动LED呼吸灯》工程改为《输入捕获模式测频率》
(2)添加驱动文件
(3)IC库函数
void TIM_DeInit(TIM_TypeDef* TIMx);
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
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_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
/*通过结构体配置输入捕获单元的函数,第一个参数选择定时器,第二个参数是包含各个配置的结构体
输入捕获和输出比较都有4个通道,ICInit4个通道共用一个函数,在结构体里会额外有一个参数,选择
具体配置哪个通道,因为可能有交叉通道的配置,所以函数合在一起比较方便*/
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
/*输入捕获的初始化函数,和上一个函数类似,都是用于初始化输入捕获单元的,但是上一个函数只是单一的配置
一个通道,这个函数可以快速配置两个通道,把外设电路配置成PPT里展示的PWMI模式*/
void TIM_BDTRConfig(TIM_TypeDef* TIMx, TIM_BDTRInitTypeDef *TIM_BDTRInitStruct);
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
/*给输入捕获结构体赋一个初始值*/
void TIM_BDTRStructInit(TIM_BDTRInitTypeDef* TIM_BDTRInitStruct);
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
void TIM_GenerateEvent(TIM_TypeDef* TIMx, uint16_t TIM_EventSource);
void TIM_DMAConfig(TIM_TypeDef* TIMx, uint16_t TIM_DMABase, uint16_t TIM_DMABurstLength);
void TIM_DMACmd(TIM_TypeDef* TIMx, uint16_t TIM_DMASource, FunctionalState NewState);
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
/*选择输入触发源TRGI,从模式的触发源选择,调用函数选择从模式的触发源*/
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
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);
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCOM(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCCDMA(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CCPreloadControl(TIM_TypeDef* TIMx, FunctionalState NewState);
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);
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);
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);
void TIM_UpdateDisableConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_UpdateRequestConfig(TIM_TypeDef* TIMx, uint16_t TIM_UpdateSource);
void TIM_SelectHallSensor(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectOnePulseMode(TIM_TypeDef* TIMx, uint16_t TIM_OPMode);
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
/*选择输出触发源TRGO,选择主模式输出的触发源*/
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
/*选择从模式*/
void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
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);
/*读写的CCR寄存器,输出比较模式下,CCR是只写的,要用SetCompare写入*/
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
/*上面4个函数分别配置通道1、2、3、4的分频器,这个参数结构体里也可以配置,是一样的效果*/
void TIM_SetClockDivision(TIM_TypeDef* TIMx, uint16_t TIM_CKD);
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
/*上面4个函数分别读取4个通道的CCR值,和上面TIM_SetCompare3是对应的,读写的都是CCR寄存器
输出比较模式下,CCR是只写的,要用SetCompare写入,输入捕获模式下,CCR是只读的,要用GetCapture读出*/
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
(4)输入捕获配置
(5)PWM.c
#include "stm32f10x.h" // Device header
/*PWM初始化函数:
第1步:RCC开启时钟,把我们需要的TIM外设和GPIO外设的时钟打开
第2步:配置时基单元,包括前面的时钟源选择和这里的时基单元,都配置好
第3步:配置输出比较单元,包括CCR的值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一配置的
第4步:配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,这个PWM和GPIO的对应关系是怎样的?可以参考引脚定义表
第5步:运行控制,启动计数器,这样就能输出PWM了
*/
void PWM_Init(void)
{
/*第1步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启挂载在APB1上的TIM2时钟
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启挂载在APB2上的AFIO时钟
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); //将TIM2的CH1通道重映射到PA15
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
// /*PA15上电后默认复用为了调试端口JTDI,所以想让它作为普通GPIO或者复用定时器通道,要先关闭调试端口的复用*/
/*第2步:选择时基单元的时钟源*/
TIM_InternalClockConfig(TIM2); //TIM2的时基单元由内部时钟来驱动
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100-1; //指定ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720-1; //指定预分频器PCS的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
/*第3步:配置输出比较单元*/
TIM_OCInitTypeDef TIM_OCInitStruct;
/*对于这个结构体变量来说,它现在是一个局部变量,如果不给它的成员赋初始值,它成员的值就是不确定的,可能回导致一些问题,
比如当要把高级定时器当作通用定时器输出PWM时,自然会把TIM2改为TIM1,这个结构体原来不用的成员现在又需要用了,而这些成员又
没有赋值,就会导致高级定时器输出PWM出现一些奇怪的问题*/
TIM_OCStructInit(&TIM_OCInitStruct); //为避免上述提到的问题,给这个结构体赋初始值
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式设置为PWM1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; //极性选择设置为高极性,有效电平是高电平,REF有效时,输出高电平
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; //输出使能
TIM_OCInitStruct.TIM_Pulse = 0; //设置CCR的值
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
/*第4步:配置GPIO*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
/*对于普通的开漏/推挽输出,引脚的控制全是来自输出数据寄存器的,如果想用定时器来控制引脚,那就需要使用复用开漏/推挽输出的模式
这里输出数据寄存器将被断开,输出控制全将转移给片上外设,只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能
通过引脚输出*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
/*第六步:运行控制,启动定时器*/
TIM_Cmd(TIM2, ENABLE);
}
/*更改占空比*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
/*改变PCS的值进而改变频率*/
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2,Prescaler,TIM_PSCReloadMode_Immediate);
}
(6)PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
void PWM_SetPrescaler(uint16_t Prescaler);
#endif
(7)IC.c
#include "stm32f10x.h" // Device header
/*初始化函数:
第1步:RCC开启时钟,把GPIO和TIM时钟打开;
第2步:GPIO初始化,把GPIO配置为输入模式,一般选择上拉输入或者浮空输入模式
第3步:配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
第4步:配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用结构体就可以统一进行配置了
第5步:选则从模式的触发源,触发源选择为TF1FP1,调用库函数给个参数就可以了
第6步:选择触发之后执行的操作,执行Reset操作,也是调用库函数即可
第7步:调用TIM_Cmd函数,开启定时器
*/
void IC_Init(void)
{
/*第1步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //开启挂载在APB1上的TIM3时钟作为输入捕获的定时器,TIM2要输出PWM
/*第2步:GPIO初始化*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; /*上拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_6);
/*第3步:选择时基单元的时钟源*/
TIM_InternalClockConfig(TIM3); //TIM2的时基单元由内部时钟来驱动
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 65536-1; //指定ARR自动重装器的值,设置大一些,防止计数溢出
TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1; //指定预分频器PCS的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
/*第4步:配置输入捕获单元*/
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; //选择TIM3的通道1
TIM_ICInitStruct.TIM_ICFilter = 0xF; //选择输入捕获的滤波器,滤除高频噪声,使信号更平缓
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; //设置边沿检测,极性选择为上升沿触发
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置分频器为不分频,我们现需要每次触发都有效
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; //配置数据选择器为直连通道
TIM_ICInit(TIM3,&TIM_ICInitStruct);
/*第5步:选则从模式的触发源*/
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
/*第6步:配置从模式为Reset*/
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
/*第7步:启动定时器*/
TIM_Cmd(TIM3,ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000/(TIM_GetCapture1(TIM3)+1); //返回一下频率,fx = fc/N,这里fc已经设置为1MHz,N是计数器的值,这里(N+1)是为了消除正负1误差
}
(8)IC.h
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
#endif
(9)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
uint8_t i;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
PWM_SetPrescaler(720-1); //Freq = 72M/(PSC+1)/(ARR+1),目前ARR固定为100
PWM_SetCompare1(50); //Duty = CCR/ARR
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
}
}
5.10 PWMI测频率占空比
5.10.1 硬件电路
5.10.2 软件部分
(1)复制《输入捕获模式测频率》工程改名为《PWMI测频率占空比》
(2)IC.c
#include "stm32f10x.h" // Device header
/*初始化函数:
第1步:RCC开启时钟,把GPIO和TIM时钟打开;
第2步:GPIO初始化,把GPIO配置为输入模式,一般选择上拉输入或者浮空输入模式
第3步:配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
第4步:配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用结构体就可以统一进行配置了
第5步:选则从模式的触发源,触发源选择为TF1FP1,调用库函数给个参数就可以了
第6步:选择触发之后执行的操作,执行Reset操作,也是调用库函数即可
第7步:调用TIM_Cmd函数,开启定时器
*/
void IC_Init(void)
{
/*第1步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //开启挂载在APB1上的TIM3时钟作为输入捕获的定时器,TIM2要输出PWM
/*第2步:GPIO初始化*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; /*上拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_6);
/*第3步:选择时基单元的时钟源*/
TIM_InternalClockConfig(TIM3); //TIM2的时基单元由内部时钟来驱动
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //指定计数模式为向上计数
TIM_TimeBaseInitStructure.TIM_Period = 65536-1; //指定ARR自动重装器的值,设置大一些,防止计数溢出
TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1; //指定预分频器PCS的值
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
/*第4步:配置输入捕获单元*/
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; //选择TIM3的通道1
TIM_ICInitStruct.TIM_ICFilter = 0xF; //选择输入捕获的滤波器,滤除高频噪声,使信号更平缓
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; //设置边沿检测,极性选择为上升沿触发
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置分频器为不分频,我们现需要每次触发都有效
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; //配置数据选择器为直连通道
TIM_ICInit(TIM3,&TIM_ICInitStruct);
/*
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2; //选择TIM3的通道2
TIM_ICInitStruct.TIM_ICFilter = 0xF; //选择输入捕获的滤波器,滤除高频噪声,使信号更平缓
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling; //设置边沿检测,极性选择为下降给沿触发
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置分频器为不分频,我们现需要每次触发都有效
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI; //配置数据选择器为交叉通道
TIM_ICInit(TIM3,&TIM_ICInitStruct);
*/
TIM_PWMIConfig(TIM3,&TIM_ICInitStruct);
//这一个函数可以实现上面注释部分的功能,自动把另一个通道初始化为相反的配置,这个函数支支持通道1和通道2的配置,不要传入通道3和通道4
/*第5步:选则从模式的触发源*/
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
/*第6步:配置从模式为Reset*/
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
/*第7步:启动定时器*/
TIM_Cmd(TIM3,ENABLE);
}
/*获取频率的函数*/
uint32_t IC_GetFreq(void)
{
return 1000000/(TIM_GetCapture1(TIM3)+1); //返回一下频率,fx = fc/N,这里fc已经设置为1MHz,N是计数器的值,这里(N+1)是为了消除正负1误差
}
/*获取占空比的函数*/
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3)+1)*100/(TIM_GetCapture1(TIM3)+1); //扩大100倍按整数显示,这个CCR总会少1个,加1补回来。
}
(3)IC.h
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GetDuty(void);
#endif
(4)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
uint8_t i;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
OLED_ShowString(2,1,"Duty:00%");
PWM_SetPrescaler(720-1); //Freq = 72M/(PSC+1)/(ARR+1),目前ARR固定为100
PWM_SetCompare1(80); //Duty = CCR/ARR
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
OLED_ShowNum(2,6,IC_GetDuty(),2);
}
}
5.11 TIM编码器接口
本节实现的功能和之前写的旋转编码器计次的功能基本都是一样的。本节代码本质上也是旋转编码器计次,只不过这个代码是通过定时器的编码器接口,来自动计次,而之前的代码是通过触发外部中断,在中断函数里手动进行计次,使用编码器接口的好处就是节约软件资源;如果使用外部中断来计次,那当电机高速旋转时,编码器每秒产生成千上万个脉冲,程序就得频繁进入中断,然后进中断的之后,完成的任务又只是简单的加一减一,软件资源就被这种简单又低级的工作给占用了,所以对于这种需要频繁执行,操作又比较简单的任务,一般都会设计一个硬件电路模块,来自动完成。本节这个编码器接口,就是用来自动给编码器计次的电路,如果每隔一段时间取一次计数值,就可以得到编码器旋转的速度了。
编码器测速一般用于电机控制的项目上,时使用PWM驱动电机,再使用编码器测量电机的速度,然后使用PID算法进行闭环控制,是一个比较常见的使用场景,一般电机旋转速度比较高,会使用无接触式的霍尔传感器或者光栅进行测速。
5.11.1 编码器接口简介
Encoder Interface 编码器接口。
编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度。
这个编码器接口其实就是相当于一个带有方向控制的外部时钟,它同时控制值CNT的计数时钟和计数方向,这样的话,CNT的值就表示了编码器的位置,如果我们每隔一段时间取一次CNT的值,再把CNT清零,那就是每次取出来的值就表示编码器的速度。这个编码器测速实际上就是测频法测正交脉冲的频率,CNT计次,然后每隔一段时间取一次计次,就是测频法的思路。只不过这个编码器计次更高级,它能根据旋转方向,不仅能自增计次,还能自减计次,是一个带方向的计次。
每个高级定时器和通用定时器都拥有1个编码器接口。
这个编码器的接口还是比较紧张的,如果一个定时器配置成了编码器接口模式,那它基本上就干不了其他活了。我们的STM32F103C8T6只有TIM1、2、3、4这四个定时器,所以最多只能接4个编码器,而且接完4个编码器,就没有定时器可用了。如果编码器比较多的话,需要考虑一下资源够不够用,不过实在不行的话,还是可以用外部中断来接编码器的,这样就是用软件资源来弥补硬件资源了。所以这里可以看出,硬件资源和软件资源是互补的,硬件资源越多,软件就会越轻松,硬件不够,那就软件来凑,比如PWM可以直接来个定时中断,然后在中断里手动计数,手动翻转电平,比如输入捕获,可以来个外部中断,然后在中断里手动把CNT取出来,放在变量里。比如编码器接口,也可以拿外部中断,然后在外部中断里,手动自增或自减计数,这都可以实现功能,什么输出比较、输入捕获、编码器接口都不需要,但是这样就是消耗软件资源了,所以一般有硬件资源的情况下,可以优先使用硬件资源,这样节约下来的软件资源,可以去干更重要的事情。
两个输入引脚借用了输入捕获的通道1和通道2。
编码器的两个输入引脚就是每个定时器的CH1和CH2引脚,CH3和CH4不能接编码器。
5.11.2 正交编码器
正交编码器一般可以测量位置,或者带有方向的速度值,它一般有两个信号输出引脚,一个是A相、一个是B相。当编码器的旋转轴转起来时,就会输出下图所示的方波信号,转的越快,方波的频率就越高,所以方波的频率就代表了速度,我们取任意一相的信号来测频率,就能直到旋转速度了。但是只有一相的信号,无法测量旋转方向,因为无论正转还是反转,它都是这样的方波。想要测量方向,还必须要有另一根线的辅助。比如可以不用这个B相,在定义一个方向输出引脚,正转置高电平,反转置低电平,这是一种解决方案,但是这样的信号并不是正交信号。另一种解决方案就是我们所说的正交信号,当正转时,A相提前B相90度;反转时A相滞后B相90度;当然这个正转1是A相提前还是A相滞后,并不是绝对的,这只是一个极性问题。毕竟正转和反转的定义也是相对的。总之就是朝一个方向转是A相提前,另一个方向是A相滞后。那使用正交信号相比较单独定义一个方向引脚的好处就是:
(1)正交信号精度更高,因为A、B相都可以计次,相当于计次频率提高了一倍;
(2)其次就是正交信号可以抗噪声,因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路,若果一个信号不变,另一个信号连选跳变,也就是产生了噪声,那这时计次值是不会变化的。
编码器接口的设计逻辑就是,首先把A相和b相的所有边沿作为计数器的计数时钟, 出现边沿信号时,就计数自增或自减,然后到底是增还是减呢?这个计数的方向由另一相的状态来确定,当出现某个边沿时,我们判断另一相的高低电平,如果对应另一相的高低状态出现在上面表里,那就是正转,计数自增;反之,另一相的状态出现在下表,那就是反转计数自减。
5.11.3 编码器接口基本情况
每个定时器只有一个编码器接口,基本定时器是没有编码器接口的。
编码器接口有两个输入端,分别要接到编码器的A相和B相, 编码器接口的两个引脚借用了输入捕获单元的前两个通道,所以最终编码器的输入引脚,就是定时器的CH1和CH2这两个引脚.其中CH1和CH2的输入捕获滤波器和边沿检测,编码器接口也有使用,但是后面的是否交叉,预分频器和CCR寄存器,与编码器接口无关。编码器接口的输出部分,其实就相当于从模式控制器了,去控制CNT的计数时钟和计数方向。在这里,我们之前一直在使用的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...但是没关系,会进行一个操作,直接把这个16位的无符号数转换为16位的有符号数,根据补码的定义,这个65535就对应-1,65534就对应-2等等。
5.11.4 工作模式
上图就是编码器接口的工作逻辑,这里TI1FP1和TI2FP2接的就是编码器的A相和B相。
5.11.5 实例(均不反相)
毛刺处就是正交编码器抗噪声的原理。
5.11.6 实例(TI1反相)
TI1反相后才是实际给编码器接口的电平。
5.12 编码器接口测速
5.12.1 硬件部分
5.12.2软件部分
(1)复制《定时器定时中断》工程改名为《编码器接口测速》
(2)添加编码器驱动文件
(3)编码器所需要的库函数
void TIM_DeInit(TIM_TypeDef* TIMx);
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
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_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_BDTRConfig(TIM_TypeDef* TIMx, TIM_BDTRInitTypeDef *TIM_BDTRInitStruct);
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_BDTRStructInit(TIM_BDTRInitTypeDef* TIM_BDTRInitStruct);
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
void TIM_GenerateEvent(TIM_TypeDef* TIMx, uint16_t TIM_EventSource);
void TIM_DMAConfig(TIM_TypeDef* TIMx, uint16_t TIM_DMABase, uint16_t TIM_DMABurstLength);
void TIM_DMACmd(TIM_TypeDef* TIMx, uint16_t TIM_DMASource, FunctionalState NewState);
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
/*第一个参数选择定时器,第二个参数选择编码器模式,然后后面两个参数分别选择通道1和通道2的电平极性*/
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);
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCOM(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectCCDMA(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_CCPreloadControl(TIM_TypeDef* TIMx, FunctionalState NewState);
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);
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);
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);
void TIM_UpdateDisableConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_UpdateRequestConfig(TIM_TypeDef* TIMx, uint16_t TIM_UpdateSource);
void TIM_SelectHallSensor(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectOnePulseMode(TIM_TypeDef* TIMx, uint16_t TIM_OPMode);
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
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);
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetClockDivision(TIM_TypeDef* TIMx, uint16_t TIM_CKD);
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
(4)Encoder.c
#include "stm32f10x.h" // Device header
/*编码器初始化函数*/
/*
第1步:RCC开启时钟,开启GPIO和定时器的时钟
第2步:配置GPIO,这里需要把PA6和PA7配置成输入模式
第3步:配置时基单元,这里预分频器我们一般选择不分频,自动重装,一般给最大65535,只需要CNT执行计数就行了
第4步:配置输入捕获单元,这里输入捕获单元只有滤波器和极性这两个参数有用
第5步:配置编码器接口模式
第6步:调用TIM_Cmd,启动定时器
电路初始化完成之后,CNT就会随着编码器旋转而自增自减,如果想测量编码器的值,直接读出CNT的值就行了;
如果想测量编码器的速度和方向,那就需要每隔一段固定的闸门时间,取出一次CNT,然后再把CNT清零。
*/
void Encoder_Init(void)
{
/*第1步:RCC开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //开启挂载在APB1上的TIM3时钟作为输入捕获的定时器
/*第2步:GPIO初始化*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
/*上拉输入(可以选择上拉、下拉或者浮空:根据接在这个引脚的外部模块输出的默认电平,如果外部模块空闲默认输出高电平,就选择上拉输入,
默认输入高电平,如果外部模块空闲默认输出低电平,配置下拉输入,默认输入低电平,和外部模块保持默认状态一致,防止默认电平打架;
如果不确定外部模块输出的默认状态或者外部信号输出功率非常小,这时就尽量选择浮空输入,没有上拉电阻和下拉电阻去影响外部信号,缺点就是
当引脚悬空时,没有默认的电平了,输入就会受噪声干扰,来回不断地跳变)*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_6 | GPIO_Pin_7);
/*第3步:选择时基单元的时钟源*/
//TIM_InternalClockConfig(TIM3); //不需要了,编码器接口会托管时钟,编码器接口就是一个带方向的外部时钟,所以这个内部时钟就没有用了
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 指定时钟划分为1分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //这个参数也没有作用了,因为计数方向也是被编码器接口托管了
TIM_TimeBaseInitStructure.TIM_Period = 65536-1; //指定ARR自动重装器的值,设置大一些,计数范围最大,而且方便换算为负数
TIM_TimeBaseInitStructure.TIM_Prescaler = 1-1; //这里不分频,编码器时钟直接驱动计数器
/*配置上面两个参数使得定时1s,也就是定时频率为1Hz,然后它们的取值都在0~65535之间*/
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //指定重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
/*第4步:配置输入捕获单元*/
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct); //因为有两个参数用不到,所以先默认初始化一下
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; //选择TIM3的通道1
TIM_ICInitStruct.TIM_ICFilter = 0xF; //选择输入捕获的滤波器,滤除高频噪声,使信号更平缓
// TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
// /*这里的上升沿参数代表的是高电平极性不反转
// 和下面TIM_EncoderInterfaceConfig配置的是同一个寄存器,所以可以删除*/
// TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; //这个参数与编码器无关
// TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; //这个参数与编码器无关
TIM_ICInit(TIM3,&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2; //选择TIM3的通道2
TIM_ICInitStruct.TIM_ICFilter = 0xF; //选择输入捕获的滤波器,滤除高频噪声,使信号更平缓
// TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
// /*这里的上升沿参数代表的是高电平极性不反转
// 和下面TIM_EncoderInterfaceConfig配置的是同一个寄存器,所以可以删除*/
TIM_ICInit(TIM3,&TIM_ICInitStruct);
/*第5步:配置编码器接口模式*/
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
/*第二个参数使用TI1和TI2都计数,第3和第4个参数配置通道不反相,如果极性与自己想要的不一致,更改第3或第4个参数其中一个,
比如将第3个改为TIM_ICPolarity_Falling*/
/*第6步:启动定时器*/
TIM_Cmd(TIM3,ENABLE);
}
///*获取计数器的值*/
//int16_t Encoder_Get(void)
//{
// return (TIM_GetCounter(TIM3));
//}
/*测速:使用闸门时间测速*/
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3,0); //给CNT清零
return Temp;
}
(5)Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
(6)mian.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
int16_t Speed;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Timer_Init(); // 初始化定时器
Encoder_Init();
OLED_ShowString(1,1,"Speed:"); // 在1行3列显示字符串
while(1)
{
OLED_ShowSignedNum(1,7,Speed,5);
// Delay_ms(1000); //闸门时间,不建议这么做,会堵塞程序,因此使用中断去做
}
}
/*TIM2的中断函数*/
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) //判断一下中断标志位状态
{
Speed = Encoder_Get(); //每隔1s读取一下速度
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除标志位
}
}