简介:面向51单片机初学者的编码器测速与测方向完整方案,包含原理图、流程图、物料清单、仿真图和Keil源码,可直接用于课程设计或项目复现。项目以电机带动编码器产生脉冲,通过单片机测量实现转速与旋转方向判断,并用LCD1602显示结果;仿真中使用带编码器的电机模型模拟机械式编码器,便于无硬件时开展实验,也解决了编码器仿真模型难以获取的问题。资源共37个文件,压缩包约661KB,核心内容包括C程序源码、Keil工程与hex烧录文件、Proteus仿真的DSN工程、AD原理图及PDF预览、元件清单Excel、流程图BMP等,覆盖从设计到调试的主要环节。目前已有236人学习下载。整体适合课程设计或毕业设计参考,可结合物料清单搭建实物,也可对照源码与仿真理解编码器脉冲计数、方向判别和LCD显示的实现思路,对入门51单片机的外设编程很有帮助。
1. 基于51单片机的编码器测速系统,先把“方向”从脉冲里读出来
增量式编码器在51单片机场景里常被当成“转一圈能输出多少个脉冲的传感器”,但真正坑人的不是数脉冲,而是方向怎么判。电机轴带编码器转动时,会从A、B两个通道输出相位差90°的方波,转速藏在脉冲频率里,旋转方向则藏在这一路相对另一路的相位关系里。只盯着单路脉冲看,永远读不出方向。这套基于51单片机的编码器速度、方向测量项目,把编码器接上电机轴,通过外部中断加定时器采集A/B相信号,最后用LCD1602显示实时转速和正反转状态,配套原理图、流程图、物料清单、Proteus仿真图和C51源码一应俱全。适合刚做完定时器和中断实验、准备切入电机控制的开发者,下文先讲清楚编码器信号本身,再落到原理图选型、源代码实现和仿真联调。
2. 编码器A/B相正交信号与方向判别:从时序到鉴相逻辑
2.1 A/B相输出为什么能同时提供速度和方向
增量式编码器内部码盘转动时,会从OUT_A和OUT_B两个通道输出相位差90°的方波。转一圈输出的脉冲数就是编码器线数(PPR),转速越高脉冲频率越高,所以速度信息实际在脉冲频率里;方向信息则藏在A、B两路的相位关系里。若A相超前B相90°,约定为正转,那么B相超前A相90°就代表反转。只看某一路脉冲本身,得不到方向,必须同时监测两路信号,这正是原理图里要把A、B两路都接进51单片机的原因。
从波形上看,正转时A相上升沿附近B相是低电平;反转时同一上升沿附近B相是高电平。基于这个约定,方向判断可以只盯住沿触发那一瞬间另一路电平的高低。具体对应关系如下表。
| A相边沿 | B相电平 | 判断方向 |
|---|---|---|
| 上升沿 | 低 | 正向 |
| 上升沿 | 高 | 反向 |
| 下降沿 | 高 | 正向 |
| 下降沿 | 低 | 反向 |
这里的方向约定取决于编码器安装时A/B哪路在前,实物装反了会出现“明明是正转却显示反转”,处理办法是交换A/B输入,或者在软件里对方向标志取反。仿真里用的MOTOR-ENCODER同理,先把两个输出通道按顺序接好,观察一遍波形再写代码最省事。
2.2 外部中断里判方向的代码模型
方向判断在51上最常见的做法是:A相接INT0(P3.2),B相接普通IO(比如P3.3),INT0配置为边沿触发。进入中断后读一次B相电平,按上面的真值表给出方向。机械编码器低速时触点会抖动,可能导致同一边沿触发多次,脉冲计数和方向都会出错,所以抖动处理要放在中断入口处。
sbit ENC_A = P3^2; /* A相接外部中断0 */ sbit ENC_B = P3^3; /* B相接普通IO */ unsigned char enc_dir = 0; /* 0:正转 1:反转 */ unsigned int enc_count = 0; /* 正转累加, 反转递减 */ void INT0_ISR(void) interrupt 0 { if (ENC_B == 0) /* A上升沿时B为低 => 正转 */ { enc_dir = 0; enc_count++; } else /* A上升沿时B为高 => 反转 */ { enc_dir = 1; enc_count--; } }外层模块每100ms读取一次enc_count差值来计算速度。enc_count按有符号语义维护,反转时减小,这样即使方向频繁切换,速度计算也不会出现“正转累加、反转也累加”的错误。中断里要尽量减少耗时操作,LCD刷新这类动作放在主循环里做,避免在中断里调用延时函数。
除了在A相上升沿判方向,还可以在A相上下沿都触发,或者A/B两路都参与判向,分别对应2倍频和4倍频。4倍频下分辨率提高4倍,但中断频率也变成4倍,12MHz晶振、低速电机场景下51完全扛得住。源码main.c里用的是单倍频加方向修正,想要更高分辨率就把INT0和INT1都利用起来,A/B相各接一路外部中断,两边都做鉴相,计数频率翻倍。跨入4倍频后计数变量要换成长整型,防止高转速下溢出。
2.3 为什么仿真里没有机械编码器
Proteus元器件库里没有单独的机械编码器元件,这类编码器仿真通常都是用MOTOR-ENCODER这个带编码器的直流电机实现的。它的电机轴转动后会输出两路脉冲,外部看起来就是一个完整的编码器信号源,还能通过调节电机电压改变转速,比纯脉冲发生器更接近真实场景。这也是仿真图里用电机自带编码器的原因:把电机转起来,就等于编码器在输出A/B相脉冲,后面所有代码逻辑不用改就能直接验证。
3. 原理图与物料清单落地:STC89C52最小系统加编码器接口设计
3.1 从Sheet1.SchDoc看电路结构
原理图用Altium Designer打开Sheet1.SchDoc查看,结构上分四块:以STC89C52RC为核心的最小系统、LCD1602显示电路、编码器接口电路和电源电路。最小系统里晶振选12MHz,两个33pF负载电容接地;复位电路用10k电阻加10uF电容;电源处放一个100uF电解电容加0.1uF瓷片电容做去耦。12MHz晶振的定时器初值好算,100ms时基下没有明显累积误差,这个项目纯LCD显示,不需要纠结11.0592MHz的串口波特率,12MHz是更省心的选择。
编码器接口在原理图上体现为排针或电机端子,A相接P3.2,B相接P3.3,两个引脚各接一个10k上拉电阻。真实增量式编码器很多是开集电极输出,没有上拉时51读不到稳定电平,上拉电阻属于必须项;仿真里MOTOR-ENCODER输出已经接近TTL电平,不加上拉也能跑,但接上更贴近实物。上拉阻值选10k是通用值,上拉能力够、功耗也低。
提示:如果实际编码器线缆超过1米,建议在A/B进入单片机前加一级施密特触发缓冲,比如74HC14,或者至少加RC低通滤波,否则电机启停瞬间的电磁干扰会让中断反复触发。
3.2 LCD1602接口与对比度
LCD1602接线用标准4位模式:D4-D7接P0.4-P0.7,RS接P1.0,EN接P1.1,RW直接接地。P0口内部没有上拉电阻,D4-D7这一组必须接10k排阻到VCC,否则LCD收到的数据全是低电平,显示出来永远是黑块。Vo脚接10k电位器中间抽头,电位器两端接VCC和GND,用来调对比度。仿真里对比度调不好最常见的问题就是屏幕有字但特别浅,或者满屏黑格。
3.3 物料清单怎么核对
资料里的元件清单.xlsx列出了完整BOM,把原理图所有器件整理成表。核对时建议按电路模块走,不要按元件类型走,最容易漏的是复位电容和LCD对比度电位器。下面简化表以实际xlsx为准。
| 模块 | 器件 | 参数/型号 | 数量 | 备注 |
|---|---|---|---|---|
| 最小系统 | 单片机 | STC89C52RC | 1 | 40pin DIP或LQFP |
| 最小系统 | 晶振 | 12MHz | 1 | 测速用整数好算 |
| 最小系统 | 负载电容 | 33pF | 2 | 晶振对地 |
| 最小系统 | 复位电容/电阻 | 10uF/10k | 各1 | 高电平复位 |
| 电源 | 去耦电容 | 100uF+0.1uF | 各1 | 电源入口处 |
| 显示 | LCD1602 | 16x2 | 1 | 4位数据模式 |
| 显示 | 排阻 | 10k | 1 | P0口上拉 |
| 显示 | 电位器 | 10k | 1 | 对比度调节 |
| 编码器 | 电机编码器 | MOTOR-ENCODER | 1 | 仿真中替代机械编码器 |
| 编码器 | 上拉电阻 | 10k | 2 | A/B相上拉 |
核对物料时要特别注意编码器的供电电压。仿真里MOTOR-ENCODER的电机部分和编码器部分供电电压可以分开,电机接可调直流电源,编码器输出端直接进单片机即可。实物编码器则要看手册,常见5V供电,与51系统共地是必须的。
3.4 接线顺序与上电检查
搭电路时建议按最小系统、显示、编码器的顺序接线。最小系统焊完先烧一个LED闪烁程序确认晶振起振,再焊接LCD1602,最后接编码器。如果LCD一上电就满屏黑格,先查Vo电位器和P0上拉;如果上电后方向显示不对,先交换A/B两根线而不是改代码,这样可以快速确认是接线问题还是程序问题。测速必须共用一条GND回路,实测中发现编码器地和单片机地分开接,脉冲经常乱跳。
4. C51程序设计:定时器计数、方向判断与LCD1602显示
4.1 软件流程对应流程图.bmp
流程图.bmp给出了完整程序结构,核心是三个任务:主循环、定时器中断、外部中断0。主流程上电后先做LCD1602初始化,然后进入while循环,循环里每100ms从标志位判断是否到了采样时刻,到了就取enc_count差值,乘上系数算转速,再调用LCD刷新函数显示速度值和方向字符串。外部中断0负责在A相每个上升沿记录一次方向并更新计数;定时器1负责产生100ms时基,置采样标志位。流程图分层就是这句话的图形化:中断里只干中断该干的事,显示和计算全部放主循环。
4.2 定时器和测速的完整代码
定时器1采用方式1,16位计数。12MHz晶振下,机器周期1us,100ms需要100000个机器周期,16位定时器最大计数65536,装不下100ms。工程里的做法是把时基拆开:定时器初值设为0x3CB0,对应50ms中断,再用一个软计数变量累加两次得到100ms中断周期。这样定时器中断50ms进一次,软计数到2才置采样标志,主循环按100ms的周期刷新一次数据。
#include <reg52.h> #include <stdio.h> #include "lcd1602.h" #define PPR 20 /* 编码器线数, 仿真电机默认每转输出20个脉冲, 按实际改 */ sbit ENC_A = P3^2; sbit ENC_B = P3^3; unsigned int enc_count = 0; bit enc_dir = 0; /* 0正向 1反向 */ bit sample_flag = 0; /* 100ms采样标志 */ unsigned char timer_soft = 0; unsigned int last_count = 0; /* 定时器1初始化: 50ms中断, 软计数2次得到100ms */ void Timer1_Init(void) { TMOD &= 0x0F; /* 只改T1部分, 保留T0配置 */ TMOD |= 0x10; /* 定时器1, 方式1, 16位 */ TH1 = 0x3C; /* 初值高8位 */ TL1 = 0xB0; /* 初值低8位, 65536-50000=15536=0x3CB0 */ ET1 = 1; EA = 1; TR1 = 1; } void Timer1_ISR(void) interrupt 3 { TH1 = 0x3C; /* 重装初值 */ TL1 = 0xB0; timer_soft++; if (timer_soft >= 2) { timer_soft = 0; sample_flag = 1; /* 通知主循环采样 */ } } void INT0_ISR(void) interrupt 0 { if (ENC_B == 0) /* A上升沿时B低 => 正转 */ { enc_dir = 0; enc_count++; } else { enc_dir = 1; enc_count--; } } void main(void) { unsigned int curr_count; unsigned int pulse_cnt; unsigned int speed; unsigned char str[16]; LCD_Init(); /* 4位模式初始化LCD */ Timer1_Init(); IT0 = 1; /* INT0下降沿触发 */ EX0 = 1; LCD_ShowString(1, 0, "Speed: 0 r/min"); LCD_ShowString(2, 0, "Dir: CW"); while (1) { if (sample_flag) { sample_flag = 0; curr_count = enc_count; /* 100ms内的脉冲数, 注意方向变化时enc_count会减少 */ pulse_cnt = (curr_count > last_count) ? (curr_count - last_count) : (last_count - curr_count); last_count = curr_count; /* 转速 = 脉冲数 / (线数 * 时间s) * 60 */ speed = pulse_cnt * 600UL / PPR; /* 100ms, 即0.1s: 600 = 60 / 0.1 */ sprintf(str, "Speed:%3d r/min", speed); LCD_ShowString(1, 0, str); if (enc_dir == 0) LCD_ShowString(2, 0, "Dir: CW "); else LCD_ShowString(2, 0, "Dir: CCW"); } } }代码里几个点需要单独说明:pulse_cnt用绝对值算,防止方向在采样间隔内翻转过一次导致差值出现负数。enc_count是带符号语义的无符号变量,正转加、反转减,只要在采样间隔内方向翻转次数远小于脉冲总数,速度计算仍正确。sprintf格式化方便,但会占用较多代码空间,STC89C52RC有8K Flash,放得下。lcd1602.h里提供的LCD_ShowString第一个参数是行号,第二个是列号,刷新前先组装好整段字符串,可以避免LCD刷新时出现闪烁残影。
4.3 测速公式怎么来的
转速计算用M法,也就是测频法。转速(r/min)= 单位时间脉冲数 / PPR × 60。100ms采样一次,那么一分钟有600个采样周期,脉冲数×600再除以PPR就是每分钟转数。PPR是关键参数,仿真MOTOR-ENCODER每转输出脉冲数在器件属性里能看到,换成实物编码器时用编码器铭牌值替换。如果用4倍频,PPR要乘4,否则转速会显示成实际的4倍。
4.4 工程编译与hex生成
Keil工程文件是main_uvproj,打开后在Options for Target里确认晶振填12MHz,勾选Create HEX File,编译后生成main.hex。目录里的main.OBJ、main.LST和main.M51是编译中间产物,烧录只用main.hex。STC系列下载时波特率选低一点更稳,下载完按复位键程序开始跑。如果LCD上一直没有显示,优先确认hex是否生成成功,而不是反复改代码——Keil里漏勾Create HEX File是初学者最常踩的坑。
5. Proteus仿真电机编码器联调:DSN搭建、波形验证与常见排错
5.1 仿真工程怎么搭
打开资料里的仿真.DSN,里面已经放好了STC89C52、MOTOR-ENCODER、LCD1602、电阻电容和电源。电机编码器要转起来必须给电机部分接独立电源,把直流电源的电压从0V慢慢调到5V左右,电机转速跟着电压变化,编码器A/B输出频率也随之改变。MOTOR-ENCODER的脉冲输出引脚在DSN底部走线,对应接到了INT0和INT1。接好后在单片机属性里加载main.hex,点击运行,LCD1602上就能看到转速和方向。
需要注意DSN里单片机型号选的是STC89C52还是AT89C52,会直接影响程序是否跑得动。仿真里没有STC的精确时序模型时,指定为AT89C52更保险,引脚兼容,源码不用改。晶振频率在单片机属性里也写成12MHz,和Keil工程设置保持一致,否则定时器初值对应的时基就不对了。
5.2 常见仿真排错
| 现象 | 检查点 | 处理办法 |
|---|---|---|
| 电机不转 | 电机电源 | 给MOTOR-ENCODER电压端接直流电源并调高电压 |
| LCD黑格 | 对比度/Vo | 调Vo电位器,确认P0口上拉排阻存在 |
| 固定字符不刷新 | 定时器中断 | 确认晶振频率12MHz,TR1已置1 |
| 转速一直为0 | PPR与实际不符 | 示波器量A相频率,反推实际PPR |
| 方向显示相反 | A/B接反 | 交换A/B输入,或软件取反方向标志 |
加了电源、加载了hex,LCD还是没反应,按表里顺序排查。电机不转说明电源没接好或电压为0;LCD有固定字符但数字不刷新,检查定时器中断有没有触发,可以临时把speed位置写死一个值来判断是显示问题还是计算问题;转速显示一直是0但电机在转,大概率是PPR和实际脉冲频率对应不上,用示波器量一下A相频率换算就知道当前转速。
5.3 用示波器验证A/B相波形
Proteus里在MOTOR-ENCODER的两个脉冲输出脚挂虚拟示波器,运行后能看到两路占空比约50%的方波,相位差约90度。把示波器的显示频率换算成转速,和LCD显示值对比:假如A相频率是30Hz,编码器每转20个脉冲,转速应该是30/20×60=90 r/min。如果LCD显示180,说明PPR写成了10或者电路里用了2倍频。这个验证动作是整套工程里性价比最高的一步,比肉眼盯着数字变化可靠得多。
提示:仿真走完不代表实物一定能跑。MOTOR-ENCODER输出是理想方波,实物编码器输出有毛刺、有抖动、有电平偏移,实物调试时要保留原理图中的上拉电阻,必要时再软件滤波。
6. 测速精度提升与验证:M法低速误差、滤波与标定技巧
6.1 M法低速误差在哪
M法固定100ms采样窗,低速时窗内可能只有几个脉冲,量化误差会被放大。比如60 r/min、PPR 20时,每秒20个脉冲,100ms只有2个脉冲,转速计算结果要么0、要么30的倍数跳变。这是51单片机教程里常说的低速测不准,解决思路是拉长采样窗或改用T法。T法测的是相邻两个脉冲的时间间隔,低速时间隔长、定时器计数多,分辨率反而高。也可以M/T混合:高速用M法,低速自动切T法,切换点通常取每秒脉冲数低于20时。
6.2 软件滤波代码
对转速做滑动平均是最实用的手段。窗口取4或8,每次采样把历史值求和取平均,响应时间和平滑程度折中。
#define FILTER_N 4 unsigned int speed_buf[FILTER_N]; unsigned char speed_idx = 0; unsigned long speed_sum = 0; unsigned int Speed_Filter(unsigned int new_val) { speed_sum -= speed_buf[speed_idx]; /* 去掉最老的一个值 */ speed_buf[speed_idx] = new_val; /* 存入新值 */ speed_sum += new_val; speed_idx = (speed_idx + 1) % FILTER_N; return (unsigned int)(speed_sum / FILTER_N); }这个函数放入主循环替换直接显示speed的地方,算出来的数值更平滑。窗口越大越平但延迟越明显,如果电机加减速很快,窗口超过8会感觉显示跟不上实际转速。仿真里电机转速稳定,看不出太大差别,实物里这个滤波代码基本是保留项。
6.3 用频率计反推PPR完成标定
标定的目的是确认PPR到底是多少。仿真里做法简单:示波器读A相频率f,LCD转速为n时,PPR = f / n × 60。实物里用频率计或示波器量A相频率,再配合一个已知转速的场景,把显示的转速和频率换算值对齐,PPR就定下来了。标定完成后,把PPR以宏定义形式放在main.c顶部,后续所有改动都围绕这个常量展开,没必要再动测速公式。用示波器确认两路脉冲频率一致时,就可以用这个PPR值直接套用公式算转速。
本文还有配套的精品资源,点击获取