tag 标签: 单片机 nand flash

相关帖子
相关博文
  • 热度 3
    2024-6-14 16:28
    253 次阅读|
    0 个评论
    文章目录   一、简介   二、速度测试   最近比较忙,也一直没空发什么文章,这算是新年第一篇吧,正好最近收到了一个雷龙的flash芯片,先拿来玩一下吧。   有兴趣的小伙伴可以去雷龙官网找小姐姐领取一个免费试用。 一、简介   大概样子就是上面这样,使用LGA-8封装,实际上驱动也是通用SD卡的驱动,相比与SD卡可以直接贴片到嵌入式设备中,并且体积更小,数据存储和SD卡存储一样。   我使用的型号是CSNP1GCR01-AOW,   不用写驱动程序自带坏块管理的NAND Flash(贴片式TF卡),   尺寸小巧,简单易用,兼容性强,稳定可靠,   固件可定制,LGA-8封装,标准SDIO接口,   兼容SPI/SD接口,兼容各大MCU平台,   可替代普通TF卡/SD卡,   尺寸6x8mm毫米,   内置SLC晶圆擦写寿命10万次,   通过1万次随机掉电测试耐高低温,   支持工业级温度-40°~+85°,   机贴手贴都非常方便,   速度级别Class10(读取速度23.5MB/S写入速度12.3MB/S)   标准的SD 2.0协议使得用户可以直接移植标准驱动代码,省去了驱动代码编程环节。   支持TF卡启动的SOC都可以用SD NAND   ,提供STM32参考例程及原厂技术支持,   主流容量:128MB/512MB/4GB/8GB,   比TF卡稳定,比eMMC便宜,   样品免费试用。   实际应用场景   新一代SD NAND主要应用领域   •5G   •机器人   •智能音箱   •智能面板(HMI)   •移动支付   •智能眼镜(AR)   •智能家居   •医疗设备   •轨道交通   •人脸识别   •3D打印机 二、速度测试   手里正好还有一张内存卡,那么就做下对比测试:   先理解下下面这四种测试的含义:   SEQ1M|Q8T1表示顺序读写,位深1024K,1线程8队列的测试速度   SEQ1M|Q1T1表示顺序读写,位深1024K,1线程1队列测试速度   RND4K|Q32T16表示随机读写,位深10244K,16线程32队列的测试速度   RND4K|Q1T1表示随机读写,位深10244K,一线程一队列的测试速度   那么由于CSNP1GCR01-AOW是512M的,那么就统一使用256M的随机读写来测试   CSNP1GCR01-AOW读写速度: ------------------------------------------------------------------------------ CrystalDiskMark 8.0.4 x64 (C) 2007-2021 hiyohiyo Crystal Dew World: https://crystalmark.info/ ------------------------------------------------------------------------------ * MB/s = 1,000,000 bytes/s * KB = 1000 bytes, KiB = 1024 bytes SEQ 1MiB (Q= 8, T= 1): 18.242 MB/s SEQ 1MiB (Q= 1, T= 1): 18.409 MB/s RND 4KiB (Q= 32, T= 1): 4.807 MB/s RND 4KiB (Q= 1, T= 1): 4.215 MB/s SEQ 1MiB (Q= 8, T= 1): 7.326 MB/s SEQ 1MiB (Q= 1, T= 1): 7.549 MB/s RND 4KiB (Q= 32, T= 1): 2.453 MB/s RND 4KiB (Q= 1, T= 1): 2.029 MB/s Profile: Default Test: 256 MiB (x5) Mode: Time: Measure 5 sec / Interval 5 sec Date: 2024/01/15 11:35:30 OS: Windows 10 Professional (x64)   内存卡读写速度: ------------------------------------------------------------------------------ CrystalDiskMark 8.0.4 x64 (C) 2007-2021 hiyohiyo Crystal Dew World: https://crystalmark.info/ ------------------------------------------------------------------------------ * MB/s = 1,000,000 bytes/s * KB = 1000 bytes, KiB = 1024 bytes SEQ 1MiB (Q= 8, T= 1): 17.579 MB/s SEQ 1MiB (Q= 1, T= 1): 18.236 MB/s RND 4KiB (Q= 32, T= 1): 5.105 MB/s RND 4KiB (Q= 1, T= 1): 4.622 MB/s SEQ 1MiB (Q= 8, T= 1): 1.676 MB/s SEQ 1MiB (Q= 1, T= 1): 7.962 MB/s RND 4KiB (Q= 32, T= 1): 0.018 MB/s RND 4KiB (Q= 1, T= 1): 0.015 MB/s Profile: Default Test: 256 MiB (x5) Mode: Time: Measure 5 sec / Interval 5 sec Date: 2024/01/15 11:53:09 OS: Windows 10 Professional (x64)
  • 2023-11-15 18:16
    1 次阅读|
    0 个评论
    前言:  很感谢深圳雷龙发展有限公司为博主提供的两片SD NAND的存储芯片,在这里博主记录一下自己的使用过程以及部分设计。  深入了解该产品:  拿到这个产品之后,我大致了解了下两款芯片的性能。CSNP4GCR01-AMW是一种基于NAND闪存和SD控制器的4Gb密度嵌入式存储;而CSNP32GCR01-AOW是一种基于NAND闪存和SD控制器的32Gb密度嵌入式存储。与原始NAND相比其具有嵌入式坏块管理和更强的嵌入式ECC。即使在异常断电,它仍然可以安全地保存数据。作为一个存储芯片,它确实做到了小巧,LGA-8的封装对比我之前用到过的TF卡,只占到了其面积的三分之一,这样对于一些嵌入式的设计就方便了很多。 编辑 ​  雷龙官方还很贴心的提供了样品的测试板,在这款测试板上,我焊接了4GB的CSNP4GCR01-AMW上去,并且跑了一下分,对于一款小的存储芯片而言,实在难得。 编辑 ​  (上图为测试板焊接图)  博主日前在设计基于H616与NB-IOT的嵌入式智能储物柜的时候考虑过存储方面的问题,当时在SD NAND和EMMC与TF卡中徘徊,以下是几个存储类型的对比。 编辑 ​ 编辑 ​  经过多方对比,本着不需要频繁更换的原则,同时也为了更好的防水和成本考虑,最终决定使用雷龙公司的SD NAND 作为设计样品的存储部分。  此外,SD NAND还具有不用写驱动程序自带坏块管理的NAND FLASH(贴片式TF卡),不标准的SDIO接口,也同时兼容SPI/SD接口,10万次的SLC晶圆擦写寿命,通过一万次的随机掉电测试耐高低温,经过跑分测得,速度级别Class10。标准的SD2.0协议,普通的SD卡可以直接驱动,支持TF卡启动的SOC都可以用SD NAND,而且雷龙官方还贴心的提供了STM32参考例程和原厂技术支持,这对于刚上手的小白而言,十分友好。  设计理念:  使用H616作为主控CPU并搭配NB-IOT来向申请下来的云端传输数据,当WIFI正常时,储物数据每搁两小时向云端传输一次,当有人取出物品时再次向云端发送一次数据(不保留在SD NAND中);一旦系统检测到WIFI出现问题,储物数据转而存储到SD NAND中,取物时输入的物品ID和取出时间一并放入SD NAND中(我也是看中了SD NAND与原始NAND相比其具有嵌入式坏块管理和更强的嵌入式ECC。即使在异常断电,它仍然可以安全地保存数据这一点)。  部分SD NAND的参考设计  根据官方数据手册提供的SD NAND参考设计,只占用8个GPIO,对于H616来说,确实很友好 编辑 ​  这里为了不泄露他人的劳动成果,我也就不粘PCB设计了。 编辑 ​  采用H616驱动SD NAND的示例代码  下面是关于H616驱动SD NAND的示例代码,这里记录一下自己当初的学习过程(注:这个代码不能直接拿过来就用,而是要根据自己的需求修改) #include #include #include #include #include "h616_sdio.h" // 定义SDIO引脚 #define SDIO_CMD_PIN 0 #define SDIO_CLK_PIN 1 #define SDIO_D0_PIN 2 #define SDIO_D1_PIN 3 #define SDIO_D2_PIN 4 #define SDIO_D3_PIN 5 // 定义NAND芯片命令 #define CMD_READ 0x00 #define CMD_WRITE 0x80 #define CMD_ERASE 0x60 #define CMD_STATUS 0x70 #define CMD_RESET 0xff // 定义NAND芯片状态 #define STATUS_READY 0x40 #define STATUS_ERROR 0x01 // 初始化SDIO控制器 void sdio_init() { // 设置SDIO引脚模式和速率 h616_sdio_set_pin_mode(SDIO_CMD_PIN, H616_SDIO_PIN_MODE_SDIO); h616_sdio_set_pin_mode(SDIO_CLK_PIN, H616_SDIO_PIN_MODE_SDIO); h616_sdio_set_pin_mode(SDIO_D0_PIN, H616_SDIO_PIN_MODE_SDIO); h616_sdio_set_pin_mode(SDIO_D1_PIN, H616_SDIO_PIN_MODE_SDIO); h616_sdio_set_pin_mode(SDIO_D2_PIN, H616_SDIO_PIN_MODE_SDIO); h616_sdio_set_pin_mode(SDIO_D3_PIN, H616_SDIO_PIN_MODE_SDIO); h616_sdio_set_clock(H616_SDIO_CLOCK_FREQ_25MHZ); // 初始化SDIO控制器 h616_sdio_init(); } // 发送NAND芯片命令 void nand_send_cmd(uint8_t cmd) { // 设置SDIO控制器传输模式和命令码 h616_sdio_set_transfer_mode(H616_SDIO_TRANSFER_MODE_WRITE); h616_sdio_set_command_code(cmd); // 发送命令 h616_sdio_send_command(); } // 发送NAND芯片地址 void nand_send_addr(uint32_t addr) { // 设置SDIO控制器传输模式和地址 h616_sdio_set_transfer_mode(H616_SDIO_TRANSFER_MODE_WRITE); h616_sdio_set_address(addr); // 发送地址 h616_sdio_send_address(); } // 读取NAND芯片数据 void nand_read_data(uint8_t *data, uint32_t size) { // 设置SDIO控制器传输模式 h616_sdio_set_transfer_mode(H616_SDIO_TRANSFER_MODE_READ); // 读取数据 h616_sdio_read_data(data, size); } // 写入NAND芯片数据 void nand_write_data(const uint8_t *data, uint32_t size) { // 设置SDIO控制器传输模式 h616_sdio_set_transfer_mode(H616_SDIO_TRANSFER_MODE_WRITE); // 写入数据 h616_sdio_write_data(data, size); } // 读取NAND芯片状态 uint8_t nand_read_status() { uint8_t status; // 发送读取状态命令 nand_send_cmd(CMD_STATUS); // 读取状态 nand_read_data(&status, 1); return status; } // 等待NAND芯片准备就绪 void nand_wait_ready() { uint8_t status; // 循环读取状态,直到NAND芯片准备就绪 do { status = nand_read_status(); } while ((status & STATUS_READY) == 0); } // 读取NAND芯片数据 void nand_read(uint32_t page, uint32_t column, uint8_t *data, uint32_t size) { // 发送读取命令和地址 nand_send_cmd(CMD_READ); nand_send_addr(column | (page << 8)); // 等待NAND芯片准备就绪 nand_wait_ready(); // 读取数据 nand_read_data(data, size); } // 写入NAND芯片数据 void nand_write(uint32_t page, uint32_t column, const uint8_t *data, uint32_t size) { // 发送写入命令和地址 nand_send_cmd(CMD_WRITE); nand_send_addr(column | (page << 8)); // 写入数据 nand_write_data(data, size); // 等待NAND芯片准备就绪 nand_wait_ready(); } // 擦除NAND芯片块 void nand_erase(uint32_t block) { // 发送擦除命令和地址 nand_send_cmd(CMD_ERASE); nand_send_addr(block << 8); // 等待NAND芯片准备就绪 nand_wait_ready(); } // 复位NAND芯片 void nand_reset() { // 发送复位命令 nand_send_cmd(CMD_RESET); // 等待NAND芯片准备就绪 nand_wait_ready(); } // 示例程序入口 int main() { uint8_t data ; memset(data, 0x5a, sizeof(data)); // 初始化SDIO控制器 sdio_init(); // 复位NAND芯片 nand_reset(); // 擦除第0块 nand_erase(0); // 写入第0页 nand_write(0, 0, data, sizeof(data)); // 读取第0页 nand_read(0, 0, data, sizeof(data)); return 0; }
  • 热度 8
    2023-6-2 18:04
    964 次阅读|
    0 个评论
    文章目录 stm32 CubeMx 实现SD卡/SD nand FATFS读写测试 1. 前言 2. 环境介绍 2.1 软硬件说明 2.2 外设原理图 3. 工程搭建 3.1 CubeMx 配置 3.2 SDIO时钟配置说明 3.2 读写测试 3.2.1 添加读写测试代码 3.3 FATFS文件操作 3.3.1 修改读写测试代码 3.4 配置问题记录 3.4.1 CubeMx生成代码bug 3.4.2 SD插入检测引脚配置 4. 结束语 1. 前言 SD卡/SD nand是嵌入式开发中常为使用的大容量存储设备,SD nand虽然当前价格比SD卡高,但胜在价格、封装以及稳定性上有优势,实际操作和SD卡没什么区别。 关于 SD卡/SDnand 的驱动,有了CubeMx之后其实基本上都自动生成了对应的驱动了,基本上把驱动配置一下之后,自己写一些应用就可以完成基本的读写了,同时关于FATFS文件系统,也可以直接采用CubeMx配置,也不用自己移植,因此使用STM32开发这些还是比较爽的!不过使用过程中也有一些坑,自动生成的驱动有时候也还是有一些bug,因此还是需要大家对对应驱动有一定的了解。 本文将主要分享关于使用 CubeMx 配置 stm32 的工程,通过SDIO总线完成 SD卡/SD nand 的读写,并配置FATFS,采用文件操作实现对 SD卡/SD nand 的读写操作;此外还将分享博主在调试过程中遇到的一些问题,比如CubeMx自动生成的驱动存在的bug等,以及分享关于驱动部分的代码分析! 2. 环境介绍 2.1 软硬件说明 硬件环境: 主控:stm32f103vet6 SD nand: CSNP1GCR01-AOW【样品CS创世SD NAND由深圳市雷龙发展有限公司免费提供的,感兴趣的可到雷龙官网申请】 软件环境: CubeMx版本:Version 6.6.1 注意:当前最新版本 V6.8.0,生成的工程配置存在bug,具体细节在后文描述 2.2 外设原理图 SD卡槽原理图部分如下: 编辑 ​ 编辑 ​ 3. 工程搭建3.1 CubeMx 配置 1.选择芯片,ACCESS TO MCU SELECTOR 编辑 ​ 2.搜索对应的芯片型号,在对应列表下方选择对应芯片 3.配置时钟方案,采用外部高速时钟,无源晶振方案 编辑 ​ 4.配置调试器,由于我采用SWD调试接口,因此选择 Serial Wrie 串行总线 编辑 ​ 5.配置SDIO外设,由于我们所使用的SD nand支持4线传输,因此此处选择4线宽度;如果你所使用的SD nand或SD卡不支持4线传输,此处应选择1线宽度;支持4线宽度的SD卡肯定可以使用1线宽度,因此如果你实在不知道你的SD卡支持几线宽度,你可以直接选择1线宽度!4线和1线宽度的差别也就在于速度上相差了4倍! (注意这里暂时不需要对SDIO的参数进行配置,后面我们再回来配置!) 编辑 ​ 6.完成时钟树配置: 配置外部晶振频率 调整时钟选择,SYSCLK由PLL产生,PLL由外部时钟倍频产生 配置SDIO外设时钟,注意此处SDIO外设比较特殊,有两个时钟!具体原因见后文! 编辑 ​ 7. 修改SDIO参数配置,主要是修改SDIOCLK的分频 由于我们上述配置的SDIO时钟为 72M,而SD卡支持的通讯速率在0MHz至25MHz之间,因此我们需要分频,配置 SDIO Clock divider bypass 为 Disable 此处设置 SDIOCLK clock divide factor CLKDIV分频系数为 8,这个受限于具体的SD卡支持的最大速度。如果设置值较小,可能由于SDIO_CK速度过高,SD卡/SDnand不支持,导致通讯失败,因此建议先将此值设大点(或查看SD卡/SDnand手册,或先设一个较大值,软件完成SD信息读取后再配置) 注意这个配置的时钟是用于SD读写通讯时候的时钟,而不是SD卡信息识别过程时的速度! 编辑 ​ 编辑 ​ 8.勾选FATFS配置,选择SD Card 编辑 ​ 9.配置SD卡检测引脚,有以下两种方案 方案一:选择一个输入IO,作为触发引脚 编辑 ​ 方案二:不配置输入IO,最后生成代码的时候无视警报即可,生成的代码会自动取消输入检测判断 编辑 ​ 10.配置调试串口,用来打印信息,此处我选择USART1,大家可根据自己硬件环境自行选择 编辑 ​ 11.配置工程信息 配置工程名 选择工程路径 配置应用程序结构,我习惯选择 Basic 结构 选择IDE工具及版本 修改堆栈大小,适当改大一点,怕不够用 编辑 ​ 12.勾选将外设初始化放置在独立的.c和.h文件,这样每个外设的初始化是独立的,方便阅读移植! 编辑 ​ 13.生成代码 编辑 ​ 3.2 SDIO时钟配置说明 在上述CubeMx时钟配置中,外设的时钟一般都是只有一路过去,但是在此处我们会发现SDIO的时钟在时钟树中有两个!没弄清楚还会以为这是CubeMx出现bug了! 编辑 ​ 其实这是SDIO外设的特殊点,我们查看数据手册上的时钟树,便可以发现,实际上是真的有两路时钟,分别是:1)SDIOCLK;2)至SDIO的AHB接口; 编辑 ​ 之后,我们看到数据手册的SDIO章节,我们可以看到SDIO外设分为:1)AHB总线接口 和 2)SDIO适配器两大块,且使用不同的时钟,这也就是我们在时钟树配置中可以看到有两路时钟配置的原因了! 从下图我们可以知道,SDIO外设不同于其他外设,其外设模块部分与中断、DMA是分开的,并采用不同的时钟! 编辑 ​ 关于AHB总线接口及SDIO适配器更多细节,大家可自行阅读参考手册部分章节内容,此处不做赘述。 此外,关于时钟配置有一个特别需要注意的,也就是SDIO_CK时钟信号。SDIO_CK时钟,也就是我们SDIO外设与SD卡/SD nand通讯的CLK时钟,从上图我们可知,SDIO_CK时钟来自SDIO适配器,也就是来自SDIOCLK,对应CubeMX时钟配置中的: 编辑 ​ 编辑 ​ 3.2 读写测试3.2.1 添加读写测试代码 1.使能 MicroLIB 微库,否则调用 printf 函数会卡住 编辑 ​ 2.修改编码规则为UTF-8,这是由于我们CubeMx中配置的FATFS的编码格式为 UTF-8导致,如果不修改为UTF-8则部分中文会乱码! //TODO:确认是由FATFS配置导致 编辑 ​ 编辑 ​ 3.添加 printf 重映射 (位置可根据自行决定) #include int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1,0xffff); return (ch); } 4.添加 sdcard 信息打印函数,查看卡片信息 HAL_SD_CardInfoTypeDef SDCardInfo; void printf_sdcard_info(void) { uint64_t CardCap; //SD卡容量 HAL_SD_CardCIDTypeDef SDCard_CID; HAL_SD_GetCardCID(&hsd,&SDCard_CID); //获取CID HAL_SD_GetCardInfo(&hsd,&SDCardInfo); //获取SD卡信息 CardCap=(uint64_t)(SDCardInfo.LogBlockNbr)*(uint64_t)(SDCardInfo.LogBlockSize); //计算SD卡容量 switch(SDCardInfo.CardType) { case CARD_SDSC: { if(SDCardInfo.CardVersion == CARD_V1_X) printf("Card Type:SDSC V1\r\n"); else if(SDCardInfo.CardVersion == CARD_V2_X) printf("Card Type:SDSC V2\r\n"); } break; case CARD_SDHC_SDXC:printf("Card Type:SDHC\r\n");break; default:break; } printf("Card ManufacturerID: %d \r\n",SDCard_CID.ManufacturerID); //制造商ID printf("CardVersion: %d \r\n",(uint32_t)(SDCardInfo.CardVersion)); //卡版本号 printf("Class: %d \r\n",(uint32_t)(SDCardInfo.Class)); //SD卡类别 printf("Card RCA(RelCardAdd):%d \r\n",SDCardInfo.RelCardAdd); //卡相对地址 printf("Card BlockNbr: %d \r\n",SDCardInfo.BlockNbr); //块数量 printf("Card BlockSize: %d \r\n",SDCardInfo.BlockSize); //块大小 printf("LogBlockNbr: %d \r\n",(uint32_t)(SDCardInfo.LogBlockNbr)); //逻辑块数量 printf("LogBlockSize: %d \r\n",(uint32_t)(SDCardInfo.LogBlockSize)); //逻辑块大小 20)); //卡容量 } 5.添加初始化及读写测试代码,注意此处我们没有直接使用FATFS的读写接口,我们先测试生成的SD驱动函数接口 int main(void) { /* USER CODE BEGIN 1 */ BYTE send_buf ; DRESULT ret; /* USER CODE END 1 */ /* ...省略若干自动生成代码... */ /* USER CODE BEGIN 2 */ SD_Driver.disk_initialize(0); printf_sdcard_info(); printf("\r\n\r\n********** 英文读写测试 **********\r\n"); ret = SD_Driver.disk_write(0, (BYTE *)"Life is too short to spend time with people who suck the happiness out of you. \ If someone wants you in their life, they’ll make room for you. You shouldn’t have to fight for a spot. Never, ever\ insist yourself to someone who continuously overlooks your worth. And remember, it’s not the people that stand by \ your side when you’re at your best, but the ones who stand beside you when you’re at your worst that are your true\ friends",20,2); printf("sd write result:%d\r\n", ret); ret = SD_Driver.disk_read(0, send_buf, 20, 2); printf("sd reak result:%d\r\n", ret); printf("sd read content:\r\n%s\r\n", send_buf); printf("\r\n\r\n********** 中文读写测试 **********\r\n"); ret = SD_Driver.disk_write(0, (BYTE *)"开发者社区的明天需要大家一同开源共创,期待下一次你的分享,让我们一同携手共进,推动人类科技的发展!!!\r\n\ 创作不易,转载请注明出处~\r\n\ 更多文章敬请关注:爱出名的狗腿子\r\n", 22, 2); printf("sd write result:%d\r\n", ret); ret = SD_Driver.disk_read(0, send_buf, 22, 2); printf("sd reak result:%d\r\n", ret); printf("sd read content:\r\n%s\r\n", send_buf); /* USER CODE END 2 */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } 6.修改烧录器配置,配置为烧录后自动运行 编辑 ​ 7.下载测试,这里由于我们采用UTF-8编码,所以使用的串口上位机也需要支持UTF-8解析,我们这里使用Mobaxterm上位机,测试结果如下: 编辑 ​ 8.main.c文件全部代码如下,供大家参考: /* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2023 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "fatfs.h" #include "sdio.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ HAL_SD_CardInfoTypeDef SDCardInfo; void printf_sdcard_info(void) { uint64_t CardCap; //SD卡容量 HAL_SD_CardCIDTypeDef SDCard_CID; HAL_SD_GetCardCID(&hsd,&SDCard_CID); //获取CID HAL_SD_GetCardInfo(&hsd,&SDCardInfo); //获取SD卡信息 CardCap=(uint64_t)(SDCardInfo.LogBlockNbr)*(uint64_t)(SDCardInfo.LogBlockSize); //计算SD卡容量 switch(SDCardInfo.CardType) { case CARD_SDSC: { if(SDCardInfo.CardVersion == CARD_V1_X) printf("Card Type:SDSC V1\r\n"); else if(SDCardInfo.CardVersion == CARD_V2_X) printf("Card Type:SDSC V2\r\n"); } break; case CARD_SDHC_SDXC:printf("Card Type:SDHC\r\n");break; default:break; } printf("Card ManufacturerID: %d \r\n",SDCard_CID.ManufacturerID); //制造商ID printf("CardVersion: %d \r\n",(uint32_t)(SDCardInfo.CardVersion)); //卡版本号 printf("Class: %d \r\n",(uint32_t)(SDCardInfo.Class)); //SD卡类别 printf("Card RCA(RelCardAdd):%d \r\n",SDCardInfo.RelCardAdd); //卡相对地址 printf("Card BlockNbr: %d \r\n",SDCardInfo.BlockNbr); //块数量 printf("Card BlockSize: %d \r\n",SDCardInfo.BlockSize); //块大小 printf("LogBlockNbr: %d \r\n",(uint32_t)(SDCardInfo.LogBlockNbr)); //逻辑块数量 printf("LogBlockSize: %d \r\n",(uint32_t)(SDCardInfo.LogBlockSize)); //逻辑块大小 20)); //卡容量 } int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1,0xffff); return (ch); } /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ BYTE send_buf ; DRESULT ret; /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_SDIO_SD_Init(); MX_USART1_UART_Init(); MX_FATFS_Init(); /* USER CODE BEGIN 2 */ SD_Driver.disk_initialize(0); printf_sdcard_info(); printf("\r\n\r\n********** 英文读写测试 **********\r\n"); ret = SD_Driver.disk_write(0, (BYTE *)"Life is too short to spend time with people who suck the happiness out of you. \ If someone wants you in their life, they’ll make room for you. You shouldn’t have to fight for a spot. Never, ever\ insist yourself to someone who continuously overlooks your worth. And remember, it’s not the people that stand by \ your side when you’re at your best, but the ones who stand beside you when you’re at your worst that are your true\ friends",20,2); printf("sd write result:%d\r\n", ret); ret = SD_Driver.disk_read(0, send_buf, 20, 2); printf("sd reak result:%d\r\n", ret); printf("sd read content:\r\n%s\r\n", send_buf); printf("\r\n\r\n********** 中文读写测试 **********\r\n"); ret = SD_Driver.disk_write(0, (BYTE *)"开发者社区的明天需要大家一同开源共创,期待下一次你的分享,让我们一同携手共进,推动人类科技的发展!!!\r\n\ 创作不易,转载请注明出处~\r\n\ 更多文章敬请关注:爱出名的狗腿子\r\n", 22, 2); printf("sd write result:%d\r\n", ret); ret = SD_Driver.disk_read(0, send_buf, 22, 2); printf("sd reak result:%d\r\n", ret); printf("sd read content:\r\n%s\r\n", send_buf); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ /** * @brief This function is executed in case of error occurrence. * @retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { } /* USER CODE END Error_Handler_Debug */ } #ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */ void assert_failed(uint8_t *file, uint32_t line) { /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */ 3.3 FATFS文件操作 移植了FATFS,当然也就可以只用通用的文件系统操作函数完成文件的读写,通用的文件系统操作API 在ff.c文件内,声明在ff.h文件内,主要使用的API接口如下: FRESULT f_open (FIL* fp, const TCHAR* path, BYTE mode); /* Open or create a file */ FRESULT f_close (FIL* fp); /* Close an open file object */ FRESULT f_read (FIL* fp, void* buff, UINT btr, UINT* br); /* Read data from a file */ FRESULT f_write (FIL* fp, const void* buff, UINT btw, UINT* bw); /* Write data to a file */ FRESULT f_forward (FIL* fp, UINT(*func)(const BYTE*,UINT), UINT btf, UINT* bf); /* Forward data to the stream */ FRESULT f_lseek (FIL* fp, DWORD ofs); /* Move file pointer of a file object */ FRESULT f_truncate (FIL* fp); /* Truncate file */ FRESULT f_sync (FIL* fp); /* Flush cached data of a writing file */ FRESULT f_opendir (DIR* dp, const TCHAR* path); /* Open a directory */ FRESULT f_closedir (DIR* dp); /* Close an open directory */ FRESULT f_readdir (DIR* dp, FILINFO* fno); /* Read a directory item */ FRESULT f_findfirst (DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern); /* Find first file */ FRESULT f_findnext (DIR* dp, FILINFO* fno); /* Find next file */ FRESULT f_mkdir (const TCHAR* path); /* Create a sub directory */ FRESULT f_unlink (const TCHAR* path); /* Delete an existing file or directory */ FRESULT f_rename (const TCHAR* path_old, const TCHAR* path_new); /* Rename/Move a file or directory */ FRESULT f_stat (const TCHAR* path, FILINFO* fno); /* Get file status */ FRESULT f_chmod (const TCHAR* path, BYTE attr, BYTE mask); /* Change attribute of the file/dir */ FRESULT f_utime (const TCHAR* path, const FILINFO* fno); /* Change times-tamp of the file/dir */ FRESULT f_chdir (const TCHAR* path); /* Change current directory */ FRESULT f_chdrive (const TCHAR* path); /* Change current drive */ FRESULT f_getcwd (TCHAR* buff, UINT len); /* Get current directory */ FRESULT f_getfree (const TCHAR* path, DWORD* nclst, FATFS** fatfs); /* Get number of free clusters on the drive */ FRESULT f_getlabel (const TCHAR* path, TCHAR* label, DWORD* vsn); /* Get volume label */ FRESULT f_setlabel (const TCHAR* label); /* Set volume label */ FRESULT f_mount (FATFS* fs, const TCHAR* path, BYTE opt); /* Mount/Unmount a logical drive */ FRESULT f_mkfs (const TCHAR* path, BYTE sfd, UINT au); /* Create a file system on the volume */ FRESULT f_fdisk (BYTE pdrv, const DWORD szt = "\r\n\r\n\ hello world!\r\n\ 开发者社区的明天需要大家一同开源共创,期待下一次你的分享,让我们一同携手共进,推动人类科技的发展!!!\r\n\ 创作不易,转载请注明出处~\r\n\ 更多文章敬请关注:爱出名的狗腿子\r\n\r\n\ "; BYTE read_buf = {0}; UINT num; FRESULT ret; /* USER CODE END 1 */ /* ... 省略初始化代码... */ /* USER CODE BEGIN 2 */ /* 挂载文件系统,挂载的时候会完成对应硬件设备(SD卡/SDnand)初始化 */ ret = f_mount(&SDFatFS, USERPath, 1); if (ret != FR_OK) { printf("f_mount error!\r\n"); goto mount_error; } else if(ret == FR_NO_FILESYSTEM) { /* 检测是否存在文件系统,如果没有则进行格式化 */ printf("未检测到FATFS文件系统,执行格式化...\r\n"); ret = f_mkfs(USERPath, 0, 0); if(ret == FR_OK) { printf("格式化成功!\r\n"); f_mount(NULL, USERPath, 1); /* 先取消挂载,后重新挂载 */ ret = f_mount(&SDFatFS, USERPath, 1); } else { printf("格式化失败!\r\n"); goto mount_error; } } else { printf("f_mount success!\r\n"); } /* 读写测试 */ printf("\r\n ========== write test ==========\r\n"); ret = f_open(&SDFile, "hello.txt", FA_CREATE_ALWAYS | FA_WRITE); if(ret == FR_OK) { printf("open file sucess!\r\n"); ret = f_write(&SDFile, write_buf, sizeof(write_buf), &num); if(ret == FR_OK) { printf("write \"%s\" success!\r\nwrite len:%d\r\n", write_buf, num); } else { printf("write error! ret:%d \r\n", ret); goto rw_error; } f_close(&SDFile); } else { printf("open file error!\r\n"); goto rw_error; } printf("\r\n ========== read test ==========\r\n"); ret = f_open(&SDFile, "hello.txt",FA_OPEN_EXISTING | FA_READ); if(ret == FR_OK) { printf("open file sucess!\r\n"); ret = f_read(&SDFile, read_buf, sizeof(read_buf), &num); if(ret == FR_OK) { printf("read data:\"%s\"!\r\nread len:%d\r\n", read_buf, num); } else { printf("read error! ret:%d \r\n", ret); goto rw_error; } } else { printf("open file error!\r\n"); goto rw_error; } rw_error: f_close(&SDFile); mount_error: f_mount(NULL, USERPath, 1); /* USER CODE END 2 */ while (1) { } } #define USERPath "0:/"表示挂载的位置,这是由于FATFS初始化的时候链接的根目录为0:/,所以挂载的文件系统需要在此目录下,当然也可以是此目录下的路径,如0:/hello,但不能是其他目录,如1:/ 编辑 ​ 测试结果如下: 编辑 ​ main.c完整内容如下: /* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2023 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "fatfs.h" #include "sdio.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include #include "fatfs.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ HAL_SD_CardInfoTypeDef SDCardInfo; void printf_sdcard_info(void) { uint64_t CardCap; //SD卡容釿 HAL_SD_CardCIDTypeDef SDCard_CID; HAL_SD_GetCardCID(&hsd,&SDCard_CID); //获取CID HAL_SD_GetCardInfo(&hsd,&SDCardInfo); //获取SD卡信恿 CardCap=(uint64_t)(SDCardInfo.LogBlockNbr)*(uint64_t)(SDCardInfo.LogBlockSize); //计算SD卡容釿 switch(SDCardInfo.CardType) { case CARD_SDSC: { if(SDCardInfo.CardVersion == CARD_V1_X) printf("Card Type:SDSC V1\r\n"); else if(SDCardInfo.CardVersion == CARD_V2_X) printf("Card Type:SDSC V2\r\n"); } break; case CARD_SDHC_SDXC:printf("Card Type:SDHC\r\n");break; default:break; } printf("Card ManufacturerID: %d \r\n",SDCard_CID.ManufacturerID); //制鿠商ID printf("CardVersion: %d \r\n",(uint32_t)(SDCardInfo.CardVersion)); //卡版本号 printf("Class: %d \r\n",(uint32_t)(SDCardInfo.Class)); //SD卡类刿 printf("Card RCA(RelCardAdd):%d \r\n",SDCardInfo.RelCardAdd); //卡相对地坿 printf("Card BlockNbr: %d \r\n",SDCardInfo.BlockNbr); //块数釿 printf("Card BlockSize: %d \r\n",SDCardInfo.BlockSize); //块大尿 printf("LogBlockNbr: %d \r\n",(uint32_t)(SDCardInfo.LogBlockNbr)); //逻辑块数釿 printf("LogBlockSize: %d \r\n",(uint32_t)(SDCardInfo.LogBlockSize)); //逻辑块大尿 20)); //卡容釿 } int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1,0xffff); return (ch); } /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ #define USERPath "0:/" BYTE write_buf = {0}; UINT num; FRESULT ret; /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_SDIO_SD_Init(); MX_USART1_UART_Init(); MX_FATFS_Init(); /* USER CODE BEGIN 2 */ /* 挂载文件系统,挂载的时候会完成对应硬件设备(SD卡/SDnand)初始化 */ ret = f_mount(&SDFatFS, USERPath, 1); if (ret != FR_OK) { printf("f_mount error!\r\n"); goto mount_error; } else if(ret == FR_NO_FILESYSTEM) { /* 检测是否存在文件系统,如果没有则进行格式化 */ printf("未检测到FATFS文件系统,执行格式化...\r\n"); ret = f_mkfs(USERPath, 0, 0); if(ret == FR_OK) { printf("格式化成功!\r\n"); f_mount(NULL, USERPath, 1); /* 先取消挂载,后重新挂载 */ ret = f_mount(&SDFatFS, USERPath, 1); } else { printf("格式化失败!\r\n"); goto mount_error; } } else { printf("f_mount success!\r\n"); } /* 读写测试 */ printf("\r\n ========== write test ==========\r\n"); ret = f_open(&SDFile, "hello.txt", FA_CREATE_ALWAYS | FA_WRITE); if(ret == FR_OK) { printf("open file sucess!\r\n"); ret = f_write(&SDFile, write_buf, sizeof(write_buf), &num); if(ret == FR_OK) { printf("write \"%s\" success!\r\nwrite len:%d\r\n", write_buf, num); } else { printf("write error! ret:%d \r\n", ret); goto rw_error; } f_close(&SDFile); } else { printf("open file error!\r\n"); goto rw_error; } printf("\r\n ========== read test ==========\r\n"); ret = f_open(&SDFile, "hello.txt",FA_OPEN_EXISTING | FA_READ); if(ret == FR_OK) { printf("open file sucess!\r\n"); ret = f_read(&SDFile, read_buf, sizeof(read_buf), &num); if(ret == FR_OK) { printf("read data:\"%s\"!\r\nread len:%d\r\n", read_buf, num); } else { printf("read error! ret:%d \r\n", ret); goto rw_error; } } else { printf("open file error!\r\n"); goto rw_error; } rw_error: f_close(&SDFile); mount_error: f_mount(NULL, USERPath, 1); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ /** * @brief This function is executed in case of error occurrence. * @retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { } /* USER CODE END Error_Handler_Debug */ } #ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */ void assert_failed(uint8_t *file, uint32_t line) { /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */ 3.4 配置问题记录 3.4.1 CubeMx生成代码bug 测试发现,使用CubeMx当前最新版本:V6.8.0版本,生成代码会存在以下问题: SD卡/SDnand 卡片信息读取成功,但是读写测试失败 经过仔细分析代码后发现,出现的问题在 MX_SDIO_SD_Init() 此初始化函数内的配置项错误导致,具体分析如下: 我们在CubeMx里面配置的时候选择的是4线宽度模式 SD 4bit Wide bus v6.8.0版本CubeMx生成的 MX_SDIO_SD_Init() SD初始化函数内,hsd.Init.BusWide = SDIO_BUS_WIDE_4B; 看上去没有什么问题,配置4线模式,对应的初始化项也使用4线模式,但是不然,我们继续分析 MX_SDIO_SD_Init() 此初始配置的调用 MX_SDIO_SD_Init() 此函数在main函数内初始化的时候调用,此函数只配置了 hsd 结构体,并未配置给SDIO硬件寄存器 之后调用 SD_Driver.disk_initialize(0); 函数的时候才真正开始进行SDIO外设配置 BSP_SD_Init() HAL_SD_Init() HAL_SD_InitCard() 在 HAL_SD_InitCard() 函数内使用Init结构体配置SDIO外设,总线宽度1bit,时钟速度<400k,以进行卡片的初始化识别。 SD_InitCard() Init) Instance, BLOCKSIZE) · 在 SD_InitCard() 函数内实现SD卡的初始化识别,之后调用 SDIO_Init() 将 MX_SDIO_SD_Init() 内对 hsd 的配置配置给SDIO外设,此处的作用主要是提升SDIO外设时钟速率为我们配置的速率; · v6.8.0版本的代码此时hsd.Init.BusWide = SDIO_BUS_WIDE_4B; ,因此v6.8.0版本代码后续SDIO外设使用4线通讯; · 之后调用 SDMMC_CmdBlockLength() 设置块大小,由于SDIO外设已切换到4线模式,而SD卡/SDnand此时仍然处于1线模式,因此配置会出错 HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) 根据前面获取到的SD卡SCR寄存器值,判断是否支持4线模式,如果支持则发送配置命令通知SD卡/SDnand进入4线模式,之后修改SDIO外设总线宽度为4线模式 6.通过以上分析可知,MX_SDIO_SD_Init() 函数内对 hsd.Init.BusWide = SDIO_BUS_WIDE_4B; 的配置会导致对SD卡块大小的配置失败,从而导致后续读写时失败,报错为块大小设置失败! 7.综上,针对当前最新版本 V6.8.0 版本CubeMx的处理方法是:手动修改此 hsd.Init.BusWide 配置为 SDIO_BUS_WIDE_1B 或更换低版本CubeMx,本人更换V6.6.1版本后无此bug。 3.4.2 SD插入检测引脚配置 使用CubeMx配置FATFS 选择 SD Card 之后,有一个配置参数,用来配置SD Card的输入检测引脚。如果我们在硬件上有设计SD卡的卡槽插入检测引脚插入连接到了MCU的IO,则可配置对应IO为输入模式,并设置对应IO为输入检测引脚,比如,我们设置PD12为输入检测引脚,则配置如下: 对应代码如下,输入检测 IO 低电平有效! 编辑 ​ 编辑 ​ 如果硬件上,没有此插入检测引脚,则可以在CubeMx内不进行配置,只是在生成代码的时候会提示警报而已,可以不用关心,生成的代码项会自动屏蔽插入检测! 编辑 ​ 4. 结束语 以上便是本文的全部内容了,欢迎大家评论区留言讨论! 使用CubeMx虽然能帮助我们快速生成驱动,但是对于SD卡/SD nand的驱动流程,我们还是需要有清晰的认识,推荐阅读: SD Nand 与 SD卡 SDIO模式应用流程 ———————————————— 【本文转载自CSDN,作者: 爱出名的狗腿子】
  • 热度 9
    2023-5-12 15:15
    764 次阅读|
    0 个评论
    文章目录 SD nand 与 SD卡的SPI模式驱动 1. 概述 2. SPI接口模式与SD接口模式区别 2.1 接口模式区别 2.2 硬件引脚 2.3 注意事项 3. SD接口协议 3.1 命令 3.1.1 命令格式 3.1.2 命令类型 3.2 响应 3.2.1 响应格式 4. SD nand(SD卡)结构描述 5. SD nand SPI通讯 5.1 SD nand SPI 通讯概述 5.2 SPI 时序 5.3 上电初始化及模式切换 5.3.1 初始化及模式切换流程说明 5.3.2 代码实现 5.4 识别过程 5.4.1 识别流程说明 5.4.2 代码实现 5.3 数据传输 5.3.1 数据写入 5.3.2 数据读取 5.3.3 代码实现 6. 总结 1. 概述 首先简单介绍下 SD卡 和SD nand: SD卡,也称之为内存卡,在早些年间的手机上出现过,主要用来存储数据; SD nand,贴片式SD卡,使用起来和SD卡一致,不同的是采用,通常采用LGA-8封装,尺寸为8mm x 6mm x 0.75mm,重点是采用贴片封装,可以直接贴在板卡上,直接解决了SD卡固定问题,再也不用为SD卡的接触稳定性操心! SD nand 与 SD卡除了封装上的区别,使用起来基本没什么不一样,因此下文中不再做区分,统一以SD nand作为描述。 SD nand 和 SD 卡、SPI Nor flash、 nand flash、eeprom一样,都是嵌入式系统中常见的用来存储数据所使用的存储芯片,这几种存储芯片主要的区别在于存储数据容量不一样、操作的大小不一样,价格不一样,因此在实际产品设计中,往往需要根据自身产品的需求来选择对应的存储芯片。 SD nand存储空间大小在上述存储系列芯片中属于偏大的,其存储空间小到 1Gb(256MB) 起步,大到可以到32G,最小读写单元通常是 512 Byte,与SD卡一样,均支持SD接口模式以及SPI接口模式(后文会详细描述其区别)。 关于采用SPI接口模式完成于SD nand和SD卡的通讯,网上也有相关资料,但描述均不是很清楚或完整,因此特整理此博客,以作记录及分享。 本博文以 CS 创世 CSNPGCR01-AOW 这颗IC为例,着重描述如何通过SPI接口完成SD nand(SD卡)的读写驱动。 2. SPI接口模式与SD接口模式区别 2.1 接口模式区别 SD nand同时支持SPI接口和SD接口,接下来主要从以下几个维度分析二者的区别: 硬件资源角度: SD接口需要控制器具有SDIO外设硬件支持 SPI接口如果控制器具有SPI硬件外设那就最好了,没有也可以使用软件模式SPI 传输效率: SD接口支持四线同时传输 SPI只有MOSI一根总线 且接口速度上SD接口速度通常要大于SPI接口,因此SD效率远高于SPI接口 控制难度: SPI协议比较简单,也是嵌入式开发中最常使用的协议之一,只有MISO和MOSI两根数据总线,因此控制难度简单; SD协议相对SPI要复杂,且需要控制的引脚多,内部还存在状态机,相比SPI较为复杂 综上分析,SD接口效率更高,但是需要芯片有对应外设支持,而SPI接口虽然效率比不上SD接口,但是控制起来简单,且对芯片外设硬件依赖不高,对于低端的控制器,亦可使用软件模式SPI来驱动SD nand。 2.2 硬件引脚 SD nand以及SD 卡在SPI接口以及SD接口模式下,硬件引脚如下图所示: SD nand SPI接口及SD接口模式IO定义 SD卡 SPI接口及SD接口模式IO定义 2.3 注意事项 此外对于使用SPI接口需要注意的是,SPI接口只是定义了物理传输层,并没有定义完整的数据传输协议,因此上层软件还是需要遵循SD接口协议! 3. SD接口协议 在2.3中我们重点强调了,SPI接口只是定义了物理层,也即硬件链路层,关于协议层并没有定义,写一次依旧遵循SD接口协议,因此我们需要首先了解下SD总线协议的内容。 SD 总线协议由SD卡协议定义,是一个通用的标准协议。首先说明的是,SD总线协议不仅仅只适用于SD卡,还支持IO卡,MMC卡等等,而且对这些不同类型的设备均能做出区分的!有点像USB一样牛逼! 我们首先来了解下SD总线协议中的命令及响应。 3.1 命令 命令由主机发出,分为广播命令和寻址命令 广播命令是针对与SD主机连接的所有设备发出的 寻址命令是指定某个地址的设备进行命令传输 3.1.1 命令格式 命令由48bit位(6字节)组成,格式如下: 起始位:1bit 固定为0 传输位:1bit 主要用于区分传输方向,1代表主机发送给从机的命令,0代表从机响应的主机命令 命令号:6bit 命令号索引,总共能表示2^6=64个命令 命令参数:32bit 命令所包含的参数信息 CRC7:7bit CRC校验位,用于保证数据传输的正确性,生成器多项式为:G(x) = x^7 + x^3 + 1 3.1.2 命令类型 命令主要有4种类型: bc:无响应广播命令 bcr:有响应广播命令 ac:寻址命令,发送到选定卡,DAT线没有数据传输 adtc:寻址数据传输命令,发送到选定的卡,且DAT线有数据传输 在SD总线协议中,经常见到的CMDx,代表的就是命令号,后面的x代表命令索引,在3.1.1中命令格式组成中描述了命令号总共占6bit,所以CMDx的范围是CMD0 - CMD63,CMD后面的数字代表的就是命令号command index的值。 对于SD这么复杂的协议,64种命令类型通常还不能涵盖所有类型的数据,因此SD协会在制定此协议的时候将命令继续细化,分了两种类型的命令:CMD和ACMD,CMD代表常规命令,ACMD代表特定应用的命令,ACMD通常为制造商特定使用的。 那么SD协议又是如何区分CMD和ACMD命令的呢? 在发送ACMD命令之前必须发送特定的CMD命令(APP_CMD)表明接下来的一帧命令是ACMD命令,在SD协议种规定此特定命令名称叫APP_CMD,也就是CMD55。 需要注意的是,CMD命令类型这么多,但实际上并没有都使用,针对SD nand(SD卡)的命令也就那么几条(注意SD模式命令的响应和SPI模式命令的响应有些许不同,SD模式请自行查阅手册) 上图中,命令序号对应3.1.1节命令格式中的命令号 command index,参数对应3.1.1节命令格式中的命令参数argument。 3.2 响应 针对需要响应的命令(bcr),SD nand(SD卡)在接收到命令之后会做出响应,根据命令的不同,响应的类型也不相同,其中命令中已规定哪个命令需要响应,并且返回什么类型的响应。 响应总共分为7中类型,分别是R1~R7,需要注意的是,SD nand(SD卡)没有R4、R5类型的响应。 响应的数据长度也并非完全一样,响应根据内容长度分为短响应和长响应,短响应长度为48bit(6Byte),长响应长度为136bit(17Byte),其中只有R2属于长响应,其他均属于短响应。 3.2.1 响应格式 其中重点讲下R1响应,在上图中我们可以看到R1返回的内容为卡的状态,关于卡状态的描述如下,每个bit均代表着对应的含义,如下图中所示: 上图是SD nand的内部结构,与SD卡完全类似,主要有五个部分组成,这里就不细述了,不然此篇文章会过于臃长,关于这块大家可以上网查找,需要重点注意的是内部有7个寄存器,主要用来对卡片进行配置和读取卡片有关的信息,描述如下,其中SD接口有些命令就指定了读取哪个寄存器的内容! 5. SD nand SPI通讯 主要参考资料:官方文档《Part_1_Pjysical_Layer_Specification_Ver2.0.0pdf》建议大家有时间的话也可以读一读,还是有收获的,如果没时间的话也可以先参考本博文 5.1 SD nand SPI 通讯概述 SD nand SPI通讯接口完成驱动主要可以分为三大部分: 上电初始化以及模式切换 SD nand(SD卡)识别 数据传输两大步 在以上三大部分中,每个部分均有命令传输,从3.1.1中我们可以知道发送给SD nand的命令为48bit,也就是8字节,那么SPI模式下与SD nand通讯,发送命令其实就是采用SPI总线往SD nand传输8个字节的数据,大家把握这这个思路去理解下文的通讯过程也就简单多了。 需要注意的是: SD nand或SD卡上电默认均为SD模式,需要对齐完成初始化以及模式切换后才能切换到SPI模式。 SD 模式,所有命令默认开启CRC校验,因此没有切换到SPI模式之前,所有命令都必须携带正确的CRC校验值 进入SPI模式后,默认关闭CRC校验,此时CRC校验字段默认填充1即可,当然也可以通过命令配置打开SPI模式的CRC校验 5.2 SPI 时序 在开始进行通讯读写前,我们先来看下SPI时序,使用SPI完成于SD nand(SD卡)的通讯与我们平常使用SPI与其他设备通讯会有一点点小小的区别,主要在于往SD nand写了数据之后,回复不是马上的,以及在必要的数据之间需要增加间隔,我们挑几个重点看下,在实际开发中有需要注意的在后文对应处有描述,不用过于担心。 1.主机发送命令给卡,卡响应,注意图中的NCR,NCR最小不是0,因此主机发送了命令之后,SD nand不是马上就响应的 2.卡连续响应两个指令之间需要有间隔,如图中的NRC 5.3 上电初始化及模式切换 5.3.1 初始化及模式切换流程说明 首先配置控制器SPI外设 SD nand(SD卡)电源应该在250ms内到大VCC,这是硬件电路要求 同时保持CS引脚为高电平状态,CLK时钟引脚至少发送74个时钟给SD nand已启动SD nand 之后SD nand进入空闲状态,发送CMD0命令至SD卡切换进入SPI模式 注意务必保证CMD0是第一包命令 SD卡选择了对应的模式之后不可切换,如果需要重新切换,需要重新上电 5.3.2 代码实现 1.SPI外设配置代码如下: #ifndef __BSP_SPI_H__ #define __BSP_SPI_H__ #include "stm32f10x.h" #define PIN_HIGH 1 #define PIN_LOW 0 int sd_spi_config(void); void set_sd_spi_cs_pin(uint8_t state); #endif /* __BSP_SPI_H__ */ #include "./spi/bsp_spi.h" /** * @brief spi gpio configuration * * @note CLK:PA5 MISO:PA6 MOSI:PA7 CS:PA8 * */ static void _spi_gpio_init(void) { GPIO_InitTypeDef GPIO_InitStructure = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* Configure SD_SPI pins: SCK */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); /* Configure SD_SPI pins: MOSI */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_Init(GPIOA, &GPIO_InitStructure); /* Configure SD_SPI pins: MISO */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); /*!< Configure SD_SPI_CS_PIN pin: SD Card CS pin */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); } /** * @brief configer spi1 peripher. * * @note Data rising edge acquisition. */ static void _spi_config(void) { SPI_InitTypeDef SPI_InitStructure = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); /*!< SD_SPI Config */ SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 0; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } int sd_spi_config(void) { _spi_gpio_init; _spi_config; return 0; } void set_sd_spi_cs_pin(uint8_t state) { if (state) GPIO_SetBits(GPIOA, GPIO_Pin_8); else GPIO_ResetBits(GPIOA, GPIO_Pin_8); } 2.SD初始化代码如下,set_sd_to_idle_state 函数向SD nand发送CMD0指令,同时由于发送CMD0时,SD nand还处于SD模式,因此手动计算CRC结果为0x95并发送,发送完CMD0之后等待SD nand的R1响应,并根据响应内容,知道SD nand操作完成。 #ifndef __SD_SPI_DRV_H__ #define __SD_SPI_DRV_H__ #include "stm32f10x.h" /** * @brief Commands: CMDxx = CMD-number | 0x40 */ #define SD_CMD_GO_IDLE_STATE 0 /*!< CMD0 = 0x40 */ #define SD_CMD_SEND_OP_COND 1 /*!< CMD1 = 0x41 */ #define SD_CMD_SEND_IF_COND 8 /*!< CMD8 = 0x48 */ #define SD_CMD_SEND_CSD 9 /*!< CMD9 = 0x49 */ #define SD_CMD_SEND_CID 10 /*!< CMD10 = 0x4A */ #define SD_CMD_STOP_TRANSMISSION 12 /*!< CMD12 = 0x4C */ #define SD_CMD_SEND_STATUS 13 /*!< CMD13 = 0x4D */ #define SD_CMD_SET_BLOCKLEN 16 /*!< CMD16 = 0x50 */ #define SD_CMD_READ_SINGLE_BLOCK 17 /*!< CMD17 = 0x51 */ #define SD_CMD_READ_MULT_BLOCK 18 /*!< CMD18 = 0x52 */ #define SD_CMD_SET_BLOCK_COUNT 23 /*!< CMD23 = 0x57 */ #define SD_CMD_WRITE_SINGLE_BLOCK 24 /*!< CMD24 = 0x58 */ #define SD_CMD_WRITE_MULT_BLOCK 25 /*!< CMD25 = 0x59 */ #define SD_CMD_PROG_CSD 27 /*!< CMD27 = 0x5B */ #define SD_CMD_SET_WRITE_PROT 28 /*!< CMD28 = 0x5C */ #define SD_CMD_CLR_WRITE_PROT 29 /*!< CMD29 = 0x5D */ #define SD_CMD_SEND_WRITE_PROT 30 /*!< CMD30 = 0x5E */ #define SD_CMD_SD_ERASE_GRP_START 32 /*!< CMD32 = 0x60 */ #define SD_CMD_SD_ERASE_GRP_END 33 /*!< CMD33 = 0x61 */ #define SD_CMD_UNTAG_SECTOR 34 /*!< CMD34 = 0x62 */ #define SD_CMD_ERASE_GRP_START 35 /*!< CMD35 = 0x63 */ #define SD_CMD_ERASE_GRP_END 36 /*!< CMD36 = 0x64 */ #define SD_CMD_UNTAG_ERASE_GROUP 37 /*!< CMD37 = 0x65 */ #define SD_CMD_ERASE 38 /*!< CMD38 = 0x66 */ #define SD_CMD_READ_OCR 58 /*!< CMD58 */ #define SD_CMD_APP_CMD 55 /*!< CMD55 返回0x01*/ #define SD_ACMD_SD_SEND_OP_COND 41 /*!< ACMD41 返回0x00*/ typedef enum { /** * @brief SD reponses and error flags */ SD_RESPONSE_NO_ERROR = (0x00), SD_IN_IDLE_STATE = (0x01), SD_ERASE_RESET = (0x02), SD_ILLEGAL_COMMAND = (0x04), SD_COM_CRC_ERROR = (0x08), SD_ERASE_SEQUENCE_ERROR = (0x10), SD_ADDRESS_ERROR = (0x20), SD_PARAMETER_ERROR = (0x40), SD_RESPONSE_FAILURE = (0xFF), /** * @brief Data response error */ SD_DATA_OK = (0x05), SD_DATA_CRC_ERROR = (0x0B), SD_DATA_WRITE_ERROR = (0x0D), SD_DATA_OTHER_ERROR = (0xFF) } SD_ERROR; //SD卡的类型 #define SD_TYPE_NOT_SD 0 //非SD卡 #define SD_TYPE_V1 1 //V1.0的卡 #define SD_TYPE_V2 2 //SDSC #define SD_TYPE_V2HC 4 //SDHC extern uint8_t SD_Type; void sd_power_on(void); SD_ERROR set_sd_to_idle_state(void); SD_ERROR get_sd_card_type(void); #endif /* __SD_SPI_DRV_H__ */ #include "./sd_nand/sd_spi_drv.h" #include "./spi/bsp_spi.h" #define SD_SPI SPI1 #define SD_DUMMY_BYTE 0xFF uint8_t SD_Type = 0; static uint8_t _spi_read_write_byte(uint8_t data) { while(SPI_I2S_GetFlagStatus(SD_SPI, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SD_SPI, data); while(SPI_I2S_GetFlagStatus(SD_SPI, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SD_SPI); } static void sd_send_cmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t data = {0}; /* command bit7 is always 1, bit6 is always 0, see SD manual. */ data &= ~(0x80); data = cmd | 0x40; 24); 16); 8); data = (uint8_t)(arg); data = crc; for (int i = 0; i < 6; i ++) _spi_read_write_byte(data ); } static uint8_t sd_read_response(uint8_t response) { uint32_t repeat = 0xfff; while (repeat --) { if (_spi_read_write_byte(SD_DUMMY_BYTE) == response) break; } if (repeat) return SD_RESPONSE_NO_ERROR; else return SD_RESPONSE_FAILURE; } void sd_power_on(void) { set_sd_spi_cs_pin(PIN_HIGH); uint32_t i = 0; for (i = 0; i <= 9; i++) { _spi_read_write_byte(SD_DUMMY_BYTE); } } SD_ERROR set_sd_to_idle_state(void) { uint32_t repeat = 0xfff; set_sd_spi_cs_pin(PIN_LOW); sd_send_cmd(SD_CMD_GO_IDLE_STATE, 0, 0x95); if (sd_read_response(SD_IN_IDLE_STATE)) //查询卡是否处于空闲状态 return SD_RESPONSE_FAILURE; set_sd_spi_cs_pin(PIN_HIGH); _spi_read_write_byte(SD_DUMMY_BYTE); //释放卡 if (repeat == 0) return SD_RESPONSE_FAILURE; else return SD_RESPONSE_NO_ERROR; } 5.4 识别过程 SD nand的识别过程颇为复杂,需要参考下图所示状态机。 其复杂的原因是,随着科技的发展,SD卡也迭代了好几轮,但是协议需要兼容所有版本的卡,因此看上去会复杂很多。 我们采用的SD nand 型号为 CSNPGCR01-AOW,为V2.0.0的卡,且容量为1Gb,因此整体识别路线为中间那条线路。 5.4.1 识别流程说明 V2.0卡识别流程: 1.SD nand上电首先完成初始化,并发送CMD0配置为SPI模式 2.之后发送CMD8命令,读取R7响应,判断SD nand的版本 如果响应值为0x01则判断为V2.0的卡(此时是这个) 如果响应值非0x01则需要进一步判断时V1.0的卡还是MMC卡 3.发送循环指令CMD55+ACMD41,(CMD55用来表示后面的CMD41为ACMD命令),读取R1响应,直到响应0x00表示SD 2.0卡初始化完成 4.发送CMD58命令,读取R3响应,R3中包含OCR寄存器的值,OCR寄存器的第31位(bit30)描述了此卡类型是否为SDHC类型,根据此位判断此卡属于标准容量卡还是高容量卡 V1.0卡识别流程: 1.SD nand上电首先完成初始化,并发送CMD0配置为SPI模式 2.之后发送CMD8命令判断SD nand的版本 如果响应值为0x01则判断为V2.0的卡 如果响应值非0x01则需要进一步判断时V1.0的卡还是MMC卡(此时是这个) 3.发送CMD58命令,并判断响应值R3,如果没有返回则不是SD V1.0的卡 4.发送ACMD41(argument为置0),并判断R1响应值,直到卡空闲 关于CMD8指令,此处重点说明: CMD8命令的参数中主要包含两个部分,Voltage Supplied(VHS)和check pattern,发送CMD8时,VHS参数应设置为主机支持的电压范围,我们的控制器通常是3.3V,因此此处设置为0001b; check pattern可以设置为任意值,当SD nand(SD卡)接收到此CMD8指令之后会返回R7响应,如果SD nand支持此电压等级,SD nand会回显 VHS 和check pattern的内容在R7中,如果SD nand不支持此电压等级,SD nand将不会返回,并始终保持在空闲状态。 5.4.2 代码实现 SD nand识别代码如下: SD_ERROR get_sd_card_type(void) { uint32_t i = 0; uint32_t count = 0xFFF; uint8_t R7R3_Resp ; uint8_t R1_Resp; set_sd_spi_cs_pin(PIN_HIGH); _spi_read_write_byte(SD_DUMMY_BYTE); set_sd_spi_cs_pin(PIN_LOW); sd_send_cmd(SD_CMD_SEND_IF_COND, 0x1AA, 0x87); /*!< Check if response is got or a timeout is happen */ while (( (R1_Resp = _spi_read_write_byte(SD_DUMMY_BYTE)) == 0xFF) && count) { count--; } if (count == 0) { /*!< After time out */ return 1; } //响应 = 0x05 非V2.0的卡 if(R1_Resp == (SD_IN_IDLE_STATE|SD_ILLEGAL_COMMAND)) { /*Activates the card initialization process*/ count = 0xfff; do { set_sd_spi_cs_pin(PIN_HIGH); _spi_read_write_byte(SD_DUMMY_BYTE); set_sd_spi_cs_pin(PIN_LOW); /*!< 发送CMD1完成V1 版本卡的初始化 */ sd_send_cmd(SD_CMD_SEND_OP_COND, 0, 0xFF); /*!< Wait for no error Response (R1 Format) equal to 0x00 */ if (sd_read_response(SD_RESPONSE_NO_ERROR)) break; } while (count --); if (count == 0) { return 2; } SD_Type = SD_TYPE_V1; //不处理MMC卡 //初始化正常 } else if (R1_Resp == 0x01) { //响应 0x01 V2.0的卡 /*!< 读取CMD8 的R7响应 */ for (i = 0; i < 4; i++) { R7R3_Resp = _spi_read_write_byte(SD_DUMMY_BYTE); } set_sd_spi_cs_pin(PIN_HIGH); _spi_read_write_byte(SD_DUMMY_BYTE); set_sd_spi_cs_pin(PIN_LOW); if(R7R3_Resp ==0x01 && R7R3_Resp ==0xAA) { //判断该卡是否支持2.7-3.6V电压 count = 200; //支持电压范围,可以操作 do { //发卡初始化指令CMD55+ACMD41 sd_send_cmd(SD_CMD_APP_CMD, 0, 0xFF); //CMD55,以强调下面的是ACMD命令 if (sd_read_response(SD_RESPONSE_NO_ERROR)) // SD_IN_IDLE_STATE return 3; //超时返回 sd_send_cmd(SD_ACMD_SD_SEND_OP_COND, 0x40000000, 0xFF); //ACMD41命令带HCS检查位 if (sd_read_response(SD_RESPONSE_NO_ERROR)) break; }while(count--); if(count == 0) return 4; //重试次数超时 //初始化指令完成,读取OCR信息,CMD58 //鉴别SDSC SDHC卡类型开始 count = 200; do { set_sd_spi_cs_pin(PIN_HIGH); _spi_read_write_byte(SD_DUMMY_BYTE); set_sd_spi_cs_pin(PIN_LOW); sd_send_cmd(SD_CMD_READ_OCR, 0, 0xFF); if (!sd_read_response(SD_RESPONSE_NO_ERROR)) break; } while (count--); if(count == 0) return 5; //重试次数超时 //响应正常,读取R3响应 /*!< 读取CMD58的R3响应 */ for (i = 0; i < 4; i++) { R7R3_Resp = _spi_read_write_byte(SD_DUMMY_BYTE); } //检查接收到OCR中的bit30(CCS) //CCS = 0:SDSC CCS = 1:SDHC if(R7R3_Resp &0x40) { //检查CCS标志 { SD_Type = SD_TYPE_V2HC; } else { SD_Type = SD_TYPE_V2; } //鉴别SDSC SDHC版本卡的流程结束 } } set_sd_spi_cs_pin(PIN_HIGH); _spi_read_write_byte(SD_DUMMY_BYTE); //初始化正常返回 return SD_RESPONSE_NO_ERROR; } 5.3 数据传输 在完成卡识别之后,便进入了数据传输过程,在输出传输过程内即可完成数据的读写操作。 SD NAND单个块为512字节,擦除、读写都是以块为单位进行的,而且SD NAND可以直接写入,不需要先擦除才能写入!!!牛逼Plus吧!哈哈! 5.3.1 数据写入 数据分为单块写入和多块写入,多块写入可循环执行多块写入实现。单个块写入使用CMD24,多个块写入使用CMD25,注意此处,SD nand的操作与SD卡可能会有所不一样,在对应位置有详细描述。 单块写入步骤如下: 1.发送CMD24,读取响应值R1,判断卡无错误 2.发送写开始指令 0xFE(SD协议中未找到此描述,此应该是SD nand所特有) 3.依次传输写入数据 4.发送两个字节的CRC校验,由于SPI默认没有开启CRC,因此填充为0xFFFF 5.读取卡的状态判断是否有误,结束 5.3.2 数据读取 数据读取也分为单块读取和多块读取,多块读取可采用循环执行单块读取逻辑实现。 单块数据读取步骤如下: 1.发送CMD17,读取响应值R1,判断有无错误 2.等待SD nand发送数据输出开始标志 0xFE 3.依次读取数据 4.多读取两位CRC值,结束 5.3.3 代码实现 #define SD_START_DATA_SINGLE_BLOCK_READ 0xFE /*!< Data token start byte, Start Single Block Read */ #define SD_START_DATA_MULTIPLE_BLOCK_READ 0xFE /*!< Data token start byte, Start Multiple Block Read */ #define SD_START_DATA_SINGLE_BLOCK_WRITE 0xFE /*!< Data token start byte, Start Single Block Write */ #define SD_START_DATA_MULTIPLE_BLOCK_WRITE 0xFD /*!< Data token start byte, Start Multiple Block Write */ #define SD_STOP_DATA_MULTIPLE_BLOCK_WRITE 0xFD /*!< Data toke stop byte, Stop Multiple Block Write */ SD_ERROR sd_write_block(uint8_t* pbuf, uint64_t addr, uint16_t size) { uint32_t i = 0; SD_ERROR ret = SD_RESPONSE_FAILURE; //SDHC卡块大小固定为512,且写命令中的地址的单位是sector if (SD_Type == SD_TYPE_V2HC) { size = 512; addr /= 512; } /*!< SD chip select low */ set_sd_spi_cs_pin(PIN_LOW); /*!< Send CMD24 (SD_CMD_WRITE_SINGLE_BLOCK) to write multiple block */ sd_send_cmd(SD_CMD_WRITE_SINGLE_BLOCK, addr, 0xFF); /*!< Check if the SD acknowledged the write block command: R1 response (0x00: no errors) */ if (!sd_read_response(SD_RESPONSE_NO_ERROR)) { /*!< Send a dummy byte */ _spi_read_write_byte(SD_DUMMY_BYTE); /*!< Send the data token to signify the start of the data */ _spi_read_write_byte(SD_START_DATA_SINGLE_BLOCK_WRITE); /*!< Write the block data to SD : write count data by block */ for (i = 0; i < size; i++) { /*!< Send the pointed byte */ _spi_read_write_byte(*pbuf); /*!< Point to the next location where the byte read will be saved */ pbuf++; } /*!< Put CRC bytes (not really needed by us, but required by SD) */ _spi_read_write_byte(SD_DUMMY_BYTE); _spi_read_write_byte(SD_DUMMY_BYTE); /*!< Read data response */ if (sd_get_data_response == SD_DATA_OK) { ret = SD_RESPONSE_NO_ERROR; } } /*!< SD chip select high */ set_sd_spi_cs_pin(PIN_HIGH); /*!< Send dummy byte: 8 Clock pulses of delay */ _spi_read_write_byte(SD_DUMMY_BYTE); /*!< Returns the reponse */ return ret; } SD_ERROR sd_read_block(uint8_t* pbuf, uint64_t addr, uint16_t size) { uint32_t i = 0; SD_ERROR ret = SD_RESPONSE_FAILURE; //SDHC卡块大小固定为512,且读命令中的地址的单位是sector if (SD_Type == SD_TYPE_V2HC) { size = 512; addr /= 512; } /*!< SD chip select low */ set_sd_spi_cs_pin(PIN_LOW); /*!< Send CMD17 (SD_CMD_READ_SINGLE_BLOCK) to read one block */ sd_send_cmd(SD_CMD_READ_SINGLE_BLOCK, addr, 0xFF); /*!< Check if the SD acknowledged the read block command: R1 response (0x00: no errors) */ if (!sd_read_response(SD_RESPONSE_NO_ERROR)) { /*!< Now look for the data token to signify the start of the data */ if (!sd_read_response(SD_START_DATA_SINGLE_BLOCK_READ)) { /*!< Read the SD block data : read NumByteToRead data */ for (i = 0; i < size; i++) { /*!< Save the received data */ *pbuf = _spi_read_write_byte(SD_DUMMY_BYTE); /*!< Point to the next location where the byte read will be saved */ pbuf++; } /*!< Get CRC bytes (not really needed by us, but required by SD) */ _spi_read_write_byte(SD_DUMMY_BYTE); _spi_read_write_byte(SD_DUMMY_BYTE); /*!< Set response value to success */ ret = SD_RESPONSE_NO_ERROR; } } /*!< SD chip select high */ set_sd_spi_cs_pin(PIN_HIGH); /*!< Send dummy byte: 8 Clock pulses of delay */ _spi_read_write_byte(SD_DUMMY_BYTE); /*!< Returns the reponse */ return ret; }此外,为了验证以上代码正常运行,编写简单测试程序进行测试,代码如下: int main(void) { USART1_Config; LED_GPIO_Config; sd_spi_config; printf("sd card test!\n"); sd_init; uint8_t tx_data = {0}; uint8_t rx_data = {0}; for (i = 0; i < 512; i ++) tx_data = 512-i; sd_write_block(tx_data, 0, sizeof(tx_data)); sd_read_block(rx_data, 0, sizeof(rx_data)); for (i = 0; i < 512; i ++) { if (tx_data != rx_data ) break; printf("%d ", rx_data ); } if (i == 512) { printf("sd card 读写测试成功\n"); } else { printf("sd card 读写测试失败, i:%d\n", i); } }代码运行如下,测试通过: 6. 总结 综上,便是关于使用SPI接口驱动SD nand的全部说明了,确实花费了不少时间整理说明,关于SD nand的驱动玩法还有很多,比如采用SD接口驱动,移植文件系统,导入日志系统等等,后续有机会有时间我也会继续做整理分享。 希望本篇博文能帮助到你对于如何使用SPI实现SD nand的驱动也有大致清晰的了解。
  • 热度 9
    2023-2-17 17:08
    452 次阅读|
    0 个评论
    一、前言 在STM32项目开发中,经常会用到存储芯片存储数据。 比如:关机时保存机器运行过程中的状态数据,上电再从存储芯片里读取数据恢复;在存储芯片里也会存放很多资源文件。比如,开机音乐,界面上的菜单图标,字库文件,方便设备开机加载。 为了让单片机更加方便的读写这些资源文件,通常都会加文件系统,如果没有文件系统,直接读取写扇区的方式,对数据不好管理。 这篇文章就手把手教大家,在STM32上完成FATFS文件系统的移植;主控芯片采用STM32F103ZET6, 存储芯片我这里采用(雷龙) CS创世 SD NAND 。 SD NAND 简单来说就是贴片式SD卡,使用起来与普通的SD卡一样,简单的区别就是:比TF卡稳定,比eMMC便宜。 下面章节里会详细介绍下 CS创世 SD NAND。 下面是CS创世 SD NAND 与STM32开发的板的接线实物图: 这是读写扇区测试的结果: 二、SD NAND 介绍 我当前使用的SD NAND型号是,CSNP32GCR01-AOW,容量是4GB。 下面是通过编写STM32代码读取的存储信息: Card Type:SDHC V2.0 Card ManufacturerID:102 Card RCA:5000 Card Capacity:3696 MB Card BlockSize:512 芯片的详细参数如下: 【1】不用写驱动程序自带坏块管理 【2】尺寸小巧,简单易用,兼容性强,稳定可靠,固件可定制,LGA-8封装 【3】标准SDIO接口,兼容SPI,兼容拔插式TF卡/SD卡,可替代普通TF卡/SD卡 【4】尺寸6.2x8mm,直接贴片,不占空间 【5】内置平均读写算法,通过1万次随机掉电测试 【6】耐高低温,机贴手贴都非常方便 【7】速度级别Class10(读取速度23.5MB/S写入速度12.3MB/S) 【8】支持标准的SD 2.0协议,用户可以直接移植标准驱动代码,省去了驱动代码编程环节。支持TF卡启动的SOC都可以用SD NAND 【9】比TF卡稳定,比eMMC便宜 **下面是芯片的实物图: ** 这是官网申请的样品,焊接了转接板,可以直接插在SD卡卡槽上测试。 最终选型之后,设计PCB板时,设计接口,直接贴片上去使用,非常稳定,抖动也不会导致,外置卡TF卡这种容易松动的问题。 这是雷龙的官网: http://www.longsto.com/product/35.html 三、编写SD NAND驱动代码 SD NAND 的驱动代码与正常的SD卡协议是一样的,支持标准的SD 2.0协议,下面我就直接贴出写好的驱动代码。 包括了模拟SPI,硬件SPI,SDIO等3种方式,完成对SD NAND 的读写。我当前使用的主控板子是STM32F103ZET6,如果你使用的板子不是这一款,可能还是其他的CPU也没关系;我这里直接贴出了SPI模拟时序的驱动代码,可以直接移植到任何单片机上使用,代码拷贝过去也只需要修改GPIO口即可,非常方便。 3.1 SPI模拟时序驱动方式 (1)整体工程代码 这是当前工程的截图: 代码采用寄存器风格编写,非常简洁。 当前工程完成SD NAND卡初始化,扇区的读写,测试芯片基本的使用情况。 (2) sd.c #include "sdcard.h" static u8 SD_Type=0; //存放SD卡的类型 /* 函数功能:SD卡底层接口,通过SPI时序向SD卡读写一个字节 函数参数:data是要写入的数据 返 回 值:读到的数据 */ u8 SDCardReadWriteOneByte(u8 DataTx) { u8 i; u8 data=0; for(i=0;i<8;i++) { SDCARD_SCK=0; if(DataTx&0x80)SDCARD_MOSI=1; else SDCARD_MOSI=0; SDCARD_SCK=1; DataTx<<=1; data<<=1; if(SDCARD_MISO)data|=0x01; } return data; } //4种: 边沿两种、电平是两种 /* 函数功能:底层SD卡接口初始化 本程序SPI接口如下: PC11 片选 SDCardCS PC12 时钟 SDCardSCLK PD2 输出 SPI_MOSI--主机输出从机输入 PC8 输入 SPI_MISO--主机输入从机输出 */ void SDCardSpiInit(void) { /*1. 开启时钟*/ APB2ENR|=1<<5; //使能PORTD时钟 APB2ENR|=1<<4; //使能PORTC时钟 /*2. 配置GPIO口模式*/ CRH&=0xFFF00FF0; CRH|=0x00033008; CRL&=0xFFFFF0FF; CRL|=0x00000300; /*3. 上拉*/ ODR|=1<<8; ODR|=1<<11; ODR|=1<<12; ODR|=1<<2; } /* 函数功能:取消选择,释放SPI总线 */ void SDCardCancelCS(void) { SDCARD_CS=1; SDCardReadWriteOneByte(0xff);//提供额外的8个时钟 } /* 函数 功 能:选择sd卡,并且等待卡准备OK 函数返回值:0,成功;1,失败; */ void SDCardSelectCS(void) { SDCARD_CS=0; SDCardWaitBusy();//等待成功 } /* 函数 功 能:等待卡准备好 函数返回值:0,准备好了;其他,错误代码 */ void SDCardWaitBusy(void) { while(SDCardReadWriteOneByte(0XFF)!=0XFF){} } /* 函数功能:等待SD卡回应 函数参数: Response:要得到的回应值 返 回 值: 0,成功得到了该回应值 其他,得到回应值失败 */ u8 SDCardGetAck(u8 Response) { u16 Count=0xFFFF;//等待次数 while((SDCardReadWriteOneByte(0XFF)!=Response)&&Count)Count--;//等待得到准确的回应 if(Count==0)return SDCard_RESPONSE_FAILURE;//得到回应失败 else return SDCard_RESPONSE_NO_ERROR;//正确回应 } /* 函数功能:从sd卡读取一个数据包的内容 函数参数: buf:数据缓存区 len:要读取的数据长度. 返回值: 0,成功;其他,失败; */ u8 SDCardRecvData(u8*buf,u16 len) { if(SDCardGetAck(0xFE))return 1;//等待SD卡发回数据起始令牌0xFE while(len--)//开始接收数据 { *buf=SDCardReadWriteOneByte(0xFF); buf++; } //下面是2个伪CRC(dummy CRC) SDCardReadWriteOneByte(0xFF); SDCardReadWriteOneByte(0xFF); return 0;//读取成功 } /* 函数功能:向sd卡写入一个数据包的内容 512字节 函数参数: buf 数据缓存区 cmd 指令 返 回 值:0表示成功;其他值表示失败; */ u8 SDCardSendData(u8*buf,u8 cmd) { u16 t; SDCardWaitBusy(); //等待忙状态 SDCardReadWriteOneByte(cmd); if(cmd!=0XFD)//不是结束指令 { for(t=0;t<512;t++)SDCardReadWriteOneByte(buf );//提高速度,减少函数传参时间 SDCardReadWriteOneByte(0xFF); //忽略crc SDCardReadWriteOneByte(0xFF); t=SDCardReadWriteOneByte(0xFF); //接收响应 if((t&0x1F)!=0x05)return 2; //响应错误 } return 0;//写入成功 } /* 函数功能:向SD卡发送一个命令 函数参数: u8 cmd 命令 u32 arg 命令参数 u8 crc crc校验值 返回值:SD卡返回的响应 */ u8 SendSDCardCmd(u8 cmd, u32 arg, u8 crc) { u8 r1; SDCardCancelCS(); //取消上次片选 SDCardSelectCS(); //选中SD卡 //发送数据 SDCardReadWriteOneByte(cmd | 0x40);//分别写入命令 24); 16); 8); SDCardReadWriteOneByte(arg); SDCardReadWriteOneByte(crc); if(cmd==SDCard_CMD12)SDCardReadWriteOneByte(0xff);//Skip a stuff byte when stop reading do { r1=SDCardReadWriteOneByte(0xFF); }while(r1&0x80); //等待响应,或超时退出 return r1; //返回状态值 } /* 函数功能:获取SD卡的CID信息,包括制造商信息 函数参数:u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 */ u8 GetSDCardCISDCardOutnfo(u8 *cid_data) { u8 r1; //发SDCard_CMD10命令,读CID r1=SendSDCardCmd(SDCard_CMD10,0,0x01); if(r1==0x00) { r1=SDCardRecvData(cid_data,16);//接收16个字节的数据 } SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; } /* 函数说明: 获取SD卡的CSD信息,包括容量和速度信息 函数参数: u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 */ u8 GetSDCardCSSDCardOutnfo(u8 *csd_data) { u8 r1; r1=SendSDCardCmd(SDCard_CMD9,0,0x01); //发SDCard_CMD9命令,读CSD if(r1==0) { r1=SDCardRecvData(csd_data, 16);//接收16个字节的数据 } SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; } /* 函数功能:获取SD卡的总扇区数(扇区数) 返 回 值: 0表示容量检测出错,其他值表示SD卡的容量(扇区数/512字节) 说 明: 每扇区的字节数必为512字节,如果不是512字节,则初始化不能通过. */ u32 GetSDCardSectorCount(void) { u8 csd ; u32 Capacity; u16 csize; if(GetSDCardCSSDCardOutnfo(csd)!=0) return 0; //取CSD信息,如果期间出错,返回0 if((csd &0xC0)==0x40) //SDHC卡,按照下面方式计算 { csize = csd + ((u16)csd << 8) + 1; Capacity = (u32)csize << 10;//得到扇区数 } return Capacity; } /* 函数功能: 初始化SD卡 返 回 值: 非0表示初始化失败! */ u8 SDCardDeviceInit(void) { u8 r1; // 存放SD卡的返回值 u8 buf ; u16 i; SDCardSpiInit();//初始化底层IO口 for(i=0;i<10;i++)SDCardReadWriteOneByte(0xFF); //发送最少74个脉冲 do { r1=SendSDCardCmd(SDCard_CMD0,0,0x95);//进入IDLE状态 闲置 }while(r1!=0X01); SD_Type=0; //默认无卡 if(r1==0X01) { if(SendSDCardCmd(SDCard_CMD8,0x1AA,0x87)==1) //SD V2.0 { for(i=0;i<4;i++)buf =SDCardReadWriteOneByte(0XFF); if(buf ==0X01&&buf ==0XAA) //卡是否支持2.7~3.6V { do { SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0x40000000,0X01);//发送SDCard_CMD41 }while(r1); if(SendSDCardCmd(SDCard_CMD58,0,0X01)==0)//鉴别SD2.0卡版本开始 { for(i=0;i<4;i++)buf =SDCardReadWriteOneByte(0XFF);//得到OCR值 if(buf &0x40)SD_Type=SDCard_TYPE_V2HC; //检查CCS else SD_Type=SDCard_TYPE_V2; } } } } printf("SD_Type=0x%X\r\n",SD_Type); SDCardCancelCS(); //取消片选 if(SD_Type)return 0; //初始化成功返回0 else if(r1)return r1; //返回值错误值 return 0xaa; //其他错误 } /* 函数功能:读SD卡 函数参数: buf:数据缓存区 sector:扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 */ u8 SDCardReadData(u8*buf,u32 sector,u32 cnt) { u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector<<=9;//转换为字节地址 if(cnt==1) { r1=SendSDCardCmd(SDCard_CMD17,sector,0X01);//读命令 if(r1==0) //指令发送成功 { r1=SDCardRecvData(buf,512); //接收512个字节 } }else { r1=SendSDCardCmd(SDCard_CMD18,sector,0X01);//连续读命令 do { r1=SDCardRecvData(buf,512);//接收512个字节 buf+=512; }while(--cnt && r1==0); SendSDCardCmd(SDCard_CMD12,0,0X01); //发送停止命令 } SDCardCancelCS();//取消片选 return r1;// } /* 函数功能:向SD卡写数据 函数参数: buf:数据缓存区 sector:起始扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 */ u8 SDCardWriteData(u8*buf,u32 sector,u32 cnt) { u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector *= 512;//转换为字节地址 if(cnt==1) { r1=SendSDCardCmd(SDCard_CMD24,sector,0X01);//读命令 if(r1==0)//指令发送成功 { r1=SDCardSendData(buf,0xFE);//写512个字节 } } else { if(SD_Type!=SDCard_TYPE_MMC) { SendSDCardCmd(SDCard_CMD55,0,0X01); SendSDCardCmd(SDCard_CMD23,cnt,0X01);//发送指令 } r1=SendSDCardCmd(SDCard_CMD25,sector,0X01);//连续读命令 if(r1==0) { do { r1=SDCardSendData(buf,0xFC);//接收512个字节 buf+=512; }while(--cnt && r1==0); r1=SDCardSendData(0,0xFD);//接收512个字节 } } SDCardCancelCS();//取消片选 return r1;// } (3) sd.h #ifndef SD_H #define SD_H_ #include "stm32f10x.h" #include "led.h" #include "usart.h" /*---------------------------------------------- 本程序SPI接口如下: PC11 片选 SDCardCS PC12 时钟 SDCardSCLK PD2 输出 SPI_MOSI--主机输出从机输入 PC8 输入 SPI_MISO--主机输入从机输出 ------------------------------------------------*/ #define SDCARD_CS PCout(11) #define SDCARD_SCK PCout(12) #define SDCARD_MOSI PDout(2) #define SDCARD_MISO PCin(8) // SD卡类型定义 #define SDCard_TYPE_ERR 0X00 //卡类型错误 #define SDCard_TYPE_MMC 0X01 //MMC卡 #define SDCard_TYPE_V1 0X02 #define SDCard_TYPE_V2 0X04 #define SDCard_TYPE_V2HC 0X06 // SD卡指令表 #define SDCard_CMD0 0 //卡复位 #define SDCard_CMD1 1 #define SDCard_CMD8 8 //命令8 ,SEND_IF_COND #define SDCard_CMD9 9 //命令9 ,读CSD数据 #define SDCard_CMD10 10 //命令10,读CID数据 #define SDCard_CMD12 12 //命令12,停止数据传输 #define SDCard_CMD13 16 //命令16,设置扇区大小 应返回0x00 #define SDCard_CMD17 17 //命令17,读扇区 #define SDCard_CMD18 18 //命令18,读Multi 扇区 #define SDCard_CMD23 23 //命令23,设置多扇区写入前预先擦除N个block #define SDCard_CMD24 24 //命令24,写扇区 #define SDCard_CMD25 25 //命令25,写多个扇区 #define SDCard_CMD41 41 //命令41,应返回0x00 #define SDCard_CMD55 55 //命令55,应返回0x01 #define SDCard_CMD58 58 //命令58,读OCR信息 #define SDCard_CMD59 59 //命令59,使能/禁止CRC,应返回0x00、 /*SD卡回应标记字*/ #define SDCard_RESPONSE_NO_ERROR 0x00 //正确回应 #define SDCard_SD_IN_IDLE_STATE 0x01 //闲置状态 #define SDCard_SD_ERASE_RESET 0x02 //擦除复位 #define SDCard_RESPONSE_FAILURE 0xFF //响应失败 //函数声明 u8 SDCardReadWriteOneByte(u8 data); //底层接口,SPI读写字节函数 void SDCardWaitBusy(void); //等待SD卡准备 u8 SDCardGetAck(u8 Response); //获得应答 u8 SDCardDeviceInit(void); //初始化 u8 SDCardReadData(u8*buf,u32 sector,u32 cnt); //读块(扇区) u8 SDCardWriteData(u8*buf,u32 sector,u32 cnt); //写块(扇区) u32 GetSDCardSectorCount(void); //读扇区数 u8 GetSDCardCISDCardOutnfo(u8 *cid_data); //读SD卡CID u8 GetSDCardCSSDCardOutnfo(u8 *csd_data); //读SD卡CSD #endif (4)运行效果