你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

#双节活动#外接 TF 卡跑起 FatFs

[复制链接]
shyi 发布时间:2026-11-6 17:31

外接 TF 卡跑起 FatFs


一、接线

信号 引脚 说明
SCK PE2 SPI4
MISO PE5
MOSI PE6
CS PE4 .ioc里标号 SPI_NSS,低电平有效

**PE4 上电必须是高电平(空闲不选中)。CubeMX 里配成 Output 且初始电平 High,生成的 **gpio.c 里能看到:

HAL_GPIO_WritePin(SPI_NSS_GPIO_Port, SPI_NSS_Pin, GPIO_PIN_SET);

这条不能丢。片选悬空的话,卡在启动期间会被误触发,之后的 SD 初始化会卡死在 CMD0。


二、驱动从哪来

不用自己写。ST 官方固件包里有现成的,就在本地:

STM32Cube_FW_F4_V1.28.3/Drivers/BSP/Adafruit_Shield/stm32_adafruit_sd.c

1030 行的协议逻辑,加 229 行头文件。它不依赖 Adafruit 那个 TFT 盾板,只要求实现 4 个空函数:

void    SD_IO_Init(void);
void    SD_IO_CSState(uint8_t state);
void    SD_IO_WriteReadData(const uint8_t *DataIn, uint8_t *DataOut, uint16_t DataLength);
uint8_t SD_IO_WriteByte(uint8_t Data);

这 4 个函数加起来不到 40 行,绑 hspi4 和 PE4 就完事。协议那 1000 行一个字不用改。

分层:

FATFS/Target/user_diskio.c      FatFs 的介质接口,5 个函数
        ↓
BSP/u429_disco_01/Src/bsp_sd.c  4 个 IO 回调 + 5 个封装
        ↓
BSP/Components/sd_spi/          ST 官方协议层

**驱动里的函数统一改成 **SD_Proto_* 前缀,板级用 BSP_SD_*。不改的话两边都叫 BSP_SD_Init,参数还不一样,C 语言直接报重定义。

板级适配里的三个必做动作

① 80 个上电时钟。 SD 卡上电后需要至少 74 个时钟完成内部复位,才能进 SPI 模式。官方驱动的 SD_GoIdleState() 一上来就发 CMD0,这一步它没做。漏了的话 CMD0 永远等不到应答,现象和"没插卡"一模一样。

**放在 **SD_IO_Init() 里:CS 保持高电平,发 10 个 0xFF。

② 速率两段式。 SD 卡在初始化阶段只接受 100~400kHz,初始化完才能提速。

本板 SPI4 挂在 APB2 = 84MHz:

阶段 分频 速率
CubeMX 默认 2 42 MHz
初始化 256 328 kHz
正常读写 8 10.5 MHz

**那个 42MHz 是陷阱——直接拿来发 CMD0,卡根本不认。所以 **SD_IO_Init() 第一件事就是降到 256 分频。

为什么工作速率是 10.5MHz 而不是 21MHz。 第一版用了 4 分频(21MHz),结果是能初始化、读不了块:

[SD] mount failed: disk I/O error (1), 1496 ms

**这里 **(1) 是 FR_DISK_ERR。这个错误码本身就是线索:

错误码 含义 说明问题出在
3=FR_NOT_READY 初始化没通过 片选、上电时钟、低速档
1=FR_DISK_ERR 初始化过了,读块失败 高速档、信号完整性

**看到 **3 说明卡压根没被叫醒;看到 1 说明握手成功了,是数据链路撑不住。这两类完全不同的故障,从错误码就能分开。

21MHz 下失败的原因:C64 模块带电平转换芯片,边沿被拉缓;再加上杜邦线,21MHz 时 MISO 采样已经错位。降到 10.5MHz 后立刻正常。

③ MISO 补上拉。 CubeMX 把 PE5 配成 GPIO_NOPULL,理由是"复用推挽不需要上下拉"。但 SD 卡在两次传输之间会把 MISO 释放成高阻态,此时引脚电平完全由外部决定——没上拉就是浮空,读到的第一位可能是 0 也可能是 1。

**C64 这类模块通常板载 10k 上拉,但如果线长或者模块省了,就需要补。在 **SD_IO_Init() 里覆盖:

GPIO_InitStruct.Pin       = BSP_SD_MISO_PIN;
GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull      = GPIO_PULLUP;      /* ← CubeMX 给的是 NOPULL */
GPIO_InitStruct.Alternate = GPIO_AF5_SPI4;
HAL_GPIO_Init(BSP_SD_MISO_PORT, &GPIO_InitStruct);

**⚠️ 这段必须放在 **sd_set_prescaler() 之后。HAL_SPI_Init() 在 State == RESET 时会调 MspInit,把 GPIO 配置冲掉。

**注意:这一条和上一条是同时改的,没法区分到底是哪个起了作用。 严格做的话应该做对照实验(只降速 / 只加上拉 / 两个都做),但 10.5MHz 对 FatFs 已经够用 **(实测读一个扇区 1ms,写 79ms 的瓶颈在卡内部编程而不在 SPI),没有继续提速的收益,就没去分辨。


