This is the Series of tutorials on the STM32 Microcontroller. The aim of this series is to provide easy and practical examples that anyone can understand. In this article, we are going to Interface the SD card with STM32 (STM32 SD Card) using SPI communication. In this demo, we have used the STM32F103C8T6 Blue Pill board. You can also try this method with other STM32 boards.
Hardware Required
STM32 SD Card Interfacing
We have assumed that you have installed the STM32CubeIDE and know how to use that IDE. If you don’t know also fine. You can follow the below procedure. You can also see the video explanation here.
Connection Diagram
The SD Card is connected to the STM32 through SPI. If you want to learn about SPI, you can check this tutorial. The SD card connection is given below.
Note: Make sure you are giving 5V to the Vcc of the SD card reader. If you give lesser than that, it won’t work.
SD Card Connection (STM32 SPI)
SD Card Reader | STM32F103C8T6 |
VCC | 5V |
GND | GND |
CS | PB12 (Port B 12) |
SCK | PB13 (Port B 13) |
MISO | PB14 (Port B 14) |
MOSI | PB15 (Port B 15) |
UART Connection
USB to Serial Converter | STM32F103C8T6 |
TX | PA10 (Port A 10) |
RX | PA9 (Port A 9) |
You can also refer to the below image.
Project Creation
Create the new project in STM32CubeIDE. In the .ioc file pinout and configuration, click Connectivity → SPI 2. Select the Mode as Full-Duplex Master. Then click PB12 in the Pinout View and set that as a GPIO_Output. This GPIO will be used for Chip Select (CS).
Now we will enable the FATFS. Click Middleware → FATFS. Then tick User-defined. In the FATFS Configuration, set USE_LFN as Enabled with static working buffer on the BSS to use the long file name. Then set the MAX_SS as 4096.
That’s all. Now save and generate the code. You can see the video explanation here if you are confused.
Source Code
Now we have the auto-generated code. We will add our code on top of this auto-generated code. You can get the complete code of this example project on GitHub.
Create a new header file named fatfs_sd_card.h
under the Core/Inc. Then copy and paste the below code to that file.
#ifndef __FATFS_SD_H #define __FATFS_SD_H /* Definitions for MMC/SDC command */ #define CMD0 (0x40+0) /* GO_IDLE_STATE */ #define CMD1 (0x40+1) /* SEND_OP_COND */ #define CMD8 (0x40+8) /* SEND_IF_COND */ #define CMD9 (0x40+9) /* SEND_CSD */ #define CMD10 (0x40+10) /* SEND_CID */ #define CMD12 (0x40+12) /* STOP_TRANSMISSION */ #define CMD16 (0x40+16) /* SET_BLOCKLEN */ #define CMD17 (0x40+17) /* READ_SINGLE_BLOCK */ #define CMD18 (0x40+18) /* READ_MULTIPLE_BLOCK */ #define CMD23 (0x40+23) /* SET_BLOCK_COUNT */ #define CMD24 (0x40+24) /* WRITE_BLOCK */ #define CMD25 (0x40+25) /* WRITE_MULTIPLE_BLOCK */ #define CMD41 (0x40+41) /* SEND_OP_COND (ACMD) */ #define CMD55 (0x40+55) /* APP_CMD */ #define CMD58 (0x40+58) /* READ_OCR */ /* MMC card type flags (MMC_GET_TYPE) */ #define CT_MMC 0x01 /* MMC ver 3 */ #define CT_SD1 0x02 /* SD ver 1 */ #define CT_SD2 0x04 /* SD ver 2 */ #define CT_SDC 0x06 /* SD */ #define CT_BLOCK 0x08 /* Block addressing */ /* Functions */ DSTATUS SD_disk_initialize (BYTE pdrv); DSTATUS SD_disk_status (BYTE pdrv); DRESULT SD_disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count); DRESULT SD_disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count); DRESULT SD_disk_ioctl (BYTE pdrv, BYTE cmd, void* buff); #define SPI_TIMEOUT 100 extern SPI_HandleTypeDef hspi2; #define HSPI_SDCARD &hspi2 #define SD_CS_PORT GPIOB #define SD_CS_PIN GPIO_PIN_12 #endif
Create a new header file named fatfs_sd_card.c
under the Core/Src. Then copy and paste the below code to that file.
#define TRUE 1 #define FALSE 0 #define bool BYTE #include "stm32f1xx_hal.h" #include "diskio.h" #include <fatfs_sd_card.h> uint16_t Timer1, Timer2; /* 1ms Timer Counter */ static volatile DSTATUS Stat = STA_NOINIT; /* Disk Status */ static uint8_t CardType; /* Type 0:MMC, 1:SDC, 2:Block addressing */ static uint8_t PowerFlag = 0; /* Power flag */ /*************************************** * SPI functions **************************************/ /* slave select */ static void SELECT(void) { HAL_GPIO_WritePin(SD_CS_PORT, SD_CS_PIN, GPIO_PIN_RESET); HAL_Delay(1); } /* slave deselect */ static void DESELECT(void) { HAL_GPIO_WritePin(SD_CS_PORT, SD_CS_PIN, GPIO_PIN_SET); HAL_Delay(1); } /* SPI transmit a byte */ static void SPI_TxByte(uint8_t data) { while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE)); HAL_SPI_Transmit(HSPI_SDCARD, &data, 1, SPI_TIMEOUT); } /* SPI transmit buffer */ static void SPI_TxBuffer(uint8_t *buffer, uint16_t len) { while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE)); HAL_SPI_Transmit(HSPI_SDCARD, buffer, len, SPI_TIMEOUT); } /* SPI receive a byte */ static uint8_t SPI_RxByte(void) { uint8_t dummy, data; dummy = 0xFF; while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE)); HAL_SPI_TransmitReceive(HSPI_SDCARD, &dummy, &data, 1, SPI_TIMEOUT); return data; } /* SPI receive a byte via pointer */ static void SPI_RxBytePtr(uint8_t *buff) { *buff = SPI_RxByte(); } /*************************************** * SD functions **************************************/ /* wait SD ready */ static uint8_t SD_ReadyWait(void) { uint8_t res; /* timeout 500ms */ Timer2 = 500; /* if SD goes ready, receives 0xFF */ do { res = SPI_RxByte(); } while ((res != 0xFF) && Timer2); return res; } /* power on */ static void SD_PowerOn(void) { uint8_t args[6]; uint32_t cnt = 0x1FFF; /* transmit bytes to wake up */ DESELECT(); for(int i = 0; i < 10; i++) { SPI_TxByte(0xFF); } /* slave select */ SELECT(); /* make idle state */ args[0] = CMD0; /* CMD0:GO_IDLE_STATE */ args[1] = 0; args[2] = 0; args[3] = 0; args[4] = 0; args[5] = 0x95; /* CRC */ SPI_TxBuffer(args, sizeof(args)); /* wait response */ while ((SPI_RxByte() != 0x01) && cnt) { cnt--; } DESELECT(); SPI_TxByte(0XFF); PowerFlag = 1; } /* power off */ static void SD_PowerOff(void) { PowerFlag = 0; } /* check power flag */ static uint8_t SD_CheckPower(void) { return PowerFlag; } /* receive data block */ static bool SD_RxDataBlock(BYTE *buff, UINT len) { uint8_t token; /* timeout 200ms */ Timer1 = 200; /* loop until receive a response or timeout */ do { token = SPI_RxByte(); } while((token == 0xFF) && Timer1); /* invalid response */ if(token != 0xFE) return FALSE; /* receive data */ do { SPI_RxBytePtr(buff++); } while(len--); /* discard CRC */ SPI_RxByte(); SPI_RxByte(); return TRUE; } /* transmit data block */ #if _USE_WRITE == 1 static bool SD_TxDataBlock(const uint8_t *buff, BYTE token) { uint8_t resp; uint8_t i = 0; /* wait SD ready */ if (SD_ReadyWait() != 0xFF) return FALSE; /* transmit token */ SPI_TxByte(token); /* if it's not STOP token, transmit data */ if (token != 0xFD) { SPI_TxBuffer((uint8_t*)buff, 512); /* discard CRC */ SPI_RxByte(); SPI_RxByte(); /* receive response */ while (i <= 64) { resp = SPI_RxByte(); /* transmit 0x05 accepted */ if ((resp & 0x1F) == 0x05) break; i++; } /* recv buffer clear */ while (SPI_RxByte() == 0); } /* transmit 0x05 accepted */ if ((resp & 0x1F) == 0x05) return TRUE; return FALSE; } #endif /* _USE_WRITE */ /* transmit command */ static BYTE SD_SendCmd(BYTE cmd, uint32_t arg) { uint8_t crc, res; /* wait SD ready */ if (SD_ReadyWait() != 0xFF) return 0xFF; /* transmit command */ SPI_TxByte(cmd); /* Command */ SPI_TxByte((uint8_t)(arg >> 24)); /* Argument[31..24] */ SPI_TxByte((uint8_t)(arg >> 16)); /* Argument[23..16] */ SPI_TxByte((uint8_t)(arg >> 8)); /* Argument[15..8] */ SPI_TxByte((uint8_t)arg); /* Argument[7..0] */ /* prepare CRC */ if(cmd == CMD0) crc = 0x95; /* CRC for CMD0(0) */ else if(cmd == CMD8) crc = 0x87; /* CRC for CMD8(0x1AA) */ else crc = 1; /* transmit CRC */ SPI_TxByte(crc); /* Skip a stuff byte when STOP_TRANSMISSION */ if (cmd == CMD12) SPI_RxByte(); /* receive response */ uint8_t n = 10; do { res = SPI_RxByte(); } while ((res & 0x80) && --n); return res; } /*************************************** * user_diskio.c functions **************************************/ /* initialize SD */ DSTATUS SD_disk_initialize(BYTE drv) { uint8_t n, type, ocr[4]; /* single drive, drv should be 0 */ if(drv) return STA_NOINIT; /* no disk */ if(Stat & STA_NODISK) return Stat; /* power on */ SD_PowerOn(); /* slave select */ SELECT(); /* check disk type */ type = 0; /* send GO_IDLE_STATE command */ if (SD_SendCmd(CMD0, 0) == 1) { /* timeout 1 sec */ Timer1 = 1000; /* SDC V2+ accept CMD8 command, http://elm-chan.org/docs/mmc/mmc_e.html */ if (SD_SendCmd(CMD8, 0x1AA) == 1) { /* operation condition register */ for (n = 0; n < 4; n++) { ocr[n] = SPI_RxByte(); } /* voltage range 2.7-3.6V */ if (ocr[2] == 0x01 && ocr[3] == 0xAA) { /* ACMD41 with HCS bit */ do { if (SD_SendCmd(CMD55, 0) <= 1 && SD_SendCmd(CMD41, 1UL << 30) == 0) break; } while (Timer1); /* READ_OCR */ if (Timer1 && SD_SendCmd(CMD58, 0) == 0) { /* Check CCS bit */ for (n = 0; n < 4; n++) { ocr[n] = SPI_RxByte(); } /* SDv2 (HC or SC) */ type = (ocr[0] & 0x40) ? CT_SD2 | CT_BLOCK : CT_SD2; } } } else { /* SDC V1 or MMC */ type = (SD_SendCmd(CMD55, 0) <= 1 && SD_SendCmd(CMD41, 0) <= 1) ? CT_SD1 : CT_MMC; do { if (type == CT_SD1) { if (SD_SendCmd(CMD55, 0) <= 1 && SD_SendCmd(CMD41, 0) == 0) break; /* ACMD41 */ } else { if (SD_SendCmd(CMD1, 0) == 0) break; /* CMD1 */ } } while (Timer1); /* SET_BLOCKLEN */ if (!Timer1 || SD_SendCmd(CMD16, 512) != 0) type = 0; } } CardType = type; /* Idle */ DESELECT(); SPI_RxByte(); /* Clear STA_NOINIT */ if (type) { Stat &= ~STA_NOINIT; } else { /* Initialization failed */ SD_PowerOff(); } return Stat; } /* return disk status */ DSTATUS SD_disk_status(BYTE drv) { if (drv) return STA_NOINIT; return Stat; } /* read sector */ DRESULT SD_disk_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { /* pdrv should be 0 */ if (pdrv || !count) return RES_PARERR; /* no disk */ if (Stat & STA_NOINIT) return RES_NOTRDY; /* convert to byte address */ if (!(CardType & CT_SD2)) sector *= 512; SELECT(); if (count == 1) { /* READ_SINGLE_BLOCK */ if ((SD_SendCmd(CMD17, sector) == 0) && SD_RxDataBlock(buff, 512)) count = 0; } else { /* READ_MULTIPLE_BLOCK */ if (SD_SendCmd(CMD18, sector) == 0) { do { if (!SD_RxDataBlock(buff, 512)) break; buff += 512; } while (--count); /* STOP_TRANSMISSION */ SD_SendCmd(CMD12, 0); } } /* Idle */ DESELECT(); SPI_RxByte(); return count ? RES_ERROR : RES_OK; } /* write sector */ #if _USE_WRITE == 1 DRESULT SD_disk_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { /* pdrv should be 0 */ if (pdrv || !count) return RES_PARERR; /* no disk */ if (Stat & STA_NOINIT) return RES_NOTRDY; /* write protection */ if (Stat & STA_PROTECT) return RES_WRPRT; /* convert to byte address */ if (!(CardType & CT_SD2)) sector *= 512; SELECT(); if (count == 1) { /* WRITE_BLOCK */ if ((SD_SendCmd(CMD24, sector) == 0) && SD_TxDataBlock(buff, 0xFE)) count = 0; } else { /* WRITE_MULTIPLE_BLOCK */ if (CardType & CT_SD1) { SD_SendCmd(CMD55, 0); SD_SendCmd(CMD23, count); /* ACMD23 */ } if (SD_SendCmd(CMD25, sector) == 0) { do { if(!SD_TxDataBlock(buff, 0xFC)) break; buff += 512; } while (--count); /* STOP_TRAN token */ if(!SD_TxDataBlock(0, 0xFD)) { count = 1; } } } /* Idle */ DESELECT(); SPI_RxByte(); return count ? RES_ERROR : RES_OK; } #endif /* _USE_WRITE */ /* ioctl */ DRESULT SD_disk_ioctl(BYTE drv, BYTE ctrl, void *buff) { DRESULT res; uint8_t n, csd[16], *ptr = buff; WORD csize; /* pdrv should be 0 */ if (drv) return RES_PARERR; res = RES_ERROR; if (ctrl == CTRL_POWER) { switch (*ptr) { case 0: SD_PowerOff(); /* Power Off */ res = RES_OK; break; case 1: SD_PowerOn(); /* Power On */ res = RES_OK; break; case 2: *(ptr + 1) = SD_CheckPower(); res = RES_OK; /* Power Check */ break; default: res = RES_PARERR; } } else { /* no disk */ if (Stat & STA_NOINIT) return RES_NOTRDY; SELECT(); switch (ctrl) { case GET_SECTOR_COUNT: /* SEND_CSD */ if ((SD_SendCmd(CMD9, 0) == 0) && SD_RxDataBlock(csd, 16)) { if ((csd[0] >> 6) == 1) { /* SDC V2 */ csize = csd[9] + ((WORD) csd[8] << 8) + 1; *(DWORD*) buff = (DWORD) csize << 10; } else { /* MMC or SDC V1 */ n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2; csize = (csd[8] >> 6) + ((WORD) csd[7] << 2) + ((WORD) (csd[6] & 3) << 10) + 1; *(DWORD*) buff = (DWORD) csize << (n - 9); } res = RES_OK; } break; case GET_SECTOR_SIZE: *(WORD*) buff = 512; res = RES_OK; break; case CTRL_SYNC: if (SD_ReadyWait() == 0xFF) res = RES_OK; break; case MMC_GET_CSD: /* SEND_CSD */ if (SD_SendCmd(CMD9, 0) == 0 && SD_RxDataBlock(ptr, 16)) res = RES_OK; break; case MMC_GET_CID: /* SEND_CID */ if (SD_SendCmd(CMD10, 0) == 0 && SD_RxDataBlock(ptr, 16)) res = RES_OK; break; case MMC_GET_OCR: /* READ_OCR */ if (SD_SendCmd(CMD58, 0) == 0) { for (n = 0; n < 4; n++) { *ptr++ = SPI_RxByte(); } res = RES_OK; } default: res = RES_PARERR; } DESELECT(); SPI_RxByte(); } return res; }
We have added the new files. Now we will modify the existing code. Open the Core/Src/stm32f1xx_it.c file. Add the below line between “USER CODE BEGIN PV” and “USER CODE END PV“.
extern uint16_t Timer1, Timer2;
Then, in the SysTick_Handler
function, add the below code between the USER CODE BEGIN SysTick_IRQn 0 and USER CODE END SysTick_IRQn 0.
if(Timer1 > 0) Timer1--; if(Timer2 > 0) Timer2--;
So the final code will be like the below.
/** * @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 */ }
Now, open the FATFS/Target/user_diskio.c
.
Include the fatfs_sd_card.h (#include <fatfs_sd_card.h>
)
In that, add the below line to the function USER_initialize
.
return SD_disk_initialize(pdrv);
So the function looks like the one below.
/** * @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 */ }
Then add the below line to the function USER_status
.
return SD_disk_status(pdrv);
The USER_status
should look like the below.
/** * @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 */ }
Then add the below line to the function USER_read
.
return SD_disk_read(pdrv, buff, sector, count);
The USER_read
should look like the below.
/** * @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 */ }
Then add the below line to the function USER_write
.
return SD_disk_write(pdrv, buff, sector, count);
The USER_write
should look like the below.
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 */ }
Finally, add the below line to the function USER_ioctl
.
return SD_disk_ioctl(pdrv, cmd, buff);
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 */ }
You can use the below function for printf. So, whenever you call printf, it will send you the print messages through UART1.
#include <stdio.h> #include <string.h> #ifdef __GNUC__ /* With GCC, small printf (option LD Linker->Libraries->Small printf set to 'Yes') calls __io_putchar() */ int __io_putchar(int ch) #else int fputc(int ch, FILE *f) #endif /* __GNUC__ */ { /* Place your implementation of fputc here */ /* e.g. write a character to the UART3 and Loop until the end of transmission */ HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }
All are set. We can use the fatfs file system in our code.
STM32 SD Card Example
In the below example, We are doing the below process.
- Mount the SD Card
- Check the SD Card Capacity and Free size
- Create a new file
- Write the data into the file
- Close the file
- Open the file again
- Read the data from that file
- Close the file
- Delete the file (If you want)
- Unmount the SD Card
void process_SD_card( void ) { FATFS FatFs; //Fatfs handle FIL fil; //File handle FRESULT fres; //Result after operations char buf[100]; do { //Mount the SD Card fres = f_mount(&FatFs, "", 1); //1=mount now if (fres != FR_OK) { printf("No SD Card found : (%i)\r\n", fres); break; } printf("SD Card Mounted Successfully!!!\r\n"); //Read the SD Card Total size and Free Size FATFS *pfs; DWORD fre_clust; uint32_t totalSpace, freeSpace; f_getfree("", &fre_clust, &pfs); totalSpace = (uint32_t)((pfs->n_fatent - 2) * pfs->csize * 0.5); freeSpace = (uint32_t)(fre_clust * pfs->csize * 0.5); printf("TotalSpace : %lu bytes, FreeSpace = %lu bytes\n", totalSpace, freeSpace); //Open the file fres = f_open(&fil, "EmbeTronicX.txt", FA_WRITE | FA_READ | FA_CREATE_ALWAYS); if(fres != FR_OK) { printf("File creation/open Error : (%i)\r\n", fres); break; } printf("Writing data!!!\r\n"); //write the data f_puts("Welcome to EmbeTronicX", &fil); //close your file f_close(&fil); //Open the file fres = f_open(&fil, "EmbeTronicX.txt", FA_READ); if(fres != FR_OK) { printf("File opening Error : (%i)\r\n", fres); break; } //read the data f_gets(buf, sizeof(buf), &fil); printf("Read Data : %s\n", buf); //close your file f_close(&fil); printf("Closing File!!!\r\n"); #if 0 //Delete the file. fres = f_unlink(EmbeTronicX.txt); if (fres != FR_OK) { printf("Cannot able to delete the file\n"); } #endif } while( false ); //We're done, so de-mount the drive f_mount(NULL, "", 0); printf("SD Card Unmounted Successfully!!!\r\n"); }
Flash the Code
Flash the code using ST-Link Programmer or using System bootloader through UART. You can get the complete code of this example project on GitHub.
Demo
Insert the SD Card in the Laptop or computer and format it in FAT32 and the Sector size as 4096. Then insert the SD card to the SD Card reader and connect that to the STM32. Then press the reset button. You should see the new file created in the SD card called “EmbeTronicX.txt“. The video demo has been given below. The below debug prints also should come.
SD Card Mounted Successfully!!! Total Space : 3674112Bytes, Free Space = 3674092Bytes Writing data!!! Read Data : Welcome to EmbeTronicX Closing File!!! SD Card Unmounted Successfully!!!
Video Explanation
Reference: STM32 SD card library is taken from github.com/eziya/STM32_SPI_SDCARD
In our next tutorial, we will see the STM32 Ethernet Example (HTTP server).
You can also read the below tutorials.

Embedded Software | Firmware | Linux Devic Deriver | RTOS
Hi, I’m SLR. I am a tech blogger and an Embedded Engineer. I am always eager to learn and explore tech-related concepts. And also, I wanted to share my knowledge with everyone in a more straightforward way with easy practical examples. I strongly believe that learning by doing is more powerful than just learning by reading. I love to do experiments. If you want to help or support me on my journey, consider sharing my articles, or Buy me a Coffee! Thank you for reading my blog! Happy learning!