STM32 Blue Pill BME280 Data Logger using STM32CubeIDE

In this tutorial, we will learn to create STM32 Blue Pill BME280 data logger to log sensor readings to a microSD card with STM32CubeIDE. We will create a .txt file in our microSD card through programming our Blue Pill board and consequently log current temperature(°C), pressure(hPa), and humidity(%) readings to that file after every second. Any preferred sensor, such as DS18B20, BME680, and MPPU6050, can be used but for this article, we will use a BME280 sensor which will be used to measure temperature, pressure, altitude, and humidity. The sensor readings will be accessed from the BME280 sensor connected with the STM32 Blue Pill. These readings will get saved on the microSD card after every second.

STM32 Blue Pill BME280 Data Logger with STM32CubeIDE

BME280 sensor Introduction

The BME280 sensor is used to measure readings regarding ambient temperature, barometric pressure, and relative humidity. It is mostly used in web and mobile applications where low power consumption is key. This sensor uses I2C or SPI to communicate data with the microcontrollers. Although there are several different versions of BME280 available in the market, the one we will be studying uses the I2C communication protocol.

I2C means Inter-Integrated Circuit and works on the principle of the synchronous, multi-master multi-slave system. With BME280, the STM32 Blue Pill acts as a master, and the BME280 sensor acts as a slave because it is an external device, that acts as a slave. The Blue Pill communicates with the BME280 sensor through the I2C protocol to get temperature, barometric pressure, relative humidity, and altitude.

Interface BME280 sensor with STM32 Blue Pill and MicroSD Card Module

STM32 Blue Pill with BME280 and MicroSD Card Module

We will need the following components for this project.

  1. STM32 Blue Pill board
  2. BME280 sensor
  3. SD Card
  4. MicroSD Card Module
  5. Breadboard
  6. Connecting Wires

The connection of BME280 with the Blue Pill is very simple. We have to connect the VCC terminal with 3.3V, ground with the ground (common ground), SCL of the sensor with SCL of the module, and SDA of the sensor with the SDA pin of the board.

The connections between the sensor and Blue Pill can be seen in the table below.

STM32 Blue PillBME280
3.3VVIN
PB7 (I2C1_SDA)SDA
PB6 (I2C_SCL)SCL
GNDGND

The BME280 sensor uses I2C communication protocol to communicate with the STM32 Blue Pill, hence we will use I2C1_SCL and I2C1_SDA pins to connect with each of the SCL and SDA pins of the sensor.

The connections between the microSD card module and Blue Pill can be seen in the table below.

MicroSD card moduleSTM32 Blue Pill
GNDGND
VCC5V
CSPA3
MOSIPA7 (SPI1_MOSI)
SCKPA5 (SPI1_SCK)
MISOPA6 (SPI1_MISO)

As shown in the table, we will connect the VCC terminal of MicroSD card module with 5V pin of STM32 Blue Pill. All grounds will be common. We are using SPI1 pins of Blue Pill to connect with the MicroSD card module.

The connections between the three devices which we are using can be seen below.

STM32 Blue Pill with BME280 and MicroSD Card Module connection diagram

Connect the BME280 sensor and the microSD card module with STM32 Blue Pill as shown in the schematic diagram below. We are using the same connections as specified in the tables above.

You can also read:

Formatting the MicroSD card

Make sure your microSD card is formatted as FAT32. We will have to follow a series of steps to accomplish it successfully.

  • First, insert your microSD card in your laptop/computer. Now go to ‘This PC’ and click on SD card icon. Then click on Format by right clicking the SD card icon.
microSD card formatting pic1
  • The following window will appear. Select FAT32 from the dialog box of ‘File System’ and click on ‘START.’
microSD card formatting pic2
  • You will receive a warning message that formatting will erase all previous data saved on the microSD card. Click ‘OK.’
microSD card formatting pic3
  • After a few moments, your microSD card will be formatted successfully. Click ‘OK.’
microSD card formatting pic4

BME280 Data Logger with STM32 Blue Pill Code

We will use STM32CubeIDE to program our STM32 board. Open the IDE and head over to a new project.

Then for the target selection, specify the STM32 Blue Pill board number. After that click on any column as shown in the picture below. Then click the ‘Next’ button.

Blue Pill STM32 using STM32Cube creating project pic 3

