STM32F4 SPI DMA FatFS SD卡驱动移植与吞吐优化

发布时间:2026/11/5 4:34:15
STM32F4 SPI DMA FatFS SD卡驱动移植与吞吐优化

简介:面向STM32F4嵌入式开发者的SPI+DMA+FatFS综合工程,围绕MicroSD卡高速文件读写与CPU负载优化展开,适合具备C语言和STM32外设基础、希望打通存储链路的学习者。压缩包共141个文件,约1.48MB,以55个C源文件和63个头文件为主体,覆盖外设驱动、FatFS文件系统及SPI/DMA通信实现,另含12个汇编启动文件与Keil工程、调试配置,目录按底层驱动、文件系统和应用工程分层组织。已有1367人学习。工程将SPI主机模式配置、DMA通道搬运、SD卡命令交互以及FatFS挂载、打开、读写、关闭流程整合为可编译项目,读者可据此理解传输完成中断处理、缓冲区管理与资源释放,并对照排查SD卡初始化失败、DMA收发异常和文件系统挂载错误,也可作为嵌入式存储方案移植的参考。对课程设计、毕业设计及小型数据采集终端的存储模块选型也有直接参考意义。

1. 板上只剩一组 SPI,还得往卡里写数据

板子定型之后 SDIO 的引脚被 LCD 和外部 SRAM 占满,只剩一组 SPI 是空的,这时候还要往卡里存采集数据——这是 STM32 SPI DMA FatFS SD卡 这套组合最典型的出现方式。SPI 模式把 4 位总线降成 1 位,带宽实打实地砍掉四分之三,换来的是一组引脚、一份代码就能在 F4 全系上跑通;再叠上 DMA,CPU 不用守着数据寄存器一位一位地喂,FatFS 也才有机会跑到几百 KB/s 而不是几十 KB/s。这份 STM32F4_SPI_DMA_FATFS.zip 围绕的就是这条链路:SPI 模式下的 SD 卡底层驱动、DMA 收发、diskio 移植层、ffconf 裁剪,以及一个能直接编译烧录进去看结果的读写示例。它适合两类人:板子已经投了、只能退到 SPI 的;以及用 SDIO 撞过玄学问题、想换条稳一点的路的。

2. SPI 模式初始化:74 个时钟、CMD8 和那张 DMA 通道映射表

2.1 先算清楚 SPI 模式的天花板在哪

选 SPI 之前得知道自己在放弃什么。SDIO 四线模式在 48MHz 下理论带宽能到 24MB/s,1 位模式也有 6MB/s;而 SPI 模式受 SD 卡规范约束,时钟上限是 25MHz,单线串行,理论极限 3.1MB/s,实际工程里跑出 300~600KB/s 就算不错。这个差距看着吓人,但落到"每秒钟存几千个传感器采样点"这类需求上,300KB/s 绰绰有余。

真正的差别在调试成本和引脚成本上。SDIO 要求 CLK、CMD、D0~D3 六根线,四根数据线还要尽量等长,走线稍有偏差就出现写几 MB 后随机报错;SPI 只要 SCK、MOSI、MISO、CS 四根,随便找一组空闲的 SPI 外设就能接,用杜邦线飞出来都能跑。这也是为什么大量的中期原型、小批量设备宁可选 SPI。

有个前提必须讲清楚:SPI 模式下的 SD 卡是个彻头彻尾的"哑"设备。它不会主动告诉你"我有数据了",主机必须不停地发字节(约定发 0xFF)去换回字节。所以 SPI 的读写天然是全双工的:发一个 0xFF 才能收一个数据字节。这个特点决定了后面所有代码的写法,也决定了很多人在HAL_SPI_Receive()上翻车。

还有一个容易被忽略的时序:上电后到第一条命令之间,必须给卡至少 74 个时钟,而且这段时间 CS 要保持高、MOSI 要保持高(也就是发 0xFF 而不是发 0x00)。这 74 个时钟是卡内部上电复位用的,少给了就会出现"冷启动十次有两三次认不到卡、热复位却正常"这种极其难受的现象,第 5 章还会专门说它。

2.2 命令帧与三种响应格式

SD 卡 SPI 模式的命令固定 6 个字节:

字节内容说明
00x40 | cmd起始位 0 + 传输位 1 + 命令号
1~432 位参数大端序,高字节先发
5CRCSPI 模式只校验 CMD0 和 CMD8,其余填 0x01 即可

