简介:这是一套面向单片机初学者的电压检测系统完整项目,以单片机为核心控制器,搭配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 而不是其他 ADC
2.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/(10+5) = 1/3,实际电压 = ADC 读数 × (5V / 255) × 3。
这里有一个容易忽略的问题:信号源内阻。如果被测对象内阻很大,分压电阻会拉低被测电压,导致读数偏小。一般的做法是在分压输出和 ADC 输入之间加一级电压跟随器(用 LM358 单电源供电就能实现),但 Proteus 仿真里直接接也可以,真实硬件上这个跟随器建议保留。
参考电压 Vref 直接决定了转换精度。ADC0809 的 Vref+ 接 +5V,Vref- 接地,所以最低有效位 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 的脉冲发生器直接接 CLK
- COMPIM 虚拟串口组件在 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 上位机和单片机之间要约定一套协议,这是整个过程中最重要的部分。我用的是经典的三字节帧:
| 字节序号 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头,固定值,用于上位机识别一帧的开始 |
| 1 | 数据高位 | 毫伏值的高 8 位 |
| 2 | 数据低位 | 毫伏值的低 8 位 |
校验和可以不加,因为串口本身有帧错误检测,而且数据只有两字节,出错概率低。如果想更严谨,在第三字节加一个简单的异或校验,上位机收到后把三字节做异或,结果是 0 就认为帧有效。串口配置为 9600bps、8 数据位、1 停止位、无校验。
单片机定时发送,不主动接收上位机指令,这样上位机只要被动收数据,逻辑最简单。发送周期设为 500ms,即每秒上报两次当前电压。通信初始化:
void uart_init(void) { TMOD = 0x20; // 定时器1 工作方式2,8位自动重装 TH1 = 0xFD; // 9600bps @ 12MHz 晶振 TL1 = 0xFD; PCON = 0x00; // 倍频位SMOD=0 SCON = 0x50; // 串口方式1,允许接收 TR1 = 1; // 启动定时器1 }注意 Keil 里 11.0592MHz 晶振配 0xFD,12MHz 晶振配 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(i+1) * 256 + buf(i+2)。拼接完再显示到文本框,同时追加到曲线数据数组里。下面是一段完整的接收事件处理程序:
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 的数据送进 VB
Proteus 里的 COMPIM 元件本质上是一个仿真串口,它必须对应电脑上的一个物理或虚拟串口才能与外部程序通信。常见做法是安装一个虚拟串口驱动软件,创建一对互相连接的虚拟串口,比如 COM3 和 COM4。然后 Proteus 里 COMPIM 的 Physical Port 设为 COM3,VB 上位机打开 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 的量化噪声不是白噪声,过采样增益有限),或者用三点中值滤波去除脉冲干扰——这两种方法在课程设计里写成文字描述都非常加分。
本文还有配套的精品资源,点击获取