一、前言
目前有个电源组需要通过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驱动也在网上搜一下安装即可。
二、资料收集
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:
三、CubeMX配置
1、方式1-X-CUBE-SMBUS(F1、F4等无法正常使用)
下载安装配置SMBUS中间件,基于i2c配置SMBus:(示例一下SMBUS配置,对于F1、F4等可以直接配置使用i2c,获取配置GPIO口软件模拟I2C)
配置开启SMBUS中间件:
第一次是没有的,需要登录下载安装:
2、HAL库方式i2c接口(不推荐,硬件i2c可能总是busy)
配置简单,接口也简单,但是调试时经常发现读写接口返回为HAL_BUSY,所以不是很推荐。
3、HAL库GPIO模拟i2c(推荐)
配置两个GPIO为SCL和SDA,然后增加us级别的延时,之后自行根据i2c协议模拟开始、结束、收、发、ack。
//
// 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)))//表示数据是1SDA_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,就是无效应答elseack = 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));//数据就是1elsedata &= ~(0x1 << (7 - i));//数据就是0SCL_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 0typedef 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进行读取:
一般常用读取的内容如下(出厂日期的计算比较特殊,需要按位分割计算,这在很多协议中比较常见):
- 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.)
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读取到的内容:
六、注意事项
- 1、注意I2C读取多个字节问题:如果发现读取多个字节时第一个字节正确,后续字节为FF,那么大概率是ACK应答设置错误,导致后续字节未正常应答;
- 2、小端结果,多字节转换时注意:多个字节时往往先读取到的是小端内容,但也要结合具体的配置,如果是大端先出则最终转换时计算结果会不同,最好结合测试工具和各字节内容先读取一个来确认一下字节序;
- 3、如果软件模拟i2c发送寄存器地址一直ack超时,可以尝试切换使用HAL库的i2c接口;
七、最后
单组电池的供电时间毕竟有限,往往我们会有多个电池组来保证续航,对于多组电池我们stm32单片机的i2c可能是不够用的,这个时候会用到TCA9548A这样的芯片来扩展复用I2C,通过选择通道来读取不同通道I2C上连接的电池的信息。