news 2026/5/7 8:08:51

手把手教你用STM32F103C8T6的ADC+DMA测市电电压(附完整代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手教你用STM32F103C8T6的ADC+DMA测市电电压(附完整代码)

基于STM32F103C8T6的市电电压监测系统实战指南

引言

在电子实验室或创客空间里,经常需要监测市电电压的稳定性。传统万用表虽然能测量,但无法持续记录数据。本文将带你用一块不到20元的STM32F103C8T6核心板(俗称"蓝桥杯"开发板),构建一个完整的交流电压监测系统。这个项目不仅适合电子爱好者练手,也能作为大学生电子设计竞赛的基础训练。

市电电压监测看似简单,实则涉及模拟电路设计、ADC采样、DMA传输、RMS算法等多个技术点。许多初学者在第一个环节——信号调理电路就会遇到困难,更别提后续的软件实现了。本文将避开教科书式的理论堆砌,直接从实战角度出发,分享我在多个项目中总结的低成本解决方案调试技巧

1. 硬件设计:安全第一的电压采样方案

1.1 市电采样电路设计

测量220V交流电首要考虑的是安全隔离。不建议初学者直接使用电阻分压法,推荐采用现成的电压互感器(如ZMCT103C),它有以下优势:

  • 原副边2500V隔离电压
  • 输出标准0-1V交流信号
  • 无需额外设计保护电路
// 典型接线示意图 市电L → 互感器输入端 → 市电N 互感器输出端 → 10Ω采样电阻 → 运放电路

如果预算有限必须使用电阻方案,务必遵守:

  1. 分压电阻总阻值≥2MΩ
  2. 使用多个串联电阻分散功率
  3. 添加TVS二极管保护

1.2 信号调理电路

STM32的ADC输入范围是0-3.3V,而互感器输出是±1V交流信号,需要经过:

  1. 电平抬升电路:用运放将信号抬升1.65V
  2. 增益调节:根据实际需求调整放大倍数

推荐电路参数:

元件参数值作用说明
R1, R210kΩ分压产生1.65V偏置
R31kΩ运放输入阻抗匹配
Rf2kΩ反馈电阻决定增益
C1100nF滤除高频噪声

注意:实际焊接时,运放建议选择轨到轨输出的型号如LMV358,避免信号削顶。

2. 软件架构:ADC与DMA的黄金组合

2.1 初始化流程详解

STM32的ADC配合DMA可以实现无CPU干预的连续采样,这是实时监测的关键。初始化顺序很重要:

  1. 先开启相关外设时钟
  2. 配置GPIO为模拟输入
  3. 初始化DMA控制器
  4. 配置ADC参数
  5. 启用DMA请求
  6. 校准ADC
void ADC_DMA_Init(void) { // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. GPIO配置 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; // 假设使用PA0 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. DMA配置 DMA_InitTypeDef DMA_InitStruct; DMA_DeInit(DMA1_Channel1); DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&adc_buffer; DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStruct.DMA_BufferSize = BUF_SIZE; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; DMA_InitStruct.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &DMA_InitStruct); DMA_Cmd(DMA1_Channel1, ENABLE); // 4. ADC配置 ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; ADC_InitStruct.ADC_ScanConvMode = DISABLE; ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStruct.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStruct); // 5. 通道配置 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 6. 校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 7. 启动转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); }

2.2 采样频率优化

市电频率为50Hz,根据奈奎斯特采样定理,理论上采样率>100Hz即可。但实际应用中:

  • 推荐采样率≥1kHz
  • 采样点数最好覆盖整数个周期
  • 使用定时器触发ADC可提高时序精度
// 使用TIM2触发ADC采样示例 void TIM_Config(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStruct.TIM_Period = 8400-1; // 84MHz/8400=10kHz TIM_InitStruct.TIM_Prescaler = 0; TIM_InitStruct.TIM_ClockDivision = 0; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStruct); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }

然后在ADC配置中将触发源改为定时器:

ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO;

3. 算法实现:从原始数据到有效值

3.1 RMS计算优化

原始采样数据需要经过有效值(RMS)计算才能反映实际电压。常见误区:

  • 直接对原始数据平方会溢出
  • 浮点运算在Cortex-M3上效率低
  • 未考虑直流偏置的影响

优化后的整数运算实现:

uint32_t CalculateRMS(uint16_t *buf, uint32_t len) { uint32_t sum = 0; uint32_t dc_offset = 2048; // 假设1.65V对应2048 uint32_t i; // 先计算直流分量(可选) // dc_offset = 0; // for(i=0; i<len; i++) dc_offset += buf[i]; // dc_offset /= len; // 计算交流分量平方和 for(i=0; i<len; i++) { int32_t diff = (int32_t)buf[i] - (int32_t)dc_offset; sum += (uint32_t)(diff * diff); } // 整数开方 return IntegerSqrt(sum / len); } // 快速整数开方算法 uint32_t IntegerSqrt(uint32_t num) { uint32_t res = 0; uint32_t bit = 1UL << 30; // 最大数的平方根 while (bit > num) bit >>= 2; while (bit != 0) { if (num >= res + bit) { num -= res + bit; res = (res >> 1) + bit; } else { res >>= 1; } bit >>= 2; } return res; }

3.2 校准与标定

将ADC读数转换为实际电压需要两个步骤:

  1. 线性校准:用已知电压源测量得到转换系数
  2. 非线性补偿:针对特定互感器的特性曲线修正

建议校准方法:

  1. 输入标准电压(如220V)
  2. 记录ADC输出值V_adc
  3. 计算转换系数K = 220 / (V_rms * 互感器变比)
  4. 在代码中应用:
float GetRealVoltage(uint32_t rms_value) { const float K = 0.978f; // 实测校准系数 return rms_value * K; }

4. 系统集成与调试技巧

4.1 OLED显示实现

0.96寸OLED是显示电压波形的理想选择。推荐使用硬件I2C驱动:

void OLED_ShowVoltage(float voltage) { char buf[16]; sprintf(buf, "Voltage: %.1fV", voltage); OLED_ShowString(0, 0, (uint8_t*)buf); // 简单波形显示 static uint8_t wave_buf[128]; static uint8_t idx = 0; wave_buf[idx] = 64 - (uint8_t)(voltage - 220) * 2; OLED_DrawLine(idx, wave_buf[(idx+127)%128], idx+1, wave_buf[idx]); idx = (idx + 1) % 128; }

4.2 常见问题排查

遇到问题时,建议按以下顺序检查:

  1. 信号通路

    • 用示波器确认运放输出波形正常
    • 检查ADC输入引脚电压范围(0-3.3V)
  2. 软件配置

    • 确认DMA缓冲区地址正确
    • 检查ADC采样时间是否足够(55.5周期约5us)
  3. 算法验证

    • 输入直流信号测试ADC读数
    • 用已知交流信号验证RMS计算

调试技巧:在ADC初始化后添加一个简单的测试代码,直接读取ADC->DR寄存器,排除DMA配置问题。

4.3 性能优化建议

当系统需要同时处理其他任务时,可以考虑:

  • 使用双缓冲DMA:一组缓冲区处理时,另一组继续采样
  • 降低采样率:对于电压监测,500Hz采样率通常足够
  • 定时唤醒:如果不需连续监测,可用低功耗模式+定时唤醒
// 双缓冲DMA配置示例 #define BUF_SIZE 256 uint16_t adc_buf1[BUF_SIZE], adc_buf2[BUF_SIZE]; void DMA_DoubleBuffer_Init(void) { // ...其他DMA配置相同 DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buf1; DMA_InitStruct.DMA_BufferSize = BUF_SIZE; DMA_Init(DMA1_Channel1, &DMA_InitStruct); // 启用双缓冲 DMA_DoubleBufferModeConfig(DMA1_Channel1, (uint32_t)adc_buf2, DMA_Memory_1); DMA_DoubleBufferModeCmd(DMA1_Channel1, ENABLE); }

5. 项目扩展方向

基础功能实现后,可以考虑:

  • 无线传输:添加ESP8266模块实现WiFi远程监控
  • 数据记录:使用SPI Flash存储历史数据
  • 报警功能:当电压超出范围时触发蜂鸣器
  • 电能计量:结合电流互感器实现简单功率测量
// 简单的超限报警实现 void CheckVoltage(float voltage) { static uint8_t alarm_cnt = 0; if(voltage < 198 || voltage > 242) { // ±10% if(++alarm_cnt > 5) { BEEP_ON(); OLED_ShowString(0, 2, (uint8_t*)"ALARM!"); } } else { alarm_cnt = 0; BEEP_OFF(); } }

6. 工程文件组织建议

规范的工程结构能提高代码复用性:

/Project ├── /CMSIS // 内核支持文件 ├── /Drivers │ ├── /STM32F10x_StdPeriph_Driver // 标准外设库 │ └── /OLED // 显示驱动 ├── /Hardware │ ├── adc.c // ADC相关代码 │ └── voltage.c // 电压计算算法 ├── /User │ ├── main.c // 主程序 │ └── stm32f10x_it.c // 中断服务程序 └── README.md // 项目说明

在Keil工程中,合理设置头文件包含路径:

../Drivers/STM32F10x_StdPeriph_Driver/inc ../Drivers/OLED ../Hardware ../User
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 8:08:33

Arm Cortex-R82处理器AArch64寄存器架构与优化实践

1. Cortex-R82处理器AArch64寄存器架构概述Arm Cortex-R82处理器作为面向实时应用的高性能处理器&#xff0c;其AArch64寄存器设计在保持Armv8架构兼容性的同时&#xff0c;针对实时系统需求进行了多项优化。与Cortex-A系列处理器相比&#xff0c;R82的寄存器设计更强调确定性和…

作者头像 李华
网站建设 2026/5/7 8:08:32

三分钟掌握iFakeLocation:iOS位置模拟的跨平台解决方案

三分钟掌握iFakeLocation&#xff1a;iOS位置模拟的跨平台解决方案 【免费下载链接】iFakeLocation Simulate locations on iOS devices on Windows, Mac and Ubuntu. 项目地址: https://gitcode.com/gh_mirrors/if/iFakeLocation iFakeLocation是一款功能强大的开源工具…

作者头像 李华
网站建设 2026/5/7 8:08:30

5个专业技巧:精通UE4/5脚本系统从零到实战

5个专业技巧&#xff1a;精通UE4/5脚本系统从零到实战 【免费下载链接】RE-UE4SS Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games 项目地址: https://gitcode.com/gh_mirrors/re/RE-UE4SS UE4SS&a…

作者头像 李华
网站建设 2026/5/7 8:07:37

电力电子谐波抑制与PFC技术解析

1. 电力电子与电能质量问题的本质电力电子设备在现代电力系统中无处不在&#xff0c;从手机充电器到高铁牵引系统&#xff0c;它们通过半导体开关的快速通断实现电能形式的转换。但这种开关操作就像用一把高速开关的水龙头去接满一桶水——水流时断时续&#xff0c;必然会在水管…

作者头像 李华