I2C

I2C and MS5611 precision Barometer using STM32

I2C or ISP protocol, what chice?

Both options are okay, ISP protocol is faster, can run over 40 MHz, but sometimes we have more than one device, so it is better sometimes to use I2C protocol.  After all, ADC conversion on this device may take up to 10 mS, so faster protocol will not yield faster reading.

How to connect MS5611 to STM32

First we need to look at schematic diagram to know how to connect MS5611 to STM32 board(s). In our case of I2C interfacing, we should look at second part of the picture below. (I included whole datasheet even more below.  Note that in the case of I2C, pin CSB play different role than in ISP mode. Leave it unconnected for address 0x77, or connect to Vdd in the case you already have the same device, this time address will be 0x76. For STM32f10x family, SDA pin is connected to GPIO B7 and SCL to GPIO B6. As this page is intended for learning I2C protocol on STM32 micro-controllers, I will talk here only about this protocol. SPI will come later, and if you are subscribed to the website, you will get e-mail notification about updates. Back to connections – pin PS on MS5611 must be on Vdd, since it is Protocol Select. In the document it says that it is low voltage device, and 3V is mentioned, but absolute maximum rating is 4V, so if STM32 works on 3.3V, that is exactly what we want.

MS5611

On the board GY-63, it is confusing where is SDI for SPI protocol. It is the same pin as SDA, but someone forget to add that pin name “SDA/SDI”. In both cases SDI/SDA gives us data, while SCL or SCLK as is on datasheet has clock. We here need for I2C  SDA and SCL only for communication. Vcc (or Vdd in the document) goes to +3.3V, GND to GND of STM32, and PS to Vdd bypass or separate wire to available +3.3V on the STM32 board.

MS5611

Here is documentation abut MS5611 precision barometer:

Download (PDF, 579KB)

For complete work in Keil, we need 7 files, all seven should be in the same directory of the project with name, for example MS5611 barometer. It has thermometer too with two digits decimal precision. Your choice of project name, but name of individual component must be as is the name of .c and .h files, since it depends of ‘#include’ statements.  Here I will give you all 7 files, of which first is barometer.c (containing main() function), wire.c, wire.h, delayUs.c, delayUs.h, printMsg.c, printMsg.h . All MCU programming is done using ST-link dongle. For data transfer to the computer, you need FTDI usb dongle.

barometer.c file:

/* More on my website: http://wp.me/p7jxwp-nD */

#include "stm32f10x.h"
#include "printMsg.h"
#include "delayUs.h"
#include "wire.h"
#include "math.h"

uint8_t  buffer[5];  //twi buffer, for now 32 elements, for other purpose (e.g. for OLED it is 1024) change as you wish
uint8_t  address=0x77, length=0;
uint16_t calibrationData[7];
int64_t OFF, OFF2, SENS, SENS2;
int pressure, D1, D2, dT, P, Pa, TEMP, T2;

void setupSensor(void)
{
	twiSend(address, 0x1E,1); //just send 1 byte that tells MS5611 to reset
	delay(20000); //delay 10 mS needed for device to execute reset
	for (int i=1;i<=6;i++)
	{
	twiReceive(address, 0xA0+i*2, 2); //read all 14 bytes for callibration data from PROM
	//printMsg("b0= 0x%x, b1= 0x%x, b2= 0x%x \n",buffer[0], buffer[1], buffer[2]); //for debug purposes
  delay(50); //at least 40 uS
	calibrationData[i] = buffer[0]<<8|buffer[1]; //pair of bytes goes into each element of callibrationData[i], global variables, 14 uint8_t into 7 uint16_t
  }
	//delay(50);
	//printMsg("b0= %d, b1= %d, b2= %d \n",buffer[0], buffer[1], buffer[2]); //for debug purposes
}

int getPressure(void)
{
	D1=0;D2=0;
	twiSend(address, 0x48,1); //set D1 OSR=4096 (overscan, maximum) 0x48
	delay(25000);//must be 15 mS or more
	twiReceive(address, 0x00, 3); //initiate and read ADC data, 3 bytes
	//printMsg("b0= 0x%x, b1= 0x%x, b2= 0x%x ===========\n",buffer[0], buffer[1], buffer[2]); //for debug purposes
	D1 = D1<<8 | buffer[0]; //shifting first MSB byte left
	D1 = D1<<8 | buffer[1]; //another byte
	D1 = D1<<8 | buffer[2]; //LSB byte last
	twiSend(address, 0x58,1); //set D2 OSR=4096 (overscan, maximum) 0x58
	delay(25000); //must be 15 mS or more
	twiReceive(address, 0x00, 3); //initiate and read ADC data, 3 bytes
	D2 = D2<<8 | buffer[0]; //shifting first MSB byte left
	D2 = D2<<8 | buffer[1]; //another byte
	D2 = D2<<8 | buffer[2]; //LSB byte last
		
	dT = D2 - ((int)calibrationData[5] << 8);
  TEMP = (2000 + (((int64_t)dT * (int64_t)calibrationData[6]) >> 23)); //temperature before second order compensation
  if (TEMP<2000)  //if temperature of the sensor goes below 20°C, it activates "second order temperature compensation"
    {
      T2=pow(dT,2)/2147483648;
      OFF2=5*pow((TEMP-2000),2)/2;
      SENS2=5*pow((TEMP-2000),2)/4;
      if (TEMP<-1500) //if temperature of the sensor goes even lower, below -15°C, then additional math is utilized
        {
          OFF2=OFF2+7*pow((TEMP+1500),2);
          SENS2=SENS2+11*pow((TEMP+1500),2)/2;
        }
    }
    else 
      {
          T2=0;
          OFF2=0;
          SENS2=0;
      }
  TEMP = ((2000 + (((int64_t)dT * (int64_t)calibrationData[6]) >> 23))-T2); //second order compensation included
  OFF = (((unsigned int)calibrationData[2] << 16) + (((int64_t)calibrationData[4] * dT) >> 7)-OFF2); //second order compensation included
  SENS = (((unsigned int)calibrationData[1] << 15) + (((int64_t)calibrationData[3] * dT) >> 8)-SENS2); //second order compensation included
  P = (((D1 * SENS) >> 21) - OFF) >> 15; 
  return P; //returns back pressure P
}

int main(void)
{
//In new (2019/03/01) added twiScan(); function. You may use it to find your I2C device. Just remove // coment characters.
          //twiScan();
	  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
	  GPIOC->CRH = 0x00300000;
    GPIOC->BSRR = GPIO_BSRR_BR13; //let's lit blue LED 
	  RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
	  usart_1_enable();
	  twiEnable();
	  setupSensor(); //set essential things
	  //printMsg("works? or stuck somewhere?"); //debug mode :)
	while(1)
	{	
    pressure = getPressure(); //get pressure and temperature, calculate pressure offset with callibration data and temperature
	  printMsg("Temperature of the sensor= %.2f, barometric pressure= %.2f\n", (float)TEMP/100, (float)pressure/100);	
		delay(2000000); //every 2 seconds it send data over the USART1 (equivalent of Serial.print on Arduino)
		//for(;;); //execute once, just for debug purpose
	}
}

wire.c file:

/* Two Wire Interface, I2C (or IIC), here will be called 'twi', and we have
   only twiEnable(), twiSend() and twiReceive(). The twiSend() function is 
	 fairly simple, we just send address of the device shifted to the left by
	 1 bit, or-red | zero (0) at free space that tell I2C bus it is for write operation.
	 The receive twiReceive() function works by sending address also shifted left
	 one bit with logic or | zero (0) at empty bit (LSB), but then we must send command 
	 to the device depending what device has. After command, we stop (although
	 we can remove STOP condition and continue to "repeated start", then we
	 must change bit after address of the device, now it is one (1) that tells
	 I2C bus we want to read. If we try only read from some address, device
	 don't know what to send. So we must first issue command, then read. For
	 specific command set read datasheet of particular device - it is different
	 for all different devices. More on my website: http://wp.me/p7jxwp-nD */

#include "stm32f10x.h"
#include "delayUs.h"
#include "wire.h"
#include "printMsg.h"

void twiEnable(void) 
{
  //just set all registries, but NOT START condition - execute once in main.c
	RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN; //B port enabled, alternate function 
	RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; //I2C 1 enabled 
	GPIOB->CRL = 0xFF000000;// setting just pins B7 (SDA) and B6 (SCL), while leaving the rest intact 50 MHz!
	I2C1->CR2 |= 50; // GPIO clock freq=50 MHz MUST !!! be equal APB frequency (GPIO, 2, 10 or 50 MHz)
	I2C1->CCR |= I2C_CCR_FS; //fast mode
	I2C1->CCR |= 30; //not sure for 400 000 - (10= 1.2 MHz, 15=800 kHz, 30=400 kHz)
	I2C1->TRISE |= 51; // maximum rise time is 1000 nS
	I2C1->CR1 |= I2C_CR1_PE; 
}

void twiScan(void)
{		int a=0; 
	 	for (uint8_t i=0;i<128;i++)
   {
			I2C1->CR1 |= I2C_CR1_START;
			while(!(I2C1->SR1 & I2C_SR1_SB));
			I2C1->DR=(i<<1|0); 
			while(!(I2C1->SR1)|!(I2C1->SR2)){}; 
			I2C1->CR1 |= I2C_CR1_STOP; 
			delay(100);//minium wait time is 40 uS, but for sure, leave it 100 uS
			a=(I2C1->SR1&I2C_SR1_ADDR);
			if (a==2)
		 {
				printMsg("Found I2C device at adress 0x%X (hexadecimal), or %d (decimal)\n",i,i);
		 }
	 }
}
	
/* Command or commands, or sending bytes, just the same name of the variable 'command' */
void twiSend(uint8_t address, uint8_t command, uint8_t length)
{
	I2C1->CR1 |= I2C_CR1_START; //START condition 
	while(!(I2C1->SR1 & I2C_SR1_SB));
	I2C1->DR=(address<<1|0); //sending address of the device, 0 = sending
  while(!(I2C1->SR1 & I2C_SR1_ADDR)|!(I2C1->SR2));		
	I2C1->DR=command; //filling data register with byte, if single - command, multiple - command(s) and data
	for (uint8_t i=0;i<length;i++)
	{ 
		I2C1->DR=buffer[i]; //filling buffer with command or data
		delay(60);
	}
	I2C1->CR1 |= I2C_CR1_STOP;
}

void twiReceive(uint8_t address, uint8_t command, uint8_t length) 
{
	I2C1->CR1 |= I2C_CR1_ACK;
  I2C1->CR1 |= I2C_CR1_START; //start pulse 
	while(!(I2C1->SR1 & I2C_SR1_SB));
	I2C1->DR=(address<<1|0); //sending address of the device, 0 = sending
	while(!(I2C1->SR1 & I2C_SR1_ADDR)|!(I2C1->SR2 & I2C_SR2_BUSY));
	I2C1->DR=command; //sending command to the device in order to request data
	I2C1->CR1 |= I2C_CR1_START; //REPEATED START condition to change from sending address + command to receive data
	while(!(I2C1->SR1 & I2C_SR1_SB));
	I2C1->DR=(address<<1|1); //sending address of the device, 1 = reading 
	while(!(I2C1->SR1 & I2C_SR1_ADDR)|!(I2C1->SR2));
	
if (length==1)  //receiving single byte, N=1
	{
		while(!(I2C1->SR1)|!(I2C1->SR2));
		I2C1->CR1 &= ~I2C_CR1_ACK; //this will send later NAK (not acknowledged) to signal it is last byte
		I2C1->CR1 |= I2C_CR1_STOP; //issuing STOP condition before (!) reading byte
		buffer[0]=I2C1->DR; //single byte is read AFTER NAK (!) and STOP condition
	} 
	if (length==2) //receiving two bytes, N=2
	{
		while(!(I2C1->SR1)|!(I2C1->SR2));
		I2C1->CR1 &= ~I2C_CR1_ACK; //this will send later NAK (not acknowledged) before last byte
    I2C1->CR1 |= I2C_CR1_STOP;
		buffer[0]=I2C1->DR; //reading N-1 byte, next to last byte is in DR, last one still in shift register
		while(!(I2C1->SR1 & I2C_SR1_RXNE)|!(I2C1->SR2));
		buffer[1]=I2C1->DR; //read last N byte now available 
	} 
  if (length>2) //receiving more than two bytes, N>2
	{
		
	  for (uint8_t i=0;i<length;i++)
	  { 
			                     
		  if (i<(length-3))      // if it is not N-2, then read all bytes
			{
				while(!(I2C1->SR1 & I2C_SR1_RXNE)|!(I2C1->SR2));
				buffer[i]=I2C1->DR;  
			}
		  else if (i==length-3) // if it is N-2 then read 
			{
				while(!(I2C1->SR1)|!(I2C1->SR2));
				buffer[i]=I2C1->DR; 
				while(!(I2C1->SR1 & I2C_SR1_RXNE)|!(I2C1->SR2));
				I2C1->CR1 &= ~I2C_CR1_ACK; //this will send later NAK (not acknowledged) before last byte
				I2C1->CR1 |= I2C_CR1_STOP;
			}
	    else if (i==length-2) // if it is N-1 then read
			{
				while(!(I2C1->SR1 & I2C_SR1_RXNE)|!(I2C1->SR2));
				buffer[i]=I2C1->DR; 
			}
			else if (i==length-1) // else it is N byte 
			{
				while(!(I2C1->SR1 & I2C_SR1_RXNE)|!(I2C1->SR2)){};
		    buffer[i]=I2C1->DR;  
			}
    } 
 }
}

wire.h file:

#ifndef wire_h
#define wire_h

#include <stdint.h>

extern uint8_t  address, command, length; 
extern uint16_t calibrationData[];
extern uint8_t  buffer[];
extern uint8_t  status1;
extern uint8_t  status2;

void twiEnable(void);
void twiSend(uint8_t address, uint8_t command, uint8_t length);
void twiReceive(uint8_t address, uint8_t command, uint8_t length);


#endif

delayUs.c file:

void delay(unsigned long cycles)
{
  while(cycles >0)
	{
		asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");
		asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");
		asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");
		asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop");
		asm("nop");asm("nop");asm("nop");  //to get 1 uS if delay(1)
  cycles--; 
	}
}

delayUs.h file:

#ifndef delayUs_h
#define delayUs_h

extern void delay(unsigned long cycles);

#endif

printMsg.c file:

#include "stm32f10x.h"
#include "stdint.h"
#include <stdio.h>
#include <stdlib.h>
#include "stdarg.h"
#include "string.h"
#include "printMsg.h"

char buff[256];

void usart_1_enable(void)
{
  //enabling pin A9 for alternating funct. for uart/usart
 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN; //clock to GPIO A enabled, port A(2), alt.funct.en(0), usart1 clock enabled(14)
	GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1; //port A9
	GPIOA->CRH &= ~GPIO_CRH_CNF9_0; //port A9
	GPIOA->CRH &= ~(GPIO_CRH_MODE10_0|GPIO_CRH_MODE10_1); //port A10 is RX
	GPIOA->CRH |= GPIO_CRH_CNF10_0; //port A10 is RX
	
	//GPIOA->CRH = 0x444444B4; // A9 is alternate output, 50 MHz, push-pull - not this time short version (!)
	//clkPer/(baudRx_16bit)=72MHZ/9600 = 7500 = 0x1D4C
	/* Remove comment line for speed that you want to use*/
	//USART1->BRR = (0xEA60); //   1200 Baud
	//USART1->BRR = (0x7530); //   2400 Baud
	//USART1->BRR = (0x3A98); //   4800 Baud
	//USART1->BRR = (0x1D4C); //   9600 Baud
	//USART1->BRR = (0x1388); //  14400 Baud
	//USART1->BRR = (0xEA6) ; //  19200 Baud
	//USART1->BRR = (0x9c4) ; //  28800 Baud
	//USART1->BRR = (0x753) ; //  38400 Baud
	//USART1->BRR = (0x505) ; //  56000 Baud
	//USART1->BRR = (0x4E2) ; //  57600 Baud
	USART1->BRR = (0x271) ; // 115200 Baud
	//USART1->BRR = (0x232) ; // 128000 Baud
	//USART1->BRR = (0x119) ; // 256000 Baud
	//USART1->BRR = (0x8C)  ; // 512000 Baud
	//USART1->BRR = (0x46)  ; // 1024000 Baud
	//USART1->BRR = (0x23)  ; // 2048000 Baud
  //USART1->BRR = (0x18)  ; // 3000000 Baud (3 MHz, max speed that HTerm can get, non-standard speed)
	
	
	USART1->CR1 |= USART_CR1_TE; //transmitter enable
	USART1->CR1 |= USART_CR1_RE; //receiver enable
	USART1->CR1 |= USART_CR1_UE; //usart enable
}

void printMsg(char *msg, ...)
{
	//char buff[120]; //was 80
	va_list args;
	va_start(args,msg); 
	vsprintf(buff,msg,args);

	for(int i=0;i<strlen(buff);i++)
	{
	  USART1->DR = buff[i];
	  while(!(USART1->SR & USART_SR_TXE)); //wait for TXE, 1 = data transferred
  }
}

printMsg.h file:

#ifndef printMsg_h
#define printMsg_h

extern int len;
extern char buff[];

void usart_1_enable(void);
void printMsg(char *msg, ...);

#endif

Here is the video:

Some related documents

Download (PDF, 125KB)

External link for STM32f10x series.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.