STM32学习-基本/通用/高级定时器&定时中断&秒计时代码实操&对射式红外传感器翻转电平计数&定时器输出比较功能&PWM脉宽调制&舵机/直流电机简介&PWM驱动呼吸灯
目录

为什么在72MHz计数时钟下可以实现最大59.65s的定时?
72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多,大家可以自己算一下。
详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。【最大值是65536,但计数是从0~65535】

一、定时器基本架构
1、基本定时器(TIM6\TIM7)

这个可编程定时器的主要部分是一个带有自动重装载的16位累加计数器,计数器的时钟通过一个预分频器得到。
软件可以读写计数器、自动重装载寄存器和预分频寄存器,即使计数器运行时也可以操作。
【对主模式触发DAC简单解析:我们有时想让DAC隔一段时间输出一段波形;按常规思路,可以用定时中断,设置对应时间后触发中断,在对应中断程序中写代码让DAC输出波形;但这样频繁进入中断,会干扰主程序运行;所以为定时器设计了一个主模式,使用这个主模式开源把定时器的更新事件直接映射到触发输出TRGO位置,然后TRGO直接接到DAC的触发转换引脚上;这样定时器的更新就不需要通过中断来触发DAC了。这个过程纯硬件,不需要软件参与?听不懂看下文】
时基单元包含:
- 预分频寄存器(TIMx_PSC):预分频可以以系数介于0至65535+1之间的任意数值对计数器时钟分频,就是对输入的基淮频率提前进行一个分频的操作。它是通过一个16位寄存器(TIMx-PSC)的计数实现分频。因为TIMx-PSC控制寄存器具有缓冲,可以在运行过程中改变它的数值,新的预分频数值将在下一个更新事件时起作用。【假设这个寄存器写0,就是不分频,或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz,所以预分频器的值和实际的分频系数相差了1,即实际分频系数=预分频器的值+1。】

由上图可知,ARR自动重装载值为FC,计数器计数到自动重装载值FC,在下一个时钟周期的上升沿时,计数器清零,且更新事件发生;
最下面3个时序曲线是描述预分频器的缓冲机制,我们读写预分频控制寄存器(TIMx_PSC),但它不直接控制分频系数;真正起作用的是影子寄存器-预分频缓冲器;
就像上图,我们在计数器还未到达ARR值时,把预分频器从0改为1,但它不会立即生效;而是等
本次计数周期结束,产生更新事件时,才通过缓冲器改变分频系数。
【注意:实际的设置计数器使能信号CNT_EN相对于CEN滞后一个时钟周期。上图CNT_EN又过了一个时钟周期,计数器才开始计数】
-
计数器寄存器(TIMx_CNT):计数器由预分频输出频率CK_CNT驱动,设置TIMx_CR1寄存器中的计数器使能位(CEN)使能计数器计数。这个计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升滑,计数器的值就加1,由于这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了。

【计数器溢出频率:就是计数器更新事件频率,倒数就是产生一次跟新事件的时间。上图的跟新中断标志位是要我们手动软件清零的】
从上图我们也可以理解为什么ARR要+1:
计数器到0036时要更新了,则到下一次时钟发生时,计数器变为了0000,但对应定时器时钟是第一个周期上升沿,定时器时钟第一个时钟周期对应计数器0000,第二个时钟周期对应计数器0001;计数器永远比时钟周期少一个;假设我们现在设ARR=9,那么对应10个时钟周期才到9;所以计算计数器溢出频率时,要ARR+1。
-
自动重裝载寄存器(TIMx_ARR):自动重装载寄存器是预加载的,每次读写自动重装载寄存器时,实际上是通过读写预加载寄存器实现。根据TIMx CR1寄存器中的自动重装载预加载使能位(ARPE)【和预分频器不同,这个ARR缓冲寄存器用不用是可以设置的】,写入预加载寄存器的内容能够立即或在每次更新事件时,传送到它的影子寄存器。当TIMx CR1寄存器的UDIS位为’0’,则每当计数器达到溢出值时,硬件发出更新事件;软件也可以产生更新事件;关于更新事件的产生,随后会有详细的介绍。

2、通用定时器(TIM2、3、4、5)
通用计时器库函数及其描述:

通用定时器基本架构:

关于图中引脚对应可以参考引脚定义图:

上面红框所标出来的意思:这个TIM2的CH1和ETR脚都复用在PA0引脚,下面还有CH2、CH3、CH4(CH是通道)和其他定时器的一些引脚,也都可以在这里找到。

上面由红框标出来的寄存器是捕获/比较寄存器,是输入捕获和输出比较电路共用的,因为输入捕获和输出比较不能同时使用,所以这里的寄存器是共用的,引脚也是共用的。
通用定时器基本架构解析:

像这样带一个黑色阴影的寄存器,都是有影子寄存器这样的的缓冲机制的,包括预分频器,自动重装寄存器和下面的捕获比较寄存器,所以计数的这个ARR自动重装寄存器,也是有一个缓冲寄存器的,并且这个缓冲寄存器是用还是不用,是可以自己设置的
时钟源的输入——时钟源
预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以你可以直接认为时基单元直接连到了输入端,也就是内部时钟CK_INT。内部时钟的来源是RCC_TIMXCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72M。
计数器的时钟由内部时钟(CK_INT)提供。TIMx CR1寄存器的CEN位和TIMx EGR寄存器的UG位是实际的控制位, (除了UG位被自动清除外)只能通过软件改变它们。一旦置CEN位为’1’,内部时钟即向预分频器提供时钟。下图示出控制电路和向上计数器在普通模式下,没有预分频器时的操作。
计数器时钟可由下列时钟源提供:
- 内部时钟(CK_INT)
- 外部时钟模式1:外部输入脚(TIx)
- 外部时钟模式2:外部触发输入(ETR)
- 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器。
- 【注:编码器接口可以读取编码器的输出波形】
时钟源详细说明:
- 内部时钟(CK_INT):


- 外部时钟模式1:外部输入脚(TIx)
当TIMx_SMCR寄存器的SMS=111时,此模式被选中。计数器可以在选定输入端的每个上升沿或下降沿计数。

当这个TRGI当做外部时钟来使用的时候,这一路就叫做“外部时钟模式1”,那通过这一路的外部时钟都有哪些呢?
- 第一个,就是ETR引脚的信号

- 然后第二个,就是ITR信号,这一部分的时钟信号是来自其他定时器,从右边可以看出,这个主模式的输出TRGO可以通向其他定时器,那通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了。

这个ITRO到ITR3分别来自其他4个定时器的TRGO输出,至于具体的连接方式是怎么的,手册的这个位置有一张表。这里可以看到,TIM2的ITRO是接在了TIM1的TRGO上,ITR1接在了TIM8,ITR2接在了TIM3,ITR3接在了TIM4,其他定时器也都可以参照一下这个表,这就是TR和定时器的连接关系。通过这一路我们就可以实现定时器级联的功能.比如我可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联。

- 这里还可以选择TI1F_ED,这里连接的是这里输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思,也就是通过这一路输入的时钟,上升沿和下降沿均有效:

- 最后,这个时钟还能通过TI1FP1和TI2FP2获得:


