I2C Battery BMS Status Display

Good day all.

I am looking at starting a little project for use with my e-bike, where I would like to be able to view real time status of the e-bike battery cells.

Lucky for me it seems that most of the hard work has already been done for me, as the BMS in the e-bike battery pack is based on the 02Micro OZ890 chip.

This chip monitors the voltage of each cell, as well and charge and drain current, etc, etc.

But at this point in time I am only looking at monitoring the real time voltage of each cell, and displaying the data on an lcd or oled display.

Now to the point where I need help and advice.
Upto now I have done very little work with i2c, I have done work with simple i2c projects with temp sensors, etc, but nothing like what I am wanting to do now.

I have uploaded a copy of the 02Micro OZ890 chip datasheet to my web site, and that can be found here: Domain parking page <Click Here (787Kb).

I have already found out that the OZ890 chip's slave address is 0x30 using my arduino uno.

But I am now not sure now to pull the data I need from the OZ890 chip and to display it.

For now I am happy with just pulling the data from the OZ890 chip and pushing it back out again over serial, so I can see the data on a serial console monitor.

From reading the OZ890 chip datasheet it looks like all the data I need is held in the Operation Registers.

Details of the Operation Registers can be found on the OZ890 datasheet page 65.

And this is where I need the most help, as the Operation Registers Map, makes no sense to me.

Can someone please try and help me understand this Operation Registers Map, so that I can get started with this project?

Thanks for your time.

Best Regards.

hi,
i've planned the same project, but i'm also not very experienced in i2c.
what i know yet:
at first, the adress of oz890 is 0x60! (see page 56)
normally PEC is disabled, so you don't have to do the crc calculation(and transmission) (see page 41)
if you want to read data you must send the reg index (e.g. 0x00 chip id page 67) and after the i2c-restart you can read the answer byte (here 0x02)
this is my problem, i don't know how to do the restart (i only know wire.write and wire.read) perhaps with wire.begin/end.transmission?
any ideas?

hi,
i suceeded (because of your hint, the adress is really 0x30!!)
here is my first code:

  // akku einlesen
  val = 0;
  // register adresse übermitteln
  Wire.beginTransmission(0x30);  // adr. 30 statt 60h!!
  Wire.write(0x0); // chip id an adr. 0
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(0x30, 1); // anzahl der gelesenen bytes
  val = Wire.read();
  
  Serial.println(stat1); 
  Serial.println(val); 
  Serial.println(stat2);

Have you gotten any further?
I'm trying to do the same thing.

yes, i can read out the voltages of each cell and the current

// akku einlesen
  i2cadr = 0x30;   // OZ: adr. 30 statt 60h!!
  adr = 0x0;         // chip id an adr. 0h
 
  // strom lesen
   adr = 0x54;         // strom an adr. 54h
  // register adresse übermitteln
  Wire.beginTransmission(i2cadr); 
  Wire.write(adr); 
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(i2cadr, 2); // anzahl der gelesenen bytes
  val1 = Wire.read();
  val2 = Wire.read();
  // bytes auswerten
  val2 =  (val2 & 0x7F); // VZ ausblenden, noch auswerten
  amp = 7.63 * (val2 * 256 + val1)/1000; // mV-Spannungsabfall

  // spannungen lesen
  adr = 0x32;         //  spannung zelle 1 an adr 32h
  zelle = 1+count % 8;   // für 8 Zellen
  adr = adr + 2*(zelle-1);
  // register adresse übermitteln
  Wire.beginTransmission(i2cadr); 
  Wire.write(adr); 
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(i2cadr, 2); // anzahl der gelesenen bytes
  val1 = Wire.read();
  val2 = Wire.read();
  // bytes auswerten
  val1 =  val1/8; // 3 bits ausblenden
  val2 =  (val2 & 0x7F); // VZ ausblenden
  
  switch (zelle) {
    case 1:
      volt1 = 1.22 * (val2 * 32 + val1);
      break;
   case 2:
      volt2 = 1.22 * (val2 * 32 + val1);
      break;
   case 3:
      volt3 = 1.22 * (val2 * 32 + val1);
      break;
   case 4:
      volt4 = 1.22 * (val2 * 32 + val1);
      break;
   case 5:
      volt5 = 1.22 * (val2 * 32 + val1);
      break;
   case 6:
      volt6 = 1.22 * (val2 * 32 + val1);
      break;
   case 7:
      volt7 = 1.22 * (val2 * 32 + val1);
      break;
   case 8:
      volt8 = 1.22 * (val2 * 32 + val1);
      break;
  }

  // Gesamt-, MIN und MAX
  volt = (volt1+volt2+volt3+volt4+volt5+volt6+volt7+volt8)/1000;
  voltMIN = min(volt1,min(volt2,min(volt3,min(volt4,min(volt5,min(volt6,min(volt7,volt8)))))));
  voltMAX = max(volt1,max(volt2,max(volt3,max(volt4,max(volt5,max(volt6,max(volt7,volt8)))))));

