第 03 节 调试 SD 卡 接口
ESP32-S3 内置一个 SD/MMC 主机控制器(SDHOST),该控制器支持 SD Memory、SDIO、MMC / eMMC 和 CE-ATA 设备,可工作在 1-bit / 4-bit / 8-bit 总线模式。
在本产品 ESP32-S3-Touch-LCD-7 当中,我们集成了一个 Micro SD 的卡槽,采用 SPI 的通信方式,可以占用更少的 GPIO 引脚。在下面的教程连接当中,我们将了解到 SD 卡的连接以及调用方式,方便用户快递验证 SD 卡的功能。

硬件设计
在本产品当中我们将使用 ESP32-S3 芯片控制 SPI 通信接口的 SD 卡,分别定义 IO11,IO12,IO13 为 MOSI, SCK, MISO 引脚与 SD 卡进行连接。并通过定义 IO8, IO9 为 I2C 的 SDA, SCL 引脚与扩展芯片 CH442G 芯片进行通信,用扩展出来的 EXIO4 引脚作为 SD 卡的片选引脚 SDCS。
以下是上述设计的一个简易的电路连接图,更详细的电路原理图请参考 原理图。

元件清单
- ESP32-S3-Touch-LCD-7 x1
- USB 线 type A 公口 转 Type C 公口 x1
- Micro SD 卡 x1

本实验需要用到一张 Micro SD 卡(不包含在套件内,请单独购买噢),使用前请将 SD 卡用读卡器插入到电脑,将 SD 卡格式化成 FAT32 格式再进行使用。
硬件连接
为了将对应的代码上传到 ESP32-S3,需要用 Type-C 转 Type-A 的线将 ESP32-S3-Touch-LCD-7 的 USB 口接到电脑的 USB 口,并且将格式化好的 SD 卡插到 SD 卡槽上:

示例代码
请先从以下地址下载例程包:ESP32-S3-Touch-LCD-7 示例程序 (如果先前已经下载,直接打开对应文件夹即可) 将例程压缩包解压之后,打开对应的 03_SD_Test 文件夹

双击打开 03_SD_Test.ino:

在上传程序之前需要选择好主板的型号 Waveshare ESP32-S3-Touch-LCD-7 和对应的端口:

设置开发板参数,在 Flash Size 当中选择 8MB(64Mb)。

接着点击左上角的 Upload 按键,等待程序上传完成即可。

运行结果
代码上传后,在菜单栏的工具当中打开串口监视器,重新运行程序可以看到串口打印结果,代码会依次执行创建文件、读取文件、编辑文件、修改文件名字等各项操作,还会输出文件传输的速度结果和 SD 卡的容量情况。

如果将 Micro SD 卡用读卡器接回电脑,还可以看到两个文件 foo.txt 和 test.txt 如下,点开 foo.txt 里面可以看到有 Hello World 的文字信息,代表上面的操作都正常执行了。
-
下载程序时如果遇到跟库相关的报错,注意查看下 Arduino IDE 首选项项目文件夹位置是否正常索引到 ESP32-S3-Touch-LCD-7-Demo\Arduino 的目录下。
-
用 USB 口下载程序后如果串口监视器没有看到输出,可以在工具选项当中将 USB CDC On Boot 进行使能,重新下载程序之后即可看到 USB 口的输出。