响应分四种,实际工程里最常打交道的是 R1 和 R3/R7:

  • R1:1 字节,bit7 恒为 0,bit0 是 idle 标志。初始化阶段判断"卡退没退出 idle"全看这一位。
  • R3/R7:R1 + 4 字节附加数据。CMD8 用 R7 回显电压和校验图样,CMD58 用 R3 读 OCR 寄存器。
  • R2:2 字节,读 CSD/CID 时用。
  • R1b:R1 后面还跟着一段 busy,靠拉低 DO 表示。

R1 的其余位含义值得记一下,排错时能省很多时间:bit6 参数错误、bit5 地址错误、bit4 擦除序列错误、bit3 CRC 错误、bit2 非法命令、bit1 擦除复位。比如 CMD8 返回的 R1 里 bit2 没置起来,说明这卡根本不认识 CMD8——要么是老 v1 卡,要么压根不是 SD 卡。

下面这段是命令发送函数,全篇的地基:

/* sd_spi.c —— 命令层,跑在低速时钟下(≤400kHz) */ static SPI_HandleTypeDef *hspi; static uint8_t spi_xfer(uint8_t tx) { uint8_t rx; /* 收一个字节必须发一个字节,这是 SPI 模式的铁律 */ HAL_SPI_TransmitReceive(hspi, &tx, &rx, 1, 100); return rx; } /* resp 传 NULL 表示这条命令没有 R3/R7 后续数据;返回 R1,0xFF 表示无响应 */ static uint8_t sd_cmd(uint8_t cmd, uint32_t arg, uint8_t crc, uint8_t *resp) { uint8_t buf[6], r1, retry = 0; buf[0] = 0x40 | cmd; buf[1] = (uint8_t)(arg >> 24); buf[2] = (uint8_t)(arg >> 16); buf[3] = (uint8_t)(arg >> 8); buf[4] = (uint8_t)arg; buf[5] = crc; CS_LOW(); spi_xfer(0xFF); /* 命令前至少 1 字节间隔,把 DO 顶高 */ for (uint8_t i = 0; i < 6; i++) spi_xfer(buf[i]); /* NCR 最长 8 字节,超过就当卡没搭理你 */ do { r1 = spi_xfer(0xFF); } while ((r1 & 0x80) && (++retry < 0x10)); if (resp) { for (uint8_t i = 0; i < 4; i++) resp[i] = spi_xfer(0xFF); } CS_HIGH(); spi_xfer(0xFF); /* 释放后补 8 个时钟收尾 */ return r1; }

参数说明:crc只在 CMD0 传 0x95、CMD8 传 0x87,其余传 0x01。retry < 0x10这个门限是经验值,规范说 NCR 最多 8 字节,留一倍余量。CS_HIGH()之后那一句spi_xfer(0xFF)不要省,某些卡在 CS 拉高后还需要额外时钟才肯放下 DO,省了会出现下一帧命令的第一个字节被吃掉。

2.3 SPI 参数与 DMA 通道:选错 Stream 后怎么都传不动

时钟这块有个硬约束:初始化阶段 SPI 时钟必须在 100~400kHz 之间,初始化完成后才能切到高速。F407 上 SPI1 挂在 APB2(84MHz),SPI2/SPI3 挂在 APB1(42MHz),分频系数和实际时钟的对应关系如下:

外设总线频率分频SCK用途
SPI184MHz256328kHz初始化阶段
SPI184MHz421MHz读写阶段
SPI2/SPI342MHz128328kHz初始化阶段
SPI2/SPI342MHz221MHz读写阶段

21MHz 已经是规范上限 25MHz 的贴身位置了,再往上(SPI1 分频 2 得 42MHz)大部分卡会直接不认,别贪。要是板子走线长、干扰大,退到 10.5MHz 更稳,代价是理论带宽减半——不过第 4 章会讲,实际写速的瓶颈根本不在这一档。

DMA 通道映射是 F4 新手第二个翻车点,F4 的 DMA 请求不是随便挂的,查表错了代码能编译、能跑、就是一个字节都传不出来:

外设方向DMAStreamChannel
SPI1_TXDMA2Stream3Channel3
SPI1_RXDMA2Stream0Channel3
SPI2_TXDMA1Stream4Channel0
SPI2_RXDMA1Stream3Channel0
SPI3_TXDMA1Stream5Channel0
SPI3_RXDMA1Stream0Channel0

