STM32电力数据采集:ADC+DMA与Modbus-RTU实践
2026/9/17 17:55:20 网站建设 项目流程

简介:《基于STM32电力数据采集系统的设计分析.pdf》是一份面向嵌入式开发、电力监测及自动化专业学习者的技术文档,围绕STM32F103ZE平台讲解电力数据采集系统的硬件与软件实现思路。资源包内仅含1个PDF文件,压缩包约45KB,适合快速查阅与方案参考。目前已有127人学习浏览,说明其在单片机课程设计、毕业设计及电力采集入门场景中具有一定关注度。文档内容涵盖模拟量采集中的电压分压、电流霍尔/CT信号调理、ADC多通道同步采样,开关量经GPIO中断或轮询读取,以及FSMC扩展NAND FLASH、RS485/CAN/以太网通信和上位机GUI交互等模块;同时讨论滤波、功率电能计算、阈值异常检测与精度、实时性、成本、扩展性等系统优势。读者可从中获取从采集前端到数据处理、远程监控的整体设计框架,为STM32电力数据采集项目提供结构化的参考脉络。

1. 电力数据采集系统为什么适合落在STM32上

配电站的智能配电柜里,一次侧要盯三相电压、三相电流、有功、无功、功率因数、频率和谐波含量,采集板往往还要给数据打时标、经 Modbus 或以太网上送,并驱动继电器执行越限告警。老方案是计量芯片加一颗 MCU 各管一摊,BOM 和面积都不小。把计量与逻辑统一收到 STM32 上,现在已很常见:STM32 的定时器能给出精确的采样节拍,ADC 加 DMA 可以连续搬运三路电压、三路电流的采样点,Cortex-M4 的 FPU 和 DSP 指令实时算出有效值与功率,片上 UART、CAN、以太网恰好对上电力协议。这套设计适合做配电监测终端、储能采集侧、充电桩计量模块的工程师,也适合作为基于 STM32 的毕业设计主线,把 ADC、定时器、DMA、通信外设一次串透。

2. STM32电力数据采集的模拟前端与器件选型

2.1 电压电流互感器与信号调理链路的选型边界

电力数据采集的第一步是把高电压大电流变成 STM32 的 ADC 能吃进去的 0~3.3V。电压侧常见做法是 ZMPT101B 这类微型电压互感器,电流侧用 TA1005 或开合式电流互感器,大电流直流场合则用分流器或者霍尔。互感器出来都是交流小信号,需要抬到 ADC 的中间电平,也就是 Vref/2 = 1.65V 附近。

调理链路的元件选型有三个硬指标。抗混叠滤波器的截止频率要跟采样率挂钩,采样率 6.4kHz 时截止频率不能高于 3.2kHz,一般取 1.5kHz 到 2kHz,用两级 RC 或者运放搭二阶 Sallen-Key。运放的带宽和压摆率要够,TLV9064、OPA2376 这类轨到轨运放比较稳。偏置电压必须用低噪声基准,别直接拿 AMS1117 的 3.3V 分压,LDO 的纹波会原封不动进 ADC。

互感器的线性区间也要看。ZMPT101B 在额定电压附近线性最好,超过 1.2 倍额定后相移开始变大,做功率计量时相移直接变成功率因数误差,所以电压通道的分压电阻要按最大工作电压再留 30% 余量设计。

2.2 STM32 系列选型与 ADC 通道分配

选型取决于同步采样路数和精度要求。常见几款对比如下。

型号内核ADC同步采样能力适合场景
STM32F103C8T6Cortex-M3 72MHz2 个 12 位,1Msps双 ADC 规则同步单相、教学验证
STM32F407VGT6Cortex-M4F 168MHz3 个 12 位,2.4Msps三重 ADC 交替/同步三相计量
STM32G474RET6Cortex-M4F 170MHz5 个 12 位,4Msps多 ADC 同步加 HRTIM高精度三相
STM32F334C8T6Cortex-M4F 72MHz2 个 12 位双 ADC 加 HRTIM数字电源

三相四线要同时采三路电压三路电流,理论最少要 6 个同步通道。F103 只有两个 ADC,做不到六路同时采样,只能用多路开关轮询,会引入相间相位误差。F407 三 ADC 可以做到三路电压同时采、三路电流同时采,再用交替模式把等效采样率翻倍。STM32G474 的 ADC 更多,还带硬件过采样,适合要做 0.5 级计量的场合。

