二、TIM定时器

简介:

1.定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断

2.16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时

3.不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口主从触发模式等多种功能

4.根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型


为什么在72MHz计数时钟下可以实现最大59.65s的定时?

72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多。

详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。【最大值是65536,但计数是从0~65535】 定时器类型

基本定时器(TIM6和TIM7)

在这里插入图片描述

这个可编程定时器的主要部分是一个带有自动重装载的16位累加计数器,计数器的时钟通过一个预分频器得到。

软件可以读写计数器、自动重装载寄存器和预分频寄存器,即使计数器运行时也可以操作。

时基单元包含:

1.预分频寄存器(TIMx_PSC)

预分频可以以系数介于1至65536之间的任意数值对计数器时钟分频,就是对输入的基淮频率提前进行一个分频的操作。它是通过一个16位寄存器(TIMx-PSC)的计数实现分频。因为TIMx-PSC控制寄存器具有缓冲,可以在运行过程中改变它的数值,新的预分频数值将在下一个更新事件时起作用。

假设这个寄存器写0,就是不分频,或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz,所以预分频器的值和实际的分频系数相差了1,即实际分频系数=预分频器的值+1。

2.计数器寄存器(TIMx_CNT)

计数器由预分频输出CK_CNT驱动,设置TIMx_CR1寄存器中的计数器使能位(CEN)使能计数器计数。这个计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加1,由于这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了。

3.自动重裝载寄存器(TIMx_ARR)

自动重装载寄存器是预加载的,每次读写自动重装载寄存器时,实际上是通过读写预加载寄存器实现。根据TIMx CR1寄存器中的自动重装载预加载使能位(ARPE),写入预加载寄存器的内容能够立即或在每次更新事件时,传送到它的影子寄存器。当TIMx CR1寄存器的UDIS位为’0’,则每当计数器达到溢出值时,硬件发出更新事件;软件也可以产生更新事件;关于更新事件的产生,随后会有详细的介绍。

通用定时器(TIM2、3、4、5)

计数器模式

在这里插入图片描述

像这样带一个黑色阴影的寄存器,都是有影子寄存器这样的缓冲机制的,包括预分频器,自动重装寄存器和下面的捕获比较寄存器,所以计数的这个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的预分频器。

高级定时器

在这里插入图片描述

最重要的是中间的时基单元。下面是运行控制:就是控制寄存器上的一些位,比如启动停止、向上或向下计数等等,操作这些寄存器就能控制时基单元的运行。

左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2,也可以选择外部时钟模式1,对应的有ETR外部时钟,TTRX其他定时器,TIX输入捕获通道,这些就是定时器的所有可用的时钟源了。最后这里还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。

接下来,时基单元和中断输出控制之间,就是计时时间到,产生更新中断后的信号去向。这里的中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。


为什么会有一个中断输出控制呢?

在这里插入图片描述

因为这个定时器模块有很多地方都要申请中断。比如图中不仅更新要申请中断,这里触发信号也会申请中断,还有下面的输入捕获和输出比较匹配时也会申请。所以这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止。简单来说,这个中断输出控制就是一个中断输出的允许位。


代码实战:定时中断和内外时钟源选择

1.定时器定时中断

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

在这里插入图片描述

void TIM_InternalClockConfig(TIM_TypeDef* TIMx)
作用:配置TIMx内部时钟,让指定的定时器(TIMx)使用内部时钟(即 APB 总线时钟)作为计数时钟源。
参数 类型 含义 示例
TIMx TIM_TypeDef* 要配置的定时器 TIM2, TIM3, TIM4, TIM5
3.配置时基单元。包括预分频器、自动重装器、计数模式等,这些参数用一个结构体就可以配置好
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
作用:根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx时基单元外设。

换句话说,这个函数决定了:

  • 定时器多久溢出一次(即中断周期);

  • 是向上数、向下数还是来回数;

  • 时钟如何被分频。

定时器溢出时间计算公式

⚙️ 定时周期(T) = (PSC + 1) × (ARR + 1) / f_TIM_CLK

其中:

  • PSC = TIM_Prescaler

  • ARR = TIM_Period

  • f_TIM_CLK = 定时器输入时钟频率

    • 若 APB1 总线倍频:f_TIMx = APB1_CLK × 2

    • 若 APB2:f_TIMx = APB2_CLK × 2

