电池管理协议SMBus/I2C在STM32CubeMX配置使用-读取SN8765电池组

一、前言

目前有个电源组需要通过i2c进行读取,获取一些电池信息,采用SMBus协议进行读取,其可以看作i2c的子集,可以直接通过i2c的接口进行读写。
SMBus建立在被广泛采用的I2C总线之上,并定义了OSI(开放系统互连)模型的链路和网络层。PMBus™使用SMBus作为其物理层,并添加了命令定义和其他新特性。大多数新特性都属于OSI模型的中到高层次。
读取的电池组控制芯片为SN8765,属于定制的。
设备地址一般为0x16,需要联系厂家获取一些设备和软件读取信息和i2c读取的信息进行比对,确认i2c读取内容大小端问题及转换等问题,寄存器地址基本也是固定的,找厂家获取对应文档即可。
一般使用德州仪器TI的EV2300 HPA002设备和对应的bq Evaluation Software等软件进行读取,基本配置SMBus/I2C的SCL、SDA、GND进行读取即可,EV2300使用USB供电,对应USB驱动也在网上搜一下安装即可。
image.png

二、资料收集

SMBus介绍:https://www.eet-china.com/mp/a25851.html
STM32CubeMX HAL库SMBUS和PMBUS介绍:https://www.stmcu.com.cn/Designresource/design_resource_detail?file_name=AN4502_%E5%9F%BA%E4%BA%8ESTM32Cube%E5%BA%93%E7%9A%84SMBUS%E5%92%8CPMBUS%E4%BB%8B%E7%BB%8D&lang=EN&ver=4.0&cat=application_note
Alert mode:https://blog.csdn.net/weixin_49259827/article/details/128217687
EV2300驱动:https://www.laptopu.ro/wp-content/uploads/wpforo/attachments/103/2452-EV2300aDeviceDriverInstallerMultilanguage0-6.rar
SN8765规格书:https://download.csdn.net/download/weixin_41602413/10193698?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-download-2%7Edefault%7ECTRLIST%7ERate-1-10193698-blog-83178838.235%5Ev43%5Epc_blog_bottom_relevance_base3&depth_1-utm_source=distribute.pc_relevant.none-task-download-2%7Edefault%7ECTRLIST%7ERate-1-10193698-blog-83178838.235%5Ev43%5Epc_blog_bottom_relevance_base3&utm_relevant_index=2
对于F1\F4等不太适合X-CUBE-SMBUS,可以直接使用I2C:
image.png

三、CubeMX配置

1、方式1-X-CUBE-SMBUS(F1、F4等无法正常使用)

下载安装配置SMBUS中间件,基于i2c配置SMBus:(示例一下SMBUS配置,对于F1、F4等可以直接配置使用i2c,获取配置GPIO口软件模拟I2C)
image.png
配置开启SMBUS中间件:
第一次是没有的,需要登录下载安装:
image.png

2、HAL库方式i2c接口(不推荐,硬件i2c可能总是busy)

配置简单,接口也简单,但是调试时经常发现读写接口返回为HAL_BUSY,所以不是很推荐。
image.png

3、HAL库GPIO模拟i2c(推荐)

配置两个GPIO为SCL和SDA,然后增加us级别的延时,之后自行根据i2c协议模拟开始、结束、收、发、ack。
image.png

//
// Created by Administrator on 2024/3/5.
//

#ifndef IIC_H
#define IIC_H

#include <stdint.h>

void IIC_Start(void);
void IIC_Stop(void);
void IIC_Send_Byte(uint8_t d);
uint8_t IIC_Wait_Ack(void);
uint8_t  IIC_Read_Byte(void);
void IIC_Ack(uint8_t ack);

#endif //IIC_H

//
// Created by Administrator on 2024/3/5.
//

#include "iic.h"

#include "delay.h"
#include "gpio.h"

/**********************************************************
   1.IIC软件模拟   使用HAL库时
   2.需要STM32CubeMX配置初始化的相关引脚为GPIO模式 SDA SCL初始状态下都是输出 推挽 上拉模式
   4.初始状态下SDA 与 SCL要给高电平 使用高低电平转换时之间要有明显的us级延时
**********************************************************/
static GPIO_InitTypeDef GPIO_InitStruct;
/**********************************************************
1.引脚配置 宏定义用IF语句
2.给引脚电平必须要给输出模式
3.SCL一直都是输出模式(输出时钟肯定是输出模式)
4.宏定义绑定引脚SDA与SCL   SDA PB5    SCL PB4
**********************************************************/
#define SCL_Type     GPIOB
#define SDA_Type     GPIOB

