鼠标滚轮编码器 | 原理、检测与维修

注:本文为 "鼠标滚轮编码器" 相关合辑。

图片清晰度受引文原图所限。

略作重排,如有内容异常,请看原文。


鼠标滚轮/编码器检测- wheel/encoder detect for mouse

阳雨风 qq125242773 于 2020-10-09 22:36:29 首次发布

已于 2025-02-06 16:37:55 修改

wheel/encoder 原理

wheel/encoder 示波器实测波形

wheel/encoder 单片机检测固件

c 复制代码
struct wheel_STR
{
    unsigned char same_status_HL;   //two state: all high (=1) or low (=2)
    unsigned char diff_status;      //two state: z1-high,z2-low (=1);z1_low,z2-high (=2);
    signed char value;
};
struct wheel_STR wheel;

/*
scan in main loop
*/
void scan_wheel(void) 
{
    unsigned char z1,z2;

z1 = HAL_GPIO_ReadPin(WHEEL_Z1_PORT, WHEEL_Z1_PIN);//read_wheel_IO1();
z2 = HAL_GPIO_ReadPin(WHEEL_Z2_PORT, WHEEL_Z2_PIN);//read_wheel_IO2();

if(z1 != z2){   //diff
		wheel.diff_status =(z1)?2:1;	
}
else{ //same
    if(z1){
		if(wheel.same_status_HL==2){
			if(wheel.diff_status==1) wheel.value++;
			else if(wheel.diff_status==2) wheel.value--;
		}
		wheel.same_status_HL =1;    //all high
    }
    else{
       if(wheel.same_status_HL==1){
			if(wheel.diff_status==1) wheel.value--;
			else if(wheel.diff_status==2) wheel.value++;
       }
       wheel.same_status_HL =2;    //all low 
    }
    wheel.diff_status =0;   //clear change flag!
}
}

/*
get the wheel vaule
*/
signed char get_wheel_value(void)
{	
	signed char tmp;
	tmp = wheel.value;
	wheel.value=0;		//clear the value!
	return tmp;
}

鼠标滚轮编码器解析

爱生活的鸭 于 2023-03-24 10:53:39 发布

前言

鼠标滚轮编码器为三引脚接入,一个公共引脚 C(通常接地),两个脉冲波形输入引脚 A、B。转动滚轮编码器时,两个脉冲输入引脚上会产生脉冲;顺时针或逆时针转动时,可根据同一时刻产生的电平信号变化进行逻辑判断。

一、鼠标滚轮编码器逻辑

正面从左到右依次为公共引脚、A 输入脚和 B 输入脚。

转动过程中,公共引脚与 A、B 脚的导通状态会改变输入至集成电路(IC)控制芯片引脚的电平,其电平变化逻辑如下:

顺时针转动时,电平变化顺序为:11 → 01 → 00 → 10

逆时针转动时,电平变化顺序为:11 → 10 → 00 → 01

二、使用方法

代码如下 g

c 复制代码
unsigned char z1,z2;

// 读取编码器两个引脚(Z1、Z2)的当前电平状态
z1 = HAL_GPIO_ReadPin(WHEEL_Z1_PORT, WHEEL_Z1_PIN);//read_wheel_IO1();
z2 = HAL_GPIO_ReadPin(WHEEL_Z2_PORT, WHEEL_Z2_PIN);//read_wheel_IO2();

if(z1 != z2){   // 若 Z1 与 Z2 电平不同(差分状态),记录当前差分相位
    // 当 Z1 为高电平时,差分状态标记为 2;Z1 为低电平时,标记为 1
    // 用于后续判断编码器转动方向(相位差特征)
    wheel.diff_status = (z1)?2:1;	
}
else{ // 若 Z1 与 Z2 电平相同(同相状态),结合历史状态判断转动方向并计数
    if(z1){ // 此时 Z1=Z2=高电平(全高状态)
        // 若上一次同相状态为全低(same_status_HL=2),说明完成一次相位跳变
        if(wheel.same_status_HL==2){
            // 根据之前记录的差分状态判断方向:diff_status=1 对应顺时针,value 递增
            // diff_status=2 对应逆时针,value 递减
            if(wheel.diff_status==1) wheel.value++;
            else if(wheel.diff_status==2) wheel.value--;
        }
        // 更新当前同相状态为全高(标记为 1)
        wheel.same_status_HL =1;    //all high
    }
    else{ // 此时 Z1=Z2=低电平(全低状态)
        // 若上一次同相状态为全高(same_status_HL=1),说明完成一次相位跳变
        if(wheel.same_status_HL==1){
            // 根据之前记录的差分状态判断方向:diff_status=1 对应逆时针,value 递减
            // diff_status=2 对应顺时针,value 递增
            if(wheel.diff_status==1) wheel.value--;
            else if(wheel.diff_status==2) wheel.value++;
        }
        // 更新当前同相状态为全低(标记为 2)
        wheel.same_status_HL =2;    //all low 
    }
    // 清除差分状态标记(同相状态下,差分状态已用于方向判断,无需保留)
    wheel.diff_status =0;   //clear change flag!
}

