《STM32H7R7开发指南 V1.1 》第五十二章 SD NAND实验

第五十二章 SD NAND实验


       在单片机系统中,大部分人喜欢用SD卡来存储数据,因为它小巧便携,简单易用,但是用SD卡存在不能机贴、容易脱落、占用PCB面积大的问题。本章介绍的SD NAND可以很好的解决SD卡的缺点,它的使用和SD卡基本一致,被称为是贴片式SD卡。SD NAND具有使用寿命长、性能稳定等优势。

       STM32H7R7开发板板载了SD NAND芯片,板载的SD NAND根据不同版本的核心板分为容量为4Gb的MKDV4GCL和容量为16Gb的MKDV16GIL,这两款不同型号的SD NAND除了容量大小不同之外,其他参数和特性基本一致,均使用SPI接口驱动、4位模式,最高通信速度可达50MHz,对于一般应用是足够的。在本章,我们将以MKDV4GCL为例,向大家介绍如何在开发板上实现SD NAND的读取。

       本章分为如下几个部分:

       52.1 SD NAND简介

       52.2 SPI寄存器

       52.3 硬件设计

       52.4 程序设计

       52.5 下载验证


        52.1 SD NAND简介


       52.1.1 SD NAND特点

       SD NAND是一个采用LGA封装形式设计的嵌入式存储芯片,跟SD卡的使用一样,SD NAND完全符合SD2.0接口,允许大多数通用CPU使用。SD NAND内置SLC晶圆,读写寿命可达5-10万次,为嵌入式而生。 SD NAND的特点包括:

       1,时钟频率最高支持50MHz

       2,兼容SD协议

       3,支持SPI模式和SD模式

       4,内置HW ECC引擎和高可靠的NAND管理机制

       5,写入速度可达6级

       6,先进的热管理功能,以最大限度地提高性能和数据保护

       7,坏块管理,智能垃圾收集和支持交错,缓和和多平面编程

       8,读取干扰管理和动态数据刷新

       9,程序/擦除:60,000周期

       SD NAND 的内部结构图,如图52.1.1.1所示:


图52.1.1.1 SD NAND内部结构图


       可以看出SD NAND内部是由一个控制器和一个flash组成,支持SD模式和SPI模式,内部控制器包含ECC、磨损均衡、电源管理、时钟控制等功能,不需要额外的驱动去做处理。

       下面来看看SD NAND的引脚分配,如图52.1.1.2所示:


图52.1.1.2 SD NAND的引脚分配图


       上述表格的“脚位数”,对应于实卡上的“金手指”数,不同类型的卡的触点数量不同,访问的速度也不相同。SD NAND允许了不同的接口来访问它的内部存储单元。最常见的是SD模式和SPI模式,根据这两种接口模式,我们也列出SD卡引脚对应于这两种不同的电路模式的引脚功能定义,如表52.1.1.1 所示。


表52.1.1.1 SD NAND引脚编号(注:S:电源 I:输入 O:推挽输出 PP:推挽)


       本例程我们选择SPI模式来驱动SD NAND。但是要注意的是SPI接口只是定义了物理传输层,并没有定义完整的数据传输协议,因此在读写数据时还是需要遵循SD接口协议。

       SD NAND有自己的寄存器,但它不能直接进行读写操作,需要通过命令来控制,SDIO协议定义了一些命令用于实现某一特定功能,SD NAND根据收到的命令要求对内部寄存器进行修改。表52.1.1.2中描述的SD NAND的6个寄存器和SD状态信息,是我们和SD NAND进行数据通讯的主要通道(注意:此卡不支持DSR),如下: 


表52.1.1.2 SD  NAND寄存器信息


       关于SD NAND的更多信息和硬件设计规范可以参考SD卡协议《Physical Layer Simplified Specification Version 2.00》的相关章节(注:因为STM32的SDIO匹配的是SD协议2.0版本,后续版本也兼容此旧协议版本,故本章仍以2.0版本为介绍对象)。


       52.1.2 命令和响应

       命令在CMD线上串行传输。命令是启动从主机到设备的操作的令牌。

       响应也在CMD线上串行传输,是应答先前接收到的命令的令牌。

       这里SD NAND的命令和响应和SD卡的一样,详情请见SD卡章节的53.1.2小节。


       52.1.3 SD NAND初始化流程


       SPI模式下的SD  NAND初始化

       《SD卡2.0协议.pdf》中提供了SD卡的SPI初始化时序,我们可以按它建议的流程进行SD NAND的初始化,如图52.1.3.1所示。


