简介:这份基于STM32的ATT7022三相计量芯片驱动源码,适合从事电力仪表、电能采集或工业计量开发的嵌入式工程师及入门新手。压缩包内含2个文件,即1个head头文件与1个C源文件,源码体积仅5KB,结构紧凑便于直接移植到现有工程。驱动覆盖电压、电流、功率、相位角等核心计量参数的读取,并额外实现了校正与温度系数补偿逻辑,解决了实际项目中计量精度受温漂影响的常见问题。所有函数均配有详细中文注释,函数接口清晰,调用关系一目了然,能帮助读者快速理解ATT7022的寄存器配置与数据解析流程,降低对照官方手册二次开发的成本。作者经项目实测验证,代码稳定可用。目前已有1397人学习下载,适合需要在STM32平台上快速集成三相计量功能、同时希望兼顾精度补偿的开发者参考。
1. ATT7022驱动包:三相计量芯片与STM32之间那层难写的「胶水」
做电力监控设备的朋友应该都有体会:ATT7022这颗三相计量芯片本身功能很强,6路ADC、有功无功一次算齐,但它与STM32之间那层SPI驱动才是真正卡人的地方——寄存器二十多个,读写时序有一点不对就读回全0,校表参数格式更是翻车重灾区。这份驱动包把SPI初始化、寄存器读写、数据换算、校表写入整套流程封装好了,适合做电表、电力监测终端、能耗管理设备时直接移植,省去从零啃芯片手册的时间。我拆过的几套同类型代码里,这份算最贴近实战的,下面把芯片机制、移植步骤和踩过的坑一次讲透。
2. ATT7022芯片机制:寄存器体系和数据换算必须先弄清楚
2.1 芯片内部结构:ADC采样与DSP计算的分工
ATT7022内部集成6路ADC,前三路接三相电压,后三路接三相电流,采样后的数据进入内部DSP完成有功、无功、视在功率以及有效值的计算。MCU这边要做的不是算数,而是通过SPI把结果读回来,再按校表增益换算成实际物理量。理解这一点很重要——驱动代码里几乎看不到浮点功率计算,只有寄存器读数和定点换算。
常见做法是把SPI速率控制在2MHz以内,时钟极性CPOL=1、相位CPHA=1,也就是SPI模式3。我一般先把SPI配置成8位模式,因为ATT7022的命令和寄存器地址都是8位对齐,数据部分再分3次8位传输拼成24位。上电时序上,芯片复位后内部DSP要跑一段自检,这期间读回的数据是无效的,驱动的初始化函数里必须预留这个等待窗口。
2.2 关键寄存器地址与数据格式详解
拿到驱动包先看头文件里的寄存器宏定义。用得最多的是下面这一组,不同渠道的芯片手册编号可能略有差异,但功能一一对应:
#define ATT7022_Ua 0x01 // A相电压有效值 #define ATT7022_Ub 0x02 // B相电压有效值 #define ATT7022_Uc 0x03 // C相电压有效值 #define ATT7022_Ia 0x04 // A相电流有效值 #define ATT7022_Ib 0x05 // B相电流有效值 #define ATT7022_Ic 0x06 // C相电流有效值 #define ATT7022_Pa 0x07 // A相有功功率 #define ATT7022_Pb 0x08 // B相有功功率 #define ATT7022_Pc 0x09 // C相有功功率 #define ATT7022_Psum 0x0A // 三相总有功功率 #define ATT7022_Qa 0x0B // A相无功功率 #define ATT7022_Qsum 0x0C // 三相总无功功率 #define ATT7022_Freq 0x0D // 电网频率逻辑说明:电压电流有效值寄存器是24位无符号数,功率寄存器是24位有符号补码,频率寄存器是16位无符号数。读回来的原始值不是物理量,必须乘以每字对应的增益系数才能得到伏特和安培,增益系数由校表过程确定。驱动里读取函数把24位数据放上32位有符号变量时要做符号扩展,否则负功率会变成一个很大的正数。
下表归纳了四类寄存器在驱动里的处理方式:
| 寄存器类型 | 位宽 | 符号 | 换算方式 |
|---|---|---|---|
| 有效值寄存器 | 24位 | 无符号 | raw × 电压/电流增益 |
| 功率寄存器 | 24位 | 有符号补码 | raw × 功率增益 |
| 频率寄存器 | 16位 | 无符号 | raw / 分辨率系数 |
| 校表寄存器 | 24位 | 视通道而定 | 拆成增益与偏置两段 |
参数说明:校表寄存器里的增益位通常是定点格式,有的是Q15,有的是Q11,用之前要确认你的驱动版本用的是哪种,拿错格式会导致读出来的物理量差8倍、16倍。
2.3 驱动代码的模块划分与文件职责
驱动包内一般分三个文件,分工很明确:
| 文件 | 职责 | 移植时需要改什么 |
|---|---|---|
| att7022.h | 寄存器地址、数据结构体、接口函数声明 | 引脚宏、SPI句柄 |
| att7022.c | SPI读写时序、24位数据拼装、换算函数 | 一般不用动 |
| att7022_calib.c | 校表参数写入、增益计算 | 按实际硬件改增益系数 |
数据流是一条直线:应用层调用读取函数,底层通过SPI发命令、收数据,拼成24位寄存器值,再乘增益得到浮点物理量。实际开发时我把读取操作全部封装成无锁轮询,不放进中断里跑,因为ATT7022单次读取需要约50us,放在中断里会挤占其他实时任务。如果必须用中断读取,建议加上临界区保护。
3. STM32移植实战:从初始化到读取三相数据的完整流程
3.1 硬件连接与GPIO配置
ATT7022与STM32的连接方式很固定——四线SPI加一根复位脚。我一般把CS单独拉到一个GPIO,不用硬件SPI的NSS,这样CS时序完全由软件控制,排查通信问题也方便。接线对应关系是这样的:SCLK接STM32的SPI时钟引脚,DIN接主机输出引脚,DOUT接主机输入引脚,CS任意GPIO。
void att7022_gpio_init(void) { GPIO_InitTypeDef gpio; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5-SCLK, PA7-DIN, PA4-CS gpio.Pin = GPIO_PIN_5 | GPIO_PIN_7; gpio.Mode = GPIO_MODE_AF_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); // PA6-DOUT, 芯片DOUT引脚通常有弱上拉 gpio.Pin = GPIO_PIN_6; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &gpio); gpio.Pin = GPIO_PIN_4; gpio.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, &gpio); ATT7022_CS_HIGH(); }逻辑说明:PA5、PA7配成复用推挽输出,对应SCLK和DIN;PA6配成复用开漏加外部上拉,或者直接复用推挽加内部上拉脉冲,读取时能稳定采集到DOUT的电平变化。PA4是普通推挽输出做CS,默认拉高保证片选无效。
参数说明:GPIO_SPEED_FREQ_HIGH对应50MHz翻转率,SPI时钟此时可以跑到18MHz,但ATT7022建议控制在2MHz以内,所以后面SPI初始化里分频系数要调大。如果板子上MISO走线较长,可以在DOUT引脚对地加一个10pF电容滤高频噪声,我遇到过不加电容时数据偶发抖动的情况。
3.2 SPI初始化与上电复位时序
void att7022_spi_init(void) { SPI_HandleTypeDef hspi; hspi.Instance = SPI1; hspi.Init.Mode = SPI_MODE_MASTER; hspi.Init.Direction = SPI_DIRECTION_2LINES; hspi.Init.DataSize = SPI_DATASIZE_8BIT; hspi.Init.CLKPolarity = SPI_POLARITY_HIGH; hspi.Init.CLKPhase = SPI_PHASE_2EDGE; hspi.Init.NSS = SPI_NSS_SOFT; hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; hspi.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi); }逻辑说明:CLKPolarity=HIGH、CLKPhase=2EDGE组合起来就是SPI模式3。ATT7022手册要求的采样边沿是第二个时钟沿,时钟空闲时为高电平,这两项必须和芯片匹配。分频系数32在72MHz主频下得到2.25MHz的SCLK,已经接近芯片上限,如果通信不稳定就再降一档到64。
上电初始化里有一个容易被忽略的点——复位后要等DSP自检完成才能读寄存器,我习惯写成这样:
void att7022_init(void) { att7022_gpio_init(); att7022_spi_init(); // 拉低复位脚至少10us ATT7022_RST_LOW(); delay_us(20); ATT7022_RST_HIGH(); // 等待DSP自检完成 delay_ms(250); // 读一次频率寄存器验证通信 int16_t freq = att7022_read_reg(ATT7022_Freq); if (freq > 0) { // 通信正常,继续后续校表 } }参数说明:delay_us(20)是复位脉冲最小宽度的裕量,芯片手册要求10us以上。250ms等待时间不能省,我踩过一次把它改成50ms的坑,结果上电后第一帧数据全是0,排查半天发现是DSP没准备好。读频率寄存器作为通信用了一条低成本验证路径——如果SPI时序不对,这里读到的就是一个随机数或0。
3.3 读取三相电压电流并换算成物理量
SPI打通之后,读取数据就简单了。核心是换算函数,注意符号扩展和增益系数:
typedef struct { float ua, ub, uc; float ia, ib, ic; float pa, pb, pc, psum; } att7022_data_t; // 增益系数由校表确定,示例值不可直接用于你的硬件 #define U_GAIN 0.00032f #define I_GAIN 0.000125f #define P_GAIN 0.00004f void att7022_read_data(att7022_data_t *d) { d->ua = (float)att7022_read_reg(ATT7022_Ua) * U_GAIN; d->ub = (float)att7022_read_reg(ATT7022_Ub) * U_GAIN; d->uc = (float)att7022_read_reg(ATT7022_Uc) * U_GAIN; d->ia = (float)att7022_read_reg(ATT7022_Ia) * I_GAIN; d->ib = (float)att7022_read_reg(ATT7022_Ib) * I_GAIN; d->ic = (float)att7022_read_reg(ATT7022_Ic) * I_GAIN; d->pa = (float)att7022_read_reg(ATT7022_Pa) * P_GAIN; d->pb = (float)att7022_read_reg(ATT7022_Pb) * P_GAIN; d->pc = (float)att7022_read_reg(ATT7022_Pc) * P_GAIN; d->psum = (float)att7022_read_reg(ATT7022_Psum) * P_GAIN; }逻辑说明:所有物理量等于原始寄存器值乘每字增益。增益值的来源是校表流程——接上已知标准源,读出原始值,用标准值除以原始值。示例里的数值是A型号参考设计算出来的,换一块板子、换一对采样电阻,增益就会变。
参数说明:U_GAIN的理论起点是满量程电压 / (2^23 - 1),比如电压通道满量程300V,那么每个LSB约等于0.0000358V。如果你算出来的值和驱动里默认值差一个数量级,先查电阻分压比和互感器变比,再查校表寄存器里是不是残留了旧增益。
3.4 校表寄存器写入与增益修正
校表是三相计量方案里绕不开的一步。驱动包一般提供写校表寄存器的函数,格式为两字节增益值加两字节偏置值。以电压通道为例:
void att7022_write_calib(uint8_t reg, uint16_t gain, uint16_t offset) { uint32_t calib_value = 0; // 拼装校表字:高16位增益,低16位偏置 calib_value = ((uint32_t)gain << 16) | (offset & 0xFFFF); ATT7022_CS_LOW(); spi_write_byte(0x40 | (reg & 0x1F)); // 写命令 spi_write_byte((calib_value >> 16) & 0xFF); spi_write_byte((calib_value >> 8) & 0xFF); spi_write_byte(calib_value & 0xFF); ATT7022_CS_HIGH(); // 写入后需发校表生效命令 att7022_write_reg(0x20, 0x01); delay_ms(10); }逻辑说明:写校表和读数据不同,一共4字节:1字节命令加3字节数据。命令字节bit7为0表示写操作,bit5为1表示写校表寄存器。写入完成后必须触发一次校表生效命令,否则数据不会刷新到DSP的运算单元,这就是下一章要讲的影子寄存器机制。
参数说明:gain和offset的比特含义不同厂商资料有差异。有的是12位增益加12位偏置凑成24位,有的是16位增益加8位偏置。写之前先确认驱动头文件里结构体定义,我见过有人把Q15格式当成Q11格式写进去,输出电压误差直接翻了16倍。
4. 数据稳定性处理:软件滤波与三相数据对齐
4.1 为什么直接读到的数据不能直接上报
ATT7022内部DSP是按固定周期刷新数据的,这个周期和电网频率相关,通常是几十毫秒一次。你在任意时刻发起SPI读取,拿到的可能是前一个周期结算完的结果,也可能刚好跨过数据更新边界。单次读回的电压电流值有明显的抖动,尤其电流通道,波动幅度能到几个百分比。
我一般会在驱动外层加一层数据处理逻辑,不直接把原始寄存器值上报给上位机或LCD。这也是工业现场调试告诉我的经验——电能表进程里要的是稳定读数,而不是每一次的瞬时值。常见做法是拿8到16次读数做滑动平均,把噪声压到千分级。
4.2 滑动平均滤波的实现
#define FILTER_N 8 typedef struct { float buf[FILTER_N]; uint8_t index; float sum; } avg_filter_t; void filter_push(avg_filter_t *f, float val) { f->sum -= f->buf[f->index]; f->buf[f->index] = val; f->sum += val; f->index = (f->index + 1) % FILTER_N; } float filter_get(avg_filter_t *f) { return f->sum / FILTER_N; }逻辑说明:这是一个典型的循环队列滑动平均。每次压入新值,把最旧的值从累加和里减掉,再把新值加进去,最后除以窗口长度。窗口长度必须大于1,小于等于16,太大会拖慢响应速度,太小滤波效果不明显。
参数说明:FILTER_N选8对于50Hz工频下的抖动已经够用,如果想更平滑可以设16,但注意响应滞后会增加到约300ms。如果设备需要做潮流方向判断,滞后太大会导致功率反向时读数迟钝,这种情况建议N保持8,再叠加一个一阶低通。
4.3 三相数据的时序对齐问题
还有一个容易忽略的细节:读取三相数据时,A相和C相可能不是一个数据刷新周期的结果。读Ua时DSP刚完成结算,读到Uc时下一个周期已经刷新了,两次读数在时间上错开了几十毫秒。
解决办法是连续读取两次同一通道,第二次数值和第一次完全一致时再使用,否则说明读取跨越了刷新边界。更进一步,我习惯把三相电压、电流、功率一次性连续读出来,放在同一个结构体里,再统一打上时间戳。虽然几毫秒内三相值本身也会有微小变化,但比分开读要可靠得多。
void att7022_read_snapshot(att7022_data_t *d, uint32_t timestamp) { // 分两个批次读取,第一批电压,第二批电流和功率 for (int i = 0; i < 2; i++) { d->ua = (float)att7022_read_reg(ATT7022_Ua) * U_GAIN; // 读同一寄存器第二次并校验 float tmp = (float)att7022_read_reg(ATT7022_Ua) * U_GAIN; if (tmp != d->ua) { d->ua = tmp; // 取新值,下一次循环再稳定 } // 其余通道同理 } d->timestamp = timestamp; }逻辑说明:两次读取同一寄存器,值不相同说明跨越了DSP刷新点,取第二次结果再校验一次。这个逻辑简单直接,代价是多花几十us,换来的数据一致性在示波器上能明显看到电压电流相位关系更稳定。
参数说明:timestamp参数由应用层传入,可以是RTC时间或系统tick。注意结构体里要定义这个字段,否则编译报错。这种快照语义的数据才适合做功率因数计算和电能累加。
5. 避坑指南:ATT7022调试中常见的四个翻车现场
5.1 读回数据全是0,CS拉低也没用
现象:SPI初始化后读任何寄存器都返回0,用逻辑分析仪看波形,SCLK和DIN都有输出,MISO一只保持低电平。
原因:最常见的是SPI时钟极性和相位不匹配。ATT7022要求SCLK空闲为高、数据在第二个边沿采样,也就是SPI模式3。如果初始化代码里沿用其他SPI器件的默认配置(模式0),芯片收不到有效命令,自然不会回数据。
解决:把CLKPolarity改成SPI_POLARITY_HIGH,CLKPhase改成SPI_PHASE_2EDGE,重新初始化再读。另外检查CS低电平期间有没有别的片选信号抢总线,SPI Flash和ATT7022共用一条SPI总线的场景下,CS没完全释放也会导致通信错乱。
5.2 电压读数正常,电流偏大好几倍
现象:电压通道读数跟万用表一致,但电流读数明显偏大,实际0.5A读出来3.2A。
原因:电流通道的采样链路和电压通道不同,电流互感器二次侧经过采样电阻转成电压信号。驱动里如果直接套用电压通道的增益系数,差的就是采样电阻阻值比例。另一个常见原因是校表寄存器里残留了上一次调试的增益值,没有清空。
解决:用标准电流源输入一个已知值,读原始寄存器值,算出真实的I_GAIN再更新到换算函数里。不要用默认值,每台设备的采样电阻误差都会导致增益有差异,至少要做一个点位的校准。清校表寄存器的方法是全部写1,再读一次确认原始值回到满量程比例。
5.3 功率读数是负数,或者三相符号不一致
现象:A相功率为正、B相为负,实际负载是对称的。
原因:电压和电流通道的相位关系不对。ATT7022计算功率依赖电压电流的相位差,某相电流互感器进出线接反,或者电压采样相序不对,DSP算出的有功功率就会出现负值。三相三线制和三相四线制在芯片侧的配置也不同。
解决:先查硬件接线——电流互感器同名端要一致,电压相序按A、B、C顺序接入。再查驱动初始化里有没有配置三线四线模式的寄存器写入,把它显式固定下来,不要依赖默认值。软件上如果确认接线没问题,就在换算函数里对负功率取绝对值,但这是治标不治本。
5.4 校表写入后读数没变化
现象:调用校表写入函数返回正常,重新读寄存器,数值跟写之前一模一样。
原因:校表寄存器是影子寄存器,写入后需要特定的生效命令把影子值加载到DSP工作寄存器。驱动包里如果漏了生效命令,或者写入顺序不对——先写了生效再写数据——数据就不会起作用。
解决:确认顺序是:先写校表数据,再写0x20寄存器置1,延时10ms,再把0x20清0。有些驱动版本要求最后一步清0,否则后续校表数据可能被锁定。调试时先把增益全改1,确认读数能对应原始值,再逐项修改,能更快定位是写入问题还是生效问题。
提示:遇到数据异常不要急着怀疑芯片坏了,先做一次全寄存器回读,对照手册确认每个寄存器当前值,70%的奇怪问题都能在寄存器层面找到线索。
6. 进阶验证:用低压自检与参数持久化确认驱动正确性
6.1 万用表比对法
驱动移植完成后,不要一上来就接三相强电。用一个隔离变压器输出一个已知低压,比如12V或24V交流,接到ATT7022的电压采样输入端,读取Ua寄存器,把原始值乘U_GAIN,对比万用表测到的实际有效值。误差在1%以内说明电压通道没问题。这个方法也叫低压自检,能暴露驱动里七成的bug,而且安全风险极低。
电流通道验证需要能输出小电流的源,一个简单做法是用功率电阻从低压电源拉一个已知电流,例如12V除以24Ω等于0.5A,让这个电流流过电流互感器原边,副边按变比折算,同样对比读数。两次验证都通过,驱动基本可以上电跑实际负载了。
6.2 校表参数持久化的最小实现
校表完成后,把增益和偏置存到STM32内部Flash里,每次上电先读Flash,有值就直接写回ATT7022,没有才执行默认参数。这样即使芯片掉电导致影子寄存器丢失,设备也能自动恢复校准数据。关键代码逻辑如下:
void att7022_restore_calib(void) { uint16_t gain = 0, offset = 0; if (calib_load_from_flash(&gain, &offset) == 0) { att7022_write_calib(ATT7022_CALIB_UA, gain, offset); } else { // Flash没有有效值,写入默认校准数据 att7022_write_calib(ATT7022_CALIB_UA, DEFAULT_GAIN, DEFAULT_OFFSET); } }这段逻辑把掉电恢复变成一道固定工序:上电拉高复位,等DSP自检,读Flash,写校表,再进入正常采样循环。从那以后我每次做完一套三相计量驱动,都会强制走一遍「低压自检→标准源校表→Flash存储→上电恢复」四步流程再交给下一个人,返工率确实降了不少。希望帮到你。
本文还有配套的精品资源,点击获取