跳转至

03 SD卡

作者:王棚嶙

一、TF卡概述

1、SD 卡

1.1 SD 卡简介

  • 定义:SD 卡(Secure Digital Card)是一种基于半导体快闪记忆器的新一代记忆设备,被广泛用于便携式设备中存储数据。
  • 特点:高存储容量、快速数据传输速度、体积小、重量轻、安全性高(支持数据加密)。

1.2 SD 卡类型与规格

  • 标准 SD 卡:原始 SD 卡规格。
  • miniSD 卡:缩小版的 SD 卡。
  • microSD 卡(又称 TF 卡):最小的 SD 卡规格,常用于智能手机和微型设备。

1.3 SD 卡工作原理

  • 文件系统:通常使用 FAT 文件系统(如 FAT16、FAT32)。
  • 通信协议:基于 SPI 协议进行数据传输。

冷知识:TF卡名字中的“T”代表“Tiny”(微型),而如今它已成为全球最小的通用存储卡标准。

2、TF卡没办法用,是怎么回事儿?

很多合宙的老朋友们的都有反馈过说自己的卡插进去了但是一直无法挂载成功怎么回事? 那么这里面就是涉及到一个兼容性的问题而TF卡兼容性是多维度的适配能力,涉及物理接口、传输协议、系统驱动及环境耐受性等。

2.1 设备兼容性

  • 工业设备支持:工业级TF卡需与工业相机、PLC控制器、机器人等专业设备兼容,确保在严苛环境下稳定工作。
  • 消费类设备:需适配智能手机、平板电脑、行车记录仪、无人机、监控摄像头等常见设备,例如:
  • 行车记录仪:实时保存高清视频数据,需支持循环录制。
  • 无人机/监控设备:保障长时间连续读写,如128G卡可支持25天1080P监控录制。
  • 扩展性要求:部分设备仅支持特定容量(如SDHC卡≤32GB,SDXC卡≥32GB),需匹配设备规范。

2.2 接口与协议兼容性

  • 速度标准:需符合设备支持的传输协议,例如:
  • UHS-I/UHS-III:提供104MB/s至624MB/s带宽,影响4K视频录制等场景的流畅性。
  • 视频速度等级(V30/V90):确保高帧率视频录制不丢帧,如V30卡可满足4K录制需求。
  • 应用性能等级(A1/A2):影响随机读写速度,A2级卡更适合安装应用或游戏(如Switch),减少加载延迟。

2.3 系统与驱动兼容性

  • 操作系统支持:
  • Linux系统:需内核驱动(如mmc模块)或用户空间驱动支持热插拔。
  • Windows/macOS/Android:需免驱即插即用,部分工业场景需定制驱动程序。
  • 文件系统适配:如FAT32/exFAT格式兼容不同设备,突发断电时需防护机制避免数据损坏。

2.4 环境与物理兼容性

  • 温度范围:
  • 工业级卡支持-25℃~85℃(如监控设备),消费级卡通常为0℃~70℃。
  • 耐用性:工业卡采用加固外壳和防震设计,适应车载、工厂等振动环境。

2.5 功能兼容性

  • 加密与安全:支持硬件加密技术,用于金融、安防等领域的数据保护。
  • 特殊功能:如Wi-Fi TF卡支持无线传输,需配套应用程序协同工作。

3、兼容性问题的常见表现与解决

  • 无法识别卡:检查驱动加载、卡槽物理损坏或文件系统错误。
  • 读写速度慢:升级高速读卡器(如支持UHS-II的型号),或更换高性能卡(如V30/A2级)。

4、我们现在测试有哪些TF卡能用

联想,闪迪,三星,三星EVO,金士顿等大品牌TF卡。所以在各位选择使用的TF卡时,我们建议不要使用白牌卡,尽量选择大品牌高速卡。

img

二、演示功能概述

本demo演示了在嵌入式环境中对TF卡(SD卡)的完整操作流程,覆盖了从文件系统挂载到高级文件操作的完整功能链。项目分为两个核心模块:

1、main.lua:主程序入口

2、tfcard_app.lua:TF卡基础应用模块,实现文件系统管理、文件操作和目录管理功能

3、http_download_file.lua:HTTP下载模块,实现网络检测与文件下载到TF卡的功能

