跳转至

14 AirSPINORFLASH_1000

作者:马亚丹 | 最后修改:2026-05-18

一、AirSPINORFLASH_1000 简介

AirSPINORFLASH_1000 是合宙推出的一款 SPI 接口,128Mb 的 SPI NOR Flash 配件小板,规格:

  1. 驱动 IC:华邦 W25Q128;
  2. 6 路插针: VCC GND CS MISO MOSI SCK

适用于 Air700ECH/Air780EHN/EHU/EHM/EHV/EGH/EPM/Air8000 全系/Air8101

二、功能演示概述

本文演示合宙 4G 模组 Air8000 使用 LuatOS 开发时, 通过 SPI 接口和 spi 核心库、lf 核心库(即 little_flash,下述都用 lf 表示 little_flash)、sfud 核心库挂载 AirSPINORFLASH_1000 应用功能.

SPI、SFUD、little_flash 是合宙 LuatOS 开发中与存储相关的三个核心概念,有以下区别和联系:

SPI(Serial Peripheral Interface)—— 通信协议,本质是一种串行外设接口协议(同步全双工通信协议),是芯片之间(如 主控与 Flash、传感器、显示屏等外设)数据传输的底层物理 / 协议标准。

功能上定义了数据传输的时序、引脚(如 SCLK 时钟线、MOSI 主机输出线、MISO 主机输入线、CS 片选线)和通信规则,用于实现 主控 与外部 SPI 设备的硬件级数据交互。

在合宙中的作用:合宙模块的外部 Flash 通过 SPI 协议与主芯片通信,是 Flash 数据读写的 “物理通道”。

SFUD(Serial Flash Universal Driver)—— 通用驱动库,本质是一款开源的串行 Flash 通用驱动库(由 armink 开发,LuatOS 集成),用于屏蔽不同品牌 / 型号 SPI Flash 的硬件差异,提供统一的操作接口。

在合宙中的作用:LuatOS 通过 SFUD 驱动模块上的 SPI Flash,开发者无需关心具体 Flash 的型号和底层 SPI 时序,直接调用 SFUD 接口即可操作 Flash。

little_flash(LuatOS 中的小容量 Flash )—— 通用驱动库,主要用于嵌入式系统中简化不同型号 Flash 存储器的驱动开发,lf 核心库是合宙自己封装的用于驱动 nor flash 和 nand flash 的库,可以把 nor flash 和 nand flash 挂载成 littlefs 文件系统,目前功能上和 sfud 核心库的使用有重复,区别是 sfud 核心库不支持挂载 nand flash,但是 if 核心库可以,因为 lf 功能更强大,所以推荐使用 lf 核心库挂载 flash。

使用 Air8000 核心板下载 Air8000 的 LuatOS 示例代码中 accessory_board/AirSPINORFLASH_1000 的例程进行验证,例程中演示了三种 flash 的使用方式:

  1. 通过原始 spi 接口对 flash 模块进行读写数据操作,详细逻辑请看示例代码 raw_spi.lua 文件
  2. 通过 lf 核心库和 io 核心库,对 flash 模块以文件系统的方式进行读写数据操作,详细逻辑请看示例代码 lf_fs.lua 文件
  3. 通过 sfud 核心库和 io 核心库,对 flash 模块以文件系统的方式进行读写数据操作,详细逻辑请看示例代码 sfud_test.lua 文件

三、准备硬件环境

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

3.2 AirSPINORFLASH_1000 小板购买链接:https://item.taobao.com/item.htm?id=968697495376&skuId=5917333268684&spm=a1z10.3-c-s.w4002-24045920836.19.6c216ee5tAwvBs

AirSPINORFLASH_1000 小板实物图:

3.3 TYPE-C USB 数据线一根 ,Air8000 核心板和数据线的硬件接线方式为:

  • Air8000 核心板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
  • TYPE-C USB 数据线直接插到开发板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;

3.4 杜邦线 6 根

Air8000 核心板与 AirSPINORFLASH_1000 按以下方式接线:

Air8000核心板
AirSPINORFLASH_1000配件版
GND(任意)
GND
VDD_EXT
VCC
GPIO12/
SPI1_CS
CS
SPI1_SLK
SCK
SPI1_MOSI
MOSI
SPI1_MISO
MISO

Air8000 核心板管脚图:

Air8000 核心板与 AirSPINORFLASH_1000 小板接线实物图:

四、准备软件环境

在开始实践本示例之前,先准备一下软件环境:

4.1 Luatools 工具,如果是第一次使用 Luatools 工具,请仔细阅读此链接教程。

4.2 内核固件文件(底层 core 固件文件):LuatOS-SoC_V2016_Air8000_1.soc;参考项目使用的内核固件;如有更新可以使用最新固件。

4.3 luatos 需要的脚本和资源文件:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/accessory_board/AirSPINORFLASH_1000

   1)   main.lua:主程序入口;

   2)   raw_spi.lua:示例功能文件,通过原始spi接口对flash模块进行读写数据操作,在main.lua中加载运行。 

   3)   lf_fs.lua:示例功能文件,通过 little_flash 核心库和 io 文件系统,对 flash 模块以文件系统的方式进行读写数据操作,在 main.lua 中加载运行。

   4)   sfud_test.lua,示例功能文件,通过 sfud 核心库和 io 文件系统,对 flash 模块以文件系统的方式进行读写数据操作,在 main.lua 中加载运行。

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

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

五、API 接口说明

Air8000 不同版本固件对以上三个 API 的支持说明:https://docs.openluat.com/air8000/luatos/firmware/

查看说明可知,111、112、11、12 号固件不支持 little_flash 核心库;

所有版本固件都支持 sfud 和 spi 核心库.

六、示例代码和功能展示

6.1 流程介绍

1、搭建好硬件环境

2、打开 main.lua,参考本文 4.3 章节按照自己的需求加载对应的 Lua 示例功能文件,其余注释掉,烧录时确保 main.lua 中只加载 raw_spi.lua、lf_fs.lua、sfud_test.lua 三个文件之一。

3、Luatools 烧录内核固件和修改后的 demo 脚本代码

4、烧录成功后,自动开机运行,查看打印日志,如果正常运行,会打印 SPI 初始化、文件系统挂载、文件系统信息、写入读取文件等。

6.2 代码和 log

6.2.1 main.lua

代码示例(点击查看完整 demo)

PROJECT = "AirSPINORFLASH_1000"
VERSION = "001.000.000"

-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)

-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
    --配置喂狗超时时间为9秒钟
    wdt.init(9000)
    --启动一个循环定时器,每隔3秒钟喂一次狗
    sys.timerLoopStart(wdt.feed, 3000)
end

-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
--     errDump.config(true, 600)
-- end

-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用

-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
--     log.info("mem.lua", rtos.meminfo())
--     log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)

-- 加载raw_spi功能模块
--require "raw_spi"

-- 加载lf_fs功能模块
require"lf_fs"

-- 加载sfud_test功能模块
--require"sfud_test"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

6.2.2 raw_spi.lua

6.2.2.1 核心代码部分(点击查看完整 demo)
-- SPI配置参数
local SPI_ID = 1                   -- SPI总线ID,根据实际情况修改
local CS_PIN = 12                  -- CS引脚,根据实际情况修改
local CPHA = 0                     -- 时钟相位
local CPOL = 0                     -- 时钟极性
local data_Width = 8               -- 数据宽度(位)
local bandrate = 2*1000*1000       -- 波特率(Hz),初始化为2MHz
local timeout = 1000                -- 操作超时时间(ms)
local cspin = gpio.setup(CS_PIN, 1) --CS脚置于高电平


-- 1. 设置并启用 SPI
local function spiDev_init_func()
    log.info("raw_spi", "SPI_ID", SPI_ID, "CS_PIN", CS_PIN)
    local spiDevice = spi.setup(SPI_ID,nil, CPHA,CPOL,data_Width,bandrate,
    spi.MSB,   --高低位顺序    可选,默认高位在前
    spi.master,--主模式     可选,默认主
    spi.half   --半双工      spi flash只支持半双工
    )

    log.info("硬件spi", "初始化,波特率:", spiDevice, bandrate)
    return true
end


-- 2. 定义功能函数:发送和接收数据
local function spi_transfer_func(sendData, recvLen)
    -- 选中设备
    cspin(0)

    if sendData then
        -- 发送数据
        spi.send(SPI_ID, sendData)
    end

    local recvData = ""
    if recvLen and recvLen > 0 then
        -- 接收数据
        recvData = spi.recv(SPI_ID, recvLen)
    end

    -- 取消选中
    cspin(1)
    return recvData
end