通道分配上一般这样安排:ADC1 采 Ua/Ub/Uc,ADC2 采 Ia/Ib/Ic,ADC3 留给温度、直流母线等高通道,或者与 ADC1 交替提高电压通道的采样率。GPIO 上要避开 JTAG 占用的 PA13/PA14/PA15/PB3/PB4,这些脚做模拟输入不划算,需要时用__HAL_AFIO_REMAP_SWJ_NOJTAG()把 JTAG 禁用掉再释放。

2.3 采样电阻、偏置与量程的参数计算

互感器二次侧是电流输出,要并一个采样电阻转成电压。以 5A/5mA 的电流互感器为例,次级并联 50Ω 得到 250mV 的交流幅值,再叠加 1.65V 偏置,落在 ADC 的 0.9V~2.4V 区间,浪费了一部分量程。想让小信号分辨率更好,采样电阻可以取到接近极限:3.3V 满量程、留 10% 余量后允许幅值 1.45V,峰值对应有效值 1.02V,采样电阻就取 1.02/0.005 ≈ 200Ω。计算过程可以写个小脚本,避免手算出错。

# 采样电阻与量程估算 VREF = 3.3 # ADC 参考电压 MARGIN = 0.9 # 留 10% 余量 CT_RATIO = 5.0 / 0.005 # 5A / 5mA 电流互感器变比 Vpk_allowed = VREF / 2 * MARGIN # 单边可用的峰值电压 Irms_max = 5.0 # 一次侧额定电流 Isec_rms = Irms_max / CT_RATIO # 二次侧有效值 = 5mA Rburden = (Vpk_allowed / 1.414) / Isec_rms print(f"采样电阻建议值: {Rburden:.1f} Ω") print(f"ADC 量化步长: {VREF/4096*1000:.3f} mV/LSB")

这段脚本做的事很直接:先按参考电压一半再乘 0.9 算出允许的交流峰值,除以根号 2 换成有效值,再除以二次侧额定电流,得到采样电阻的阻值。输出的量化步长告诉我们每个 LSB 对应多少毫伏,用来判断分辨率够不够——0.8mV/LSB 配合 200Ω 采样电阻,一次侧能分辨约 2mA,做 1% 精度的电表绰绰有余。

提示:偏置网络用两个 10kΩ 电阻分压得到 1.65V 时,务必在分压点并一个 10uF 加 100nF 的电容,否则 3.3V 电源上的开关噪声会原封不动地被采进 ADC。

3. 基于STM32定时器触发ADC与DMA的连续采集实现

3.1 为什么软件轮询采不出可用的电力波形

很多人第一次做基于 STM32 的电力采集,会在主循环里依次HAL_ADC_GetValue()轮流读通道,然后自己算有效值。这样采出来的波形看着像正弦,但一算功率因数就飘。原因是软件触发的采样间隔完全由代码执行时间决定,中断一来、if 一分支,采样间隔就抖,等效于对信号做了随机时间偏移的采样,相位信息被破坏。

正确做法是让定时器做采样节拍源。TIM 的更新事件经 TRGO 输出到 ADC 的触发输入,ADC 每收到一个触发就转换一次,转换完 DMA 把数据搬进缓冲区,CPU 全程不参与。这样采样间隔由定时器时钟决定,抖动在纳秒级。想测频率的话,顺带用 TIM 的输入捕获通道接一路过零比较信号,就能同步知道当前工频周期,这一路输入捕获测频率的做法在很多 STM32 项目里是标配。

3.2 TIM+ADC+DMA 的配置代码与关键寄存器

以 STM32F407 三重 ADC 同步采样为例,用 CubeMX 生成骨架后手改几处关键配置。

/* ADC1/2/3 多重同步模式初始化,定时器 TIM2 TRGO 触发 */ static void MX_ADC_Multi_Init(void) { ADC_MultiModeTypeDef multi = {0}; hadc1.Instance = ADC1; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ENABLE; /* 扫描三路电压 */ hadc1.Init.ContinuousConvMode = DISABLE; /* 由触发启动,不连续 */ hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONV_EDGE_RISING; hadc1.Init.DMAContinuousRequests = ENABLE; /* DMA 循环模式必须开 */ hadc1.Init.NbrOfConversion = 3; HAL_ADC_Init(&hadc1); /* 多重模式:三重 ADC 交替,等效采样率翻三倍 */ multi.Mode = ADC_TRIPLEMODE_INTERLACED; multi.DMAAccessMode = ADC_DMAACCESSMODE_2; /* 公共 DMA 搬 32 位 */ multi.TwoSamplingDelay = ADC_TWOSAMPLINGDELAY_5CYCLES; HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multi); }