代码回顾
本例程包含 SD_Test.ino 测试例程文件,和库文件 waveshare_sd_card.cpp 以及 waveshare_sd_card.h,下面分别对这三个文件的关键部分做讲解。
1. 在 wabeshare_sd_card.h 当中分别作出了各个引脚的定义和函数声明
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <esp_io_expander.hpp>
包含了四个库的头文件,包括 esp32 自带的 FS 文件系统库、SD 库和 SPI 库,以及 CH422G 芯片需要用到的 esp_io_expander 库。
#define TP_RST 1 // Touch screen reset pin
#define LCD_BL 2 // LCD backlight pinout
#define LCD_RST 3 // LCD reset pin
#define SD_CS 4 // SD card select pin
#define USB_SEL 5 // USB select pin
定义了扩展芯片 CH422G 1~5 号引脚对应的硬件功能,由这 5 个扩展引脚分别去控制 LCD、SD、USB 等多个外设
- 当你的主控(比如 ESP32、STM32)IO 不够用了,可以用一个扩展芯片例如 CH422G,在不占用太多引脚的情况下,扩展出更多的输入/输出口。例如:ESP32 要同时驱动多个 SD 卡模块、传感器、屏幕,但片选 CS 引脚不够用,这时可以用 CH422G 的 IO 来做“片选控制”。
- CH422G 是一款由南京沁恒(WCH)推出的 I2C 接口的 8 路 GPIO 扩展芯片 + 4 路开漏输出(OC),可以实现用 I2C 总线(SCL/SDA 两根线)来控制额外的数字 IO 引脚。在本产品当中,我们使用到了 IO1~IO6 一共 6 个扩展引脚。
#define EXAMPLE_I2C_ADDR (ESP_IO_EXPANDER_I2C_CH422G_ADDRESS)
#define EXAMPLE_I2C_SDA_PIN 8 // I2C data line pins
#define EXAMPLE_I2C_SCL_PIN 9 // I2C clock line pin
这几行代码定义了 ESP32 的 I2C 通信引脚,使用 ESP32 的 GPIO8 作为 SDA,GPIO9 作为 SCL。另外这里使用了 #define 预处理指令将 ESP_IO_EXPANDER_I2C_CH422G_ADDRESS 命名为 EXAMPLE_I2C_ADDR,让程序更容易阅读和维护。
ESP_IO_EXPANDER_I2C_CH422G_ADDRESS是库中预定义的常量 CH422G 芯片的 I2C 通信地址,可在 ESP32-S3-Touch-LCD-7-Demo\Arduino\libraries\ESP32_IO_Expander\src\port 的 esp_io_expander_ch422g.h 文件中查询到。
#define SD_MOSI 11 // SD card master output slave input pin
#define SD_CLK 12 // SD card clock pin
#define SD_MISO 13 // SD card master input slave output pin
#define SD_SS -1 // SD card select pin (not used)
这几行代码定义了 ESP32-S3 与 SD 卡进行通信的 SPI 引脚。定义 GPIO11 为 MOSI 引脚,GPIO12 为 CLK 引脚,GPIO13 为 MISO;由于 SD 的 CS 片选引脚由扩展芯片的引脚进行控制,不由 ESP32 直接控制,因此这里的 SD_SS 设置为-1,表示该引脚未被使用到。
void listDir(fs::FS &fs, const char * dirname, uint8_t levels);
void createDir(fs::FS &fs, const char * path);
void removeDir(fs::FS &fs, const char * path);
void writeFile(fs::FS &fs, const char * path, const char * message);
void appendFile(fs::FS &fs, const char * path, const char * message);
void readFile(fs::FS &fs, const char * path);
void deleteFile(fs::FS &fs, const char * path);
void renameFile(fs::FS &fs, const char * path1, const char * path2);
void testFileIO(fs::FS &fs, const char * path);
这些是 ESP32 操作文件系统(FS, File System) 时常用的基础函数声明,这些函数可以让你像在电脑上一样,对 SD 卡或 SPIFFS 文件系统里的文件进行操作(创建、读取、删除、重命名等)。具体的函数定义会在** waveshare_sd_card.cpp** 当中一一列出,这部分的函数也是由 esp32 库提供的 SD 库的例程,相当于我们在电脑上“文件管理器”的命令:
| 函数 | 类比 | 作用 |
|---|---|---|
| listDir() | 查看文件夹 | 列出指定目录下的所有文件和子目录 |
| createDir() | 新建文件夹 | 创建一个新的目录 |
| removeDir() | 删除文件夹 | 删除一个目录 |
| writeFile() | 新建并写入文件 | 往一个文件里写入内容(覆盖原内容) |
| appendFile() | 追加内容 | 在已有文件的末尾继续写入 |
| readFile() | 打开文件查看内容 | 读取文件内容并打印出来 |
| deleteFile() | 删除文件 | 删除指定文件 |
| renameFile() | 重命名文件 | 修改文件的名字 |
| testFileIO() | 测试性能 | 用来测试读写速度或稳定性 |
2. SD_Test.ino 主函数详解
开头先设置头文件与全局变量:
#include "waveshare_sd_card.h"
esp_expander::CH422G *expander = NULL;
引入封装好的驱动库 waveshare_sd_card.h;用 esp_expander::CH422G *expander = NULL 定义一个指向 CH422G 对象的指针,稍后会用 new 动态创建它;
接着开始 setup() 初始化部分,只在上电或复位时执行一次。
Serial.begin(115200);
Serial.println("Initialize IO expander");
打开串口调试输出,方便查看运行日志。
expander = new esp_expander::CH422G(EXAMPLE_I2C_SCL_PIN, EXAMPLE_I2C_SDA_PIN, EXAMPLE_I2C_ADDR);
expander->init();
expander->begin();
创建一个新的 CH422G 对象,并指定 I2C 时钟线(SCL)、数据线(SDA)和 I2C 地址。调用 init() 和 begin() 完成初始化。
Serial.println("Set the IO0-7 pin to output mode.");
expander->enableAllIO_Output();
输出 debug 信息,并配置所有引脚为输出模式。
expander->digitalWrite(TP_RST , HIGH);
expander->digitalWrite(LCD_RST , HIGH);
expander->digitalWrite(LCD_BL , HIGH);
expander->digitalWrite(SD_CS, LOW);
// SD card 的片选引脚,当设置为 LOW 时,SD 卡被选中,可以被 SPI 总线访问。
expander->digitalWrite(LCD_BL, LOW);
// 临时关闭背光以节省功耗或调试
expander->digitalWrite(USB_SEL, LOW);
// 当 USB_SEL = HIGH 时,启用 FSUSB42UMX 芯片,此时 GPIO19/20 用作 CAN_TX/CAN_RX。当 USB_SEL = LOW 时,禁用 FSUSB42UMX,可正常使用 USB。
初始化扩展引脚 IO1~IO6 的电平状态。
SPI.setHwCs(false);
SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_SS);
用 SPI.setHwCs(false) 禁用硬件自动片选; SPI.begin() 设置 SPI 四条线,其各个引脚已经在 waveshare_sd_card.h 进行了定义(这里 SD_SS 实际上由 CH422G 控制,但仍要给 SPI 指定一个占位参数)。
if (!SD.begin(SD_SS)) {
Serial.println("Card Mount Failed"); // SD card mounting failed
return;
}
用 SD.begin() 挂载 SD 卡文件系统, SD.begin() 会:
- 初始化 SPI 接口;
- 向 SD 卡发送初始化命令;
- 检测卡片存在;
- 挂载 FAT 文件系统。
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD card attached"); // No SD card connected
return;
}
Serial.print("SD Card Type: "); // SD card type
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN"); // Unknown Type
}
用 SD.cardType() 来检测 SD 的类型,并定义了用 cardType 来接收结果;接着用 if 语句判断 SD 卡是否已经连接成功,如果 SD 卡未成功连接,则打印 debug 信息并退出程序;否则挂载成功并打印 SD 卡类型。
常见类型:
- CARD_MMC:老式 MMC 卡;
- CARD_SD:标准容量 SD;
- CARD_SDHC:高容量卡;
- CARD_NONE:未检测到。
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize); // SD card size
检测 SD 卡的容量并打印结果。
上面这些步骤完成后,即可开始测试 SD 卡文件系统功能:
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");
这几行会依次执行:
- 查看根目录;
- 创建文件夹 /mydir;
- 删除刚创建的目录 /mydir;
- 写入 “Hello World” 到/hello.txt;
- 读取文件/hello.txt;
- 重命名 hello.txt 为 foo.txt;
- 测试大文件读写速度。 所有这些函数的实现在 waveshare_sd_card.h / .cpp 里,它们都是对 FS 类函数的封装。
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024)); // Total space
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024)); // Used space
接着输出 SD 卡的总容量与已使用空间。
// Main Loop
void loop() {
}
当前示例不需要重复操作,所以 void loop() 留空。