Bus I2C


DOCUMENTATION


On s’appuie sur le capteur de température LM75 pour présenter des échanges sur le bus I2C.
Ces explications valent bien entendu pour tout type de composant I2C.
Il est important de faire le parallèle avec la datasheet du LM75 :

datasheet_LM75

Un bus I2C est un bus série :

  • synchrone : Transmission de l’horloge

  • bidirectionnel Half Duplex : on parle chacun son tour (cf un seul fil de données)

  • Plusieurs composants peuvent être branchés sur un même bus I2C –> Adressage

  • !! En fonction des docs, l’adresse du composant peut inclure ou non le bit du sens de transfert (read / write)

i2c_bus.svg

  • Un composant I2C peut être vu comme un composant complexe comportant plusieurs registres.
  • Le but de la communication I2C est donc d’accéder à ces registres (en lecture ou écriture).

i2c_modele.svg

REMARQUE :

Les composants I2C ont généralement des sorties à collecteur ouvert.
Il est important de vérifier que des résistances de pull-up sont présentes sur le bus pour alimenter les transistors.

i2c_pull_up.svg

Toute communication I2C commence par l’adressage (ou le pointage) d’un registre dans le composant esclave.

Lecture de la Température

Cliquer sur la figure ci dessous:

Configuration du capteur (Mise en Veille)

Cliquer sur la figure ci dessous:

Lecture de deux octets

Un cas fréquent : La lecture de plusieurs registres de l’esclave.
Le maître acquite chaque octet envoyé par l’esclave.
Le non acquitement met fin au transfer.

i2c_read_2_bytes.svg


Programmation du Périphérique I2C

RAPPEL : Les registres sont détaillés dans la documentation RefManual_STM32F411.pdf

L’objectif est d’établir une communication entre le capteur LM75 se trouvant sur le carte mbed shield, et notre microtonctôleur, via la liaison I2C.

Configuration des Broches

Le STM32F411 comporte 3 périphériques I2C ( I2C1, I2C2, I2C3 )
Nous allons utiliser I2C1, qui est relié au capteur de température LM75 ( carte mbed shield ) via les broches PB8 et PB9.

Extrait de la documentation de la carte mbed shield :

REMARQUE : Les résistances de pull up sont déjà présentes sur la carte.

Les broches à configurer sont donc PB8 (SCL) et PB9 (SDA) ( Datasheet STM32F411 ) :

broches_i2c1.svg

PB8 et PB9 doivent donc être configurées en Alternate Function 04.

Nous utiliserons le périphérique I2C avec des interruptions basées sur des événements ( I2C1_EV_IRQn ) et des erreurs de transmission ( I2C1_ER_IRQn ).

stm32f4xx_hal_msp.c
void HAL_I2C1_MspInit(void)
{
	  GPIO_InitTypeDef  GPIO_InitStruct;

	  __I2C1_CLK_ENABLE();

	  GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
	  GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
	  GPIO_InitStruct.Pull = GPIO_NOPULL;
	  GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;
	  GPIO_InitStruct.Alternate =   GPIO_AF4_I2C1 ; // hal_gpio_ex.h

	  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

	  NVIC_SetPriority(I2C1_ER_IRQn, I2C1_ER_IRQ_PRIO);
	  NVIC_EnableIRQ(I2C1_ER_IRQn);

	  NVIC_SetPriority(I2C1_EV_IRQn, I2C1_EV_IRQ_PRIO);
	  NVIC_EnableIRQ(I2C1_EV_IRQn);
}

Configuration du périphérique I2C

Nous allons considérer la structure I2C_HandleTypeDef suivante :

stm32f4xx_hal_i2c.h
typedef struct __I2C_HandleTypeDef
{
  I2C_TypeDef     *Instance;      /*!< I2C registers base address               */
  I2C_InitTypeDef  Init;           /*!< I2C communication parameters             */
  volatile int	status;				// driver status
  uint32_t      Devaddress;     /*!< I2C Target device address*/
  uint8_t       *pBuffPtr;      /*!< Pointer to I2C transfer buffer           */
  uint8_t		op;					// read/write operation
  uint32_t		n_to_read;			// how many bytes to be read?
  uint32_t		n_to_write;			// how many bytes to write?
  uint32_t		n_wr;				// how many data actually written?
  uint32_t		n_rd;				// how many data actually read?
} I2C_HandleTypeDef;

