[原创] SD卡操作深入学习~

xielijuan   2010-11-26 09:56 楼主

Lm3s8962评估套件上有采用SPI方式的SD卡接口,我的SD卡是256M的,所以接下来就开始SD卡的读写程序设计。

 

SPI接口的SD

1SPI接口

   SPI是一种全双工、同步串行通信方式接口,这里用到了四个IO口:分别是时钟线SCK、输出口MOSI、输入口MISO、模式从机选择线NSS

2SPI接口与SD

STM32与采用SPI接口的SD卡,就像两个CPU通信一样,STM32处理器通过SPI接口发出命令,SD卡执行命令后返回相应的状态。命令有读写命令、也有参数设置命令。

3SD卡的内部结构:几个重要寄存器

1OCR寄存器:保存着卡的供电允许范围,位31表示卡上电后的状态,1表示空闲。

2CSD寄存器:总共128位,表示了卡的大部分配置信息。

3)状态寄存器:命令响应的状态。

 

回复评论 (15)

4、常用的SPI模式命令
(1)命令由六个字节组成:01-六位命令号-四个字节的命令参数-7位校验码-结束位1。
(2)命令分为10个类:
SPI支持:类0基本控制的(0复位、1激活初始化、9读CSD寄存器、10读CID寄存器、12多块过程中停止传输、13读状态寄存器),
类2块读的(16设置块长度、17读一个数据块、18读多个数据块,直到发命令12);
类4块写的(24写块、25写多个块、27写CSD的可编程为);
类5擦除的(32设置擦除块的起始地址、33设置终止块地址、38擦除先前选择的所有块);
块6写保护的(可选28设置写保护、29清除写保护、30读写保护状态);
类7的锁卡命令(可选42上锁或者解锁);
类8的指定应用(55通知SD卡下个是特殊应用命令、56获取或写入一个数据块)。

5、SPI命令的响应状态
(1)有R1(1个字节)、R1B(1个字节)、R2(两个字节)、R3(五个字节),大部分命令的响应是R1。读取状态寄存器响应R2、读取OCR响应R3、R1B跟R1接近(有些命令只需要0或非0反馈,就用R1B,比如停止传输、擦除等命令)
(2)每一个命令都有对应的响应长度,故发送命令函数要经过特殊的处理。




下面对例程的代码进行了详细的解读
#include
#include "inc/hw_memmap.h"
#include "inc/hw_types.h"
#include "driverlib/gpio.h"
#include "driverlib/interrupt.h"
#include "driverlib/sysctl.h"
#include "driverlib/systick.h"
#include "utils/cmdline.h"
#include "utils/uartstdio.h"
#include "fatfs/src/ff.h"
#include "fatfs/src/diskio.h"
本例程使用的是第一个UART串口,之前已经深入的学习过UART的操作。

#define PATH_BUF_SIZE   80
定义用于存放路径缓冲区大小,或SD卡的临时数据的大小。有两个缓冲区分配的是这个大小。缓冲区大小必须大到足以容纳最长预期的完整路径名,包括文件名和一个结尾的空字符。

#define CMD_BUF_SIZE    64
定义用于保存命令行的缓冲区大小为64

static char g_cCwdBuf[PATH_BUF_SIZE] = "/";
这个缓冲区用于保存当前工作目录的完整路径
初始化为根目录“/”

static char g_cTmpBuf[PATH_BUF_SIZE];
操作文件路径或阅读SD卡的数据时使用的一个临时的数据缓冲区。

static char g_cCmdBuf[CMD_BUF_SIZE];
用于保存命令行的缓冲区

static FATFS g_sFatFs;
static DIR g_sDirObject;
static FILINFO g_sFileInfo;
static FIL g_sFileObject;
以上是FatFs使用的数据结构

typedef struct
{
    FRESULT fresult;
    char *pcResultStr;
}
tFresultString;
一个结构体,用于保存FRESULT数值和一个字符串represenation之间的代码映射。 FRESULT是从FatFs FAT文件系统驱动程序返回的代码。
#define FRESULT_ENTRY(f)        { (f), (#f) }
定义一个宏用于简单的添加结果代码到表中

