STM32: Работа с SD-картами через SDIO


Когда устройству нужна относительно большая энергонезависимая память с возможностью быстрой замены, SD или microSD-карта — один из самых удобных и доступных вариантов.
Компактность, простое подключение, поддержка микроконтроллерами и низкая цена при большом объёме делают SD-карту стандартом для логирования, хранения конфигураций и обновления прошивок.
Многие считают работу с SD-картой чем-то сложным и низкоуровневым. На практике, если использовать SDIO + FatFS, бо́льшая часть сложности уже спрятана библиотеками и аппаратными модулями микроконтроллера.
В этой статье я покажу, как добавить поддержку SD-карт в проекты на STM32: что лежит в основе, и как она настраивается.
Цель статьи — не исчерпывающее руководство, а готовый понятный прототип, который можно взять за основу в реальных проектах и для своих экспериментов.
Полный исходный код примера из статьи доступен в моём GitHub репозитории
Как микроконтроллер работает с SD-картой
Микроконтроллер для работы с SD-картой может использовать один из двух физических интерфейсов: SDIO или SPI.
Перед началом работы с SD-картой её необходимо проинициализировать. Для нативного режима (SDIO) часть микроконтроллеров STM32 имеет встроенные аппаратные блоки, которые берут часть рутины по инициализации на себя.
Для режима SPI всю последовательность команд, контроль линий CS и разбор байтовых ответов программисту приходится прописывать вручную или использовать библиотеки.
Если не рассматривать ранние версии стандарта, SD-карты являются блочными устройствами. В них используется LBA-адресация, как у жёстких и оптических дисках.
SD-картой можно работать и без файловой системы, но это скорее частные случаи. Карту можно разбить на несколько разделов и поставить различные файловые системы. Но обычно на карте или один раздел с файловой системой FAT16, FAT32, exFAT в зависимости от размера карты.
Для работы с FAT использование библиотеки FatFS, можно сказать, является стандартом. Особенностью FatFS является то, что код библиотека не зависит от физического устройства, на котором располагается файловая система. Нужно написать несколько функций, которые определяют, как считать информацию с устройства, как записать и ещё несколько других важных операций.
Подключение SD-карты
Я использовал отладочную плату WeAct Studio на базе микроконтроллера STM32H750VBT6. У платы распаян microSD-слот, и все необходимые пины подтянуты к питанию, а микроконтроллер поддерживает аппаратный SDIO/SDMMC.
Если у микроконтроллера есть SDIO/SDMMC и вам не критично количество пинов — используйте SDIO. Это действительно почти так же просто, как работать с USB или Ethernet в STM32.
SPI имеет смысл использовать, когда:
нет свободного SDIO,
нужно использовать произвольные пины,
хотите сэкономить выводы,
делаете очень простой/дешёвый проект.
Если у вас какая-то другая плата, и вы будете использовать внешний модуль SD-карты, нужно убедиться, что разведены все пины для SDIO интерфейса и подтянуты к питанию пины (существуют модули microSD, которые можно использовать только в режиме SPI или без подтягивающих резисторов).
Мне известны следующие модули для microSD-карт:
Hw-203,
Hw-125,
Adafruit 4682,
Micro SD(TF) Storage Board.
Для работы в режиме SDIO можно только с последними двумя.
Для большинства SD-карт и логические уровни 3,3 В. Это необходимо помнить, чтобы не повредить карту.
Фактически карты поддерживают диапазон сигнальных напряжений от 2,7 В до 3,6. Также в современных высокоскоростных стандартах используется более низкое напряжение.
SD-Карты могут быть разного форм фактора: SD, MicroSD, MiniSD.
Иметь разную адресацию, побайтовую и LBA.
Разные классы скорости и производительности.
Также существуют различные слоты для SD/microSD карт: с механическим контактом для определения наличия карты и без, подпружиненные и нет.
В статье рассматривается работа с самой распространённой картой и которая была у меня — microSD SDHC с классом скорости Class 10. В качестве слота использовался тот, что на моей отладочной плате.
Настройка STM32CubeMX
Настроить и сгенерировать шаблонный код для работы с SD-картой удобно в STM32CubeMX.
Не все микроконтроллеры STM32 поддерживают аппаратный SDIO. Читая спецификацию конкретного микроконтроллера, можно узнать, поддерживает он или нет. В более современных версиях микроконтроллеров она называется SDMMC.
Запускаем STM32CubeMX.
Выбираем наш микроконтроллер (STM32H750VBT6). Нажимаем
Start Project.Соглашаемся в появившемся окне с вопросом о предконфигурировании MPU.
Включаем SDMMC1, выбрав в выпадающем списке
ModeзначениеSD 4 bits Wide busилиSD 1 bit. Это значение определяет, сколько линий интерфейса будет использоваться для обмена данными. Для моей платы можно выбрать любое из этих двух. При использовании одной линии обмен будет медленнее.
На вкладке
Parameter SettingsвключаемSDMMC hardware flow controlи устанавливаем значение дляSDMMC Clock Divide Factorравным2. Если это не сделать, карта на моей плате не инициализировалась.
Подключаем FatFS. Ставим флажок
SD Card
На вкладке
Platform Settingsнам предлагается указать пин, который будет использоваться для детектирования того, что SD-карта вставлена в слот:
У меня на отладочной плате была не закорочена перемычка
SW2, отвечающая за подключение пина слота SD-карты к микроконтроллеру. Поэтому я соединил контакты перемычки припоем.Кликаем на пине PD4 и выбираем GPIO_Input.

Для FatFs на вкладке
Advanced SettingsвыбираемPD4дляDetect_SDIO.
Настраиваем диагностический вывод (я использовал Semihosting, так как он в режиме отладки будет работать везде). Выбираем
Debug. Для поляDebugвыбираемSerial Wire.
Переходим на вкладку
Clock Configurationи соглашаемся на разрешения конфликтов для тактовых частот.
На вкладке
Project Managerвводим имя проекта в полеProject Nameи выбираемCMakeв полеToolchain/IDE
На вкладке
Code Generatorустанавливаем флажокGenerate periferal initialization as a pair of '.c/.h' per peripheral
Генерируем проект.
Структура проекта следующая:
.
├── cmake
│ ├── gcc-arm-none-eabi.cmake
│ ├── starm-clang.cmake
│ └── stm32cubemx
│ └── CMakeLists.txt
├── CMakeLists.txt
├── CMakePresets.json
├── Core
│ ├── Inc
│ │ ├── gpio.h
│ │ ├── main.h
│ │ ├── sdmmc.h
│ │ ├── stm32h7xx_hal_conf.h
│ │ └── stm32h7xx_it.h
│ └── Src
│ ├── gpio.c
│ ├── main.c
│ ├── sdmmc.c
│ ├── stm32h7xx_hal_msp.c
│ ├── stm32h7xx_it.c
│ ├── syscalls.c
│ ├── sysmem.c
│ └── system_stm32h7xx.c
├── Drivers
│ ├── CMSIS
│ │ ├── Core
│ │ ├── Core_A
│ │ ├── DAP
│ │ ├── Device
│ │ ├── DSP
│ │ ├── Include
│ │ ├── LICENSE.txt
│ │ ├── NN
│ │ ├── RTOS
│ │ ├── RTOS2
│ │ └── st_readme.txt
│ └── STM32H7xx_HAL_Driver
│ ├── Inc
│ ├── LICENSE.txt
│ └── Src
├── FATFS
│ ├── App
│ │ ├── fatfs.c
│ │ └── fatfs.h
│ └── Target
│ ├── bsp_driver_sd.c
│ ├── bsp_driver_sd.h
│ ├── fatfs_platform.c
│ ├── fatfs_platform.h
│ ├── ffconf.h
│ ├── sd_diskio.c
│ └── sd_diskio.h
├── Middlewares
│ └── Third_Party
│ └── FatFs
├── startup_stm32h750xx.s
├── STM32H750xx_FLASH.ld
├── stm32-sdio-sample.ioc
└── User
├── utils.c
└── utils.h
В директории Core находится основной сгенерированный код. Drivers содержит CMSIS и HAL библиотеки для конкретных микроконтроллеров STM32, в нашем случае STM32H7xx. В директории FATFS находится сгенерированный код, который позволяет работать FatFs с SD-картой. Сама библиотека FatFs находится здесь Midleware/Third_Party/FatFs.
Файл stm32-sdio-sample.ioc содержит настройки, на основании которых STM32CubeMX генерирует код проекта на C.
Код, который я написал сам, я помещаю в директорию User. Чтобы этот код был виден в проекте, директорию и .c файлы необходимо подключить в файле CMakeLists.txt.
# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add user sources here
User/utils.c
)
# Add include paths
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
# Add user defined include paths
User
)
Код начальной загрузки (startup_stm32h750xx.s), скрипт для компоновщика (STM32H750xx_FLASH.ld), наверное, интересны для изучения, но не в этой статье.
Работу с SD-картой можно сделать, если настроить DMA. Но из-за того, что я использовал STMH7 и STM32CubeMX генерирует немного некорректный код, в этой статье DMA рассматривать не будем.
Сгенерированный проект нужно сымпортировать в STM32CubeIDE и настроить semihosting.
Настройка semihosting
Настраиваем semihosting, который позволит нам увидеть вывод функции printf в консоли отладчика.
В файле
main.cдобавляем:/* USER CODE BEGIN PFP */ extern void initialise_monitor_handles(void); /* USER CODE END PFP */ int main(void) { /* USER CODE BEGIN 1 */ // Инициализация semihosting должна быть вызвана до любого ввода-вывода initialise_monitor_handles(); /* USER CODE END 1 */В файле
CMakeLists.txt:Исключаем файл
Core/Src/syscalls.cиз компиляции:set_source_files_properties(Core/Src/syscalls.c PROPERTIES HEADER_FILE_ONLY TRUE)Настраиваем компоновщик:
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE --specs=rdimon.specs )# Add linked libraries target_link_libraries(${CMAKE_PROJECT_NAME} stm32cubemx # Add user defined libraries rdimon )В конфигурации отладки для проекта в STM32CubeIDE не забываем включить semihosting.
Работа с FatFS
Бо́льшую часть работы по инициализации SD-карты выполняет аппаратный блок SDMMC микроконтроллера STM32. A STM32CubeMX генерирует немного кода, который инициализирует блок SDMMC.
void MX_SDMMC1_SD_Init(void)
{
/* USER CODE BEGIN SDMMC1_Init 0 */
/* USER CODE END SDMMC1_Init 0 */
/* USER CODE BEGIN SDMMC1_Init 1 */
/* USER CODE END SDMMC1_Init 1 */
hsd1.Instance = SDMMC1;
hsd1.Init.ClockEdge = SDMMC_CLOCK_EDGE_RISING;
hsd1.Init.ClockPowerSave = SDMMC_CLOCK_POWER_SAVE_ENABLE;
hsd1.Init.BusWide = SDMMC_BUS_WIDE_4B;
hsd1.Init.HardwareFlowControl = SDMMC_HARDWARE_FLOW_CONTROL_ENABLE;
hsd1.Init.ClockDiv = 2;
if (HAL_SD_Init(&hsd1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SDMMC1_Init 2 */
/* USER CODE END SDMMC1_Init 2 */
}
SD-карта может содержать разделы с файловыми системами, а может быть без таблицы разделов (супердискета). FatFs поддерживает оба случая. Библиотеку FatFS можно конфигурировать, например, включить поддержку длинных имён. Перед тем как работать с файлами, необходимо смонтировать файловую систему при помощи функции f_mount().
Чтобы проверить, что мы всё верно настроили для работы с SD-карты, напишем функцию Test_SD_Card, которая выводит содержимое карты, проверяет есть ли файл в корне с именем “hello.txt”, если нет, то создаёт файл с текстом “Hello World”, закрывает его, потом открывает его и печатает содержимое.
Функция показывает использование основных структур данных и функции, которые вам понадобятся при работе с файлами на SD-карте.
Структуры:
FATFS,
FIL,
DIR,
FILINFO,
FRESULT,
Функции:
f_mount(),
f_opendir(),
f_closedir(),
f_open(),
f_close(),
f_read(),
f_write(),
f_stat()
Поместим её определение в файл User/utils.с, а определение - в User/utils.h
Содержимое User/utils.с:
#include <stdio.h>
#include <string.h>
#include "ff.h" // Main FatFs header file
FATFS fs; // File system object
FIL file; // File object
DIR dir; // Directory object
FILINFO fno; // File/folder information structure
FRESULT res; // Variable to check FatFs return codes
void Test_SD_Card(void)
{
UINT bytes_written = 0;
UINT bytes_read = 0;
char read_buffer[64];
const char* filename = "hello.txt";
const char* text_to_write = "Hello World";
printf("=== Starting SD Card Test ===\n");
// 1. Mount the file system (0: is the default logical drive, 1 forces immediate mounting)
res = f_mount(&fs, "0:", 1);
if (res != FR_OK) {
printf("f_mount error: %d. Check card connection or formatting.\n", res);
return;
}
printf("File system successfully mounted.\n");
// 2. List the contents of the root directory
printf("\n--- Root Directory Contents: ---\n");
res = f_opendir(&dir, "0:/");
if (res == FR_OK) {
while (1) {
res = f_readdir(&dir, &fno); // Read next item
if (res != FR_OK || fno.fname[0] == '\0') break; // End of list or error
// Check if it is a directory or a file
if (fno.fattrib & AM_DIR) {
printf(" [DIR] %s\n", fno.fname);
} else {
printf(" [FILE] %s (Size: %lu bytes)\n", fno.fname, fno.fsize);
}
}
f_closedir(&dir);
} else {
printf("Failed to open root directory, error: %d\n", res);
}
// 3. Check if "hello.txt" exists using f_stat
printf("\n--- Checking for file: %s ---\n", filename);
res = f_stat(filename, &fno);
if (res == FR_NO_FILE) {
// File does not exist — create it
printf("File %s not found. Creating...\n", filename);
// Open file with flags to create a new file and allow writing
res = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
if (res == FR_OK) {
// Write "Hello World" text to the file
res = f_write(&file, text_to_write, strlen(text_to_write), &bytes_written);
if (res == FR_OK) {
printf("Successfully wrote %u bytes.\n", bytes_written);
} else {
printf("f_write error: %d\n", res);
}
// File must be closed to physically save data to the card
f_close(&file);
} else {
printf("f_open error during creation: %d\n", res);
}
} else if (res == FR_OK) {
printf("File %s already exists on the card.\n", filename);
} else {
printf("f_stat check error: %d\n", res);
}
// 4. Open the file for reading and print its content
printf("\n--- Reading file: %s ---\n", filename);
res = f_open(&file, filename, FA_READ);
if (res == FR_OK) {
// Clear the buffer before reading
memset(read_buffer, 0, sizeof(read_buffer));
// Read data from the file into the buffer
res = f_read(&file, read_buffer, sizeof(read_buffer) - 1, &bytes_read);
if (res == FR_OK) {
printf("Read from file: \"%s\" (Total: %u bytes)\n", read_buffer, bytes_read);
} else {
printf("f_read error: %d\n", res);
}
// Close the file after reading
f_close(&file);
} else {
printf("f_open error during read: %d\n", res);
}
// 5. Unmount the file system to release resources
f_mount(NULL, "0:", 0);
printf("\n=== Test finished, card unmounted ===\n");
}
Содержимое User/utils.h:
#ifndef UTILS_H
#define UTILS_H
extern void Test_SD_Card(void);
#endif
Запустив прошивку на отладку со вставленной отформатированной и пустой SD-картой, в консоли отладчика мы увидим:
=== Starting SD Card Test ===
File system successfully mounted.
--- Root Directory Contents: ---
--- Checking for file: hello.txt ---
File hello.txt not found. Creating...
Successfully wrote 11 bytes.
--- Reading file: hello.txt ---
Read from file: "Hello World" (Total: 11 bytes)
=== Test finished, card unmounted ===
Заключение
Почти все функции библиотеки FatFs являются интуитивно понятными.
Иногда из-за настроек, функция f_mount() и другие могут возвращать ошибку. В большинстве случаев помогает снижение частоты клока для SD-карты.
Также ошибки работы с SD-картой возникают из-за отсутствующих или низкого номинала подтягивающих резисторов, длинных проводников (если слот у вас не распаян на плате).
Я намеренно не усложнял статью использованием DMA и RTOS, чтобы было ощущение простоты реализации работы с SD-картой. Теперь у вас есть базис, позволяющий, кроме всего прочего, понять и сравнить как реализована в других микроконтроллерах, таких как ESP32 и RP2040.
Низкоуровневые осциллограммы иногда могут дать ответ о причинах сбоев в работе SD-карты, но если у вас нет осциллографа или логического анализатора, проверьте тактовую частоту SD-карты, длину проводников и наличие подтягивающих резисторов. В большинстве случаев это причина некорректной работы.
Я использовал semihosting для вывода отладочной информации, что требует запуска прошивки в режиме отладки, вы же можете использовать другие способы отладочной информации, изложенные в моей предыдущей статье (RTT, SWO, UART).
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.