no problem, here it is (STM32 family). It is a bit interwoven with the rest of the module through some globals (e.g. time = 0.5ms tick, fCellConversion = raw 10 bit ADC conversion result from VCOUT pin, VCC=3.3V), but I hope it is still clear enough.
float fCellConversion = 0;
float fCellVoltageRaw[6] = {0,0,0,0,0,0};
float fCellVoltage[6] = {0,0,0,0,0,0};
float fRefOffset = 0;
float fRefGain = 0;
float fAdcGain = 0;
float fCellOffset[6] = {0,0,0,0,0,0};
float fCellGain[6] = {0,0,0,0,0,0};
float fBalTemp = 0;
bool balPresent = FALSE;
enum
{
CHECK_CHARGER,
CHECK_CONNECTION,
MEASURE_VREF,
MEASURE_V0,
MEASURE_V1,
MEASURE_V2,
MEASURE_V3,
MEASURE_V4,
MEASURE_V5,
MEASURE_TEMP,
DO_BALANCING,
} balState = CHECK_CONNECTION;
uint32_t balSwitches = 0;
bool bChargerConnected = FALSE;
uint32_t timeout_bal = 0;
uint32_t timeout_chrg_check = 0;
bool bBalSwap = FALSE;
bool bChargeEnable = FALSE;
bool bChargeComplete = FALSE;
///////////////////////////////////////////////////////////////////////////////
void waitTicks( uint32_t ticks )
{
uint32_t then;
then = time + ticks;
while( (int32_t)(then - time) > 0 );
}
///////////////////////////////////////////////////////////////////////////////
void wait( float delay )
{
waitTicks( (uint32_t)(delay / TIME_STEP) );
}
///////////////////////////////////////////////////////////////////////////////
bool WaitI2CEvent( uint32_t flag )
{
uint32_t event, timeout = 1000;
do
{
event = I2C_GetLastEvent(I2C1);
if( !--timeout )
{
return FALSE;
}
}
while( (event & flag) == 0 );
return TRUE;
}
///////////////////////////////////////////////////////////////////////////////
void WriteBalancer( uint8_t reg, uint8_t value)
{
I2C_GenerateSTART(I2C1, (FunctionalState)ENABLE);
if( !WaitI2CEvent(I2C_FLAG_SB) )
{
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
return;
}
I2C_Send7bitAddress(I2C1, (0x20 | reg) << 1, I2C_Direction_Transmitter);
if( !WaitI2CEvent(I2C_FLAG_ADDR) )
{
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
return;
}
I2C_SendData(I2C1, value);
WaitI2CEvent(I2C_FLAG_BTF);
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
while( I2C_GetLastEvent(I2C1) & I2C_FLAG_BUSY );
}
///////////////////////////////////////////////////////////////////////////////
uint8_t ReadBalancer( uint8_t reg )
{
uint8_t result;
I2C_GenerateSTART(I2C1, (FunctionalState)ENABLE);
if( !WaitI2CEvent(I2C_FLAG_SB) )
{
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
return 0;
}
I2C_Send7bitAddress(I2C1, (0x20 | reg) << 1, I2C_Direction_Receiver);
if( !WaitI2CEvent(I2C_FLAG_BTF) )
{
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
return 0;
}
result = I2C_ReceiveData(I2C1);
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
while( I2C_GetLastEvent(I2C1) & I2C_FLAG_BUSY );
return result;
}
///////////////////////////////////////////////////////////////////////////////
int32_t compl2( uint32_t number, uint32_t bits )
{
number &= ( 1 << bits ) - 1;
if( number & (1 << (bits - 1) ) )
return (int32_t)number - (int32_t)(1 << bits);
else
return (int32_t)number;
}
///////////////////////////////////////////////////////////////////////////////
enum
{
BAL_STATUS = 0x00,
BAL_CELL_CTL,
BAL_BAL_CTL,
BAL_CONFIG_1,
BAL_CONFIG_2,
BAL_POWER_CTL,
BAL_CHIP_ID = 0x07,
BAL_VREF_CAL = 0x10,
BAL_VC1_CAL,
BAL_VC2_CAL,
BAL_VC3_CAL,
BAL_VC4_CAL,
BAL_VC5_CAL,
BAL_VC6_CAL,
BAL_VC_CAL_EXT1,
BAL_VC_CAL_EXT2,
BAL_VREF_CAL_EXT = 0x1B,
};
void updateBalancer( void )
{
I2C_InitTypeDef I2C_InitStruct;
uint8_t reg1, reg2;
uint16_t reg3;
float fBalTempRaw;
// wait balancer discharging period before performing next action
if( timeout_bal > time )
{
return;
}
// stop balancing in case it is enabled
WriteBalancer( BAL_BAL_CTL, 0 );
// check if all cells are full and balanced
bChargeComplete = TRUE;
for( int i=0; i<6; i++ )
{
bChargeComplete &= (fCellVoltage[i] >= 4.19f) && (fCellVoltage[i] <= 4.21f);
}
// control charging switch based on charger presence, cell voltages, temperature, and balancer activity
bChargeEnable = ( balPresent
&& bChargerConnected
&& !bChargeComplete
&& (fBattTemp < 50.0f)
&& (balSwitches == 0) ) ? TRUE:FALSE;
if(bChargeEnable)
{
GPIO_WriteBit( GPIOB, GPIO_CHRG_EN, Bit_SET );
}
else
{
GPIO_WriteBit( GPIOB, GPIO_CHRG_EN, Bit_RESET );
}
// work through balancer state machine
switch( balState )
{
//////////////////////////////////////////////////////////////////////////////
case CHECK_CHARGER:
// periodically check if charger is still connected
if( time > timeout_chrg_check )
{
// This disconnects the charger voltage from the bus voltage,
// which is necessary to detect if the charger is still connected.
GPIO_WriteBit( GPIOB, GPIO_CHRG_EN, Bit_RESET );
// allow some time to discharge input capaticance
waitTicks(20);
// check if charger is connected
bChargerConnected = (fChrgVoltage > 24.0f) ? TRUE:FALSE;
// update timeout for next check
timeout_chrg_check += (uint32_t)(5.0f / TIME_STEP);
}
// loop through all states again
balState = CHECK_CONNECTION;
break;
//////////////////////////////////////////////////////////////////////////////
case CHECK_CONNECTION:
if( ReadBalancer( BAL_CHIP_ID ) == 0x10 )
{
WriteBalancer( BAL_CONFIG_2, 0x01 ); // VCOUT gain 0.6, VREF = 3.0V nom
WriteBalancer( BAL_POWER_CTL, 0x47 ); // SLEEP_DIS, vc+ref+thermistor enabled
// Calculate VREF offset and gain
reg1 = ReadBalancer( BAL_VREF_CAL );
reg2 = ReadBalancer( BAL_VREF_CAL_EXT );
fRefOffset = (float)compl2( ((reg2 & 0x06) << 3) | (reg1 >> 4 ), 6 ) * 0.001f;
fRefGain = 1.0f + (float)compl2( ((reg2 & 0x01) << 4) | (reg1 & 0x0F), 5 ) * 0.001f;
// Calculate VC[0..5] offset and gain
reg1 = ReadBalancer( BAL_VC_CAL_EXT1 );
reg2 = ReadBalancer( BAL_VC_CAL_EXT2 );
reg3 = ((uint16_t)(reg1 & 0xF0) << 4) | reg2;
for( int i=0; i<6; i++ )
{
reg1 = ReadBalancer( BAL_VC1_CAL + i );
fCellOffset[i] = (float)compl2( (((reg3 >> (11 - 2*i)) & 0x01 ) << 4) | (reg1 >> 4 ), 5 ) * 0.001f;
fCellGain [i] = 1.0f + (float)compl2( (((reg3 >> (10 - 2*i)) & 0x01 ) << 4) | (reg1 & 0x0F), 5 ) * 0.001f;
}
balPresent = TRUE;
// connection successful, continue with next state
balState = MEASURE_VREF;
}
else
{
fCellVoltage[0] =
fCellVoltage[1] =
fCellVoltage[2] =
fCellVoltage[3] =
fCellVoltage[4] =
fCellVoltage[5] = 0;
// perform I2C reset in case of lockup
I2C_Cmd(I2C1, (FunctionalState)DISABLE);
I2C_SoftwareResetCmd( I2C1, (FunctionalState)ENABLE );
I2C_SoftwareResetCmd( I2C1, (FunctionalState)DISABLE );
I2C_InitStruct.I2C_ClockSpeed = 50000;
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStruct.I2C_OwnAddress1 = 0;
I2C_InitStruct.I2C_Ack = I2C_Ack_Disable;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStruct);
I2C_Cmd(I2C1, (FunctionalState)ENABLE);
balPresent = FALSE;
// balancer does not respond, back to charger detection
balState = CHECK_CHARGER;
}
break;
//////////////////////////////////////////////////////////////////////////////
case MEASURE_VREF:
WriteBalancer( BAL_CELL_CTL, 0x30 ); // VREF * 0.85
waitTicks(5);
fAdcGain = ( ( 3.0f * fRefGain + fRefOffset ) * 0.85f ) / fCellConversion;
if( fAdcGain < 20e-6 || fAdcGain > 80e-6 )
{
// value out of range, assume problem with balancer and restart
balState = CHECK_CHARGER;
}
else
{
// continue with next state
balState = MEASURE_V0;
}
break;
//////////////////////////////////////////////////////////////////////////////
case MEASURE_V0:
case MEASURE_V1:
case MEASURE_V2:
case MEASURE_V3:
case MEASURE_V4:
case MEASURE_V5:
WriteBalancer( BAL_CELL_CTL, 0x10 | (balState - MEASURE_V0) ); // VC[0..5]
waitTicks(5);
fCellVoltageRaw[balState - MEASURE_V0] =
( fCellConversion * fAdcGain + fCellOffset[balState - MEASURE_V0] ) * fCellGain[balState - MEASURE_V0] / 0.6f;
balState++;
break;
//////////////////////////////////////////////////////////////////////////////
case MEASURE_TEMP:
WriteBalancer( BAL_CELL_CTL, 0x16 ); // Vtemp,int
waitTicks(5);
fBalTempRaw = 25.0f - ( ( fCellConversion * fAdcGain - 1.2f ) * 1.35f /*+++ datasheet gain and offset seem to be wrong? */ ) / 4.4e-3f;
if( fBalTemp == 0.0f )
{
fBalTemp = fBalTempRaw;
}
else
{
fBalTemp = fBalTemp * 0.9f + fBalTempRaw * 0.1f;
}
// continue with next state
balState = DO_BALANCING;
break;
//////////////////////////////////////////////////////////////////////////////
case DO_BALANCING:
// update cell voltage values
for( int i=0; i<6; i++ )
{
if( fCellVoltageRaw[i] > 0.0f && fCellVoltageRaw[i] < 10.0f )
{
if( fCellVoltage[i] == 0 )
{
fCellVoltage[i] = fCellVoltageRaw[i];
}
fCellVoltage[i] = fCellVoltage[i] * 0.9f + fCellVoltageRaw[i] * 0.1f;
}
}
// perform balancing as necessary
balSwitches = 0;
if( !bChargeComplete )
{
// check which cell needs balancing
for( int i=0; i<6; i++ )
{
if( fCellVoltage[i] > 4.2f )
{
balSwitches |= (1<< i);
}
}
// adjacent cells cannot be balanced simultaneously
if( bBalSwap )
{
bBalSwap = FALSE;
balSwitches &= ~((balSwitches & (1+4+16)) << 1);
balSwitches &= ~((balSwitches & (1+4+16)) >> 1);
}
else
{
bBalSwap = TRUE;
balSwitches &= ~((balSwitches & (2+8+32)) << 1);
balSwitches &= ~((balSwitches & (2+8+32)) >> 1);
}
// apply balancing
if( balSwitches )
{
WriteBalancer( BAL_BAL_CTL, balSwitches );
// start 1 sec discharge timer
timeout_bal = time + (uint32_t)(1.0f / TIME_STEP);
}
}
// continue with next state
balState = CHECK_CHARGER;
break;
//////////////////////////////////////////////////////////////////////////////
default:
while(1);
}
}
Depending on what you want to do, this solution could also be an option:
https://www.eevblog.com/forum/projects/introducing-my-project-'kicksurfer'/msg1203499/#msg1203499 (https://www.eevblog.com/forum/projects/introducing-my-project-'kicksurfer'/msg1203499/#msg1203499)