写在前面

这是一篇记录在xv6上移植Fatfs文件系统过程的日志,在写下这段话时,我还没有开始移植,不知道会遇到什么困难。因为不是移植成功后再回顾过程写的,做到哪里写到哪里,所以组织上会很乱。但是可以完整的记录下我遭遇和解决问题的过程。希望能给有需要的读者提供一些宝贵的经验。本人由于水平有限,难免错误,欢迎大佬指正。

制作文件镜像

本次移植主要在qemu上面,首先需要制作一个文件镜像fs.img。下面是制作fat32格式文件镜像的命令,放在一个.sh脚本中。先在里面预先放置一些文件。测试时使用。

#!/bin/sh
# dst = /mnt  space aside "=" is wrong!!!
dst=/mnt
# echo $dst
dd if=/dev/zero of=fs.img bs=512k count=512
# echo $dst
mkfs.vfat -F 32 -s 4 fs.img
# echo $dst
sudo mount fs.img $dst
# echo $dst
# echo hi
cd $dst
sudo mkdir dir1
# pwd
sudo mkdir dir2
sudo mkdir dir3
sudo dash -c "echo hello world > file1"
cd dir1
sudo mkdir dir4
sudo sh -c "echo hi my bro! > file2"
cd dir4
sudo sh -c "echo yes my girl! > file3"
cd /
sudo umount $dst

没仔细学过shell脚本,将就着用吧。

将新制作的fs.img替换掉原来的,重起os,很高兴的看到如下错误:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EDW5U4nF-1652438890032)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220509105828275.png)]

可见是在调用fsinit的过程中出现了问题。

接下来我们要做的工作就是移植FATFS,让它正确的运行起来了。

磁盘访问函数对接

文件镜像有了,解决下来我的想法是测试一下上层的函数,比如f_mountf_read等。先不急着改系统调用,先试一试能不能从文件镜像之中正确的读写文件。

那么接下来需要把Media Access Interface和qemu的虚拟磁盘接口对接。这是一个棘手的地方。比如怎么fatfs中diskio.c的函数disk_read和qemu虚拟磁盘读写函数virtio_disk_rw连接起来呢?这就要去研究一下了,真令人头大啊……

仔细思索一番,决定重新定义一个disk_rw_struct来连接接口。

struct disk_rw_struct{const uchar *data;int disk;uint64 sectorno;
};
DRESULT disk_read (BYTE pdrv,        /* Physical drive nmuber to identify the drive */BYTE *buff,        /* Data buffer to store read data */LBA_t sector,   /* Start sector in LBA */UINT count     /* Number of sectors to read */
)
{for(int i = 0; i < count; i++){struct disk_rw_struct a;a.data = buff;a.sectorno = sector + i;virtio_disk_rw(&a, 0);}return 0;
}

使用下面的测试函数测试一下基本功能,放在os启动的main函数中。如果能够成功输出我们在上面设置的目录的话,就算是成功了。

int fs_test(void)
{static FATFS sdcard_fs;FRESULT status;DIR dj;FILINFO fno;printf("/********************fs test*******************/\n");status = f_mount(&sdcard_fs, _T("0:"), 1);printf("mount sdcard:%d\n", status);if (status != FR_OK)return status;printf("printf filename\n");status = f_findfirst(&dj, &fno, _T("0:"), _T("*"));while (status == FR_OK && fno.fname[0]) {if (fno.fattrib & AM_DIR)printf("dir:%s\n", fno.fname);elseprintf("file:%s\n", fno.fname);status = f_findnext(&dj, &fno);}f_closedir(&dj);return 0;
}

然后更改好了上述接口,解决了头文件的冲突等众多问题之后,总算能够正常编译链接了,启动!运行!结果……

scause的值为D代表load page fault。通过回溯发现f_mount就错了,但是到底哪里有问题呢?似乎又陷入了一筹莫展的境地……

经过分析,有很多bug,一个套着一个。这里被好好折磨了一番。最终靠着队友找出了问题,有大腿抱就是好,汗颜!

首先f_test最终会调用到sleepyield进程时要考虑是否初始化了进程?基于这个考虑,似乎不能放在main中了,我们把这个函数应该在fork_ret里面调用。

其次修改BSIZE的值,从1024改为512。这个错误和前面一个错误,经过回溯,都是在函数virtio_disk_rw调用sleep之后panic了。错误是D,但是光有这些信息,实在是很难猜测具体哪里有问题。令人头疼!

另外virtio_disk_initmain中调用正常,但是在进程的环境下,通过disk_initialize调用又有异常了。我猜测是因为页表的原因?virtio_disk_init只用初始化一次,干脆就放在main中。

不管如何,现在终于好了,没有panic了,但是没有按照预期的输出文件名:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZbAqq9EI-1652438890034)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220509211539337.png)]

直接卡在了fs_test之后的for(;;);。为什么没有输出呢?是不是ffconf.h中的设置问题?难道好不容易迎来了胜利的曙光……

最终发现是语言格式的问题,修改ffconf.h中的FF_CODE_PAGE437,问题得到解决。可能Fatfs的作者是日本人?所以字符编码默认是日语?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRwcji14-1652438890034)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220509214314524.png)]

最终总算是读出了,虽然文件名是都是粗暴的大写(听队友说是linux和windows的不同,如果是windows格式化镜像文件可以正常显示大小写)。

修改系统调用

经过昨天一天的苦战~~“划水”~~,part1总算是完成了,接下来进行part2,修改系统调用。这一个阶段的目的是修改几个系统调用之后,可以正确运行类似ls之类的基本的用户程序。

就先拿open来“开刀”。

这里岔开一下,在做的过程中,突然想到了一个问题,我能不能直接在用户态调用Fatfs提供的接口呢?比如写一个ls.c用户程序,正常的途径是通过openreadclosereaddir等系统调用,进入内核执行,也还是调用FatFs提供的接口,那么我为什么不直接使用Fatfs的接口写ls.c的程序呢?

这样做肯定是不可以的。因为用户程序二进制文件没有和kernel二进制文件链接在一起,FatFs的接口都链接在kernel中,所以用户程序直接访问这些接口肯定是访问不到的。

用户程序肯定是只能直接调用操作系统提供的接口(系统调用函数),不能直接使用操作系统内部的函数(例如FatFs的接口)。

sys_open

这里我们借用内核中的sys_open来实现用户层上的open函数。

原来一开始想直接上手openat,结果发现要考虑的有点多,因为FatFs中file和directory是分开的两个结构体,处理起来比较麻烦。这也是一个经验,在构建复杂的程序时,先一切从简,把程序跑起来,后面可以再考虑进行优化之类的。

这里再做的时候,发现了一些问题,比如f_open这类函数的路径,是只能传入绝对路径吗?能不能是相对路径呢?

实验了一下,似乎是可以的,这样就好办了。

重新定义了file结构体:

struct file{enum {FD_NONE, FD_FILE, FD_DIR} type;union obj{FIL f;DIR d;}obj; int ref;
};

然后sys_open先利用f_stat找到相关路径的文件,判断其类型,再根据其类型决定调用f_open还是f_opendir

ok了,进行到这里,新的sys_open就算是写好了,那么先测试一下吧。

怎么测试呢?本来想通过用户态下写一个fstest程序,调用open,然后在新的sys_open里面结束时打印出所有打开的文件。

这样做的话,os就要和原来的文件系统解耦了。于是,我满怀信心的注释掉了原文件系统(fs)的一些函数,取消了一些文件,潇洒的解决了编译错误。make!run!结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLWnnWPy-1652438890035)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220510154629458.png)]

一堆链接错误!主要是因为fs相关的代码我虽然取消了,但是其他的模块,如exec.cproc.c等,还用到了文件系统相关的代码,需要将其替换。瞬间萎了……好吧,看来步子迈的太大了。回去一点一点来……

不管怎么样,替换整个文件系统都是必须要做的,看来是逃不掉了,做好背水一战的准备!就按照上图中的错误一点一点解决!开始!见“替换文件系统”一节。

对于文件名为console的文件,在init打开时初始化,要特殊处理。

sys_write

调用filewrite,一般文件调用Fatfs提供的接口。区分DIR和FIL即可,其他的细节可以参考原xv6。为了实现用户空间的printf(需要调用write系统调用),可以先把类型为设备的文件的逻辑先写了。

sys_read

在没有实现时(空函数,返回0),shell程序会自动退出(进程退出),然后init.c里面wait的进程再重启shell。所以可以先实现设备的读取。