三、🔴 内存配置问题

这一节是这份文档的重点。三个问题都是"能编译、能运行、但行为不对"的类型。

3.1 512 字节的堆,撞上 512 字节的 malloc

链接脚本里:

_Min_Heap_Size = 0x200;      /* 512 字节 */

而官方驱动里读/写块各有一处:

ptr = malloc(sizeof(uint8_t)*BlockSize);   /* BlockSize = 512 */

512 字节的堆,申请 512 字节,减掉 malloc 自己的块头(8~16 字节)必然失败。

症状特别迷惑:BSP_SD_Init() 成功(它不分配内存),第一次读块就失败。很容易判成卡没插好、接触不良、或者速率不对,实际是内存不够。

修法:改成静态缓冲。

static uint8_t s_sd_dummy_buf[512];   /* 放 .bss,不占堆 */

代价是 512 字节常驻 RAM,换来的是"不会因为堆碎片而随机失败"。嵌入式里这种一次性的块缓冲本来就不该动态分配。

推广:凡是"申请大小 ≈ 堆总大小"的一次性分配,等于必然失败。改静态,或者把堆开大,别赌。

3.2 FIL 结构体里藏着 512 字节

ffconf.h 里 _FS_TINY = 0,这是"普通模式":每个打开的文件对象自带一份扇区缓冲。

实测:

20014920 00000230 b s_fil      ← 560 字节

**一个 **FIL 就是 560 字节。如果声明成局部变量,直接在任务栈上压掉半条命。

**本工程最小任务的栈才 1024 字节,放一个 **FIL 进去再加上 printf,必爆栈。

**所以在 **app_sd_card.c 里把 FIL、DIR、读写缓冲全做成 static:

static FIL     s_fil;         /* 560 B */
static uint8_t s_buf[512];    /* 512 B */
static DIR     s_dir;         /*  48 B */
static FILINFO s_fno;         /*  24 B */

代价是不可重入。但自检本来就是单次动作,无所谓。

如果想让这些函数可重入,就得把它们放栈上,那时任务栈要按 560 + 512 + 100 ≈ 1200 B 起步算,而且每次调用都要这么多。

还有一个省法:_FS_TINY = 1 会把扇区缓冲从"每个文件一份"改成"整个模块共用一份",FIL 能瘦到 30 字节左右。代价是同一时刻只能有一个文件在读写——在单任务场景下完全够用。

3.3 FatFs 的 malloc 和 SD 驱动的 malloc 不是同一个堆

这一条最容易搞混。

/* ffconf.h 末尾 */
#define ff_malloc  pvPortMalloc
#define ff_free    vPortFree

FatFs 内部走的是 FreeRTOS 的 heap(32KB),不是 newlib 那个 512 字节的堆。

**而 SD 驱动原版直接用 **malloc(),走 newlib 堆。两者混在同一个工程里,看到"内存不足"时根本分不清是谁的。

判断方法:看这个分配发生在哪个文件里。ff.c / ff_gen_drv.c 里的走 pvPortMalloc;sd_spi.c 里的走 malloc(已改掉)。

3.4 _USE_LFN = 0 的取舍

_MAX_LFN = 255,但 _USE_LFN = 0,所以长文件名功能整个关掉了。

**省下的内存:每个打开的文件要 **_MAX_LFN * 2 + 2 字节的宽字符缓冲,也就是 512 字节/文件。不开就一分不花。

代价:文件名只能用 8.3 格式。

f_open(&fil, "SDTEST.TXT", ...);        /* ✅ */
f_open(&fil, "sd_test.txt", ...);       /* ❌ 返回 FR_INVALID_NAME */

那个 fr_convert 相关的 option/ccsbcs.c 也不能加——它开头有:

#if !_TBLDEF || !_USE_LFN
#error This file is not needed at current configuration. Remove from the project.
#endif

_USE_LFN = 0 时加进去直接编译失败。这条坑我踩了一下。

3.5 实测占用

版本 RAM FLASH
加 SD 之前 85,472 B 378,428 B
+ 驱动(sd_spi.c+bsp_sd.c) 86,280 B(+808) 384,388 B(+5,960)
+ 自检模块(app_sd_card.c+ 任务) 89,216 B(+2,936) 400,060 B(+15,672)
自检开关关掉 86,280 B 384,388 B

自检模块那 2,936 字节 RAM 的构成:

项 字节
s_sd_stack(任务栈 1024) 1,024
s_sd_tcb 92
s_fil 560
s_buf 512
s_dir+s_fno 72
其他(FatFs 静态、printf 缓冲) ~676

**FLASH 那 15,672 字节主要是 **ff.c ——之前没人调 f_mount(),整个文件系统内核被 --gc-sections 裁掉了,现在有人用才链接进来。

关掉自检开关能把这 2,936 B 全退回来,因为任务函数被 #if 掉之后,栈、TCB、FIL 全都没有引用,链接器一并裁掉。


四、FatFs 的几项关键配置

