Lm3s8962评估套件上有采用SPI方式的SD卡接口,我的SD卡是256M的,所以接下来就开始SD卡的读写程序设计。
SPI接口的SD卡
1、SPI接口
SPI是一种全双工、同步串行通信方式接口,这里用到了四个IO口:分别是时钟线SCK、输出口MOSI、输入口MISO、模式从机选择线NSS。
2、SPI接口与SD卡
STM32与采用SPI接口的SD卡,就像两个CPU通信一样,STM32处理器通过SPI接口发出命令,SD卡执行命令后返回相应的状态。命令有读写命令、也有参数设置命令。
3、SD卡的内部结构:几个重要寄存器
(1)OCR寄存器:保存着卡的供电允许范围,位31表示卡上电后的状态,1表示空闲。
(2)CSD寄存器:总共128位,表示了卡的大部分配置信息。
(3)状态寄存器:命令响应的状态。
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(); //调用时钟计时器
}
系统时钟中断服务子程序,用于处理系统时钟中断。
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);
}
//如果出现问题,检查并返回错误。
UARTprintf(", %10uK bytes free\n", ulTotalSize * pFatFs->sects_clust / 2);
//显示可用空间的量。
return(0); //到这里,没有错误,返回0。
}
这个函数用于执行“ls”命令,它打开了当前目录,并将内容打印出来,为每个找到的条目打印一行。显示一些详细信息,如文件属性,时间和日期,并且文件大小,以及这个名字。显示文件大小及胜于空间的摘要。下面是执行ls命令的试验:
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);
//返回成功
}
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);
//返回成功。
}
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));
}
}
}
之后要做的事情是对文件系统移植的步骤进行总结~
回复 沙发 xielijuan 的帖子
楼主,这个例程中有读SD卡的文件,却没有写文件到SD卡,要怎么写呢?
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个字节呢
回复 楼主 xielijuan 的帖子
很好的东西,学习了
回复 沙发 xielijuan 的帖子
真是好帖子,下工夫了哦