网站建设编辑器网络技术服务有限公司
网站建设编辑器,网络技术服务有限公司,网站源码安全吗,网站开发学校基于天空星HC32F4A0的EC11旋转编码器驱动移植与防抖实战
最近在做一个需要旋钮调节参数的项目#xff0c;选用了常见的EC11旋转编码器模块。这东西用起来手感不错#xff0c;但驱动起来总感觉有点“玄学”——旋转方向判断不准#xff0c;偶尔还会误触发。正好手头有天空星的…基于天空星HC32F4A0的EC11旋转编码器驱动移植与防抖实战最近在做一个需要旋钮调节参数的项目选用了常见的EC11旋转编码器模块。这东西用起来手感不错但驱动起来总感觉有点“玄学”——旋转方向判断不准偶尔还会误触发。正好手头有天空星的HC32F4A0开发板我就把整个驱动和防抖的代码移植了一遍调通之后效果很稳定。今天就把这个实战过程分享出来手把手教你怎么在HC32F4A0上搞定EC11编码器。这篇文章适合正在使用天空星HC32F4A0开发板的嵌入式爱好者或工程师。我会从EC11的工作原理讲起然后详细拆解驱动代码重点讲解如何用定时器实现10ms消抖最后给出完整的移植步骤和测试方法。跟着做一遍你就能在自己的项目里用上这个旋钮了。1. EC11旋转编码器它到底是怎么工作的在写代码之前咱们得先搞清楚EC11这个器件是怎么“说话”的。理解了它的输出信号规律后面的代码逻辑就一目了然了。EC11是一种机械式的旋转编码器你可以把它想象成一个高级的、带按压功能的旋转开关。它内部通过机械触点产生两路相位差90度的方波信号通常称为A相和B相通过这两路信号的先后顺序单片机就能判断出你是顺时针拧还是逆时针拧。1.1 相位差与方向判断这是最核心的原理。EC11有两个信号输出引脚CLK对应A相和DT对应B相。它们不是简单的开关信号而是两路有相位关系的脉冲。顺时针旋转A相CLK的波形会超前B相DT90度。具体到电平变化时刻就是当A相出现下降沿时B相是低电平当A相出现上升沿时B相是高电平。逆时针旋转B相DT的波形会超前A相CLK90度。也就是当A相出现下降沿时B相是高电平当A相出现上升沿时B相是低电平。注意这里的“超前”指的是波形变化的时间先后。你可以想象两个错开的方波谁先跳变谁就“领先”。为了方便编程我们可以总结成一个真值表来判断。我们只需要在A相CLK的电平发生跳变上升沿或下降沿的那个瞬间去读取B相DT的电平状态并与B相上一次的状态进行比较就能确定方向。根据原始资料里的分析判断逻辑可以简化为当A、B两相同时为上升沿或同时为下降沿时是顺时针旋转。当A、B两相的边沿变化不同时一个上升沿一个下降沿是逆时针旋转。1.2 机械抖动与消抖的必要性EC11是机械结构内部的金属触点在旋转或按下时不可避免地会产生接触抖动。这个抖动在示波器上看就是在电平稳定前会有几次快速的跳变。如果不处理单片机可能会把这几次抖动误判成多次旋转操作。所以消抖Debounce是驱动EC11时必须做的。常见的软件消抖方法有延时法和定时扫描法。咱们这里采用更可靠、不阻塞系统的定时器扫描法用一个定时器每隔固定时间比如10ms去检查一次编码器的状态。只要扫描间隔大于抖动时间就能有效滤除抖动。1.3 模块引脚说明咱们用的这个EC11模块有5个引脚用2.54mm排针引出非常方便插在面包板或直接焊接。引脚定义如下引脚标号引脚名称功能说明1CLK (或 A)A相脉冲输出2DT (或 B)B相脉冲输出3SW按压开关按下时接通GND4电源正极5V5GND电源地在硬件连接时模块的VCC接5VGND接开发板GND。CLK、DT、SW三个信号引脚分别连接到HC32F4A0的任意GPIO引脚并配置为上拉输入模式因为模块输出是开漏的需要上拉电阻。在咱们的示例代码中连接到了PA6、PA4和PA7。2. 驱动代码移植与详解理解了原理咱们就开始动手写代码。我会把核心的驱动代码拆开揉碎了讲你可以直接用到自己的工程里。2.1 硬件引脚与宏定义 (ec11.h)首先在头文件里定义好引脚连接和要用到的宏这样修改硬件连接时非常方便。#ifndef _BSP_ENCODER_H_ #define _BSP_ENCODER_H_ #include hc32_ll.h // 定义编码器连接的端口和引脚 #define PORT_GPIO GPIO_PORT_A #define GPIO_ENCODER_SW GPIO_PIN_07 // 按压开关引脚 - PA7 #define GPIO_ENCODER_LCK GPIO_PIN_06 // CLK (A相)引脚 - PA6 #define GPIO_ENCODER_DT GPIO_PIN_04 // DT (B相)引脚 - PA4 // 读取引脚状态的宏后续判断方向时直接使用代码更清晰 #define GET_CLK_STATE GPIO_ReadInputPins(PORT_GPIO, GPIO_ENCODER_LCK) #define GET_DT_STATE GPIO_ReadInputPins(PORT_GPIO, GPIO_ENCODER_DT) #define GET_SW_STATE GPIO_ReadInputPins(PORT_GPIO, GPIO_ENCODER_SW) // 定时器相关定义 (用于10ms定时扫描消抖) #define BSP_TIMER_FCG FCG2_PERIPH_TMR2_1 // 定时器2_1的时钟 #define BSP_TIMER CM_TMR2_1 // 定时器实例 #define BSP_TIMER_CH TMR2_CH_A // 使用通道A #define BSP_TIMER_MATCH TMR2_INT_MATCH_CH_A // 匹配中断 #define BSP_TIMER_INT INT_SRC_TMR2_1_CMP_A // 中断源 #define BSP_TIMER_IRQ INT050_IRQn // 中断号 #define BSP_TIMER_IRQHANDLER TMR2_Cmp_IrqCallback // 中断服务函数 // 函数声明 void Encoder_GPIO_Init(void); unsigned char Encoder_Sw_Down(void); int Encoder_Rotation_left(void); int Encoder_Rotation_right(void); #endif2.2 初始化函数GPIO与定时器 (ec11.c)Encoder_GPIO_Init这个函数做了两件大事初始化编码器的三个引脚并配置一个10ms周期的定时器用于消抖扫描。void Encoder_GPIO_Init(void) { stc_gpio_init_t stcGpioInit; (void)GPIO_StructInit(stcGpioInit); // 先用默认值填充结构体 // 配置GPIO为输入模式并开启内部上拉电阻 stcGpioInit.u16PinState PIN_STAT_SET; // 默认上拉到高电平 stcGpioInit.u16PinDir PIN_DIR_IN; // 输入模式 stcGpioInit.u16PullUp PIN_PU_ON; // 使能上拉 // 初始化SW、CLK、DT三个引脚 (void)GPIO_Init(PORT_GPIO, GPIO_ENCODER_SW, stcGpioInit); (void)GPIO_Init(PORT_GPIO, GPIO_ENCODER_LCK, stcGpioInit); (void)GPIO_Init(PORT_GPIO, GPIO_ENCODER_DT, stcGpioInit); // 解除GPIO等外设的写保护这是HC32库函数操作前常需要的一步 LL_PERIPH_WE(LL_PERIPH_GPIO | LL_PERIPH_FCG | LL_PERIPH_PWC_CLK_RMU); /* 配置定时器2_1产生10ms周期中断 */ FCG_Fcg2PeriphClockCmd(BSP_TIMER_FCG, ENABLE); // 使能定时器时钟 stc_tmr2_init_t stcTmr2Init; (void)TMR2_StructInit(stcTmr2Init); // 默认参数初始化 // 核心定时器参数计算与配置 stcTmr2Init.u32ClockSrc TMR2_CLK_PCLK1; // 时钟源为PCLK1假设为120MHz stcTmr2Init.u32ClockDiv TMR2_CLK_DIV1024; // 1024分频 stcTmr2Init.u32Func TMR2_FUNC_CMP; // 比较匹配模式 // 比较值计算(120MHz / 1024) / 100Hz 约1171.875取1172-1 stcTmr2Init.u32CompareValue (uint32_t)(1172 - 1); // 产生100Hz中断即10ms一次 (void)TMR2_Init(BSP_TIMER, BSP_TIMER_CH, stcTmr2Init); // 配置定时器中断 stc_irq_signin_config_t stcIrq; stcIrq.enIntSrc BSP_TIMER_INT; stcIrq.enIRQn BSP_TIMER_IRQ; stcIrq.pfnCallback BSP_TIMER_IRQHANDLER; // 指向我们的中断服务函数 (void)INTC_IrqSignIn(stcIrq); NVIC_ClearPendingIRQ(stcIrq.enIRQn); NVIC_SetPriority(stcIrq.enIRQn, DDL_IRQ_PRIO_03); NVIC_EnableIRQ(stcIrq.enIRQn); // 使能定时器的比较匹配中断并启动定时器 TMR2_IntCmd(BSP_TIMER, BSP_TIMER_MATCH, ENABLE); TMR2_Start(BSP_TIMER, BSP_TIMER_CH); }提示定时器比较值1172-1是怎么来的120MHz / 1024 117187.5 Hz这是分频后的定时器计数频率。我们要10ms中断一次即100Hz。117187.5 / 100 ≈ 1171.875向上取整为1172因为计数器从0开始所以比较值设为1172 - 1。2.3 核心方向判断函数 (Encoder_Scanf)这个函数是整个驱动的“大脑”它根据A、B两相的当前状态和上一次状态判断旋转方向。它被设计成在定时器中断里每10ms调用一次。char Encoder_Scanf(void) { static uint8_t EC11_CLK_Last RESET; // 保存A相上一次状态 static uint8_t EC11_DT_Last RESET; // 保存B相上一次状态 char ScanResult 0; // 关键逻辑只有当A相CLK状态发生变化时才进行判断 if(GET_CLK_STATE ! EC11_CLK_Last) { // 判断A相是上升沿还是下降沿 if(GET_CLK_STATE 1) // A相为上升沿 { // 根据B相状态变化判断方向 if((EC11_DT_Last 1) (GET_DT_STATE 0)) ScanResult 1; // 正转A上升沿时B从1变0下降沿 if((EC11_DT_Last 0) (GET_DT_STATE 1)) ScanResult 2; // 反转A上升沿时B从0变1上升沿 // 处理特殊边界情况例如从静止开始旋转的第一格 if((EC11_DT_Last GET_DT_STATE) (GET_DT_STATE 0)) ScanResult 1; // B保持0视为正转 if((EC11_DT_Last GET_DT_STATE) (GET_DT_STATE 1)) ScanResult 2; // B保持1视为反转 } else // A相为下降沿 { // 原理同上但方向逻辑相反 if((EC11_DT_Last 1) (GET_DT_STATE 0)) ScanResult 2; // 反转A下降沿时B从1变0 if((EC11_DT_Last 0) (GET_DT_STATE 1)) ScanResult 1; // 正转A下降沿时B从0变1 // 处理特殊边界情况 if((EC11_DT_Last GET_DT_STATE) (GET_DT_STATE 0)) ScanResult 2; // B保持0视为反转 if((EC11_DT_Last GET_DT_STATE) (GET_DT_STATE 1)) ScanResult 1; // B保持1视为正转 } // 更新状态记录为下一次判断做准备 EC11_CLK_Last GET_CLK_STATE; EC11_DT_Last GET_DT_STATE; return ScanResult; // 返回0无动作1正转2反转 } return 0; // A相无变化直接返回0 }这个函数看起来有点长但核心思想很简单盯住A相的变化在它变化的瞬间看B相是啥样。代码里包含了所有可能的A、B状态组合并且处理了边界情况非常健壮。2.4 按键检测与旋转事件处理编码器除了旋转还能按下去。按键检测相对简单但也要消抖。unsigned char Encoder_Sw_Down(void) { // SW引脚按下时接地低电平平时被上拉到高电平 if( GET_SW_STATE SET ) // 引脚为高没按下 { delay_ms(100); // 简单延时消抖 return 0; } else // 引脚为低按下了 { delay_ms(100); // 简单延时消抖 return 1; } }注意这里的delay_ms(100)是阻塞式延时在实时性要求不高的场合可以这样用。如果系统不允许长时间阻塞可以改用状态机或外部中断定时器的方式来消抖。当检测到旋转事件后我们通常需要执行一些用户代码比如增加/减少一个数值。示例里提供了两个服务函数框架int Encoder_Rotation_left(void) { static int left_num 0; left_num; // 在这里添加左转时要执行的代码比如变量减1 printf(left num %d\r\n,left_num); // 你的代码结束 return left_num; } int Encoder_Rotation_right(void) { static int right_num 0; right_num; // 在这里添加右转时要执行的代码比如变量加1 printf(right num %d\r\n,right_num); // 你的代码结束 return right_num; }2.5 定时器中断服务函数消抖与事件分发最后我们把所有逻辑串起来。定时器每10ms中断一次在中断里调用Encoder_Scanf()检测是否有有效的旋转动作然后调用对应的处理函数。void TMR2_Cmp_IrqCallback(void) { char dat 0; dat Encoder_Scanf(); // 扫描编码器 if( dat ! 0 ) // 如果有转动 { // 根据返回的方向值调用不同的处理函数 // 注意这里的1和2与你的方向定义有关如果反了交换一下即可 if( dat 2 ) { Encoder_Rotation_left(); // 处理左转 } else { Encoder_Rotation_right(); // 处理右转 } } // 非常重要清除定时器中断标志位 TMR2_ClearStatus(BSP_TIMER, BSP_TIMER_MATCH); }这里就是10ms消抖的关键无论机械触点如何抖动在10ms内Encoder_Scanf函数只会判断A相第一次变化时的状态后续的抖动因为A相状态没有“再次变化”而被忽略。这样就实现了稳定的方向判断。3. 在工程中集成与测试代码都准备好了现在把它们放到你的天空星HC32F4A0工程里测试。3.1 移植步骤添加文件将ec11.c和ec11.h复制到你的工程源码目录下例如User/Src和User/Inc。包含路径在IDE的工程设置中添加头文件包含路径。修改引脚根据你的实际硬件连接修改ec11.h开头的GPIO_ENCODER_SW、GPIO_ENCODER_LCK、GPIO_ENCODER_DT这三个宏定义指向你实际使用的引脚。调用初始化在主函数main()中初始化完系统时钟和串口后调用Encoder_GPIO_Init()。3.2 主函数示例一个简单的主循环可以这样写用于测试旋转和按键功能#include board.h #include bsp_uart.h #include stdio.h #include ec11.h int32_t main(void) { board_init(); // 开发板基础初始化 uart1_init(115200U); // 初始化串口用于打印 LL_PERIPH_WE(LL_PERIPH_GPIO); // 解除GPIO写保护 Encoder_GPIO_Init(); // 初始化编码器驱动 printf(EC11 Encoder Demo Start\r\n); while(1) { // 检测按键是否按下 if( Encoder_Sw_Down() 1 ) { printf(Encoder Pressed!\r\n); // 等待按键释放 while( Encoder_Sw_Down() 1 ) { // 可以在这里加个短延时避免死循环占用全部CPU } printf(Encoder Released.\r\n); } // 旋转事件的处理已经在定时器中断中自动完成了 // 我们只需要在 Encoder_Rotation_left/right 函数里写逻辑 } }3.3 测试与调试编译下载程序后打开串口助手。旋转编码器你应该能看到串口打印出left num ...或right num ...的信息。按下编码器会打印Encoder Pressed!和Encoder Released.。如果发现旋转方向与打印的左右相反很简单只需要在TMR2_Cmp_IrqCallback中断函数里交换一下Encoder_Rotation_left()和Encoder_Rotation_right()的调用条件即可。常见问题排查完全没有反应检查硬件连接VCC GND 三根信号线检查代码中的引脚定义是否与实际连接一致。方向反了按上面说的交换中断函数里的处理逻辑。反应不灵敏或连跳可能是机械抖动太严重可以尝试增大定时器的扫描周期比如从10ms改为20ms修改stcTmr2Init.u32CompareValue的计算值。按键反应慢主循环中检测按键如果循环内还有其他耗时任务可能导致检测不及时。可以考虑将按键检测也放到定时器中断中或者使用GPIO外部中断来检测下降沿。这个驱动方案我在几个项目里都用过稳定性很好。核心就是利用定时器中断做周期扫描既实现了消抖又避免了在主循环里做忙等待。你可以直接把代码拿去用根据实际需求修改旋转事件的处理函数就行。