☰
C51增量式旋转编码器实战:从脉冲采集到方向判别的完整实现
2026/10/19 13:06:48 网站建设 项目流程

简介:这份资源是面向单片机初学者与嵌入式开发者的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 相接 INT0,B 相接普通 IO,A 相下降沿触发中断,在中断里读 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)正转下一状态反转下一状态
000110
011100
111001
100011

正转时状态按 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,是正转,计数加一;反之减一。参数上,IT0=1 设下降沿触发,EX0=1 开中断,EA=1 开总中断。这三个缺一不可。计数变量 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。参数上,TR0=1 启动计数,读取时先读 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_count:

void main(void) { Encoder_Init(); UART_Init(); while (1) { // 每秒打印一次计数 UART_SendByte(enc_count >> 8); UART_SendByte(enc_count & 0xFF); Delay_ms(1000); } }

逻辑说明:TH1=0xFD 对应 9600 波特率,前提是晶振 11.0592MHz。如果你用的是 12MHz 晶振,这个值不对,串口会出乱码。参数上,SCON=0x50 是串口模式 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 是 9600,12MHz 配 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 是否合理。从那以后我每次拿到新的编码器,都强制走一遍「示波器看波形、串口打计数、手动转一圈核对」这三步,再往项目里集成。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询