总结

鼠标编码器的连接方式为:第一引脚接 GND,第二引脚和第三引脚为输出端。

编码器通过相位差判断滑动方向,通过输出低电平的持续时间判断滑动速度;但无论滑动速度和方向如何,各引脚输出的脉冲宽度均保持一致。


鼠标滚轮编码器检测代码性能优化

一、三点

针对嵌入式系统的特性(资源有限、实时性要求高),代码性能主要围绕以下三点展开:

  1. 执行效率:减少单次状态处理的时间开销,提升代码运行速度
  2. 抗干扰能力:降低外部噪声与机械抖动对信号检测的影响,提高稳定性
  3. 资源占用:内存使用与 CPU 占用率,适配嵌入式系统有限的硬件资源

二、方案

1. 中断驱动替代轮询(CPU 资源)

问题:传统主循环轮询方式需持续检测编码器状态,导致 CPU 资源被无意义占用,尤其在多任务系统中影响其他功能响应速度。

将编码器的 Z1、Z2 引脚配置为双边沿触发中断(上升沿与下降沿均触发),仅在电平状态变化时执行处理逻辑。示例代码如下(以 STM32 为例):

c 复制代码
// 中断服务程序(ISR)
void EXTI9_5_IRQHandler(void) {
    // 检测 Z1 引脚中断
    if (__HAL_GPIO_EXTI_GET_IT(WHEEL_Z1_PIN) != RESET) {
        HAL_GPIO_EXTI_IRQHandler(&wheel_z1_irq);  // 调用中断处理回调
        __HAL_GPIO_EXTI_CLEAR_IT(WHEEL_Z1_PIN);   // 清除中断标志
    }
    // 检测 Z2 引脚中断
    if (__HAL_GPIO_EXTI_GET_IT(WHEEL_Z2_PIN) != RESET) {
        HAL_GPIO_EXTI_IRQHandler(&wheel_z2_irq);  // 调用中断处理回调
        __HAL_GPIO_EXTI_CLEAR_IT(WHEEL_Z2_PIN);   // 清除中断标志
    }
}

优势

  • CPU 仅在编码器状态变化时工作,空闲时可执行其他任务,降低 CPU 占用率
  • 适用于多任务场景(如同时处理鼠标按键、位移检测等功能)

2. 状态机+查表法(执行效率)

问题 :传统多层if-else分支判断逻辑复杂,执行效率低,尤其在无分支预测机制的单片机中影响性能。

将编码器的状态(Z1、Z2 电平组合)抽象为有限状态机,通过"当前状态-上一状态"的组合查表直接获取转动方向,替代分支判断。示例代码如下:

c 复制代码
// 状态转换表(4×4 矩阵,行:上一状态,列:当前状态,值:转动方向(+1 顺时针,-1 逆时针,0 无有效转动))
const int8_t wheel_dir_table[4][4] = {
    { 0, -1, 1, 0},  // 上一状态为 00(Z1=0,Z2=0)时的方向映射
    { 1, 0, 0, -1},  // 上一状态为 01(Z1=0,Z2=1)时的方向映射
    {-1, 0, 0, 1},   // 上一状态为 10(Z1=1,Z2=0)时的方向映射
    { 0, 1, -1, 0}   // 上一状态为 11(Z1=1,Z2=1)时的方向映射
};

