The microcontroller has to read sequential bytes from a slave, and it needs to send a NAK to the slave after the last read byte, in order to inform it that the sequence is complete. This picture shows the expected sequence:

This code does the job when left alone (meaning no interrupt activity in the background).
bool I2C_WaitEvent( uint32_t flag )
{
uint32_t timeout = 1000;
while( I2C_CheckEvent( I2C1, flag ) != SUCCESS )
{
if( !--timeout )
{
return FALSE;
}
}
return TRUE;
}
bool I2C_BattRead( uint8_t address, uint8_t * data, uint32_t length )
{
I2C_GenerateSTART(I2C1, (FunctionalState)ENABLE);
if( !I2C_WaitEvent(I2C_EVENT_MASTER_MODE_SELECT) )
{
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
return FALSE;
}
I2C_Send7bitAddress(I2C1, 0x28 << 1, I2C_Direction_Transmitter);
if( !I2C_WaitEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) )
{
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
return FALSE;
}
I2C_SendData(I2C1, address);
I2C_WaitEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED);
I2C_GenerateSTART(I2C1, (FunctionalState)ENABLE);
I2C_WaitEvent(I2C_EVENT_MASTER_MODE_SELECT);
length--;
if( length == 0 )
{
I2C_AcknowledgeConfig(I2C1, (FunctionalState)DISABLE);
}
else
{
I2C_AcknowledgeConfig(I2C1, (FunctionalState)ENABLE);
}
I2C_Send7bitAddress(I2C1, 0x28 << 1, I2C_Direction_Receiver);
I2C_WaitEvent(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
while( length )
{
I2C_WaitEvent(I2C_EVENT_MASTER_BYTE_RECEIVED);
// for( volatile int i=1; i<1000;i++); // artificial delay!
length--;
if( length == 0 )
{
I2C_AcknowledgeConfig(I2C1, (FunctionalState)DISABLE);
}
else
{
I2C_AcknowledgeConfig(I2C1, (FunctionalState)ENABLE);
}
*data++ = I2C_ReceiveData(I2C1);
}
I2C_GenerateSTOP(I2C1, (FunctionalState)ENABLE);
*data++ = I2C_ReceiveData(I2C1);
I2C_WaitEvent(I2C_FLAG_BUSY);
return TRUE;
}
void updatePackState( void )
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStruct;
uint8_t data[50];
RCC_APB1PeriphClockCmd( RCC_APB1Periph_I2C1, (FunctionalState)ENABLE );
// Toggle clock line to release potentially locked slave
GPIO_InitStructure.GPIO_Pin = GPIO_SCL | GPIO_SDA;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init( GPIOB, &GPIO_InitStructure );
GPIO_WriteBit( GPIOB, GPIO_SDA, Bit_SET );
for( int j=0; j<8; j++ )
{
GPIO_WriteBit( GPIOB, GPIO_SCL, Bit_RESET );
for( volatile int i=1; i<50;i++);
GPIO_WriteBit( GPIOB, GPIO_SCL, Bit_SET );
for( volatile int i=1; i<50;i++);
}
// Init IOs to battery pack for I2C operation
GPIO_InitStructure.GPIO_Pin = GPIO_SCL | GPIO_SDA;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init( GPIOB, &GPIO_InitStructure );
GPIO_PinRemapConfig( GPIO_Remap_I2C1, (FunctionalState)ENABLE );
// Deinit and reset I2C peripheral in case of internal lockup
I2C_Cmd(I2C1, (FunctionalState)DISABLE);
I2C_SoftwareResetCmd( I2C1, (FunctionalState)ENABLE );
I2C_SoftwareResetCmd( I2C1, (FunctionalState)DISABLE );
// Init I2C peripheral
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);
I2C_BattRead( 0x80, data, 2 );
}
Screenshot of transmitted sequence:

The problem starts when this code has to live in an environment with interrupts running in the background. As this is a realtime motor control application, the interrupts tend to have substantial runtime, I cannot change this. The latency that is randomly created by the interrupts break the behavior of my I2C code. To test this, I uncommented the line "// for( volatile int i=1; i<1000;i++); // artificial delay!" above. The result is as shown here:

The microcontroller apparently acknowledges the last received byte, and the slave thinks that it should continue transmitting.
So I checked the STM32 datasheet again, and realized that, when in master receive mode, it seems to clock in 8 bits, store it in the data register, and immediately clock in the next 8 bits into the shift register. I need to tell the peripheral that the second transfer should not be acknowledged, but I miss that opportunity when interrupted.
Can someone confirm this observation (meaning the STM32 I2C peripheral is not really suited for polling mode), or did I miss something?