tFresultString g_sFresultStrings[] =
{
    FRESULT_ENTRY(FR_OK),  
    FRESULT_ENTRY(FR_NOT_READY),   //为准备好
    FRESULT_ENTRY(FR_NO_FILE),    //文件不存在
    FRESULT_ENTRY(FR_NO_PATH),  //路径不存在
    FRESULT_ENTRY(FR_INVALID_NAME),  // 文件名不合法
    FRESULT_ENTRY(FR_INVALID_DRIVE),  //驱动无效
    省略一部分,看名字就知道是什么意思。
};
一个表用于保存一个字符串来表示数值FRESULT代码和它的名字之间的映射。用于查找打印到控制台的错误代码。

#define NUM_FRESULT_CODES (sizeof(g_sFresultStrings) / sizeof(tFresultString))
定义一个宏用于保存结果代码的数目。

const char *
StringFromFresult(FRESULT fresult)
{
    unsigned int uIdx;


    for(uIdx = 0; uIdx < NUM_FRESULT_CODES; uIdx++)
    {
        //循环搜索错误代码表来匹配错误代码。
      
        if(g_sFresultStrings[uIdx].fresult == fresult)
        {  //如果匹配到了,就返回一个相应的错误名称。
            return(g_sFresultStrings[uIdx].pcResultStr);
        }
    }

    //到这里如果还没有匹配到错误代码,则显示“未知错误”。
    return("UNKNOWN ERROR CODE");
}
这个函数返回一个从函数调用FatFs返回的用字符串表示的一个错误代码。它可用于打印出来人类可读的错误信息。

void
SysTickHandler(void)
{
    disk_timerproc();  //调用时钟计时器
}
系统时钟中断服务子程序,用于处理系统时钟中断。
点赞  2010-11-26 09:57
Ls命令
int
Cmd_ls(int argc, char *argv[])
{
    unsigned long ulTotalSize;
    unsigned long ulFileCount;
    unsigned long ulDirCount;
    FRESULT fresult;
    FATFS *pFatFs;
    fresult = f_opendir(&g_sDirObject, g_cCwdBuf);
    //打开访问当前目录,初始化为根目录。
    if(fresult != FR_OK)
    {
        return(fresult);
    }
//如果出现了问题,检查错误并返回。

    ulTotalSize = 0;
    ulFileCount = 0;
    ulDirCount = 0;
    UARTprintf("\n");
    //List前显示一个额外的空行。

    for(;;)
    {
        //循环列举目录里的所有条目。
        fresult = f_readdir(&g_sDirObject, &g_sFileInfo);  //从目录中读取一个条目。

        if(fresult != FR_OK)
        {
            return(fresult);  //如果出现问题,检查并返回错误
        }

        if(!g_sFileInfo.fname[0])
        {   //如果文件名空白,则说明是最后一个条目。
            break;
        }

        if(g_sFileInfo.fattrib & AM_DIR)
        {   //如果属性石目录,则增加一个目录数目。
            ulDirCount++;
        }

        else
        {  //否则,就是一个文件,文件数增加,并且把文件大小添加到总大小中。
            ulFileCount++;
            ulTotalSize += g_sFileInfo.fsize;
        }

     
        UARTprintf("%c%c%c%c%c %u/%02u/%02u %02u:%02u %9u  %s\n",
                    (g_sFileInfo.fattrib & AM_DIR) ? 'D' : '-',
                    (g_sFileInfo.fattrib & AM_RDO) ? 'R' : '-',
                    (g_sFileInfo.fattrib & AM_HID) ? 'H' : '-',
                    (g_sFileInfo.fattrib & AM_SYS) ? 'S' : '-',
                    (g_sFileInfo.fattrib & AM_ARC) ? 'A' : '-',
                    (g_sFileInfo.fdate >> 9) + 1980,
                    (g_sFileInfo.fdate >> 5) & 15,
                     g_sFileInfo.fdate & 31,
                    (g_sFileInfo.ftime >> 11),
                    (g_sFileInfo.ftime >> 5) & 63,
                     g_sFileInfo.fsize,
                     g_sFileInfo.fname);
    }   // 单行格式打印条目属性,如日期,时间,大小,名称。


    UARTprintf("\n%4u File(s),%10u bytes total\n%4u Dir(s)",
                ulFileCount, ulTotalSize, ulDirCount);
//打印会总行,显示文件,目录,总大小。

    fresult = f_getfree("/", &ulTotalSize, &pFatFs);
//获取空闲空间。

    if(fresult != FR_OK)
    {
        return(fresult);
    }
//如果出现问题,检查并返回错误。
点赞  2010-11-26 10:04

UARTprintf(", %10uK bytes free\n", ulTotalSize * pFatFs->sects_clust / 2);

//显示可用空间的量。

 

    return(0);   //到这里,没有错误,返回0

}

 

