资讯动态

基于STM32的简易智能电表:互感器采样、FFT与OLED显示全流程

发布时间:2026/10/15 12:04:39 来源:尧图企业网站定制
简介这份资源是一套基于STM32的简易智能电表完整项目资料面向具备一定嵌入式基础的电子爱好者与STM32开发者用于学习电力参数采集与计量系统的软硬件设计。项目以STM32为核心通过电压、电流传感器配合ADC采样实现功率、功率因数与累积用电量的实时计算并借助定时器与数学处理单元完成相位与积分运算是理解嵌入式测量原理的典型实践案例。资源包共440个文件约199.59MB包含C源码、头文件、工程配置、原理图与PCB文件、PDF文档及编译中间文件等覆盖从底层驱动到硬件接口的完整链路。目前已有1365人学习下载。读者可从中获取STM32初始化配置、ADC与定时器设置、电力参数计算逻辑以及硬件连接原理图适合对照分析系统架构、掌握测量方法并提升动手能力。1. 从一块拆机板说起为什么我要复刻这个简易智能电表前阵子整理工位翻出一块吃灰的 STM32F103C8T6 最小系统板旁边还躺着个拆机下来的单相机械电表。当时脑子里冒出一个念头能不能用这块板子把电表改造成能读电压、电流、功率、电量的东西搜了一圈发现基于 STM32 的简易智能电表这个方向资料不少但要么是纯论文讲原理要么是代码扔一堆没注释真正能照着接线、烧录、跑通、看到数据的完整路径反而不好找。于是我自己动手搭了一套从互感器采样到 OLED 显示中间踩了不少坑也摸清了这个方案的能力边界。这篇笔记就是把这套东西拆开讲清楚它是什么、能解决什么、适合谁。如果你手头有 STM32 开发板想做一个能实际测量市电参数的小设备或者正在做课程设计、毕业设计需要一套可复现的采样方案那这套思路可以直接拿去用。它不是计量级电表但作为学习采样、ADC、FFT、串口通信的载体足够扎实。2. 硬件选型与信号链从 220V 到 3.3V 之间到底发生了什么2.1 为什么不用霍尔而用互感器测交流电参数第一步永远是怎么把高压大电流变成单片机敢碰的小信号。常见方案有三种电阻分压、霍尔传感器、互感器。电阻分压最简单但完全不隔离一旦采样电阻虚焊或者运放击穿220V 直接灌进 MCU 的 ADC 引脚芯片当场报废人也有风险。霍尔传感器隔离性好但线性度一般温漂大而且价格偏高。电流互感器和电压互感器是这类简易电表里最平衡的选择天然隔离、线性度够用、成本低。我用的是一组常见的微型精密互感器电流通道用 1000:1 的电流互感器电压通道用 1000:1000 的电压互感器。电流互感器次级并联一个采样电阻把电流信号转成电压信号电压互感器次级同样并电阻分压。两路信号都是交流小信号幅值在毫伏到几百毫伏之间需要抬升到 ADC 能采的正电压范围。2.2 偏置电路把交流信号抬到 1.65VSTM32 的 ADC 只能采 0 到 3.3V 的单极性信号而互感器输出是正负交替的交流。所以必须加一个偏置网络把信号整体抬到 1.65V 附近让交流波形在 1.65V 上下摆动。常见做法是用两个等值电阻分压得到 1.65V再通过一个运放跟随器做缓冲然后经隔直电容和偏置电阻接入 ADC。这里有个细节偏置电压的稳定性直接决定测量精度。如果直接用电阻分压而不加缓冲ADC 采样瞬间的电荷注入会把偏置点拉偏读数就会跳。我一开始省了运放结果功率读数每隔几秒就飘一次后来加了电压跟随器才稳住。信号链完整走一遍互感器 → 采样电阻 → 偏置网络 → RC 低通滤波 → STM32 ADC 引脚。RC 滤波的截止频率一般设在 1kHz 到 5kHz 之间既能滤掉高频噪声又不会把 50Hz 基波和几次谐波衰减掉。2.3 采样电阻和量程的换算关系采样电阻的取值决定了量程和分辨率。以电流通道为例假设电流互感器变比是 1000:1初级额定电流 5A那次级电流就是 5mA。如果采样电阻取 100Ω次级电压就是 0.5V 有效值对应 ADC 读到的交流幅值约 0.7V偏置在 1.65V 上摆幅在 0.95V 到 2.35V 之间离 ADC 的 0V 和 3.3V 都有余量不会削顶。如果采样电阻取太大比如 200Ω幅值到 1.4V摆幅变成 0.25V 到 3.05V接近轨到轨稍微有点温漂就削顶波形一削顶FFT 算出来的有效值就偏小功率自然不准。所以采样电阻的选择原则是在最大额定电流下信号摆幅不超过 ADC 量程的 80%。这个余量留出来温漂和器件离散性才不会把你坑死。3. 固件实现ADC 采样、FFT 计算与 OLED 显示3.1 ADC 双通道交替采样与 DMA 搬运STM32F103 的 ADC 支持规则通道扫描和 DMA 搬运这是做交流采样的标准配置。我配置的是 ADC1 的通道 0 和通道 1分别接电压和电流信号用定时器触发采样率设为 3200Hz。为什么是 3200Hz因为要对 50Hz 信号做 FFT采样率最好是基波的整数倍3200 64 × 50这样采 64 个点正好覆盖一个完整周期做 64 点 FFT 时基波落在第 1 根谱线不会产生频谱泄漏。DMA 配置成循环模式每次搬运 128 个半字64 个电压点 64 个电流点搬完一轮触发一次中断在中断里做数据处理。下面是我用的 ADC DMA 初始化核心代码// ADC1 初始化双通道扫描 定时器触发 DMA 循环 void ADC_DMA_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0 电压通道PA1 电流通道模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // DMA1 通道1 搬运 ADC1 数据 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 128; // 64 电压 64 电流 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_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // ADC1 配置扫描模式连续转换外部触发 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 由定时器触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_CC2; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; ADC_Init(ADC1, ADC_InitStructure); // 规则通道顺序先电压后电流 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这段代码的关键点有三个。第一DMA_BufferSize设为 128是因为每个周期采 64 个电压点和 64 个电流点交替存放所以缓冲区是 128 个半字。第二ADC_ExternalTrigConv_T2_CC2表示用定时器 2 的捕获比较通道 2 来触发 ADC这样采样率由定时器精确控制比软件延时靠谱得多。第三采样时间设为 55.5 个 ADC 时钟周期是因为互感器输出阻抗加上 RC 滤波的等效源阻抗不低采样时间太短采样保持电容充不满读数会偏小。这个参数要根据实际信号源阻抗调整如果发现读数系统性偏低先把采样时间加长试试。3.2 64 点 FFT 算有效值和功率拿到 64 个电压点和 64 个电流点后下一步是算有效值、有功功率和功率因数。最直接的方法是用均方根公式在时域算但时域法对噪声和直流偏置很敏感而且算不了谐波。用 FFT 把信号变到频域取基波幅值算有效值同时用电压和电流基波的相位差算功率因数精度更高还能顺带看谐波含量。STM32F103 没有硬件浮点单元64 点 FFT 用定点运算跑起来很快但要注意旋转因子的定标。我用的是经典的定点 FFT 算法输入数据先减去 1.65V 对应的 ADC 码值约 2048把交流信号变成有正有负的序列再做 FFT。下面是核心计算逻辑// 在 DMA 传输完成中断中调用 void Process_Data(void) { int i; float v_rms, i_rms, p_active, pf; float v_real, v_imag, i_real, i_imag; float v_phase, i_phase, phase_diff; // 1. 去直流偏置把 ADC 码值转成有符号数 for(i 0; i 64; i) { fft_input_v[i] (float)(adc_buffer[i * 2] - 2048); fft_input_i[i] (float)(adc_buffer[i * 2 1] - 2048); } // 2. 对电压和电流分别做 64 点 FFT FFT_Compute(fft_input_v, 64); FFT_Compute(fft_input_i, 64); // 3. 取基波第 1 根谱线的实部和虚部 v_real fft_input_v[2]; // 实部 v_imag fft_input_v[3]; // 虚部 i_real fft_input_i[2]; i_imag fft_input_i[3]; // 4. 算基波幅值再转有效值 // 定点 FFT 结果需要除以 N/2 还原幅值 v_rms sqrtf(v_real * v_real v_imag * v_imag) / 32.0f / 1.414f; i_rms sqrtf(i_real * i_real i_imag * i_imag) / 32.0f / 1.414f; // 5. 算相位差和功率因数 v_phase atan2f(v_imag, v_real); i_phase atan2f(i_imag, i_real); phase_diff v_phase - i_phase; pf cosf(phase_diff); // 6. 有功功率 视在功率 × 功率因数 p_active v_rms * i_rms * pf; // 7. 电量累加功率 × 时间间隔单位 Wh energy_wh p_active * (1.0f / 3200.0f * 64.0f) / 3600.0f; }这段代码里有几个容易翻车的地方。第一定点 FFT 的输出幅值需要除以 N/2 才能还原成时域幅值64 点 FFT 就是除以 32这个系数忘了除读数会差几十倍。第二atan2f返回的是弧度算功率因数时直接取余弦没问题但如果电压和电流通道之间存在硬件相位延迟比如 RC 滤波参数不一致算出来的功率因数会偏小需要在软件里做相位补偿。第三电量累加的时间间隔要跟采样率对齐3200Hz 采 64 个点对应 20ms也就是一个工频周期累加时用这个时间算。如果采样率改了这个系数也要跟着改否则电量会差得离谱。3.3 OLED 显示与串口上报算出来的数据要让人看到。我用的是 0.96 寸 OLEDI2C 接口刷一屏电压、电流、功率、电量四个参数。OLED 驱动网上很多直接拿现成的库改一下显示函数就行。刷新率不用太高每秒刷两次足够刷太快反而闪。另外我把数据通过串口以 115200 波特率上报到电脑格式是逗号分隔的 ASCII 字符串方便用串口助手或者 Python 脚本抓下来画曲线。串口上报的好处是调试阶段不用盯着小屏幕数据直接进电脑用 Excel 或者 matplotlib 一画波形和趋势一目了然。下面是对应的串口打印代码// 每 500ms 调用一次上报数据 void Report_Data(void) { printf(V%.1f,I%.3f,P%.1f,PF%.2f,E%.3f\r\n, v_rms, i_rms, p_active, pf, energy_wh); }printf重定向到串口需要实现fputc函数这个在 Keil 和 GCC 下写法略有不同但都是标准操作。注意浮点格式化在 STM32 上会显著增加代码体积如果 Flash 紧张可以把浮点转成整数再打印比如电压乘 10 取整显示的时候再除回去。4. 避坑与排查那些让我熬夜的采样异常4.1 读数周期性跳动像呼吸一样现象电压和电流读数稳定但功率读数每隔几秒周期性跳动幅度还不小。原因偏置电压没有加缓冲ADC 采样瞬间的电荷注入把偏置点拉偏下一轮采样前偏置还没恢复导致电压和电流通道的直流偏置不一致相位差算出来忽大忽小。解决在偏置分压点后面加一个运放电压跟随器输出阻抗降到毫欧级电荷注入瞬间就能恢复。如果不想加运放至少把偏置分压电阻降到 1kΩ 以下并在 ADC 引脚旁边加 0.1μF 去耦电容但效果不如跟随器彻底。4.2 小电流时功率显示为零现象接一个 5W 的 LED 灯电流读数有 0.02A 左右但功率显示 0.0W。原因小电流时电流互感器输出信号很小经过偏置和滤波后ADC 读到的交流幅值只有几个码值FFT 算出来的基波幅值被噪声淹没相位差也算不准功率因数趋近于零乘出来功率就没了。解决在软件里设一个功率死区当视在功率小于某个阈值比如 1VA时强制功率因数为 1直接按电压乘电流算有功功率。这个做法在简易电表里很常见牺牲小电流下的功率因数精度换取读数不跳零。4.3 电压读数比万用表高 5%现象用万用表测市电 220V电表显示 231V差了 5%。原因电压互感器的变比标称值和实际值有偏差采样电阻的精度也不够再加上 ADC 参考电压用的是 MCU 的 3.3V 供电这个电压本身就有 ±3% 的误差。解决用万用表测一个已知电压比如 220V然后调整软件里的校准系数把读数拉到和万用表一致。校准系数就是一个乘法因子存在 Flash 里上电时读出来用。注意校准要在额定电压附近做因为互感器的非线性在远离额定点时会更明显。4.4 FFT 算出来的有效值比时域均方根小现象用 FFT 取基波算有效值和直接用 64 个点算均方根比FFT 的结果偏小。原因市电波形不是纯正弦有 3 次、5 次谐波FFT 只取了基波谐波能量没算进去所以有效值偏小。解决如果只关心基波参数这个偏差可以接受如果要算总有效值需要把 2 到 10 次谐波的幅值也平方累加进去。简易电表一般只算基波因为家用电器的谐波含量不大基波有效值已经够用。但如果测的是开关电源或者变频器谐波含量高只算基波会明显偏低。4.5 串口打印浮点数导致程序卡死现象加了串口打印之后程序跑几秒就卡死OLED 也不刷新了。原因printf浮点格式化在 STM32 上会调用较大的库函数如果栈空间不够或者中断里调用printf导致重入就会卡死。解决不要在中断里调用printf把数据存到全局变量在主循环里打印。栈空间至少给 1KB 以上Keil 里在启动文件里改 Stack_Size。如果还不行把浮点打印改成整数打印用%d代替%f自己手动拆分整数和小数部分。5. 进阶技巧用滑动平均和相位补偿把精度再拉一档这套简易电表跑通之后读数已经能看了但离稳还有距离。我后来加了两个小技巧效果立竿见影。第一个是滑动平均对连续 8 个周期的功率读数做滑动平均输出刷新率降到每秒 6 次左右但读数跳动从 ±5% 降到 ±1% 以内。滑动平均的窗口长度要跟刷新率权衡窗口太长响应慢窗口太短滤波效果差。我一般取 8 个周期对应 160ms人眼看数字基本不跳负载突变时也能在 200ms 内跟上。第二个是相位补偿。前面提到电压和电流通道的 RC 滤波参数不可能完全一致导致基波相位差有额外偏移。这个偏移是固定的可以在软件里标定接一个纯电阻负载比如白炽灯或者电热丝功率因数理论上是 1如果电表显示 0.95说明有 18 度左右的相位偏差在软件里把这个角度补回去。补偿之后再测电机类负载功率因数就能算准。下面是我用的相位补偿代码片段// 相位补偿在算功率因数之前把电流相位加上补偿角 float phase_compensation 0.31f; // 弧度根据纯电阻负载标定得到 i_phase atan2f(i_imag, i_real) phase_compensation; phase_diff v_phase - i_phase; pf cosf(phase_diff); // 限制功率因数范围防止补偿过头 if(pf 1.0f) pf 1.0f; if(pf -1.0f) pf -1.0f;补偿角怎么标定接一个已知功率因数为 1 的负载读电表显示的功率因数用acos反算出角度差取反就是补偿角。注意补偿角跟采样率有关如果改了采样率或者 RC 参数要重新标定。标定一次之后只要硬件不改这个值就固定了。还有一个进阶方向是加谐波分析。64 点 FFT 能覆盖到 32 次谐波把 2 到 10 次谐波的幅值打印出来就能看负载的谐波特性。这个功能对排查电能质量很有用比如发现 3 次谐波特别大可能是附近有大量单相整流负载。不过谐波分析对采样精度的要求更高偏置和增益的微小误差都会在谐波谱线上放大所以先把基波精度做扎实再碰谐波。从那以后我每次做采样类项目都强制走一遍偏置缓冲 → 采样时间加长 → 纯电阻标定相位 → 滑动平均这个流程四步走完读数基本就稳了。这套简易智能电表的方案不复杂但每个环节都有细节细节做到位它就能从一个玩具变成一个能用的测量工具。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