假设定时1s,也就是定时频率为1Hz,那我们就可以PSC给一个7200,ARR给一个10000,然后两个参数都再减一个1,因为预分频器和计数器都有1个数的偏差,所以这里要再减个1。然后注意这个PSC和ARR的取值都要在0~65535之间,不要超范围了

4.配置中断输出控制,允许更新中断输出到NVIC(开启更新中断到NVIC的通路)
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)
作用:启用或禁用指定的TIM中断

用于使能或失能定时器的某一个中断类型(例如更新中断、捕获中断等)。

换句话说:

  • 你用 TIM_TimeBaseInit() 设置好了定时器“多长时间溢出一次”,

  • 那么要让 CPU 收到中断,就必须调用 TIM_ITConfig() 去“打开这个中断通道”。

常见用法:TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
意思是:允许 TIM2 在溢出(更新事件)时产生中断请求。
5.配置NVIC,在NMC中打开定时器中断的通道,并分配一个优先级。这部分在上节我们也用过,流程基本是一样的
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
6.运行控制:整个模块配置完成后,我们还需要使能一下计数器。要不然计数器是不会运行的。当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断。
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
作用:启用或禁用指定的TIM外设。
Timer.c:
​
#include "stm32f10x.h"                  // Device header
​
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//开时钟
    
    TIM_InternalClockConfig(TIM2);//选内部时钟作计数源
    
    //配置时基(时钟分频 / 计数方式 / 预分频 / 重装
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    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_ClearFlag(TIM2, TIM_FLAG_Update);//在中断输出控制前先清楚,那之前可能残留的更新标志位清零,避免系统刚启动时误触发一次中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//中断输出控制
    
    //配置 NVIC、打开 TIM2 的中断入口
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
    
    //运行控制:启动定时器开始计数
    TIM_Cmd(TIM2, ENABLE);
}
main.c:
​
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
​
uint16_t Num;
​
int main(void)
{
    OLED_Init();
    Timer_Init();
    
    OLED_ShowString(1, 1, "Num:");
    
    while (1)
    {
        OLED_ShowNum(1, 5, Num, 5);
    }
}
​
//定时器中断服务函数
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)// 检查更新中断标志
    {
        Num ++; // 累加计数
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除中断标志位
    }
}
​

备注代码分析:

TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

它们都可以“清除定时器的某个状态位”, 但它们操作的寄存器位定义不同、使用时机也不同。

TIM_ClearFlag() —— 清除事件标志(用于任何场合)

TIM_ClearITPendingBit() —— 清除中断挂起标志(用于中断函数里)

项目 TIM_ClearFlag() TIM_ClearITPendingBit()
清除的对象 事件标志(SR寄存器位) 中断挂起标志(SR寄存器位)
使用场合 初始化阶段或普通逻辑中 中断服务函数(ISR)内部
意图语义 “把溢出事件状态清空” “告诉 NVIC:中断我处理完了”
是否影响 NVIC ❌ 不影响 ✅ 会让 NVIC 知道该中断已结束
典型写法 TIM_ClearFlag(TIM2, TIM_FLAG_Update); TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

2.定时器外部中断

timer.c:
​
#include "stm32f10x.h"                  // Device header
​
void Timer_Init(void)
{
    //给 TIM2 打开 APB1 外设时钟,不开时钟后续寄存器都写不进去
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    //给 GPIOA 打开 APB2 外设时钟,因为要用 PA0 当 TIM2 的 ETR 输入脚。
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    //配置GPIO口
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//把 PA0 配成 上拉输入(Input Pull-Up)。
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//选择要配置的引脚:PA0(它是 TIM2 的 ETR 引脚)。
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    //把 TIM2 设为 外部时钟模式2(ETR当时钟):
    TIM_ExtTRGPSC_OFF:ETR 预分频关(每个有效边沿都计数)
    TIM_ExtTRGPolarity_NonInverted:不反相,常用为上升沿有效
    0x0F:最大数字滤波(ETF=15),强力去抖/抗毛刺(采样窗口最宽)
    TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
    
    //配置时基单元
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;
    TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
    
    TIM_ClearFlag(TIM2, TIM_FLAG_Update);
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
    
    TIM_Cmd(TIM2, ENABLE);
}
main.c:
​
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
​
uint16_t Num;
​
int main(void)
{
    OLED_Init();
    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);
    }
}

TIM输出比较

CNT:时基单元里面的计数器 CCR:捕获/比较寄存器

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

