HC32 SD 卡读写 — 擦除、写入与读回校验

HC32F460 SDIOC 实验:4 线 50MHz 高速模式初始化 SD 卡,擦除 4 个扇区并验证擦除结果,写入递增数据后读回用 memcmp 校验,LED 指示测试结果。

SD 卡读写:擦除、写入与读回校验

源码路径:example/HC32F460JEUA/6.sdioc/sd_multi_write_multi_read_4bit_50mhz/source/main.c

1. 功能简介

这个例子做的是一次完整的 SD 卡读写自检,就像出厂测试程序一样,一步步验证卡到底能不能正常用。

程序流程(上电自动跑一遍,不需要按键):

  1. 初始化 SD 卡:4 线总线 + 50MHz 高速模式。
  2. 擦除从第 0 个扇区开始的 4 个扇区。
  3. 读回来检查擦除结果:擦除后的数据应该全是 0xFFFFFFFF0x00000000(不同卡的擦除填充值不同)。如果不是,说明擦除没成功。
  4. 写入数据:往这 4 个扇区写入 0、1、2、3……递增的数字(这种「有规律的数据」最容易看出错在哪)。
  5. 再读回来
  6. memcmp 比对写进去的和读出来的。
  7. 全部通过亮绿灯,任何一步失败亮红灯,然后进死循环。

只要看到绿灯,就说明「SD 卡插好了、驱动没问题、读写都正常」。这是玩 SD 卡的第一步——先把底层通道验通,再谈上面的文件系统。


2. 硬件准备

用 HC32F460JEUA KIT 开发板,板载有 TF 卡(microSD)卡座,插卡即用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        HC32F460                          TF / SD 卡座
      ┌───────────┐                     ┌──────────────┐
      │ PB15  CK  ├────────────────────▶│ CLK          │
      │ PA6   CMD ├◀───────────────────▶│ CMD          │
      │ PB4   D0  ├◀───────────────────▶│ DAT0         │
      │ PA8   D1  ├◀───────────────────▶│ DAT1         │
      │ PA9   D2  ├◀───────────────────▶│ DAT2         │
      │ PB5   D3  ├◀───────────────────▶│ DAT3 / CS    │
      │ PH2   CD  ├◀────────────────────│ 卡检测(可选)│
      └───────────┘                     └──────────────┘
                                            3.3V 供电

准备工作

  • 一张 TF / microSD 卡(建议 8GB 或 16GB 的正品卡,容量太大或杂牌卡容易初始化失败)。
  • 卡插到底、听到「咔」的一声。

⚠️ 重要警告 这个程序会擦除并覆写 SD 卡第 0~3 号扇区! **第 0 号扇区就是 MBR(主引导记录),存着分区表。**覆写它等于把卡「格式化坏了」——插到电脑上会提示「需要格式化」。 千万别用存着重要资料的卡跑这个例子! 用一张空白的测试卡。 测完想恢复正常使用,在电脑上重新格式化一次就好。


3. 完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include "sd_card.h"              // SD 卡驱动库(内部已包含 hc32_ddl.h)

/* ---------- SDIOC 引脚定义 ---------- */
#define SDIOC_CD_PORT    (PortH)  /* 卡检测(本例未启用)*/
#define SDIOC_CD_PIN     (Pin02)

#define SDIOC_CK_PORT    (PortB)  /* 时钟 */
#define SDIOC_CK_PIN     (Pin15)

#define SDIOC_CMD_PORT   (PortA)  /* 命令线 */
#define SDIOC_CMD_PIN    (Pin06)

#define SDIOC_D0_PORT    (PortB)  /* 数据线 0 */
#define SDIOC_D0_PIN     (Pin04)
#define SDIOC_D1_PORT    (PortA)  /* 数据线 1 */
#define SDIOC_D1_PIN     (Pin08)
#define SDIOC_D2_PORT    (PortA)  /* 数据线 2 */
#define SDIOC_D2_PIN     (Pin09)
#define SDIOC_D3_PORT    (PortB)  /* 数据线 3 */
#define SDIOC_D3_PIN     (Pin05)

/* 测试范围:从第 0 个扇区开始,共 4 个扇区 */
#define SD_SECTOR_START  (0u)
#define SD_SECTOR_COUNT  (4u)

