MSP430F5438A单片机基于SPI的FatFs移植笔记
2018-09-19 来源:eefocus
不管移植什么程序,最重要的就是,
不要自以为是
一定要先查资料,花一周查资料,查到查不到为止,否则你编了一半的程序再参考别人的,直接后果是你下不了决心推翻重来
1. FatFs移植要点:
相信能看到这个博客的都知道FatFs是什么了,目前应该是0.11版本,我就不多废话了,一个开源的文件系统,不全面的说,作用就是让你编程序操作写SD卡的内容能够被PC机读出来(有不对的话懂的大神请指正)
它的好处就是只要写底层的几个硬件驱动函数就OK了,上层的函数都已经写好了,清楚格式直接调用就可以了。
所谓“硬件驱动”函数,就是告诉单片机,完成一个动作(比如初始化)具体需要哪个IO口怎样变化,哪个IO口该高,哪个IO口该低,通信端口选哪个,发什么东西,这些基本动作组合在一起,就能够完成初始化。
FatFs所需要的驱动函数共有五个,diskio.h 文件里面看就是这样的:
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
函数返回值:
DSTATUS
是一个枚举类型变量,包含5个元素,依次是成功、读写错误、写保护、未准备好、参数错误:
/* Results of Disk Functions */
typedef enum {
RES_OK = 0, /* 0: Successful */
RES_ERROR, /* 1: R/W Error */
RES_WRPRT, /* 2: Write Protected */
RES_NOTRDY, /* 3: Not Ready */
RES_PARERR /* 4: Invalid Parameter */
} DRESULT;
实际在写程序的时候返回值分得细有助于快速定位问题的所在,但是我移植的时候图省事只用了成功、读写错误、参数错误三个,原因是写保护我没有编写专门的函数去判断,而未准备好出现的概率很低。
各个函数的输入参数具体到每个函数再进行一一说明
那么下面首先以初始化程序
DSTATUS disk_initialize (BYTE pdrv);
具体说一下:通过TI的单片机MSP430F5438A进行函数的实现步骤
这里需要参考的良心文档以及网站有:
1. FatFs官方网站:http://www.elm-chan.org/fsw/ff/00index_e.html
说明很浅显,优点是易懂缺点是靠他说的那点儿说明实现简直不可能
2. 一个叫Tilen Majerle的老外的网站,基于STM32系列单片机开发的FatFs:http://stm32f4-discovery.com/2014/07/library-21-read-sd-card-fatfs-stm32f4xx-devices/
注意这个哥们把全套的实现文件分成了好几个下载链接,分类的效果挺好,一开始看的人就晕了,这个文件下载下来,代码里面的函数在另外一个下载链接里……不过是真的很全,虽然可能跟你需要实现的平台不大一样,但是对于了解实现过程和时序很有帮助
3. 一个密歇根大学的人写的技术报告,MSP430F149写的程序,但是只实现了单次读写,并且使用了DMA(内存直读)这种高端的功能(我会上载到资料,待补全)
4. SD卡的官方协议说明书,这个基本上太重要了,名字是part1_410,看来只是整个的一部分,不过很详细,说得很明白,只要你耐心细心,很多东西都可以直接找到,如果你关心的是SPI模式的话第7章是重点!
2. disk_initialize函数实现
这个函数在diskio.c当中的函数体是这样的:
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case ATA :
result = ATA_disk_initialize();
// translate the reslut code here
return stat;
case MMC :
result = MMC_disk_initialize();
// translate the reslut code here
return stat;
case USB :
result = USB_disk_initialize();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
可以看到FatFs很“贴心”的提供了ATA、MMC、USB三款接口存储器的初始化程序,可是你要是真的认为它贴心你就错了……
首先来说输入参数pdrv,这个具体可以理解为你要操作的存储器编号,我们操作单片机的通常也就一个存储器(我项目里面是SD卡),所以在ffconf.h的定义中:
#define _VOLUMES 1
这样一来,所有的输入参数pdrv就都是0了,而ATA MMC USB的定义是这样的:
/* Definitions of physical drive number for each drive */
#define ATA 0 /* Example: Map ATA harddisk to physical drive 0 */
#define MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
#define USB 2 /* Example: Map USB MSD to physical drive 2 */
你妹啊亏我一开始还天真的认为我的是SD卡,不过脑子就只实现了
MMC_disk_initialize();
哪知道单个存储器条件下人家根本执行不到这儿啊!
所以,如果你们只有一个盘,一定要实现的是ATA那个,管他名字是什么,这里我最后实现的时候折衷了一下,把
MMC_disk_initialize();
拷贝到ATA下面了,也算是偷了个懒。那么
MMC_disk_initialize();
这个函数怎么实现呢?
下面的思路是一步一步深入到最底层,所以可能你一眼看不完这个函数的实现过程,不要着急
我们主要通过这个函数的实现过程一步一步搞清楚所有的运作机制
就用到了我上面提到的几个良心文件了
流程其实很多人都提到了也很清楚,这里先上一张经典流程图,来自part1_410.pdf(171/202)(以下我就简称这个文档是410文档了)
可见,第一步是发送命令CMD0,关于各个命令的说明410文档中同样有叙述(179/202),注意SPI用户要看第七章的表格
一个命令的组成分以下部分:
一个命令一共是6个byte
第一个byte是命令头
最高位是起始位,0,第二位是传输位,1,后面6位就是command index了,上面那个流程图里面是CMD几,这里换算就可以了,例如我们首先发送CMD0,那么这头一个字节就是
0x40 ( 0100 0000b )
需要说明,SD卡所支持的SPI通信的发送方式是3pin 8bit MSB First,也就是高位先发送,一次8位,所以CMD0最先发的就是0x40
第二个到第五个共4 bytes 是 命令参数 argument
这个对应的命令都能够在410当中找到argument的含义,CMD0的argument标注的是stuff bits填充位,一律写0就OK
第六个byte是7位CRC校验位和最后一个始终是1的end bit
这个CRC校验位的计算过程我没有理会,因为SPI模式下,除了CMD0和后面提到的CMD8,其它的校验位SD卡不会去管对错的
这里对于CMD0,加入你的argument全0,那么这个最后一个byte固定为
0x95,固定即可不用管它
说完了命令组成,那么具体来说一个命令是如何发送的呢?下面用MSP430F5438A举例子来说
这个单片机的SPI接口有好几个,我用的是P3.1,P3.2,P3.3组成的3pin的SPI,具体配置方法应该很容易查到,TI的user's guide当中也会有的
主要来说说SPI的通信机制,其实和我们常见的RS232有类似也有不同
SPI的3个pin分别是 输入,输出,时钟,此外,SD卡还有一个片选端口CS需要连接一个GPIO,只有这个片选端口为低电平的时候,SD卡才有反应(当然各个阶段也有需要在CS为高电平的时候需要干活的情况)
通信中,Master(单片机)提供时钟信号,Slave(SD卡)接收时钟信号,双方都是根据时钟的沿来分别采样输入或者输出线上根据所发送数据而变化的电压,从而实现数据的传输
与串口类似的是,如果你的单片机需要向SD卡发送数据,在合适的时间(后面会说什么时间合适)向发送缓冲区写需要发送的数据就OK
与串口不同的是,如果你的单片机需要接收SD卡发送的数据,接收1个byte的数据,单片机需要写入一个0xFF的数据将需要接收的数据“推过来”,知道了这个,剩下的就好办了
对于一个SPI通信端口,5438A配有:
一个发送中断标志位:UCTXIFG
一个接收中断标志位:UCRXIFG
一个发送缓冲器:UCxTXBUF
一个接收缓冲器:UCxRXBUF
中断标志位虽然可以手动清零,但是我个人不推荐。因为TI有自动的机制,我们查询就可以了,
发送数据:
不断查询确认发送中断标志位UCTXIFG,直到发送中断标志位为1,表示发送缓冲器可以写入数据,这时将需要发送的1byte数据写入发送缓冲器即可,写入后发送中断标志位自动归0,发送完成后重新变为1
接收数据:
不断查询接收中断标志位UCRXIFG,为1表明接收缓冲器中的数据有效,1byte可进行读取,读取后,接收中断标志位自动归零,直到再次接收到完整数据
有了这个流程,那么单个byte的发送和接收也不是问题了,我们可以顺利的发送命令CMD0了? NO,还有一个时序的问题
这里参考了另一个良心文献,振南的SD说明书
久违的中文啊哈哈,时序是这样的
首先在CS为高电平的情况下,输入8个唤醒时钟(可能落下了周期两个字,应该是8个时钟周期)(我理解是保险起见吧?)输入唤醒时钟周期的方法是,8个唤醒时钟周期就向写入缓冲器写入一个0xFF就可以了,因为SI是高的所以写FF,写入的时候时钟就运行了
然后拉低CS电平
然后读入nx8个时钟,同样是读入字节,方法是写入0xFF然后查输入寄存器,然后读数,注意如果SD卡就绪那么读入的数应该是0xFF
确定就绪后,写入6个byte的命令,之后保持CS为低电平,直到读入的返回值非0xFF表明命令处理完毕,SD卡开始发送回复数据,根据命令的恢复类型不同,读取不同byte数目的回复,读取完成之后将CS信号设为高电平,一次命令发送过程结束!
回复类型分为 R1,R1b,R2,R3,R7等等等等,同样可以在410当中找到
CMD的命令为回复为R1,长度1个byte,包含SD卡的状态,初始化时的正常回复为Idle:0x01,表明SD卡空闲!
发送CMD的完整代码如下:
// 发送CMD命令
char SD_Send_Command(unsigned char cmd,
unsigned char response_type,
unsigned char *response,
unsigned char *argument)
{
int i;
char response_length;
unsigned char tmp;
// 根据振南的建议,添加一个唤醒的过程:
SPI_CS_HIGH();
SPI_SendByte(0xFF);
//--------------------------------------------
while(0xFF != SPI_RcveByte());
// 发送命令前将CS置低
SPI_CS_LOW();
// 发送CMD头
tmp = 0x40 + cmd;
SPI_SendByte(tmp);
// 发送Argument
for (i=3; i>=0; i--) //注意!这里是MSB First,但是不太明白这么写到底有Argument的是如何排列的,参见 Application Note P15
{
SPI_SendByte(argument[i]);
}
// 发送CRC
SPI_SendByte(0x95); //说白了只有CMD0需要,其它之后的都不用了,所以这里索性把所有的CRC都写成CMD0的
// 确定回复种类
response_length = 0;
switch (response_type)
{
case R1:
case R1B:
response_length = 1;
break;
case R2:
response_length = 2;
break;
case R3:
response_length = 5;
break;
default:
break;
}
// 等待回复-有效回复的第一位是0,所以要设置一个有退出机制的循环来等待这个0开头的回复byte
i=0;
do
{
tmp = SPI_RcveByte();
i++;
}
while( ((tmp&0x80)!=0) && (i // 如果失败只能返回0退出 if ( i >= SD_MAX_CMD_RETRY ) { // DEBUG4 /* if(cmd == 13) { sd_RS232TX_PROC('ACMD13 FAIL!'); sd_RS232TX_PROC(sd_NewRow); } */ SPI_CS_HIGH(); return 0; } // 如果成功 i = response_length - 1; response[i] = tmp; i--; while (i>=0) { tmp = SPI_RcveByte(); response[i] = tmp; i--; } // 下面的内容涉及到:如果返回值是 R1B 即 BusyType,那么有一些额外的任务要做: // SD 卡会输出一连串的 “0”,所以,任何非零回复是 BUSY 状态结束的标志 i=0; if (response_type == R1B) { do { i++; tmp = SPI_RcveByte(); } while (tmp != 0xFF); SPI_SendByte(0xFF);// 最后这句不知道什么意思,先这样 } // 能到这里说明已经顺利完成命令执行并得到相应结果,返回退出 SPI_CS_HIGH(); return 1; } 以上程序参考了密歇根大学的老兄,几点说明: argument 和 response分别是函数所在的C文件中定义的全局static char类型数组 元素个数分别为4 和 5,因为R3和R7类型会返回5个byte的response 发送CRC时,没有考虑CMD8的情况,所以针对CMD8需要单独编写一个函数,或者在这里添加一个判断,都是没有问题的(CMD8干什么的后面再说) 关于R1 R1b 等等的定义其实都是一些便于区别的整数,按顺序从0开始定义就是了 关于输入变量cmd,可以在h文件中定义 CMD0 为 0, 其它同理,例如 CMD24 为 24 等等,即便是ACMD也按照排号定义就可以了,只不过ACMD命令之前要首先发送前缀命令CMD55 此外,函数中有一些地方比较匪夷所思,我的函数和密歇根那个哥们的也有一点区别,我这个是实际调试通过的,不知道和我自己用的开发板有关系没有,有兴趣的朋友可以重新看一下他的原版程序 以上就是发送命令的完整函数,以后发送各个命令就直接说“发送CMDxxx”了,不再详述过程(CMD8 和 ACMD怎么发会另外在后面说细一些) 今天先到这里