Arduino 入门学习笔记(二十四):SPI_SDCARD 实验

开发板:正点原子ESP32S3
外设: SD卡
没有LCD屏可以用串口打印进行测试
例程源码在文章顶部可免费下载

1. SD 卡介绍

1.1 SD 卡介绍

如果需要使用大量数据或保存大量数据, Arduino 自带的 EEPROM 和 FLASH 存储空间就显得捉襟见肘了。虽然说,模组内有 SPI FLASH 以及开发板上板载 EERPOM 芯片,但是对于大容量存储来说,还是不够用的,所以在这里推荐使用 SD 卡来解决大量数据存储的需求。
SD 卡, Secure Digital Card,称为安全数字卡。 SD 卡系列主要有三种: SD 卡、 Mini SD 卡和 Micro SD 卡。目前, Mini SD 卡已经被 micro SD 卡取代, micro SD 卡又被称为 TF 卡。 ESP32-S3 开发板上板载的卡槽为 TF 卡,所以实验程主要就是讲解 TF 卡。
接下来,来看一下 TF 卡实物图以及引脚定义,如下图所示:
在这里插入图片描述
TF 卡支持两种驱动方式: SPI 或 SDIO, 由于引脚资源十分紧缺,所以开发板给 TF 卡设计的是 SPI 接口。使用 SPI 去驱动 TF 卡,只需要用到 4 根数据线: CS、 MOSI、 CLK 和 MISO。
TF 的驱动会涉及到比较多的知识,但使用 Arduino 去驱动 TF 卡会比较简单,在这里我们只介绍一下使用方法,原理性东西就不多讲了。

1.2 SD 卡库介绍

本实验介绍到的函数可在以下文件中找到:
C:\Users\ 用户名 \Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\SD\src\SD.cpp
C:\Users\ 用户名 \Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\FSsrc\FS.cpp
SD.cpp 主要是 SD 卡的操作相关函数,而 FS.cpp 是提供了读/写文件功能,与串口相关函数有点相似。
在 SD.cpp 中已经定义好了 SD 对象,直接使用 SD 即可。
接下来,我们介绍一下本章节所用到的 SD 卡相关函数。
第一个函数: begin 函数,该函数功能是初始化 SD 卡。

bool SDFS::begin(uint8_t ssPin, SPIClass &spi, uint32_t frequency, const char *
mountpoint, uint8_t max_files, bool format_if_empty)

参数 ssPin 为片选引脚;
参数 spi 为 SPI 对象,因为用到 SPI 接口;
参数 frequency 为时钟频率;
参数 mountpoint 为挂载点;
参数 max_files 为文件最大同时打开数;
参数 format_if_empty 为存储卡空时是否格式化;
返回值:布尔类型。初始化成功返回 true,否则返回 false。
第二个函数: cardType 函数,该函数功能是返回存储卡类型。

sdcard_type_t SDFS::cardType()

无参数;
返回值: 0 为 CARD_NONE 未连接存储卡; 1 为 CARD_MMC 即 MMC 卡; 2 为 CARD_SD即 SD 卡(最大 2G); 3 为 CARD_SDHC 即 SDHC 卡(最大 32G); 4 为 CARD_UNKNOWN 即未知存储卡。
第三个函数: cardSize 函数,该函数功能是返回存储卡大小字节数。

uint64_t SDFS::cardSize()

无参数;
返回值为存储卡大小的字节数。
跟 cardSize 函数相似的,还有 totalBytes 和 usedBytes 函数,前者是获取文件系统大小,后者是获取文件系统已使用的大小。
从第四个函数开始介绍文件操作函数, open 函数,该函数功能是打开 SD 卡上的一个文件。

File FS::open(const String& path, const char* mode, const bool create)

参数 path 指的是需要打开的文件命名。其中可以包含路径,路径用“/”分隔。
参数 mode 为打开文件的方式, FILE_READ 为只读方式打开文件, FILE_WRITE 为写入方式打开文件, FILE_APPEND 为以添加内容方式打开文件。
参数 create 不用管,可以不用传递参数。
返回值:一个 File 类型的对象,其实就是打开的文件对象。
第五个函数: close 函数,该函数功能是关闭文件并确保数据已经被完全写入到 SD 卡中。注意:打开文件后,操作完成,最后一定需要调用 close 函数关闭文件,这样子向文件操作的内容才会被保存。

void File::close()

无参数;
无返回值。
第六个函数: write 函数,该函数功能是向文件中写入 1 字节的内容。

size_t File::write(uint8_t c)

参数 c 为写入到文件的 1 字节数据
返回值为写入的字节数。
注意: write 函数还有另外一种样式 write(const uint8_t *buf, size_t size),其中 buf 为要写入数据的缓冲区,而 size 为要写入的长度。
除了以上两种方式向文件写入内容,还可以使用 file.print 或者 file.println, 前者是输出数据到文件,后者是输出数据到文件并回车换行, 具体使用方法请参考例程。
在使用写入函数前,我们要确保要写入的文件已经被打开。
第七个函数: read 函数,该函数功能是从文件中读取 1 字节数据。