这个函数用于执行“ls”命令,它打开了当前目录,并将内容打印出来,为每个找到的条目打印一行。显示一些详细信息,如文件属性,时间和日期,并且文件大小,以及这个名字。显示文件大小及胜于空间的摘要。下面是执行ls命令的试验:

11.jpg

点赞  2010-11-26 10:04
cd命令
(1)这个函数实现了“cd”命令。它需要一个参数来明确知名当前工作的目录。路径分隔符必须使用正斜杠“/”。这个CD的变量可以是以下几个:
根(“/“)
一个完全的指定路径 ("/my/path/to/mydir")
当前目录的一个单独目录名  ("mydir")
父目录   ("..")
(2)这里不支持相对路径,类似于:("../my/new/path")
(3)一旦指定新的目录,就会尝试打开它以确保它存在。
如果成功打开新的路径,则当前工作目录会转变到新打开的目录下面。


int
Cmd_cd(int argc, char *argv[])
{
    unsigned int uIdx;
    FRESULT fresult;

    strcpy(g_cTmpBuf, g_cCwdBuf);
    //复制当前工作路径到一个临时缓冲区,以便操作。

    if(argv[1][0] == '/')
    {
        //如果第一个字符是“/“,则是一个完全指定路径,可直接使用。
        if(strlen(argv[1]) + 1 > sizeof(g_cCwdBuf))
        {   
//确保新的路径不会比缓冲区大。
            UARTprintf("Resulting path name is too long\n"); //如果比缓冲区大,则打印“生成的路径名太长”
            return(0);
        }

        else
        {  //如果不是太长的话,就把它拷贝到临时缓冲区,以便于检查。
            strncpy(g_cTmpBuf, argv[1], sizeof(g_cTmpBuf));
        }
    }

    else if(!strcmp(argv[1], ".."))
    {
        //如果变量是“..”,则尝试移动到CWD缓冲区的最底端。
        uIdx = strlen(g_cTmpBuf) - 1;

        //获取当前路径的最后一个字符。
        //从路径的最末位备份,直到遇见分隔符”/”,或路径的起始。

        while((g_cTmpBuf[uIdx] != '/') && (uIdx > 1))
        {
            uIdx--;  //备份一个字符
        }

        g_cTmpBuf[uIdx] = 0;
//这里正处于最底层的分隔符,无论是在当前路径,或在字符串(root)的启示。因此,设置在这里新的结束串,有效的移除该路径的最后一部分。
    }

    else
    {
        //否则就是当前目录的正常路径名,这需要追加到当前目录。

        if(strlen(g_cTmpBuf) + strlen(argv[1]) + 1 + 1 > sizeof(g_cCwdBuf))
        {
            UARTprintf("Resulting path name is too long\n");
            return(0);
        }
        //测试以确保当新的额外的路径被补充到当前路径,缓冲区有空间给新的完全的路径。包括一个新的分隔符,和一个结尾的空字符。

        //新的路径可以了,就添加分隔符,并追加新的目录给路径
        else
        {
           
            if(strcmp(g_cTmpBuf, "/"))
            {    //如果不是在根目录,就加一个分隔符”/”
                strcat(g_cTmpBuf, "/");
            }
      
            strcat(g_cTmpBuf, argv[1]);
               //添加新的目录到路径中
        }
    }

    //到这里,新的目录被保存在chTmpBuf.缓冲区,尝试打开它,以确保它的存在。
    fresult = f_opendir(&g_sDirObject, g_cTmpBuf);

    if(fresult != FR_OK)
{   
//  如果打不开,则说明是一个坏的路径,通知用户并返回。
        UARTprintf("cd: %s\n", g_cTmpBuf);
        return(fresult);
    }

    else
    {    //否则,听就是一个新的路径,所以复制它到CWD缓冲区
        strncpy(g_cCwdBuf, g_cTmpBuf, sizeof(g_cCwdBuf));
    }

return(0);
//返回成功
}
点赞  2010-11-26 10:05
Pwd命令
int
Cmd_pwd(int argc, char *argv[])
{
    UARTprintf("%s\n", g_cCwdBuf);
//打印当前工作目。

return(0);
//返回成功
}
//这个函数实现了“打印工作目录”命令。它简单地打印当前工作目录。

