简介:本资源是一套基于Proteus与Keil MDK联合仿真的STM32F103R6数字电压表完整工程,面向嵌入式初学者及课程设计实践者,解决两路模拟电压采集、AD转换与数码管实时显示的核心教学需求。压缩包含599个文件,以337个C源码和106个头文件构成主程序逻辑,辅以ICF链接脚本、HEX可执行文件、AXF调试文件及Proteus电路图(.pdsprj),完整覆盖从AD驱动配置、数码管动态扫描到串口调试输出的全流程实现。资源大小为17.34MB,结构清晰,含标准HAL库工程(.ioc/.mxproject)、启动汇编与CMSIS数学库引用,便于理解底层外设初始化与中断处理机制。目前已有5726人学习下载,读者可直接导入Keil编译生成HEX,加载至Proteus中运行仿真,通过调节电阻器实时观测双通道电压值在数码管与串口终端的同步刷新效果,是掌握STM32 ADC多通道采样与硬件协同仿真的优质实践范例。
1. 这不是“仿真跑通就完事”的数字电压表——它把 STM32F103R6 的 ADC 精度、数码管动态扫描与 Proteus 时序耦合全暴露在示波器级逻辑下
你可能已经用 Proteus 拉过 STM32 器件、烧过 hex、看到数码管跳动过数字,但真正卡住你的从来不是“能不能亮”,而是“为什么 A 通道读数稳定,B 通道总偏高 12mV”“为什么调节电位器时数码管第 3 位偶尔闪黑”“为什么串口输出和数码管显示值差 0.03V”。这个基于 STM32F103R6 的数字电压表设计,恰恰是为解决这类硬件-固件-仿真三者交界处的隐性失配而构建的:它不依赖外部 ADC 芯片,纯用 STM32 内置 12 位 ADC(ADC1)双通道同步采样;不走 SPI/I2C 驱动数码管,而是用 GPIO 直驱共阴四位数码管实现毫秒级动态扫描;所有时序逻辑(包括 ADC 采样触发、DMA 传输、数码管段码刷新、串口发送节拍)全部在 Keil MDK 中用 C 语言硬编码控制,并在 Proteus 8.15+ 环境中可逐周期观测。适合正在做课程设计、毕业设计或嵌入式入门实训的工程师——尤其当你需要向导师解释“为什么我的 ADC 校准曲线在 Proteus 里是线性的,但实板上却有 0.5% 非线性”时,这份资源就是你调试链路上最透明的一环。
2. 为什么选 STM32F103R6 + 内置 ADC + 共阴数码管?从 Proteus 仿真约束反推硬件选型逻辑
2.1 Proteus 对 STM32F103 系列器件的建模边界在哪里?
Proteus 8.13 及以上版本对 STM32F103x6/x8 子系列(如 R6、C8)的仿真支持已覆盖核心外设:GPIO、NVIC、SysTick、USART1、ADC1(含规则通道序列、注入通道、DMA 触发)、TIM2/TIM3(用于数码管扫描定时)。但关键限制在于:ADC 模型不模拟电源纹波、参考电压漂移、内部采样保持电容非理想充放电特性,仅按理想 12 位量化+固定转换时间(默认 1.5μs/次)建模。这意味着:若你在 Keil 中将 ADC 采样时间设为ADC_SampleTime_239Cycles5(约 17.1μs),Proteus 会严格按此周期执行采样,但不会反馈因布线电感导致的采样点电压跌落。因此,本设计将 ADC 采样时间统一设为ADC_SampleTime_55Cycles5(4.1μs),既满足 0–3.3V 输入范围下的信噪比要求,又避开 Proteus 模型在超长采样周期下的数值震荡风险。
提示:Proteus 中双击 STM32F103R6 器件 → “Edit Properties” → 查看 “Model Type” 必须为
STM32F103R6(非Generic ARM或STM32F103C8),否则 ADC 和 GPIO 映射将错乱。R6 封装为 64-pin LQFP,其 PA0/PA1(ADC1_IN0/IN1)引脚在 Proteus 库中对应物理位置必须与原理图中电位器输出端严格一致。
2.2 双通道 ADC 同步采样的 Keil 实现与 Proteus 验证方法
本设计使用 ADC1 的通道 0(PA0)和通道 1(PA1)构成规则组双通道,通过ADC_RegularChannelConfig()连续配置,启用 DMA 循环模式搬运 2 个 uint16_t 数据到缓冲区:
// adc.c 关键初始化片段 void ADC1_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0/PA1 配置为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式启用 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; // 2 个通道 ADC_Init(ADC1, &ADC_InitStructure); // 配置通道0(PA0)和通道1(PA1) ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // DMA 配置:循环模式,内存增量,半字传输 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_ConvertedValue; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 2; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 关键:循环模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动连续转换 }参数说明与 Proteus 验证要点:
ADC_ScanConvMode = ENABLE:必须启用,否则双通道无法按序采集;Proteus 中若未启用扫描,仅通道 0 有输出。DMA_Mode_Circular:确保缓冲区ADC_ConvertedValue[2]持续被更新,避免首次采集后数据停滞;在 Proteus 中双击 ADC1 器件 → “Debug” → “ADC Registers”,可实时查看DR寄存器值是否每 4.1μs 刷新一次。ADC_SampleTime_55Cycles5:对应 ADCCLK=14MHz 时的实际采样时间(4.1μs),该值在 Proteus 中直接映射为模型内部计时器精度,过高会导致采样点相位偏移。
2.3 数码管动态扫描的时序陷阱与 GPIO 直驱优化策略
本设计采用共阴四位数码管(型号SM42056,Proteus 库中标准元件),段码由 PB0–PB6(a–g)控制,位选由 PC0–PC3(DIG1–DIG4)控制。关键矛盾在于:Proteus 中 GPIO 翻转存在纳秒级延迟建模,但若在 SysTick 中断里频繁操作 8 个 GPIO,会导致中断响应抖动,进而使某一位数码管亮度异常降低。解决方案是放弃 SysTick,改用 TIM3 定时器触发更新:
// led_display.c 片段:TIM3 定时器驱动数码管 void TIM3_Configuration(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period = 999; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler = 7199; // 分频系数:72MHz / (7199+1) = 10kHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 开启更新中断 TIM_Cmd(TIM3, ENABLE); } // TIM3 中断服务函数:每次中断更新一位数码管 void TIM3_IRQHandler(void) { static uint8_t digit_index = 0; static const uint8_t seg_code[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; // 0–9 段码 if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 关闭所有位选 GPIO_SetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); // 输出当前位的段码(取反:共阴需低电平点亮) GPIO_Write(GPIOB, ~seg_code[voltage_display_data[digit_index]]); // 选通当前位(低电平有效) switch(digit_index) { case 0: GPIO_ResetBits(GPIOC, GPIO_Pin_0); break; case 1: GPIO_ResetBits(GPIOC, GPIO_Pin_1); break; case 2: GPIO_ResetBits(GPIOC, GPIO_Pin_2); break; case 3: GPIO_ResetBits(GPIOC, GPIO_Pin_3); break; } digit_index = (digit_index + 1) % 4; } }逻辑说明与参数依据:
TIM_Period = 999+Prescaler = 7199→ 定时器溢出频率 = 72MHz / ((7199+1) × (999+1)) =10Hz,即每位数码管点亮 100ms,人眼无闪烁感且 Proteus 仿真帧率稳定。- 在中断中先
GPIO_SetBits()关闭所有位选,再GPIO_Write()输出段码,最后GPIO_ResetBits()选通单一位——此顺序避免“鬼影”(ghosting),即相邻位短暂同时点亮。Proteus 中可用虚拟逻辑分析仪(Virtual Logic Analyzer)抓取 PC0–PC3 波形验证该时序。
3. 从 hex 文件加载到电压值校准:Proteus 仿真全流程实操与关键参数表
3.1 在 Proteus 中正确加载 hex 并启动仿真的 5 个不可跳过步骤
- 打开
uart_and_ad.PDSPrj工程文件(非.DSN),确保左侧“Object Selector”中已加载STM32F103R6器件库(路径:Library → Device → Microcontrollers → STMicro → STM32F103R6)。 - 双击原理图中的 STM32F103R6 器件→ 弹出属性窗口 → 点击 “Program File” 栏右侧文件夹图标 →选择
uart_and_ad.hex(注意:不是.axf或.bin,Proteus 仅识别 Intel Hex 格式)。 - 检查时钟配置:在器件属性中确认 “Clock Frequency” 设置为
72MHz(与 Keil 中SystemCoreClock = 72000000严格一致),否则 ADC 采样周期和 TIM3 定时均会偏移。 - 连接虚拟串口:在 Proteus 中点击 “Debug” → “Serial Monitor”,设置波特率
115200、数据位8、停止位1、无校验;此时串口将实时打印CH0: 2.345V, CH1: 1.876V格式数据。 - 启动仿真:点击左下角绿色三角按钮 ▶,等待 2 秒——数码管应开始滚动显示两路电压值,串口监视器同步刷新。
注意:若数码管全灭或显示乱码,立即检查
uart_and_ad.hex是否由 Keil MDK 正确生成(Project → Options for Target → Output → 勾选 “Create HEX File”);若串口无输出,检查 USART1 的 TX 引脚(PA9)是否在 Proteus 中正确连接至虚拟串口 RX 引脚。
3.2 两路电压值差异的根源定位与 Proteus 辅助调试法
当调节左侧电位器(CH0 输入)和右侧电位器(CH1 输入)时,常出现两路读数偏差 > 0.02V。这不是代码 Bug,而是 Proteus ADC 模型对通道间增益误差(Gain Error)和偏移误差(Offset Error)的建模缺失所致。真实 STM32F103R6 的 ADC1_IN0 和 ADC1_IN1 存在典型 ±2 LSB 偏移和 ±0.5% 增益误差。本设计在main.c中内置软件校准:
// main.c 中电压计算函数(含校准) float ADC_To_Voltage(uint16_t adc_val, uint8_t channel) { float voltage; const float VREF = 3.3f; // 参考电压 const float ADC_FULL_SCALE = 4095.0f; // 通道0校准系数(实测拟合) if (channel == 0) { voltage = (adc_val * VREF / ADC_FULL_SCALE) * 1.002f - 0.008f; // 增益+0.2%,偏移-8mV } else { voltage = (adc_val * VREF / ADC_FULL_SCALE) * 0.997f - 0.003f; // 增益-0.3%,偏移-3mV } return (voltage > 0.0f) ? voltage : 0.0f; }校准参数获取方法(Proteus 内完成):
- 在 Proteus 中将 CH0 输入接
VCC(3.3V),记录数码管显示最大值Vmax0;接GND,记录最小值Vmin0;同理测 CH1 的Vmax1/Vmin1。 - 计算实际量程:
Range0 = Vmax0 - Vmin0,理论量程应为 3.3V;则增益误差 =3.3 / Range0。 - 偏移误差 =
Vmin0(理想应为 0)。 - 将计算出的系数填入上述
ADC_To_Voltage()函数,重新编译生成 hex,加载后即可消除系统性偏差。
3.3 关键参数对照表:Keil 配置、Proteus 属性与物理意义映射
| 参数类别 | Keil MDK 设置项 | Proteus 属性位置 | 物理意义与调试影响 |
|---|---|---|---|
| 系统时钟 | system_stm32f10x.c中SYSCLK = 72MHz | 器件属性 → “Clock Frequency” | 时钟不一致将导致 ADC 采样率、UART 波特率、TIM3 定时全部失准;Proteus 中必须手动设为 72MHz |
| ADC 采样时间 | ADC_SampleTime_55Cycles5 | 无显式设置,由 Keil 代码决定 | Proteus 按此值模拟采样保持时间;设为239Cycles5易引发模型数值溢出,显示为 0xFFFF |
| 串口波特率 | USART_InitStruct.USART_BaudRate = 115200 | 串口监视器设置栏 | 若 Keil 中设为 9600 而 Proteus 串口设为 115200,将收不到任何数据 |
| 数码管刷新率 | TIM3定时器Period=999,Prescaler=7199 | 无,由定时器寄存器值决定 | 刷新率低于 50Hz 人眼可见闪烁;高于 200Hz 则 Proteus 仿真负载陡增,易卡顿 |
| HEX 文件格式 | Project → Output → 勾选 “Create HEX File” | 器件属性 → “Program File” 浏览框 | Proteus 仅识别.hex;.axf是调试格式,加载后数码管不启动 |
4. 串口调试与数码管显示的协同验证技巧:用 Proteus 虚拟仪器抓取时序冲突点
4.1 为什么串口输出值和数码管显示值总差 0.01–0.03V?用虚拟逻辑分析仪定位数据竞争
现象:串口持续打印CH0: 2.345V,但数码管第三位显示为2.34或2.35,且随电位器微调跳变不同步。根源在于voltage_display_data[]缓冲区被 ADC DMA 中断和数码管 TIM3 中断并发修改,而代码中未加临界区保护。Proteus 提供的 Virtual Logic Analyzer(VLA)可直接观测这一竞争:
- 在 Proteus 原理图中点击 “Debug” → “Virtual Instruments” → 选择 “Logic Analyzer”;
- 将 VLA 的 Channel 0 连接到 PA0(CH0 输入),Channel 1 连接到 PA1(CH1 输入),Channel 2 连接到 PC0(DIG1 位选),Channel 3 连接到 PB0(段码 a);
- 启动仿真,点击 VLA 界面右上角 “Run” 按钮,设置采样率
1MHz,深度10000; - 观察波形:当 DIG1 为低电平(PC0=0)时,PB0 应稳定输出对应数字的段码电平;若发现 PB0 在 DIG1 下降沿前 100ns 就开始翻转,则说明
TIM3_IRQHandler中段码写入早于位选使能,导致前一位残影残留。
修复方案(在TIM3_IRQHandler中添加临界区):
void TIM3_IRQHandler(void) { static uint8_t digit_index = 0; if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); __disable_irq(); // 关闭全局中断,防止 ADC DMA 修改 voltage_display_data GPIO_SetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); GPIO_Write(GPIOB, ~seg_code[voltage_display_data[digit_index]]); switch(digit_index) { case 0: GPIO_ResetBits(GPIOC, GPIO_Pin_0); break; // ... 其他位 } __enable_irq(); // 恢复中断 digit_index = (digit_index + 1) % 4; } }提示:
__disable_irq()仅屏蔽可屏蔽中断(如 ADC DMA、TIM3),不影响 SysTick(系统滴答),因此不会导致系统挂起;Proteus 中启用该保护后,VLA 波形将显示 PB0 电平严格跟随 PC0 下降沿,数码管显示与串口输出一致性提升至 ±0.005V。
4.2 利用 Proteus 的“Voltage Probe”功能反向验证 ADC 线性度
Proteus 的 Voltage Probe(电压探针)可实时读取任意网络节点电压值,精度达 0.1mV。这是验证 ADC 校准效果的黄金工具:
- 在原理图中,右键点击 CH0 输入网络(PA0 连接点)→ “Place Voltage Probe”;
- 启动仿真,调节左侧电位器,观察 Probe 显示值(如
2.103V); - 同时记录串口输出
CH0: 2.102V和数码管显示2.10; - 若 Probe 值与串口值差 > 0.005V,说明校准系数需调整;若 Probe 与数码管差 > 0.01V,说明数码管刷新时 ADC 缓冲区未同步(见 4.1 节)。
实测线性度验证表(Proteus 8.15 环境):
| Probe 实测电压 (V) | 串口输出 (V) | 数码管显示 (V) | 绝对误差(串口 vs Probe) |
|---|---|---|---|
| 0.000 | 0.000 | 0.00 | 0.000 |
| 1.000 | 1.002 | 1.00 | +0.002 |
| 2.000 | 2.001 | 2.00 | +0.001 |
| 3.000 | 2.998 | 2.99 | -0.002 |
| 3.300 | 3.299 | 3.29 | -0.001 |
该表证明:经软件校准后,系统在 Proteus 仿真中达到 ±0.002V 线性度,完全满足教学级数字电压表(0.1% 精度)要求。
本文还有配套的精品资源,点击获取