使用这个PWM波形,是用来等效地实现一个模拟信号的输出


问题:数字输出端口控制LED,按理说LED只能有完全亮和完全灭两种状态,怎么能实现控制亮度大小呢?

通过PWM就可以实现,让LED不断电量、熄灭、点亮、熄灭,当这个点亮熄灭的频率足够大时,LED就不会闪烁了,而是呈现出一个中等亮度。当我们调控这个点亮熄灭的时间比例时,就能让LED呈现出不同的亮度级别。


那接下来我们就来具体地分析一下,定时器的输出比较模块是怎么来输出PWM波形的,我们先看一下通用定时器的这个结构。

在这里插入图片描述

接下来我们还需要看一下这个输出模式控制器,它具体是怎么工作的。什么时候给REF高电平,什么时候给REF低电平。我们看一下下面的这个表,这就是输出比较的8种模式,也就是这个输出模式控制器里面的执行逻辑。这个模式控制器的输入是CNT和CCR的大小关系,输出是REF的高低电平,里面可以选择多种模式来更加灵活地控制REF输出。这个模式可以通过寄存器来进行配置,具体操作看下面的表

1.冻结 那这个模式也比较简单,它根本就不管CNT谁大谁小,直接REF保持不变、维持上一个状态就行了,这有什么用呢?比如你正在输出PWM波,突然想暂停一会儿输出,就可以设置成这个模式,一但切换为冻结模式后,输出就暂停了,并且高低电平也维持为暂停时刻的状态,保持不变。这就是冻结模式的作用。

2.匹配时...

这三个模式都是当CNT与CCR值相等时,执行操作。 这些模式就可以用做波形输出了,比如相等时电平翻转这个模式,这个可以方便地输出一个频率可调,占空比始终为50%的PWM波形。比如你设置CCR为0,那CNT每次更新清0时,就会产生一次CNT=CCR的事件,这就会导致输出电平翻转一次,每更新两次,输出为一个周期,并且高电平和低电平的时间是始终相等的,也就是占空比始终为50%,当你改变定时器更新频率时,输出波形的频率也会随之改变。它俩的关系是输出波形的频率=更新频率/2,因为更新两次输出才为一个周期。这就是匹配时电平翻转模式的用途。 在这里插入图片描述

那上面这两个相等时置高电平和低电平,感觉用途并不是很大,因为它们都只是一次性的,置完高或低电平后,就不管事了,所以这俩模式不适合输出连续变化的波形。如果你想定时输出一个一次性的信号,那可以考虑一下下这两个模式。

3.强制为无效电平|有效电平 如果你想暂停波形输出,并且在暂停期间保持低电平或者高电平,那你就可以设置这两个强制输出模式。 4.PWM模式1|2 它们可以用于输出频率和占空比都可调的PWM波形,也是我们主要使用的模式。这个情况比较多,一般我们都只使用向上计数,PWM模式2实际上就是PWM模式1输出的取反(改变PWM模式1和PWM模式2,就只是改变了REF电平的极性而已),是因为REF输出之后还有一个极性的配置,所以使用PWM模式1的正极性和PWM模式2的反极性最终的输出是一样的。所以使用的话,我们可以只使用PWM模式1,并且是向上计数,这一种模式就行了


那PWM模式1向上计数是怎么输出频率和占空比都可调的PWM波形的呢?

在这里插入图片描述

在这里插入图片描述

代码实战:PWM的实际使用

3.PWM驱动LED呼吸灯

2.配置时基单元
3.配置输出比较单元,里面包含这个CCR的值、输出比较模式、极性选择、输出使能这些参数
void TIM_OCXInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct)
作用:配置定时器某个通道的“输出比较”特性,决定该通道在计数到某个值时如何对外输出信号。

在 STM32 中,定时器的每个通道(CH1~CH4)都可以做输出比较。 例如 TIM2 有 4 个通道,就有:

  • TIM_OC1Init()

  • TIM_OC2Init()

  • TIM_OC3Init()

  • TIM_OC4Init()

这些就是 TIM_OCxInit() 的具体实现版本。


TIM_OCXInit的X为1、2、3、4,对应4个输出比较单元,或者说输出比较通道。你需要初始化哪个通道,就调用哪个函数。不同的通道对应的GPIO口也是不一样的,所以这里要按照你GPIO口的需求来。这里使用的是PAO口,对应的就是第一个输出比较通道。对于TIM2来说,就是下图对应引脚 在这里插入图片描述

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