I2C_TypeDef Permet d’accéder aux différents registre du périphérique I2C :

stm32f411xe.h
typedef struct
{
  __IO uint32_t CR1;        /*!< I2C Control register 1,     Address offset: 0x00 */
  __IO uint32_t CR2;        /*!< I2C Control register 2,     Address offset: 0x04 */
  __IO uint32_t OAR1;       /*!< I2C Own address register 1, Address offset: 0x08 */
  __IO uint32_t OAR2;       /*!< I2C Own address register 2, Address offset: 0x0C */
  __IO uint32_t DR;         /*!< I2C Data register,          Address offset: 0x10 */
  __IO uint32_t SR1;        /*!< I2C Status register 1,      Address offset: 0x14 */
  __IO uint32_t SR2;        /*!< I2C Status register 2,      Address offset: 0x18 */
  __IO uint32_t CCR;        /*!< I2C Clock control register, Address offset: 0x1C */
  __IO uint32_t TRISE;      /*!< I2C TRISE register,         Address offset: 0x20 */
  __IO uint32_t FLTR;       /*!< I2C FLTR register,          Address offset: 0x24 */
} I2C_TypeDef;

La fonction HAL_I2C_Init() permet de configurer l’horloge I2C et d’autoriser l’utilisation du périphérique.

i2c_cr1.svg

i2c_cr2.svg

stm32f4xx_hal_i2c.c
int  HAL_I2C_Init(I2C_HandleTypeDef *hi2c)
{
	// make a software reset to deal with spurious busy states
	hi2c->Instance->CR1|=1<<15; // SR2[1]==1 ...
	hi2c->Instance->CR1 = 0; // disable the peripheral before changing any configuration
	hi2c->Instance->CR2 = (sysclks.apb1_freq/1000000); // peripheral input clock frequency + Event & Buffer & Error Interrupt Enable
	
	// clock control register config : Fast Mode (400kHz), duty=0 (duty cycle 2/3)
	// Ti2c = (2+1)*CCR*Tapb1 ==> CCR = (apb1_freq/(3*400e3))
	if (hi2c->Init.ClockSpeed == 0) {hi2c->Init.ClockSpeed = 400000;}
	hi2c->Instance->CCR = (1<<15) | (sysclks.apb1_freq/(3* hi2c->Init.ClockSpeed));
	
	//Configure the rise time register (from ST Cube library)
	// rise time:
	//  standard mode: 1000ns
	//  fast mode    : 300ns
	// TRISE = risetime / Tapb1 = 300e-9 * apb1_freq = 300 * apb1_freq_MHz * 1e6 / 1e9
	//  =(((i2c_freq) <= 100000) ? ((apb1_freq/10e6) + 1) : ((((apb1_freq/10e6) * 300) / 1000) + 1))
	hi2c->Instance->TRISE =(((sysclks.apb1_freq/1000000) * 300) / 1000) + 1;
	hi2c->Instance->FLTR = hi2c->Instance->FLTR & (~0x1F); // analog noise filter on, digital filter off
	hi2c->Instance->CR1 |= 1; // enable the peripheral
		
	return I2C_OK;
}

Appel de la Fonction :

main.c
	hi2c1.Instance = I2C1;
	hi2c1.Init.ClockSpeed = 400000;
	HAL_I2C_Init(&hi2c1);

Transmission d’une donnée ( uC –> Capteur )

La fonction HAL_I2C_Master_Transmit_IT initialise le contexte hi2c, en indiquant le nombre d’octets à envoyer, l’ emplacement des données à envoyer et le type d’opération ( écriture ).
Une fois le bit de start envoyé, il y aura interruption sur événement, et c’est dans cette interruption qu’on décidera de la suite pour envoyer toute la trame.
Tant que la trame n’est pas totalement envoyée, on est dans l’état I2C_BUSY.