// 状态处理函数
void process_wheel_state(uint8_t z1, uint8_t z2) {
    static uint8_t prev_state = 0;  // 静态变量存储上一状态,初始化为 0
    uint8_t curr_state = (z1 << 1) | z2;  // 组合 Z1、Z2 电平为当前状态(0~3)
    wheel.value += wheel_dir_table[prev_state][curr_state];  // 查表更新转动计数
    prev_state = curr_state;  // 更新上一状态
}

优势

  • 用数组查表操作替代多层分支判断,减少 CPU 指令周期消耗
  • 执行速度提升显著,尤其在高频检测场景下效果明显

3. 硬件+软件防抖(抗干扰能力)

问题:编码器机械触点抖动或电磁干扰可能导致电平误跳变,引发转动方向误判。

  • 硬件防抖:在编码器引脚串联 10 kΩ电阻与 100 nF 电容,形成 RC 低通滤波器(时间常数约 10 μs),滤除高频噪声。
  • 软件防抖:通过多次采样并采用"多数表决"机制确认电平状态,避免单次抖动影响。示例代码如下:
c 复制代码
// 带软件防抖的引脚读取函数
uint8_t read_stable_pin(GPIO_TypeDef* port, uint16_t pin) {
    uint8_t count = 0;  // 高电平计数
    for (uint8_t i = 0; i < 5; i++) {  // 连续采样 5 次
        count += (port->IDR & pin) ? 1 : 0;  // 记录高电平次数
        delay_us(5);  // 每次采样间隔 5 μs,避开抖动期
    }
    return (count >= 3) ? 1 : 0;  // 多数表决:3 次及以上高电平则判定为高电平
}

优势

  • 显著降低误触发概率,适应不同环境的噪声水平
  • 硬件与软件结合,兼顾滤波效果与灵活性

4. 内存与变量

问题:多任务或中断环境下,共享变量可能因编译器优化或并发访问导致数据不一致。

  • volatile修饰共享变量:防止编译器对频繁访问的变量进行优化(如缓存到寄存器),确保每次读取均来自内存。

    c 复制代码
    typedef struct {
        volatile int8_t value;  // 转动计数(共享变量)
        volatile uint8_t state; // 状态标记(共享变量)
    } WheelData;
  • 原子操作保护数据更新:在读取并清零计数等关键操作中,通过关闭中断实现原子操作,避免并发修改导致的数据错误。

    c 复制代码
    int8_t get_wheel_value(WheelData* wheel) {
        int8_t val;
        __disable_irq();  // 关闭中断,禁止并发访问
        val = wheel->value;  // 读取当前计数
        wheel->value = 0;    // 清零计数
        __enable_irq();   // 恢复中断
        return val;
    }

优势

  • 保证多任务/中断环境下共享数据的一致性
  • 避免因并发访问导致的计数错误或状态混乱

5. 寄存器直接操作(执行效率)

问题 :使用 HAL 库函数(如HAL_GPIO_ReadPin)读取引脚电平时,存在冗余的参数校验与抽象层操作,增加函数调用开销。

直接访问 GPIO 寄存器(如 STM32 的IDR寄存器)读取电平状态,减少中间环节。示例代码如下:

c 复制代码
// 宏定义:直接读取 Z1、Z2 引脚电平(以 STM32 为例)
#define READ_Z1() ((WHEEL_Z1_PORT->IDR & WHEEL_Z1_PIN) ? 1 : 0)
#define READ_Z2() ((WHEEL_Z2_PORT->IDR & WHEEL_Z2_PIN) ? 1 : 0)

优势

  • 减少函数调用的栈操作与参数处理开销
  • 执行速度比库函数快约 30%,适合高频检测场景

三、指标评估

1. 四项关键指标

指标 定义 测量方式
CPU 占用率 代码在单位时间内占用 CPU 的时间比例 调试器的 CPU Utilization 工具、RTOS 任务统计、Cortex-M 的 DWT Cycle Counter
单次执行时间 处理一次编码器状态所需的实际时间(μs) 硬件 SysTick 定时器、DWT Cycle Counter
误触发率 误判为转动的次数占总检测次数的比例 统计错误计数与总计数的比值(人工测试或自动仿真)
实时响应性 从信号变化到处理完成的端到端延迟(μs) 示波器捕获外部信号与内部处理完成的时间差、代码时间戳差值计算