总结一下就是,外部时钟模式1的输入可以是ETR引脚、其他定时器,CH1引脚的边沿、CH1引脚和CH2引脚,这还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了。上面设置这么复杂的输入,不仅仅是为了扩大时钟输入的范围,更多的还是为了某些特殊应用场景而设计的,比如为了定时器的级联而设计的ITRx引脚,最后的一部分,我们之后讲输入捕获和测频率时,还会继续讲到。
注:对于时钟输入而言,最常用的还是内部的72MHz的时钟,如果要使用外部时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单、最直接。
- 外部时钟模式2:外部触发输入(ETR)
计数器能够在外部触发ETR的每一个上升沿或下降沿计数。
![]()
这个ETR(External)引脚的位置,可以参考一下引脚定义表。

可以看到这里有TIM2_CH1_ETR,意思就是这个TIM2的CH1和ETR都是复用在了这个位置,也就是PA0引脚,下面还有CH2,CH3,CH4和其他定时器的一些引脚,也都可以在这里找到。
那这里我们可以在这个TIM2的ETR引脚,也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这两块电路可以对外部时钟进行一定的整形。因为是外部引脚的时钟,所以难免会有的毛刺,那这些电路就可以对输入的波形进行滤波,同时也可以选择一下极性和预分频器。最后,滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了。
如果你想在ETR外部引脚提供时钟或者想对ETR时钟进行计数,把这个定时器当做计数器来用的话,那就可以配置这一路的电路,在STM32中,这一路也叫做“外部时钟模式2“。

例如,要配置在ETR下每2个上升沿计数一次的向上计数器,使用下列步骤:
- 1,本例中不需要滤波器,置TIMx_SMCR寄存器中的ETF[3:0]= 0000
- 2,设置预分频器,置TIMx_SMCR寄存器中的ETPS[1:0]= 1
- 3.设置在ETR的上升沿检测,置TIMx_SMCR寄存器中的ETP=0
- 4,开启外部时钟模式2,置TIMx_SMCR寄存器中的ECE=1
- 5.,启动计数器,置TIMx_CR1寄存器中的CEN=1
计数器在每2个ETR上升沿计数一次。
在ETR的上升沿和计数器实际时钟之间的延时取决于在ETRP信号端的重新同步电路。

内部触发输入(ITRx)(定时器同步)
所有TIMx定时器在内部相连,用于定时器同步或链接。当一个定时器处于主模式时,它可以对另一个处于从模式的定时器的计数器进行复位、启动、停止或提供时钟等操作。

- 配置定时器1为主模式,它可以在每一个更新事件UEV时输出一个周期性的触发信号。在TIM1_CR2寄存器的MMS='010’时,每当产生一个更新事件时在TRGO1上输出一个上升沿信号。
- 连接定时器1的TRGO1输出至定时器2,设置TIM2_SMCR寄存器的TS =‘000’,配置定时器2为使用ITR1作为内部触发的从模式。(为什么是‘000’,硬件底层已经根据不同选择定义好了)

- 然后把从模式控制器置于外部时钟模式1(TIM2 SMCR寄存器的SMS-111):这样定时器2即可由定时器1周期性的上升沿(即定时器1的计数器溢出)信号驱动。
- 最后,必须设置相应(TIMx_CR1寄存器)的CEN位分别启动两个定时器。如果OCx已被选中为定时器1的触发输出(MMS=1xx),它的上升沿用于驱动定时器2的计数器。【注:如果OCx已被选中为定时器1的触发输出(MMS=1xx),它的上升沿用于驱动定时器2的计数器。】
【这一段内容是涉及参考手册14.3.15的内容,关于这个模式还有更多功能,比如:使用一个定时器使能另一个定时器;使用一个定时器去启动另一个定时器;使用一个定时器作为另一个的预分频器;使用一个外部触发同步地启动2个定时器,感兴趣的可以自己去了解】
- 编码器模式
这部分电路可以把内部的一些事件映射到这个TRGO引脚上,比如我们刚才讲基本定时器分析的,将更新事件映射到TRGO,用于触发DAC。这里也是一样,它可以把定时器内部的一些事件映射到这里来,用于触发其它定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。

这部分电路可以把内部的一些事件映射到这个TRGO引脚上,比如我们刚才讲基本定时器分析的,将更新事件映射到TRGO,用于触发DAC。这里也是一样,它可以把定时器内部的一些事件映射到这里来,用于触发其它定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。

输入捕获输出比较电路粗讲

那有关输入捕获和输出比较这部分电路,在之后具体分析。
了解:通用定时器中异或门的作用

3、高级定时器(TIM1、TIM8)
水平太低了,用不到死区生成这些高大上的功能
二、定时中断
定时中断和内外时钟源选择及如何配置。

首先中间最重要的还是PSC(Prescaler)预分频器、CNT (Counter)计数器、ARR (AutoReloadRegister)自动重装器这三个寄存器构成的时基单元。下面这里是运行控制,就是控制寄存器的一些位,比如启动停止、向上或向下计数等等,我们操作这些寄存器就能控制时基单元的运行了。

左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2。在本小节示例程序里,第一个定时器定时中断就是用的内部时钟这一路,第二个定时器外部时钟就是用的外部时钟模式2这一路。当然还可以选择这里的触发输入当做外部时钟,即外部时钟模式1,对应的有ETR外部时钟、TTRX其他定时器、TlX输入捕获通道,这些就是定时器的所有可选的时钟源了。最后这里,还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。

接下来右边这里,就是计时时间到,产生更新中断后的信号去向。那这里中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。
TIM_ITITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
***为什么会有一个中断输出控制呢?******

因为这个定时器模块有很多地方都要申请中断。比如上面这个图不仅更新要申请中断,这里触发信号也会申请中断,还有下面的输入捕获和输出比较匹配时也会申请。Timer定时器只有一根线连接中断控制NVIC,所以要让NVIC知道是哪个部分要中断;这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止。简单来说,这个中断输出控制就是一个中断输出的允许位。
代码实战:定时中断和内外时钟源选择
1、定时器定时中断

- 第一步,RCC开启时钟,这个基本上每个代码都是第一步。在这里打开时钟后,定时器的基准时钟和整个外设的工作时钟就都会同时打开了
- 第二步,选择时基单元的时钟源。对于定时中断,我们就选择内部时钟源

注:没选择时钟,会默认内部时钟
然后最后一个函数,TIM_ETRConfig,这个不是用来选择时钟的,就是单独用来配置ETR引脚的预分频器、极性、滤波器这些参数的
涉及函数如下:void TIM_InternalClockConfig(TIM_TypeDef* TIMx)
作用:配置TIMx内部时钟
参数说明:
| 参数 | 说明 |
| TIMx | 所选择的 TIM 外设 |
- 第三步,配置时基单元。包括这里的预分频器、自动重装器、计数模式等等,这些参数用一个结构体就可以配置好了。涉及函数如下:
- void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
- 作用:根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx时基单元外设。参数说明:
| 参数 | 说明 |
| TIMx | 所选择的 TIM 外设 |
| TIM_TimeBaseStruct | 指向结构 TIM_TimeBaseInitTypeDef 的指针,包含了 TIMx 时间基数单位的配置信息 |

