简介:本资源是一套面向嵌入式开发工程师与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,十个里面有八个是下面这几个原因:
- CS没有在整个帧期间保持低电平。用逻辑分析仪看CS信号,确认是不是在收发过程中被拉高了。
- 时钟极性和相位配置反了。先把Mode 0和其他三种模式轮着试一遍,很多例程就是靠这个找到正确模式的。
- SDO引脚没配置成复用推挽输出,或者虚焊。这种情况读回来经常是0xFF。
- 芯片供电异常或者复位脚被拉低。读回来数据会表现为无规律乱跳。
我的建议是:遇到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例程能跑起来,不是因为代码写得有多精巧,而是作者已经在硬件上帮大家把坑踩完了。你拿到例程后,不要急着改功能,先把通信链路用逻辑分析仪完整抓一遍,确认波形和手册一致,再往上加自己的逻辑。这样下来,整个开发周期会顺畅很多。
本文还有配套的精品资源,点击获取