图52.1.3.1 SD NAND的SPI初始化流程(SPI Mode Initialization Flow)


       要使用SPI模式驱动SD NAND,先得让SD NAND进入SPI模式。方法如下:在SD NAND收到复位命令(CMD0)时,CS为有效电平(低电平)则SPI模式被启用。不过在发送CMD0之前,要发送>74个时钟,这是因为SD卡内部有个供电电压上升时间,大概为64个CLK,剩下的10个CLK用于SD卡同步,之后才能开始CMD0的操作,在卡初始化的时候,CLK时钟最大不能超过400Khz!

       接着我们看看SD NAND的初始化,由于SD NAND是先发送数据高位的,初始化过程如下:

       1、初始化与SD NAND连接的硬件条件(MCU的SPI配置,IO口配置);

       2、拉低片选信号,上电延时(>74个CLK);

       3、复位卡(CMD0),进入IDLE状态;

       4、发送CMD8,检查是否支持2.0协议;

       5、根据不同协议检查SD卡(命令包括:CMD55、ACMD41、CMD58和CMD1等);

       6、取消片选,发多8个CLK,结束初始化

       这样我们就完成了对SD NAND的初始化,注意末尾发送的8个CLK是提供SD卡额外的时钟,完成某些操作。在完成了初始化之后,就可以开始读写数据了。

       SD NAND单扇区读取数据,这里通过CMD17来实现,具体过程如下:

       1、发送CMD17;

       2、接收卡响应R1;

       3、接收数据起始令牌0XFE;

       4、接收数据;

       5、接收2个字节的CRC,如果不使用CRC,这两个字节在读取后可以丢掉。

       6、禁止片选之后,发多8个CLK;

       以上就是一个典型的读取SD NAND数据过程,SD NAND的写与读数据差不多,写数据通过CMD24来实现,具体过程如下:

       1、发送CMD24;

       2、接收卡响应R1;

       3、发送写数据起始令牌0XFE;

       4、发送数据;

       5、发送2字节的伪CRC;

       6、禁止片选之后,发多8个CLK;

       以上就是一个典型的写SD NAND过程。关于SD NAND的介绍,我们就介绍到这里,更详细的介绍请参考光盘资料→7,硬件资料→SD卡资料→SD卡V2.0协议。


        52.2 SPI寄存器

       由于前面第四十七章无线通信章节已经介绍过SPI接口了,这里就不赘述了,有需要可以查看47.1.4小节的内容。接下来给大家介绍一下本实验用到的SPI寄存器。


       l SPI配置寄存器1 (SPI_CFG1)

       SPI配置寄存器1,该寄存器定义如图52.2.1.1所示:


图52.2.1.1 SPI_CFG1配置寄存器1


       该寄存器我们关注位MBR[2:0],配置为111,即使用256分频,速度最低;


       l SPI配置寄存器2 (SPI_CFG2)

       SPI配置寄存器2,该寄存器定义如图52.2.1.2所示:


图52.2.1.2 SPI配置寄存器2


       下面讲解一下本实验配置的位,我们配置位CPHA为1,数据采样从第二个时钟边沿开始;位CPOL置1,在空闲状态是2,SCK保持高电平;位MASTER置1,配置为主机模式;位LSBFRST置0,MSB先传输,


        52.3 硬件设计


       1. 例程功能

       本章实验功能简介:开机的时候先初始化SD NAND,如果SD NAND初始化完成,则提示LCD初始化成功。按下KEY0,读取SD NAND第0个块的数据,然后通过串口发送到电脑。如果没初始化通过,则在LCD上提示初始化失败。同样用LED0来指示程序正在运行。


       2. 硬件资源


       1)LED灯

              LED0:LED0 – PD14


       2)独立按键

              KEY0 – PE9


       3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(包括MCU屏和RGB屏,都支持)


       4)串口1 (PB14/PB15连接在板载USB转串口芯片CH340上面)


       5)SD NAND(SPI5接口,SPI5_SCK – PF15、SPI5_MOSI – PF14、SPI5_MISO – PF12)


       3. 原理图

       前面介绍SD卡时我们已经介绍了SD卡对外的接口部分,实际上SD卡对于我们来说是可以灵活变更的部分,实际使用时,业界常用SD卡卡座用于专门放置SD卡。

       下面我们介绍一下板载的SD卡接口和STM32的连接关系,如图53.4.1所示:


图52.4.1 SD卡接口与STM32连接原理图


       microSD卡座在开发板正面,卡座和STM32开发板上是直接连接在一起的,硬件上不需要任何改动。 


        52.4 程序设计 


       52.4.1 程序解析


       1. SD NAND驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SD NAND驱动源码包括两个文件:spi_sdnand.c和spi_sdnand.h。 

       首先是spi_sdnand.h文件,根据我们STM32的复用功能和我们的硬件设计,我们把用到的管脚用宏定义,需要更换其它的引脚时也可以通过修改宏实现快速移植。这里与SD NAND相关的引脚共有四个,SPI_SCK、SPI_MISO、SPI_MOSI和SD_CS,它们列出如下:

#define SDNAND_SPI                        SPI5
#define SDNAND_SPI_CLK_ENABLE()           do { __HAL_RCC_SPI5_CLK_ENABLE(); } while (0)
#define SDNAND_SPI_CLK_DISABLE()          do { __HAL_RCC_SPI5_CLK_DISABLE(); } while (0)
#define SDNAND_SPI_SCK_GPIO_PORT          GPIOF
#define SDNAND_SPI_SCK_GPIO_PIN           GPIO_PIN_15
#define SDNAND_SPI_SCK_GPIO_AF            GPIO_AF5_SPI5
#define SDNAND_SPI_SCK_GPIO_CLK_ENABLE()  do { __HAL_RCC_GPIOF_CLK_ENABLE(); } while (0)
#define SDNAND_SPI_MOSI_GPIO_PORT         GPIOF
#define SDNAND_SPI_MOSI_GPIO_PIN          GPIO_PIN_14
#define SDNAND_SPI_MOSI_GPIO_AF           GPIO_AF5_SPI5
#define SDNAND_SPI_MOSI_GPIO_CLK_ENABLE() do { __HAL_RCC_GPIOF_CLK_ENABLE(); } while (0)
#define SDNAND_SPI_MISO_GPIO_PORT         GPIOF
#define SDNAND_SPI_MISO_GPIO_PIN          GPIO_PIN_12
#define SDNAND_SPI_MISO_GPIO_AF           GPIO_AF5_SPI5
#define SDNAND_SPI_MISO_GPIO_CLK_ENABLE() do { __HAL_RCC_GPIOF_CLK_ENABLE(); } while (0)
#define SDNAND_CS_GPIO_PORT               GPIOF
#define SDNAND_CS_GPIO_PIN                GPIO_PIN_13
#define SDNAND_CS_GPIO_CLK_ENABLE()       do { __HAL_RCC_GPIOF_CLK_ENABLE(); } while (0)

       接下来我们看一下sdnand_init.c代码中的初始化函数,代码如下:

/**
 * @brief   初始化SD NAND
 * @param   无
 * @retval  初始化结果
 * @arg     0: 初始化成功
 * @arg     1: 初始化失败
 */
