资讯动态

C51增量式编码器测试程序:四倍频计数与方向判别实战

发布时间:2026/10/19 8:33:21 来源:尧图企业网站定制
简介这份资源是面向单片机初学者与嵌入式开发者的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.uvprojKeil 工程文件是STARTUP.A5151 启动代码是*.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.2INT0 外部中断引脚B 相接 P3.3INT1 外部中断引脚为什么优先选 P3.2 和 P3.3因为 51 单片机的这两个引脚自带外部中断功能编码器脉冲频率稍高时用中断比用轮询靠谱得多。如果程序里用的是轮询方式那接普通 IO 口也能跑但高速转动时容易丢脉冲。具体接哪两个脚以encoder.h里的宏定义为准下面会讲到怎么改。提示接线前先用万用表确认编码器供电正常A/B 相在缓慢转动时电压应在 0V 和 5V 之间跳变。如果一直不变可能是编码器坏了或者线接错了。2.3 串口输出的硬件准备程序要把计数值打印出来就得用串口。51 单片机通过 TXDP3.1发送数据RXDP3.0接收。你只需要把开发板的 TXD 接到 USB 转串口模块的 RXDGND 对接然后在电脑上用串口助手打开对应端口波特率设成程序里配置的值常见是 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对应 INT0interrupt 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单独抽出来当成一个独立模块维护主程序只调用接口。这样换芯片、换编码器、改倍频方式时改动范围可控不会牵一发动全身。希望这份拆解能帮你少走点弯路顺利把编码器数据读稳。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