☰
STM32F103数字万用表Proteus仿真方案:从原理到跑通
2026/10/17 17:57:21 网站建设 项目流程

简介:一份基于STM32F103的数字万用表Proteus仿真完整资料包,面向嵌入式系统学习者与电子设计爱好者,可用于在虚拟环境中理解数字万用表的硬件电路、STM32固件开发流程及Proteus联合仿真方法。资源共1020个文件,压缩包约14.29MB,以C/H源码、Proteus工程文件(pdsprj/pdsbak)、Keil工程文件(uvprojx/uvoptx)为核心,同时包含编译生成的hex/axf固件、库文件、链接脚本及大量中间构件,便于检查从源码到固件的完整构建链条。已有3088人学习下载。包内完整保留ADC模拟采集、LCD显示驱动、按键交互、测量模式切换与数值处理等关键模块,并附带math库和启动文件,可直接在Proteus中加载仿真,也可结合Keil分模块阅读源码,适合自学入门、课程设计及电子竞赛备赛时参考。项目文件夹按功能区分,目录层次清晰,便于检索与二次开发。通过这套工程,读者能快速掌握STM32外设初始化、HAL库调用、模拟信号处理与虚拟示波器调试等实用技能。

1. 基于STM32F103的数字万用表:一套能跑通的Proteus仿真方案

做单片机开发最头疼的事,可能就是“代码写了一堆,手里却没有一块能点亮的板子”。尤其像数字万用表这种项目,涉及ADC采集、量程切换、LCD显示、分压电阻网络,任何一个环节出错,放在实物上都是拿万用表量来量去排查半天。而Proteus仿真恰恰把这层物理屏障拆掉了——你可以在纯软件环境里把STM32F103跑起来,接上虚拟的电阻网络和LCD,测电压、测电流、测电阻,逻辑对了再往硬件上搬。

这套基于STM32F103的数字万用表仿真项目,给我的第一印象是“麻雀虽小,五脏俱全”。它没有用高端的专用测量芯片,而是靠STM32内置ADC加上模拟前端的分压网络完成多量程测量,显示部分走LCD1602。适合三类读者:一是正在做课程设计、手里只有Proteus没有开发板的学生;二是想把ADC多通道采集和量程自动切换逻辑一次搞清楚的人;三是准备把仿真项目改成实物、但想先验证算法正确性的开发者。下面我按自己拆这个项目的顺序,把原理、代码、仿真跑通和改造方向一条条讲透。

2. 整体架构拆解:一个测量系统是怎么在仿真里分层的

2.1 系统模块划分和信号流向

整个系统如果画成框图,大概可以分成四个部分:模拟前端(分压网络和量程选择)、STM32F103主控(ADC采集和逻辑处理)、显示输出(LCD1602)、电源和基准部分。我刚拿到这个项目时没急着看代码,先把Proteus工程里的原理图打开,对照MCU的引脚把信号追了一遍,那感觉比直接读代码清晰得多。

模拟前端的设计思路是典型的分压电阻网络。比如要测0-200V的直流电压,直接把200V灌进STM32的ADC引脚肯定烧芯片,所以先用大阻值电阻分压,把电压按比例降到3.3V以内。量程切换靠的是改变分压比——这项目里用的是手动切换挡位的方式,每个挡位对应一条分压支路,模拟了实物万用表旋转开关的逻辑。这种设计在Proteus里实现特别直观,就是一个多路开关(可以用按键模拟)去选通不同的电阻组合。电流测量则通过采样电阻把电流转换成电压,再走同一条ADC通道。

STM32F103这边承担的任务比较清晰。它内部有3个ADC,12位分辨率,16个外部通道,对这个项目来说绰绰余。测量电压时,ADC采集模拟前端的实时电压,然后根据当前挡位的分压比反算出实际输入电压;测量电阻时,内部产生一个恒流源或者用上拉电阻分压,测出待测电阻两端的压降再换算。项目用的HAL库,初始化逻辑写在main.c里,测量核心放在ADC中断回调中完成,整个代码结构是“主循环轮询模式”,没有跑RTOS,对仿真来说足够轻量。

