news 2026/9/1 1:18:27

HLW8112电能计量芯片开发:STM32通过SPI与UART读取数据全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HLW8112电能计量芯片开发:STM32通过SPI与UART读取数据全解析

简介:本资源是一套面向嵌入式开发工程师与STM32初学者的HLW8112电能计量芯片实战开发例程,聚焦SPI与UART双通信接口在STM32平台上的完整驱动实现,解决电能参数(电压、电流、功率、电能)采集与传输的核心问题。压缩包共191个文件,含47个C源码(如stm32f4xx_tim.c、stm32f4xx_rtc.c等外设驱动)、45个头文件(定义寄存器映射与协议结构)、25个编译中间文件(.o/.d/.crf),以及Keil工程配置文件(.uvproj/.uvopt)、烧录镜像(.hex/.axf)和自动化脚本(keilkilll.bat),总大小6.51MB,结构完整,可直接编译运行。已有2316人学习下载,配套工程支持STM32F4系列主控,涵盖SPI初始化、寄存器读写时序、UART指令解析、错误校验及调试日志输出等关键环节,代码注释详尽,便于理解HLW8112通信协议并快速集成到实际电表或智能插座等电源管理项目中。 最近在捣鼓一块单相电能采集板,要求把电压、电流、有功功率都读出来,再通过Modbus吐给上位机。第一版我用互感器加运放加STM32内置ADC的方案,说实话能跑,但温漂和相位补偿调得人很暴躁,精度一直在1%左右晃。后来换成HLW8112这颗计量芯片,硬件设计一下清爽了许多——电压电流通道进芯片,输出直接就是有效值寄存器,MCU要做的只剩下两件事:主动把数据读回来,或者等芯片把数据吐出来。这两件事分别对应SPI和串口两种通信方式,也是HLW8112开发例程里最核心的部分。

网上搜HLW8112的例程,好一点的会单独给SPI版本或者串口版本,但很少把两条链路放在一起讲透。这篇就按我实际调试的顺序,把STM32通过SPI主动读取、通过UART被动接收这两条路都拆开:硬件怎么接、寄存器读写怎么写、HAL库代码怎么组织、实测中有哪些坑。适合正在用HLW8112做智能插座、功率计、电量采集模块,或者单纯想学电能计量芯片通信的同学参考。

1. 双模通信是HLW8112开发例程里真正花时间的部分

先说说为什么一颗计量芯片,例程里最难写的不是算法,而是通信。

1.1 计量芯片省掉的麻烦,都会在通信链路里找回来

用分立方案做电能采集时,电压互感器、电流互感器、差分放大、ADC采样、有效值算法、频率测量、相位校准全部要自己写。HLW8112把这些活全包了:内部有ADC和DSP,直接输出电压有效值、电流有效值、有功功率、频率等数据。MCU这边的代码量一下子小了很多,只需要定时读取结果。

但换来一个代价:你必须先和这颗芯片正常通信,后面所有功能才谈得上。我看到不少朋友拿着例程烧进去,读回来的数据要么是0,要么是乱跳,最后发现根本不是算法问题,而是SPI时序不对或者串口帧没对齐。所以HLW8112的开发例程,本质就是一套经过验证的通信例程。

1.2 SPI和UART各自适合什么场景

HLW8112同时支持SPI和UART,这是它比很多单接口计量芯片方便的地方。选哪种,取决于你的系统形态。

对比项SPI模式UART模式
通信方向MCU主动发起读操作芯片主动周期性上报
接线数量SCLK/CS/SDI/SDO约4根TX/RX约2根
通信速度可到1MHz以上,适合高频读取常见4800bps,每秒一帧数据
MCU开销需要主动发起读,占用定时器或主循环接收中断即可,处理简单
调试难度时序敏感,容易踩时钟相位和片选的坑帧解析易错位,校验要对
适合场景实时监控、保护联动、高频数据采集智能插座、电量统计、低功耗上报

简单说,SPI适合“我要什么就读什么”的场合,UART适合“芯片主动告诉我”的场合。如果你做的是需要对功率突变快速响应的设备,SPI模式更顺手;如果只是隔一段时间记录一下电量,UART模式一根线就能搞定。

