The best tools to make your project dreams come true

Login or Signup
USD


By ShawnHymel

Making a Temperature Logger with the Adafruit Feather STM32F405 Express

While CircuitPython is likely the preferred method of programming the new Adafruit Feather STM32F405 Express board, what happens if you want to write some good, old-fashioned C? Good news! We can use the official STM32 integrated development environment, STM32CubeIDE, to write and upload programs to the nifty Feather board.

Before beginning, I highly recommend reviewing how to set up and use the STM32CubeIDE.

Additionally, I recommend reading up on how to use STM32 HAL to communicate with sensors via I2C.

Required Components

You will need the following components to complete this tutorial:

Hardware Hookoup

Plug the SD card into the SD card slot on the bottom of the Feather board.

Plugging SD card into Feather STM32F405 Express

Connect the PCT2075 temperature sensor to the Feather board using a Qwiic cable.

Connect Qwiic to STM32F405

Format SD Card

Make sure to backup any data you might want on your SD card--you’re about to lose it. Use your favorite formatting tool (on Windows: right-click the drive and click Format…) to format the drive with the FAT32 file system and a cluster size (Windows: Allocation unit size) of 512 bytes

Format SD card

The cluster size seems to need to be in between the MAX_ and MIN_SS (sector size) in the FATFS configuration parameters (under Set Defines when you get to the Cube settings part).

Create a New Project in STM32CubeIDE

I recommend watching the accompanying video to see how to configure STM32CubeIDE to work with the STM32F405 on the Feather board. There are a good number of steps that you need to take to set up the peripherals and configure the clocks!

 

Start a new STM32 project and select the STM32F405RG as your processor (it’s the exact part found on the Feather board).

Set up STM32CubeIDE

Give your project a good name and open the Cube perspective. Change PB12 to GPIO_Input (our SD card detect pin), and change PC1 to GPIO_Output (connected to onboard red LED).

Configure pins

Under System Core > GPIO, click on PC1 and change User Label to LED. This will allow us to refer to that pin in code as “LED.” 

Configure LED pin

Under System Core > RCC, change High Speed Clock (HSE) to Crystal/Ceramic Resonator as the Feather board has an external 12 MHz crystal connected to the microcontroller.

Configure clocks

If you plan to use step-through debugging (e.g. with an ST-LINK), go into System Core > SYS and change Debug to Serial Wire

Configure debug port

In Connectivity > I2C1, change I2C mode from Disable to I2C. Since the Feather board already has pull-ups on the I2C1 lines, we can disable the internal interrupts. So, under GPIO Settings, change the GPIO Pull-up/Pull-down settings to No pull-up and no pull-down for both SDA and SCL lines.

Configure I2C

In Connectivity > SDIO, change Mode to SD 4 bit Wide bus. Make the following changes:

  • NVIC Settings: click to enable SDIO global interrupt
  • DMA Settings: click Add to create SDIO_RX and SDIO_TX DMA requests (leave all other settings at their defaults)
  • GPIO Settings: Change all pins to have internal Pull-up resistors enabled EXCEPT SDIO_D3 and SDIO_CLK. D3 has an external pull-up on the Feather board, and CLK does not need a pull-up.

Configure SD card port

Go to Connectivity > USB_OTG_FS, and change Mode to Device_Only. We’re going to use the USB connection as a virtual COM port to transmit temperature data back to our computer, which is mostly useful for debugging. Leave the Configuration settings as default.

Configure USB

In Middleware > FATFS, click to enable SD Card. Make the following changes in the Configuration setting:

  • Platform Settings: change Detect_SDPIO pin from Undefined to PB12
  • Advanced Settings: change Use dma template to Enabled (we need to do this since we’re using DMA for SD card transfers)

Configure SD

In Middleware > USB_DEVICE, change Class for FS IP to Communication Device Class (Virtual Port Com). Leave the Configuration settings as their defaults. This allows our STM32 to enumerate as a virtual COM port (VCP) so we can communicate via serial commands back to our host computer.