-- 3. 定义功能函数:读取并验证芯片ID
local function spi_readChipId_func()
    --0x9F指令读取JEDEC ID
    local id = spi_transfer_func(string.char(0x9F), 3)
    --读取成功会返回 3 字节(制造商 + 设备 ID)
    if #id == 3 then
        local b1, b2, b3 = id:byte(1, 3)
        log.info("spi", "芯片ID: 0x%02X 0x%02X 0x%02X", b1, b2, b3)

        -- 验证是否为W25Q系列:
        --制造商 ID(第 1 字节): 0xEF(代表 Winbond)
        -- 设备类型(第 2 字节): 0x40(表示 W25Q 系列 NOR Flash)
        -- 容量代码(第 3 字节): 0x18(对应 128Mbit = 16MB 容量)
        if b1 == 0xEF and (b2 == 0x40 or b2 == 0x18) then
            return true
        end
    end

    log.error("spi", "读取芯片ID失败")
    return false
end

-- 4. 定义功能函数:写数据使能
local function spi_writeEnable_func()
    --0x06指令设置Write Enable
    spi_transfer_func(string.char(0x06))
    return 0
end

-- 5. 定义功能函数:写数据禁用
local function spi_writeDisable_func()
    --0x04指令设置Write Disable
    spi_transfer_func(string.char(0x04))
end

-- 6. 定义功能函数:读取状态寄存器
local function spi_readStatus_func()
    --0x05指令读取寄存器状态
    local status = spi_transfer_func(string.char(0x05), 1)
    if #status == 1 then
        --返回0 表示WIP=0芯片就绪且未使能写操作
        return status:byte(1)
    end
end


--7.  等待写入完成
local function spi_waitForWriteComplete_func()
    while timeout > 0 do
        local status = spi_readStatus_func()
        -- WIP位为0表示写入完成
        if bit.band(status, 0x01) == 0 then
            return true
        end
        sys.wait(10)
        timeout = timeout - 10
    end

    log.error("spi", "等待写入超时")
    return false
end

--8. 扇区擦除,根据需要修改
-- 0x20:扇区擦除(4KB)
-- 0xD8:块擦除(64KB)
-- 0xC7:整片擦除
--address是要擦除的扇区的起始地址,本demo演示扇区擦除,块擦除和整片擦除可自行研究
local function spi_erase_sector(address)
    -- 使能写操作
    if not spi_writeEnable_func() then
        log.error("SPI", "写使能失败")
        return false
    end
    -- 发送扇区擦除指令
    local result = spi_transfer_func(string.char(0x20) ..
        string.char(bit.rshift(address, 16) & 0xFF) ..
        string.char(bit.rshift(address, 8) & 0xFF) ..
        string.char(address & 0xFF))
    if not result then
        log.error("SPI", "发送扇区擦除指令失败")
        return false
    end
    -- 等待写入完成
    return spi_waitForWriteComplete_func()
end

--9. 页编程(写入数据到指定地址)
local function spi_pageProgram_func(address, data)
    -- 检查数据长度(不能超过256字节)
    local len = #data
    if len == 0 or len > 256 then
        log.error("spi", "数据长度无效:", len)
        return false
    end
    -- 准备写入命令和地址
    local cmd = string.char(0x02) ..
        string.char(bit.rshift(address, 16) & 0xFF) ..
        string.char(bit.rshift(address, 8) & 0xFF) ..
        string.char(address & 0xFF)
    -- 写数据使能
    spi_writeEnable_func()
    -- 发送写命令和数据
    spi_transfer_func(cmd .. data)
    -- 等待写入完成
    return spi_waitForWriteComplete_func()
end

-- 10. 读取数据
local function spi_readData_func(address, length)
    if length <= 0 then
        return ""
    end
    -- 准备读取命令和地址
    local cmd = string.char(0x03) ..
        string.char(bit.rshift(address, 16) & 0xFF) ..
        string.char(bit.rshift(address, 8) & 0xFF) ..
        string.char(address & 0xFF)
    -- 发送读取命令并接收数据
    return spi_transfer_func(cmd, length)
end


-- 11. 关闭SPI设备,成功返回0
local function spi_close_func()    
    log.info("关闭spi", spi.close(SPI_ID))
end