这段配置里有三个地方最容易出错。ExternalTrigConv必须指向T2_TRGO,如果定时器的 TRGO 输出没配成 Update 事件,ADC 就永远等不到触发。DMAContinuousRequests要开,否则一轮转换搬完 DMA 就停了。ADC_DMAACCESSMODE_2表示用 ADC1 的 DMA 通道同时搬三个 ADC 的数据,32 位一次装 3 个 12 位结果,高 4 位丢弃,这样 CPU 拿到的就是一个结构整齐的采样帧。

定时器一侧:

/* TIM2 输出 TRGO 作为 ADC 采样节拍 */ static void MX_TIM2_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 0; htim2.Init.Period = 168000000 / 6400 - 1; /* 168MHz/6.4kHz = 26250 */ htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim2); TIM_MasterConfigTypeDef sMaster = {0}; sMaster.MasterOutputTrigger = TIM_TRGO_UPDATE; /* 更新事件即 TRGO */ sMaster.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMaster); }

Period是按 6.4kHz 采样率算的,168MHz 的定时器时钟除以 26250 正好是 6400Hz。这个值跟着目标采样率走,改采样率就重装 Period 或改 Prescaler,不要两边都动,不然频率不好对。

3.3 采样率、每周波点数与频率跟踪的参数关系

电力算法喜欢整周期采样,离散傅里叶才好做。50Hz 工频下,每周波点数决定了谐波分析的上限。

每周波点数采样率(50Hz)可分析谐波次数一周期采样缓冲区(3 相)
643.2kHz到 31 次192 个 uint16
1286.4kHz到 63 次384 个 uint16
25612.8kHz到 127 次768 个 uint16
51225.6kHz到 255 次1536 个 uint16

按 128 点算,6.4kHz 采样率下每个 ADC 采样间隔 156.25us,三重交替后等效 19.2k 次转换/s,F407 的 ADC 2.4Msps 完全扛得住。缓冲区用两个半区交替,DMA 搬满一半就半满中断里做一次 FFT,另一半继续采,这是双缓冲的标准打法。

工频会漂,49.8Hz 到 50.2Hz 都算正常。固定采样率的后果是采样窗口跟信号周期不严格对齐,会漏频谱。解决办法有两种:一种是用过零检测测出实际周期,反过来算 TIM 的 Period 重装载值,做频率跟踪;另一种是采样率固定、在算法侧做准同步修正。前一种实时性好,后一种实现简单,做 0.5 级以下精度时前一种更划算。

/* 过零检测修正 TIM2 的重装载值 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t last_cap = 0; if (GPIO_Pin == ZERO_CROSS_PIN) { uint32_t now = __HAL_TIM_GET_COUNTER(&htim5); uint32_t period_ticks = (now - last_cap) & 0xFFFF; last_cap = now; /* 一个工频周期内要采 128 点,重装值取周期计的 1/128 */ uint32_t new_period = period_ticks / 128; __HAL_TIM_SET_AUTORELOAD(&htim2, new_period); } }

回调里用 TIM5 自由运行计一个周波的 tick 数,除以 128 得到新的重装载值,写回 TIM2 的自动重载寄存器。每次改 ARR 最好在更新事件之后再写,否则当前周期会被截断。周期跳变时要做限幅,防止一次误触发把采样率带跑偏。

注意:DMA 缓冲区要按uint32_t对齐,三重模式下每个元素高 4 位是无效数据,取数时先和 0x0FFF 掩码再移位,不然算出来的有效值会大得离谱。

4. 电力参数计算与Modbus-RTU上传的落地细节

4.1 有效值、有功功率与功率因数的计算实现

缓冲区拿到的是三路电压三路电流交错的原始码值。第一步减偏置,第二步乘标定系数,第三步才算电参量。F407 带 FPU,用 float 直接算就行;如果跑 F103,最好转成 Q 格式定点。