/* 用 SDIOC 单元 1 */
#define SDIOC_UNIT       (M4_SDIOC1)

/*==============================================================
 * 把 7 根引脚全部复用为 SDIO 功能
 *=============================================================*/
static en_result_t SdiocInitPins(void)
{
    PORT_SetFunc(SDIOC_D0_PORT,  SDIOC_D0_PIN,  Func_Sdio, Disable);
    PORT_SetFunc(SDIOC_D1_PORT,  SDIOC_D1_PIN,  Func_Sdio, Disable);
    PORT_SetFunc(SDIOC_D2_PORT,  SDIOC_D2_PIN,  Func_Sdio, Disable);
    PORT_SetFunc(SDIOC_D3_PORT,  SDIOC_D3_PIN,  Func_Sdio, Disable);
//  PORT_SetFunc(SDIOC_CD_PORT,  SDIOC_CD_PIN,  Func_Sdio, Disable);  /* 卡检测未用 */
    PORT_SetFunc(SDIOC_CK_PORT,  SDIOC_CK_PIN,  Func_Sdio, Disable);
    PORT_SetFunc(SDIOC_CMD_PORT, SDIOC_CMD_PIN, Func_Sdio, Disable);

    return Ok;
}

/*==============================================================
 * 主函数
 *=============================================================*/
int32_t main(void)
{
    uint32_t i;
    en_result_t enTestResult = Ok;                 /* 测试结果累积标志 */

    static stc_sd_handle_t stcSdhandle;            /* SD 卡句柄 */

    /* 写缓冲区:512 个 uint32_t = 2048 字节 = 正好 4 个扇区(4×512)*/
    static uint32_t au32WriteBlocks[512];
    /* 读缓冲区:和写缓冲区一样大 */
    static uint32_t au32ReadBlocks[ARRAY_SZ(au32WriteBlocks)];

    /* SD 卡初始化配置:4 线、50MHz、高速模式、不用 DMA */
    stc_sdcard_init_t stcCardInitCfg =
    {
        SdiocBusWidth4Bit,       /* 4 位总线宽度 */
        SdiocClk50M,             /* 50MHz 时钟 */
        SdiocHighSpeedMode,      /* 高速模式 */
        NULL,                    /* DMA 配置为空 → 用轮询方式传输 */
    };

    /* 准备写入的数据:填 0,1,2,3,... 递增值,方便看出错在哪一位 */
    for (i = 0u; i < ARRAY_SZ(au32WriteBlocks); i++)
    {
        au32WriteBlocks[i] = i;
    }

    MEM_ZERO_STRUCT(au32ReadBlocks);      /* 读缓冲区清零 */

    /* BSP 初始化 */
    BSP_CLK_Init();
    BSP_LED_Init();

    /* 关掉 JTAG 调试脚复用(TDI/TDO/TRST 和 SDIOC 引脚有冲突)*/
    PORT_DebugPortSetting(TDI, Disable);
    PORT_DebugPortSetting(TDO_SWO, Disable);
    PORT_DebugPortSetting(TRST, Disable);

    SdiocInitPins();                       /* 配置 SDIOC 引脚 */

    /* ===== ① 初始化 SD 卡 ===== */
    stcSdhandle.SDIOCx = SDIOC_UNIT;
    if (Ok != SDCARD_Init(&stcSdhandle, &stcCardInitCfg))
    {
        enTestResult = Error;
    }

    /* ===== ② 擦除 4 个扇区(超时给 20000)===== */
    if (Ok != SDCARD_Erase(&stcSdhandle, SD_SECTOR_START, SD_SECTOR_COUNT, 20000u))
    {
        enTestResult = Error;
    }

    /* ===== ③ 读回来,验证擦除是否真的成功 ===== */
    if (Ok != SDCARD_ReadBlocks(&stcSdhandle, SD_SECTOR_START, SD_SECTOR_COUNT,
                                (uint8_t *)au32ReadBlocks, 2000u))
    {
        enTestResult = Error;
    }

    /* 擦除后每个字应该是全 F 或全 0(不同卡的填充值不一样,两种都算合格)*/
    for (i = 0u; i < ARRAY_SZ(au32WriteBlocks); i++)
    {
        if ((au32ReadBlocks[i] != 0xFFFFFFFFul) &&
            (au32ReadBlocks[i] != 0x00000000ul))
        {
            enTestResult = Error;
            break;
        }
    }

    /* ===== ④ 写入数据 ===== */
    if (Ok != SDCARD_WriteBlocks(&stcSdhandle, SD_SECTOR_START, SD_SECTOR_COUNT,
                                 (uint8_t *)au32WriteBlocks, 2000u))
    {
        enTestResult = Error;
    }

    /* ===== ⑤ 再读回来 ===== */
    if (Ok != SDCARD_ReadBlocks(&stcSdhandle, SD_SECTOR_START, SD_SECTOR_COUNT,
                                (uint8_t *)au32ReadBlocks, 20000u))
    {
        enTestResult = Error;
    }

    /* ===== ⑥ 比对写进去的和读出来的 ===== */
    if (0 != memcmp(au32WriteBlocks, au32ReadBlocks, sizeof(au32ReadBlocks)))
    {
        enTestResult = Error;
    }

    /* ===== ⑦ 用 LED 显示最终结果 ===== */
    if (Ok == enTestResult)
    {
        BSP_LED_On(LED_GREEN);    /* 全部通过 → 绿灯 */
    }
    else
    {
        BSP_LED_On(LED_RED);      /* 有任何一步失败 → 红灯 */
    }

    while (1)
    {
        /* 测完就停在这里 */
    }
}