Specify the name of your project then click ‘Finish’ to complete the setup of your project.

First of all head over to Connectivity and select the I2C channel. STM32 Blue Pill consists of two I2C channels, I2C1 and I2C2. After selecting the channel, you will have to select the I2C mode from the list shown below.

STM32 Configure I2C Module in STMCube IDE 1

After selecting the mode as ‘I2C’, you will be able to view the parameter settings. This includes the I2C speed mode and clock speed for the master. For the I2C slave, we can set the I2C device address length, value and enable/disable the clock in no stretch mode etc.

STM32 Configure I2C Module in STMCube IDE 2

Now head over to Connectivity > SPI1. Select the SPI mode as ‘Full Duplex Master.’

STM32 Blue Pill with MicroSD Card Module Setup SPI

Configure PA3 as GPIO_Output for the CS pin.

STM32 Blue Pill BME280 Data Logger Configure GPIO Output for CS pin

Now head over to Middleware > FATFS. In the Mode section, tick User-defined. Then click ‘Set Defines’ in the Configuration section. Scroll down and enable with static working buffer for USE_LFN. Also, set MAX_SS as 4096.

STM32 Blue Pill with MicroSD Card Module Setup FATFS

Now we will save our file. Press Ctrl + S. The following window will appear. Click ‘Yes.’ This will generate a template code for you.

Blue Pill STM32 using STM32Cube creating project pic 11

Another window will appear that will ask if you want to open the perspective. Click ‘Yes.’

Blue Pill STM32 Creating project Digital Input picture 11

BME280 Libraries

To acquire BME280 temperature, pressure and humidity readings with STM32 Blue Pill using STMCube IDE, we will require some BME280 libraries. Let us show you how to include them in your project in order to access the APIs provided by bme280.h

Create a new folder called BME280 inside the Src folder. Copy and save the following files inside the BME280 folder.

MicroSD Card Module Libraries

As we are working with an SD card with our STM32 Blue Pill, we will require the fatfs_sd.h library. Let us show you how to add the required libraries and make some modifications in some files which is necessary.

fatfs_sd.h

Go to Core > Inc and create a new file called ‘fatfs_sd.h‘  Copy the following code from this link and save it to this file.

fatfs_sd.c

Similarly, head over to Core > Src and create a new file called ‘fatfs_sd.c‘ Copy the following code from this link and save it to this file.

Modifications

Go to Core > Src and replace the code in the stm32f1xx_it.c file with the code given below:

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    stm32f1xx_it.c
  * @brief   Interrupt Service Routines.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f1xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN TD */

/* USER CODE END TD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
extern uint16_t Timer1, Timer2;

/* USER CODE END 0 */

/* External variables --------------------------------------------------------*/

/* USER CODE BEGIN EV */

/* USER CODE END EV */

/******************************************************************************/
/*           Cortex-M3 Processor Interruption and Exception Handlers          */
/******************************************************************************/
/**
  * @brief This function handles Non maskable interrupt.
  */
void NMI_Handler(void)
{
  /* USER CODE BEGIN NonMaskableInt_IRQn 0 */

  /* USER CODE END NonMaskableInt_IRQn 0 */
  /* USER CODE BEGIN NonMaskableInt_IRQn 1 */
  while (1)
  {
  }
  /* USER CODE END NonMaskableInt_IRQn 1 */
}

/**
  * @brief This function handles Hard fault interrupt.
  */
void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */

  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Memory management fault.
  */
void MemManage_Handler(void)
{
  /* USER CODE BEGIN MemoryManagement_IRQn 0 */

  /* USER CODE END MemoryManagement_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
    /* USER CODE END W1_MemoryManagement_IRQn 0 */
  }
}

/**
  * @brief This function handles Prefetch fault, memory access fault.
  */
void BusFault_Handler(void)
{
  /* USER CODE BEGIN BusFault_IRQn 0 */

  /* USER CODE END BusFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_BusFault_IRQn 0 */
    /* USER CODE END W1_BusFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Undefined instruction or illegal state.
  */
void UsageFault_Handler(void)
{
  /* USER CODE BEGIN UsageFault_IRQn 0 */

  /* USER CODE END UsageFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_UsageFault_IRQn 0 */
    /* USER CODE END W1_UsageFault_IRQn 0 */
  }
}