替换文件系统

修改exec.c

写到这里已经是第三天了,继续加油!

exec函数中是使用namei获得ip,然后使用readi读取ip中的内容。我使用f_open替代namei,读取文件,然后使用f_read读取文件。但是发现f_read居然不支持偏移(off),难道每次读取都只能从文件开头读吗?在Load program into memory的for循环中,可见是需要off的。

看到FIL结构体中的fprt字段,我灵机一动,能否通过修改这个字段来达到偏移读取的效果呢?验证一下。

似乎不行……没有输出。

通过阅读f_read的源代码,发现只有fptr在sector的边界,才会读磁盘,一开始就手动给它赋一个非0的初值,会导致f_read跳过了

if (fp->fptr % SS(fs) == 0)

这条判断语句,导致没有把磁盘中的内容读到FIL结构体的buf中,直接用memcpy复制其中的内容,所以没有输出。知道了这一点,我们现在需要修改f_read源代码。

阅读代码f_read的代码,记录一下细节:

  1. csect = (UINT)(fp->fptr / SS(fs) & (fs->csize - 1));
    

    csize是一个cluster包含的sector的数量。csect计算了当前的fptr位于一个cluster中的sector的号。

  2. 如果fptr是sector对齐的,则先计算出要读的sector块号:

    csect = (UINT)(fp->fptr / SS(fs) & (fs->csize - 1)); /* Sector offset in the cluster */sect = clst2sect(fs, fp->clust);  /* Get current sector */sect += csect;
    

    其中如果csect为0,需要重新计算fp->clust

    上面clst2sect计算得到的sect是每个cluster开始的sector号,csect是sector在簇中的偏移号,二者相加。

    如果要读的内容(btr)大于一整个sector(cc = btr / SS(fs);),cc大于0,则把位于当前cluster的sector直接读入目标地址,continue。

    否则读入当前的文件buf。

    退出for循环之后,把buf的值复制到目标地址。

    总的来说,这样的代码设计,没有考虑到文件不按顺序,自己指定偏移的情况。

    总结下来,我只需要补充一种情况的处理。传进来的文件,如果偏移是sector对齐的,则没有问题;如果不是对齐的,但是buf中有缓存(f->sect == sect),也没有问题;只要处理非对齐但是又没有缓存的情况。

    我们解决的方法是在f_read的for循环加一段代码:如果fptr非sector对齐,判断其属于的sector号,如果没有缓存,则缓存。

千言万语,在for循环前加上这么一段代码:

/*****************************new added zyy*******************************/csect = (UINT)(fp->fptr / SS(fs) & (fs->csize - 1));   /* Sector offset in the cluster */if (csect == 0) {                   /* On the cluster boundary? */
//从 ==0 改为 < 512,说明是第一个块if (fp->fptr < 512) {         /* On the top of the file? 小于512,说明是第一个块*/clst = fp->obj.sclust;     /* Follow cluster chain from the origin */} else {                      /* Middle or end of the file */
#if FF_USE_FASTSEEKif (fp->cltbl) {clst = clmt_clust(fp, fp->fptr);  /* Get cluster# from the CLMT */} else
#endif{clst = get_fat(&fp->obj, fp->clust);  /* Follow cluster chain on the FAT */}}if (clst < 2) ABORT(fs, FR_INT_ERR);if (clst == 0xFFFFFFFF) ABORT(fs, FR_DISK_ERR);fp->clust = clst;                /* Update current cluster */}sect = clst2sect(fs, fp->clust);   /* Get current sector */if (sect == 0) ABORT(fs, FR_INT_ERR);sect += csect;#if !FF_FS_TINYif (fp->sect != sect) {           /* Load data sector if not in cache */
#if !FF_FS_READONLYif (fp->flag & FA_DIRTY) {        /* Write-back dirty sector cache */if (disk_write(fs->pdrv, fp->buf, fp->sect, 1) != RES_OK) ABORT(fs, FR_DISK_ERR);fp->flag &= (BYTE)~FA_DIRTY;}
#endifif (disk_read(fs->pdrv, fp->buf, sect, 1) != RES_OK)   ABORT(fs, FR_DISK_ERR); /* Fill sector cache */}
#endiffp->sect = sect;/*****************************new added zyy*******************************/

其中大部分是复制的。

思来想去,总感觉似乎还是不对。cluster是按照顺序找的,最开始的时候fp->clust为0,是找不到的。看来简单的修改是不行了,整个f_read函数要重写……

先放一放吧。不修改f_read,遇到读取偏移的情况,先顺序读取,定位到那里吧,先不管效率了,重新封装一下f_read函数。

FRESULT f_read_off (FIL* fp,     /* Open file to be read */void* buff,   /* Data buffer to store the read data */UINT btr,   /* Number of bytes to read */UINT* br,  /* Number of bytes read */uint off   // offset
)
{//reset fpfp->fptr = 0;fp->sect = 0;char temp_buf[FF_MAX_SS];for(int i = 0; i < off/FF_MAX_SS; i++){f_read(fp, temp_buf, FF_MAX_SS, br);}f_read(fp, temp_buf, off % FF_MAX_SS, br);return f_read(fp, buff, btr, br);}

接下来就是使用读取file替代读取inode了。

修改proc.c

主要是forkexit中的问题。涉及到p->cwd的代码。这里它的类型换为file *。当然type是DIR。

突然又意识到一个问题。使用了FatFs后,当前目录(cwd)记录在文件系统里(不是想xv6那样记录在struct proc中),那么多个进程如果是不同的cwd怎么办?

初始化文件系统

forkret里面调用一个函数fatfs_init,这个函数调用f_mount

重新制作文件镜像

第四天!

现在我们使用用户程序测试内核函数,需要重新制作一下文件镜像。基本思想是生成一个fat32格式的文件镜像,然后把用户程序拷贝进去。

我们先把init.c生成的程序复制进fs.img

然后执行。结果不知道为什么,在初始化内核后,本该执行进入用户空间执行init.c时卡住了哪里出了bug?

__attribute__((section(".startup"))) void main() {__attribute__((unused)) char *argv[2];// set arg// for(;;);argv[0] = path;argv[1] = 0;ktest();exec(path, argv);for(;;);
}

如我们上图的initcode的代码所示。forkret之后,执行以上代码,然后通过exec执行init.c的程序。

好吧,以上代码的exec应该不是直接执行exec.c中的函数,而是调用sys_exec系统调用。而这个系统调用我还没有完成!

修改了sys_exec后,成功进入了exec.c中的exec函数,这个函数把elf文件从磁盘加载到内存并执行。然而又发生了可怕的kerneltrap!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PEdj3zVp-1652438890037)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220512105904576.png)]

问题很简单,上一个bug是因为在f_openbr参数传入了NULL。但是修改后,又发生了新的问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Td8OFvL-1652438890038)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220512111058660.png)]

没有正确的返回用户空间执行init.c,问题在哪里?是没有正确的把文件读入内存吗?

使用objdump打印汇编。发现应该是进入内核usertrap处理系统调用的时候,发生了“not from user mode”。(无论是调用printf还是open,都是应该进入内核态)。

关键原因在于不是用户态?所以不能往a7里面加载指令(系统调用)。所以li a7 x这条指令是非法指令?

经过和队友痛苦的排查,推测是非法指令的问题,即init文件读入有误。为了验证猜测,把initcode的开始两个字节的指令改为0x00 0x00。发生了相似的错误。但是为什么会panic: usertrap: not from user mode呢?这个原因是我们对于xv6的内核更改之后发生的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3f7r7ccO-1652438890039)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220512185119714.png)]

最终确定是我们使用了rustsbi,在u态发生指令异常错误时,没有直接进入s态,而是进入了m态,然后再进s态,设置了spp。这就要修改sbi的do_transfer_trap代码,这部分我之前没有熟读指令集手册,对于riscv的中断异常代理是一窍不通,说多了都是泪:

if ctx.mstatus.mpp() == MPP::User{mstatus::set_spp(SPP::User);} else{mstatus::set_spp(SPP::Supervisor);}

可见没有panic: usertrap: not from user mode,非法指令的进程直接被killed了。这个问题算是告一段落。再次感叹,有个强大的队友真好。

接下来还是老问题,把elf文件加载到内存里失败了,还是要修好这个bug!

第五天!这几天搞得身心疲惫,但是还是要继续,这就是生活吧。

使用下列代码,打印内存中的值:

uint64* temp = (uint64*)pa;
printf(green("%x"), *(uint64*)((uint64)temp + 0xaf8));

