1. 从SD卡到SD nand:为什么SPI模式依然是开发者的心头好?
如果你玩过树莓派或者一些早期的单片机开发板,大概率接触过那种可以插拔的SD卡,用来存储系统镜像或者数据。但当你真正要把存储功能做到自己的产品里,比如一个小型的智能设备、一个数据采集器,你就会发现,那张小小的、可以拔插的SD卡,反而成了“阿喀琉斯之踵”——接触不良、在振动环境中容易松动、体积也不够紧凑。这时候,SD nand就该登场了。
你可以把SD nand理解为SD卡的“贴片版本”。它把SD卡的核心存储颗粒和控制器,封装成了一个只有8mm x 6mm大小的LGA-8贴片芯片,可以直接焊在你的电路板上。这样一来,SD卡的所有优点——大容量、标准化协议、相对简单的操作——它全都有,同时还解决了物理固定和连接可靠性的老大难问题。我自己在好几个需要可靠存储的工业项目里,都从SD卡换成了SD nand,再也没有收到过“存储设备丢失”的现场反馈。
那么,怎么跟这个贴片的小家伙通信呢?它和SD卡一样,支持两种模式:SD模式和SPI模式。SD模式速度快,但需要MCU有专门的SDIO硬件控制器,协议也复杂。而SPI模式,虽然速度慢一些,但它的优势太明显了:几乎所有的MCU,哪怕是最基础的51单片机,都有或者可以用GPIO模拟SPI接口;协议简单,四根线(SCK, MOSI, MISO, CS)搞定,驱动编写和理解起来也直观得多。对于很多数据吞吐量要求不是极端高,但成本敏感、开发资源有限的项目来说,SPI模式驱动SD nand/SD卡,是一个既稳又省心的选择。
这篇文章,我就结合自己踩过的坑和实战经验,带你彻底搞懂SD nand在SPI模式下的驱动。从硬件怎么连,到协议怎么发命令,再到代码怎么写、怎么优化,我会用最直白的话和能直接用的代码示例,让你从零到一实现驱动,并且知道每一步为什么这么做。无论你是嵌入式新手,还是想快速在项目里用上SD nand的老手,这篇内容都能给你实实在在的帮助。
2. 硬件连接与协议基础:SPI不是全部
在撸起袖子写代码之前,我们必须把硬件和协议这两块基石打牢。很多朋友驱动失败,一半的问题都出在这里:要么线接错了,要么对协议的理解有偏差。
2.1 硬件连线:四根线的舞蹈
SPI驱动SD nand,硬件连接非常简单,本质上就是将一个SPI从设备连接到MCU的SPI主控制器上。下面是核心的四根信号线:
- SCK (Serial Clock):时钟线,由主机(MCU)产生,用于同步数据。
- MOSI (Master Out Slave In):主机输出、从机输入数据线,主机通过它向SD nand发送命令和数据。
- MISO (Master In Slave Out):主机输入、从机输出数据线,SD nand通过它向主机返回响应和数据。
- CS (Chip Select):片选线,低电平有效。当MCU需要与某个SD nand通信时,将其对应的CS线拉低。
这里有一个非常关键的细节:SD nand和SD卡的引脚定义在SPI模式下是完全兼容的。这意味着,无论是你焊接在板子上的SD nand芯片,还是通过卡座连接的SD卡,在SPI模式下,你都只需要关心这四根线。下图清晰地展示了两种器件在SPI模式下的引脚对应关系:
| SD nand (LGA-8) 引脚 | SD卡 (TF卡座) 引脚 | SPI 信号 | 说明 |
|---|---|---|---|
| DAT2/NC | DAT2 | NC | SPI模式下未连接 |
| CD/DAT3 | DAT3 | CS | 片选 (Chip Select) |
| CMD | CMD | MOSI | 主机输出数据 |
| VSS | GND | GND | 电源地 |
| VCC | VCC | VCC | 电源 (通常3.3V) |
| CLK | CLK | SCK | 串行时钟 |
| VSS | GND | GND | 电源地 |
| DAT0 | DAT0 | MISO | 主机输入数据 |
| DAT1 | DAT1 | NC | SPI模式下未连接 |
注意:上表中的“NC”表示在SPI模式下不需要连接。务必确保你的SD nand的VCC供电电压与你的MCU系统电压匹配,通常是3.3V。上电时序也很重要,需要在CS为高电平时,先提供至少74个时钟脉冲,让SD nand内部电路稳定,这个步骤我们会在初始化代码里体现。
2.2 理解SD协议:SPI只是搬运工
这是最容易混淆的一点:我们虽然用了SPI物理接口,但通信的“语言”依然是SD协议。 SPI只定义了“怎么送”(时钟同步、逐位传输),而“送什么”(命令格式、响应含义、数据块结构)完全由SD协议规定。
可以把这想象成寄信。SPI是邮差,负责把信件(数据位)从你家(MCU)送到朋友家(SD nand)。而SD协议就是信纸上的书写格式和语法,比如开头要写“亲爱的”,结尾要署名,这样对方才能看懂。你用SPI这个“邮差”,送出去的必须是符合SD协议“格式”的信件。
SD协议的核心是命令-响应机制。主机发送一个命令帧,SD nand回送一个响应帧。命令帧固定为6字节(48位),结构如下:
字节0:[起始位0][传输位1][6位命令号]
字节1-4:32位命令参数
字节5:[7位CRC校验][结束位1]
举个例子,让SD卡进入空闲状态的CMD0命令,其命令号是0。在SPI模式下,命令号的最高两位是固定的(01),所以实际发送的第一个字节是 0x40(即0b01000000)。如果命令参数是0,CRC在初始化阶段(未进入SPI模式前)需要计算,对于CMD0,固定是0x95。所以,完整的CMD0命令帧就是:0x40, 0x00, 0x00, 0x00, 0x00, 0x95。
响应也有多种格式,最常见的是R1响应(1字节),其中的每一个比特位都代表了SD nand当前的一种状态,比如是否处于空闲状态、上一条命令是否非法等。理解这些状态位,是调试驱动时定位问题的关键。
3. 驱动实现第一步:上电、初始化和模式切换
万事开头难,SD nand的驱动初始化过程稍微有点繁琐,但只要我们严格按照状态机来,就能一步步把它“唤醒”并切换到SPI模式。这个过程就像和一个刚开机、还说着一口陌生方言(SD模式)的设备建立共同语言(SPI模式)。
3.1 上电与时钟供给
SD nand上电后,并不会立刻进入工作状态。它需要主机先提供一个稳定的时钟信号,同时片选CS保持在高电平(即未选中状态)。协议规定,主机需要在不选中器件(CS=1)的情况下,向CLK线提供至少74个时钟周期。这个操作的目的,是让SD nand内部的数字电路有足够的时间完成上电复位和同步。
在实际代码中,我们通常在初始化SPI硬件后,先拉高CS,然后连续调用SPI发送函数(发送任意数据,如0xFF)至少10次(因为一次发送8位,10次就是80个时钟),来满足这个要求。
void sd_power_on(void) {
// 1. 确保CS引脚为高电平(不选中)
set_sd_spi_cs_pin(PIN_HIGH);
// 2. 提供至少74个时钟周期
for(int i = 0; i < 10; i++) {
spi_read_write_byte(0xFF); // 发送哑元数据,产生时钟
}
}
3.2 发送CMD0:进入SPI模式的钥匙
完成时钟供给后,SD nand就进入了“空闲状态”。此时,它默认处于SD模式,使用自己的DAT[3:0]数据线。我们的目标是将它切换到SPI模式。
切换的“咒语”就是CMD0命令。拉低CS片选信号选中设备,然后通过SPI总线发送CMD0命令帧。这里有一个至关重要的细节:在发送CMD0时,设备仍处于SD模式,而SD模式是强制开启CRC校验的。因此,我们必须为CMD0计算并附上正确的CRC值,对于CMD0,这个值固定是0x95。如果CRC错误,SD nand会直接忽略这条命令。
SD_ERROR set_sd_to_idle_state(void) {
uint32_t timeout = 0x0FFF; // 设置一个超时计数器
// 1. 选中SD nand
set_sd_spi_cs_pin(PIN_LOW);
// 2. 发送CMD0命令,参数为0,CRC为0x95
// sd_send_cmd 函数会封装命令格式:0x40, 0, 0, 0, 0, 0x95
sd_send_cmd(SD_CMD_GO_IDLE_STATE, 0, 0x95);
// 3. 等待R1响应,期待收到0x01 (IN_IDLE_STATE)
if (sd_read_response(SD_IN_IDLE_STATE) != SD_RESPONSE_NO_ERROR) {
set_sd_spi_cs_pin(PIN_HIGH);
return SD_RESPONSE_FAILURE; // 超时或无响应
}
// 4. 取消选中,并发送8个额外时钟
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
return SD_RESPONSE_NO_ERROR;
}
如果这个函数返回成功,恭喜你!你的SD nand已经成功从SD模式切换到了SPI模式。一旦进入SPI模式,后续所有的命令通信都将通过SPI总线(MOSI/MISO)进行,并且CRC校验默认被关闭(除非你用命令重新开启),之后的命令CRC字段直接填0xFF即可,这大大简化了驱动逻辑。
4. 识别卡类型:与不同版本的SD卡对话
成功进入SPI模式后,我们还需要“认识”一下手中的这张卡。SD协议为了兼容历代产品(如SD V1.0, SD V2.0, SDHC, SDXC),设计了一个小小的识别流程。我们需要通过一系列命令对话,来确定卡的版本和容量类型(是标准容量SDSC还是高容量SDHC/SDXC),因为这对后续的寻址方式有决定性影响。
4.1 识别流程与状态机
这个过程最好对照着SD协议的状态机图来看,但别怕,我把它翻译成人话。核心步骤就三步:
-
试探:发送CMD8。这个命令是SD V2.0协议引入的,用来询问卡:“你支持2.7V-3.6V的电压吗?”我们在参数里告诉卡我们提供这个电压(参数
0x1AA),并附上一个检查模式0xAA。如果卡是V2.0或更新的,它会回一个R7响应,里面会把我们发送的电压和检查模式原样返回。如果卡是古老的V1.0或MMC卡,它会直接回复一个“非法命令”的错误响应。 -
初始化:发送ACMD41。这个命令是让卡结束空闲状态,进入准备工作的状态。这里有个小弯要绕:ACMD41是一个“应用特定命令”,发送它之前,必须先发一个CMD55,告诉卡“下一个命令是ACMD哦”。对于V2.0的卡,我们发送的ACMD41参数里可以设置一个HCS位,来询问卡是否支持高容量(SDHC)。
-
确认容量类型:发送CMD58。发送这个命令可以读取卡的OCR寄存器。OCR寄存器的第30位(CCS位)就是关键:如果这位是1,说明这是SDHC或SDXC卡,其寻址单位是扇区;如果是0,则是标准容量的SDSC卡,寻址单位是字节。这个区别直接影响我们后面读写数据时传入的地址参数。
4.2 代码实现与细节剖析
下面是我在实际项目中使用的识别函数的核心逻辑,加了大量注释帮你理解每一步:
SD_ERROR get_sd_card_type(void) {
uint8_t response;
uint8_t ocr_response[4];
uint32_t timeout;
// 第一步:发送CMD8,探测卡版本
set_sd_spi_cs_pin(PIN_LOW);
sd_send_cmd(SD_CMD_SEND_IF_COND, 0x1AA, 0x87); // VHS=0001(2.7-3.6V), Check Pattern=0xAA
response = sd_wait_response(); // 等待并读取R1响应
if (response == 0x05) { // 响应0x05 = 空闲状态(0x01) + 非法命令(0x04)
// 卡回复“非法命令”,说明它是V1.0卡或MMC卡
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
// 对于V1.0卡,使用CMD1进行初始化(MMC卡流程不同,此处略过)
timeout = 0x0FFF;
do {
set_sd_spi_cs_pin(PIN_LOW);
sd_send_cmd(SD_CMD_SEND_OP_COND, 0, 0xFF); // CMD1
if (sd_read_response(SD_RESPONSE_NO_ERROR) == SD_RESPONSE_NO_ERROR) {
break; // 初始化成功
}
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
} while(timeout--);
if(timeout == 0) return SD_RESPONSE_FAILURE;
SD_Type = SD_TYPE_V1;
}
else if (response == 0x01) {
// 卡回复0x01(空闲状态),说明它是V2.0或更新的卡,并且支持我们提供的电压
// 读取R7响应的后4个字节(电压和检查模式回显)
for(int i=0; i<4; i++) ocr_response[i] = spi_read_write_byte(0xFF);
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
// 第二步:发送CMD55 + ACMD41循环,初始化V2.0卡
timeout = 200; // 尝试200次
do {
set_sd_spi_cs_pin(PIN_LOW);
sd_send_cmd(SD_CMD_APP_CMD, 0, 0xFF); // 先发CMD55
sd_read_response(SD_RESPONSE_NO_ERROR);
sd_send_cmd(SD_ACMD_SD_SEND_OP_COND, 0x40000000, 0xFF); // 再发ACMD41,带HCS标志位
if (sd_read_response(SD_RESPONSE_NO_ERROR) == SD_RESPONSE_NO_ERROR) {
break; // 初始化成功,卡退出空闲状态
}
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
} while(timeout--);
if(timeout == 0) return SD_RESPONSE_FAILURE;
// 第三步:发送CMD58,读取OCR寄存器,判断卡容量类型
set_sd_spi_cs_pin(PIN_LOW);
sd_send_cmd(SD_CMD_READ_OCR, 0, 0xFF);
sd_read_response(SD_RESPONSE_NO_ERROR);
for(int i=0; i<4; i++) ocr_response[i] = spi_read_write_byte(0xFF); // 读取OCR值
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
// 检查OCR寄存器的CCS位(第30位)
if (ocr_response[0] & 0x40) { // 响应字节0的第6位对应OCR的bit30
SD_Type = SD_TYPE_V2HC; // SDHC 或 SDXC 卡
} else {
SD_Type = SD_TYPE_V2; // SDSC 卡
}
} else {
// 其他响应,可能是错误
return SD_RESPONSE_FAILURE;
}
return SD_RESPONSE_NO_ERROR;
}
这个函数执行成功后,全局变量 SD_Type 就保存了卡的准确类型,为后续的数据读写提供了关键依据。记住,对于SDHC/SDXC卡,读写命令中的地址参数是扇区号;对于SDSC卡,地址参数是字节地址。这是很多驱动在兼容不同卡时出错的地方。
5. 数据读写实战:单块与多块操作
经过前面的“握手”和“自我介绍”,MCU和SD nand已经建立了稳固的SPI通信关系,并且彼此知根知底。现在,终于到了最激动人心的环节——读写数据。SD nand的数据读写以“块”为单位进行,标准块大小是512字节。这比SPI Flash的页编程、Nor Flash的字节写入要规整得多,也意味着我们不需要处理复杂的擦除-写入周期(SD nand内部控制器处理了磨损均衡和坏块管理),可以直接写入,非常方便。
5.1 单块写入:CMD24的精确操作
写入一个数据块,我们使用CMD24命令。这个过程就像寄出一个包裹:先告诉快递点(SD nand)要寄件(发命令),等对方准备好(响应正确),再把包裹(数据块)给过去,最后确认对方收妥(检查数据响应)。
具体步骤如下,我结合代码和注意事项一起说:
- 发送写命令:拉低CS,发送CMD24命令,参数是目标地址(对于SDHC卡是扇区号,需要转换)。等待R1响应,确认卡准备就绪且无错误。
- 发送数据起始令牌:发送一个特殊的字节
0xFE。这个令牌告诉SD nand:“后面的数据是给你的有效载荷,请开始接收”。这个令牌在SD协议SPI模式的数据传输章节有定义。 - 发送数据:连续发送512字节的数据。这里就是简单的SPI连续写操作。
- 发送CRC:紧接着数据,发送两个字节的CRC校验码。由于我们通常工作在SPI模式且不开启CRC,所以直接发送两个
0xFF即可。 - 等待并检查数据响应:数据发送完毕后,SD nand会返回一个数据响应令牌。这是一个字节,我们需要读取它,并判断其内容。成功的响应是
0x05(二进制0101,其中低三位101表示数据被接受)。其他值则代表CRC错误、写错误等。 - 等待写入完成:即使数据被接受,SD nand内部真正把数据编程到存储单元也需要时间。我们需要持续读取它的状态(通过发送
0xFF并读回),直到读回的字节不是0x00(忙状态),表示写入完成。
SD_ERROR sd_write_single_block(uint8_t* data_buffer, uint32_t sector_addr) {
uint8_t response;
uint16_t i;
SD_ERROR ret = SD_RESPONSE_FAILURE;
// 地址转换:如果是SDHC卡,地址已是扇区号;否则需除以512
if(SD_Type == SD_TYPE_V2HC) {
// sector_addr 直接使用
} else {
sector_addr *= 512; // 将字节地址转换为块地址(假设传入的是块号)
}
set_sd_spi_cs_pin(PIN_LOW);
// 1. 发送CMD24写命令
sd_send_cmd(SD_CMD_WRITE_SINGLE_BLOCK, sector_addr, 0xFF);
if(sd_read_response(SD_RESPONSE_NO_ERROR) != SD_RESPONSE_NO_ERROR) {
goto exit; // 命令响应错误
}
// 2. 发送数据起始令牌
spi_read_write_byte(0xFE);
// 3. 发送512字节数据
for(i = 0; i < 512; i++) {
spi_read_write_byte(data_buffer[i]);
}
// 4. 发送2字节哑CRC
spi_read_write_byte(0xFF);
spi_read_write_byte(0xFF);
// 5. 读取数据响应令牌
response = spi_read_write_byte(0xFF);
if((response & 0x1F) == 0x05) { // 判断低5位是否为00101
ret = SD_RESPONSE_NO_ERROR;
} else {
ret = SD_DATA_WRITE_ERROR;
}
// 6. 等待写入完成(卡不再繁忙)
while(spi_read_write_byte(0xFF) == 0x00);
exit:
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF); // 额外8个时钟
return ret;
}
5.2 单块读取:CMD17与数据流解析
读取操作相对写入更简单一些,使用CMD17命令。步骤是:
- 发送读命令:拉低CS,发送CMD17命令及地址参数。等待R1响应确认。
- 等待数据起始令牌:持续读取SPI数据,直到收到
0xFE令牌。这个令牌标志着有效数据流的开始。这里一定要加超时判断,防止卡死。 - 读取数据:连续读取512字节的数据到你的缓冲区。
- 丢弃CRC:读取随后的两个字节CRC(可丢弃)。
- 完成:拉高CS,发送额外时钟。
SD_ERROR sd_read_single_block(uint8_t* data_buffer, uint32_t sector_addr) {
uint16_t i;
uint32_t timeout;
SD_ERROR ret = SD_RESPONSE_FAILURE;
// 地址转换(同写操作)
if(SD_Type != SD_TYPE_V2HC) {
sector_addr *= 512;
}
set_sd_spi_cs_pin(PIN_LOW);
// 1. 发送CMD17读命令
sd_send_cmd(SD_CMD_READ_SINGLE_BLOCK, sector_addr, 0xFF);
if(sd_read_response(SD_RESPONSE_NO_ERROR) != SD_RESPONSE_NO_ERROR) {
goto exit;
}
// 2. 等待数据起始令牌0xFE,加超时
timeout = 0xFFFF;
while((spi_read_write_byte(0xFF) != 0xFE) && timeout--);
if(timeout == 0) {
ret = SD_DATA_OTHER_ERROR;
goto exit;
}
// 3. 读取512字节数据
for(i = 0; i < 512; i++) {
data_buffer[i] = spi_read_write_byte(0xFF);
}
// 4. 丢弃2字节CRC
spi_read_write_byte(0xFF);
spi_read_write_byte(0xFF);
ret = SD_RESPONSE_NO_ERROR;
exit:
set_sd_spi_cs_pin(PIN_HIGH);
spi_read_write_byte(0xFF);
return ret;
}
5.3 多块读写与性能优化初探
单块操作是基础,但在实际应用中,比如存储一个文件、记录一段连续的数据,我们更需要连续读写多个块。SD协议提供了多块读写命令(CMD18/CMD25),可以显著减少命令开销。
多块读(CMD18):发送一次CMD18命令,带上起始地址,SD nand就会连续输出多个数据块,每个块前面都有0xFE令牌,后面跟CRC。主机需要持续读取,直到发送CMD12命令来终止传输。
多块写(CMD25):发送CMD25命令和起始地址后,主机可以连续发送多个数据块。每个数据块前需要加0xFC令牌(多块写起始令牌),最后一个块发送完后,需要发送一个特殊的“停止传输令牌”0xFD,然后等待卡完成所有块的写入。
实现多块读写能大幅提升连续读写的速度。但这里有一个重要的优化点:SPI的时钟速度。在初始化识别阶段,为了兼容性,我们通常使用较低的SPI时钟(比如400kHz)。一旦识别完成,我们可以通过命令(CMD16设置块长度,虽然不是必须的;更关键的是通过调整MCU的SPI时钟分频器)将SPI时钟提升到卡支持的最高速度(具体看卡的OCR寄存器或CSD寄存器)。在我的STM32F103项目里,识别完成后将SPI时钟从PCLK/2提升到PCLK/8,读取速度能有数倍的提升。不过要注意,提升时钟的前提是确保你的PCB布线质量能支持高速信号,否则可能导致通信错误。
6. 避坑指南与高级优化技巧
代码跑起来,灯亮了,数据好像也对,但做产品开发,稳定性和性能才是终极考验。下面这些坑,都是我亲自踩过,或者帮别人调试时总结出来的,希望能帮你省下大量排查时间。
6.1 常见问题排查清单
当你发现驱动不工作,可以按这个清单逐一检查:
- 电源和硬件是第一关:用万用表量一下SD nand的VCC脚,真的是3.3V吗?纹波大不大?电源不稳是一切奇怪的根源。上电顺序务必保证:先供电,稳定后再给时钟,最后才操作CS和命令。
- 74个时钟脉冲给了吗? 这是协议强制要求的上电初始化步骤,少了这一步,卡可能无法正确进入SPI模式。确保在拉低CS发CMD0之前,CS为高时已经输出了足够时钟。
- CMD0的CRC对吗? 这是新手最容易栽跟头的地方。在发送CMD0切换模式时,CRC必须正确(
0x95)。一旦进入SPI模式,后续命令的CRC填0xFF即可。很多开源库的驱动代码,在SPI模式下都忽略了CRC计算,但唯独CMD0是例外。 - 响应超时处理了吗? 代码里所有等待响应的地方(如
sd_read_response),必须要有超时机制。SD nand响应命令可能需要一定时间(尤其是写操作后),但绝不是无限长。一个while循环死等,分分钟让程序卡死。合理的超时值(比如重试1000次)和超时后的错误返回是健壮性的保证。 - SPI相位和极性(CPOL/CPHA)设对了吗? 绝大多数SD nand和SD卡在SPI模式下,都要求CPOL=1, CPHA=1(即时钟空闲时为高电平,在第二个时钟边沿采样数据)。这是SD物理层规范里明确写着的。如果你的SPI模式设成了0,0或1,0,通信肯定失败。
- 地址参数用对了吗? 再次强调:对于SDHC/SDXC卡(高容量卡),读写命令中的地址是扇区号(LBA)。比如你要读写第100个扇区,地址就填100。而对于标准容量的SDSC卡,地址是字节地址。如果你把SDHC卡当成SDSC卡,传入地址
100*512,那实际访问的将是第51200个扇区,远远超出了范围,通常会返回地址错误。
6.2 驱动性能优化实战
驱动稳定后,我们可以考虑让它跑得更快。除了前面提到的提升SPI时钟频率,还有几个优化手段:
- 启用SPI的DMA传输:对于512字节这样的数据块,使用CPU一个个字节通过SPI DR寄存器搬运是非常浪费的。启用SPI的TX/RX DMA,可以让CPU在数据传输期间去处理其他任务,大幅提升系统效率,尤其是在进行多块连续读写时。以STM32的HAL库为例,你可以使用
HAL_SPI_TransmitReceive_DMA函数来替代轮询发送。 - 实现双缓冲或乒乓缓冲:在进行连续数据流记录(如音频采集)时,可以准备两个512字节的缓冲区。当DMA正在将缓冲区A的数据写入SD nand时,CPU可以同时将新采集的数据填充到缓冲区B。待A写入完成,立刻切换B进行写入,同时CPU填充A。这样几乎可以实现无缝连续记录。
- 合理使用多块命令:频繁地单块读写,每个块都要经历“发命令-等响应-传数据-等完成”的过程,命令开销很大。如果业务逻辑允许,尽量攒够多个块的数据,使用CMD25进行多块写入。读取时也尽量使用CMD18连续读。我实测过,连续读写4个块,总耗时比单独读写4次能减少30%以上。
- 降低SPI时钟的毛刺:在高速(比如>10MHz)SPI通信下,PCB布局布线的影响会凸显出来。尽量让SCK、MOSI、MISO走线等长、短且远离干扰源。在信号线上串联一个22-33欧姆的小电阻,有时能有效改善信号完整性,减少误码。
最后,分享一个调试小技巧:如果你有一个逻辑分析仪,哪怕是最便宜的那种,把它连接到SPI的四根线上,捕获通信波形。你可以清晰地看到命令帧、响应帧、数据令牌、数据流。对照SD协议手册的时序图,任何错误都无所遁形。这比单纯用printf打印调试信息要直观和高效得多,是开发底层驱动不可或缺的神器。
把上面这些点都做到,你的SD nand SPI驱动就不仅限于“能用”,而是达到了“稳定、高效”的产品级水准。从我自己的项目经验来看,经过这些优化,一个基于STM32F4和SD nand的系统,实现持续数MB/s的写入速度来记录传感器数据,是完全可行的。希望这些从实战中总结出的细节,能让你在开发路上走得更顺。
2409



被折叠的 条评论
为什么被折叠?