img

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

在这里插入图片描述

但是如果当你中途想把高级定时器当做通用定时器输出PWM时,那你自然就会把TIM_OCXInit的TIM2改成TIM1。这样的话,这个结构体原本没有用到的成员,现在需要使用,但是对于那些成员并没有赋值,那就会导致高级定时器输出PWM出现一些奇怪的问题最终找到的原因,就是因为这里结构体成员没有配置完整。所以为了避免程序中出现不确定的因素,把结构体所有的成员都配置完整;需要么就先给结构体成员都赋一个初始值,再修改部分的结构体成员, 所以

void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct)有了用武之地。
作用:给 TIM_OCInitTypeDef 结构体所有成员赋默认值。

默认值代表“关闭输出、占空比 0、高电平有效、普通定时模式”等。

相当于:

“先把配置结构体清空,然后再只改需要的部分。”

4.配置GPIO.把PWM对应的GPIO口,初始化为复用推挽输出的配置。

为什么选择这个模式呢?对于普通的开漏/推挽输出,引脚的控制权是来自于输出数据寄存器的

在这里插入图片描述

那通过刚才看到引脚定义表,我们就知道了,这里片上外设引脚连接的就是TIM2的CH1通道。所以,只有把GPIO设置成复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能通过引脚输出。

5.就是运行控制了.启动计数器,这样就能输出PWM了
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1)(通道1 )
作用:设置定时器通道 1(CH1)的比较寄存器值 CCR1。
模式 CCR1 的意义
输出比较模式 (OC) 定时器计数器 CNT 与 CCR1 相等时,产生输出事件(翻转、置位、复位等)
PWM 模式 决定 PWM 的占空比(高电平持续时间)
输入捕获模式 (IC) 存放捕获到的输入信号时刻的计数值

重映射: 根据你所要重映射的引脚,在下图找到所需要的模式,比如:如果我们想把PAO改到PA15,就可以选择这个部分重映射方式1,或者完全重映射。

在这里插入图片描述

PWM.C:
​
#include "stm32f10x.h"                  // Device header
​
void PWM_Init(void)
{
    //开时钟,使能TIM2 和 GPIOA 的外设时钟。
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
​
    //配置 PWM 引脚(PA0 = TIM2_CH1)为复用推挽输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    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 = 100 - 1;     //ARR
    TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;      //PSC
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
    
    //配置通道1为 PWM1 模式 PWM1 语义:CNT < CCR1 输出为高;否则为低(高电平有效)。
    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_OC1Init(TIM2, &TIM_OCInitStructure);
    
    //启动计数
    TIM_Cmd(TIM2, ENABLE);
}
​
//运行期调整占空比
void PWM_SetCompare1(uint16_t Compare)
{
    TIM_SetCompare1(TIM2, Compare);
}
//占空比 = CCR1 / (ARR+1)。由于 ARR=99,Compare 取 0~99 → 0%~99%。
main.c:
​
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
​
uint8_t i;
​
int main(void)
{
    OLED_Init();
    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);
        }
    }
}
  • 第一个 for:把 CCR1 从 0、1、2…调到 100 → 占空比从 0% → 100% 渐亮

  • 第二个 for:把 CCR1 从 100、99、…调到 0 → 占空比从 100% → 0% 渐暗

  • 每步停 10ms,所以上升约 101×10ms≈1.01s,下降同理,完整“呼吸”一来回约 2.02s

4.PWM驱动舵机

5.PWM驱动直流电机

TIM输入捕获

输入捕获对于PID控制算法很重要,没有输入捕获就不能完成闭环控制

输入捕获电路的工作流程

由四个问题来深入输入捕获的工作流程

  • 输入捕获和输出比较的区别?

  • 为什么要进行一个交叉连接呢?

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

  • 如何自动清零CNT呢?


1.输入捕获和输出比较的区别?

在这里插入图片描述

它们本质上都是围绕着 定时器计数器 CNT比较寄存器 CCRx 之间的关系来工作的。 只是方向相反:

  • 输出比较:我想在某个时刻做事

  • 输入捕获:我想知道某个时刻是什么

    🧩 一、定时器的“计数器机制”回顾

所有“比较/捕获”操作都是围绕 CNT 与 CCR 的比较或记录进行的。

⚙️ 二、输出比较(Output Compare)