Configure VCP

Navigate over to the Clock Configuration tab and make the following changes:

  • Set the Input Frequency to 12 (MHz)
  • Click to change the PLL Source Mux input to HSE
  • Click to change the System Clock Mux input to PLLCLK
  • Change the HCLK number to 168 (MHz)

Press the ‘enter’ key (or press the Resolve Clock Issues button) to let the clock configurator automatically choose the correct dividers to meet these clock requirements.

Configure clocks

Code: Fixing USB Issues

On Windows, the VCP driver sometimes will not work. To fix this, we can put some code in the usbd_cdc_if.c file to echo the line coding scheme back to Windows. This is a bit of a hack, but it seems to work.

Credit for this fix goes to GitHub user Ant1882. The original code for this fix can be found in this repo commit.

Replace the CDC_Control_FS() function with the following code:

Copy Code
/**
* @brief Manage the CDC class requests
* @param cmd: Command code
* @param pbuf: Buffer containing command data (request parameters)
* @param length: Number of data to be sent (in bytes)
* @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length)
{
/* USER CODE BEGIN 5 */
uint8_t tempbuf7 = {0,0,0,0,0,0,0};
switch(cmd)
{
case CDC_SEND_ENCAPSULATED_COMMAND:

break;

case CDC_GET_ENCAPSULATED_RESPONSE:

break;

case CDC_SET_COMM_FEATURE:

break;

case CDC_GET_COMM_FEATURE:

break;

case CDC_CLEAR_COMM_FEATURE:

break;

/*******************************************************************************/
/* Line Coding Structure */
/*-----------------------------------------------------------------------------*/
/* Offset | Field | Size | Value | Description */
/* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/
/* 4 | bCharFormat | 1 | Number | Stop bits */
/* 0 - 1 Stop bit */
/* 1 - 1.5 Stop bits */
/* 2 - 2 Stop bits */
/* 5 | bParityType | 1 | Number | Parity */
/* 0 - None */
/* 1 - Odd */
/* 2 - Even */
/* 3 - Mark */
/* 4 - Space */
/* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */
/*******************************************************************************/
case CDC_SET_LINE_CODING:
tempbuf0 = pbuf0;
tempbuf1 = pbuf1;
tempbuf2 = pbuf2;
tempbuf3 = pbuf3;
tempbuf4 = pbuf4;
tempbuf5 = pbuf5;
tempbuf6 = pbuf6;
break;

case CDC_GET_LINE_CODING:
pbuf0 = tempbuf0;
pbuf1 = tempbuf1;
pbuf2 = tempbuf2;
pbuf3 = tempbuf3;
pbuf4 = tempbuf4;
pbuf5 = tempbuf5;
pbuf6 = tempbuf6;
break;

case CDC_SET_CONTROL_LINE_STATE:

break;

case CDC_SEND_BREAK:

break;

default:
break;
}

return (USBD_OK);
/* USER CODE END 5 */
}

Save that file.

Code: Main

Open main.c. Copy in the following sections of code (note that I’ve collapsed the auto-generated initialization routines):

Copy Code
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>&copy; Copyright (c) 2019 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "fatfs.h"
#include "usb_device.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
#include "usbd_cdc_if.h"
/* USER CODE END Includes */

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

/* USER CODE END PTD */

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

/* USER CODE END PD */

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

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
I2C_HandleTypeDef hi2c1;

SD_HandleTypeDef hsd;
DMA_HandleTypeDef hdma_sdio_rx;
DMA_HandleTypeDef hdma_sdio_tx;