1.3 动手前先搞清楚寄存器读写模型

我在调试前习惯先做一件事:把HLW8112手册里的寄存器映射表整理成头文件宏。电压寄存器、电流寄存器、功率寄存器、状态寄存器都是哪些地址,哪些只读,哪些可写,先列清楚。

不要觉得这一步多余。SPI模式下,你要往芯片发寄存器地址然后读回数据;UART模式下,数据帧里也会带寄存器地址字段。如果你连地址都拿不准,后面代码肯定改来改去。把寄存器表理清楚之后,代码写起来就是机械劳动。

另外提醒一句:HLW8112的数据寄存器一般是24位或32位,不同批次可能有差异。做上层解析时,别假设所有寄存器都是同一个宽度,以你手上这颗芯片手册为准。

1.4 上电别急着读,先等芯片稳定

这是个特别容易被忽略的点。HLW8112上电后,内部ADC、基准源、通信接口需要一点时间进入稳定状态。我实际测试时发现,如果MCU复位后立刻去读寄存器,前几帧数据经常是0或者明显不对。

我的做法是在主程序初始化通信接口之后,加一个100ms左右的延时,再做第一次读取。这个延时不用很精确,但必须有。否则你会在排查问题的方向上浪费很多时间,总觉得是代码问题,实际是芯片还没准备好。

2. SPI例程拆解:引脚连接、寄存器读取和HAL层实现

SPI模式是我自己项目里用得最多的方式,因为采集模块需要较高频率地读取功率数据来做保护判断。这里把完整流程拆出来讲。

2.1 接线的几个关键点

HLW8112的SPI接口通常是SCLK、CS、SDI、SDO这四根线,和STM32的硬件SPI连接很直接。连线时注意几点:

  • SCLK接STM32的SPI_SCK,CS接任意GPIO,SDI接MOSI,SDO接MISO。
  • CS推荐用软件GPIO控制,不要依赖硬件片选。HLW8112这种低速从机,GPIO片选完全够用,而且排查问题时更方便。
  • 如果PCB空间紧张,CS也可以连到SPI的NSS引脚,但要注意HAL库对NSS的处理方式,容易踩坑。
  • 电平方面,STM32F103这类3.3V主控配3.3V供电的HLW8112一般不需要电平转换。如果是5V系统,中间必须加转换芯片,别硬接。

2.2 CS信号一定要在整个帧期间保持低电平

这是SPI模式里最容易翻车的细节。很多人在STM32上写SPI读寄存器,习惯每收发一个字节就拉一次CS,因为很多普通SPI外设就是这么操作的。

但HLW8112不是这个玩法。一次完整的寄存器读取,CS必须在发送地址到读回数据的整个过程中保持低电平。中途拉高会导致芯片认为访问被异常终止,数据自然不对。

正确做法是:先拉低CS,发送寄存器地址,然后连续接收数据字节,最后拉高CS。整个过程CS只翻转两次。我第一次写例程时按照普通SPI Flash的习惯每字节拉一次CS,读出来的数据完全不对,后来看波形才发现问题。

2.3 HAL库SPI读寄存器的标准套路

用CubeMX把SPI1配置成全双工主机,Mode 0,8位数据,MSB first。下面是一段我项目里实际在用的读取代码:

#define HLW8112_CS_LOW() HAL_GPIO_WritePin(HLW_CS_GPIO_Port, HLW_CS_Pin, GPIO_PIN_RESET) #define HLW8112_CS_HIGH() HAL_GPIO_WritePin(HLW_CS_GPIO_Port, HLW_CS_Pin, GPIO_PIN_SET) // 读取一个8位寄存器 uint8_t hlw8112_spi_read_reg(uint8_t reg) { uint8_t tx[2] = { reg, 0x00 }; uint8_t rx[2] = { 0 }; HLW8112_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100); HLW8112_CS_HIGH(); return rx[1]; } // 读取一个24位数据寄存器 uint32_t hlw8112_read_24bit(uint8_t reg) { uint8_t tx_addr = reg; uint8_t rx_data[3] = { 0 }; uint32_t val = 0; HLW8112_CS_LOW(); HAL_SPI_Transmit(&hspi1, &tx_addr, 1, 100); HAL_SPI_Receive(&hspi1, rx_data, 3, 100); HLW8112_CS_HIGH(); val = ((uint32_t)rx_data[0] << 16) | ((uint32_t)rx_data[1] << 8) | ((uint32_t)rx_data[2]); return val; }

