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.
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
We will need the following components for this project.
- STM32 Blue Pill board
- BME280 sensor
- SD Card
- MicroSD Card Module
- Breadboard
- 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 Pill | BME280 |
---|---|
3.3V | VIN |
PB7 (I2C1_SDA) | SDA |
PB6 (I2C_SCL) | SCL |
GND | GND |
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 module | STM32 Blue Pill |
---|---|
GND | GND |
VCC | 5V |
CS | PA3 |
MOSI | PA7 (SPI1_MOSI) |
SCK | PA5 (SPI1_SCK) |
MISO | PA6 (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.
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.
- The following window will appear. Select FAT32 from the dialog box of ‘File System’ and click on ‘START.’
- You will receive a warning message that formatting will erase all previous data saved on the microSD card. Click ‘OK.’
- After a few moments, your microSD card will be formatted successfully. Click ‘OK.’
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.
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.
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.
Now head over to Connectivity > SPI1. Select the SPI mode as ‘Full Duplex Master.’
Configure PA3 as GPIO_Output for the 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.
Now we will save our file. Press Ctrl + S. The following window will appear. Click ‘Yes.’ This will generate a template code for you.
Another window will appear that will ask if you want to open the perspective. Click ‘Yes.’
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), ®_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), ®_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:
- the pointer to the filesystem object. In our case it is ‘&fs.’
- the pointer to the logical drive number. In our case the string does not consist of a drive number which denotes the default drive.
- 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:
- The pointer to the blank file object structure.
- The pointer to the null-terminated string that denotes the file name to open or create.
- 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.
Flags | Description |
FA_READ | To read data from a file. |
FA_WRITE | To write data to a file. If it is combined with FA_READ then both read-write access is granted. |
FA_OPEN_EXISTING | Open an already existing file. If the file does not exist, then this function fails. |
FA_CREATE_NEW | Create a new file. If the file already exists, then this function fails. |
FA_CREATE_ALWAYS | Create a new file. If the file already exists, then the file gets overwritten. |
FA_OPEN_ALWAYS | Create a new file, if the file does not exist. If the file exists, then it opens the file. |
FA_OPEN_APPEND | Create 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:
- The pointer to the open file object.
- 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:
- The pointer to the null terminated string to be written.
- 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), ®_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.
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.
STM32 | ST-LINK V2 |
---|---|
VCC 3.3V pin | pin8 3.3V |
SWDIO pin | pin2 SWDIO |
SWCLK pin | pin6 SWCLK |
GND pin | pin4 GND |
Additionally, move the BOOT jumper to the right to enable the microcontroller to go into 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.
Open the data.txt file. Inside it, you can view the BME280 sensor readings that were logged into the file:
You may also like to read:
- DHT22 Sensor with STM32 Blue Pill using STM32CubeIDE
- HC-SR04 Ultrasonic Sensor with STM32 Blue Pill using STM32CubeIDE
- SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE
- HC-05 Bluetooth Module with STM32 Blue Pill using STM32CubeIDE
- STM32 Blue Pill Timer Encoder Mode with Rotary Encoder Example
Other MicroSD related projects:
- Data Logger with Raspberry Pi Pico and Micro SD Card
- ESP32-CAM Capture Photo and Save to MicroSD Card
- BME280 Data Logger with Arduino and Micro SD Card
- DHT22 Data Logger with Arduino and Micro SD Card
- GPS Data Logger with Arduino and Micro SD Card – GPS Tracker
- ESP32 Web Server Hosting Files from Micro SD card (Arduino IDE)
How much?
Which SD card did you use? Does it work for any SD Card or only for a particular make and class?