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.

STM32 USART sending and receiving data

Using USART for sending and receiving data

USART is the same in ‘Arduino world’ and Keil programming, but getting message out is bit different – instead using “Serial.print” we are using here custom made function “printMsg” which uses ‘vsprintf’ function for formatting whole message which is then transferred to the data register and shifted out by shift register.
For formatted print, there is cplusplus reference fro programmers.

Diagram from Reference manual RM0008:

USART

No DMA yet, just manual TX/RX-ing

DMA on USART can help a lot, but for now I did not learn enough to give you an example how to use it. As I cover part by part of STM32 programming in Keil, I am trying to make it simple so that everyone can understand. DMA (Direct Memory Access) is one way to move data from one part of the MCU to another. But also, there is old ‘manual’ ways.

Using HTerm console

USART

Instead Putty or Serial monitor from Arduino, I like rather to use HTerm, it is free and has everything one programmer need. You can debug your program and see output data in various formats: ASCII, decimal, hexadecimal and binary form. There are other things as is flow control, saving received data, sending saved or generated file out over the USART and many other nice things.

Here are example codes, you should to include all three files into your Keil project: main.c, pringMsg.c and printMsg.h – make it so that all three files are in the same folder. Don’t forget that we need FTDI adapter.

Example codes

main.c file:

/*********************************************
 USART1 TX on STM32 is at pin A9, RX is at pin
 A10. Use pinout, or CubeMX to find those two
 pins. 
==========STM32===========FTDI================
           GND------------GND pin
   pin A9  TX-------------RX  pin
   pin A10 RX-------------TX  pin
 *********************************************/

#include "stm32f10x.h"
#include "printMsg.h"

int len=0;
char buff[256];

void delay(long cycles)
{
  while(cycles >0)
  cycles--; // 'wasted clock cycles', not exact time
}



int main()
{
	usart_1_enable();
	printMsg("Here is message %d 0x%X \n", 65, 65);
			
	while(1)
	{
	
    readMsg();
				
		
    //this part just send back to the HTerm console - the same text as received		
		for (int i=0;i<len+1;i++)
	  {
	    USART1->DR = buff[i];
	    while(!(USART1->SR & USART_SR_TXE)); //TXE becomes true (1) when the content of DR (data register) goes into shift register
	  }
	}
}

printMsg.c file:

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

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, alt.funct.en, USART1 clock enabled
	GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1; //port A9
	GPIOA->CRH &= ~GPIO_CRH_CNF9_0; //port A9 is TX pin
	GPIOA->CRH &= ~(GPIO_CRH_MODE10_0|GPIO_CRH_MODE10_1); //port A10 is RX pin
	GPIOA->CRH |= GPIO_CRH_CNF10_0; //port A10 is RX pin
	
	//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
  }
}

int readMsg(void)
{ 
  
	for( len =0;len<256;len++)
	{
    while(!(USART1->SR & USART_SR_RXNE));
	  buff[len]= USART1->DR;
    if (buff[len]==10 || buff[len]==13) break; //if enter is pressed, providing that it is 10 or 0xA (line feed), or 13 (0xD) (carriage return) 
	}
	return len; //just returning number of entered characters, not including line feed nor carriage return (!)
}

printMsg.h file:

#ifndef printMsg_h_
#define printMsg_h_

extern int len;
extern char buff[];

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

#endif

In printMsg.c file is RCC->APB2ENR register with everything we need to provide clock to the USART and GPIO pins (A9 is TX, A10 is RX):

RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN;

Setting pins A9 (TX) and A10 (RX) for USART1:

GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1; //port A9 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

Note that this time GPIO port A9 uses ‘alternate’ function instead ‘general purpose’. If you set it incorrectly, it will not send data.

Next thing is setting USART speed in Bauds.  Here is example for 9600 Baud:

USART1->BRR = (0x1D4C); // 9600 Baud

To get this number needed for Bit Rate Register BRR, You need to divide your clock with 9600, 72000000/9600=7500, but it is less confusing to use hexadecimal value at this place, so use your programmers calculator and convert 7500 into 0x1D4C. In printMsg.c file I calculated all mostly common using USART speeds, plus few not so common. The maximum speed HTerm can transmit and receive is 3000000 Bauds (~ 3 megabauds). Putty can’t cope with that, and did not tried Arduino serial monitor.

