在嵌入式开发领域,直接操作硬件而不依赖操作系统(即"裸机"开发)始终是检验工程师底层功力的试金石。最近我在一个工业数据采集项目中,需要使用STM32H743这颗高性能Cortex-M7芯片,在裸机环境下实现对SD卡的读写操作。这个需求看似基础,但实际开发过程中遇到了不少值得记录的细节问题。
SD卡作为最常用的嵌入式存储介质,其SPI和SDIO两种通信方式各有优劣。H743系列自带SDMMC控制器,理论上能提供比SPI模式更高的传输速率(理论上可达50MHz),但相应的驱动开发复杂度也更高。本文将完整记录从硬件设计到驱动调通的全部过程,重点分享寄存器配置、初始化序列、DMA传输优化等核心环节的实战经验。
STM32H743的SDMMC1控制器通过以下引脚与SD卡槽连接:
关键提示:即使只使用1位数据线模式,也必须将D0引脚正确连接,这是SD协议的基本要求。我曾因漏接D0导致卡在初始化阶段长达两小时。
电源部分需特别注意:
根据项目经验推荐以下配置:
SDMMC外设时钟需要精确配置:
c复制// 在SystemClock_Config()中添加以下配置
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_SDMMC;
PeriphClkInit.SdmmcClockSelection = RCC_SDMMCCLKSOURCE_PLL1Q;
HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
// 确保PLL1Q输出为200MHz
// SDMMC时钟分频计算:200MHz/(2*CLKDIV) ≤ 卡支持的最大时钟
实测发现:
完整的初始化流程如下:
避坑指南:ACMD41需要先发送CMD55(APP_CMD),这是新手最容易遗漏的步骤。我在首次实现时因此卡在初始化阶段。
启用DMA可显著提升性能:
c复制// DMA流配置示例(SDMMC1_RX)
hdma_sdmmc1_rx.Instance = DMA2_Stream3;
hdma_sdmmc1_rx.Init.Request = DMA_REQUEST_SDMMC1;
hdma_sdmmc1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_sdmmc1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_sdmmc1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_sdmmc1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_sdmmc1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_sdmmc1_rx.Init.Mode = DMA_PFCTRL;
hdma_sdmmc1_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_sdmmc1_rx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_sdmmc1_rx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
hdma_sdmmc1_rx.Init.MemBurst = DMA_MBURST_INC4;
hdma_sdmmc1_rx.Init.PeriphBurst = DMA_PBURST_INC4;
实测性能对比:
| 传输模式 | 块大小 | 传输速率 |
|---|---|---|
| 轮询模式 | 512B | 2.1MB/s |
| DMA模式 | 512B | 8.7MB/s |
| DMA模式 | 4KB | 11.2MB/s |
在diskio.c中需要实现以下关键函数:
c复制DSTATUS disk_initialize(BYTE pdrv) {
// 调用前面实现的SD_Initialize()
}
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) {
// 调用SD_ReadBlocks()
}
DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) {
// 调用SD_WriteBlocks()
}
配置文件ffconf.h关键参数:
c复制#define FF_USE_FASTSEEK 1 // 启用快速定位
#define FF_MAX_SS 512 // 扇区大小
#define FF_LBA_UNIT 1 // 启用LBA寻址
#define FF_FS_EXFAT 1 // 支持exFAT
c复制FIL file;
UCHAR buffer[4096]; // 4K对齐缓冲区
f_open(&file, "data.log", FA_READ | FA_WRITE);
f_lseek(&file, f_size(&file));
f_write(&file, buffer, sizeof(buffer), &bytesWritten);
c复制__attribute__((aligned(4))) uint8_t sectorBuffer[512];
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| CMD8返回0x05 | 电压范围不匹配 | 检查SD卡规格书确认支持电压 |
| ACMD41无响应 | 未先发送CMD55 | 确保每个ACMD前发送CMD55 |
| DMA传输数据错位 | 缓冲区未对齐 | 使用__attribute__((aligned(4))) |
| 写入后读取数据不一致 | 未正确等待写入完成 | 检查CMD13返回的写入状态 |
| 高频噪声导致通信失败 | 未加终端电阻 | 数据线串联33Ω电阻 |
使用Saleae Logic Pro 16抓取SDMMC信号时建议配置:
典型问题诊断流程:
实现零等待时间的连续数据采集:
c复制// 双缓冲配置
uint32_t bufferA[1024], bufferB[1024];
HAL_SD_ReadBlocks_DMA(&hsd, bufferA, startSector, 1024/512);
// 在HAL_SD_RxCpltCallback中切换缓冲区
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) {
if(currentBuffer == bufferA) {
processData(bufferA);
HAL_SD_ReadBlocks_DMA(hsd, bufferB, nextSector, 1024/512);
} else {
processData(bufferB);
HAL_SD_ReadBlocks_DMA(hsd, bufferA, nextSector, 1024/512);
}
nextSector += 2;
}
通过SD_CMD38实现擦除前断电保护:
c复制void SD_EraseWithPowerLossProtection(uint32_t startAddr, uint32_t endAddr) {
SDMMC_CmdInitTypeDef cmd;
// 发送CMD38启用断电保护
cmd.Argument = 0;
cmd.CmdIndex = SDMMC_CMD_ERASE;
cmd.Response = SDMMC_RESPONSE_SHORT;
cmd.WaitForInterrupt = SDMMC_WAIT_NO;
cmd.CPSM = SDMMC_CPSM_ENABLE;
HAL_SD_SendCommand(&hsd, &cmd);
// 正常擦除操作
HAL_SD_Erase(&hsd, startAddr, endAddr);
}
在STM32H743@480MHz下的基准测试结果:
| 操作类型 | 块大小 | 平均耗时 | 传输速率 |
|---|---|---|---|
| 单块读取(SPI) | 512B | 2.4ms | 213KB/s |
| 多块读取(SDIO) | 4KB | 0.36ms | 11.1MB/s |
| 单块写入(SPI) | 512B | 5.7ms | 89KB/s |
| 多块写入(SDIO) | 4KB | 1.2ms | 3.3MB/s |
| FAT32文件创建 | - | 8.2ms | - |
从数据可以看出,SDIO 4位宽模式相比SPI模式有近50倍的性能提升,特别是在读取操作上优势明显。但要注意实际文件系统操作会引入额外开销,实测FAT32的文件创建操作需要约8ms。