Как я подружил SD-карту с DMA на STM32H750


На Хабре уже есть статья о том, как работать с SD-картами в DMA режиме на микроконтроллерах STM32, поэтому я думал, что на STM32H7 будет также просто это реализовать.
На деле же оказалось, что такой мощный чип хранит в себе подводные камни, и реализация работы с SD-картой при помощи DMA требует лучшего понимания архитектуры STM32.
DMA
С периферийными устройствами можно работать тремя способами, используя polling, прерывания или DMA.
Это три основных способа организации обмена, но они не являются взаимоисключающими.
Например, в STM32 можно запустить передачу через DMA, а после её завершения получить прерывание. В этом случае DMA выполняет передачу данных, а прерывание сообщает процессору о её завершении.
В микроконтроллерах STM32H7 используется усовершенствованный периферийный контроллер SDMMC (в STM32F4 используется SDIO), предназначенный для работы с картами памяти SD и другими совместимыми устройствами. По сравнению с реализациями предыдущих поколений он поддерживает более новые версии стандартов, высокоскоростные режимы обмена и встроенный DMA-контроллер IDMA, позволяющий передавать данные между периферией и памятью без постоянного участия процессора.
Я не нашёл в интернете гайд, который рассказывает подробно об особенностях работы SDMMC DMA. Надеюсь, что эта статья поможет вам сэкономить время, когда вы столкнётесь с этими особенностями.
Конфигурирование микроконтроллера
Создадим новый проект в STM32CubeMX и выполним конфигурирование.
Настройка отладки
Включаем 2-проводной интерфейс отладки и прошивки (SWD).
Pinout & Configuration -> Trace and Debug -> DEBUG -> Debug -> Serial Wire
Включаем 3-й модуль универсального синхронно-асинхронного приёмопередатчика (USART) в классическом асинхронном режиме (UART).
Connectivity -> USART3 -> Mode -> Asynchronous
Настройка SDMMC
Переводим SDMMC в режим работы по 4-битной шине данных.
Connectivity -> SDMMC1 -> Mode -> SD 4 Bit Wide bus
Для надёжности и защиты от потери данных включаем аппаратное управление потоком данных для SDMMC
Connectivity -> Configuration -> Parameter Settings -> SDMMC parameters -> SDMMC hardware flow control -> The hardware control is enabled
В STM32H7 используется отдельный внутренний DMA для интерфейса SDMMC, поэтому использовать каналы DMA, как для чипов STM32F4, не нужно. Включаем обработку прерываний на аппаратном уровне для SDMMC1 через контроллер прерываний NVIC (Nested Vectored Interrupt Controller).
Connectivity -> Configuration -> NVIC Settings -> SDMMC1 global interrupt
Указываем, что для работы с SD-картой использовать шаблон DMA (Direct Memory Access — прямого доступа к памяти).
Middleware and Software Packs -> Configuration -> Advanced Settings -> SDIO/SDMMC -> Use dma template -> Enabled
Настройка FatFs
Настраиваем один из пинов для детекта карты. Кликаем на PD4 и выбираем GPIO Input
Подключаем библиотеку FatFs и связываем её с драйвером SD-карты.
Middleware and Software Packs -> Mode -> FATFS -> SD Card
Указываем библиотеке FatFs, к какой конкретно физической ножке микроконтроллера подключён контакт Card Detect (CD) слота SD-карты (в нашем случае — к пину PD4).
Middleware and Software Packs -> Configuration -> Platform Settings -> Detect SDIO -> Found Solutions -> PD4
Настройка системы тактирования
Выполняем Clock configuration. Соглашаемся в появившемся окне. Устанавливаем частоту клока для SDMMC 50 МГц.
*SDMMC1,2 Clock Mux -> 50
Генерация проекта
Вводим название проекта:
Project Manager -> Project -> Project Settings -> Project Name -> stm32-sdmmc-dma-sample
Указываем, что генерировать проект для CMake:
Project Manager -> Project -> Project Settings -> Toolchain / IDE -> CMake
Указываем, что нужно генерировать отдельные файлы для каждой периферии:
*** Project Manager -> Code Generator -> Generated files -> Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral**
Нажимаем кнопку
Generate Code
Код приложения
Добавляем в сгенерированный проект два файла
User/utils.hиUser/utils.cUser/utils.c
#include <stdio.h> #include <string.h> #include "ff.h" // Main FatFs header file #include <assert.h> // Declare global or static structures to avoid stack overflow FATFS fs; 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 ```cpp extern void Test_SD_Card(void);Модифицируем
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 )Добавляем определение функции
_write()вmain.c, чтобыprintf()выводила информацию на UART./* USER CODE BEGIN PV */ int _write(int file, char *ptr, int len) { HAL_UART_Transmit(&huart3, (uint8_t *)ptr, len, HAL_MAX_DELAY); return len; } /* USER CODE END PV */Подключаем заголовочный файл
utils.hвmain.c/* USER CODE BEGIN Includes */ #include "utils.h" /* USER CODE END Includes */Добавляем вызов нашей функции Test_SD_Card().
/* USER CODE BEGIN 2 */ Test_SD_Card(); /* USER CODE END 2 */
Почему не работает DMA
Запускаем — и что мы видим? Микроконтроллер посылает DMA-запрос и не получает ответа, падает по таймауту.
Дело не в SD-карте, так как без DMA всё работало.
Поиск в интернете и чтение документации дают ответ на вопрос, почему такое происходит. У STM32H750 оперативная память разбита на регионы и домены. Внутренний SDMMC DMA не видит DTCMRAM.
Смотрим в файле STM32H750xx_FLASH.ld как распределяет память компоновщик.
Объявлены следующие области памяти:
/* Specify the memory areas */
MEMORY
{
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K
}
Данные и константы (секции .data и .bss) помещаются в DTCMRAM
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
*(.RamFunc) /* .RamFunc sections */
*(.RamFunc*) /* .RamFunc* sections */
. = ALIGN(4);
} >DTCMRAM AT> FLASH
```cpp
.bss (NOLOAD) : ALIGN(4)
{
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
PROVIDE( __bss_end = .);
} >DTCMRAM
Вывод распределения памяти при сборке это подтверждает.
Решение проблемы с DMA
Нужно найти способ, как разместить буферы, которые используются DMA в другом домене.
Можно пойти радикально, и все данные разместить в RAM. Кстати, в LD- скриптах, поставляемых с Platform.io, так и делается.
Но работа с RAM медленнее, поэтому мы добавим ещё одну секцию в LD-скрипт, а в программе добавим атрибуты для переменных типа FIL и FATFS, указывающие, что эти переменные следует размещать в этой секции.
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
_sdma_buffer_start = .;
*(.dma_buffer)
*(.dma_buffer.*)
. = ALIGN(32);
_sdma_buffer_end = .;
} > RAM
__attribute__((section(".dma_buffer"))) FATFS fs;
__attribute__((section(".dma_buffer"))) FIL file; // File object
Не забываем пересобрать проект, после того как внесли изменение в .ld файл.
Проблема с кешем
Запускаем, всё вроде бы работает, но оказывается, что у STM32H7 инструкции и данные могут кэшироваться.
Включим D-Cache в STM32CubeMX.
Pinout & Configuration -> CORTEX_M7 -> Cortex Interface Settings -> CPU ICache -> Enabled
Pinout & Configuration -> CORTEX_M7 -> Cortex Interface Settings -> CPU DCache -> Enabled
А DMA этот кэш не видит. При простом чтении данных с SD-карты мы даже не увидим проблемы. Но, например, возьмём сценарий, когда обнулим буфер, а потом прочитаем в него данные с SD-карты. Процессор будет видеть те данные, которые он записал в буфер, а не то, что прочитал DMA с SD. То есть в буфере будут 0.
Решение проблемы с кешем
Как решается проблема с буфером? Документация по STM32H7 советует использовать функцию SCB_CleanDCache_by_Addr() и SCB_InvalidateDCache_by_Addr() при использовании DMA.
В заглушке, которую генерирует STM32CubeMX, есть закомментированный макрос ENABLE_SD_DMA_CACHE_MAINTENANCE.
/* USER CODE BEGIN enableSDDmaCacheMaintenance */
/* #define ENABLE_SD_DMA_CACHE_MAINTENANCE 1 */
/* USER CODE END enableSDDmaCacheMaintenance */
Макрос добавляет код для правильной работы с D-Cache.
Раскомментируем макрос. Собираем и запускаем. Всё работает.
Обратите внимание, что в заглушке, которую генерирует CubeMX, процессор ждёт в цикле, пока не считаются данные по DMA. Что в принципе практически сводит на нет все преимущества DMA. Поэтому, по-хорошему, нужно переписывать весь код самим на основе колбеков или использовать FreeRTOS, в котором вытесняющий планировщик отдаст управление другой задаче, пока первая будет ждать ответа по DMA.
Но это уже, как говорится, другая история. А сейчас у нас есть работающий через DMA SDMMC-интерфейс.
Заключение
Использовать DMA для работы с SD-картой на STM32H7 сложнее, чем на микроконтроллерах более простых серий.
В интернете практически нет информации по этому вопросу. В основном пользователи спрашивают, почему у них что-то не работает, но так и не получают чёткого ответа от сообщества.
Чтобы заставить DMA работать с SD-картой, мне пришлось:
разбираться, как работает компоновщик,
где находятся и что содержат LD-скрипты.
вспоминать, что такое выравнивание.
Код для работы с SD-картой, генерируемый CubeMX, не является оптимальным: без использования FreeRTOS работа через DMA почти не даёт никаких преимуществ.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.