Thanks a lot.

Which pins did you use to connect the three pins of the BMS?
Don't you have to set those pins beforehand?

Pins: SDA, SCL und GND
Board I2C / TWI pins
Uno A4 (SDA), A5 (SCL)
Mega2560 20 (SDA), 21 (SCL)
Leonardo 2 (SDA), 3 (SCL)
Due 20 (SDA), 21 (SCL)

I use wire.h library Wire - Arduino Reference

#include <Wire.h> 
....
}

I have to learn to tick the reply notifications.

Well, ok.
I took your code, changed the pins for the LCD display, which is wired up correctly and working.
I used pins A4 and A5 as I am using an Arduino Nano. I've read that those are the pins to use. I also connected ground on the arduino and the bms.
Please don't be too hard on me, it was only yesterday that I had my first successful output on a 20x4 character display.
I get loads of errors, mostly "error: expected constructor, destructor, or type conversion before '=' token"

Could somebody point me in the right direction. As a first step, it would be great if I could display the voltage of one cell and the total voltage.

// akku einlesen
#include <Wire.h>
#include <LiquidCrystal.h>
//LCD stuff
LiquidCrystal lcd(12, 11, 10, 9, 8, 7, 6);
int backLight = 13;    // pin 13 will control the backlight



//BMS stuff
  i2cadr = 0x30;   // OZ: adr. 30 statt 60h!!
  adr = 0x0;         // chip id an adr. 0h
 
  // strom lesen
   adr = 0x54;         // strom an adr. 54h
  // register adresse übermitteln
  Wire.beginTransmission(i2cadr); 
  Wire.write(adr); 
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(i2cadr, 2); // anzahl der gelesenen bytes
  val1 = Wire.read();
  val2 = Wire.read();
  // bytes auswerten
  val2 =  (val2 & 0x7F); // VZ ausblenden, noch auswerten
  amp = 7.63 * (val2 * 256 + val1)/1000; // mV-Spannungsabfall

  // spannungen lesen
  adr = 0x32;         //  spannung zelle 1 an adr 32h
  zelle = 1+count % 13;   // für 8 Zellen (13 statt 8 eingegeben)
  adr = adr + 2*(zelle-1);
  // register adresse übermitteln
  Wire.beginTransmission(i2cadr); 
  Wire.write(adr); 
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(i2cadr, 2); // anzahl der gelesenen bytes
  val1 = Wire.read();
  val2 = Wire.read();
  // bytes auswerten
  val1 =  val1/13; // 3 bits ausblenden 13 statt 8 eingegeben
  val2 =  (val2 & 0x7F); // VZ ausblenden
  
  switch (zelle) {
    case 1:
      volt1 = 1.22 * (val2 * 32 + val1);
      break;
   case 2:
      volt2 = 1.22 * (val2 * 32 + val1);
      break;
   case 3:
      volt3 = 1.22 * (val2 * 32 + val1);
      break;
   case 4:
      volt4 = 1.22 * (val2 * 32 + val1);
      break;
   case 5:
      volt5 = 1.22 * (val2 * 32 + val1);
      break;
   case 6:
      volt6 = 1.22 * (val2 * 32 + val1);
      break;
   case 7:
      volt7 = 1.22 * (val2 * 32 + val1);
      break;
   case 8:
      volt8 = 1.22 * (val2 * 32 + val1);
      break;
  }

  // Gesamt-, MIN und MAX
  volt = (volt1+volt2+volt3+volt4+volt5+volt6+volt7+volt8)/1000;
  voltMIN = min(volt1,min(volt2,min(volt3,min(volt4,min(volt5,min(volt6,min(volt7,volt8)))))));
  voltMAX = max(volt1,max(volt2,max(volt3,max(volt4,max(volt5,max(volt6,max(volt7,volt8)))))));
  



void setup()
{
  pinMode(backLight, OUTPUT);
  digitalWrite(backLight, HIGH);
  lcd.begin(20,4);   
  lcd.clear();       
  
lcd.setCursor(0,0);
  lcd.print(volt1);

  }

hi,

i think you have to call the serial communication with the bms in the main programm!

i'll send you the complete code of my project:

  • LCD, keys and BMS with I2C
  • speed and PAS signals in interrupts
    it isn't really finished, but already working like this:
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

// Konstanten-Deklaration
const float umfang = 2070; // Radumfang 26" 
const int PAS_magnet = 5;      // Anzahl der Magneten im PAS

// Variablen-Deklaration
// LCD 
LiquidCrystal_I2C lcd(0x21,20,4);  // set the LCD address to 0x21, for a 20 chars and 4 line display
int spos = 0;    // spalte 0-20
int zpos = 0;    // zeile 0-3
char str1[20] = "E-bike Control V1.1";
char str2[20] = " ";
char z1 = 192;  //ascii
char z2 = 192; 

// Tastatur (4 Tasten)
boolean T0 = 0;
boolean T1 = 0;
boolean T2 = 0;
boolean T3 = 0;

// IO
int Gas_in_ch = 0;  // Ain0 für Gasgriff
int Gas_in = 0;
int Gas_out_ch = 0;  // Ain0 für Gasgriff
int Gas_out = 0;
int PAS_in_ch = 2;   // Din2 (IRQ0) für PAS-Signal
boolean PAS_in = 0;
int spd_in_ch = 3;   // Din3 (IRQ1) für speed-Signal

boolean PAS_alt = 0; // für flanke
unsigned long PAS_1 = 0;       // msec 
unsigned long PAS_2 = 0;       // msec 
float PAS = 0;         // U/min
float PAS_MW = 0;         // gleitender MW 
unsigned long PAS_h_time = 0;    //Altwert PAS_H millis
unsigned long PAS_l_time = 0;    //Altwert PAS_L millis
boolean PAS_dir = 0;     // Trittrichtung

unsigned long spd_1 = 0;       // msec 
float spd = 0;
float spd_MW = 0;      // gleitender MW 
unsigned long spd_time = 0;       

// allgemein
int count = 0;    // zähler
int tmp_data = 0;
unsigned long tmp_long = 0;
boolean test = 1;  // Testbetrieb
unsigned long time;
boolean temp_bool = 0;
int n1 = 0;
int n2 = 0;
byte val = 0;

// OZ
byte val1 = 0;
byte val2 = 0;
byte adr = 0;
int zelle = 0;
int i2cadr = 0;
byte stat1 = 0;
byte stat2 = 0;
float volt = 0;
float volt1 = 0;
float volt2 = 0;
float volt3 = 0;
float volt4 = 0;
float volt5 = 0;
float volt6 = 0;
float volt7 = 0;
float volt8 = 0;
float voltMIN = 0;
float voltMAX = 0;
float amp = 0;
float watt = 0;