如何确定时间参数讲解?

假设定时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中断。
参数说明:
| 参数 | 说明 |
| TIMx | 所选择的 TIM 外设 |
| TIM_IT | 待使能或者失能的 TIM 中断源。该参数参考下图 |
| NewState | TIMx 中断的新状态。这个参数可以取:ENABLE 或者 DISABLE |

【注: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外设。
参数说明:
| 参数 | 说明 |
| TIMx | 所选择的 TIM 外设 |
| NewState | TIMx 中断的新状态。这个参数可以取:ENABLE 或者 DISABLE |
这样初始化基本上就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);获取当前的预分频器的值
- 最后我们再写一个定时器的中断函数。这样这个中断函数每隔一段时间就能自动执行一次了。

2、定时器外部时钟

提示:
这里推荐配置是浮空是输入,但是我一般不太喜欢浮空输入平因为一旦悬空,电平就会跳个没完,所以我准备给上拉输入,这也是可以的。
那什么时候需要用浮空输入呢?就是如果你外部的输入信号功率很小,内部的这个上拉电阻可能会影响到这个输入信号,这时就可以用一下浮空输入,防止影响外部输入的电平。
在1的基础上更改,尤其注意在第二步更改时基单元的时钟源,通过ETR引脚的外部时钟模式2配置。
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter)
作用:配置TIMx外部时钟模式2
参数说明:
| 参数 | 说明 | ||
| TIMx | 所选择的 TIM 外设 | ||
| TIM_ExtTRGPrescaler | 外部触发预分频 | ||
|
外部时钟极性(视频说这里暂时就不用滤波器了,写0x00就行了,但是根据实测,针对不同的传感器其敏感度不同,会发生噪音,还是写上值比较好) |



三、TIM输出比较
1、TIM输出比较配置
捕获/比较寄存器是输入捕获和输出比较共用的,当使用输入捕获时,它就是捕获寄存器;当使用输出比较时,它就是比较寄存器。那在输出比较这里,这块电路会比较CNT和CCR的值,CNT计数自增,CCR是我们给定的一个值,当CNT大于CCR、小于CCR或者等于CCR时,这里输出就会对应的置1、置0、置1、置0,这样就可以输出一个电平不断跳变的PWM波形了。这就是输出比较的基本功能。


使用这个PWM波形,是用来等效地实现一个模拟信号的输出
问题:数字输出端口控制LED,按理说LED只能有完全亮和完全灭两种状态,怎么能实现控制亮度大小呢?
通过PWM就可以实现,我们让LED不断点亮、熄灭、点亮、熄灭,当这个点亮、熄灭的频率足够大时,LED就不会闪烁了,而是呈现出一个中等亮度。当我们调控这个点亮和熄灭的时间比例时,就能让LED呈现出不同的亮度级别。对于电机调速也是一样。
当然,PWM的应用场景必须要是一个惯性系统,就是说LED在熄灭的时候,由于余晖和人眼视觉暂留现象,LED不会立马熄灭,而是有一定的惯性,过一小段时间才会熄灭。电机也是,当电机断电时,电机的转动不会立马停止,而是有一定的惯性,过一会才停。
那接下来我们就来具体地分析一下,定时器的输出比较模块是怎么来输出PWM波形的,我们先看一下通用定时器的这个结构。
接下来我们还需要看一下这个输出模式控制器,它具体是怎么工作的。什么时候给REF高电平,什么时候给REF低电平。我们看一下下面的这个表,这就是输出比较的8种模式,也就是这个输出模式控制器里面的执行逻辑。这个模式控制器的输入是CNT和CCR的大小关系,输出是REF的高低电平,里面可以选择多种模式来更加灵活地控制REF输出。这个模式可以通过寄存器来进行配置,具体操作看下面的表。

- 冻结:那这个模式也比较简单,它根本就不管CNT谁大谁小,直接REF保持不变、维持上一个状态就行了,这有什么用呢?比如你正在输出PWM波,突然想暂停一会儿输出,就可以设置成这个模式,一但切换为冻结模式后,输出就暂停了,并且高低电平也维持为暂停时刻的状态,保持不变。这就是冻结模式的作用
这个有效电平和无效电平,一般是高级定时器里面的一个说法,是和关断、刹车这些功能配合表述的,它说的比较严谨,所以叫有效电平和无效电平。在这里为了理解方便,你可以直接认为置有效电平就是置高电平,置无效电平就是置低电平.
- 匹配时…:这三个模式都是当CNT与CCR值相等时,执行操作。这些模式就可以用做波形输出了,比如相等时电平翻转这个模式,这个可以方便地输出一个频率可调,占空比始终为50%的PWM波形。比如你设置CCR为0,那CNT每次更新清0时,就会产生一次CNT=CCR的事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期,并且高电平和低电平的时间是始终相等的,也就是占空比始终为50%,当你改变定时器更新频率时,输出波形的频率也会随之改变。它俩的关系是输出波形的频率=更新频率/2,因为更新两次输出才为一个周期。这就是匹配时电平翻转模式的用途。

那上面这两个相等时置高电平和低电平,感觉用途并不是很大,因为它们都只是一次性的,置完高或低电平后,就不管事了,所以这俩模式不适合输出连续变化的波形。如果你想定时输出一个一次性的信号,那可以考虑一下下这两个模式。
- 强制为无效电平|有效电平:如果你想暂停波形输出,并且在暂停期间保持低电平或者高电平,那你就可以设置这两个强制输出模式。
- PWM模式1|2:它们可以用于输出频率和占空比都可调的PWM波形,也是我们主要使用的模式。这个情况比较多,一般我们都只使用向上计数,PWM模式2实际上就是PWM模式1输出的取反(改变PWM模式1和PWM模式2,就只是改变了REF电平的极性而已),是因为REF输出之后还有一个极性的配置,所以使用PWM模式1的正极性和PWM模式2的反极性最终的输出是一样的。所以使用的话,我们可以只使用PWM模式1,并且是向上计数,这一种模式就行了。
那PWM模式1向上计数是怎么输出频率和占空比都可调的PWM波形的呢?
在这里,我给出了输出PWM的基本结构,这也是我们本节课的重点内容

我们就再来看一下PWM的参数是如何计算的

高级定时器的输出比较电路了解即可。
2、代码实战:PWM的实际使用
1>呼吸灯

现象:在PA0端口接入LED,LED在不断地变换亮度,实现了一个呼吸灯的效果。

- 第一步,RCC开启时钟,把我们要用的TIM外设和GPIO外设的时钟打开
- 第二步,配置时基单元
- 第三步,配置输出比较单元,里面包括这个CCR的值、输出比较模式、极性选择、输出使能这些参数。涉及函数:
- void TIM_OCXInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct)其中TIM_OCXInit的X为1、2、3、4,对应4个输出比较单元,或者说输出比较通道。你需要初始化哪个通道,就调用哪个函数。不同的通道对应的GPIO口也是不一样的,所以这里要按照你GPIO口的需求来。这里使用的是PAO口,对应的就是第一个输出比较通道。对于TIM2来说,就是下图对应引脚。