/**
  * @brief This function handles System service call via SWI instruction.
  */
void SVC_Handler(void)
{
  /* USER CODE BEGIN SVCall_IRQn 0 */

  /* USER CODE END SVCall_IRQn 0 */
  /* USER CODE BEGIN SVCall_IRQn 1 */

  /* USER CODE END SVCall_IRQn 1 */
}

/**
  * @brief This function handles Debug monitor.
  */
void DebugMon_Handler(void)
{
  /* USER CODE BEGIN DebugMonitor_IRQn 0 */

  /* USER CODE END DebugMonitor_IRQn 0 */
  /* USER CODE BEGIN DebugMonitor_IRQn 1 */

  /* USER CODE END DebugMonitor_IRQn 1 */
}

/**
  * @brief This function handles Pendable request for system service.
  */
void PendSV_Handler(void)
{
  /* USER CODE BEGIN PendSV_IRQn 0 */

  /* USER CODE END PendSV_IRQn 0 */
  /* USER CODE BEGIN PendSV_IRQn 1 */

  /* USER CODE END PendSV_IRQn 1 */
}

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */
	if(Timer1 > 0)
		Timer1--;
	if(Timer2 > 0)
		Timer2--;

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

/******************************************************************************/
/* STM32F1xx Peripheral Interrupt Handlers                                    */
/* Add here the Interrupt Handlers for the used peripherals.                  */
/* For the available peripheral interrupt handler names,                      */
/* please refer to the startup file (startup_stm32f1xx.s).                    */
/******************************************************************************/

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

Similarly, go to FATFS > Target and replace the code in the user_diskio.c file with the following code:

/* USER CODE BEGIN Header */
/**
 ******************************************************************************
  * @file    user_diskio.c
  * @brief   This file includes a diskio driver skeleton to be completed by the user.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
 /* USER CODE END Header */

#ifdef USE_OBSOLETE_USER_CODE_SECTION_0
/*
 * Warning: the user section 0 is no more in use (starting from CubeMx version 4.16.0)
 * To be suppressed in the future.
 * Kept to ensure backward compatibility with previous CubeMx versions when
 * migrating projects.
 * User code previously added there should be copied in the new user sections before
 * the section contents can be deleted.
 */
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
#endif

/* USER CODE BEGIN DECL */

/* Includes ------------------------------------------------------------------*/
#include <string.h>
#include "ff_gen_drv.h"

/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/

/* Private variables ---------------------------------------------------------*/
/* Disk status */
static volatile DSTATUS Stat = STA_NOINIT;

/* USER CODE END DECL */

/* Private function prototypes -----------------------------------------------*/
DSTATUS USER_initialize (BYTE pdrv);
DSTATUS USER_status (BYTE pdrv);
DRESULT USER_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count);
#if _USE_WRITE == 1
  DRESULT USER_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count);
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
  DRESULT USER_ioctl (BYTE pdrv, BYTE cmd, void *buff);
#endif /* _USE_IOCTL == 1 */

Diskio_drvTypeDef  USER_Driver =
{
  USER_initialize,
  USER_status,
  USER_read,
#if  _USE_WRITE
  USER_write,
#endif  /* _USE_WRITE == 1 */
#if  _USE_IOCTL == 1
  USER_ioctl,
#endif /* _USE_IOCTL == 1 */
};

/* Private functions ---------------------------------------------------------*/

/**
  * @brief  Initializes a Drive
  * @param  pdrv: Physical drive number (0..)
  * @retval DSTATUS: Operation status
  */
DSTATUS USER_initialize (
	BYTE pdrv           /* Physical drive nmuber to identify the drive */
)
{
  /* USER CODE BEGIN INIT */
    return SD_disk_initialize(pdrv);
  /* USER CODE END INIT */
}

/**
  * @brief  Gets Disk Status
  * @param  pdrv: Physical drive number (0..)
  * @retval DSTATUS: Operation status
  */
DSTATUS USER_status (
	BYTE pdrv       /* Physical drive number to identify the drive */
)
{
  /* USER CODE BEGIN STATUS */
    return SD_disk_status(pdrv);
  /* USER CODE END STATUS */
}