cat命令
//下面这个函数实现了“cat”的命令。它读取一个文件的内容,并打印到控制台。注意了,这只用于文本文件。如果是在一个二进制文件中使用,就有可能是一堆乱码在控制台上打印出来。
int
Cmd_cat(int argc, char *argv[])
{
    FRESULT fresult;
    unsigned short usBytesRead;

    //首先,检查并确定当前工作路径,再加上文件名,加上分隔符以及末尾的空字符,使其能保存在临时缓冲区,用于保存文件的名称。文件名必须是完全指定,有路径。
    if(strlen(g_cCwdBuf) + strlen(argv[1]) + 1 + 1 > sizeof(g_cTmpBuf))
    {
        UARTprintf("Resulting path name is too long\n");
        return(0);
    }

//复制当前工作路径给临时缓冲区,利于操作。
    strcpy(g_cTmpBuf, g_cCwdBuf);
    if(strcmp("/", g_cCwdBuf))
{
   //如果不是根目录,加上分隔符
        strcat(g_cTmpBuf, "/");
    }

    //最后,加上文件名,形成一个一个完全指定的文件。
    strcat(g_cTmpBuf, argv[1]);

    fresult = f_open(&g_sFileObject, g_cTmpBuf, FA_READ);
    //打开要读的文件

    if(fresult != FR_OK)
    {   //如果打开文件出现问题,则返回一个错误
        return(fresult);
    }

    do
    {
        //进入一个循环来重复的从文件里读取数据并显示出来,知道最后一个字符。
        // Read a block of data from the file.  Read as much as can fit
        // in the temporary buffer, including a space for the trailing null.
        //
        fresult = f_read(&g_sFileObject, g_cTmpBuf, sizeof(g_cTmpBuf) - 1,
                         &usBytesRead);
        //从文件中读一个区块的数据,读尽可能多的数据,知道临时缓冲区装不下了,注意,要包含一个末尾空字符的空间
   
        if(fresult != FR_OK)
        {   
//如果出现了一个读取错误,则打印新的一行并返回错误信息给用户
            UARTprintf("\n");
            return(fresult);
        }

        g_cTmpBuf[usBytesRead] = 0;
        //空字符作为最后一个块被读取的结束标志。

        UARTprintf("%s", g_cTmpBuf);
      //打印接受的最后一个块。
      //继续读取直到读满了为止,也就是说缓冲区的最后一个字符被读取。
    }
    while(usBytesRead == sizeof(g_cTmpBuf) - 1);
   
return(0);
//返回成功。
}
点赞  2010-11-26 10:05
help命令
//这个函数实现了“帮助”命令。打印简要介绍可用的命令列表。
int
Cmd_help(int argc, char *argv[])
{
    tCmdLineEntry *pEntry;

    UARTprintf("\nAvailable commands\n");
    UARTprintf("------------------\n");
//打印一些标题文件

    pEntry = &g_sCmdTable[0];
    //指向命令表的起始处。
    // Enter a loop to read each entry from the command table.  The
    // end of the table has been reached when the command name is NULL.
    //
    while(pEntry->pcCmd)
    {
        //进入一个循环来读取命令表的每一项,当命令名字为“NULL”时,表示读取结束。

        UARTprintf("%s%s\n", pEntry->pcCmd, pEntry->pcHelp);
        //打印命令名和详细的描述。
        pEntry++;
//自加,进入到表的下一项
    }
return(0);
//返回成功
}