定时器自己数到某个值(CCR)时,主动做某件事。

💡 功能定位

  • 定时器内部事件 触发;

  • 常用于:PWM输出、方波输出、定时触发信号、DAC触发、对齐同步等。

⚙️ 工作原理

  • 你预先写入一个“比较值”到 CCRx

  • 当定时器计数器 CNT == CCRx 时:

    • 产生“输出比较事件”;

    • 根据模式(Active、Toggle、PWM1、PWM2…)改变输出引脚电平;

    • 可选择是否触发中断或 DMA。

🧠 直观理解

就像“闹钟定时”:

“当时间到了 7:00(CNT=CCR),我响铃一次。”

✅ 举例:PWM 输出(输出比较模式之一)

CNT < CCR → 输出高电平  
CNT >= CCR → 输出低电平

→ 可调占空比。

⚡ 三、输入捕获(Input Capture)

外部引脚来了信号时,把当前定时器值“拍下来”。

💡 功能定位

  • 外部信号 触发;

  • 常用于:测量脉冲宽度、周期、频率、相位、时间间隔等。

⚙️ 工作原理

  • 外部信号输入到定时器通道引脚(如 TIMx_CH1);

  • 在选定边沿(上升/下降/双边)到来时:

    • 把当前 CNT 的值锁存到 CCRx

    • 产生“捕获事件”,可触发中断或 DMA;

    • 用户通过读出 CCRx 得到信号的时刻。

🧠 直观理解

就像“秒表”:

“每次外部事件发生时,我把当前时间记下来。”

✅ 举例:测频率

  1. 捕获两次上升沿,读出 CCR1(n)CCR1(n-1)

  2. 周期 = CCR1(n) - CCR1(n-1)

  3. 频率 = f_timer / 周期

📊 四、对比总结表

对比项 输出比较 (OC) 输入捕获 (IC)
触发源 定时器内部计数达到设定值 外部信号的边沿
主要功能 输出信号、PWM、定时触发 测量信号时间、频率、脉宽
寄存器 写入 CCRx(比较值) 读取 CCRx(捕获值)
方向 定时器 → 外部 外部 → 定时器
典型用途 PWM、输出脉冲、控制信号 测频、测距、红外解码、输入脉冲检测
输出引脚 TIMx_CHx 作为 输出 TIMx_CHx 作为 输入
对应模式 TIM_OCMode_* TIM_ICPolarity_*
常配合函数 TIM_OCxInit()TIM_SetCompareX() TIM_ICInit()TIM_GetCaptureX()
事件方向 “我想在某时刻做事” “我想知道某时刻是什么”

2.交叉连接的目的:

🧩 一、什么叫“交叉连接”(Cross Connection)

在 STM32 的定时器模块中, 每个定时器都有多个通道(CH1~CH4), 每个通道既可以作为输入(捕获)也可以作为输出(比较 / PWM)。

所谓 “交叉连接” , 就是让 一个通道的输入信号(例如 CH2) 被 另外一个通道的输入捕获单元(例如 CH1) 使用。

也就是说:

把定时器内部不同通道的输入捕获信号线“交叉接起来”。

STM32 允许你通过寄存器配置,让:

  • TI1(通道1的输入)接收 CH2 的信号;

  • TI2(通道2的输入)接收 CH1 的信号。

这就叫 交叉连接(Input Cross Connection)

🧠 二、为什么要“交叉连接”?(目的)

主要目的: 👉 实现双通道配合测量外部信号的“相对时间关系”。

常见应用场景:

场景 说明
🕓 测量脉冲宽度(高电平时间) 用 CH1 捕获上升沿、CH2 捕获下降沿,交叉连接可测完整脉宽
🧮 测量周期 / 频率 两个通道捕获同一信号不同边沿,计算周期
⚡ 测两路信号相位差 CH1 捕获信号A,CH2 捕获信号B,交叉连接保证时间基一致
⚙️ 编码器接口模式(正交编码器) CH1、CH2 分别接 A/B,相互交叉解码旋转方向
🔁 高级同步/互补控制 PWM 输出端可以“交叉触发”其他通道(例如互补输出的死区控制)

🧭 三、以测量脉宽为例

假设你要测一个脉冲信号的 高电平宽度

1️⃣ 信号接入 TIM2_CH1 (PA0)

  • 上升沿开始:捕获时间 T1

  • 下降沿结束:捕获时间 T2