其中0xaf8init.asm中的地址值,发现此地址中为0!所以产生了非法指令的情况!为什么是0呢?为什么?!为什么?!

把内存中pa的值打印,和init.asm对比:

/*********************************************************/if(1){uint64* temp = (uint64*)pa;// printf(green("%08x"), *(uint64*)((uint64)temp + 0xb7c)); for(int j = i; j < i+n; j += 4){printf(yellow("%x: "), j);printf(green("%08x\n"), *(uint64*)((uint64)temp + j)); }}
//这里有错误,除了temp前的uint64,其他都改为uint32!这里使用了08x打印高8位,所以看上去没错?
/*********************************************************/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-usFSyjtL-1652438890042)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220513103117125.png)]

发现前面几条还一样,但是从188开始,指令码不一样了!后面有的正确,有的错误,为什么会有这种情况呢?现在读取的是pa,可见pa错了。那么是文件本身错了,还是文件读取到pa时出错了?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L2Huq7Rz-1652438890046)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220513110813580.png)]

/**********************直接从文件中读取,检测文件是否正确*******************/char buf[4];for(i = 0; i < ph.filesz; i+=4){printf(yellow("%x: "), i);f_read_off(&f, (void *)buf, 4, &br, ph.off+i);printf("%08x\n", *(uint32*)buf);}for(;;);/**********************直接从文件中读取,检测文件是否正确*******************/

直接从文件中读,而不是pa,可见是正确的。我又看到了一点希望。说明是loadseg的问题。我开始怀疑了,难道是我的f_read_off设计的有问题?

这回我在loadseg里面改变了策略,先用buf从文件中读4个字节,然后复制到pa(先不考虑效率),这下子正确了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VRyrg3t0-1652438890047)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220513115807788.png)]

因为open时,没有名为console的文件,所以sys_open报错panic。还是回到上一个问题,为什么f_read_off失败了,不解决始终是个隐患!不过由于时间问题,当务之急是先把程序跑通。

init.c

init.c中,使用open打开console文件,由于设备文件是特殊文件,所以我们需要在sys_open中特殊处理。

处理特殊文件

linux里“一切皆为文件“,除了普通文件,还有一些特殊的文件,像是设备文件,pipe等。由于我们移植了fatfs,这些文件就要特殊处理了,一定程度上丧失了一些“优雅”和设计上的精巧,不过这也是没有办法的事情。

console

原来的xv6创建设备文件时,先使用mknod创建对应的inode,然后使用open打开文件。移植了fatfs后,似乎mknod没有了必要。

然后注意要实现一下sys_dup,不然open打开了文件描述符0,但是printf使用文件描述符1,而sys_dup未能复制文件描述符,会出现输出的错误。

最终结果

sh程序复制进文件镜像里面。shell成功启动了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qLgsa4O2-1652438890049)(https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/image-20220513164758658.png)]

接下来是写用户程序,并且继续完善系统调用的时候了。可能还是会有一些小问题,但是对于总体来说已经无关紧要了。关键是队友那边自己写的fat32已经调通了,我这边的工作就没有进行下去的必要了。如果以后有需要,再继续完善吧。

回顾总结

这次移植,历时五天,完全是把FatFs当做黑盒在用。除了在读取偏移值的时候仔细看了f_read,其他的部分基本没看。对于程序的行为不了解,用起来总是有那么一种束手束脚的感觉。比如在loadseg函数中读取偏移值发生的问题,至今未解。和队友交谈了一下,发现了读取失败的地址是连续的1024,512字节……似乎有那么一些规律,难道是缓存的问题?但是我已经无心再研究。

最后感谢我的队友,如果没有他的帮助,我还要花费更多的时间。

