基于STM32F103C8T6的市电电压监测系统实战指南引言在电子实验室或创客空间里经常需要监测市电电压的稳定性。传统万用表虽然能测量但无法持续记录数据。本文将带你用一块不到20元的STM32F103C8T6核心板俗称蓝桥杯开发板构建一个完整的交流电压监测系统。这个项目不仅适合电子爱好者练手也能作为大学生电子设计竞赛的基础训练。市电电压监测看似简单实则涉及模拟电路设计、ADC采样、DMA传输、RMS算法等多个技术点。许多初学者在第一个环节——信号调理电路就会遇到困难更别提后续的软件实现了。本文将避开教科书式的理论堆砌直接从实战角度出发分享我在多个项目中总结的低成本解决方案和调试技巧。1. 硬件设计安全第一的电压采样方案1.1 市电采样电路设计测量220V交流电首要考虑的是安全隔离。不建议初学者直接使用电阻分压法推荐采用现成的电压互感器如ZMCT103C它有以下优势原副边2500V隔离电压输出标准0-1V交流信号无需额外设计保护电路// 典型接线示意图 市电L → 互感器输入端 → 市电N 互感器输出端 → 10Ω采样电阻 → 运放电路如果预算有限必须使用电阻方案务必遵守分压电阻总阻值≥2MΩ使用多个串联电阻分散功率添加TVS二极管保护1.2 信号调理电路STM32的ADC输入范围是0-3.3V而互感器输出是±1V交流信号需要经过电平抬升电路用运放将信号抬升1.65V增益调节根据实际需求调整放大倍数推荐电路参数元件参数值作用说明R1, R210kΩ分压产生1.65V偏置R31kΩ运放输入阻抗匹配Rf2kΩ反馈电阻决定增益C1100nF滤除高频噪声注意实际焊接时运放建议选择轨到轨输出的型号如LMV358避免信号削顶。2. 软件架构ADC与DMA的黄金组合2.1 初始化流程详解STM32的ADC配合DMA可以实现无CPU干预的连续采样这是实时监测的关键。初始化顺序很重要先开启相关外设时钟配置GPIO为模拟输入初始化DMA控制器配置ADC参数启用DMA请求校准ADCvoid 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/840010kHz 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(i0; ilen; i) dc_offset buf[i]; // dc_offset / len; // 计算交流分量平方和 for(i0; ilen; 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读数转换为实际电压需要两个步骤线性校准用已知电压源测量得到转换系数非线性补偿针对特定互感器的特性曲线修正建议校准方法输入标准电压如220V记录ADC输出值V_adc计算转换系数K 220 / (V_rms * 互感器变比)在代码中应用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[(idx127)%128], idx1, wave_buf[idx]); idx (idx 1) % 128; }4.2 常见问题排查遇到问题时建议按以下顺序检查信号通路用示波器确认运放输出波形正常检查ADC输入引脚电压范围0-3.3V软件配置确认DMA缓冲区地址正确检查ADC采样时间是否足够55.5周期约5us算法验证输入直流信号测试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