开发环境:Window 7 32bit
开发工具:Keil uVision4
硬件:stm32f103vct6
目录
前言:已经有段时间没有写博客了,可能是事有点多(是我懒...额),最近又想来写一些;这次做的是stm32和SD卡的应用。SD卡的使用都很普遍,但是在单片机上的应用却少;我们知道单片机的处理速度有限,在大文件、大数据面前,根本是发挥不了作用的。但是因为SD卡价格优惠性价比很高,而在某些场合需要常年工作的单片机,可用它来记录单片机收集的数据;同时也可以通过SD卡给单片机更新自身程序(iap升级)等。接下来要做的是,利用stm32通过spi外设,驱动SD卡;当然如果要从SD卡上读取、写入文件,还需要移植文件系统,我选的是Fatfs(一个免费开源的文件系统)。
下载fatfs系统源码:
官网地址:http://elm-chan.org/fsw/ff/00index_e.html,拉到下面点击 Previous Releases,选择0.11a版本点击下载。
下载解压后,有两个文件夹,doc文件夹是帮助文档,src里面是是源码。
doc里面很多资料,详细介绍了fatfs系统的架构和使用说明,一些接口函数不明白怎么使用的话可以在里面找到说明。下面介绍src文件:
option文件夹:可选的的扩展功能,比如支持中文。我这次没有用到它。
00history:版本记录。官网每发布一次版本都会记录更改或者添加了那些功能,里面还有日期,可以看到它进化的历程。
00readme:这个文件里面就是做着我现在做的事情,说明每个文件的作用。
diskio.c: 这个是接口层文件,与芯片外设相关,里面有些函数需要我们实现,需要我们修改。
diskio.h:头文件里面声明的函数是让ff.c文件调用的,不需要我们修改。
ff.c:fatfs模块源码,核心东西,需要一定的代码能力才能看懂,不需要我们修改。
ff.h:fatfs模块应用接口,不需要我们修改。
ffconf.h关键参数配置,配置一些宏的值, 不同的值满足不同的需求,需要我们修改。
integer.h数据类型定义,与编译器有关,一般不需要修改。
接下我们要做两个事情,修改diskio.c文件和ffconf.h文件。
先说一下ffconf.h的配置,我只是改了下面两个宏:
#define _VOLUMES 5 //支持的逻辑设备数
#define _FS_NORTC 0 //暂时不加入RTC,先关闭, 不然编译报错;因为打开的话要实现get_fattime()来获取RCT时间
关于其他的宏暂时不改动,每个宏所起的作用在源码里有详细的英文说明,可以了解一下。
再说一下diskio.c文件,里面共有5个函数分别是:
/*
功能:设备初始化函数
参数:pdrc是设备号,fatfs系统可以同时挂载多个设备(SD卡、MMC等)
*/
1. DSTATUS disk_initialize (BYTE pdrv);
/*获取设备状态*/
2. DSTATUS disk_status (BYTE pdrv);
/*
功能:从设备读取若干个扇区的数据
buff: 读取的数据存放的地址
sector:扇区地址
count:所读取的扇区总个数
*/
3. DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
/*
功能:往设备写入若干个扇区的数据
buff: 写入的数据地址
sector:扇区地址
count:所写的扇区总个数
*/
4. DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
/*
功能:设备控制,或获取设备的参数
pdrv:设备号
cmd:命令
buff:发送/接收缓冲区指针
*/
5. DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
面提到的扇区可能大家会有疑问,不同的设备,扇区大小不一样。SD卡每个扇区是512字节,型号W25Q128FV的spi Flash芯片每个扇区是4096字节。如果我用的是这个Flash芯片,那么在ffconf.c里面的_MAX_SS就要改大才能兼容。可见fatfs可兼容不同的扇区大小的设备。由上面参数可见读、写都是以扇区为单位,一个设备根据容量的不同,会分成若干个扇区。
再者,上面的每一个函数都有pdrv参数,为了兼容多个或者不同的设备,在上面每个函数里面会有一个switch分支,来区别具体要操作哪个设备,我只使用一个SD卡,所以只需要增加一个分支即可。
对于stm32来说,在提供的库函数就有SPI外设的使用接口函数,非常方便,但这仅仅是数据的收发;想要从SD卡中读取信息,读/写扇区数据,还需要了解SD卡的通讯协议(通讯协议有几个版本网上有公开资料,可自行选择了解)。在stm32的标准库跟fatfs系统之间还需要一个中间层。它的作用是根据通讯协议提供的命令参数,从SD卡里获取设备型号、容量、设备状态、读/写扇区等操作,这也是这次讲解的重点。
diskio.c文件具体的改动这里不细说,直接看我的源码,接下来就要开始动手了(我怕我再啰嗦的话可能就留不住人了)。
1、硬件设计
接线如下图:
左边的是串口小板,接到电脑看打印信息;中间的是stm32f103vct6;右上角红线、黑线分别是5V电源线、地线。下方的是16G、SD卡的SPI转接小板, 网上一搜可以买到,下面是它的原理图:
如果你买的小板跟我的一样,接到小板的电压一定要5v,在这个小板上MISO、MOSI、SCK引脚接了上拉电阻。可参照第一张图接好线,杜邦线不宜过长;另外,我用的是J-link下载器。
2、软件设计
编程要点:
- 配置一路usart串口,用来输出printf打印信息。
- 配置一个TIM计时器,提供系统滴答,用来满足超时的设计。
- 初始化SPI外设,配置合适的参数。
- 根据SD卡的通讯协议,初始化SD卡,并实现一些相关的读/写操作函数。
打开源码工程:
先说一下main.c文件,main函数比较简单,主要调用了ff.h里面的f_mount和f_open函数。
#include "stm32f10x.h"
#include "USART1.h"
#include "ff.h"
#include "diskio.h"
void GPIO_Configuration(void);
void Delay(uint32_t nCount);
static FATFS g_fileSystem; /* File system object */
const TCHAR driverNumberBuffer[3U] = {'3', ':', '/'};//这里的3对应diskio.c里面的pdrv设备号
int main(void)
{
FIL fd,outfd;
GPIO_Configuration(); //配置一个led闪烁
USART1_Configuration();//初始化串口,用来输出printf信息
//挂载一个设备到路径“3:/”,这个函数里面会调用disk_initialize进行初始化SD卡
if (f_mount(&g_fileSystem, driverNumberBuffer, 1))
{
printf("Mount volume failed.\r\n");
}
else
{
printf("Mount volume succeed.\r\n");
}
//打开事先在SD卡创建的readme.txt文件
if(f_open(&fd, "3:/readme.txt", FA_READ) )
{
printf("f_open failed.\r\n");
}
else
{
printf("f_open succeed.\r\n");
}
while (1)
{
GPIO_SetBits(GPIOB,GPIO_Pin_0);
Delay(0xfffff);
Delay(0xfffff);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
Delay(0xfffff);
Delay(0xfffff);
printf("app runing \n");
}
}
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB , ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
void Delay(uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
再说一下app_spiSD.c文件,这个是对照SD卡的通讯协议做出来的。这个文件大部分代码是我从NXP LPC54110芯片的例程复制过来的,其中改了一些地方。下面我主要说几个重要的点:
(1)、SPI收发数据
SPI初始化部分这里不细说,配置参数正确就行了。下面是SPI收发函数,SPI发送一个字节后,必定会接收一个字节,当SPI要接收SD卡响应数据时,可以发送0xFF(无效指令)来接收数据,因为SD卡是从机,时钟线由主机控制,发送0xFF是为了产生时钟,从机才能把数据传出;加入超时出错的机制,以防SD卡未插入时,程序阻塞在此处。下面是SPI收发函数spi_exchange:
/*
in: 发送数据的缓冲地址,不可能为NULL;即使只接收数据,也要发0xFF
out:接收数据的缓冲地址,如果是NULL,代表只发送数据,不用保存接收的数据
size:收发数据的大小
*/
status_t spi_exchange(uint8_t *in, uint8_t *out, uint32_t size)
{
uint32_t rxRemainingBytes,txRemainingBytes,tmp32;
uint32_t SPITimeout;
if(((in==NULL)&&(out==NULL))||size==0){
return kStatus_InvalidArgument;
}
GPIO_ResetBits(GPIOA,SDCard_SPI_CS_PIN);//片选拉低
rxRemainingBytes=out != NULL? size : 0;
txRemainingBytes=in != NULL? size : 0;
while(rxRemainingBytes || txRemainingBytes){
SPITimeout=timer_get_current_milliseconds();
while(SPI_I2S_GetFlagStatus(SDCard_SPI,SPI_I2S_FLAG_TXE) == RESET){
if((timer_get_current_milliseconds()-SPITimeout)>SPIT_FLAG_TIMEOUT)
return kStatus_Timeout;
}
if(txRemainingBytes){
SPI_I2S_SendData(SDCard_SPI,*in);
in++;
txRemainingBytes--;
}else{
PI_I2S_SendData(SDCard_SPI,Dummy_Byte);
}
SPITimeout=timer_get_current_milliseconds();
while(SPI_I2S_GetFlagStatus(SDCard_SPI,SPI_I2S_FLAG_RXNE) == RESET){
if((timer_get_current_milliseconds()-SPITimeout)>SPIT_FLAG_TIMEOUT)
return kStatus_Timeout;
}
tmp32 = SPI_I2S_ReceiveData(SDCard_SPI);
if(rxRemainingBytes){
*out = tmp32;
out++;
rxRemainingBytes--;
}
}
while(SPI_I2S_GetFlagStatus(SDCard_SPI,SPI_I2S_FLAG_TXE) == RESET);
GPIO_SetBits(GPIOA,SDCard_SPI_CS_PIN);//片选拉高,结束通讯
return kStatus_Success;
}
(2)、向SD卡发送的命令格式:
命令长度共48位,包括起始位,传输位,命令码,命令参数,校验位以及停止位。在协议里面CMD0就是0,CMD16就是16,其他以此类推。下面在协议里面截取一部分命令描述:
(3)、SD卡应答命令的响应
不同的命令,对应不同格式的应答;在协议里面规定,每一个发出去CMD命令都对应了一种应答格式;所以在发送命令后,我们就已经预先地知道了接下来将要接收怎么样的应答格式,并做好接收应答准备。
Format R1b:长度为1字节,如果R1b=0,代表SD卡处于忙碌状态;如果R1b不为0,那么按照R1格式解读就行。
Format R2 :长度为2个字节,是在R1格式下再加一个字节的信息,如图:
Format R3: 长度为5字节,R1(8bit)+OCR(32)寄存器的值。
Formats R4 & R5 :这两个响应格式是为I/O模式保留的。
Format R7:长度为5个字节,第一个字节是R1格式,后4个字节包含卡的工作电压信息和检查模式的回显。如下如:
以上两个知识点体现在本实验源码的SDSPI_SendCommand函数,如下:
static status_t SDSPI_SendCommand(sdspi_host_t *host, sdspi_command_t *command, uint32_t timeout)
{
uint8_t buffer[6];
uint8_t response;
uint8_t i;
uint8_t timingByte = 0xFFU; /* The byte need to be sent as read/write data block timing requirement */
if ((kStatus_Success != SDSPI_WaitReady(host, timeout)) && (command->index != kSDMMC_GoIdleState))
{
return kStatus_SDSPI_WaitReadyFailed;
}
/* Send command. */
buffer[0U] = (command->index | 0x40U);//起始位+命令码
buffer[1U] = ((command->argument >> 24U) & 0xFFU);
buffer[2U] = ((command->argument >> 16U) & 0xFFU);
buffer[3U] = ((command->argument >> 8U) & 0xFFU);
buffer[4U] = (command->argument & 0xFFU);
buffer[5U] = ((SDSPI_GenerateCRC7(buffer, 5U, 0U) << 1U) | 1U);//crc+停止位
if (host->exchange(buffer, NULL, sizeof(buffer)))
{
return kStatus_SDSPI_ExchangeFailed;
}
//等待应答,最多接收9个字节,若接收不到正确应答,当做错误处理
for (i = 0U; i < 9U; i++)
{
if (kStatus_Success != host->exchange(&timingByte, &response, 1U))
{
return kStatus_SDSPI_ExchangeFailed;
}
//当接收到的一个字节的最左边的位是0,那么就是正确的应答,退出循环。往下继续接收剩下的应答信息
if (!(response & 0x80U))
{
break;
}
}
if (response & 0x80U) //这个条件满足,意味着应答错误
{
return kStatus_SDSPI_ResponseError;
}
command->response[0U] = response;//将应答的第一个字节保存,接着接收其余字节或返回。
switch (command->responseType)//根据预先知道的应答类型接收应答
{
case kSDSPI_ResponseTypeR1:
break;
case kSDSPI_ResponseTypeR1b:
if (kStatus_Success != SDSPI_WaitReady(host, timeout))
{
return kStatus_SDSPI_WaitReadyFailed;
}
break;
case kSDSPI_ResponseTypeR2:
if (kStatus_Success != host->exchange(&timingByte, &(command->response[1U]), 1U))
{
return kStatus_SDSPI_ExchangeFailed;
}
break;
case kSDSPI_ResponseTypeR3:
case kSDSPI_ResponseTypeR7:
/* Left 4 bytes in response type R3 and R7(total 5 bytes in SPI mode) */
if (kStatus_Success != host->exchange(&timingByte, &(command->response[1U]), 4U))
{
return kStatus_SDSPI_ExchangeFailed;
}
break;
default:
return kStatus_Fail;
}
return kStatus_Success;
}
(4)、SD卡初始化流程
SD卡SPI模式的初始化流程在SD卡协议文档的106页,下面是中文的流程以及多了一些说明,同时可以对照着diskio.c文件里的SDSPI_Init函数来理解这个流程图:
本实验代码只支持对SD2.0版本的检测,我手上也只有一张卡;初始化函数里某一个环节出错都会立即返回,如果出现初始化不成功,可以设置断点,排查错误在哪里个环节发生。SDSPI_Init函数:
status_t SDSPI_Init(sdspi_card_t *card)
{
sdspi_host_t *host;
uint32_t applicationCommand41Argument = 0U;
uint32_t startTime;
uint32_t currentTime;
uint32_t elapsedTime;
uint8_t response[5U];
uint8_t applicationCommand41Response[5U];
bool likelySdV1 = false;
host = card->host;
/* Card must be initialized in 400KHZ. */
if (host->setFrequency(SDMMC_CLOCK_400KHZ))
{
return kStatus_SDSPI_SetFrequencyFailed;
}
/* Reset the card by CMD0. */
if (kStatus_Success != SDSPI_GoIdle(card))
{
return kStatus_SDSPI_GoIdleFailed;
}
/* Check the card's supported interface condition. */
if (kStatus_Success != SDSPI_SendInterfaceCondition(card, 0xAAU, response))
{
likelySdV1 = true;
}
else if ((response[3U] == 0x1U) || (response[4U] == 0xAAU))
{
applicationCommand41Argument |= kSD_OcrHostCapacitySupportFlag;
}
else
{
return kStatus_SDSPI_SendInterfaceConditionFailed;
}
/* Set card's interface condition according to host's capability and card's supported interface condition */
startTime = host->getCurrentMilliseconds();
do
{
if (kStatus_Success !=
SDSPI_ApplicationSendOperationCondition(card, applicationCommand41Argument, applicationCommand41Response))
{
return kStatus_SDSPI_SendOperationConditionFailed;
}
currentTime = host->getCurrentMilliseconds();
elapsedTime = (currentTime - startTime);
if (elapsedTime > 500U)
{
return kStatus_Timeout;
}
if (!applicationCommand41Response[0U])
{
break;
}
} while (applicationCommand41Response[0U] & kSDSPI_R1InIdleStateFlag);
if (!likelySdV1)
{
if (kStatus_Success != SDSPI_ReadOcr(card))
{
return kStatus_SDSPI_ReadOcrFailed;
}
if (card->ocr & kSD_OcrCardCapacitySupportFlag)
{
card->flags |= kSDSPI_SupportHighCapacityFlag;
}
}
/* Force to use 512-byte length block, no matter which version. */
if (kStatus_Success != SDSPI_SetBlockSize(card, 512U))
{
return kStatus_SDSPI_SetBlockSizeFailed;
}
if (kStatus_Success != SDSPI_SendCsd(card))
{
return kStatus_SDSPI_SendCsdFailed;
}
/* Set to max frequency according to the max frequency information in CSD register. */
SDSPI_SetMaxFrequencyNormalMode(card);
/* Save capacity, read only attribute and CID, SCR registers. */
SDSPI_CheckCapacity(card);
SDSPI_CheckReadOnly(card);
if (kStatus_Success != SDSPI_SendCid(card))
{
return kStatus_SDSPI_SendCidFailed;
}
if (kStatus_Success != SDSPI_SendScr(card))
{
return kStatus_SDSPI_SendCidFailed;
}
return kStatus_Success;
}
在读取SD卡的CSD寄存器后,可得到该SD卡所支持的SPI的最大波特率,然后根据其最大的波特率和本地SPI设备所支持的最大波特率两者选其中较小一个。下图的busBandRate是设置本地支持的最大波特率,若SD卡支持的波特率大于它,那么就选用它。虽然stm32的spi最大支持36M,但是这里我设置了18M,可能是距离太长不能用36M的。如果有条件可以自己试一下36M,当然你用的SD卡支持的波特率一定要大于它才能用。
3、下载验证
保证开发板相关硬件连接正确,用USB线连接开发板“USB转串口”接口及电脑,在电脑端打开串口助手,把编译好的程序下载到开发板。我用的是J-LINK下载器,不知道是不是我的电脑的原因,下载器输出的电源只有3V多,SD卡的转接板需要5V电源,所以我用另外一个5V的电源给开发板和SD卡的转接板供电。SD卡和开发板的电源要接到一起,共地。程序下载后可以点调试运行,也可以断电重启;观察串口助手打印的信息:
打印信息显示SD卡初始化正常,可读取“readme.txt”文件。
关于SD卡的其他信息我没有打印出来,在SD卡初始化的时候已经把所有信息读取保存在g_card变量,只需要根据SD卡的协议去解读这里面的值就行;当然如果有J-Link调试器,可以将g_card添加到Watch窗口观察。设置断点调试,如果没有调试工具可以不做:
由上图可看出我用的SD卡所支持的最大频率是0x03473BC0,即55MHz。这里只是举个例子,在初始化过程中,如果出现错误返回,可以通过设置断点来定位错误的位置,同时把关键的变量添加到Watch里观察,记得把View-Periodic Window Update打开。
4、注意事项
- 我第一次验证的时候也遇到了很多问题,一开始我怀疑是spi收发的设计问题,后来发现是SD卡供电的问题,导致SD卡初始化一直不成功,而且断点调试时出错返回的节点位置不定。所以一定要保证SD卡引脚的供电是2.0-3.6V,我买的转接板供电要5V,之前我给转接板供电3V多时,就一直初始化不成功。
- 开发板连接到SD卡转接板的杜邦线不能太长,因为SPI传输的距离短,属于板载通讯,不适合拉线;距离太远,会有线耗,导致通讯不稳定。
- 确保SD卡的文件系统是FAT12、FAT16、FAT32,否则无法识别。若格式不符合,可将重要数据备份后把SD卡格式化成FAT32格式。在电脑上查看SD卡的文件系统格式:
5、实验可改进的地方
- 在ff.h里面有很多文件的操作函数,我这里只调用了f_mount和f_open,main函数可以进一步开发,加入f_write和f_read等应用接口,对SD卡进行文件读写数据的操作,测试数据的传输效率。
- 实验所用的spi波特率最大是18M,改stm32芯片支持36M,但由于我接线过长的原因不能使用36M,可优化电路,将SD卡座直接与开发板焊接,缩短传输距离;再验证是否可用36M传输,从而大大提高数据传输的效率。
- 若要支持中文编码,需要把option文件夹里面的cc936.c文件加入到工程,并在ffconf.c里配置_USE_LFN 宏和_CODE_PAGE宏。但由于我用的芯片flash不足,无法通过编译。
- 增加SD卡插入检测,本实验对SD卡的初始化是上电默认进行的,但是如果在MCU运行中,插入SD卡将无法调用SD卡的初始化函数。所以加入SD卡插入检测引脚后,当SD卡插入信号发生时,自动调用f_mount函数,对SD卡进行驱动使用。
水平有限,仅供参考,错误之处以及不足之处还望多多指教。
《路漫漫其修远兮,吾将上下而求索。 -------屈原》