代码本身不复杂,但有几个细节要说清楚。

第一,HAL_SPI_TransmitReceive发送的时候,接收缓冲区会拿到同样长度的数据,所以第二个字节读回来的内容就是寄存器值。如果你分开用Transmit和Receive,要注意发送地址之后芯片可能需要一点时间准备数据,必要时在中间加一小段延时。

第二,24位寄存器的字节序,不同手册写法不同。有的高位在前,有的低位在前。如果读回来的值和实际负载明显不对,先把三个字节的顺序互换一下试试,这个动作比翻手册快得多。

第三,实际项目中如果SPI总线上只有HLW8112一个设备,建议在初始化时把SPI时钟频率先调低一点,比如1MHz以下。HLW8112不是高速器件,跑太高频率容易在线上出现信号质量问题。

2.4 读回0xFF或0x00时,先按这个顺序排查

SPI调试遇到读回数据全0xFF或者全0x00,十个里面有八个是下面这几个原因:

  1. CS没有在整个帧期间保持低电平。用逻辑分析仪看CS信号,确认是不是在收发过程中被拉高了。
  2. 时钟极性和相位配置反了。先把Mode 0和其他三种模式轮着试一遍,很多例程就是靠这个找到正确模式的。
  3. SDO引脚没配置成复用推挽输出,或者虚焊。这种情况读回来经常是0xFF。
  4. 芯片供电异常或者复位脚被拉低。读回来数据会表现为无规律乱跳。

我的建议是:遇到SPI通信异常,不要急着改代码,先上逻辑分析仪或者示波器,同时看CS、SCLK、MOSI、MISO四根线。只要波形正常,问题基本就在寄存器地址或者字节序上;波形不对,优先查硬件连接和初始化配置。

2.5 硬件SPI不方便时,软件模拟也完全够用

有些项目为了PCB布局方便,把HLW8112接到了普通GPIO上,这时候可以用软件模拟SPI。HLW8112的数据速率不高,软件模拟完全撑得住。

软件模拟的核心时序就是:拉低CS,然后按位把寄存器地址发出去,再按位把数据读回来,最后拉高CS。时钟的极性要和芯片手册一致。需要注意一点:软件模拟SPI时,发送时钟的翻转操作最好关中断,否则一个中断进来,时序中间被插入一截停顿,芯片可能误判。