三、准备硬件环境

注意:本功能780EPM不支持。

1、Air780EHV开发板一块(Air780EHM/780EGH/780EHV三种模块的开发板接线方式相同,这里以Air780EHV为例)

2、sim卡一张

3、TYPE-C USB数据线一根

4、AirMICROSD_1010模块一个和SD卡一张

5、Air780EHM/780EGH/780EHV开发板和数据线的硬件接线方式为

  • Air780EHV开发板通过TYPE-C USB口供电;(开发板USB旁边的开关拨到USB供电一端)

  • TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口

  • sd卡插入卡座中

6、Air780EHV开发板和AirMICROSD_1010模块接线方式

Air780EHV AirMICROSD_1010
GND(任意) GND
VDD_EXT VCC
GPIO16/SPI0_CS CS,片选
SPI0_SLK CLK,时钟
SPI0_MOSI MOSI,主机输出,从机输入
SPI0_MISO MISO,主机输入,从机输出

参考:硬件环境清单第二章节内容,准备以及组装好硬件环境。

Air780EHV核心板淘宝购买链接:点击购买

AirMICROSD_1010配件板:点击购买

四、准备软件环境

1、Luatools下载调试工具:下载调试工具

2、内核固件固件版本(底层 core 固件文件):内核固件下载

3、LuatOS 需要的脚本和资源文:脚本资源下载

4、lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件

5、本教程用到的API接口参考:

fatfs_api地址

io_api地址

fs_api地址

准备好软件环境之后,接下来查看如何使用工具烧录,将本篇文章中演示使用的项目文件烧录到 Air780EHV核心板中

五、演示核心步骤

1、CH390控制模块(ch390_manager.lua)

硬件初始化(780系列的开发板与核心板使用方式不同)

在使用Air780EHM/EHV/EGH开发板时,打开这个功能模块

在使用Air780EHM/EHV/EGH核心板时,关闭这个功能模块

-- 打开ch390供电脚
-[[详细解释为什么必须要先初始化打开ch390,并拉高:

 1. 本demo使用的是Air780EHM/EHV/EGH开发板硬件环境测试;
    在Air780EHM/EHV/EGH开发板上,spi0上同时外挂了tf卡和ch390h以太网芯片两种spi从设备,这两种外设通过不同的cs引脚区分;
    测试tf功能前,需要将ch390h的cs引脚拉高,这样可以保证ch390h不会干扰到tf功能;
    将ch390h的cs引脚拉高的方法为:打开ch390h供电,然后将ch390h的pin_cs,也就是gpio8输出高电平;

 2. 本功能模块是针对Air780EHM/EHV/EGH开发板写的,并不是通用代码,如果使用其他硬件环境,需要根据硬件原理图自行修改;
    例如:如果tf独立占用一路spi,就不需要加载本功能模块。

]]

-- 打开ch390供电脚
gpio.setup(20, 1, gpio.PULLUP) 

--上拉ch390使用spi的cs引脚避免干扰
gpio.setup(8,1)

2、TF卡核心演示模块(tfcard_app.lua)

2.1 文件系统挂载

调用FatFS库挂载TF卡到/sd路径

 -- 挂载失败默认格式化,
    -- 如无需格式化应改为fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000, nil, 1, false),
    -- 一般是在测试硬件是否有问题的时候把格式化取消掉
    mount_ok, mount_err = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)

2.2 文件操作演示

