正点原子STM32(基于HAL库)5

SRAM 实验

STM32F103ZET6 自带了64K 字节的RAM,对一般应用来说,已经足够了,不过在一些对
内存要求高的场合,比如做华丽效果的GUI,处理大量数据的应用等,STM32 自带的这些内存
就可能不太够用了。好在嵌入式方案提供了扩展芯片RAM 的方法,本章将介绍我们开发板上
使用的RAM 拓展方案:使用SRAM 芯片,并驱动这个外部SRAM 提供程序需要的一部分RAM
空间,对其进行读写测试。

存储器简介

使用电脑时,我们会提到内存和内存条的概念,电脑维修的朋友有时候会说加个内存条电
脑就不卡了。实际上对于PC 来说一些情况下卡顿就是电脑同时运行的程序太多了,电脑处理
速度变慢的现象。而程序是动态加载到内存中的,一种解决方法就是增加电脑的内存来增加同
时可处理的程序的数量。对于单片机也是一样的,高性能有时候需要通过增加大内存来获得。内
存是存储器的一种,由于微机架构设计了不同的存储器放置不同的数据,所以我们也简单来了
解一下存储器。
存储器实际上是时序逻辑电路的一种,用来存放程序和数据信息。构成存储器的存储介质
主要采用半导体器件和磁性材料。存储器中最小的存储单位就是一个双稳态半导体电路或一个
CMOS 晶体管或磁性材料的存储元,它可存储一个二进制代码。由若干个存储元组成一个存储
单元,然后再由许多存储单元组成一个存储器。按不同的分类方式,存储器可以有表47.1.1 所
示的分类:
在这里插入图片描述
对于上述分类,在我们STM32 编程学习中我们常常只关心按读写功能分类的ROM 和RAM
两种,因为嵌入式程序主要对应到这两种存储器。对于RAM,目前常见的是SRAM 和DRAM,
它们因工作方式不同而得名,它们主要有以下的特性,如表47.1.2 所示:
在这里插入图片描述
在STM32 上,我们编译的程序,编译器一般会根据对应硬件的结构把程序中不同功能的数
据段分为ZI\RW\RO 这样的数据块,执行程序时分别放到不同的存储器上,这部分参考我们《第
九章STM32 启动过程分析》中关于map 文件的描述。对于我们编写的STM32 程序中的变量,
在默认配置下是加载到STM32 的RAM 区中执行的。而像程序代码和常量等编译后就固定不变
的则会放到ROM 区。
存储器的知识我们就介绍到这里,限于篇幅只能作简单的引用和介绍,大家可以查找资料
拓展对各种存储器作一下加深了解。

SRAM 方案简介

RAM 的功能我们已经介绍过了,SRAM 更稳定,但因为结构更复杂且造价更高,所以有更
大片上SRAM 的STM32 芯片造价也更高。而且由于SRAM 集成度低的原因,MCU 也不会把
片上SRAM 做得特别大,基于以上原因,计算机/微机系统中都允许采用外扩RAM 的方式提高
性能。

  1. SRAM 芯片介绍
    IS62WV51216 方案
    IS62WV51216 是ISSI (Integrated Silicon Solution, Inc)公司生产的一颗16 位宽512K(51216,
    即1M 字节)容量的CMOS 静态内存芯片。该芯片具有如下几个特点:
    ⚫ 高速。具有45ns/55ns 访问速度。
    ⚫ 低功耗。
    ⚫ TTL 电平兼容。
    ⚫ 全静态操作。不需要刷新和时钟电路。
    ⚫ 三态输出。
    ⚫ 字节控制功能。支持高/低字节控制。
    IS62WV51216 的功能框图如图47.1.1 所示:
    在这里插入图片描述
    图中A0~18 为地址线,总共19 根地址线(即2^19=512K,1K=1024);IO0~15 为数据线,
    总共16 根数据线。CS2 和CS1 都是片选信号,不过CS2 是高电平有效CS1 是低电平有效;OE
    是输出使能信号(读信号);WE 为写使能信号;UB 和LB 分别是高字节控制和低字节控制信
    号;
    XM8A51216 方案
    国产替代一直是国内嵌入式领域的一个话题,国产替代的优势一般是货源稳定,售价更低,
    也有专门研发对某款芯片作Pin to Pin 兼容的厂家,使用时无需修改PCB,直接更换元件即可,
    十分方便。
    正点原子开发板目前使用的一款替代IS62WV51216 的芯片是XM8A5121 ,它与
    IS62WV51216 一样采用TSOP44 封装,引脚顺序也与前者完全一致。
    XM8A51216 是星忆存储生产的一颗16 位宽512K(512
    16,即1M 位)容量的CMOS 静
    态内存芯片。采用异步SRAM 接口并结合独有的XRAM 免刷新专利技术,在大容量、高性能
    和高可靠及品质方面完全可以匹敌同类SRAM,具有较低功耗和低成本优势,可以与市面上同
    类型SRAM 产品硬件完全兼容,并且满足各种应用系统对高性能和低成本的要求,XM8A51216
    也可以当做异步SRAM 使用,该芯片具有如下几个特点:
    ⚫高速。具有最高访问速度10/12/15ns。
    ⚫低功耗。
    ⚫TTL 电平兼容。
    ⚫全静态操作。不需要刷新和时钟电路。
    ⚫三态输出。
    ⚫字节控制功能。支持高/低字节控制。
    该芯片与IS62WV51216 引脚和完全兼容,控制时序也类似,大家可以方便地直接替换。
    本章,我们使用FSMC 的BANK1 区域3 来控制SRAM 芯片,关于FSMC 的详细介绍,我
    们在学习LCD 的章节已经介绍过,我们采用的是读写不同的时序来操作TFTLCD 模块(因为
    TFTLCD 模块读的速度比写的速度慢很多),但是在本章,因为IS62WV51216/XM8A51216 的
    读写时间基本一致,所以,我们设置读写相同的时序来访问FSMC。关于FSMC 的详细介绍,
    请大家看《TFT LCD 实验》和《STM32F10xxx 参考手册_V10(中文版).pdf》。

硬件设计

  1. 例程功能
    本章实验功能简介:开机后,显示提示信息,然后按下KEY0 按键,即测试外部SRAM 容
    量大小并显示在LCD 上。按下KEY1 按键,即显示预存在外部SRAM 的数据。LED0 指示程
    序运行状态。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)按键:
    KEY0:PE4
    KEY1:PE3
    3)SRAM 芯片:
    XM8A51216/IS62WV51216
    4)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    5)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
  3. 原理图
    SRAM 芯片与STM32 的连接关系,如下图所示:
    在这里插入图片描述

图47.3.1 STM32 和SRAM 连接原理图(XM8A51216/IS62WV51216 封装相同)
SRAM 芯片直接是接在STM32F1 的FSMC 外设上,具体的引脚连接关系如下表47.3.1 所
示。
在这里插入图片描述
在上面的连接关系中,SRAM 芯片的A[0:18]并不是按顺序连接STM32F1 的FMSC_A[0:18],
这样设计的好处,就是可以方便我们的PCB 布线。不过这并不影响我们正常使用外部SRAM,
因为地址具有唯一性,只要地址线不和数据线混淆,就可以正常使用外部SRAM。

程序设计

操作SRAM 时要通过多个地址线寻址,然后才可以读写数据,在STM32 上可以使用FSMC
来实现,在TFT_LCD 一节我们也已经讲解过FSMC 接口的驱动,与之前的用法类似,关于HAL
库的部分我们这里就不重复介绍了。
使用SRAM 的配置步骤:
1)使能FSMC 时钟,并配置FSMC 相关的IO 及其时钟使能。
要使用FSMC,当然首先得开启其时钟。然后需要把FSMC_D015,FSMCA018 等相关
IO 口,全部配置为复用输出,并使能各IO 组的时钟。
2)设置FSMC BANK1 区域3 的相关寄存器。
此部分包括设置区域3 的存储器的工作模式、位宽和读写时序等。本章我们使用模式A、
16 位宽,读写共用一个时序寄存器。
3)使能BANK1 区域3。
最后,需要通过FSMC_BCR 寄存器使能BANK1 的区域3,使FSMC 工作起来。
通过以上几个步骤,我们就完成了FSMC 的配置,初始化FSMC 后就可以访问SRAM 芯
片时行读写操作了,这里还需要注意,因为我们使用的是BANK1 的区域3 ,所以
HADDR[27:26]=10,故外部内存的首地址为0X68000000。

程序流程图

在这里插入图片描述

程序解析

  1. SRAM 驱动
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SRAM 驱动源码
    包括两个文件:sram.c 和sram.h。
    为方便修改,我们在sram.h 中使用宏定义SRAM 的读写控制和片选引脚,它们定义如下:
#define SRAM_WR_GPIO_PORT GPIOD
#define SRAM_WR_GPIO_PIN GPIO_PIN_5
#define SRAM_WR_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOD_CLK_ENABLE();}while(0)
#define SRAM_RD_GPIO_PORT GPIOD
#define SRAM_RD_GPIO_PIN GPIO_PIN_4
#define SRAM_RD_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)
/* SRAM_CS(需要根据SRAM_FSMC_NEX设置正确的IO口) 引脚定义*/
#define SRAM_CS_GPIO_PORT GPIOG
#define SRAM_CS_GPIO_PIN GPIO_PIN_10
#define SRAM_CS_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOG_CLK_ENABLE();}while(0)

根据stm32f1 参考手册,SRAM 可以选择FSMC 对应的存储块1 上的4 个区域之一作为访
问地址,它上面有四块相互独立的64M 的连续寻址空间,为了能灵活根据不同的计算出使用的
地址空间,我们定义了以下的宏:

/* FSMC相关参数定义
* 注意: 我们默认是通过FSMC块3来连接SRAM, 块1有4个片选: FSMC_NE1~4
*
* 修改SRAM_FSMC_NEX, 对应的SRAM_CS_GPIO相关设置也得改
*/
#define SRAM_FSMC_NEX 3 /* 使用FSMC_NE3接SRAM_CS,取值范围只能是: 1~4 */
/*****************************************************************/
/* SRAM基地址, 根据SRAM_FSMC_NEX 的设置来决定基址地址
* 我们一般使用FSMC的块1(BANK1)来驱动SRAM, 块1地址范围总大小为256MB,均分成4块:
* 存储块1(FSMC_NE1)地址范围: 0X6000 0000 ~ 0X63FF FFFF
* * 存储块2(FSMC_NE2)地址范围: 0X6400 0000 ~ 0X67FF FFFF
* 存储块3(FSMC_NE3)地址范围: 0X6800 0000 ~ 0X6BFF FFFF
* 存储块4(FSMC_NE4)地址范围: 0X6C00 0000 ~ 0X6FFF FFFF
*/
#define SRAM_BASE_ADDR (0X60000000 + (0X4000000 * (SRAM_FSMC_NEX - 1)))

上述定义SRAM_FSMC_NEX 的值为3,即使用FSMC 存储块1 的第3 个地址范围,上面
的SRAM_BASE_ADDR 则根据我们使用的存储块计算出SRAM 空间的首地址,存储块3 对应
的是0X68000000 ~ 0X6BFFFFFF 的地址空间。
sram_init 的类似于LCD,我们需要根据原理图配置SRAM 的控制引脚,复用连接到SRAM
芯片上的IO 作为FSMC 的地址线,根据SRAM 芯片上的进序设置地址线宽度、等待时间、信
号极性等,则sram 的初始化函数我们编写如下:

void sram_init(void) {
    
    
    GPIO_InitTypeDef GPIO_Initure;
    FSMC_NORSRAM_TimingTypeDef fsmc_readwritetim;
    SRAM_CS_GPIO_CLK_ENABLE(); /* SRAM_CS脚时钟使能*/
    SRAM_WR_GPIO_CLK_ENABLE(); /* SRAM_WR脚时钟使能*/
    SRAM_RD_GPIO_CLK_ENABLE(); /* SRAM_RD脚时钟使能*/
    __HAL_RCC_FSMC_CLK_ENABLE(); /* 使能FSMC时钟*/
    __HAL_RCC_GPIOD_CLK_ENABLE(); /* 使能GPIOD时钟*/
    __HAL_RCC_GPIOE_CLK_ENABLE(); /* 使能GPIOE时钟*/
    __HAL_RCC_GPIOF_CLK_ENABLE(); /* 使能GPIOF时钟*/
    __HAL_RCC_GPIOG_CLK_ENABLE(); /* 使能GPIOG时钟*/
    GPIO_Initure.Pin = SRAM_CS_GPIO_PIN;
    GPIO_Initure.Mode = GPIO_MODE_AF_PP;
    GPIO_Initure.Pull = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SRAM_CS_GPIO_PORT, & GPIO_Initure); /* SRAM_CS引脚模式设置*/
    GPIO_Initure.Pin = SRAM_WR_GPIO_PIN;
    HAL_GPIO_Init(SRAM_WR_GPIO_PORT, & GPIO_Initure); /* SRAM_WR引脚模式设置*/
    GPIO_Initure.Pin = SRAM_RD_GPIO_PIN;
    HAL_GPIO_Init(SRAM_RD_GPIO_PORT, & GPIO_Initure); /* SRAM_CS引脚模式设置*/
    /* PD0,1,4,5,8~15 */
    GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
        GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13
    GPIO_PIN_14 | GPIO_PIN_15;
    GPIO_Initure.Mode = GPIO_MODE_AF_PP; /* 推挽复用*/
    GPIO_Initure.Pull = GPIO_PULLUP; /* 上拉*/
    GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
    HAL_GPIO_Init(GPIOD, & GPIO_Initure);
    /* PE0,1,7~15 */
    GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
        GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 |
        GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
    HAL_GPIO_Init(GPIOE, & GPIO_Initure);
    /* PF0~5,12~15 */
    GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
        GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_12 | GPIO_PIN_13 |
        GPIO_PIN_14 | GPIO_PIN_15;
    HAL_GPIO_Init(GPIOF, & GPIO_Initure);
    /* PG0~5,10 */
    GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1 |
        GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
    HAL_GPIO_Init(GPIOG, & GPIO_Initure);
    g_sram_handler.Instance = FSMC_NORSRAM_DEVICE;
    g_sram_handler.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
    g_sram_handler.Init.NSBank = (SRAM_FSMC_NEX == 1) ? FSMC_NORSRAM_BANK1 : \
        (SRAM_FSMC_NEX == 2) ? FSMC_NORSRAM_BANK2 : \
        (SRAM_FSMC_NEX == 3) ? FSMC_NORSRAM_BANK3 : \
        FSMC_NORSRAM_BANK4; /* 根据配置选择FSMC_NE1~4 */
    /* 地址/数据线不复用*/
    g_sram_handler.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
    g_sram_handler.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; /* SRAM */
    /* 16位数据宽度*/
    g_sram_handler.Init.MemoryDataWidth = SMC_NORSRAM_MEM_BUS_WIDTH_16;
    /* 是否使能突发访问,仅对同步突发存储器有效,此处未用到*/
    g_sram_handler.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
    /* 等待信号的极性,仅在突发模式访问下有用*/
    g_sram_handler.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
    /* 存储器是在等待周期之前的一个时钟周期还是等待周期期间使能NWAIT */
    g_sram_handler.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
    /* 存储器写使能*/
    g_sram_handler.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
    /* 等待使能位,此处未用到*/
    g_sram_handler.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
    /* 读写使用相同的时序*/
    g_sram_handler.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
    /* 是否使能同步传输模式下的等待信号,此处未用到*/
    g_sram_handler.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
    g_sram_handler.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; /* 禁止突发写*/
    /* FMC读时序控制寄存器*/
    /* 地址建立时间(ADDSET)为1个HCLK 1/72M=13.8ns */
    fsmc_readwritetim.AddressSetupTime = 0x00;
    fsmc_readwritetim.AddressHoldTime = 0x00; /* 地址保持时间(ADDHLD)模式A未用到*/
    fsmc_readwritetim.DataSetupTime = 0x01; /* 数据保存时间为3个HCLK=4*13.8=55ns */
    fsmc_readwritetim.BusTurnAroundDuration = 0X00;
    fsmc_readwritetim.AccessMode = FSMC_ACCESS_MODE_A; /* 模式A */
    HAL_SRAM_Init( & g_sram_handler, & fsmc_readwritetim, & fsmc_readwritetim);
}