#define SAMPLE_NUM 128 #define ADC_MID 2048.0f /* 1.65V 对应码值 */ #define U_SCALE (250.0f / 2048.0f) /* 电压通道标定系数 V/LSB */ #define I_SCALE (20.0f / 2048.0f) /* 电流通道标定系数 A/LSB */ typedef struct { float urms; /* 相电压有效值 V */ float irms; /* 相电流有效值 A */ float p; /* 有功功率 W */ float q; /* 无功功率 var */ float pf; /* 功率因数 */ } PhaseParam; /* in: 交错采样的 uint16 缓冲区,每 6 个一组 [ua,ia,ub,ib,uc,ic] */ void CalcPhase(volatile uint16_t *buf, PhaseParam out[3]) { float sum_u2[3] = {0}, sum_i2[3] = {0}, sum_ui[3] = {0}; for (int n = 0; n < SAMPLE_NUM; n++) { for (int ph = 0; ph < 3; ph++) { float u = (buf[n * 6 + ph * 2] - ADC_MID) * U_SCALE; float i = (buf[n * 6 + ph * 2 + 1] - ADC_MID) * I_SCALE; sum_u2[ph] += u * u; sum_i2[ph] += i * i; sum_ui[ph] += u * i; } } for (int ph = 0; ph < 3; ph++) { out[ph].urms = sqrtf(sum_u2[ph] / SAMPLE_NUM); out[ph].irms = sqrtf(sum_i2[ph] / SAMPLE_NUM); float p = sum_ui[ph] / SAMPLE_NUM; float s = out[ph].urms * out[ph].irms; out[ph].p = p; out[ph].q = sqrtf(s * s > p * p ? s * s - p * p : 0.0f); out[ph].pf = s > 1e-6f ? p / s : 0.0f; } }

这段代码的逻辑链条是:码值减中点得到带符号的交流瞬时值,乘标定系数还原成实际电压电流,再在一个工频窗口里分别累加 u²、i²、ui。有效值是均方根,有功功率是有功分量的均值,视在功率是有效值相乘,无功开方出来,功率因数就是有功比视在。s*s > p*p ? ... : 0这个判断是为了防止因测量噪声导致开根号出现负数。整段计算在 168MHz 的 M4 上,三相加起来约 40us 就出结果,完全跟得上 128 点一个窗口的节拍。

提示:减法后的u是带符号浮点,别用 uint16 存中间值,一旦出现负半周被截断的现象,功率因数会稳定漂到 0.9 附近下不来。

4.2 Modbus-RTU 从站寄存器映射设计

算出来的电参量要上送,电力行业最省事的就是 Modbus-RTU。寄存器映射建议按 03 功能码读保持寄存器,每个参数两个寄存器,放到的是 IEEE754 浮点的两个 16 位字,或者乘 100 之后的 Q16 整数。用放大整数的做法兼容性更好,老的组态软件不会因为浮点字节序问题读错。

寄存器地址参数格式系数单位
0x0000~0x0001Ua 有效值uint32 大端×100V
0x0002~0x0003Ia 有效值uint32 大端×1000A
0x0004~0x0005Pa 有功int32 大端×10W
0x0006~0x0007PFaint16×1000
0x0008~0x0009频率uint16×100Hz
0x000A~0x000B有功电能uint32×100kWh
0x0010越限告警标志uint16 位域

系数怎么定看量程。电压量程 250V、放大 100 倍后是 25000,uint16 装得下,用 uint32 是为了三相统一留余量。电流量程 20A、放大 1000 倍是 20000,也够。电能累加要放在定时器里按秒累加,别在通信中断里算,掉电前存进片内 Flash 或者外挂 FRAM。

4.3 串口 DMA 接收与帧解析的时序处理

Modbus-RTU 靠 3.5 个字符间隔判帧边界,115200 波特率下约 305us。ST 的 HAL 库配空闲中断加 DMA 可以比较干净地实现,主流程只在空闲中断里判断帧长和 CRC。

/* 串口空闲中断 + DMA 接收 Modbus 请求帧 */ void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); HAL_UART_DMAStop(&huart1); uint16_t len = MODBUS_BUF_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx); if (len >= 8 && len <= 256) { uint16_t crc = modbus_crc16(mb_rx_buf, len - 2); uint16_t recv = mb_rx_buf[len - 2] | (mb_rx_buf[len - 1] << 8); if (crc == recv) { modbus_slave_process(mb_rx_buf, mb_tx_buf, &tx_len); HAL_UART_Transmit_DMA(&huart1, mb_tx_buf, tx_len); } } /* 重新武装接收,缓冲区必须清零 */ memset(mb_rx_buf, 0, MODBUS_BUF_LEN); HAL_UART_Receive_DMA(&huart1, mb_rx_buf, MODBUS_BUF_LEN); } HAL_UART_IRQHandler(&huart1); }