2. 基准测试(优化前)

  • 测试环境 :保留原始轮询实现(scan_wheel()),关闭所有中断触发代码,在同一硬件平台(如 STM32F103)、相同系统时钟(72 MHz)下运行。
  • 调试配置:开启调试工具的监控功能(如 Keil µVision 的 System Viewer、STM32CubeIDE 的 Profiler),记录基准指标。

3. 实现(优化后)

结合中断驱动、查表法、防抖与寄存器直接操作的优化方案,示例代码如下:

c 复制代码
/* 中断入口:仅标记状态变化,避免 ISR 过长 */
void EXTI9_5_IRQHandler(void) {
    if (__HAL_GPIO_EXTI_GET_IT(WHEEL_Z1_PIN)) {
        __HAL_GPIO_EXTI_CLEAR_IT(WHEEL_Z1_PIN);
        wheel_irq_flag = 1;  // 标记状态变化
    }
    if (__HAL_GPIO_EXTI_GET_IT(WHEEL_Z2_PIN)) {
        __HAL_GPIO_EXTI_CLEAR_IT(WHEEL_Z2_PIN);
        wheel_irq_flag = 1;  // 标记状态变化
    }
}

/* 主循环处理:查表法更新状态 */
void process_wheel(void) {
    static uint8_t prev_state = 3;  // 初始状态假设为 11(Z1=1,Z2=1)
    uint8_t z1 = READ_Z1();         // 寄存器直接读取 Z1 电平
    uint8_t z2 = READ_Z2();         // 寄存器直接读取 Z2 电平
    uint8_t curr_state = (z1 << 1) | z2;  // 组合当前状态
    wheel.value += wheel_dir_table[prev_state][curr_state];  // 查表更新计数
    prev_state = curr_state;  // 更新上一状态
}

/* 主循环 */
int main(void) {
    HAL_Init();
    MX_GPIO_Init();         // 初始化 GPIO
    MX_EXTI_Init();         // 配置双边沿中断
    while (1) {
        if (wheel_irq_flag) {  // 仅在状态变化时处理
            wheel_irq_flag = 0;
            process_wheel();
        }
        // 执行其他任务...
    }
}

4. 指标测量方法

4.1 CPU 占用率

使用 Cortex-M 的 DWT Cycle Counter 统计 1 s 内的 CPU 占用周期:

c 复制代码
// 启用 DWT 计数器
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

uint32_t start = DWT->CYCCNT;
HAL_Delay(1000);  // 运行 1 s
uint32_t cycles = DWT->CYCCNT - start;
float cpu_percent = (float)cycles / (SystemCoreClock * 1.0f) * 100.0f;  // 计算占比
4.2 单次执行时间

通过 DWT 计数器测量process_wheel()函数的执行时间:

c 复制代码
uint32_t t0 = DWT->CYCCNT;
process_wheel();  // 执行一次状态处理
uint32_t t1 = DWT->CYCCNT;
float exec_time_us = (t1 - t0) * 1e6f / SystemCoreClock;  // 转换为μs
4.3 误触发率

误触发率计算公式为:
误触发率 = 错误计数 总检测次数 × 100 % \text{误触发率} = \frac{\text{错误计数}}{\text{总检测次数}} \times 100\% 误触发率=总检测次数错误计数×100%

  • 人工测试:手动转动滚轮 1000 次,记录wheel.value与实际转动次数的差值作为错误计数。
  • 自动仿真:使用逻辑分析仪捕获编码器信号,通过 Python 脚本统计错误计数。
4.4 实时响应性(端到端延迟)

延迟计算公式为:
Latency (μs) = proc_done_ts − irq_entry_ts SystemCoreClock × 1 0 6 \text{Latency (μs)} = \frac{\text{proc\_done\_ts} - \text{irq\_entry\_ts}}{\text{SystemCoreClock}} \times 10^6 Latency (μs)=SystemCoreClockproc_done_ts−irq_entry_ts×106

其中,irq_entry_ts为中断入口时刻的时间戳,proc_done_ts为状态处理完成时刻的时间戳,通过 DWT 计数器记录。

5. 综合对比(示例数据)

指标 优化前(轮询) 优化后(综合方案) 提升幅度
CPU 占用率 30 % 5 % ↓ 83 %
单次执行时间 20 μs 8 μs ↓ 60 %
误触发率 8 % 0.5 % ↓ 94 %
实时响应性(端到端) 150 μs 30 μs ↓ 80 %

