51单片机电机转速表设计:AD0832采样与定时器中断实现 简介电机转速表设计的51单片机完整工程包面向单片机初学者与嵌入式系统开发者用于掌握电机测速从脉冲采集、A/D转换到显示输出的全链路实现。工程核心为C语言源码包含AD0832采样、定时器/计数器测频、中断服务程序以及数码管或液晶显示驱动逻辑两个Hex文件可直接烧录至单片机运行。工程还包含Proteus仿真文件DSN主电路、DBK备份、PWI工程配置支持在无硬件环境下完成电路搭建、波形观测与系统排错便于快速验证阅读源码时理解的测速算法与中断响应流程。包内文件共7个压缩包仅61KB结构精简不冗余已有141人浏览学习对理解51单片机中断系统、定时器应用及嵌入式软硬件协同设计具有较高参考价值。1. 电机转速表里为什么同时出现AD0832和定时器拿到「51单片机源码程序-电机转速表设计」这个实例包时很多人会先打开ex100.DSN看到飞线密布的Proteus仿真图。真正卡住的是AD0832这颗硬件芯片它到底是用来测什么的为什么文件名里既有ex100.hex又有MoNi.hex这个项目用51单片机作为控制核心通过AD0832采集与转速成比例的模拟电压再结合定时器中断刷新显示最终在数码管上实时输出转速值。对于想完成单片机课程设计或者入门嵌入式系统的人来说一套能同时讲清模拟采样、中断定时和动态显示的样板工程非常难得。下面我把信号链路、源码框架和仿真排错逐层拆开。2. 转速信号链路模拟量采集与脉冲计数如何取舍2.1 测速发电机的输出电压为什么不能直接进51单片机电机转速表设计方案里被测量的物理量是转速但传感器端输出的往往是模拟电压。测速发电机的输出电压与转速近似成正比例如某型号在1000RPM时输出1V2000RPM时输出2V。51单片机是8位微控制器内部没有ADC模块数字量输入只能识别高电平或低电平。如果直接把模拟电压接到P1口引脚读到的不是1就是0无法得到中间值。因此必须外接AD0832这种8位串行A/D转换器把连续变化的电压离散成0到255之间的数字量再交给51单片机换算。这里与常见的脉冲计数方案有本质区别。脉冲计数法不需要ADC它把光电编码器或霍尔传感器的输出直接接到外部中断或计数器引脚通过测量脉冲频率得到转速。两者各有适用场景模拟量方案适合测速发电机、电流检测这类输出连续变化的设备接线简单但动态响应受采样周期影响脉冲计数方案适合输出TTL方波的传感器精度高、实时性好但传感器需要额外供电和整形电路。实例包里之所以有AD0832.DBK这个备份文件说明作者至少在仿真里完整搭了模拟量测速链路。2.1.1 AD0832接口连接与Proteus中的芯片匹配在ex100.DSN仿真电路里AD0832的引脚功能和实际芯片一致VCC接5V电源GND接地CS、CLK、DI、DO分别连接到51单片机的普通I/O口。差分或单端输入方式通过DI脚在转换启动时配置。下面给出一个常见连接参考表。AD0832引脚功能51单片机连接说明CS片选P1.0低电平启动转换转换完成后拉高CLK时钟输入P1.1由单片机产生串行时钟DI数据输入P1.2输入启动位和通道配置位DO数据输出P1.38位转换结果逐位输出CH0/CH1模拟输入传感器输出单端模式时一个接信号一个接地注意仿真不是实物DSN里的AD0832模型对时序比较敏感时钟频率太高容易读不到数据。我一般在P1口上并联20pF小电容限制边沿速率仿真时用时钟脉冲间隔两个_nop_()实测读出来的AD值才稳定。2.2 定时器/计数器在测速系统中的定位不管哪种测速方案转速最终都要落到“单位时间内的变化量”。对于模拟量方案51单片机需要知道“什么时刻去读AD值”并且要保证显示刷新不卡顿这就靠定时器中断来产生固定时间片。对于脉冲计数方案定时器用来产生1秒的闸门时间计数器用来累计外部脉冲两者配合才能算出频率。在本实例中定时器0运行在模式1也就是16位定时器模式。晶振12MHz时机器周期是1微秒。要产生50ms中断一次的中断节拍初值计算为65536-50000。这样每秒进入20次中断会计数变量每秒滚动20次。主程序里每隔20次中断做一次AD值平均得到1秒窗口内的平均转速。与纯延时函数不同中断方式让ADC采样和显示动态扫描各自在时间片上执行不会因为显示刷新占用CPU太久而丢失AD转换数据。2.3 从AD0832读取数据时序代码与换算公式下面这段C语言函数是读取单通道AD值的典型写法适用于Keil C51编译环境。引脚定义需要根据你自己的原理图修改。#include reg51.h #include intrins.h sbit ADCS P1^0; // 片选 sbit ADCLK P1^1; // 时钟 sbit ADDI P1^2; // 数据输入 sbit ADDO P1^3; // 数据输出 unsigned char read_ad0832(unsigned char channel) { unsigned char i, dat 0; ADCS 0; // 拉低片选启动转换 ADCLK 0; ADDI 1; // 启动位第一个时钟上升沿必须是高 _nop_(); ADCLK 1; _nop_(); ADCLK 0; ADDI channel; // 通道选择CH0或CH1 _nop_(); ADCLK 1; _nop_(); ADCLK 0; ADDI 1; // 单端模式选择位 _nop_(); ADCLK 1; _nop_(); ADCLK 0; for (i 0; i 8; i) // 读取8位转换结果 { ADCLK 1; _nop_(); ADCLK 0; _nop_(); dat 1; if (ADDO) dat | 0x01; } ADCS 1; // 结束转换释放片选 return dat; }这段代码的关键在于时序顺序CS拉低后必须先给一个时钟上升沿确认启动位再依次送入通道选择位和单端模式位。数据输出发生在CLK下降沿之后因此读取代码里先让CLK拉高再拉低下降沿稳定后再判断ADDO电平。如果顺序颠倒比如在上升沿读数很可能采到的都是前一位的残留值导致8位数据整体错位。得到AD值后还需换算成转速。假设参考电压Vref5V分辨率是5/255≈19.6mV。测速发电机灵敏度k0.001V/RPM即每转1圈输出1毫伏零速输出电压为零。那么转速计算公式如下#define VREF 5.0 #define K_SPEED 0.001 float v (float)ad_value * VREF / 255.0; float speed v / K_SPEED;浮点运算在51单片机上是能用但慢的如果只做4位数码管显示可以用定点整数避免float库带来的代码膨胀。比如已知1000RPM时AD采样值约等于51那么转速可以近似为unsigned int speed (unsigned int)ad_value * 1000u / 51u;这个简化在标定点附近精度尚可但要注意除法顺序。先乘1000可能导致16位溢出ad_value是8位乘1000最大255000超过unsigned int的65535因此需要强制转成unsigned longunsigned long temp (unsigned long)ad_value * 1000u; unsigned int speed (unsigned int)(temp / 51u);这样得到的是整数转速适合直接查表转换成数码管段码。3. 51单片机源码驱动与显示中断、滤波、动态扫描3.1 定时器0初始化与50ms中断节拍主程序里的定时器初始化决定了整个系统的采样节拍。使用12MHz晶振时51单片机的机器周期为1us定时器0在模式1下最大能定时65536us。为了兼顾显示刷新和AD滤波我习惯把中断周期设成50ms。void timer0_init(void) { TMOD 0xF0; // 不改变计数器1的配置 TMOD | 0x01; // 定时器0工作在模式1 TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; ET0 1; // 打开定时器0中断 TR0 1; // 启动定时器 EA 1; // 开总中断 } unsigned char tick20; void timer0_isr(void) interrupt 1 { TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; tick20; }注意定时器中断服务函数里必须重装初值。如果使用自动重装模式模式2初值只有8位适合短定时。50ms用16位模式更合适因为8位模式最大只能256us。重装初值放在进入中断后立刻执行顺序不能颠倒否则第一次中断后的定时会多出几十个指令周期累积误差到每秒刷新时会比较明显。3.2 软件滤波为什么取20次AD采样再平均电机带上负载后测速发电机输出电压会叠加纹波AD值在相邻两次采样之间可能跳变十几个字。单纯拿一次AD值直接显示转速会在某个范围内来回跳看起来像“抖动”。常见做法是在定时中断里每50ms采一次AD值连续采样20次也就是1秒然后求平均用这个平均值去换算转速。unsigned int ad_sum; unsigned char ad_count; void process_adc(void) { ad_sum read_ad0832(0); ad_count; if (ad_count 20) { unsigned char avg ad_sum / 20; unsigned long temp (unsigned long)avg * 1000u; current_speed (unsigned int)(temp / 51u); ad_sum 0; ad_count 0; } }这里要注意ad_sum的类型。20次乘以最大255结果最大5100unsigned int足够。如果扩大到100次就要用unsigned long否则会溢出。平均滤波的代价是响应变慢从转速突变到显示稳定大约需要1秒。如果你在Proteus里用电压源模拟转速变化会看到数码管末尾数字缓慢爬升这属于正常现象。改成每10次平均能提升响应速度但显示抖动会略微增加。3.3 数码管动态扫描与段码映射显示部分在实例中用的是四位共阴或共阳数码管。动态扫描的思路是同一时间只点亮一位通过快速轮询让视觉上感觉四位数同时亮。定时器中断里不适合做长扫描因此扫描通常放在主循环里用软件延时控制每位显示时长。code unsigned char seg_code[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; void display_digit(unsigned char pos, unsigned char num) { P0 0x00; // 消隐 P2 0xFF ~(1 pos); // 位选共阴数码管低电平选中 P0 seg_code[num]; // 段选 delay_short(1); }注意消隐顺序先关闭所有段码再切换位选最后送入新段码。顺序不对会看到拖影或幻影。P0口内部没有上拉电阻在Proteus仿真中接数码管时通常需要外部上拉或直接使用总线驱动。这个实例包里包含源码程序你可以直接修改seg_code数组来适配共阳数码管共阳段码是共阴的按位取反。3.4 主循环整合把采样、换算、显示串起来主函数逻辑相对直接初始化定时器清零全局变量进入死循环不断调用采样函数和显示函数。由于采样由中断驱动主循环不需要精确延时只要保证每秒都有新数据可用即可。void main(void) { timer0_init(); while (1) { process_adc(); display_rpm(current_speed); } }display_rpm内部把整数拆成千位、百位、十位、个位分别调用display_digit。因为每位扫描时间极短整体转速刷新会自然出现1秒更新一次的数字跳变这正是20次平均带来的效果。如果你希望显示更顺滑可以把平均窗口缩小到10次并根据实测数据调整除法系数。4. Proteus仿真加载与排错从DSN到hex的完整链路4.1 打开ex100.DSN并加载hex文件在Proteus中打开工程文件ex100.DSN如果第一次打开看不到菜单栏变化检查文件路径是否包含中文或空格Proteus对这类路径很敏感。双击仿真图中的51单片机芯片在Program File栏里加载ex100.hex。如果加载的是源程序直接编译生成的Release版本也可以选择MoNi.hex两者对应不同时期的源码改动。Last Loaded状态说明文件在上次保存时已经绑定重新打开后会自动关联。使用Keil编译源码时注意工程目标选项中要勾选Create HEX File这样每次编译都会在输出目录生成新的hex文件。晶振频率一栏要与Proteus中单片机属性里的频率保持一致否则定时器初值算出来虽然没错但时间基准变了转速显示会对不上。更隐蔽的问题是Keil的优化等级如果开启Optimization Level 3则代码中一些延时循环会被优化掉导致AD0832时序异常。建议先用默认Level 0调试。4.2 仿真不运行或数码管乱码的检查表按下面顺序排查仿真问题能解决大多数情况现象可能原因处理方式点击运行后无反应单片机没加载hex双击芯片检查Program File数码管全亮但不变化EA引脚悬空将EA接VCC数码管显示随机跳变AD0832的CLK频率太高增加_nop_()或降低单片机频率转速值恒定不变CH0输入信号没接用电压表测AD0832输入的模拟电压显示错位缺笔段码表与数码管类型不匹配更换seg_code为共阳取反程序跑飞反复复位看门狗没关或复位电路异常检查RST引脚是否正常仿真环境下最容易忽略的是AD0832输入悬空。很多学员把测速发电机的输出引线拉到AD0832的CH0却忘记共地。结果CH0引脚电位不确定AD值随机数码管呈现出无规律的乱跳。检查时在仿真里对地测量CH0对GND电压应该能看到和转速成正比的电位。4.3 从模拟量到数字量的标定在仿真中怎么实现Proteus里没有真实的测速发电机常见的替代方法是用一个可调电源或信号发生器输出电压模拟传感器的输出。实例DSN中应该已经放置了类似激励源。如果你把它换成任意的DC电压源就可以手动验证标定公式。比如让电压源输出1V观察AD值是否约等于51数码管应显示1000RPM左右。如果显示偏差大检查Vref是不是5V以及AD0832通道选择是否正确。注意AD0832的参考电压直接取自VCC如果在仿真里修改了电源电压换算系数就要同步调整。当你把5V改成3.3V之后读取到的AD最大值会变小代码里的255和VREF5.0都要改否则转速显示会整体偏移。比较稳妥的做法是把参考电压用一条独立的5V网络单独供电ADC采集精度和稳定性都能提升。5. 脉冲计数法替换AD0832的进阶改造5.1 把读取AD值的代码换成计数器1外部计数在51单片机上使用脉冲计数法测速需要让51单片机的外部计数输入引脚T1P3.5接收霍尔传感器或光电编码器输出的方波信号。每检测到一个下降沿计数器1自动加1整个计数过程不占用CPU时间。把第2章的AD0832代码去掉在main函数中重新设计定时和计数逻辑。具体做法定时器0仍然产生50ms中断计数器1工作在模式1即16位计数方式。在初始化时把计数器1配置为外部计数模式然后启动计数。每20次定时中断也就是1秒到读取计数器1的值用公式 speed count * 60 / pulses_per_rev 计算出转速。若电机每转输出1个脉冲speed count * 60。void counter1_init(void) { TMOD 0x0F; // 清零计数器1的部分 TMOD | 0x50; // 计数器1模式1外部脉冲计数 TH1 0; TL1 0; TR1 1; }注意TMOD的低4位是定时器0高4位是计数器1。前面定时器0初始化使用TMOD | 0x01这里使用TMOD | 0x50两者不会互相干扰但前提是初始化顺序中使用了与运算清掉无关位。5.2 避免计数器溢出和获取瞬时转速16位计数器最大计数65535如果电机转速超过65535RPM1秒内计数会溢出显示就不对了。简单做法是把计数窗口缩短到0.1秒读取值乘以10再乘以60或者用定时器0每50ms读一次两次读取差值乘以系数。短窗口还能让转速显示更灵敏适合负载快速变化的电机。脉冲计数法在仿真中的标定比ADC更直观给T1引脚接一个频率为f的信号源1秒内计数N理论上等于f转速计算公式为RPMf*60/每转脉冲数。如果信号源频率是100Hz每转一个脉冲转速就是6000RPM。这个数值不依赖Vref和标定系数误差主要来自闸门时间积累。闸门时间越长频率分辨率越高但显示刷新越慢。项目中如果电机实际转速较低建议使用每转多脉冲的编码器比如每转60个脉冲那么100Hz对应100RPM低速性能明显改善这也是硬件芯片选型时容易被忽略的关键点。本文还有配套的精品资源点击获取