-- 1. 创建目录
    if io.mkdir(dir_path) then
        log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
    else
        -- 检查是否目录已存在
        if io.exists(dir_path) then
            log.warn("io.mkdir", "目录已存在,跳过创建", "路径:" .. dir_path)
        else
            log.error("io.mkdir", "目录创建失败且目录不存在", "路径:" .. dir_path)
            goto resource_cleanup
        end
    end

    -- 2. 创建并写入文件
    file_path = dir_path .. "/boottime"
    file = io.open(file_path, "wb")
    if file then
        file:write("这是io库API文档示例的测试内容")
        file:close()
        --在LuatOS文件操作中,执行file:close()是必须且关键的操作,它用于关闭文件句柄,释放资源,并确保数据被正确写入磁盘。
        -- 如果不执行file:close(),可能会导致数据丢失、文件损坏或其他不可预测的问题。
        log.info("文件创建", "文件写入成功", "路径:" .. file_path)
    else
        log.error("文件创建", "文件创建失败", "路径:" .. file_path)
        goto resource_cleanup
    end

    -- 3. 检查文件是否存在
    if io.exists(file_path) then
        log.info("io.exists", "文件存在", "路径:" .. file_path)
    else
        log.error("io.exists", "文件不存在", "路径:" .. file_path)
        goto resource_cleanup
    end

    -- 4. 获取文件大小
    file_size = io.fileSize(file_path)
    if file_size then
        log.info("io.fileSize", "文件大小:" .. file_size .. "字节", "路径:" .. file_path)
    else
        log.error("io.fileSize", "获取文件大小失败", "路径:" .. file_path)
        goto resource_cleanup
    end

    -- 5. 读取文件内容
    file = io.open(file_path, "rb")
    if file then
        content = file:read("*a")
        log.info("文件读取", "路径:" .. file_path, "内容:" .. content)
        file:close()
    else
        log.error("文件操作", "无法打开文件读取内容", "路径:" .. file_path)
        goto resource_cleanup
    end

    -- 6. 启动计数文件操作
    count = 0
    --以只读模式打开文件
    file = io.open(file_path, "rb")
    if file then
        data = file:read("*a")
        log.info("启动计数", "文件内容:", data, "十六进制:", data:toHex())
        count = tonumber(data) or 0
        file:close()
    else
        log.warn("启动计数", "文件不存在或无法打开")

    end

    log.info("启动计数", "当前值:", count)
    count=count + 1
    log.info("启动计数", "更新值:", count)

    file = io.open(file_path, "wb")
    if file then
        file:write(tostring(count))
        file:close()
        log.info("文件写入", "路径:" .. file_path, "内容:", count)
    else
        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
        goto resource_cleanup
    end

    -- 7. 文件追加测试
    append_file = dir_path .. "/test_a"
    -- 清理旧文件
    os.remove(append_file)

    -- 创建并写入初始内容
    file = io.open(append_file, "wb")
    if file then
        file:write("ABC")
        file:close()
        log.info("文件创建", "路径:" .. append_file, "初始内容:ABC")
    else
        log.error("文件创建", "无法创建文件", "路径:" .. append_file)
        goto resource_cleanup
    end

    -- 追加内容
    file = io.open(append_file, "a+")
    if file then
        file:write("def")
        file:close()
        log.info("文件追加", "路径:" .. append_file, "追加内容:def")
    else
        log.error("文件追加", "无法打开文件进行追加", "路径:" .. append_file)
        goto resource_cleanup

    end

    -- 验证追加结果
    file = io.open(append_file, "r")
    if file then
        data = file:read("*a")
        log.info("文件验证", "路径:" .. append_file, "内容:" .. data, "结果:",
            data == "ABCdef" and "成功" or "失败")
        file:close()
    else
        log.error("文件验证", "无法打开文件进行验证", "路径:" .. append_file)
        goto resource_cleanup
    end

    -- 8. 按行读取测试
    line_file = dir_path .. "/testline"
    file = io.open(line_file, "w")
    if file then
        file:write("abc\n")
        file:write("123\n")
        file:write("wendal\n")
        file:close()
        log.info("文件创建", "路径:" .. line_file, "写入3行文本")
    else
        log.error("文件创建", "无法创建文件", "路径:" .. line_file)
        goto resource_cleanup
    end

    -- 按行读取文件
    file = io.open(line_file, "r")
    if file then
        log.info("按行读取", "路径:" .. line_file, "第1行:", file:read("*l"))
        log.info("按行读取", "路径:" .. line_file, "第2行:", file:read("*l"))
        log.info("按行读取", "路径:" .. line_file, "第3行:", file:read("*l"))
        file:close()
    else
        log.error("按行读取", "无法打开文件", "路径:" .. line_file)
        goto resource_cleanup
    end

    -- 9. 文件重命名
    old_path = append_file
    new_path = dir_path .. "/renamed_file.txt"
    success, err = os.rename(old_path, new_path)
    if success then
        log.info("os.rename", "文件重命名成功", "原路径:" .. old_path, "新路径:" .. new_path)

        -- 验证重命名结果
        if io.exists(new_path) and not io.exists(old_path) then
            log.info("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
        else
            log.error("验证结果", "重命名验证失败")
        end
    else
        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
        goto resource_cleanup
    end

    -- 10. 列举目录内容
    log.info("目录操作", "===== 开始目录列举 =====")

    ret, data = io.lsdir(dir_path, 50, 0) -- 50表示最多返回50个文件,0表示从目录开头开始
    if ret then
        log.info("fs", "lsdir", json.encode(data))
    else
        log.info("fs", "lsdir", "fail", ret, data)
        goto resource_cleanup
    end

    -- 11. 删除文件测试
    -- 测试删除renamed_file.txt文件
    if os.remove(new_path) then
        log.info("os.remove", "文件删除成功", "路径:" .. new_path)

        -- 验证renamed_file.txt删除结果
        if not io.exists(new_path) then
            log.info("验证结果", "renamed_file.txt文件删除验证成功")
        else
            log.error("验证结果", "renamed_file.txt文件删除验证失败")
        end
    else
        log.error("io.remove", "renamed_file.txt文件删除失败", "路径:" .. new_path)
        goto resource_cleanup
    end

    -- 测试删除testline文件
    if os.remove(line_file) then
        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)

        -- 验证删除结果
        if not io.exists(line_file) then
            log.info("验证结果", "testline文件删除验证成功")
        else
            log.error("验证结果", "testline文件删除验证失败")
        end
    else
        log.error("io.remove", "testline文件删除失败", "路径:" .. line_file)
        goto resource_cleanup
    end

    if os.remove(file_path) then
        log.info("os.remove", "文件删除成功", "路径:" .. file_path)

        -- 验证删除结果
        if not io.exists(file_path) then
            log.info("验证结果", "boottime文件删除验证成功")
        else
            log.error("验证结果", "boottime文件删除验证失败")
        end
    else
        log.error("io.remove", "boottime文件删除失败", "路径:" .. file_path)
        goto resource_cleanup
    end

    -- 12. 删除目录(不能删除非空目录,所以在删除目录前要确保目录内没有文件或子目录)
    if io.rmdir(dir_path) then
        log.info("io.rmdir", "目录删除成功", "路径:" .. dir_path)

        -- 验证删除结果
        if not io.exists(dir_path) then
            log.info("验证结果", "目录删除验证成功")
        else
            log.error("验证结果", "目录删除验证失败")
        end
    else
        log.error("io.rmdir", "目录删除失败", "路径:" .. dir_path)
        goto resource_cleanup
    end