__HAL_DMA_GET_COUNTER拿到的是剩余未搬的字节数,用缓冲总长减掉它才是这一帧的实际长度。CRC 校验放在帧长判断之后,先卡长度再卡 CRC,避免半帧被当成合法请求。发送回帧用 DMA 而不是阻塞式HAL_UART_Transmit,否则 256 字节在 115200 下要堵 22ms,会直接挤掉采样中断的响应时间。回帧发完要等 TC 标志再开下一次接收,不然会出现收发打架。

注意:Modbus 从站地址、波特率、校验位这几个参数最好存片内 Flash 且带 CRC,现场改完配置重启后要能恢复,别硬编码在源码里。

5. 采集系统的抗干扰布局与精度验证技巧

5.1 模拟前端布板与供电噪声的几个致命细节

采样精度掉到 1% 以外,九成问题出在布板而不是算法。模拟地和数字地单点连接是基础,重点是那个连接点的位置:要选在 ADC 的 AGND 引脚附近,而不是电源入口。ADC 的参考电压脚要并 1uF 加 100nF,走线尽量短粗。互感器到调理运放的走线要成对等长,避开定时器、晶振、DC-DC 这些噪声大户。

电源这块有个容易踩的坑:AMS1117 这类老 LDO 的输出电容原来是钽电容,有人图省事换成等容量的陶瓷电容,结果 LDO 环路相位裕度不够,输出自激振荡,ADC 读出来全是大数在跳。换成陶瓷电容时要看数据手册推荐的 ESR 范围,必要时串一个零点几欧的电阻补偿。模拟电源和数字电源各走各的 LDO,别共用一路。

5.2 用标准源比对与两点校准的验证流程

硬件焊完先别急着测真电网,用标准信号源给额定值,看读数偏多少。校准用两点法:先给满量程的 20% 记录读数,再给 100% 记录读数,解一个一次方程得到增益和偏置,把这两个系数写进代码的标定系数里。三相分别校,别指望三相一致性天然就好,互感器本身有分散性。

验证时重点看几个指标:电压电流读数误差跟标准源读数比,0.5 级表要求误差在 ±0.5% 以内;功率因数 1.0 和 0.5L 两个点都要测,0.5L 时误差容易放大;连续运行 24 小时看零点漂移,断开输入信号时有效值读数应小于量程的 0.2%;谐波含量用标准源注入 3、5、7 次,看 FFT 结果是否落在正确频点。

# 用标准源注入 220V/5A/50Hz,记录 30 组读数算相对误差 for i in $(seq 1 30); do # modpoll 读保持寄存器 0x0000 起连续 12 个,地址 1,115200-8N1 modpoll -m rtu -a 1 -b 115200 -p none -r 1 -c 12 /dev/ttyUSB0 sleep 1 done | tee reading.log

这段脚本用 modpoll 每秒读一次从站的前 12 个寄存器,连续采 30 组,输出重定向到文件方便后面算均值和最大偏差。参数里-a是从站地址,-r是起始寄存器,-c是寄存器个数,一条命令就把三相电压电流全读回来。做验证时把标准源读数填到表格另一列,两组数据一减就是绝对误差。

5.3 采样不动、DMA 不触发、delay 卡死的排查顺序

现场调试会碰到几类固定症状,按顺序查能省很多时间。ADC 一直不转换,先看 TIM 的 TRGO 有没有真的输出,把 TRGO 引到一个空闲 GPIO 上用示波器打一下最直接。DMA 搬一次就停,检查DMAContinuousRequests和 DMA 本身的循环模式是不是都开了,两个地方是分开配的。程序跑着跑着卡在HAL_Delay里出不来,八成是某个中断优先级比 SysTick 高又长时间不退出,把采样中断的抢占优先级设成比 SysTick 低,或者干脆别在中断里做浮点大循环。

还有个隐蔽的坑是重映射。把 SWJ 关了释放 PA13~PA15 之后忘了留 SWD,就再也连不上调试器,只能靠 BOOT 引脚进系统存储器擦除。释放 JTAG 但保留 SWD 用__HAL_AFIO_REMAP_SWJ_NOJTAG(),别用SWJ_DISABLE。采样通道选择上,别把 ADC 引脚配成带内部上拉的复用模式,上拉电阻会跟互感器的采样电阻并联,标定系数直接偏掉,这种问题示波器看不出来,只有比对读数才发现。

工频频率和采样率的关系在长时间运行里会漂,跑满 24 小时后重新算一次频率跟踪的重装载值,比一次标定管到底要靠谱。采集系统在现场跑的是加速度和温漂,不是一次实验室数据,把重装载值的上下限卡死在 ±2% 范围,超限就退回默认值报警,比等算法发散再回头查要省事得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询