/**
  * @brief  Reads Sector(s)
  * @param  pdrv: Physical drive number (0..)
  * @param  *buff: Data buffer to store read data
  * @param  sector: Sector address (LBA)
  * @param  count: Number of sectors to read (1..128)
  * @retval DRESULT: Operation result
  */
DRESULT USER_read (
	BYTE pdrv,      /* Physical drive nmuber to identify the drive */
	BYTE *buff,     /* Data buffer to store read data */
	DWORD sector,   /* Sector address in LBA */
	UINT count      /* Number of sectors to read */
)
{
  /* USER CODE BEGIN READ */
    return SD_disk_read(pdrv, buff, sector, count);
  /* USER CODE END READ */
}

/**
  * @brief  Writes Sector(s)
  * @param  pdrv: Physical drive number (0..)
  * @param  *buff: Data to be written
  * @param  sector: Sector address (LBA)
  * @param  count: Number of sectors to write (1..128)
  * @retval DRESULT: Operation result
  */
#if _USE_WRITE == 1
DRESULT USER_write (
	BYTE pdrv,          /* Physical drive nmuber to identify the drive */
	const BYTE *buff,   /* Data to be written */
	DWORD sector,       /* Sector address in LBA */
	UINT count          /* Number of sectors to write */
)
{
  /* USER CODE BEGIN WRITE */
  /* USER CODE HERE */
    return SD_disk_write(pdrv, buff, sector, count);
  /* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 */

/**
  * @brief  I/O control operation
  * @param  pdrv: Physical drive number (0..)
  * @param  cmd: Control code
  * @param  *buff: Buffer to send/receive control data
  * @retval DRESULT: Operation result
  */
#if _USE_IOCTL == 1
DRESULT USER_ioctl (
	BYTE pdrv,      /* Physical drive nmuber (0..) */
	BYTE cmd,       /* Control code */
	void *buff      /* Buffer to send/receive control data */
)
{
  /* USER CODE BEGIN IOCTL */
   return SD_disk_ioctl(pdrv, cmd, buff);
  /* USER CODE END IOCTL */
}
#endif /* _USE_IOCTL == 1 */

STM32 BME280 Data Logger STM32CubeIDE Code

We will show you how to log BME280 sensor readings in the data.txt file in the microSD card. Now let us look at our main.c file that was generated.

Inside the main.c file, ensure the following code is part of your script by including the lines of code below.

#include "main.h"
#include "fatfs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "./BME280/bme280.h"

FATFS fs;
FIL fil;

I2C_HandleTypeDef hi2c1;

SPI_HandleTypeDef hspi1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_I2C1_Init(void);
static void MX_SPI1_Init(void);

float temperature;
float humidity;
float pressure;

struct bme280_dev dev;
struct bme280_data comp_data;
int8_t rslt;

char hum_string[50];
char temp_string[50];
char press_string[50];

int8_t user_i2c_read(uint8_t id, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
  if(HAL_I2C_Master_Transmit(&hi2c1, (id << 1), &reg_addr, 1, 10) != HAL_OK) return -1;
  if(HAL_I2C_Master_Receive(&hi2c1, (id << 1) | 0x01, data, len, 10) != HAL_OK) return -1;

  return 0;
}

void user_delay_ms(uint32_t period)
{
  HAL_Delay(period);
}

int8_t user_i2c_write(uint8_t id, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
  int8_t *buf;
  buf = malloc(len +1);
  buf[0] = reg_addr;
  memcpy(buf +1, data, len);

  if(HAL_I2C_Master_Transmit(&hi2c1, (id << 1), (uint8_t*)buf, len + 1, HAL_MAX_DELAY) != HAL_OK) return -1;

  free(buf);
  return 0;
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_SPI1_Init();
  MX_FATFS_Init();

	  /* BME280 init */
	    dev.dev_id = BME280_I2C_ADDR_PRIM;
	    dev.intf = BME280_I2C_INTF;
	    dev.read = user_i2c_read;
	    dev.write = user_i2c_write;
	    dev.delay_ms = user_delay_ms;

	    rslt = bme280_init(&dev);

	    /* BME280 settings */
	    dev.settings.osr_h = BME280_OVERSAMPLING_1X;
	    dev.settings.osr_p = BME280_OVERSAMPLING_16X;
	    dev.settings.osr_t = BME280_OVERSAMPLING_2X;
	    dev.settings.filter = BME280_FILTER_COEFF_16;
	    rslt = bme280_set_sensor_settings(BME280_OSR_PRESS_SEL | BME280_OSR_TEMP_SEL | BME280_OSR_HUM_SEL | BME280_FILTER_SEL, &dev);

	    HAL_Delay(500);
	    f_mount(&fs, "", 0);
	    f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
	  	f_lseek(&fil, fil.fsize);
	  	f_puts("Logging BME280 Sensor Data...\n", &fil);
	  	f_close(&fil);



	    while (1)
	    {
	      /* Forced mode setting, switched to SLEEP mode after measurement */
	      rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, &dev);
	      dev.delay_ms(40);
	      /*Get Data */
	      rslt = bme280_get_sensor_data(BME280_ALL, &comp_data, &dev);
	      if(rslt == BME280_OK)
	      {
	        temperature = comp_data.temperature / 100.0;
	        humidity = comp_data.humidity / 1024.0;
	        pressure = comp_data.pressure / 10000.0;

	        /*Log Data */
	        memset(hum_string, 0, sizeof(hum_string));
	        memset(temp_string, 0, sizeof(temp_string));
	        memset(press_string, 0, sizeof(press_string));

	        sprintf(hum_string, "Humidity %03.1f%% ", humidity);
	        sprintf(temp_string, "Temperature %03.1fC ", temperature);
	        sprintf(press_string, "Pressure %03.1fhPa\n", pressure);

	        f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
	        f_lseek(&fil, fil.fsize);
	        f_puts(hum_string, &fil);
	        f_puts(temp_string, &fil);
	        f_puts(press_string, &fil);
	        f_close(&fil);

	      }

	      HAL_Delay(1000);
  }
}