Inside printMsg() function, there is final sending part:

USART1->DR = buff[i];USART1->DR = buff[i];   while(!(USART1->SR & USART_SR_TXE));

What id does, it send first byte from our array buff[] into register USART1->DR, then we should wait until it is transferred into shift register and send out. This means that we should make waiting while loop and ‘watch’ TXE flag to appear – sending of one byte is done, now we can continue loading next byte into USART1-DR data register.

Receiving part is little bit different. We first wait flag RXNE that told us that data register is filled with one byte and ready for read.

while(!(USART1->SR & USART_SR_RXNE));while(!(USART1->SR & USART_SR_RXNE));   buff[len]= USART1->DR;

To recap: for sending byte out, we first send one byte, then wait flag XE, for receiving byte in, we first wait flag RXNE, then we can read.

On the console, there should be enabled one of three options – after typing message, when we press enter, all data are send, but we need extra byte or two to tell MCU that there is end of the message. Else, our loop may wait until whole buffer is full. So, enabling CR and/or LF (Carriage Return / Line Feed), with decimal values of 13 and 10 respectively. Inside for loop, there is:

if (buff[len]==10 || buff[len]==13) break;

Which checking whether received byte is either of those two options, CR and/or LF, then it exit or break loop. After that it just returns length of the message “return len;”. Since ‘buff[256]’ is global array, after returning back from readMsg() function, this buffer is filled with our data, and we need to know how much. Variable ‘len’ gives us exactly that number.

I made simple test ‘loopback’ code that sends everything back to the console:

for (int i=0;i<len+1;i++) for (int i=0;i<len+1;i++)   {     USART1->DR = buff[i];     while(!(USART1->SR & USART_SR_TXE)); //TXE becomes true (1) when the content of DR (data register) goes into shift register   }

Note that inside for() loop, there is ‘len+1’, that is because length of array is certain number, then we ad our CR and/or LF character that tells HTerm to go in new line.

Custom USART function

Sometimes we need just send data from our MCU inputs. For example potentiometer data. Then we have no need for formatted print or anything, just making some loop that read ADC for example, and send byte by byte out (and waiting TXE flag):

USART1->DR = pot_value;     while(!(USART1->SR & USART_SR_TXE));

Where ‘pot_value’ is some data from ADC which measure voltage on your potentiometer.

If you want to externally change something by typing, then use this:

while(!(USART1->SR & USART_SR_RXNE));
our_variable= USART1->DR;

 

STM32 blink LED – first programming steps in Keil

STM32 first steps – blink LED

On board of STM32 (some people call it “blue pill”) there is green LED, or at other boards there is blue LED. First step is to get familiar with Keil environment, set everything properly and start programming. It is not difficult, but require some time to get used to this ‘new way’ aside ‘Arduino way’ or making ‘sketch’-es. This is true C programming, which is maybe less popular than Arduino, but Keil has some import functions so that you can import .ino file and convert it to new way. I found it works faster and has many cool functions. The problem is that there is almost no ‘helper’ libraries, or those are hidden or not available in free version of the Keil IDE.

Use ST-Link V2 programmer, support debug and goes up to 4 MHz uploat to the board with MCU. Do not forget ST-Link driver.

Before everything, here are links to reference manual and two MCUs (both STM32, different model) showed on the video below.

STM32F103C8  and

STM32F103VET6

Main reference manual for STM32F101 to STM32f107.

First blink program:

#include "stm32f10x.h"

void delay(long cycles)
{
  while(cycles >0)
  cycles--; // Some stupid delay, it is not in milliseconds or microseconds, but rather in some 'wasted clock cycles'
}

void gpio_ports_enable(void)
{
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN |     RCC_APB2ENR_IOPBEN|RCC_APB2ENR_IOPCEN; //ports A & B clock enabled
  GPIOC->CRH = 0x44344444; //Pin C13 enable. If you don't like number 13, use pin C_1_P.M. LOL
}

int main(void)
{
	gpio_ports_enable();

	for(;;)  //main loop - read "forever", or you may use 'while(1)'
	{
		  GPIOC->BSRR = GPIO_BSRR_BS13; 
		  delay(5000000);
		  GPIOC->BSRR = GPIO_BSRR_BR13; 
		  delay(5000000); 
		}	
	}
}

Thee thing with enabling port, if you remember Arduino way it is ‘PinMode(13, OUTPUT);’, here is not the case, we can write in few ways, for example long one:

