STM8用IO模拟SPI读取SD卡:低资源MCU日志存储实践

发布时间:2026/9/3 19:46:56
STM8用IO模拟SPI读取SD卡:低资源MCU日志存储实践 简介这是一份面向嵌入式开发者的STM8 I/O模拟SPI读取SD卡完整工程实现在资源受限的STM8上通过普通GPIO模拟SPI时序与SD卡通信并完成Petit FatFs轻量级文件系统的部分移植支持读取FAT12/16/32文件适用于低成本数据记录、离线采集等场景。压缩包共100个文件以C源码和头文件为主包含stm8s标准外设库、pff.c等FatFs移植代码以及IAR工程配置ewp、ewd、eww、编译输出o、out、hex等文件整体约1.13MB结构清晰便于直接导入学习。已有562人浏览学习。通过分析GPIO模拟SPI的SCK/MISO/MOSI/CS时序控制、SD卡初始化命令流程及Petit FatFs挂载与读取调用可掌握底层硬件模拟协议和文件系统移植的关键思路对STM8嵌入式开发项目具有直接参考价值。 最近把一块 STM8S003F3P6 的 IO 口折腾了一遍用普通 GPIO 模拟 SPI 读取 SD 卡把设备几个月的运行日志从 TF 卡里导了出来。这个需求在低资源单片机上很常见STM8 内核主频不高、Flash 不大很多小家电和传感器采集方案连硬件 SPI 都舍不得占用要接 SD 卡就只能靠 IO 模拟。做完以后我把整个流程、代码和踩坑记录梳理了一下给打算在低资源 MCU 上接 SD 卡的朋友做个参考。先说结论STM8 完全可以用 IO 模拟 SPI 读取 SD 卡。只要时序正确、初始化顺序按规定来读取速度和稳定性都能满足小数据量的日志记录需求。下面从选型、接线、时序、代码、排障几个方面完整拆开讲。1. 为什么选择 IO 模拟 SPI 而不是硬件 SPI1.1 低资源 MCU 接 SD 卡的方案对比STM8 系列里的部分型号确实带了硬件 SPI 外设但实际项目里不见得能用。我这次用的 STM8S003F3P6硬件 SPI 引脚和 SWIM 烧录口靠得很近PCB 布局时为了把烧录座和 SD 卡座分开硬件 SPI 引脚反而被占用了。这种情况下与其重新画板子不如用三个普通 IO 口模拟 SPI这也是低资源 MCU 接 SD 卡最常用、最灵活的路线。对比一下常见方案方案优点缺点适用场景硬件 SPI速度快、CPU 占用低引脚固定、和 SWIM 或其他外设冲突引脚规划比较宽裕的板子IO 模拟 SPI引脚任意、代码复用性强速度有限、占用 CPU小数据量日志记录、低成本方案SDIO 接口吞吐率极高适合音视频STM8 没有这个外设高性能单片机专用IO 模拟 SPI 的另一个好处是软件片选。普通 SPI 多设备连接时要考虑硬件片选和软件片选的差异硬件片选占用引脚少但切换速度受限于外设寄存器软件片选完全由代码控制接多个 SPI 设备时反而灵活很多。SD 卡、SPI Flash、传感器各挂一路 CS只要保证同一时刻只有一个 CS 拉低就行。1.2 SD 卡的 SPI 模式是怎么回事SD 卡默认走的是 SD 总线协议SD 模式下用 1 根命令线加 4 根数据线同一时刻收发时序比较复杂。而 SPI 模式是 SD 卡为兼容微控制器专门提供的一种简化工作模式把命令、读写、响应全部掰成同步串行方式主机只靠 CS、SCK、MOSI、MISO 四根线就能控制卡。网上有人把 SD 卡的 SPI 模式比作两个人打电话时必须等对方说完才能接话命令、响应、数据块严格按顺序轮流出现在同一根线上。这个类比很形象因为 SD 卡在 SPI 模式下根本没有硬件中断通知“我准备好了”主机只能用轮询方式一遍遍发 0xFF 去问卡的状态。这也是 IO 模拟 SPI 的核心思路发一个字节的同时必定收回一个字节发出去的是时钟和命令收回来的是状态和数据。2. 硬件连接与 SPI 时序设计2.1 最少引脚的接线方案我用的 STM8S003 工作在 3.3VSD 卡座也接 3.3V 供电。这里要特别注意SD 卡的数据线和电源绝对不能直接接 5V我第一次用开发板上的 5V 电源给卡座供电结果读卡时好时坏后来查了 SD 卡手册才发现 SPI 模式下所有信号线电压范围就是 2.7V 到 3.6V超过 3.6V 会损伤卡。接线方式如下SD 卡引脚功能连接到 STM8VDD3.3V 电源3.3VVSS地GNDCS片选低有效PD_CSSCLK时钟PD_SCKDI数据输入PD_MOSIDO数据输出PD_MISOCS、SCK、MOSI 三个信号线上最好串一个 33 欧姆电阻防止走线过长时振铃。MISO 和 CS 建议各接一个 10k 上拉到 3.3V因为 STM8 的 GPIO 在输出模式下虽然有推挽能力但 SD 卡座的 DO 在空闲时是高阻态没有上拉电阻很容易读到不确定电平导致响应解析混乱。线不要太长我实测超过 15 厘米后信号完整性明显下降初始化成功率低了很多。SPI 不是长距离通信协议超过 20 厘米就要考虑降低速或者加驱动这一点和串口 UART 完全是两回事。2.2 SPI 四种模式与 SD 卡的匹配学 SPI 协议时都会遇到 CPOL 和 CPHA 这两个参数对应 SPI 的四种模式。CPOL 决定时钟空闲时的电平CPHA 决定数据是在第一个边沿还是第二个边沿采样。SD 卡手册里明确写了 SPI 模式下支持 Mode 0CPOL0、CPHA0和 Mode 3CPOL1、CPHA1也就是时钟极性低还是高都行关键是主机和从机的采样、更新边沿要一致。我用 IO 模拟 SPI建议按 Mode 0 来写也就是 SCK 空闲为低电平第一个时钟沿采集数据。Mode 0 的好处是逻辑简单SCK 拉高、主机把数据放到 MOSI、SCK 拉低整个过程刚好是半个周期即使中间偶尔有延时抖动只要拉高的脉宽和拉低的脉宽都足够稳定卡就能正确识别。这其实就是软件模拟时序的优势硬件 SPI 一旦主频配置出错很容易采错边沿而 IO 模拟可以逐个时钟去控制。2.3 初始化为什么必须慢速时钟SD 卡有个非常容易踩的坑上电初始化阶段时钟频率必须低于 400kHz。原因在于卡片刚上电时内部逻辑还在稳定过程中高速时钟可能导致卡无法识别命令。我一开始图省事直接把延时函数调到最小结果 CMD0 发出去返回的不是 0x01而是 0xFF卡完全没反应。正确流程是上电后先让 CS 为高用慢速时钟给卡送至少 74 个时钟脉冲这是 SD 卡协议里明确要求的“唤醒周期”。然后再把 CS 拉低开始发命令。初始化成功后才可以提高时钟频率我实测 STM8 内部 16MHz 主频下用 IO 模拟 SPI 跑到 1MHz 很稳定再高就会出现偶发读错误。3. 从零开始写代码STM8 模拟 SPI 读 SD 卡3.1 GPIO 模拟 SPI 收发一个字节STM8 的 GPIO 用寄存器操作和其他 MCU 相比非常直接。我先把引脚宏定义和发送一个字节的核心函数写好#define SD_CS_HIGH PD_ODR | (1 3) #define SD_CS_LOW PD_ODR ~(1 3) #define SD_SCK_HIGH PD_ODR | (1 5) #define SD_SCK_LOW PD_ODR ~(1 5) #define SD_MOSI_HIGH PD_ODR | (1 6) #define SD_MOSI_LOW PD_ODR ~(1 6) #define SD_MISO_READ (PD_IDR (1 4)) static void delay_us(void) { // 通过循环空转实现微秒级延时实际值在16MHz主频下约1us __asm(nop); __asm(nop); __asm(nop); } unsigned char SPI_ReadWriteByte(unsigned char byte) { unsigned char i; unsigned char tmp 0; for (i 0; i 8; i) { if (byte 0x80) SD_MOSI_HIGH; else SD_MOSI_LOW; byte 1; SD_SCK_HIGH; delay_us(); tmp 1; if (SD_MISO_READ) tmp | 0x01; SD_SCK_LOW; delay_us(); } return tmp; }这段代码的核心逻辑是“发一位收一位”。每个时钟周期内主机先把 MOSI 上的数据准备好然后拉高 SCK接着马上读 MISO。这里有个初学者经常搞混的点读 SD 卡时并不是只读而是要持续发 0xFF。因为 SD 卡在 SPI 模式下MISO 的数据输出是依赖时钟的你给它几个时钟它就吐出几位数据。所以“SPI 读”本质上是一个全双工过程发出去的 0xFF 只是为了让卡继续向前移出数据。3.2 命令帧构造与 R1 响应解析SD 卡 SPI 模式的命令格式非常统一固定是 6 个字节第一个字节高两位是 01低 6 位是命令号中间 4 个字节是参数按大端序排列最后一个字节是 CRC7 校验。SD 卡在 SPI 模式下默认不检查除 CMD0、CMD8 以外的命令 CRC所以要计算 CRC 的命令只有前面这两条。发送命令时要先拉低 CS然后先发一个 0xFF 填充时钟再发送完整的 6 字节命令。代码如下unsigned char SD_SendCmd(unsigned char cmd, unsigned int arg, unsigned char crc) { unsigned char res; unsigned char t; SD_CS_LOW; SPI_ReadWriteByte(0xFF); // 发送前先给一个时钟 SPI_ReadWriteByte(cmd | 0x40); // 命令号 SPI_ReadWriteByte((unsigned char)(arg 24)); SPI_ReadWriteByte((unsigned char)(arg 16)); SPI_ReadWriteByte((unsigned char)(arg 8)); SPI_ReadWriteByte((unsigned char)arg); SPI_ReadWriteByte(crc); // R1响应格式最高位为0表示命令被接受其他位是状态标志 for (t 0; t 16; t) { res SPI_ReadWriteByte(0xFF); if (!(res 0x80)) break; } SD_CS_HIGH; return res; }循环最多读 16 个字节是因为卡准备响应的时间不确定前面的 0xFF 是过渡状态。一旦读到一个最高位不是 1 的字节就说明这是真正的 R1 响应。R1 响应里几个关键的位0x00 表示卡初始化完成且命令执行成功0x01 表示卡处于 idle 状态也就是说初始化还没完成0x05 表示非法命令通常是 SD V1 卡不支持某个命令。实际操作中把返回值和这些标志比对就能快速定位问题。3.3 初始化流程CMD0、CMD8、ACMD41 的顺序不能乱SD 卡初始化的规矩比一般 SPI 从机严格得多。我把实际可用的初始化流程贴出来unsigned char SD_Init(void) { unsigned char res; unsigned char i; // 上电后 CS 拉高先送 80 个时钟脉冲 SD_CS_HIGH; for (i 0; i 80; i) SPI_ReadWriteByte(0xFF); // 进入 SPI 模式 SD_CS_LOW; res SD_SendCmd(0, 0, 0x95); // CMD0CRC 固定 0x95 if (res ! 0x01) return 1; // 检查供电电压 res SD_SendCmd(8, 0x1AA, 0x87); // CMD8参数 0x1AACRC 固定 0x87 if (res ! 0x01) return 2; // 循环发送 ACMD41直到卡退出 idle 状态 for (i 0; i 100; i) { SD_SendCmd(55, 0, 0x00); // CMD55 是 ACMD 的前导命令 res SD_SendCmd(41, 0x40000000, 0x00); // ACMD41HCS 置 1 if (res 0x00) break; delay_ms(1); } if (res ! 0x00) return 3; return 0; }初始化过程有几个要点。CMD0 发送成功后R1 返回 0x01表示卡已经进入 SPI 模式此时卡处于 idle 状态。紧接着发 CMD8参数 0x1AA 的意思是询问卡是否支持 2.7V 到 3.6V 电压范围返回的 R7 响应里包含了这个信息。如果 CMD8 返回非法命令说明卡可能是老版本 SD V1需要用 CMD1 方式兼容不过现在市面上几乎都是 SDHC 卡CMD8 都会正常响应。ACMD41 之前必须先发 CMD55这是一个比较别扭的设定卡把“初始化大容量卡”这个命令归类到特定应用命令里只有先发 CMD55 告诉卡“接下来是应用命令”ACMD41 才有效。我见过有人直接把 CMD55 和 ACMD41 发错顺序卡就永远初始化不成功。初始化成功后我用 CMD58 读取 OCR 寄存器确认卡的 CCS 位这个位决定卡是标准容量还是高容量。标准容量卡的地址单位是字节高容量卡的地址单位是 512 字节块。我后面的读取代码全部按块地址处理所以如果卡不是 SDHC地址换算要除以 512。3.4 用 CMD17 读取一个扇区初始化完成后读数据反而简单了。SD 卡在 SPI 模式下的读操作流程是发 CMD17地址为块地址等待响应然后等卡从 MISO 上吐出一个 0xFE 数据开始令牌这个令牌后面就是 512 字节数据最后还有两个字节的 CRC读出来扔掉即可。unsigned char SD_ReadSector(unsigned int block_addr, unsigned char *buf) { unsigned char res; unsigned char i; unsigned int t; SD_CS_LOW; res SD_SendCmd(17, block_addr, 0x00); if (res ! 0x00) { SD_CS_HIGH; return 1; } // 等待数据开始令牌最长等 500ms t 0; do { res SPI_ReadWriteByte(0xFF); t; } while (res 0xFF t 500); if (res ! 0xFE) { SD_CS_HIGH; return 2; } for (i 0; i 512; i) buf[i] SPI_ReadWriteByte(0xFF); SPI_ReadWriteByte(0xFF); // CRC 高位 SPI_ReadWriteByte(0xFF); // CRC 低位 SD_CS_HIGH; return 0; }读取期间 CS 必须保持拉低直到读完 CRC 之后才允许拉高。如果中途把 CS 拉高了卡会认为当前命令被中断后续的 CMD17 可能失效。在 CS 拉高之前我还额外发几个 0xFF 时钟确保卡彻底结束本次数据传输状态这个细节能避免不少偶发问题。4. 实际调试中踩过的坑与排查技巧4.1 卡在初始化的几种典型原因初始化是 SD 卡调试里最容易出问题的阶段。我遇到的问题主要集中在信号电平和时序上。首先CMD0 返回的不是 0x01而是 0xFF九成是硬件问题。检查顺序是MISO 上拉电阻有没有焊CS 有没有在发命令前拉低SCK 有没有正常翻转。我用示波器看过很多时候 0xFF 其实是 MISO 一直悬空读到的电平不是卡真的回了 0xFF。其次CMD8 返回 0x05非法命令说明命令格式没错但卡不认。这个情况一般出现在很老的低容量卡上要不就换卡要不就改成 CMD1 初始化流程。现在这种卡基本绝迹了实际遇到的概率很低。还有一个很隐蔽的问题ACMD41 循环几十次都超时。我后来发现是初始化时延时不均匀循环里 CPU 偶尔被中断打断导致时钟脉冲间隔过长卡内部看门狗超时。解决办法是初始化期间暂时关掉定时器中断或者在延时函数里加足够的余量。4.2 读数据异常时的检查顺序如果初始化成功但读出来的数据全是 0xFF先别急着怀疑代码重读一次扇区对比验证。我用这个办法排掉了很多干扰项。数据全 0xFF 的常见原因是卡还没准备好数据我读取时卡的状态还没切换过来或者 CS 拉高时机不对相当于命令被提前中断。另一个容易忽略的问题是扇区编号。很多新手第一次读出来的“根目录”完全对不上是因为 SD 卡最开始的扇区是 MBR 和分区表不是真正的文件系统数据。要从 FAT 分区读数据得先解析分区表找到分区起始扇区然后把起始扇区号加到你要读的偏移上。这个阶段看起来不像 SPI 问题但很容易被误判成读取失败。如果读出来偶尔有一两个字节错误优先怀疑时钟速度太高。把前面 delay_us() 函数里的 nop 指令加几行降到 500kHz 以下再测试。IO 模拟 SPI 不像硬件 SPI 有严格的主频配置只要代码里延时足够稳定抗干扰能力还挺强的。4.3 工程编译与烧录环节的典型问题STM8 开发常用的 IAR 环境里很多人会遇到Fatal Error[L210]: I/O error on input file这个报错。第一次遇到的时候我以为是代码写错了查了半天才发现是上一步编译生成的中间文件被杀毒软件锁住了或者输出目录里还有残留进程。解决办法是关掉杀毒软件对工程目录的实时监控或者把输出文件目录改到纯英文且不带空格的路径。还有一个经验是用管理员权限启动 IAR很多文件访问权限问题会直接消失。烧录环节要注意 SWIM 在线编程引脚和 SD 卡座的电气互斥。如果 SWIM 引脚和 SD 卡的某个信号线在 PCB 上离得很近烧录时可能出现干扰导致连接失败。我一般在烧录器连接期间先断开 SD 卡供电等程序跑起来后再上电卡座这样能避免设备互相拉扯电平。5. 这趟跑下来的一些实感用 STM8 的 IO 模拟 SPI 读 SD 卡这件事说白了就是拿 CPU 时间去换硬件外设资源。STM8 主频 16MHz读一个 512 字节扇区大概要花几毫秒用来存传感器数据、系统日志完全够用。我最后把读取速度稳定在 1MHz连续读写 4GB 的 SDHC 卡没有出现问题。调试过程中最大的体会是SD 卡是一个非常依赖时序的从机初始化唤醒时钟、命令顺序、响应等待窗口这些细节一步都不能错。出问题的时候不要盲目改代码先拿示波器看 SCK 和 MISO 的实际波形比什么都管用。IO 模拟 SPI 的代码结构也尽量保持独立后续要换成硬件 SPI 或移植到 STM32只需要改最底层的 SPI_ReadWriteByte 和引脚宏定义就够了。如果你的项目也要在低资源 MCU 上折腾存储不妨按这个思路大胆试一下路走通了以后什么 SPI Flash、温湿度传感器都不在话下。本文还有配套的精品资源点击获取