stm32f4xx_hal_i2c.c
int HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
	hi2c->status = I2C_BUSY;

	hi2c->Devaddress=DevAddress;
	hi2c->pBuffPtr = pData;
	hi2c->n_to_read  = 0;
	hi2c->n_to_write = Size;
	hi2c->n_wr = 0;
	hi2c->n_rd = 0;
	hi2c->op = I2C_WRITE;
		
	hi2c->Instance->CR2 |= I2C_IT_ERR | I2C_IT_EVT | I2C_IT_BUF;
	hi2c->Instance->CR1 |= I2C_CR1_START;		// send start condition
	
	while (hi2c->status == I2C_BUSY) ;			// wait for the transaction to be done
	
	for (int i=0;i<100;i++)
		__asm volatile("nop");
			
	return hi2c->status;
}

La routine d’interruption HAL_I2C_EV_IRQHandler() est appelée au cours des différentes étapes de l’envoi de la trame :

  1. Un bit de start a été envoyé –> J’envoie l’adresse de l’esclave et le type d’opération
  2. Si l’esclave a acquité l’adresse –> J’envoie un octet de donnée.
  3. Tant qu’il y a des octets à envoyer, je les envoie.
  4. Quand tout est envoyé, j’envoie un bit de stop et je passse de l’état I2C_BUSY à I2C_OK
stm32f4xx_hal_i2c.c
void HAL_I2C_EV_IRQHandler(I2C_HandleTypeDef *hi2c)
{
	uint32_t reg32 __attribute__((unused));
	
	I2C_t *i2c = hi2c->Instance;
	uint32_t sr1 = i2c->SR1;
	
	nb_irqs_i2c++;
	
	if (sr1 & I2C_SR1_SB) {					
		// Start or Repeated Start sent
		i2c->DR = (hi2c->Devaddress <<1) | hi2c->op;	//   send address + mode (R/_W)
	} else if (sr1 & I2C_SR1_ADDR) { 	// A slave acknowledged the address
		if (hi2c->op == I2C_WRITE) {				//   write the first data
			reg32 = i2c->SR2;
			i2c->DR = hi2c->pBuffPtr[hi2c->n_wr++];
			if (hi2c->n_wr == hi2c->n_to_write) {
				i2c->CR2 &= ~I2C_IT_BUF;
			}
	    } else if (hi2c->op==I2C_READ) {			// Some data to read?
	    	if (hi2c->n_to_read == 1) {			//   special case: 1 data to read
				i2c->CR1 &= ~I2C_CR1_ACK;
				reg32 = i2c->SR2;				//   irq acknowledge
			} else {							//   general case
				i2c->CR1 |= I2C_CR1_ACK;
				reg32 = i2c->SR2;				//   irq acknowledge
			}
		}
	} else if (sr1 & I2C_SR1_TxE) {				// some more data to write
		if (hi2c->n_wr < hi2c->n_to_write) {
			i2c->DR = hi2c->pBuffPtr[hi2c->n_wr++];
			if (hi2c->n_wr == hi2c->n_to_write) {
				i2c->CR2 &= ~I2C_IT_BUF;		// when no more data disable BUF irq to disallow TxE events
			} 
		} else if ((hi2c->n_wr == hi2c->n_to_write) && (sr1 & I2C_SR1_BTF)) {	// no more data to write and last written byte transmitted, ack received
			if (hi2c->n_to_read) {				//   anything to read?
				hi2c->op=I2C_READ;
				hi2c->n_rd=0;
				i2c->CR2 |= I2C_IT_BUF;
				i2c->CR1 |= I2C_CR1_START | I2C_CR1_ACK;	// send repeated start
			} else {							// no more data to write: stop
				i2c->CR1 |= I2C_CR1_STOP;		// send stop condition
				i2c->CR2 &= ~I2C_IT_EVT;
				hi2c->status = I2C_OK;
			}
		}
	} else if (sr1 & I2C_SR1_RxNE) {			// a data was received
		hi2c->pBuffPtr[hi2c->n_rd++] = i2c->DR;
			if (hi2c->n_rd == hi2c->n_to_read-1) {
				i2c->CR1 &= ~I2C_CR1_ACK;
			} else if (hi2c->n_rd == hi2c->n_to_read) {
				i2c->CR1 |= I2C_CR1_STOP;
				hi2c->status = I2C_OK;
			}
	}
}