uint8_t sdnand_init(void)
{
    uint8_t index;
    uint16_t t;
    uint8_t resp7[4];
    
    /* 初始化片选信号 */
    sdnand_chip_select_init();
    
    /* SD NAND初始化前低速模式初始化SPI */
    sdnand_spi_init(SDNAND_SPI_SPEED_LOW);
    
    /* 发送至少74个时钟以上电SD */
    for (index = 0; index < ((74 + 7) / 8); index++)
    {
       sdnand_spi_read_write_byte(0xFF);
    }
    
    /* 进入IDLE状态 */
    t = 20;
    while (t--)
    {
        if (sdnand_send_cmd(SDNAND_CMD0, 0UL) == 0x01)
        {
            break;
        }
    }
    if (t == 0)
    {
        sdnand_chip_select_disable();
        return 1;
    }
    
    /* 发送容量支持信息并激活SD NAND初始化程序 */
    t = 20;
    while (t--)
    {
        if (sdnand_send_cmd(SDNAND_CMD55, 0UL) <= 0x01)
        {
            if (sdnand_send_cmd(SDNAND_ACMD41, 0UL) == 0x00)
            {
                break;
            }
        }
    }
    if (t == 0)
    {
        sdnand_chip_select_disable();
        return 1;
    }
    
    /* SD NAND初始化后高速模式初始化SPI */
    sdnand_spi_init(SDNAND_SPI_SPEED_HIGH);
    
    /* 设置块大小 */
    if (sdnand_send_cmd(SDNAND_CMD16, SDNAND_BLOCK_SIZE) != 0x00)
    {
        sdnand_chip_select_disable();
        return 1;
    }
    if (sdnand_recv_data(sdnand_info.csd, sizeof(sdnand_info.csd)) != 0)
    {
        sdnand_chip_select_disable();
        return 1;
    }
    
    /* 读取CSD */
    if (sdnand_send_cmd(SDNAND_CMD9, 0UL) != 0x00)
    {
        sdnand_chip_select_disable();
        return 1;
    }
    if (sdnand_recv_data(sdnand_info.csd, sizeof(sdnand_info.csd)) != 0)
    {
        sdnand_chip_select_disable();
        return 1;
    }
    sdnand_chip_select_disable();
    
    /* 计算SD NAND参数信息 */
    sdnand_info.block_size = (1UL << (sdnand_info.csd[5] & 0x0FUL));
sdnand_info.block_num = (((((((uint32_t)sdnand_info.csd[6] & 0x03UL) << 8)
| sdnand_info.csd[7]) << 2UL) | (((uint32_t)sdnand_info.csd[8] & 0xC0UL) 
>> 6UL)) + 1UL) * (1UL << ((((sdnand_info.csd[9] & 0x03UL) << 1UL) |
((sdnand_info.csd[10] & 0x80UL) >> 7UL)) + 2));
    sdnand_info.chip_size = sdnand_info.block_num * sdnand_info.block_size;
    sdnand_info.logic_block_size = SDNAND_BLOCK_SIZE;
sdnand_info.logic_block_num = sdnand_info.chip_size / 
sdnand_info.logic_block_size;
    
    return 0;
}

       该函数先初始化与SD NAND相关的IO口以及SPI初始化,然后发送CMD0,进入IDLE状态,并设置SD NAND为SPI模式通信,然后判断SD NAND类型,完成SD NAND的初始化,注意该函数调用的sdnand_spi_init等函数,实际是对SPI5的相关函数进行了一层封装,方便移植。

       下面介绍一下SD NAND读取函数,也是按照前面分析的读取数据的过程进行操作,主要是通过CMD17进行实现,用于从SD NAND读取扇区的数据,代码如下:

/**
 * @brief   读SD NAND指定数量块的数据
 * @param   data: 数据缓冲区指针
 * @param   block_index: 起始块
 * @param   block_num: 块数量
 * @retval  读取结果
 * @arg     0: 读取成功
 * @arg     1: 读取失败
 */
uint8_t sdnand_read_disk(uint8_t *data, uint32_t block_index, uint32_t block_num)
{
    uint32_t address;
    
    if (block_num == 0)
    {
        return 0;
    }
    
    if (data == NULL)
    {
        return 1;
    }
    
    if ((block_index + block_num) > sdnand_info.logic_block_num)
    {
        return 1;
    }
    
    /* 计算块地址 */
    address = block_index * sdnand_info.logic_block_size;
    
    /* 单块读 */
    if (block_num == 1)
    {
        if (sdnand_send_cmd(SDNAND_CMD17, address) != 0x00)
        {
            sdnand_chip_select_disable();
            return 1;
        }
        
        if (sdnand_recv_data(data, sdnand_info.logic_block_size) != 0)
        {
            sdnand_chip_select_disable();
            return 1;
        }
    }
    /* 多块读 */
    else
    {
        if (sdnand_send_cmd(SDNAND_CMD18, address) != 0x00)
        {
            sdnand_chip_select_disable();
            return 1;
        }
        
        while (block_num--)
        {
            if (sdnand_recv_data(data, sdnand_info.logic_block_size) != 0)
            {
                sdnand_chip_select_disable();
                return 1;
            }
            data += sdnand_info.logic_block_size;
        }
        
        sdnand_send_cmd(SDNAND_CMD12, 0);
    }
    
    /* 取消片选 */
    sdnand_chip_select_disable();
    
    return 0;
}

        此函数根据要读取扇区的多少,发送CMD17/CMD18命令,然后读取一个/多个扇区的数据。这个扇区读取的过程在前面也有提及到了,实现过程也不是很难理解,所以这里就不做展开了。

       SD NAND写函数跟读函数差异不大,写函数是通过CMD24来实现,代码如下:

/**
 * @brief   写SD NAND指定数量块的数据
 * @param   data: 数据缓冲区指针
 * @param   block_index: 起始块
 * @param   block_num: 块数量
 * @retval  写入结果
 * @arg     0: 写入成功
 * @arg     1: 写入失败
 */
