SMBus - System Management Bus
The System Management Bus (SMBus) is simple two-wire bus works very similar like I2C communication. Usually used in computers, e.g. laptop computers to communicate with smart batteries use SMBus.
I won't give information "How to replace battery cells on my laptop battery", is it worth while? This page can help using smart battery on your own built system. I will use a BQ2040 IC to demonstrate the communication.
BQ2040 in Smart battery
The bq2040 Gas Gauge IC With SMBus Interface can monitor the battery state/capacity, so your system can warn you before losing power. It has charge control functions too. The bq2040 learns battery capacity meanwhile you fully charge and discharge it, so I think it really knows the battery condition and current capacity. It uses an external EEPROM to load and store settings.
Read more from BQ2040.
As SMBus is derived from I2C, i2c-dev can handle, and it works similar, moreover in some conditions you can mix the different devices in the same line. In this case you have to solve e.g. different signal voltage levels.
I have connected BQ2040 to my CH341a USB-I2C adapter (SMBCLK to SCL, SMBDATA to SDA) and it recognized that I2C/SMBus device connected at 0x0B. The EEPROM behind the Gas Gauge IC wasn't shown, as it wasn't really connected, it is in a separate I2C Line.
As I developed my simple I2C thermostat, my plan to connect it to a smart battery, so the MCU can get informations from remaining capacity and can send alarm to change/charge battery. This would be more interesting on other systems, which ones use much more power.
Programming
Programming SMBus is similar to I2C. As bq2040 is supports SMBus 1.0 the main difference you would see that mostly the answers are words and not bytes. The commands based on SBData.
If you would like to communicate with SMBus 1.1 or later devices, you may need to deal with PEC (packet error checking) data. SMBus Quick Start Guide
Here is a not full example (only some function coded) of accessing BQ2040 through i2c-dev:
//
// Test program for accessing BQ2040 Smart battery IC through i2c-dev + i2c-ch341a-usb in C++
//
// Copyright (c)2017-2020 Tóthpál István, istvan@tothpal.eu
//
#include <linux/i2c-dev.h>
#include <stdio.h>
// -------------------------------- My classes -----------------------------
#include "include/i2c/bq2040.h"
int main (void) {
__s32 r;
__u8 result[50];
char string[256];
// init i2c line4 at 0x0B address in my hardware
bq2040 *mybq2040 = new bq2040( 4, BQ2040_BASE_ADDRESS );
if ( (r = mybq2040->get_BatteryStatus()) >= 0 ) {
mybq2040->decode_BatteryStatus(r, string );
printf("Status: %x - %s\n", r, string);
}
if ( (r = mybq2040->get_ManufacturerName( result )) >= 0 ) {
printf("Manufacturer: %s\n", result );
}
if ( (r = mybq2040->get_DeviceName( result )) >= 0 ) {
printf("Device: %s\n", result );
}
if ( (r = mybq2040->get_DesignCapacity()) >= 0 ) {
printf("Design capacity: %dmAh ", r );
if ( (r = mybq2040->get_FullChargeCapacity()) >= 0 ) {
printf("Full charge capacity: %dmAh ", r );
}
if ( (r = mybq2040->get_RemainingCapacity()) >= 0 ) {
printf("Remaining: %dmAh", r );
}
printf("\n");
}
if ( (r = mybq2040->get_DesignVoltage()) >= 0 ) {
printf("Voltage: %dmV ", r );
if ( (r = mybq2040->get_Voltage()) >= 0 ) {
printf("Currently: %dmV", r );
}
printf("\n");
}
if ( (r = mybq2040->get_Current()) >= 0 ) {
printf("Current: %dmA\n", r );
}
if ( (r = mybq2040->get_Temperature()) >= 0 ) {
printf("Temperature: %d.%d°K %d.%d°C\n", r/10, r%10, (r-2732)/10, (r-2732)%10 ); // 0°K = -273.15°C
}
delete mybq2040;
return 0;
}
I've connected an old wrong battery and the test result:
include/i2c/bq2040.h:
//
// Class header file BQ2040 Smart battery IC through i2c-dev + i2c-ch341a-usb in C++
//
// Copyright (c)2017-2020 Tóthpál István, istvan@tothpal.eu
//
#define BQ2040_BASE_ADDRESS 0x0B
#define BQ2040_TEMPERATURE_REGISTER 0x08
#define BQ2040_VOLTAGE_REGISTER 0x09
#define BQ2040_CURRENT_REGISTER 0x0a
#define BQ2040_REMAININGCAPACITY_REGISTER 0x0f
#define BQ2040_FULLCHARGECAPACITY_REGISTER 0x10
#define BQ2040_BATTERYSTATUS_REGISTER 0x16
#define BQ2040_CYCLECOUNT_REGISTER 0x17
#define BQ2040_DESIGNCAPACITY_REGISTER 0x18
#define BQ2040_DESIGNVOLTAGE_REGISTER 0x19
#define BQ2040_MANUFACTURERNAME_REGISTER 0x20
#define BQ2040_MANUFACTURERNAME_MAXLEN 11
#define BQ2040_DEVICENAME_REGISTER 0x21
#define BQ2040_DEVICENAME_MAXLEN 7
class bq2040 {
int i2c_line;
int i2c_address;
int myfile;
public:
//constructor
bq2040( int line, int address );
// destructor
~bq2040();
int get_i2c_line();
int get_i2c_address();
__s32 get_Temperature();
__s32 get_Voltage();
__s32 get_Current();
__s32 get_RemainingCapacity();
__s32 get_DesignCapacity();
__s32 get_FullChargeCapacity();
__s32 get_DesignVoltage();
__s32 get_BatteryStatus();
int decode_BatteryStatus( __s32 statuscode, char *statusstring );
__s32 get_ManufacturerName( __u8 *result );
__s32 get_DeviceName( __u8 *result );
};
bq2040.c:
//
// Class file for BQ2040 Smart battery IC through i2c-dev + i2c-ch341a-usb in C++
//
// Copyright (c)2017-2020 Tóthpál István, istvan@tothpal.eu
//
#include <i2c/smbus.h>
#include <linux/i2c-dev.h>
#include "../include/i2c/bq2040.h"
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
/*
********************************************************************
* BQ2040 CLASS *
********************************************************************
*/
// --------- Constructor ---------
bq2040::bq2040( int line, int address ) {
__s32 opResult = 0; // for error checking of operations
char line_str[20];
i2c_line = line;
i2c_address = address;
sprintf(line_str, "/dev/i2c-%d", line);
myfile = open( line_str, O_RDWR );
opResult = ioctl( myfile, I2C_SLAVE_FORCE, address );
if (opResult < 0) {
printf("%mn", opResult );
exit(EXIT_FAILURE);
}
else {
read_registers();
}
}
// ---------- Destructor ------------
bq2040::~bq2040() {
close( myfile );
}
int bq2040::get_i2c_line() {
return i2c_line;
}
int bq2040::get_i2c_address() {
return i2c_address;
}
__s32 bq2040::get_BatteryStatus() {
return i2c_smbus_read_word_data( myfile, BQ2040_BATTERYSTATUS_REGISTER );
}
__s32 bq2040::get_Temperature() {
return i2c_smbus_read_word_data( myfile, BQ2040_TEMPERATURE_REGISTER );
}
__s32 bq2040::get_Voltage() {
return i2c_smbus_read_word_data( myfile, BQ2040_VOLTAGE_REGISTER );
}
__s32 bq2040::get_Current() {
return i2c_smbus_read_word_data( myfile, BQ2040_CURRENT_REGISTER );
}
__s32 bq2040::get_RemainingCapacity() {
return i2c_smbus_read_word_data( myfile, BQ2040_REMAINIGCAPACITY_REGISTER );
}
__s32 bq2040::get_FullChargeCapacity() {
return i2c_smbus_read_word_data( myfile, BQ2040_FULLCHARGECAPACITY_REGISTER );
}
__s32 bq2040::get_DesignCapacity() {
return i2c_smbus_read_word_data( myfile, BQ2040_DESIGNCAPACITY_REGISTER );
}
__s32 bq2040::get_DesignVoltage() {
return i2c_smbus_read_word_data( myfile, BQ2040_DESIGNVOLTAGE_REGISTER );
}
// Get Manufacturer Name
__s32 bq2040::get_ManufacturerName( __u8 *result ){
return i2c_smbus_blockread( myfile, i2c_address, BQ2040_MANUFACTURERNAME_REGISTER, result,
BQ2040_MANUFACTURERNAME_MAXLEN);
}
// Get Device Name
__s32 bq2040::get_DeviceName( __u8 *result ){
return i2c_smbus_blockread( myfile, i2c_address, BQ2040_DEVICENAME_REGISTER, result,
BQ2040_DEVICENAME_MAXLEN);
}
Accessing from ATTiny
After I have finished the code of my simple I2C thermostat, I could create it easily.
This example only asks the smart battery about the manufacturer and checks if it is "GTK" as a validation that the communication works well.
I show only main.c, you can realize that I used the same i2c read function I have written previously. If the communication success the green led will lit only, if on error the red led stays on. If only red led lit, means that you get data successfully, but not what it is expected.
//
// Test program for accessing bq2040 through SMBus/i2c from ATTiny45 in C
//
// Copyright (c)2017-2020 Tóthpál István, istvan@tothpal.eu
//
// ======================================================================================
// ATtiny Smart battery
// 25/45/85
// +--------+
// RESET --+ o Vcc +------------
// Red LED1 - PB3 --+ +-- SCK (PB2) ---------> SMBC
// Green LED2 - PB4 --+ +-- NC (PB1)
// ------------+ GND +-- SDA (PB0) ---------> SMBD
// +--------+
// ======================================================================================
#ifndef __globals__
#include "globals.h"
#endif
#include <avr/io.h>
#ifndef __i2csmbus__
#include "include/i2csmbus.h"
#endif
int main(void) {
DDRB &= 0b11100000;
DDRB |= (1 << LED_PORT1); // set PortB to output
DDRB |= (1 << LED_PORT2);
PORTB &= 0b11100000;
PORTB |= 1 << LED_PORT1; // set Power Led ON
_delay_ms(100);
PORTB |= 1 << LED_PORT2; // set Status led ON
// do communication
uint8_t buffer[48];
int bq2040a = 0x0b; // address at SMBus
int bq2040c = 0x20; // command (get Manufacturer)
int error = 0;
error |= i2c_getline( 0 );
error |= i2c_blockread( bq2040a, bq2040c, buffer, 4 );
// have to return "GTK", this is in my battery
if ( (buffer[0]!=3) | (buffer[1]!=0x47) | (buffer[2]!=0x54) | (buffer[3]!=0x4B) ) {
PORTB &= (0b00011111 ^ ( 1 << LED_PORT2) ); // Incorrect DATA received
error = -1;
}
error |= i2c_leaseline();
_delay_ms(100);
if (error == 0) { // Turn RED led off, everything is OK
PORTB &= (0b00011111 ^ ( 1 << LED_PORT1) ); // set Power Led OFF - Process finished, Status led stay ON
_delay_ms(100);
}
return 0;
}
MLX90614 Digital Non-Contact Infrared Thermometer
This is an SMBus 1.1 device from Melexis. My version is MLX90614-BAA means that it works on ~3V and the accuracy of measurement is +/-0.5°C. If you need more accuracy you should choose different version. (e.g. DCI - medical version)
You can directly connect to CH341A I2C pins, - don't forget to set to 3.3V, - and can detect with i2cdetect as I've shown at I2C.
The example C++ code and the result:
//
// Test program for accessing MLX90614 Thermometer through i2c-dev + i2c-ch341a-usb in C++
//
// Copyright (c)2020 Tóthpál István, istvan@tothpal.eu
//
// 3.3V !!!!!!!!!
#include <linux/i2c-dev.h>
#include <stdio.h>
// -------------------------------- My classes -----------------------------
#include "include/i2c/mlx90614.h"
float temp_to_Kelvin( __s32 tempdata ){
float kelvin = tempdata*0.02;
return kelvin;
}
float temp_to_Celsius( __s32 tempdata ){
float celsius = temp_to_Kelvin(tempdata) - 273.15; // 0°K = -273.15°C;
return celsius;
}
int main (void) {
__s32 r;
// init i2c line4 at 0x5a address in my hardware
mlx90614 *mythermometer = new mlx90614( 4, MLX90614_BASE_ADDRESS );
if ( (r = mythermometer->get_Ambient_Temperature()) > 0 ) {
printf("Ambient Temperature: %.2f°K %.2f°Cn", temp_to_Kelvin(r), temp_to_Celsius(r) );
}
if ( (r = mythermometer->get_TObject1_Temperature()) > 0 ) {
printf("TObject1 Temperature: %.2f°K %.2f°Cn", temp_to_Kelvin(r), temp_to_Celsius(r) );
}
if ( (r = mythermometer->get_TObject2_Temperature()) > 0 ) {
printf("TObject2 Temperature: %.2f°K %.2f°Cn", temp_to_Kelvin(r), temp_to_Celsius(r) );
}
delete mythermometer;
return 0;
}
//
// Class header file MLX90614 Thermometer through i2c-dev + i2c-ch341a-usb in C++
//
// Copyright (c)2020 Tóthpál István, istvan@tothpal.eu
//
#define MLX90614_BASE_ADDRESS 0x5A
// Commands:
//
// 000x xxxx - RAM access /Read Only/
// 001x xxxx - EEPROM access ( to modify first have to clear with writing 0x00 )
// 1111 0000 - Read flags
// 1111 1111 - Enter Sleep mode
#define MLX90614_REG_IR1_RAW_DATA 0x04
#define MLX90614_REG_IR2_RAW_DATA 0x05
#define MLX90614_REG_TA_DATA 0x06
#define MLX90614_REG_TObj1_DATA 0x07
#define MLX90614_REG_TObj2_DATA 0x08
#define MLX90614_REG_TOmax 0x20
#define MLX90614_REG_TOmin 0x21
#define MLX90614_REG_PWMCTRL 0x22
#define MLX90614_REG_TArange 0x23
#define MLX90614_REG_Emissivity 0x24
#define MLX90614_REG_Config1 0x25
#define MLX90614_REG_SMBus_Address 0x2E
#define MLX90614_REG_FLAGS 0xF0
#define MLX90614_REG_ENTER_SLEEP 0xFF
class mlx90614 {
int i2c_line;
int i2c_address;
int myfile;
public:
//constructor
mlx90614( int line, int address );
// destructor
~mlx90614();
int get_i2c_line();
int get_i2c_address();
__s32 get_Ambient_Temperature();
__s32 get_TObject1_Temperature();
__s32 get_TObject2_Temperature(); // in DUAL zone sensores
...
};
//
// Class file for MLX90614 Thermometer through i2c-dev + i2c-ch341a-usb in C++
//
// Copyright (c)2020 Tóthpál István, istvan@tothpal.eu
//
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#include "../include/i2c/mlx90614.h"
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
/*
********************************************************************
* MLX90614 CLASS *
********************************************************************
*/
// --------- Constructor ---------
mlx90614::mlx90614( int line, int address ) {
__s32 opResult = 0; // for error checking of operations
char line_str[20];
i2c_line = line;
i2c_address = address;
sprintf(line_str, "/dev/i2c-%d", line);
myfile = open( line_str, O_RDWR );
opResult = ioctl( myfile, I2C_SLAVE_FORCE, address );
if (opResult < 0) {
printf("%mn", opResult );
exit(EXIT_FAILURE);
}
else {
ioctl(myfile, I2C_PEC, 1); // Enable PEC generation
}
}
// ---------- Destructor ------------
mlx90614::~mlx90614() {
close( myfile );
}
int mlx90614::get_i2c_line() {
return i2c_line;
}
int mlx90614::get_i2c_address() {
return i2c_address;
}
__s32 mlx90614::get_Ambient_Temperature() {
return i2c_smbus_read_word_data( myfile, MLX90614_REG_TA_DATA );
}
__s32 mlx90614::get_TObject1_Temperature() {
return i2c_smbus_read_word_data( myfile, MLX90614_REG_TObj1_DATA );
}
__s32 mlx90614::get_TObject2_Temperature() {
return i2c_smbus_read_word_data( myfile, MLX90614_REG_TObj2_DATA );
}
Different surfaces has different emissivity, but the sensor cannot recognise them, You can define. Default setting is 100% and it is near good for the human skin ~95-98%, but not for all surfaces. For better results you should set the correct emissivity parameter.