3、HTTP下载功能 (http_download_file.lua)

 local function http_download_file_task()

    -- 阶段1: 网络就绪检测

    while not socket.adapter(socket.dft()) do
        log.warn("HTTP下载", "等待网络连接", socket.dft())
        -- 等待IP_READY消息,超时设为1秒
        sys.waitUntil("IP_READY", 1000)
    end

    -- 检测到了IP_READY消息
    log.info("HTTP下载", "网络已就绪", socket.dft())

    -- 如果使用核心板演示环境,请打开33——36行代码,同时关闭38——43行的代码。
    -- 如果使用开发板演示环境,请打开38——43行代码,同时关闭33——36行的代码。
    -- 在Air780EHM/EHV/EGH核心板上TF卡的的pin_cs为gpio8,spi_id为0.请根据实际硬件修改
    spi_id, pin_cs = 0, 8
    spi.setup(spi_id, nil, 0, 0, 400 * 1000)
    -- 初始化后拉高pin_cs,准备开始挂载TF卡
    gpio.setup(pin_cs, 1)

    -- Air780EHM/EHV/EGH开发板上的pin_cs为gpio16,spi_id为0.请根据实际硬件修改
    -- spi_id, pin_cs = 0, 16
    -- spi.setup(spi_id, nil, 0, 0, 400 * 1000)
    -- 设置片选引脚同一spi总线上的所有从设备在初始化时必须要先拉高CS脚,防止从设备之间互相干扰。
    -- 在Air780EHM/EHV/EGH开发板上,TF卡和ch390共用SPI0总线。
    -- gpio.setup(pin_cs, 1)

    -- 挂载文件系统
    local mount_ok = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
    if not mount_ok then
        log.error("HTTP下载", "文件系统挂载失败")
        fatfs.unmount("/sd")
        spi.close(spi_id)
        return
    end

    -- 阶段2: 执行下载任务
    log.info("HTTP下载", "开始下载任务")

    -- 核心下载操作开始 (支持http和https)
    -- local code, headers, body = http.request("GET", "...", nil, nil, {dst = "/sd/1.mp3"}).wait()
    -- 其中 "..."为url地址, 支持 http和https, 支持域名, 支持自定义端口。
    local code, headers, body_size = http.request("GET",
                                    "https://gitee.com/openLuat/LuatOS/raw/master/module/Air780EHM_Air780EHV_Air780EGH/demo/audio/1.mp3",
                                    nil, nil, {dst = "/sd/1.mp3"}).wait()
    -- 阶段3: 记录下载结果
    log.info("HTTP下载", "下载完成", 
        code==200 and "success" or "error", 
        code, 
        -- headers是下载的文件头信息
        json.encode(headers or {}), 
        -- body_size是下载的文件大小(字节数)
        body_size) 

    if code == 200 then
        -- 获取实际文件大小
        local actual_size = io.fileSize("/sd/1.mp3")
        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)

        if actual_size~= body_size then
            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
        end
    end

    -- 阶段4: 资源清理
    fatfs.unmount("/sd")
    spi.close(spi_id)
    log.info("HTTP下载", "资源清理完成")
