拓冰建站拓冰建站
首页 / 资讯中心 / 正文

51单片机+ADC0808电压采集系统设计与Proteus仿真实现

简介面向51单片机学习者的电压采集课程设计资源覆盖单路ADC0808模数转换、LCD1602液晶显示、VB上位机串口通信和Proteus仿真全流程适合本科单片机课程设计、毕业设计及相关技能训练。硬件原理图使用Altium Designer绘制随包附有元件清单、下位机流程图和上位机流程图程序基于Keil编写提供完整工程、C语言源码及可烧录的HEX文件。资源包共47个文件涵盖源代码、工程文件、电路图纸、仿真文件、位图和可执行程序等类型整体大小5.31MB目录结构清晰便于快速定位所需模块。当前已有589人在线学习。读者可从中获取从下位机采集到上位机显示的完整方案同时包含虚拟串口工具、课程设计说明书及多格式原理图备份能帮助理解模数转换、液晶驱动、串口通信和简单上位机开发等核心环节直接用于答辩展示或二次开发。1. 为什么是51ADC0808而不是一颗STM32刚拿到“基于51单片机的电压采集”这个题目时多数人第一反应是51没有内置ADC为什么不用STM32的内部ADC但题目摆明了用ADC0808背后其实在考两件事一是并口ADC和单片机之间的时序配合二是下位机与VB上位机之间的串口协议。这套资源把原理图、Proteus仿真、Keil源码、上位机VB工程全部放在一起属于典型的51单片机课程设计完整包适合马上要交课程设计、或者想用最小成本复现“采集—显示—上传”完整链路的人。整条链路并不复杂一路0~5V直流电压进入ADC0808的IN08位并行结果送给51的P0口LCD1602实时显示电压值同时通过串口传给上位机上位机发一个启动字节下位机转换一次并把结果回传转换期间一个LED点亮结束后熄灭。难的不是任何单个模块而是把时序和协议拧在一起时不掉链子。下面按我从这套工程里拆出来的顺序讲先确定系统框架和引脚分配再写下位机驱动接着搭Proteus仿真最后用VB6上位机收数据并排错。2. 系统框架与关键选型ADC0808时序、LCD1602接口、串口协议2.1 信号流与引脚分配整机可以拆成四个模块模拟输入、ADC转换、显示、串口通信。电位器把5V电源分压成0~5V电压接到ADC0808的IN0ADC0808负责把模拟量变成8位数字量51单片机经过P0口读回数据一部分送给LCD1602显示另一部分按协议打包经串口发到上位机。上位机发的启动命令则从RXD进入51触发一次新的转换。这套资源里的原理图把ADC0808直接挂在P0口上控制引脚用P1口LCD1602用P2口。具体连接关系可以整理成表方便对照原理图ADC0808引脚接到51说明IN0电位器中心抽头输入0~5V通道0A/B/CP1.0/P1.1/P1.2地址选择000选中IN0ALEP1.3地址锁存允许STARTP1.4启动转换上升沿有效EOCP1.5转换结束信号转换中为低OEP1.6输出使能高电平把结果放到数据总线D0~D7P0.0~P0.7并行数据输出CLK外部时钟源频率建议10kHz~1280kHz注意A/B/C三条地址线必须接成000ADC0808内部才选中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或实物ADC0808EOC极性可能存在反转如果在仿真里发现读到的数据全0或全1先用示波器看EOC脚电平变化方向把两个while的顺序对调即可。2.3 LCD1602接口4线模式释放GPIOLCD1602既支持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工作在模式28位自动重装 TH1 0xFD; // 重装值生成9600波特率 TL1 0xFD; TR1 1; // 启动定时器1 SM0 0; SM1 1; // 方式18位UART波特率由T1决定 REN 1; // 使能接收 EA 1; // 开总中断 ES 1; // 开串口中断 }很多读者会问为什么TH1和TL1是0xFD因为波特率计算公式是Baud 11.0592MHz / 12 / (256 - TH1) / 32代入TH10xFD即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_OE1之后并且最好加一个小的空操作延时等总线稳定后再读。实际工程中我在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置1TI需要用软件清零。校验字节直接取反发送时由串口逐位输出接收端再做一次((0xA5adcVal) 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()接5VVREF(-)和GND接地VCC接5VA、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或STC89C52RC1主控ADCADC080818位并行ADC液晶屏LCD16021本地显示电位器10k多圈电位器1模拟0~5V输入排阻10k×8 SIP1P0口上拉晶振11.0592MHz1串口波特率基准瓷片电容30pF2晶振负载电解电容10uF1复位电路电阻10k1复位上拉电阻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.0MSComm控件它把打开串口、收发数据、触发事件都封装好了不需要直接写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字节返回帧代码更短。发送启动命令的按钮逻辑就是向串口写入一个字节0xAAPrivate 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两个值多半是RThreshold3时把两帧数据黏在了一起下位机连续发送的速度很快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另一条设置物理串口为COM3VB上位机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三条地址线必须接低电平否则通道选错会读到另外一路的电压。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门