初始化成功后,FSMC 控制器就能根据扩展的地址线访问SRAM 的数据,于是我们可以直
接根据地址指针来访问SRAM,我们定义SRAM 的写函数如下;

扫描二维码关注公众号,回复: 16523914 查看本文章
void sram_write(uint8_t * pbuf, uint32_t addr, uint32_t datalen) {
    
    
    for (; datalen != 0; datalen--) {
    
     * (volatile uint8_t * )(SRAM_BASE_ADDR + addr) = * pbuf;
        addr++;
        pbuf++;
    }
}

同样地,也是利用地址,可以构造出一个SRAM 的连续读函数:

void sram_read(uint8_t * pbuf, uint32_t addr, uint32_t datalen) {
    
    
    for (; datalen != 0; datalen--) {
    
     * pbuf++ = * (volatile uint8_t * )(SRAM_BASE_ADDR + addr);
        addr++;
    }
}

注意以上两个函数是操作unsigned char 类型的指针,当使用其它类型的指针时需要注意指
针的偏移量。难点主要是根据SRAM 芯片上的时序来初始化FSMC 控制器,大家参考芯片手册
上的时序结合代码来理解这部分初始化的过程。
2. main.c 代码
初始化好了SRAM,我们就可以使用SRAM 中的存储进行编程了,我们利用ARM 编译器
的特性:可以在某一绝对地址定义变量。为方便测试,我们直接定义一个与SRAM 容量大小类
似的数组,由于是1M 位的RAM,我们定义了uint32_t 类型后,大小要除4,故定义的测试数组如下:

/* 测试用数组, 起始地址为: SRAM_BASE_ADDR */
#if (__ARMCC_VERSION >= 6010050)
uint32_t g_test_buffer[250000] __attribute__((section(".bss.ARM.__at_0x68000000")));
#else
uint32_t g_test_buffer[250000] __attribute__((at(SRAM_BASE_ADDR)));
#endif

这里的__attribute__(())是ARM 编译器的一种关键字,它有很多种用法,可以通过特殊修饰
指定变量或者函数的属性。大家可以去MDK 的帮助文件里查找这个关键字的其它用法。这里
我们要用这个关键字把变量放到指定的位置,而且用了条件编译,因为MDK 的AC5 和AC6 下
的语法不同。
通过前面的描述,我们知道SRAM 的访问基地址是0x68000000,如果我们定义一个与
SRAM 空间大小相同的数组,而且数组指向的位置就是0x68000000 的话,则这通过数组就可
以很方便直接操作这块存储空间。所以回来前面所说的__attribute__这个关键字。对于AC5,它
可以用__attribute__((at(地址)))的方法来修饰变量,而且这个地址可以是一个算式,这样编译器
在编译时就会通过这个关键字判断并把这个数组放到我们定义的空间,如果硬件支持的情况下,
我们就可以访问这些指定空间的变量或常量了。但是对于AC6,同样指定地址,需要用
attribute((section(“.bss.ARM.__at_地址”)))的方法,指定一个绝对地址才能把变量或者常量放
到我们所需要定义的位置。这里这个地址就不支持算式了,但是这个语法对于相对而言更加地
通用,其它平台的编译器如gcc 也有类似的语法,而且AC5 下也可以用AC6 的这种语法来达
到相同效果,两者之间的差异,大家可以多实践以进行区分。
完成SRAM 部分的代码,main 函数只要实现对SRAM 的读写测试即可,我们。加入按键
和LCD 显示来辅助显示,在main 函数中编写代码如下:

int main(void) {
    
    
    uint8_t key;
    uint8_t i = 0;
    uint32_t ts = 0;
    HAL_Init(); /* 初始化HAL库*/
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72); /* 延时初始化*/
    usart_init(115200); /* 串口初始化为115200 */
    usmart_dev.init(72); /* 初始化USMART */
    led_init(); /* 初始化LED */
    lcd_init(); /* 初始化LCD */
    key_init(); /* 初始化按键*/
    sram_init(); /* SRAM初始化*/
    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "SRAM TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Test Sram", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY1:TEST Data", RED);
    for (ts = 0; ts < 250000; ts++) {
    
    
        g_test_buffer[ts] = ts; /* 预存测试数据*/
    }
    while (1) {
    
    
        key = key_scan(0); /* 不支持连按*/
        if (key == KEY0_PRES) {
    
    
            fsmc_sram_test(30, 150); /* 测试SRAM容量*/
        } else if (key == KEY1_PRES) /* 打印预存测试数据*/ {
    
    
            for (ts = 0; ts < 250000; ts++) {
    
     /* 显示测试数据*/
                lcd_show_xnum(30, 170, g_test_buffer[ts], 6, 16, 0, BLUE);
            }
        } else {
    
    
            delay_ms(10);
        }
        i++;
        if (i == 20) {
    
    
            i = 0;
            LED0_TOGGLE(); /* LED0闪烁*/
        }
    }
}

下载验证

在在代码编译成功之后,我们通过下载代码到开发板上,得到如图47.4.1 所示界面:
在这里插入图片描述
此时,我们按下KEY0,就可以在LCD 上看到内存测试的画面,同样,按下KEY1,就可
以看到LCD 显示存放在数组g_test_buffer 里面的测试数据,我们把数组的下标直接写到SRAM
中,可以看到这个数据在不断地更新,SRAM 读写操作成功了,如图47.4.2 所示:
在这里插入图片描述
该实验我们还可以借助USMART 来测试,如图47.4.3 所示:
在这里插入图片描述

内存管理实验

本章,我们将介绍内存管理。我们将使用内存的动态管理减少对内存的浪费。

内存管理简介

内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如
何高效、快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,
其实最终都是要实现两个函数:malloc 和free。malloc 函数用来内存申请,free 函数用于内存释
放。
本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法
的实现原理,如图48.1.1 所示:
在这里插入图片描述
从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为了
n 块,对应的内存管理表,大小也为n,内存管理表的每一个项对应内存池的一块内存。
内存管理表的项值代表的意义为:当该项值为0 的时候,代表对应的内存块未被占用,当
该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。
比如某项值为10,那么说明包括本项对应的内存块在内,总共分配了10 个内存块给外部的某
个指针。
内存分配方向如上图所示,是从顶→底的分配方向。即首先从最末端开始找空内存。当内
存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
分配原理:
当指针p 调用malloc 申请内存的时候,先判断p 要分配的内存块数(m),然后从第n 开
始,向下查找,直到找到m 块连续的空内存块(即对应内存管理表项为0),然后将这m 个内存管理表项的值都设置为m(标记被占用),最后,把最后的这个空内存块的地址返回指针p,
完成一次分配。注意:如果当内存不够的时候(找到最后也没有找到连续m 块空闲内存),则
返回NULL 给p,表示分配失败。
释放原理:
当p 申请的内存用完,需要释放的时候,调用free 函数实现。free 函数先判断p 指向的内
存地址所对应的内存块,然后找到对应的内存管理表项目,得到p 所占用的内存块数目m(内
存管理表项目的值就是所分配内存块的数目),将这m 个内存管理表项目的值都清零,标记释
放,完成一次内存释放。

硬件设计

  1. 例程功能
    每次按下按键KEY0 就申请2K 字节内存,每次按下KEY1 就写数据到申请到的内存里面,
    每次按下WK_UP 按键用于释放内存。LED0 闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)独立按键
    KEY0 –PE4
    KEY1 –PE3
    WK_UP –PA0
    3)串口1(USMART 使用)
    4)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    5)STM32 自带的SRAM
    6)开发板板载的SRAM

程序设计

程序流程图

在这里插入图片描述

程序解析

1.内存管理代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。内存管理驱动源
码包括两个文件:malloc.c 和malloc.h。这两个文件放在Middlewares 文件夹下面的MALLOC
文件夹。
下面我们直接介绍malloc.h 中比较重要的一个结构体和内存参数宏定义,其定义如下:

/* mem1内存参数设定.mem1是F103内部的SRAM. */
#define MEM1_BLOCK_SIZE 32 /* 内存块大小为32字节*/
#define MEM1_MAX_SIZE 40 * 1024 /* 最大管理内存40K, F103ZE内部SRAM总共512KB*/
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE /* 内存表大小*/
/* mem2内存参数设定.mem3是F103外扩SRAM */
#define MEM2_BLOCK_SIZE 32 /* 内存块大小为32字节*/
#define MEM2_MAX_SIZE 963 *1024 /* 最大管理内存963K, F103外扩SRAM大小1024KB */
#define MEM2_ALLOC_TABLE_SIZE MEM2_MAX_SIZE/MEM2_BLOCK_SIZE /* 内存表大小*/
/* 内存管理控制器*/
struct _m_mallco_dev
{
    
    
	void (*init)(uint8_t); /* 初始化*/
	uint16_t (*perused)(uint8_t); /* 内存使用率*/
	uint8_t *membase[SRAMBANK]; /* 内存池管理SRAMBANK个区域的内存*/
	MT_TYPE *memmap[SRAMBANK]; /* 内存管理状态表*/
	uint8_t memrdy[SRAMBANK]; /* 内存管理是否就绪*/
};

我们可以定义几个不同的内存管理表,再分配相应的指针给到管理控制器即可。程序中我
们用宏定义MEM1_BLOCK_SIZE 来定义malloc 可以管理的内部内存池总大小,实际上我们定
义为一个大小为MEM1_BLOCK_SIZE 的数组,这样编译后就能获得一块实际的连续内存区域,
这里是40Kb,MEM1_ALLOC_TABLE_SIZE 代表内存池的内存管理表大小。我们可以定义多
个内存管理表,这样就可以同时管理多块内存。
从这里可以看出,如果内存分块越小,那么内存管理表就越大,当分块为2 字节1 个块的
时候,内存管理表就和内存池一样大了(管理表的每项都是uint16_t 类型),显然是不合适。我
们这里取32 字节,比例为1:16,内存管理表相对就比较小了。
通过这个内存管理控制器_m_malloc_dev 结构体,我们把分块式内存管理的相关信息,其
初始化函数、获取使用率、内存池、内存管理表以及内存管理的状态保存下来,实现对内存池
的管理控制。其中,内存池的定义为:

/* 内存池(64字节对齐) */
static __align(64) uint8_t mem1base[MEM1_MAX_SIZE]; /* 内部SRAM内存池*/
static __align(64) uint8_t mem2base[MEM2_MAX_SIZE]
__attribute__((at(SRAM_BASE_ADDR))); /* 外扩SRAM内存池*/
/* 内存管理表*/
static MT_TYPE mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; /* 内部SRAM内存池MAP */
static MT_TYPE mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attrib-ute__((at(SRAM_BASE_ADDR + MEM2_MAX_SIZE))); /* 外扩SRAM内存池MAP */

这里我们定义了两个内存池:一个是内部的SRAM,另一个是外部的SRAM。
MDK 支持用__attirbute__((at(地址)))的方法把变量定义到指定的区域,而且这个变量支持
是算式,大家可以去MKD 的帮助文件中查找__attribute__这个关键字查找相关信息,有比较详
细的介绍。(SRAM 实验一章我们也有过说明了)我们这里是通过这个关键字,指定
mem2mapbase 这个大数组的存放位置为SRAM 上的空间,如果不加这个关键字修饰,MDK 会
默认把这些变量定义到STM32 的内部空间,这样的话就超出了STM32 内部的SRAM 空间,编
译时会直接报错。当然还有其它把变量定义到指定位置的方法,大家可以自行研究下。
其中,MEM1_MAX_SIZE 是在头文件中定义的内存池大小。__ align(64)定义内存池为64 字
节对齐,这个非常重要!如果不加上这个限制,在某些情况下(比如分配内存给结构体指针),
可能出现错误,所以一定要加上这个。
上面的写法是对于AC5 来说的,但是如果你想换成AC6 编译器的话就比较麻烦了,指定
变量位置的函数变成__attribute__((section(“.bss.ARM.__at_地址”)))的方式,其中的.bss 表示初
始化值为0,而且这个方式不支持算式,所以还用上面的方法直接用宏计算出SRAM 的地址的
方法不可行了,所以我们需要直接手动算出SRAM 对应的内存地址,同样地__align(64)在AC6
下的写法也变成了__ALIGNED(64),还有其它差异的部分,大家参考MDK 官方提供的AC5 到
AC6 的迁移方法的文档,这样定义的方法就变成下面的方式:

/* 内存池(64字节对齐) */
static __ALIGNED(64) uint8_t mem1base[MEM1_MAX_SIZE]; /* 内部SRAM内存池*/
static __ALIGNED(64) uint8_t mem2base[MEM2_MAX_SIZE] __attribute__((sec-tion(".bss.ARM.__at_0X68000000"))); /* 外扩SRAM内存池*/
/* 内存管理表*/
static MT_TYPE mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; /* 内部SRAM内存池MAP */
static MT_TYPE mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((sec-tion(".bss.ARM.__at_0X680F0C00"))); /* 外扩SRAM内存池MAP */

整个malloc 代码的核心函数:my_mem_malloc 和my_mem_free,分别用于内存申请和内存
释放。思路就是前面48.1 所介绍的分配内存和释放内存,不过在这里,这两个函数知识内部调
用,外部调用我们另外定义了mymalloc 和myfree 两个函数,其他函数我们就不多介绍了。下
面看一下分配内存和释放内存相关函数,其定义如下:

/**
 * @brief 内存分配(内部调用)
 * @param memx : 所属内存块
 * @param size : 要分配的内存大小(字节)
 * @retval 内存偏移地址
 * @arg 0 ~ 0XFFFFFFFE : 有效的内存偏移地址
 * @arg 0XFFFFFFFF : 无效的内存偏移地址
 */
static uint32_t my_mem_malloc(uint8_t memx, uint32_t size) {
    
    
        signed long offset = 0;
        uint32_t nmemb; /* 需要的内存块数*/
        uint32_t cmemb = 0; /* 连续空内存块数*/
        uint32_t i;
        if (!mallco_dev.memrdy[memx]) {
    
    
            mallco_dev.init(memx); /* 未初始化,先执行初始化*/
        }
        if (size == 0) return 0XFFFFFFFF; /* 不需要分配*/
        nmemb = size / memblksize[memx]; /* 获取需要分配的连续内存块数*/
        if (size % memblksize[memx]) nmemb++;
        for (offset = memtblsize[memx] - 1; offset >= 0; offset--) /*搜索整个内存控制区*/ {
    
    
            if (!mallco_dev.memmap[memx][offset]) {
    
    
                cmemb++; /* 连续空内存块数增加*/
            } else {
    
    
                cmemb = 0; /* 连续内存块清零*/
            }
            if (cmemb == nmemb) /* 找到了连续nmemb个空内存块*/ {
    
    
                for (i = 0; i < nmemb; i++) /* 标注内存块非空*/ {
    
    
                    mallco_dev.memmap[memx][offset + i] = nmemb;
                }
                return (offset * memblksize[memx]); /* 返回偏移地址*/
            }
        }
        return 0XFFFFFFFF; /* 未找到符合分配条件的内存块*/
    }
    /**
     * @brief 释放内存(内部调用)
     * @param memx : 所属内存块
     * @param offset : 内存地址偏移
     * @retval 释放结果
     * @arg 0, 释放成功;
     * @arg 1, 释放失败;
     * @arg 2, 超区域了(失败);
     */