你要使用哪个外设,就只能用对应的引脚,不过,但是虽然它是定死的,STM32还是给了我们一次更改的机会的,这就是重定义,或者叫重映射。比如如果你既要用USART2的TX引脚,又要用TIM2的CH3通道,它俩冲突成,没办法同时用,那我们就可以在这个重映射的列表里找一下,比如这里我们找到了TIM2的CH3,那TIM2的CH3就可以从原来的引脚,换到这里的引脚,这样就避免了两个外设引脚的冲突。如果这个重映射的列表里找不到,那外设复用的GPIO就不能挪位置.这就是重映射的功能,配置重映射是用AFIO来完成的,重映射在最后会讲。

作用:根据TIM_OCInitStruct中指定的参数初始化TIMx channel。
参数说明:
| 参数 | 说明 |
| TIMx | 所选择的TIM外设,对于通用定时器来说是2、3、4 |
| TIM_OCInitStruct | 指向结构 TIM_OCInitTypeDef 的指针,包含了 TIMx 时间基数单位的配置信息 |

实际上通用计时器只用到了这些结构体成员,但结构体里面还有些成员是面向高级定时器,比如:

但是如果当你中途想把高级定时器当做通用定时器输出PWM时,那你自然就会把TIM_OCXInit的TIM2改成TIM1。这样的话,这个结构体原本没有用到的成员,现在需要使用,但是对于那些成员并没有赋值,那就会导致高级定时器输出PWM出现一些奇怪的问题最终找到的原因,就是因为这里结构体成员没有配置完整。所以为了避免程序中出现不确定的因素,把结构体所有的成员都配置完整;需要么就先给结构体成员都赋一个初始值,再修改部分的结构体成员,
所以void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct)有了用武之地。
作用:TIM_OCInitStruct 中的每一个参数按缺省值填入
参数说明:
| 参数 | 说明 |
| TIMx | 所选择的TIM外设,对于通用定时器来说是2、3、4 |
| TIM_OCInitStruct | 指向结构 TIM_OCInitTypeDef 的指针,待初始化 |
- 第四步,配置GPIO.把PWM对应的GPIO口,初始化为复用推挽输出的配置。为什么选择这个模式呢?对于普通的开漏/推挽输出,引脚的控制权是来自于输出数据寄存器的

那通过刚才看到引脚定义表,我们就知道了,这里片上外设引脚连接的就是TIM2的CH1通道。所以,只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能通过引脚输出。
- 那最后,第五步,就是运行控制了.启动计数器,这样就能输出PWM了
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1)(通道1 )
作用:设置TIMx捕获比较寄存器值(CCR)
参数说明:
| 参数 | 说明 |
| TIMx | 所选择的TIM外设,对于通用定时器来说是2、3、4 |
| Compare1 | 指定捕获比较程序寄存器的新值。 |
重映射:
根据你所要重映射的引脚,在下图找到所需要的模式,比如:如果我们想把PAO改到PA15,就可以选择这个部分重映射方式1,或者完全重映射。
但是PA15在引脚定义图里没有加粗,因为它上电后已经默认复用为了调试端口JTDI,所以如果想让他作为普通的GPIO或者复用定时器的通道。那还需要先关闭调试端回的复用,也是用这个GPIO PinRemapConfig函数
如果你想让PA15、PB3、PB4这三个引脚当做GPIO来使用的话,那就加一下这里的第一句和第三句,先打开AFIO时钟,再用AFIO将JTAG复用解除掉,这样就行了;
如果你想重映射定时器或者其他外设的复用引脚,那就加一下这里的第一句和第二句,先打开AFIO时钟,再用AFIO重映射外设复用的引脚,这样就行了;
如果你重映射的引脚又正好是调试端口,那这三句就都得加上,打开AFIO时钟,重映射引脚,解除调试端口,这样才行。
遇到的问题:

今天由重新写了江协的PWM呼吸灯,碰到一个很少见的问题:
上文中我定义了一个无符号16位变量i,在进行for(i=100;i>=0;i--)时,我发现它不会出现PWM现象,而当我把i的变量类型变为无符号8位变量时,出现了现象;
马上我就想明白了,在运行for(i=100;i>=0;i--)时,会出现i=0继续减一情况,我当然认为就停止循环了,但此时i-1=65535,因为他是无符号变量,不会有负值,所以i=0时再减1会是65535,此时一方面函数PWM_SetCompare()会出现问题,还有就是至少要等65536*10ms灯才会再灭;而之所以U8类型可以,是U8最大就255,虽然被困死在for(i=100;i>=0;i--),但还可以快速赋予i在0-100的值。

再看看江协的代码,真的挑不出一点毛病,太强了,他肯定是遇到过相关问题的,不会轻易出现无“符号变量--”操作。
2>PWM驱动舵机
!!这里一定要注意正负极!!接错可能会烧坏电脑!!
接线说明:
SG90舵机,它有三根线,第一个GND,就是棕色线,接在面包板的GND;第二个5V正极,就是红色线,这里要接5V的电机电源,大家不要把它接在面包板的正极了,这个STM32芯片正极只有3.3V的电压,而且输出功率不太,带不动电机的,所以我们需要把它接在STLINK的5V输出引脚;然后看第三个引脚,PWM信号,就是橙色线,接在PA1引脚上(这里用的是PA1的通道2)【看数据手册,里面的引脚定义表,PA0的复用功能是TIM2_CH1(通道一),PA1的复用功能是TIM2_CH2(通道2)】
那最后,再在PB1接一个按键,用来控制舵机,这样这个电路就完成了。



如果单独供电的话,供电的负极要和STM32共地,然后正极接在5V供电引脚上。不同的电源需要共地
可以看出,舵机其实并不是一种单独的电机,它的内部是由直流电机驱动的,它里面还有一个控制电路板,是一个电机的控制系统。大概的执行逻辑是:PWM信号输入到控制板,给控制板一个指定的目标角度,然后,这个电位器检测输出轴的当前角度。如果大于目标角度,电机就会反转;如果小于目标角度,电机就会正转,最终使输出轴固定在指定角度,这就是舵机的内部工作流程。
棕色是电源负,红色是电源正,橙色是信号线
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();
Servo_Init();
Key_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);
}
}
PWM.C/H
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
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_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 = 0; //CCR
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare);
}
//H文件
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare2(uint16_t Compare);
#endif
Servo.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Servo_Init(void)
{
PWM_Init();
}
void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);
KeyNum = 1;
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
3>PWM驱动直流电机



main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Motor.h"
#include "Key.h"
uint8_t KeyNum;
int8_t Speed;
int main(void)
{
OLED_Init();
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);
}
}
PWM.h
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_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 = 0; //CCR
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare);
}
Motor.c/h
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Motor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
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_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare3(-Speed);
}
}
//h文件
#ifndef __MOTOR_H
#define __MOTOR_H
void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
四、输入捕获


测频法:定时器中断,并记录捕获次数;测周法:捕获中断,并记录定时器次数。
1、输入捕获电路的工作流程
由四个问题来深入输入捕获的工作流程
- 输入捕获和输出比较的区别?
- 为什么要进行一个交叉连接呢?
- 滤波器具体是怎么工作的呢?
- 如何自动清零CNT呢?
1、输入捕获和输出比较的区别?