/* USER CODE BEGIN PV */
static const uint32_t I2C_DELAY = 1000; // Time (ms) to wait for I2C
static const uint8_t PCT_I2C_ADDR = 0x37 << 1; // Use 8-bit address
static const uint8_t PCT_REG_TEMP = 0x00; // Temperature register
static const uint16_t PCT_ERROR = 0xFFFF; // I2C/PCT error code
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_SDIO_SD_Init(void);
static void MX_I2C1_Init(void);
/* USER CODE BEGIN PFP */
uint16_t ReadPCTTemperature(uint8_t i2c_addr);
FRESULT AppendToFile(char* path, size_t path_len, char* msg, size_t msg_len);
void BlinkLED(uint32_t blink_delay, uint8_t num_blinks);
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
FRESULT fres;
uint16_t raw_temp;
float temp_c;
char log_path = "/TEMPLOG.TXT";
char buf20;
/* USER CODE END 1 */


/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_SDIO_SD_Init();
MX_FATFS_Init();
MX_USB_DEVICE_Init();
MX_I2C1_Init();
/* USER CODE BEGIN 2 */

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{

// Attempt to read temperature from sensor
raw_temp = ReadPCTTemperature(PCT_I2C_ADDR);
if ( raw_temp == PCT_ERROR ) {
BlinkLED(100, 5);
} else {

// Convert raw to 2's complement, since temperature can be negative
if ( raw_temp > 0x3FF ) {
raw_temp |= 0xF800;
}

// Convert to float temperature value (Celsius)
temp_c = (int16_t)raw_temp * 0.125;

// Convert temperature to decimal format (without float conversion)
temp_c *= 100;
sprintf((char*)buf,
"%u.u C\r\n",
((unsigned int)temp_c / 100),
((unsigned int)temp_c % 100));

// Print temperature to console
CDC_Transmit_FS((uint8_t*)buf, strlen(buf));

// Turn LED on while writing to file
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
fres = AppendToFile(log_path, strlen(log_path), buf, strlen(buf));
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);

// If error writing to card, blink 3 times
if ( fres != FR_OK) {
BlinkLED(200, 3);
}
}

// Wait before sampling again
HAL_Delay(1000);

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
...
}

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

/**
* @brief SDIO Initialization Function
* @param None
* @retval None
*/
static void MX_SDIO_SD_Init(void)
{
...
}

/**
* Enable DMA controller clock
*/
static void MX_DMA_Init(void)
{
...
}

/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
...
}

/* USER CODE BEGIN 4 */

// Read temperature from PCT2075
uint16_t ReadPCTTemperature(uint8_t i2c_addr) {

HAL_StatusTypeDef ret;
uint8_t buf2;
uint16_t val;

// Tell PCT2075 that we want to read from the temperature register
buf0 = PCT_REG_TEMP;
ret = HAL_I2C_Master_Transmit(&hi2c1, PCT_I2C_ADDR, buf, 1, I2C_DELAY);

// If the I2C device has just been hot-plugged, reset the peripheral
if ( ret == HAL_BUSY ) {
if (HAL_I2C_DeInit(&hi2c1) != HAL_OK){
Error_Handler();
}
MX_I2C1_Init();
}

// Throw error if communication not OK
if ( ret != HAL_OK ) {
return PCT_ERROR;
}

// Read 2 bytes from the temperature register
ret = HAL_I2C_Master_Receive(&hi2c1, PCT_I2C_ADDR, buf, 2, I2C_DELAY);
if ( ret != HAL_OK ) {
return PCT_ERROR;
}

// Combine the bytes and return raw value
val = ((uint16_t)buf0 << 3) | (buf1 >> 5);

return val;
}

