写在前面
代码开源
主要参考资料
- RPI Pico文档:pico_datasheet pico-c-c++-sdk getting-started-with-pico
-
SD协会的文档:Archives | SD Association 自留pdf
-
lqmlmo的博客:STM32利用SPI读写SD卡的程序详解_spi tf卡-CSDN博客
-
FatFs by chaN:FatFs - Generic FAT Filesystem Module
基本数据类型的别称
typedef unsigned char u8;
typedef char i8;
typedef unsigned short u16;
typedef short i16;
typedef unsigned int u32;
typedef int i32;
typedef unsigned long long u64;
typedef long long i64;
typedef unsigned long usz;
typedef long isz;
疏漏之处
由于笔者调研(很有)可能并不充分,无法做到完美的兼容性,只能保证笔者手中的几张卡(V2.0版本、HC容量)利用这种方式进行读写基本上都还是没有问题的,请各位读者见谅。
硬件连接
我们将使用Pico自带的SPI0控制器与SD卡进行通信。我们可以从Pico引脚图上看出,属于SPI0控制器的引脚为GP16至GP19,功能分别为RX(接收)、CSn(片选)、SCK(时钟)和TX(发送)引脚。
当然,GP3、GP4、GP6、GP7的组合也是可用的。
注意:在与SD卡通信时,Pico是SPI总线的主设备(Master),而SD卡是从设备(Slave)。因此,在此时,Pico上的RX引脚也可以称为MISO(Master In Slave Out),而TX引脚可以称为MOSI(Master Out Slave In)。
片选信号CSn,CS代表chip select,而n代表低电平有效。
SPI总线的初始化与读写
引脚初始化
首先,我们先将pico对应的针脚进行初始化。这么做的原因可以看上图中的GP16引脚:它同时承担了SPI0 RX
、I2C0 SDA
、UART0 TX
这几个功能(其实还有SIO
和PIO
),在使用前,需要指定好所需的功能,否则就会“乱套”。
在底层,“设置功能”这一操作事实上改变的是GPIO Pin多路选择器的状态,使GPIO Pin连接到对应的控制器上。
可能读者已经注意到,片选信号CSn
是单独进行初始化的。SPI作为总线,理应支持挂接多个设备。MISO
、MOSI
和SCK
是公用的数据和同步信号线,只用初始化一次即可。而CSn
则是每个设备专有的。
因此,若将公有信号与专有信号分开,当我们挂接多个设备时,只需初始化一次公有信号,而各个设备专有的CSn
引脚,我们可以分别初始化。
#include <hardware/spi.h>
// mosi: tx, miso: rx
void
spi_pins_init(int sclk, int mosi, int miso) {
#define INIT_AS_SPI(PIN, OUT) \
gpio_set_function(PIN, GPIO_FUNC_SPI); \
gpio_set_dir(PIN, OUT);
INIT_AS_SPI(sclk, 1);
INIT_AS_SPI(mosi, 1);
INIT_AS_SPI(miso, 0);
#undef INIT_AS_SPI
}
void
spi_cs_init(int cs) {
gpio_set_function(cs, GPIO_FUNC_SIO);
gpio_set_dir(cs, GPIO_OUT);
gpio_pull_up(cs);
gpio_put(cs, 1);
}
INIT_AS_SPI
宏:大佬有云:重复三次及以上的代码段,应当进行一定的封装。我深以为然。哈哈!
SPI读写总线函数
前面已经知道,SPI具有两条数据线:MOSI
与MISO
,这也赋予了SPI总线全双工通信的能力。因此当Pico通过MOSI
线向SPI总线写一个字节的同时,也可以从MISO
线读到一个字节。
Pico SDK为我们提供了spi_write_read_blocking
函数,用以读写SPI总线上的数据串。为了方便起见,我们将其做个封装,每次调用只读写一个字节,以参数和返回值的形式传值(而非指针),方便使用与理解。
static inline u8
spi_rwbyte(spi_inst_t *spi, u8 src) {
u8 ret;
spi_write_read_blocking(spi, &src, &ret, 1);
return ret;
}
当然,对每个字节都调用一次上述函数很可能会显著增加调用开销。读者也可以针对这一点进行一定的优化。
SD卡命令基础
SD卡命令格式(Section 7.3)
SD卡的命令通过SPI总线发送给SD卡。SD卡命令可以认为是由8位命令号、32位命令参数与8位CRC值构成的,实际情况则稍微复杂一些,请看下图。
- 在8位命令号中,有效位只有6位,最高两位是固定值01。因此,在构造命令时,需要将命令号“或”上
0x40
。例如:对于CMD8
而言,命令号部分将会是0x40 | 8 = 0x48
-
在命令参数中,如果命令参数是一个整体,则先发MSB,再发LSB。
-
CRC7在大部分情况下可以忽略。
Pico向SD卡发送命令:
static void
sd_start_cmd(sdcard_t *sd, u8 cmd, u32 args, u8 crc) {
sd_cs_enable(sd);
spi_rwbyte(sd->spi, cmd | 0x40); // 此处命令号要“与”0
spi_rwbyte(sd->spi, (args >> 24) & 0xff);
spi_rwbyte(sd->spi, (args >> 16) & 0xff);
spi_rwbyte(sd->spi, (args >> 8) & 0xff);
spi_rwbyte(sd->spi, (args) & 0xff);
spi_rwbyte(sd->spi, crc);
}
sdcard_t是什么?是操作SD卡所需信息的集合,下面会讲到。
SD卡返回值格式(Section 7.3)
发送命令后,SD卡经过一定时间会从SPI总线上返回一个值,表示命令的执行结果。SD卡的返回值格式包括R1
、R3
、R7
等。我们主要介绍上面提到的这三种返回值格式。
- R1返回格式:长度一个字节,字节的各个位表示的含义如下所示。
- R3返回格式:长度5个字节。第一个字节为R1。后四个字节为SD卡的OCR寄存器。
- R7返回格式:长度5个字节。第一个字节为R1。后四个字节为SD卡的IF_COND信息,表示SD卡支持的接口条件(如电压等)。
Pico读取R1/R3/R7返回格式:
static u8
sd_get_r1_response(sdcard_t *sd) {
u8 ret = 0xff;
// 多等待几个周期,给SD卡充足的反应时间
u32 retry_cnt = 0xfff;
do {
ret = spi_rwbyte(sd->spi, 0xff);
} while (0xff == ret && 0 != (retry_cnt--));
return ret;
}
static u32
sd_get_r3r7_response(sdcard_t *sd) {
u32 ret = 0;
u8 r1;
// 获取R3/R7的后32个字节
r1 = spi_rwbyte(sd->spi, 0xff);
ret |= (r1 << 24);
r1 = spi_rwbyte(sd->spi, 0xff);
ret |= (r1 << 16);
r1 = spi_rwbyte(sd->spi, 0xff);
ret |= (r1 << 8);
r1 = spi_rwbyte(sd->spi, 0xff);
ret |= r1;
return ret;
}
需要注意的是,由于R3和R7回复包含R1回复,因此调用
sd_get_r3r7_response
前,需要先调用一次sd_get_r1_response
。
接收返回值后的收尾工作
主要是将SD卡的片选信号拉高。顺便在总线上塞几个无用字节0xff
,提高兼容性与稳定性。
static void
sd_end_cmd(sdcard_t *sd) {
sd_cs_disable(sd);
for (int i=0 ; i<4 ; ++i) {
spi_rwbyte(sd->spi, 0xff);
}
}
SD命令参考文档
可以参考主要参考资料中“SD协会的文档”部分。章节7.3.1.3的表格详细列出了各个命令的参数、返回值和功能。下图为表格节选。
SD卡的初始化
sdcard_t
数据结构
为了面向对象!为了避免全局变量乱飞,同时为了多卡的扩展性,因此有必要将操作一张SD卡需要的信息打包成一个结构体sdcard_t
。
typedef struct sdcard_t {
// need to fill before initialize
spi_inst_t *spi; // 该卡使用的Pico SPI控制器,一般为spi0或spi1
int pin_cs; // 该卡的CSn引脚GPIO编号
// filled when initialize
i32 version; // SD卡版本
u32 ocr; // SD卡OCR寄存器
u64 csd_low; // SD卡CSD寄存器的低64位
u64 csd_high; // SD卡CSD寄存器的高64位
bool initialized; // SD卡已初始化标志
} sdcard_t;
其中spi
与pin_cs
都属于SPI总线的部分,在SD卡初始化流程前就应当通过上面SPI初始化的相关函数进行初始化,并填入sdcard_t
数据结构中。后面的信息在SD卡初始化过程中被读取,由初始化函数进行填充。
SD卡初始化流程
在此,首先放出一张来自SD卡官方文档的图片。我们所要做的事情,就是把图片上的流程(部分地)实现即可。
从图片上来看,首先应当发送的是CMD0指令。不过在此之前,首先要让SD卡进入SPI模式,方法为:在片选有效的情况下,向SD发送一定数量的时钟脉冲,此处我选择的脉冲数量为20个字节(160个脉冲)。
sd_cs_enable(sd);
for (int i=0 ; i<20 ; ++i) {
spi_rwbyte(sd->spi, 0xff);
}
sd_cs_disable(sd);
发送CMD0:软复位
随后即可开始正式进入流程。首先发送CMD0,CMD0的作用是将SD卡复位。CMD0的参数和CRC,我们分别设置为0和0x95,后面我们就不必操心CRC了。
各个命令的作用、命令号、参数和返回值,都可以查阅SD协会的文档的相应部分。
下面这段发送CMD0的流程是很典型的发送命令的流程:
- 首先设置一个重试次数
count
。 -
然后在do-while循环中依序运行发送命令函数
sd_start_cmd
、接收返回值函数sd_get_r1_response
和结束命令函数sd_end_cmd
,完成一次命令发送的流程。 -
当返回值不是无效字节
0xff
或重试次数用尽后跳出循环,在后面针对返回值做进一步处理。
注意:虽然不同命令可能具有不同的行为,但是处理思路都是相似的。我们不期待发送的命令一次就被SD卡执行成功,我们要给SD卡充足的反应时间。
count = 500;
do {
sd_start_cmd(sd, SD_CMD0, 0, 0x95);
resp = sd_get_r1_response(sd);
sd_end_cmd(sd);
--count;
} while (0xff == resp && count > 0);
if (0xff == resp) {
printf("SD init timeout: CMD0.\n");
return;
}
发送CMD8:检查卡版本
随后,我们向SD卡发送CMD8。CMD8的主要目的是检查卡是否支持接口电压,此处我们默认接口电压是合规的。CMD8的参数,我们设置成0x1aa就好,这表明我们向提供了所有合理的电压选项。
除了检查电压外,CMD8还可用于分辨使用1.0版本协议的卡和使用2.0版本(及以上)协议的卡。分辨方式也很容易从上面的初始化流程图中看出。当CMD8返回的R1返回值中,若ILLEGAL_COMMAND
位被置位,则该卡是1.0版本的卡,否则是2.0版本(及以上)的。
事实上,可以从文档中读到,CMD8的返回值格式是R7,只不过R7中包含R1,所以说“CMD8的R1返回值”也是没有问题的。
那么,区分SD卡的1.0与2.0版本,很重要吗?当然!当我们需要获取SD卡的某些信息时(例如容量),1.0版本与2.0版本的操作方式稍有不同。当然,也可以抛弃一部分兼容性。没人喜欢旧版的小容量卡啊!
CMD8的代码,填充了sdcard_t
的version
字段,标识了SD卡的版本。
count = 500;
do {
sd_start_cmd(sd, SD_CMD8, 0x1aa, 0x87);
resp = sd_get_r1_response(sd);
r3r7_resp = sd_get_r3r7_response(sd);
sd_end_cmd(sd);
--count;
} while (0xff != resp && count > 0);
if (0xff == resp) {
printf("SD init timeout: CMD8.\n");
return;
}
// 此处根据ILLEGAL_CMD标志位判断卡版本
if (SD_R1_ILLEGAL_CMD & resp) {
sd->version = 1;
} else {
sd->version = 2;
}
获取OCR、CSD寄存器,进入传输模式
随后,根据流程图,我们首先通过CMD58,获取SD卡的OCR寄存器,然后通过ACMD41,使得卡进入读取模式,最后在通过CMD9,获取SD卡的CSD寄存器。里面较为值得说道说道的,主要时ACMD41的发送和CSD寄存器的获取。
首先说说ACMD41的发送。ACMD相对CMD而言,需要发送两个命令。首先发送固定的CMD55命令,这告诉SD卡,接下来要发送的将是ACMD命令。随后再发送CMD41命令,此时命令号41就不会被SD卡当成一般的命令处理,而是当作ACMD处理。
ACMD41命令的目的是让SD卡从复位(IDLE)状态转为读写状态。因此,我们的循环条件就是ACMD41返回的R1中,IN_IDLE_STATE
位被清零。ACMD41的参数是固定的。
while (SD_R1_IN_IDLE_STATE & resp) {
sd_start_cmd(sd, SD_CMD55, 0, 0);
sd_get_r1_response(sd);
sd_end_cmd(sd);
sd_start_cmd(sd, SD_ACMD41, 0x40000000, 0);
resp = sd_get_r1_response(sd);
printf("ACMD41 returns 0x%02X.\n", (int)resp);
sd_end_cmd(sd);
}
接下来讲一讲CSD寄存器的获取。为什么不说说OCR寄存器的获取呢?因为OCR寄存器只有32位,通过R3返回格式就能轻松返回;但是CSD寄存器足足有128位,需要通过块读的方式获取,有一定的差异性。
CSD寄存器的获取需要首先发送一个CMD9,标识请求CSD寄存器。
sd_start_cmd(sd, SD_CMD9, 0x0, 0x0);
count = 500;
do {
resp = sd_get_r1_response(sd);
--count;
} while (0xff == resp && count > 0);
随后,我们在SPI总线上等待一个多块读取的开始标志0xFE。等待的过程中,一直在总线上放0xFF就好。当读到了0xFE,接下来的16个字节就是从MSB到LSB的CSD寄存器。由于C语言原生的数据类型只到64位,我们就将CSD的高64位和低64位写到两个u64
中。 最后以sd_end_cmd
潇洒作结。
while (SD_START_DATA_MULTIPLE_BLOCK_READ != spi_rwbyte(sd->spi, 0xff)) {
tight_loop_contents();
}
for (int i=7 ; i>=0 ; --i) {
u64 byte = (u64)spi_rwbyte(sd->spi, 0xff);
u32 shft = (i<<3);
sd->csd_high |= (byte << shft);
}
for (int i=7 ; i>=0 ; --i) {
u64 byte = (u64)spi_rwbyte(sd->spi, 0xff);
u32 shft = (i<<3);
sd->csd_low |= (byte << shft);
}
sd_end_cmd(sd);
sd_end_cmd(sd);
将CSD寄存器和OCR寄存器的读取操作分别封装到sd_get_ocr
和sd_get_csd
函数中,最终看起来就是:
sd_get_ocr(sd);
while (SD_R1_IN_IDLE_STATE & resp) {
sd_start_cmd(sd, SD_CMD55, 0, 0);
sd_get_r1_response(sd);
sd_end_cmd(sd);
sd_start_cmd(sd, SD_ACMD41, 0x40000000, 0);
resp = sd_get_r1_response(sd);
printf("ACMD41 returns 0x%02X.\n", (int)resp);
sd_end_cmd(sd);
}
sd_get_csd(sd);
笔者实测时,将sd_get_ocr与ACMD41的发送调换了位置,似乎也没有问题。不过按照标准流程来肯定没错。
SD卡的基本操作与读写
本章节中只写了SD卡单块读写程序,因为笔者不想写多块。读者若想自行实现,其实也并不算困难,哈哈,只是因为懒罢了。
SD卡容量的获取
此处,SD卡2.0版本与1.0版本显示出了其区别。
2.0版本的SD卡计算容量时非常方便,直接读取CSD寄存器的69位至48位即可。数值以512KB为单位,直接除以2,即可得到以MB为单位的容量。
而1.0版本的SD卡则需要在CSD的三个位置收集三个字段(C_SIZE、C_SIZE_MULT、READ_BL_LEN),经过一系列公式换算出SD卡容量。
不同版本CSD寄存器的文档,可以查看SD协会文档的5.3节。
以字节为单位的SD卡容量计算代码。
u64
sd_size_byte(sdcard_t *sd) {
if (sd->version == 2) {
u32 c_size_low = (sd->csd_low >> 48); // [63..48]
u32 c_size_high = (sd->csd_high & 0x3f); // [69..64]
u64 c_size = c_size_low | (c_size_high << 16);
return (c_size + 1) << 19;
}
u32 c_size_low = (sd->csd_low >> 62); // [63..62]
u32 c_size_high = (sd->csd_high & 0x3ff); // [73..64]
u64 c_size = c_size_low | (c_size_high << 2);
u8 c_size_mult = (sd->csd_low >> 47) & 0x3; // [49..47]
u8 read_bl_len = (sd->csd_high >> 20) & 0xf; // [95..84]
return (c_size + 1) * (1 << (c_size_mult + 2)) * (1 << read_bl_len);
}
SDHC、数据块、块长与块地址
数据块是SD卡读写的基本单位。在默认情况下,我们可以认为SD卡的块长均为512字节。
在块地址方面,高容量SD卡(SDHC)和普通容量SD卡具有一定的区别。高容量SD卡按块寻址,而普通容量SD卡按字节寻址。当地址加1时,高容量SD卡前进512个字节,而普通容量SD卡则只前进1个字节。因此在编写读取、写入函数时,需要注意这一点。
关于块长对于高容量SD卡(SDHC)而言,数据块长是512字节,不能修改。而对于普通容量卡SD卡而言,则可以通过
CMD16
修改块长。在未经修改的情况下,普通容量SD卡的块长也是512字节。这也是为什么在默认情况下,我们可以无需理会两种卡的差异性,统一认为块长是512字节。
那么,我们如何分辨SDHC卡与普通容量卡呢?首先,早期1.0版本的卡全是标准容量卡。而2.0版本的卡可以通过OCR寄存器的第30位标识来判断是否是标准容量卡,若该位为1,则为高容量卡,否则为标准容量卡。
static inline bool
sd_is_hc(sdcard_t *sd) {
return (sd->version == 1) ? true : ((sd->ocr & BIT(30)) != 0);
}
超高容量SD卡(SDXC)由于不支持SPI协议,因此不在讨论范围内。
读取一个块
读取一个块事实上与读取CSD寄存器的流程类似,只不过读取CSD寄存器时,发送的是CMD9,而读取一个块时,发送的是CMD17。
我们编写一个平平无奇的块读函数,签名如下:
void sd_read_block(sdcard_t *sd, u8 *buf, u32 block_id);
有趣的地方来了。注意到我们的函数参数给的是块地址block_id。前面已经提到,HC卡与普通卡的寻址方式不同,HC卡按块寻址而普通卡按字节寻址。因此,在面对HC卡时,我们可以直接传块地址block_id,而面对普通卡时,我们需要将块地址block_id转为字节地址!!方法就是平平无奇地将块地址block_id乘上块长512,也即左移9位,如下所示。
if (!sd_is_hc(sd)) {
block_id <<= 9;
}
接下来就是发送CMD17,将处理后的block_id当作参数传入。还是重复发送的套路,只不过换了一种写法。
u32 retry_cnt = 0xff;
do {
sd_start_cmd(sd, SD_CMD17, block_id, 0);
resp = sd_get_r1_response(sd);
if (0x00 == resp) {
break;
}
if (0 == retry_cnt) {
printf("read block fail. CMD17 timeout.\n");
sd_end_cmd(sd);
return;
}
sd_end_cmd(sd);
--retry_cnt;
} while (1);
在总线上等待块开始标志0xFE...
retry_cnt = 0xffff;
do {
resp = spi_rwbyte(sd->spi, 0xff);
--retry_cnt;
} while (SD_START_DATA_SINGLE_BLOCK_READ != resp && 0 != retry_cnt);
在读到0xFE后,连续读512个字节,这就是块内容。
for (int i=0 ; i<512 ; ++i) {
buf[i] = spi_rwbyte(sd->spi, 0xff);
}
512字节读完后,SD卡为了防止传输错误,特意发送了两个CRC位供我们校验。我们的选择是:综合性能消耗与出错概率,以及实现的复杂度,在工程上是可以直接丢弃CRC位的。总线上的信号质量非常好,肯定不会出错,没有人需要校验CRC啊!如果我们连总线都不能信任了,那我们还能信任谁???
最后以平平无奇的sd_end_cmd
作结。
// pretend that we have got crc
spi_rwbyte(sd->spi, 0xff);
spi_rwbyte(sd->spi, 0xff);
sd_end_cmd(sd);
sd_end_cmd(sd);
写入一个块
写入一个块与读取一个块的流程类似,也需要根据SD卡的类型(是不是SDHC卡)来转换用户传入的地址。发送的命令是CMD24
。
在发送完CMD24后,我们需要自行发送块开始标志0xFE。可以在发送块开始标志前发送几个无用字节0xFF,让SD卡准备充分。在CRC部分,我们可以直接发送两个0xFF进行占位。
spi_rwbyte(sd->spi, 0xff);
spi_rwbyte(sd->spi, SD_START_DATA_SINGLE_BLOCK_WRITE);
for (int i=0 ; i<512 ; ++i) {
spi_rwbyte(sd->spi, buf[i]);
}
// dummy crc
spi_rwbyte(sd->spi, 0xff);
spi_rwbyte(sd->spi, 0xff);
最后,我们从总线上一直读取数据,直到读取的字节中,后五位为5(0b00101
)时,就表明SD卡写块成功。在获取到5后,再次一直连续读取,直到SD卡返回0xFF,表明SD卡完成整个写操作,进入空闲状态。直到这里,我们才能安全地退出。
retry_cnt = 0xff;
do {
resp = spi_rwbyte(sd->spi, 0xff);
--retry_cnt;
} while (0xff == resp && retry_cnt > 0);
if ((resp & 0x1f) != 0x5) {
printf("write sector failed, sd card respond 0x%02x.", resp);
}
while (spi_rwbyte(sd->spi, 0xff) != 0xff) {
tight_loop_contents();
}
sd_end_cmd(sd);
sd_end_cmd(sd);
FAT32文件系统
移植FatFs
想要移植FatFs,我们只需完成diskio.c中的函数即可。这个函数主要是定义了FAT文件系统与存储设备(也就是我们的SD卡)的调用接口。下面将diskio.c中的函数做个说明。
disk_initialize
:初始化存储设备。我们可以将上面写到的SD卡初始化流程写在里面,并根据出错的情况返回磁盘的状态DSTATUS
。
DSTATUS的可取值定义在diskio.h头文件中,以
STA_
开头。如果磁盘情况一切正常,我们可以直接返回0。disk_status
:返回存储设备的状态。也返回DSTATUS
。-
disk_read
与disk_write
:读取/写入块。这里直接调用我们之前编写的sd_read_block
与sd_write_block
就行。传入的地址也是块地址。 -
disk_ioctl
:获取存储设备的一些信息。例如存储设备具有多少块、存储设备的块长是多少。我们需要实现的命令是diskio.h
中/* Generic command (Used by FatFs) */
下定义的命令,具体有:/* Generic command (Used by FatFs) */ #define CTRL_SYNC 0 /* Complete pending write process (needed at FF_FS_READONLY == 0) */ #define GET_SECTOR_COUNT 1 /* Get media size (needed at FF_USE_MKFS == 1) */ #define GET_SECTOR_SIZE 2 /* Get sector size (needed at FF_MAX_SS != FF_MIN_SS) */ #define GET_BLOCK_SIZE 3 /* Get erase block size (needed at FF_USE_MKFS == 1) */ #define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at FF_USE_TRIM == 1) */
由于我们的SD卡读写接口都是同步接口,且不存在块擦除的问题,我们无需实现
CTRL_SYNC
、CTRL_TRIM
。我们只需实现获取块数与块长的命令。一个参考实现如下:DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(LBA_t*)buff = sd_is_hc(&sd13) ? sd_size_byte(&sd13) << 9 : sd_size_byte(&sd13); break; case GET_SECTOR_SIZE: case GET_BLOCK_SIZE: *(WORD*)buff = sd_is_hc(&sd13) ? 1 << 9 : 1; break; default: break; } return RES_OK; }
读者可能注意到,每一个disk函数后面都跟着一个pdrv参数。这是FatFs库管理多个存储设备时需要用到的。此处,若只有一张SD卡,则无需理会这个参数。若有多张SD卡(多个存储设备),请参考FatFs相关文档进行适配,这里不再赘述。
使用FatFs
FatFs提供了一套很接近C标准库的一组文件操作函数。首先利用f_mount
将SD卡上的文件系统挂载上,然后通过opendir
/readdir
组合列出目录下的文件。利用f_open
打开文件,再利用f_read
与f_write
对文件进行读写。最后f_umount
卸载文件系统。
下面是一段代码示例,SD卡上需要提前准备好一个FAT32文件系统,然后向根目录下创建一个config.txt文本文件,并在里面写点什么。放些其他文件和目录也是可以的。
#include <...>
int main(void) {
stdio_init_all();
FATFS fs;
FIL fp;
DIR dp;
FILINFO fi;
FRESULT res;
// 挂载sd卡文件系统
if (FR_OK != (res=f_mount(&fs, "", 0))) {
printf("f_mount failed, %d.\n", (i32)res);
}
// 打开根目录并列出根目录下的文件
if (FR_OK != (res=f_opendir(&dp, "/"))) {
printf("f_opendir failed, %d.\n", (i32)res);
}
for (;;) {
if (FR_OK != (res=f_readdir(&dp, &fi)) || 0 == fi.fname[0]) {
break;
}
printf("file: %s, %d Bytes\n", fi.fname, fi.fsize);
}
if (FR_OK != res) {
printf("f_readdir failed, %d.\n", (i32)res);
}
char buf[4096];
UINT br, bw;
// 读取config.txt文件内容,并打印
if (FR_OK != (res=f_open(&fp, "/config.txt", FA_READ))) {
printf("f_open failed, %d.\n", (i32)res);
} else {
int has_put = 0;
for (;;) {
f_read(&fp, buf, 4096, &br);
if (0 == br) {
break;
}
}
f_close(&fp);
}
// 将config.txt文件内容写入write.txt中
if (FR_OK != (res=f_open(&fp, "/write.txt", FA_WRITE | FA_CREATE_ALWAYS))) {
printf("f_open failed, %d.\n", (i32)res);
} else {
if (FR_OK != (res = f_write(&fp, buf, br_acc, &bw))) {
printf("f_write failed, %d.", (i32)res);
}
printf("bw: %d.\n", (i32)bw);
f_close(&fp);
}
// 卸载文件系统
if (FR_OK != (res=f_unmount(""))) {
printf("f_unmount failed, %d.\n", (i32)res);
}
while (1) {
tight_loop_contents();
}
return 0;
}
然后,我们就可以将SD卡(安全)拔出,然后到插到电脑上检查一下write.txt文件是不是真的被写入了。完结撒花!
写在后面
为什么写这篇文章?
2022年,与同学组队参加了全国大学生计算机系统能力大赛。该比赛的主要目的是让同学们在RISC-V硬件平台上,自行设计并制作一个支持部分POSIX系统调用的操作系统内核程序。
当时项目的情况是:人有三名,任务有两个你不是其中之一。另外两名队员负责编写操作系统的核心功能(进程管理、内存管理以及虚拟文件系统VFS)。而笔者则为我们的主要外设——SD卡——编写驱动程序。
作为一名已经上了几年学的计算机学生,笔者对于外部设备的认识基本停留在Linux中/dev
目录下的一个文件node。有大赛这个机会,能够接触一些底层原理,于我而言还是很有吸引力的。哈!尽管可能没有什么大用处,权当满足我的好奇心了。当然,经历了很多痛苦,查了很多文档与博客,最后还是做出来了。
最近想用pico做一个小项目,需要读读配置文件、写点持久化数据啥的。于是就把老代码翻出来,接着用。当然,把代码跨平台(RV64到ARM)、跨语言(Rust到C)迁移,免不了很多问题。因此又迅速地浏览了一遍相关资料。代码调通了,顺便记录一下,于是就有了这篇文章。
文章评论