对比一下输出比较,就是:
输出比较,引脚是输出端口;输入捕获,引脚是输入端口;
输出比较,是根据CNT和CCR的大小关系来执行输出动作;输入捕获,是接收到输入信号,执行CNT锁存到CCR的动作。
交叉连接的目的:


2、为什么要进行一个交叉连接呢?
这样做的目的,个人认为主要有两个,第一个目的,可以灵活切换后续捕获电路的输入;第二个目的,也是它交叉的主要目的,就是可以把一个引脚的输入,同时映射到两个捕获单元,这也是PWMI模式的经典结构。第一个捕获通道,使用上升沿触发,用来捕获周期,第二个通道,使用下降沿触发,用来捕获占空比。两个通道同时对一个引脚进行捕获,就可以同时测量频率和占空比,这就是PWMI模式,等会儿再来继续分析。一个通道灵活切换两个引脚,和两个通道同时捕获一个引脚,这就是这里交叉一下的作用和目的。同样,下面通道3和通道4,也是一样的结构,可以选择各自独立连接,也可以选择进行交叉。另外,这里还有一个TRC信号,也可以选择作为捕获部分的输入,这样设计,也是为了无刷电机的驱动。
到这里,电路的整个工作流程讲完了。比如我们可以配置上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为这个CNT计数器是由内部的标准时钟驱动的,所以CNT的数值,其实就可以用来记录两个上升沿之间的时间间隔,这个时间间隔,就是周期,再取个倒数,就是测周法测量的频率了。另外这里还有个细节问题,就是每次捕获之后,我们都要把CNT清0一下,这样下次上升沿再捕获的时候,取出的CNT才是两个上升沿的时间间隔,这个在一次捕获后自动将CNT清零的步骤,我们可以用主从触发模式,自动来完成。
接下来就是执行细节的问题,把电路执行的细节都了解清楚,这样写程序的时候才能得心应手。好,那接着看一下这里,这是输入捕获通道1的一个更详细的框图,基本功能都是一样的。

3、滤波器具体是怎么工作的呢?

可以看一下手册,在CCMR1寄存器这里有IC1F位,根据它的描述简单理解,这个滤波器工作原理就是:以采样频率对输入信号进行采样,当连续N个值都为高电平,输出才为高电平,连续N个值都为低电平,输出才为低电平。如果你信号出现高频抖动,导致连续采样N个值不全都一样,那输出就不会变化,这样就可以达到滤波的效果。采样频率越低,采样个数N越大说滤波效果就越好,那下面这些描述,就是每个参数对应的采样频率和采样个数。在实际应用中,如果波形噪声比较大入100,就可以把这个参数设置大一些,这样就可以过滤噪声了。

4、如何自动清零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这两个引脚了,这两个输入可以灵活切换。

2、代码实战
输入捕获模式测频率和占空比
1、输入捕获模式测频率
现象:在这里,为了测量外部信号的频率,我们先得有个信号源,产生一个频率和占空比可调的波形,但是考虑到大家可能没有信号发生器,所以我这里就借用了一下上一小节的代码。先用PWM模块,在PAO端口输出一个频率和占空比可调的波形,然后我们本节的代码,测量波形的输入口是PA6,所以我们直接用一根线,把PAO和PA6连在一起,这样就能测量自己PWM模块产生波形的频率了。
目前这个程序只能测频率,还不能测量占空比,如果想同时测量频率和占空比,STM32的输入捕获还设计了一个PWM模式,即PWM输入模式。
在6-3 PWM驱动LED呼吸灯的工程基础上写
前置操作:
PWM模块这里,我们还要再进行一些改进。目前这个代码的逻辑是初始化TIM2的通道1,产生一个PWM波形,输出引脚是PA0。然后通过SetCompare1函数,可以调节CCR1寄存器的值,从而控制PWM的占空比。但是目前PWM的频率,是在初始化里写好了的,是固定的,运行的时候调节不太方便,所以我们在最后再加一个函数,用来便捷地调节PWM频率。
如何调节PWM频率呢?
通过公式,我们知道PWM频率=更新频率=72M/(PSC+1/(ARR+1),所以PSC和ARR都可以调节频率,但是占空比=CCR/(ARR+1),所以通过ARR调节频率,还同时会影响到占空比,而通过PSC调节频率,不会影响占空比,显然比较方便。所以我们的计划是,固定ARR为100-1,通过调节PSC来改变PWM频率,另外ARR为100-1,CCR的数值直接就是占空比,用起来比较直观。
当然实际使用也是有技巧的,一般我们可以根据分辨率的要求,先确定好ARR,比如分辨率,1%就足够了;那ARR给100-1,这样PSC决定频率,CCR决定占空比。如果我想要更高的分辨率,比如0.1%,那ARR就先固定1000-1,这样频率就是72M/预分频/1000,占空比就是CCR/1000,这样也好算。
在这里,目前ARR我们固定给100-1,初始化操作的PSC就先不管,我们后面再写一个函数,在初始化之后单独修改PSC。
例如:定义一个void PWM_SetPrescaler(uint16_t Prescaler)函数,在自定义函数里面,我们就要调用库函数里单独写入PSC的函数了,TIM_PrescalerConfig,就是单独写入PSC的函数。因为这个函数还有一个重装模式的参数,所以它并不叫SetPrescaler,而叫PrescalerConfig。这是这个库的命名规范。
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode)

可能是因为手册版本太低了,并没有提到中间参数,那我们就看库里面的注释。

参数Prescaler:要写入PSC的值。
接下来就可以写输入捕获的代码
第一步,RCC开启时钟,把GPIO和TIM的时钟打开
注意:我们这个代码还需要TIM2输出PWM,所以输入捕获的定时器要换一个,我们就换到TIM3(这里在组建IC捕获模块,TIM2是PWM已经定义好的,捕获模块要重新定义一个)。其次我们这里用到的是TIM3通道1,查引脚定义表,你就知道为什么连PA6。
第二步,GPIO初始化,把GPIO配置成输入模式,一般选择上拉输入或者浮空输入模式
第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行,这一步和之前的代码是一样的
ARR自动重装值,根据之前的分析,arr越大,输入捕获越能更精准地测更小的频率,其次防止计数溢出。
72M/预分频,就是计数器自增的频率,就是计数标准频率。这个需要根据你信号频率的分布范围来调整,我暂时先给72-1,这样标准频率就是72M/72=1MHz。
第四步,配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用一个结构体就可以统一进行配置了

第五步,选择从模式的触发源。触发源选择为TI1FP1,这里调用一个库函数,给一个参数就行了
第六步,选择触发之后执行的操作。执行Reset操作,这里也是调用一个库函数就行了

