本帖最后由 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  
 
附件:  |