uint8_t hlw8112_spi_soft_read(uint8_t reg) { uint8_t val = 0; HLW8112_CS_LOW(); // 发送8位寄存器地址,这里省略具体位翻转代码 // 每发一个bit,翻转SCLK // 然后接收8位数据,每收一个bit,翻转SCLK HLW8112_CS_HIGH(); return val; }

软件模拟的好处是引脚随便选,适合已经画好板子改不了布线的情况。缺点是占CPU时间,如果主循环里还有其他实时任务,建议还是用硬件SPI。

3. 串口例程拆解:被动接收一份带校验的数据帧

如果你的应用不需要高频读取数据,HLW8112的UART模式更省事。芯片会上电后主动向MCU发送数据帧,MCU只需要在串口中断里收帧、解析、校验。

3.1 串口模式的接线和参数

HLW8112的串口连接很简单:TXD接STM32的RX,RXD接STM32的TX,两边共地。这里有两个需要注意的地方。

第一,很多串口模式下默认配置是4800bps、8位数据、无校验、1位停止位。9600也有部分型号支持,但最稳的做法是先按4800来,等通信稳定了再尝试提高。

第二,如果HLW8112模块的TXD/RXD是开漏输出,外部需要接上拉电阻。否则MCU接收到的波形上升沿很缓,容易出现丢字节。我之前在一个小模块上没加上拉,串口接收偶发丢帧,一直怀疑代码问题,后来用示波器一看,波形上升沿都快成斜坡了。

3.2 帧结构:不要死记,要会解析

HLW8112的UART数据帧结构,不同手册和例程里略有差异,但大体上都是帧头加数据加校验。常见的帧头有0x5A 0xA5,也有用0x55 0xAA的。数据区里会包含电压、电流、功率、状态等字段,帧尾通常是校验和。

我的思路是:先别管具体协议,把帧解析器写成通用结构。第一步找帧头,第二步按固定长度收数据,第三步做校验和,第四步把有效数据提取出来。

校验和的算法一般是把某一区间内所有字节相加,取低8位或者取反。用代码实现就是:

uint8_t calc_checksum(const uint8_t *buf, uint16_t len) { uint8_t sum = 0; for (uint16_t i = 0; i < len; i++) { sum += buf[i]; } return sum; }

收到一帧后,把算出来的校验和和帧里的校验字段比对,一致才处理,不一致直接丢掉。这一步非常关键,我不止一次在环境比较复杂、串口线上有干扰的场合,收到校验错误的帧。如果不做校验,数据会偶尔跳变,排查起来极难。

3.3 用HAL库空闲中断接收不定长帧

串口接收HLW8112数据帧,推荐用HAL库的UART空闲中断加DMA,这样CPU不用一个字节一个字节地处理。

在CubeMX里打开UART1的DMA接收,接收模式选Normal,然后在代码里启动接收:

uint8_t rx_buf[128]; HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf));