How the Code Works?

We start off by including the necessary libraries for this project which include the bme280.h for the sensor and the fatfs.h library to access the APIs for FATFS.

#include "main.h"
#include "fatfs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "./BME280/bme280.h"

We create the file system object called ‘fs’ and the file object structure ‘fil.’

FATFS fs;
FIL fil;

Create some variables that we will use later on in the sketch to store the BME280 sensor data.

float temperature;
float humidity;
float pressure;

struct bme280_dev dev;
struct bme280_data comp_data;
int8_t rslt;

char hum_string[50];
char temp_string[50];
char press_string[50];

The user_i2c_read() function calls the data transmission and reception I2C HAL APIs in blocking mode for read purpose. This function returns 0 if I2C read data transmission and reception for master occurs successfully. Otherwise, if either of them fails, -1 is returned instead.

int8_t user_i2c_read(uint8_t id, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
  if(HAL_I2C_Master_Transmit(&hi2c1, (id << 1), &reg_addr, 1, 10) != HAL_OK) return -1;
  if(HAL_I2C_Master_Receive(&hi2c1, (id << 1) | 0x01, data, len, 10) != HAL_OK) return -1;

  return 0;
}

The user_delay_ms() function takes in a single parameter which is ‘period’ inside it. This function will be called to cause a delay according to the period set.

void user_delay_ms(uint32_t period)
{
  HAL_Delay(period);
}

The user_i2c_write() function calls the data transmission I2C HAL APIs in blocking mode for write purpose. This function returns 0 if I2C write data transmission for master occurs successfully. Otherwise, -1 is returned instead.

int8_t user_i2c_write(uint8_t id, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
  int8_t *buf;
  buf = malloc(len +1);
  buf[0] = reg_addr;
  memcpy(buf +1, data, len);

  if(HAL_I2C_Master_Transmit(&hi2c1, (id << 1), (uint8_t*)buf, len + 1, HAL_MAX_DELAY) != HAL_OK) return -1;

  free(buf);
  return 0;
}

main()

Inside the main function, first all the peripherals are initialized, system clock is configured and all the configured peripherals are initialized.

  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_SPI1_Init();
  MX_FATFS_Init();
Initialize BME280 Sensor

Specify the BME280 device structure parameters for initialization including device id, interface, read function pointer, write function pointer and delay function pointer. Initialize the BME280 sensor by calling bme280_init() which reads the chip-id and calibrates data from the sensor. This function takes in a single parameter which is the pointer to the bme280 device structure.