#define SCL_GPIO    GPIO_PIN_4
#define SDA_GPIO    GPIO_PIN_5
//设置输出高低电平模式
#define SDA_OUT(X)   if(X) \
                     HAL_GPIO_WritePin(SDA_Type, SDA_GPIO, GPIO_PIN_SET); \
                     else  \
                     HAL_GPIO_WritePin(SDA_Type, SDA_GPIO, GPIO_PIN_RESET);

#define SCL_OUT(X)   if(X) \
                     HAL_GPIO_WritePin(SCL_Type, SCL_GPIO, GPIO_PIN_SET); \
                     else  \
                     HAL_GPIO_WritePin(SCL_Type, SCL_GPIO, GPIO_PIN_RESET);

#define SDA_IN         HAL_GPIO_ReadPin(SDA_Type,SDA_GPIO)//只有输入模式才能读取电平状态

/*****************************************
  SDA引脚转变为 OUT输出模式(输出模式给停止 开始信号)
******************************************/
void IIC_SDA_Mode_OUT(void) {
    GPIO_InitStruct.Pin = SDA_GPIO;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SDA_Type, &GPIO_InitStruct);
}

/*****************************************
  SDA引脚转变为 输入模式(输入模式传输具体的数据)
******************************************/
void IIC_SDA_Mode_IN(void) {
    GPIO_InitStruct.Pin = SDA_GPIO;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SDA_Type, &GPIO_InitStruct);
}

/*****************************************
  IIC开始信号
******************************************/
void IIC_Start(void)//IIC开始信号
{
    //设置为输出模式
    IIC_SDA_Mode_OUT();

    //空闲状态两个引脚是高电平
    SDA_OUT(1);
    SCL_OUT(1);
    delay_us(5);

    //拉低数据线
    SDA_OUT(0);
    delay_us(5);

    //再拉低时钟线
    SCL_OUT(0);
    delay_us(5);
}

//IIC停止信号
void IIC_Stop(void) {
    //设置为输出模式
    IIC_SDA_Mode_OUT();

    //拉低
    SDA_OUT(0);
    SCL_OUT(0);
    delay_us(5);

    //时钟线先拉高
    SCL_OUT(1);
    delay_us(5);

    //再把数据线拉高
    SDA_OUT(1);
    delay_us(5);
}

void IIC_Send_Byte(uint8_t d)//主机发送8位数据给从机MSB 高位先发
{
    uint8_t i = 0;
    //设置为输出模式
    IIC_SDA_Mode_OUT();

    SDA_OUT(0);
    SCL_OUT(0);
    delay_us(5);
    for (i = 0; i < 8; i++) {
        if (d & (0x1 << (7 - i)))//表示数据是1
            SDA_OUT(1)
        else SDA_OUT(0);

        delay_us(5);
        SCL_OUT(1);//拉高时钟线,告诉对方你可以读了

        delay_us(5);
        SCL_OUT(0);//拉低时钟线,告诉对方你暂时别读,我在准备数据
    }

}

uint8_t IIC_Wait_Ack(void)//等待从机给主机应答或者不应答
{
    uint8_t ack = 0;
    //设置为输入模式
    IIC_SDA_Mode_IN();

    //时钟线拉高,时钟线为高电平期间,不管是数据还是ack都是有效的
    SCL_OUT(1);
    delay_us(5);

    if (SDA_IN == 1)
        ack = 1;//无效ACK,就是无效应答
    else
        ack = 0;//有效ACK,就是有效应答

    SCL_OUT(0);

    delay_us(5);
    return ack;
}

uint8_t IIC_Read_Byte(void)//从机发送8位数据给主机
{
    uint8_t i = 0;
    uint8_t data = 0;
    //设置为输入模式
    IIC_SDA_Mode_IN();
    //先拉低时钟线,准备数据
    SCL_OUT(0);
    delay_us(5);

    for (i = 0; i < 8; i++) {
        SCL_OUT(1);//时钟线为高电平期间数据才是有效的
        delay_us(5);
        if (SDA_IN == 1)
            data |= (0x1 << (7 - i));//数据就是1
        else
            data &= ~(0x1 << (7 - i));//数据就是0

        SCL_OUT (0);//告诉对方此时准备数据,先别读写
        delay_us(5);
    }
    return data;
}

//主机发送应答或者不应答给从机,1高电平不应答,反之应答
void IIC_Ack(uint8_t ack)
{
    //设置为输出模式
    IIC_SDA_Mode_OUT();

    SDA_OUT(0);
    SCL_OUT(0);
    delay_us(5);

    SDA_OUT(ack);//发送高/低电平--->发送不应答/应答
    delay_us(5);

    SCL_OUT(1);//告诉从机我已经准备好数据,你可以读取了
    delay_us(5);

    SCL_OUT (0);//拉低时钟线,发送ack结束
    delay_us(5);
}