接收完成后,会在中断回调里拿到本次接收到的数据长度:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART1) { // 解析HLW8112数据帧 parse_hlw8112_frame(rx_buf, Size); // 重新启动接收,否则只收一次就停了 HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf)); } }

这里有一个非常常见的坑:回调里解析完数据后,忘了重新调用ReceiveToIdle函数,结果只收到一帧就再也不进回调了。只要把重启接收放在回调末尾,就能连续工作。

另外,解析函数里尽量不要做耗时操作,比如浮点运算、打印调试信息。中断回调里只做数据搬运和标志位设置,具体的换算放到主循环里做。

3.4 帧错位怎么办:滑动窗口对齐

串口通信有一个很实际的问题:MCU可能从数据帧中间开始接收。比如芯片已经跑了一会儿,MCU才上电,接收缓冲区的第一个字节可能不是帧头。

如果解析器写得太死板,上来就要求从buf[0]开始是帧头,那数据会一直对不齐。

解决办法是滑动窗口对齐:从缓冲区第一个字节开始,逐字节寻找匹配的帧头。找到帧头后,如果剩余字节足够组成一帧,就按帧长度取帧并校验;校验通过就消费这帧数据,校验不通过则从下一个字节继续找。

用这种思路写出来的解析器,哪怕第一次接收时从半截开始,下一帧开始也能自动恢复同步。这个经验在处理UART上报型计量芯片时通用,HLW8112、HLW8032这类芯片都很适用。

4. 从例程到移植:接口封装、中断优先级和滤波策略

例程跑通只是第一步。真正把代码放进产品里,还需要做一些工程化处理,否则后面维护会很难受。

4.1 让上层代码不认识HLW8112

我习惯在应用层和底层驱动之间加一层接口。上层只调用类似这样的函数:

float meter_read_voltage(void); float meter_read_current(void); float meter_read_power(void); uint16_t meter_read_frequency(void);

底层可以是SPI实现,也可以是UART实现,上层完全不关心。这样做的直接好处是:如果哪天要换计量芯片,或者要从SPI改成UART,只要重写底层,上层应用代码一行都不用动。

HLW8112本身的寄存器结构、通信时序、帧格式,全部封装在meter_hlw8112.c这个文件里。头文件里只暴露上面几个函数。这就是“关注点分离”在实际项目中的具体应用。

4.2 中断优先级和DMA模式的选择

串口空闲中断处理帧解析,优先级不能设得太随意。我一般把UART中断优先级放在比较靠后但又不低于系统关键中断的位置。

原因很简单:SysTick这些系统中断需要及时响应,否则任务调度会出问题;但UART中断也不能被其他不太重要的外设频繁打断,否则丢数据。

DMA接收建议用Normal模式而不是Circular模式。Circular模式会自动覆盖缓冲区,你用起来会觉得“一直都在收”,但帧边界很难判断。Normal模式配合ReceiveToIdle回调,每次收到一帧后拿到精确的长度,解析逻辑最清晰。

4.3 原始值换算成真实电压电流

HLW8112读出来的电压、电流、功率都是寄存器原始值,不是最终单位。换算需要乘以系数,系数来自硬件电路的分压电阻、采样电阻和芯片内部增益。

换算公式大概是这样:

  • 电压真实值 = 电压寄存器原始值 * 电压系数
  • 电流真实值 = 电流寄存器原始值 * 电流系数
  • 功率真实值 = 电压真实值 * 电流真实值,或者直接读功率寄存器再乘系数

未校准的情况下,你可以通电后用一个稳定的负载,对比万用表和HLW8112读到的值,把系数反算出来:

电压系数 = 万用表读数 / 寄存器原始值

这个系数在同一个硬件设计下是基本固定的,保存到Flash里,量产时通过校准工装写进去即可。

4.4 滑动平均比一阶滤波更直观

串口模式一般一秒一帧,如果直接把原始值用于显示,数字会上下跳,看起来非常不专业。我的做法是做五点滑动平均。

整型实现可以这样写:

int32_t avg_value = 0; int32_t moving_average(int32_t new_value, int32_t *avg, uint8_t n) { *avg = *avg - *avg / n + new_value / n; return *avg; }

注意运算顺序,先减掉旧平均值的一个分量,再加上新值的一个分量,避免溢出和瞬间跳变。这种滤波在50Hz工频环境下已经够用,不需要上太复杂的算法。

5. 实测中容易翻车的三个细节

例程调试过程中,我总结了三个特别容易让人头疼的细节,写在这里供大家参考。

5.1 复位引脚悬空导致偶发读回全FF

有一版测试板,HLW8112的复位引脚没有接任何东西,悬空放着。大部分时间通信正常,但偶尔会读回全0xFF,过一会儿又自己恢复。

后来排查发现,复位引脚悬空时容易受干扰,芯片可能被意外复位或者进入异常状态。解决办法很简单:给复位脚加一个10k电阻上拉到VCC,再并一个0.1uF电容到地,上电时也可以由STM32的GPIO控制复位时序。

这个问题的隐蔽之处在于它不是稳定复现,而是偶发出现。如果你也遇到这种“时好时坏”的通信问题,先把复位引脚处理干净。

5.2 先看清SPI是四线还是单线

HLW8112不同型号的SPI接口并不完全一样。有的型号是标准四线:SCLK、CS、SDI、SDO;有的型号把SDI和SDO合并成一个双向引脚,走单线半双工协议。

如果是后一种,你拿标准硬件SPI去连接会发现数据怎么都不对,因为硬件SPI默认是全双工,MOSI和MISO是分开的。

处理办法有两种:一是用软件模拟单线SPI,手动切换引脚方向;二是干脆用UART模式,绕开这个问题。拿到板子先看原理图,数一下是四根线还是三根线,能省下好几个小时的调试时间。

5.3 串口波形异常优先检查上拉和共地

串口通信不正常的时候,我建议第一时间用示波器或者逻辑分析仪看TXD引脚的波形,而不是改代码。

如果波形幅度很低,先查上拉电阻;如果波形反相,查TXD/RXD是不是接反了;如果波形有毛刺,查两边电路有没有可靠共地。串口通信其实很朴实,只要电平正确、波特率一致,基本都能通。怕的是硬件已经有问题,你还在软件里反复找原因。

6. 一个很省事的调试方法:把收到的原始帧直接打印成十六进制

调试HLW8112串口解析的时候,我习惯做一件事:在串口回调里不急着解析,先把rx_buf里的原始字节用printf打印成十六进制字符串发到调试串口。

比如打印出来的是:

5A A5 01 02 03 04 05 06 07 08 3F

你先肉眼看一眼:帧头对不对,长度对不对,校验位大概在哪个位置。然后再写解析代码,或者把现有解析代码打印出来的字段和原始帧对照。这个方法虽然土,但在调试协议解析时比任何调试器都好用。

SPI调试也一样。先用逻辑分析仪抓CS、SCLK、MOSI、MISO四条线,对着手册数时钟周期,看地址发出去之后芯片有没有在MISO上返回数据。基本上只要你会看波形,SPI问题就没有秘密。

我自己调试HLW8112这套例程时,最大的感受是:芯片本身不是难点,难的是通信链路里那些不起眼的小细节——CS什么时候拉低、帧头从哪个字节开始、上电后等多久再读。把这些细节稳住,后面的电压、电流、功率换算都只是简单的数学运算。

最后分享一个实际体会:网上很多HLW8112例程能跑起来,不是因为代码写得有多精巧,而是作者已经在硬件上帮大家把坑踩完了。你拿到例程后,不要急着改功能,先把通信链路用逻辑分析仪完整抓一遍,确认波形和手册一致,再往上加自己的逻辑。这样下来,整个开发周期会顺畅很多。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 1:17:59

恒生电子2015校招笔试解析:C语言与数据结构核心考点

1. 先聊聊这套题背后的逻辑恒生电子这名字&#xff0c;放到2015年的校招圈子里&#xff0c;基本等同于“金融IT的黄埔军校”。那几年券商、基金、银行集体搞系统升级&#xff0c;恒生在国内证券交易系统这块的份额摆在那儿&#xff0c;对计算机专业的应届生来说&#xff0c;进恒…

作者头像 李华
网站建设 2026/9/1 1:17:17

智能体服务的并发边界与验收

智能体服务的并发边界与验收本文以假设的并发任务场景说明 Agent 的工程约束&#xff0c;不将其当作已发生的故障复盘。并发规模、工具限额和持久化策略必须结合实际负载验证。 在产品演示阶段&#xff0c;智能 Agent&#xff08;智能体&#xff09;展现出了令人惊艳的能力&…

作者头像 李华
网站建设 2026/9/1 1:17:15

让故障复盘进入日常交付

让故障复盘进入日常交付这里讨论的是常见的流程缺口&#xff0c;而不是某个团队的事故结论。是否自动拦截&#xff0c;取决于规则是否稳定、误报成本是否可接受。 许多工程团队都经历过这样的场景&#xff1a;线上发生重大故障后&#xff0c;大家围在一起开复盘会&#xff0c;经…

作者头像 李华
网站建设 2026/9/1 1:15:28

智能体委派范围检查工具:从输入校验到离线报告的完整实现

智能体委派范围检查工具&#xff1a;从输入校验到离线报告的完整实现 项目编号&#xff1a;20260831-002。本文代码、测试、文档、示例数据和效果图均为独立编写&#xff0c;不包含热点产品或开源项目源码、品牌素材与官方截图。 问题与目标 核对委派目标、可用工具、数据边界…

作者头像 李华
网站建设 2026/9/1 1:12:27

基于EKF扩展卡尔曼滤波的电池SOC估计Matlab仿真

简介&#xff1a;本资源是一套面向电池管理系统&#xff08;BMS&#xff09;算法开发者与新能源方向研究生的MATLAB实践方案&#xff0c;聚焦非线性锂电池SOC&#xff08;荷电状态&#xff09;实时估计算法实现。基于扩展卡尔曼滤波&#xff08;EKF&#xff09;构建状态观测器&…

作者头像 李华
网站建设 2026/9/1 1:04:41

Python+YOLOv8打造智能驾驶员状态监测系统:从训练到UI实现

简介&#xff1a;本资源是一套基于Python与YOLOv8实现的智能驾驶员状态监测系统完整项目&#xff0c;面向高校毕业设计、课程设计及AI视觉开发初学者&#xff0c;聚焦疲劳驾驶行为检测这一典型工业落地场景。项目支持闭眼、张嘴、睁眼、闭嘴四类关键状态识别&#xff0c;含约30…

作者头像 李华