一.旋转编码器
1.旋转编码器是把转轴角位移、转速、转向转换成数字脉冲的传感器。旋转编码器有许多类型,但今天我们介绍旋钮内最常见的旋转编码器------增量型旋转编码器。
2.增量型旋转编码器:一般有A,B两相输出信号。
(1)当旋转编码器未检测到旋转 时,A,B两相均没有电平变化,稳定输出高电平或低电平。

(2)当旋转编码器被顺时针 方向旋转时,A相与B相都会输出方波。但区别是B相方波会领先A相90度 ,即B相先产生上升沿,稍后A相产生上升沿。下降沿也是,B相先产生,A相后产生。即A相上升沿时,B相高电平;A相下降沿时,B相低电平。
当旋转编码器逆时针 旋转,B相比A相落后90度。或者说A相上升沿时,B相低电平。A相下降沿时,B相高电平。

这样,我们可以通过计数A相或者B相上升沿或者下降沿的数量 ,获得旋转编码器旋转角度 ,而且可以根据A相边沿时B相电平情况 得知当前旋转方向。
(3)注意:
~方波和 90°是理想情况下旋转编码器匀速旋转,非匀速状态下波形会根据速度的快慢有长有短,但依旧符合刚才的规律
~不同旋转方向下,A,B相谁在前谁在后,也可能根据具体元件型号的不同反过来。
以stm32上的旋钮为例,输出A,B两相信号,告诉旋钮的正反转以及旋转角度 。许多电机自带的旋转编码器,向单片机反馈当前电机的旋转角度与旋转方向,单片机可以通过单位时间内的电机旋转角度计算得到电机的旋转速度。
如何判断正反转与旋转角度呢?
解决方法一:将A,B相信号接入到GPIO口后,将A相GPIO口设置为上升沿(或下降沿)触发中断,在中断回调函数中读取B相的GPIO口的电平状态,来判断旋转方向,同时根据旋转方向对计数值+1或-1来记录脉冲数量。根据手册得出每360度输出20个脉冲,即每个脉冲代表18度。因此用脉冲数量乘以18度,可以得到真实旋转角度。当然实际中可以只看用户向哪个方向旋转的程度,不必计算真实旋转角度。通过外部中断方式处理编码器信号可以,任意两个GPIO口都可以进行,处理转的非常的快电机的旋转编码器,那就会因为频繁触发中断占用太多CPU软件计算资源,导致其它任务无法正常执行。同时太快可能导致软件处理跟不上,导致丢步问题。
解决方法二:通用/高级定时器为增量型编码器准备了专门的编码器接口 ,将A,B两相信号同时输入,就可以实现正转时计数器自增,反转时计数器自减 。注意编码器接口对上下边沿都敏感,对A,B相上的一组脉冲会计数两次 。比如A相上升沿时,B相是高电平,计数器+1;A相是下降沿时,B相是低电平,计数器又+1。反向时也一样。那我们如何将A,B相信号输入到编码器接口呢?编码器两个输入接口就是TI1FP1与TI2FP2 ,即将A,B相信号接入到定时器的通道1与通道2 ,就可以接到编码器接口 ,让编码器根据A,B相信号控制计数器进行增加和减少,而且可以利用这两个通道的滤波器与边沿检测器,对A,B相信号进行初步处理。
二.CubeMX
实现旋转旋钮调节小灯亮度的功能,并且可以切换小灯颜色,还要在屏幕上显示小灯的亮度进度条。
1.打开Serial Wire

2.打开外部晶振

设置主频为72MHz

3.根据旋转编码器原理图:
发现旋转编码器的A相,B相输出分别连接到STM32的PA8,PA9

点击PA8,发现它连接到的是TIM1的通道1,PA9是TIM1的通道2

TIM1:
在组合通道Combined Channels中选择编码器模式Encoder Mode,PA8与PA9自动被设置
预分频器设置二分频,解决每次计2次数的问题。

调换正反转效果

对通道1与通道2进行设置

4.根据编码器原理图,发现其中有个按键结构,因此这旋钮可以按下,作为按键使用。按键连接的PB15设置为输入引脚,用来切换小灯颜色,我们设置用户标签为Key。
根据原理图,发现无外部上拉,我们要开启内部上拉。

5.实现亮度调节:找到三色小灯所连GPIO口对应的TIM3
勾选内部时钟源 + TIM3->PWM:


6.OLED屏幕:
打开I2C1

为每个外设生成.c/.h文件

保存并生成代码
将font.c与oled.c文件放入Src中,将font.h与oled.h放入Inc文件中
三.代码
1.完成基本起手式:


启动编码器函数:HAL_TIM_Encoder_Start(htim, Channel)



cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include"oled.h"
#include<stdio.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
MX_TIM1_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
int channel_index = 0;
uint32_t channels[3] = {TIM_CHANNEL_1, TIM_CHANNEL_2, TIM_CHANNEL_3};
HAL_Delay(20);
OLED_Init();
//启动编码器函数:
HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);//包含两个通道
//不写TIM_CHANNEL_1与TIM_CHANNEL_2,而是写TIM_CHANNEL_ALL代表所有通道
HAL_TIM_PWM_Start(&htim3, channels[channel_index]);//启动TIM3的PWM输出控制小灯
int count = 0;
char message[20] = "";
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
OLED_NewFrame();
count = __HAL_TIM_GET_COUNTER(&htim1);//获取计数器的值
//小灯亮度是从0-100调节,计数器是从0-65535,需要限制
count = __HAL_TIM_GET_COUNTER(&htim1);
if(count > 60000)
{
count = 0;
__HAL_TIM_SET_COUNTER(&htim1, 0);
}
else if(count > 100)
{
count = 100;
__HAL_TIM_SET_COUNTER(&htim1, 100);
}
//拼接字符串
sprintf(message, "count: %d", count);
OLED_PrintString(13, 0, message, &font16x16, OLED_COLOR_NORMAL);
//画进度条:
OLED_DrawRectangle(13, 25, 101, 12, OLED_COLOR_NORMAL);
OLED_DrawFilledRectangle(13, 26, count, 11 , OLED_COLOR_NORMAL);
//按键:
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]);
}
while(HAL_GPIO_ReadPin(Key_GPIO_Port, Key_Pin) == GPIO_PIN_RESET)
{}
}
//实现小灯跟着旋钮一起调节
__HAL_TIM_SET_COMPARE(&htim3, channels[channel_index], count);
OLED_ShowFrame();
HAL_Delay(100);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