4. 代码分段讲解

4.1 缓冲区大小和扇区数必须严格对应

1
2
static uint32_t au32WriteBlocks[512];   // 512 × 4 字节 = 2048 字节
#define SD_SECTOR_COUNT   (4u)          // 4 × 512 字节 = 2048 字节  ✓ 相等

这是 SD 卡编程的第一要点。SD 卡按 512 字节扇区读写,你要读 4 个扇区,缓冲区就必须至少有 4×512 = 2048 字节。

这里 uint32_t[512] 刚好是 2048 字节,完美对应。

如果你改成读 8 个扇区却不改数组大小,SDCARD_ReadBlocks 会往数组外面写 2048 字节——栈/内存直接被踩坏,程序表现会极其诡异(随机崩溃、其他变量莫名变化)。这种 bug 非常难查,所以两个数字一定要一起改

安全的写法是让扇区数从数组大小推导出来:

1
#define SD_SECTOR_COUNT  (sizeof(au32WriteBlocks) / 512u)

4.2 为什么要关掉 JTAG 调试脚

1
2
3
PORT_DebugPortSetting(TDI, Disable);
PORT_DebugPortSetting(TDO_SWO, Disable);
PORT_DebugPortSetting(TRST, Disable);

芯片刚上电时,一部分引脚默认是 JTAG 调试功能(TDI、TDO、TRST)。而 SDIOC 用到的 PA8、PA9、PB4 这些引脚,正好和 JTAG 脚有复用冲突。

不关掉的话,你 PORT_SetFunc 设成 SDIO 功能也不生效——引脚还被 JTAG 占着。这就是「代码看着完全正确但外设死活不工作」的又一个经典原因。

注意关掉这些脚会影响 JTAG 调试(SWD 调试不受影响,因为 SWD 只用 SWCLK/SWDIO)。所以正常用 SWD 下载调试没问题。

4.3 错误累积的写法

1
2
3
4
5
6
en_result_t enTestResult = Ok;

if (Ok != SDCARD_Init(...))  { enTestResult = Error; }
if (Ok != SDCARD_Erase(...)) { enTestResult = Error; }
...
if (Ok == enTestResult) { 绿灯 } else { 红灯 }

注意这里的设计:每一步失败都只是标记一下,不立刻退出,最后统一看结果。

好处是不管中间哪一步出错,最终都会有明确的 LED 指示。缺点是初始化失败后还会继续跑后面的读写(这些必然也失败),有点浪费。

工程实践中更常见的写法是失败立即返回(early return),或者用 goto error_handler 跳到统一的错误处理。这样逻辑更清晰、也不会在坏状态下继续操作硬件。

4.4 擦除后的验证很有讲究