2.2 关键器件选型和参数依据

要说这个项目里最值得琢磨的,其实是分压电阻网络的阻值选配。我拆解的时候逐个算了一遍,以200V挡为例:如果输入200V,要降到3.0V以内(给ADC留一点裕量),分压比要大于67:1。常规做法是串联一个1M和15K电阻,200V分压后大概是2.94V,这是一个合理的工程取值。如果读者想改成别的量程,只要保证“最大输入电压 / 分压比 = 3.3V以内的电压”就行,这个公式是整套量程设计的地基。

LCD部分选的是LCD1602(16列2行字符屏),数据引脚直接挂在STM32的GPIO上,走8位或4位并口模式。这个项目用了4线模式,只占PB8-PB11共4个数据脚加上RS、RW、EN三个控制脚,比8线模式省引脚。Proteus里有个坑是LCD的仿真模型对时序要求比较严,初始化时序不标准就会显示乱码或完全不亮,我在第5章会单独展开讲。电源部分直接由Proteus的虚拟电源供给,5V给LCD,3.3V给MCU和模拟前端,要注意模拟前端的参考地必须和MCU共地,否则ADC读出来的值会飘。

2.3 目录结构和工程配置说明

我拿到这个项目的压缩包后,第一件事是看目录结构。一个规范的Proteus + STM32工程,通常会分几层:Hardware目录放的是Proteus的.pdsprj仿真文件和原理图文档;Software目录下是STM32的Keil工程,里面又拆出Core(启动文件和主函数)、Hardware(外设驱动,比如LCD、按键)、System(延时、串口等工具函数)。如果你的源文件里没有分层也不要紧,自己按这个习惯重组织一遍,后面加功能会顺手很多。

Keil工程编译后会生成一个.hex文件,Proteus需要加载这个hex文件才能在虚拟MCU里运行。具体的配置步骤是:双击原理图里的STM32F103元件,在弹出的属性窗口里找到Program File选项,选中编译生成的hex文件,再把晶振频率设成8MHz(如果用的是外部晶振),然后点运行。有个关键的细节:Proteus里STM32的电源引脚默认连接到VCC/VSS网络,不需要像51单片机那样手动接电源,但VDDA和VSSA这些模拟电源引脚要在属性里确认没有浮空,否则ADC采样会出问题。

提示:如果你改过代码重新编译,记得先停止Proteus仿真,再重新加载hex文件。Proteus不会自动检测文件更新,直接点运行会执行旧程序,这是新手最容易踩的一个坑。

3. 核心代码逐段拆解:从ADC采集到LCD显示

3.1 ADC多通道采集和DMA传输配置

数字万用表的核心是“测得准”,而测得准的基础是ADC采样。这个项目里,STM32F103的ADC工作在扫描模式,同时采样电压通道和电流通道。我先贴一段初始化代码,这是我在HAL库版本下写的ADC配置:

void MX_ADC1_Init(void) { ADC_ChannelConfTypeTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ENABLE; // 扫描模式,多通道依次转换 hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换,配合DMA使用 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发,不依赖定时器 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐,方便直接计算 hadc1.Init.NbrOfConversion = 2; // 本项目中转换2个通道 if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } // 通道0:电压测量输入 sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); // 通道1:电流采样输入 sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }

这段配置里有几个参数值得展开说。ScanConvMode开启后,ADC会按Rank序号依次转换通道0和通道1,两个通道共用同一个ADC外设,不需要切换通道的额外代码。ContinuousConvMode配合DMA是高效方案——ADC转换完一个通道就触发一次DMA搬运,所有通道转换完成后会进入一次DMA传输完成中断,这样CPU不用傻等采样结果。采样时间我选了55.5个时钟周期,这是精度和速度之间的折中。如果采样时间太短(比如1.5个周期),内部采样电容还没充放电完成就锁存结果,读数会偏低;如果太长,在仿真里会拖慢整体节奏。

然后是DMA这块。ADC1的DMA请求映射到DMA1的Channel1,需要把两个通道的采样结果存进一个长度为2的数组,代码这样写:

__IO uint16_t adc_buf[2] = {0}; void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_adc1.Instance = DMA1_Channel1; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不自增,始终读ADC1->DR hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 内存地址自增,依次填充adc_buf[0]和[1] hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式,不断刷新采样结果 HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); }

