旋转编码器消抖方案:树莓派 Pico MicroPython 定时器实现

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

1280X1280.small.jpg

1280X1280 (1).PNG

1280X1280.JPEG

元器件接线情况如下:

微信图片_20260607190913_4_4.png

以下实验中所有代码开源在我们提供的资料包中 elegance-devkit v1\Demo\24 TIMER_RotaryEncoder 文件夹找到。

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

在以下代码中,我们使用软件定时器为旋转编码器进行消抖,对短时间内多次采集到的编码器信号进行判断,只有在连续几次检测到同样的编码器信号时才认为有效,代码如下所示:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/19 下午7:46   
# @Author  : 李清水            
# @File    : main.py       
# @Description : Timer类实验,读取旋转编码器的值,使用定时器做软件消抖

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

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

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

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

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

# EC11旋转编码器类
class EC11Encoder:
    """
​    EC11Encoder 类,用于处理 EC11 旋转编码器的信号。

​    该类通过 GPIO 引脚读取旋转编码器的 A 相、B 相信号以及按键信号,支持旋转方向的检测和按键状态的判断。
​    通过消抖处理确保信号的稳定性,并提供旋转计数和按键状态的查询功能。

​    Attributes:
​        pin_a (Pin): A 相信号的 GPIO 引脚实例。
​        pin_b (Pin): B 相信号的 GPIO 引脚实例。
​        pin_btn (Pin): 按键信号的 GPIO 引脚实例。
​        rotation_count (int): 旋转计数器,记录旋转的步数(正值表示顺时针,负值表示逆时针)。
​        button_pressed (bool): 按键状态,True 表示按键被按下,False 表示按键未按下。
​        debounce_timer_a (Timer): A 相信号的消抖定时器实例。
​        debounce_timer_btn (Timer): 按键信号的消抖定时器实例。
​        debouncing_a (bool): A 相信号的消抖状态标志。
​        debouncing_btn (bool): 按键信号的消抖状态标志。
​    """

​    ​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 引脚编号。
​        """
​        ​# 初始化A相和B相的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
        # 按键状态,用于记录按键是否被按下
        self.button_pressed = False

        # 消抖相关:定时器
        # A相消抖定时器
        self.debounce_timer_a   = Timer(-1)
        # 按键消抖定时器
        self.debounce_timer_btn = Timer(-1)

        # 标记是否在进行消抖处理
        # A相消抖标志
        self.debouncing_a = False
        # 按键消抖标志
        self.debouncing_btn = False

        # 为A相引脚设置中断处理,只检测上升沿(避免重复计数)
        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 相信号引脚实例。

​        Returns:
​            None
​        """
​        ​# 如果正在消抖中,直接返回
        if self.debouncing_a:
            return

        # 启动消抖定时器,延迟1ms后执行判断
        self.debouncing_a = True
        self.debounce_timer_a.init(mode=Timer.ONE_SHOT, period=1, callback=self._check_debounce_a)

    def _check_debounce_a(self, t: Timer) -> None:
        """
​        消抖定时器回调函数,检测 A 相是否稳定在高电平状态。

​        Args:
​            t (Timer): 触发回调的定时器实例。

​        Returns:
​            None
​        """
​        ​# 1ms后A相仍然为高电平,确认有效
        if self.pin_a.value() == 1:
            # 获取B相当前的状态(高或低电平)
            current_state_b = self.pin_b.value()

            # A相上升沿时,判断B相的状态决定旋转方向
            if current_state_b == 0:
                # 顺时针旋转,计数器加1
                self.rotation_count += 1
            else:
                # 逆时针旋转,计数器减1
                self.rotation_count -= 1

        # 重置消抖状态
        self.debouncing_a = False

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

​        Args:
​            pin (Pin): 触发中断的按键引脚实例。

​        Returns:
​            None
​        """
​        ​# 如果正在消抖中,直接返回
        if self.debouncing_btn:
            return

        # 启动消抖定时器,延迟5ms后执行判断
        self.debouncing_btn = True
        self.debounce_timer_btn.init(mode=Timer.ONE_SHOT, period=5, callback=self._check_debounce_btn)

    def _check_debounce_btn(self, t: Timer) -> None:
        """
​        按键消抖定时器回调函数,检测按键是否稳定。

​        Args:
​            t (Timer): 触发回调的定时器实例。

​        Returns:
​            None
​        """
​        ​if self.pin_btn.value() == 0:
            # 按键按下,更新状态
            self.button_pressed = True
        else:
            # 按键释放,更新状态
            self.button_pressed = False

        # 旋转编码器计数值清零
        self.reset_rotation_count()
        # 重置消抖状态
        self.debouncing_btn = False

    def get_rotation_count(self) -> int:
        """
​        获取旋转的总次数(正值表示顺时针,负值表示逆时针)。

​        Args:
​            None

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

    def reset_rotation_count(self) -> None:
        """
​        重置旋转计数器,将旋转步数清零。

​        Args:
​            None

​        Returns:
​            None
​        """
​        ​self.rotation_count = 0

    def is_button_pressed(self) -> bool:
        """
​        返回按键是否被按下的状态。

​        Args:
​            None

​        Returns:
​            bool: True 表示按键被按下,False 表示按键未按下。
​        """
​        ​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 : Using GPIO read Rotary Encoder value, use software debounce by timer")

# 创建EC11旋转编码器对象,使用GPIO26和GPIO27作为A相和B相,使用GPIO28作为按键
# 如果你想改变编码器计数值变大的旋转方向(例如原本是逆时针变大,想改成顺时针变大)
# 只需要在初始化编码器对象时,将参数 pin_a 和 pin_b 的值互换即可
encoder = EC11Encoder(pin_a=26, pin_b=27, pin_btn=28)
# 创建终端进度条对象,用于显示进度,旋转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)

定时器消抖的基本逻辑为:

  • 旋转编码消抖​:在 A 相上升沿触发时暂时不立即调整旋转计数值,而是启动一个 1ms 的单次定时器,1ms 后再检查 A 相是否依然为高电平;如果是高电平,表示此次触发有效;否则认为是抖动,忽略此次触发
  • 按键消抖​:在按键状态改变时(按下或释放),启动一个定时器,等待 5ms 后检查按键的状态,确保按键稳定在低电平或高电平,再更新按键的状态

同时,我们定义了 debounce_timer_btndebouncing_btn 消抖标志,中断触发后在第一次中断触发的回调期间不再执行回调中断函数,防止在消抖期间重复触发中断。

烧录代码,打开终端,输出如下:

image.png

可以看到,这次我们转动编码器时,不再有原先因为抖动而产生的旋转计数器不准确的情况出现。

mmexport1780839440241.gif

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