简介:面向51单片机学习者的电压采集课程设计资源,覆盖单路ADC0808模数转换、LCD1602液晶显示、VB上位机串口通信和Proteus仿真全流程,适合本科单片机课程设计、毕业设计及相关技能训练。硬件原理图使用Altium Designer绘制,随包附有元件清单、下位机流程图和上位机流程图;程序基于Keil编写,提供完整工程、C语言源码及可烧录的HEX文件。资源包共47个文件,涵盖源代码、工程文件、电路图纸、仿真文件、位图和可执行程序等类型,整体大小5.31MB,目录结构清晰,便于快速定位所需模块。当前已有589人在线学习。读者可从中获取从下位机采集到上位机显示的完整方案,同时包含虚拟串口工具、课程设计说明书及多格式原理图备份,能帮助理解模数转换、液晶驱动、串口通信和简单上位机开发等核心环节,直接用于答辩展示或二次开发。
1. 为什么是51+ADC0808而不是一颗STM32
刚拿到“基于51单片机的电压采集”这个题目时,多数人第一反应是:51没有内置ADC,为什么不用STM32的内部ADC?但题目摆明了用ADC0808,背后其实在考两件事:一是并口ADC和单片机之间的时序配合,二是下位机与VB上位机之间的串口协议。这套资源把原理图、Proteus仿真、Keil源码、上位机VB工程全部放在一起,属于典型的51单片机课程设计完整包,适合马上要交课程设计、或者想用最小成本复现“采集—显示—上传”完整链路的人。
整条链路并不复杂:一路0~5V直流电压进入ADC0808的IN0,8位并行结果送给51的P0口,LCD1602实时显示电压值,同时通过串口传给上位机;上位机发一个启动字节,下位机转换一次并把结果回传,转换期间一个LED点亮,结束后熄灭。难的不是任何单个模块,而是把时序和协议拧在一起时不掉链子。下面按我从这套工程里拆出来的顺序讲:先确定系统框架和引脚分配,再写下位机驱动,接着搭Proteus仿真,最后用VB6上位机收数据并排错。
2. 系统框架与关键选型:ADC0808时序、LCD1602接口、串口协议
2.1 信号流与引脚分配
整机可以拆成四个模块:模拟输入、ADC转换、显示、串口通信。电位器把5V电源分压成0~5V电压,接到ADC0808的IN0;ADC0808负责把模拟量变成8位数字量;51单片机经过P0口读回数据,一部分送给LCD1602显示,另一部分按协议打包,经串口发到上位机。上位机发的启动命令则从RXD进入51,触发一次新的转换。
这套资源里的原理图把ADC0808直接挂在P0口上,控制引脚用P1口,LCD1602用P2口。具体连接关系可以整理成表,方便对照原理图:
| ADC0808引脚 | 接到51 | 说明 |
|---|---|---|
| IN0 | 电位器中心抽头 | 输入0~5V,通道0 |
| A/B/C | P1.0/P1.1/P1.2 | 地址选择,000选中IN0 |
| ALE | P1.3 | 地址锁存允许 |
| START | P1.4 | 启动转换,上升沿有效 |
| EOC | P1.5 | 转换结束信号,转换中为低 |
| OE | P1.6 | 输出使能,高电平把结果放到数据总线 |
| D0~D7 | P0.0~P0.7 | 并行数据输出 |
| CLK | 外部时钟源 | 频率建议10kHz~1280kHz |
注意A/B/C三条地址线必须接成000,ADC0808内部才选中IN0。很多proteus仿真跑不出数据,就是因为这三根线没接或接到了高位,通道选错后读到的自然是另一路的信号。另外P0口在51内部是开漏结构,没有内部上拉,所以D0~D7这一组必须在外部加上拉电阻,否则读回的数据电平均不确定,这在2.2会细说。
2.2 ADC0808时序:启动一次转换要“等两段”
熟悉ADC0808之后会发现,它的转换过程特别强调时序,代码不能简单地“读P0”。完整的转换序列是:先给A/B/C设置好地址,ALE产生一个正脉冲把地址锁存进去;然后在START上给一个至少100ns的正脉冲,转换才真正开始。启动后EOC会先拉低,表示正在转换,经过约64个时钟周期后转换结束,EOC恢复高电平;这时再把OE置高,D0~D7上才会出现稳定的8位结果。
用C语言把这个过程写出来就是下面这段,我一般把它封装成一个函数:
unsigned char ReadADC(void) { unsigned char val; ADC_LED = 1; // 转换期间点亮指示灯 ADC_ALE = 1; // 锁存通道地址 ADC_START = 1; // START上升沿启动 _nop_(); _nop_(); ADC_START = 0; // START恢复低电平 ADC_ALE = 0; while (ADC_EOC); // 第一次等待:等待EOC从高变低,进入转换状态 while (!ADC_EOC); // 第二次等待:等待EOC从低变高,转换完成 ADC_OE = 1; // 打开输出使能 val = P0; // 读回8位转换结果 ADC_OE = 0; ADC_LED = 0; // 转换结束,指示灯熄灭 return val; }代码里“while (ADC_EOC);”和“while (!ADC_EOC);”这两行是很多初学者最容易丢的。第一次等待是为了避开START脉冲之后EOC还没来得及拉低的几个时钟周期,第二次等待才是真正等转换结果。CLK若不是640k这样偏快的频率,而是用了很低的中断输出频率,这段等待时间会明显变长,指示灯也能看出“亮一瞬”的效果。请注意不同版本的Proteus或实物ADC0808,EOC极性可能存在反转,如果在仿真里发现读到的数据全0或全1,先用示波器看EOC脚电平变化方向,把两个while的顺序对调即可。
2.3 LCD1602接口:4线模式释放GPIO
LCD1602既支持8位数据模式,也支持4位数据模式。在这个工程里,P0口被ADC0808数据线占满,LCD如果也走8根数据线就无IO可用了,所以资源里的原理图采用4线模式,只用D4~D7接LCD的数据口,控制线RS、RW、EN再加三根,总共只需5个GPIO,还占了P2口的高四位和低三位。这样的好处是P2口低四位还留了一两根IO,可以给指示灯或按键用。
4线模式的写命令时序是:先送高4位,再送低4位,两次配合EN引脚产生下降沿锁存。初始化序列必须严格按照LCD1602的上电时序来,不然屏幕只亮不显示。我习惯在初始化里延时100ms以上,再按0x02、0x28、0x0C、0x01、0x06的顺序写。下面这段是写命令和写数据最基本的两个底层函数:
void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; // 命令模式 LCD_D4 = (cmd >> 4) & 0x01; // 先送高4位 LCD_D5 = (cmd >> 5) & 0x01; LCD_D6 = (cmd >> 6) & 0x01; LCD_D7 = (cmd >> 7) & 0x01; LCD_EN = 1; _nop_(); LCD_EN = 0; // EN下降沿写入 LCD_D4 = (cmd >> 0) & 0x01; // 再送低4位 LCD_D5 = (cmd >> 1) & 0x01; LCD_D6 = (cmd >> 2) & 0x01; LCD_D7 = (cmd >> 3) & 0x01; LCD_EN = 1; _nop_(); LCD_EN = 0; }其中LCD_RS等位的具体定义放在头文件里,不同课设工程的引脚可能稍有出入,切换时只要改宏定义,函数逻辑不用动。参数说明:cmd是一个8位命令字节,比如0x80表示第一行起始地址,0xC0表示第二行起始地址;移位操作是为了先拆出高四位,需要确保LCD_D4不是连接P2.4,而是连接LCD数据线的D4脚。
2.4 上位机与下位机的串口协议
这个资源里的上位机是VB6工程,和下位机之间不是裸传ASCII字符串,而是定义了一种“命令-应答”协议。协议只有两种帧,从功能上说已经很紧凑:
| 方向 | 帧格式 | 说明 |
|---|---|---|
| 上位机→下位机 | 0xAA | 启动一次转换 |
| 下位机→上位机 | 0xA5 + ADC结果 + 校验字节 | 校验=~(0xA5+结果) |
之所以用0xAA而不是发送字符'A',是为了和普通调试数据区分开。0xA5作为返回帧头,下位机发送前先计算0xA5加ADC结果的累加和,再取反作为校验字节。上位机收到三个字节后先判断帧头,再校验,全部通过才更新电压值。这样即使偶发出现串口噪声,也不会把一个错误数值显示到界面上。
协议里的ADC结果是一个0~255的整数,对应0~5V电压,因此上位机的换算公式是voltage = adcVal * 5.0 / 255.0。这个方式在VB和C#上位机里都通用,换语言时只要保留相同的帧头和处理逻辑,下位机代码完全可以不动。到这里,系统框架、引脚分配、时序关系和通信协议都定了,后面三段代码才有地方安放。
3. Keil下位机实现:从引脚初始化到串口上传
3.1 头文件与引脚宏:把硬件连接写进一处
写51程序最怕把引脚在代码里写死,改硬件时满文件找。这套工程里的main.c和lcd1602.h把引脚都收拢在宏定义里,我拆工程时第一件事就是看这段定义。下面是lcd1602.h里关键的部分:
#ifndef __LCD1602_H__ #define __LCD1602_H__ #include <reg52.h> #include <intrins.h> #define LCD_DATA P2 sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_EN = P2^2; sbit LCD_D4 = P2^4; sbit LCD_D5 = P2^5; sbit LCD_D6 = P2^6; sbit LCD_D7 = P2^7; #endif注意这里LCD_DATA和LCD_D4同时映射到P2,其实只用了高四位和低三位,读写数据时不能直接对整个P2赋值,否则会同时影响RS、RW、EN三位。我在实际写代码时很少用LCD_DATA这个宏,而是直接操作D4~D7四个位,这样最安全。头文件里还应该放LCD延时函数、初始化函数和显示函数的声明,这些声明不参与编译大小,但对工程维护很有帮助。
3.2 串口与定时器1初始化
51的串口是依赖定时器1产生波特率,晶振频率直接决定波特率误差。这个工程给定11.0592MHz,就是为了让9600波特率误差恰好为0。初始化代码是每个单片机串口工程都绕不开的基础:
void UART_Init(void) { TMOD &= 0x0F; // 只修改T1的部分,保留T0原配置 TMOD |= 0x20; // T1工作在模式2:8位自动重装 TH1 = 0xFD; // 重装值,生成9600波特率 TL1 = 0xFD; TR1 = 1; // 启动定时器1 SM0 = 0; SM1 = 1; // 方式1:8位UART,波特率由T1决定 REN = 1; // 使能接收 EA = 1; // 开总中断 ES = 1; // 开串口中断 }很多读者会问为什么TH1和TL1是0xFD,因为波特率计算公式是Baud = 11.0592MHz / 12 / (256 - TH1) / 32,代入TH1=0xFD即253,得到9600。如果晶振换成12MHz,这个公式算出的波特率会和9600有偏差,串口会出现乱码。参数说明:TMOD的低4位属于T0,高4位属于T1,所以先“与”0x0F清零T1部分,再“或”0x20,这样不会把T0的模式改掉。
3.3 ADC0808软件驱动:直接查询EOC
前面读过ReadADC()的核心流程,这里需要补充的是如何保证它不阻塞主循环太久。ADC0808在640kHz时钟下有大约100us的转换时间,对51来说并不是特别长,主循环里每收到一条启动命令才调一次ReadADC(),完全等得起。查询EOC这种方式最大的优点是代码简单,缺点是转换期间不能干别的事。如果以后要连续采集,可以改成中断方式,把EOC接到51的外部中断0引脚,转换完成时触发一次INT0。这个工程选择查询方式,我认为是正确的,因为场景就是单路、低速、上位机启动式采集。
在main.c里,ReadADC()读取到的数字量一方面送给显示,另一方面打包发送。需要注意val = P0这行代码必须放在ADC_OE=1之后,并且最好加一个小的空操作延时,等总线稳定后再读。实际工程中我在ADC_OE置位后插了两个_nop_(),以避免刚打开三态门瞬间就采样到不稳定电平。如果省略这个延时,偶发读出的最低位会跳变,显示值抖动明显。
3.4 电压值显示:用整数运算代替浮点
LCD1602显示电压时最直观的是显示“0.00V”这样的字符串。在51上直接写adcVal * 5.0 / 255.0会引入浮点库,代码体积和运行时间都会增加。这个工程里的做法是先把ADC值换算成毫伏整数,再拆成字符显示,逻辑如下:
void ShowVoltage(unsigned char adcVal) { unsigned int mv = (unsigned int)adcVal * 5000UL / 255UL; char buf[6]; buf[0] = '0' + mv / 1000; // 整数位 buf[1] = '.'; buf[2] = '0' + (mv % 1000) / 100; // 十分位 buf[3] = '0' + (mv % 100) / 10; // 百分位 buf[4] = 'V'; buf[5] = '\0'; LCD_ShowString(0, 0, buf); // 第一行第1列显示 }这里把“*5000/255”放在无符号16位整数中计算,避免了浮点数。参数说明:adcVal为0~255,乘以5000后最大为1275000,超过16位范围,所以表达式写成了(unsigned int)adcVal * 5000UL,其中的UL让运算提升为无符号长整型,再除以255得到毫伏值,最大约5000,正好落在unsigned int内。这个细节在Keil C51里很容易踩坑,如果不加UL,编译器会用16位乘法溢出,结果全错。
3.5 主循环:命令接收与应答
最后是主循环。串口采用中断方式接收,收到任何字节都放进uartRxBuf,置位uartRxFlag。主循环发现标志后取出命令,如果等于0xAA就执行一次“读取—显示—发送”。发送函数和主循环代码如下:
void UART_SendByte(unsigned char dat) { SBUF = dat; while (!TI); TI = 0; } void main(void) { unsigned char cmd; unsigned char adcVal; UART_Init(); LCD_Init(); while (1) { if (uartRxFlag) { uartRxFlag = 0; cmd = uartRxBuf; if (cmd == 0xAA) { adcVal = ReadADC(); ShowVoltage(adcVal); UART_SendByte(0xA5); UART_SendByte(adcVal); UART_SendByte(~(0xA5 + adcVal)); // 校验字节 } } } }注意UART_SendByte里while(!TI)是发完一个字节必须等待硬件把TI置1,TI需要用软件清零。校验字节直接取反,发送时由串口逐位输出,接收端再做一次((0xA5+adcVal) & 0xFF)与校验值比较。主循环里没有任何延时函数,所以LCD刷新和串口应答都不会因为软件延时丢失时序。如果上位机出现了“点启动卡死”的现象,先检查UART_Init有没有被调用,再检查是不是中断服务函数里忘了对RI标志清零。
4. Proteus仿真搭建:原理图、物料清单和常见连线
4.1 从原理图到仿真电路的关键连线
资源里的原理图有PDF和SchDoc格式,Proteus仿真文件是仿真.DSN。自己动手从零搭建时,最值得注意的地方是ADC0808的CLK引脚不能悬空。在Proteus左侧工具栏里选择Generator Mode,放置一个Clock元件连接到ADC0808的CLK脚,双击把Frequency设为640k。这个设置是这个工程仿真能不能跑通的关键,很多读者拿到的仿真文件能运行,自己重画一遍就黑屏,问题基本出在这里。
除CLK外,还有几处容易漏掉的连线:ADC0808的VREF(+)接5V,VREF(-)和GND接地;VCC接5V;A、B、C三根地址线全部接地;START、ALE、EOC、OE分别连到51的P1.4、P1.3、P1.5、P1.6。P0口0~7脚需要接一个10k排阻到5V,否则从ADC读取的数据不稳定,Proteus仿真中不接上拉电阻时常见现象是显示0.00V或5.00V的跳变。LCD1602的V0接一个电位器中点,用来调对比度,不接对比度引脚会要么全黑要么看不见字。
4.2 用元件清单(BOM)核对物料
这个工程附带的“元件清单.xlsx”可以直接当采购单用。我把它整理成一张更通用的表,实验板制作和Proteus仿真都能对应上:
| 元件 | 型号/参数 | 数量 | 在电路中作用 |
|---|---|---|---|
| 单片机 | AT89C51或STC89C52RC | 1 | 主控 |
| ADC | ADC0808 | 1 | 8位并行ADC |
| 液晶屏 | LCD1602 | 1 | 本地显示 |
| 电位器 | 10k多圈电位器 | 1 | 模拟0~5V输入 |
| 排阻 | 10k×8 SIP | 1 | P0口上拉 |
| 晶振 | 11.0592MHz | 1 | 串口波特率基准 |
| 瓷片电容 | 30pF | 2 | 晶振负载 |
| 电解电容 | 10uF | 1 | 复位电路 |
| 电阻 | 10k | 1 | 复位上拉 |
| 电阻 | 1k | 若干 | LED限流 |
| LED | 红色或绿色 | 2~3 | 电源指示、转换指示 |
| 电平转换 | MAX232或CH340模块 | 1 | 串口转USB |
| 电源 | +5V稳压模块 | 1 | 给所有芯片供电 |
注意Proteus里的ADC0808元件名是ADC0808,有些搜出来是ADC0808LCN,功能一样。实际制板时如果用到STC89C52RC,它的P0口在复位瞬间会输出高电平,接ADC0808数据线是安全的;但STC的IO口驱动能力和AT89C51略有差别,指示灯限流电阻取1k时电流约4mA,属于安全范围。
4.3 运行仿真前必须检查的5个默认状态
很多51单片机课程设计程序在实物上没问题,换到Proteus仿真却卡住,原因往往是仿真软件的默认状态和真实芯片不一样。我自己的检查顺序是固定的,这里直接列出来:
- 双击51单片机,把Clock Frequency改成11.0592MHz,不要用默认的12MHz,否则串口波特率在那里是准的,实际程序却按11.0592计算,容易误导排错。
- 给ADC0808的CLK接上独立的Clock信号发生器,频率设640k。
- 检查LCD1602的RW脚是否接GND。多数只做写操作的工程把RW直接接地,如果接到P2.1并且初始化函数里没有把LCD_RW置0,屏幕数据根本写不进去。
- 用Proteus的电压表工具,确保IN0处的电位器在转动时电压在0~5V内变化。
- 运行仿真后如果串口没数据,先查COMPIM的Physical port是否设成了本机实际存在的串口,以及波特率、数据位、校验位是否和程序里一致。
4.4 几个反直觉的仿真现象
在proteus仿真里见到最多的“反直觉”现象是:ADC0808的EOC引脚在启动后不是立刻拉低,而是先保持高电平几个微秒,如果代码里只写了“while(!ADC_EOC);”,程序在启动瞬间看见EOC为高会直接跳过等待,读到的数据是上一次的旧值。这也是前面ReadADC()为什么要写两段等待的原因。另一个反直觉现象是P0口在仿真电路里如果不加上拉,LM1117等电源模型会把数据线电平钳到2V多,看似有电压,实际读回的是不可靠的中间态,最终数值乱跳。遇到这类问题,先查电平再查时序,比盲目改代码高效得多。
5. VB6上位机:MSComm控件、数据解析和启动转换
5.1 为什么用MSComm控件而不是API
这套资源的上位机工程用VB6编写,工程文件里是Form1.frm和工程1.vbp。在VB6里操作串口最简单的方案是使用Microsoft Comm Control 6.0(MSComm控件),它把打开串口、收发数据、触发事件都封装好了,不需要直接写CreateFile、ReadFile那堆API。如果安装了VB6或完整Visual Studio 6,控件默认在工具箱里;如果没有,需要运行regsvr32 mscomm32.ocx注册一下。这里不涉及外链,只说一个定位思路:在VB6窗体上右键选择“部件”,勾选Microsoft Comm Control 6.0,工具箱里就会出现电话机图标。
MSComm控件的三个关键属性是CommPort、Settings和InputMode。CommPort指定COM口号,Settings对应波特率、校验位、数据位和停止位,与下位机的UART_Init必须完全一致。InputMode要设成二进制方式,这样读取返回帧时才能拿到非ASCII的0xA5字节。注意不要用文本模式,否则0xA5会被当作ANSI字符,编解码都很别扭。
5.2 串口初始化和“启动转换”按钮
在Form_Load里完成串口初始化,并打开指定COM口:
Private Sub Form_Load() MSComm1.CommPort = 1 ' COM1,按实际设备修改 MSComm1.Settings = "9600,N,8,1" ' 与下位机完全一致 MSComm1.InputMode = comInputModeBinary MSComm1.RThreshold = 3 ' 收到3个字节触发OnComm事件 MSComm1.SThreshold = 0 MSComm1.PortOpen = True ' 打开串口 End Sub参数说明:9600,N,8,1分别表示波特率9600、无校验、8位数据、1位停止位;RThreshold等于3,表示接收缓冲区内每累积3个字节就触发一次OnComm事件。有人会把RThreshold设成1,那样每收到一个字节触发一次,需要在事件里自己拼帧,做法也没错;设成3更贴合这边定义的固定3字节返回帧,代码更短。
发送启动命令的按钮逻辑就是向串口写入一个字节0xAA:
Private Sub cmdStart_Click() Dim buf(0) As Byte buf(0) = &HAA MSComm1.Output = buf End Sub这里“Dim buf(0) As Byte”其实是申请一个包含一个元素的Byte数组,VB6里必须用数组才能通过Output发送二进制数据。如果直接写MSComm1.Output = Chr(&HAA),会因为字符编码问题发送出别的字节,这是新手比较容易踩的VB6串口坑。
5.3 接收处理:校验字节和电压换算
下位机返回的是固定3字节帧:0xA5、ADC结果、校验字节。在MSComm1_OnComm事件里用Select Case判断收到事件,代码如下:
Private Sub MSComm1_OnComm() Dim inData() As Byte Dim b1 As Byte, b2 As Byte, b3 As Byte Dim adcVal As Integer Select Case MSComm1.CommEvent Case comEvReceive inData = MSComm1.Input If UBound(inData) >= 2 Then b1 = inData(0) b2 = inData(1) b3 = inData(2) If b1 = &HA5 Then If b3 = ((Not (b1 + b2)) And &HFF) Then adcVal = b2 txtVoltage.Text = Format(adcVal * 5# / 255#, "0.00") End If End If End If End Select End Sub代码逻辑说明:先用UBound(inData)判断数组长度至少为3,防止只收到半个帧时越界;其次判断帧头0xA5;再校验第三个字节。校验表达式里“Not (b1 + b2)”在VB中是按位取反,得到的是32位整数,所以用“And &HFF”截成低8位。计算电压时adcVal * 5# / 255#,其中5#表示双精度浮点数5,目的就是把adcVal转换成浮点再计算,否则两个整数相除结果只会取整。
如果发现在上位机界面上显示的数字只有0和5两个值,多半是RThreshold=3时把两帧数据黏在了一起:下位机连续发送的速度很快,Windows接收缓冲区可能一次性攒了6个字节。判断方法是清空接收缓冲区后再测试,或把RThreshold改成1,用状态机按帧头0xA5重新对齐。这个方法放在第6章展开,这里先记住现象。
5.4 和C#上位机对比:协议不变,代码迁移成本低
很多读者手里有VS2019开发的C#上位机源码,想拿过来直接用。C#里对应的是SerialPort类,没有MSComm的InputMode属性,而是用Encoding.GetEncoding("ISO-8859-1")或直接读字节。只要下位机发送的还是0xA5帧、校验算法不变,上位机只需修改串口事件里的字节处理部分,Form和定时器逻辑可以原样搬。反过来,如果手头只有VB6工程,想改成C#,重点看Form1.frm里的这段OnComm处理,把它翻译成SerialPort.DataReceived处理器即可。协议中0xAA启动、0xA5帧头、校验字节这三个约束保持不变,两边就能互通。
6. 用虚拟串口和逻辑探针验证整条链路
没有真实单片机板子时,Proteus的COMPIM模型配合虚拟串口软件,可以做一次“全虚拟联调”。这套资源里带的虚拟串口6.9.rar就是干这个用的,安装后创建一对虚拟串口,比如COM3和COM4,它们被系统看成两条物理串口。Proteus里的COMPIM元件一条接单片机TXD/RXD,另一条设置物理串口为COM3;VB上位机MSComm1.CommPort设为4。这样上位机发出的命令先到COM4,虚拟串口对把数据转到COM3,再由Proteus里的COMPIM收进单片机,反向同理。
虚拟串口联调时,改变Proteus里电位器的分压比,VB界面显示的电压值会跟着变。此时如果上下位机数值不一致,先查两边的波特率是否都是9600,再查校验算法。一个非常实用的排查技巧是在Proteus里加一个逻辑探针挂在TXD脚,用示波器观察串口波形。51的TXD空闲时为高电平,发送0xA5时波形应出现一个低起始位加8个数据位。若波形完全是乱的,说明波特率重装值0xFD和晶振不匹配。
最后一个值得记住的坑是ADC0808的EOC极性问题。不同教材对EOC的极性描述不一致,实物芯片的输出级有些带反相,Proteus元件模型也可能出现差异。读到的ADC值恒为0x00或0xFF时,不要急着改协议,先用电压表量IN0电压,再用逻辑探针看EOC、OE和数据线。确定EOC是“转换中为低”还是“转换中为高”后,把ReadADC()里的两个while等待顺序对调,问题通常会直接消失。A、B、C三条地址线必须接低电平,否则通道选错会读到另外一路的电压。
本文还有配套的精品资源,点击获取