从GD32F103到F303:SD卡与FatFS驱动移植实战指南
简介面向GD32嵌入式开发者这份资源提供将GD32F103的SD卡驱动与FatFS文件系统例程移植到GD32F303的完整工程。GD32F103与GD32F303在处理器内核、外设接口和资源占用上存在差异资源给出驱动适配、内存管理和中断服务调整的具体实现可解决外设差异带来的文件读写兼容问题。压缩包共222个文件约4.37MB以C源码.c/.h为主还包含Keil工程文件、启动汇编代码、链接脚本、编译中间文件.o/.d/.crf/.lst/.map及可直接烧录的HEX固件便于对照源码、编译流程和最终输出。已有1206人学习适合正在学习GD32平台或需要快速实现SD卡存储的开发者。工程内含SD卡初始化、文件创建/删除/读写等FatFS核心例程覆盖SPI/SDIO通信、命令结构、寄存器配置、中断处理等底层驱动代码并涉及FAT16/FAT32文件系统的目录结构与簇链机制能帮助理解跨型号移植的适配思路与实用调试方法。1. 从 GD32F103 裸机例程迁到 GD32F303最先出事的永远是 SD 卡拿到一份号称“303空”的工程实际上是某块 GD32F103 开发板上的 SD 卡例程被直接拉到了 GD32F303 上。原始工程里有 ff.c、cc936.c、sdcard.c 和一堆驱动文件编译能过但上板后 f_mount 返回 3FR_NOT_READY或者 f_open 卡在读目录扇区。这类问题九成不在 FatFS 本身而在底层驱动没有跟上新芯片的时钟树、外设总线映射和 SDIO/SPI 引脚配置。GD32F303 虽然和 GD32F103 同为 Cortex-M3 核心但主频从 108MHz 提到 120MHz外设时钟树从单一的 AHB/APB 分频变成了可独立配置的上游时钟源直接导致原本在 F103 上能跑的 sdcard.c 里的延时、分频系数全部失效。这篇文章就是从这份工程里拆出来的移植笔记适合正在把老 GD32F103 工程往 F303 上搬又绕不开 SD 卡和 FatFS 的人。2. 硬件差异与外设映射SDIO/SPI 到底接在哪2.1 GD32F103 与 GD32F303 的外设端口差异GD32F103 的 SDIO 外设挂在 APB2 上而 GD32F303 的 SDIO 挂在 AHB 上两者访问地址和时钟使能寄存器完全不同。如果你原来的驱动习惯性操作RCU_APB2EN去打开 SDIO 时钟在 F303 上这个位段可能对应的是别的外设后果是寄存器访问无效SDIO 状态永远停在没上电的状态。另一个显著差异是引脚复用逻辑F103 用GPIO_AFIOGPIO_PinRemapConfigF303 改成了GPIO_AFgpio_af_set。这两套 API 的映射表对不上SDIO 的时钟线和数据线根本没有连接到控制器上去。我一般会在移植前先整理一份引脚和外设时钟对照表把 F103 和 F303 的 SDIO 引脚、复用功能号、时钟源列出来如下表所示信号GD32F103 引脚/复用GD32F303 引脚/复用时钟域SDIO_CKPC12 / AFIO 默认PC12 / AF12F103: APB2; F303: AHBSDIO_CMDPD2 / AFIO 默认PD2 / AF12同上SDIO_D0PC8 / AFIO 默认PC8 / AF12同上SDIO_D1PC9 / AFIO 默认PC9 / AF12同上SDIO_D2PC10 / AFIO 默认PC10 / AF12同上SDIO_D3PC11 / AFIO 默认PC11 / AF12同上查芯片参考手册的 alternate function 表时注意 F303 的 AF12 是 SDIO 专用不要想当然沿用 F103 的默认复用。SD 卡的 D3 引脚还要特别注意如果 D3 被用作卡检测或者 SPI 片选会造成 SDIO 模式无法进入 4-bit 模式。2.2 时钟树配置从 108MHz 到 120MHz 的分频计算SDIO 时钟最大一般不能超过 SD 卡允许的 25MHz初始化阶段和 50MHz高速模式。GD32F303 的 SDIO 时钟源是 AHBAHB 最高 120MHz必须通过SDIO_CLKCR[17:16]的分频位把时钟降下来。F103 时代常用的 72MHz/324MHz 分频到 F303 上就变成了 120MHz/340MHz超过了普通 SDHC 卡在初始化阶段的时钟上限卡不回应 CMD0 就是正常的。下面是 F303 上初始化 SDIO 时钟和引脚的典型配置直接放在sdcard_hw_init()里void sdcard_hw_init(void) { rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_GPIOD); rcu_periph_clock_enable(RCU_SDIO); gpio_af_set(GPIOC, GPIO_AF_12, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12); gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_2); gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12); gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_2); gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_12); gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_2); sdio_deinit(); sdio_clock_config(SDIO_CLKCR_CLKEDGE_RISING, SDIO_CLKCR_BYPASS_OFF, SDIO_CLKCR_CLKDIV_3); sdio_widetbus_config(SDIO_BUSWIDTH_1B); sdio_power_state_set(DISABLE); sdio_power_state_set(ENABLE); }代码里SDIO_CLKCR_CLKDIV_3对应 120MHz/(23)24MHz这和 F103 上 72MHz/(21)24MHz 是等价的结果。注意 F303 固件库的SDIO_CLKCR_CLKDIV宏从 0 开始计实际分频是HCLK/(2CLKDIV)不要照着 F103 的SDIO_CLKCR_CLKDIV_2硬抄。gpio_output_options_set只对推挽输出信号有效数据线 D0-D3 是双向开漏不设置这一项也没关系但 CK 和 CMD 必须保证推挽能力。2.3 初始化时序先慢后快的上电流程SD 卡上电后必须先发至少 74 个时钟周期的空时钟让卡内部电压稳定。很多从 F103 搬过来的代码直接跑初始化流程省掉了这串时钟或者用delay_ms(1)代替。在 F303 上如果主频提高导致delay_ms实际变短这 74 个时钟可能不够卡会一直处于 unidentified 状态。推荐用下面的方式产生上电时钟void sdcard_power_on_clock(void) { uint32_t i; sdio_clock_config(SDIO_CLKCR_CLKEDGE_RISING, SDIO_CLKCR_BYPASS_OFF, SDIO_CLKCR_CLKDIV_15); sdio_power_state_set(ENABLE); for (i 0; i 80; i) { (void)sdio_flag_get(SDIO_FLAG_TXBUFFE); } sdio_clock_config(SDIO_CLKCR_CLKEDGE_RISING, SDIO_CLKCR_BYPASS_OFF, SDIO_CLKCR_CLKDIV_3); }这里的CLKDIV_15把初始化前窗口压到 120MHz/17≈7MHz比普通卡的 400kHz 高一点但对绝大多数卡够用如果卡还是没反应就把CLKDIV_127降到 1MHz 以下。读取 TXBUFFE 标志只是为了产生可见的寄存器读写延迟比delay_ms更可控不依赖 SysTick。3. FatFS 底层 diskio 实现把扇区请求映射到 SDIO3.1 diskio 的五个接口要做什么FatFS 不是驱动它只负责组织和解析 FAT 结构真正的硬件操作全部落在disk_initialize、disk_status、disk_read、disk_write、disk_ioctl五个函数上。移植时把这五个函数填对了ff.c 不需要改一行。很多下载例程把 SD 卡命令直接写在 ff.c 里这是错误示范。正规做法是在 sdcard.c 中实现sd_initialize()、sd_read_sector()、sd_write_sector()再在 diskio.c 里薄薄包一层。我在移植这份工程时直接把原来 F103 的 sdcard.c 里以SDIO_ReadData为名字的函数逐一改成带sd_前缀然后对齐参数类型。FatFS 的disk_read签名是DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)这里buff必须指向 4 字节对齐的内存因为 SDIO 的 FIFO 要求字对齐读取。如果你看到HardFault出现在读取代码中先怀疑传入缓冲区是不是一个字节对齐的结构体成员。3.2 读扇区实现命令加数据的老套路SDIO 读单块数据的流程分三步发送 CMD17、等待 DCRC 和 DTO、从 FIFO 读走 512 字节。下面是基于 GD32F303 固件库的读扇区代码注意我用的是SDIO_CMD_CMD17这类宏uint8_t sd_read_sector(uint32_t sector, uint8_t *buff) { sdio_cmd_struct cmd; if (sdio_flag_get(SDIO_FLAG_TXBUFFE) RESET) return 1; cmd.cmd_index SDIO_CMD_CMD17; cmd.cmd_argument sector; cmd.cmd_response SDIO_RESP_SHORT; cmd.cmd_wait SDIO_WAIT_NO; cmd.cmd_crc ENABLE; sdio_cmd_send(cmd); sdio_cmd_attact(SDIO_WAIT_SHORT); while (sdio_flag_get(SDIO_FLAG_RXBUFFE) RESET); for (uint16_t i 0; i 128; i) { *((uint32_t *)buff i) sdio_data_receive(); } sdio_flag_clear(SDIO_FLAG_DTO); return 0; }这段代码有一个隐患没有检查SDIO_STAT_DCRC错误标志。如果卡片返回的数据 CRC 校验失败循环会因为 RXBUFFE 上不来而卡死。实际项目里应该在循环里加超时超过 100ms 就清空 FIFO 并返回读错误。sdio_data_receive()每次返回 32 位数据128 次正好读完 512 字节所以buff必须能容纳至少 512 字节并且要允许被强转成uint32_t *。写扇区用的是 CMD24流程对称差别是写完后要等卡退出 busy 状态。很多例程漏了这一步f_write 返回 OK 但重启后数据是旧的。等待 busy 的伪代码如下发送 CMD24 后循环读取SDIO_STAT_RXDAV直到卡在数据线上拉高。3.3 关键命令的参数与返回值SD/SDHC 卡初始化命令序列看起来繁琐但对应关系固定。下面整理了一份移植时高频用到的命令表参数直接抄命令参数响应用途CMD00x00000000R1 (0x01)复位卡进入 idleCMD80x000001AAR7确认 SDHC 电压范围ACMD410x40000000R3 (OCR)上电初始化HCS 位置 1CMD20R2 (CID)读卡识别信息CMD30R6 (RCA)获取相对地址CMD17扇区地址R1读单块CMD24扇区地址R1写单块CMD550x00000000R1告诉卡下一条是应用命令ACMD41 要反复发送直到响应中的 ready bitbit31为 1。我在移植时踩过一个坑F103 例程在循环里加了delay_us(100)到 F303 上这段延时因为时钟变快而变短卡还没准备好就跳出了循环导致 CMD2 一直拿不到 CID。解决办法是把延时改成固定for (i0; i1000; i);这种基于指令周期的空转或者在 RTOS 下用vTaskDelay(1)。GD32F303 和 F103 的delay_us底层实现并不一致不要跨平台假设精度。4. 挂载失败与读写异常从错误码反推驱动问题4.1 f_mount 返回码的含义FatFS 的f_mount不是挂载设备而是注册一个 FATFS 工作区真正的卷挂载发生在第一次f_open或f_getfree时。如果返回FR_NOT_READY(3)问题在disk_status或disk_initialize流程根本没走到读扇区。返回FR_NO_FILESYSTEM(13)则说明初始化OK也读到了扇区但 boot sector 里没有被识别的 FAT 标志。这两种错误对应完全不同的排查方向。FRESULT fr f_mount(fs, 0:, 1); if (fr FR_NOT_READY) { // 查 sd_initialize 返回值打印 CMD0/ACMD41 状态 } else if (fr FR_NO_FILESYSTEM) { // 查读到的扇区 0 的最后两字节是否 0x55 0xAA }实际移植中最常见的是第一类错误。我习惯把sd_initialize里每一条命令的原始响应用串口打印出来格式如下这样能在半分钟里定位到具体是上电时钟问题还是 CMD8 参数错误printf(CMD0 R10x%02x\r\n, r1); printf(CMD8 R30x%08x\r\n, r3); printf(ACMD41 R30x%08x\r\n, ocr);正常流程中 CMD0 的 R1 应为 0x01CMD8 的 R7 低 12 位应为 0x1AA。如果你看到的是 0x05说明 CRC 校验失败检查cmd_crc是否置位如果全部读到 0xFF说明 SDIO 时钟或引脚复用配置有误回头查第 2 章的表。4.2 DMA 缓存对齐怎么处理Sampled 例程里如果用了 DMA一句“底层没对齐”会出现两个完全不同的症状一是f_read返回 FRESULT OK 但用户缓冲区数据错位二是直接进入 HardFault。GD32F303 的 SDIO DMA 要求数据缓冲区的地址按 4 字节对齐并且传输长度是 4 的倍数。FatFS 默认的FF_FS_MINIMIZE不关闭缓冲对齐所以只能从缓冲区下手。常见做法是在 bss 段定义一个大数组然后让用户缓冲区指向该数组的某个对齐位置。#if defined(__CC_ARM) __align(4) uint8_t sdio_buffer[512]; #else uint8_t sdio_buffer[512] __attribute__((aligned(4))); #endif然后在disk_read中判断传入的buff是否对齐不对齐就先用sdio_buffer接收再 memcpy 给用户。注意 memcpy 会多耗几十微秒但对兼容性和可靠性提升很大。如果你用的是 Keil/AC5__align(4)必须放在变量声明的#if之前否则编译器会忽略对齐属性。4.3 格式化与卡速度等级SD 卡插到 F303 上显示“没有文件”不一定是驱动问题。很多工业级 SD 卡出厂是 exFAT 格式FatFS 默认只支持 FAT12/16/32exFAT 需要额外配置FF_FS_EXFAT。还有一部分杂牌卡标称 32GB实际是 16GB 的卡改了分区信息读取到扇区 0 的容量字段时会得到 0 或者非法值f_mount 也会失败。处理方法是使用 SDFormatter 类的工具重新格式化为 FAT32分配单元大小选 4096 字节不要在 Windows 系统下直接格式化。GD32F303 的 SDIO 驱动对卡的类型非常敏感不支持 SDHC 的老卡ACMD41 里 HCS 位必须置 0否则卡不回 R3 响应。所以移植时要保留一个编译开关把初始化参数同时兼容 SDSC 和 SDHC 两种卡。5. 一个验证技巧把 CMD 响应和 FAT 扇区内容一起打印出来5.1 关键响应逐条打印与其在代码里到处插断点不如做一个sd_debug_report()函数把初始化完成后的卡类型、扇区数、RCA 地址以及引导扇区里的 BPB 信息一次性吐出来。这个函数只用于调试不要放在 release 里。它解决的问题是卡初始化成功了但 f_open 仍然找不到文件这时需要确认disk_read(0, buff, 0, 1)是否真的读到了 FAT32 的引导记录。void sd_debug_report(void) { uint8_t b[512]; if (sd_read_sector(0, b) 0) { printf(Boot sig0x%02x%02x\r\n, b[511], b[510]); printf(BPB sec_per_clus%d\r\n, b[13]); printf(BPB byts_per_sec%d\r\n, b[11] | (b[12] 8)); printf(FAT type%c%c%c\r\n, b[82], b[83], b[84]); } }如果b[510]0x55 b[511]0xAA说明硬件链路完整如果FAT type显示为FAT或EXF说明分区表正常。如果读出的是随机数据问题还是在底层驱动。5.2 用单扇区 DMA 来验证缓存对齐检查系统内存分配情况时最后加一条 512 字节缓冲区的对齐打印printf(align%d\r\n, (uint32_t)sdio_buffer 0x03)。输出 0 说明对齐符合要求。如果输出非 0需要检查链接脚本里的 bss 段起始地址是否满足 4 字节对齐。GD32F303 的链接脚本_sdata和_sbss通常是按 4 字节对齐声明的但如果你手动把一个大数组放到了某个未对齐的 section这个问题就会突然出现。5.3 提高移植成功率的小改动我建议把以下三个配置项写入ffconf.h而不是保持默认值FF_FS_EXFAT设为 0减小内存占用FF_VOLUMES设为 1FF_USE_FASTSEEK设为 0。如果你只是做个日志记录功能还可以打开FF_FS_RPATH0关闭相对路径支持。这些改动把 FatFS 的 RAM 开销压到 1KB 左右对 GD32F303 的 8KB SRAM 更友好。另一个技巧是给 SDIO 初始化失败加一个while(1)循环避免卡在挂载过程导致 WDT 复位后反复重启不过最终产品里记得改成重试逻辑。本文还有配套的精品资源点击获取