最后,当这些电路都配置好之后,调用TIM_Cmd函数,开启定时器,这样所有的电路就能配合起来,按照我们的要求工作了。直接读取CCR寄存器,然后按照fc/N,(N是读取CCR的值)计算一下就行了。这就是整个程序的思路
fc=72M/(PSC+1)
这个程序的大概流程就是用一个1MHZ的信号源(是TIM2_CH1的输入捕获信号)求测量一个PWM信号一个周期(两个上升沿)的时间(就是信号源CNT自增次数),再用测周法公式计算出PWM的频率
IC.h
#include "stm32f10x.h" // Device header
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM2) + 1);
}
void IC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
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);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period=65536-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=72-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter=0xF;
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
TIM_SelectInputTrigger(TIM2,TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM2,TIM_SlaveMode_Reset);
TIM_Cmd(TIM2,ENABLE);
//
}
PWM.h,也就是输入捕获的信号源
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period=100-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=720-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
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=0;
TIM_OC1Init(TIM3,&TIM_OCInitStructure);
TIM_Cmd(TIM3,ENABLE);
}
void PWM_SetCompare(uint16_t count)
{
TIM_SetCompare1(TIM3,count);
}
void PWM_SetPrescaler(uint16_t Psc)
{
TIM_PrescalerConfig(TIM3,Psc,TIM_PSCReloadMode_Immediate);
}
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Delay.h"
#include "IC.h"
#include "PWM.h"
extern void PWM_Init(void);
extern void PWM_SetCompare(uint16_t count);
extern void PWM_SetPrescaler(uint16_t Psc);
int main(void)
{
OLED_Init();
PWM_Init();
IC_Init();
PWM_SetCompare(50);
PWM_SetPrescaler(720-1);
while(1)
{
OLED_ShowNum(1,1,IC_GetFreq(),5);
}
}

2、 PWMI模式测频率占空比
1、输入捕获模式测频率做修改需要将输入捕获初始化的部分,需要进行一下升级,配置成两个通道同时捕获同一个引脚的模式,怎么配置呢?
两种方法:
第一种,把这个通道初始化的部分,复制一份,这个结构体定义的不要复制了。然后呢,通道1是直连输入,上升沿触发,沿用这个配置。接着下面,通道1改成通道2,直连输入,改成这个交叉输入,上升沿触发,改成下降沿触发,这样看一下,是不是就对应我们PPT的这个结构了。通道1,直连输入,上升沿触发;通道2,交叉输入,下降沿触发,这样就可以了。

第二种:库里有专门的封装函数。只针对于通道1和通道2
写一个获取占空比的函数,根据上一小节的分析,高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里,我们用CCR2/CCR1,就能得到占空比了
CCR总少1,应该是CCR从0开始计数的
[6-6] 输入捕获模式测频率&PWMI模式测频率占空比_哔哩哔哩_bilibili
最后,我们来研究一下这个测频率的性能。
首先是测频率的范围,目前我们给的标准频率是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的晶振不是那么准,在计次几百几万次之后,误差累积起来,也会造成一些影响。
使用中断实现测频率
就是为了应对一下情况:
- CNT的值是有上限的,ARR—般设置为最大65535,那CNT最大也只能计65535个数。如果信号频率太低,CNT计数值可能会溢出(因为CNT计数的快慢是根据时基单元的时钟频率而变化的,如果时钟频率很高,CNT增长非常快,如果被测信号频率太低,完全有可能CNT计满65536都不到被测信号的一个周期)。
- 另外还有就是,这个从模式的触发源选择,在这里看到,只有TI1FP1和TI2FP2,没有TI3和TI4的信号,所以这里如果想使用从模式自动清零CNT,就只能用通道1和通道2。对于通道3和通道4,就只能开启捕获中断,在中断里手动清零了;



中断服务函数流程(TIM3_IRQHandler)
注意一点:TIM3所有的中断都是在
TIM3_IRQHandler()里处理的,所以在上述中断里有两个中断判断,这是合理的;只需要:
配置中断时要开启两个中断就行了;
这是测频的核心逻辑,处理两类中断(溢出中断、捕获中断),完成频率计算,步骤如下:
1. 处理定时器更新(溢出)中断
- 检查 TIM3 是否触发更新(溢出)中断;
- 若触发:将溢出计数变量
Capture_Period加 1(记录定时器溢出次数,解决长周期脉冲测不准问题); - 清除更新中断标志位(
TIM_FLAG_Update),避免重复触发中断。
2. 处理 TIM3 通道 1(CC1)捕获中断
- 检查 TIM3 是否触发 CC1 捕获中断;
- 第一次捕获(未开始捕获:
Capture_StartFlag=0):- 定时器计数器清 0(重新开始计时);
- 溢出计数
Capture_Period、捕获值Capture_CcrValue清 0; - 置位
Capture_StartFlag=1(标记 “已启动捕获”);
- 第二次捕获(已开始捕获:
Capture_StartFlag=1):- 读取 TIM3 通道 1 捕获寄存器值到
Capture_CcrValue(记录单次捕获的计数值); - 计算频率:总计时数 = 溢出次数 ×(65535+1) + (捕获值 + 1),频率 = 1MHz ÷ 总计时数(定时器时钟为 1MHz),结果存入
oscilloscope.gatherFreq; - 复位
Capture_StartFlag=0,置位Capture_FinishFlag=1(标记 “本次捕获完成”);
- 读取 TIM3 通道 1 捕获寄存器值到
- 清除 CC1 捕获中断标志位,避免重复触发中断。
总结(核心关键点)
- 核心逻辑:通过 “两次上升沿捕获”+“溢出计数补偿”,实现外部脉冲频率的精准测量,支持短 / 长周期脉冲(长周期靠溢出次数补充计时);
- 关键细节:
- 频率计算时
+1是因为定时器计数器从 0 开始计数,需补全实际时钟周期数; - 代码存在两处关键问题:① 中断使能时错误使用
TIM_FLAG_Update(应改为TIM_IT_Update);② 仅配置上升沿捕获且未在中断中切换边沿,实际测量的是 “上升沿到下一个上升沿” 的周期(而非上升 + 下降沿);
- 频率计算时
- 标志位作用:
Capture_StartFlag控制捕获流程(开始 / 结束),Capture_FinishFlag标记频率计算完成,Capture_Period补偿长周期溢出计时。
五、TIM编码器借口简介
1、编码器接口简介
- 编码器接口可接收增量(正交)编码器的信号,什么是正交信号?输出的两个方波信号相位相差90度,超前90度或者滞后90度,分别代表正转和反转,这就是正交编码器。编码器接口可以根据编码器旋转产生的正交信号脉冲,自动控制自增或自减,从而指示编码器的位置,旋转方向和旋转速度,这就是编码器接口的工作流程,就是接收正交信号,自动执行CNT的自增或自减。最终现象就是:
- 这是一个编码器,它有两个输出,一个是A相,一个是B相,然后接入到STM32定时器的编码器接口,编码器接口自动控制定时器时基单元中的CNT计数器进行自增或自减,比如初始化之后CNT初始值为0,然后编码器右转CNT++,右转产生一个脉冲,CNT就加一次,比如右转产生10个脉冲后停下来,那么这个过程CNT就由零自增到10停下来,编码器左转CNT–,左传产生1个脉冲CNT减一次,比如我编码器在左转产生5个脉冲,那CNT就在原来10的基础上自减5停下来。编码机接口其实就相当于一个带有方向控制的外部时钟,它同时控制着CNT的计数时钟和计数方向,这样的话CNT的值就表示了编码器的位置。如果我们每隔一段时间取一次CNT的值,再把CNT清零,则每次取出来的值就表示了编码器的速度,再有上小节测频法和测周法知识点,编码器测速实际上就是测频法测正交脉冲的频率,CNT计次然后每隔一段时间取一次计次,这就是测评法的思路。只不过编码器接口计次更高级,它能根据旋转方向不仅能自增一次,还能自减一次,是一个带方向的测速。
- 每个高级定时器和通用定时器都有1个编码器接口,由此可以看出编码器接口的资源还是比较紧张的,如果一个定时器配置成了编码器接口模式,他基本上就干不了其他活了。STM32的C8T6芯片只有4个定时器,所以最多只能接4个编码器,而且接满4个编码器就没有定时器可以用了。所以如果编码器比较多的话,需要考虑一下资源是否够用,不过还是可以用外部中断来接编码器,这样就是用软件资源来弥补硬件资源了。所以这里也可以看出,硬件资源和软件资源是互补的,硬件资源越多,软件就会越轻松,硬件不够就软件来凑。比如PWM,可以直接给定时中断,然后在中断里手动计数、手动翻转电平;比如说捕获,可以给外部中断,然后在中断中手动把CNT取出来放在变量里;比如编码器接口,也可以给外部中断,然后在中断里手动自增或自减计数,这都可以实现。这样输出比较输出捕获、编码器接口都不需要,但是这样会消耗软件资源。所以一般有硬件资源的情况下,可以优先使用硬件资源,这样节约下来的软件资源可以去干更重要的事情
- 两个输入引脚借用了输入捕获的通道1和通道2
2.正交编码器