void delay_us(uint32_t udelay)
{
    uint32_t startval,tickn,delays,wait;

    startval = SysTick->VAL;
    tickn = HAL_GetTick();
    //sysc = 72000;  //SystemCoreClock / (1000U / uwTickFreq);
    delays =udelay * 72; //sysc / 1000 * udelay;
    if(delays > startval)
    {
        while(HAL_GetTick() == tickn)
        {

        }
        wait = 72000 + startval - delays;
        while(wait < SysTick->VAL)
        {

        }
    }
    else
    {
        wait = startval - delays;
        while(wait < SysTick->VAL && HAL_GetTick() == tickn)
        {

        }
    }
}

四、示例代码

I2C需要确认方向、设备地址,寄存器地址,之后读写多个字节的流程就比较固定了,这里结合SN8765读取做了简单封装:

#include <stdio.h>
#include <string.h>
#include "battery.h"
//#include "i2c.h"
#include "iic.h"

#define SN8765_DEVICE_ADDRESS 0x16
#define I2C_READ 1
#define I2C_WRITE 0

typedef enum {
    BATTERY_SBS_CMD_TEMPERATURE = 0x08,
    BATTERY_SBS_CMD_VOLTAGE,
    BATTERY_SBS_CMD_CURRENT,
    BATTERY_SBS_CMD_RELATIVE_STATE_CHARGE = 0x0d,
    BATTERY_SBS_CMD_CYCLE_COUNT = 0x17,
    BATTERY_SBS_CMD_MANUFACTURER_DATE = 0x1b,
    BATTERY_SBS_CMD_SERIAL_NUMBER,
    BATTERY_SBS_CMD_MANUFACTURER_NAME = 0x20,
    BATTERY_SBS_CMD_DEVICE_NAME,
} BATTERY_SBS_CMD;

uint8_t i2c_read_word(uint8_t cmd, uint8_t *res, uint8_t read_size) {
    //开始
    IIC_Start();

    //发送器件地址+写命令
    IIC_Send_Byte(SN8765_DEVICE_ADDRESS|I2C_WRITE);

    if(IIC_Wait_Ack()) {
        printf("wait ack failed1.\n");
        return 0;
    }

    //发送寄存器地址
    IIC_Send_Byte(cmd);

    if(IIC_Wait_Ack()) {
        printf("wait ack failed2.\n");
        return 0;
    }

    IIC_Start();

    //发送器件地址+读命令
    IIC_Send_Byte(SN8765_DEVICE_ADDRESS|I2C_READ);

    if (IIC_Wait_Ack()) {
        printf("wait ack failed3.\n");
        return 0;
    }

    //读单个或多个字节
    int i;
    for (i = 0; i < read_size; i++) {
        res[i] = IIC_Read_Byte();

        if (i != read_size - 1) {
            //应答
            IIC_Ack(0);
        } else {
            //最后一个字节读取后不应答
            IIC_Ack(1);
        }
    }

    //停止
    IIC_Stop();

    return i;
}

void readAllBatteryInfo(T_BatteryInfo *batteryInfo) {
    uint8_t data[32] = {'\0'};
    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_TEMPERATURE, data, 2);
    batteryInfo->temperature = data[0] + (data[1] * 256);
    printf("data0:%02x,data1:%02x,temp:%d\n", data[0], data[1], batteryInfo->temperature);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_VOLTAGE, data, 2);
    batteryInfo->voltage = data[0] + (data[1] * 256);
    printf("data0:%02x,data1:%02x,voltage:%d\n", data[0], data[1], batteryInfo->voltage);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_CURRENT, data, 2);
    batteryInfo->current = (short)(data[0] + (data[1] * 256));
    printf("data0:%02x,data1:%02x,current:%d\n", data[0], data[1], batteryInfo->current);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_RELATIVE_STATE_CHARGE, data, 1);
    batteryInfo->relativeStateOfCharge = data[0];
    printf("data0:%02x,relativeStateOfCharge:%d\n", data[0], batteryInfo->relativeStateOfCharge);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_CYCLE_COUNT, data, 2);
    batteryInfo->cycleCount = data[0] + (data[1] * 256);
    printf("data0:%02x,data1:%02x,cycleCount:%d\n", data[0], data[1], batteryInfo->cycleCount);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_MANUFACTURER_DATE, data, 2);
    batteryInfo->manufactureDate = data[0] + (data[1] * 256);
    printf("data0:%02x,data1:%02x,manufactureDate:%d\n", data[0], data[1], batteryInfo->manufactureDate);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_SERIAL_NUMBER, data, 2);
    batteryInfo->serialNumber[1] = data[0];
    batteryInfo->serialNumber[0] = data[1];
    printf("data0:%02x,data1:%02x,serialNumber:%d,%d\n", data[0], data[1], batteryInfo->serialNumber[0], batteryInfo->serialNumber[1]);

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_MANUFACTURER_NAME, data, 12);
    printf("manufacturer name:");
    for (int i = 0; i < 12; i++) {
        printf("%02x ", data[i]);
    }
    printf("\n");

    memset(data, '\0', sizeof(data));
    i2c_read_word(BATTERY_SBS_CMD_DEVICE_NAME, data, 8);
    printf("device name:");
    for (int i = 0; i < 8; i++) {
        printf("%02x ", data[i]);
    }
    printf("\n");
}
//
// Created by Administrator on 2024/3/5.
//