end

4、安全卸载流程

   -- 卸载文件系统和关闭SPI
    ::resource_cleanup::

    log.info("结束", "开始执行关闭操作...")  
    -- 如已挂载需先卸载文件系统,未挂载直接关闭SPI
    if mount_ok then
        if fatfs.unmount("/sd") then
            log.info("文件系统", "卸载成功")
        else
            log.error("文件系统", "卸载失败")
        end
    end

    -- 2. 关闭SPI接口
    spi.close(spi_id)
    log.info("SPI接口", "已关闭")

六、日志展示

1、搭建好硬件环境

2、通过Luatools将demo与固件烧录到核心板或开发板中

3、烧录好后,板子开机将会在Luatools上看到如下打印:

1TF卡初始化与挂载
[2025-08-24 19:51:24.383][000000002.408] D/SPI_TF 卡容量 122138624KB
[2025-08-24 19:51:24.430][000000002.408] D/SPI_TF sdcard init OK OCR:0xc0ff8000!
[2025-08-24 19:51:24.477][000000002.412] I/user.fatfs.mount 挂载成功 0
[2025-08-24 19:51:24.535][000000002.617] I/user.fatfs getfree {"free_sectors":244262144,"total_kb":122132480,"free_kb":122131072,"total_sectors":244264960}
[2025-08-24 19:51:24.583][000000002.618] I/user.fs lsmount [{"fs":"ec7xx","path":""},{"fs":"inline","path":"\/lua\/"},{"fs":"ram","path":"\/ram\/"},{"fs":"luadb","path":"\/luadb\/"},{"fs":"fatfs","path":"\/sd"}]