// Append string to file given at path
FRESULT AppendToFile(char* path, size_t path_len, char* msg, size_t msg_len) {

FATFS fs;
FIL myFILE;
UINT testByte;
FRESULT stat;

// Bounds check on strings
if ( (pathpath_len != 0) || (msgmsg_len != 0) ) {
return FR_INVALID_NAME;
}

// Re-initialize SD
if ( BSP_SD_Init() != MSD_OK ) {
return FR_NOT_READY;
}

// Re-initialize FATFS
if ( FATFS_UnLinkDriver(SDPath) != 0 ) {
return FR_NOT_READY;
}
if ( FATFS_LinkDriver(&SD_Driver, SDPath) != 0 ) {
return FR_NOT_READY;
}

// Mount filesystem
stat = f_mount(&fs, SDPath, 0);
if (stat != FR_OK) {
f_mount(0, SDPath, 0);
return stat;
}

// Open file for appending
stat = f_open(&myFILE, path, FA_WRITE | FA_OPEN_APPEND);
if (stat != FR_OK) {
f_mount(0, SDPath, 0);
return stat;
}

// Write message to end of file
stat = f_write(&myFILE, msg, msg_len, &testByte);
if (stat != FR_OK) {
f_mount(0, SDPath, 0);
return stat;
}

// Sync, close file, unmount
stat = f_close(&myFILE);
f_mount(0, SDPath, 0);

return stat;
}

// Blink onboard LED
void BlinkLED(uint32_t blink_delay, uint8_t num_blinks) {
for ( int i = 0; i < num_blinks; i++ ) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
HAL_Delay(blink_delay);
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
HAL_Delay(blink_delay);
}
}

/* 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 */

/* 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,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

Code Analysis

Let’s take a moment to talk about what some of these sections are doing.

First, we have our includes:

Copy Code
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
#include "usbd_cdc_if.h"
/* USER CODE END Includes */

Since we are manipulating strings, the standard string libraries prove very helpful here. Secondly, we need to include the USB CDC header file. I’m not sure why this isn’t auto-included in the generated code (we told the Cube software that we wanted USB CDC), but it’s not for whatever reason. If we don’t put it here, the compiler yells at us.

Next, we set a number of private defines:

Copy Code
/* USER CODE BEGIN PV */
static const uint32_t I2C_DELAY = 1000;         // Time (ms) to wait for I2C
static const uint8_t PCT_I2C_ADDR = 0x37 << 1;  // Use 8-bit address
static const uint8_t PCT_REG_TEMP = 0x00;       // Temperature register
static const uint16_t PCT_ERROR = 0xFFFF;        // I2C/PTC error code
/* USER CODE END PV */

Rather than using the MAX value for the I2C timeout, we specify a real time here (1 second) so that we can throw an error if the temperature sensor is not plugged in. The others have to do with reading from the PCT2075 sensor. Refer to the STM32 I2C tutorial if you need a refresher on I2C. Finally, the ERROR is simply a number that’s not in range of the 11-bit value returned from the sensor (0x000 - 0x7FF). We can use that to determine if there was an I2C read error.

We then declare a few functions for us to use:

Copy Code
/* USER CODE BEGIN PFP */
uint16_t ReadPCTTemperature(uint8_t i2c_addr);
FRESULT AppendToFile(char* path, size_t path_len, char* msg, size_t msg_len);
void BlinkLED(uint32_t blink_delay, uint8_t num_blinks);
/* USER CODE END PFP */

These should be pretty straightforward--we’ll discuss what the functions do later.

In main(), we define a few local variables:

Copy Code
  /* USER CODE BEGIN 1 */
  FRESULT fres;
  uint16_t raw_temp;
  float temp_c;
  char log_path = "/TEMPLOG.TXT";
  char buf20;
  /* USER CODE END 1 */

We’ll need these when computing temperature and writing to the SD card. Note that we’re declaring the text file as a constant here--you’re free to manipulate it as necessary (e.g. you could append a number to the end of the file if you want to create a new log each time you power on the device).

FATFS uses a pretty limited (and old!) scheme for filenames: all capital letters, at most 8 characters for the filename, and 3 characters for the extension (e.g. TXT). It also seems to be easier to just work in the root directory of the SD card (hence the ‘/’ before the filename).

In our while(1) loop, we perform our basic sample-and-log functions. First, we try to read from the sensor:

Copy Code
    // Attempt to read temperature from sensor
    raw_temp = ReadPCTTemperature(PCT_I2C_ADDR);
    if ( raw_temp == PCT_ERROR ) {
      BlinkLED(100, 5);
    } else {

We’ll write the ReadPCTTemperature() function in a minute, but for now, just know that if it returns an error (which we defined above, 0xFFFF), we will flash the onboard LED very quickly 5 times. This lets the user know if something is wrong with I2C comms.

If we don’t get an error, then we move on to the next part:

Copy Code
      // Convert raw to 2's complement, since temperature can be negative
      if ( raw_temp > 0x3FF ) {
        raw_temp |= 0xF800;
      }

      // Convert to float temperature value (Celsius)
      temp_c = (int16_t)raw_temp * 0.125;

      // Convert temperature to decimal format (without float conversion)
      temp_c *= 100;
      sprintf((char*)buf,
            "%u.%u C\r\n",
            ((unsigned int)temp_c / 100),
            ((unsigned int)temp_c % 100));

This is very similar to the math performed in the STM32 I2C tutorial example (with the TMP102). We expect an 11-bit, unsigned value from our ReadPCTTemperature() function, so we convert it to 2’s complement: if the value is over 0x3FF, we add 1’s to the first 5 bits and when we cast it as a signed int16_t value, the compiler will take care of computing the positive or negative value for us.

Then, we multiply that value by 0.125 to get the temperature in Celsius (as per the math found in the datasheet). After that, we use sprintf() to construct a string representing the temperature. Notice that we multiply the value by 100 to separate the decimal and fractional parts (this is an easier trick than enabling and relying on the ARM floating point module to perform the calculations for us).

Finally, we spit out the temperature:

Copy Code
      // Print temperature to console
      CDC_Transmit_FS((uint8_t*)buf, strlen(buf));

      // Turn LED on while writing to file
      HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
      fres = AppendToFile(log_path, strlen(log_path), buf, strlen(buf));
      HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);

      // If error writing to card, blink 3 times
      if ( fres != FR_OK) {
        BlinkLED(200, 3);
      }
    }

First, we call the CDC_Transmit_FS() function (part of the VCP framework that we enabled and generated) to send out the contents of our string buffer to the serial port on our computer.

Next, we call our own AppendToFile() function, which will write out the buffer to the end of the TEMPLOG.TXT file. Notice that we’re turning on the onboard LED just while writing the file. This will help us know when a write is happening; if you remove the card or remove power during this time, you risk corrupting the filesystem.

After that, we wait for 1 second before sampling again:

Copy Code
    // Wait before sampling again
    HAL_Delay(1000);

Under USER CODE BEGIN 4 in main.c, we can define our functions. First up:

Copy Code
// Read temperature from PCT2075
uint16_t ReadPCTTemperature(uint8_t i2c_addr) {

  HAL_StatusTypeDef ret;
  uint8_t buf2;
  uint16_t val;

  // Tell PCT2075 that we want to read from the temperature register
  buf0 = PCT_REG_TEMP;
  ret = HAL_I2C_Master_Transmit(&hi2c1, PCT_I2C_ADDR, buf, 1, I2C_DELAY);

  // If the I2C device has just been hot-plugged, reset the peripheral
  if ( ret == HAL_BUSY ) {
    if (HAL_I2C_DeInit(&hi2c1) != HAL_OK){
      Error_Handler();
    }
    MX_I2C1_Init();
  }

  // Throw error if communication not OK
  if ( ret != HAL_OK ) {
    return PCT_ERROR;
  }

  // Read 2 bytes from the temperature register
  ret = HAL_I2C_Master_Receive(&hi2c1, PCT_I2C_ADDR, buf, 2, I2C_DELAY);
  if ( ret != HAL_OK ) {
    return PCT_ERROR;
  }

  // Combine the bytes and return raw value
  val = ((uint16_t)buf0 << 3) | (buf1 >> 5);
  return val;
}

This should look very much like the code found in the previous STM32 I2C tutorial. The interesting part is after the HAL_I2C_Master_Transmit() function. I noticed that if you unplug the I2C sensor and plug it back in, you’ll get a HAL_BUSY response, and it doesn’t seem to go away. So, if we see that response, we simply de-initialize the I2C driver and then re-initialize it. 