dev.dev_id = BME280_I2C_ADDR_PRIM;
  dev.intf = BME280_I2C_INTF;
  dev.read = user_i2c_read;
  dev.write = user_i2c_write;
  dev.delay_ms = user_delay_ms;

  rslt = bme280_init(&dev);

Next, we set the settings of the BME280 sensor which includes the humidity, pressure and temperature oversampling values. The function bme280_set_sensor_settings() is responsible for setting the oversampling, filter, and standby duration. It takes in two parameters which are the desired settings and the pointer to the bme280 device structure.

  dev.settings.osr_h = BME280_OVERSAMPLING_1X;
  dev.settings.osr_p = BME280_OVERSAMPLING_16X;
  dev.settings.osr_t = BME280_OVERSAMPLING_2X;
  dev.settings.filter = BME280_FILTER_COEFF_16;
  rslt = bme280_set_sensor_settings(BME280_OSR_PRESS_SEL | BME280_OSR_TEMP_SEL | BME280_OSR_HUM_SEL | BME280_FILTER_SEL, &dev);

Mount Filesystem

After a slight delay, we fount the filesystem. We call f_mount() function that provides the work area to the FatFs module. This function takes in three parameters:

  1. the pointer to the filesystem object. In our case it is ‘&fs.’
  2. the pointer to the logical drive number. In our case the string does not consist of a drive number which denotes the default drive.
  3. the mounting option. In our case it is ‘0’ which means it will be mounted on the first access to the volume. If ‘1’ was specified as a parameter instead then it meant that the volume would be force mounted to check if it is ready to work.
 f_mount(&fs, "", 0);

Open

Then we call f_open() to create or open a file. This function takes in three parameters:

  1. The pointer to the blank file object structure.
  2. The pointer to the null-terminated string that denotes the file name to open or create.
  3. Lastly, the mode flags. They denote the type of access and open method for the file. The table below shows the combination of different flags which may be used.
FlagsDescription
FA_READTo read data from a file.
FA_WRITETo write data to a file. If it is combined with FA_READ then both read-write access is granted.
FA_OPEN_EXISTINGOpen an already existing file. If the file does not exist, then this function fails.
FA_CREATE_NEWCreate a new file. If the file already exists, then this function fails.
FA_CREATE_ALWAYSCreate a new file. If the file already exists, then the file gets overwritten.
FA_OPEN_ALWAYSCreate a new file, if the file does not exist. If the file exists, then it opens the file.
FA_OPEN_APPENDCreate a new file, if the file does not exist. If the file exists, then it opens the file. The read/write pointer is set at end of the file.

Here we are creating a file called ‘data.txt’ and giving it read-write access.

 f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);

Next, we call the f_lseek() function which will transfer the file read/write pointer of an open file object. Moreover, it can also be used to increase the size of the file. This function takes in two parameters:

  1. The pointer to the open file object.
  2. The byte offset from top of the file which will be used to configure the read/write pointer.
f_lseek(&fil, fil.fsize);

Write

Then we write a string to the file using f_puts() function. This function takes in two parameters:

  1. The pointer to the null terminated string to be written.
  2. The pointer to the open file object structure.

Here we are writing the string “Logging BME280 Sensor Data…” to the file.

f_puts("Logging BME280 Sensor Data...\n", &fil);

After writing to the file, we close the open file using the function f_close(). This function takes in a single parameter which is the pointer to the open file object structure that is to be closed.

f_close(&fil);
Obtain Sensor Data

Inside the infinite while loop, we first set the BME280 sensor in forced mode setting and then switch it to sleep mode after the measurement. The mode of the sensor is configured through the function bme280_set_sensor_mode().

  rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, &dev);
   dev.delay_ms(40);

After a short delay, we will start accessing the sensor data from the sensor. The bme280_get_sensor_data() will read the temperature, pressure and humidity readings from the BME280 sensor, compensate them and store them in the bme280_data structure.

  rslt = bme280_get_sensor_data(BME280_ALL, &comp_data, &dev);
Log Sensor Data