tCmdLineEntry g_sCmdTable[] =
{
    { "help",   Cmd_help,      " : Display list of commands" },
    { "h",      Cmd_help,   "    : alias for help" },
    { "?",      Cmd_help,   "    : alias for help" },
    { "ls",     Cmd_ls,      "   : Display list of files" },
    { "chdir",  Cmd_cd,         ": Change directory" },
    { "cd",     Cmd_cd,      "   : alias for chdir" },
    { "pwd",    Cmd_pwd,      "  : Show current working directory" },
    { "cat",    Cmd_cat,      "  : Show contents of a text file" },
    { 0, 0, 0 }
};
//这是一张表,用来保存命令行的名字,以及实现的功能和详细介绍。

好,下面就是main函数了。
程序先贴出来:
int
main(void)
{
    int nStatus;
    FRESULT fresult;

    SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_OSC |
                   SYSCTL_XTAL_8MHZ | SYSCTL_OSC_MAIN);

    //设置系统时钟工作在主振荡器的8 MHZ下
    SysCtlPeripheralEnable(SYSCTL_PERIPH_UART0);
    SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0);
    SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
   //打开外设

    SysTickPeriodSet(SysCtlClockGet() / 100);
    SysTickEnable();
    SysTickIntEnable();
//配置100Hz的时钟中断,FatFs需要一个10ms的计时器。

    IntMasterEnable();
     //打开中断

    GPIOPinTypeUART(GPIO_PORTA_BASE, GPIO_PIN_0 | GPIO_PIN_1);
     //设置GPIO的A端口的0,1号引脚为UART。

    UARTStdioInit(0);
//初始化UART为文本控制台的输入输出。

    UARTprintf("\n\nSD Card Example Program\n");
    UARTprintf("Type \'help\' for help.\n");
//打印一个交互界面给用户。

    fresult = f_mount(0, &g_sFatFs);
    if(fresult != FR_OK)
    {    //挂载文件系统,使用逻辑磁盘0
        UARTprintf("f_mount error: %s\n", StringFromFresult(fresult));
        return(1);
    }

    while(1)
    {
        //进入一个无限循环来读取用户的读取以及操作命令

        UARTprintf("\n%s> ", g_cCwdBuf);
        //打印一个提示到控制台,显示当前工作路径

        UARTgets(g_cCmdBuf, sizeof(g_cCmdBuf));
        //获取来自用户的文本行

        nStatus = CmdLineProcess(g_cCmdBuf);
        //从用户那传递命令行给处理器进行解析和执行
        if(nStatus == CMDLINE_BAD_CMD)
        {
            UARTprintf("Bad command!\n");
        }
//处理错误的命令

        else if(nStatus == CMDLINE_TOO_MANY_ARGS)
        {
            UARTprintf("Too many arguments for command processor!\n");
        }
//处理参数太多的问题。

        else if(nStatus != 0)
        {   
//如果命令没什么问题就被执行,如果命令有问题,就打印错误提示。
            UARTprintf("Command returned error code %s\n",
                        StringFromFresult((FRESULT)nStatus));
        }
    }
}

之后要做的事情是对文件系统移植的步骤进行总结~
点赞  2010-11-26 10:05
学习了。不错
点赞  2010-11-26 11:42
很详细啊,用到的时候好好看一看...
点赞  2010-11-26 20:09
很好很强大哈~
谢谢楼主分享了哈~
点赞  2010-11-27 13:00
很不错啊,谢谢分享啊。。。。
我的博客
点赞  2010-11-27 15:43

回复 沙发 xielijuan 的帖子

楼主,这个例程中有读SD卡的文件,却没有写文件到SD卡,要怎么写呢?
点赞  2010-12-2 13:33
sd卡的结构资料不知哪儿有?
点赞  2011-3-14 16:56
static
BYTE rcvr_spi (void)
{
    DWORD rcvdat;

    SSIDataPut(SDC_SSI_BASE, 0xFF); /* write dummy data */

    SSIDataGet(SDC_SSI_BASE, &rcvdat); /* read data frm rx fifo */

    return (BYTE)rcvdat;
}
为什么在读之前,要先写一次呢?
从 FIFO 中读取一个4个字节却返回一个字节,另外3个字节呢
点赞  2011-4-23 18:54

回复 楼主 xielijuan 的帖子

很好的东西,学习了
点赞  2012-4-1 13:10

回复 沙发 xielijuan 的帖子

真是好帖子,下工夫了哦
点赞  2012-4-8 11:44
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复