简介:这份资源是面向单片机初学者与嵌入式开发者的C51增量式旋转编码器读取测试程序,围绕脉冲编码器信号采集这一常见场景,重点演示goto跳转与不等比较跳转的流程控制写法,并配合定时器开关、延时2秒显示参数等环节,帮助读者理解编码器读取与状态输出的基本逻辑。压缩包共22个文件,约56KB,包含2个c源码、2个hex烧录文件、2个uvproj工程文件及uvopt配置,另有lst、m51、plg等编译中间文件与bak备份,便于直接打开工程编译验证或对照分析。资源已有1432人学习下载,适合刚接触C51与编码器接口的读者参考。通过源码与工程文件,可掌握跳转判断、P0口输出、定时器启停与延时显示等关键写法,并借助编译产物排查问题,快速搭建自己的编码器测试环境。
1. 从“电机转了多少格”说起:一份能直接烧进板子的 C51 增量式编码器测试程序
很多做单片机项目的开发者第一次接旋转编码器,都会遇到同一个尴尬:串口打印出来的数要么纹丝不动,要么疯狂乱跳,明明电机在转,程序却像在“装死”。这份 C51 增量式读旋转编码器测试程序,解决的就是这个最基础也最容易翻车的问题——它用 51 单片机把增量式脉冲编码器的 A/B 两相脉冲读进来,做四倍频计数和方向判别,再把结果通过串口输出,让你能直观看到正转加、反转减。它适合刚接触脉冲编码器的新手拿来跑通第一组数据,也适合做电机测速、位置闭环的开发者当底层驱动模板。整个工程是 Keil C51 可直接编译的完整工程,不是零散代码片段,烧录后接上编码器就能验证。
2. 工程结构与硬件接线:先搞清楚信号从哪来、进哪个引脚
拿到一个 .rar 压缩包,别急着双击打开就编译。我一般先看目录,确认它是不是一个完整的 Keil 工程,还是只有几个 .c 文件需要自己搭壳。这份测试程序的结构比较典型,属于“最小可运行工程”的布局,理解它之后你换任何一款 51 芯片都能快速迁移。
2.1 目录里都有什么
解压后通常能看到这样的文件分布,不同版本可能略有出入,但核心文件跑不掉:
| 文件/目录 | 作用 | 是否必须 |
|---|---|---|
main.c | 主循环、编码器读取、串口打印 | 是 |
encoder.c | 编码器脉冲采集与方向判别 | 是 |
encoder.h | 引脚定义、函数声明、全局计数变量 | 是 |
uart.c | 串口初始化与发送 | 是 |
uart.h | 波特率、串口函数声明 | 是 |
delay.c | 短延时函数 | 是 |
project.uvproj | Keil 工程文件 | 是 |
STARTUP.A51 | 51 启动代码 | 是 |
*.hex | 编译好的烧录文件 | 否,可自己重新编译 |
看到STARTUP.A51和.uvproj同时存在,基本可以确定这是一个用 Keil uVision 建的完整工程,不需要你手动去配启动文件。如果压缩包里只有.c和.h,那就得自己新建工程、选芯片型号、把文件加进去,这一步对新手来说反而是最容易卡住的地方。
2.2 编码器 A/B 相到底接哪两个引脚
增量式脉冲编码器一般有四根线:VCC、GND、A 相、B 相。有些还带 Z 相(零位信号),这份测试程序通常只处理 A/B 两相,Z 相不接或悬空即可。A/B 两相输出的是方波,相位差 90 度,正转时 A 超前 B,反转时 B 超前 A,程序就是靠这个相位关系判断方向的。
接线时我一般会这样分配:
- 编码器 VCC 接单片机开发板的 5V(注意编码器供电范围,有的是 3.3V)
- 编码器 GND 接开发板 GND,共地是必须的,不共地信号会飘
- A 相接 P3.2(INT0 外部中断引脚)
- B 相接 P3.3(INT1 外部中断引脚)
为什么优先选 P3.2 和 P3.3?因为 51 单片机的这两个引脚自带外部中断功能,编码器脉冲频率稍高时,用中断比用轮询靠谱得多。如果程序里用的是轮询方式,那接普通 IO 口也能跑,但高速转动时容易丢脉冲。具体接哪两个脚,以encoder.h里的宏定义为准,下面会讲到怎么改。
提示:接线前先用万用表确认编码器供电正常,A/B 相在缓慢转动时电压应在 0V 和 5V 之间跳变。如果一直不变,可能是编码器坏了或者线接错了。
2.3 串口输出的硬件准备
程序要把计数值打印出来,就得用串口。51 单片机通过 TXD(P3.1)发送数据,RXD(P3.0)接收。你只需要把开发板的 TXD 接到 USB 转串口模块的 RXD,GND 对接,然后在电脑上用串口助手打开对应端口,波特率设成程序里配置的值(常见是 9600 或 115200)。如果打印出来是乱码,十有八九是波特率对不上,或者晶振频率和程序里写的不一致——这是新手最常踩的坑之一。
3. 核心代码拆解:四倍频计数和方向判别是怎么实现的
这份程序最值钱的部分,就是编码器读取那几十行代码。它决定了你能不能拿到稳定、不丢步的计数值。下面按“引脚定义 → 中断服务 → 方向判别 → 主循环输出”的顺序,把关键实现逐段讲清楚。
3.1 引脚与全局变量定义
先看encoder.h里通常会有的定义:
// encoder.h #ifndef __ENCODER_H #define __ENCODER_H #include <reg52.h> // 编码器 A/B 相引脚定义,按实际接线修改 sbit ENC_A = P3^2; // A 相接 INT0 sbit ENC_B = P3^3; // B 相接 INT1 // 全局计数值,volatile 防止编译器优化 extern volatile int encoder_count; // 函数声明 void encoder_init(void); int encoder_get_count(void); void encoder_reset(void); #endif这里有两个细节值得说。第一,sbit是 Keil C51 特有的位定义方式,直接把变量绑到具体引脚上,读写它就等于读写引脚电平。第二,encoder_count用了volatile修饰,因为它在中断里被修改、在主循环里被读取,不加volatile编译器可能把它缓存到寄存器里,导致主循环读到的永远是旧值——这个坑我见过太多次,现象就是“中断明明进了,主循环数值不变”。
3.2 外部中断初始化
编码器脉冲来一个就得处理一个,所以用外部中断最合适。初始化代码一般长这样:
// encoder.c #include "encoder.h" volatile int encoder_count = 0; void encoder_init(void) { // 配置 INT0 和 INT1 为下降沿触发 IT0 = 1; // INT0 下降沿触发 IT1 = 1; // INT1 下降沿触发 EX0 = 1; // 使能 INT0 中断 EX1 = 1; // 使能 INT1 中断 EA = 1; // 打开总中断 }IT0 = 1和IT1 = 1把两个外部中断都设成下降沿触发。为什么用下降沿而不是低电平?因为低电平触发在信号持续为低时会反复进中断,计数直接翻倍乱掉。下降沿触发每个脉冲只响应一次,干净利落。EX0、EX1是中断使能位,EA是总开关,三个都得打开,少一个中断都不会进。
3.3 中断服务函数与方向判别
这是整个程序的核心逻辑。A/B 两相各接一个中断,在中断里读另一相的电平来判断方向:
// INT0 中断服务:A 相下降沿触发 void int0_isr(void) interrupt 0 { // A 相下降沿时,若 B 相为高,说明 A 超前 B,正转 if (ENC_B == 1) { encoder_count++; } else { encoder_count--; } } // INT1 中断服务:B 相下降沿触发 void int1_isr(void) interrupt 2 { // B 相下降沿时,若 A 相为低,说明 A 超前 B,正转 if (ENC_A == 0) { encoder_count++; } else { encoder_count--; } }逻辑说明:正转时 A 相超前 B 相 90 度。当 A 相出现下降沿的那一刻,B 相还是高电平,所以ENC_B == 1判为正转,计数加一。反过来,B 相出现下降沿时,A 相已经是低电平,所以ENC_A == 0也判为正转。两个中断配合,A 相和 B 相的每个下降沿都计一次,一个完整周期计两次,这就是所谓的“二倍频”。如果再加上上升沿触发,就能做到四倍频,分辨率再翻一倍。
参数说明:interrupt 0对应 INT0,interrupt 2对应 INT1,这是 51 单片机固定的中断向量号,不能随便改。中断函数里尽量只做计数,不要放串口打印、延时这类耗时操作,否则高频脉冲时会丢步。
3.4 主循环读取与串口输出
中断负责计数,主循环负责把数取出来打印:
// main.c #include "encoder.h" #include "uart.h" #include <stdio.h> void main(void) { int last_count = 0; int now_count = 0; uart_init(); // 串口初始化,波特率 9600 encoder_init(); // 编码器中断初始化 printf("Encoder Test Start\r\n"); while (1) { now_count = encoder_get_count(); // 只有计数变化时才打印,避免串口刷屏 if (now_count != last_count) { printf("Count = %d\r\n", now_count); last_count = now_count; } } }这里用了一个小技巧:只有计数值变化时才打印。如果每轮循环都打印,串口会被无意义的数据淹没,反而看不清变化。encoder_get_count()内部就是返回encoder_count,但建议在函数里先关中断、读值、再开中断,保证读到的值不会被中断打断:
int encoder_get_count(void) { int val; EA = 0; // 关总中断 val = encoder_count; // 读取计数值 EA = 1; // 开总中断 return val; }对于 16 位变量,51 单片机读取时不是原子操作,可能读到一半被中断改掉,导致数据错乱。关中断再读是最简单的保护方式,读一个 int 的时间极短,不会影响脉冲采集。
4. 编译、烧录与串口验证:让数字真正动起来
代码看懂了,接下来得让它跑起来。这一章按实际操作顺序走一遍,从 Keil 编译到串口看到数据,每一步都给出判断标准,避免你卡在某个环节不知道对不对。
4.1 Keil 工程编译设置
打开.uvproj工程后,先确认三件事:
第一,芯片型号选对了没有。在Project → Options for Target → Device里看选的型号,常见的是 AT89C51、AT89S52、STC89C52 等。型号不对可能编译能过但烧录不进去。
第二,晶振频率填对了没有。在Target标签页的Xtal (MHz)里填实际晶振频率,一般是 11.0592MHz 或 12MHz。这个值直接影响串口波特率,填错了打印就是乱码。
第三,输出 HEX 文件勾上了没有。在Output标签页勾选Create HEX File,否则编译完找不到烧录文件。
设置好之后点Rebuild,如果底部窗口显示0 Error(s),说明编译通过。有警告可以先不管,但错误必须解决。
4.2 烧录与接线检查
用烧录器把生成的.hex文件写进单片机。烧录完成后,按第 2 章说的接好编码器 A/B 相和串口线。上电前再检查一遍:
- 编码器供电是否在额定范围
- A/B 相是否接到了程序定义的引脚
- 串口 TXD/RXD 是否交叉连接(开发板 TXD 接模块 RXD)
- 共地是否接好
上电后,打开串口助手,选对端口号,波特率设成 9600(和程序里一致),数据位 8、停止位 1、无校验。缓慢转动编码器,应该能看到Count值跟着变化:正转增加,反转减少。
4.3 验证计数是否准确
怎么判断计数值对不对?一个简单方法:手动把编码器转一整圈,看计数变化量是否等于“线数 × 倍频数”。比如编码器标称 20 线,二倍频后转一圈应该是 40 个计数。如果差太多,可能是丢步;如果正转反转都增加,说明方向判别逻辑反了,把两个中断里的判断条件对调即可。
注意:手动转动速度要慢且均匀,快速甩动容易超过中断响应速度导致丢步,这不能说明程序有问题。
5. 避坑与常见问题排查:那些让我熬夜的编码器玄学
编码器调试有几个经典问题,几乎每个做 51 编码器项目的人都会遇到。下面按“现象 → 原因 → 解决”整理,都是实际踩过的坑。
5.1 计数只增不减,或者只减不增
现象:无论正转反转,计数值都朝一个方向跑。
原因:方向判别逻辑写反了,或者 A/B 相接反了。中断里判断另一相电平的条件和实际相位关系不匹配。
解决:先把 A/B 相接线对调试试;如果不行,把中断服务函数里的if条件取反。比如 INT0 里原来是if (ENC_B == 1),改成if (ENC_B == 0)。两个中断要同时改,保持逻辑一致。
5.2 数值乱跳,静止时也在变
现象:编码器不转,串口却不断打印变化的计数值。
原因:最常见的是引脚悬空或接触不良,输入电平不确定,干扰被当成脉冲。其次是编码器供电不稳,或者没有共地。
解决:检查 A/B 相接线是否牢固,必要时在 A/B 相与地之间加 10k 下拉电阻,让悬空时电平确定为低。确认编码器和单片机共地。如果用的是长线,考虑加屏蔽或缩短线长。
5.3 串口打印乱码
现象:串口助手收到一堆看不懂的字符。
原因:波特率不匹配,或者晶振频率和程序里配置的不一致。比如程序按 11.0592MHz 算的波特率,实际焊的是 12MHz 晶振。
解决:确认实际晶振频率,改uart.c里的定时器初值,或者直接换 11.0592MHz 晶振。11.0592MHz 能整除出标准波特率,串口通信更稳,这也是为什么老工程师都推荐用它。
5.4 高速转动时丢步
现象:慢转计数准确,快转时计数值明显偏小。
原因:中断响应不过来,或者中断里做了耗时操作。51 单片机中断响应需要时间,脉冲频率太高时确实会丢。
解决:精简中断函数,只保留计数逻辑,去掉任何打印、延时。如果还不行,考虑换更高主频的芯片,或者改用硬件计数器(如定时器的计数模式)来采集脉冲,而不是靠外部中断。
5.5 编译报错“未定义符号”
现象:Keil 编译提示某个函数或变量找不到。
原因:头文件没包含,或者.c文件没加到工程里。Keil 不会自动扫描目录下的所有源文件,必须手动添加。
解决:在左侧工程树里右键Source Group,选择Add Files to Group,把encoder.c、uart.c等加进去。确认每个.c文件都包含了对应的.h。
6. 二次开发改造:从测试程序到能用的测速与位置闭环
这份测试程序本身只是个“能读数”的 Demo,但它的价值在于可以当底层驱动直接嵌进更大的项目。下面说几个我实际改过的方向,以及改造时要注意的地方。
6.1 把计数改成测速
光有位置计数还不够,很多场景要的是转速。思路很简单:固定时间间隔(比如 100ms)读一次计数,两次计数之差就是这段时间内的脉冲数,再换算成转速。
// 每 100ms 调用一次,计算转速 int last_cnt = 0; int speed_rpm = 0; void calc_speed(void) { int now_cnt = encoder_get_count(); int diff = now_cnt - last_cnt; // 100ms 内的脉冲数 last_cnt = now_cnt; // 假设编码器 20 线,二倍频,转一圈 40 个脉冲 // 100ms 是 1/600 分钟,所以 rpm = diff * 600 / 40 speed_rpm = diff * 600 / 40; }这个计算放在定时器中断里,每 100ms 触发一次。注意diff可能是负数(反转),显示时取绝对值或者带符号都行,看你的需求。实际项目中我会把40这个数定义成宏PULSE_PER_ROUND,换编码器时只改一处。
6.2 改成四倍频提高分辨率
前面用的是下降沿触发,一个周期计两次。如果改成双边沿触发,一个周期能计四次,分辨率翻倍。51 单片机的外部中断只支持下降沿或低电平,做不了双边沿,但可以用定时器的计数模式,或者用普通 IO 配合定时器轮询来抓上升沿。另一种取巧办法是 A/B 两相都接外部中断,同时在中断里读另一相,这样已经做到了二倍频;想再翻倍就得换方案。
我一般会先评估精度够不够。如果 20 线编码器二倍频后转一圈 40 个计数,对于大多数测速场景已经够用。真要更高分辨率,直接换高线数编码器比改程序省事。
6.3 加一个位置闭环的接口
如果要做位置控制,可以把encoder_count作为反馈量,和目标位置比较,输出 PWM 控制电机。改造时注意:计数变量是int类型,范围 -32768 到 32767,高线数编码器转几圈就可能溢出。解决办法是改成long类型,或者在溢出前做归零处理。
// 位置闭环伪代码 long target_pos = 1000; // 目标位置 long current_pos = 0; void position_loop(void) { current_pos = encoder_get_count(); long error = target_pos - current_pos; if (error > 0) { // 正转,输出 PWM } else if (error < 0) { // 反转 } else { // 到位,停止 } }这里只是示意逻辑,实际还要加 PID 调节、死区处理、限幅保护。但底层计数部分不用动,直接复用这份测试程序的encoder.c就行。
6.4 改造时最容易忽略的一点
我改这份程序时翻过一次车:把encoder_count从int改成long之后,忘了在encoder_get_count()里同步改返回类型,结果高位数据被截断,位置一超过 32767 就跳回负数。从那以后我每次改变量类型,都强制走一遍“声明 → 定义 → 返回 → 打印”四个地方,确认全部同步。这个习惯帮我省了不少调试时间。
如果你也打算在这份测试程序上做二次开发,建议先把encoder.c和encoder.h单独抽出来,当成一个独立模块维护,主程序只调用接口。这样换芯片、换编码器、改倍频方式时,改动范围可控,不会牵一发动全身。希望这份拆解能帮你少走点弯路,顺利把编码器数据读稳。
本文还有配套的精品资源,点击获取