单片机系统常需大容量存储设备,如U盘、FLASH芯片、SD卡等。
其中,SD卡因容量大、支持SPI/SDIO驱动、尺寸多样,成为单片机系统的优选。
STM32F4开发板自带SD卡接口,使用SDIO接口驱动,支持高速数据传输。
1.1 SDIO 简介
1.1.1 SDIO 主要功能及框图
STM32F4的SDIO控制器多种存储卡,包括 MMC卡、SD存储卡、SD I/O卡和 CE-ATA设备。
它兼容多种版本的系统规格,并支持1位、4位和8位数据总线模式。
SDIO 控制器由 SDIO适配器模块和 APB2总线接口组成,复位后默认使用 SDIO_D0进行数据传输,但可以通过命令改变总线宽度。
对于 MMC卡,通常使用 1位数据线;
而对于 SD或 SD I/O卡,则可以选择 1位或 4位数据线。
SDIO_CMD信号在初始化时采用开路模式(仅适用于旧版MMC卡),在命令传输时则采用推挽模式。
SDIO控制器在 8位总线模式下,数据传输速率最高可达 48MHz。
1.1.2 SDIO 的时钟
SDIO有三种时钟:
卡时钟(SDIO_CK)、SDIO适配器时钟(SDIOCLK)
和
APB2总线接口时钟(PCLK2)。
SDIO_CK频率根据卡类型变化,范围分别是0-20MHz、0-48MHz和0-25MHz。
SDIOCLK一般为48MHz,用于产生SDIO_CK。
PCLK2频率为HCLK/2,一般为84MHz。
SDIO_CK与SDIOCLK关系(时钟分频器不旁路时)为:
SDIO_CK=SDIOCLK/(2+CLKDIV)。
卡时钟 = 卡适配器时钟/(2+分频器)
CLKDIV通过SDIO寄存器设置,确保SDIO_CK不超过卡的最大频率。
注意,初始化时SDIO_CK不能超过400KHz,初始化后可设置到最大(但不能超过卡的最大频率)。
1.1.3 SDIO 的命令与响应
SDIO命令分为ACMD(相关命令)和CMD(通用命令),发送ACMD前需先发送CMD55。
所有命令和响应通过SDIO_CMD引脚传输,命令长度固定为48位。
STM32F4发出命令,硬件控制开始位、传输位、CRC7和结束位,
用户只需要设置命令索引和参数。
SD卡在接收命令后回复应答,这个应答我们称之为响应,有短响应(48位)和长响应(136位)两种,均带CRC错误检测。
硬件滤除起始位、传输位、CRC7和结束位,
短响应的命令索引存放在SDIO_RESPCMD寄存器,参数存放在SDIO_RESP1寄存器;
长响应的CID/CSD存放在SDIO_RESP1~SDIO_RESP4寄存器。
SD存储卡有5类响应(R1,R2,R3,R6,R7),
以R1为例,为短响应,包含起始位、传输位、命令索引、卡状态、CRC7校验位和结束位。
R1响应为短响应,提供命令索引和卡状态信息。
数据在SDIO控制器与SD卡间以数据块形式传输,SDIO硬件自动处理CRC校验。
单数据块读无需停止命令,多数据块读需发送CMD12停止。
读操作包括主机发送读命令,卡响应后发送数据块;
写操作类似,但需判断卡是否繁忙。
多块数据读写时,SD卡一直发送/接收数据,直到主机发送STOP命令(CMD12)。
数据块均带CRC校验,确保数据完整性和准确性。
1.1.4 SDIO 相关寄存器介绍
第一个,SDIO 电源控制寄存器(SDIO_POWER),
要启用SDIO,需先设SDIO_POWER寄存器最低2位为1,以开启电源和卡时钟。
第二个,SDIO 时钟控制寄存器(SDIO_CLKCR)。
SDIO_CLKCR寄存器控制SDIO时钟,设置SDIO时钟分配系数、时钟开关和数据位宽。
具体为:
WIDBUS= 1 表示4位总线;
BYPASS= 0 禁用旁路;
CLKEN= 1 启用时钟;
CLKDIV= 0 设定24MHz频率。
这些确保SDIO通信顺畅。
时钟分频器旁路(Bypass)是指在时钟系统中,通过特定的设置或配置,使时钟信号绕过分频器直接传输,而不对其进行分频处理。
第三个,SDIO 参数寄存器(SDIO_ARG),32位寄存器,存储命令参数,写命令前必须先写入此寄存器。
第四个,SDIO 命令响应寄存器(SDIO_RESPCMD),32位寄存器,低6位有效,存储最后命令响应的命令索引。
第五个,SDIO响应寄存器组(SDIO_RESP1~SDIO_RESP4),4个32位寄存器,存放卡响应信息,短响应存于SDIO_RESP1,长响应依次存放于(SDIO_RESP1~SDIO_RESP4)。
第七个,SDIO命令寄存器(SDIO_CMD)的低6位[5:0]表示命令索引,位[7:6]设置等待响应位,指示CPSM(命令通道状态机)的等待需求。
第八个,SDIO数据定时器寄存器(SDIO_DTIMER)存储数据超时时间,以SDIO_CK为周期。
计数器在DPSM进入Wait_R或繁忙状态时递减,若减为0则设置超时标志。
DPSM(数据通道状态机)负责监控和控制SDIO接口上的数据传输,确保数据能够按照预定的协议和时序正确地进行传输。
注意:数据传输前必须先写入SDIO_DTIMER和数据长度寄存器(SDIO_DLEN)。
第九个,SDIO数据长度寄存器(SDIO_DLEN)的低 25位用于设置传输的数据字节长度,对于块数据传输,其值需为数据块长度(由SDIO_DCTRL设置)的倍数。
第十个,SDIO数据控制寄存器(SDIO_DCTRL)用于控制DPSM,包括数据传输使能、方向、模式、DMA使能及数据块长度等设置,需根据实际情况配置实现正常数据收发。
第十一个,SDIO接口中的状态寄存器(SDIO_STA)、清除中断寄存器(SDIO_ICR)和中断屏蔽寄存器(SDIO_MASK)位定义相同,但功能各异。
SDIO_STA反映状态,SDIO_ICR用于清除中断,SDIO_MASK则控制中断屏蔽。
第十二个,SDIO的数据FIFO寄存器(SDIO_FIFO)包括接收和发送FIFO,各由16个32位寄存器组成,共32个地址。
CPU可通过FIFO进行多数据读写,
读SD卡数据需读SDIO_FIFO,
写数据则写SDIO_FIFO。
每次读写最多处理8个字(32字节),且操作必须以4字节对齐,否则出错。
1.1.5 SD 卡初始化流程
- 电源开启:流程始于SD卡电源开启。
- CMD0与CMD8检测:通过CMD0和CMD8命令检测SD卡是否响应。
- 电压与版本检查:
- 若无响应,检查是否为Ver2.00或更高版本的标准容量SD卡,确认电压范围是否兼容。
- 若电压不匹配或版本不兼容,则标记为不可用卡。
- 高容量支持(HCS)设置:对于兼容电压范围的卡片,检查是否支持高容量,并相应设置HCS值。
- ACMD41响应判断:
- 使用ACMD41命令,分别设置HCS=0和HCS=1(或未设置),判断卡片是否准备好。
- 根据响应码(OSS),区分卡片为标准容量、高容量或不可用的SD卡。
- 不可读卡处理:对于非SD存储卡,标记为“非SD存储卡”,并提示可能为MMC卡。
各类卡(SDHC、SDSC、SD1.x、MMC)上电后,
首先需设置SDIO_POWER[1:0]=11,
然后发送CMD0进行软复位,
接着发送CMD8命令以区分SD2.0卡与其他不支持该命令的卡(MMC、SD1.x)。
发送CMD8时,可通过参数设置VHS位,向SD卡传达主机的供电状态。
使用参数0x1AA发送CMD8,告知SD卡主机供电为2.7~3.6V。
若SD卡支持CMD8且电压范围兼容,则通过R7响应返回相同参数;否则,不响应。
发送CMD8后,需先发送CMD55,再发送ACMD41,以确认卡的操作电压范围,并通过HCS位告知SD卡主机是否支持高容量卡(SDHC)。
ACMD41 得到的响应(R3)包含 SD 卡 OCR 寄存器内容,
对于支持CMD8的卡,主机通过ACMD41的HCS位告知SD卡是否支持SDHC;不支持CMD8的卡,HCS设0。
SD卡接收ACMD41后返回OCR内容,主机据此判断卡类型及上电状态。
MMC卡不支持ACMD41和CMD55,需在CMD0后发送CMD1进行初始化。
最后,发送CMD2和CMD3获取SD卡的CID和RCA,完成类型区分及初始化。
RCA,SD卡的相对地址
CMD2,用于获得 CID 寄存器的数据,
SD卡收到CMD2后,返回R2长响应含CID信息,存于SDIO_RESP1~4寄存器。
CMD3用于设置RCA,SD卡自动返回,MMC卡需主机主动设置。
获得RCA后,发CMD9带RCA参数获CSD信息,含容量、扇区大小等。
最后,发CMD7选中SD卡开始读写。其他命令请参考《SD卡2.0协议.pdf》。
1.2 硬件设计
可以看到,SD卡有 4根数据线,一根时钟线,一根指令线。
分别连接到 STM32的复用引脚上。
1.2 FATFS文件系统
FATFS是免费开源的FAT文件系统模块,专为小型嵌入式系统设计。
它用标准C编写,具有良好的硬件平台独立性,易移植至多种单片机。
支持FAT12/16/32,多存储媒介,独立缓冲区,优化8/16位单片机。
特点包括 Windows兼容、平台无关、代码高效、配置多样、支持多卷、长文件名、多种代码页、RTOS、多种扇区大小及只读API等。
应用层用户无需了解 FATFS内部和 FAT协议,只需调用接口函数如 f_open, f_read, f_write, f_close等即可操作文件。
中间层 FATFS模块实现FAT读写协议,提供 ff.c和 ff.h,用户通常无需修改。
底层接口需用户编写移植代码,包括存储媒介读写和实时时钟。
目前FATFS最新版本为R0.10b,包含doc和src文件夹。
与平台无关的文件有 ffconf.h、ff.h、ff.c等,而与平台相关的代码需用户提供,主要是diskio.c。
移植FATFS时,通常只需修改ffconf.h和diskio.c。
ffconf.h包含所有配置项,如_FS_TINY、_FS_READONLY等,可根据需求进行配置。
移植步骤分为三步:
①在 integer.h中定义数据类型;
②通过 ffconf.h配置功能;
③编写 diskio.c中的底层驱动函数。
使用 MDK5编译器时,若数据类型与 integer.h一致,则无需改动。
1.3 底层磁盘I/O模块函数
底层磁盘 I/O模块不属于 FATFS,必须由用户提供,以实现物理磁盘的读写和时间获取。
底层磁盘 I/O模块涉及的函数一般有 6个,在 diskio.c里面。
1.3.1 dick_initialize()
首先是初始化磁盘驱动器,disk_initialize() 函数,
// 函数声明:初始化磁盘驱动器
DSTATUS disk_initialize(BYTE Drive) {
// 备注:此处应包含具体的初始化代码,如设备选择、参数配置等
// 由于具体实现与硬件相关,此处仅给出函数框架
// 假设初始化成功,清零 STA_NOINIT 标志(示例值,实际应根据硬件状态确定)
DSTATUS status = 0; // 假设返回值为 0 表示成功,具体值依据 DSTATUS 定义
// 备注:在实际应用中,需要根据硬件的响应来设置 status 的值
// 例如,如果初始化失败,可能需要设置相应的错误标志位
return status; // 返回磁盘状态值
}
// 示例用法:初始化驱动器 0
BYTE drive = 0; // 指定逻辑驱动器号
DSTATUS status = disk_initialize(drive); // 调用初始化函数
// 检查初始化是否成功(假设 STA_NOINIT 为 0x01,实际值应依据 DSTATUS 定义)
if ((status & STA_NOINIT) == 0) {
// 初始化成功,可以进行后续操作
} else {
// 初始化失败,处理错误情况
}
1.3.2 disk_status()
第二个函数是返回磁盘驱动器的状态,disk_status() 函数。
// 函数声明:获取指定逻辑驱动器的磁盘状态
DSTATUS disk_status(BYTE Drive) {
// 备注:此处应包含与硬件交互的代码以获取磁盘状态
// 由于实现与具体硬件相关,以下仅给出示例返回值
// 假设磁盘已初始化且未写保护,返回值为 0(具体值依据实际情况和 DSTATUS 定义)
DSTATUS status = 0; // 示例返回值,表示无错误状态
// 备注:在实际应用中,需要根据硬件的响应来设置 status 的值
// 例如,如果磁盘未初始化,可能需要设置 STA_NOINIT 标志位
// 如果磁盘被写保护,可能需要设置 STA_PROTECTED 标志位
return status; // 返回磁盘状态值
}
// 示例代码:获取驱动器 0 的状态
BYTE drive = 0; // 指定逻辑驱动器号
DSTATUS status = disk_status(drive); // 调用函数获取状态
// 检查磁盘状态(假设 STA_NOINIT 和 STA_PROTECTED 的值分别为 0x01 和 0x02,实际值应依据 DSTATUS 定义)
if (status & STA_NOINIT) {
// 磁盘未初始化,处理相应情况
}
if (status & STA_PROTECTED) {
// 磁盘被写保护,处理相应情况
}
// 其他状态检查和处理...
1.3.3 disk_read()
第三个函数是 disk_read() 函数,用于从磁盘驱动器上读取扇区。
/*读取磁盘扇区*/
DRESULT disk_read(BYTE Drive, //指定逻辑驱动器号(0-9)
BYTE* Buffer, //指向存储读取数据的字节数组指针,需足够大以存储所有扇区数据
DWORD SectorNumber, //起始扇区的逻辑块(LBA)地址
BYTE SectorCount) //要读取的扇区数(1-128)
{
// 检查参数有效性
if (Drive < 0 || Drive > 9 || Buffer == NULL || SectorNumber == 0 || SectorCount < 1 || SectorCount > 128) {
return RES_PARERR; // 传入非法参数
}
// 检查磁盘驱动器是否已初始化
if (!disk_is_initialized(Drive)) {
return RES_NOTRDY; // 磁盘驱动器未初始化
}
// 执行读取操作(此处为伪代码,需根据具体硬件实现)
// 假设 disk_hardware_read 为硬件相关的读取函数
// 注意:需确保Buffer地址对齐或处理非对齐情况
if (!disk_hardware_read(Drive, Buffer, SectorNumber, SectorCount)) {
return RES_ERROR; // 读操作期间发生不可恢复的错误
}
return RES_OK; // 函数执行成功
}
1.3.4 disk_write()
第四个函数是 disk_write() 函数,用于向磁盘写一个或多个扇区。
/*向磁盘写入*/
DRESULT disk_write(BYTE Drive, //Drive: 指定逻辑驱动器编号(0-9)
const BYTE* Buffer, //指向要写入字节数组的指针,需确保数据对齐或处理非对齐情况
DWORD SectorNumber, //起始扇区的逻辑块(LBA)地址
BYTE SectorCount) //要写入的扇区数(1-128)
{
// 检查参数有效性
if (Drive < 0 || Drive > 9 || Buffer == NULL || SectorNumber == 0 || SectorCount < 1 || SectorCount > 128) {
return RES_PARERR; // 传入非法参数
}
// 检查磁盘驱动器是否已初始化
if (!disk_is_initialized(Drive)) {
return RES_NOTRDY; // 磁盘驱动器未初始化
}
// 检查写保护状态(假设 disk_is_write_protected 为检查写保护的函数)
if (disk_is_write_protected(Drive)) {
return RES_WRPRT; // 媒体被写保护
}
// 执行写入操作(此处为伪代码,需根据具体硬件实现)
// 假设 disk_hardware_write 为硬件相关的写入函数
// 注意:需确保Buffer地址对齐或处理非对齐情况
if (!disk_hardware_write(Drive, Buffer, SectorNumber, SectorCount)) {
return RES_ERROR; // 写入期间产生错误且无法恢复
}
return RES_OK; // 函数执行成功
}
// 返回值:
// - RES_OK(0): 函数执行成功
// - RES_ERROR: 写入期间产生错误且无法恢复
// - RES_WRPRT: 媒体被写保护
// - RES_PARERR: 传入非法参数
// - RES_NOTRDY: 磁盘驱动器未初始化
// 所在文件:ff.c
// 注意事项:在只读配置中不需要此函数
1.3.5 disk_ioctl()
第五个函数是 disk_ioctl() 函数,用于控制设备指定特性和除了读写外的杂项功能。
#include "ff.h" // 假设包含FatFs相关的头文件
/*控制设备特性和功能*/
DRESULT disk_ioctl(BYTE Drive, //指定逻辑驱动器号(0-9)
BYTE Command, //指定命令代码
void* Buffer) //指向参数缓冲区的指针,根据命令代码确定,不使用时指定为 NULL
{
// 检查参数有效性
if (Drive < 0 || Drive > 9 || Buffer == NULL) {
// Drive参数超出范围或Buffer为NULL
return RES_PARERR;
}
// 检查磁盘驱动器是否已初始化
if (!disk_is_initialized(Drive)) {
return RES_NOTRDY; // 磁盘驱动器未初始化
}
// 根据命令代码执行相应操作
switch (Command) {
case CTRL_SYNC:
// 确保磁盘驱动器完成写处理,刷新写回缓存的扇区
// 假设 disk_sync 为实际执行同步操作的函数
if (!disk_sync(Drive)) {
return RES_ERROR; // 同步期间发生错误
}
break;
case GET_SECTOR_SIZE:
// 返回磁盘扇区大小,假设每个扇区大小为512字节
// 实际应用中应根据具体磁盘特性返回正确值
*(WORD*)Buffer = 512; // 假设扇区大小为512字节
break;
case GET_SECTOR_COUNT:
// 返回可利用的扇区数
// 假设 disk_get_sector_count 为获取扇区数的函数
*(DWORD*)Buffer = disk_get_sector_count(Drive);
break;
case GET_BLOCK_SIZE:
// 获取擦除块大小
// 假设每个擦除块大小为4096字节(实际应用中应根据具体磁盘特性返回正确值)
*(WORD*)Buffer = 4096; // 假设擦除块大小为4096字节
break;
case CTRL_ERASE_SECTOR:
// 强制擦除指定扇区
// 假设 disk_erase_sector 为执行擦除操作的函数
if (!disk_erase_sector(Drive, *(DWORD*)Buffer)) {
return RES_ERROR; // 擦除期间发生错误
}
break;
default:
// 未知命令代码
return RES_PARERR; // 传入非法参数
}
return RES_OK; // 函数执行成功
}
// 返回值:
// - RES_OK(0): 函数执行成功
// - RES_ERROR: 操作期间发生错误且无法恢复
// - RES_PARERR: 传入非法参数
// - RES_NOTRDY: 磁盘驱动器未初始化
// 所在文件:ff.c
1.3.6 get_fattime()
第六个函数是 get_fattime() 函数,用于获取当前时间。
// 函数名称: get_fattime
// 功能描述: 获取当前时间,并以特定格式封装返回
// 所在文件: ff.c
// 注意事项: 必须返回一个合法的时间值,不能为0,除非文件确实没有合法时间
#include "ff.h" // 假设包含FatFs相关的头文件
#include <time.h> // 用于获取系统时间的头文件
DWORD get_fattime() {
// 假设系统支持实时时钟,使用time函数获取当前时间
time_t current_time = time(NULL);
struct tm* time_info = localtime(¤t_time);
// 提取时间信息并封装成DWORD返回
// 注意:这里的秒是0~29,因为FAT文件系统的时间精度为2秒
DWORD fattime = 0;
fattime |= ((time_info->tm_year - 80) << 25); // 年份从1980年开始计算
fattime |= ((time_info->tm_mon + 1) << 21); // 月份1~12
fattime |= (time_info->tm_mday << 16); // 日期1~31
fattime |= (time_info->tm_hour << 11); // 小时0~23
fattime |= (time_info->tm_min << 5); // 分钟0~59
fattime |= (time_info->tm_sec / 2); // 秒0~29,FAT时间精度为2秒
return fattime; // 返回封装好的时间值
}
// 备注:
// 1. 如果系统不支持实时时钟,需要实现一个替代方案来生成合法的时间值。
// 2. 在只读配置下,此函数不会被调用,但仍然需要提供一个有效的实现。
// 3. 函数返回的时间值必须符合FAT文件系统的时间格式要求。
1.4 硬件设计
可以看到,SD卡有 4根数据线,一根时钟线,一根指令线。
分别连接到 STM32的复用引脚上。
1.5 软件设计
本章实验功能简介:
开机的时候先初始化 SD 卡,
初始化成功之后,注册两个工作区(一个给 SD 卡用,一个给 SPI FLASH 用),
然后获取 SD 卡的容量和剩余空间,并显示在 LCD模块上,最后等待 USMART 输入指令进行各项测试。
在工程目录下新建了FATFS文件夹,解压了FATFS R0.10b程序包,并新建了exfuns文件夹存放扩展代码。
1. 磁盘初始化与配置
- 在
diskio.c
中实现了磁盘初始化、状态获取、扇区读写和控制函数。 - 定义了SD卡和SPI FLASH的卷标和扇区大小。
- 初始化了SPI FLASH的前12M字节供FATFS使用。
#define SD_CARD 0 // 定义SD卡设备号
#define EX_FLASH 1 // 定义外部闪存设备号
#define FLASH_SECTOR_SIZE 512 // 定义闪存扇区大小
u16 FLASH_SECTOR_COUNT=2048*12; // 定义闪存扇区数量
// 磁盘初始化函数
DSTATUS disk_initialize(BYTE pdrv) {
u8 res=0; // 初始化结果变量
switch(pdrv) {
case SD_CARD:
res=SD_Init(); // 初始化SD卡
break;
case EX_FLASH:
W25QXX_Init(); // 初始化外部闪存
FLASH_SECTOR_COUNT=2048*12; // 设置闪存扇区数量
break;
default:
res=1; // 未知设备,设置错误状态
}
return res ? STA_NOINIT : 0; // 返回初始化状态
}
// 磁盘读取函数
DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) {
u8 res=0; // 初始化结果变量
if (!count) return RES_PARERR; // 如果读取扇区数为0,返回参数错误
switch(pdrv) {
case SD_CARD:
res=SD_ReadDisk(buff, sector, count); // 从SD卡读取数据
break;
case EX_FLASH:
for(;count>0;count--) { // 循环读取每个扇区
W25QXX_Read(buff, sector*FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE); // 从外部闪存读取数据
sector++; // 增加扇区号
buff+=FLASH_SECTOR_SIZE; // 移动缓冲区指针
}
res=0; // 设置读取成功状态
break;
default:
res=1; // 未知设备,设置错误状态
}
return res ? RES_ERROR : RES_OK; // 返回读取结果状态
}
// 类似地实现了 disk_write 和 disk_ioctl 函数
2. FATFS配置与内存管理
- 在
ffconf.h
中根据需求修改了相关配置。 - 实现了
ff_memalloc
和ff_memfree
函数用于动态内存分配。
3. 扩展功能与测试
- 在
exfuns
文件夹中编写了扩展代码,包括全局变量定义和磁盘容量获取函数。 - 编写了
fattester.c
用于测试FATFS函数,并通过USMART调用。
4. 主函数流程
- 初始化系统、延时、串口、LED、LCD、按键和SPI FLASH。
- 检测SD卡并初始化,若失败则提示错误。
- 挂载SD卡和SPI FLASH,若SPI FLASH文件系统错误则格式化。
- 显示SD卡总容量和剩余容量。
- 进入死循环,等待USMART测试。
int main(void) {
// 初始化各部分
while(SD_Init()) {
// 提示SD卡错误
}
exfuns_init();
f_mount(fs[0],"0:",1);
if(f_mount(fs[1],"1:",1)==0X0D) {
// 格式化SPI FLASH
}
// 显示SD卡容量信息
while(1) {
// 等待USMART测试
}
}
5. USMART配置
- 在
usmart_config.c
中添加了FATFS测试函数的名称和描述,以便通过USMART调用。
本工程通过FATFS文件系统实现了对SD卡和SPI FLASH的管理,并提供了测试功能。
打开串口助手,记得测试指令加换行