static uint8_t my_mem_free(uint8_t memx, uint32_t offset) {
    
    
        int i;
        if (!mallco_dev.memrdy[memx]) /* 未初始化,先执行初始化*/ {
    
    
            mallco_dev.init(memx);
            return 1; /* 未初始化*/
        }
        if (offset < memsize[memx]) /* 偏移在内存池内. */ {
    
    
            int index = offset / memblksize[memx]; /* 偏移所在内存块号码*/
            int nmemb = mallco_dev.memmap[memx][index]; /* 内存块数量*/
            for (i = 0; i < nmemb; i++) /* 内存块清零*/ {
    
    
                mallco_dev.memmap[memx][index + i] = 0;
            }
            return 0;
        } else {
    
    
            return 2; /* 偏移超区了. */
        }
    }
    /**
     * @brief 释放内存(外部调用)
     * @param memx : 所属内存块
     * @param ptr : 内存首地址
     * @retval 无
     */
void myfree(uint8_t memx, void * ptr) {
    
    
        uint32_t offset;
        if (ptr == NULL) return; /* 地址为0. */
        offset = (uint32_t) ptr - (uint32_t) mallco_dev.membase[memx];
        my_mem_free(memx, offset); /* 释放内存*/
    }
    /**
     * @brief 分配内存(外部调用)
     * @param memx : 所属内存块
     * @param size : 要分配的内存大小(字节)
     * @retval 分配到的内存首地址.
     */
void * mymalloc(uint8_t memx, uint32_t size) {
    
    
    uint32_t offset;
    offset = my_mem_malloc(memx, size);
    if (offset == 0XFFFFFFFF) /* 申请出错*/ {
    
    
        return NULL; /* 返回空(0) */
    } else /* 申请没问题, 返回首地址*/ {
    
    
        return (void * )((uint32_t) mallco_dev.membase[memx] + offset);
    }
}
  1. main.c 代码
    main.c 代码如下:
const char * SRAM_NAME_BUF[SRAMBANK] = {
    
    
    " SRAMIN ", " SRAMEX "
};
int main(void) {
    
    
    uint8_t paddr[20]; /* 存放P Addr:+p地址的ASCII值*/
    uint16_t memused = 0;
    uint8_t key;
    uint8_t i = 0;
    uint8_t * p = 0;
    uint8_t * tp = 0;
    uint8_t sramx = 0; /* 默认为内部sram */
    HAL_Init(); /* 初始化HAL库*/
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72); /* 延时初始化*/
    usart_init(115200); /* 串口初始化为115200 */
    usmart_dev.init(72); /* 初始化USMART */
    led_init(); /* 初始化LED */
    lcd_init(); /* 初始化LCD */
    key_init(); /* 初始化按键*/
    sram_init(); /* SRAM初始化*/
    my_mem_init(SRAMIN); /* 初始化内部SRAM内存池*/
    my_mem_init(SRAMEX); /* 初始化外部SRAM内存池*/
    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "MALLOC TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Malloc & WR & Show", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY_UP:SRAMx KEY1:Free", RED);
    lcd_show_string(60, 160, 200, 16, 16, " SRAMIN ", BLUE);
    lcd_show_string(30, 176, 200, 16, 16, "SRAMIN USED:", BLUE);
    lcd_show_string(30, 192, 200, 16, 16, "SRAMEX USED:", BLUE);
    while (1) {
    
    
        key = key_scan(0); /* 不支持连按*/
        switch (key) {
    
    
            case KEY0_PRES:
                /* KEY0按下*/
                p = mymalloc(sramx, 2048); /*申请2K字节,并写入内容,显示在lcd屏幕上面*/
                if (p != NULL) {
    
     /* 向p写入一些内容*/
                    sprintf((char * ) p, "Memory Malloc Test%03d", i);
                    /* 显示P的内容*/
                    lcd_show_string(30, 260, 209, 16, 16, (char * ) p, BLUE);
                }
                break;
            case KEY1_PRES:
                /* KEY1按下*/
                myfree(sramx, p); /* 释放内存*/
                p = 0; /* 指向空地址*/
                break;
            case WKUP_PRES:
                /* KEY UP按下*/
                sramx++;
                if (sramx > 1) sramx = 0;
                lcd_show_string(60, 160, 200, 16, 16, (char * ) SRAM_NAME_BUF[sramx], BLUE);
                break;
        }
        if (tp != p) {
    
    
            tp = p;
            sprintf((char * ) paddr, "P Addr:0X%08X", (uint32_t) tp);
            /* 显示p的地址*/
            lcd_show_string(30, 240, 209, 16, 16, (char * ) paddr, BLUE);
            if (p) {
    
     /* 显示P的内容*/
                lcd_show_string(30, 260, 280, 16, 16, (char * ) p, BLUE);
            } else {
    
    
                lcd_fill(30, 260, 209, 296, WHITE); /* p=0,清除显示*/
            }
        }
        delay_ms(10);
        i++;
        if ((i % 20) == 0) /* DS0闪烁. */ {
    
    
            memused = my_mem_perused(SRAMIN);
            sprintf((char * ) paddr, "%d.%01d%%", memused / 10, memused % 10);
            /* 显示内部内存使用率*/
            lcd_show_string(30 + 112, 176, 200, 16, 16, (char * ) paddr, BLUE);
            memused = my_mem_perused(SRAMEX);
            sprintf((char * ) paddr, "%d.%01d%%", memused / 10, memused % 10);
            /* 显示TCM内存使用率*/
            lcd_show_string(30 + 112, 192, 200, 16, 16, (char * ) paddr, BLUE);
            LED0_TOGGLE(); /* LED0闪烁*/
        }
    }
}

该部分代码比较简单,主要是对mymalloc 和myfree 的应用。不过这里提醒大家,如果对
一个指针进行多次内存申请,而之前的申请又没释放,那么将造成“内存泄露”,这是内存管理
所不希望发生的,久而久之,可能导致无内存可用的情况!所以,在使用的时候,请大家一定
记得,申请的内存在用完以后,一定要释放。
另外,本章希望利用USMART 调试内存管理,所以在USMART 里面添加了mymalloc 和
myfree 两个函数,用于测试内存分配和内存释放。大家可以通过USMART 自行测试。

下载验证

将程序下载到开发板后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示
的内容如图48.4.1 所示:
在这里插入图片描述
可以看到,内存的使用率均为0%,说明还没有任何内存被使用。我们可以通过KEY_UP
选择申请内存的位置:SRAIN 为内部,SRAMEX 为外部。此时我们选择从内部申请内存,按下
KEY0,就可以看到申请了5%的一个内存块,同时看到下面提示了指针p 所指向的地址(其实
就是被分配到的内存地址)和内容。效果如图48.4.2 所示。
KEY0 键用来更新p 的内容,更新后的内容将重新显示在LCD 模块上。多按几次
KEY0,可以看到内存使用率持续上升(注意比对p 的值,可以发现是递减的,说明是从顶部
开始分配内存!)。每次申请一个内存块后,可以通过按下KEY0 释放本次申请的内存,如果我们每次申请完内存不再使用却不及时释放掉,再按KEY1 将无法释放之前的内存了,当这
样的情况重复了多次,就会造成“内存泄漏”。我们程序就是模拟这样一个情况,告诉请大家
在实际使用的时候去注意到这种做法的危险性,必须在编程时严格避免内存泄漏的情况发生。

在这里插入图片描述
本章,我们还可以借助USMART,测试内存的分配和释放,有兴趣的朋友可以动手试试。
如图48.4.2 所示:
在这里插入图片描述
图中,我们先申请了4660 字节的内存,然后得到申请到的内存首地址为:0x20009080,说
明我们申请内存成功(如果不成功,则会收到0),然后释放内存的时候,参数是指针的地址,
即执行:myfree(0x200097FC),就可以释放我们申请到的内存。其他情况,大家可以自行测试并
分析。

SD 卡实验

很多单片机系统都需要大容量存储设备,以存储数据。目前常用的有U 盘,FLASH 芯片,
SD 卡等。他们各有优点,综合比较,最适合单片机系统的莫过于SD 卡了,它不仅容量可以做
到很大(32GB 以上),支持SPI/SDIO 驱动,而且有多种体积的尺寸可供选择(标准的SD 卡尺
寸及Micor SD 卡尺寸等),能满足不同应用的要求。
只需要少数几个IO 口即可外扩一个高达32GB 或以上的外部存储器,容量从几十M 到几
十G 选择范围很大,更换也很方便,编程也简单,是单片机大容量外部存储器的首选。
ALIENTKE 战舰V4 版本以后,使用的接口是Micro SD 卡接口,卡座带自锁功能,使用
STM32F1 自带的SDIO 接口驱动,4 位模式,最高通信速度可达24Mhz,最高每秒可传输数据
12M 字节,对于一般应用足够了。在本章中,我们将向大家介绍,如何在正点原子战舰
STM32F103 上实现Micor SD 卡的读取。

SD 卡简介

SD 物理结构

SD 卡的规范由SD 卡协会明确,可以访问https://www.sdcard.org 查阅更多标准。SD 卡主
要有SD、Mini SD 和microSD(原名TF 卡,2004 年正式更名为Micro SD Card,为方便本文用
microSD 表示)三种类型,Mini SD 已经被microSD 取代,使用得不多,根据最新的SD 卡规格
列出的参数如表49.1.1.1 所示:
在这里插入图片描述
上述表格的“脚位数”,对应于实卡上的“金手指”数,不同类型的卡的触点数量不同,访问
的速度也不相同。SD 卡允许了不同的接口来访问它的内部存储单元。最常见的是SDIO 模式和
SPI 模式,根据这两种接口模式,我们也列出SD 卡引脚对应于这两种不同的电路模式的引脚功
能定义,如表49.1.1.2 所示。
在这里插入图片描述
我们对比着来看一下microSD 引脚,可见只比SD 卡少了一个电源引脚VSS2,其它的引
脚功能类似。
在这里插入图片描述
SD 卡和micorSD 只有引脚和形状大小不同,内部结构类似,操作时序完全相同,可以使
用完全相同的代码驱动,下面以9’Pin SD 卡的内部结构为为例,展示SD 卡的存储结构。
在这里插入图片描述
SD 卡有自己的寄存器,但它不能直接进行读写操作,需要通过命令来控制,SDIO 协议定
义了一些命令用于实现某一特定功能,SD 卡根据收到的命令要求对内部寄存器进行修改。图
49.1.1.4 中描述的SD 卡的寄存器是我们和SD 卡进行数据通讯的主要通道,如下:
在这里插入图片描述
关于SD 卡的更多信息和硬件设计规范可以参考SD 卡协议《Physical Layer Simplified Specification Version 2.00》的相关章节(注:因为STM32F1 的SDIO 匹配的是SD 协议2.0 版
本,后续版本也兼容此旧协议版本,故本章仍以2.0 版本为介绍对象)。

命令和响应

一个完整的SD 卡操作过程是:主机(单片机等)发起“命令”,SD 卡根据命令的内容决定是
否发送响应信息及数据等,如果是数据读/写操作,主机还需要发送停止读/写数据的命令来结束
本次操作,这意味着主机发起命令指令后,SD 卡可以没有响应、数据等过程,这取决于命令的
含义。这一过程如图49.1.2.1 所示。
在这里插入图片描述
SD 卡有多种命令和响应,它们的格式定义及含义在《SD 卡协议V2.0》的第三和第四章有
详细介绍,发送命令时主机只能通过CMD 引脚发送给SD 卡,串行逐位发送时先发送最高位
(MSB),然后是次高位这样类推……接下来,我们看看SD 卡的命令格式,如表49.1.2.1 所示:
在这里插入图片描述
SD 卡的命令固定为48 位,由6 个字节组成,字节1 的最高2 位固定为01,低6 位为命令
号(比如CMD16,为10000B 即16 进制的0X10,完整的CMD16,第一个字节为01010000,
即0X10+0X40)。字节2~5 为命令参数,有些命令是没有参数的。字节6 的高七位为CRC 值,
最低位恒定为1。
SD 卡的命令总共有12 类,分为Class0~Class11,本章,我们仅介绍几个比较重要的命令,
如表49.1.2.2 所示:
在这里插入图片描述
上表中,大部分的命令是初始化的时候用的。表中的R1、R3 和R7 等是SD 卡的应答信号,
每个响应也有规定好的格式,如图49.1.2.2 所示:
在这里插入图片描述
在规定为有响应的命令下,每发送一个命令,SD 卡都会给出一个应答,以告知主机该命令
的执行情况,或者返回主机需要获取的数据,应答可以是R1~R7,R1 的应答,各位描述如表
49.1.2.3 所示:
在这里插入图片描述
R2~R7 的响应,限于篇幅,我们就不介绍了,但需要注意的是除了R2 响应是128 位外,
其它的响应都是48 位,请大家参考SD 卡2.0 协议。

卡模式

SD 卡系统(包括主机和SD 卡)定义了SD 卡的工作模式,在每个操作模式下,SD 卡都有几
种状态,参考表49.1.3.1,状态之间通过命令控制实现卡状态的切换。
在这里插入图片描述
对于我们来说两种有效操作模式:卡识别模式和数据传输模式。在系统复位后,主机处于
卡识别模式,寻找总线上可用的SDIO 设备,对SD 卡进行数据读写之前需要识别卡的种类:
V1.0 标准卡、V2.0 标准卡、V2.0 高容量卡或者不被识别卡;同时,SD 卡也处于卡识别模式,
直到被主机识别到,即当SD 卡在卡识别状态接收到CMD3 (SEND_RCA)命令后,SD 卡就进入
数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式。
在卡识别模式下,主机会复位所有处于“卡识别模式”的SD 卡,确认其工作电压范围,识
别SD 卡类型,并且获取SD 卡的相对地址(卡相对地址较短,便于寻址)。在卡识别过程中,要
求SD 卡工作在识别时钟频率FOD 的状态下。卡识别模式下SD 卡状态转换如图49.1.3.1。
主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。主机也可以发送
GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并不会
复位。
主机在开始与卡通信前,需要先确定双方在互相支持的电压范围内。SD 卡有一个电压支持
范围,主机当前电压必须在该范围可能才能与卡正常通信。SEND_IF_COND(CMD8)命令就是
用于验证卡接口操作条件的(主要是电压支持)。卡会根据命令的参数来检测操作条件匹配性,
如果卡支持主机电压就产生响应,否则不响应。而主机则根据响应内容确定卡的电压匹配性。
CMD8 是SD 卡标准V2.0 版本才有的新命令,所以如果主机有接收到响应,可以判断卡为V2.0
或更高版本SD 卡。
SD_SEND_OP_COND(ACMD41)命令可以识别或拒绝不匹配它的电压范围的卡。ACMD41
命令的VDD 电压参数用于设置主机支持电压范围,卡响应会返回卡支持的电压范围。对于对
CMD8 有响应的卡,把ACMD41 命令的HCS 位设置为1,可以测试卡的容量类型,如果卡响
应的CCS 位为1 说明为高容量SD 卡,否则为标准卡。卡在响应ACMD41 之后进入准备状态,
不响应ACMD41 的卡为不可用卡,进入无效状态。ACMD41 是应用特定命令,发送该命令之前必须先发CMD55。