配置上几个要点:DMA 方向外设到内存(RX)时,外设地址不自增、内存地址自增、数据宽度都是 Byte、模式用 Normal 不要用 Circular(Circular 会让计数器自己回绕,收完 512 字节后又从头覆盖);优先级给 High,因为 SD 卡对时序间隔是有要求的。SPI 这边要把NSS设成软件管理(SPI_NSS_SOFT),因为 CS 是我们自己用 GPIO 控制的;SPI_DATASIZE_8BIT;SPI_FIRSTBIT_MSB;模式 0(CPOL=0, CPHA=0)——SD 卡 SPI 模式固定支持模式 0 和模式 3,绝大多数用模式 0。

/* CubeMX 生成的骨架,这里只标出与 SD 卡强相关的项 */ hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; /* 全双工,收发都要 */ hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; /* 模式 0 */ hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; /* CS 自己控 */ hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /* 先低速 */ hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi1);

初始化完成之后切高速,别去改hspi1.Init.BaudRatePrescaler再调HAL_SPI_Init()(那样会把外设整个复位一遍),直接改寄存器更快也更安全:

/* 初始化跑完之后再切高速 */ void sd_spi_speed_up(SPI_HandleTypeDef *hspi) { __HAL_SPI_DISABLE(hspi); hspi->Instance->CR1 &= ~SPI_CR1_BR; /* 清掉分频位 */ hspi->Instance->CR1 |= SPI_BAUDRATEPRESCALER_4; /* SPI1 → 21MHz */ __HAL_SPI_ENABLE(hspi); }

2.4 上电到就绪:一段不能跳步的初始化流程

把前面几块拼起来,完整的上电流程是这样的:

uint8_t SD_Init(SPI_HandleTypeDef *spi) { hspi = spi; uint8_t r1, r7[4]; uint32_t t0; /* 1) 上电后 ≥1ms,再给 80 个时钟,CS 高、MOSI 高 */ HAL_Delay(2); CS_HIGH(); for (uint8_t i = 0; i < 10; i++) spi_xfer(0xFF); /* 2) CMD0 进 idle,CRC 固定 0x95 */ t0 = HAL_GetTick(); do { r1 = sd_cmd(0, 0, 0x95, NULL); } while (r1 != 0x01 && HAL_GetTick() - t0 < 500); if (r1 != 0x01) return SD_ERR_CMD0; /* 3) CMD8 问电压和 0x1AA 图样,区分 v1/v2 卡 */ r1 = sd_cmd(8, 0x1AA, 0x87, r7); if (r1 == 0x01) { if (r7[2] != 0x01 || r7[3] != 0xAA) return SD_ERR_VOLT; card_type = SD_V2; } else if (r1 & 0x04) { card_type = SD_V1; /* 非法命令位置起来了 = 老卡 */ } else { return SD_ERR_NOT_SD; /* 压根不是 SD 卡 */ } /* 4) ACMD41 轮询到卡退出 idle;v2 卡带 HCS 位申请块寻址 */ t0 = HAL_GetTick(); do { sd_cmd(55, 0, 0x01, NULL); r1 = sd_cmd(41, (card_type == SD_V2) ? 0x40000000UL : 0, 0x01, NULL); } while (r1 != 0x00 && HAL_GetTick() - t0 < 2000); if (r1 != 0x00) return SD_ERR_ACMD41; /* 5) CMD58 读 OCR,bit30 决定地址按块还是按字节 */ if (sd_cmd(58, 0, 0x01, r7) == 0x00) { block_addr = (r7[0] & 0x40) ? 1 : 0; } /* 6) 显式设块长 512,v1 卡必须设,v2 卡设了无害 */ if (sd_cmd(16, 512, 0x01, NULL) != 0x00) return SD_ERR_CMD16; sd_spi_speed_up(spi); /* 到这里才允许切高速 */ return SD_OK; }

几个参数值得单独说。CMD8的参数 0x1AA 不是随便编的:低 12 位 0x1AA 是校验图样,高 4 位 0x1 表示供电范围 2.7~3.6V,卡会把这 4 个字节原样回显,对不上说明电压不匹配,这时候换卡比改代码有用。ACMD41的参数里 0x40000000 是 HCS 位(Host Capacity Support),置了表示主机支持大容量卡,卡答应了就会在 OCR 里把 CCS 置 1,之后所有地址都按块算;不置或者卡不答应,就只能按字节算,sector * 512这一步不能忘。CMD58读回来的r7[0]是 OCR 的最高字节,bit6 对应 CCS。