--12. 功能演示核心函数
local function spi_test_func()
    --1.判断SPI初始化
    if not spiDev_init_func() then
        return
    end
    --2.判断flash芯片ID
    if not spi_readChipId_func() then
        spi_close_func()
        return
    end

    -- 3.读取寄存器状态
    local status = spi_readStatus_func()
    --返回状态0表示芯片就绪且未使能写操作
    log.info("spi", "寄存器状态为: 0x%02X", status)

    -- 4.擦除扇区(4KB)
    --0x000000是要擦除的扇区的起始地址,即第一个存储单元的位置,
    --擦除从地址0x000000开始的整个 4KB 扇区,该扇区包含地址0x000000到0x000FFF的所有存储单元,可按需修改
    log.info("spi", "擦除扇区 0x000000...")
    if not spi_erase_sector(0x000000) then
        log.error("spi", "擦除失败")
        spi_close_func() --
        return
    end

    -- 5.读取擦除后的数据(应为0xFF)
    local erasedData = spi_readData_func(0x000000, 16)
    log.info("spi", "擦除后数据:", string.toHex(erasedData))

    -- 测试数据
    local testData = "Hello, SPI Flash! "

    -- 6.写入数据到地址0x000000
    log.info("spi", "写入数据:", testData)
    if spi_pageProgram_func(0x000000, testData) then
        -- 读取数据
        log.info("spi", "正在验证数据...")
        local readData = spi_readData_func(0x000000, #testData)

        -- 验证数据
        if readData == testData then
            log.info("spi", "数据验证成功!,读取到数据为:" .. readData)
        else
            log.error("spi", "数据验证失败!")
            log.info("spi", "预期读到的数据是:", testData)
            log.info("spi", "实际读取的数据是:", string.toHex(readData))
        end
    else
        log.error("spi", "写入操作失败")
    end
    -- 7.禁用写操作
    spi_writeDisable_func()
    -- 8.关闭SPI设备
    spi_close_func()
end


sys.taskInit(spi_test_func)
6.2.2.2 例程 log 打印如下:
[2025-10-17 15:45:21.154][000000000.619] I/user.main AirSPINORFLASH_1000 001.000.000
[2025-10-17 15:45:21.157][000000000.630] I/user.raw_spi SPI_ID 1 CS_PIN 12
[2025-10-17 15:45:21.161][000000000.630] SPI_HWInit 552:spi1 speed 2000000,1994805,154
[2025-10-17 15:45:21.168][000000000.630] I/user.硬件spi 初始化,波特率: 0 2000000
[2025-10-17 15:45:21.171][000000000.631] I/user.spi 芯片ID: 0x%02X 0x%02X 0x%02X 239 64 23
[2025-10-17 15:45:21.177][000000000.632] I/user.spi 寄存器状态为: 0x%02X 0
[2025-10-17 15:45:21.184][000000000.632] I/user.spi 擦除扇区 0x000000...
[2025-10-17 15:45:21.228][000000000.708] I/user.spi 擦除后数据: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 32
[2025-10-17 15:45:21.232][000000000.709] I/user.spi 写入数据: Hello, SPI Flash! 
[2025-10-17 15:45:21.235][000000000.710] I/user.spi 正在验证数据...
[2025-10-17 15:45:21.242][000000000.711] I/user.spi 数据验证成功!,读取到数据为:Hello, SPI Flash! 
[2025-10-17 15:45:21.247][000000000.711] I/user.关闭spi 0
6.2.2.3 luatools 运行日志图示

6.2.3 lf_fs.lua

6.2.3.1 核心代码部分(点击查看完整 demo)
-- SPI配置参数
local SPI_ID = 1        -- SPI总线ID,根据实际情况修改
local CS_PIN = 12       -- CS引脚,根据实际情况修改
local CPHA = 0          -- 时钟相位
local CPOL = 0          -- 时钟极性
local data_Width = 8    -- 数据宽度(位)
local bandrate = 2*1000*1000 -- 波特率(Hz),初始化为2MHz

-- 1. 以对象方式设置并启用 SPI,返回设备对象
local function spiDev_init_func()
    log.info("lf_fs", "SPI_ID", SPI_ID, "CS_PIN", CS_PIN)

    --以对象的方式初始化spi,高位在前,主模式,半双工模式
    --spi flash只能半双工模式
    local spi_device = spi.deviceSetup(SPI_ID, CS_PIN, CPHA, CPOL, data_Width, bandrate, spi.MSB, 1, 0)

    log.info("硬件spi", "初始化,波特率:", spi_device, bandrate)
    if not spi_device then
        log.error("SPI初始化", "失败")
        return nil
    end
    log.info("SPI初始化", "成功,波特率:",bandrate)
    return spi_device
end

-- 2. 初始化Flash设备,返回设备对象
local function init_flash_device(spi_device)
    log.info("Flash初始化", "开始")
    local flash_device = lf.init(spi_device)
    if not flash_device then
        log.error("Flash初始化", "失败")
        return nil
    end
    log.info("Flash初始化", "成功,设备:", flash_device)
    return flash_device
end

-- 3. 挂载文件系统
local function mount_filesystem(flash_device, mount_point)
    log.info("文件系统", "开始挂载:", mount_point)
    -- 检查是否支持挂载功能
    if not lf.mount then
        log.error("文件系统", "lf模块不支持挂载功能")
        return false
    end

    -- 尝试挂载
    local mount_ok = lf.mount(flash_device, mount_point)
    if not mount_ok then
        log.warn("文件系统lf", "挂载失败,尝试重新挂载...")
        mount_ok = lf.mount(flash_device, mount_point)
        if not mount_ok then
            log.error("文件系统", "仍挂载失败")
            return false
        end
    end
    log.info("文件系统", "挂载成功:", mount_point)
    return true
end

-- 4. 打印文件系统信息
local function print_filesystem_info(mount_point)
    log.info("文件系统信息", "开始查询:", mount_point)
    -- 获取文件系统详细信息,总块数/已用块数等
    local ok, total_blocks, used_blocks, block_size, fs_type = fs.fsstat(mount_point)
    if ok then
        log.info("  总block数:", total_blocks)
        log.info("  已用block数:", used_blocks)
        log.info("  block大小:", block_size, "字节")
        log.info("  文件系统类型:", fs_type)
    else
        log.warn("  无法获取详细信息")
    end
end

-- 5. 执行文件操作测试
local function test_file_operations(mount_point)
    log.info("文件操作测试", "开始")
    -- 测试写入文件
    local test_file = mount_point .. "/test.txt"
    local f, err = io.open(test_file, "w")
    if not f then
        log.error("  写入失败", test_file, "错误:", err)
        return false
    end

    local write_data = "当前时间: " .. os.date()
    f:write(write_data)
    f:close()
    log.info("  写入成功", test_file, "内容:", write_data)

    -- 测试读取文件
    local read_data, read_err = io.readFile(test_file)
    if not read_data then
        log.error("  读取失败", test_file, "错误:", read_err)
        return false
    end
    log.info("  读取成功", test_file, "内容:", read_data)

    -- 验证内容一致性
    if read_data ~= write_data then
        log.warn("  内容不一致", "写入:", write_data, "读取:", read_data)
    end
    -- 测试文件追加
    local append_file = mount_point .. "/append.txt"
    -- 清除旧文件
    os.remove(append_file) 
    -- 初始写入
    io.writeFile(append_file, "LuatOS 测试") 

    local f_append, append_err = io.open(append_file, "a+")
    if not f_append then
        log.error("  追加失败", append_file, "错误:", append_err)
        return false
    end
    local append_data = " - 追加时间: " .. os.date()
    f_append:write(append_data)
    -- 执行完操作后,一定要关掉文件
    f_append:close()

    local final_data = io.readFile(append_file)
    log.info("  追加后内容:", final_data)   
    log.info("文件操作测试", "完成")

    return true
end

-- 7. 关闭SPI设备对象,成功返回true
local function spi_close_func()    
    log.info("关闭spi", spi_device:close())
end

-- ===主任务函数:调用各功能函数====
local function spinor_test_func()
    --流程1.判断SPI初始化
    spi_device = spiDev_init_func()
    if not spi_device then
        log.error("主流程", "SPI初始化失败,终止")
        return
    end

    -- 流程2:初始化Flash设备
    local flash_device = init_flash_device(spi_device)
    if not flash_device then
        log.error("主流程", "Flash初始化失败,终止")
        spi_close_func()
        return
    end

    -- 流程3:挂载文件系统
    local mount_point = "/little_flash"
    if not mount_filesystem(flash_device, mount_point) then
        log.error("主流程", "文件系统挂载失败,终止")
        return
    end

    -- 流程4:打印文件系统信息
    print_filesystem_info(mount_point)

    -- 流程5:执行文件操作测试
    if not test_file_operations(mount_point) then
        log.warn("主流程", "文件操作测试部分失败")
    end

     -- 6.关闭SPI设备
    spi_close_func()
end

sys.taskInit(spinor_test_func)
6.2.3.2 例程 log 打印如下:
[2025-10-17 15:51:47.917][000000000.648] I/user.lf_fs SPI_ID 1 CS_PIN 12
[2025-10-17 15:51:47.931][000000000.649] SPI_HWInit 552:spi1 speed 2000000,1994805,154
[2025-10-17 15:51:47.939][000000000.649] I/user.硬件spi 初始化,波特率: SPI*: 0C7F5BD0 2000000
[2025-10-17 15:51:47.950][000000000.650] I/user.SPI初始化 成功,波特率: 2000000
[2025-10-17 15:51:47.962][000000000.650] I/user.Flash初始化 开始
[2025-10-17 15:51:47.973][000000000.650] I/little_flash Found SFDP Header. The Revision is V1.5, NPN is 0, Access Protocol is 0xFF.
[2025-10-17 15:51:47.982][000000000.651] I/little_flash Parameter Header is OK. The Parameter ID is 0xFF00, Revision is V5.1, Length is 16,Parameter Table Pointer is 0x000080.
[2025-10-17 15:51:47.999][000000000.652] I/little_flash Found a flash chip. Size is 8388608 bytes.
[2025-10-17 15:51:48.009][000000000.702] I/user.Flash初始化 成功,设备: userdata: 0C0F9ED8
[2025-10-17 15:51:48.024][000000000.702] I/user.文件系统 开始挂载: /little_flash
[2025-10-17 15:51:48.034][000000000.751] D/little_flash lfs_mount 0
[2025-10-17 15:51:48.045][000000000.751] D/little_flash vfs mount /little_flash ret 0
[2025-10-17 15:51:48.063][000000000.752] I/user.文件系统 挂载成功: /little_flash
[2025-10-17 15:51:48.078][000000000.752] I/user.文件系统信息 开始查询: /little_flash
[2025-10-17 15:51:48.087][000000000.794] I/user.  总block数: 2048
[2025-10-17 15:51:48.103][000000000.794] I/user.  已用block数: 2
[2025-10-17 15:51:48.117][000000000.794] I/user.  block大小: 4096 字节
[2025-10-17 15:51:48.136][000000000.794] I/user.  文件系统类型: lfs
[2025-10-17 15:51:48.151][000000000.794] I/user.文件操作测试 开始
[2025-10-17 15:51:48.173][000000000.820] I/user.  写入成功 /little_flash/test.txt 内容: 当前时间: Sun Jan  0 08:00:00 1900
[2025-10-17 15:51:48.487][000000000.869] I/user.  读取成功 /little_flash/test.txt 内容: 当前时间: Sun Jan  0 08:00:00 1900
[2025-10-17 15:51:48.646][000000001.050] I/user.  追加后内容: LuatOS 测试 - 追加时间: Sun Jan  0 08:00:00 1900
[2025-10-17 15:51:48.670][000000001.051] I/user.文件操作测试 完成
[2025-10-17 15:51:48.686][000000001.051] I/user.关闭spi true
6.2.3.3 luatools 运行日志图示

6.2.4 sfud_test.lua

6.2.4.1 核心代码部分(点击查看完整 demo)
-- SPI配置参数
local SPI_ID = 1        -- SPI总线ID,根据实际情况修改
local CS_PIN = 12       -- CS引脚,根据实际情况修改
local CPHA = 0          -- 时钟相位
local CPOL = 0          -- 时钟极性
local data_Width = 8    -- 数据宽度(位)
local bandrate = 2*1000*1000 -- 波特率(Hz),初始化为2MHz
-- flash操作起始地址(示例值,需根据需求调整)
local erase_addr = 4096 
-- 擦除数据的大小(示例值,需匹配 Flash 扇区 大小)
local erase_size = 4096   
--需要操作的数据(示例值,需根据需求调整)
local data = "testdata"

-- 1. 以对象方式设置并启用 SPI,返回设备对象
local function spiDev_init_func()
    log.info("sfud", "SPI_ID", SPI_ID, "CS_PIN", CS_PIN)

    --以对象的方式初始化spi,高位在前,主模式,半双工模式
    --spi flash只能半双工模式
    spi_device = spi.deviceSetup(SPI_ID, CS_PIN, CPHA, CPOL, data_Width, bandrate, spi.MSB, 1, 0)    
    log.info("硬件spi", "初始化,波特率:", spi_device, bandrate)
    if not spi_device then
        log.error("SPI初始化", "失败")
        return nil
    end
    log.info("SPI初始化", "成功,波特率:",bandrate)
    return spi_device
end


-- 2. 初始化Flash设备,返回设备对象
local function init_sfud_device(spi_device)
    log.info("sfud初始化", "开始")
    local sfud_flash_device = sfud.init(spi_device)
    if not sfud_flash_device then
        log.error("Flash初始化", "失败")        
    else 
        return true
    end

end

-- 3. 挂载文件系统
local function mount_filesystem(sfud_device, mount_point)
    log.info("文件系统", "开始挂载:", mount_point)

    -- 检查是否支持挂载功能
    if not sfud.mount then
        log.error("文件系统", "不支持挂载功能")
        return false
    end

    -- 尝试挂载
    local mount_ok = sfud.mount(sfud_device, mount_point)
    if not mount_ok then
        log.warn("文件系统", "挂载失败,尝试重新挂载...")
        mount_ok = sfud.mount(sfud_device, mount_point)
        if not mount_ok then
            log.error("文件系统", "仍挂载失败")
            return false
        end
    end

    log.info("文件系统", "挂载成功:", mount_point)
    return true
end

-- 4. 打印文件系统信息
local function print_filesystem_info(mount_point)
    log.info("文件系统信息", "开始查询:", mount_point)

    -- 获取文件系统详细信息,总块数/已用块数等
    local ok, total_blocks, used_blocks, block_size, fs_type = io.fsstat(mount_point)
    if ok then
        log.info("  总block数:", total_blocks)
        log.info("  已用block数:", used_blocks)
        log.info("  block大小:", block_size, "字节")
        log.info("  文件系统类型:", fs_type)
    else
        log.warn("  无法获取详细信息")
    end
end

-- 5. 执行文件操作测试
local function test_file_operations(mount_point)
    log.info("文件操作测试", "开始")

    -- 测试写入文件
    local test_file = mount_point .. "/test.txt"
    local f, err = io.open(test_file, "w")
    if not f then
        log.error("  写入失败", test_file, "错误:", err)
        return false
    end
    local write_data = "当前时间: " .. os.date()
    f:write(write_data)
    f:close()
    log.info("  写入成功", test_file, "内容:", write_data)

    -- 测试读取文件
    local read_data, read_err = io.readFile(test_file)
    if not read_data then
        log.error("  读取失败", test_file, "错误:", read_err)
        return false
    end
    log.info("  读取成功", test_file, "内容:", read_data)

    -- 验证内容一致性
    if read_data ~= write_data then
        log.warn("  内容不一致", "写入:", write_data, "读取:", read_data)
    end

    -- 测试文件追加
    local append_file = mount_point .. "/append.txt"
    os.remove(append_file) -- 清除旧文件
    io.writeFile(append_file, "LuatOS 测试") -- 初始写入

    local f_append, append_err = io.open(append_file, "a+")
    if not f_append then
        log.error("  追加失败", append_file, "错误:", append_err)
        return false
    end
    local append_data = " - 追加时间: " .. os.date()
    f_append:write(append_data)
    -- 执行完操作后,一定要关掉文件
    f_append:close()

    local final_data = io.readFile(append_file)
    log.info("  追加后内容:", final_data)

    log.info("文件操作测试", "完成")

    return true
end

-- 7. 关闭SPI设备,成功返回0
local function spi_close_func()    
    log.info("关闭spi", spi_device:close())
end

-- 主任务函数:按流程调用各功能函数
local function spinor_test_func()
    --1.判断SPI初始化  
    spi_device = spiDev_init_func()
    if not spi_device then
        log.error("主流程", "SPI初始化失败,终止")
        spi_close_func()
        return
    end

    -- 流程2:初始化sfud设备
    local sfud_init = init_sfud_device(spi_device)
    if not sfud_init then
        log.error("主流程", "sfud 初始化失败,终止")   
        spi_close_func()
        return
    end

    -- 流程3:获取Flash设备,并进行数据擦除、读写操作
    local sfud_device = sfud.getDeviceTable()    
    log.info("获取flash设备信息表:", sfud_device)
    log.info("获取 Flash 容量和page大小:", sfud.getInfo(sfud_device))
    log.info("擦除一个扇区的数据:", sfud.erase(sfud_device, erase_addr, erase_size))   
    log.info("写入数据:", sfud.write(sfud_device, erase_addr, data))    
    log.info("读取数据:", sfud.read(sfud_device, erase_addr, erase_size ))    
    log.info("先擦除再写入数据:", sfud.eraseWrite(sfud_device, erase_addr, data))
    --sys.wait (1000)


    -- 流程4:挂载flash为文件系统
    local mount_point = "/sfud_flash"
    if not mount_filesystem(sfud_device, mount_point) then
        log.error("主流程", "文件系统挂载失败,终止")
        spi_close_func()
        return
    end

    -- 流程5:打印文件系统信息
    print_filesystem_info(mount_point)

    -- 流程6:执行文件操作测试
    if not test_file_operations(mount_point) then
        log.warn("主流程", "文件操作测试部分失败")
    end  

     -- 流程7:关闭SPI设备
    spi_close_func()
end

sys.taskInit(spinor_test_func)
6.2.4.2 例程 log 打印如下:

注意:下图中log"擦除一个块的数据:",修正为“擦除一个扇区的数据”

[2025-10-17 15:30:30.614][000000000.427] I/user.main AirSPINORFLASH_1000 001.000.000
[2025-10-17 15:30:30.620][000000000.442] I/user.sfud SPI_ID 1 CS_PIN 12
[2025-10-17 15:30:30.633][000000000.442] SPI_HWInit 552:spi1 speed 200000,200000,64
[2025-10-17 15:30:30.645][000000000.443] I/user.硬件spi 初始化,波特率: SPI*: 0C7F5AD0 200000
[2025-10-17 15:30:30.658][000000000.443] I/user.SPI初始化 成功,波特率: 200000
[2025-10-17 15:30:30.671][000000000.444] I/user.sfud初始化 开始
[2025-10-17 15:30:30.673][000000000.448] I/sfud Found a Winbond flash chip. Size is 8388608 bytes.
[2025-10-17 15:30:30.682][000000000.469] I/sfud LuatOS-sfud flash device initialized successfully.
[2025-10-17 15:30:30.687][000000000.469] I/user.获取flash设备信息表: userdata: 0C0E0328
[2025-10-17 15:30:30.694][000000000.470] I/user.获取 Flash 容量和page大小: 8388608 4096
[2025-10-17 15:30:30.702][000000000.541] I/user.擦除一个块的数据: 0
[2025-10-17 15:30:30.712][000000000.544] I/user.写入数据: 0
[2025-10-17 15:30:30.875][000000000.743] I/user.读取数据: 
[2025-10-17 15:30:30.884][000000000.744] testdata
[2025-10-17 15:30:30.888][000000000.820] I/user.先擦除再写入数据: 0
[2025-10-17 15:30:30.899][000000000.821] I/user.文件系统 开始挂载: /sfud_flash
[2025-10-17 15:30:30.916][000000001.137] D/sfud lfs_mount 0
[2025-10-17 15:30:30.921][000000001.138] D/sfud vfs mount /sfud_flash ret 0
[2025-10-17 15:30:30.934][000000001.138] I/user.文件系统 挂载成功: /sfud_flash
[2025-10-17 15:30:30.949][000000001.138] I/user.文件系统信息 开始查询: /sfud_flash
[2025-10-17 15:30:30.954][000000001.375] I/user.  总block数: 2048
[2025-10-17 15:30:30.959][000000001.376] I/user.  已用block数: 2
[2025-10-17 15:30:30.965][000000001.376] I/user.  block大小: 4096 字节
[2025-10-17 15:30:30.972][000000001.376] I/user.  文件系统类型: lfs
[2025-10-17 15:30:30.978][000000001.376] I/user.文件操作测试 开始
[2025-10-17 15:30:30.983][000000001.535] I/user.  写入成功 /sfud_flash/test.txt 内容: 当前时间: Sun Jan  0 08:00:01 1900
[2025-10-17 15:30:30.996][000000001.679] I/user.  读取成功 /sfud_flash/test.txt 内容: 当前时间: Sun Jan  0 08:00:01 1900
[2025-10-17 15:30:31.078][000000002.518] I/user.  追加后内容: LuatOS 测试 - 追加时间: Sun Jan  0 08:00:02 1900
[2025-10-17 15:30:31.085][000000002.519] I/user.文件操作测试 完成
[2025-10-17 15:30:31.108][000000002.519] I/user.关闭spi true
6.2.4.3 luatools 运行日志图示

七、总结

本文档主要介绍 AirSPINORFLASH_1000 的使用方法。

结合 demo 例程讲解了 nor flash 的应用流程,介绍了 lf\spi\sfud 的 API 接口使用,旨在最简单的快速上手 Air8000 的 LuatOS 的 nor flash 应用.

八、常见问题

暂无