STM32F103用电器分析识别完整方案:从互感器采样到FFT频谱

📅 发布时间:2026/10/14 14:11:59
STM32F103用电器分析识别完整方案:从互感器采样到FFT频谱
简介2021年全国大学生电子竞赛H题荣获省级一等奖的完整方案资料基于STM32F103C8T6微控制器适合初次参加电子设计竞赛或正在自学嵌入式开发的新手参考学习。压缩包为RAR格式大小约208.5MB内含参赛团队整理过的源代码、电路原理图、设计报告等资料覆盖STM32的GPIO配置、定时器PWM、串口UART、ADC采样、中断处理等核心外设应用并演示了HAL库与LL库编写驱动的工程架构与调试思路。目前已有1321人浏览学习这套资源其价值在于它不是零散代码而是一个获得省一等奖作品的完整复盘。读者可以跟着电路图、源码和报告逐步理解从硬件连接、程序逻辑到联机调试的每一处关键决策还能学习到文档撰写与项目管理方法缩短自己从入门到完成竞赛项目的爬坡过程。1. 一块 STM32F103C8T6 也能拿省一H题到底考什么如果你正备战全国电子竞赛想在有限时间内让一块 STM32F103C8T6 单片机跑完整个嵌入式测量方案那 2021 年这道 H 题用电器分析识别装置就是最典型的样本。题目只给一个 220V 插座口你要通过互感器采到电压电流波形算功率、功率因数、谐波占比最后在屏幕上认出白炽灯、风机、电热水壶还是开关电源。全程不靠复杂外设靠的就是单片机最基础的 ADC、定时器、DMA 和串口。这份省一资源的工程结构对新手很友好主循环做识别中断只管搬数据程序从初始化到出结果大概三百行真正的坑全在硬件链路和采样参数上。适合正在备赛的学生、想入门嵌入式信号处理的开发者以及需要一套能跑通全流程样例的从业者。2. 硬件底子电流互感器、电压互感器与运放偏置怎么选2.1 用互感器做隔离采样关系式与电路骨架市电 220V 直接电阻分压送 ADC共地噪声和人身安全都是大问题。我一般会选电压互感器电流互感器方案把强电变成毫安级电流信号再转成运放能处理的小电压。常见选型是 2mA/2mA 的微型电压互感器原边串联 100kΩ~120kΩ 限流电阻副边并联采样电阻电流互感器选变比 1000:1如 5A:5mA原边直接穿线副边接负载电阻。这个架构的核心关系式很简单副边电压 副边电流 × 采样电阻。电压互感器副边额定 2mA配 100Ω 得 0.2V 峰值电流互感器副边额定 5mA配 100Ω 得 0.5V 峰值。电路骨架是L 线穿过电流互感器→互感器副边电流流过采样电阻→运放放大并叠加 1.65V 直流偏置→ADC 引脚。整个采样端始终被钳在 0~3.3V 之间。要注意采样电阻的值不要贪大超过额定输出会让磁芯进入饱和区波形顶部削平谐波特征直接报废。给一个保守参考互感器副边额定电流的 70% 对应采样电阻压降控制在 0.5V 以内剩余增益交给运放。提示电流互感器副边绝对不允许开路。调试时先确认采样电阻焊好、运放输入端没悬空再上强电。2.2 运放增益和 1.65V 偏置的取值计算运放电路我习惯用同相加法器把交流信号抬到 1.65V 中心。输入信号幅度太小必须放大。电流通道输入 0.5V 峰值想送到 ADC 时有 1.25V 峰值就需要约 2.5 倍增益。用非反相放大Rg1kΩ、Rf1.5kΩ 时增益 2.5 倍。电压通道输入 0.2V 峰值放大到 1.3V 峰值需约 6.5 到 7 倍。要注意 STM32 的 ADC 是单极性偏置后波形不能削顶也不能穿底上限留到 3.2V 以下下限留到 0.1V 以上。经验值是把叠加后的波峰顶到 2.9V 附近既不浪费量程也不踩 LDO 输出带载后的跌落风险。运放选型方面LM358 便宜但带宽增益积只有 1MHz放大 7 倍后可用带宽只剩约 140kHz看 50Hz~1kHz 的谐波够用如果还想看清 20 次谐波建议换低失调的 OP07 或 LMV358。偏置电压别直接从一个电阻分压拉出来那样负载一变偏置就漂。我一般用 1% 精密电阻把 VDDA 分压到 1.65V再接一个运放电压跟随器做缓冲输出阻抗低后级增益电路怎么拉都不会偏。计算公式是 Vout 1.65 G × V_in_peak × sin(ωt)校准时用万用表量实际偏置填入程序而不是纸面上算完就完事。2.3 电源与基准先稳住 3.3V再谈识别精度比如现场用 24V 适配器供电板子上一颗 AMS1117 降到 3.3V看起来万用表测很稳可 ADC 参考和供电都是这颗 LDO问题就出在这里。常见踩坑是VDDA 纹波几十毫伏而 ADC 参考正是 VDDA等于每次采样都叠加了随机偏置。FFT 结果里会出现一堆不属于负载的杂散谱线。解决办法LDO 输出先串一个 10Ω 电阻再接 100uF 电解和 1uF 陶瓷电容到 VDDA构成两级低通条件允许就直接上基准芯片比如 REF3030 出 3.0V 或 TL431 搭 3.0V接到 VREF。地线也要注意模拟地 VSSA 和数字地之间单点汇合别让数字开关噪声绕到模拟地回路里。3. 采集与预处理把 220V 波形变成干净的频谱3.1 用定时器触发 ADC DMA采样率必须是工频的整数倍为什么不用软件延时采样因为主循环里的任何中断都会抖动采样时刻而 FFT 对采样时基的一致性极其敏感。正确做法是用定时器触发 ADCDMA 循环搬运。核心参数市电 50Hz每周期采 128 点采样率就是 6400Hz一帧采 1024 点正好是 8 个完整工频周期。这样 FFT 分辨率是 6400/10246.25Hz基波 50Hz 正好落在第 8 个频率点3 次谐波 150Hz 落在第 24 点不会出现频谱泄漏。static uint16_t adc_buf[1024]; // 一帧数据8个完整周期 void ADC_DMA_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 72MHz / (112491) 6400Hz TIM_TimeBaseInitTypeDef tim; TIM_TimeBaseStructInit(tim); tim.TIM_Prescaler 11249; tim.TIM_Period 0; TIM_TimeBaseInit(TIM2, tim); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize 1024; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; // 循环采集不需要手动重启 dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, dma); ADC_InitTypeDef adc; adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode DISABLE; // 不连续转换等待外部触发 adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }单通道代码演示了定时器触发DMA 循环模型。真项目用两路采样把电压电流分别接到 PA1、PA2规则序列里配置两个通道DMA BufferSize 改 2048前 1024 点存电压、后 1024 点存电流。这里解释一下 6400Hz 的由来定时器分频值 11249 是把 72MHz 时钟除以 11250得到每次更新事件间隔 156.25us正好是 1/6400 秒。ADC 采样时间取最长档239.5 个 ADC 周期是为了降低信号源内阻造成的建立误差对 50Hz 正弦波来说这个采样窗口完全跟得上信号变化。3.2 1024 点 FFT从 adc 数组到基波/谐波幅值STM32F103 Cortex-M3 跑手工写的 FFT 不是不行但没必要。直接用 CMSIS-DSP 库的 arm_cfft_f321024 点复数运算在 72MHz 下大概几毫秒远小于采集一帧 160ms 的时间。要先把 ADC 原始值减去 1.65V 偏置否则直流分量会占掉 FFT 的最大能量把基波谐波全部淹没在泄漏里。#include arm_math.h static float32_t fft_in[2048]; // 1024点复数实部、虚部交替存放 static float32_t mag_out[1024]; void Calc_Spectrum(uint16_t *adc_buf) { uint32_t i; for (i 0; i 1024; i) { fft_in[2*i] (float32_t)adc_buf[i] / 4096.0f * 3.3f - 1.65f; fft_in[2*i1] 0.0f; // 虚部清零 } arm_cfft_f32(arm_cfft_sR_f32_len1024, fft_in, 0, 1); arm_cmplx_mag_f32(fft_in, mag_out, 1024); // 频率间隔 6400 / 1024 6.25Hz/bin // 基波 50Hz 在 bin83次谐波在 bin245次在 bin40 float ratio3 mag_out[24] / (mag_out[8] 1e-6f); float ratio5 mag_out[40] / (mag_out[8] 1e-6f); // ratio3、ratio5 作为特征量 }arm_cfft_f32 的输入必须是复数数组把实数波形放实部、虚部填 0 即可。arm_cmplx_mag_f32 输出每个频率点的模。这里的 mag_out[8] 就是基波幅值mag_out[24] 是 3 次谐波幅值。为什么直接用比值而不是绝对幅值因为 FFT 结果里 2/N 的换算常数在比值中会被约掉我们关心的是各次谐波相对于基波的占比而不是多少伏这个绝对数。3.3 有功功率、功率因数与谐波占比的计算口径很多人想用电压、电流基波的 FFT 幅值去合成功率但那样只能得到基波功率且相位精度受频谱泄漏影响很大。最稳的做法是在时域直接做瞬时功率平均也叫直接积分法P 1/N × Σ(v[k]×i[k])。有功功率是标量电压电流同时刻相乘再平均天然包含所有谐波分量不会漏。// v[]、i[] 已折算成实际电压、实际电流序列 double v_rms 0, i_rms 0, p_sum 0; for (int k 0; k 1024; k) { double vk (v[k] / 4096.0 * 3.3 - 1.65) / V_GAIN; double ck (i[k] / 4096.0 * 3.3 - 1.65) / I_GAIN; v_rms vk * vk; i_rms ck * ck; p_sum vk * ck; } double V sqrt(v_rms / 1024); double I sqrt(i_rms / 1024); double P p_sum / 1024; double S V * I; double PF P / S;V_GAIN 和 I_GAIN 是整条采样链路的系统增益包含互感器变比、采样电阻和运放倍数。标定时接一个纯电阻负载白炽灯调整 V_GAIN 和 I_GAIN直到程序算出的 P 和屏幕上的功率计读数一致。这样各级硬件的具体误差一次性吸收进系数里以后不用再关心运放实际放大倍数是多少。每换一批板子必须重新标定一次这是血泪教训。4. 识别算法与联调特征表怎么建、阈值怎么标4.1 基波/谐波/功率特征表与查表识别流程拿到的特征有三个足够有功功率 P、功率因数 PF、3 次谐波占比。这三个量在常见负载上的区分度最大而且都能在 C8T6 上快速算出来。5 次谐波可以作为辅助但对风机和开关电源的区分帮助有限。特征选择的大原则两个特征之间不能相关度太高否则等于用一个特征在投票。比如视在功率和有功功率在阻性负载上数值接近但无法单独分辨是白炽灯还是电热水壶。先建特征表负载类型典型功率范围(W)功率因数3次谐波占比启动特征白炽灯40~2000.99~1.02%一开即稳异步风机30~1000.45~0.755%~12%启动电流爬升 2~3s电热水壶800~18000.98~1.03%功率恒定开关电源适配器5~600.80~0.9815%~35%启动瞬间冲击电流阈值怎么标定方法很简单每种负载采 30 组数据算每个特征的均值 μ 和标准差 σ阈值取 μ±3σ。不要拿着单独一次测量值就定死窗口换一个同型号负载时功率会差几十瓦。比如白炽灯标称 100W实测可能 95W也可能 103W窗口就要覆盖这个波动范围。做一张识别表程序里查表即可typedef struct { float p_min, p_max; // 有功功率窗口 float pf_min, pf_max; // 功率因数窗口 float h3_min, h3_max; // 3次谐波占比窗口 const char *name; } LoadModel; const LoadModel models[] { {Lamp, 30, 250, 0.95, 1.00, 0.00, 0.05}, {Motor, 25, 120, 0.40, 0.85, 0.04, 0.15}, {Kettle, 780, 1900, 0.95, 1.00, 0.00, 0.06}, {Adapter, 4, 65, 0.80, 0.98, 0.10, 0.40}, }; int Recognize(float p, float pf, float h3) { for (int i 0; i 4; i) { if (p models[i].p_min p models[i].p_max pf models[i].pf_min pf models[i].pf_max h3 models[i].h3_min h3 models[i].h3_max) { return i; } } return -1; // 未知负载 }注意电机和适配器在功率上可能都落在 30~60W 区域但电机 PF 低、适配器 PF 高3 次谐波也差了一倍不会混淆。设计窗口时让两个模型在某个维度上尽量不重叠就是这章最核心的工程智慧。4.2 单一负载识别到组合负载功率近似匹配组合负载是 H 题的得分点白炽灯风机同时开要求识别结果带出两个名字。单一负载特征表显然不够。常见做法是有功功率近似匹配总功率等于各负载功率标量之和因为有功功率是标量不涉及矢量相位叠加。先测总有功功率 P_total再遍历特征库中所有 1~2 个负载的组合找到组合功率与实测总功率之差小于阈值的候选。// 伪代码功率近似匹配 for (int i 0; i N; i) { if (fabsf(p_total - models[i].p_avg) 15.0f) { // 可能是单一负载 } for (int j i 1; j N; j) { float diff fabsf(p_total - (models[i].p_avg models[j].p_avg)); if (diff 15.0f) { // 再核对谐波占比是否近似叠加 } } }为什么不拿总电流做匹配因为感性负载的电流和阻性负载不同相矢量叠加不是简单相加需要知道相位关系工程上太容易翻车。有功功率标量相加的误差很小15W 阈值足够覆盖同型号差异和传感器误差。谐波可以近似线性叠加用实测 h3 与两个模型 h3 的组合值做二次校验。如果出现同一类型负载两个同时开两盏白炽灯在特征库里允许“同模型双倍功率”组合即可这个细节在现场测试里很值分。5. 避坑采样率、互感器相位、参考电压与串口中断5.1 采样率不是工频整数倍频谱泄漏让阈值永远调不稳现象FFT 之后 50Hz 附近出现两个靠在一起的峰3 次谐波占比在 15%~30% 之间乱跳白炽灯和开关电源的边界线完全糊在一起阈值怎么调都调不好。关掉串口输出问题依旧说明不是干扰是采样配置本身出了问题。原因有人为了“采样快一点”用 8000Hz1024 点对应 8.2 个工频周期。FFT 默认把这一帧数据当成一个完整的周期信号做循环延拓结果信号在首尾不连续能量泄漏到周围频率点上。本应落在第 8 个 bin 的基波能量被摊到第 6、7、9 个 bin 上谐波比值自然不稳定。电网频率还有 ±0.2Hz 的漂移问题只会更严重。解决严格把采样率设为工频的整数倍即 6400Hz50Hz×128 点/周期一帧 1024 点正好是 8 个完整周期。这样 FFT 分辨率 6.25Hz50Hz 落在 bin8、150Hz 落在 bin24所有特征分量都压在整数频点上。如果现场电网频率实际漂到 50.1Hz误差还在一帧 8 周期内被放大我的办法是开机先用定时器测两个过零点的间隔算出实际频率再动态调整定时器分频值这个自适应采样功能竞赛加分不少。5.2 电流互感器相位偏移有功功率算出来偏低现象接上纯白炽灯电压电流理论上同相可算出来功率因数只有 0.93有功功率明显比电压乘电流小。用示波器看电压和电流波形发现电流过零点和电压过零点差了差不多 3°。原因电流互感器不是理想变压器激磁阻抗让副边电流出现固定相位偏移运放的摆率和输入电容还会再加几度。这个相移在纯电阻负载上表现得最明显因为它没有任何真实相位差来掩盖系统误差。解决做一个系统相位标定。接纯白炽灯用 FFT 分别求电压和电流基波的相位角两者之差就是系统固定相移 Φ_sys存进程序里。之后每次算功率因数和有功功率都把 Φ_sys 从相位差中减掉。要注意补偿值会随电流大小轻微变化标定时电流要接近被测区间的中段如果负载范围覆盖几十瓦到几千瓦就分段标定做线性插值。这招做好后白炽灯的 PF 能算到 0.99 以上风机这种感性负载的 PF 也稳了。5.3 VREF 纹波过大同一电器两次识别结果差 5%现象同一盏灯上午识别正确下午报“未知负载”波形数据拿 PC 画出来基波幅值每次都差 2%~5%像是电压在跳。用万用表量 MCU 的 3.3V 引脚稳得看不出问题可 ADC 结果就是不重复。原因ADC 的参考电压 VREF 内部接到 VDDAVDDA 上叠加的是整流后 100Hz 纹波和板子数字开关噪声。万用表是平均值响应看不到这些毫伏级纹波但 ADC 的采样时刻正好落在纹波波峰或波谷时量化结果就偏了。FFT 把这部分纹波当成了真实信号成分。解决给 VDDA 做 LC 滤波LDO 输出串 10Ω220uF再并 1uF 陶瓷电容有条件就上独立基准芯片REF3030 输出 3.0V 接 VREF供电和参考彻底分开。检查方法非常直接把 ADC 输入对地短路采一帧做 FFT如果频谱上还能看到明显的低频谱线就说明参考电压没弄干净。这个方法后来成为我测试所有采集板的固定动作。5.4 在 DMA 中断里 printf波形出现周期性毛刺现象串口调试助手开着看波形时波形上出现周期性的小方缺口识别结果和拔出串口线后不一样。有时候数据突然丢一段主循环识别却一直没报错。原因printf 是阻塞式函数波特率 9600 下发一个字符要 1ms 左右一条调试信息打印几十毫秒。如果在 DMA 传输中断里调用 printfDMA 搬运被挂起采样窗口中间就漏掉了一截数据。中断处理时间越长丢的采样点越多而且这个缺口是周期性出现的直接污染 FFT 结果。解决中断回调里只置标志位主循环检测到标志再数据处理和串口输出。想看波形时把一帧 1024 点完整采集后一次性用 DMA 发送不要边采边打。发送期间下一帧数据可以先丢弃识别输出频率本来就不需要每帧都出一次结果丢一帧不影响判定稳定性。6. 验证与进阶串口绘图仪与最后一公里6.1 用串口把原始波形搬到 PC看看频谱再调阈值排查识别不准时光靠板载串口打印几个特征值很难看出问题根源。我的习惯是把原始电压电流序列通过串口发到 PC用 Python 画时域波形和频谱确认“信号源没问题”再去怀疑算法。STM32 端发送前把 ADC 原始值转成 int16 或直接发原始码流PC 端再做换算这样串口带宽占用小也不会因为浮点打印格式浪费大量时间。import numpy as np import matplotlib.pyplot as plt data np.genfromtxt(frame.csv, delimiter,, skip_header1) fs 6400.0 N len(data) t np.arange(N) / fs plt.subplot(2, 1, 1) plt.plot(t, data[:, 0], labelvoltage) plt.plot(t, data[:, 1], labelcurrent) plt.legend() win np.hanning(N) sp np.fft.rfft(data[:, 1] * win) freq np.fft.rfftfreq(N, d1 / fs) plt.subplot(2, 1, 2) plt.plot(freq, np.abs(sp)) plt.xlim(0, 500) plt.show()这几行脚本的重点是使用了汉宁窗。板载 FFT 用矩形窗因为采样率刻意选成整周期PC 端做离线分析时加了汉宁窗可以容忍电网频率的微小漂移两种方法互相印证。如果 PC 频谱里出现板载 FFT 没有的分量优先怀疑 DMA 配置和串口发送时序而不是算法。6.2 自适应阈值与启动过程识别从省一到国赛的最后一手稳态识别只是基本功H 题真正隐藏的难度在电器启动瞬间。风机启动时电流是稳定态的好几倍如果直接把启动段拿去算 FFT结果必然是乱码。我的策略是连续采 3 帧前两帧观察启动电流轮廓第三帧才做稳态识别。设备上电后先空载采集一次记录环境底噪包括电流互感器励磁电流和现场其他设备串进来的干扰。之后所有特征都减去这个底噪再查表。这个底噪不是常数温度变化会让互感器励磁电流缓慢漂移所以我在程序里加了一个定时任务每隔 5 分钟自动更新一次背景帧不重启系统也能保持阈值有效。从那以后我每次做交流采样项目都会强制走一遍“空载底噪采集、整周期校验、串口波形闭环”这三个步骤然后再写识别逻辑。希望帮到你。本文还有配套的精品资源点击获取