在这里插入图片描述
ALL_SEND_CID(CMD2)用来控制所有卡返回它们的卡识别号(CID),处于准备状态的卡在
发送CID 之后就进入识别状态。之后主机就发送SEND_RELATIVE_ADDR(CMD3)命令,让卡
自己推荐一个相对地址(RCA)并响应命令。这个RCA 是16bit 地址,而CID 是128bit 地址,使
用RCA 简化通信。卡在接收到CMD3 并发出响应后就进入数据传输模式,并处于待机状态,
主机在获取所有卡RCA 之后也进入数据传输模式。

数据模式

在数据模式下我们可以对SD 卡的存储块进行读写访问操作。SD 卡上电后默认以一位数据
总线访问,可以通过指令设置为宽总线模式,可以同时使有4 位总线并行读写数据,这样对于
支持宽总线模式的接口(如:SDIO 和QSPI 等)都能加快数据操作速度。
在这里插入图片描述
SD 卡有两种数据模式,一种是常规的8 位宽,即一次按一字节传输,另一种是一次按512
字节传输,我们只介绍前面一种。当按8-bit 连续传输时,每次传输从最低字节开始,每字节从
最高位(MSB)开始发送,当使用一条数据线时,只能通过DAT0 进行数据传输,那它的数据传输结构如图49.1.4.1 1 所示。
当使用4 线模式传输8-bit 结构的数据时,数据仍按MSB 先发送的原则,DAT[3:0]的高位
发送高数据位,低位发送低数据位。硬件支持的情况下,使用4 线传输可以提升传输速率。

在这里插入图片描述
只有SD 卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以将主
机SD 时钟频率设置为FPP,默认最高为25MHz,频率切换可以通过CMD4 命令来实现。数据
传输模式下,SD 卡状态转换过程见图49.1.4.3。
在这里插入图片描述
CMD7 用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能
有多个卡都是出于待机状态,必须选择一个RCA 地址目标卡使其进入传输状态才可以进行数
据通信。同时通过CMD7 命令也可以让已经被选择的目标卡返回到待机状态。
数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处于传
输状态下可以通过命令对卡进行数据读写、擦除。CMD12 可以中断正在进行的数据通信,让卡
返回到传输状态。CMD0 和CMD15 会中止任何数据编程操作,返回卡识别模式,注意谨慎使用,不当操作可能导致卡数据被损坏。
至此,我们已经介绍了SD 卡操作的一些知识,并知道了SD 卡操作的命令、响应和数据传
输等状态,接下来我们来分析实际的硬件接口如何向SD 卡发送我们需要的数据。

SDIO 接口简介

前面提到SD 卡的驱动方式之一是用SDIO 接口通讯,正点原子战舰STM32F103 自带SDIO
接口,本节,我们将简单介绍STM32F1 的SDIO 接口,包括:主要功能及框图、时钟、命令与
响应和相关寄存器简介等。

SDIO 主要功能及框图

SDIO 于2001 年推出,透过SD 总线连接多样设备的特性使得SDIO 逐渐被用于连接各种
嵌入式I/O 设备上。由于SD 总线简单的连接特性与支持更高的总线速度模式,SDIO 也变得越
来越普及。嵌入式解决方案让主机能在任何时间存取SDIO 装置而SD 卡插槽则可让用户使用
SD 存储卡。
SDIO 本来是记忆卡的标准,由于SD 卡方便即插即用的特性,现在也可以把SDIO 拿来插
上一些外围接口使用,如SDIO 的WIFI 卡、Bluetooth 卡、Radio/TV card 等。这些卡使用的SDIO
命令略有差异。根据SDIO 的类别,SDIO 的家族大概是这样:

在这里插入图片描述
STM32F1 的SDIO 控制器支持多媒体卡(MMC 卡)、SD 存储卡、SDI/O 卡和CE-ATA 设
备等。SDIO 的主要功能如下:
➢ 与多媒体卡系统规格书版本4.2 全兼容。支持三种不同的数据总线模式:1 位(默认)、4 位
和8 位。
➢ 与较早的多媒体卡系统规格版本全兼容(向前兼容)。
➢ 与SD 存储卡规格版本2.0 全兼容。SD 卡规范版本2.0,包括SD 和高容量SDHC 标准卡,
故不支持超大容量SDXC/SDUC 标准卡,所以STM32F1xx 的SDIO 可以支持的最高卡容
量是32GB。
➢ 与SDI/O 卡规格版本2.0 全兼容:支持良种不同的数据总线模式:1 位(默认)和4 位。
➢ 完全支持CE-ATA 功能(与CE-ATA 数字协议版本1.1 全兼容)。8 位总线模式下数据传输速
率可达48MHz。
➢ 数据和命令输出使能信号,用于控制外部双向驱动器。
➢ SDIO 没有SPI 兼容的通信模式。故用SPI 方式驱动的SD 卡我们会单独介绍。
STM32F1 的SDIO 控制器包含2 个部分:SDIO 适配器模块和AHB 总线接口,其功能框图
如图49.1.1.1 所示:
在这里插入图片描述
复位后默认情况下SDIO_D0 用于数据传输。初始化后主机可以改变数据总线的宽度(通过
ACMD6 命令设置)。如果一个多媒体卡接到了总线上,则SDIO_D0、SDIO_D[3:0]或SDIO_D[7:0]
可以用于数据传输。MMC 版本V3.31 和之前版本的协议只支持1 位数据线,所以只能用
SDIO_D0(为了通用性考虑,在程序里面我们只要检测到是MMC 卡就设置为1 位总线数据)。
如果一个SD 或SDI/O 卡接到了总线上,可以通过主机配置数据传输使用SDIO_D0 或
SDIO_D[3:0]。所有的数据线都工作在推挽模式。SDIO_CMD 有两种操作模式:
①用于初始化时的开路模式(仅用于MMC 版本V3.31 或之前版本)
②用于命令传输的推挽模式(SD/SDI/O 卡和MMCV4.2 在初始化时也使用推挽驱动)

SDIO 的时钟

从图49.2.1.1 我们可以看到SDIO 总共有3 个时钟,分别是:
①卡时钟(SDIO_CK):每个时钟周期在命令和数据线上传输1 位命令或数据。对于多媒
体卡V3.31 协议,时钟频率可以在0MHz 至20MHz 间变化;对于多媒体卡V4.0/4.2 协议,时
钟频率可以在0MHz 至48MHz 间变化;对于SD 或SDI/O 卡,时钟频率可以在0MHz 至25MHz
间变化。
②SDIO 适配器时钟(SDIOCLK):该时钟用于驱动SDIO 适配器,其频率等于AHB 总线
频率(HCLK),并用于产生SDIO_CK 时钟。
③AHB 总线接口时钟(HCLK/2):该时钟用于驱动SDIO 的AHB 总线接口,其频率为
HCLK/2。
前面提到,我们的SD 卡时钟(SDIO_CK),根据卡的不同,可能有好几个区间,这就涉及
到时钟频率的设置,SDIO_CK 与SDIOCLK 的关系为:
在这里插入图片描述
其中,SDIO CLK 为HCLK,一般是72Mhz,而CLKDIV 则是分配系数,可以通过SDIO
的SDIO_CLKCR 寄存器进行设置(确保SDIO_CK 不超过卡的最大操作频率)。
这里要提醒大家,在SD 卡刚刚初始化的时候,其时钟频率(SDIO_CK)是不能超过400Khz
的,否则可能无法完成初始化。在初始化以后,就可以设置时钟频率到最大了(但不可超过SD
卡的最大操作时钟频率)。

SDIO 的命令与响应

前机介绍SD 卡寄器时已经说过SD 卡是需要通过命令控制的,下面我们将介绍一些主要操作命令和响应过程,没介绍完的部分,大家可以对照光盘《SD 卡2.0 协议.pdf 》或
《STM32F10xxx 参考手册_V10(中文版).pdf》第20 章进行更深入的学习。
SDIO 的命令分为应用相关命令(ACMD)和通用命令(CMD)两部分,应用相关命令(ACMD)
的发送,必须先发送通用命令(CMD55),然后才能发送应用相关命令(ACMD)。
SDIO 的所有命令和响应都只通过SDIO_CMD 引脚传输的,任何命令的长度都是固定为48
位,SDIO 的命令格式如表49.2.3.1 所示:

在这里插入图片描述
所有的命令都是由STM32F1 发出,其中开始位、传输位、CRC7 和结束位由SDIO 硬件控
制,我们需要设置的就只有命令索引和参数部分。其中命令索引(如CMD0,CMD1 之类的)
在SDIO_CMD 寄存器里面设置,命令参数则由寄存器SDIO_ARG 设置。
一般情况下,选中的SD 卡在接收到命令之后,都会回复一个应答(注意CMD0 是无应答
的),这个应答我们称之为响应,响应也是在CMD 线上串行传输的。STM32F1 的SDIO 控制器
支持2 种响应类型,即:短响应(48 位)和长响应(136 位),这两种响应类型都带CRC 错误
检测(注意不带CRC 的响应应该忽略CRC 错误标志,如CMD1 的响应)。短响应的格式如表
49.2.3.2 所示:
在这里插入图片描述
长响应的格式如表49.2.3.3 所示:
在这里插入图片描述
同样,硬件为我们滤除了开始位、传输位、CRC7 以及结束位等信息,对于短响应,命令索
引存放在SDIO_RESPCMD 寄存器,参数则存放在SDIO_RESP1 寄存器里面。对于长响应,则
仅留CID/CSD 位域,存放在SDIO_RESP1~SDIO_RESP4 等4 个寄存器。
SD 存储卡总共有5 类响应(R1、R2、R3、R6、R7),我们这里以R1 为例简单介绍一下。
R1(普通响应命令)响应属于短响应,其长度为48 位,R1 响应的格式如表49.2.3.4 所示:
在这里插入图片描述
在收到R1 响应后,我们可以从SDIO_RESPCMD 寄存器和SDIO_RESP1 寄存器分别读出
命令索引和卡状态信息。
主要的响应就介绍到这,最后,我们看看数据在SDIO 控制器与SD 卡之间的传输。对于
SDI/SDIO 存储器,数据是以数据块的形式传输的,而对于MMC 卡,数据是以数据块或者数据
流的形式传输。本节我们只考虑数据块形式的数据传输。SDIO(多)数据块读操作,如图49.2.3.1
所示:
在这里插入图片描述
从上图,我们可以看出,从机在收到主机相关命令后,开始发送数据块给主机,所有数据
块都带有CRC 校验值(CRC 由SDIO 硬件自动处理),单个数据块读的时候,在收到1 个数据
块以后即可以停止了,不需要发送停止命令(CMD12)。但是多块数据读的时候,SD 卡将一直
发送数据给主机,直到接到主机发送的STOP 命令(CMD12)。SDIO(多)数据块写操作,如
图49.2.3.2 所示:
在这里插入图片描述
数据块写操作同数据块读操作基本类似,只是数据块写的时候,多了一个繁忙判断,新的
数据块必须在SD 卡非繁忙的时候发送。这里的繁忙信号由SD 卡拉低SDIO_D0,以表示繁忙,
SDIO 硬件自动控制,不需要我们软件处理。
SDIO 的命令与响应就为大家介绍到这里。

SDIO 相关寄存器介绍

这部分将结合《STM32F10xxx 参考手册_V10(中文版).pdf》的内容和大家一起分析使用
SDIO 时我们主要用的一些寄存器的情况。
⚫ SDIO 电源控制寄存器(SDIO_POWER)
SDIO 电源控制寄存器(SDIO_POWER)寄存器复位值为0,所以SDIO 的电源是关闭的,
我们要启用SDIO,第一步就是要设置该寄存器最低2 个位均为1,让SDIO 上电,开启卡时钟。
该寄存器定义如图49.2.4.1 所示:
在这里插入图片描述
⚫ SDIO 时钟控制寄存器(SDIO_CLKCR)
SDIO 时钟控制寄存器(SDIO_CLKCR),该寄存器主要用于设置SDIO_CK 的分配系数,
开关等,并可以设置SDIO 的数据位宽,该寄存器的定义如图49.2.4.2 所示:
图49.2.4.2 SDIO_CLKCR 寄存器位定义

上图仅列出了部分我们要用到的位设置,WIDBUS 用于设置SDIO 总线位宽,正常使用的
时候,设置为1,即4 位宽度。BYPASS 用于设置分频器是否旁路,我们一般要使用分频器,所
以这里设置为0,禁止旁路。CLKEN 则用于设置是否使能SDIO_CK,我们设置为1。最后,
CLKDIV 则用于控制SDIO_CK 的分频,设置为1,即可得到24Mhz 的SDIO_CK 频率。
⚫ SDIO 参数制寄存器(SDIO_ARG)
SDIO 参数制寄存器(SDIO_ARG),该寄存器比较简单,就是一个32 位寄存器,用于存储
命令参数,不过需要注意的是,必须在写命令之前先写这个参数寄存器!
⚫ SDIO 命令响应寄存器(SDIO_RESPCMD)
SDIO 命令响应寄存器(SDIO_RESPCMD),该寄存器为32 位,但只有低6 位有效,比较简单,用于存储最后收到的命令响应中的命令索引。如果传输的命令响应不包含命令索引,则
该寄存器的内容不可预知。
⚫ SDIO 响应寄存器组(SDIO_RESP1~SDIO_RESP4)
SDIO 响应寄存器组(SDIO_RESP1~SDIO_RESP4),该寄存器组总共由4 个32 位寄存器
组成,用于存放接收到的卡响应部分信息。如果收到短响应,则数据存放在SDIO_RESP1 寄存
器里面,其他三个寄存器没有用到。而如果收到长响应,则依次存放在
SDIO_RESP1~SDIO_RESP4 里面,如表49.2.4.3 所示:

在这里插入图片描述
⚫ SDIO 命令寄存器(SDIO_CMD)
SDIO 命令寄存器(SDIO_CMD)各位定义如图49.2.4.3 所示:
在这里插入图片描述
图中只列出了部分位的描述,其中低6 位为命令索引,也就是我们要发送的命令索引号(比
如发送CMD1,其值为1,索引就设置为1)。位[7:6],用于设置等待响应位,用于指示CPSM
是否需要等待,以及等待类型等。这里的CPSM,即命令通道状态机,我们就不详细介绍了,
请参阅《STM32F10xxx 参考手册_V10(中文版).pdf》第368 页,有详细介绍。命令通道状态
机我们一般都是开启的,所以位10 要设置为1。
⚫ SDIO 数据定时器寄存器(SDIO_DTIMER)
SDIO 数据定时器寄存器(SDIO_DTIMER)用于存储以卡总线时钟(SDIO_CK)为周期的
数据超时时间,一个计数器将从SDIO_DTIMER 寄存器加载数值,并在数据通道状态机(DPSM)
进入Wait_R 或繁忙状态时进行递减计数,当DPSM 处在这些状态时,如果计数器减为0,则设
置超时标志。这里的DPSM,即数据通道状态机,类似CPSM,详细请参考《STM32F10xxx 参
考手册_V10(中文版).pdf》第372 页。注意:在写入数据控制寄存器,进行数据传输之前,必
须先写入该寄存器(SDIO_DTIMER)和数据长度寄存器(SDIO_DLEN)!
⚫ SDIO 数据长度寄存器(SDIO_DLEN)
SDIO 数据长度寄存器(SDIO_DLEN)低25 位有效,用于设置需要传输的数据字节长度。
对于块数据传输,该寄存器的数值,必须是数据块长度(通过SDIO_DCTRL 设置)的倍数。
⚫ SDIO 数据控制寄存器(SDIO_DCTRL)
SDIO 数据控制寄存器(SDIO_DCTRL)各位定义如图49.2.4.4 所示:
在这里插入图片描述
该寄存器,用于控制数据通道状态机(DPSM),包括数据传输使能、传输方向、传输模式、
DMA 使能、数据块长度等信息,都是通过该寄存器设置。我们需要根据自己的实际情况,来配
置该寄存器,才可正常实现数据收发。
接下来,我们介绍几个位定义十分类似的寄存器,他们是:状态寄存器(SDIO_STA)、清
除中断寄存器(SDIO_ICR)和中断屏蔽寄存器(SDIO_MASK),这三个寄存器每个位的定义都
相同,只是功能各有不同。所以可以一起介绍,以状态寄存器(SDIO_STA)为例,该寄存器各
位定义如图49.2.4.5 所示:
在这里插入图片描述
状态寄存器可以用来查询SDIO 控制器的当前状态,以便处理各种事务。比如SDIO_STA
的位2 表示命令响应超时,说明SDIO 的命令响应出了问题。我们通过设置SDIO_ICR 的位2
则可以清除这个超时标志,而设置SDIO_MASK 的位2,则可以开启命令响应超时中断,设置
为0 关闭。其他位我们就不一一介绍了,请大家自行学习。
最后,我们向大家介绍SDIO 的数据FIFO 寄存器(SDIO_FIFO),数据FIFO 寄存器包括
接收和发送FIFO,他们由一组连续的32 个地址上的32 个寄存器组成,CPU 可以使用FIFO 读
写多个操作数。例如我们要从SD 卡读数据,就必须读SDIO_FIFO 寄存器,要写数据到SD 卡,
则要写SDIO_FIFO 寄存器。SDIO 将这32 个地址分为16 个一组,发送接收各占一半。而我们
每次读写的时候,最多就是读取发送FIFO 或写入接收FIFO 的一半大小的数据,也就是8 个字
(32 个字节),这里特别提醒,我们操作SDIO_FIFO(不论读出还是写入)必须是以4 字节对齐
的内存进行操作,否则将导致出错!
至此,SDIO 的相关寄存器介绍,我们就介绍完了。还有几个不常用的寄存器,我们没有介
绍到,请大家参考《STM32F10xxx 参考手册_V10(中文版).pdf》第20 章相关章节。

SD 卡初始化流程

SDIO 模式下的SD 卡初始化

这一节,我们来看看SD 卡的初始化流程,要实现SDIO 驱动SD 卡,最重要的步骤就是
SD 卡的初始化,只要SD 卡初始化完成了,那么剩下的(读写操作)就简单了,所以我们这里
重点介绍SD 卡的初始化。从《SD 卡2.0 协议》(见光盘资料)文档,我们得到SD 卡初始化流
程图如图49.3.1.1 所示:
在这里插入图片描述
从图中,我们看到,不管什么卡(这里我们将卡分为4 类:SD2.0 高容量卡(SDHC,最大
32G),SDv2.0 标准容量卡(SDSC,最大2G),SD1.x 卡和MMC 卡),首先我们要执行的是卡
上电(需要设置SDIO_POWER[1:0]=11),上电后发送CMD0,对卡进行软复位,之后发送CMD8
命令,用于区分SD 卡2.0,只有2.0 及以后的卡才支持CMD8 命令,MMC 卡和V1.x 的卡,是
不支持该命令的。CMD8 的格式如表49.3.1.1 所示:
在这里插入图片描述
这里,我们需要在发送CMD8 的时候,通过其带的参数我们可以设置VHS 位,以告诉SD
卡,主机的供电情况,VHS 位定义如表49.3.1.2 所示:
在这里插入图片描述
这里我们使用参数0X1AA,即告诉SD 卡,主机供电为2.7~3.6V 之间,如果SD 卡支持
CMD8,且支持该电压范围,则会通过CMD8 的响应(R7)将参数部分原本返回给主机,如果
不支持CMD8,或者不支持这个电压范围,则不响应。
在发送CMD8 后,发送ACMD41(注意发送ACMD41 之前要先发送CMD55),来进一步
确认卡的操作电压范围,并通过HCS 位来告诉SD 卡,主机是不是支持高容量卡(SDHC)。
ACMD41 的命令格式如表49.3.1.3 所示:
在这里插入图片描述
ACMD41 得到的响应(R3)包含SD 卡OCR 寄存器内容,OCR 寄存器内容定义如表49.3.1.4
所示:
在这里插入图片描述
对于支持CMD8 指令的卡,主机通过ACMD41 的参数设置HCS 位为1,来告诉SD 卡主
机支SDHC 卡,如果设置为0,则表示主机不支持SDHC 卡,SDHC 卡如果接收到HCS 为0,
则永远不会反回卡就绪状态。对于不支持CMD8 的卡,HCS 位设置为0 即可。
SD 卡在接收到ACMD41 后,返回OCR 寄存器内容,如果是2.0 的卡,主机可以通过判断
OCR 的CCS 位来判断是SDHC 还是SDSC;如果是1.x 的卡,则忽略该位。OCR 寄存器的最
后一个位用于告诉主机SD 卡是否上电完成,如果上电完成,该位将会被置1。
对于MMC 卡,则不支持ACMD41,不响应CMD55,对MMC 卡,我们只需要在发送CMD0
后,在发送CMD1(作用同ACMD41),检查MMC 卡的OCR 寄存器,实现MMC 卡的初始化。
至此,我们便实现了对SD 卡的类型区分,图49.1.5.1 中,最后发送了CMD2 和CMD 3 命
令,用于获得卡CID 寄存器数据和卡相对地址(RCA)。
CMD2,用于获得CID 寄存器的数据,CID 寄存器数据各位定义如表49.3.1.5 所示:
在这里插入图片描述
SD 卡在收到CMD2 后,将返回R2 长响应(136 位),其中包含128 位有效数据(CID 寄
存器内容),存放在SDIO_RESP1~4 等4 个寄存器里面。通过读取这四个寄存器,就可以获得
SD 卡的CID 信息。
CMD3,用于设置卡相对地址(RCA,必须为非0),对于SD 卡(非MMC 卡),在收到
CMD3 后,将返回一个新的RCA 给主机,方便主机寻址。RCA 的存在允许一个SDIO 接口挂
多个SD 卡,通过RCA 来区分主机要操作的是哪个卡。而对于MMC 卡,则不是由SD 卡自动
返回RCA,而是主机主动设置MMC 卡的RCA,即通过CMD3 带参数(高16 位用于RCA 设
置),实现RCA 设置。同样MMC 卡也支持一个SDIO 接口挂多个MMC 卡,不同于SD 卡的
是所有的RCA 都是由主机主动设置的,而SD 卡的RCA 则是SD 卡发给主机的。
在获得卡RCA 之后,我们便可以发送CMD9(带RCA 参数),获得SD 卡的CSD 寄存器
内容,从CSD 寄存器,我们可以得到SD 卡的容量和扇区大小等十分重要的信息。CSD 寄存器
我们在这里就不详细介绍了,关于CSD 寄存器的详细介绍,请大家参考《SD 卡2.0 协议.pdf》。
至此,我们的SD 卡初始化基本就结束了,最后通过CMD7 命令,选中我们要操作的SD
卡,即可开始对SD 卡的读写操作了,SD 卡的其他命令和参数,我们这里就不再介绍了,请大
家参考《SD 卡2.0 协议.pdf》,里面有非常详细的介绍。

SPI 模式下的SD 卡初始化

STM32 的SDIO 驱动模式和SPI 模式不兼容,二者使用时需要区分开来。《SD 卡2.0 协
议.pdf》中提供了SD 卡的SPI 初始化时序,我们可以按它建议的流程进行SD 卡的初始化,如
图49.3.2.1 所示。
在这里插入图片描述
要使用SPI 模式驱动SD 卡,先得让SD 卡进入SPI 模式。方法如下:在SD 卡收到复位命
令(CMD0)时,CS 为有效电平(低电平)则SPI 模式被启用。不过在发送CMD0 之前,要发
送>74 个时钟,这是因为SD 卡内部有个供电电压上升时间,大概为64 个CLK,剩下的10 个
CLK 用于SD 卡同步,之后才能开始CMD0 的操作,在卡初始化的时候,CLK 时钟最大不能超
过400Khz!
接着我们看看SD 卡的初始化,由前面SD 卡的基本介绍,我们知道SD 卡是先发送数据高
位的,SD 卡的典型初始化过程如下:
1、初始化与SD 卡连接的硬件条件(MCU 的SPI 配置,IO 口配置);
2、拉低片选信号,上电延时(>74 个CLK);
3、复位卡(CMD0),进入IDLE 状态;
4、发送CMD8,检查是否支持2.0 协议;
5、根据不同协议检查SD 卡(命令包括:CMD55、ACMD41、CMD58 和CMD1 等);
6、取消片选,发多8 个CLK,结束初始化
这样我们就完成了对SD 卡的初始化,注意末尾发送的8 个CLK 是提供SD 卡额外的时
钟,完成某些操作。通过SD 卡初始化,我们可以知道SD 卡的类型(V1、V2、V2HC 或者MMC),
在完成了初始化之后,就可以开始读写数据了。
SD 卡单扇区读取数据,这里通过CMD17 来实现,具体过程如下:
1、发送CMD17;
2、接收卡响应R1;
3、接收数据起始令牌0XFE;
4、接收数据;
5、接收2 个字节的CRC,如果不使用CRC,这两个字节在读取后可以丢掉。
6、禁止片选之后,发多8 个CLK;
以上就是一个典型的读取SD 卡数据过程,SD 卡的写于读数据差不多,写数据通过CMD24
来实现,具体过程如下:
1、发送CMD24;
2、接收卡响应R1;
3、发送写数据起始令牌0XFE;
4、发送数据;
5、发送2 字节的伪CRC;
6、禁止片选之后,发多8 个CLK;
以上就是一个典型的写SD 卡过程。关于SD 卡的介绍,我们就介绍到这里,更详细的关于
SPI 操作SD 卡方法可以参考我们《STM32F1 Mini 板》关于SD 卡的章节,我们这里就不再展
开了。

硬件设计

  1. 例程功能
    本章实验功能简介:开机的时候先初始化SD 卡,如果SD 卡初始化完成,则提示LCD
    初始化成功。按下KEY0,读取SD 卡扇区0 的数据,然后通过串口发送到电脑。如果没初
    始化通过,则在LCD 上提示初始化失败。同样用DS0 来指示程序正在运行。
  2. 硬件资源
    1)LED 灯
    DS0 : LED0 –PB5
    2)KEY0 按键
    3)TFTLCD 模块
    4)microSD Card(使用大卡的情况类似,大家可根据自己设计的硬件匹配选择)
  3. 原理图
    在这里插入图片描述
    前面介绍SD 卡时我们已经介绍了SD 卡对外的接口部分,实际上SD 卡对于我们来说是可
    以灵活变更的部分,实际使用时,业界常用SD 卡卡座用于专门放置SD 卡。
    战舰STM32F103 板载的SD 卡接口使用SDIO 方式来操作SD 卡,和STM32 的连接关系,
    如图49.2.1 所示。SD 卡座在JTAG 插座附近,SD 卡座与开发板的连接在开发板上是直接连接
    在一起的,硬件上不需要任何改动。大家准备好SD 卡就可以开始我们的程序设计和验证了。

程序设计

SD 卡的HAL 库驱动

STM32 的HAL 库为SD 卡操作封装了一些函数,主要存放在stm32f1xx_hal_sd.c/h 下,下
面我们来分析我们主要使用到的几个函数。

  1. HAL_SD_Init 函数
    要使用一个外设首先要对它进行初始化,所以先看sdio 的初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_SD_Init(SD_HandleTypeDef *hsd)
    ⚫ 函数描述:
    根据SD 参数,初始化SDIO 外设以便后续操作SD 卡。
    ⚫ 函数形参:
    形参1 是SD 卡的句柄,结构体类型是SD_HandleTypeDef ,我们不使用
    USE_HAL_SD_REGISTER_CALLBACKS 宏来拓展SD 卡的自定义函数,精简后其定义如下:
/**
* @brief SD操作句柄结构体定义
*/
typedef struct
{
    
    
	SD_TypeDef *Instance; /* SD相关寄存器基地址*/
	SD_InitTypeDef Init; /* SDIO初始化变量*/
	HAL_LockTypeDef Lock; /* 互斥锁,用于解决外设访问冲突*/
	uint8_t *pTxBuffPtr; /* SD发送数据指针*/
	uint32_t TxXferSize; /* SD发送缓存按字节数的大小*/
	uint8_t *pRxBuffPtr; /* SD接收数据指针*/
	uint32_t RxXferSize; /* SD接收缓存按字节数的大小*/
	__IO uint32_t Context; /* HAL库对SD卡的操作阶段*/
	__IO HAL_SD_StateTypeDef State; /* SD卡操作状态*/
	__IO uint32_t ErrorCode; /* SD卡错误代码*/
	DMA_HandleTypeDef *hdmatx; /* SD DMA数据发送指针*/
	DMA_HandleTypeDef *hdmarx; /* SD DMA数据接收指针*/
	HAL_SD_CardInfoTypeDef SdCard; /* SD卡信息的*/
	uint32_t CSD[4]; /* 保存SD卡CSD寄存器信息*/
	uint32_t CID[4]; /* 保存SD卡CID寄存器信息*/
}SD_HandleTypeDef;

⚫ HAL_SD_CardInfoTypeDef 卡信息结构体:
上面的初始化结构体中HAL_SD_CardInfoTypeDef 用于初始化后提取卡信息,包括卡类型、
容量等参数。

/**
* @brief SD 卡信息结构定义
*/
typedef struct
{
    
    
	uint32_t CardType; /* 存储卡类型标记:标准卡、高速卡*/
	uint32_t CardVersion; /* 存储卡版本*/
	uint32_t Class; /* 卡类型*/
	uint32_t RelCardAdd; /* 卡相对地址*/
	uint32_t BlockNbr; /* 卡存储块数*/
	uint32_t BlockSize; /* SD卡每个存储块大小*/
	uint32_t LogBlockNbr; /* 以块表示的卡逻辑容量*/
	uint32_t LogBlockSize; /* 以字节为单位的逻辑块大小*/
}HAL_SD_CardInfoTypeDef;

⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值,有4 个,分别是HAL_OK 表示成功,HAL_ERROR 表
示错误,HAL_BUSY 表示忙碌,HAL_TIMEOUT 超时。后续遇到该结构体也是一样的。只有
返回HAL_OK 才是正常的卡初始化状态,遇到其它状态则需要结合硬件分析一下代码。
2. HAL_SD_ConfigWideBusOperation 函数
SD 卡上电后默认使用1 位数据总线进行数据传输,卡如果允许,可以在初始化完成后重
新设置SD 卡的数据位宽以加快数据传输过程:

HAL_StatusTypeDef HAL_SD_ConfigWideBusOperation(SD_HandleTypeDef *hsd, uint32_t WideMode);

⚫ 函数描述:
这个函数用于设置数据总线格式的数据宽度,用于加快卡的数据访问速度,当然前提是硬
件连接和卡本身能支持这样操作。
⚫ 函数形参:
形参1 是SD 卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO 初始化结
束后才能使用,我们需要通过使用初始化后的SDIO 结构体的句柄访问外设。
形参2 是总线宽度,根据函数的形参检查规则我们可知它实际上只有三个可选值:

#define SDIO_BUS_WIDE_1B ((uint32_t)0x00000000U)
#define SDIO_BUS_WIDE_4B SDIO_CLKCR_WIDBUS_0
#define SDIO_BUS_WIDE_8B SDIO_CLKCR_WIDBUS_1