这里要注意DMA_CIRCULAR循环模式的选择。如果选普通模式,DMA搬运完2个数据就停了,后续ADC数据不会再更新,必须手动重新启动DMA。循环模式则是一劳永逸——DMA不停的把新的采样结果覆盖到数组里,主循环任何时候去读adc_buf拿到的都是最新的ADC值。内存地址自增配合外设地址不自增这个配置,是“一个外设数据源往两个内存位置搬运”的关键,少了MEMINC_ENABLE,两个通道的结果会互相覆盖。

3.2 量程切换和电压换算的完整算法

ADC采到的原始值是0-4095的整数,要变成人能读的电压值,还需要一步换算。比如参考电压3.3V,ADC读2048,那对应的就是1.65V。这个项目里分了三挡量程:0-2V用直通通道(不经过分压),0-20V经过11:1分压,0-200V经过101:1分压。挡位由按键控制,每按一次切换一个量程。量程不同,换算系数就不同,代码如下:

typedef enum { RANGE_2V = 0, RANGE_20V, RANGE_200V } VoltRange_TypeDef; VoltRange_TypeDef current_range = RANGE_2V; // 分压比参数:直通/11:1/101:1 const float voltage_divider[3] = {1.0f, 11.0f, 101.0f}; float get_measured_voltage(uint16_t adc_value) { float voltage_at_pin = (float)adc_value * 3.3f / 4095.0f; float real_voltage = voltage_at_pin * voltage_divider[current_range]; return real_voltage; } void switch_range(void) { if (current_range == RANGE_200V) current_range = RANGE_2V; else current_range++; // 切换挡位后清空DMA缓存,避免读到旧挡位的残留数据 adc_buf[0] = 0; adc_buf[1] = 0; }

voltage_divider数组的三个值直接对应硬件分压网络。以20V挡为例,输入20V经过11:1分压后,ADC引脚上大概1.82V,ADC读数约2260,20V除以1.82V再乘2260个码值,结果还是20V左右。这里有个容易被忽略的地方:分压电阻有精度误差,Proteus里如果选理想电阻(默认1%),算出来没问题,但实物上用5%的贴片电阻,你必须用精密电阻或者在软件里做校准偏移。校准的思路也很简单——输入一个已知电压,读ADC值,反算出实际分压比,把voltage_divider数组里的值替换成校准后的值。

量程切换的按键消抖也是这类项目的标配问题。机械按键在按下瞬间会产生几十毫秒的电平抖动,不加处理的话一次按下可能被当成三次触发。常见做法是检测到按键变低后延时20ms再确认,确认有效后才执行切换。代码可以放到定时器中断里做状态机,也可以用HAL_Delay简单实现,后者在仿真里完全够用。

3.3 LCD显示驱动:如何把浮点数和单位拼成可读字符

ADC换算出来的电压值是一个浮点数,要显示在LCD1602上必须先转成字符串。LCD1602本身只接收ASCII字符,所以需要用到sprintf这个格式化函数。不过STM32的嵌入式中,sprintf这个函数可能占用较多代码空间,而且默认不带浮点支持(Keil里默认用MicroLIB的话,浮点格式化会被裁剪)。我的处理方式是自己写一个浮点转字符串函数,避免这个坑。

