
简介这是一套面向单片机初学者的电压检测系统完整项目以单片机为核心控制器搭配VB上位机与Proteus仿真环境覆盖电压信号采集、数据处理与可视化显示全流程适合课程设计、电子竞赛或嵌入式入门实践。压缩包共42个文件包含Proteus仿真文件DSN/PWI、VB上位机程序VBP/FRM/EXE、下位机C与汇编源码C/ASM/HEX、电路图及说明文档还附带随机电压产生测试程序整包大小仅为147KB。资料从硬件电路搭建到上位机界面设计均有涉及可对照单片机原理、UART串口通信、VB编程和Proteus仿真等知识点展开学习已有95人浏览学习。文件目录按上、下位机与仿真模块划分便于快速查找与二次开发既能直接复用工程模板也可深入理解各模块联动逻辑对提升软硬件综合能力很有帮助。1. 从一块 ADC 到一个上位机这套电压检测系统到底在做什么把“基于单片机的电压检测系统VB上位机proteus仿真文件程序.zip”拆开看它其实是一套完整的“采集—传输—显示”链路单片机通过 ADC 芯片采集被测电压把结果换算成真实电压值再通过串口发给 VB 编写的上位机上位机负责显示、存储和简单的波形绘制。Proteus 仿真文件的作用是让没有实物板子的人也能把这套链路跑通——它用虚拟仪表替代万用表和电压表用虚拟串口替代物理串口调好的程序烧进 AT89C51 就能看到数据流动的全过程。这个标题适合三类人正在做单片机课程设计或毕业设计的本科生需要一个“从硬件采集到 PC 端显示”的完整闭环刚接触 VB6 串口编程、想找一个能跟硬件对话的练手项目的开发者以及想验证 Proteus 仿真和真实串口通信差异的嵌入式爱好者。它的核心价值不在“检测电压”这个动作本身而在于把弱信号采集、通信协议、上位机解析这三个环节串起来。下面按原理、电路、程序、上位机、联调排错的顺序展开每一段都给出可以直接抄的代码和参数。2. 方案选型和电压采集电路为什么是 ADC0809 而不是其他 ADC2.1 三种常见 ADC 方案的取舍先从最核心的选型说起。51 单片机没有内置 ADC必须外挂转换芯片。课程设计里最常见的三个选择是 ADC0809、ADC0832、PCF8591它们的差别直接决定电路复杂度和程序写法。ADC0809 是 8 位、8 通道、逐次逼近型 ADC并行输出转换时间约 100μs供电 5V。它的优点是引脚多、时序直观Proteus 里有现成元件仿真时能看到完整的数据总线读写过程教学意义很强缺点是占用的 I/O 口太多——8 根数据线加 4 根控制线共 12 个引脚。ADC0832 是 8 位、2 通道、串行输出只需要 3 根线CLK、DO/DI、CS省引脚但 Proteus 里它的时序不如 ADC0809 直观而且芯片本身已经逐步停产实物采购不如 ADC0809 方便。PCF8591 是 8 位、4 通道、I2C 接口只需要 SCL 和 SDA 两根线还能同时输出模拟量功能最现代。但它要求写 I2C 时序对新手不友好调试时逻辑分析仪是必需品。这套系统选择 ADC0809 是合理折中51 单片机 I/O 口够用Proteus 仿真对并行 ADC 的建模更成熟EOC、ALE、START、OE 这些控制信号在示波器上看得一清二楚。如果你打算改成 STC 单片机做实物那就直接用片内 ADC省掉整个外部采集电路这是后话。2.2 分压电路和参考电压的计算ADC0809 的输入范围是 0~5V但实际要测的电压经常超过 5V。常见的处理是用电阻分压把被测电压缩放到 0~5V 区间。比如要测 0~15V 的直流电压就用两只电阻串联上面 10kΩ、下面 5kΩ输出接到 IN0 通道。分压比是 5/(105) 1/3实际电压 ADC 读数 × (5V / 255) × 3。这里有一个容易忽略的问题信号源内阻。如果被测对象内阻很大分压电阻会拉低被测电压导致读数偏小。一般的做法是在分压输出和 ADC 输入之间加一级电压跟随器用 LM358 单电源供电就能实现但 Proteus 仿真里直接接也可以真实硬件上这个跟随器建议保留。参考电压 Vref 直接决定了转换精度。ADC0809 的 Vref 接 5VVref- 接地所以最低有效位 LSB 5V/256 ≈ 19.5mV。这句话的意思是8 位 ADC 只有 255 个量化台阶无法分辨小于 20mV 的变化。如果你的应用要求毫伏级精度8 位 ADC 从物理上就不够用得换 10 位或 12 位芯片或者用过采样技术后面会提到。2.3 Proteus 元件库里的关键器件排查在 Proteus 元件模式里输入关键字搜索按 Enter 确认放置。这一步经常卡住人的是搜“ADC0809”能出来但引脚不全容易漏接。元件搜索名称ADC0809数据手册里叫 ADC0808/0809两者引脚兼容Proteus 里都可用AT89C51 的 ALE 引脚默认输出频率是晶振的 1/6。12MHz 晶振下 ALE 是 2MHz不能直接作为 ADC0809 的 CLK 输入——它的要求是 10kHz~1280kHz典型值 500kHz 左右。常见做法是用一个 74LS74 或 74LS90 做分频或者干脆放一个频率 500kHz、幅值 5V 的脉冲发生器直接接 CLKCOMPIM 虚拟串口组件在 Proteus 工具面板里属于“Virtual Instruments”类别双击它可以把 Physical Port 设置成电脑上的虚拟串口比如 COM3这是连接 VB 上位机的关键桥接点2.4 完整电路接线表下面是这套系统里所有关键节点的接线关系照着接就能少走弯路。数据总线 P0 口在 Proteus 里默认是开漏输出必须接上拉电阻排RESPACK-8否则读回来的数据全是对的但就是不干活这是新手最容易踩的坑。信号连接位置说明ADC0809 IN0分压电阻输出被测电压接入通道START单片机 P3.0上升沿启动转换EOC单片机 P3.1转换结束输出低电平脉冲OE单片机 P3.2高电平时允许读数据ALE脉冲电源 500kHz相当于时钟信号A/B/C 地址接 VCC/GND/GND选中 IN0 通道D0-D7单片机 P0.0-P0.7数据总线需接上拉Vref / Vref-5V / GND参考电压可外接精密基准接完电路后在 Proteus 里放置一个 DC Voltmeter并联在分压输出端用来做后面“上位机显示值 vs 仿真仪表值”的对照校验。这个电压表属于“Virtual Instruments”能直接把当前节点的仿真电压读出来精度比人意料的准用它来测量参考电压再合适不过。3. 单片机程序ADC0809 时序、数据换算和串口发送协议3.1 ADC0809 的启动和读取时序ADC0809 的转换时序要严格按顺序来。START 引脚需要一个至少 100ns 宽的高电平脉冲来启动转换转换期间 EOC 引脚从高电平变为低电平表示忙碌转换完成后 EOC 变回高电平。所以读数据的标准流程是给 START 一个低-高-低的脉冲然后死等 EOC 引脚变高最后把 OE 拉高P0 口上的数据就是转换结果。程序里需要注意一个细节启动转换后不要立刻去查 EOC 引脚因为它在转换开始瞬间有个短暂的高电平毛刺误判为转换完成会导致读到无效数据。保险的做法是在 START 发出后延时几微秒或者连续两次检测 EOC 为高才算完成。AT89C51 工作在 12MHz 下一条 NOP 约 1μs加两三条空指令就能避开这个问题。3.2 可抄作业的 C51 采集驱动代码#include reg51.h #include intrins.h sbit ADC_START P3^0; sbit ADC_EOC P3^1; sbit ADC_OE P3^2; #define ADC_DATA P0 // 从 IN0 通道读取一次 8 位 ADC 结果 unsigned char adc_read(unsigned char channel) { unsigned char t; // 选择通道A/B/C 引脚组合这里固定 IN0 ADC_START 0; _nop_(); ADC_START 1; // 上升沿启动转换 _nop_(); _nop_(); ADC_START 0; // 等待 EOC 变高表示转换结束 // 注意检测前的延时至少 5 微秒避免毛刺误判 for (t 0; t 5; t) _nop_(); while (ADC_EOC 0); // 转换未结束继续等待 ADC_OE 1; // 打开输出使能P0 上出现数据 _nop_(); t ADC_DATA; // 读取 8 位结果 ADC_OE 0; // 关闭输出使能释放总线 return t; }时序要点有三个。第一通道选择 A/B/C 必须在 START 引脚给出上升沿之前保持稳定这个设计在代码里体现为通道切换和 START 置高是顺序执行的第二EOC 检测是阻塞式等待如果在真实硬件上 EOC 一直没变高程序就死等在这里这时候优先检查 500kHz 时钟有没有接上、START 波形有没有出来第三读取数据后必须把 OE 拉低否则 ADC0809 会把数据线一直霸占着影响后续外设访问 P0 口。3.3 电压值换算策略整数传输避免浮点精度陷阱ADC 读回来的裸值 t 是一个 0~255 的数换算公式为 V t × (Vref/255) × 分压比。如果直接把这个公式里的浮点运算做在单片机里用 Keil C51 的内存模式编译浮点库会额外占用约 1KB 的代码空间而且运行一次浮点除法要消耗几百个指令周期对 12MHz 的 AT89C51 来说毫无必要。合理做法是全部用整数运算只在上位机做一次除法显示。例如 Vref 5V分压比 3要显示到 0.01V 精度可以定义// 实际电压(mV) 裸值 * 5000 * 3 / 255 // 裸值 * 15000 / 255 // 裸值 * 1000 / 17 unsigned int calc_voltage_mv(unsigned char adc_value) { return (unsigned int)adc_value * 1000 / 17; }这样算出来的结果单位是“毫伏”乘法最大结果是 255×1000 255000没有溢出 unsigned int上限 65535这里超了需要在代码里把运算提升成 unsigned long。正确写法是return ((unsigned long)adc_value * 1000 / 17);这个低级错误在仿真时表现不明显因为 255×1000 的溢出结果看起来像是乱码有经验的一眼就能从数据特征判断出来。上位机收到毫伏值显示时除以 1000 得到带两位小数的伏特值全程不出现浮点数。3.4 串口通信协议帧头 数据 校验VB 上位机和单片机之间要约定一套协议这是整个过程中最重要的部分。我用的是经典的三字节帧字节序号内容说明00xAA帧头固定值用于上位机识别一帧的开始1数据高位毫伏值的高 8 位2数据低位毫伏值的低 8 位校验和可以不加因为串口本身有帧错误检测而且数据只有两字节出错概率低。如果想更严谨在第三字节加一个简单的异或校验上位机收到后把三字节做异或结果是 0 就认为帧有效。串口配置为 9600bps、8 数据位、1 停止位、无校验。单片机定时发送不主动接收上位机指令这样上位机只要被动收数据逻辑最简单。发送周期设为 500ms即每秒上报两次当前电压。通信初始化void uart_init(void) { TMOD 0x20; // 定时器1 工作方式28位自动重装 TH1 0xFD; // 9600bps 12MHz 晶振 TL1 0xFD; PCON 0x00; // 倍频位SMOD0 SCON 0x50; // 串口方式1允许接收 TR1 1; // 启动定时器1 }注意 Keil 里 11.0592MHz 晶振配 0xFD12MHz 晶振配 0xFD 也接近 9600但有 0.3% 的频率误差。Proteus 仿真时 12MHz 完全够用如果做真实硬件建议换 11.0592MHz 晶振否则长时间通信会偶发错帧。void send_frame(unsigned int voltage_mv) { SBUF 0xAA; while (TI 0); TI 0; SBUF (unsigned char)(voltage_mv 8); while (TI 0); TI 0; SBUF (unsigned char)(voltage_mv 0xFF); while (TI 0); TI 0; }4. VB 上位机MSComm 控件的串口接收与电压显示4.1 VB6 串口编程的控件配置VB6 里做串口通信常规做法是通过 MSComm 控件它要求系统里注册 mscomm32.ocx 文件64 位 Win10/11 上需要把文件放进 SysWOW64 目录并手动注册 regsvr32这是老程序时代的遗留问题。如果不想折腾注册表用 VB.NET 的 SerialPort 类也可以但从代码量来说 VB6 MSComm 更简洁适合课程设计答辩时展示。在 VB 窗体上放置一个 MSComm 控件一个文本框用于显示电压值一个按钮用于打开/关闭串口再加一个 PictureBox 用于画简单的电压曲线。控件属性设置MSComm1.CommPort 3 使用 COM3虚拟串口端口号 MSComm1.Settings 9600,N,8,1 波特率9600无校验8数据位1停止位 MSComm1.InputMode comInputModeBinary 二进制方式接收 MSComm1.RThreshold 1 每收到1个字节触发一次 OnComm 事件 MSComm1.InputLen 0 一次读入接收缓冲区的全部内容 MSComm1.PortOpen True 打开串口RThreshold 设置为 1 的意图是每个字节到达都触发事件这样自己能完全控制帧拼接逻辑。代价是高波特率下事件触发频繁会出现同时收到半条帧或多条帧的粘包问题下面第 5 章会展开讲如何用状态机处理。4.2 二进制数据接收把字节数组拼成电压值MSComm 的 InputMode 必须设为 comInputModeBinary这样 Input 属性返回的是字节数组。注意在 VB6 里不能直接把 Variant 类型转成 Byte 数组得用赋值语句转一下Dim buf() As Byte Dim data_in As Variant data_in MSComm1.Input 读出所有待处理字节 buf data_in 转为字节数组注意括号的写法拿到 buf 之后用状态机的思路逐字节扫描寻找 0xAA 帧头。找到帧头后把后面两个字节拼接成整型毫伏值voltage_mv buf(i1) * 256 buf(i2)。拼接完再显示到文本框同时追加到曲线数据数组里。下面是一段完整的接收事件处理程序Private Sub MSComm1_OnComm() Dim data_in As Variant Dim buf() As Byte Dim i As Integer Dim mv As Long If MSComm1.CommEvent comEvReceive Then data_in MSComm1.Input buf data_in For i 0 To UBound(buf) If buf(i) HAA Then 检查帧长度是否足够 If i 2 UBound(buf) Then mv buf(i 1) * 256 buf(i 2) txtVoltage.Text Format$(mv / 1000, 0.00) V 把值存入曲线数组用于绘图 Call AddPoint(mv / 1000) End If End If Next i End If End Sub这个解析逻辑有一个隐蔽的问题如果收到的是“数据字节本身误碰 0xAA”就会被误判为帧头。解决方法是把帧头改成两个连续字节 0xAA 0x55或者直接在第三字节加校验和。课程设计里一个帧头通常够用但丢帧率确实存在后面排错章节会给出验证方法。4.3 曲线绘制和日志保存有了毫伏值之后可以画一条滚动曲线来直观展示电压变化。在 PictureBox 上画曲线的基本思路是维护一个固定长度的数组比如 200 个点每收到一个新值就把它追加到数组末尾数组满了就把最早的点丢弃然后整条重画。VB6 里的 Line 方法足够胜任Private Sub DrawChart() Dim i As Integer Dim x1 As Single, y1 As Single Dim x2 As Single, y2 As Single Picture1.Cls 假设 Picture1.ScaleWidth 映射到 200 个点ScaleHeight 映射到 0~15V For i 1 To UBound(values) - 1 x1 (i - 1) * (Picture1.ScaleWidth / UBound(values)) y1 Picture1.ScaleHeight - values(i - 1) / 15 * Picture1.ScaleHeight x2 i * (Picture1.ScaleWidth / UBound(values)) y2 Picture1.ScaleHeight - values(i) / 15 * Picture1.ScaleHeight Picture1.Line (x1, y1)-(x2, y2) Next i End Sub日志保存最简单的方法是每隔一段时间把当前的电压值追加到一个文本文件或者用 CommonDialog 让用户选择保存路径。文件格式就三列时间戳、毫伏值、伏特值答辩时能拿出这段历史记录比口头描述有说服力得多。5. Proteus 与 VB 的串口桥接虚拟串口配对和联调排错5.1 用虚拟串口软件把 Proteus 的数据送进 VBProteus 里的 COMPIM 元件本质上是一个仿真串口它必须对应电脑上的一个物理或虚拟串口才能与外部程序通信。常见做法是安装一个虚拟串口驱动软件创建一对互相连接的虚拟串口比如 COM3 和 COM4。然后 Proteus 里 COMPIM 的 Physical Port 设为 COM3VB 上位机打开 COM4这样单片机发的数据从 COM3 出、从 COM4 进 VB链路就通了。5.2 RThreshold 和缓冲区导致的丢帧问题串口参数里最容易影响上位机稳定性的就是缓冲区大小和 RThreshold 的配合。MSComm 默认输入缓冲区大小是 4096 字节9600 波特率下每秒约 960 字节500ms 发 3 字节的节奏完全不会溢出。问题出在另外一种情况如果你把 RThreshold 设成大于 1 的数比如 3那么只有在缓冲区积满 3 字节时才触发一次 OnComm。这在低速发送时没问题但在数据涌入很快时如果一帧数据恰好被分成两批到达你的拼接逻辑就可能把上一帧的尾巴当成下一帧的头。所以 RThreshold 设 1每次自己拼帧是稳的做法。5.3 联调时看哪几个关键信号Proteus 仿真里加上虚拟示波器在工具面板里选择 Oscilloscope把探头接到 ADC0809 的 EOC 引脚和单片机串口 TXD 引脚上。上电后示波器上应该能看到以下两个特征。EOC 引脚规律性出现低电平脉冲——每个脉冲表示一次 ADC 转换完成TXD 引脚出现串行波形——波特率 9600 下帧间隔明显波形不是一条直线。这里用 Proteus 示波器的 Cursor 模式测量脉冲宽度可以核对波特率是否准确。如果 EOC 一直是高电平说明 START 没有正确启动转换。用示波器量 START 引脚如果 EOC 出现高电平毛刺且随后变低说明时序竞争问题存在按第 3 章的方法加延时。如果能看到 EOC 波形但上位机收不到数据把示波器探头移到 TXD 引脚没有波形基本可以断定单片机死循环或串口初始化失败。这里强调一点优先检查程序是否停在 while(ADC_EOC 0) 的循环里用 Keil 仿真跑单步是最快的定位方式。这里能用的另一个方法在 while 循环外层加一个延时上限循环超过 10000 次就强制退出并返回 0这样即使硬件异常程序也不会死锁。5.4 从上位机反查验证收到的电压值是否可信收尾的关键一步是验证整个链路的数据可信度。在 Proteus 里放一个 DC Voltmeter接在 ADC0809 的 IN0 输入前记录它的读数然后看 VB 上位机文本框里的显示值。两者做对比误差来源于量化误差±1 LSB ±19.5mV和分压电阻精度仿真里用理想电阻误差为 0。如果误差大于 100mV去检查上拉电阻有没有放、电阻阻值是不是看错了。这个验证步骤也是你在答辩时最能体现工程素养的地方能说清楚你的系统误差来源是什么、理论误差是多少、实测误差是多少比停留在“电压显示正常”要深一个层次。如果你还想把采集变得更准可以在 VB 上位机里对同一电压连续采样十次求平均这样能把随机噪声压低到原值的 1/√10注意 ADC0809 的量化噪声不是白噪声过采样增益有限或者用三点中值滤波去除脉冲干扰——这两种方法在课程设计里写成文字描述都非常加分。本文还有配套的精品资源点击获取