1
2
3
4
5
if ((au32ReadBlocks[i] != 0xFFFFFFFFul) &&
    (au32ReadBlocks[i] != 0x00000000ul))
{
    enTestResult = Error;
}

为什么要接受两种值?因为 SD 卡擦除后的填充值不是统一标准——有的卡擦成全 1(0xFF),有的卡擦成全 0(0x00)。这取决于卡内部控制器的实现。

所以代码宽容地认为「全 F 或全 0 都算擦干净了」。这是很实用的兼容性处理,说明写代码的人真正踩过不同品牌卡的坑。

4.5 用递增数据做测试的妙处

1
2
3
4
for (i = 0u; i < ARRAY_SZ(au32WriteBlocks); i++)
{
    au32WriteBlocks[i] = i;      // 0, 1, 2, 3, 4, ...
}

为什么不用随机数、不用全 0?因为递增序列是最好的调试数据

  • 如果读回来的是 0,1,2,3,... 但从中间某处开始错乱,你一眼就能看出错在第几个字
  • 如果整体偏移了(读回来是 128,129,130...),说明起始扇区算错了
  • 如果每 128 个字重复一次,说明扇区边界处理有问题

用全 0 或全 F 就完全看不出这些规律。测试存储器一定要用有规律但不重复的数据,这是个很实用的技巧。

4.6 超时参数为什么不一样

1
2
3
4
SDCARD_Erase(..., 20000u);       // 擦除给 20000
SDCARD_ReadBlocks(..., 2000u);   // 读给 2000
SDCARD_WriteBlocks(..., 2000u);  // 写给 2000
SDCARD_ReadBlocks(..., 20000u);  // 这次读又给 20000

因为不同操作耗时差异很大:擦除 » 写 > 读。擦除是 SD 卡最慢的操作,超时必须给得宽松,否则明明在正常工作却被判超时失败。

(代码里两次读的超时值不一致,属于示例的随意之处,实际用统一给足即可。)

4.7 ARRAY_SZMEM_ZERO_STRUCT

这两个是 ddl 库的实用宏:

1
2
ARRAY_SZ(arr)          // 求数组元素个数 = sizeof(arr)/sizeof(arr[0])
MEM_ZERO_STRUCT(x)     // 把 x 清零 = memset(&x, 0, sizeof(x))

ARRAY_SZ 而不是写死 512,是好习惯——以后改数组大小,循环次数自动跟着变,不会漏改。


5. 实验现象

插好 TF 卡,上电或复位:

  • 绿灯亮 → 全部测试通过 ✓(初始化 OK、擦除 OK、写入 OK、读回数据完全一致)
  • 红灯亮 → 某一步失败 ✗

程序跑完就停在 while(1),灯的状态保持不变。想再测一次就按复位键。

测试完成后,这张卡的第 0~3 扇区已经被写成递增数据了。插到电脑上会提示「需要格式化」——这是正常的,因为 MBR 被覆盖了。重新格式化即可恢复。


6. 常见问题

Q1:一直红灯,怎么定位到底哪一步失败? 最有效的办法是加串口打印。先初始化串口,然后:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
DDL_PrintfInit(BSP_PRINTF_DEVICE, BSP_PRINTF_BAUDRATE, BSP_PRINTF_PortInit);

if (Ok != SDCARD_Init(&stcSdhandle, &stcCardInitCfg))
{
    printf("SDCARD_Init failed!\r\n");
    enTestResult = Error;
}
if (Ok != SDCARD_Erase(...))
{
    printf("Erase failed!\r\n");
    enTestResult = Error;
}
// ... 每一步都加

这样串口助手里直接告诉你卡在哪儿。这就是串口章节说的「打印大法」的实战价值。

Q2:SDCARD_Init 就失败了? 最常见,按顺序查:

  • 卡插到底了吗? 听到「咔」声才算到位。
  • PORT_DebugPortSetting 三行写了吗? 漏了 JTAG 会占着 PA8/PA9 等引脚。
  • 换张卡试试。 杂牌卡、容量特别大(64GB 以上 SDXC)的卡兼容性差,初始化协议可能不支持。建议用 8GB/16GB 正品 Class10 卡。
  • 降速试试:把 SdiocClk50M 改成更低的时钟、SdiocHighSpeedMode 改成 SdiocNormalSpeedMode。50MHz 对信号质量要求高,杜邦线飞线基本跑不起来。
  • 先用 1 线模式SdiocBusWidth4Bit 改成 SdiocBusWidth1Bit。能通说明是 D1/D2/D3 那几根线的问题。