void format_voltage(float voltage, char *out_str) { int int_part = (int)voltage; int dec_part = (int)((voltage - int_part) * 100 + 0.5f); // 保留两位小数,+0.5做四舍五入 if (dec_part >= 100) // 进位场景处理,比如1.999变成2.00 { int_part += 1; dec_part = 0; } out_str[0] = (int_part / 10) + '0'; out_str[1] = (int_part % 10) + '0'; out_str[2] = '.'; out_str[3] = (dec_part / 10) + '0'; out_str[4] = (dec_part % 10) + '0'; out_str[5] = 'V'; out_str[6] = '\0'; }

这段代码的本质是把浮点数分解成整数部分和小数部分,逐位转成字符。+0.5f的偏移是四舍五入的关键,不然1.23会显示成1.22。为什么要手写而不直接用sprintf?因为Keil工程里如果不勾选MicroLIB,sprintf的浮点版本体积极大——一个STM32F103只有64KB Flash,一个浮点格式化函数就能吃掉四五KB,这对小型项目是很大的负担。手写版本只有十几行代码,而且逻辑可控。

LCD1602的写入时序是另一个考点。它需要先拉低RS(选择指令/数据模式),然后往数据引脚写值,最后产生一个使能脉冲。以下是一段标准的4线模式写字符代码:

void lcd_write_char(unsigned char c) { PBout(4) = 1; // RS = 1,数据模式 PBout(5) = 0; // RW = 0,写模式 // 写入高4位 GPIOC->ODR = (GPIOC->ODR & 0xFF0F) | ((c >> 4) & 0x0F) << 4; PBout(6) = 1; // EN拉高 delay_us(40); PBout(6) = 0; // EN拉低,数据被锁存 // 写入低4位 GPIOC->ODR = (GPIOC->ODR & 0xFF0F) | (c & 0x0F) << 4; PBout(6) = 1; delay_us(40); PBout(6) = 0; delay_us(50); }

4线模式和8线模式的最大差别在于:8线模式一个操作周期内送上完整8位数据,4线模式要分两次送上高4位和低4位。初始化的时候,LCD控制器需要看到特定序列才能进入4线模式——先按8位模式发送0x30两次,再发送0x20切换成4线模式。如果初始化序列顺序颠倒,LCD会一直处于8线模式,数据错位,显示出一堆乱码。Proteus的LCD仿真模型对这段时序格外敏感,我用血泪经验告诉大家:初始化函数不能省略延迟,每条指令之间至少等40微秒。

4. 在Proteus里跑通整个仿真:从新建工程到读取测量值

4.1 元件选型:Proteus库里的关键词搜索

要把这个项目从代码变成能跑的仿真,第一步是在Proteus里画出电路原理图。打开Proteus 8 Professional,新建工程后在元件模式里输入关键词搜索器件。STM32F103C8直接在库里的搜索词是STM32F103C8,LCD1602搜索LM016L,这是Proteus自带的兼容型号。分压电阻可以用RES,电位器用POT,开关用BUTTON,虚拟电压表用DC VOLTMETER。

很多人在这里会翻车:Proteus里没有STM32F103C8这个型号,只有STM32F103R6或STM32F103V6。遇到这种情况,选STM32F103R6也完全没问题,它们的引脚定义和F103C8高度兼容,只是Flash和RAM容量有差异。在仿真层面,代码编译成hex后跑的是指令集,跟具体型号的存储容量关系不大,除非你的代码超过了型号的Flash限制。我在Proteus里验证这个项目时用的就是R6型号,运行正常。

4.2 原理图连线要点和电源网络

连线是整个仿真里最拉低效率的环节。我建议先把STM32的引脚分类:电源引脚(VDDA、VSSA等)、ADC输入引脚(PA0、PA1)、控制引脚(LCD的RS/RW/EN、按键GPIO)、LCD数据引脚(PC4-PC7如果按上面代码的端口来)。连线时按照“信号流从左到右”的布局,模拟前端放左边,MCU放中间,LCD放右边,这样信号方向一目了然,排查问题的时候不用来回拖图。

电源网络要特别交代。Proteus里默认的VCC是5V,STM32F103的供电范围是2.0-3.6V,如果你直接把VCC接到STM32的电源引脚,会超压。这个项目里需要建立两个电源网络:把VCC设成5V给LCD供电,另外建一个3.3V网络给MCU供电。在Proteus里,你可以放置一个POWER端子,双击它修改电压值为3.3V,然后所有MCU的电源引脚都往这个端子上连。ADC的参考电压也来自3.3V网络,这保证了采样比例和计算一致。

分压电阻网络这里,我建议用单个多引脚拨码开关(DIPSWC)来模拟挡位切换,比用多个独立按键更直观。每个挡位对应一组电阻,拨码开关的输出接ADC引脚。仿真的时候切换挡位,MCU读到不同电位,LCD显示的数值会跟着变化。这里正好能验证前面说的量程切换逻辑:2V挡直通时,ADC输入直接是待测电压;切到20V挡,同一个输入源会被分压11倍,LCD上显示的数值不变(因为换算系数变了),这才是正确行为。

4.3 编译烧录和在线调试技巧

代码在Keil里编译通过后,Output目录下会生成.hex文件。回到Proteus,双击MCU元件,在Program File一栏选这个hex文件。然后设置晶振频率为8MHz——Proteus里STM32的默认内部RC是8MHz,如果你在代码里用了外部晶振启动时序,必须把这里的频率改成你代码里配置的值,否则串口波特率、定时器延时全都会偏掉。还有一个容易被忽略的细节:MCU元件的Advanced Properties里有Clock Divider选项,默认值是1,如果你发现仿真跑得特别慢,把它调大可以加速,但会影响定时器的真实时间计算,量测定时类功能时不建议改。

启动仿真后,如果LCD一直黑屏,先别急,按这个顺序排查:看MCU的电源引脚是否有3.3V、看hex文件是否正确加载、看晶振设置是否生效、最后用虚拟示波器(OSCILLOSCOPE)挂到RS引脚上看看有没有高低电平变化。用示波器看信号是我在Proteus调试时最常用的手段,比肉眼盯着屏幕猜高效得多。

提示:Proteus的仿真运行速度依赖MCU的主频设置,如果工程里所有延时都正常但LCD字符刷新慢,把MCU频率从8MHz改成72MHz(外部晶振8MHz,锁相环倍频到72MHz的配置)能明显改善。STM32F103的最大主频是72MHz,这是它的标准工作频率。

4.4 验证测量精度:虚拟信号发生器

仿真跑通后,怎么确认测量值是准确的?Proteus左侧工具栏里有VIRTUAL INSTRUMENTS,里面有一个信号发生器和一个虚拟电压表。把电压表的输出稳定在3.5V(注意这已经超出了3.3V ADC参考电压,所以必须经过分压挡位),比如你先打到200V挡,理论上分压后ADC引脚电压是约34.65mV,非常小,ADC读到的码值精度不够,读出的电压可能偏差较大。这种情况在仿真里很常见,解决方法是内部再加一级前置放大,或者在200V挡位下换用更大分压比的电阻组合,让ADC输入始终在1-3V的区间工作,码值分辨率就能保证毫伏级别的精度。

我在验证这个项目时用了一个更稳妥的办法:测试多个电压点,绘制误差曲线。3V、6V、12V、24V分别测一遍,记录下来再对比理论值,你会发现误差呈线性关系——因为主要误差来源就是分压电阻的比例误差,这种系统性误差可以通过软件校准消除。在代码里维护一个校准系数数组,每个量程对应一个,测量结果乘上这个系数即可。

5. 避坑指南:我在这个项目上踩过的五个典型问题

5.1 现象:LCD1602显示乱码或完全空白

原因有两个层级。第一层是初始化时序不满足LCD控制器的要求。LCD1602上电后需要等一段时间(约40ms)才能接受指令,如果初始化函数在MCU复位后立即执行,控制器可能还没准备好。第二层是4线模式下的数据引脚接错位。比如代码里约定高4位接PC4-PC7,但连线时接到了PC0-PC3,显示出来自然全是错乱的符号。

解决方法是先用8线模式把简单的字符显示出来验证通路,再改成4线模式。我在调试时还会在初始化函数里加一个长延时,确保上电稳定后再发指令。如果还是没有输出,用示波器挂到EN引脚,看是否产生了下降沿脉冲。

5.2 现象:ADC采样值一直满码或空码

满码(4095)说明ADC输入电压超过了参考电压或引脚悬空,空码(0)说明引脚被拉到地或始终没有信号输入。但我在仿真里遇到过一种更隐蔽的情况:ADC引脚在原理图里忘连了,Proteus会默认把它们接一个不确定的电位,ADC读数会无规律跳动,而不是稳定的0或4095。

解决方法是把所有ADC通道的引脚都显式连接到测试点,然后在这个测试点上挂一个已知电压源,确认读数和理论值一致。这个排查步骤消除了一大类“代码没问题但读数不对”的疑难杂症。

5.3 现象:仿真运行后点击什么都没反应

有一种常见故障是代码死在了HAL_Init()之前或while(1)之前的某个初始化函数里,最常见的元凶是HAL_Delay()函数依赖systick中断,而Proteus虚拟环境的中断配置和真实芯片有差异。另一种情况是代码里打开了看门狗,仿真时主循环跑得太慢或停止在断点,看门狗超时把MCU复位了。

解决方法是先精简代码,把初始化部分去掉,只留一个空的while(1)循环里翻转GPIO,验证最小系统能跑。然后一步步加回外设初始化,每加一个就运行一次,直到复现问题,这一步定位到具体模块。

5.4 现象:切换挡位后读数跳变不规律

量程切换时按键消抖不够彻底,一次按下被识别成多次触发。另一个原因是切换后ADC缓存里存的还是上一个挡位的数据,新挡位的采样还没来得及覆盖,界面先用旧数据算了一遍,显示出错误的中间值。

解决方法是软件层面处理:在切换动作执行后立即清除adc_buf数组并等待至少一个完整的ADC转换周期(读取到非零新值)再继续显示。我在代码里用一个range_changed标志位,主循环检测到标志置位时就执行一次清缓存和延时等待流程,然后再恢复正常显示。

5.5 现象:LCD能显示但数值在低位跳动

这是仿真精度和理想环境带来的“假象”。Proteus里的电压源是有内部噪声的,如果噪声有几毫伏,反映在ADC上就是几个码值的波动,对应到LCD显示的个位和小数点后两位会跳动。真实硬件也有类似问题,但程度更小。

解决方法不是消除噪声,而是做软件滤波。简单做法是连续采样8次取平均,或者用一阶低通滤波:new_value = old_value * 0.8 + current_sample * 0.2,效果立竿见影。但是注意,如果你追求响应速度,滤波会带来延迟——调滤波系数就是一个精度和响应速度的博弈。

6. 二次开发:把仿真项目改造成你需要的功能

6.1 功能扩展:从手动挡变成自动量程

原项目用按键手动切换量程,改成自动量程的思路其实很直观。测量开始时先用最大量程(200V挡)采样,如果ADC读到的电压码值低于满量程的十分之一(比如低于400码),说明输入电压很小,此时切到更小的量程,然后继续读值判断。核心是加一个自动挡位选择的循环,直到找到一个ADC读数在合理区间的量程为止。

void auto_range_voltage(void) { uint16_t raw_value; // 先切到200V挡,测大信号 set_range(RANGE_200V); delay(10); raw_value = read_adc_average(ADC_CH0, 8); if (raw_value < 400) // 200V挡测得的值太小,降挡位 { set_range(RANGE_20V); delay(10); raw_value = read_adc_average(ADC_CH0, 8); if (raw_value < 400) // 20V挡还是小,降到2V挡 { set_range(RANGE_2V); delay(10); raw_value = read_adc_average(ADC_CH0, 8); } } }

自动量程要注意的是“量程振荡”问题。如果输入电压恰好卡在临界点附近,系统会在两个量程之间反复切换,LCD数值来回跳。常规做法加入滞回比较——进入低挡位的条件比退出低挡位的条件更苛刻。比如20V挡的读数大于18V时才切到200V挡,而200V挡读数小于1.2V时才切回20V挡,中间这个滞回区间就是防震荡的缓冲带。

6.2 测量维度扩展:加入二极管通断检测和电容测量

数字万用表一个实用功能是二极管检测。原理很简单:STM32的GPIO配置成推挽输出,输出3.3V经过限流电阻接到待测二极管正极,二极管负极接地。如果二极管导通,其正向压降约0.6-0.7V(硅管),这个压降经ADC测量后就能区分导通还是短路。通断检测则是一个连续采集模式——每秒刷新几次,一旦电阻低于阈值(比如20欧姆)就触发蜂鸣器响。

电容测量会复杂一些,常见低成本方案是RC充电时间法。把一个已知电阻和待测电容串联,GPIO输出高电平开始充电,ADC监测电压上升到阈值的时间,根据时间常数计算电容值。这个功能需要定时器的输入捕获配合,工程量大,适合进阶改。我在自己的项目X里就这么加过,还顺手把测得的电容值显示到LCD第二行,第一行显示电压,互不干扰。

6.3 显示升级:从LCD1602换成OLED并增加波形显示

LCD1602只能显示字符,如果你想看电压变化波形,可以换成I2C接口的OLED(比如SSD1306驱动的128x64屏)。STM32F103的I2C外设可以直接驱动,Proteus仿真库里也有OLDE元件支持。OLED上绘制波形的思路是:维护一个环形缓冲区,每采样一次把电压值换算成屏幕高度的坐标点,然后用画点函数连起来。

我在改造时踩过一个坑:Proteus的虚拟I2C时序和真实OLED芯片的时序有细微差别,如果代码完全按数据手册的延时来写,仿真能跑但实物不亮。后来我改成基于状态机的I2C驱动(而不是用HAL的阻塞式传输),两者兼容性最好。OLED显示的价值在于,它让这个项目从“只能读数”变成了“能看出趋势”,比如测量电容充电过程,电压爬升曲线一目了然。

6.4 数据输出:增加串口把测量值发给PC

如果你不满足于只在仿真里看LCD,可以通过USART把测量数据实时发到PC端的串口助手。STM32F103的USART1挂载在PA9/PA10上,USB转TTL模块连接后,波特率设115200,每秒发送一次格式化数据。Proteus里的虚拟串口元件可以和PC的物理串口桥接,这样和真实设备调试的流程完全一致。

串口发送的代码核心就是重定向fputc函数,然后把printf的输出引到USART上。但这个项目里我建议别直接用printf浮点格式化,原因前面提过——代码体积和效率不划算。可以复用自己写的format_voltage函数,把字符串组好之后调HAL_UART_Transmit发送。我在最后的习惯是把测量值、量程挡位、ADC原始码值一起发出去,这样PC端定位问题更快,不用在仿真里一个个猜。

从做这个项目到现在,我每次搭新的Proteus仿真都会强制走一遍同样的流程:先在原理图上核对电源和地,再确认MCU时钟配置,然后才写外设驱动代码;跳变、乱码、满码这几个老问题,全部是用这套顺序排查出来的。自动化量程、OLED波形、串口输出这些都是项目跑通之后顺手加的扩展,但它们反过来又逼我把ADC采样、DMA搬运、量程切换这些基础逻辑重新看了一遍——所谓二次开发,其实就是用新需求检验老代码的边界。希望这次的拆解能让你在Proteus里把这块数字万用表顺利点亮,少走点我当时绕过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询