- 当编码器的旋转轴转起来时,a相和b相就会输出这样的方波信号,转的越快,这个方波的频率就越高,所以方波的频率就代表了速度,我们取出任意一项的信号来测频率,就能知道旋转速度了。但是只有一项的信号无法测量旋转方向,因为无论正转还是反转,它都是这样的方波,想要测量方向还必须要有另一根线的辅助,比如不要B相,再定义一个方向输出角,正转是高电平,反转是低电平,这是一种解决方案。如下图

- 但是这样的信号并不是正交信号。另一种解决方案就是正交信号,当正转时a相提前b相90°,反转时a相滞后b相90°。需要注意的是a相提前还是a相滞后,并不是绝对的,这只是一个极性问题,毕竟正转和反转的定义也是相对的。总之就是朝一个方向转是a相提前,另一个方向是a相滞后,使用正交信号相比较单独定义一个方向引脚有什么好处呢?首先就是正交信号精度更高,因为ab相都可以计次,相当于计次频率提高了一倍,其次就是正交信号可以抗噪声,因为正交信号两个信号必须是交替跳变的,所以可以设计一个抗噪声电路,若一个信号不变,另一个信号连续跳变,也就是产生了噪声,此时实际的计数值是不会变化的(后续详细说明)。
- 如右表,思考正交信号如何计次和区分旋转方向?首先观察一下波形的特点,在正转的时候第一个时刻a相上升沿,对应b相此时是低电平,也就是表里的第一行。继续,第二个时刻,b相上升沿,对应a相高电平,是表里的第三行。继续第三个时刻,a相下降沿,对应b相高电平,是表里的第二行,最后是b相下降沿,对应a相低电平是表里的第四行。再然后就是a相上升沿,b相低电平和第一个状态重复。所以在正转的时候,我们总结了右边这个表,当出现这些边缘时对应另一项的状态是这4种

- 再看反转,第一个时刻b相上升沿对应a相低电平,第二个时刻a相上升沿对应b相高电平。第三个时刻b相下降沿对应a相高电平,第四个时刻a相下降沿对应b相低电平,然后把这4种状态也列个表。这里就可以发现当ab相出现这些边缘时,正转和反转正好是相反的,比如a相上升沿,若正转,b相就是低电平;若反转,b相就是高电平,以此类推正反转的状态也都是相反的。

- 因此编码器接口的设计逻辑就是首先把a相和b相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,那如何判断自增还是自减?这个计数的方向由另一相的状态来确定,当出现某个边沿时,判断另一相的高低电平,如果对应另一项的状态出现在上面这个表里,那就是正转,计次自增。反之另一相的状态出现在列表里,那就是反转计次自减,这样就能实现编码器接口的功能了,这也是stm32定时器编码器接口的执行逻辑。
- 如图为定时器的框图,由此来了解编码器接口的电路是如何设计的。这里编码器接口处于定时器的位置,高级定时器和通用定时器都是一样的,每个定时器都只有一个编码器接口,基本定时器没有编码器接口。

- 编码器接口有两个输入端,分别要接到编码器的a相和b相,然后这里是两个网络标号,分别写的是TI1FP1和TI2FP2,对应的就是下面通道1、2的TI1FP1和TI2FP2。由图可以看出编码器接口的两个引脚借用了输入捕获单元的前两个通道,所以最终编码器的输入引脚就是定时器的CH1和CH2这两个引脚。信号的通路是CH1中间电路通向编码器接口,CH2通过中间电路通向编码器接口,CH3和CH4与编码器接口无关,其中CH1和CH2的输入捕获滤波器和边沿检测,编码器接口也有使用,但是后面的是否交叉、预分频器和CCR寄存器与编码器接口无关,这就是编码器接口的输入部分。编码器接口的输出部分相当于从模式控制器,去控制CNT的计数时钟和计数方向。简单来说,其输出执行流程就是按照前述方式进行的,即如果出现了边缘信号,并且对应另一项的状态为正转,则控制CNT自增,否则控制CNT自减。需要注意的是:之前使用的72MHz内部时钟和在时基单元初始化时设置的计数方向此时并不会使用,因为此时计数时钟和计数方向都处于编码器接口托管的状态,计数器的自增和自减受编码器控制
2.编码器接口基本结构

- 输入捕获的前两个通道,通过GPIO口接入编码器的A、B相,然后通过滤波器和边缘检测极性选择产生TI1FP1和TI2FP2通向编码器接口,编码器接口通过预分频器控制CNT计数器的时钟,同时编码器接口还根据编码器的旋转方向,控制CNT的计数方向。编码器正转时CNT自增,编码器反转时CNT自减。另外ARR也是有效的,一般会设置ARR为65535最大量程,这样的话利用补码的特性很容易得到负数。如CNT初始为0,正转时,CNT自增,但反转时, CNT自减,0下一个数就是65535,接着是65534、65533等,而正常应该显示-1、-2…,因此这里会进行一个操作,直接把16位的无符号数转化为16位的有符号数,根据补码来定义65535就对应-1,65534就对应-2,65533就对应-3…,这样就可以直接得到负数
3.工作模式

- 该表格描述的就是上述的编码器接口的工作逻辑。这里TI1FP1和TI2FP2接的就是编码器的A、B相,在A相和B相的上升沿或者下降沿触发计数,向上计数还是向下计数取决于边缘信号发生的时刻,另一相的电平状态即表中相对信号的电平。这里另一项电平的意思就是TI1FP1对应TI2和TI2FP2对应TI1。表中编码器分三种工作模式,分别是仅在TI1FP1下计数、仅在TI2下计数和TI1、TI2都计数。前两者对比后者的区别就是计数精度下降
- 如何判断该表合适自增何时自减?与之前表对比即可(TI2FP2同理分析)所以总结一句话就是正转的状态都向上计数,反转的状态都向下计数,这就是编码器接口执行的逻辑。然后上面两种模式同理,只在一相的边沿计数,另一相的边沿忽略,逻辑和最下面模式是一样的。一般情况下使用最下面的模式,因为这个模式计数精度最高。上面这两个模式,如果有需求可以了解