Q3:初始化过了,但擦除或读写失败?

  • 超时给太小了。 试着把超时参数加大 10 倍。
  • 降低时钟频率。速度越快越容易出错。
  • 检查 D1/D2/D3 三根线(只有 4 线模式才用到它们,1 线模式下不影响初始化但影响读写)。

Q4:memcmp 比对失败,但读写函数都返回 Ok?

  • 说明数据传输过程中出错了(信号质量问题)。降低时钟频率是第一手段。
  • 也检查缓冲区大小和扇区数是否匹配(见 4.1)。

Q5:我的卡插上后电脑不认了!

  • 这是预期行为,因为程序覆写了第 0 扇区(MBR)。在电脑上重新格式化即可恢复。
  • 想避免破坏文件系统,把 SD_SECTOR_START 改成一个很大的值(比如 100000u),去测卡的中后段区域,就不会碰到 MBR 和文件系统的关键结构了。

Q6:怎么让读写更快?

  • DMA 版本。把 stcCardInitCfg 最后那个 NULL 换成 DMA 配置,CPU 就不用参与搬数据了。SDK 里有现成的 sd_multi_write_multi_read_4bit_dma_50mhz 示例。
  • 一次多读几个扇区。读 100 个扇区比读 100 次单扇区快得多(命令开销省下来了)。

7. 小结

  • SD 卡按 512 字节扇区读写,缓冲区大小和扇区数必须严格对应,否则内存越界。
  • SDCARD_Init / Erase / WriteBlocks / ReadBlocks 四个函数,每个都带超时参数(擦除要给大)。
  • 必须先关 JTAG 调试脚复用PORT_DebugPortSetting),否则引脚被占用。
  • 擦除后的值可能是全 F 或全 0,两种都要当成正常。
  • 测存储器用递增数据,最容易定位错在哪。
  • 出问题按这个顺序试:降时钟 → 换 1 线 → 换卡 → 加串口打印定位
  • 本例会覆写第 0 扇区(MBR),用测试卡,别用有重要数据的卡

8. 拓展应用

底层通道验通之后,SD 卡真正的威力才开始:

  • 移植 FatFS 文件系统(最重要的下一步) 把 SDCARD_ReadBlocks / WriteBlocks 接到 FatFS 的 disk_read / disk_write 接口上,你就能这样写代码:

    1
    2
    3
    
    f_open(&file, "data.csv", FA_WRITE | FA_CREATE_ALWAYS);
    f_printf(&file, "%d,%d\r\n", time, temperature);
    f_close(&file);
    

    生成的文件电脑能直接打开! 这是 SD 卡最大的价值所在——数据不再是裸扇区,而是人能看懂的文件。

  • 数据记录仪 / 数据采集器 传感器数据按时间戳存成 CSV 文件,拔卡插电脑,Excel 直接打开画曲线。温湿度记录、电池测试、振动监测都这么做。

  • 黑匣子 / 运行日志 设备运行状态、错误码持续写入日志文件。出故障了把卡拔出来分析,比接串口盯着看实用得多。

  • 音频播放器 WAV 文件存卡里,用 FatFS 读出 PCM 数据送给 DAC 或 I2S 功放,就是个音乐播放器。语音提示、报警音同理。

  • 图片 / 界面资源加载 GUI 的图片、字库、皮肤存在卡里,需要时读出来显示。好处是改素材不用重新编译固件,换张卡就换了界面。

  • 固件升级(OTA / 卡升级) 把新固件 .bin 放到卡里,设备上电时 Bootloader 检测到有升级文件就自动更新。这是工业设备最省事的升级方式——现场维护人员只需要插张卡。

  • 配置文件 设备参数写成卡里的 config.txt,用户用记事本改改就能配置设备,不用专门做上位机软件。非常适合小批量产品。

  • 摄像头存图 接个 OV2640 摄像头,拍照存成 JPG 到卡里,做个简易行车记录仪或监控。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计