2️⃣ 但是捕获上升和下降两个不同边沿,需要两个输入单元协作。

👉 于是使用交叉连接模式

  • 通道1 捕获上升沿;

  • 通道2 捕获下降沿;

  • 让 CH2 的输入连接到 CH1 的信号线上(TI1FP2)

配置代码通常如下:

TIM_ICInitTypeDef TIM_ICInitStructure;
​
/* 通道1 捕获上升沿 */
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
​
/* 通道2 捕获下降沿(交叉连接:TI1) */
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;  // 间接输入,交叉连接
TIM_ICInit(TIM2, &TIM_ICInitStructure);

🔍 TIM_ICSelection_IndirectTI 表示:

通道2 不接自己的 CH2 引脚,而是接通道1的输入信号线(TI1)。

这样:

  • T1(上升沿)由 CH1 捕获;

  • T2(下降沿)由 CH2 捕获;

  • 脉宽 = T2 - T1。

⚙️ 四、在寄存器层面的理解

每个通道输入信号有两个来源:

模式 含义
DirectTI 直接使用自己的输入端(TI1FP1、TI2FP2 等)
IndirectTI 使用其他通道的输入端(交叉连接)
TRC 来自触发控制信号(用于同步)

交叉连接就是把“IndirectTI”接上。

通道 DirectTI 来源 IndirectTI 来源
CH1 TI1 TI2
CH2 TI2 TI1

到这里,电路的整个工作流程讲完了。比如我们可以配置上升沿触发捕获,每来一个上升沿,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,实现硬件全自动测量,这就是主从触发模式的用途。 那有关这些信号的具体解释,可以看手册

在这里插入图片描述

然后还有几个注意事项说明一下,首先是这里CNT的值是有上限的,ARR—般设置为最大65535,那CNT最大也只能计65535个数。如果信号频率太低,CNT计数值可能会溢出(因为CNT计数的快慢是根据时基单元的时钟频率而变化的,如果时钟频率很高,CNT增长非常快,如果被测信号频率太低,完全有可能CNT计满65536都不到被测信号的一个周期)。另外还有就是,这个从模式的触发源选择,在这里看到,只有TI1FP1和TI2FP2,没有TI3和TI4的信号,所以这里如果想使用从模式自动清零CNT,就只能用通道1和通道2。对于通道3和通道4,就只能开启捕获中断,在中断里手动清零了,不过这样,程序就会处于频繁中断的状态,比较消耗软件资源,这个注意一下。


这个PWMI模式,使用了两个通道同时捕获一个引脚,可以同时测量周期和占空比。 我们来看一下,上面这部分结构,和刚才演示的一样,下面这里多了一个通道。 首先,TI1FP1配置上升沿触发,触发捕获和清零CNT,正常地捕获周期,这时我们再来一个TI1FP2,配置为下降沿触发,通过交叉通道,去触发通道2的捕获单元,这时会发生什么呢? 我们看一下左上角的这个图,最开始上升沿,CCR1捕获,同时清零CNT,之后CNT一直++,然后,在下降沿这个时刻,触发CCR2捕获,所以这时CCR2的值,就是CNT从这里到这里的计数值,就是高电平期间的计数值,CCR2捕获,并不触发CNT清零,所以CNT继续++。 在这里插入图片描述

直到下一次上升沿,CCR1捕获周期,CNT清零,这样执行之后CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,我们用CCR2/CCR1,是不是就是占空比了。这就是PWMI模式,使用两个通道来捕获频率和占空比的思路。

另外这里,你可以两个通道同时捕获第一个引脚的输入,这样通道2的前面这一部分就没有用到。

在这里插入图片描述

代码实战:输入捕获模式测频率和占空比

6.输入捕获模式测频率

现象:在这里,为了测量外部信号的频率,我们先得有个信号源,产生一个频率和占空比可调的波形,但是考虑到大家可能没有信号发生器,所以我这里就借用了一下上一小节的代码。先用PWM模块,在PAO端口输出一个频率和占空比可调的波形,然后我们本节的代码,测量波形的输入口是PA6,所以我们直接用一根线,把PAO和PA6连在一起,这样就能测量自己PWM模块产生波形的频率了。 目前这个程序只能测频率,还不能测量占空比,如果想同时测量频率和占空比,STM32的输入捕获还设计了一个PWM模式,即PWM输入模式。 在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的值。