// --------------------------------
// Init
void setup()
{
  Serial.begin(9600);
// IO init
  pinMode(PAS_in_ch, INPUT_PULLUP); 
  pinMode(4, OUTPUT);
  PAS_l_time = millis();
  PAS_h_time = millis();
  
// Interrupt Din2 ist IRQ 0 (PAS), Din3 ist IRQ 1 (speed)
  attachInterrupt(0, irqprg0, CHANGE);
  attachInterrupt(1, irqprg1, RISING);
  
// LCD init
  lcd.init();               // initialize the lcd  
  lcd.backlight();
  lcd.setCursor(0,0);
  lcd.print(str1);           // Print a message to the LCD.
  delay(1000);
//    lcd.clear();

// Tastatur-PCF8574 auf input
  Wire.beginTransmission(0x38);
  Wire.write(0xff); // alle IO high
  Wire.endTransmission();
  
  // Tastatur einlesen
  Wire.requestFrom(0x38, 1);
  tmp_data = Wire.read();
  T0= (tmp_data & 0x01);  // Taste 0 beim Einschalten gedrückt
//  test = T0               // -> Testbetrieb
  Serial.println(tmp_data); 
}
// --------------------------------
// main
void loop()
{
  // Zeit
  time = millis();
  
  // test
  temp_bool = count % 2;
  digitalWrite(4,temp_bool);
  
  // Tastatur einlesen
   Wire.requestFrom(0x38, 1);
   tmp_data = Wire.read();
   
   // Tasten ausmaskieren 
   T0= (tmp_data & 0x01);
   T1= (tmp_data & 0x02) >> 1;
   T2= (tmp_data & 0x04) >> 2;
   T3= (tmp_data & 0x08) >> 3;
   
   // IOs einlesen  
   Gas_in = analogRead(Gas_in_ch);  
    
// Erfassung PAS jetzt mit IRQ0  
// Drehrichtung fehlt noch
if ((PAS_1 > 0) and (PAS_2 > 0) and ((millis() - PAS_h_time) < 500))
{
   // Trittfrequenz in U/min
   PAS = 60000/((PAS_1 + PAS_2)*PAS_magnet);
   // gleitender Mittelwert
   PAS_MW = (PAS_MW * PAS_magnet + PAS)/(1+PAS_magnet);
}
else
{
   PAS = 0;
   PAS_MW = 0;
}
//   PAS_h_time = pulseIn(PAS_in_ch, HIGH);  
//   PAS_l_time = pulseIn(PAS_in_ch, LOW);  

// Erfassung speed  mit IRQ1  
if (((spd_1 > 50) and (spd_1 < 4000)) and ((millis() - spd_time) < 4000))
{
   // Geschwindigkeit in km/h
   spd = umfang/spd_1;
   // gleitender Mittelwert über 3 Werte
   spd_MW = (spd_MW * 3 + spd)/(1+3);
}
else
{  
   spd = 0;
   spd_MW = 0;
}
   
   if (test = 1) 
   { 
    // Testprogramm
    delay(1000);
      
 // akku einlesen
  i2cadr = 0x30;   // OZ: adr. 30 statt 60h!!
  adr = 0x0;         // chip id an adr. 0h
 
  // strom lesen
   adr = 0x54;         // strom an adr. 54h
  // register adresse übermitteln
  Wire.beginTransmission(i2cadr); 
  Wire.write(adr); 
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(i2cadr, 2); // anzahl der gelesenen bytes
  val1 = Wire.read();
  val2 = Wire.read();
  // bytes auswerten
  val2 =  (val2 & 0x7F); // VZ ausblenden, noch auswerten
  amp = 7.63 * (val2 * 256 + val1)/1000; // mV-Spannungsabfall
  
  // Leistung berechnung
  watt = amp * volt;
  
  // spannungen lesen
  adr = 0x32;         //  spannung zelle 1 an adr 32h
  zelle = 1+count % 8;   // für 8 Zellen
  adr = adr + 2*(zelle-1);
  // register adresse übermitteln
  Wire.beginTransmission(i2cadr); 
  Wire.write(adr); 
  stat1 = Wire.endTransmission(false); // 0:sucess
  // register daten lesen
  stat2 = Wire.requestFrom(i2cadr, 2); // anzahl der gelesenen bytes
  val1 = Wire.read();
  val2 = Wire.read();
  // bytes auswerten
  val1 =  val1/8; // 3 bits ausblenden
  val2 =  (val2 & 0x7F); // VZ ausblenden
  
  switch (zelle) {
    case 1:
      volt1 = 1.22 * (val2 * 32 + val1);
      break;
   case 2:
      volt2 = 1.22 * (val2 * 32 + val1);
      break;
   case 3:
      volt3 = 1.22 * (val2 * 32 + val1);
      break;
   case 4:
      volt4 = 1.22 * (val2 * 32 + val1);
      break;
   case 5:
      volt5 = 1.22 * (val2 * 32 + val1);
      break;
   case 6:
      volt6 = 1.22 * (val2 * 32 + val1);
      break;
   case 7:
      volt7 = 1.22 * (val2 * 32 + val1);
      break;
   case 8:
      volt8 = 1.22 * (val2 * 32 + val1);
      break;
  }
  // Gesamt-, MIN und MAX
  volt = (volt1+volt2+volt3+volt4+volt5+volt6+volt7+volt8)/1000;
  voltMIN = min(volt1,min(volt2,min(volt3,min(volt4,min(volt5,min(volt6,min(volt7,volt8)))))));
  voltMAX = max(volt1,max(volt2,max(volt3,max(volt4,max(volt5,max(volt6,max(volt7,volt8)))))));
  
 // Display-ASusgabe
   zpos = 1;
   spos = 0;
    
   lcd.setCursor(spos,zpos);
 //  lcd.print(tmp_data);
   lcd.print(volt);
   lcd.setCursor(5,zpos);
   lcd.print(" Tasten: ");
   lcd.print(T0);
   lcd.print(T1);
   lcd.print(T2);
   lcd.print(T3);
   
   zpos = 2;
   spos = 0;
   
   lcd.setCursor(spos,zpos);
   lcd.print(PAS,0);  
   lcd.setCursor(5,zpos);
   lcd.print(PAS_MW,0);  
   lcd.setCursor(10,zpos);
   lcd.print(spd,1);  
   lcd.setCursor(15,zpos);
   lcd.print(spd_MW,1);
 
   zpos = 3;
   spos = 0;
   
   lcd.setCursor(spos,zpos);
   lcd.print(voltMIN);  
   lcd.setCursor(10,zpos);
   lcd.print(voltMAX);  
  
 // debugging   
  Serial.println(stat1); 
  Serial.println(val); 
  Serial.println(stat2); 

 //   Serial.print(PAS_1);
 //   Serial.print(" ");
 //   Serial.print(PAS_2);
 //   Serial.print(" ");
 //   Serial.print(PAS);
 //   Serial.print(" ");
 //   Serial.print(spd);
 //   Serial.print(" ");
 //   Serial.print(" ");
 //   Serial.println(" ");
    
   count = count+1;
   }
}

// --------------------------------
// Interruptprogramme
void irqprg0()
  // Trittfrequenz PAS-Signale -> Timer 
{
   PAS_in = digitalRead(PAS_in_ch);
     if (PAS_in == 1)  //steigend
     {
       PAS_h_time = millis();
       PAS_2 = PAS_h_time - PAS_l_time;
     }
     else           // fallend
     {
       PAS_l_time = millis();
       PAS_1 = PAS_l_time - PAS_h_time;
     }
}
void irqprg1()
  // speed-Signale -> Timer 
{
    spd_1 = millis() - spd_time;
    spd_time = millis();
}