1. 项目缘起:从模拟到数字的桥梁搭建
最近在整理实验室的旧项目资料,翻到了当年用51单片机做的一个经典实验——A/D和D/A转换。这个实验可以说是单片机从“数字世界”迈向“模拟世界”的第一步,也是很多同学从点亮LED、驱动数码管这类纯数字控制,转向处理真实物理信号(比如温度、压力、声音)的必经之路。我记得当时为了搞懂ADC0809和DAC0832这两个芯片的时序,没少在示波器前蹲着,调通的那一刻,看到电压表指针随着电位器的转动而平稳变化,那种成就感至今难忘。
这个实验的核心,就是教会我们如何用51单片机去“感知”和“控制”模拟量。A/D转换(模数转换),好比是单片机的“耳朵”和“眼睛”,它能将外部连续变化的模拟信号(比如0-5V的电压)转换成单片机能够理解和处理的数字量(比如0-255)。而D/A转换(数模转换),则是单片机的“嘴巴”和“手”,它能把单片机内部的数字指令,转换成连续的模拟电压输出,去驱动电机、控制亮度或者发出声音。ADC0809和DAC0832就是实现这两个功能非常经典且教学常用的芯片。
对于正在学习51单片机汇编语言的朋友来说,这个实验的价值在于,它迫使你跳出纯逻辑的编程思维,去理解和操控硬件时序。你需要精确地控制几个控制引脚的高低电平顺序(这就是时序),才能让芯片正确地开始转换、读取数据或输出模拟量。这个过程,是理解单片机作为“系统控制器”角色的关键一步。下面,我就结合当年的笔记和代码,把这个实验从头到尾捋一遍,把那些容易踩坑的细节和调试心得也分享出来。
2. 实验核心器件与电路原理剖析
在动手写代码之前,我们必须先搞清楚要用的两个“主角”芯片是怎么工作的,以及它们如何与我们的51单片机(这里以经典的AT89C51为例)连接。一知半解地接上线,代码大概率是跑不起来的。
2.1 ADC0809:八路模拟输入的“采样官”
ADC0809是一个8位分辨率、逐次逼近型的A/D转换器。所谓8位,意味着它能把一个模拟电压(假设量程是0-5V)分成2的8次方,也就是256个等级,输出的数字量范围是00H到FFH(0-255)。逐次逼近型是一种常见的转换原理,速度中等,精度满足一般教学和工控需求。
它的引脚很多,但我们需要重点关注的也就那么几个:
- IN0-IN7:8路模拟信号输入通道。你可以把它想象成一个8选1的开关,一次只能选一路信号进行转换。
- ADDA, ADDB, ADDC:通道选择地址线。这三位二进制编码(000到111)就决定了当前选中哪一路模拟输入(IN0到IN7)。例如,CBA=000,就选中IN0通道。
- ALE:地址锁存允许信号。这是一个上升沿有效的引脚。当ALE从低电平跳到高电平时,它会将ADDA、ADDB、ADDC引脚上的通道地址“锁存”进芯片内部的地址锁存器。简单说,就是告诉0809:“记住,我现在要转换的是这个通道的信号”。
- START:转换启动信号。这是一个下降沿有效的引脚。给START一个从高到低的跳变(一个脉冲),ADC0809就会开始对当前选中的通道进行模数转换。
- EOC:转换结束信号。这是一个输出引脚。当ADC开始转换时,EOC会变低;当转换完成,数据准备好后,EOC会自动变回高电平。这个信号非常关键,我们可以通过查询EOC是否变高,来判断转换是否完成,从而去读取数据。
- OE:输出允许信号。这是一个输入引脚,高电平有效。只有当OE被单片机置为高电平时,ADC0809才会把转换好的8位数字量放到数据总线D0-D7上,否则数据总线是高阻态(相当于断开)。所以,读数据前必须先给OE一个高电平。
- CLK:时钟信号。ADC0809工作需要外部提供一个时钟,典型频率是640kHz,范围在10kHz到1280kHz。通常我们用单片机的ALE信号(频率是晶振的1/6)经过分频后提供。
与51单片机的典型连接思路:数据线D0-D7直接连接到单片机的P0口(需外加上拉电阻,因为P0口是开漏输出)。地址线ADDA-C、ALE、START、OE这些控制信号,可以连接到单片机的其他I/O口(如P2或P3的某些位)来单独控制。更常见的做法是,利用单片机系统的地址总线进行统一编址,将ADC0809当作一个外部RAM单元来访问,这样可以用MOVX指令来统一产生控制时序,代码更简洁。本实验代码采用的就是这种方法。
2.2 DAC0832:将数字命令变为模拟输出的“执行者”
DAC0832是一个8位分辨率的电流输出型数模转换器。它接收8位数字量,输出一个与数字量成比例的模拟电流。要得到电压,还需要外接一个运算放大器构成电流-电压转换电路。
它的关键引脚:
- DI0-DI7:8位数字量输入线。
- CS:片选信号,低电平有效。
- WR1:输入寄存器写选通信号,低电平有效。
- ILE:输入寄存器锁存允许信号,高电平有效。通常直接接高电平(VCC)。
- XFER:数据传送控制信号,低电平有效。
- WR2:DAC寄存器写选通信号,低电平有效。 DAC0832内部有两级锁存器,这种结构允许它在保持当前模拟输出的同时,接收下一个要输出的数字量,适用于需要多路D/A同步输出的场合。在我们的单路输出实验中,通常将两级锁存器同时直通:即把WR2和XFER接地(一直有效),CS和WR1由单片机控制。这样,当CS和WR1同时有效时,数字量就直接进入DAC寄存器并开始转换。
输出电路:DAC0832输出的是电流(Iout1和Iout2)。我们需要一个运算放大器(如常用的LM358)来构成一个反相比例放大器,将电流转换成电压。典型的接法是:Iout1接运放的反相输入端,Iout2和运放的同相输入端接地,反相输入端和输出端之间接一个反馈电阻Rfb(内部通常已集成一个15kΩ的,也可以外接)。这样,输出电压 Vout = - (数字量/256) * Vref。其中Vref是参考电压,决定了输出电压的范围。例如,Vref接-5V,则输出就是0到+5V。
2.3 整体电路框架与调试要点
将ADC0809和DAC0832接入51单片机最小系统,就构成了一个完整的“感知-处理-控制”闭环。典型的实验场景是:用一个电位器产生一个可调的模拟电压(0-5V),接到ADC0809的IN0通道。单片机读取这个电压值(数字量),然后直接将这个数字量送给DAC0832输出。用万用表或示波器测量DAC的输出电压,理论上它应该跟随电位器调整的输入电压变化。
注意:在面包板或实验板上搭建这个电路时,电源去耦电容必不可少。务必在ADC0809和DAC0832的VCC和GND引脚附近,并联一个0.1uF的瓷片电容和一个10uF的电解电容,用于滤除电源噪声,这对保证转换精度和稳定性至关重要,尤其是DAC的输出,电源纹波会直接反映在模拟输出上。
另一个调试关键是共地。单片机的地、ADC0809的地、DAC0832的地、运放的地以及模拟信号源的地,必须全部连接在一起,建立一个统一的“零电位”参考点。否则,测量会出现巨大误差甚至无法工作。
3. 汇编代码逐行解读与核心时序实现
理解了硬件,我们来看软件如何驱动。汇编语言的优势在这里体现得淋漓尽致——你可以精确控制每一个引脚电平变化的时刻,这正是ADC/DAC芯片时序所要求的。下面我提供一套经过测试的参考代码,并附上详细注释。
假设我们的硬件连接如下(采用外部RAM编址方式):
- ADC0809的片选(CS)连接地址线A15(P2.7),因此其通道0的地址为7FF8H(A15=0,且低三位地址A2A1A0=000选择IN0)。
- DAC0832的片选(CS)连接地址线A14(P2.6),因此其地址为BFFFH(A14=0,其他无关地址位取1)。
- ADC0809的EOC连接单片机P3.2(INT0),用于查询转换状态。
;=========================================== ; 文件名:AD_DA.ASM ; 功能: 循环读取ADC0809通道0的模拟电压, ; 并将读取的数字量直接输出给DAC0832, ; 实现电压跟随器功能。 ; 硬件连接:见上文描述 ;=========================================== ADC_ADDR EQU 7FF8H ; ADC0809 通道0的地址 (A15=0, A2A1A0=000) DAC_ADDR EQU 0BFFFH ; DAC0832 的地址 (A14=0) ORG 0000H LJMP MAIN ORG 0100H MAIN: MOV SP, #60H ; 设置堆栈指针 LOOP: LCALL READ_ADC ; 调用子程序,读取ADC值 LCALL WRITE_DAC ; 调用子程序,将值写入DAC SJMP LOOP ; 无限循环 ;=========================================== ; 子程序:READ_ADC ; 功能: 启动ADC0809通道0转换,并等待转换完成, ; 然后读取转换结果存入累加器A ; 使用: A, DPTR ;=========================================== READ_ADC: MOV DPTR, #ADC_ADDR ; DPTR指向ADC地址 MOVX @DPTR, A ; 关键操作1:向该地址写任意数据 ; 这个写操作会产生一个WR*信号。 ; 对于ADC0809的典型接法,WR*信号会同时连接到 ; ALE和START引脚。因此,一次写操作会依次产生: ; 1. ALE上升沿,锁存通道地址(由地址总线低三位A2A1A0提供,我们已固定为000)。 ; 2. START下降沿,启动转换。 ; 等待转换完成 (查询EOC引脚,这里假设EOC接P3.2) WAIT_EOC_HIGH: JB P3.2, WAIT_EOC_HIGH ; 如果EOC为低,说明正在转换,继续等待 WAIT_EOC_LOW: JNB P3.2, WAIT_EOC_LOW ; 如果EOC为高,说明转换完成,跳出循环 ; 注意:这里等待了一个从低到高的跳变,更严谨。 MOVX A, @DPTR ; 关键操作2:从同一地址读取数据 ; 这个读操作会产生一个RD*信号。 ; RD*信号通常连接到ADC0809的OE引脚。 ; OE变高,ADC将转换结果输出到数据总线,单片机通过P0口读入A。 RET ;=========================================== ; 子程序:WRITE_DAC ; 功能: 将累加器A中的数字量写入DAC0832 ; 使用: A, DPTR ;=========================================== WRITE_DAC: MOV DPTR, #DAC_ADDR ; DPTR指向DAC地址 MOVX @DPTR, A ; 向该地址写数据。这个写操作会产生WR*信号, ; 连接到DAC0832的CS和WR1(假设WR2和XFER已接地直通)。 ; 当CS和WR1同时有效(低电平),数据从单片机数据总线 ; 写入DAC0832的输入寄存器,并立即进入DAC寄存器开始转换。 RET END代码核心逻辑与时序分析:
启动ADC转换 (
MOVX @DPTR, A):这行代码是精髓。当执行MOVX @DPTR, A写外部RAM时,51单片机会在P3.6(WR)引脚产生一个低电平有效的写脉冲。我们的电路设计将这个WR信号同时接到了ADC0809的ALE和START上。于是,这个脉冲的上升沿(WR从低变高)使ALE有效,锁存地址;紧接着的下降沿(实际上,WR脉冲结束时从低变高,但START是下降沿触发,需要一点小技巧,通常用WR经过非门或与地址信号组合来产生START,但很多教材为了简化,假设连接正确)启动转换。代码中向一个固定地址写数据,地址总线的低三位(对应ADDA-C)由硬件连接决定,从而选择了通道。等待转换完成(查询法):代码中通过查询P3.2(EOC)的状态来等待。
JB P3.2, WAIT_EOC_HIGH是在等EOC变低(转换开始),JNB P3.2, WAIT_EOC_LOW是在等EOC再变高(转换结束)。这是一个完整的“低-高”跳变检测,比只检测高电平更可靠,避免了芯片上电初始状态可能带来的误判。读取ADC结果 (
MOVX A, @DPTR):执行此指令时,单片机在P3.7(RD)引脚产生读脉冲。我们将RD连接到OE。OE变高,ADC0808内部的输出三态门打开,转换好的数据出现在数据总线(P0口)上,被读入累加器A。写入DAC输出 (
MOVX @DPTR, A):这个过程相对简单。再次使用MOVX @DPTR, A写指令,向DAC的地址写入数据。WR脉冲使DAC0832的CS和WR1有效,数据被锁存并立即转换,输出电压随之改变。
重要心得:很多同学在这里会迷惑,为什么读和写用的是同一个地址?因为对于ADC,我们关心的是对它的“操作”(写操作启动转换,读操作读取结果),而不是地址本身的数据。我们利用的是执行
MOVX指令时自动产生的控制信号(WR或RD)来驱动ADC的START、OE等引脚。地址线只是用来选中这个外设芯片而已。这是一种非常典型的“总线式”扩展I/O口的方法。
4. 调试过程中常见的坑与解决方案
即使代码和电路看起来都对,第一次做这个实验也大概率不会一帆风顺。下面是我和同学们常遇到的几个问题及排查思路。
4.1 ADC转换结果不准或不变化
- 现象:调节电位器,但单片机读回来的数字量不变,或者变化没有规律。
- 排查步骤:
- 检查模拟输入:用万用表直接测量接到ADC0809 IN0引脚的电压,确认它是否随电位器平滑变化(0-5V)。
- 检查参考电压:ADC0809需要稳定的参考电压(Vref+和Vref-)。通常Vref+接+5V,Vref-接地(0V)。确保这两个引脚电压准确。参考电压的精度直接决定转换精度。
- 检查时钟信号:用示波器测量ADC0809的CLK引脚,看是否有约640kHz的方波。如果没有时钟,芯片根本无法工作。检查时钟电路(通常是一个与非门或D触发器构成的振荡电路,或者由单片机ALE分频而来)。
- 检查控制时序:这是最复杂的一步。需要用双通道示波器,同时抓取WR(或START)、EOC和RD(或OE)的波形。
- 触发一次转换后,应该先看到WR(START)有一个负脉冲。
- 随后EOC引脚应该立刻变低(持续约100us,取决于时钟频率)。
- 大约100us后,EOC自动变高,表示转换结束。
- 此时,当你执行读指令时,应该能看到RD(OE)上产生一个负脉冲。 如果时序不对,比如OE脉冲在EOC变高之前就出现了,那读到的就是无效数据。重点检查WR、RD与ALE、START、OE之间的硬件连接逻辑是否正确。
- 检查数据线:确保P0口的上拉电阻(通常用10kΩ排阻)已正确连接且接触良好。P0口在不接上拉时无法输出高电平。
4.2 DAC输出不对或没有输出
- 现象:万用表测量运放输出端电压为0,或者始终为某个固定值(如电源电压或负电源电压),不随输入数字量变化。
- 排查步骤:
- 检查运放电路:这是最容易出错的地方。首先确认运放(如LM358)的正负电源(如+12V和-12V或单电源+5V)已正确接入。检查反相输入端、同相输入端、输出端以及反馈电阻的连接是否正确无误。一个快速判断方法是:将DAC的数字输入DI0-DI7全部接地(输入00H),理论上输出应为0V。如果输出不是0V,多半是运放电路问题。
- 检查DAC基准电压:测量DAC0832的Vref引脚电压。如果你期望输出0-5V,Vref通常接-5V(因为运放是反相接法)。确保这个-5V电源稳定。
- 检查控制信号:确保DAC0832的CS和WR1信号能被正确的写脉冲触发。可以用示波器观察,当执行
MOVX @DPTR, A向DAC地址写数据时,对应的片选和写引脚上应该有负脉冲。 - 检查电流输出端:如果运放电路复杂,可以先断开Iout1与运放的连接,在Iout1和地之间接一个精密电阻(如5kΩ),测量电阻两端的电压。根据公式 Iout1 = (数字量/256) * (Vref / Rfb内部)。假设Vref=-5V,数字量FFH,则Iout1 ≈ (255/256)* (5V/15kΩ) ≈ 0.332mA。在5kΩ电阻上产生的电压约为1.66V。用万用表测量这个电压是否随数字量线性变化,可以快速判断DAC芯片本身是否工作正常。
4.3 系统干扰与稳定性问题
- 现象:数字量读取或模拟量输出值在小范围内跳动(抖动),不稳定。
- 解决方案:
- 电源滤波:重申一遍,在每个芯片的电源引脚附近,紧贴芯片焊接或安装0.1uF的瓷片电容。
- 地线布局:尽量使用粗短的导线连接地线,避免形成地线环路。模拟地(ADC、DAC、运放、电位器的地)和数字地(单片机、逻辑芯片的地)可以在一点汇合,俗称“单点接地”。
- 软件滤波:对于ADC读取的抖动,可以在软件中采用多次采样取平均的方法。例如,连续读取10次ADC值,去掉最大最小值后求平均,再将这个平均值送给DAC。这能有效抑制随机干扰。
; 简单的软件平均滤波示例(读取5次取平均) READ_ADC_AVG: MOV R2, #5 ; 采样5次 MOV R3, #0 ; 和的高字节清零 MOV R4, #0 ; 和的低字节清零 SAMPLING_LOOP: LCALL READ_ADC ; 调用之前的单次读取子程序 ADD A, R4 ; 将本次结果加到和的低字节 MOV R4, A CLR A ADDC A, R3 ; 处理进位 MOV R3, A DJNZ R2, SAMPLING_LOOP ; 循环5次 ; 现在R3R4中是5次采样的和(16位) ; 除以5(右移2位近似除以4,再处理余数,这里简化处理) MOV A, R4 SWAP A ANL A, #0FH ; 取高四位,相当于除以16 ... ; 更精确的除法需要编写除法子程序,此处略 RET
5. 实验的进阶玩法与思路拓展
当你成功实现了基本的电压跟随器后,这个实验平台还可以玩出很多花样,深化对单片机系统的理解。
5.1 实现一个简单的数字电压表
将读取的ADC数字量(0-255)转换成实际的电压值(0.00-5.00V)并显示在数码管或LCD上。这涉及到标度变换。 公式很简单:电压值 = (数字量 / 255) * 5.0 V。 但在汇编里做浮点除法很麻烦。一个实用的技巧是:电压值 (单位mV) ≈ 数字量 * 5000 / 255。 可以先计算数字量 * 5000(16位乘法),再除以255。或者更取巧,因为5000/255 ≈ 19.6078,可以近似为电压值 (mV) ≈ 数字量 * 20,然后通过校准修正误差。显示部分就需要调用数码管动态扫描或LCD写字符串的子程序了。
5.2 生成简单的波形
让DAC输出不再简单地跟随输入,而是由程序控制输出特定的数字序列,从而生成波形。
- 锯齿波:让输出数字量从0递增到255,再回到0,不断循环。
OUTPUT_SAWTOOTH: MOV A, #0 SAW_LOOP: LCALL WRITE_DAC ; 输出当前值 LCALL DELAY ; 调用一个微小的延时,控制波形频率 INC A ; 值加1 CJNE A, #0, SAW_LOOP ; 加到256(溢出为0)则循环 SJMP OUTPUT_SAWTOOTH - 方波:先输出255(高电平)一段时间,再输出0(低电平)一段时间,循环。
- 正弦波:需要预先计算好一个正弦函数表(0-255范围内的值),存储在程序存储器中,然后循环查表输出。这是学习查表法和DDS(直接数字频率合成)概念的好例子。
5.3 引入中断机制优化程序
上述代码用的是查询方式等待EOC,单片机在等待期间一直在空循环,效率很低。我们可以将EOC信号连接到单片机的外部中断引脚(如INT0),利用中断方式来读取ADC结果。
- 优点:启动转换后,单片机可以继续执行其他任务(如刷新显示、处理数据),等ADC转换完成产生中断时,再进入中断服务程序读取数据。大大提高了CPU利用率。
- 修改要点:
- 初始化时,设置外部中断0为下降沿或低电平触发。
- 在中断服务程序
INT0_ISR中,读取ADC数据并存入一个全局变量。 - 主循环中,只需启动转换,然后就可以去做其他事情,需要用到ADC数据时,直接读取那个全局变量即可。
从查询到中断的升级,是单片机编程能力提升的一个重要标志。这个实验虽然基础,但涵盖了硬件连接、时序控制、软件编程、调试排错等多个核心技能点,吃透它,你对单片机系统的理解会上一个坚实的台阶。希望这份详细的回顾和解读,能帮你顺利跨过这道坎。