配置 值 影响
_FS_REENTRANT 1 开可重入,需要 option/syscall.c提供信号量封装
_SYNC_t osSemaphoreId 用 CMSIS 的信号量做同步对象
_FS_TIMEOUT 1000 拿不到卷时等多久
_FS_LOCK 2 最多 2 个文件同时打开
_FS_TINY 0 每个文件自带扇区缓冲(560 B)
_MAX_SS 512 扇区大小
_USE_LFN 0 不支持长文件名

_FS_REENTRANT = 1 意味着 option/syscall.c 必须在构建里。CubeMX 已经把它加进 FatFs_Src 了,不用管。

**如果不加,链接时会报 **ff_req_grant / ff_rel_grant 未定义。这两个函数很多教程都不提,从报错反推得花一阵。


五、自检流程

App_SDCard_SelfTest() 做的事:

f_mount → 读卡容量 → 列根目录 → 建文件写 → 关 → 重开读 → 逐字节比对 → 删 → 卸载

为什么不只看 f_mount 返回 OK。

f_mount() 返回 FR_OK 只说明"逻辑卷注册成功",它不读卡上任何一个扇区。卡没格式化、FAT 表损坏、扇区寻址错,f_mount 全都是 OK。

**真正会失败的是第一次 **f_opendir 或 f_open——那时才去读引导扇区。所以必须一路做到"写进去再读出来一样"。

**挂在最低优先级的独立任务里跑,跑完 **vTaskDelete(NULL) 自杀。

常见错误码

排障时先看这个对照表,能省一半时间。

返回码 含义 优先查什么
FR_NOT_READY(3) 卡未就绪,初始化没过 有没有插卡、片选、上电时钟、低速档
FR_DISK_ERR(1) 初始化过了,读块失败 高速档、MISO 上拉、信号质量
FR_NO_FILESYSTEM 卡上没有 FAT 卡需要格式化(FAT32)
FR_INVALID_NAME 文件名非法 8.3 格式限制
FR_DENIED 访问被拒 卡的写保护开关
FR_NOT_ENOUGH_CORE 内存不足 LFN 缓冲、FreeRTOS heap

上板实测(2026-11-06,3594 MB 的 TF 卡)

[TASK] AppSdTask started (prio 1, stack 1024 B)
[SD] ==== mount & list ====
[BSP] SD: init OK in 3 ms, SPI 10500 kHz (PCLK2 84 MHz / 8)
[SD] capacity: 3594 MB (7360512 blocks x 512 B)
[SD] --- root dir ---
[SD]  SYSTEM~1            0 B <DIR>
[SD]  JA6_70~1.GCO 17355490 B
[SD] --- end of list ---
[SD] ==== write / read / verify ====
[SD] write ok: 41 B in 79 ms
[SD] verify ok: 41 B match
[SD] RESULT: PASS | 3594 MB | mount 13 ms | write 79 ms | read 1 ms | 2 entries
指标 数值 说明
SD 初始化 3 ms 卡已经在稳态,上电时钟和 80 个 dummy 之后就绪
挂载 + 读引导扇区 13 ms 含一次扇区读
写 41 字节(含 close) 79 ms 慢,但这是卡内部编程时间,不是 SPI 限制
读 41 字节 1 ms 10.5MHz 下读一个扇区约 0.4ms,加开销 1ms 合理

写为什么比读慢 80 倍。 f_write 之后调 f_close,要落盘的东西有三份:数据块本身、FAT 表更新、目录项更新。每次块写都要等卡内部编程完成,而 SD 卡写一页最坏允许 250ms。所以 79ms 属于正常范围,提速 SPI 对此没有帮助——这也解释了为什么 10.5MHz 够用。

**根目录里的 **SYSTEM~1 是短名显示,实际是 Windows 的 System Volume Information;JA6_70~1.GCO 同理。这正是 _USE_LFN = 0 的直观后果:长名字都被压缩成 8.3 格式。


六、一个必须记住的调用位置

MX_FATFS_Init() 在 main.c 里,位置在调度器启动前。它只调 FATFS_LinkDriver() 注册函数指针,不碰硬件,所以安全。

但 f_mount() 必须在任务里调。

**它会一路走到 **USER_initialize() → BSP_SD_Init() → HAL_Delay()。调度器没跑起来时 uwTick 是冻的,HAL_Delay 不返回,主流程永久卡死,连任务都创建不出来。

**盘符是 **"0:/"(USERPath 由 FATFS_LinkDriver 填好)。


七、几件记一下

  • **加载荷超过 512 字节的驱动前,先看链接脚本的 **_Min_Heap_Size
  • FIL 不是小结构体,_FS_TINY = 0 时它有 560 字节
  • FatFs 用 FreeRTOS 堆,别的驱动可能用 newlib 堆,两个不是一回事
  • f_mount 成功不代表卡上有文件系统
  • _USE_LFN = 0 时不能加 ccsbcs.c,它会自己 #error 拒绝编译
赞 收藏 评论0 发布时间:2026-11-6 17:31

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版