mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1287 字
3 分钟
STM32-10:TIM定时器-编码器模式
2026-07-31
2026-08-01

在编码器模式下,通过两根输入信号线,可以完成对旋转角度的检测等任务。本文章采用增量式旋转编码器。


增量型旋转编码器原理#

在增量型旋转编码器下,一般有A和B两相输出信号,当顺时针旋转时,A相和B相均输出方波,不过B相的方波比A相方波提前,则A相上升沿时对应B相高电平,A相下降沿时对应B相低电平;而逆时针旋转时,B相比A相落后,因此A相上升沿时对应B相低电平,A相下降沿时对应B相高电平。这样便可以得知当前的旋转方向,再结合A相(或者B相)的上升沿数量,得知当前旋转角度。

NOTE

A相和B相谁先谁后并不绝对,也可能根据具体原件的型号反转。此时便要在芯片配置界面中进行极性翻转等修改。

虽然可以用A相上升沿触发中断再检测B相电平信号实现计数,但高频的中断会占用过多计算资源,其他的任务反而无法正常运行。
因此,通用/高级定时器中为增量型编码器准备了专用的编码器接口。将A相和B相的信号接入到TI1和TI2中,引伸的TI1FP1和TI2FP2便接入到编码器接口,根据A相B相的信号控制计数器的增减。


目的:旋转旋转编码器时,舵机同步旋转;顺时针旋转使LED亮度增加,逆时针旋转使LED亮度减小。亮度限制在0-100之间,并将亮度实时显示在屏幕上。按下按钮可以切换LED。

芯片配置#

  1. 在左侧Timers中选择TIM1,在Combined Channels下拉选择Encoder Mode即可启用定时器的编码器模式;
  2. 在右侧的芯片引脚处,可以看到PA8PA9已经分别被设定为TIM1_CH1TIM1_CH2
  3. 在下侧的设置中,我们可以修改更多编码器模式的属性。
  • Encoder Mode:使用哪一通道的输入信号作为计数的标准,选择TI1则在TI1的上升沿/下降沿进行计数,当然也可以选择二者都计数;

  • Polarity:极性翻转,若需翻转上升沿/下降沿(高电平/低电平),则可更改此项,在其中一个通道启用后便可翻转增减;

  • Input Filter:输入滤波,写入15即可;

  • 由于无法单独进行上升沿或下降沿的检测,因此我们将预分频器设置为二分频,即将Prescaler设置为2-1

  1. 为了实现按键切换LED,再为PB15启用外部中断并添加用户标签KEY,并在NVIC中启用EXTI line[15<10>] interrupts,并调节优先级使其优先级次于Time base tick timer

代码配置#

main.c中声明宏与变量,要注意部分变量需要在main函数之外声明,之后会在stm32f1xx_it.c中使用:

#define BASE 32767 //使计数器以32767为原点,防止计数器从0减一到65535
#define INITIAL_BRIGHTNESS 50-BRIGHTNESS_STEP //初始数据和实际的INITIAL_BRIGHTNESS相差BRIGHTNESS_STEP,使程序开始进行时屏幕上显示出的信息是实际的INITIAL_BRIGHTNESS
#define INITIAL_DUTY 150-DUTY_STEP //初始数据和实际的INITIAL_DUTY相差DUTY_STEP,使程序开始进行时屏幕上显示出的信息是实际的INITIAL_BRIGHTNESS
#define MIN_BRIGHTNESS 0
#define MAX_BRIGHTNESS 255
#define MIN_DUTY 50
#define MAX_DUTY 250
#define BRIGHTNESS_STEP 1 //每个编码器脉冲时,brightness增加多少
#define DUTY_STEP -1*20 //每个编码器脉冲时,duty增加多少
#define CHANNEL_RED 2
#define CHANNEL_GREEN 1
#define CHANNEL_BLUE 0
uint16_t counter=0;
uint8_t duty=INITIAL_DUTY;//占空比转换:counter每加1,duty-=20
uint8_t brightness=INITIAL_BRIGHTNESS;//亮度转换:counter-BASE+50
uint8_t channel_index=CHANNEL_BLUE;
uint32_t channels[3]={TIM_CHANNEL_1,TIM_CHANNEL_2,TIM_CHANNEL_3};

用于启用编码器的函数:

HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);

与之前一致:

  • &htim1:操作的定时器的句柄;
  • TIM_CHANNEL_ALL:启用的通道,TIM_CHANNEL_ALL则表示启用所有的通道。

完成对定时器的启动和初始化:

HAL_Delay(20);
OLED_Init();
HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);
HAL_TIM_PWM_Start(&htim3, channels[channel_index]);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
__HAL_TIM_SET_COUNTER(&htim1, BASE+1);//初始数据和BASE相差1,用于触发更新,使程序开始进行时屏幕上就显示出信息
__HAL_TIM_SET_COMPARE(&htim3, channels[channel_index], brightness);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, duty);

while(1)中写入代码,用于在计数器发生变化时更新OLED屏幕和PWM输出:

  1. 首先,检测计数器的变化,只有计数器值产生变化时才更新LED和舵机的占空比;
  2. 检测到计数器的变化后,与原点值BASE:32767进行对比与计算即可知道有几个脉冲,以及是顺时针还是逆时针;
  3. 接着,利用counterBASE作差得到的diff,计算出期望的占空比。若占空比超出了合法范围(MIN_DUTY-MAX_DUTY),则需将其约束回范围内;
  4. 然后,更新OLED屏幕的显示状况;
  5. 最后,将计数器锚定回到原点值BASE,防止计数器数值越界。至此,完成了一次检测与修改。

具体代码如下:

while (1)
{
uint16_t counter=__HAL_TIM_GET_COUNTER(&htim1);
int16_t diff=(int16_t)(counter-BASE);
if(diff){//计数器值产生变化时:
//设置duty
int16_t newduty=duty+DUTY_STEP*diff;
if((newduty>=MIN_DUTY) && (newduty<=MAX_DUTY)){//若newduty还在MIN_DUTY(50)到MAX_DUTY(250)之间,则直接更改
duty+=DUTY_STEP*diff;
}
else if(newduty<MIN_DUTY){//若newduty已经超出了MIN_DUTY(50)的范围,说明已经达到下限,则将其设为MIN_DUTY(50)
duty=MIN_DUTY;
}
else if(newduty>MAX_DUTY){//若newduty已经超出了MAX_DUTY(250)的范围,说明已经达到下限,则将其设为MAX_DUTY(250)
duty=MAX_DUTY;
}
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3,duty);
//设置brightness
int16_t newbrightness=brightness+BRIGHTNESS_STEP*diff;
if((newbrightness>=0) && (newbrightness<=255)){//若newbrightness还在MIN_BRIGHTNESS(0)到MAX_BRIGHTNESS(255)之间,则直接更改
brightness+=BRIGHTNESS_STEP*diff;
}
else if(newbrightness<MIN_BRIGHTNESS){//若newbrightness已经超出了MIN_BRIGHTNESS(0)的范围,说明已经达到下限,则将其设为MIN_BRIGHTNESS(0)
brightness=MIN_BRIGHTNESS;
}
else if(newbrightness>MAX_BRIGHTNESS){//若newbrightness已经超出了MAX_BRIGHTNESS(255)的范围,说明已经达到下限,则将其设为MAX_BRIGHTNESS(255)
brightness=MAX_BRIGHTNESS;
}
__HAL_TIM_SET_COMPARE(&htim3, channels[channel_index],brightness);
//重新锚定计数器为BASE(32767),防止计数器越界
__HAL_TIM_SET_COUNTER(&htim1, BASE);
//OLED屏幕显示
OLED_NewFrame();
char message[20]="";
sprintf(message, "brightness:%d",brightness);
OLED_PrintString(0, 0, message, &font16x16, OLED_COLOR_NORMAL);
sprintf(message, "duty:%d(%d.%d%%)",duty,(int)(2.5+(((float)duty-MIN_DUTY)/(MAX_DUTY-MIN_DUTY)*(12.5-2.5))),(int)(10*(2.5+(((float)duty-MIN_DUTY)/(MAX_DUTY-MIN_DUTY)*(12.5-2.5))))%10);//50-250 -> 2.5-12.5
OLED_PrintString(0, 16, message, &font16x16, OLED_COLOR_NORMAL);
OLED_ShowFrame();
}
}

要实现按下按钮切换LED,需要用到之前讲过的外部中断。打开文件stm32f1xx_it.c,在这里修改中断函数EXTI15_10_IRQHandler(void)以实现使用外部中断切换LED。
但是在修改之前,还需要使用externmain.c中引入外部变量以及引入tim.h

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "tim.h"
/* USER CODE END Includes */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
extern uint8_t brightness;
extern uint8_t channel_index;
extern uint32_t channels[3];
/* USER CODE END PV */

然后,修改中断回调函数实现按键中断切换LED灯:

void EXTI15_10_IRQHandler(void)
{
/* USER CODE BEGIN EXTI15_10_IRQn 0 */
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin)==GPIO_PIN_RESET){
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin)==GPIO_PIN_RESET){
HAL_TIM_PWM_Stop(&htim3, channels[channel_index]);
channel_index=(channel_index+1)%3;
HAL_TIM_PWM_Start(&htim3, channels[channel_index]);
__HAL_TIM_SET_COMPARE(&htim3, channels[channel_index], brightness);
}
}
/* USER CODE END EXTI15_10_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(KEY_Pin);
/* USER CODE BEGIN EXTI15_10_IRQn 1 */
/* USER CODE END EXTI15_10_IRQn 1 */
}

总结#

相关函数#

HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim, uint32_t Channel);//启用编码器模式

旋转编码器原理#

通过A相和B相两根输入信号线,并借助定时器的编码器模式,可以得知当前旋转角度。

这个系列都出到第十章了啊…

STM32-10:TIM定时器-编码器模式
https://nf3eb.github.io/posts/stm32/stm32-10/
作者
NF3eb
发布于
2026-07-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录