接下来就可以写输入捕获的代码

1.RCC开启时钟,把GPIO和TIM的时钟打开

注意:我们这个代码还需要TIM2输出PWM,所以输入捕获的定时器要换一个,我们就换到TIM3(这里在组建IC捕获模块,TIM2是PWM已经定义好的,捕获模块要重新定义一个)。其次我们这里用到的是TIM3通道1,查引脚定义表,你就知道为什么连PA6。

2.GPIO初始化,把GPIO配置成输入模式,一般选择上拉输入或者浮空输入模式
3.配置时基单元,让CNT计数器在内部时钟的驱动下自增运行,这一步和之前的代码是一样的

ARR自动重装值,根据之前的分析,arr越大,输入捕获越能更精准地测更小的频率,其次防止计数溢出。 72M/预分频,就是计数器自增的频率,就是计数标准频率。这个需要根据你信号频率的分布范围来调整,我暂时先给72-1,这样标准频率就是72M/72=1MHz。

4.配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用一个结构体就可以统一进行配置了
TIM_ICInit()
5.选择从模式的触发源。触发源选择为TI1FP1,这里调用一个库函数,给一个参数就行了
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
6.选择触发之后执行的操作。执行Reset操作,这里也是调用一个库函数就行了
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
7.当这些电路都配置好之后,调用TIM_Cmd函数,开启定时器,这样所有的电路就能配合起来,按照我们的要求工作了。直接读取CCR寄存器,然后按照fc/N,(N是读取CCR的值)计算一下就行了。这就是整个程序的思路
ic.c:
​
#include "stm32f10x.h"                  // Device header
​
void IC_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    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 = 65536 - 1;       //ARR
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;       //PSC
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &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(TIM3, &TIM_ICInitStructure);
    
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
    
    TIM_Cmd(TIM3, ENABLE);
}
​
uint32_t IC_GetFreq(void)
{
    return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
​
int main(void){
    PWM_Init();   // TIM2_CH1 在 PA0 输出PWM(默认1kHz,占空比0%)
    IC_Init();    // TIM3_CH1 在 PA6 捕获周期(复位从模式)
    while(1){
        uint32_t f = IC_GetFreq(); // 读测得的频率
        // 显示/打印/用于控制
    }
}

7.PWMI模式测频率占空比

在6 输入捕获模式测频率做修改

需要将输入捕获初始化的部分,需要进行一下升级,配置成两个通道同时捕获同一个引脚的模式,怎么配置呢?

两种方法:

第一种,把这个通道初始化的部分,复制一份,这个结构体定义的不要复制了。然后呢,通道1是直连输入,上升沿触发,沿用这个配置。接着下面,通道1改成通道2,直连输入,改成这个交叉输入,上升沿触发,改成下降沿触发,这样看一下,是不是就对应我们PPT的这个结构了。通道1,直连输入,上升沿触发;通道2,交叉输入,下降沿触发,这样就可以了。 在这里插入图片描述

第二种:库里有专门的封装函数。只针对于通道1和通道2

在这里插入图片描述

TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);

写一个获取占空比的函数,根据上一小节的分析,高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里,我们用CCR2/CCR1,就能得到占空比了

uint32_t IC_GetDuty(void)
{
    return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}
​
TIM编码器接口

注意:❤️ 编码器接口 只能接定时器的通道1和通道2

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述那使用正交信号相比较单独定义一个方向引脚,有什么好处呢? 首先就是正交信号精度更高,因为A、B相都可以计次,相当于计次频率提高了一倍;其次就是正交信号可以抗噪声,因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路。如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那这时计次值是不会变化的。 在这里插入图片描述

所以我们编码器接口的设计逻辑就是,首先把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,然后到底是增还是减呢,这个计数的方向由另一相的状态来确定。当出现某个边沿时,我们判断另一相的高低电平,如果对应另一相的状态出现在上面这个表里,那就是正转,计数自增;反之,另一相的状态出现在下面这个表里那就是反转,计数自减,这样就能实现编码器接口的功能了,这也是我们STM32定时器编码器接口的执行逻辑。 接下来,我们就来看一下这个定时器的框图,看一下这个编码器接口的电路是如何设计的。

在这里插入图片描述

注意使用编码器模式的时候,我们之前一直在使用的72MHz内部时钟,和我们在时基单元初始化时设置的计数方向,并不会使用。因为此时计数时钟和计数方向都处于编码器接口托管的状态,计数器的自增和自减,受编码器控制.