且对于F103 实际不支持8B 模式,使用时需要特别注意。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到HAL_OK 表示成功。
3. HAL_SD_ReadBlocks 函数
SD 卡初始化后从SD 卡的指定扇区读数据:

HAL_StatusTypeDef HAL_SD_ReadBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout);

这个函数是直接读取,不使用硬件中断。
⚫ 函数描述:
从SD 卡的指定扇区读取一定数量的数据。
⚫ 函数形参:
形参1 是SD 卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO 初始化结
束后才能使用,我们需要通过使用初始化后的SDIO 结构体的句柄访问外设。
形参2 pData 是一个指向8 位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3 BlockAdd 指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD 卡这
样的大存储块也同样是通过位置标识来访问不同的数据。
形参4 NumberOfBlocks 对应的是我们本次要从指定扇区读取的字节数。
形参5 Timeout 表示读的超时时间。HAL 库驱动在达到超时时间前还没读到数据会进行重
试和等待,,达到超时时间后或者本次读取成功才退出本次操作。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到HAL_OK 表示成功。
类似功能的函数还有,我们的例程没有使用DMA 和中断方式,故不使用以下两个接口:

HAL_StatusTypeDef HAL_SD_ReadBlocks_IT(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);
HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);

它们分别使用了中断方式和DMA 方式来实现类似的功能,它们的调用非常相似
4. HAL_SD_WriteBlocks 函数
SD 卡初始化后,在SD 卡的指定扇区写入数据:

HAL_StatusTypeDef HAL_SD_WriteBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout);

⚫ 函数描述:
从SD 卡的指定扇区读取一定数量的数据。
⚫ 函数形参:
形参1 是SD 卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO 初始化结
束后才能使用,我们需要通过使用初始化后的SDIO 结构体的句柄访问外设。
形参2 pData 是一个指向8 位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3 BlockAdd 指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD 卡这
样的大存储块也同样是通过位置标识来访问不同的数据。
形参4 NumberOfBlocks 对应的是我们本次要从指定扇区读取的字节数。
形参5 Timeout 表示写动作的超时时间。HAL 库驱动在达到超时时间前还没读到数据会进
行重试和等待,达到超时时间后或者本次写入成功才退出本次操作。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到HAL_OK 表示成功。
类似于读函数,写函数同样有中断版本,我们的例程没有使用DMA 和中断方式,故不使
用以下两个接口:

HAL_StatusTypeDef HAL_SD_WriteBlocks_IT(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);
HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);

它们分别使用了中断方式和DMA 方式来实现类似的功能,它们的调用非常相似,这里就
不重复介绍了,大家查看对应的函数实现即可。
5. HAL_SD_GetCardInfo 函数
SD 卡初始化后,根据设备句柄读SD 卡的相关状态信息:

HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd, HAL_SD_CardIn-foTypeDef *pCardInfo);

⚫ 函数描述:
从SD 卡的指定扇区读取一定数量的数据。
⚫ 函数形参:
形参1 是SD 卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO 初始化结
束后才能使用,我们需要通过使用初始化后的SDIO 结构体的句柄访问外设。
形参2 pData 是一个指向8 位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3 BlockAdd 指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD 卡这
样的大存储块也同样是通过位置标识来访问不同的数据。
形参4 NumberOfBlocks 对应的是我们本次要从指定扇区读取的字节数。
形参5 Timeout 表示读的超时时间。HAL 库驱动在达到超时时间前还没读到数据会进行重
试和等待,达到超时时间后才退出本次操作。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到HAL_OK 表示成功。
类似的函数还有:

HAL_StatusTypeDef HAL_SD_SendSDStatus(SD_HandleTypeDef *hsd, uint32_t *pSDstatus);
HAL_SD_CardStateTypeDef HAL_SD_GetCardState(SD_HandleTypeDef *hsd);
HAL_StatusTypeDef HAL_SD_GetCardCID(SD_HandleTypeDef *hsd, HAL_SD_CardCIDTypeDef *pCID);
HAL_StatusTypeDef HAL_SD_GetCardCSD(SD_HandleTypeDef *hsd, HAL_SD_CardCSDTypeDef *pCSD);
HAL_StatusTypeDef HAL_SD_GetCardStatus(SD_HandleTypeDef *hsd, HAL_SD_Card-StatusTypeDef *pStatus);
HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd, HAL_SD_CardIn-foTypeDef *pCardInfo);

它们分别使用了中断方式和DMA 方式来实现类似的功能,它们的调用非常相似。
SDIO 驱动SD 卡配置步骤
1)使能SDIO 和相关GPIO 时钟,并设置好GPIO 工作模式
我们通过SDIO 读写SD 卡,所以先需要使能SDIO 以及相关GPIO 口的时钟,并设置好
GPIO 的工作模式。
2)初始化SDIO
HAL 库通过SDIO_Init 完成对SDIO 的初始化,不过我们并不直接调用该函数,而是通过:
HAL_SD_Init→HAL_SD_InitCard→SDIO_Init 的调用关系,来完成对SDIO 的初始化。我们只
需要配置好SDIO 相关工作参数,然后调用HAL_SD_Init 函数即可,详见本例程源码。
3)初始化SD 卡
HAL 库通过HAL_SD_InitCard 函数完成对SD 卡的初始化,如上可知,我们也只需要调用
HAL_SD_Init 函数,即可完成对SD 卡的初始化。
4)实现SD 卡读取&写入函数
在初始化SDIO 和SD 卡完成以后,我们就可以访问SD 卡了,HAL 库提供了两个基本的
SD 卡读写函数:HAL_SD_ReadBlocks 和HAL_SD_WriteBlocks,用于读取和写入SD 卡。我们
对这两个函数再进行一次封装,以便更好的适配文件系统,再封装后,我们使用:sd_read_disk
来读取SD 卡,使用:sd_write_disk 来写入SD 卡,详见本例程源码。

程序流程图

在这里插入图片描述
初始化调试相关的外设后,我们遵循SD 卡协议对SD 卡进行初始化操作,成功后读取并打
印SD 卡的容量等信息。我们可以读写任意扇区以验证我们编写的SD 卡读写函数,这里我们
通过按键触发一次读操作并打印到串口。

程序解析

  1. SDIO 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SDIO 驱动源码包
    括两个文件:sdio_sdcard.c 和sdio_sdcard.h。
    sdio_sdcard.h 我们主要介绍一下GPIO 宏定义,根据我们STM32 的复用功能和我们的硬件
    设计,我们把用到的管脚用宏定义,需要更换其它的引脚时也可以通过修改宏实现快速移植,
    它们列出如下:
/* SDIO的信号线: SD_D0 ~ SD_D3/SD_CLK/SD_CMD 引脚定义
* 如果你使用了其他引脚做SDIO的信号线,修改这里写定义即可适配.
*/
#define SD_D0_GPIO_PORT GPIOC
#define SD_D0_GPIO_PIN GPIO_PIN_8
/* 所在IO口时钟使能*/
#define SD_D0_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define SD_D1_GPIO_PORT GPIOC
#define SD_D1_GPIO_PIN GPIO_PIN_9
/* 所在IO口时钟使能*/
#define SD_D1_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define SD_D2_GPIO_PORT GPIOC
#define SD_D2_GPIO_PIN GPIO_PIN_10
/* 所在IO口时钟使能*/
#define SD_D2_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define SD_D3_GPIO_PORT GPIOC
#define SD_D3_GPIO_PIN GPIO_PIN_11
/* 所在IO口时钟使能*/
#define SD_D3_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define SD_CLK_GPIO_PORT GPIOC
#define SD_CLK_GPIO_PIN GPIO_PIN_12
/* 所在IO口时钟使能*/
#define SD_CLK_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define SD_CMD_GPIO_PORT GPIOD
#define SD_CMD_GPIO_PIN GPIO_PIN_2
/* 所在IO口时钟使能*/
#define SD_CMD_GPIO_CLK_ENABLE() do{
      
       __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)

sdio_sdcard.c 我们主要介绍三个函数:sd_init、sd_read_disk 和sd_write_disk。
1)sd_init 函数
sd_init 的设计就比较简单了,我们只需要填充SDIO 结构体的控制句柄,然后使用HAL
库的初始化驱动即可,根据外设的情况,我们可以设置数据总线宽度为4 位:

/**
 * @brief 初始化SD卡
 * @param 无
 * @retval 返回值:0 初始化正确;其他值,初始化错误
 */
uint8_t sd_init(void) {
    
    
    uint8_t SD_Error;
    /* 初始化时的时钟不能大于400KHZ */
    g_sdcard_handler.Instance = SDIO;
    g_sdcard_handler.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING; /* 上升沿*/
    /* 不使用bypass模式,直接用HCLK进行分频得到SDIO_CK */
    g_sdcard_handler.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
    /* 空闲时不关闭时钟电源*/
    g_sdcard_handler.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
    g_sdcard_handler.Init.BusWide = SDIO_BUS_WIDE_1B; /* 1位数据线*/
    g_sdcard_handler.Init.HardwareFlowControl =
        SDIO_HARDWARE_FLOW_CONTROL_ENABLE; /* 开启硬件流控*/
    /* SD传输时钟频率最大25MHZ */
    g_sdcard_handler.Init.ClockDiv = SDIO_TRANSFER_CLK_DIV;
    SD_Error = HAL_SD_Init( & g_sdcard_handler);
    if (SD_Error != HAL_OK) {
    
    
        return 1;
    }
    /* 使能宽总线模式,即4位总线模式,加快读取速度*/
    SD_Error = HAL_SD_ConfigWideBusOperation( & g_sdcard_handler, SDIO_BUS_WIDE_4B);
    if (SD_Error != HAL_OK) {
    
    
        return 2;
    }
    return 0;
}

2)sd_read_disk 函数
这个函数比较简单,实际上我们使用它来对HAL 库的读函数HAL_SD_ReadBlocks 进行了
二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据
读函数返回值自己作其它处理。为了保护SD 卡的数据操作,我们在进行操作时暂时关闭了中
断以防止数据读过程发生意外。

uint8_t sd_read_disk(uint8_t * pbuf, uint32_t saddr, uint32_t cnt) {
    
    
    uint8_t sta = HAL_OK;
    uint32_t timeout = SD_TIMEOUT;
    long long lsector = saddr;
    __disable_irq(); /* 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!) */
    sta = HAL_SD_ReadBlocks( & g_sdcard_handler, (uint8_t * ) pbuf, lsector,
        cnt, SD_TIMEOUT); /* 多个sector的读操作*/
    /* 等待SD卡读完*/
    while (get_sd_card_state() != SD_TRANSFER_OK) {
    
    
        if (timeout-- == 0) {
    
    
            sta = SD_TRANSFER_BUSY;
        }
    }
    __enable_irq(); /* 开启总中断*/
    return sta;
}

3)sd_write_disk 函数
这个函数比较简单,实际上我们使用它来对HAL 库的读函数HAL_SD_WriteBlocks 进行了
二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据
读函数返回值自己作其它处理。为了保护SD 卡的数据操作,我们在进行操作时暂时关闭了中
断以防止数据写过程发生意外。

uint8_t sd_write_disk(uint8_t * pbuf, uint32_t saddr, uint32_t cnt) {
    
    
    uint8_t sta = HAL_OK;
    uint32_t timeout = SD_TIMEOUT;
    long long lsector = saddr;
    __disable_irq(); /* 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!) */
    sta = HAL_SD_WriteBlocks( & g_sdcard_handler, (uint8_t * ) pbuf, lsector,
        cnt, SD_TIMEOUT); //多个sector的写操作
    /* 等待SD卡写完*/
    while (get_sd_card_state() != SD_TRANSFER_OK) {
    
    
        if (timeout-- == 0) {
    
    
            sta = SD_TRANSFER_BUSY;
        }
    }
    __enable_irq(); /* 开启总中断*/
    return sta;
}
  1. main.c 代码
    在main.c 就比较简单了,按照我们的流程图的思路,为了方便测试,我们编写了
    sd_test_read()\sd_test_write()\show_sdcard_info ()三个函数分别用于读写测试和卡信息打印,也都
    是基于对前面HAL 库的代码进行简单地调用,代码也比较容易看懂,这里就不单独介绍这几个
    函数了,大家查看光盘中的源代码即可。
    最后,我们编写的main 函数如下:
int main(void) {
    
    
    uint8_t key;
    uint32_t sd_size;
    uint8_t t = 0;
    uint8_t * buf;
    HAL_Init(); /* 初始化HAL库*/
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72); /* 延时初始化*/
    usart_init(115200); /* 串口初始化为115200 */
    usmart_dev.init(72); /* 初始化USMART */
    led_init(); /* 初始化LED */
    lcd_init(); /* 初始化LCD */
    key_init(); /* 初始化按键*/
    sram_init(); /* SRAM初始化*/
    my_mem_init(SRAMIN); /* 初始化内部SRAM内存池*/
    my_mem_init(SRAMEX); /* 初始化外部SRAM内存池*/
    lcd_show_string(30, 50, 200, 16, 16, "STM32F103", RED);
    lcd_show_string(30, 70, 200, 16, 16, "SD TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Read Sector 0", RED);
    while (sd_init()) /* 检测不到SD卡*/ {
    
    
        lcd_show_string(30, 130, 200, 16, 16, "SD Card Error!", RED);
        delay_ms(500);
        lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
        delay_ms(500);
        LED0_TOGGLE(); /* 红灯闪烁*/
    }
    /* 打印SD卡相关信息*/
    show_sdcard_info();
    /* 检测SD卡成功*/
    lcd_show_string(30, 130, 200, 16, 16, "SD Card OK ", BLUE);
    lcd_show_string(30, 150, 200, 16, 16, "SD Card Size: MB", BLUE);
    lcd_show_num(30 + 13 * 8, 150, SD_TOTAL_SIZE_MB( & g_sdcard_handler),
        5, 16, BLUE); /* 显示SD卡容量*/
    while (1) {
    
    
        key = key_scan(0);
        if (key == KEY0_PRES) /* KEY0按下了*/ {
    
    
            sd_test_read(0, 1); /* 从0扇区读取1*512字节的内容*/
        }
        t++;
        delay_ms(10);
        if (t == 20) {
    
    
            LED0_TOGGLE(); /* 红灯闪烁*/
            t = 0;
        }
    }
}

main 函数初始化了LED 和LCD 用于显示效果,初始化按键和ADC 用于辅助显示ADC。

下载验证

在代码编译成功之后,我们通过下载代码到ALIENTEK 战舰STM32F103 上,我们测试使
用的是16Gb 标有“SDHC”标志的卡,安装方法如图49.5.1 所示:
在这里插入图片描述
在这里插入图片描述
SD 卡成功初始化后,LCD 显示本程序的一些必要信息,如图49.6.2:
在这里插入图片描述
在进入测试的主循环前,我们如果已经通过USB 连接开发板的串口1 和电脑,可以看到串
口端打印出SD 卡的相关信息(也可以在接好SD 卡后按Reset 复位开发板),我们测试使用的
是16Gb 标有“SDHC”标志的卡,SD 卡成功初始化后的信息,如图49.6.3:
在这里插入图片描述
可见我们用程序读到的SD 卡信息与我们使用的SD 卡一致。伴随DS0 的不停闪烁,提示
程序在运行。此时,我们按下KEY0,调用我们编写的SD 卡测试函数,这里我们只用到了读函
数,写函数的测试大家可以添加代码进行演示。按下后LCD 显示按下,信息如图49.6.4,数量
量较多的情况我们用串口打印,得到的SD 卡扇区0 存储的512 个字节的信息如图49.6.5 所示:
在这里插入图片描述
在这里插入图片描述
对SD 卡的使用的简单介绍就到这里了,另外,利用USMART 测试的部分,我们这里就不
做介绍了,大家可自行验证下。

