Proteus仿真STM32F103数字万用表:ADC采样与分压电路全解析 简介一套基于STM32F103的数字万用表Proteus仿真项目资源面向嵌入式学习者和电子设计开发者聚焦在虚拟环境中实现电压、电流、电阻测量与LCD显示的全流程开发。项目涉及ADC采集、DAC参考电压、测量算法、按键交互及低功耗管理等关键环节可帮助读者掌握从芯片外设配置到系统联调的完整思路也适合课程设计或自学实训。资源共1020个文件约14.29MB以C语言源码558个.c、243个.h为主辅以Proteus工程文件pdsprj、Keil工程配置uvprojx/uvoptx、编译输出hex/axf及调试辅助文件覆盖源码、工程、仿真和固件产物便于对照学习和复用已有3088人浏览学习。通过本包可获取可直接打开的数字万用表仿真工程结合源码分析STM32的ADC数据处理、LCD驱动和模式切换逻辑同时看到按键交互、量程选择及低功耗管理的具体实现。Proteus仿真环境支持在线调试适合作为嵌入式课程设计或入门STM32综合实训的参考方案。1. Proteus仿真下的STM32F103数字万用表先把虚拟表盘跑亮再上真板子Proteus里搭一个基于STM32F103的数字万用表听起来就是“ADC加LCD”真动手你会发现坑几乎都藏在接口上ADC引脚电压怎么压到3.3V以内、读数怎么换算成真实电压、换挡瞬间读数怎么稳住。这份资源把整条链路串了起来解决的是“没有硬件也能把万用表逻辑调通”的问题特别适合正在学STM32 ADC的人以及做课程设计想先验证方案再画板的人。核心思路一句话用电阻分压把被测电压压到3.3V以内F103内部12位ADC采样量程切换和档位显示全由代码层面处理。你不需要额外的模拟前端Proteus里一套电阻就能跑通。2. 硬件电路仿真分压网络、ADC引脚与Proteus元件选型2.1 万用表量程设计的核心矛盾ADC参考电压只有3.3VSTM32F103内置ADC是12位满量程对应参考电压3.3V也就是说ADC引脚上超过3.3V读数直接饱和成4095。数字万用表要测几十伏电压第一步必须是分压不是靠ADC硬扛。我一般先把量程设计成两档一个直测档和一个分压档。直测档对应0到3.3V适合测电池、传感器输出分压档对应0到100V适合当课程设计里的高压档。分压网络选R190k、R23k分压比是93k比3k也就是31:1。100V输入时ADC引脚上只有100/31≈3.226V留了一点余量又不浪费ADC的12位分辨率。分压网络的等效输出阻抗是R1和R2的并联值约2.9k对STM32F103的ADC来说是可接受的。这里有个容易被忽略的点万用表要求输入阻抗尽量高但分压电阻越大ADC看到信号源阻抗越高采样越不准。真板子上通用做法是分压网络后面加一级电压跟随器用LM358或者OP07把阻抗隔开Proteus仿真里可以不加跟随器直接连前提是分压网络的等效阻抗别超过10k量级。R190k、R23k这个组合等效阻抗2.9k仿真里完全没问题上真板子就会翻车所以别把仿真里的参数直接抄到板子上。顺带说一句Proteus里电阻默认是理想元件没有容差。想模拟真实情况右键电阻在Edit Properties里把Tolerance设为1%仿真过程中就能看到电阻偏差对读数的影响。调代码阶段建议先保持0容差把逻辑跑通了再开容差验证边界不然你会误以为是自己程序写错了。2.2 Proteus里的元件清单和连接要点Proteus里要用到的元件不多一张表就能说清元件取值/型号作用R190k分压网络上臂R23k分压网络下臂RV110k电位器LCD对比度调节LCDLM016L显示电压值MCUSTM32F103C8主控DC源0-100V可调模拟被测电压接线要点分压网络输出接PA0也就是ADC1通道0VREF接3.3VVREF-和VSSA接地LCD用4位模式RS接PB8、E接PB9数据D4-D7接PB0-PB3。注意PB3和PB4在F103上默认被JTAG占用了代码里要先把SWJ关闭才能当普通IO用否则LCD显示会乱。Proteus里的STM32F103C8模型晶振默认是8MHz双击芯片在Program File里加载编译好的hex文件就能运行。加载hex时路径不要有中文和空格否则容易加载失败但不报错仿真界面上看起来就是芯片不跑。这个问题放到后面避坑章里细说先知道这个现象就行。2.3 分压电阻取值的工程边界输入阻抗、功耗与采样电容分压电阻的选择有三个约束输入阻抗、功耗、采样时间。输入阻抗方面万用表理想情况下是无穷大实际要靠分压电阻串联值撑起来。R1R293k比真实万用表的10M低很多但Proteus仿真里PA0后面没有实际的布线寄生电容93k完全够用。功耗方面100V输入时分压网络的功耗约100V乘100V除以93k也就是107mW左右R1上要承受约100mW。仿真里元件不发热如果做真板子R1至少要选1/2W的功率电阻。采样时间方面F103的ADC内部有采样电容外部信号源阻抗越高需要的采样时间越长。用2.9k的等效阻抗ST库里的ADC_SampleTime_55Cycles5就够如果用10M加100k这种组合就得把采样时间调到239.5周期否则读数会偏低。如果你要做三档以上量程常见做法是加一块模拟开关CD4051把几组分压电阻并联用单片机引脚选通。CD4051的导通电阻几十到几百欧会改变分压比所以取电阻值时要把这部分预留掉。仿真阶段建议先不加等代码调通了再把CD4051拖进Proteus验证切换逻辑这样出问题的时候容易定位。3. STM32F103 ADC采样代码从初始化到LCD显示的全流程3.1 ADC初始化引脚模式、触发方式与采样周期先看初始化代码这是整个万用表的起点#include stm32f10x.h void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入不带上拉下拉 GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); }逻辑说明先把PA0设成模拟输入GPIO_Mode_AIN这一步漏了ADC读出来永远是0。ADC用独立模式、单次转换、软件触发不搞扫描因为只采一个通道。右对齐12位数据方便直接当无符号整数用。采样时间55.5周期配合2.9k源阻抗足够。参数说明如果换了分压电阻导致等效阻抗超过10k把ADC_SampleTime_55Cycles5改成ADC_SampleTime_239Cycles5这是F103能设的最长采样时间代价是ADC转换速度掉到几十kHz级别但万用表应用完全无所谓。3.2 采样子程序软件触发加滤波别直接读寄存器直接读ADC寄存器不是不行只是Proteus仿真里可能看不出问题上了真板子就会遇到毛刺。我习惯在这里加滤波uint16_t adc_read_filtered(uint8_t times) { uint32_t sum 0; uint16_t min 0xFFFF, max 0, v; for (uint8_t i 0; i times; i) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); v ADC_GetConversionValue(ADC1); sum v; if (v min) min v; if (v max) max v; } return (uint16_t)((sum - min - max) / (times - 2)); }逻辑软件启动一次转换等EOC标志置位后取结果。循环times次累加的同时记录最大最小值最后用总和减掉极值再求平均相当于把最大毛刺和最小毛刺都扔掉。参数times取8到16都行。取太少滤波效果不明显取太多按键响应会变肉。万用表显示更新频率一般在每秒3到5次所以times等于12是个比较稳的中值。Proteus的ADC模型本身没有真实噪声这个滤波函数在仿真里收益不明显但保证了代码能直接移植到真板子上。3.3 电压换算与量程切换滞回区间和分压系数ADC原始值要还原成真实电压靠的是下面这段逻辑#define VREF 3.30f #define DIV_RATIO 31.0f float raw_to_voltage(uint16_t raw, uint8_t range) { float vadc (float)raw * VREF / 4095.0f; if (range RANGE_DIRECT) { return vadc; } else { return vadc * DIV_RATIO; } }逻辑ADC值除以4095得到相对满量程的比例乘参考电压3.3V还原成ADC引脚电压。直测档直接返回分压档乘分压比31。参数说明VREF这里写死3.3V在Proteus里电源就是精确3.3V没问题上真板子后必须改成ADC实际测到的参考电压。常见做法是用万用表量一下VREF引脚把量到的值填进这个宏。不改的话板上参考电压如果是3.28V每个档位的读数会整体偏移0.6%左右这种误差很隐蔽。自动量程切换要加滞回区间不然临界点会反复跳档if (range RANGE_DIRECT) { if (vin 3.0f) { range RANGE_DIV; delay_ms(20); // 等分压网络稳定 } } else { if (vin * DIV_RATIO 2.8f) { range RANGE_DIRECT; delay_ms(20); } }逻辑直测档读数超过3.0V才切到分压档分压档换算回输入电压低于2.8V才切回直测档。中间0.2V就是滞回区防止输入电压在门限附近抖动时档位来回跳。3.4 LCD显示与小数拆解浮点数不能直接丢给LCD很多初学者喜欢用sprintf直接格式化浮点在Keil MDK默认配置下sprintf的%f支持会被MicroLIB裁掉编译不过或者显示乱码。我一般手动拆整数和小数char buf[16]; uint16_t v_int (uint16_t)vin; uint16_t v_dec (uint16_t)(((vin - v_int) 0.0005f) * 1000.0f); sprintf(buf, U%d.%03dV, v_int, v_dec); lcd_puts(buf);逻辑先把浮点电压的整数部分和小数部分拆开sprintf只输出整数避免在单片机上用%f。加0.0005f是四舍五入否则浮点数的二进制尾巴会在临界点把显示值拉偏。v_dec乘1000表示保留三位小数匹配mV分辨率。参数如果LCD显示空间不够改成乘100保留两位也行。但量程越高显示位数越重要100V档一位小数就够2V档没有三位小数你会被误差搞得没法判断代码到底对不对。LCD初始化时序也要提一句4位模式启动时先拉低RS、E、D4-D7延时40ms以上再发0x30三次进入8位模式然后再切4位模式。Proteus仿真速度很快延时写太短LM016L会间歇性不工作表现就是某一个字符缺笔画。4. 避坑实录Proteus仿真里最容易翻车的五个点注意下面这几条按实际排错出现频率排序第一条最常见。每条都按现象、原因、解决的顺序写遇到问题可以对号入座。4.1 现象仿真能跑但读数一直是0.000现象程序下载进去LCD显示正常可不管怎么调输入电压读数都是0.000切换档位也没变化。原因十有八九是GPIO没有被配置成模拟输入。PA0默认状态可能是浮空输入或推挽输出ADC外设读到的是引脚驱动电路的状态而不是外部电压。另一个高频原因是VREF没接3.3VADC没有参考电压转换结果没有意义。解决把PA0的GPIO_Mode设成GPIO_Mode_AINProteus里把VREF接到3.3V网络VREF-接地。改完这两处用直流电压源从PA0给一个精确的1V看读数是否在1.0V附近。如果还不对把滤波函数临时改成单次采样直接打印原始值先确认ADC寄存器有变化再继续往下查。4.2 现象分压电阻一大读数偏低且不稳定现象分压网络用10M配100k时仿真里测5V显示4.2V而且末位小数一直跳。原因分压网络等效输出阻抗约99k太大了。F103内部ADC采样电容需要从外部信号源吸取电荷充电源阻抗高充电时间不够每次采样电容都没充满读数就偏低。Proteus的ADC模型保留了这部分特性所以仿真里就能看出问题。解决把分压电阻整体降到90k配3k这种级别等效阻抗在3k左右问题消失。如果为了模拟真实万用表的高输入阻抗必须用大电阻那就得在分压网络和ADC之间加电压跟随器。用LM358就行Proteus里给LM358接3.3V电源输出直连PA0仿真立即恢复正常。4.3 现象换挡瞬间读数跳到满量程现象按下换挡键LCD先显示一个300多伏的离谱值过几百毫秒才恢复正常。原因切换分压支路的瞬间ADC引脚上电压悬空或跳变采样到了不可信状态。软件换挡时如果立刻做下一次转换刚好落在毛刺上。仿真里最常见的是分压比和量程标志不同步量程标志已经切到新档位分压网络还在旧状态甚至悬空。解决换挡后做两件事。第一软件上延时10到20ms等分压网络的RC稳定。第二连续采3次要求三次结果之间的偏差小于1%才更新显示否则保持旧值。这个不信任瞬间值的逻辑对上真板子同样有效。4.4 现象Proteus加载hex后芯片不运行现象Program File里选好hex点击运行芯片周围的引脚电平没有变化LCD没有任何反应。原因最常见的是hex路径含中文或空格Proteus加载失败但没弹窗提示。其次是芯片的时钟配置不对程序卡在SystemInit的循环里。解决把所有工程文件挪到纯英文路径路径里不要有中文和空格。双击STM32F103C8把Clock Frequency设为8MHz跟工程里HSE_VALUE一致。芯片型号优先用stm32f103c8这个型号的Proteus模型外设支持最全。加载成功后芯片上会显示程序计数器在跑引脚电平能肉眼看到变化。4.5 现象LCD1602显示方块或只有背光现象LCD上电后只有一排黑色方块或者背光亮了但一个字都没有。原因Proteus的LM016L模型第3脚VEE不接对比度调节直接接地或悬空时对比度可能压死字符显示不出来。很多人习惯了实物上的固定偏压在仿真里就忘了调对比度。解决在VEE和地之间接一个10k电位器中间抽头接VEE仿真运行后调电位器直到字符清晰。代码初始化部分上电后先延时50ms再发LCD初始化指令序列。Proteus仿真速度极快上电延时不够LM016L内部状态机还没准备好初始化指令就丢了。这两个坑同时踩的时候现象完全一样排查顺序是先调对比度再查初始化时序。5. 校准与精度验证用仿真数据反过来找代码的问题5.1 基准电压校准法给ADC一个已知的标准答案仿真里放一个高精度直流电压源在直测档分别给0.5V、1.0V、2.0V、3.0V四组输入记录ADC原始值adc_raw [622, 1244, 2489, 3736] known_voltage [0.5, 1.0, 2.0, 3.0] slope (adc_raw[-1] - adc_raw[0]) / (known_voltage[-1] - known_voltage[0]) print(f每伏ADC增量 {slope:.2f}, 理论值 {4095 / 3.3:.2f})逻辑用最小值和最大值两个点求出斜率看这个斜率是否接近1240.9。偏差在±5以内说明链路没问题偏差大了先查VREF是不是精确3.3V再查分压电阻是否带容差。参数说明两个点就能算出增益误差取四个点是为了顺带看线性度。四个点如果不在一条直线上问题不是增益误差而是采样时间不够导致的大信号压缩。Proteus里没有温度漂移如果看到非线性直接怀疑采样电容没充满。校准之外还要分清三种误差量化误差在12位分辨率下约0.8mV以内这是ADC天生自带的增益误差可以通过校准系数修正非线性误差在仿真里几乎为零。真实万用表之所以贵主要就是把这三种误差都压住了Proteus仿真能帮你把增益误差调出来但非线性误差不用花精力去追。5.2 两档交叉验证同一电压在两个档位下读数必须一致分压档和直测档的覆盖范围有重叠区。给一个2V输入直测档显示2.003V分压档显示1.987V这就暴露了16mV的矛盾。矛盾来自DIV_RATIO这个常数写得不够准或者电阻容差。验证方法是输入一个稳定电压分别读两档算出实际分压比。比如分压档理论输出应该是直测档的1/31实测是1/30.4那就把DIV_RATIO从31改成30.4。真实万用表校准也是这个逻辑只不过用的是精密电阻网络修正量小。仿真里改常数就行不用换电阻。校准常数不要散落在主程序各个角落。我习惯把所有scale和offset集中放在一个头文件里#define VREF_CAL 3.30f #define DIV_RATIO_CAL 31.0f #define DISPLAY_DELAY_MS 200参数说明这三个宏分别是参考电压、分压比、显示刷新间隔。校准的时候只动头文件不动业务代码。代码越短返工越少。5.3 自动量程门限验证滞回区间不是拍出来的如果代码里做了自动量程切换必须验证切换门限。用可调直流源从0开始慢慢升压记录从直测档跳到分压档时的电压值再慢慢降压记录跳回来的电压值。这两个值的差就是滞回区间。我一般设成升到3.0V切高档降到2.8V切回低档中间0.2V是滞回区。如果没有滞回输入电压在3.0V附近抖动时档位会来回跳显示值也跟着跳。验证时注意Proteus里的直流源调整要缓慢每次只改0.05V别大跨度跳否则捕捉不到真实切换点。这一步花十分钟能省掉后面半天排查显示跳变的功夫。6. 进阶玩法把Proteus仿真的数据导出来做上位机显示6.1 用虚拟串口把测量值送出去Proteus里加一个COMPIM组件TX和RX接STM32的PA9、PA10对应USART1。再用虚拟串口工具创建一对互联串口上位机程序打开其中一个就能收到数据。这样不占额外硬件调显示链路非常方便。串口发送用固定帧格式接收端好解析uint8_t frame[8] {0xA5, 0x5A, 0x04, range_code, (uint8_t)(v_int 8), (uint8_t)v_int, (uint8_t)(v_dec 0xFF), 0x00}; uint8_t sum 0; for (uint8_t i 0; i 7; i) sum frame[i]; frame[7] sum; for (uint8_t i 0; i 8; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, frame[i]); }逻辑帧头固定0xA5 0x5A第三个字节是长度第四字节是量程后面两字节整数电压、一字节小数部分最后一字节是前七字节累加校验。接收端校验失败直接丢帧。参数量程和电压共用一个帧上位机不用猜当前是哪个档位。波特率设9600数据位8、停止位1、无校验跟Proteus里COMPIM的设置保持一致。6.2 上位机Python解码示例import serial ser serial.Serial(COM3, 9600, timeout1) while True: buf ser.read(8) if len(buf) 8 or buf[0] ! 0xA5 or buf[1] ! 0x5A: continue if sum(buf[:7]) 0xFF ! buf[7]: continue voltage int.from_bytes(buf[4:6], big) buf[6] / 1000.0 print(frange{buf[3]}, voltage{voltage:.3f}V)逻辑校验通过才解析帧校验失败说明波特率或接线错误。Proteus仿真里9600波特率很稳实际调试时先把串口工具的参数统一成8N1。6.3 新手的验证顺序我个人的验证习惯是先在Proteus里用固定直流源把直测档调好确认LCD读数正确再开分压档最后才接串口。串口会把所有问题一次性混在一起跳过了前两步出了错很难定位到底是ADC的问题还是通信的问题。从那以后我每次搭Proteus仿真都会强制走一遍这个顺序查VREF查GPIO模式查分压比。这三个地方占掉了八成以上翻车现场。希望帮到你。本文还有配套的精品资源点击获取