然后我们看一下这里,我给出的一个编码器接口基本结构。

在这里插入图片描述

输入捕获的前两个通道,通过GPIO口接入编码器的A、B相,然后通过滤波器和边沿检测极性选择 ,产生TI1FP1和TI2FP2,通向编码器接口。编码器接口通过预分频器控制CNT计数器的时钟,同时,编码器接口还根据编码器的旋转方向,控制CNT的计数方向,编码器正转时,CNT自增,编码器反转时,CNT自减。 另外这里ARR也是有效的,一般我们会设置ARR为65535,最大量程,这样的话,利用补码的特性,很容易得到负数。比如CNT初始为0,我正转,CNT自增,0、 1、2、3、4、5、6、7等等,显示都没问题,但是我反转呢,CNT自减,0下一个数就是65535,接着是65534、65533等等这里负数不应该是-1、-2吗,65535是不是就出问题了。但是没关系,直接把这个16位的无符号数转换为16位的有符号数。根据补码的定义,这个65535就对应-1,65534就对应-2(有符号编码时负数按补码计算,2^16 的补码= -1)等等,这样就可以直接得到负数,非常方便,这就是我们读出数据得到负数的一个小技巧。

最后我们来看一些工作细节,和两个小例子。 这个工作描述的表,描述的就是我们刚才说什么时候正转、反转的,编码器接口的工作逻辑 在这里插入图片描述

在这里插入图片描述

这个实例展示的是极性的变化对计数的影响。 TI1反相是什么意思呢?

在这里插入图片描述

此时看下这个图,这里TI1和TI2进来,都会经过这个极性选择的部分。

在这里插入图片描述

在输入捕获模式下,这个极性选择是选择上升没有效还是下降沿有效的。但是根据我们刚才的分析,编码器接口,显然始终都是上升沿和下降沿都有效的,上升沿和下降沿都需要计次,所以在编码器接口模式下,这里就不再是边沿的极性选择了而是高低电平的极性选择。如果我们选择上升沿的参数,就是信号直通过来,高低电平极性不反转;如果选择下降沿的参数,就是信号通过一个非门过来,高低电平极性反转,所以这里就会有两个控制极性的参数,选择要不要在这里加一个非门,反转一下极性。

代码实战:编码器接口测速

这里编码器测速一般应用在电机控制的项目上,使用PWM驱动电机,再使用编码器测量电机的速度,然后再用PID算法进行闭环控制。 现象:接了一个旋转编码器模块,这个代码和之前我们写的旋转编码器计次的代码,实现的功能基本都是一样的。目前我们这个代码,本质上也是旋转编码器计次,只不过这个代码是通过定时器的编码器接口,来自动计次。而我们之前的代码是通过触发外部中断,然后在中断函数里手动进行计次,使用编码器接口的好处就是节约软件资源, 如果使用外部中断来计次,那当电机高速旋转时,编码器每秒产生成千上万个脉冲,程序就得频繁进中断,然后进中断之后,完成的任务又只是简单的加—减一,是不是我们的软件资源就被这种简单而又低级的工作给占用了。所以,对于这种需要频繁执行,操作又比较简单的任务,一般我们都会设计一个硬件电路模块,来自动完成。那我们本节这个编码器接口,就是用来自动给编码器进行计次的电路。如果我们每隔一段时间取一下计次值,就能得到编码器旋转的速度了。

在这里插入图片描述在这里插入图片描述

1.RCC开启时钟,开启GPIO和定时器时钟
2.配置GPIO,把PA6和PA7配置成输入模式
3.配置时基单元,这里预分频器我们一般选择不分频
4.配置输入捕获单元。不过这里输入捕获单元只有滤波器和极性这两个参数有用,后面的参数没有用到,与编码器无关
5.配置编码器接口模式。这个直接调用一个库函数就可以了
6.调用TIM_Cmd,启动定时器
encoder.c:
​
#include "stm32f10x.h"                  // Device header
​
void Encoder_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
        
    TIM_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_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICStructInit(&TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
    
    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
    
    TIM_Cmd(TIM3, ENABLE);
}
​
int16_t Encoder_Get(void)
{
    int16_t Temp;
    Temp = TIM_GetCounter(TIM3);
    TIM_SetCounter(TIM3, 0);
    return Temp;
}
​

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