STM32F103R6数字电压表:ADC双通道采样与数码管动态扫描时序协同设计

发布时间:2026/9/16 18:16:25
STM32F103R6数字电压表:ADC双通道采样与数码管动态扫描时序协同设计 简介本资源是一套基于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 位 ADCADC1双通道同步采样不走 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μsProteus 会严格按此周期执行采样但不会反馈因布线电感导致的采样点电压跌落。因此本设计将 ADC 采样时间统一设为ADC_SampleTime_55Cycles54.1μs既满足 0–3.3V 输入范围下的信噪比要求又避开 Proteus 模型在超长采样周期下的数值震荡风险。提示Proteus 中双击 STM32F103R6 器件 → “Edit Properties” → 查看 “Model Type” 必须为STM32F103R6非Generic ARM或STM32F103C8否则 ADC 和 GPIO 映射将错乱。R6 封装为 64-pin LQFP其 PA0/PA1ADC1_IN0/IN1引脚在 Proteus 库中对应物理位置必须与原理图中电位器输出端严格一致。2.2 双通道 ADC 同步采样的 Keil 实现与 Proteus 验证方法本设计使用 ADC1 的通道 0PA0和通道 1PA1构成规则组双通道通过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); // 配置通道0PA0和通道1PA1 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对应 ADCCLK14MHz 时的实际采样时间4.1μs该值在 Proteus 中直接映射为模型内部计时器精度过高会导致采样点相位偏移。2.3 数码管动态扫描的时序陷阱与 GPIO 直驱优化策略本设计采用共阴四位数码管型号SM42056Proteus 库中标准元件段码由 PB0–PB6a–g控制位选由 PC0–PC3DIG1–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 / (71991) 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 999Prescaler 7199→ 定时器溢出频率 72MHz / ((71991) × (9991)) 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或.binProteus 仅识别 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 输入接VCC3.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 中必须手动设为 72MHzADC 采样时间ADC_SampleTime_55Cycles5无显式设置由 Keil 代码决定Proteus 按此值模拟采样保持时间设为239Cycles5易引发模型数值溢出显示为 0xFFFF串口波特率USART_InitStruct.USART_BaudRate 115200串口监视器设置栏若 Keil 中设为 9600 而 Proteus 串口设为 115200将收不到任何数据数码管刷新率TIM3定时器Period999,Prescaler7199无由定时器寄存器值决定刷新率低于 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 AnalyzerVLA可直接观测这一竞争在 Proteus 原理图中点击 “Debug” → “Virtual Instruments” → 选择 “Logic Analyzer”将 VLA 的 Channel 0 连接到 PA0CH0 输入Channel 1 连接到 PA1CH1 输入Channel 2 连接到 PC0DIG1 位选Channel 3 连接到 PB0段码 a启动仿真点击 VLA 界面右上角 “Run” 按钮设置采样率1MHz深度10000观察波形当 DIG1 为低电平PC00时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 Probe0.0000.0000.000.0001.0001.0021.000.0022.0002.0012.000.0013.0002.9982.99-0.0023.3003.2993.29-0.001该表证明经软件校准后系统在 Proteus 仿真中达到 ±0.002V 线性度完全满足教学级数字电压表0.1% 精度要求。本文还有配套的精品资源点击获取