FATFS 实验

上一章,我们学习了SD 卡的使用,并实现了简单的读写扇区功能。在电脑上我们的资料
常以文件的形式保存,通过文件名我们可以快速对自己的文件数据等进行分类。对于SD 卡这
种容量可以达到非常大的存储介质,按扇区去管理数据已经变得不方便,我们希望单片机也可
以像电脑一样方便地用文件的形式去管理,在需要做数据采集的场合也会更加便利。
本章,我们将介绍FATFS 这个软件工具,利用它在STM32 上实现类似电脑上的文件管理
功能,方便管理SD 卡上的数据,并设计例程在SD 卡上生成文件并对文件实现读写操作。

FATFS 简介

FATFS 是一个完全免费开源的FAT/exFAT 文件系统模块,专门为小型的嵌入式系统而设计。
它完全用标准C 语言(ANSI C C89)编写,所以具有良好的硬件平台独立性,只需做简单的修
改就可以移植到8051、PIC、AVR、ARM、Z80、RX 等系列单片机上。它支持FATl2、FATl6 和
FAT32,支持多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对8 位单片
机和16 位单片机做了优化。
FATFS 的特点有:。
⚫ Windows/dos 系统兼容的FAT/exFAT 文件系统
⚫ 独立于硬件平台,方便跨硬件平台移植
⚫ 代码量少、效率高
⚫ 多种配置选项
支持多卷(物理驱动器或分区,最多10 个卷)
多个ANSI/OEM 代码页包括DBCS
支持长文件名、ANSI/OEM 或Unicode
支持RTOS
支持多种扇区大小
只读、最小化的API 和I/O 缓冲区等
新版的exFAT 文件系统,突破了原来FAT32 对容量管理32Gb 的上限,可支持更
巨大容量的存储器
FATFS 的这些特点,加上免费、开源的原则,使得FATFS 应用非常广泛。FATFS 模块的层次结构
如图50.1.1 所示:
在这里插入图片描述
最顶层是应用层,使用者无需理会FATFS 的内部结构和复杂的FAT 协议,只需要调用F ATFS
模块提供给用户的一系列应用接口函数,如f_open,f_read,f_write 和f_close 等,就可以像在
PC 上读/写文件那样简单。
中间层FATFS 模块,实现了FAT 文件读/写协议。FATFS 模块提供的是ff.c 和ff.h。除非
有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。
需要我们编写移植代码的是FATFS 模块提供的底层接口,它包括存储媒介读/写接口
(diskI/O)和供给文件创建修改时间的实时时钟。
FATFS 的源码及英文详述,大家可以在:http://elm-chan.org/fsw/ff/00index_e.html 这个网站
下载到,教程目前使用的版本为R0.14。本章我们就使用最新版本的FATFS 作为介绍,下载最
新版本的FATFS 软件包,解压后可以得到两个文件夹:documents 和source。documents 里面主
要是对FATFS 的介绍,而source 里面才是我们需要的源码。source 文件夹详情表,如表41.1.1.1
所示:
在这里插入图片描述
FATFS 模块在移植的时候,我们一般只需要修改2 个文件,即ffconf.h 和diskio.c。FATFS
模块的所有配置项都是存放在ffconf.h 里面,我们可以通过配置里面的一些选项,来满足自己
的需求。接下来我们介绍几个重要的配置选项。
1)FF_FS_TINY。这个选项在R0.07 版本中开始出现,之前的版本都是以独立的C 文件
出现(FATFS 和TinyFATFS),有了这个选项之后,两者整合在一起了,使用起来更方
便。我们使用FATFS,所以把这个选项定义为0 即可。
2)FF_FS_READONLY。这个用来配置是不是只读,本章我们需要读写都用,所以这里设
置为0 即可。
3)FF_USE_STRFUNC。这个用来设置是否支持字符串类操作,比如f_putc,f_puts 等,
本章我们需要用到,故设置这里为1。
4)FF_USE_MKFS。用来定时是否使能格式化,本章需要用到,所以设置这里为1。
5)FF_USE_FASTSEEK。这个用来使能快速定位,我们设置为1,使能快速定位。
6)FF_USE_LABEL。这个用来设置是否支持磁盘盘符(磁盘名字)读取与设置。我们设
置为1,使能,就可以通过相关函数读取或者设置磁盘的名字了。
7)FF_CODE_PAGE。这个用于设置语言类型,包括很多选项(见FATFS 官网说明),我
们这里设置为936,即简体中文(GBK 码,同一个文件夹下的ffunicode.c 根据这个宏
选择对应的语言设置)。
8)FF_USE_LFN。该选项用于设置是否支持长文件名(还需要_CODE_PAGE 支持),取
值范围为03。0,表示不支持长文件名,13 是支持长文件名,但是存储地方不一样,
我们选择使用3,通过ff_memalloc 函数来动态分配长文件名的存储区域。
9)FF_VOLUMES。用于设置FATFS 支持的逻辑设备数目,我们设置为2,即支持2 个设
备。
10)FF_MAX_SS。扇区缓冲的最大值,一般设置为512。
11)FF_FS_EXFAT。新版本增加的功能,使用exFAT 文件系统,用于支持超过32Gb 的超
大存储。它们使用的是exFAT 文件系统,使用它时必须要根据设置FF_USE_LFN 这个参数
的值以决定exFATs 系统使用的内存来自堆栈还是静态数组。
其他配置项,我们这里就不一一介绍了,FATFS 的说明文档里面有很详细的介绍,大家自
己阅读http://elm-chan.org/fsw/ff/doc/config.html 即可。下面我们来讲讲FATFS 的移植,FATFS
的移植主要分为3 步:
①数据类型:在integer.h 里面去定义好数据的类型。这里需要了解你用的编译器的数据
类型,并根据编译器定义好数据类型。
②配置:通过ffconf.h 配置FATFS 的相关功能,以满足你的需要。
③函数编写:打开diskio.c,进行底层驱动编写,一般需要编写5 个接口函数,如图41.1.2
所示:
在这里插入图片描述
通过以下三步,我们即可完成对FATFS 的移植。
第一步,我们使用的是MDK5.34 编译器,器数据类型和integer.h 里面定义的一致,所以此
步,我们不需要做任何改动。
第二步,关于ffconf.h 里面的相关配置,我们在前面已经有介绍(之前介绍的11 个配置),
我们将对应配置修改为我们介绍时候的值即可,其他的配置用默认配置。
第三步,因为FATFS 模块完全与磁盘I/O 层分开,因此需要下面的函数来实现底层物理磁
盘的读写与获取当前时间。底层磁盘I/O 模块并不是FATFS 的一部分,并且必须由用户提供。
这些函数一般有5 个,在diskio.c 里面。
首先是disk_initialize 函数,该函数介绍如表41.1.2 所示:
在这里插入图片描述
第二个函数是disk_status 函数,该函数介绍如图41.1.3 所示:
在这里插入图片描述
在这里插入图片描述
第三个函数是disk_read 函数,该函数介绍如表41.1.4 所示:
在这里插入图片描述
第四个函数是disk_write 函数,该函数介绍如表41.1.5 所示:
在这里插入图片描述
第五个函数是disk_ioctl 函数,该函数介绍如表41.1.6 所示:
在这里插入图片描述
在这里插入图片描述
以上五个函数,我们将在软件设计部分一一实现。通过以上3 个步骤,我们就完成了对
FATFS 的移植,就可以在我们的代码里面使用FATFS 了。
FATFS 提供了很多API 函数,这些函数FATFS 的自带介绍文件里面都有详细的介绍(包括
参考代码),我们这里就不多说了。这里需要注意的是,在使用FATFS 的时候,必须先通过f_mount
函数注册一个工作区,才能开始后续API 的使用,关于FATFS 的介绍,我们就介绍到这里。大
家可以通过FATFS 自带的介绍文件进一步了解和熟悉FATFS 的使用。

硬件设计

  1. 例程功能
    本章实验功能简介:开机的时候先初始化SD 卡,初始化成功之后,注册两个磁盘:一个
    给SD 卡用,一个给SPI FLASH 用,之所以把SPI FLASH 也当成磁盘来用,一方面是为了演示
    大容量的SPI Flash 也可以用FATFS 管理,说明FATFS 的灵活性;另一方面可以展示FATFS 方
    式比原来直接按地址管理数据便利性,使板载SPI Flash 的使用更具灵活性。挂载成功后获取
    SD 卡的容量和剩余空间,并显示在LCD 模块上,最后定义USMART 输入指令进行各项测试。
    本实验通过DS0 指示程序运行状态。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)KEY0 按键
    3)TFTLCD 模块
    4)microSD Card(使用大卡的情况类似,大家可根据自己设计的硬件匹配选择)
    5)NOR FLASH
    这几个外设原理图我们在之前的章节已经介绍过了,这里就不重复介绍了,不清楚的话可
    以查看本文之前章节的描述或对光盘资料提供的开发板原理图。

程序设计

FATFS 的驱动为一个硬件独立的组件,因此我们把FATFS 的移植代码放到“Middlewares”
文件夹下。
本章,我们在“SD 卡实验”的基础上进行拓展。在“Middlewares”下新建一个FATF S 的
文件夹,然后将FATFS R0.14 程序包解压到该文件夹下。同时,我们在FATFS 文件夹里面新
建一个exfuns 的文件夹,用于存放我们针对FATFS 做的一些扩展代码(这个我们后面再讲)。
操作结果如图50.3.1 所示:
在这里插入图片描述

程序流程图

在这里插入图片描述
我们初始化stm32 内核和LCD、LED、串口等用于显示信息,然后初始化内存管理单元用
于分配程序需要大内存的部分,我们用SD 卡和NOR FLASH 作为磁盘介质,然后挂载磁盘到
文件系统,成功后即可用文件操作函数访问磁盘上的资源。

程序解析

  1. FATFS 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。
    diskio.c/.h 为我们提供了规定好的底层驱动接口的返回值。这个函数需要使用到我们的硬件
    接口,所以需要把使用到的硬件驱动的头文件包进来。
#include "./MALLOC/malloc.h"
#include "./FATFS/source/diskio.h"
#include "./BSP/SDIO/sdio_sdcard.h"
#include "./BSP/NORFLASH/norflash.h"

按照47.1 的描述,接下来我们来对这几个接口进行补充实现。本章,我们用FATFS 管理了
2 个磁盘:SD 卡和SPI FLASH,我们设置SD_CARD 为0,EX_FLASH 位为1,对应到
disk_read/disk_write 函数里面。SD 卡比较好说,但是SPI FLASH,因为其扇区是4K 字节大小,
我们为了方便设计,强制将其扇区定义为512 字节,这样带来的好处就是设计使用相对简单,坏处就是擦除次数大增,所以不要随便往SPI FLASH 里面写数据,非必要最好别写,如果频繁写的话,很容易将SPI FLASH 写坏。

#define SD_CARD 0 /* SD卡,卷标为0 */
#define EX_FLASH 1 /* 外部qspi flash,卷标为1 */
/**
* 对于25Q128 FLASH芯片, 我们规定前12M 给FATFS使用, 12M以后
* 紧跟字库, 3个字库+ UNIGBK.BIN, 总大小3.09M, 共占用15.09M
* 15.09M以后的存储空间大家可以随便使用.
*/
#define SPI_FLASH_SECTOR_SIZE 512
#define SPI_FLASH_SECTOR_COUNT 12 * 1024 * 2 /* 25Q128,前12M字节给FATFS占用*/
#define SPI_FLASH_BLOCK_SIZE 8 /* 每个BLOCK有8个扇区*/
#define SPI_FLASH_FATFS_BASE 0 /* FATFS 在外部FLASH的起始地址从0开始*/

另外,diskio.c 里面的函数,直接决定了磁盘编号(盘符/卷标)所对应的具体设备,比如,
以上代码中,我们就通过switch 来判断,到底要操作SD 卡,还是SPI FLASH,然后,分别执
行对应设备的相关操作。以此实现磁盘编号和磁盘的关联。
(1) disk_initialize 函数
要使用FAFTS 管理,首先要对它进行初始化,所以先看磁盘的初始化函数,其声明如下:

DSTATUS disk_initialize ( BYTE pdrv)

⚫ 函数描述:
初始化指定编号的磁盘,磁盘所指定的存储区。使用每个磁盘前进行初始化,那在代码中
直接根据编号调用硬件的初始化接口即可,这样也能保证代码的扩展性,硬件的顺序可以
根据自己的喜好定义。
⚫ 函数形参:
形参1 是FATFS 管理的磁盘编号pdrv : 磁盘编号0~9,我们配置FF_VOLUMES 为2 来支
持两个磁盘,因此可选值为0 和1。
代码实现如下:

#define SD_CARD 0 /* SD卡,卷标为0 */ # define EX_FLASH 1 /* 外部qspi flash,卷标为1 */
/**
 * @brief 初始化磁盘
 * @param pdrv : 磁盘编号0~9
 * @retval 无
 */
DSTATUS disk_initialize(
    BYTE pdrv /* Physical drive nmuber to identify the drive */
) {
    
    
    uint8_t res = 0;
    switch (pdrv) {
    
    
        case SD_CARD:
            /* SD卡*/
            res = sd_init(); /* SD卡初始化*/
            break;
        case EX_FLASH:
            /* 外部flash */
            norflash_init(); /* 外部flash初始化*/
            break;
        default:
            res = 1;
    }
    if (res) {
    
    
        return STA_NOINIT;
    } else {
    
    
        return 0; /* 初始化成功*/
    }
}

⚫ 函数返回值:
DSTATUS 枚举类型的值,FATFS 规定了自己的返回值来管理各接口函数的操作结果,方便
后续函数的操作和判断,我们看看它的定义:

/* Status of Disk Functions */
typedef BYTE DSTATUS;
/* Disk Status Bits (DSTATUS) */
#define STA_NOINIT 0x01 /* Drive not initialized */
#define STA_NODISK 0x02 /* No medium in the drive */
#define STA_PROTECT 0x04 /* Write protected */

定义时也写出了各个参数的含义,根据ff.c 中的调用实例可知操作返回0 才是正常的状态,
其它情况发生的话就需要结合硬件进行分析了。
(2) disk_status 函数
便于我们知道当前磁盘驱动器的状态,所以FATFS 也有disk_status 函数提供给我们使用,
其声明如下:

DSTATUS disk_status (BYTE pdrv)

⚫ 函数描述:
返回当前磁盘驱动器的状态。
⚫ 函数形参:
FATFS 管理的磁盘编号pdrv : 磁盘编号0~9。我们配置FF_VOLUMES 为2 来支持两个磁
盘,因此可选值为0 和1。为了简单测试,所以我们这里没有加入硬件状态的判断,测试
代码很简单,这里就不贴出来了。
⚫ 函数返回值:
直接返回RES_OK。
(3) disk_read 函数
disk_read 实现直接从硬件接口读取数据,这个函数接口是给FATFS 的其它读操作接口函数
调用,其声明如下:

DRESULT disk_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count)

⚫ 函数描述:
从磁盘驱动器上读取扇区数据。
⚫ 函数形参:
形参1:是FATFS 管理的磁盘编号pdrv : 磁盘编号0~9,我们配置FF_VOLUMES 为2 来
支持两个磁盘,因此可选值为0 和1。
形参2:buff 指向要保存数据的内存区域指针,为字节类型。
形参3:sector 为实际物理操作时要访问的扇区地址。
形参4:count 为本次要读取的数据量,最长为unsigned int,读到的数量为字节数。
代码实现如下,同样要根据我们定义的设备标号,在swich-case 中添加对应硬件的驱动:

DRESULT disk_read(
    BYTE pdrv, /* Physical drive nmuber to identify the drive */
    BYTE * buff, /* Data buffer to store read data */
    DWORD sector, /* Sector address in LBA */
    UINT count /* Number of sectors to read */ ) {
    
    
    uint8_t res = 0;
    if (!count) return RES_PARERR; /* count不能等于0,否则返回参数错误*/
    switch (pdrv) {
    
    
        case SD_CARD:
            /* SD卡*/
            res = sd_read_disk(buff, sector, count);
            while (res) /* 读出错*/ {
    
    
                if (res != 2) sd_init(); /* 重新初始化SD卡*/
                res = sd_read_disk(buff, sector, count);
                //printf("sd rd error:%d\r\n", res);
            }
            break;
        case EX_FLASH:
            /* 外部flash */
            for (; count > 0; count--) {
    
    
                norflash_read(buff, SPI_FLASH_FATFS_BASE + sector *
                    SPI_FLASH_SECTOR_SIZE, SPI_FLASH_SECTOR_SIZE);
                sector++;
                buff += SPI_FLASH_SECTOR_SIZE;
            }
            res = 0;
            break;
        default:
            res = 1;
    }
    /* 处理返回值,将返回值转成ff.c的返回值*/
    if (res == 0x00) {
    
    
        return RES_OK;
    } else {
    
    
        return RES_ERROR;
    }
}

⚫ 函数返回值:
DRESULT 为枚举类型,diskio.h 中有其定义,根据返回值的含义确认操作结果即可。在这
里也把该结果展示出来,如下所示:

/* Results of Disk Functions */
typedef enum
{
    
    
	RES_OK = 0, /* 0: 操作成功*/
	RES_ERROR, /* 1: 读/写错误*/
	RES_WRPRT, /* 2: 写保护状态*/
	RES_NOTRDY, /* 3: 设备忙*/
	RES_PARERR /* 4: 其它情形*/
} DRESULT;

根据返回值的含义确认操作结果即可。
(4) disk_write 函数
disk_write 实现直接从硬件接口写入数据,这个函数接口给FATFS 的其它写操作接口函数
调用,其声明如下:

DRESULT disk_write ( BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)

⚫ 函数描述:
初始化指定编号的磁盘,磁盘所指定的存储区。
⚫ 函数形参:
形参1:是FATFS 管理的磁盘编号pdrv : 磁盘编号0~9,我们配置FF_VOLUMES 为2 来
支持两个磁盘,因此可选值为0 和1。
形参2:buff 指向要保存数据的内存区域指针,为字节类型。
形参3:sector 为实际物理操作时要访问的扇区地址。
形参4:count 为本次要读取的数据量,最长为unsigned int,读到的数量为字节数。
代码实现如下,同样要根据我们定义的设备标号,在swich-case 中添加对应硬件的驱动:

DRESULT disk_write(
    BYTE pdrv, /* Physical drive nmuber to identify the drive */
    const BYTE * buff, /* Data to be written */
        DWORD sector, /* Sector address in LBA */
        UINT count /* Number of sectors to write */
) {
    
    
    uint8_t res = 0;
    if (!count) return RES_PARERR; /* count不能等于0,否则返回参数错误*/
    switch (pdrv) {
    
    
        case SD_CARD:
            /* SD卡*/
            res = sd_write_disk((uint8_t * ) buff, sector, count);
            while (res) /* 写出错*/ {
    
    
                sd_init(); /* 重新初始化SD卡*/
                res = sd_write_disk((uint8_t * ) buff, sector, count);
                //printf("sd wr error:%d\r\n", res);
            }
            break;
        case EX_FLASH:
            /* 外部flash */
            for (; count > 0; count--) {
    
    
                norflash_write((uint8_t * ) buff,
                    SPI_FLASH_FATFS_BASE + sector * SPI_FLASH_SECTOR_SIZE,
                    SPI_FLASH_SECTOR_SIZE);
                sector++;
                buff += SPI_FLASH_SECTOR_SIZE;
            }
            res = 0;
            break;
        default:
            res = 1;
    }
    /* 处理返回值,将返回值转成ff.c的返回值*/
    if (res == 0x00) {
    
    
        return RES_OK;
    } else {
    
    
        return RES_ERROR;
    }
}

⚫ 函数返回值:
DRESULT 为枚举类型,diskio.h 中有其定义,编写读函数时已经介绍了,注意要把返回值转
成这个枚举类型的参数。
(5) disk_ioctl 函数
disk_ioctl 实现一些控制命令,这个接口为FATFS 提供一些硬件操作信息,其声明如下:

DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff)

⚫ 函数描述:
控制设备指定特性和除了读/写外得杂项功能。
⚫ 函数形参:
形参1:FATFS 管理的磁盘编号pdrv :磁盘编号0~9,我们配置FF_VOLUMES 为2 来支持
两个磁盘,因此可选值为0 和1。
形参2:cmd 是FATFS 定义好的一些宏,用于访问硬盘设备的一些状态。我们实现几个简
单的操作接口,用于获取磁盘容量这些基础信息(diskio.h 中已经定义好了),为方便,我
们先只实现几个标准的应用接口,关于SDIO/MMC 的一些扩展命令我们再根据需要进行
支持。

/* Command code for disk_ioctrl fucntion */
/* Generic command (Used by FatFs) */
#define CTRL_SYNC 0 /* 完成挂起的写入过程(当FF_FS_READONLY == 0) */
#define GET_SECTOR_COUNT 1 /* 获取磁盘扇区数(当FF_USE_MKFS == 1) */
#define GET_SECTOR_SIZE 2 /* 获取磁盘存储空间大小(当FF_MAX_SS != FF_MIN_SS) */
#define GET_BLOCK_SIZE 3 /* 每个扇区块的大小(当FF_USE_MKFS == 1) */

下面是从http://elm-chan.org/fsw/ff/doc/dioctl.html 得到的参数实现效果,我们也可以参考原
有的disk_ioctl 的实现来理解这几个参数。
在这里插入图片描述
形参3:buff 为void 形指针,根据命令的格式和需要,我们把对应的值转成对应的形式传
给它。
参考原有的disk_ioctl 的实现,我们的函数实现如下。

DRESULT disk_ioctl(
    BYTE pdrv, /* Physical drive nmuber (0..) */
    BYTE cmd, /* Control code */
    void * buff /* Buffer to send/receive control data */
) {
    
    
    DRESULT res;
    if (pdrv == SD_CARD) /* SD卡*/ {
    
    
        switch (cmd) {
    
    
            case CTRL_SYNC:
                res = RES_OK;
                break;
            case GET_SECTOR_SIZE:
                * (DWORD * ) buff = 512;
                res = RES_OK;
                break;
            case GET_BLOCK_SIZE:
                * (WORD * ) buff = g_sdcard_handler.SdCard.BlockSize;
                res = RES_OK;
                break;
            case GET_SECTOR_COUNT:
                * (DWORD * ) buff = ((long long) g_sdcard_handler.SdCard.Block Nbr * g_sdcard_handler.SdCard.BlockSize) / 512;
                res = RES_OK;
                break;
            default:
                res = RES_PARERR;
                break;
        }
    } else if (pdrv == EX_FLASH) /* 外部FLASH */ {
    
    
        switch (cmd) {
    
    
            case CTRL_SYNC:
                res = RES_OK;
                break;
            case GET_SECTOR_SIZE:
                * (WORD * ) buff = SPI_FLASH_SECTOR_SIZE;
                res = RES_OK;
                break;
            case GET_BLOCK_SIZE:
                * (WORD * ) buff = SPI_FLASH_BLOCK_SIZE;
                res = RES_OK;
                break;
            case GET_SECTOR_COUNT:
                * (DWORD * ) buff = SPI_FLASH_SECTOR_COUNT;
                res = RES_OK;
                break;
            default:
                res = RES_PARERR;
                break;
        }
    } else {
    
    
        res = RES_ERROR; /* 其他的不支持*/
    }
    return res;
}

⚫ 函数返回值:
DRESULT 为枚举类型,diskio.h 中有其定义,编写读函数时已经介绍了,注意要把返回值转
成这个枚举类型的参数。
以上实现了我们47.1 节提到的5 个函数,ff.c 中需要实现get_fattime (void),同时因为在
ffconf.h 里面设置对长文件名的支持为方法3,所以必须在ffsystem.c 中实现get_fattime、ff_me-malloc 和ff_memfree 这三个函数。这部分比较简单,直接参考我们修改后的ffsystem.c 的源码。
至此,我们已经可以直接使用FATFS 下的ff.c 下的f_mount 的接口挂载磁盘,然后使用类似
标准C 的文件操作函数,就可以实现文件操作。但f_mount 还需要一些文件操作的内存,为了
方便操作,我们在FATFS 文件夹下还新建了一个exfuns 的文件夹,该文件夹用于保存一些FATFS
一些针对FATFS 的扩展代码,如刚才提到的FATFS 相关函数的内存申请方法等。
本章,我们编写了4 个文件,分别是:exfuns.c、exfuns.h、fattester.c 和fattester.h。其中exfuns.c
主要定义了一些全局变量,方便FATFS 的使用,同时实现了磁盘容量获取等函数。而fattester.c
文件则主要是为了测试FATFS 用,因为FATFS 的很多函数无法直接通过USMART 调用,所以
我们在fattester.c 里面对这些函数进行了一次再封装,使得可以通过USMART 调用。
这几个文件的代码,大家可以直接使用本例程源码,我们将exfuns.c/.h 和fattester.c/.h 加入
FATFS 组下的exfuns 文件下,直接使用即可。
(6) exfuns_init 函数
我们在使用文件操作前,需要用f_mount 函数挂载磁盘,我们在挂载SD 卡前需要一些文
件系统的内存,为了方便管理,我们定义一个全局的fs[FF_VOLUMES]指针,定成数组是我们
要管理多个磁盘,而f_mount 也需要一个FATFS 类型的指针,定义如下:

/* 逻辑磁盘工作区(在调用任何FATFS相关函数之前,必须先给fs申请内存) */
FATFS *fs[FF_VOLUMES];

接下来只要用内存管理部分的知识来实现对fs 指针的内存申请即可。

/**
 * @brief 为exfuns申请内存
 * @param 无
 * @retval 0, 成功; 1, 失败.
 */
uint8_t exfuns_init(void) {
    
    
    uint8_t i;
    uint8_t res = 0;
    for (i = 0; i < FF_VOLUMES; i++) {
    
    
        fs[i] = (FATFS * ) mymalloc(SRAMIN, sizeof(FATFS)); /* 为磁盘i工作区申请内存*/
        if (!fs[i]) break;
    }#
    if USE_FATTESTER == 1 /* 如果使能了文件系统测试*/
    res = mf_init(); /* 初始化文件系统测试(申请内存) */ #
    endif
    if (i == FF_VOLUMES && res == 0)
        return 0; /* 申请有一个失败,即失败. */
    else
        return 1;
}

其它的函数我们暂时没有使用到,这里先不介绍了,大家使用时查阅源码即可。
2. main.c 代码
在main.c 就比较简单了,按照我们的流程图的思路编写即可,在成功初始化后,我们通过
LCD 显示文件操作的结果。
最后,我们编写的main 函数如下:

int main(void) {
    
    
    uint32_t total, free;
    uint8_t t = 0;
    uint8_t res = 0;
    HAL_Init(); /* 初始化HAL库*/
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72); /* 延时初始化*/
    usart_init(115200); /* 串口初始化为115200 */
    usmart_dev.init(72); /* 初始化USMART */
    led_init(); /* 初始化LED */
    lcd_init(); /* 初始化LCD */
    key_init(); /* 初始化按键*/
    sram_init(); /* SRAM初始化*/
    my_mem_init(SRAMIN); /* 初始化内部SRAM内存池*/
    my_mem_init(SRAMEX); /* 初始化外部SRAM内存池*/
    lcd_show_string(30, 50, 200, 16, 16, "STM32F103", RED);
    lcd_show_string(30, 70, 200, 16, 16, "FATFS TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "2020/4/28", RED);
    lcd_show_string(30, 130, 200, 16, 16, "Use USMART for test", RED);
    while (sd_init()) /* 检测不到SD卡*/ {
    
    
        lcd_show_string(30, 150, 200, 16, 16, "SD Card Error!", RED);
        delay_ms(500);
        lcd_show_string(30, 150, 200, 16, 16, "Please Check! ", RED);
        delay_ms(500);
        LED0_TOGGLE(); /* LED0闪烁*/
    }
    exfuns_init(); /* 为fatfs相关变量申请内存*/
    f_mount(fs[0], "0:", 1); /* 挂载SD卡*/
    res = f_mount(fs[1], "1:", 1); /* 挂载FLASH. */
    if (res == 0X0D) /* FLASH磁盘,FAT文件系统错误,重新格式化FLASH */ {
    
    
        /* 格式化FLASH */
        lcd_show_string(30, 150, 200, 16, 16, "Flash Disk Formatting...", RED);
        /* 格式化FLASH,1:,盘符;0,使用默认格式化参数*/
        res = f_mkfs("1:", 0, 0, FF_MAX_SS);
        if (res == 0) {
    
    
            /* 设置Flash磁盘的名字为:ALIENTEK */
            f_setlabel((const TCHAR * )
                "1:ALIENTEK");
            lcd_show_string(30, 150, 200, 16, 16, "Flash Disk Format Finish",
                RED); /* 格式化完成*/
        } else
            lcd_show_string(30, 150, 200, 16, 16, "Flash Disk Format Error ",
                RED); /* 格式化失败*/
        delay_ms(1000);
    }
    lcd_fill(30, 150, 240, 150 + 16, WHITE); /* 清除显示*/
    while (exfuns_get_free("0", & total, & free)) /* 得到SD卡的总容量和剩余容量*/ {
    
    
        lcd_show_string(30, 150, 200, 16, 16, "SD Card Fatfs Error!", RED);
        delay_ms(200);
        lcd_fill(30, 150, 240, 150 + 16, WHITE); /* 清除显示*/
        delay_ms(200);
        LED0_TOGGLE(); /* LED0闪烁*/
    }
    lcd_show_string(30, 150, 200, 16, 16, "FATFS OK!", BLUE);
    lcd_show_string(30, 170, 200, 16, 16, "SD Total Size: MB", BLUE);
    lcd_show_string(30, 190, 200, 16, 16, "SD Free Size: MB", BLUE);
    lcd_show_num(30 + 8 * 14, 170, total >> 10, 5, 16, BLUE); /*显示SD卡总容量MB*/
    lcd_show_num(30 + 8 * 14, 190, free >> 10, 5, 16, BLUE); /*显示SD卡剩余容量*/
    while (1) {
    
    
        t++;
        delay_ms(200);
        LED0_TOGGLE(); /* LED0闪烁*/
    }
}

下载验证

在代码编译成功之后,我们通过下载代码到正点原子战舰STM32F103 上,我们测试使用
的是16Gb 标有“SDHC”标志的micorSD 卡,我们通过下载代码到开发板上,可以看到LCD
显示如图50.4.1 所示:
在这里插入图片描述
打开串口调试助手,我们就可以串口调用前面添加的各种FATFS 测试函数了,比如我们输
入mf_scan_files(“0:”),即可扫描SD 卡根目录的所有文件,如图50.4.2 所示:
在这里插入图片描述
其他函数的测试,用类似的办法即可实现。注意这里0 代表SD 卡,1 代表SPI FLASH。
另外,提醒大家,mf_unlink 函数,在删除文件夹的时候,必须保证文件夹是空的,才可以正常
删除,否则不能删除。

猜你喜欢

转载自blog.csdn.net/zhuguanlin121/article/details/131146261