简介:面向STM32电能计量与显示场景,程序基于F103C8T6搭配HT7036/HT7038,用I2C驱动OLED,通过按键切换显示电压、电流等参数,功能已调通。包内共216个文件,约6.55MB,含C/C++源码(.c/.cpp)、头文件(.h)、Keil工程配置(.uvprojx/.uvoptx)、编译中间文件(.o/.d/.crf)及hex烧录文件,便于直接阅读、改工程和烧录。已有1096人学习下载。资源对引脚分配做了明确说明:OLED接PA3/PA4,按键分别接PA0、PB9、PB0,HT7036走SPI(PA5/PA6/PA7),方便快速接线与移植。适合正在做电能采集显示、或想学习STM32与HT7036 SPI通信及OLED菜单切换的开发者参考。 做电能计量、电力监控或者智能断路器的朋友,对“STM32F103C8T6 + HT7036/HT7038”这个组合应该不陌生。MCU负责通信、逻辑和控制,计量芯片负责把电压、电流、功率、电能这些参数算好,主控只读寄存器,整体成本压得很低。我在几个项目里用这套方案做过单相/三相计量模块,也帮同事排查过几块板子的读数问题,积累了不少实战细节。这篇文章就把这套程序从硬件连接、SPI时序、寄存器解析到校表流程完整梳理一遍,适合正在做电表、电力监控、能效管理或智能断路器开发的工程师参考,也适合刚接触计量芯片的嵌入式开发者入门。
1. 方案选型与整体思路拆解
1.1 为什么是HT7036/HT7038
做计量类产品,前端方案通常是“MCU + 专用计量芯片”或者“MCU + ADC 自己算”。强烈建议用前者,尤其在对精度和稳定度有要求的场合。HT7036/HT7038 属于同一系列的计量芯片,内部集成了多路 ADC、数字滤波器和 DSP 运算单元,能直接输出电压有效值、电流有效值、有功/无功功率、功率因数、频率、电能等参数,主控不需要做复杂的傅里叶变换,也不用担心运算耗时和量化误差。
这个系列芯片的寄存器映射和 SPI 通信协议几乎一致,区别主要在量程、精度等级和通道数量上。HT7038 的精度规格更高,适合做关口表或高精度监测;HT7036 成本上更有优势,适合做批量大的末端设备。因为寄存器结构基本对齐,写一套主控代码就能同时兼容两款芯片,换料时不用大改程序,这在缺货、备料和生产切换时非常实用。
1.2 为什么选STM32F103C8T6
C8T6 在国产替代、成本控制和资料丰富程度上都很有优势。它内置 64KB Flash、20KB RAM,主频 72MHz,带 3 个 USART、2 个 SPI、1 个 CAN,驱动一颗计量芯片绰绰有余,还能同时跑 Modbus 从站协议、RS485 收发、LCD/OLED 显示和继电器控制。对中小批量产品来说,C8T6 的价格和供货稳定性非常友好,生态也成熟,CubeMX 几分钟就能完成初始化。
还有一个重要考量:HT7036/HT7038 的 SPI 电平是 3.3V,C8T6 的 IO 正好匹配,不需要额外做电平转换。如果用 5V 的 STM32F103ZET6 之类,虽然也能通过电阻分压接,但多一层硬件就多一个出错点,用 C8T6 更省事。Flash 只有 64KB,不过计量程序本身不复杂,把协议栈、显示驱动、数据存储都算上,一般也能控制在 40KB 以内。
2. 硬件连接与通信协议核心细节
2.1 芯片供电与复位电路
计量芯片对电源纹波很敏感。模拟电源和数字电源建议分开走线,至少要用独立的 3.3V LDO 给计量芯片供电,不要直接跟 STM32 的数字电源共用同一个 LDO 输出,否则电流波动会直接耦合进 ADC 参考,导致读数跳变。复位引脚建议加一个 10kΩ 上拉电阻和 0.1μF 电容到地,防止上电瞬间误复位。
晶振也要注意。HT7036/HT7038 通常需要外部时钟输入,常见做法是用一个有源晶振或者 MCU 的 MCO 引脚提供时钟。如果计量芯片自身带晶振接口,那就按数据手册选匹配的晶体和负载电容,布线尽量短。晶振不起振或频率不准时,读出的寄存器数据会乱跳,而且很难从软件侧排查,遇到“读数完全不对”的板子,先量晶振波形。
2.2 主控引脚分配建议
整套系统里,STM32 和计量芯片之间的连接其实很少:一组 SPI(SCK、MISO、MOSI、CS)加一个中断引脚。但实际项目中还要预留通信和交互用的 IO,我在设计时一般会这样安排:
| 功能 | 引脚示例 | 说明 |
|---|---|---|
| SPI1_SCK | PA5 | 时钟,速率先设为 1MHz 测试 |
| SPI1_MISO | PA6 | 计量芯片数据输出 |
| SPI1_MOSI | PA7 | 计量芯片数据输入 |
| SPI1_CS | PA4 | 软件控制片选,不要用硬件 NSS |
| 计量中断 | PB0 | 接入 EXTI,触发校表/事件读取 |
| 电压/电流/功率的脉冲输出 | PB1(可选) | 用于校表或标准表对比 |
CS 一定要用普通 GPIO 软件控制,不要依赖硬件 NSS。硬件 NSS 在多主机、多从机场景下行为复杂,容易引发误片选,软件控制更可控。中断引脚尽量选支持 EXTI 的 IO,比如 PB0、PB1,这样可以用外部中断实时响应计量芯片的 FIFO 中断或校表脉冲。
2.3 SPI 时序理解与寄存器读取思路
HT7036/HT7038 的 SPI 通信属于“楼主从机”结构,主控先拉低 CS,然后发送命令和寄存器地址,再读取或写入数据。网上很多人读不到寄存器,八成是 SPI 模式没配对。根据我实测和多数成功案例,SPI 通常配置为 CPOL=0、CPHA=1(即 SPI Mode 1),数据在第二个边沿采样;但不同批次或不同型号也可能存在差异,所以最稳妥的办法是看芯片手册里的 SPI 时序图,数一下 SCLK 空闲电平和采样边沿,再在 CubeMX 里选对应模式。
这里给一个可参考的通用读寄存器流程:
- 拉低 CS
- 发送命令/地址字节
- 按寄存器位数读取数据(常见是 32bit,也有 16bit)
- 拉高 CS
- 解析收到的数据
对应 HAL 库代码大致如下:
uint32_t HT703x_ReadReg(uint32_t addr) { uint8_t tx[6] = {0}; uint8_t rx[6] = {0}; uint32_t value = 0; tx[0] = (uint8_t)((addr >> 8) & 0xFF); tx[1] = (uint8_t)(addr & 0xFF); SPI_CS_Low(); HAL_SPI_Transmit(&hspi1, tx, 2, 100); HAL_SPI_Receive(&hspi1, rx, 4, 100); SPI_CS_High(); value = ((uint32_t)rx[0] << 24) | ((uint32_t)rx[1] << 16) | ((uint32_t)rx[2] << 8) | rx[3]; return value; }注意,这个代码是示意写法,实际项目里命令字、地址长度和数据长度一定要以你手里那个型号的最新数据手册为准。不同厂商的时序图细节不同,照抄网上代码容易踩坑。
3. 主控侧程序框架与关键代码实现
3.1 初始化流程
程序初始化我习惯按这个顺序走:时钟和 GPIO -> SPI -> 串口 -> 外部中断 -> 计量芯片软复位 -> 配置寄存器 -> 读取校准参数 -> 进入主循环。
计量芯片上电后先做一次软复位,把内部状态机拉回已知状态,然后延时等待芯片稳定。软复位的具体命令也要查手册,有的芯片是写系统控制寄存器,有的芯片有专门复位命令。复位后建议读一次版本号或者状态寄存器,确认 SPI 通信链路是通的,再往下走。
CubeMX 里 SPI1 的配置可以参考这个模板:
- Mode:Full-Duplex Master
- 数据大小:8bit
- 时钟极性 CPOL:Low
- 时钟相位 CPHA:1 Edge(如果 Mode 1 不对就换 2 Edge)
- NSS:Disable(软件管理)
- 波特率预分频:32(约 2.25MHz,稳定后再提速)
时钟速率建议从低频开始。有些板子布线差、干扰大,高速 SPI 下数据容易错位,先降到 1~2MHz 调通逻辑,再逐步提高,省去不少排查时间。
3.2 SPI 读写函数与数据拼接
计量芯片的寄存器读取,核心是“把地址发出去,然后把数据收回来”。写入则是在地址后追加数据,最后等 CS 拉高后芯片才锁存。整个过程要保证 CS 低电平期间 SCLK 连续,中间不能有太长延时,否则芯片可能判定通信超时。
我常用的一组底层封装:
void HT703x_WriteReg(uint32_t addr, uint32_t data) { uint8_t tx[6] = {0}; tx[0] = (uint8_t)((addr >> 8) & 0xFF); tx[1] = (uint8_t)(addr & 0xFF); tx[2] = (uint8_t)((data >> 24) & 0xFF); tx[3] = (uint8_t)((data >> 16) & 0xFF); tx[4] = (uint8_t)((data >> 8) & 0xFF); tx[5] = (uint8_t)(data & 0xFF); SPI_CS_Low(); HAL_SPI_Transmit(&hspi1, tx, 6, 100); SPI_CS_High(); }读回来的数据是补码格式还是原码,要看具体寄存器。电压、电流、功率这些测量值大多是 24bit 或 32bit 的二进制补码,解析时要注意符号位扩展,否则负功率、反向电流会变成很大的正数。我的建议是写一个通用的“补码转有符号 int32”的小函数,统一处理。
3.3 寄存器数据解析:电压、电流、功率、电能
以常见三相计量芯片为例,基本测量寄存器包括 A/B/C 相电压有效值、A/B/C 相电流有效值、A/B/C 相有功功率、总有功功率、频率等。每个寄存器对应的满量程比例关系不同,手册里会给出计算公式,例如“电压寄存器 1 LSB = 0.01V”或者“电流寄存器需要结合电流互感器变比换算”。
实际项目里要先确定外部传感器参数。比如用 5A/2.5mA 的电流互感器,配合锰铜分流器或互感器采样,那么电流寄存器的原始值要乘上变比系数才是实际一次侧电流。这个系数可以预留在配置结构体里,方便现场校表时调整,不要写死在代码各处。
电能寄存器一般分成“电能有功”和“电能无功”两组,有些芯片还分正向/反向。读取后要注意溢出和清零策略。如果产品需要长时间计量,建议定期把电能值写入外部 Flash 或 EEPROM,防止掉电丢数据。掉电瞬间的电能保存,很多设计是用 MCU 检测掉电中断,然后利用电容余电通过 SPI 把电能写入非易失存储,这个时序要专门处理。
3.4 校表与增益校准流程
新打的板子或换批次的互感器、电阻后,计量精度往往不达标,必须校表。校表有两种思路:一是用标准源加标准表,人工对比后写增益系数;二是产线自动校表,通过上位机下发指令批量调整。不管哪种,核心都是调整芯片的增益寄存器。
校表基本步骤可以这样理解:
- 给被测表加额定电压(如 220V)和额定电流(如 5A),功率因数设为 1.0。
- 读标准表的实际功率值 P_std,同时读计量芯片的有功功率寄存器值 P_read。
- 计算校正系数:ratio = P_std / P_read。
- 把 ratio 换算成增益寄存器需要的格式,写入功率增益寄存器。
- 再次读取功率寄存器,确认误差在允许范围内,通常要求小于 0.1% 或 0.2%。
电压和电流通道也要单独校,因为互感器在不同幅值下的线性度不一样。有条件的话至少校三个点:1%Ib、5%Ib、100%Ib(Ib 是基本电流),这样才能保证轻载和小信号下的精度。
校表参数要固化保存,不能靠每次上电重新校。我的做法是定义一份校表结构体,存到片内 Flash 的独立扇区,上电读取并写入计量芯片。如果 CRC 校验失败,就恢复默认参数并上报错误,避免设备在错误参数下运行。
4. 常见问题与现场排查速查表
4.1 读出的寄存器全是0xFF或0x00
这是新手最常见的问题。我排查的顺序是:先量 CS 低电平期间的 SCLK 是否有波形,再看 MISO 上有没有数据回传,最后用逻辑分析仪抓 SPI 时序和手册对比。多数情况是 SPI 模式不对,CPOL/CPHA 组合选错,芯片完全无法识别主控的命令。第二多发的原因是 CS 引脚被硬件 NSS 占用,或者 CS 没有拉低到位,导致通信被忽略。
还有一部分原因是时钟频率太高。芯片的 SPI 最高速率有限,比如可能只支持 2MHz 或 4MHz,主控默认跑到 18MHz,通信必然失败。把预分频调大,降到 1MHz 再试,很多时候就通了。
4.2 数值跳变、为0或与标准表偏差过大
数值跳变先看供电和地。计量芯片电源纹波大、地线处理不好,读数会像神经病一样乱跳。用示波器量芯片电源引脚,如果噪声超过 50mV,就要加强滤波。其次看时钟波形,晶振幅度不稳、频率偏了,所有测量值都会受影响。
偏差过大但数值稳定,通常不是通信问题,而是传感器参数或增益系数不对。检查电流互感器变比是否和代码里配置一致,检查电压分压电阻是否匹配,检查校表系数有没有正确写入。有一个很容易忽略的点:校表系数写入后,芯片可能需要重新启动或重新加载参数才生效,如果代码里写完增益寄存器就直接读取,读到的一半是旧值,会误以为校表没生效。
4.3 中断引脚不触发或误触发
中断引脚是计量芯片通知主控“有事件”的通道,常见事件包括电能寄存器更新、校表脉冲输出、过压欠压、断相等。不触发时先看功能是否在配置寄存器里打开了,很多中断源默认是关闭的;再看中断标志是不是没有清除,芯片的中断标志如果一直保持,会导致后续中断无法再次触发。
误触发则多为外部干扰,中断引脚建议加一个 RC 滤波或用主控内部上拉,同时在中断服务函数里做软件消抖,比如连续读到两次有效电平才确认事件。我在产品里一般还会加一个超时看门狗,如果长时间没有中断产生,就主动查询一次计量芯片状态寄存器,防止死等中断导致功能失效。
4.4 一套程序兼容HT7036和HT7038的技巧
虽然两者寄存器大致兼容,但有些寄存器地址或功能位会有差异。为了不维护两套代码,我通常会在初始化时读一个“芯片版本寄存器”,根据返回值动态设置能力标志位。
uint32_t chip_id = HT703x_ReadReg(REG_CHIP_ID); if (chip_id == CHIP_ID_HT7038) { feature_hi_precision = 1; // 启用 HT7038 特有寄存器配置 } else { feature_hi_precision = 0; // 按 HT7036 默认参数配置 }这样换料时只需要烧同一个固件,程序自动识别型号。升级固件和维护都省事很多,尤其适合产线批量生产时混料的情况。
5. 几个容易忽略的工程细节
5.1 晶振和复位引脚别乱接
计量芯片对外部时钟的要求比普通 MCU 严格。有的板子为了省元件,直接用 STM32 的 MCO 输出 8MHz 给计量芯片,MCO 引脚输出能力有限,如果走线太长,波形边沿会很差,导致芯片工作不稳定。最好用有源晶振,或者把 STM32 的 MCO 配置成推挽输出并加缓冲。复位引脚加一个 RC 延时电路,上电时给芯片一个可靠的复位低电平脉冲,比只靠上电默认状态更稳。
5.2 Flash保存校表数据要自己做磨损均衡
C8T6 的 Flash 擦写寿命有限,校表参数虽然不常写,但如果产线频繁校表测试,同一扇区反复擦写也容易提前报废。做法是分配多个扇区轮着写,或者用片外 EEPROM。片外 EEPROM 虽然多几毛钱成本,但对于需要频繁校表或远程改参数的产品,可靠性会高出很多。
5.3 固件读保护是一个值得做的小功能
C8T6 支持设置读保护等级,防止 J-Link/ST-Link 直接读 Flash。通过修改 Option Bytes 开启 RDP Level 1,固件就无法被仿真器读取。做产品量产时,这个功能至少能挡住一部分抄板行为。代价是后续想再烧录调试会麻烦,需要先解除保护并擦除整片 Flash,所以建议在开发后期、准备交付生产版本时再开启。
6. 从模块到产品的扩展思路
6.1 加Modbus RTU,让设备进SCADA系统
计量模块如果只在自己板子上显示数字,价值有限。加上 RS485 收发器和 Modbus RTU 从站协议后,就能把电压、电流、功率、电能等数据上传到 PLC、触摸屏或组态软件。协议栈可以用 FreeModbus 开源库,也可以自己写简单功能码,根据寄存器映射表把数据按 0x03/0x04 功能码返回。
F103C8T6 的 RAM 有限,但跑 Modbus RTU 完全够用。需要注意串口中断接收和定时器超时判断,避免把多个报文粘包处理错。数据刷新率不用太快,电压电流 1 秒刷新一次,电能 1~5 秒刷新一次,对监控系统来说足够了。
6.2 配合无线模块做远程抄表
很多场景下设备装在配电箱里,人没法天天去看。加上 4G Cat.1 模组或 LoRa 模块,就能把电参量定时上报到云平台。上报策略要注意流量成本,不要做成每 100ms 上报一次,一般是每分钟或每 5 分钟聚合上报一次,包含当前值、历史累计电能和告警状态。
这套组合的功耗也不是完全不能做低。计量芯片本身功耗很小,MCU 可以进入 STOP 模式,定时醒来读取一次数据然后继续休眠。如果做电池供电的传感器,就要特别关注外设的关断和 SPI 总线的电平状态,避免漏电流。
6.3 成本优化和国产替代
F103C8T6 现在有大量国产替代型号,比如 GD32E103C8T6、APM32F103C8T6 等,引脚和寄存器基本兼容,程序直接编译就能跑。不过替换时要注意两点:一是启动文件和 Flash 等待周期可能要调,二是 ADC 和外设时钟配置略有差异。HT7036/HT7038 系列本身在国产计量芯片里属于应用成熟的选择,资料多、支持好,不像某些冷门芯片一样遇到问题只能自己啃。
写在最后的调试心得
最后分享一下我自己的调试经验。这套方案看起来简单,但“SPI 通了”和“数据准了”之间还有很大的距离。我第一次调的时候,在 SPI 模式上卡了一整个晚上,最后靠逻辑分析仪抓波形才发现 CPHA 选反了。所以强烈建议调试工具里常备一个逻辑分析仪,别看它便宜,排查 SPI 时序问题是真高效。
另外,拿到新板子不要急着跑完整程序。先把最小通信调通:CS、SCK、MISO、MOSI 四根线确认无误,读一个版本号或者状态寄存器,确保链路可靠,然后再去写配置、读测量值、做校表。这样每层都是稳的,出了问题也容易定位。
如果项目里也有同学在跟你并行开发,建议把寄存器地址表和校表流程整理成一份简单的 Markdown 文档放到代码仓库里。计量芯片的寄存器多,名字还都长得很像,有一个统一参考能少踩很多坑。这套代码我后来在新项目里直接改成“版本识别 + 动态配置”的结构,HT7036、HT7038 换着用,一颗固件通吃,产线那边也不用追着问“烧的是哪个版本”了。
本文还有配套的精品资源,点击获取