STM32 und I2C

I2C ist ein weit verbreiteter serieller Bus, ursprünglich von Phillips/NXP erfunden, und findet man es heutzutage überall, auch unter anderen Namen. Atmel nennt es TWI, für PHYs wird es MDIO genannt, SMBus gibt es, PMBus und viele mehr.
Das Prinzip ist immer das gleiche. Man hat die Leitungen SDA(Serial Data) und SCL(Serial Clock), die jeweils über einen eignen Pull-Up Widerstand(1k - 10k, je nach Leitungslänge und Teilnehmerzahl) gegen die Logik-Versorgungsspannung gezogen werden. Um Daten zu senden werden die Leitungen gegen Masse gezogen, so können viele Teilnehmer am selben Bus zugleich hängen.
Ursprünglich war nur der Single-Master Mode vorgesehen, es gibt einen Master, der jede Kommunikation initiiert. Heutzutage gibt es aber auch die Möglichkeit für Multimaster-Systeme, der STM32 unterstütz dies.
Eine jede Kommunikation beginnt damit, dass der Master die Start Condition erzeugt, die Slave-Adresse sendet, wobei das LSB für die Unterscheidung Lesen/Schreiben genutzt wird. Danach werden entweder Daten vom Master gesendet(LSB = 0) oder Empfangen(LSB = 1). Ein jedes Byte wird mit einem ACK oder NACK quittiert. Ein Empfangszyklus wird mit einem NACK quittiert, ein Sendezyklus mit der Stop-Condition oder dem erneuten senden der Start-Condition(Restart).
Der hier gezeigte Code basiert auf der SPL und wurde auf einem STM32F207ZTG6 getestet.

Initialisierung

Wie immer gilt es den Takt für das genutzte Peripheriemodul sowie den genutzten GPIO zu aktivieren. Die Pins werden Konfiguriert als Alternate Function, Open Drain mit Pull-Up oder gänzlich ohne Pull-Up/Down Widerstände:

oid InitI2C()
{
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef I2C_InitStructure;
 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
 
    /* Reset I2Cx IP */
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);
 
    /* Release reset signal of I2Cx IP */
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);
 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);
 
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;        //Eigene Adresse für Slave oder Multimaster betrieb, ansonsten irrelevant
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Disable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;   //Adress-Modus, normalerweise 7-Bit
    I2C_InitStructure.I2C_ClockSpeed = 50000;        //Taktgeschwindigkeit von SCL, hier 50kHz
 
    I2C_Init(I2C1, &I2C_InitStructure);
 
    I2C_ITConfig(I2C1, I2C_IT_EVT | I2C_IT_BUF, DISABLE);  //Keine Interrupts
 
    I2C_Cmd(I2C1, ENABLE);
}

Die genaue Konfiguration des I2C Moduls muss im Einklang mit dem Prozessor und der am I2C-Bus hängenden ICs getroffen werden. Auch hier gilt, dass die Konfiguration sich je nach verwendeten Prozessor ändern kann.

Wrapper Funktionen

Um nun die Nutzung des I2C Moduls einfacher zu machen, werden eine reihe an Wrapper Funktionen definiert.
Mit I2C_start wird die Start-Condition auf dem Bus erzeugt und die Adresse des Slaves gesendet, damit befindet sich der STM32 im Master Mode. Durch direction wird angegeben, ob nun Bytes gesendet oder empfangen werden.

void I2C_start(I2C_TypeDef* I2Cx, uint8_t address, uint8_t direction){
	// wait until I2C1 is not busy any more
	while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
 
	// Send I2C1 START condition
	I2C_GenerateSTART(I2Cx, ENABLE);
 
	// wait for I2C1 EV5 --> Slave has acknowledged start condition
	while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT));
 
	// Send slave Address for write
	I2C_Send7bitAddress(I2Cx, address, direction);
 
	/* wait for I2Cx EV6, check if
	 * either Slave has acknowledged Master transmitter or
	 * Master receiver mode, depending on the transmission
	 * direction
	 */
	if(direction == I2C_Direction_Transmitter){
		while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
	}
	else if(direction == I2C_Direction_Receiver){
		while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
	}
}


