目录
WiFi物联网智能插座硬件设计的重点就是电能计量,为此单独写一篇博文讲解电量计量的设计方案和实现原理。
电量计量选用上海贝岭的BL0942芯片,最主要有原因是:硬件方案设计简单、计量精度不错且免校准、价格便宜以及软件驱动方法简单。
BL0942?能够测量电流、电压有效值、有功功率、有功电能量等参数,可输出快速电流有效值(用于过流保护),以及波形输出等功能,外围元件满足一定条件下可以免校准,当然BL0942?也是支持校准的。
芯片特性如下:
本项目选用SSOP10L封装,UART驱动。
本项目通过UART总线读取或设置BL0942芯片寄存器,寄存器说明如下图所示:
BL0942芯片UART通信特性如下:
TSSOP14L?封装可支持器件片选功能,硬件片选地址管脚为[A2_NCS,A1],可选器件?0~3。可支持?4?片?BL0942?挂在?UART?总线上进行数据通信,只占用?MCU?的一个?UART?接口。
在UART通信模式下,先发送?8bit?识别字节(0x58)?或(0xA8),(0x58)是读操作识别字节,(0xA8)是写操作识别字节,然后再发送寄存器地址字节,决定访问寄存器的地址(请参见?BL0942?寄存器列表),一帧数据传送完成,BL0942?重新进入通信模式。
帧结构有两种,写操作帧和读操作帧。
写操作帧格式如下所示:
主机UART?写数据时序如下图所示,主机先发送命令字节{1,0,1,0,1,0,A2,A1},然后发送需要写入数据的寄存器字节(ADDR),接下来依次发送数据字节(低字节在前,高字节在后,数据有效字节不足?3?字节的,无效位补?0),最后校验和字节。
{1,0,1,0,1,0,A2,A1}为写操作的帧识别字节。假设{A2,A1}=10,器件地址?2,帧识别字节为0xAA。?
ADDR?为写操作对应的?BL0942?的内部寄存器地址。?
CHECKSUM?字节为({1,0,1,0,1,0,A2,A1}+ADDR+DATA[7:0]+DATA[15:8]+DATA[23:16])&0xFF?取反。
读操作帧格式如下所示:
主机UART?读数据时序如下图所示,主机先发送命令字节{0,1,0,1,1,0,A2,A1},然后发送需要读取的寄存器地址字节(ADDR),接下来?BL0942?依次发送数据字节(低字节在前,高字节在后,数据有效字节不足?3?字节的,无效位补?0),最后校验和字节。?
{0,1,0,1,1,0,A2,A1}为读操作的帧识别字节,假设{A2,A1}=10,器件地址?2,帧识别字节为0x5A。?
ADDR?为读操作对应的?BL0942?的内部寄存器地址。?
CHECKSUM?字节为({0,1,0,1,1,0,A2,A1}+ADDR+DATA[7:0]+DATA[15:8]+DATA[23:16])&0xFF?取反。?
注意:SSOP10L?封装的器件地址是?0,即{A2,A1}=00。
时序要求如下图所示:
本项目读取BL0942电能计量参数,就是读取全电参数数据包。
通过命令“{0,1,0,1,1,0,A2,A1}+?0xAA”,BL0942?会返回一个全电参数数据包。返回的数据包共?22?个字节,当使用?4800bps?时,用时约?48ms。?
全电参数包格式如下:
checksum=(({0,1,0,1,1,0,A2,A1}?+?0x55?+?data1_l?+?data1_m?+?data1_h?+…….)&?0xff)再按位取反。
使用模式寄存器?UART_RATE_SEL(MODE[9:8])和管脚?SCLK_BPS?可以配置波特率。?芯片每次上电时?RATE_SEL?复位值为?0x0,此时根据管脚?SCLK_BPS?确定波特率。?
BL0942芯片内置了一些保护机制,如下所示:
BL0942?主要分为模拟信号处理和数字信号处理两块,模拟部分主要包括两通道?PGA、两通道Sigma-Delta?ADC、内置时钟(internal?clock)、上下电监测(Power?on/reset)、LDO?等相关模拟模块,数字部分为数字信号处理模块(DSP)。
电流和电压分别通过模拟模块放大器(PGA)和高精度的模数转换(ADC)得到两路1bit?PDM?给数字模块,数字模块经过降采样滤波器(SINC3)、高通滤波器(HPF)、通道偏置校正等模块,得到需要的电流波形数据和电压波形数据(I_WAVE,V_WAVE)。?
采集到的负载电流和电压波形数据以?7.8k?的速率更新,每个采样数据为?20bit?有符号数,并分别存入波形寄存器(I_WAVE,V_WAVE),SPI?速率配置大于?375Kbps,可连续读取一个通道的波形值。
注:寄存器为?24bit,不足位数,高位补零。?
有功功率计算公式:
其中,𝐼(𝐴),𝑉(𝑉)为通道管脚输入信号的有效值(mV),φ为?I(A)、V(V)交流信号的相位夹角,Vref?为内置基准电压,典型值为?1.218V。
该寄存器表示当前有功功率是正功还是负功,Bit[23]为符号位,Bit[23]=0,当前功率为正功,Bit[23]=1,当前功率为负功,补码形式。?
BL0942?具有专利功率防潜功能,保证无电流输入的时候板级噪声功率不会累积电量。?
有功防潜动阈值寄存器(WA_CREEP),为?8bit?无符号数,缺省为?0BH。该值与有功功率寄存器值对应关系见下面公式,当输入有功功率信号绝对值小于这个值时,输出有功功率设为?0。这可以使在无负载情况下,即使有小的噪声信号,输出到有功功率寄存器中的值为?0,电能不累积。
可以根据功率寄存器?WATT?的值设置?WA_CREEP,它们的对应关系如下:
注:当前通道处于防潜状态时,该通道的电流有效值不测量,也切除到?0。
BL0942?提供电能脉冲计量,有功瞬时功率按时间进行积分,可获得有功能量,并可进一步输出校验脉冲?CF。CF_CNT?寄存器保存输出电能脉冲?CF?的个数,具体如下图所示:
可直接从有功电能脉冲计数寄存器?CF_CNT?读取用电量,也可通过配置?OT_FUNX?寄存器后,由?I/O?中断从?CF1/CF2/ZX?引脚直接对脉冲个数进行计数,CF?的周期小于?160ms?时,为?50%占空比的脉冲,大于等于?160ms?时,高电平固定脉宽?80ms。?
CF_EN?为能量脉冲输出总开关,关闭后,CF_CNT?停止计数,CF1/CF2/ZX?引脚停止输出电能脉冲计数。?
可通过?CF_CNT_CLR_SEL?寄存器,选择?CF?计数寄存器(CF_CNT)读后是否清零。可通过CF_CNT_ADD_SEL?对脉冲能量累加模式进行选择。?
注:CF_CNT?寄存器默认电能脉冲绝对值累积方式。?
每个?CF?脉冲的累积时间如下所示:
其中WATT?为对应的有功功率寄存器值(WATT)。
电流和电压通道的有效值如下图所示,经过平方电路(X?2)、低通滤波器(LPF_RMS)、开根电路(ROOT),得到有效值的瞬时值?RMS_t,再经过平均得到两个通道的平均值(I_RMS?和?V_RMS)。?
设置?MODE[3].RMS_UPDAT_SEL,可选择有效值平均刷新时间是?400ms?或?800ms,默认?400ms。?
当通道处于防潜状态时,该电流通道的有效值为零。?
电流有效值转换公式:
电压有效值转换公式:
𝑉𝑟𝑒𝑓是参考电压,典型值是?1.218V。?
注:I(A)是?IP,IN?管脚间的输入信号(mV),V(V)是?VP?管脚的输入信号(mV)。
BL0942?可快速采集电流有效值实现过流检测功能。I_WAVE_F?取绝对值后进行半周波或周波时间累加,存于?I_FAST_RMS?寄存器,与电流快速有效值阈值寄存器?I_FAST_RMS_TH?进行比较后通过引脚输出过流中断。
通过?I_FAST_RMS_TH?快速有效值阈值寄存器,设定快速有效值阈值(即过流阈值)。?
取?I_FAST_RMS?寄存器的?Bit[23:8]与过流阀值?I_FAST_RMS_TH?[15:0]比较,若大于等于设置的阀值,则过流报警输出指示管脚?CF1/CF2/ZX?输出高电平。CF1/CF2/ZX?由?OT_FUNX?输出配置寄存器进行设置。?
通过?I_FAST_RMS_CYC?快速有效值刷新周期寄存器,设定快速有效值刷新周期。其中周波根据MODE[5]的设置值可选?50H?或者?60Hz。如选择?50hz,默认?1?周波即?20ms?刷新一次。如选择最快的?0.5周波累加时,I_FAST_RMS?寄存器的误差会相对较大。?
需要注意,快速有效值和有效值的算法不一样。快速有效值仅用于大信号时的测量判断。在小信号时快速有效值的测量会由于包含直流偏置成分不准确。如果需要去除直流偏置成分,设置FAST_RMS_SEL(MODE[4])=1,I_WAVE_F?选择?HPF?后的波形。?
通过?MODE[5]设置交流电频率。?
BL0942?提供电压和电流过零检测,可由引脚?CF1/CF2/ZX?输出过零信号,管脚输出零表示波形正半周,管脚输出?1?表示波形负半周。与实际输入信号的时延?570us?。
通过?OT_?FUNX?对输出管脚进行配置(SSOP10L?封装只有?CF1)。
若电压或电流有效值过低,过零检测输出信号不稳定。?
当电压有效值V_RMS高5bit等于0时,V_ZX_LTH_F为1,表示电压有效值过低,小于满量程的1/32,电压过零指示关闭,保持为?0。?
当电流有效值?I_RMS?高?6bit?等于?0?时,I_ZX_LTH_F?为?1,表示电流有效值过低,小于满量程的?1/64,电流过零指示关闭,保持为?0。
BL0942?具有线电压频率检测功能,每个若干设定的周期(FREQ_CYC)刷新一次,所检测的是全波电压波形。?
线电压测量的分辨率为?2us/LSB(500KHz?时钟),相当于?50Hz?线路频率时的?0.01%或?60Hz?线路频率时的?0.012%。线电压寄存器(FREQ)与实际线电压频率的折算关系:?
其中默认模式下?fs=500KHz;对于?50Hz?的市电网络,测得?FREQ?的值为?20000(十进制),对于?60Hz?的市电网络,测得?FREQ?的值为?16667(十进制)。?
另外,电压有效值低于过零判断阈值时,线电压频率检测关闭。
使用Arduino?IDE驱动ESP8266周期读取全电参数数据包,源文件如下所示:
/******************************************************************************
*
* File Name : bl0942.cpp
*
* Functional Description:
* bl0942驱动库文件
*
* Change Logs:
* Date Author Notes explain
* 2022-12-6 yangjunjie V1.0
*
*******************************************************************************/
/******************************************************************************
* Include files
******************************************************************************/
#include "bl0942.h"
/******************************************************************************
* Global variable definitions
******************************************************************************/
static char serial_data[SERIAL_RX_MAXLEN];
// 串口接收数据队列句柄
extern struct tk_queue serial_receive_dataqueue;
// 此参数尚未使用,可用作二次校准设备
static float adjust_volrate = 1;
static float adjust_currentrate = 1;
static float adjust_powerrate = 1;
/******************************************************************************
* Local type definitions ('typedef')
******************************************************************************/
static void sendCommand(void);
static status_t receiveData(void);
/******************************************************************************
* function realize
******************************************************************************/
/**
******************************************************************************
** \brief 初始化BL0942芯片
**
** \param 无
**
** \retval 无
**
******************************************************************************/
void Init_BL0942(void)
{
Serial.begin(4800, SERIAL_8N1); // 4800bps 无校验
Serial.setTimeout(30); // 设置串口超时时间为30ms
memset(serial_data, 0, SERIAL_RX_MAXLEN);
}
/**
******************************************************************************
** \brief 每秒钟刷新一次,依次读(电压或者电流,和功率)
**
** \param 无
**
** \retval 无
**
******************************************************************************/
void Updata_BL0942(void)
{
sendCommand(); // 发送指令
receiveData(); // 处理串口接收数据
}
/**
******************************************************************************
** \brief 串口发送指令获取电参数据
**
** \param 无
**
** \retval 无
**
******************************************************************************/
static void sendCommand(void)
{
Serial.write((byte)0X58);
Serial.write((byte)0XAA);
}
/**
******************************************************************************
** \brief 接收并解析串口接收到的电参指令
**
** \param 无
**
** \retval STATUS_SUCCESS:数据校验成功 STATUS_ERROR:数据校验失败
**
******************************************************************************/
static status_t receiveData(void)
{
status_t ret = STATUS_SUCCESS;
bool temp_flag = false, receive_flag = false;
char temp_data = 0X00;
uint8_t data_index = 0X00;
uint8_t checksum = 0X58;
char temp_serial_data[SERIAL_RX_MAXLEN];
while(Serial.available())
{
temp_data = (char)Serial.read();
if(temp_data == 0X55) // 判断帧头
{
temp_flag = true;
}
if(temp_flag == true)
{
temp_serial_data[data_index++] = temp_data;
}
if (data_index >= SERIAL_RX_MAXLEN)
{
// tk_queue_push_multi(&serial_receive_dataqueue, temp_serial_data, SERIAL_RX_MAXLEN); // 装数据长度到缓存
data_index = 0;
temp_flag = false;
receive_flag = true;
// memset(temp_serial_data, 0, SERIAL_RX_MAXLEN);
}
else
{
receive_flag = false;
}
}
// 未用到中断接收,缓存模式暂时用不到,此功能保留
// if(tk_queue_empty(&serial_receive_dataqueue) == false) // 缓存有数据
// {
// tk_queue_pop_multi(&serial_receive_dataqueue, temp_serial_data, SERIAL_RX_MAXLEN); // 从缓存区获取数据
if(receive_flag == true)
{
for(uint8_t i = 0; i < SERIAL_RX_MAXLEN - 1; i++) // 校验数据
{
checksum += temp_serial_data[i];
}
checksum = ~(checksum & 0XFF);
if(checksum == temp_serial_data[SERIAL_RX_MAXLEN - 1])
{
memcpy(serial_data, temp_serial_data, SERIAL_RX_MAXLEN);
Log.verboseln("serial receive OK");
}
else
{
Log.errorln("serial receive ERROR");
ret = STATUS_ERROR;
}
}
return ret;
}
/**
******************************************************************************
** \brief 获取电流
**
** \param 无
**
** \retval 电流数据
**
******************************************************************************/
float getCurrent(void)
{
uint32_t parm = 0;
float current = 0.0;
parm = ((uint32_t)serial_data[3] << 16) + ((uint32_t)serial_data[2] << 8) + serial_data[1];
current = (float)parm * V_REF * adjust_currentrate * 1000 / (305978 * RL_CURRENT); // mA
return current;
}
/**
******************************************************************************
** \brief 获取电压
**
** \param 无
**
** \retval 电压数据
**
******************************************************************************/
float getVoltage(void)
{
uint32_t parm = 0;
float voltage = 0.0;
parm = ((uint32_t)serial_data[6] << 16) + ((uint32_t)serial_data[5] << 8) + serial_data[4];
voltage = (float)parm * V_REF * (R2_VOLTAGE + R1_VOLTAGE) * adjust_volrate / (73989 * R1_VOLTAGE * 1000);
return voltage;
}
/**
******************************************************************************
** \brief 过流检测,快速采集电流有效值实现过流检测功能
**
** \param 无
**
** \retval 电流快速有效值数据
**
******************************************************************************/
float getFastCurrent(void)
{
uint32_t parm = 0;
parm = ((uint32_t)serial_data[9] << 16) + ((uint32_t)serial_data[8] << 8) + serial_data[7];
float fcurrent = (float)parm * V_REF * adjust_currentrate * 1000 / (305978 * RL_CURRENT); // mA
return fcurrent;
}
/**
******************************************************************************
** \brief 获取有功功率
**
** \param 无
**
** \retval 有功功率数据
**
******************************************************************************/
float getActivePower(void)
{
uint32_t parm = 0;
float power = 0.0;
parm = ((uint32_t)serial_data[12] << 16) + ((uint32_t)serial_data[11] << 8) + serial_data[10];
if (1 == bitRead(serial_data[12], 7))
{
parm = 0xFFFFFF - parm + 1; // 取补码
}
power = (float)parm * adjust_powerrate * V_REF * V_REF * (R2_VOLTAGE + R1_VOLTAGE) / (3537 * RL_CURRENT * R1_VOLTAGE * 1000);
return power;
}
/**
******************************************************************************
** \brief 获取用电量
**
** \param 无
**
** \retval 用电量数据
**
******************************************************************************/
float getEnergy(void)
{
uint32_t parm = 0;
float energy = 0.0;
parm = ((uint32_t)serial_data[15] << 16) + ((uint32_t)serial_data[14] << 8) + serial_data[13];
energy = (float)parm * 1638.4 * 256 * V_REF * V_REF * (R2_VOLTAGE + R1_VOLTAGE) / (3600000.0 * 3537 * RL_CURRENT * R1_VOLTAGE * 1000);
return energy;
}
/**
******************************************************************************
** \brief 线电压频率检测功能
**
** \param 无
**
** \retval 线电压频率数据
**
******************************************************************************/
float getFREQ(void)
{
uint32_t parm = 0;
float FREQ = 0.0;
parm = ((uint32_t)serial_data[18] << 16) + ((uint32_t)serial_data[17] << 8) + serial_data[16];
if (parm > 0)
{
FREQ = 500 * 1000 * 2 / (float)parm;
}
return FREQ;
}
/**
******************************************************************************
** \brief 工作状态
**
** \param 无
**
** \retval 工作状态数据
**
******************************************************************************/
uint8_t getSTATUS(void)
{
return (uint8_t)serial_data[19];
}
/******************************************************************************/
/* EOF (not truncated) */
/******************************************************************************/
头文件如下所示:
/******************************************************************************
*
* File Name : bl0942.h
*
* Functional Description:
* bl0942驱动库文件
*
* Change Logs:
* Date Author Notes explain
* 2022-12-6 yangjunjie V1.0
*
*******************************************************************************/
#ifndef __BL0942_H__
#define __BL0942_H__
/******************************************************************************/
/* Include files */
/******************************************************************************/
#include <Arduino.h>
#include "tk_queue.h"
/* C binding of definitions if building with C++ compiler */
#ifdef __cplusplus
extern "C"
{
#endif
//@{
/******************************************************************************
* Local pre-processor symbols/macros ('#define')
******************************************************************************/
#define V_REF 1.218
#define RL_CURRENT 1 // 0.001欧,单位为毫欧
#define R2_VOLTAGE 1950 // 390K*5,单位为K欧
#define R1_VOLTAGE 0.51 // 0.51K,单位为K欧
#define SERIAL_RX_MAXLEN 23
/******************************************************************************
** Local type definitions ('typedef')
******************************************************************************/
/******************************************************************************
* Global variable definitions ('extern')
******************************************************************************/
// 判断队列是否为空
extern bool tk_queue_empty(struct tk_queue *queue);
// 向队列压入(入队)多个元素数据
extern uint16_t tk_queue_push_multi(struct tk_queue *queue, char *pval, uint16_t len);
// 从队列弹出(出队)多个元素数据
extern uint16_t tk_queue_pop_multi(struct tk_queue *queue, char *pval, uint16_t len);
/*****************************************************************************
* Function definitions - global ('extern') and local ('static')
******************************************************************************/
// 初始化BL0942芯片
void Init_BL0942(void);
// 每秒钟刷新一次,依次读(电压或者电流,和功率)
void Updata_BL0942(void);
// 获取电流
float getCurrent(void);
// 获取电压
float getVoltage(void);
// 过流检测,快速采集电流有效值实现过流检测功能
float getFastCurrent(void);
// 获取有功功率
float getActivePower(void);
// 获取用电量
float getEnergy(void);
// 线电压频率检测功能
float getFREQ(void);
// 工作状态
uint8_t getSTATUS(void);
//@}
#ifdef __cplusplus
}
#endif
#endif /* __BL0942_H__ */
/******************************************************************************/
/* EOF (not truncated) */
/******************************************************************************/
上海贝岭也提供了需要校准的电量计量芯片:BL0937。
BL0937?在定义产品时考虑到智能插座类产品厂家不是专业计量器具厂家,没有专业昂贵的校准设备,对电能计量精度要求也相对较低,只是提供用电参考信息,不作计费标准。智能插座只需要读取功率,电压,电流,并根据功率计量累积电量,所以BL0937?与?MCU?间不要复杂的通讯协议去实时的读取计量芯片寄存器,计量精度校准也相对简单,只需在额定功率负载时校准系数,也不需要复杂的校准设备。?
BL0937方案的应用电路如下所示,经过实际测试稳定可靠:
感兴趣的朋友可以研究一下这个方案,由于不是本项目所使用的方案,不再赘述。