简介:基于Proteus仿真的51单片机AD与DA转换实验资源,面向单片机初学者和嵌入式实验课程学习者。资源以DAC0832数模转换和ADC0809模数转换为核心,配合82C55可编程并行接口扩展端口,实现可变电阻分压后的0~5V电压采集,并经数码管显示且保留两位小数;同时可输出幅值-5V~5V的方波、三角波和锯齿波,借助示波器直观查看波形变化。压缩包共21个文件,约132KB,包含Keil工程文件(uvproj/uvopt)、C语言源程序、STARTUP.A51启动文件、Proteus仿真工程(pdsprj)与备份、hex可执行文件以及lst/obj等中间列表文件,便于从代码编译到电路仿真进行完整对照学习。已有1305人学习下载,资料内置完整的项目工程与README说明,适合作为课程设计、实验报告或单片机AD/DA编程入门的参考素材。
1. 拿到单片机AD和DA实验包,先想清楚这组实验在做什么
实验7-单片机AD和DA实验,单看名字像是课设归档材料,但把这组实验吃透,等于一次性补了两块基本功:数字系统怎么把外部电压读进来(AD),以及怎么把内部数字量变成可观测的模拟信号(DA)。这类实验包通常包含仿真工程、C51 源码和实验说明文档,核心目标浓缩成两句话——电位器拧动时数码管数字跟着变,程序里改一个数,运放输出端电压就跟着变。
我做嵌入式前几年踩过的坑,一大半和这类接口实验有关。AD 和 DA 处在数字世界与真实世界的交界处,报错不会像逻辑电平那样干脆,经常出现读值乱跳、波形出不来、仿真正常实物失灵这类玄学问题。这篇不贴课程报告,按一个可复现的方案讲清楚电路怎么搭、参数怎么设、哪里最容易翻车。适合正在学 51 单片机、想把手头实验真正跑通的学生,也适合想回头补模拟接口调试基础的工程人员。
2. AD采集跑通:用51驱动ADC0809的最小电路与采样代码
2.1 先弄清AD转换在单片机里的定位:采样、量化、编码
模拟电压进入单片机只有一条路:量化成数字。8 位 ADC 把 0 到 VREF 的电压范围分成 256 份,每一份是 1 LSB。参考电压 5V 时 1 LSB 约等于 19.5mV,读回来的数字乘上这个刻度就是真实电压。AD 实验的全部价值,就是把这条链路拆开看清楚:采样保持、逐次逼近、输出使能分别由哪个引脚控制,时序上谁先谁后。
多数实验资料选 ADC0808/0809 这颗芯片,不是因为它性能好,而是因为它的控制逻辑足够直白:8 路输入、8 位结果、每个控制信号都能用示波器单独量到。相比单片机内置 ADC,用这类独立芯片反而更容易理解转换过程。ADC0809 的引脚分工如下表。
| 引脚 | 方向 | 作用 |
|---|---|---|
| IN0~IN7 | 输入 | 8 路模拟输入通道 |
| A/B/C | 输入 | 通道选择地址,组合选中 IN0~IN7 |
| ALE | 输入 | 上升沿把 A/B/C 选中的通道锁存 |
| START | 输入 | 上升沿启动一次转换 |
| EOC | 输出 | 转换中为低,转换完成变高 |
| OE | 输入 | 高电平时把 8 位结果放到数据线 |
| CLOCK | 输入 | 转换时钟,典型值 500kHz |
| VREF(+) / VREF(-) | 输入 | 参考电压,决定量程 |
| D0~D7 | 输出 | 转换结果,8 位数字量 |
一次完整读取的时序是:地址稳定 → ALE 上升沿锁存通道 → START 上升沿启动转换 → 等待 EOC 变高 → OE 拉高 → 从数据线取数。这个顺序在后面写代码、排查问题时都要反复用到,建议先记住再动手布线。
2.2 搭出仿真电路:时钟、通道选择、参考电压三个参数一次设对
仿真里搭这套电路很简单,但三个参数最容易设错:CLOCK 频率、参考电压接法、通道地址时序。我一般习惯先把电路连成下面这张表的样子,再写程序,避免电路和代码同时出问题时不知道先查哪个。
| 信号 | 连接到 | 参数和说明 |
|---|---|---|
| IN0 | 电位器中心抽头 | 电位器两端分别接 VCC 和 GND |
| CLOCK | 信号源方波输出 | 频率 500kHz,占空比 50% |
| A/B/C | P2.0 / P2.1 / P2.2 | 通道地址选择 |
| ALE / START | P2.3 | 两根引脚并联到同一 IO,先拉高再拉低 |
| EOC | P2.4 | 查询等待转换结束 |
| OE | P2.5 | 读数据前置高 |
| D0~D7 | P0 口 | 外接 10kΩ 排阻上拉 |
| VREF(+) | VCC | 参考电压上端 5V |
| VREF(-) | GND | 参考电压下端接地 |
CLOCK 是第一个坑。ADC0809 的时钟范围是 10kHz 到 1280kHz,典型值 500kHz,此时单次转换约 100μs。直接把单片机晶振 12MHz 接过来会超出上限,转换逻辑直接乱掉。仿真里给它一个独立的 500kHz 脉冲源即可。参考电压决定量程:VREF(+) 接 VCC、VREF(-) 接 GND 时,0V 对应 0x00,5V 对应 0xFF,换算关系最直观。A/B/C 三根地址线必须在 ALE 上升沿之前稳定,否则锁存进去的是错误通道。
提示:ALE 和 START 并联到同一根 IO 是常见做法,两个信号都是上升沿有效,一次从低到高的跳变先触发锁存、后触发启动,顺序刚好正确。如果希望时序更严格,可以拆成两个 IO 分别控制。
2.3 用Keil C51写最小采样代码,并换算成电压显示
下面这段是能从零跑通的最小采样程序,逻辑遵循前面列的时序。数据线接 P0,地址和控制线分散在 P2,EOC 用查询方式等待。
#include <reg52.h> sbit AD_A = P2^0; // 通道地址最低位 sbit AD_B = P2^1; sbit AD_C = P2^2; sbit AD_ALE = P2^3; // ALE sbit AD_STR = P2^3; // START 与 ALE 共用一根线 sbit AD_EOC = P2^4; // 转换结束标志 sbit AD_OE = P2^5; // 输出使能 #define AD_DATA P0 // 转换结果数据线 unsigned char adc_read(unsigned char ch) { unsigned char val; // 先稳定通道地址 AD_A = ch & 0x01; AD_B = (ch >> 1) & 0x01; AD_C = (ch >> 2) & 0x01; // 产生两个上升沿:先锁存地址,再启动转换 AD_ALE = 0; AD_STR = 0; AD_ALE = 1; AD_STR = 1; AD_ALE = 0; AD_STR = 0; while (AD_EOC == 0); // 转换中 EOC 为低,完成后变高 AD_OE = 1; // 打开 8 位输出 val = AD_DATA; AD_OE = 0; return val; } void main(void) { unsigned char res; while (1) { res = adc_read(0); // 采样 IN0 通道 P1 = res; // P1 口接 LED,直接观察二进制 } }逻辑说明:地址位先就绪,然后 ALE 和 START 各产生一次上升沿;等待 EOC 变高表示转换结束;OE 拉高后数据线才输出有效结果。把 ALE 和 START 并联的做法在这里不会出问题,因为两次上升沿由同一个 IO 依次触发,通道地址在第一个上升沿之前已经稳定。
参数说明:adc_read 的入参 ch 范围 0~7,对应 IN0~IN7,后续要扩展成多通道采样时直接换参数即可。P1 口接 LED 看二进制不够直观,换算成电压更实用:voltage = res * 5.0 / 256,0x80 对应 2.5V,0xFF 对应约 4.98V。这个换算用的分母是 256 还是 255 不必纠结,差值不到 1%,实际以万用表实测 VREF 为准。如果显示值跳动明显,连续采样三次取中值再送显示,能滤掉大部分抖动。
注意:8051 的 P0 口是开漏结构,仿真软件里悬空也能跑,但实物必须接上拉,否则高电平拉不到位。这个差异在第 4 章还会单独展开。
3. DA输出做波形:DAC0832的电流转电压与查表法
3.1 一个反直觉的地方:DAC0832输出的是电流,不是电压
DA 部分首先要把一个反直觉的事实立起来:DAC0832 的输出端 IOUT1、IOUT2 出来的是电流,不是电压。芯片内部是 R-2R 梯形电阻网络,把 8 位数字量转换成对应的加权电流,必须再接一级运算放大器做 I/V 转换,才能在输出端量到电压。很多人仿真里画完线,把电压表直接搭在 IOUT1 上量,读数是零,问题就出在这里。
转换公式在教科书上写得很简洁:Vout = -(VREF × D / 256)。注意前面的负号:VREF 接 +5V 时输出是 0 到 -5V,接 -5V 时输出才是 0 到 +5V。这个极性经常被忽略,我见过不少人在仿真里搭完电路发现波形方向不对,其实就是参考电压极性接反了。
DAC0832 还分单缓冲和双缓冲两种接法。实验用单缓冲就够:让数字量直接进入 DAC 寄存器,数据一变输出立即跟变。双缓冲是为了多路 DAC 同时刷新输出,工业控制里才会用到,初学阶段不必展开,但要知道它存在,否则看别人电路图时会对 WR2、XFER 两个引脚的作用产生困惑。
3.2 搭出 DA 电路:运放、基准电压、数据线
DA 电路的连线比 AD 简单,但要同时处理数据线、控制线、基准电源和运放四个部分。我常用的接法如下表,让 DAC0832 工作在直通状态,省去 WR 时序控制,仿真阶段最不容易出错。
| DAC0832 引脚 | 接法 | 说明 |
|---|---|---|
| DI0~DI7 | P1 口 | 8 位数字量输入 |
| CS | GND | 片选常选中 |
| WR1 | GND | 直通模式,数据随时写入 |
| WR2 | GND | 第二级锁存器直通 |
| XFER | GND | 传输控制常有效 |
| ILE | VCC | 输入锁存使能 |
| VREF | DC -5V | 负基准,让输出为正电压 |
| IOUT1 | 运放反相输入端 | 电流输出端,必须做 I/V 转换 |
| IOUT2 | 运放同相输入端并接地 | 参考回流 |
| RFB | 运放输出端连回反相端 | 内部反馈电阻,构成放大回路 |
运放这里用一个通用型号即可,比如 UA741 或 LM358。注意要给运放接双电源,比如 ±5V,同相输入端直接接地;单电源供电时输出在接近 0V 的区间会失真,波形底部会被削平。VREF 是我最想强调的一个点:它必须接一个独立的 -5V 直流源,而不是接地。VREF 接地等于基准为零,整个 DAC 就不会有输出。
注意:如果手里没有负压源,可以把 VREF 接 +5V,此时输出为 0 到 -5V 的反向电压,示波器上波形倒过来看也能验证。实际项目中再用一级反相放大器把极性翻回来即可。
3.3 用查表法生成正弦波:表怎么来、定时器怎么配、频率怎么算
生成正弦波最常见、最可靠的做法是查表法,而不是实时计算。51 单片机没有硬件浮点单元,实时算 sin 既慢又占代码空间。正确做法是预先算好一张 256 点的正弦表,程序运行时按索引依次把表里的值送给 DAC0832,定时器决定每个点输出多久。
表可以由上位机脚本生成。下面这段 Python 输出 256 个 0~255 范围的值,中心落在 128,幅度取 ±127,保证正弦峰峰接近满量程但不会削顶。
import math n = 256 values = [] for i in range(n): v = int(128 + 127 * math.sin(2 * math.pi * i / n)) values.append(v) line = ", ".join(str(x) for x in values) print(line)把脚本输出的数组贴进 C 文件的 code 段,声明为常量数组,不占宝贵的 RAM。然后配置定时器,每 50μs 中断一次,中断里把当前索引对应的值写到 P1 口:
#include <reg52.h> #define DA_DATA P1 // DAC0832 数据线接 P1 口 #define ARR_LEN 256 code unsigned char sine_tab[ARR_LEN] = { // 贴入上面 Python 脚本生成的 256 个数值 128, 131, 134, 137, 140, 143, /* ... */ }; unsigned char idx = 0; void timer0_init(void) { TMOD = 0x01; // 定时器 0,模式 1,16 位 TH0 = (65536 - 50) >> 8; // 12MHz 晶振,机器周期 1us TL0 = (65536 - 50) & 0xFF; // 50us 中断一次,计数 50 个 ET0 = 1; TR0 = 1; EA = 1; } void timer0_isr(void) interrupt 1 { TH0 = (65536 - 50) >> 8; // 重装初值 TL0 = (65536 - 50) & 0xFF; DA_DATA = sine_tab[idx]; // 查表输出一个样本 idx = (idx + 1) % ARR_LEN; } void main(void) { timer0_init(); while (1); }参数说明:12MHz 晶振下机器周期 1μs,50μs 定时需要计数 50,初值就是 65536 - 50 = 65486。中断频率是 20kHz,表长 256,正弦波频率约为 20k / 256 ≈ 78Hz。想升频有三个手段:缩短定时周期、缩短表长、或让索引每次加一个大步长。大步长方式就是 DDS 的思路,输出频率等于中断频率乘步长再除表长,代价是点数变疏,台阶感更强,这个尺度要在第 4 章的排查里讲。
用示波器探运放输出端,正常情况下看到的是一条以约 2.5V 为中心、峰峰接近 5V 的正弦波。因为表中心是 128,对应理论输出 5V × 128 / 256 = 2.5V,正负半轴各有约 ±2.48V 的摆动空间,正好落在量程内又不削顶。
4. AD和DA实验容易踩的5个坑:现象、原因和排查顺序
4.1 AD读回固定 0x00 或 0xFF,电位器拧了没反应
现象:数码管显示 00 或 FF,无论怎么拧电位器数值都不变。这个现象出现频率最高,也最容易被误判成代码问题。
原因:多数情况是 CLOCK 引脚没有接时钟源。仿真软件对悬空时钟引脚不报错,芯片内部比较器没有节拍就完全不工作;还有人是把 12MHz 晶振直接接到了 CLOCK,超出 1280kHz 上限,转换逻辑同样乱掉。
解决:给 CLOCK 接一个独立方波信号源,频率设置在 500kHz。先用示波器量 CLOCK 引脚确认有波形再往下查。我自己的排查习惯是先看时钟、再看参考电压、最后才动代码,因为前两个在仿真里最容易被忽略。
4.2 数值乱跳,两次读同一个通道结果差几十个 LSB
现象:电位器停在固定位置,但显示值在 90 到 150 之间来回跳,完全稳不住。
原因:启动转换后没有等待 EOC 变高就直接读数据。8 位数据线上还是上次转换的残留值,或者输出使能还没建立完成,读进来的数字自然没有意义。另一种情况是参考电压 VREF 上有纹波,导致量化阈值本身在漂。
解决:严格按「地址稳定 → 锁存 → 启动 → 等待 EOC → 开 OE → 读数据」的顺序写代码,EOC 查询必须放在 OE 拉高之前。VREF 纹波导致的问题,可以连续采样三次取中值;如果接的是开关电源,换线性电源或加 100nF 去耦电容通常能改善。
4.3 DAC0832 输出电压永远是 0,示波器一条直线
现象:程序里给 DI 口写 0xFF,运放输出端量到的电压还是 0V,波形线一动不动。
原因:最常见是把电压表或示波器探头直接接到了 IOUT1 上。IOUT 是电流输出端,没有运放做 I/V 转换就测不到电压。第二个常见原因是 VREF 接到了 GND,基准为零导致内部加权电流也为零。
解决:确认 IOUT1 进运放反相端、RFB 接回反馈回路、IOUT2 接地,运放用双电源供电。再确认 VREF 接的是 -5V 而不是 GND。如果还是没输出,把数字量固定成 0x80,用万用表量运放输出端应有约 2.5V 的静态电压,这一步能快速区分是电路问题还是波形程序问题。
4.4 正弦波变成锯齿或台阶感明显,不像光滑曲线
现象:示波器上波形轮廓能看出是正弦,但被切成一段段的台阶,或者干脆像三角波。
原因:正弦表点数太少,只有 16 或 32 点,相邻两个样本之间电压跳变达到几十毫伏甚至上百毫伏,波形自然出现阶梯。另一个原因是索引步进取得太大,相当于每个周期实际只用了少数几个样本点。
解决:表长至少用 128 点,推荐 256 点;索引步进控制在 4 以内,超过这个值台阶感会非常明显。配合 50μs 中断,步进 1 时波形已经很平滑。先确认静态电压正确、波形轮廓正常,再通过提高步进去拉频率,这样定位问题更容易。
4.5 仿真一切正常,实物板子全不工作
现象:Proteus 里 AD 和 DA 单独验证都通过,焊到板子上之后要么读数乱飞、要么 DA 无输出,查不出程序问题。
原因:8051 的 P0 口内部没有上拉电阻。仿真软件默认数据线高电平是理想 5V,实际单片机 P0 口驱动外部负载时高电平拉不上去,ADC/DAC 读到的数字量在阈值边缘抖动。
解决:P0 口接 10kΩ 排阻上拉到 VCC。如果板子上没有排阻,最简单的办法是把数据线从 P0 挪到 P2 或 P1,避开开漏问题。这个差异是仿真与实物之间最常见的翻车点,也提醒一个习惯:画原理图时先确认每个用到 P0 的外设是否带了上拉。
5. 把AD和DA串成回环自测:一张检查表和两个进阶技巧
把 AD 和 DA 串在一起做回环,是验证整个实验是否真正理解的好方法。做法很直接:DA 输出一个已知电压,AD 采样回来,把读到的数字和理论值对比。程序核心逻辑如下。
unsigned char dac_val; unsigned char adc_back; for (dac_val = 0; dac_val < 255; dac_val += 51) { DA_DATA = dac_val; // DA 输出固定数字量 delay_ms(100); // 等待运放和 ADC 稳定 adc_back = adc_read(0); // AD 采样 IN0 通道 show_value(adc_back); // 数码管显示读回结果 }对比下面这张表,误差在 ±2 LSB 以内属于正常范围。误差偏大先量 VREF 和运放电源,而不是怀疑程序,这是我从这类实验里学到的第一个系统排查习惯。
| DA 写入值 | 理论输出(VREF = -5V) | AD 理论读数 |
|---|---|---|
| 0 | 0.00V | 0 |
| 51 | 1.00V | 51 |
| 102 | 1.99V | 102 |
| 153 | 2.99V | 153 |
| 204 | 3.99V | 204 |
| 255 | 4.98V | 255 |
进阶技巧有两个。一是把回环的采样值经过换算后送数码管显示成电压,能直观看到「DA 写 1.0V → AD 读回 1.0V」的闭环效果。二是用步进方式做 DDS 调频:中断里把索引更新从 idx + 1 改成 idx + step,输出频率 = 中断频率 × step / 256。这个扩展用不了十行代码,却能把实验从 78Hz 固定频率变成任意可调信号源,也是后面做信号发生器类项目的基础。
我最早调这组实验时在 DA 极性上栽过跟头,后来养成一个习惯:不急着看波形,先把数字量固定成 0x80,量一下运放输出是不是基准电压的一半,确认基准和运放没问题再看正弦。这个习惯帮我省了很多时间,也推荐给你。希望帮到你。
本文还有配套的精品资源,点击获取