第五十二章 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 串口调试助手界面