Réception d’une donnée ( Capteur –> uC )

La fonction HAL_I2C_Master_Receive_IT initialise le contexte hi2c, en indiquant le nombre d’octets à recevoir, l’ emplacement des données à recevoir et le type d’opération ( lecture ).
Une fois le bit de start envoyé, il y aura interruption sur événement, et c’est dans cette interruption qu’on décidera de la suite pour envoyer toute la trame.
Tant que la trame n’est pas terminée, on est dans l’état I2C_BUSY.
C’est encore au maître ( le microcontrôleur ) d’initier un dialogue avec le capteur, en envoyant l’adresse de ce dernier après avoir déclenché un bit de start.
Comme il y a opération de lecture, le capteur est autorisé à envoyer des données octet par octet ( tant que le microcontrôleur acquitte chaque octet ).

stm32f4xx_hal_i2c.c
int HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
	hi2c->status = I2C_BUSY;

	hi2c->Devaddress=DevAddress;
	hi2c->pBuffPtr = pData;
	hi2c->n_to_read  = Size;
	hi2c->n_to_write = 0;
	hi2c->n_wr = 0;
	hi2c->n_rd = 0;
	hi2c->op = I2C_READ;
		
	hi2c->Instance->CR2 |= I2C_IT_ERR | I2C_IT_EVT | I2C_IT_BUF;
	hi2c->Instance->CR1 |= I2C_CR1_START;					// send start condition
	
	while (hi2c->status == I2C_BUSY) ;			// wait for the transaction to be done
	
	for (int i=0;i<100;i++)
		__asm volatile("nop");
			
	return hi2c->status;
}

Le Capteur LM75B

Examen de la Doc du capteur

datasheet_LM75

L’adresse du capteur est par défaut 0x48.
Les 3 bits de poids faible peuvent être modifiés matériellement ( en reliant les broches A2 A1 A0 à la masse ou à 3.3V ).

Un capteur peut être vu comme une sorte de processeur complexe avec lequel il faut dialoguer par l’intermédiaire de lectures ou d’écritures dans des registres.

Le capteur LM75 contient les registres suivants :

Nous nous interesserons uniquement au registre Temp d’adresse 0x00.
La température mesurée est disponible sur 2 octets.

Les 5 bits de poids faible sont à ignorer, les 11 bits restants représentent une température au format virgule fixe 8.3.

Scénario Détaillé pour l’acquisition de la température :

main.c
...
uint8_t buff[10];
buff[0]=0x00;
HAL_I2C_Master_Transmit_IT(&hi2c1, 0x48, buff, 1, 0);
HAL_I2C_Master_Receive_IT(&hi2c1, 0x48, buff, 2, 0);
...

1. Envoi de la valeur 0x00 au capteur pour désigner le registre Température

scenario_transmission

2. Réception de 2 Octets représentant la température mesurée

scenario_reception


Travaux Pratiques

PROJET SOURCE


WORKSPACE_F411_HAL_STM32CUBE

Q1. Compléter la fonction lm75_read_temp(int* temp) afin de récupérer la mesure de température du capteur.
Cette fonction doit :

  • Transmettre la valeur 0 au capteur afin de pointer sur le registre contenant la température.
  • Effectuer un accès en lecture du capteur afin de récupérer 2 octets correspondants au poids fort et au poids faible de la donnée.
  • réunir poids fort et poids faible pour reconstituer la donnée de température
  • supprimer les bits de poids faibles inutiles dans la données reconstituée
  • Réaliser l’extension de format pour passer d’un format 11 bits signé à un format 32 bits signé.

Vérifier le bon fonctionnement du programme en observant la donnée mesurée et traitée à l’aide du debugger.

Q2. Réaliser l’enregistrement des trames I2C avec un analyseur logique.

20230123_154752.jpg

analyseur_logique_i2c.png

Q3. Réaliser l’affichage de la donnée avec la fonction uart_printf(), de telle sorte qu’on observe sur le terminal la donnée en °C, avec 3 chiffres après la virgule.