2)文件操作演示
[2025-08-24 19:51:24.685][000000002.619] I/user.文件操作 ===== 开始文件操作 =====
[2025-08-24 19:51:25.145][000000003.032] I/user.io.mkdir 目录创建成功 路径:/sd/io_test
[2025-08-24 19:51:25.231][000000003.043] I/user.文件创建 文件写入成功 路径:/sd/io_test/boottime
[2025-08-24 19:51:25.297][000000003.046] I/user.io.exists 文件存在 路径:/sd/io_test/boottime
[2025-08-24 19:51:25.376][000000003.049] I/user.io.fileSize 文件大小:41字节 路径:/sd/io_test/boottime
[2025-08-24 19:51:25.467][000000003.052] I/user.文件读取 路径:/sd/io_test/boottime 内容:这是io库API文档示例的测试内容
[2025-08-24 19:51:25.547][000000003.056] I/user.启动计数 文件内容: 这是io库API文档示例的测试内容 十六进制: E8BF99E698AF696FE5BA93415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 82
[2025-08-24 19:51:25.616][000000003.056] I/user.启动计数 当前值: 0
[2025-08-24 19:51:25.693][000000003.057] I/user.启动计数 更新值: 1
[2025-08-24 19:51:25.736][000000003.068] I/user.文件写入 路径:/sd/io_test/boottime 内容: 1
[2025-08-24 19:51:25.795][000000003.081] I/user.文件创建 路径:/sd/io_test/test_a 初始内容:ABC
[2025-08-24 19:51:25.852][000000003.088] I/user.文件追加 路径:/sd/io_test/test_a 追加内容:def
[2025-08-24 19:51:25.909][000000003.091] I/user.文件验证 路径:/sd/io_test/test_a 内容:ABCdef 结果: 成功
[2025-08-24 19:51:25.954][000000003.102] I/user.文件创建 路径:/sd/io_test/testline 写入3行文本
[2025-08-24 19:51:26.001][000000003.106] I/user.按行读取 路径:/sd/io_test/testline 1: abc
[2025-08-24 19:51:26.048][000000003.106] I/user.按行读取 路径:/sd/io_test/testline 2: 123
[2025-08-24 19:51:26.093][000000003.107] I/user.按行读取 路径:/sd/io_test/testline 3: wendal
[2025-08-24 19:51:26.140][000000003.112] I/user.os.rename 文件重命名成功 原路径:/sd/io_test/test_a 新路径:/sd/io_test/renamed_file.txt
[2025-08-24 19:51:26.188][000000003.116] D/fatfs f_open /io_test/test_a 4
[2025-08-24 19:51:26.238][000000003.116] D/vfs fopen /sd/io_test/test_a r not found
[2025-08-24 19:51:26.312][000000003.117] I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
[2025-08-24 19:51:26.367][000000003.117] I/user.目录操作 ===== 开始目录列举 =====
[2025-08-24 19:51:26.424][000000003.121] I/user.fs lsdir [{"name":"boottime","size":0,"type":0},{"name":"testline","size":0,"type":0},{"name":"renamed_file.txt","size":0,"type":0}]
[2025-08-24 19:51:26.478][000000003.127] I/user.os.remove 文件删除成功 路径:/sd/io_test/renamed_file.txt
[2025-08-24 19:51:26.539][000000003.129] D/fatfs f_open /io_test/renamed_file.txt 4
[2025-08-24 19:51:26.593][000000003.130] D/vfs fopen /sd/io_test/renamed_file.txt r not found
[2025-08-24 19:51:26.656][000000003.130] I/user.验证结果 renamed_file.txt文件删除验证成功
[2025-08-24 19:51:26.734][000000003.137] I/user.os.remove testline文件删除成功 路径:/sd/io_test/testline
[2025-08-24 19:51:26.856][000000003.139] D/fatfs f_open /io_test/testline 4
[2025-08-24 19:51:26.922][000000003.140] D/vfs fopen /sd/io_test/testline r not found
[2025-08-24 19:51:27.113][000000003.140] I/user.验证结果 testline文件删除验证成功
[2025-08-24 19:51:27.197][000000003.147] I/user.os.remove 文件删除成功 路径:/sd/io_test/boottime
[2025-08-24 19:51:27.251][000000003.149] D/fatfs f_open /io_test/boottime 4
[2025-08-24 19:51:27.302][000000003.150] D/vfs fopen /sd/io_test/boottime r not found
[2025-08-24 19:51:27.365][000000003.150] I/user.验证结果 boottime文件删除验证成功
[2025-08-24 19:51:27.407][000000003.158] I/user.io.rmdir 目录删除成功 路径:/sd/io_test
[2025-08-24 19:51:27.461][000000003.159] D/fatfs f_open /io_test 4
[2025-08-24 19:51:27.536][000000003.159] D/vfs fopen /sd/io_test r not found
[2025-08-24 19:51:27.610][000000003.159] I/user.验证结果 目录删除验证成功
[2025-08-24 19:51:27.668][000000003.160] I/user.文件操作 ===== 文件操作完成 =====
[2025-08-24 19:51:27.712][000000003.160] I/user.系统清理 开始执行关闭操作...
[2025-08-24 19:51:27.772][000000003.160] I/user.文件系统 卸载成功
[2025-08-24 19:51:27.867][000000003.160] I/user.SPI接口 已关闭