Mit I2C_restart wird eine restart-condition erzeugt, um z.B bei einem Lesevorgang nach senden des zu lesenden Registers direkt mit dem Lesen des Registers fortgeführt werden kann, ohne den Bus frei zu geben.

void I2C_restart(I2C_TypeDef * I2Cx, uint8_t address, uint8_t direction)
{
    while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
    I2Cx->CR1 |= I2C_CR1_START;
 
    while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT));
 
	// Send slave Address for write
	I2C_Send7bitAddress(I2Cx, address, direction);
 
	/* wait for I2Cx EV6, check if
	 * either Slave has acknowledged Master transmitter or
	 * Master receiver mode, depending on the transmission
	 * direction
	 */
	if(direction == I2C_Direction_Transmitter){
		while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
	}
	else if(direction == I2C_Direction_Receiver){
		while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
	}
}
<code c>
I2C_write wartet bis zum senden des letzten Bytes und schreibt data in das Ausgangs-Shift Register.
<code c>
void I2C_write(I2C_TypeDef* I2Cx, uint8_t data)
{
	// wait for I2C1 EV8 --> last byte is still being transmitted (last byte in SR, buffer empty), next byte can already be written
	while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
	I2C_SendData(I2Cx, data);
}

I2C_read_ack liest ein Byte vom Bus und sendet ein ack, beendet damit also nicht das Empfangen:

uint8_t I2C_read_ack(I2C_TypeDef* I2Cx){
	// enable acknowledge of received data
	I2C_AcknowledgeConfig(I2Cx, ENABLE);
	// wait until one byte has been received
	while( !I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED) );
	// read data from I2C data register and return data byte
	uint8_t data = I2C_ReceiveData(I2Cx);
	return data;
}

I2C_read_nack liest ein Byte vom Bus und sendet ein NACK, beendet also den Empfangszyklus:

uint8_t I2C_read_nack(I2C_TypeDef* I2Cx){
	// disable acknowledge of received data
	// nack also generates stop condition after last byte received
	// see reference manual for more info
	I2C_AcknowledgeConfig(I2Cx, DISABLE);
	I2C_GenerateSTOP(I2Cx, ENABLE);
	// wait until one byte has been received
	while( !I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED) );
	// read data from I2C data register and return data byte
	uint8_t data = I2C_ReceiveData(I2Cx);
	return data;
}

I2C_stop wartet auf das Senden des letzten Bytes und erzeugt dann die Stop-Condition, beendet damit einen Schreibzyklus und gibt den Bus frei.

void I2C_stop(I2C_TypeDef* I2Cx){
    while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
	// Send I2C1 STOP Condition after last byte has been transmitted
	I2C_GenerateSTOP(I2Cx, ENABLE);
	// wait for I2C1 EV8_2 --> byte has been transmitted
	while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}

Nutzung

Nachdem I2C initialisiert wurde, kann man I2C mit den Wrapperfunktionen folgendermaßen nutzen:

InitI2C();
[...]
//Kombinierter Schreib-Lese zyklus
//Bitte beachten, SLAVE_ADDR muss entweder um 1 nach links geshifted werden, damit das LSB gesetzt werden kann 
//oder gleich so definiert sein, wie es hier der Fall ist
I2C_start(I2C1, SLAVE_ADDR, I2C_Direction_Transmitter);
I2C_write(I2C1, 0x00);
I2C_write(I2C1, 0x00);
I2C_restart(I2C1, SLAVE_ADDR, I2C_Direction_Receiver);
byte1 = I2C_read_ack(I2C1);
byte2 = I2C_read_nack(I2C1);
//Schreibzyklus:
I2C_start(I2C1, SLAVE_ADDR, I2C_Direction_Transmitter);
I2C_write(I2C1, BYTE1);
I2C_write(I2C1, BYTE2);
I2C_write(I2C1, BYTE3);
I2C_write(I2C1, BYTE4);
I2C_stop(I2C1);
//Nur lesen
I2C_start(I2C1, SLAVE_ADDR, I2C_Direction_Receiver);
byte1 = I2C_read_ack(I2C1);
byte2 = I2C_read_nack(I2C1);