目录
(1)RCC、GPIO、USART6、SYS、CodeGenrator
阅读本文需要的基本知识可以参考本文作者发布的文章:
细说STM32单片机使用轮询模式直接访问SD卡的方法及其应用-CSDN博客 https://wenchm.blog.csdn.net/article/details/149218456?spm=1011.2415.3001.5331
SD的HAL驱动程序提供了DMA方式读写SD卡的函数,即HAL_SD_WriteBlocks_DMA()和HAL_SD_ReadBlocks_DMA()。当SD卡读写数据量比较大时,使用DMA方式可以减少处理器负荷,提高运行效率。
本文用一个示例演示DMA方式读写SD卡的操作。
一、示例:以DMA方式读写SD卡
1、示例功能与CubeMX项目设置
通过串口在串口助手上互动,使用DMA模式测试SD卡的信息、擦除、读写。
(1)RCC、GPIO、USART6、SYS、CodeGenrator
与参考文章相同。
(2)SDIO
对SDIO进行DMA设置,SDIO有SDIO_RX(接收)和SDIO_TX(发送)两个DMA请求,需要分别关联DMA流。两个DMA流的模式都自动设置为PeripheralFlow Control(外设流程控制),都会自动使用FIFO,数据宽度自动设置为Word,Memory端开启地址自增功能。
对SDIO进行DMA配置后,还必须打开SDIO的全局中断。如果不打开SDIO的全局中断,在使用DMA方式进行读写时,有时会出现失败的情况。SDIO全局中断的抢占优先级应高于或等于DMA流的抢占优先级。
(3)NVIC
2、主程序与外设初始化
(1)初始主程序
完成设置后,CubeMX会自动生成代码。初始的主程序代码如下,代码中增加了DMA初始化函数MX_DMA_Init():
//main.c
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "sdio.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "keyled.h"
#include <stdio.h>
/* USER CODE END Includes */
/* USER CODE BEGIN PV */
uint8_t SDBuf_TX[BLOCKSIZE]; //Data Sending Cache, BLOCKSIZE=512
uint8_t SDBuf_RX[BLOCKSIZE]; //Data Received Cache
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
//此处及之后的内容见下文
}
上述程序定义了两个uint8_t类型的全局数组SDBuf_TX和SDBuf_RX,其大小都是BLOCKSIZE,也就是512字节。因为以DMA方式读写数据是非阻塞式的,需要使用全局变量作为存储数据的缓冲区。
(2)DMA初始化
函数MX_DMA_Init()用于DMA初始化,在自动生成的文件dma.h和dma.c中定义和实现。这个函数的源代码如下,其功能就是使能DMA2控制器时钟,配置两个DMA流的中断优先级。
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file dma.c
* @brief This file provides code for the configuration
* of all the requested memory to memory DMA transfers.
******************************************************************************
* @attention
*
* Copyright (c) 2025 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 "dma.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/*----------------------------------------------------------------------------*/
/* Configure DMA */
/*----------------------------------------------------------------------------*/
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/**
* Enable DMA controller clock
*/
void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA2_CLK_ENABLE();
/* DMA interrupt init */
/* DMA2_Stream3_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
/* DMA2_Stream6_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);
}
(3)SDIO接口和SD卡初始化
文件sdio.c中的初始化函数MX_SDIO_SD_Init()以及相关变量和函数的代码如下:
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "sdio.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
SD_HandleTypeDef hsd;
DMA_HandleTypeDef hdma_sdio_rx;
DMA_HandleTypeDef hdma_sdio_tx;
/* SDIO init function */
void MX_SDIO_SD_Init(void)
{
/* USER CODE BEGIN SDIO_Init 0 */
/* USER CODE END SDIO_Init 0 */
/* USER CODE BEGIN SDIO_Init 1 */
/* USER CODE END SDIO_Init 1 */
hsd.Instance = SDIO;
hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
hsd.Init.BusWide = SDIO_BUS_WIDE_1B;
hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
hsd.Init.ClockDiv = 0;
if (HAL_SD_Init(&hsd) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SDIO_Init 2 */
/* USER CODE END SDIO_Init 2 */
}
void HAL_SD_MspInit(SD_HandleTypeDef* sdHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(sdHandle->Instance==SDIO)
{
/* USER CODE BEGIN SDIO_MspInit 0 */
/* USER CODE END SDIO_MspInit 0 */
/* SDIO clock enable */
__HAL_RCC_SDIO_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
/**SDIO GPIO Configuration
PC8 ------> SDIO_D0
PC12 ------> SDIO_CK
PD2 ------> SDIO_CMD
*/
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_SDIO;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_SDIO;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/* SDIO DMA Init */
/* SDIO_RX Init */
hdma_sdio_rx.Instance = DMA2_Stream3;
hdma_sdio_rx.Init.Channel = DMA_CHANNEL_4;
hdma_sdio_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_sdio_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_sdio_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_sdio_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_sdio_rx.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_sdio_rx.Init.Mode = DMA_PFCTRL;
hdma_sdio_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_sdio_rx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_sdio_rx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
hdma_sdio_rx.Init.MemBurst = DMA_MBURST_INC4;
hdma_sdio_rx.Init.PeriphBurst = DMA_PBURST_INC4;
if (HAL_DMA_Init(&hdma_sdio_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(sdHandle,hdmarx,hdma_sdio_rx);
/* SDIO_TX Init */
hdma_sdio_tx.Instance = DMA2_Stream6;
hdma_sdio_tx.Init.Channel = DMA_CHANNEL_4;
hdma_sdio_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_sdio_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_sdio_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_sdio_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_sdio_tx.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_sdio_tx.Init.Mode = DMA_PFCTRL;
hdma_sdio_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_sdio_tx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_sdio_tx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
hdma_sdio_tx.Init.MemBurst = DMA_MBURST_INC4;
hdma_sdio_tx.Init.PeriphBurst = DMA_PBURST_INC4;
if (HAL_DMA_Init(&hdma_sdio_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(sdHandle,hdmatx,hdma_sdio_tx);
/* SDIO interrupt Init */
HAL_NVIC_SetPriority(SDIO_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(SDIO_IRQn);
/* USER CODE BEGIN SDIO_MspInit 1 */
/* USER CODE END SDIO_MspInit 1 */
}
}
void HAL_SD_MspDeInit(SD_HandleTypeDef* sdHandle)
{
if(sdHandle->Instance==SDIO)
{
/* USER CODE BEGIN SDIO_MspDeInit 0 */
/* USER CODE END SDIO_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_SDIO_CLK_DISABLE();
/**SDIO GPIO Configuration
PC8 ------> SDIO_D0
PC12 ------> SDIO_CK
PD2 ------> SDIO_CMD
*/
HAL_GPIO_DeInit(GPIOC, GPIO_PIN_8|GPIO_PIN_12);
HAL_GPIO_DeInit(GPIOD, GPIO_PIN_2);
/* SDIO DMA DeInit */
HAL_DMA_DeInit(sdHandle->hdmarx);
HAL_DMA_DeInit(sdHandle->hdmatx);
/* SDIO interrupt Deinit */
HAL_NVIC_DisableIRQ(SDIO_IRQn);
/* USER CODE BEGIN SDIO_MspDeInit 1 */
/* USER CODE END SDIO_MspDeInit 1 */
}
}
MSP初始化函数HAL_SD_MspInit()中增加了DMA初始化功能,上述程序定义了两个DMA流对象与SDIO的两个DMA请求关联。
3、程序功能实现
(1)主程序
在主程序中修改和添加代码,完成的主程序代码如下:
// 继续上面的main.c代码
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_SDIO_SD_Init();
MX_USART6_UART_Init();
/* USER CODE BEGIN 2 */
uint8_t startstr[] = "Demo13_2: SD card R/W-DMA. \r\n";
HAL_UART_Transmit(&huart6,startstr,sizeof(startstr),0xFFFF);
uint8_t startstr1[] = "Read/write SD card via DMA. \r\n\r\n";
HAL_UART_Transmit(&huart6,startstr1,sizeof(startstr1),0xFFFF);
//show menu
printf("[S2]KeyUp =SD card info. \r\n");
printf("[S3]KeyDown =Erase 0-10 blocks. \r\n");
printf("[S4]KeyLeft =Write block. \r\n");
printf("[S1]KeyRight=Read block. \r\n\r\n");
while(1)
{
KEYS waitKey=ScanPressedKey(KEY_WAIT_ALWAYS); //Waiting for the key
if (waitKey==KEY_UP)
{
SDCard_ShowInfo();
printf("Reselect menu item or reset. \r\n");
}
else if (waitKey== KEY_DOWN)
{
SDCard_EraseBlocks(); //EraseBlocks 0-10
printf("Reselect menu item or reset. \r\n");
}
else if (waitKey== KEY_LEFT)
SDCard_TestWrite_DMA();
else if (waitKey== KEY_RIGHT)
SDCard_TestRead_DMA();
HAL_Delay(500);
}
/* USER CODE END 2 */
//以下的代码内容在下文接续
主程序显示了4个菜单项,通过4个按键选择操作。SDCard_ShowInfo()和SDCard_EraseBlocks()的代码与前一示例的完全相同,就不再显示和解释了。函数SDCard_TestWrite_DMA()以DMA方式向SD卡写入数据,函数SDCard_TestRead_DMA()以DMA方式从SD卡读取数据。
(2)中断ISR和回调函数
在示例中,SDIO的全局中断开启了,两个DMA流中断也是自动打开的,在文件stm32f4xx_it.c中,自动生成了这3个中断的ISR,代码如下:
//stm32f4xx_it.h
/* Exported functions prototypes ---------------------------------------------*/
// 此处的函数声明省略
void SDIO_IRQHandler(void);
void DMA2_Stream3_IRQHandler(void);
void DMA2_Stream6_IRQHandler(void);
/**
* @brief This function handles SDIO global interrupt.
*/
void SDIO_IRQHandler(void)
{
/* USER CODE BEGIN SDIO_IRQn 0 */
/* USER CODE END SDIO_IRQn 0 */
HAL_SD_IRQHandler(&hsd);
/* USER CODE BEGIN SDIO_IRQn 1 */
/* USER CODE END SDIO_IRQn 1 */
}
/**
* @brief This function handles DMA2 stream3 global interrupt.
*/
void DMA2_Stream3_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream3_IRQn 0 */
/* USER CODE END DMA2_Stream3_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_sdio_rx);
/* USER CODE BEGIN DMA2_Stream3_IRQn 1 */
/* USER CODE END DMA2_Stream3_IRQn 1 */
}
/**
* @brief This function handles DMA2 stream6 global interrupt.
*/
void DMA2_Stream6_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream6_IRQn 0 */
/* USER CODE END DMA2_Stream6_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_sdio_tx);
/* USER CODE BEGIN DMA2_Stream6_IRQn 1 */
/* USER CODE END DMA2_Stream6_IRQn 1 */
}
这些代码都是由IDE自动生成的。
SDIO有两个最主要的回调函数,HAL_SD_TxCpltCallback()是在中断方式或DMA方式发送数据完成时执行的回调函数,HAL_SD_RxCpltCallback()是在中断方式或DMA方式接收数据完成时执行的回调函数。本示例需要重新实现这两个回调函数,为了便于使用全局数组SDBuf_TX和SDBuf_RX,这两个回调函数放在文件main.c里实现。
(3)DMA方式写数据
函数HAL_SD_WriteBlocks_DMA()以DMA方式向SD卡写入数据。在文件main.c中自定义的函数SDCard_TestWrite_DMA()就用这个函数测试向SD卡写入数据,同时,还需要实现回调函数HAL_SD_TxCpltCallback()。
// 继续上面的main.c的代码
// 此处代码在/* USER CODE BEGIN 4/END 4 */代码对内
/* USER CODE BEGIN 4 */
/* HAL_SD_GetCardInfo(), Display SD card information */
void SDCard_ShowInfo()
{
HAL_SD_CardInfoTypeDef cardInfo; //SD card information structure
HAL_StatusTypeDef res=HAL_SD_GetCardInfo(&hsd, &cardInfo);
if (res!=HAL_OK)
{
printf("HAL_SD_GetCardInfo() error. \r\n");
return;
}
printf("*** HAL_SD_GetCardInfo() info *** \r\n\r\n");
printf("Card Type= %ld \r\n", cardInfo.CardType);
printf("Card Version= %ld \r\n", cardInfo.CardVersion);
printf("Card Class= %ld \r\n", cardInfo.Class);
printf("Relative Card Address= %ld \r\n", cardInfo.RelCardAdd);
printf("Block Count= %ld \r\n", cardInfo.BlockNbr);
printf("Block Size(Bytes)= %ld \r\n", cardInfo.BlockSize);
printf("LogiBlockCount= %ld \r\n", cardInfo.LogBlockNbr);
printf("LogiBlockSize(Bytes)= %ld \r\n", cardInfo.LogBlockSize);
uint32_t cap= (cardInfo.BlockNbr*cardInfo.BlockSize); //bytes
cap = cap/(1e6); //MB
printf("SD Card Capacity(MB)= %ld \r\n", cap);
}
/* HAL_SD_Erase(), Erase SD card Blocks*/
void SDCard_EraseBlocks()
{
uint32_t BlockAddrStart=0; // Block 0,Addresses using block number
uint32_t BlockAddrEnd=10; // Block 10
printf("\r\n*** Erasing blocks *** \r\n\r\n");
if (HAL_SD_Erase(&hsd,BlockAddrStart, BlockAddrEnd)==HAL_OK)
{
printf("Erasing blocks,OK. \r\n");
printf("Blocks 0-10 is erased. \r\n");
}
else
printf("Erasing blocks,fail. \r\n");
HAL_SD_CardStateTypeDef cardState=HAL_SD_GetCardState(&hsd);
printf("GetCardState()= %ld \r\n", cardState);
// The following code has nothing to do with erasure and can be deleted.[wen]
while(cardState != HAL_SD_CARD_TRANSFER) //Wait for return to transmission status
{
HAL_Delay(1);
cardState=HAL_SD_GetCardState(&hsd);
return; //Otherwise, it will not come out after entering the loop
}
}
/* HAL_SD_WriteBlocks_DMA(), Write SD card */
void SDCard_TestWrite_DMA() //DMA mode, test write
{
printf("\r\n*** DMA Writing blocks *** \r\n\r\n");
for(uint16_t i=0;i<BLOCKSIZE; i++)
SDBuf_TX[i]=i; //generate data
printf("Writing block 6. \r\n");
printf("Data in [10:15] is: %d ",SDBuf_TX[10]);
for (uint16_t j=11; j<=15;j++)
{
printf(", %d", SDBuf_TX[j]);
}
printf("\r\nHAL_SD_WriteBlocks_DMA() is to be called. \r\n");
uint32_t BlockAddr=6; //Block Address
uint32_t BlockCount=1; //Block Count
HAL_SD_WriteBlocks_DMA(&hsd,SDBuf_TX,BlockAddr,BlockCount); //can erase block automatically
}
/* HAL_SD_ReadBlocks_DMA(), Read SD card */
void SDCard_TestRead_DMA() //test read
{
printf("\r\n*** DMA Reading blocks *** \r\n\r\n");
printf("\r\nHAL_SD_ReadBlocks_DMA() is to be called. \r\n");
uint32_t BlockAddr=6; //Block Address
uint32_t BlockCount=1; //Block Count
HAL_SD_ReadBlocks_DMA(&hsd,SDBuf_RX,BlockAddr,BlockCount);
}
/* SD write callback func */
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("DMA write complete. \r\n");
printf("HAL_SD_TxCpltCallback() is called. \r\n");
printf("Reselect menu item or reset. \r\n");
}
/* SD read callback func */
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("DMA read complete. \r\n");
printf("HAL_SD_RxCpltCallback() is called. \r\n");
printf("Data in [10:15] is: %d\r\n",SDBuf_RX[10]);
for (uint16_t j=11; j<=15;j++)
{
printf(", %d", SDBuf_RX[j]);
}
printf("\r\nReselect menu item or reset. \r\n");
}
上述程序首先将数组SDBuf_TX填满数据,然后调用函数HAL_SD_WriteBlocks_DMA()将数组SDBuf_TX里的数据写入SD卡的Block6。函数HAL_SD_WriteBlocks_DMA()可以一次写入多个数据块,写入之前无须擦除块,函数里会自动擦除。
函数HAL_SD_WriteBlocks_DMA()的原型定义详见参考文章。
函数HAL_SD_WriteBlocks_DMA()返回HAL_OK只表示DMA操作正确,并不表示数据写入完成了。以DMA方式向SD卡写入数据完成后,会执行回调函数HAL_SD_TxCpltCallback(),重新实现的这个回调函数的代码很简单,就是显示信息,表示DMA写入已完成。运行时会发现,此回调函数能被正常调用,表示DMA方式写入数据成功。
(4)DMA方式读取数据
函数HAL_SD_ReadBlocks_DMA()以DMA方式读取SD卡的数据,SDCard_TestRead_DMA()使用这个函数测试读取SD卡数据,同时,还需要实现回调函数HAL_SD_RxCpltCallback()。
函数SDCard_TestRead_DMA()调用函数HAL_SD_ReadBlocks_DMA()读取Block6的数据,保存到全局数组SDBuf_RX里。同样地,函数HAL_SD_ReadBlocks_DMA()返回HAL_OK只是表示DMA操作正确,并不表示数据读取完成了。
以DMA方式读取SD卡数据完成后,会执行回调函数HAL_SD_RxCpltCallback(),重新实现的这个回调函数显示了数组SDBuf_RX里的部分内容。在开发板上运行测试,如果Block 6被写入了数据,会发现读出的数据与写入的数据一致;如果Block 6被擦除后没有写入数据,读出的数组内容全部是0。
SD卡被擦除后,存储单元的内容可能是0,也可能是0xFF,会因SD卡的不同而不一样。
(5)其它代码
串口:
// 继续上面的main.c的代码
// 此处代码在/* USER CODE BEGIN 4/END 4 */代码对内
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart6,(uint8_t*)&ch,1,0xFFFF);
return ch;
}
/* USER CODE END 4 */
main.h中声明私有函数:
/* USER CODE BEGIN EFP */
void SDCard_ShowInfo();
void SDCard_EraseBlocks();
void SDCard_TestWrite_DMA();
void SDCard_TestRead_DMA();
/* USER CODE END EFP */
二、运行与调试
下载并运行,本示例的项目仍然使用1线数据线,下载后在串口助手上不能显示开始菜单(这个现象不正常),按下开发板的复位键后显示开始菜单:
S2:
S3:
S4:
S1: