简介:基于AT89C52单片机的直流电机调速系统设计文档,为2021年9月收藏的完整课程设计/毕业设计参考方案,主要面向电子信息、自动化等专业学生及51单片机入门者。方案以AT89C52为控制核心,采用DAC0832数模转换器将数字信号转为电压从而调节电机转速,并通过数码管显示转速与档位,解决传统调速电路结构复杂、精度不足的问题。文档从系统总体设计、硬件接口电路到软件流程均有清晰说明,包含DAC0832直通模式配置、8255扩展数码管驱动、定时器/计数器频率测量等关键细节,并给出C语言实现思路,整体结构完整,可直接作为设计报告框架参考。资源包共1个doc文件,约202KB,属于纯文档型资料,适合用于课程设计报告撰写、答辩讲解或项目前期参考;已有99人学习,具有一定参考价值。
1. 为什么这个直流电机调速系统敢用 DAC0832 而不是 PWM
2021 年 9 月 16 日我整理一份课程设计资料时,发现一个有意思的设计:直流电机调速没有用常见的 PWM 脉冲调制,而是让 AT89C52 单片机配合 DAC0832 数模转换器,把数字量直接换算成模拟电压来推动电机。大多数人在实际项目里会默认选 PWM,因为开关管效率高、CPU 开销小。但这个系统反其道而行,用 DAC 输出线性电压调速,好处是电压纹波小、调速曲线平滑,调试时用万用表就能直接看到 DA 输出电压和转速的对应关系,这在教学演示和低速平稳控制场景里反而比 PWM 更直观。适合做单片机课程设计、蓝桥杯备赛的在校生,以及想快速验证 DA 转换链路完整性的嵌入式工程师。下面把接口电路、数码管扫描、按键档位状态机和仿真排错一条线讲透。
2. DAC0832 直通模式接口电路与三角波输出验证
2.1 DAC0832 内部结构与工作模式选型
DAC0832 是一颗 8 位全 MOS 中速 D/A 转换器,内部采用 R-2R T 形电阻解码网络,转换结果是电流型输出,转换时间约 1us。它支持单电源 +5V 到 +15V 供电,参考电压允许在 -10V 到 +10V 范围内选取,这里直接取 +5V 作为参考电压。为什么选它而不是更常见的 TLC5615 或 PCF8591?因为本设计的要求很明确:数据位宽 8 位就够、转换速度要求不高、需要直观的电压输出范围可控,并且 DAC0832 可以工作在完全不需要写时序的直通模式,把数据总线拉上去就能输出对应电压。DAC0832 有三种工作方式:直通方式、单缓冲方式、双缓冲方式。双缓冲适合多路 DAC 同步输出的场景,单缓冲适合输出寄存器需要锁存控制的场景。本系统只控制一台电机,转速更新不需要与外部同步,因此选直通方式更合适——CS、XFER、WR2 全部接数字地,芯片内部寄存器直通,P0 口数据一变,输出电流立即跟随变化,减少了两层软件时序开销。
下表给出直通模式下 AT89C52 与 DAC0832 的实际连接关系:
| AT89C52 引脚 | DAC0832 引脚 | 电平/信号说明 |
|---|---|---|
| P0.0 - P0.7 | DI0 - DI7 | 8 位数字量输入 |
| GND | CS | 片选接地,始终有效 |
| GND | XFER | 传输控制接地 |
| GND | WR2 | 写信号接地 |
| +5V 电源 | Vref | 参考电压 +5V |
| GND | RFB | 反馈电阻端,配合运放使用 |
这里要注意一个隐藏坑:AT89C52 的 P0 口是开漏结构,内部没有上拉电阻,直接接 DAC0832 的数据输入端时,高电平驱动能力不足。常见做法是在 P0 口外接 10k 排阻到 VCC,否则高电平输出会被拉低,导致 DA 输出电压到不了满幅值,实测可能最大只有 4V 左右。很多课程设计做到整机调试时发现转速上不去,问题就出在这里。
2.2 直通模式下的输出特性
直通模式下,DAC0832 的电流输出与数字量 D 的关系为 Iout = (D / 256) * Vref / Rfb。因为电机需要电压驱动而不是电流驱动,所以输出端要接一个 I/V 转换电路,常见做法是用运放搭电流转电压,比如 LM358 接成反相放大器。注意 DAC0832 是电流型输出,IOUT1 和 IOUT2 是互补电流输出,单纯用万用表去量 IOUT1 对地电压是量不准的,必须经过运放转换成电压。Rfb 引脚是内部反馈电阻的引出端,接运放输出端能补偿温漂系数。这里的 R-2R 网络建立时间很快,跟上单片机 P0 口的输出速率完全没压力,所以不需要在数据总线上加锁存器。
为了在焊板子之前先验证数字量到模拟电压这条链路是否打通,我一般会在系统主程序之前单独跑一个开环测试代码,让 P0 口输出线性变化的数字量,观察运放输出端是否出现三角波:
#include <reg52.h> // 开环测试:DAC0832 直通模式三角波发生 void main() { unsigned char val = 0; bit direction = 0; // 0: 递增, 1: 递减 // P0 口输出低电平,先保证 DAC 输出电压接近 0V P0 = 0x00; while (1) { if (direction == 0) { P0 = val++; if (val == 255) direction = 1; // 到满幅值改递减 } else { P0 = val--; if (val == 0) direction = 0; // 回到 0 改递增 } // 延时约 200us,控制三角波周期,可调 _nop_(); _nop_(); _nop_(); _nop_(); } }这段代码的作用不是调速,而是验证 P0 口到运放输出端的通路是否正常。变量 direction 用来控制数据增减方向,val 从 0 递增到 255 再递减回 0,对应 DAC 输出从 0V 逐步升到接近 Vref 再降回来,运放输出端应该出现线性三角波。四个_nop_()是空指令,用来做短暂延时,实际调试时建议换成 1ms 级延时,便于示波器观察一个完整周期。如果波形有台阶而不是线性斜坡,优先检查 P0 口上拉电阻是否牢固;如果波形幅度偏小,重点量 Vref 引脚是不是标准的 5V。
这里的参数意义要说明白:val 的步进是 1,对应 DA 分辨率约 5V / 256 = 19.5mV,也就是说每次数字量加 1,理论输出电压抬升 19.5mV。在实际测速过程中,这个粒度已经足够让转速稳定分辨到肉眼可见的变化。
2.3 从三角波切换回调速闭环
三角波验证只适合开环测试,真正投入调速时,P0 口的输出量不再线性扫动,而是由按键设置的档位决定。档位写入 DAC 的数字量不是随意定的,需要根据电机额定电压和运放放大倍数做比例换算。假如电机额定电压 12V,功率放大级把运放输出放大到 12V,那么 DAC 数字量 255 对应 5V 参考电压再放大 2.4 倍,这组比例关系会直接写进显示缓冲区,并在数码管上同时显示档位和 DA 输出值。
3. 8255A 扩展驱动的数码管动态扫描与显示缓冲结构
3.1 为什么不用单片机 I/O 口直接驱动数码管
系统运行状态需要显示“DJ-XX”和当前 D/A 输出的数字量,这一步拆开来看包含多种信息:档位编号、单位标识、DA 输出十六进制数。如果全部用动态扫描方式在单片机上完成,需要 8 个 I/O 口做段码、6 个 I/O 口做位选,AT89C52 还剩多少口可以接按键和 DAC?另外数码管内部是 LED 器件,单片机 I/O 口提供的是“虚电压”,能输出高低电平但驱动能力有限,直接点亮亮度很低。所以这里通过 8255A I/O 扩展芯片,占用单片机 P0 和 P2 口,由 8255A 的 PA 口输出段码、PC 口输出位选,再经过达林顿管或 74HC245 增强驱动能力。8255A 在这里实际上充当了显示寄存器和 IO 扩展器的双重角色,把显示刷新任务从 CPU 中断中剥离出来。
硬件连接上,8255A 的地址线 A0、A1 接 AT89C52 的 P2.0 和 P2.1,片选 CS 接 P2.7,这样通过地址就能选中 8255A 的 PA、PB、PC 和控制寄存器。PA 口作为输出方式 0,接数码管段码;PC 口低 3 位做位选,同时 PC 口还要单独接一位到“档位状态灯”。8255A 方式 0 是无握手直通输出,不需要额外状态查询,适合显示刷新这类简单输出场景。
3.2 段码表与扫描节奏设计
共阴极数码管需要软件给出段码表,按共阴极接法,要显示字符“0”到“9”加上“H”和空字符,按位排列成数组:
// 共阴极数码管段码表 // 位序: dp g f e d c b a unsigned char code segCode[] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 0x76, // H 0x00 // 空白 };段码表按位顺序排列,依靠的是共阴极数码管内部发光二极管阳极接段码、阴极接位选的结构。动态扫描的核心思路是同一瞬间只点亮一位,高频率轮流点亮所有位,利用人眼视觉暂留合成完整显示。扫描频率不能太低,低于 50Hz 会觉得闪,也不能高到超过位选三极管开关速度,推荐 1ms 到 2ms 切换一位,6 位数码管完成一轮扫描约 6ms,刷新率约 160Hz,既无闪烁也不产生明显拖影。
3.3 显示缓冲区如何组织
把要显示的内容先放进缓冲区,再统一交给扫描程序,这是显示系统的关键设计。实际项目里显示缓冲区定义如下:
unsigned char data disBuf[6]; // 6 位数码管显示缓冲区 unsigned char data curScanBit; // 当前扫描位其中 disBuf[0] 到 disBuf[5] 分别对应显示内容从高位到低位。比如电机档位是 3、DA 输出数字量是 0xA5,需要显示“d 3 A 5”,那么 disBuf[0] = 0x00(灭)、disBuf[1] = 0x00(灭)、disBuf[2] = 0x00(灭)、disBuf[3] = 0x5B(显示 3)、disBuf[4] = 0x77(显示 A,但段码表未列,需要用 0x77 自行补充)、disBuf[5] = 0x6D(显示 5)。我一般会把“d J - ”作为固定前缀,或者在最高位显示档位、后面接 DA 值,具体布局可以调整。但缓冲区大小固定为 6,扫描函数不关心内容,只负责把 disBuf 的值搬到位上,这样逻辑上解耦,改显示格式时不碰扫描代码。
扫描服务函数如下,放在定时器中断里调用:
void scanDisplay() interrupt 1 using 1 { TH0 = 0xFC; // 重装初值,定时 1ms,12MHz 晶振 TL0 = 0x18; // 关位选防止拖影 P2 &= 0x1F; // 8255A PC 口低 3 位清零,关闭所有位 // 段码输出 PA_OUT = segCode[disBuf[curScanBit]]; // 打开当前位 P2 = (P2 & 0x1F) | ((curScanBit + 1) << 3); curScanBit++; if (curScanBit >= 6) curScanBit = 0; }这段代码里,8255A 的 PA 口映射为 PA_OUT,PC 口低 3 位作为位选。先关全部位选,再送段码,最后开本位,这个顺序不能反,否则会出现鬼影和暗亮。TH0 和 TL0 的重装值在 12MHz 晶振下定时 1ms,扫描一轮 6ms,肉眼看不到闪烁。如果用的是 STC 单片机,IRC 时钟频率可能存在误差,需要根据实际主频重算初值。
3.4 档位显示与 DA 数值的动态关联
显示内容和实际电机转速不是独立事件。键盘设置档位后,程序需要把档位映射成 DAC 数字量,再同时写入显示缓冲区:
void setGearToDisplay(unsigned char gear) { unsigned char index = gear & 0x0F; // 档位限制在 0-15 unsigned char dacValue = gearVolt[index]; // 查表获得 DA 值 unsigned char highNib = dacValue >> 4; unsigned char lowNib = dacValue & 0x0F; disBuf[0] = 0x00; // 灭 disBuf[1] = 0x00; // 灭 disBuf[2] = segCode[gear / 10 % 10]; // 档位十位 disBuf[3] = segCode[gear % 10]; // 档位个位 disBuf[4] = segCode[highNib]; disBuf[5] = segCode[lowNib]; }gearVolt 是一张档位到 DA 值的映射表,常见做法是线性映射:档位 1 对应 0x20,档位 15 对应 0xF0。因为电机低速段非线性明显,线性查表不一定合适,可以按实际电机特性手工修改这张表。我通常会在实验记录本上先测量不同 DA 值对应的电机转速,再反过来修正表项,这个过程是整个调速系统调试中最耗时的部分。
4. 定时计数器测频与键盘档位状态机的 C 语言实现
4.1 频率测量的时间基准与计数通道
数码管上显示的转速需要量化,系统里面用两个定时/计数器资源分别充当定时器和计数器。定时器 0 用来产生固定闸门时间,定时器 1 工作在计数模式统计电机轴上码盘输出的脉冲数。具体做法是定时器 0 设置为方式 1,定时 500ms;定时器 1 设置为方式 1,计数外部脉冲在 T1 引脚的下降沿。等定时器 0 溢出时读取定时器 1 的计数值,乘以 2 就是 1 秒的脉冲频率,再除以每转脉冲数得到转速。这种测频法适合频率相对稳定的信号,如果电机转速跳变很快,需要改用测周法,但这超出了本系统的适用范围。
中断服务函数关键部分如下:
unsigned long pulseCnt = 0; unsigned char timeFlag = 0; void timer0_isr() interrupt 1 { static unsigned char tick; TH0 = 0x3C; // 12MHz 下定时 50ms TL0 = 0xB0; tick++; if (tick >= 10) { tick = 0; timeFlag = 1; // 500ms 闸门到达 } } void timer1_isr() interrupt 3 { pulseCnt++; // 外部脉冲计数 }注意定时器 1 溢出周期要远大于闸门时间,否则 pulseCnt 累计到 65535 溢出会丢数据。如果电机转速高、脉冲频率超过 100kHz,建议把定时器 1 改为方式 2 自动重装,并在中断里维护一个 16 位或 32 位计数变量。这个细节在蓝桥杯单片机备赛里常考,实际工程中也很容易踩坑。
4.2 键盘扫描与档位状态机
档位设置是三个按键配合实现的:一个“设置键”用于切换输入位,两个“加/减键”用于修改当前位的数值。参照原文流程图的设计,每次按下设置键,有效输入位从第六段数码管依次跳到第五段、第四段,对应百位、十位和个位;第三次输入完成后把 V 值存入 disbuf 中。这个过程本质是一个四状态状态机,用当前输入位数 curDigitIndex 作为状态变量。
#define SEG_DIGIT_3 0 // 第三段数码管,存储最终 V 值 #define SEG_DIGIT_4 1 #define SEG_DIGIT_5 2 #define SEG_DIGIT_6 3 unsigned char curDigitIndex; unsigned char setValue[3]; void keyScan() { if (key_set_pressed) { curDigitIndex++; if (curDigitIndex > 3) curDigitIndex = 0; } if (curDigitIndex >= 1 && curDigitIndex <= 3) { if (key_up_pressed) { setValue[curDigitIndex - 1]++; if (setValue[curDigitIndex - 1] > 9) setValue[curDigitIndex - 1] = 9; disBuf[SEG_DIGIT_6 - curDigitIndex] = segCode[setValue[curDigitIndex - 1]]; } if (key_down_pressed) { if (setValue[curDigitIndex - 1] > 0) setValue[curDigitIndex - 1]--; disBuf[SEG_DIGIT_6 - curDigitIndex] = segCode[setValue[curDigitIndex - 1]]; } } if (curDigitIndex == 0 && setOK) { // 组合三位数为最终 V 值 unsigned char v = setValue[0] * 100 + setValue[1] * 10 + setValue[2]; P0 = v; // 直接输出到 DAC curDigitIndex = 0; } }这里 key_set_pressed、key_up_pressed、key_down_pressed 分别对应三个按键的下降沿检测结果,实际板级调试时需要在主循环里用软件消抖,每 10ms 扫描一次键盘,连续两次读到相同电平才确认按键有效。状态机的好处是逻辑集中在 keyScan 一个函数里,不管用户怎么乱按,curDigitIndex 都不会越界。注意第三次输入完成后把 V 值送入 P0,这是整个系统的核心动作:数字量 V 经过 DAC0832 变成模拟电压控制电机转速。
4.3 频率显示格式与单位切换
显示模块还要处理量程自动切换。原文明确给出规则:频率低于 1MHz 时,六位数码管直接显示频率数值;频率高于 1MHz 时,前四位显示数值,最后一位显示字母 H。这里虽然测的是转速,但在教学演示里直接把脉冲频率当作显示对象。切换逻辑不复杂,比较 frequency 和 1000000 的大小,然后往 disBuf 里填不同的字符。需要注意频率值本身不是以 BCD 码形式存储的,如果直接用整型除以 10 的幂取各位,在 Keil C51 里要注意 unsigned long 运算的 bank 切换,我一般会把除法运算提取成独立函数,避免 lcall 太深导致变量覆盖错误。
5. Proteus 仿真排错与 DA 更新时序处理技巧
5.1 Proteus 里最容易出的三个问题
用 Proteus 仿真 51 单片机项目时,DAC0832 直通模式往往能跑通,但数码管动态扫描经常遇到两个现象:亮度不均和数字跳动。亮度的核心问题是位选驱动没有加限流阻抗,仿真模型中 8255A 的 PC 口直接下拉数码管阴极,电流过大会让段码电压被拉偏,解决方法是每个位选输出串一个 330 欧电阻,或者用 PNP 三极管做高电平驱动。
第二个常见问题是程序从 Keil 编译后导入 Proteus 时丢失了 HEX 文件,仿真无法运行。这属于操作问题,但值得在排错清单里放第一条。正确的做法是在 Keil 的 Output 选项卡勾选 Create HEX File,编译后把生成的 .hex 文件路径填写到 Proteus 单片机属性里。
第三个坑是 DAC0832 的电流输出没有做 I/V 转换,仿真里电机模型无法识别电流,表现为电机不转。Proteus 中的直流电机模型实际上是根据端电压运动的,所以必须用 opamp 把 DAC 输出电流转换成电压,再接到 ENG 驱动电机。我习惯在 D/A 输出端接一个 LM358 反相比例放大电路,RFB 接输出,RIN 接地,增益设为 -1,这样 0 到 5V 的 DA 输出被映射为 0 到 -5V,再通过反相器恢复正电压给电机。
5.2 DA 更新与动态扫描的时序避让
实际调试中发现,如果在主循环里频繁更新 P0 口,数码管会出现肉眼可见的闪动,原因是 P0 口同时承担 8255A 的数据传输,虽然 8255A 锁存了显示数据,但动态扫描期间 CPU 对 P0 的写入操作会短暂拉低数据线,导致段码出错。一个有效的技巧是把 DAC0832 的数字量更新放在定时器中断里,而不是主循环直接赋值。具体做法是维护一个全局变量 dacOutputVal,在键盘状态机里只修改这个变量,然后由定时器 0 中断服务函数在扫描完当前位之后附带把 P0 更新为 dacOutputVal。
我一般会这样设计:
void timer0_isr() interrupt 1 { // 原本的定时器 0 重装代码 TH0 = 0xFC; TL0 = 0x18; // 在显示扫描之前更新 DAC 输出 P0 = dacOutputVal; // 再执行数码管动态扫描 scanDisplay(); }这样 P0 口在每次中断里先被更新为 DAC 当前值,随后 scanDisplay 里对 P0 的访问变成向 8255A 输出数据,两者在时间上严格串行,不会出现主循环里写了一半 P0 就被显示代码抢走的问题。但要注意 scanDisplay 里凡是访问 8255A 的语句都会改变 P0 的值,所以 P0 保持 DAC 输出值的时间只维持到下一句指令,DAC0832 直通模式下输出会有微型毛刺,不过对于直流电机这种大惯性负载,几百纳秒的毛刺无实际影响。如果做更精细的波形输出,可以把 DAC0832 改成单缓冲模式,用锁存器保存数据,这样才能保证 DAC 输出长时间稳定。
5.3 验证“档位 + DA 输出数字量”是否真的对得上
最终验证方法是逐档按下按键,用万用表测 DAC0832 运放输出端电压,记录每个档位对应的电压值和数码管显示的数字量,绘制成一张曲线表。如果档位 1 到 15 的 DA 值按 0x20 到 0xF0 均匀递增,电压应近似线性递增;如果中间某一档电压跳变异常,优先检查 P0 口上拉电阻和运放电路,而不是单片机的程序——因为数字量到模拟量之间只隔了这两层。这个方法比看转速准得多,也是快速定位“为什么 3 档和 4 档转速一样”这类问题的最有效手段。
本文还有配套的精品资源,点击获取