int File::read()

无参数;
返回值为读取到的数据,当为-1 即没有数据可被读取。
注意: read 函数还有另外一种样式 read(uint8_t* buf, size_t size),其中 buf 为要读取数据的缓冲区,而 size 为要读取的长度。
在使用读取函数前,我们要确保要读取的文件已经被打开。
第八个函数: available 函数,该函数的功能为检查当前文件中可读数据的字节数。

int File::available()

2. 硬件设计

2.1 例程功能

程序下载完成, 向 TF 卡槽插入 TF 卡, LCD 显示屏上显示 SD 卡容量大小。 当按下 BOOT按键时,进行 SD 卡测试。 LED 灯用来指示程序正在运行中。

2.2 硬件资源

  • LED 灯
    LED-IO1
  • 独立按键
    BOOT-IO0
  • USART0
    U0TXD-IO43 U0RXD-IO44
  • XL9555
    IIC_SDA-IO41
    IIC_SCL-IO42
  • SPILCD
    CS-IO21
    SCK-IO12
    SDA-IO11
    DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
    PWR- IO1_3(XL9555)
  • SD
    CS-IO2 SCK-IO12
    MOSI-IO11 MISO-IO13

2.3 原理图

TF 卡原理图,如下图所示:
在这里插入图片描述
注意: TF 卡的 CS 引脚与红外遥控接收器的数据引脚公用,需要分时复用。

3. 软件设计

3.1 程序流程图

.下面看看本实验的程序流程图:
在这里插入图片描述

3.2 程序解析

SPI_SDCARD 驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。 SPI_SDCARD 驱动源码包括两个文件: spi_sdcard.cpp、 spi_sdcard.h。 另外还写了文件系统操作源码 myfs.cpp 和myfs.h,这部分代码就是对文件系统操作接口进行封装,测试使用,大家自行查看。
下面我们先解析 spi_sdcard.h 的程序。 对 SD 卡相关引脚做了相关定义。

#define SD_CS_PIN 2
#define SD_MISO_PIN 13
#define SD_MOSI_PIN 11
#define SD_SCK_PIN 12

我们选择使用 IO2 作为 SD 卡的片选信号线, IO13 作为 SD 卡 SPI 接口的 MOSI 线, IO11作为 SD 卡 SPI 接口的 SCL 线, IO12 作为 SD 卡 SPI 接口的 SCK 线。
下面我们再解析 spi_sdcard.cpp 的程序,首先先来看一下初始化函数 sdcard_init,代码如下:

/**
* @brief 初始化 SD 卡
* @param 无
* @retval 返回值:0 初始化正确;其他值,初始化错误
*/
uint8_t sdcard_init(void)
{
	spi_sdcard.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
	/* 设置 SD 卡的 SPI 用到的引脚 */
	pinMode(SD_CS_PIN, OUTPUT); /* 片选引脚设置为输出 */
	if (!SD.begin(SD_CS_PIN, spi_sdcard)) /* SD 卡初始化 */
	{
		Serial.println("SD 卡初始化失败!");
		if (!SD.begin(SD_CS_PIN, spi_sdcard)) /* SD 卡初始化失败再次初始化 */
		{
			Serial.println("SD 卡初始化失败!");
			return 1;
		}
	}
	Serial.println("SD 卡初始化成功");
	uint8_t cardType = SD.cardType(); /* 获取卡的类型 */
	if (cardType == CARD_NONE) /* 未连接存储卡 */
	{
		Serial.println("没有卡连接");
		return 2;
	}
	Serial.print("SD Card Type: ");
	if (cardType == CARD_MMC) /* mmc 卡 */
	{
		Serial.println("MMC");
	}
	else if (cardType == CARD_SD) /* sd 卡,最大 2G */
	{
		Serial.println("SDSC");
	}
	else if (cardType == CARD_SDHC) /* sdhc 卡,最大 32G */
	{
		Serial.println("SDHC");
	}
	else /* 未知存储卡 */
	{
		Serial.println("UNKNOWN");
	}
	return 0;
}

在 SDCARD 初始化函数中, 首先调用 spi 接口的 begin 函数初始化 SD 卡用到的 SPI 接口,然后用 SD 接口的 begin 函数初始化 SD 卡,后面就调用 cardType 函数获取存储卡的类型。
下面介绍的是 SD 卡的测试函数,定义如下:

/**
* @brief SD 卡测试代码
* @param 无
* @retval 无
*/
void sd_test(void)
{
	myFile = SD.open("/test.txt", FILE_WRITE); /* 打开创建文件 */
	if (myFile) /* 文件成功被打开 */
	{
		Serial.print("Writing to test.txt...");
		myFile.println("testing 1, 2, 3."); /* 写数据到文件中 */
		myFile.close(); /* 关闭文件 */
		Serial.println("done.");
	}
	else /* 文件打开失败 */
	{
		Serial.println("error opening test.txt");
	}
	myFile = SD.open("/test.txt"); /* 打开前面操作的文件 */
	if (myFile)
	{
		Serial.println("test.txt:");
		while (myFile.available())
		{
			Serial.write(myFile.read()); /* 读取文件中全部内容 */
		}
		myFile.close(); /* 关闭文件 */
	}
	else /* 文件打开失败 */
	{
		Serial.println("error opening test.txt");
	}
	listDir(SD, "/", 0); /* 遍历目录下的文件 */
	createDir(SD, "/mydir"); /* 创建文件夹 */
	listDir(SD, "/", 0); /* 遍历目录下的文件 */
	removeDir(SD, "/mydir"); /* 移除文件夹 */
	listDir(SD, "/", 2); /* 遍历目录下的文件 */
	writeFile(SD, "/hello.txt", "Hello "); /* 写数据到文件中 */
	appendFile(SD, "/hello.txt", "World!\n"); /* 在文件后追加数据 */
	readFile(SD, "/hello.txt"); /* 从文件中读取数据 */
	deleteFile(SD, "/foo.txt"); /* 删除文件 */
	renameFile(SD, "/hello.txt", "/foo.txt"); /* 修改文件名字 */
	readFile(SD, "/foo.txt"); /* 从文件中读取数据 */
	testFileIO(SD, "/test.txt"); /* 测试文件 IO 性能 */
	Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
	/* 打印 SD 卡文件系统总容量大小 */
	Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
	/* 打印 SD 卡文件系统已用容量大小 */
}

21_spi_sdcard.ino 代码
在 21_spi_sdcard.ino 里面编写如下代码:

#include "led.h"
#include "key.h"
#include "uart.h"
#include "xl9555.h"
#include "spilcd.h"
#include "spi_sdcard.h"
#include <SD.h>
/**
* @brief 当程序开始执行时,将调用 setup()函数,通常用来初始化变量、函数等
* @param 无
* @retval 无
*/
void setup()
{
	led_init(); /* LED 初始化 */
	key_init(); /* KEY 初始化 */
	uart_init(0, 115200); /* 串口 0 初始化 */
	xl9555_init(); /* IO 扩展芯片初始化 */
	lcd_init(); /* LCD 初始化 */
	lcd_show_string(30, 50, 200, 16, LCD_FONT_16, "ESP32-S3", RED);
	lcd_show_string(30, 70, 200, 16, LCD_FONT_16, "SD TEST", RED);
	lcd_show_string(30, 90, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED);
	while (sdcard_init()) /* 检测不到 SD 卡 */
	{
		lcd_show_string(30, 110, 200, 16, LCD_FONT_16, "SD Card Error!", RED);
		delay(500);
		lcd_show_string(30, 110, 200, 16, LCD_FONT_16, "Please Check! ", RED);
		delay(500);
		LED_TOGGLE(); /* 红灯闪烁 */
	}
	lcd_show_string(30, 110, 200, 16, LCD_FONT_16, "SD Card OK ", BLUE);
	lcd_show_string(30, 130, 200, 16, LCD_FONT_16, "SD Card Size: MB", BLUE);
	lcd_show_num(30+13*8, 130, SD.cardSize()/(1024*1024), 5, LCD_FONT_16, BLUE);
	/* 显示 SD 卡容量, 转换成 MB 单位 */
}
/**
* @brief 循环函数,通常放程序的主体或者需要不断刷新的语句
* @param 无
* @retval 无
*/
void loop()
{
	if (KEY == 0)
	{
		sd_test();
	}
	LED_TOGGLE();
	delay(500);
}

在 setup 函数中,调用 led_init 函数完成 LED 初始化, 调用 key_init 函数完成 KEY 初始化,调用 uart_init 函数完成串口初始化,调用 xl9555_init 函数完成 XL9555 初始化,调用 lcd_init 函数完成 LCD 屏初始化,调用 sdcard_init 函数完成 SDCARD 初始化, LCD 显示实验信息和 SD卡容量。
在 loop 函数中, 通过按下 KEY 触发 SD 卡测试。 LED 灯每隔 500 毫秒状态翻转,实现闪烁效果。

4. 下载验证

假定 TF 卡已经接上去正确的位置,将程序下载到开发板后,可以看到 LED0 不停的闪烁,提示程序已经在运行了。 LCD 显示实验信息和 SD 卡容量信息如下图所示:
在这里插入图片描述
然后按下 BOOT 按键进行 SD 卡测试功能,通过串口助手查看操作信息,如下图所示。
在这里插入图片描述
把 SD 卡通过读卡器插入计算机进行查看,如下图所示:
在这里插入图片描述
可以看到, SD 卡的实际情况与程序执行的效果是一致的。 大家也可以调用测试一下别的接口函数。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