4.实例
(1)均不反相

- 如图132的TI1和TI2的时序信号和下面计数器值的变化情况,第一个状态:TI1上升沿TI2低电平,查表对应向上计数,所以计数器变高了一级。后面的这几个状态同理。
- 如下图所示。在这里TI2没有变化,但是TI1却发生多次跳变,这不符合正交编码器的信号规律:正交信号两个输出交替变化。通过表格就可以将此处的噪声过滤,TI1上升沿TI2低电平,查表得向上计数,这里自增,然后下一个状态TI1下降沿,TI2还是低电平,查表得向下计数,所以这里自减,然后TI1上升沿体TI2低电平,查表得自增。继续TI1下降沿,TI2低电平,查表自减。因此可以看出如果出现了一个引脚不变,另一个引脚连续跳变多次的毛刺信号,计数器就会加减的来回摆动,最终计数值还是原来的数,并不受毛刺噪声的影响,这就是正交编码及抗噪声的原理

- 然后继续往右看反转的波形(向后),查表可得都是向下计数,这里计数值就下降。然后TI1不动,TI2多次跳变,计数值也是来回摆动,过滤噪声,最后是正转向上计数
- 该图展示了什么时候向上技术,什么时候向下计数以及正交编码器抗噪声的原理。
(2)TI1反相

如图展示的是极性的变化对计数的影响。TI1反向是什么意思呢?如图:
- 这里TI1和TI2进来都会经过极性选择的部分,在输入捕获模式下,极性选择是选择上升沿有效还是下降沿有效,但是根据刚才的分析编码器接口显然始终都是上升沿和下降沿都有效,所以在编码器接口模式下,这里就不再是边沿的极性选择,而是高低电平的极性选择。如果我们选择上升沿的参数,就是信号直通过来,高低电平极性不反转,如果选择下降沿的参数,就是信号通过一个非门进来,高低电平极性反转,所以这里就会有两个控制极性的参数,选择要不要加一个非门反转一下极性。对于(1)小节两个信号都不反转,若此时把TI1高低电平反转一下,就是(2)的TI1反相,分析时,TI1和TI2画的是输入信号,如果直接对照上面这个表得到的计数方向就是错误的。比如第一个状态TI1上升沿TI2低电平,查表应该是向上计数,但是这里实际却是向下计数,所以TI1反相后,先把TI1高低电平取反,这才是反向后实际给编码器接口的电平。然后再查表第一个状态,应该是TI1下降沿TI2低电平,查表得到是向下计数和计数器是对应的。后续状态同理,不在赘述。
- TI1反向之后,对照(1)小节图,显然这两个图的计数方向是相反的,这个的作用就是:接编码器,发现数据的加减方向反了,此时想要正转的方向结果自减了,想反转的方向结果它自增了,这时就可以调整一下极性,把任意一个引脚反相,此时就能变成反转计数方向。除此之外,如果想改变计数方向的话,还可以直接把A、B相两个引脚换交换即可
5.编码器接口测速代码
(1)接线图

(2)配置步骤
- 第一步 RCC开启时钟。开启GPIO和定时器的时钟
- 第二步 配置GPIO。这里需要把PA6和PA7配置成输入模式
- 第三步 配置时基单元。预分频器一般选择不分频,自动重装一般给最大65535,只需要CNT执行计数即可
- 第四步 配置输入捕获单元。只需配置滤波器和极性选择器即可
- 第五步 配置编码器接口模式。直接调用一个库函数即可
- 第六步 调用TIM_Cmd启动定时器
- 电路初始化完成后,CNT就会随着编码器旋转而自增自减,若想要测量编码器的位置,直接读出CNT值即可;若想测量编码器的速度和方向,就需要每隔一段固定的闸门时间,取出一次CNT,然后清0CNT,这样就可测出速度

(3)库函数
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
//定时器编码器接口配置 参数一:选定时器 参数二:选择编码器模式 参数三、四:选择通道1、2的电平极性
(4)代码实现
本次代码需要注意一下上拉输入和下拉输入的选择原则: 一般可以看接在该引脚的外部模块输出的默认电平,若外部模块空闲默认输出高电平,选择上拉输入,默认输入高电平,反之选择下拉输入。和外部模块保持默认状态一致,防止默认电平打架。若不确定外部模块输入的默认状态,或者外部信号输出功率很小,此时尽量选择浮空输入状态
(1)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行1列显示字符串Speed:
while (1)
{
OLED_ShowSignedNum(1, 7, Speed, 5); //不断刷新显示编码器测得的最新速度
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Speed = Encoder_Get(); //每隔固定时间段读取一次编码器计数增量值,即为速度值
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
(2)Encode.c
#include "stm32f10x.h" // Device header
/**
* 函 数:编码器初始化
* 参 数:无
* 返 回 值:无
*/
void Encoder_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启TIM3的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6和PA7引脚初始化为上拉输入
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
/*输入捕获初始化*/
TIM_ICInitTypeDef TIM_ICInitStructure; //定义结构体变量
TIM_ICStructInit(&TIM_ICInitStructure); //结构体初始化,若结构体没有完整赋值
//则最好执行此函数,给结构体所有成员都赋一个默认值
//避免结构体初值不确定的问题
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择配置定时器通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //输入滤波器参数,可以过滤信号抖动
TIM_ICInit(TIM3, &TIM_ICInitStructure); //将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //选择配置定时器通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //输入滤波器参数,可以过滤信号抖动
TIM_ICInit(TIM3, &TIM_ICInitStructure); //将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
/*编码器接口配置*/
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//配置编码器模式以及两个输入通道是否反相
//注意此时参数的Rising和Falling已经不代表上升沿和下降沿了,而是代表是否反相
//此函数必须在输入捕获初始化之后进行,否则输入捕获的配置会覆盖此函数的部分配置
/*TIM使能*/
TIM_Cmd(TIM3, ENABLE); //使能TIM3,定时器开始运行
}
/**
* 函 数:获取编码器的增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回CNT后将其清零*/
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0);
return Temp;
}
(3)TIM.c
#include "stm32f10x.h" // Device header
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/* 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
关于测量编码器速度的问题


上述的代码有一个点容易让人误解,就是我的定时中断时间设置的是1S,容易让人觉得,之所以把CNT当作速度,是由于V=S/T,而T刚好为1秒;
其实不是的,之所以设置1S是因为这个程序是测量旋转编码器的,是我们手动的,速度很慢,[定时中断频率越大,速度的误差越大,因为测得太快了],所以设置1S中断检测速度没问题;但是如果电机旋转很快,1S时间CNT早就到65535,清零了;所以记住:
- 电机运动越快,定时中断/测量频率越大;但是随之而来的问题就是测量误差也会变大;
- 我们在乎的是固定比例,不在乎实际速度;
而如果我们想要实际速度,会比较烦,比较我们要路程/时间:




更多推荐










所有评论(0)