xv6移植Fatfs相关推荐

  1. STM32CubeMX系列09——SDIO(SD卡读写、SD卡移植FATFS文件系统)

    文章目录 1. 准备工作 1.1. 简单扫盲 1.1.1. SD卡 1.1.2. TF卡 1.1.3. SDIO接口 1.2. 所用硬件及原理图 2. 创建工程 2.1. 选择主控 2.2. 系统配置 ...

  2. 【FatFs】手动移植FatFs,将SRAM虚拟U盘

    [FatFs]手动移植FatFs,将SRAM转化为文件系统 1. 实验环境 Keil5 MDK-ARM,编译器使用ARM Compiler V6.16 NUCLEO-H723ZG STM32CubeM ...

  3. 【FatFs】基于STM32 SD卡移植FatFs文件系统

    相关文章 <[SDIO]SDIO.SD卡.FatFs文件系统相关文章索引> 1.前言 FatFs是一个通用的FAT/exFAT文件系统模块,用于小型嵌入式系统.它完全是由 ANSI C 语 ...

  4. stm32f769 寄存器配置SD卡---移植fatfs

    昨天开始在上周实现的SD卡读写基础上移植fatfs文件系统,开始不是很顺利,之前没有搞过,完全按照f767的例程移植的,但是在加载SD卡时一直是失败的,很郁闷,折腾了一天,结果还不理想,睡了个好觉,思 ...

  5. stm32f103c8t6移植Fatfs文件系统出现的一些问题

    一.环境 keil5,使用库函数 二.移植对象 stm32f103zet6 >> stm32f103c8t6 三.连接方式 硬件SPI1(PA5,6,7) 四.主函数代码 #include ...

  6. 使用vscode + gcc进行 STM32 单片机开发(三)DMA读写SD卡,移植FATFS文件系统

    背景 在本系列的前两篇文章( 使用vscode + gcc进行 STM32 单片机开发(一)编译及调试 使用vscode + gcc进行 STM32 单片机开发(二)gcc环境 移植rtthread) ...

  7. 【STM32Cube_20】在SD卡上移植FATFS文件系统

    本篇详细的记录了如何使用STM32CubeMX移植FATFS文件系统到SD卡上. 1. 准备工作 硬件准备 开发板 首先需要准备一个开发板,这里我准备的是STM32L4的开发板(BearPi): Mi ...

  8. STM32移植FatFS文件系统最新版R0.14b

    STM32移植FatFS文件系统 目录 一.前言 二.硬件及软件准备 三.移植FatFS文件到工程文件夹下 四.将移植文件添加到工程中 五.修改"diskio.c"文件 六.配置& ...

  9. [SDCard_FatFs笔记][一]STM32F7使用SDMMC外设移植FatFs遇到f_mount挂载成功,而f_open函数未运行的解决方法

    关于STM32F7使用SDMMC外设移植FatFs遇到f_mount 挂载成功,而f_open 函数未运行的解决方法 本文开发环境: [ IDE环境:Keil_MDK_5.28 ] [ MCU型号:S ...

最新文章

  1. 大数据分布式集群搭建(6)
  2. 【百度地图API】如何制作多途经点的线路导航——驾车篇
  3. VTK:线性挤压用法实战
  4. UVa 264 - Count on Cantor
  5. denison php,Parker / Denison丹尼逊柱塞泵首相系列相关说明
  6. 计算机的只读储存器,只读存储器
  7. vue数据定义格式_用好单元格自定义格式,让Excel按照你的要求显示数据
  8. 试试Write Live Writer在博客园的使用
  9. iOS开发小技巧 -- tableView-section圆角边框解决方案
  10. CISCO OSPF-NSSA实验
  11. 190117每日一句
  12. windows10 快速切换网络适配器
  13. 使用wireshark分析HTTPS数据包
  14. 树莓派驱动数码管c 语言,用树莓派驱动八段数码管实现倒计时
  15. 从帝王之术中窥探天机
  16. PostgreSQL 源码解读(216)- 实现简单的扩展函数
  17. c语言-网吧管理系统
  18. SDH原理--2.SDH信号的帧结构
  19. java定义一个eat方法,java基础5实战开发Day2/方法/2020-04-26
  20. java中Scanner用法

热门文章

  1. 在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加“#include pch.h”
  2. 交通事故2018数据_现实世界数据科学项目:交通事故分析
  3. 【Python游戏】用Python 和 Pyglet 编写一个我的世界小游戏 | 附源码
  4. 四川一度智信:2021网店推广技巧分享
  5. 迅雷云加速开放平台接口说明
  6. 织梦表单html模板,dedecms织梦模板 自定义表单分页+模版显示的源码
  7. Spring Boot+Vue开源项目
  8. 一键生成,LOGO免费设计小技巧
  9. Dreamweaver 快速批量增加图片链接
  10. 疫情下的远程办公与桌面分享