After that, if we don’t see a HAL_OK response (which happens if there is no I2C device found at the given address or we just re-initialized the driver), then return and throw an error to the caller (which is the 0xFFFF we defined earlier).

Next, we have our function to append the temperature to the file:

Copy Code
// Append string to file given at path
FRESULT AppendToFile(char* path, size_t path_len, char* msg, size_t msg_len) {

  FATFS fs;
  FIL myFILE;
  UINT testByte;
  FRESULT stat;

  // Bounds check on strings
  if ( (pathpath_len != 0) || (msgmsg_len != 0) ) {
    return FR_INVALID_NAME;
  }

  // Re-initialize SD
  if ( BSP_SD_Init() != MSD_OK ) {
    return FR_NOT_READY;
  }

  // Re-initialize FATFS
  if ( FATFS_UnLinkDriver(SDPath) != 0 ) {
    return FR_NOT_READY;
  }
  if ( FATFS_LinkDriver(&SD_Driver, SDPath) != 0 ) {
    return FR_NOT_READY;
  }

  // Mount filesystem
  stat = f_mount(&fs, SDPath, 0);
  if (stat != FR_OK) {
    f_mount(0, SDPath, 0);
    return stat;
  }

  // Open file for appending
  stat = f_open(&myFILE, path, FA_WRITE | FA_OPEN_APPEND);
  if (stat != FR_OK) {
    f_mount(0, SDPath, 0);
    return stat;
  }

  // Write message to end of file
  stat = f_write(&myFILE, msg, msg_len, &testByte);
  if (stat != FR_OK) {
    f_mount(0, SDPath, 0);
    return stat;
  }

  // Sync, close file, unmount
  stat = f_close(&myFILE);
  f_mount(0, SDPath, 0);
  return stat;
}

The individual FATFS calls (f_mount, f_open, etc.) should be fairly well commented in the code above. The only trick there is the idea of unmounting the filesystem (with f_mount(0, SDPath, 0); ) before returning an error. Additionally, f_close() is supposed to automatically perform a sync operation before closing, which is why there is no explicit call to f_sync(). This will make sure that the file on the card is fully written to before returning.

Other than that, the real trick is that I re-initialize the SD card driver and FATFS as well as re-mount the filesystem every time this function is called. Yes, it’s much slower, but it’s also more robust. If you remove the SD card and plug it back in, the code can recover from it. In other words, it allows for hot-plugging the SD card.

The better way to do this would be to create an interrupt that triggers whenever the card detect line changes to automatically de-initialize and re-initialize the driver and filesystem. Try as I might, I could not get the card detect line to trigger properly on SD card insertion. You might have better luck than me.

Finally, we have our blink function:

Copy Code
// Blink onboard LED
void BlinkLED(uint32_t blink_delay, uint8_t num_blinks) {
  for ( int i = 0; i < num_blinks; i++ ) {
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
    HAL_Delay(blink_delay);
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
    HAL_Delay(blink_delay);
  }
}

This just toggles the LED pin a given number of times with a given delay. It allows us to easily create blinking status codes to alert the user that something happened (e.g. the program could not write to the SD card). Notice that we’re using the LED label we assigned to pin PC1 here: LED_GPIO_Port and LED_Pin.

Enable Bootloader Mode

Most STM32 microcontrollers come with a built-in bootloader. To enable it, we need to tie the BOOT0 pin to 3.3V and reset the device. Use a wire (or a button, if you have the Feather board on a breadboard) to connect the B0 pin to 3.3V. With it connected, press the RESET button. The STM32 should enumerate as a new USB device on your computer.

Putting STM32F405 into DFU bootloader mode

Once in bootloader mode, you can remove the wire or disconnect B0 from 3.3V. It will stay in bootloader mode until the next reset.

Windows Only: Install DFU driver

If you installed the STM32CubeIDE and accepted all the defaults, you should not need to install any drivers. Just in case, open your Device Manager (with your Feather board in bootloader mode and connected to your computer) and check that the STM32 enumerated as STM32 BOOTLOADER

Install driver

If you do not see this (Windows will likely list the STM32 as an unknown device), you will need to install the WinUSB driver. Doing that requires slight hacking of Windows and forcing a driver down its throat. Here’s your Warning: Be very careful! You could easily overwrite drivers for other devices, potentially causing irreversible harm to your operating system.

Download and install the Zadig software from https://zadig.akeo.ie/

Run Zadig. Under Options, select List All Devices. Find and select the STM32 bootloader device (yours might show something different, as mine, listed as “STM32 BOOTLOADER,” already has a proper driver installed). Make sure that the driver to the right of the arrow has an updated WinUSB listed. Click Upgrade Driver and wait while the driver installs.

Run Zadig to install driver

Close out of Zadig.

Upload and Run It!

To do step-through debugging, you’ll need to solder some wires to the SWD pins onto the test points on the back of the board. Check out the video if you’d like to see an overview of that.

In lieu of real debugging, we can use the built-in bootloader and dfu-util to upload our program to the STM32. In the IDE, right-click on the project and select Properties. Go to C/C++ Build > Settings > Tool Settings > MCU Post build outputs. Click to enable Convert to binary file (-O binary). This will cause the IDE to generate an additional .bin file with our code, which we can upload using the bootloader.

Select Project > Build Project to compile.

Next, we need to install dfu-util. Head to http://dfu-util.sourceforge.net/ and go to the Releases page. Download the most recent dfu-util for your operating system and unzip/install it. I unzipped mine in the directory C:\VSARM\dfu-util.

Open a file browser, and navigate to your project directory. Go into the Debug folder and copy the .bin file that you find there. Navigate to your dfu-util folder (or some other place easily accessible) and paste it there. This will make calling the next command much easier.

Copy bin file

Open a command window and navigate to the dfu-util directory. Enter the following to list out the DFU bootloader devices that are connected to your computer:

Copy Code
dfu-util -l

You should see a few, but the important one is the Alt 0 device, which points to the internal flash of our STM32.

 Enter the following command (replace the <MY_PROGRAM>.bin filename with your .bin file):

Copy Code
dfu-util -a 0 -s 0x08000000:leave -D <MY_PROGRAM>.bin

The -a 0 refers to the Alt device (which we found that Alt 0 is the internal flash where we want to send our program). The -s option allows us to specify where we want our program to reside in memory. By default, the STM32 devices put user applications starting at 0x0800000. The “leave” parameter tells the STM32 that it should exit bootloader mode and start running our program immediately after flashing. Finally, -D specifies the file we want to send to the STM32.

Press enter, and dfu-util should upload the program to your device.

Uploading via dfu-util

With any luck, your device should start collecting data! The red LED should flash briefly every second to let you know that it’s working (and writing data to the SD card).

LED blinking to show working temperature logging

View Results

With the Feather board plugged into your computer, find which COM (or serial) port is associated with the device. Open your favorite serial terminal program and connect to it. Note that VCP is auto-baud, so any baud rate will do (I used 115200). You should see the temperature in Celsius being reported every second.

USB VCP reporting temperature

Try lightly touching the temperature sensor to see if the reported numbers increase!

When you are done, remove the SD card and plug it into your computer. Navigate to the associated drive and open TEMPLOG.TXT. You should see the same numbers reported there!

Viewing temperature log

Going Further

That’s it for the temperature logger! I hope this gives you a good starting place for using the Adafruit Feather STM32F405 Express with STM32CubeIDE (if you fancy using C or C++).

Here are some resources that might help you:

Happy hacking!

Key Parts and Components

Add all Digi-Key Parts to Cart
  • 1528-4369-ND
  • 1528-4382-ND
  • P17027-ND
  • Q1238-ND
  • 1528-4210-ND
  • 1528-1837-ND