本帖最后由 suyong_yq 于 2015-5-21 01:29 编辑
SD卡SPI模式移植pfatfs文件系统
——基于YL-KL26Z开发板
suyong_yq 2015-05
【pfatfs情缘】【SD卡SPI驱动】
【sdcard_spi简介】
【移植sdcard_spi】
【pfatfs的移植】
【总结】
【pfatfs情缘】
今天我们的主角是pfatfs,但要先提一下fatfs。
Fatfs是一种可以在小型嵌入式应用中使用的FAT文件系统,完全由C89标准的C语言编写而成,并且在设计的时候做了非常好的分层,核心软件部分与具体平台无关,这就意味着它非常容易移植到各种不同的嵌入式平台上。事实上,在很多资源非常有限的小型嵌入式平台上都可以运行fatfs文件系统作为海量存储的扩展组件,比如说经典的8051、PIC、AVR等,当红小生ARM单片机自然也广泛使用了fatfs文件系统。总之,我们知道fatfs能让吊丝单片机搞定高大上的文件系统,真是个好东西。
但随着fatfs软件的不断发展,功能越来越完善,代码变得越来越复杂,对应用系统的资源占用也变大。当然,现在的单片机配置也在不断提高,完全能够满足应用程序日益增长的复杂度。但是,作为一个思想传统的电子工程师,我还是很怀念当初小巧精悍的单片机小程序。其实在单片机的应用中,对单个组件强大功能的需求并不是那么强烈,只要够用就好,更复杂的内容应该放在应用程序的设计上。我猜fatfs的作者ChaN也是这样的想法,因此在fatfs诞生之后,他还继续发挥余热,创作了pfatfs。突然想起来,uIP也是在LwIP之后由同一个作者搞出来的,大道至简才是硬道理。
pfatfs是fatfs的一个子集,特别适用于资源更加受限的8位单片机,甚至RAM比一个扇区还小都能搞得定(~o^,SD卡的一个扇区的典型大小是512字节)。
pfatfs在分层的架构设计上继承了fatfs结构清晰的优良作风,但是代码更简单,占用资源更少。其关键特性如下:
· 超小的RAM内存消耗(44字节的静态内存占用+栈空间)。 · 超小的ROM代码占用(2K - 4K字节)。 · 支持FAT12, FAT16和FAT32文件系统。 · 只支持单个卷和单文件操作。 · 将文件作为数据流进行操作,在写入文件的时候有一些限制。
总之,同fatfs相比,pfatfs的特点就是“小”,真的是太小了。
从架构上看,pfatfs对硬件的需求比fatfs小很多,如图1所示。
|
| 图1-a fatfs结构 | 图1-b pfatfs结构 | 图1 fatfs vs pfatfs结构 |
本着够用就好的原则 ,pfatfs仅仅提供了常用的API,没有fatfs那么花哨:
· pf_mount:挂在一个卷 · pf_open:打开文件 · pf_read:读文件 · pf_write:写文件 · pf_lseek:移动读写文件的指针 · pf_opendir:打开目录 · pf_readdir:读一个目录
不过可惜的是,这里没有提供创建文件和删除文件的API,否则pfatfs就真的圆满了。
pfatfs在移植上也比fatfs简单不少,只需要实现三个底层的函数:
· disk_initialize:初始化存储介质 · disk_readp:读一个扇区 · disp_writep:写一个扇区
而在移植fatfs的时候,要实现六个底层的函数,除了disk_initialize、disk_read和disk_write,还有disk_status、disk_ioctl和get_fattime,其中getfattime还需要硬件上提供一个定时器,而disk_ioctl更是隐含地包含了无数个需要实现的功能。这样看来pfatfs真是太有良心啦。
在项目主页上可以戳开各个函数的说明页面,了解它们的详细用法。
【SD卡SPI驱动】
【sdcard_spi简介】
在上一节提到,我们要移植pfatfs,需要实现与底层相关的三个函数disk_initialize,disk_readp,disk_writep,分别实现对存储介质的初始化,读扇区和写扇区。我们比较常用的大容量存储介质当然是SD卡啦,这个东东可以使用SPI总线驱动,当然用专用的SDIO总线驱动性能更好,但SDIO一般都是在高端单片机配置,屌丝单片机用SPI也是不错的。
关于使用SPI驱动SD卡过程,ChaN的网站上也有说明,不得不感叹,这个日本兄弟真是活力无限啊。当然,网页是用英文写的(还好不是日文),如果觉得看得费劲,还在baidu.com中搜索“SD卡编程指南”可以找到这份文档的中文翻译版本,那是我早年翻译的作品,原稿已经在某次硬盘崩溃中遗失了,却不成想有人竟然把PDF版本的文档放到了网上,感慨。。。
如果连中文也不愿意看,这里我还有一个大招,就是代码库。哈哈,这次连编译都不需要了。我照虎画猫,将对SD卡的SPI操作逻辑也做了一个封装,做了一个sdcard_spi的构件,只要在具体的平台上实现标准的SPI驱动并注册到sdcard_spi中,就可以方便地用sdcard_spi的API操作SD卡,“从此妈妈再也不用担心我在写SD卡驱动浪费时间啦。。。”
sdcard_spi构件提供了一个sdcard_spi.lib的库文件,这是在KEIL中已经编译好的库,被封装起来了,不会被用户不小心改掉代码。对应地,还有描述用户接口的sdcard_spi.h文件,预览其内容如下:
- /* sdcard_spi.h */
- #ifndef __SDCARD_SPI_H__
- #define __SDCARD_SPI_H__
- #include <stdint.h>
- #include <stdbool.h>
- #define SDC_BLOCK_SIZE_BYTE (512U)
- typedef struct
- {
- void (*SPI_InitFunc)(void);
- uint8_t (*SPI_SwapByteFunc)(uint8_t txData);
- void (*SPI_SetBaudrateFunc)(uint32_t baudrate);
- void (*SPI_AssertCsFunc)(bool enable);
- uint32_t Baudrate;
- } SDC_SPICallback_T;
- typedef enum
- {
- eSDCCardUnknown = 0U,
- eSDCCardTypeOfSDv2 = 1U,
- eSDCCardTypeOfSDHC = 2U
- } SDC_CardType_T;
- typedef struct
- {
- SDC_CardType_T CardType;
- uint32_t CardBlkCnt;
- } SDC_Info_T;
- bool SDC_Install(const SDC_SPICallback_T *ioPtr);
- bool SDC_InitCard(SDC_Info_T *infoPtr);
- bool SDC_WriteBlock(uint32_t blkIdx, uint8_t *txPtr);
- bool SDC_ReadBlock(uint32_t blkIdx, uint8_t *rxPtr);
- uint32_t SDC_WriteBlocks(uint32_t blkIdx, uint32_t blkCnt, uint8_t *txPtr);
- uint32_t SDC_ReadBlocks(uint32_t blkIdx, uint32_t blkCnt, uint8_t *rxPtr);
- #endif /* __SDCARD_SPI_H__ */
复制代码
另外,还提供了功能完备的测试应用代码demo_main.c和demo_inc.h,可以在移植成功后进行验证。
【移植sdcard_spi】
之前参加社区活动,收到了一块YL-KL26的板子,板子上有MicroSD卡的插座,并且就是用SPI接口进行连接的,刚好可以作为实验平台。我一时手痒,就在这块板子上移植了sdcard_spi组件。
首先,当然是要准备KL26芯片的SPI驱动程序了,我使用的是自家维护的LiteFwLib固件库中的fsl_spi驱动程序,预览一下接口文件fsl_spi.h。
- /* fsl_spi.h */
- #ifndef __FSL_SPI_H__
- #define __FSL_SPI_H__
- #include <stdint.h>
- #include <stdbool.h>
- /*
- * Enumeration.
- */
- typedef enum
- {
- /* Clock line is low when idle. Tx data line is high when idle.
- * Data valid when at rising edge.
- * CPOL = 0, CPHA = 0 */
- eSPI_PolPhaMode0 = 0U,
- /* Clock line is low when idle. Tx data line is high when idle.
- * Data valid when at falling edge.
- * CPOL = 0, CPHA = 1 */
- eSPI_PolPhaMode1 = 1U,
- /* Clock line is high when idle. Tx data line is high when idle.
- * Data valid when at falling edge.
- * CPOL = 1, CPHA = 0 */
- eSPI_PolPhaMode2 = 2U,
- /* Clock line is high when idle. Tx data line is high when idle.
- * Data valid when at rising edge.
- * CPOL = 1, CPHA = 1 */
- eSPI_PolPhaMode3 = 3U
- } SPI_PolPhaMode_T;
- /*
- * Structure.
- */
- /* Defaultly work as master. */
- typedef struct
- {
- uint32_t BusClkHz;
- uint32_t Baudrate;
- SPI_PolPhaMode_T PolPhaMode;
- bool enAutoCs; /* Default is controlled by GPIO. */
- bool enLSB; /* Default is MSB. */
- } SPI_MasterConfig_T;
- typedef struct
- {
- SPI_PolPhaMode_T PolPhaMode;
- bool enLSB; /* Default is MSB. */
- } SPI_SlaveConfig_T;
- /*
- * API.
- */
- /* Config. */
- bool SPI_ConfigMaster(uint32_t idx, const SPI_MasterConfig_T *configPtr);
- bool SPI_ConfigSlave(uint32_t idx, const SPI_SlaveConfig_T *configPtr);
- uint32_t SPI_SetMasterBaudrate(uint32_t idx, uint32_t busClkHz, uint32_t baudrate);
- void SPI_SetTxEmptyIntEnable(uint32_t idx, bool enable);
- void SPI_SetRxFullIntEnable(uint32_t idx, bool enable);
- void SPI_SetTxEmptyDmaEnable(uint32_t idx, bool enable);
- void SPI_SetRxFullDmaEnable(uint32_t idx, bool enable);
- /* Tx. */
- void SPI_PutTxData(uint32_t idx, uint8_t txData);
- bool SPI_IsTxBufferEmpty(uint32_t idx);
- void SPI_PutTxDataBlocking(uint32_t idx, uint8_t txData);
- void SPI_WaitTxDataDoneBlocking(uint32_t idx);
- /* Rx. */
- uint8_t SPI_GetRxData(uint32_t idx);
- bool SPI_IsRxBufferFull(uint32_t idx);
- uint8_t SPI_GetRxDataBlocking(uint32_t idx);
- #endif /* __FSL_SPI_H__ */
复制代码
然后在sdcard_spi_adapter.c文件中实现了KL26的SPI向sdcard_spi的注册,让sdcard_spi在YL-KL26Z板上“接地气”。
- /* sdcard_spi_sdapter.c */
- #include "app_inc.h"
- #define mSDCARD_SPI_BAUDRATE (1000000U)
- static void mSPI_Init(void);
- static uint8_t mSPI_SwapByte(uint8_t txData);
- static void mSPI_SetBaudrate(uint32_t baudrate);
- static void mSPI_AssertCs(bool enable);
- const SDC_SPICallback_T gSdcSpiCallbackStruct =
- {
- mSPI_Init, /* SPI_InitFunc */
- mSPI_SwapByte, /* SPI_SwapByteFunc */
- mSPI_SetBaudrate, /* SPI_SetBaudrateFunc */
- mSPI_AssertCs, /* SPI_AssertCsFunc */
- mSDCARD_SPI_BAUDRATE /* Baudrate */
- };
- const SPI_MasterConfig_T mSpiMasterConfigStruct =
- {
- BSP_CLK_BUSCLK_HZ, /* BusClkHz */
- 100000U, /* Baudrate */
- eSPI_PolPhaMode0, /* PolPhaMode */
- false, /* enAuotCs */
- false /* enLSB */
- };
- static void mSPI_Init(void)
- {
- GPIO_SetPinLogic(BSP_GPIO_SDCARD_SPI_CS_PORT,
- BSP_GPIO_SDCARD_SPI_CS_PIN, true);
- GPIO_SetPinDir(BSP_GPIO_SDCARD_SPI_CS_PORT,
- BSP_GPIO_SDCARD_SPI_CS_PIN, true);
- SPI_ConfigMaster(BSP_SPI_SDCARD_IDX, &mSpiMasterConfigStruct);
- }
- static uint8_t mSPI_SwapByte(uint8_t txData)
- {
- //return SPI_MasterSwapDataBlocking(BSP_SPI_SDCARD_INSTANCE, txData);
- SPI_PutTxDataBlocking(BSP_SPI_SDCARD_IDX, txData);
- return SPI_GetRxDataBlocking(BSP_SPI_SDCARD_IDX);
- }
- static void mSPI_SetBaudrate(uint32_t baudrate)
- {
- SPI_SetMasterBaudrate(BSP_SPI_SDCARD_IDX, BSP_CLK_BUSCLK_HZ, baudrate);
- }
- static void mSPI_AssertCs(bool enable)
- {
- if (enable)
- {
- GPIO_SetPinLogic(BSP_GPIO_SDCARD_SPI_CS_PORT,
- BSP_GPIO_SDCARD_SPI_CS_PIN, false);
- }
- else
- {
- GPIO_SetPinLogic(BSP_GPIO_SDCARD_SPI_CS_PORT,
- BSP_GPIO_SDCARD_SPI_CS_PIN, true);
- }
- }
- /* EOF. */
复制代码
而应用程序的main函数的逻辑也是在sdcard_spi构件中写好的,直接拿来用就可以编译运行。
最终样例工程在KEIL中的工程组织如图2所示。
图2 sdcard_spi样例工程文件组织结构
在附件中提供了完整的程序及工程,大家可以下载代码包,编译工程直接在YL-KL26Z板子上运行。
打开之中的“05_spi_sdcard”工程,编译下载,然后打开串口终端软件,启动程序可以看到输出log,如图3所示:
从输出中可以看到卡的类型和大小。实际上我用的是一个2G的卡,一般这个大小的卡都是SDv2的,如果大家用8G的卡,基本上识别出来的都是SDHC卡。 然后提示说正在进行单个扇区读写的实验,我根据提示信息向第7个块中写“c”这个字符,输出log说已经写进去了,“c”的ASCII码是0x63。如图4所示。
图4
但是,是真的写进去了么?别急,下面就可以读出来看看了。根据log的提示,设定即将读取块的编号仍为7,这个时候就读出SD卡中存储在第7块中的内容了。如图5所示。
图5
哈哈,没错,就是0x63!
图6
如图6所示,这时,log提示要不要继续玩单块读写的实验,如果想再玩几次,输入“Y”即可,读写可以是不同的扇区,大家可以多试试。这里我输入“N”进入下一个实验,多块读写。如图7所示。
图7
在多块读写实验中,我实际设计的是连续操作两个块,但只要输入较小的块号就可以了。这次要写入的是8-9块,写入内容是“u”,对应的ASCII码是0x75。如图8所示。
图8
然后再从第7块开始读。等等,为什么不是第8块呢?之前我们在第7块里写了0x63,这次在第8块和第9块中写了0x75,读出来的时候就能看到差别了。好吧,试试看。如图9所示。
图9
Bingo,又对啦!
这个实验仍能连续玩多次,输入“Y”继续,输入“N”退出。
这个应用工程的main函数使用的就是sdcard_spi组件提供的标准验证流程。
好了,带着大家嗨皮这么久,就是要验证sdcard_spi能够正常工作,现在我们已经可以在板子上玩转SD卡啦。至此,保存代码,关闭工程,向代码仓库提交一次代码。歇一下,喝口水,然后创建下一个工程,向着最终的目标,pfatfs,进发!
【pfatfs的移植】
有了sdcard_spi的基础,这个阶段已经可以很轻松地读写SD卡了,下面我们就要实现对pfatfs的移植。
PS:已经移植好到样例工程也在附件的代码包中,为“05_spi_sdcard_pfatfs”工程。如果没空听我在这里鬼扯,可以略过这段,直接看代码。
从项目主页上下载的pfatfs代码我基本没动,就是在pff.h中把对几个API的支持全部打开了。 - #define _USE_READ 1 /* 1:Enable pf_read() */
- #define _USE_DIR 1 /* 1:Enable pf_opendir() and pf_readdir() */
- #define _USE_LSEEK 1 /* 1:Enable pf_lseek() */
- #define _USE_WRITE 1 /* 1:Enable pf_write() */
- #define _FS_FAT12 0 /* 1:Enable FAT12 support */
- #define _FS_FAT32 1 /* 1:Enable FAT32 support */
复制代码
对不需要的API还可以进行裁剪已进行进一步的瘦身,pfatfs真是工程师的贴心小棉袄,考虑得真周到!
我在pfatfs_diskio.c中仿照原装diskio.c的模板实现了pfatfs向sdcard_spi的对接。
- /*-----------------------------------------------------------------------*/
- /* Low level disk I/O module skeleton for Petit FatFs (C)ChaN, 2009 */
- /*-----------------------------------------------------------------------*/
- #include "diskio.h"
- #include "app_inc.h"
- /*-----------------------------------------------------------------------*/
- /* Initialize Disk Drive */
- /*-----------------------------------------------------------------------*/
- static uint32_t SDCardRxBufArea[128U];
- static uint32_t SDCardTxBufArea[128U];
- static uint8_t *SDCardRxBufPtr = (uint8_t *)SDCardRxBufArea;
- static uint8_t *SDCardTxBufPtr = (uint8_t *)SDCardTxBufArea;
- static DWORD lastWriteSectorIndex = (uint32_t)-1;
- static DWORD lastWriteSectorOffset = 0U;
- SDC_Info_T gSdcInfoStruct;
- DSTATUS disk_initialize (void)
- {
- DSTATUS stat;
- SDC_Install(&gSdcSpiCallbackStruct);
-
- // Put your code here
- /* Enable the Card power. */
- if (SDC_InitCard(&gSdcInfoStruct))
- {
- stat = RES_OK;
- }
- else
- {
- stat = RES_ERROR;
- }
- return stat;
- }
- /*-----------------------------------------------------------------------*/
- /* Read Partial Sector */
- /*-----------------------------------------------------------------------*/
- DRESULT disk_readp (
- BYTE* dest, /* Pointer to the destination object */
- DWORD sector, /* Sector number (LBA) */
- WORD sofs, /* Offset in the sector */
- WORD count /* Byte count (bit15:destination) */
- )
- {
- DRESULT res;
- bool bRet;
- uint32_t i;
- // Put your code here
- bRet = SDC_ReadBlock(sector, SDCardRxBufPtr);
- //printf("SDCard_ReadBlocks Sector %d.\r\n", sector);
- if (bRet)
- {
- //lastReadSectorIndex = sector;
- res = RES_OK;
- //dest = SDCardRxBufPtr+sofs;
- for (i = 0U; i < count; i++)
- {
- dest = SDCardRxBufPtr[sofs+i];
- }
- }
- else
- {
- res = RES_ERROR;
- }
- return res;
- }
- /*-----------------------------------------------------------------------*/
- /* Write Partial Sector */
- /*-----------------------------------------------------------------------*/
- DRESULT disk_writep (
- const BYTE* buff, /* Pointer to the data to be written,
- NULL:Initiate/Finalize write operation */
- DWORD sc /* Sector number (LBA) or Number of bytes to send */
- )
- {
- DRESULT res = RES_OK;
- bool bRet;
- if (!buff)
- {
- if (sc)
- {
- /* Initiate write process */
- lastWriteSectorIndex = sc;
- bRet = SDC_ReadBlock(sc, SDCardTxBufPtr);
- //printf("SDCard_ReadBlocks Sector %d.\r\n", lastWriteSectorIndex);
- if (bRet)
- {
- res = RES_OK;
- }
- else
- {
- res = RES_ERROR;
- }
- } else
- {
- /* Finalize write process */
- bRet = SDC_WriteBlock(lastWriteSectorIndex, SDCardTxBufPtr);
- //printf("SDCard_WriteBlocks Sector %d.\r\n", lastWriteSectorIndex);
- if (bRet)
- {
- res = RES_OK;
- }
- else
- {
- res = RES_ERROR;
- }
- lastWriteSectorOffset = 0U;
- }
- } else
- {
- /* Send data to the disk */
- while (sc != 0U)
- {
- SDCardTxBufPtr[lastWriteSectorOffset++] = *(uint8_t *)buff++;
- sc--;
- }
- }
- return res;
- }
复制代码
关于这三个函数的实现内容,在项目主页中说得很明白了,我就不再这里重复了,但是在具体实现的时候有几个点要特别注意:
1. disk_writep函数的不同参数组合对应不同的行为,并不是说这个函数只是单纯地向SD写数,在准备写数的过程中还会先从SD卡上把整个块读出来,改掉要写的部分,然后再把整块的数据整个写回去。 2. 我在移植的时候对读写内容进行了优化,如果连续写一个块,只要读一次就可以了,这样可以减少对SD卡的读写次数,提高整个程序的执行效率,同时延长SD卡的使用寿命。
样例程序中执行的内容是读出SD卡中的所有文件路径。样例工程的组织结构如图10所示。
图10 sdcard_spi_pfatfs样例工程文件组织结构图
根据惯例,这里也贴出来样例工程在串口终端输出的log。编译下载工程之后,开始运行,串口终端输出如图11所示。
图11
至此,大功告成,搞定收工!
【总结】
这次在YL-KL26Z板子上折腾了一下SD卡,有了两个阶段性成果:
1. 移植了sdcard_spi实现对SD卡的读写操作。 2. 移植了pfatfs在SD上建立起来的文件系统。
具体地,在LiteFwLib完成了两个样例工程,“05_spi_sdcard”和“05_spi_sdcard_pfatfs”,图12展示了sdcard_spi和pfatfs应用程序的层次结构。
| | | 图12-b sdcard_spi_pfatfs应用结构 | 图12 sdcard_spi vs sdcard_spi_pfatfs 应用结构 |
有了这个知识积累,就可以玩转SD卡了,为后面在YL-KL26上写一个音频播放器的程序奠定了基础。
- End
附件: |