If the function returns a successful response, then we will start logging the sensor readings on the data.txt file in the SD card. The readings will be logged after every second.

 if(rslt == BME280_OK)
	      {
	        temperature = comp_data.temperature / 100.0;
	        humidity = comp_data.humidity / 1024.0;
	        pressure = comp_data.pressure / 10000.0;

	        /*Log Data */
	        memset(hum_string, 0, sizeof(hum_string));
	        memset(temp_string, 0, sizeof(temp_string));
	        memset(press_string, 0, sizeof(press_string));

	        sprintf(hum_string, "Humidity %03.1f%% ", humidity);
	        sprintf(temp_string, "Temperature %03.1fC ", temperature);
	        sprintf(press_string, "Pressure %03.1fhPa\n", pressure);

	        f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
	        f_lseek(&fil, fil.fsize);
	        f_puts(hum_string, &fil);
	        f_puts(temp_string, &fil);
	        f_puts(press_string, &fil);
	        f_close(&fil);

	      }
     

First of all, we will obtain individual compensated temperature readings in degree Celsius, compensated humidity readings in percentage and compensated pressure readings in hPa from the bme_280 structure.

temperature = comp_data.temperature / 100.0;
humidity = comp_data.humidity / 1024.0;
pressure = comp_data.pressure / 10000.0;

Then we convert the float variables to strings consisting of readings and units and log them on the SD card.

memset(hum_string, 0, sizeof(hum_string));
memset(temp_string, 0, sizeof(temp_string));
memset(press_string, 0, sizeof(press_string));

sprintf(hum_string, "Humidity %03.1f%% ", humidity);
sprintf(temp_string, "Temperature %03.1fC ", temperature);
sprintf(press_string, "Pressure %03.1fhPa\n", pressure);

f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
f_lseek(&fil, fil.fsize);
f_puts(hum_string, &fil);
f_puts(temp_string, &fil);
f_puts(press_string, &fil);
f_close(&fil);

main.c file

This is how a complete main.c file will be after modification.

#include "main.h"
#include "fatfs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "./BME280/bme280.h"

FATFS fs;
FIL fil;

I2C_HandleTypeDef hi2c1;

SPI_HandleTypeDef hspi1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_I2C1_Init(void);
static void MX_SPI1_Init(void);

float temperature;
float humidity;
float pressure;

struct bme280_dev dev;
struct bme280_data comp_data;
int8_t rslt;

char hum_string[50];
char temp_string[50];
char press_string[50];

int8_t user_i2c_read(uint8_t id, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
  if(HAL_I2C_Master_Transmit(&hi2c1, (id << 1), &reg_addr, 1, 10) != HAL_OK) return -1;
  if(HAL_I2C_Master_Receive(&hi2c1, (id << 1) | 0x01, data, len, 10) != HAL_OK) return -1;

  return 0;
}

void user_delay_ms(uint32_t period)
{
  HAL_Delay(period);
}