GPIOC->CRH |= GPIO_CRH_MODE13_0|GPIO_CRH_MODE13_1;
GPIOC->CRH &= ~(GPIO_CRH_CNF13_0|GPIO_CRH_CNF13_1);

Which will ‘yield’ the same effect as simple expression:

GPIOC->CRH = 0x44344444;

Where numbers 4 are default or reset value which make pins input, floating – or least amount of current consumption. To make your task easier, use calculator in “Programmers” mode (at leas Windows has it). First type in the calculator HEX value you found in the datasheet for the register of interest, then click on icon next to DWORD (initially there is QWORD – click until DWORD appears), and it will show you map with groups of four bits – ‘nibbles’. One byte contains two nibbles. Click on that table to change values and compare with values that is needed for your application and compare it with the datasheet.  Here is how it looks after setting pin PC13 (or just C13) in the calculator:

STM32

To get this calculator in Windows, pres Windows key and type ‘calc’, or if it does not works, press and hold windows key and key ‘R’ to get command prompt, then type ‘calc’. On calculator, there is option ‘standard’, ‘scientific’, but we need ‘programmers’.

Another PWM blink program:

#include "stm32f10x.h"

void delay(long cycles)
{
  while(cycles >0)
  cycles--; // Some stupid delay, it is not in milliseconds or microseconds, but rather in some 'wasted clock cycles'
}

void gpio_ports_enable(void)
{
	RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN|RCC_APB2ENR_IOPCEN; //ports A & B clock enabled
	GPIOC->CRH = 0x44344444; //Pin C13 enable. If you don't like number 13, use pin C_1_P.M. LOL
}

int main(void)
{
	gpio_ports_enable();

	for(;;)  //main loop - read "forever"
	{
		
		for (int i=0;i<3000;i++)
		{
		  GPIOC->BSRR = GPIO_BSRR_BS13; 
		  delay(i);
		  GPIOC->BSRR = GPIO_BSRR_BR13; 
		  delay(3000-i); 
		}
				for (int i=0;i<3000;i++)
		{
		  GPIOC->BSRR = GPIO_BSRR_BS13; 
		  delay(3000-i);
		  GPIOC->BSRR = GPIO_BSRR_BR13; 
		  delay(i); 
		}
	}
}

This time the same LED on board slowly going bright, then dim at the same rate on STM32 boards. So far, I have only two versions: STM32F103C8 and STM32F103VET6. Firs one is cheapo popular “Blue Pill”, while second one is some advanced version with more pins, more memory and what not. Also more expensive. And I did not found pinout of this board. For this purpose, you may wish to install CubeMX program. This program generates file for starting, but that is some weird “HAL” structure, which I am not familiar, nor want to mess with that, since it is so confusing. But, at least it has MCU with designations on each pin after you click on it. Example:

 STM32Note that this is not programming environment, just “initialization code generator” for STM32.

Be sure to use Notepad++ for saving copy of the codes. In this type of Notepad, if you save program as for example ‘.c’ extension, it will gives you ‘colored’ nice looking text. I usually print from this thing and also saving this code to another place on the disk in the case that I lost trace of the original program which is somewhere in Keil directory.

Continue reading STM32 blink LED – first programming steps in Keil

STM32 programming in Keil

STM32 MCUs programming in Keil

Not so long time ago, I decided to play with micro controllers. My first MCU was PIC and my first programs were in assembler code. That is because low memory space and need for fast execution. Later someone said that I should try to program it in C language. And that solved many problems, and things going faster. Then ‘hit the wall’ because of lack of information and examples how to do something. After that, some time spent in Arduino environment, which is fine for beginners and for really fast results. Just… after a while, there arrive need for something faster, and instead Atmel chips, I decided to use STM32 series. Luckily there are libraries and everything in Arduino IDE thing. But, some of the libraries does not behave as I wish. So, next logical choice is to use something that resembles ‘true C’ rather than ‘sketch’ things. Start is really difficult. For that reason, I want to share with you some experience in programming STM32 MCU family and Keil environment. Keil is free for use, but with limited options and with limited code use – maximum 32 kByte. For beginner like me, that is more than enough. Later will decide what will do – or buy (well, it is expensive), or switch to something else if exist at all.

Later will include some YouTube videos.

 

This page is still under construction…