3)网络连接与HTTP下载
[2025-08-24 20:31:49.405][000000006.268] I/user.HTTP下载 开始下载任务
[2025-08-24 20:31:49.438][000000006.275] dns_run 674:gitee.com state 0 id 1 ipv6 0 use dns server2, try 0
[2025-08-24 20:31:49.471][000000006.277] D/mobile TIME_SYNC 0
[2025-08-24 20:31:49.503][000000006.297] dns_run 691:dns all done ,now stop
[2025-08-24 20:31:54.800][000000012.080] I/user.HTTP下载 下载完成 success 200 
[2025-08-24 20:31:54.872][000000012.080] {"Age":"0","Cache-Control":"public, max-age=60","Via":"1.1 varnish","Transfer-Encoding":"chunked","Date":"Sun, 24 Aug 2025 12:31:49 GMT","Access-Control-Allow-Credentials":"true","Vary":"Accept-Encoding","X-Served-By":"cache-ffe9","X-Gitee-Server":"http-pilot 1.9.21","Connection":"keep-alive","Server":"ADAS\/1.0.214","Access-Control-Allow-Headers":"Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-CustomHeader,Content-Range,Range,Set-Language","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; sandbox","X-Request-Id":"1f7e4b55-53c8-440a-9806-8894aa823f50","Accept-Ranges":"bytes","Etag":"W\/\"6ea36a6c51a48eaba0ffbc01d409424e7627bc56\"","Content-Type":"text\/plain; charset=utf-8","Access-Control-Allow-Methods":"GET, POST, PUT, PATCH, DELETE, OPTIONS","X-Frame-Options":"DENY","X-Cache":"MISS","Set-Cookie":"BEC=1f1759df3ccd099821dcf0da6feb0357;Path=\/;Max-Age=126000"}
[2025-08-24 20:31:54.910][000000012.080]  411922
[2025-08-24 20:31:54.936][000000012.082] I/user.HTTP下载 文件大小验证 预期: 411922 实际: 411922
[2025-08-24 20:31:54.979][000000012.083] I/user.HTTP下载 资源清理完成

七、常见问题

1、为什么不能识别新购买的 sd 卡

文件系为 FAT32 格式(windows、linux 都可以正常识别),所以非 FAT 格式的 SD 卡会挂载失败,而无法正常识别。

2、SD 卡的读写路径是什么?

可以通过 fatfs.mount()的第二个参数去自定义 sd 卡的文件夹名称,本文演示的是"/sd",SD 卡文件访问通过路径前加上"/sd",如果 sd 卡中有一个文件 test.txt ,那这个文件的路径就是"/sd/test.txt"。

3、http 下载的文件可以直接保存到 sd 卡里吗?

支持,http.request 接口支持直接下载到文件系统中,下载到 sd 卡中的时候只需要注意路径设置。参考 demo 中的 HTTP 部分。

4、注意事项

建议可以先阅读readme文档以熟悉整体流程。

PS:合宙所有新系列产品都支持TF卡功能,包括780EHM/EGH/EHV,Air8000系列,Air8101系列。使用核心板进行外挂的时候,各位要记得接线不要接错了哦。并且一定要注意SPI的ID是否有冲突。

重点:当各位使用8000整机开发板或者是780新品系列的开发板在测试TF卡功能的时候一定要将CH390初始化并拉高。

在Air780EHM/EHV/EGH开发板上,spi0上同时外挂了tf卡和ch390h以太网芯片两种spi从设备,这两种外设通过不同的cs引脚区分;

测试tf功能前,需要将ch390h的cs引脚拉高,这样可以保证ch390h不会干扰到tf功能;

将ch390h的cs引脚拉高的方法为:打开ch390h供电,然后将ch390h的pin_cs,也就是gpio8输出高电平;

当spi总线上挂载多台从设备的时候,只要是上电的,如果正常使用就初始化spi和cs;

不使用的也得初始化,并拉高cs脚,如果没有初始化并拉高cs脚,对应的gpio就是输入高阻态,就可能是低电平,在spi总线发送数据时它就可能会响应,从而在miso上有输出,这会导致两个从设备产生冲突。