int8_t user_i2c_write(uint8_t id, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
  int8_t *buf;
  buf = malloc(len +1);
  buf[0] = reg_addr;
  memcpy(buf +1, data, len);

  if(HAL_I2C_Master_Transmit(&hi2c1, (id << 1), (uint8_t*)buf, len + 1, HAL_MAX_DELAY) != HAL_OK) return -1;

  free(buf);
  return 0;
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_SPI1_Init();
  MX_FATFS_Init();

	  /* BME280 init */
	    dev.dev_id = BME280_I2C_ADDR_PRIM;
	    dev.intf = BME280_I2C_INTF;
	    dev.read = user_i2c_read;
	    dev.write = user_i2c_write;
	    dev.delay_ms = user_delay_ms;

	    rslt = bme280_init(&dev);

	    /* BME280 settings */
	    dev.settings.osr_h = BME280_OVERSAMPLING_1X;
	    dev.settings.osr_p = BME280_OVERSAMPLING_16X;
	    dev.settings.osr_t = BME280_OVERSAMPLING_2X;
	    dev.settings.filter = BME280_FILTER_COEFF_16;
	    rslt = bme280_set_sensor_settings(BME280_OSR_PRESS_SEL | BME280_OSR_TEMP_SEL | BME280_OSR_HUM_SEL | BME280_FILTER_SEL, &dev);

	    HAL_Delay(500);
	    f_mount(&fs, "", 0);
	    f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
	  	f_lseek(&fil, fil.fsize);
	  	f_puts("Logging BME280 Sensor Data...\n", &fil);
	  	f_close(&fil);



	    while (1)
	    {
	      /* Forced mode setting, switched to SLEEP mode after measurement */
	      rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, &dev);
	      dev.delay_ms(40);
	      /*Get Data */
	      rslt = bme280_get_sensor_data(BME280_ALL, &comp_data, &dev);
	      if(rslt == BME280_OK)
	      {
	        temperature = comp_data.temperature / 100.0;
	        humidity = comp_data.humidity / 1024.0;
	        pressure = comp_data.pressure / 10000.0;

	        /*Log Data */
	        memset(hum_string, 0, sizeof(hum_string));
	        memset(temp_string, 0, sizeof(temp_string));
	        memset(press_string, 0, sizeof(press_string));

	        sprintf(hum_string, "Humidity %03.1f%% ", humidity);
	        sprintf(temp_string, "Temperature %03.1fC ", temperature);
	        sprintf(press_string, "Pressure %03.1fhPa\n", pressure);

	        f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
	        f_lseek(&fil, fil.fsize);
	        f_puts(hum_string, &fil);
	        f_puts(temp_string, &fil);
	        f_puts(press_string, &fil);
	        f_close(&fil);

	      }

	      HAL_Delay(1000);
  }
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief I2C1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_I2C1_Init(void)
{

  /* USER CODE BEGIN I2C1_Init 0 */

  /* USER CODE END I2C1_Init 0 */

  /* USER CODE BEGIN I2C1_Init 1 */

  /* USER CODE END I2C1_Init 1 */
  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 100000;
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I2C1_Init 2 */

  /* USER CODE END I2C1_Init 2 */

}

/**
  * @brief SPI1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_SPI1_Init(void)
{

  /* USER CODE BEGIN SPI1_Init 0 */

  /* USER CODE END SPI1_Init 0 */

  /* USER CODE BEGIN SPI1_Init 1 */

  /* USER CODE END SPI1_Init 1 */
  /* SPI1 parameter configuration*/
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 10;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN SPI1_Init 2 */

  /* USER CODE END SPI1_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET);

  /*Configure GPIO pin : PA3 */
  GPIO_InitStruct.Pin = GPIO_PIN_3;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

Save the main.c file after modifying it. Now we are ready to build our project.

Building the Project

To build our project press Ctrl + B or go to Project > Build All.

Your project will start building. After a few moments, your project will be successfully built if there are no errors.

Connecting ST-Link Programmer with STM32

Now as we have successfully built our project let us move ahead and upload the code to our STM32 board. To do that, first we will have to connect our Blue Pill STM32 with an ST-Link programmer. We will be using ST-Link V2.

ST-Link V2 programmer

This will provide an interface between our computer and our STM32 board. It consists of 10 pins. We will be using pin2 SWDIO, pin6 SWCLK, pin4 GND, and pin8 3.3V to connect with our STM32 board. The SWDIO is the data input/output pin and the SWCLK is the clock pin. Follow the pin configuration given on ST-LINK V2 to identify each pin.

Follow the table below to connect both devices correctly.

STM32ST-LINK V2
VCC 3.3V pinpin8 3.3V
SWDIO pinpin2 SWDIO
SWCLK pinpin6 SWCLK
GND pinpin4 GND
ST-Link V2 with STM32 connection

Additionally, move the BOOT jumper to the right to enable the microcontroller to go into programming mode.

STM32 in programming mode
  • Now connect your ST-LINK V2 to your computer via the USB port. Both devices will power ON.
  • Next press the RUN button in the IDE. The ‘Edit configuration’ window will open up. Click ‘OK’.
  • After a few moments, the code will be successfully sent to the STM32 board. Otherwise, press the RESET button on your STM32 board.
  • Now to bring the Blue pill back to normal mode make sure you bring the BOOT jumper back at its place.

Once the code is uploaded to the board, take the microSD card out of the module and insert it in your system to view the data.txt file.

STM32 Blue Pill BME280 Data Logger Open data.txt file

Open the data.txt file. Inside it, you can view the BME280 sensor readings that were logged into the file:

STM32 Blue Pill BME280 Data Logger demo

You may also like to read:

Other MicroSD related projects:

2 thoughts on “STM32 Blue Pill BME280 Data Logger using STM32CubeIDE”

Leave a Comment