uint8_t sdnand_write_disk(uint8_t *data, uint32_t block_index, uint32_t block_num)
{
    uint32_t address;
    uint8_t t;
    
    if (block_num == 0)
    {
        return 0;
    }
    
    if (data == NULL)
    {
        return 1;
    }
    
    if ((block_index + block_num) > sdnand_info.logic_block_num)
    {
        return 1;
    }
    
    /* 计算块地址 */
    address = block_index * sdnand_info.logic_block_size;
    
    /* 单块写 */
    if (block_num == 1)
    {
        if (sdnand_send_cmd(SDNAND_CMD24, address) != 0x00)
        {
            sdnand_chip_select_disable();
            return 1;
        }
        
        if (sdnand_send_block_data(0xFE, data) != 0)
        {
            sdnand_chip_select_disable();
            return 1;
        }
    }
    /* 多块写 */
    else
    {
        t = 20;
        while (t--)
        {
            if (sdnand_send_cmd(SDNAND_CMD55, 0UL) <= 0x01)
            {
                if (sdnand_send_cmd(SDNAND_ACMD23, block_num) == 0x00)
                {
                    break;
                }
            }
        }
        if (t == 0)
        {
            sdnand_chip_select_disable();
            return 1;
        }
        
        if (sdnand_send_cmd(SDNAND_CMD25, address) != 0x00)
        {
            sdnand_chip_select_disable();
            return 1;
        }
        
        while (block_num--)
        {
            if (sdnand_send_block_data(0xFC, data) != 0)
            {
                sdnand_chip_select_disable();
                return 1;
            }
            data += sdnand_info.logic_block_size;
        }
        if (sdnand_send_block_data(0xFD, NULL) != 0)
        {
            sdnand_chip_select_disable();
            return 1;
        }
        
        sdnand_send_cmd(SDNAND_CMD12, 0);
    }
    
    /* 取消片选 */
    sdnand_chip_select_disable();
    
    return 0;
}

       此函数根据要写扇区的多少,发送CMD24/CMD23命令,然后写入一个/多个扇区的数据。这个扇区写入的过程在前面也提及到了。


       2. main.c代码

       最后,我们在main.c编写的程序如下:

int main(void)
{
    uint8_t t = 0;
    uint8_t key;
    
    sys_mpu_config();                   /* 配置MPU */
    sys_cache_enable();                 /* 使能Cache */
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(300, 6, 2);    /* 配置时钟,600MHz */
    delay_init(600);                    /* 初始化延时 */
    usart_init(115200);                 /* 初始化串口 */
    led_init();                         /* 初始化LED */
    key_init();                         /* 初始化按键 */
    hyperram_init();                    /* 初始化HyperRAM */
    lcd_init();                         /* 初始化LCD */
    my_mem_init(SRAMIN);                /* 初始化AXI-SRAM1~4内存池 */
    my_mem_init(SRAMEX);                /* 初始化XSPI2 HyperRAM内存池 */
    my_mem_init(SRAM12);                /* 初始化AHB-SRAM1~2内存池 */
    my_mem_init(SRAMDTCM);              /* 初始化DTCM内存池 */
    my_mem_init(SRAMITCM);              /* 初始化ITCM内存池 */
    sdnand_init();                      /* SD NAND初始化 */
    
    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "SD NAND TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    
    /* 显示SD NAND信息 */
    show_sdnand_info();
    
    while (1)
    {
        key = key_scan(0);
        if (key == KEY0_PRES)
        {
            /* SD NAND读测试 */
            sdnand_read_test();
        }
        
        if (++t == 20)
        {
            t = 0;
            LED0_TOGGLE();
        }
        
        delay_ms(10);
    }
}

       main函数先初始化相关外设和SD NAND,初始化成功后,调用show_sdnand_info函数获取SD NAND的信息。然后按下按键KEY0,则通过sdnand_read_test进行SD NAND读测试,用于读取SD NAND指定扇区的数据,并将读到的数据通过串口1输出。


        52.5   下载验证

       将程序下载到开发板后, SD NAND成功初始化后,LCD显示本程序的一些必要信息,如图52.5.1:


图52.5.1 程序运行效果图


       伴随LED0的不停闪烁,提示程序在运行。此时,我们按下KEY0,调用我们编写的SD测试函数,这里我们只用到了读函数,写函数的测试大家可以添加代码进行演示。按下后LCD显示按下,信息如图52.5.2,数量较多的情况我们用串口打印,得到的SD NAND扇区0的信息如图52.5.3所示:


图52.5.2 按下KEY0的开发板界面


图52.5.3 串口调试助手界面


请使用浏览器的分享功能分享到微信等