#ifndef BATTERY_H
#define BATTERY_H
#include <stdint.h>

typedef struct {
    uint16_t manufacturerAccess;
    uint16_t remainingCapacityAlarm;
    uint16_t remainingTimeAlarm;
    uint16_t batteryMode;
    uint16_t atRate;
    uint16_t atRateTimeToFull;
    uint16_t atRateTimeToEmpty;
    uint16_t atRateOK;
    uint16_t temperature;
    uint16_t voltage;
    int16_t current;
    uint16_t averageCurrent;
    uint8_t maxError;
    uint8_t relativeStateOfCharge;
    uint8_t absoluteStateOfCharge;
    uint16_t remainingCapacity;
    uint16_t fullChargeCapacity;
    uint16_t runTimeToEmpty;
    uint16_t averageTimeToEmpty;
    uint16_t averageTimeToFull;
    uint16_t chargingCurrent;
    uint16_t chargingVoltage;
    uint16_t batteryStatus;
    uint16_t cycleCount;
    uint16_t designCapacity;
    uint16_t designVoltage;
    uint16_t specificationInfo;
    uint16_t manufactureDate;
    uint8_t serialNumber[2];
    uint8_t manufacturerName[12];
    uint8_t deviceName[8];
    uint8_t deviceChemistry[5];
    uint8_t manufacturerDataOrCalibrationData[26];
    uint8_t authenticateOrManufacturerInput[32];
    uint16_t cellVoltage4;
    uint16_t cellVoltage3;
    uint16_t cellVoltage2;
    uint16_t cellVoltage1;
} T_BatteryInfo;

void readAllBatteryInfo(T_BatteryInfo *batteryInfo);

#endif //BATTERY_H

五、测试及比对结果

软件使用网图:

通过ti的软件结合EV2300 HPA002进行读取:
SMBUS_READ.png
一般常用读取的内容如下(出厂日期的计算比较特殊,需要按位分割计算,这在很多协议中比较常见):

  • 1、Cycle Count:充满一次电用完一次的循环次数,可用于充放电次数限制,强行报废电池;
  • 2、Temperature:温度(0.1K,0.1开尔文度数,需要转换为摄氏度)
  • 3、Voltage:电压(mV)
  • 4、Current:电流(mA)
  • 5、Relative State of Charge:相对电量值(百分比)
  • 6、Manufacturer Date:出厂日期(A.28 ManufactureDate (0x1b)

This read-word function returns the date the pack was manufactured in a packed integer. The date is
packed in the following fashion:
(year–1980) x 512 + month x 32 + day
The default value for this function is stored in Manuf Date.
When the SN8765 is in UNSEALED or higher security mode, this block is R/W.)
6259f3d7edc2e9e92e348b840b214a4.png
101010 1010 11101转换为2022年10月29日

  • 7、Manufacturer Name:厂家信息(有空格 16进制转ASCII:08 53 54 4c 2d 53 4f 4e 59 95 95 95)
  • 8、Device Name:设备名称(有空格,16进制转ASCII 06 54 54 4c 32 32 4d 59 )

在线进制转换:http://www.hiencode.com/jinzhi.html
I2C读取到的内容:
4e89d27d4319ffc72eaa79473fc5eda6.png

六、注意事项

  • 1、注意I2C读取多个字节问题:如果发现读取多个字节时第一个字节正确,后续字节为FF,那么大概率是ACK应答设置错误,导致后续字节未正常应答;
  • 2、小端结果,多字节转换时注意:多个字节时往往先读取到的是小端内容,但也要结合具体的配置,如果是大端先出则最终转换时计算结果会不同,最好结合测试工具和各字节内容先读取一个来确认一下字节序;
  • 3、如果软件模拟i2c发送寄存器地址一直ack超时,可以尝试切换使用HAL库的i2c接口;

七、最后

单组电池的供电时间毕竟有限,往往我们会有多个电池组来保证续航,对于多组电池我们stm32单片机的i2c可能是不够用的,这个时候会用到TCA9548A这样的芯片来扩展复用I2C,通过选择通道来读取不同通道I2C上连接的电池的信息。

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昵称系统有问题

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值