C51增量式旋转编码器实战:从脉冲采集到方向判别的完整实现
简介这份资源是面向单片机初学者与嵌入式开发者的C51增量式旋转编码器读取测试程序围绕脉冲编码器信号采集这一常见场景提供可直接编译运行的Keil工程与源码参考。压缩包共22个文件约56KB包含2份c源码、2份hex烧录文件、2份uvproj工程文件以及lst、m51、plg、obj、lnp、uvopt等编译中间与调试输出文件另附encoder与bmq两组工程目录便于对照学习工程组织方式。程序重点演示goto跳转读取编码器的测试逻辑通过无符号整型变量比较跳转在标号处输出并让P0口显示2017 5 11同时涉及关闭定时器运算、结束后开启定时器延时2秒显示参数等处理可帮助读者理解编码器脉冲计数、条件跳转与端口输出的配合关系。目前已有1432人学习下载适合需要快速验证编码器接口、借鉴C51跳转与定时器控制写法的读者参考。1. 增量式旋转编码器在 C51 上的落地从脉冲到方向一个压缩包能省掉多少试错如果你手头有一块 51 单片机一个增量式旋转编码器想读出转速和方向大概率会经历这么一段先写外部中断发现抖动严重再加消抖发现高速时丢脉冲换到定时器计数又搞不清正交解码的相位关系。折腾两三天代码没几行时间全花在试错上。这个名为「C51增量式读旋转编码器测试程序」的压缩包就是把这些试错过程提前做完的一份参考实现。它面向的是用 8051 内核单片机做电机测速、旋钮调节、位置反馈的开发者核心解决的是增量式编码器 A/B 两相脉冲的采集、方向判别和计数问题。适合刚接触编码器的新手照着跑通也适合熟手拿来对照自己的中断配置和消抖策略。2. 增量式编码器与 C51 的接口原理为什么不能只靠一个外部中断2.1 增量式编码器的 A/B/Z 三路信号到底在说什么增量式旋转编码器输出的是脉冲不是绝对位置。它内部有一个带缝隙的码盘和光电对管转起来就出方波。常见的是 A、B 两相外加一个 Z 相零位信号。A 和 B 在物理上错开 90 度电角度这就是所谓的正交输出。正转时 A 相超前 B 相 90 度反转时 B 相超前 A 相 90 度。方向信息就藏在这个超前滞后关系里而不是藏在某一相的脉冲数里。Z 相每转一圈输出一个脉冲用来做零点校准。很多低成本编码器直接省掉 Z 相只留 A/B。你拿到手先确认型号是增量式还是绝对式输出是集电极开路还是推挽供电是 5V 还是 24V。这些决定了你要不要加上拉电阻、要不要光耦隔离。我一般会先用示波器或者逻辑分析仪抓一下 A/B 波形确认相位差和电平范围再往单片机引脚上接。这一步省不得血泪经验是直接怼上去烧过输入引脚。C51 指的是 Intel 8051 内核的单片机典型型号如 STC89C52、AT89S52 等。这类芯片资源有限通常两个外部中断、两三个定时器、几百字节 RAM。用它们读编码器核心矛盾就是脉冲来得快CPU 处理慢中断里不能干重活。2.2 四种常见读取方案与选型理由在 51 上读增量式编码器常见做法有这么几种各有适用边界。第一种是纯外部中断法。把 A 相接 INT0B 相接普通 IOA 相下降沿触发中断在中断里读 B 相电平判断方向然后计数。优点是代码简单缺点是每个脉冲都进一次中断高速时中断频繁主循环被切得稀碎。适合转速低、脉冲数少的场景比如手动旋钮。第二种是定时器计数法。把 A 相接到定时器的外部计数引脚 T0 或 T1让硬件自动计数CPU 定时去读计数值。方向判断另想办法比如用 B 相配合中断。优点是计数不占 CPU能测较高频率。缺点是 51 的定时器是 16 位会溢出需要软件处理溢出而且方向判断和计数是分离的逻辑要小心。第三种是状态机法。A/B 两相都接 IO用定时器定期采样把两次采样值组成一个状态查表判断正转、反转还是不动。优点是抗抖动好不需要外部中断。缺点是采样率要足够高否则高速时漏状态。适合脉冲频率不高的场合。第四种是专用解码芯片。比如用 LS7183、LS7184 这类正交解码 IC硬件完成方向判别和计数单片机只读结果。优点是 CPU 负担最小性能最好。缺点是要多一颗芯片成本上去布线也复杂。这个压缩包里的测试程序从命名看是围绕 C51 直接读取的大概率用的是外部中断加 IO 判向或者定时器计数配合中断判向。你拿到源码后先看它用了哪个定时器、哪个中断就能判断属于哪一类。选型没有绝对优劣关键看你的编码器线数、最高转速和单片机主频。线数乘以转速就是脉冲频率这个数超过 10kHz纯中断法就吃力了。2.3 正交解码的状态转移表不管用哪种方案正交解码的核心逻辑是一样的。把 A、B 两相的电平组合成一个两位状态正转和反转的状态序列不同。当前状态 (A,B)正转下一状态反转下一状态000110011100111001100011正转时状态按 00→01→11→10→00 循环反转时按 00→10→11→01→00 循环。你只要在每次状态变化时查表就能知道方向并加减计数。这个表是后面所有代码的基础建议先记牢。3. 把测试程序跑起来工程结构、关键配置与逐步验证3.1 拿到压缩包后先做的三件事解压之后别急着编译。先做三件事看目录结构、看芯片型号、看引脚定义。目录里通常有 .c 源文件、.h 头文件、Keil 工程文件.uvproj 或 .uvprojx可能还有一份说明文档。芯片型号决定了你用什么烧录工具和下载电路。引脚定义决定了你怎么接线。我一般会先打开主 .c 文件找到引脚宏定义比如// 编码器引脚定义根据实际接线修改 sbit ENC_A P3^2; // A 相接 INT0 sbit ENC_B P3^4; // B 相接普通 IO sbit ENC_Z P3^5; // Z 相可选这段代码说明 A 相用了 P3.2也就是 INT0 引脚。B 相接在 P3.4。你要做的是核对自己板子上的实际接线不一致就改这里。改完再编译否则读出来的方向是反的你还以为是编码器坏了。3.2 外部中断初始化与计数变量如果程序用的是外部中断法初始化部分大概长这样// 外部中断0初始化A相下降沿触发 void Encoder_Init(void) { IT0 1; // INT0 下降沿触发 EX0 1; // 使能外部中断0 EA 1; // 开总中断 enc_count 0; // 计数清零 enc_dir 0; // 方向标志清零 } // 外部中断0服务函数A相每个下降沿进入 void INT0_ISR(void) interrupt 0 { if (ENC_B 0) { enc_count; // B相为低正转 enc_dir 1; } else { enc_count--; // B相为高反转 enc_dir 0; } }逻辑说明A 相下降沿到来时进中断此时读 B 相电平。如果 B 相是低电平说明 A 超前 B是正转计数加一反之减一。参数上IT01 设下降沿触发EX01 开中断EA1 开总中断。这三个缺一不可。计数变量 enc_count 建议用 int 或 long因为正反转会到负数用 unsigned 会翻车。注意这段代码没有消抖。机械编码器抖动时一个脉冲可能进多次中断计数虚高。后面避坑章节会讲怎么处理。3.3 定时器计数法的配置差异如果程序用的是定时器计数法初始化会不一样// 定时器0做外部计数A相接T0引脚 void Timer0_Count_Init(void) { TMOD 0xF0; TMOD | 0x05; // T0 模式1外部计数 TH0 0; TL0 0; TR0 1; // 启动计数 } // 定时读取计数值并处理溢出 void Read_Encoder(void) { static unsigned int last_cnt 0; unsigned int now_cnt (TH0 8) | TL0; int delta now_cnt - last_cnt; // 计算增量 last_cnt now_cnt; enc_count delta; }逻辑说明TMOD 的低四位设成 0x05表示 T0 工作在模式 1、外部计数方式计数脉冲从 T0 引脚输入。TH0 和 TL0 组成 16 位计数器溢出后回零。Read_Encoder 里用当前值减上次值得到增量这样即使溢出也能算对前提是两次读取间隔内增量不超过 65535。参数上TR01 启动计数读取时先读 TH0 再读 TL0顺序不能反否则可能读到错位的数据。这种方案下方向判断通常还要靠 B 相中断或者状态机。如果程序里只有计数没有方向那它可能只适合测速不适合测位置。3.4 用串口打印验证计数和方向代码烧进去之后怎么知道读对了最直接的办法是用串口把计数打出来。初始化串口// 串口初始化9600波特率11.0592MHz晶振 void UART_Init(void) { SCON 0x50; // 模式1允许接收 TMOD 0x0F; TMOD | 0x20; // T1 模式2自动重装 TH1 0xFD; // 9600 波特率 TL1 0xFD; TR1 1; // 启动 T1 } // 发送一个字节 void UART_SendByte(unsigned char dat) { SBUF dat; while (!TI); TI 0; }然后在主循环里定期打印 enc_countvoid main(void) { Encoder_Init(); UART_Init(); while (1) { // 每秒打印一次计数 UART_SendByte(enc_count 8); UART_SendByte(enc_count 0xFF); Delay_ms(1000); } }逻辑说明TH10xFD 对应 9600 波特率前提是晶振 11.0592MHz。如果你用的是 12MHz 晶振这个值不对串口会出乱码。参数上SCON0x50 是串口模式 1、允许接收。打印时把 16 位计数拆成高低字节发出去上位机再拼回来。验证方法手动正转编码器计数应该增加反转应该减少。如果方向反了把中断里 B 相判断的高低电平对调即可。4. 避坑与排查编码器读数不准的五个典型现场4.1 计数只增不减或只减不增现象不管正转反转计数一直往一个方向走。原因方向判断逻辑写反了或者 B 相根本没接对。解决先确认 B 相接线再用示波器看 A/B 相位关系。如果硬件没问题把中断里if (ENC_B 0)改成if (ENC_B 1)试试。还有一种可能是编码器输出是差分信号你只接了单端相位信息丢了。4.2 低速时计数虚高高速时丢脉冲现象慢慢转的时候计数比实际多转快了又比实际少。原因低速虚高是机械抖动导致一个脉冲进多次中断高速丢脉冲是中断响应不过来。解决低速抖动加软件消抖比如进中断后延时几十微秒再读 B 相或者用状态机法。高速丢脉冲只能换方案用定时器硬件计数或者降低编码器线数、提高单片机主频。这两个问题是矛盾的消抖延时太长又会加剧高速丢脉冲需要折中。4.3 串口打印乱码现象串口助手收到的是乱码不是正常数字。原因波特率不对通常是晶振频率和 TH1 初值不匹配。解决确认晶振是 11.0592MHz 还是 12MHz。11.0592MHz 配 0xFD 是 960012MHz 配 0xFD 会有误差。换晶振或者重算初值。另外检查串口助手波特率、数据位、停止位设置是否一致。4.4 中断进不去现象转动编码器计数纹丝不动。原因中断没使能或者触发方式不对。解决检查 IT0、EX0、EA 三个位是否都置位。如果编码器输出是集电极开路没有上拉电阻电平拉不高中断永远触发不了。加一个 4.7k 到 10k 的上拉电阻到 VCC。还有可能是引脚被其他功能占用了比如 P3.2 同时也是某些芯片的 PWM 输出要确认复用功能没开。4.5 计数偶尔跳变一个大数现象计数大部分时候正常偶尔跳一个很大的值。原因读取 16 位计数值时没有保护读 TH0 和 TL0 之间发生了进位。解决读之前先关中断读完再开或者连续读两次两次一致才采用。定时器计数法尤其要注意这个血泪经验是溢出和读取撞在一起数据就飞了。5. 从测试程序到可用模块把计数封装成可复用接口测试程序跑通之后下一步是把它变成你自己项目里能直接调的模块。我一般会做三件事把编码器相关代码抽成独立的 .c/.h 文件对外只暴露初始化、读计数、读方向、清零四个接口把消抖和溢出处理封在内部调用者不用关心加一个转速计算函数用定时器定期算脉冲频率再换算成 RPM。// encoder.h 对外接口 void Encoder_Init(void); int Encoder_GetCount(void); unsigned char Encoder_GetDir(void); void Encoder_Clear(void); int Encoder_GetSpeedRPM(unsigned int ppr, unsigned int ms);Encoder_GetSpeedRPM 的思路是在固定时间窗口 ms 内读两次计数差值除以线数 ppr再换算成每分钟转速。参数 ppr 是编码器每转脉冲数注意有些编码器标的是线数四倍频后才是每转脉冲数别搞混。这个函数我一般放在定时器中断里每秒调一次主循环只管取结果。验证方法用手转动编码器一整圈看计数变化是否等于 ppr 的四倍如果用了四倍频或一倍。再配合秒表转一圈看 RPM 是否合理。从那以后我每次拿到新的编码器都强制走一遍「示波器看波形、串口打计数、手动转一圈核对」这三步再往项目里集成。希望帮到你。本文还有配套的精品资源点击获取