【嵌入式】如何使用 STM32H750 的 SDMMC1
# 如何在 STM32H750 使用 Phat 库,通过 SDMMC1 读取 SD 卡## 设计需求
我的目标是通过 SD 卡读取 FAT 文件系统里面的 AVI 文件进行播放,视频分辨率是 320x240,帧数 30fps;音频使用单声道 PCM S16LE 44100Hz,**因此有带宽需求**,也就是带宽必须满足以足够的帧数播放 AVI 文件的比特率要求。按 MJPEG 编码的视频文件,把品质调到最高,视频方面的比特率撑死 5Mbit/s(我测试的视频文件里面视频流比特率最大的就是 5199kbps);音频部分因为并没有压缩,比特率是定死的 705kbps,**全部加起来就算 6 Mbit/s**。那因此我的 SD 卡的通信频率不能低于这个值。通过时钟分频,我给 SDMMC1 外设提供 200 MHz 的时钟输入,然后设置分频系数为 1,这样可以支持能够达到 100 MHz 速度的 SD 卡,远超我的 6 Mbit/s 的带宽需求(它是 4 线传输,100 MHz 可以达到 400 Mbit/s 的最大带宽)。
## STM32CubeMX 的配置
### SDMMC1 外设配置
我不打算使用外部控制器来控制电压转换、检测 SD 卡插入,我打算直接用STM32H750 的 GPIO 去莽连 SD 卡。
在 STM32CubeMX 的 Pinout & Configuration 里面找到左边栏,有 Connectivity,将其展开,找到 SDMMC,然后中间栏上部有 Mode,设为 SD 4 bits Wide bus(SD 卡四线宽总线)
* 模式: SD 4 bits Wide bus
* 参数设置:
* Clock transition on which the bit capture is made(在哪个时钟边沿采样数据):因为是 SD 卡所以选 Rising transition。
* SDMMC Clock output enable when the bus is idle(总线闲置时时钟信号是否仍然保持开启):选 Disable the power save for the clock,以避免它为了省电而在闲置时把时钟设为高阻抗,导致振铃,或者意外的时钟信号发生。
* SDMMC hardware flow control(硬件流控制):设为启用,我要配合 DMA 去使用它。
* SDMMC clock divide factor(时钟配置):我给 SDMMC1 提供的时钟是 200 MHz,但是我期望的 SD 卡通信频率是 100 MHz,因此我把它设为 1。
* Is external transceiver present?(是否有外部的收发器)设为 no。
* 因此 GPIO 只有这些:
* SDMMC_CK
* SDMMC_CMD
* SDMMC_D0
* SDMMC_D1
* SDMMC_D2
* SDMMC_D3
* SDMMC_CD(卡检测。数字输入模式,这个 GPIO 不被 SDMMC 外设控制,我用了 PD15 脚。)
### 关于 Phat 库
请看:
https://www.0xaa55.com/thread-27644-1-1.html
Phat 库的 `BSP_Phat.c` 已经对 STM32H750 做了适配,它会调用 `HAL_SD_ReadBlocks_DMA()`、`HAL_SD_WriteBlocks_DMA()` 等函数进行 SD 卡交互,并提供对 FAT12/16/32 的文件系统操作的完整支持,通过内存拷贝来解决读写缓冲区地址未对齐、读写缓冲区未在 IDMA 可用地址范围的问题。接口虽然不像 POSIX 的风格,但是更加适合嵌入式使用,且具有 LRU 缓存功能。
#### Repo
(https://github.com/0xAA55/Phat)
(https://gitee.com/a5k3rn3l/phat)
以上两个 Repo 内容相同,你如果无法使用 GitHub 则可以使用 Gitee。
具体用法请看 Phat 库的 `Phat/test.c` 源码文件,如下。
```C
#include <stdio.h>
#include <stdlib.h>
#include "phat.h"
Phat_t phat;
void Error_Handler()
{
exit(1);
}
#define V(x) if (x != PhatState_OK) Error_Handler()
#define V_(x) do {PhatState s = x; if (s != PhatState_OK) fprintf(stderr, #x ": %s\n", Phat_StateToString(s));} while (0)
int main(int argc, char**argv)
{
PhatState res = PhatState_OK;
Phat_DirInfo_t dir_info = { 0 };
Phat_FileInfo_t file_info = { 0 };
uint32_t file_size;
char *file_buf = NULL;
V(Phat_Init(&phat));
V(Phat_Mount(&phat, 0));
printf("==== Root directory files ====\n");
V(Phat_OpenDir(&phat, L"", &dir_info));
for (;;)
{
res = Phat_NextDirItem(&dir_info);
if (res != PhatState_OK) break;
if (dir_info.attributes & ATTRIB_DIRECTORY)
printf("Dir:%S\n", dir_info.LFN_name);
else
printf("File: %S\n", dir_info.LFN_name);
}
Phat_CloseDir(&dir_info);
printf("==== Files in `TestPhat` directory ====\n");
V_(Phat_OpenDir(&phat, L"TestPhat", &dir_info));
for (;;)
{
res = Phat_NextDirItem(&dir_info);
if (res != PhatState_OK) break;
if (dir_info.attributes & ATTRIB_DIRECTORY)
printf("Dir:%S\n", dir_info.LFN_name);
else
printf("File: %S\n", dir_info.LFN_name);
}
Phat_CloseDir(&dir_info);
V_(Phat_CreateDirectory(&phat, L"TestPhatMkDir"));
V_(Phat_RemoveDirectory(&phat, L"TestPhatMkDir"));
V_(Phat_OpenFile(&phat, L"TestPhat/The Biography of John Wok.txt", 1, &file_info));
Phat_GetFileSize(&file_info, &file_size);
file_buf = calloc(file_size + 1, 1);
if (!file_buf) goto FailExit;
V_(Phat_ReadFile(&file_info, file_buf, file_size, NULL));
Phat_CloseFile(&file_info);
printf("File contents:\n%s\n", file_buf);
free(file_buf);
FailExit:
V_(Phat_Unmount(&phat));
V_(Phat_DeInit(&phat));
return 0;
}
```
## 坑点
1. STM32H750 的 SDMMC1 的 DMA 只支持 IDMA,而 IDMA 只支持 AXI FLASH 和 AXI SRAM,也就是 0x08000000 到 0x0801FFFF 和 0x24000000 到 0x2407FFFF 区域的 RAM。**除了这个区域以外的区域都无法写入,IDMA 会卡死。**
2. 使用 IDMA 搬数据的时候,如果地址不是 4 字节对齐的,**它自己会默默地强行对齐地址**,然后开始搬数据。因此如果读写扇区,但是给了一个非对齐缓冲区地址的话,**它会把地址改到往前攒了几个字节的位置**,并且不会触发任何异常。
3. 当你拔了 SD 卡,然后你却尝试使用 SDMMC1 去读写 SD 卡,正常会失败(因此可以判定为 SD 卡被拔/无法识别);**但是卡被重新插回去后仍然无法初始化 SDMMC1**。这是因为一个 `HAL_SD_DeInit(&hsd1)` **并不足以使其重置**。必须使用以下的方式才能完全重置 SDMMC1 外设:
```C
__HAL_RCC_SDMMC1_FORCE_RESET();
HAL_Delay(2);
__HAL_RCC_SDMMC1_RELEASE_RESET();
// 此后可以用 HAL_SD_Init(&hds1) 初始化成功。
```
## 主板设计
### 布线规则
**先说结论:就正常布线就行了,不用过多考虑等长,10 cm 的差异值都是可以接受的。**
最快的传输方式应该是以 200 MHz 的频率进行四线传输,但是实际上支持这样的速度的 SD 卡是非常少而且非常贵的,如果需要支持这样的超级高速 SD 卡,你还必须要有外部电压转换器,使用 1.7V 左右的电压去控制 SD 卡。然而根据 HAL 的代码,看起来 STM32H750 似乎是不支持这种卡的高速通信的。
那按照 100 MHz 的布线要求来,实际上等于没啥等长方面的要求。反倒是不要过分为了做等长而导致串扰。如果你要做等长布线,请务必让每根线之间都有辅地,或者使用多层板提供尽可能靠近布线的 GND 平面,以提供寄生电容并减少串扰,然后再给每根线串接阻尼电阻(至少提供焊盘)。注意布线越长,支持的通信频率就越低,我这边布线 7cm 左右,100 MHz 带 DMA 可以正常通讯,PIO 通讯会提示 CRC 错误但是读出的数据似乎没有问题。
**阻尼电阻 + 寄生电容 = 上升沿变缓**,阻值越大,上升沿越缓。
用示波器实际测量 SDMMC_CK 的波形和其余每根数据线的波形,确保数据线的波形先到,SDMMC_CK 的波形后到。如果你的 SDMMC_CK 布线较短,可以给 SDMMC_CK 串接较大的阻尼电阻如 100Ω,其它较短的布线串接 50Ω 或者 22Ω,较大的阻值可以减少过冲和振铃,但是如果本来就没啥过冲,那就要减少阻值。较长的布线串接较小的阻尼电阻,仅抑制过冲和振铃。
**VCC 供电的线要稍微粗一点**。
### 滤波电容
每个负载加上一个 100nF 的去耦电容就行了。
## 焊接
我焊接 TF 卡座的时候手法比较糙,经常焊接失败,因为如果整个 TF 卡座都被加热了的话,里面的塑料片会变软,弹簧会破坏卡座的塑料片;以及如果助焊剂进入了 TF 卡座的底部,就会黏住自弹卡座使其无法自弹。按照我的经验,焊接 TF 卡座的时候 **推荐用电烙铁** 而不是风枪。使用锡浆而不是锡丝。用锡浆的针筒在焊盘上加点锡浆,然后加助焊剂,再用电烙铁烫开锡浆,让助焊剂里自由流动的锡珠自动吸附到焊盘和引脚上。
焊完后,需要用手用力在卡座的盖子上面摁一下,这样它就能挤压 TF 卡使其触点能够良好接触。
我现在已经不这么折腾了,小项目对成本不是太敏感的都直接上资源足够的芯片,然后用platformio+arduino:lol:lol
页:
[1]