轮询超时给 2 秒是有讲究的。便宜卡内部做上电自检偶尔要一两秒,给 500ms 会出现"好卡能认、烂卡认不到",而现场恰恰全是烂卡。

3. FatFS 移植:diskio.c 五个函数与 ffconf.h 的裁剪账

3.1 文件职责与加入编译的坑

FatFS 的源码包摊开就那几个文件,但每一份的职责边界要清楚,否则改起来会互相打架:

文件职责移不移
ff.c文件系统核心,FAT12/16/32/exFAT 解析必须加进编译
ff.h对外 API 与类型定义头文件路径
ffconf.h全部裁剪开关必须改
diskio.c物理层适配,五个函数必须自己写
diskio.hDRESULT、DSTATUS定义头文件路径
ffunicode.c长文件名/Unicode 转换表开 LFN 才要

diskio.c里一共五个函数要填:disk_status、disk_initialize、disk_read、disk_write、disk_ioctl,另外可选的get_fattime。前两个返回DSTATUS(位标志,STA_NOINIT/STA_NODISK/STA_PROTECT),后三个返回DRESULT。返回值这套东西一定要当回事:RES_OK是 0,RES_ERROR是 1,RES_WRPRT是 2,RES_NOTRDY是 3,RES_PARERR是 4。写错一个数字,FatFS 上层就把磁盘错误翻译成FR_DISK_ERR,然后你对着它查半天硬件。

还有一个版本差异得提前说:disk_read/disk_write的第三个参数,R0.12 及更早是DWORD sector,R0.13 之后改成了LBA_t sector。LBA_t在FF_LBA64 == 0时就是DWORD,逻辑上兼容,但如果你把别处的代码整段抄过来,函数原型对不上会直接编译报错。照着手上的ff.h改,别照抄。

3.2 disk_status 与 disk_initialize:状态机要自己维护

disk_status的职责只有一个——告诉 FatFS 这张卡现在能不能用。它不该做任何耗时操作,尤其是别在里面重新初始化卡,那会导致f_mount卡住几百毫秒。正确做法是自己维护一个静态状态变量:

static volatile DSTATUS card_state = STA_NOINIT; DSTATUS disk_status(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT | STA_NODISK; return card_state; } DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT | STA_NODISK; card_state = STA_NOINIT; if (SD_Init(&hspi1) == SD_OK) { card_state = 0; /* 清掉 STA_NOINIT 才算就绪 */ /* 顺手把容量读出来缓存,disk_ioctl 里直接用 */ SD_ReadCapacity(&sector_count); } return card_state; }

这里的门道在于:STA_NOINIT是"未初始化",FatFS 看到它就会调disk_initialize;清零之后才认为卡可用。如果disk_initialize失败返回STA_NOINIT,f_mount会直接返回FR_NOT_READY,不会再去读引导扇区。所以看到FR_NOT_READY就去查 SPI 底层,看到FR_NO_FILESYSTEM才是文件系统的问题,这两个错误码别混。

3.3 disk_read:多块读之后必须发 CMD12

读这块的流程是:发 CMD17(单块)或 CMD18(多块)→ 等数据起始令牌 0xFE → 收 512 字节 → 收 2 字节 CRC。多块读的时候每块前面都会有一个 0xFE,最后必须发 CMD12 让卡停下来,否则卡会一直停在这个状态里,下一条命令发出去全是 0xFF。

DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (card_state & STA_NOINIT) return RES_NOTRDY; if (!count) return RES_OK; /* v1 卡按字节寻址,这一步漏了就是读到一片乱码 */ if (!block_addr) sector *= 512; if (count == 1) { if (sd_cmd(17, sector, 0x01, NULL) != 0x00) return RES_ERROR; } else { if (sd_cmd(18, sector, 0x01, NULL) != 0x00) return RES_ERROR; } while (count--) { if (sd_wait_token(0xFE, 200) != SD_OK) { /* 数据令牌,超时单位 ms */ if (count) sd_cmd(12, 0, 0x01, NULL); /* 中途失败要收尾 */ return RES_ERROR; } if (sd_dma_read(buff, 512) != SD_OK) return RES_ERROR; sd_dma_read(NULL, 2); /* 啃掉 2 字节 CRC */ buff += 512; } if (count != 0 || count_start != 1) {} /* 见下 */ return RES_OK; }

上面最后那段是伪代码占位,实际写法用一个UINT total = count;在循环前存一份,循环结束后判断if (total > 1) sd_cmd(12, 0, 0x01, NULL);。这个收尾不能省,很多"读几十个扇区没问题、读几百个就报错"的现场,原因就是这里漏了 CMD12。

sd_dma_read内部用HAL_SPI_Receive_DMA(),注意它必须发 0xFF 出去才能收数据。HAL 的HAL_SPI_Receive_DMA在 SPI 上会自己往 DR 里塞哑元,行为是对的,但前提是 SPI 的Direction必须是SPI_DIRECTION_2LINES,如果配成SPI_DIRECTION_2LINES_RXONLY,DMA 请求不会产生,函数直接返回 HAL_ERROR 或者一直不完成。

3.4 disk_write:数据响应令牌和 busy 等待

写比读多两步:写完数据要收一个数据响应令牌,然后等卡把内部编程做完(DO 被拉低表示 busy)。

static DRESULT sd_write_block(const BYTE *buff) { uint8_t resp; uint32_t t0; spi_xfer(0xFF); /* 至少 1 字节间隔 */ spi_xfer(0xFE); /* 单块写起始令牌 */ if (sd_dma_write(buff, 512) != SD_OK) return RES_ERROR; spi_xfer(0xFF); /* 2 字节 CRC,SPI 模式卡不校验但必须发 */ spi_xfer(0xFF); resp = spi_xfer(0xFF); /* 响应格式 xxx0_0101,低 5 位是 0x05 才算收下 */ if ((resp & 0x1F) != 0x05) return RES_ERROR; /* 等 busy:DO 被拉低期间读到的是 0x00 */ t0 = HAL_GetTick(); while (spi_xfer(0xFF) != 0xFF) { if (HAL_GetTick() - t0 > 500) return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (card_state & STA_NOINIT) return RES_NOTRDY; if (!count) return RES_OK; if (!block_addr) sector *= 512; if (count == 1) { if (sd_cmd(24, sector, 0x01, NULL) != 0x00) return RES_ERROR; return sd_write_block(buff); } /* 多块写:先用 ACMD23 预擦除,能明显缩短后面的编程等待 */ sd_cmd(55, 0, 0x01, NULL); sd_cmd(23, count, 0x01, NULL); if (sd_cmd(25, sector, 0x01, NULL) != 0x00) return RES_ERROR; while (count--) { if (sd_write_block(buff) != RES_OK) return RES_ERROR; buff += 512; } /* STOP_TRAN 令牌 0xFD 收尾,后面还得再等一次 busy */ spi_xfer(0xFF); spi_xfer(0xFD); uint32_t t0 = HAL_GetTick(); while (spi_xfer(0xFF) != 0xFF) { if (HAL_GetTick() - t0 > 500) return RES_ERROR; } return RES_OK; }

ACMD23这个预擦除是很多人不知道的一步。SD 卡内部是 NAND,写之前要擦;不告诉它要写多少块,卡只能一边写一边擦,等待时间就长。ACMD23 把"接下来要写 N 块"提前告诉它,卡可以一次性擦好。实测在写大文件时能省 15%~30% 的时间,具体幅度看卡。

3.5 disk_ioctl 与 ffconf.h:几条直接影响成败的开关

disk_ioctl里必实现的是三个命令:

命令作用没实现的后果
CTRL_SYNC确保写缓存落盘一般没大事,但f_sync会失效
GET_SECTOR_COUNT返回总扇区数f_mkfs算错容量,f_getfree返回乱数
GET_BLOCK_SIZE返回擦除块大小(扇区数)格式化对不齐,写速掉一半
GET_SECTOR_SIZE返回 512只在FF_MAX_SS != FF_MIN_SS时要

GET_SECTOR_COUNT的值从 CSD 寄存器算:发 CMD9,收到 16 字节 CSD,v2 卡(CSD_STRUCTURE == 1)的容量 = (C_SIZE + 1) × 512KB,C_SIZE 在 bit 69:48;v1 卡要用 READ_BL_LEN 和 C_SIZE_MULT 三个字段算,麻烦且现在基本遇不到。工程上更省事的做法是在disk_initialize里读一次 CSD 缓存起来,disk_ioctl直接返回缓存值,不要每次现读——f_getfree会反复调它。

ffconf.h是第二个高发区。下面这几个宏建议按需求逐条确认:

宏建议值说明
FF_FS_READONLY0要写就设 0,设 1 时disk_write整个不会被调用
FF_FS_MINIMIZE0 或 10 保留f_lseek,随机写必须有;只要顺序追加可以设 1
FF_USE_MKFS按需只在设备端格式化时开,开一次多几 KB Flash
FF_USE_FASTSEEK1随机访问多的场景必开,靠簇链缓存跳过 FAT 遍历
FF_USE_LFN10=不支持长名,1=静态缓冲(占FF_MAX_LFN*2+2字节 RAM),2=栈上,3=堆上
FF_MAX_LFN255中文名多的话要给足
FF_CODE_PAGE936中文文件名必须改,默认 437 会出乱码
FF_FS_TINY1(RAM 紧)1 时省掉 512 字节 file 对象缓冲,代价是每次写都直接打到扇区缓冲
FF_VOLUMES1多卡才往上加
FF_MAX_SS/FF_MIN_SS都设 512两个相等时 FatFS 会省掉一层动态分配

FF_FS_TINY这一项值得单独说:设成 1 省 512 字节 RAM,但f_write的数据会直接进扇区窗口,写不满一个扇区的时候就要"读-改-写"整个 512 字节。对连续大块写没影响,对小包高频写就是灾难,写速能掉到原来的三分之一。

4. 吞吐优化:单块写的瓶颈其实不在 SPI 时钟上

4.1 把一次单块写拆开算

很多人第一反应是把 SPI 时钟从 10.5MHz 拉到 21MHz,觉得写速能翻倍。先算笔账,21MHz 下每字节约 0.38µs:

阶段字节数21MHz 耗时
CMD24 命令帧62.3µs
R1 响应 + NCR~20.8µs
起始令牌10.4µs
数据512195µs
CRC20.8µs
数据响应令牌10.4µs
小计(纯 SPI)约 200µs
卡内部编程 busy—1~5ms

结论很直白:数据本身只占 200µs,而卡内部编程要 1~5ms,占到整个事务的 85%~96%。把时钟从 10.5MHz 提到 21MHz,省下的是 200µs 那一半,也就是 100µs,对 3ms 的总时间来说只有 3% 的改善。

真正的杠杆有三个:减少 busy 次数、减少命令开销、把 CPU 从等待里解放出来。前者靠多块写,中者靠一次写更多扇区,后者靠 DMA。

这也解释了一个常见困惑:为什么有人换了高速卡写速还是没变?因为瓶颈在协议开销和卡的编程策略上,卡本身快不快只影响那 1~5ms 的绝对长度。

4.2 多块写 + 前端缓冲:把 512 字节变成 8KB

FatFS 的f_write在数据量超过一个扇区时会连续调disk_write,但每次传的 count 不一定大。默认配置下,写 32KB 数据,FatFS 可能分成 8 次disk_write(..., count=8)调用,也可能因为缓冲区切换变成 64 次count=1。所以要在disk_write里做分流:count >= 4走 CMD25 多块写路径,count小走 CMD24 单块。

另一个层面是应用侧的缓冲。如果业务是"每采一个点就f_write一次",那 FatFS 每次都在做读-改-写,效率极低。正确做法是攒够整扇区的倍数再写:

#define FILE_BUF_SECTORS 16 /* 8KB */ static uint8_t file_buf[FILE_BUF_SECTORS * 512] __attribute__((aligned(4))); static uint32_t file_buf_len; static FRESULT flush_buf(FIL *fp) { UINT bw; FRESULT fr; if (!file_buf_len) return FR_OK; fr = f_write(fp, file_buf, file_buf_len, &bw); if (fr != FR_OK || bw != file_buf_len) return FR_DISK_ERR; file_buf_len = 0; /* 只有全部写成功才清空,否则下次重试会丢数据 */ return FR_OK; } /* 采集回调里只管往内存里攒,不碰文件系统 */ void on_sample(const void *data, uint32_t len) { if (file_buf <p> <a href="https://download.csdn.net/download/jiang_milner/12660363" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>