注:实际数据受 MCU 型号、时钟频率、编码器特性及环境噪声影响,以上为参考示例。

6. 工具与脚本

  • CPU/任务统计:Keil µVision(System Viewer)、STM32CubeIDE(Profiler)、FreeRTOS Trace
  • 周期计数:Cortex-M 的 DWT Cycle Counter(代码级时间测量)
  • 误触发统计:Python + pySerial(读取串口日志并自动计算)
  • 延迟波形分析:示波器(如 Tektronix TBS1000)、逻辑分析仪(如 Saleae Logic)

四、注意事项

  1. 中断服务程序(ISR)需保持简洁,仅执行状态标记等轻量操作,避免长时间占用 CPU 影响系统响应。
  2. 状态转换表需根据编码器的实际相位特性调整,不同型号编码器的状态映射可能存在差异。
  3. 防抖参数(采样次数、间隔时间)需结合硬件特性调试,平衡抗干扰能力与响应速度。
  4. 对于高速编码器(如高分辨率电竞鼠标),建议采用更高效的寄存器操作与中断优先级配置,确保信号无漏检。

鼠标滚轮编码器:原理、故障与维修指南

一、编码器基础认知

1. 类型与特性

  • 光栅式:早期广泛应用,依赖红外线收发信号,无物理接触,稳定性强、寿命较长,但光源会自然衰减,对主控编程有特定要求,现仅少数厂商使用。

  • 机械式:目前主流类型,结构简单、主控编程便捷,具备清晰机械刻度手感与精准定位特性,使用寿命已从 10 万圈提升至 200 万圈,适配多数鼠标。

2. 常见故障及成因

故障表现
  • 滚轮无规律上下跳动、失灵;
  • 滚动方向异常(如下滑触发上滑)或滚动过度;
  • 游戏场景中视野无规律缩放、无法聚焦;
  • 滚动信号丢失,反应时有时无或完全失效。
故障成因
  • 零部件精度不足,使用中产生误差;
  • 编码器触点材质较差,长期使用易磨损导致接触不良;
  • 鼠标内部卷入异物,阻碍机械结构运行。

3. 编码器选型三项关键参数:

  1. 安装高度(如 TTC 11 mm 规格);
  2. 编码器类型(光栅式/机械式);
  3. 结构细节(六边形方孔朝向、是否为高端鼠标定制款)。

二、编码器高度测量

结构

参数

Hole Size
Torque Field
Height Options

安装高度测量

直接在拆解后的鼠标面板上测量,确保与新配件高度一致。

安装尺寸

三、编码器常见类型

103 系列(通用型)

123 系列(部分带线款)

四、维修流程

1. 准备工具与配件

  • 基础工具:小型十字螺丝刀、镊子/薄刃工具;
  • 焊接工具:电烙铁、吸锡器、焊锡丝;
  • 核心配件:与原鼠标参数匹配的全新编码器。

2. 鼠标拆解步骤

  1. 移除鼠标底部脚贴/贴纸,露出隐藏螺丝;

  2. 拧下螺丝,沿卡扣缝隙缓慢分离上盖,避免损坏内部排线;

  3. 取出主板,定位滚轮下方圆柱形编码器组件(焊接固定或插头连接);

  4. 焊接固定款需用电烙铁+吸锡器拆旧,插头款直接拔插分离。

3. 编码器更换操作

  1. 用橡皮擦清洁滚轮金属触点,去除氧化层;

  2. 拆旧编码器:新手逐点清理焊盘残留焊锡后取出,熟手可同时熔化所有焊点快速拆卸;

  3. 装新编码器:精准对位后,焊接固定各引脚(熟手可利用残留焊锡定位,无需补焊);

  4. 功能测试:通电验证滚轮灵敏度与定位准确性;

  5. 重组鼠标:按拆解反向顺序复位主板、上盖,紧固螺丝与卡扣。

4. 维修注意事项

  • 具备基础动手能力与电子设备认知者可自行操作,无维修经验建议寻求专业支持;
  • 鼠标在保修期内时,优先联系制造商咨询免费维修/更换服务,避免自行拆解影响保修。

via: