52 Pico+MicroPython 打造旋转编码器进度条,硬件 + 原理 + 代码全解析

1.旋转编码器基本知识

这里,我们使用的是 20 脉冲 20 定位点的 EC11 旋转编码器元件,结构如下:

其中,各个部件名称和作用如下:

  • 波动盘​:编码器的核心部件,通过旋转操作生成信号。盘上有等距分布的导电条,当旋转时,导电条依次与输出触点接触,从而产生脉冲信号;
  • 定位片​:定位每个旋转位置,提供机械反馈,使编码器具有清晰的转动感觉,我们使用的 EC11 具有 20 个定位点,即每旋转 18 度有一个停顿;
  • 触点​:与波动盘协同工作,检测到导电条的位置变化并产生信号;
  • 轴套和外壳​:为编码器提供物理结构支撑,通常外壳上安装有电子元件和引脚。

EC11 的引脚通常为 5 个,如下图所示:

  • A 引脚​(红色)信号输出 1:输出旋转脉冲信号
  • B 引脚​(蓝色)信号输出 2:同样输出脉冲信号,但与 A 引脚有相位差
  • C 引脚​(绿色)公共端:A 和 B 信号通道的公共接地端
  • D、E 引脚​:按压开关的两个端子,当按钮按下时,这两个引脚短接

其工作原理为,EC11 通过两个信号通道(A、B 引脚)输出旋转的增量信号,在旋转过程中,A 和 B 信号通道产生交错的脉冲,通过比较两者的相位关系,可以判断旋转的方向(顺时针或逆时针):

简单来说,就是:

  • 当是顺时针旋转时​,A 相超前 B 相 90 度,即 A 相为下降沿时,B 相为低电平;A 相为上升沿时,B 相为高电平。
  • 当是逆时针旋转时​,B 相超前 A 相 90 度,即 A 相为下降沿时,B 相为高电平;A 相为上升沿时,B 相为低电平。

不难发现,每当 A 相或 B 相发生电平跳变时,只需要检测另一相的状态,即可判断旋转方向:

  • 顺时针​:两相同时为上升沿或者同时为下降 ;
  • 逆时针​:两相不同时为上升沿或者不同时为下降沿;

同时,结合脉冲计数可以确定旋转的步数(增量)。

这里,EC11 旋转编码器还有一个按压开关(D、E 引脚):当按压编码器时,内部的机械结构触发按键信号,通常用于控制或确认操作。

2.应用实验

在以下代码中,我们自定义了一个 EC11Encoder 类,通过中断处理来读取旋转编码器的旋转步数和按键状态,在主程序循环中更新终端上的进度条,从而显示旋转编码器的旋转程度。

这里,我们需要将 风雅一号板-通用兼容扩展板 插入到 风雅一号板-LCD屏幕人机交互板 上,并且打开 SWITCH2 拨码开关上 RE-CLKRE-DTRE-SW,实物连接图如下所示:

元器件接线情况如下:

EC11 引脚功能 树莓派 Pico 引脚 说明
VCC (+3.3V) 3V3(OUT) 供电电压,接 Pico 的 3.3V 电源输出
GND GND 公共地线
A 相 GPIO26 编码器旋转信号 A,相位检测用
B 相 GPIO27 编码器旋转信号 B,相位检测用
按键 SW GPIO21 编码器按钮开关,按下时输出低电平

示例代码如下,在配套资料的文件夹:elegance-devkit v1\Demo\05 GPIO_RotaryEncoder 中:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time    : 2024/9/18 下午10:44
# @Author  : 李清水
# @File    : main.py
# @Description : GPIO类实验,读取旋转编码器的值

# ======================================== 导入相关模块 =========================================

# 导入硬件相关模块
from machine import Pin
# 导入时间相关模块
import time

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

class EC11Encoder:
    """
​    EC11 旋转编码器类,用于通过 GPIO 引脚读取旋转编码器的信号,检测旋转方向和计数,并响应按键事件。

​    该类使用 GPIO 引脚读取旋转编码器的 A 相、B 相信号,并根据 A 相信号的上升沿和 B 相信号的状态
​    判断旋转方向。同时,监控按键引脚的按下和释放事件,用于重置旋转计数。每次按键按下都会清除计数器,
​    使得设备可以通过按键进行复位操作。

​    Attributes:
​        pin_a (Pin): A 相信号的 GPIO 引脚对象。用于读取旋转编码器的信号。
​        pin_b (Pin): B 相信号的 GPIO 引脚对象。用于与 A 相信号一起判断旋转方向。
​        pin_btn (Pin): 按键信号的 GPIO 引脚对象。用于检测按键的按下和释放。

​        rotation_count (int): 旋转计数器,记录旋转编码器的旋转次数。正值表示顺时针旋转,负值表示逆时针旋转。
​        last_state_a (int): 上一次读取的 A 相信号状态。用于判断旋转方向。
​        button_pressed (bool): 按键是否被按下。若按键被按下为 True,否则为 False。

​    Methods:
​        get_rotation_count(): 获取当前旋转计数。
​        reset_rotation_count(): 重置旋转计数器。
​        is_button_pressed(): 检测按键是否被按下。
​        _handle_rotation(pin: Pin): 内部中断处理函数,用于处理旋转信号并更新旋转计数。
​        _handle_button(pin: Pin): 内部中断处理函数,用于检测按键按下和释放事件。
​    """

​    ​def __init__(self, pin_a: int, pin_b: int, pin_btn: int) -> None:
        """
​        初始化 EC11 旋转编码器。

​        Args:
​            pin_a (int): A 相信号的 GPIO 引脚编号。
​            pin_b (int): B 相信号的 GPIO 引脚编号。
​            pin_btn (int): 按键信号的 GPIO 引脚编号。
​        """
​        ​self.pin_a = Pin(pin_a, Pin.IN)
        self.pin_b = Pin(pin_b, Pin.IN)
        self.pin_btn = Pin(pin_btn, Pin.IN, Pin.PULL_UP)

        # 旋转计数器
        self.rotation_count = 0
        # 上一次 A 相的状态
        self.last_state_a = self.pin_a.value()
        # 按键是否被按下
        self.button_pressed = False

        # 设置中断处理
        self.pin_a.irq(trigger=Pin.IRQ_RISING, handler=self._handle_rotation)
        self.pin_btn.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=self._handle_button)

    def _handle_rotation(self, pin: Pin) -> None:
        """
​        中断回调函数,检测旋转编码器的旋转方向。

​        根据 A 相的上升沿触发,并读取 B 相状态来判断旋转方向。

​        Args:
​            pin (Pin): 触发中断的 A 相信号引脚。
​        """
​        ​current_state_b = self.pin_b.value()

        # 顺时针旋转
        if current_state_b == 0:
            self.rotation_count += 1
        # 逆时针旋转
        else:
            self.rotation_count -= 1

    def _handle_button(self, pin: Pin) -> None:
        """
​        中断回调函数,检测按键的按下和释放。

​        按键按下时更新状态,释放时重置旋转计数。

​        Args:
​            pin (Pin): 触发中断的按键信号引脚。
​        """
​        ​# 按键按下
        if self.pin_btn.value() == 0:
            self.button_pressed = True
        # 按键释放
        else:
            self.button_pressed = False
        # 重置旋转计数
        self.rotation_count = 0

    def get_rotation_count(self) -> int:
        """
​        获取当前旋转计数。

​        返回旋转计数,正值表示顺时针,负值表示逆时针。

​        Returns:
​            int: 当前旋转计数。
​        """
​        ​return self.rotation_count

    def reset_rotation_count(self) -> None:
        """
​        重置旋转计数器。

​        将旋转计数器清零。
​        """
​        ​self.rotation_count = 0

    def is_button_pressed(self) -> bool:
        """
​        检测按键是否被按下。

​        返回按键状态,按键按下返回 True,未按下返回 False。

​        Returns:
​            bool: 按键是否被按下。
​        """
​        ​return self.button_pressed

# 终端进度条类
class ProgressBar:
    """
​    终端进度条类,用于在终端显示一个可更新的进度条。

​    该类通过在终端中绘制进度条的方式来显示当前任务的进度,支持动态更新显示进度条,并且允许用户
​    自定义最大值和进度条的长度。进度条的显示形式使用绿色表示已完成部分,红色表示剩余部分。

​    Attributes:
​        max_value (int): 进度条的最大值,表示任务的总进度。当当前进度达到此值时,进度条显示 100%。
​        bar_length (int): 进度条的总长度,用于控制进度条的宽度,默认为 50。

​    Methods:
​        update(current_value: int) -> None: 更新进度条的显示,传入当前进度值。
​        reset() -> None: 重置进度条,将进度条重置为 0%,显示全红色的条形。
​    """
​    ​
​    ​def __init__(self, max_value: int, bar_length: int = 50) -> None:
        """
​        初始化进度条。

​        Args:
​            max_value (int): 进度条的最大值,达到此值时进度条显示100%。
​            bar_length (int): 进度条的总长度,默认为 50。
​        """
​        ​self.max_value = max_value
        self.bar_length = bar_length

    def update(self, current_value: int) -> None:
        """
​        更新进度条。

​        根据当前值计算并更新进度条的显示,确保当前值不超过最大值。

​        Args:
​            current_value (int): 当前进度条的值。
​        """
​        ​if current_value > self.max_value:
            current_value = self.max_value
        if current_value < 0:
            current_value = 0

        progress = current_value / self.max_value
        block = int(self.bar_length * progress)

        # 绿色表示进度,红色表示剩余
        bar = '\033[92m' + '█' * block + '\033[91m' + '-' * (self.bar_length - block) + '\033[0m'
        print(f"\r[{bar}]", end='')

    def reset(self) -> None:
        """
​        重置进度条。

​        将进度条重置为 0%,即全为红色。
​        """
​        ​print(f"\r\033[91m[{'-' * self.bar_length}]\033[0m", end='')

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试信息
print("FreakStudio : GPIO read Rotary Encoder value")

# 创建EC11旋转编码器对象,使用GPIO26和GPIO27作为A相和B相,使用GPIO21作为按键
encoder = EC11Encoder(pin_a=26, pin_b=27, pin_btn=21)
# 创建终端进度条对象,用于显示进度,旋转20次达到100%
progress_bar = ProgressBar(max_value=20)

# ========================================  主程序  ===========================================

# 主循环中获取旋转计数值和按键状态
while True:
    # 获取旋转计数值
    current_rotation = encoder.get_rotation_count()
    # print(f"Rotation count: {current_rotation}")
    # 更新进度条
    progress_bar.update(current_rotation)
    # 按键被按下时,重置进度条
    if encoder.is_button_pressed():
        progress_bar.reset()
    # 每隔10ms秒更新一次
    time.sleep_ms(10)

这里,检测旋转编码器旋转次数和旋转方向的核心部分是 _handle_rotation 方法;我们设置连接旋转编码器 A 相的引脚为输入模式和上升沿触发中断,并设置其中断回调函数为 _handle_rotation 方法:

def _handle_rotation(self, pin: Pin) -> None:
    """
​    中断回调函数,检测旋转编码器的旋转方向。

​    根据 A 相的上升沿触发,并读取 B 相状态来判断旋转方向。

​    Args:
​        pin (Pin): 触发中断的 A 相信号引脚。
​    """
​    ​current_state_b = self.pin_b.value()

    # 顺时针旋转
    if current_state_b == 0:
        self.rotation_count += 1
    # 逆时针旋转
    else:
        self.rotation_count -= 1

当 A 相信号的上升沿被检测到时,进入 _handle_rotation 方法获取 B 相信号状态并判断:

  • B 相信号为低电平时,认为是顺时针旋转,因此旋转计数器 self.rotation_count 加 1
  • B 相信号为高电平时,认为是逆时针旋转,因此旋转计数器 self.rotation_count 减 1

同时,我们也设置旋转编码器的按键为下降沿触发,当按键按下时,调用 _handle_button 方法,在其中更新按键状态并重置旋转计数器。

我们通过自定义的 ProgressBar 类,用于在终端上动态显示进度,我们可以通过 update 方法动态更新进度条的显示,或调用 reset 方法将进度条清零,方便重新开始显示,其核心部分代码为:

def update(self, current_value: int) -> None:
    """
​    更新进度条。

​    根据当前值计算并更新进度条的显示,确保当前值不超过最大值。

​    Args:
​        current_value (int): 当前进度条的值。
​    """
​    ​if current_value > self.max_value:
        current_value = self.max_value
    if current_value < 0:
        current_value = 0

    progress = current_value / self.max_value
    block = int(self.bar_length * progress)

    # 绿色表示进度,红色表示剩余
    bar = '\033[92m' + '█' * block + '\033[91m' + '-' * (self.bar_length - block) + '\033[0m'
    print(f"\r[{bar}]", end='')

在这段代码中,我们确保 current_value 在有效范围内,即不超过最大值和不小于 0;然后计算当前值占最大值的比例从而得到进度条中显示的块的数量。

在构建进度条部分的代码中:

bar = '\033[92m' + '█' * block + '\033[91m' + '-' * (self.bar_length - block) + '\033[0m'

各个部分含义如下:

  • '\033[92m': ANSI 转义码,设置文本颜色为绿色
  • '█' * block: 绿色进度部分
  • '\033[91m': ANSI 转义码,设置文本颜色为红色
  • '-' * (self.bar_length - block)​: 红色剩余部分
  • '\033[0m'​: ANSI 转义码,重置文本颜色为默认值

接着,我们使用 Print 方法打印进度条,这里我们使用了回车符 '\r' 回到行首以在终端中覆盖之前的进度条输出;然后添加了 end 参数为 end='',避免每次打印都换行,使得进度条只在同一行打印动态更新的内容。

在主程序部分,我们持续读取旋转编码器的值并更新进度条,当检测到按键按下时重置进度条。

烧录代码,打开终端,显示如下:

当我们顺时针旋转 EC11 编码器时,可以看到进度条绿色矩形框增加:

而当我们逆时针旋转 EC11 编码器时,可以看到进度条绿色矩形框减少:

这里,我们可能会发现当旋转编码器旋转时,终端进度条有时候会跳转好几个单位,究其原因是因为这里旋转编码器计数值在旋转编码器旋转一次时,并非只改变一个单位,我们在主循环中添加如下代码打印旋转编码器的旋转计数值:

print(f"Rotation count: {current_rotation}")

烧录代码,并运行,打开终端:

可以看到旋转编码器的计数值在旋转一次时有时会跳转好几个单位,主要是由于旋转编码器作为一种机械设备,当用户旋转时,接触点会在短时间内反复接触和分离,导致信号抖动,从而产生多余的计数脉冲。

若是想要解决这个问题,我们可以使用电容或 RC 电路对信号进行滤波,减缓信号的快速变化,平滑抖动;或在软件方面对短时间内多次采集到的信号进行判断,只有在连续几次检测到同样的信号时才认为有效,进行解决。

在定时器章节中,我们将会使用定时器每隔 10ms 扫描一次编码器状态来进行消抖。

这里,关于 Python 中面向对象编程和自定义类的相关知识可以查看如下文章:
https://www.cnblogs.com/FreakEmbedded/p/18262988


posted @ 2026-08-31 11:25  FreakStudio  阅读(24)  评论(0)    收藏  举报