
简介本资源是一套面向嵌入式开发工程师与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_SCKCS接任意GPIOSDI接MOSISDO接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 08位数据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的RXRXD接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例程能跑起来不是因为代码写得有多精巧而是作者已经在硬件上帮大家把坑踩完了。你拿到例程后不要急着改功能先把通信链路用逻辑分析仪完整抓一遍确认波形和手册一致再往上加自己的逻辑。这样下来整个开发周期会顺畅很多。本文还有配套的精品资源点击获取