
简介围绕STM32电力数据采集系统的设计与实现展开面向嵌入式开发学习者、自动化及电气专业师生与工程技术人员可用于课程设计、毕业设计或电力监测项目方案参考。内容分硬件与软件两条主线硬件部分梳理STM32F103ZE片上资源、Cortex-M3内核特性、12位ADC多通道同步采样、定时器触发注入模式、FSMC扩展NAND FLASH以及电压电流信号调理、霍尔传感器与CT应用、GPIO开关量读取软件部分涵盖数据滤波与功率电能计算、阈值异常检测、RS485/CAN/以太网通信协议及上位机GUI交互。文档仅含1个PDF文件压缩包约45KB已有127人学习。读者可借此掌握采集系统架构、关键器件选型与软硬件协同设计思路并对照高精度、大存储、实时性、低成本等优势为实际项目搭建提供可落地参考。1. 从一块配电监测板说起STM32F103ZE 电力数据采集系统在采什么调试台上这类板子长得都差不多三路相电压、三路相电流从端子排进来旁边一排 24 路开关量光耦板子背面挂一片 NAND FLASH对外只有一路 RS485 和一路以太网。很多人的第一反应是外挂专用计量芯片或者高速并行 ADC但用 STM32F103ZE 自带的 12 位逐次逼近型 ADC配合定时器触发注入组做同步采样把 0.5 级以内的电力参数采集做出来是完全可行的BOM 成本能压下一大截。这套方案要解决的核心问题就三个把 220V/380V 和几十安培的强电安全地搬进 03.3V 的 ADC 输入窗口在多通道同时转换的前提下保住电压与电流之间的相位关系在无人值守的现场把数据可靠地存下来、传出去。下面按调理电路、固件采样、存储通信、现场校准排错几层拆开讲。2. 强电搬进 3.3V信号调理电路与 ADC 通道选型2.1 电压通道电阻分压网络与电压互感器的取舍现场进来的工频强电峰值能到 311V相电压 220V甚至 537V线电压 380V而 STM32 的 ADC 引脚绝对最大耐压只有 VDD0.3V超一点点就是锁死或者直接烧脚。所以第一级一定是降压而且要先算功耗和耐压再选器件不能拿一个 1MΩ 的 0805 电阻直接顶上去。直接电阻分压适合 400V 以下的低压配电场景成本最低。算例相电压有效值 220V峰值约 311V想在 ADC 端拿到 1.55V 峰值分压比取 1:201。上臂用 4 个 51kΩ/0.25W 金属膜电阻串联成 204kΩ下臂取 1kΩ。流过支路的峰值电流约 311V/205kΩ ≈ 1.5mA上臂总功耗约 0.33W摊到 4 个电阻上每个约 0.083W1/4W 封装留了 3 倍余量。下臂并联一个 100nF 电容和 1kΩ 组成 RC 低通截止频率约 1.6kHz用来压掉高频干扰和 ADC 采样保持回路的电荷注入。交流采样必须保留瞬时波形才能算有功功率所以 ADC 端不能只看到正半周。常见做法是给下臂串一个 1.65V 的直流偏置把 ±1.55V 的交流信号抬到 0.13.2V 之间正好落在 ADC 的线性区间内。偏置电压从 TL431 或者 REF3033 取千万别用 MCU 的 3.3V 数字电源分压那个纹波会直接进采样结果。方案适用电压精度隔离成本备注电阻分压≤400V0.2%无极低需要额外做隔离和爬电距离设计电压互感器 PT100V1kV0.1%天然隔离中有相位误差需软件补偿霍尔电压传感器任意0.5%天然隔离高温漂较大提示分压电阻要选温漂 25ppm/℃ 以内的金属膜电阻普通碳膜电阻温漂能到 300ppm夏天柜内温度上到 60℃增益直接飘掉 1%。2.2 电流通道CT、霍尔与采样电阻的组合电流通道的选型比电压通道更纠结因为要区分测的是交流还是直流。CT电流互感器只能耦合交流变比常见 1000:1 或 2000:1二次侧接一个采样电阻burden resistor转成电压。比如 1000:1 的 CT一次侧 5A 时二次侧 5mA挂 100Ω 采样电阻得到 0.5V再经一级同相放大器放大到 1.5V 送 ADC。CT 的优点是便宜、隔离好、线性度高缺点是励磁电流会带来相位超前误差在功率因数接近 1 的场合影响不大但在 0.5L 以下会明显拉低有功功率的计算精度。霍尔电流传感器开环 ACS712、闭环 LEM 系列能测直流带宽也高适合光伏、储能这类有直流分量的场景。开环霍尔零点漂移大闭环霍尔精度好但单价能到 CT 的 5 倍以上。做纯交流配电监测CT 是性价比最高的选择。方案测直流带宽相位误差成本CT 采样电阻否50kHz0.5°2°低开环霍尔是80kHz1°中闭环霍尔是200kHz0.1°高分流电阻 隔离运放是100kHz极小中CT 二次侧绝对不能开路开路瞬间二次侧会感应出上千伏电压能把放大器和 MCU 一起带走。采样电阻虚焊是现场最常见的炸板原因装配时优先用带保护的接线端子。2.3 STM32F103ZE 的 ADC 注入组与定时器同步触发STM32F103ZE 内部有 3 个 12 位逐次逼近型 ADCADCCLK 最高 14MHzPCLK2 六分频后 12MHz采样时间可编程为 1.5、7.5、41.5、55.5、71.5、239.5 个周期转换时间 采样周期 12.5/ ADCCLK。取 41.5 周期时单次转换约 4.5μs对应约 222kSPS做 64 点/周期的工频采样绰绰有余。同步采样的关键在于用定时器的 TRGO 事件同时触发多个 ADC而不是让 CPU 挨个去启动。规则组通道多、支持 DMA但每个 ADC 同一时刻只能有一个规则组在转换注入组每路有独立的 JDR1JDR4 寄存器四路注入通道的转换结果能锁存在同一时刻相位一致性更好。所以三相电压电流这类必须同相采集的信号走注入组慢速的温度、直流母线电压这类走规则组加 DMA。/* 1. 关闭 JTAG保留 SWD释放 PA15/PB3/PB4 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); /* 2. ADCCLK PCLK2 / 6 12MHz */ RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* 3. 注入组配置4 路顺序 IA - IB - IC - UA */ ADC_InitTypeDef adc; ADC_StructInit(adc); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; /* 注入组按序列自动扫描 */ adc.ADC_ContinuousConvMode DISABLE; /* 由定时器触发单次 */ adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_InjectedSequencerLengthConfig(ADC1, 4); ADC_InjectedChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_41Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_41Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_41Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_41Cycles5); /* 4. TIM1 更新事件作为 TRGO触发注入转换 */ ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_T1_TRGO); ADC_ExternalTrigInjectedConvCmd(ADC1, ENABLE); ADC_ITConfig(ADC1, ADC_IT_JEOC, ENABLE); /* 注入组只能用中断读没有 DMA */ ADC_Cmd(ADC1, ENABLE);逻辑说明先做一次ADC_ResetCalibration/ADC_StartCalibration校准再使能 ADC。参数上采样时间选 41.5 周期是精度和速度的折中信号源内阻越大比如分压网络等效输出阻抗高采样时间就得越长否则采样保持电容充不满读数会偏低。注入组在 F1 系列上不支持 DMA只能在 JEOC 中断里依次读ADC_GetInjectedConversionValue()所以中断频率等于采样率4kHz 中断对 72MHz 的 M3 来说压力不大但中断服务程序里别做浮点运算。2.4 开关量输入与 JTAG 引脚复用、外设 CS 管理开关量信号代表继电器、接触器的分合状态现场一般用光耦隔离后再进 GPIO配合 10kΩ 上拉和 100nF 滤波。软件上不能只靠轮询否则高频抖动能把计数器刷爆常见做法是 1ms 定时器里做状态机去抖连续 5 次读到同一状态才更新同时开 EXTI 下降沿中断做快速响应。这里有个新手极易踩的坑PA15、PB3、PB4 默认是 JTAG 的 JTDI、JTDO、NJTRST 复用功能直接配成 GPIO 输入是不生效的必须调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)关掉 JTAG、保留 SWD。同理用 SPI 挂外部 EEPROM 或者 Flash 时片选 CS 引脚如果落在这几个脚上也会出现「IO 翻转正常但外设毫无反应」的现象——这就是常说的 stm32 cs 管理问题排错时先查引脚复用表再看 CS 时序别一上来就怀疑芯片。/* 开关量输入初始化PB3 作为 DI1需先释放 JTAG */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitTypeDef io; io.GPIO_Pin GPIO_Pin_3; io.GPIO_Mode GPIO_Mode_IPU; /* 上拉输入光耦导通时拉低 */ io.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, io); /* EXTI3 下降沿触发 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource3); EXTI_InitTypeDef exti; exti.EXTI_Line EXTI_Line3; exti.EXTI_Mode EXTI_Mode_Interrupt; exti.EXTI_Trigger EXTI_Trigger_Falling; exti.EXTI_LineCmd ENABLE; EXTI_Init(exti);参数说明GPIO_Mode_IPU是上拉输入对应光耦集电极开路输出如果前端是推挽输出改成GPIO_Mode_IN_FLOATING并外置下拉。EXTI 中断里只置标志位实际状态判定放到主循环或定时器任务里做避免中断里做长延时。3. 定时器触发 三重同步采样固件层的数据搬运与功率计算3.1 规则组 DMA 搬运与 ADC 三重同步模式注入组解决了同相采样但采样点数一多中断开销就上来了。做谐波分析要 128 点甚至 256 点每周期这时候换个思路把 ADC1 配成主、ADC2 和 ADC3 配成从工作在ADC_Mode_RegSimult三重同步模式三个 ADC 的规则通道由同一个 TIM2 TRGO 同时触发转换结果分别落到各自的 DR 寄存器只有 ADC1 的 DR 请求 DMA。这样一次搬运就能拿到三路同相数据CPU 完全不参与。采样周期数ADCCLK12MHz 转换时间适用场景1.51.17μs高速但要求源阻抗极低7.51.67μs运放直驱41.54.5μs分压网络、CT 采样电阻239.521μs高阻信号源、内部温度通道/* ADC1 规则组 DMA三重同步模式 */ ADC_InitTypeDef adc; adc.ADC_Mode ADC_Mode_RegSimult; /* 三重同步 */ adc.ADC_ScanConvMode ENABLE; adc.ADC_ContinuousConvMode DISABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 3; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_41Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_41Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 3, ADC_SampleTime_41Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); /* DMA1 通道1循环模式目标缓冲区 3*N 个半字 */ 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 3 * SAMPLE_N; 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); DMA_Cmd(DMA1_Channel1, ENABLE);逻辑说明DMA_Mode_Circular让缓冲区自动回卷主循环只需在半传输和传输完成中断里翻页处理省掉一次内存拷贝。参数上DMA_PeripheralDataSize必须是半字因为 ADC 的 DR 是 16 位DMA_BufferSize要等于通道数乘以每周期采样点数改采样率时这个值要同步改否则会出现通道错位——表现是 A 相电压的数据跑到 B 相电流里去了。3.2 采样值标定与真有效值计算ADC 拿到的是 04095 的原始码值先减掉偏置对应的码值再乘标定系数才是真实的电压电流瞬时值。三点必须注意偏置码值要在零输入时实测不能拍脑袋用 2048标定系数用两点法算比单点缩放准得多所有中间运算用int32_t或float别用int16_t硬扛平方运算很容易溢出。真有效值的做法是每周期采 N 点求平方和再开方。电压电流共用一套#define SAMPLE_N 64 /* u_buf / i_buf 为已扣除偏置、折算成实际工程量的浮点数组 */ float calc_rms(const float *buf, uint16_t n) { float sum 0.0f; for (uint16_t k 0; k n; k) { sum buf[k] * buf[k]; /* 累加平方注意用 float 防溢出 */ } return sqrtf(sum / n); /* 均方根 */ } /* 有功功率同相瞬时值乘积的平均 */ float calc_active_power(const float *u, const float *i, uint16_t n) { float sum 0.0f; for (uint16_t k 0; k n; k) { sum u[k] * i[k]; /* 电压电流必须同相采样否则功率偏小 */ } return sum / n; }参数说明SAMPLE_N取 64 是 50Hz 下每周波 64 点采样率 3.2kHz对 20 次以内的谐波1kHz满足奈奎斯特定理。如果要测到 50 次谐波SAMPLE_N要提到 256采样率 12.8kHz此时 ADC 转换时间必须压到 7.5 周期以内DMA 缓冲区也要同步扩大 4 倍。相位偏差会直接体现成功率误差电压电流之间差 1° 相位功率因数 0.9 时功率误差约 0.5%所以 CT 的相位补偿系数必须实测标定。3.3 有功功率与电能累计瞬时功率算出来以后电能就是功率对时间的积分离散化之后就是累加/* 每 20ms 调用一次一个工频周期 */ void energy_accumulate(float p_watt) { static float energy_wh 0.0f; /* 20ms 20/3600/1000 小时 */ energy_wh p_watt * (20.0f / 3600000.0f); if (energy_wh 0.01f) { /* 每 0.01Wh 存入一次减少擦写 */ eeprom_save_energy(energy_wh); energy_wh 0.0f; } }参数说明累加周期必须和实际采样窗口对齐如果采样窗口是 20ms 但定时器实际周期是 20.5ms长时间跑下来电能会有 2.5% 的系统误差。稳妥做法是用定时器的实际计数值反推时间或者直接用 RTC 的秒脉冲做长时间基准。掉电保存不能在主循环里直接写 Flash要开 PVD 电压监测中断检测到掉电后靠电容储能完成最后一次写入。4. FSMC 挂 NAND FLASH 与 RS485 上报存储和通信的落地细节4.1 FSMC 地址映射与 NAND FLASH 片选时序STM32F103ZE 的 FSMC 可以把 NAND FLASH 直接映射到内存空间CPU 读写就像访问数组一样。NAND 挂在 Bank2 时基址是 0x70000000A16 接 CLE命令锁存、A17 接 ALE地址锁存、NE2 接片选 NCE这三个信号决定了三个不同的访问地址。理解这个映射是写驱动的前提也解释了为什么 stm32 cs 在 FSMC 场景下不是普通 GPIO而是由硬件自动拉低的。/* Bank2 地址映射A16CLE, A17ALE, NE2片选 */ #define NAND_CMD_AREA ((uint32_t)0x70000000) /* A161 */ #define NAND_ADDR_AREA ((uint32_t)0x70020000) /* A171 */ #define NAND_DATA_AREA ((uint32_t)0x70040000) /* 均不置位读写数据 */ static void nand_write_cmd(uint8_t cmd) { *(volatile uint8_t *)NAND_CMD_AREA cmd; } static void nand_write_addr(uint8_t addr) { *(volatile uint8_t *)NAND_ADDR_AREA addr; } /* 读 ID0x90 命令 0x00 地址然后连读 5 字节 */ uint32_t nand_read_id(void) { nand_write_cmd(0x90); nand_write_addr(0x00); uint8_t id[5]; for (int k 0; k 5; k) { id[k] *(volatile uint8_t *)NAND_DATA_AREA; } return (id[0] 24) | (id[1] 16) | (id[2] 8) | id[3]; }逻辑说明时序参数在FSMC_NANDInitTypeDef里的FSMC_TCLR、FSMC_TAR、FSMC_THOLD、FSMC_THIZ四个字段单位是 HCLK 周期。K9F1G08 这类常见 NANDTCLR 取 2、TAR 取 2、THOLD 取 2 基本能跑通如果读 ID 返回全 0xFF 或者全 0x00八成是片选没选中或者时序太快先把FSMC_Bank2_NAND的FSMC_SetupTime调大试试。硬件 ECC 记得开FSMC_NANDInitStructure.FSMC_ECC设为 ENABLE每 512 字节自动算一次省掉软件 ECC 的 CPU 开销。4.2 数据记录格式与坏块管理电力数据的存储策略是「循环覆盖 定长记录」。每条记录固定 32 字节时间戳 4 字节、三相电压电流各 2 字节共 12 字节、有功功率 4 字节、电能 4 字节、开关量状态 4 字节、CRC16 2 字节、预留 2 字节。按每秒一条算一天 86400 条不到 3MB1Gb 的 NAND 能存小半年。坏块管理用最简单的方式就够NAND 的每个块第 1 页 spare 区的第 1 个字节是坏块标记非 0xFF 即为坏块上电时扫一遍建表写满一块就换下一块。擦除是 NAND 的最小单位512 页一块的 K9F1G08 每块 128KB擦除时间约 2ms写入前必须先擦除别指望能原地改写。4.3 Modbus RTU over RS485 的收发切换对外通信走 RS485 半双工用 Modbus RTU 从站协议最省事上位机侧几乎所有组态软件都支持。帧格式地址 1 字节 功能码 1 字节 数据 N 字节 CRC16 2 字节帧间至少 3.5 个字符时间的静默作为分隔。功能码含义本项目用途0x03读保持寄存器读实时电压电流、功率0x04读输入寄存器读电能累积值0x06写单个寄存器设置 CT 变比、报警阈值0x10写多个寄存器批量下装参数驱动上最容易翻车的是收发切换时刻。发送完最后一个字节后立刻拉低 DE最后一个字节会因为移位寄存器还没发完而被截断上位机看到的就是 CRC 错误。正确做法是等 USART 的 TC 标志置位再切。/* RS485 发送拉高 DE发完等 TC 再拉低 */ #define RS485_DE_H() GPIO_SetBits(GPIOB, GPIO_Pin_9) #define RS485_DE_L() GPIO_ResetBits(GPIOB, GPIO_Pin_9) void rs485_send(const uint8_t *buf, uint16_t len) { RS485_DE_H(); for (uint16_t k 0; k len; k) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, buf[k]); } while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); RS485_DE_L(); /* 必须等 TC不是 TXE */ }参数说明USART_FLAG_TXE只表示发送数据寄存器空可以塞下一个字节USART_FLAG_TC才表示移位寄存器也发完了。很多人用 TXE 关 DE通信时好时坏查半天查不出原因。另外 RS485 总线两端要各接一个 120Ω 终端电阻中间节点不要接节点数超过 32 个要换低负载的收发器。5. 两点校准与现场排错把采样误差压到 0.5 级以内5.1 两点校准法消除增益与偏移误差ADC 的偏移误差和增益误差是分开的单点校准只能修一个。标准做法是给两个已知输入比如电压通道加 20% 和 80% 额定值记录原始码值码₁、码₂和对应的标准值标₁、标₂解出线性系数/* 两点标定x 为 ADC 原始码值y 为标准表读数 */ void calib_two_point(uint16_t code1, float std1, uint16_t code2, float std2, float *k, float *b) { *k (std2 - std1) / ((float)code2 - (float)code1); /* 增益 */ *b std1 - (*k) * (float)code1; /* 偏移 */ } /* 实际使用y k * code b */参数说明两个校准点要拉开至少 50% 量程靠得太近时code2-code1很小除法会把噪声放大。校准要在常温、额定负载下做做完把 k、b 存进 Flash 或者 MCU 的选项字节。CT 通道还要额外做相位补偿方法是加纯阻性负载观察功率因数的偏差角在固件里对电流数组做插值移相。5.2 现场常见故障对照现象可能原因排查顺序采样值整体偏低 5%采样时间不足保持电容没充满增采样周期数 → 查源阻抗某相数据始终为 0DMA 缓冲区通道错位查 BufferSize → 查序列配置通信时好时坏DE 切换用 TXE 而非 TC改代码 → 示波器看波形程序卡在 delay 里SysTick 被高优先级中断抢占查 NVIC 优先级分组上电后 IO 无反应JTAG 复用引脚未释放加 SWJ_JTAGDisable 重映射读 NAND 全 0xFF片选或时序配置错误调 TCLR/TAR → 量 NCE 波形delay函数卡死是个高频问题如果 ADC 中断的抢占优先级高于 SysTick而中断服务程序执行时间又超过了 1msdelay_ms里的计数值永远不会被更新程序就死在那儿了。解决办法是把 SysTick 的优先级设到最高或者干脆用 DWT 的周期计数器做延时不受中断影响。5.3 用定时器输入捕获测频做电网异常判据电力数据采集系统除了采数据还得判断电网是否正常。把一路电压信号经过比较器整形成方波送进 TIM3 的 CH1用输入捕获测周期就能实时拿到电网频率。频率超出 49.550.5Hz 范围持续 200ms判为频率异常同时结合电压有效值低于 80% 额定值判欠压高于 110% 判过压。这三个判据配合起来比单纯看某一相电压是否越限要可靠得多能有效滤掉电机启动瞬间的电压跌落误报。/* TIM3 CH1 输入捕获上升沿捕获两次捕获值相减即周期 */ void TIM3_IRQHandler(void) { static uint16_t last_ccr 0; if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { uint16_t ccr TIM_GetCapture1(TIM3); uint16_t period ccr - last_ccr; /* 1MHz 计数周期值即微秒 */ last_ccr ccr; g_freq_x100 100000000UL / period; /* 频率 ×100整数运算 */ TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } }参数说明定时器预分频设成 71计数时钟 1MHz一个工频周期 20000 个计数16 位计数器放得下但接近上限建议开溢出中断做扩展计数。period直接相减能正确处理计数器回卷这是无符号减法的特性不用额外判断。测到的频率值再做 16 点滑动平均把比较器抖动带来的 ±0.2Hz 波动滤掉判据才稳。本文还有配套的精品资源点击获取