Sfoglia il codice sorgente

add: 添加bsp/air302

Wendal Chen 5 anni fa
parent
commit
a226034c8c

+ 1 - 0
.gitignore

@@ -56,3 +56,4 @@ Module.symvers
 Mkfile.old
 dkms.conf
 
+bsp/air302/FlashToolCLI/

+ 59 - 0
bsp/air302/README.md

@@ -0,0 +1,59 @@
+# Air302@LuatOS
+
+## Air302是什么?
+
+合宙Air302, 是基于移芯EC616的NB-IOT模块, 封装尺寸兼容合宙Air202,功能管脚基本上一一对应.
+
+## LuatOS为它提供哪些功能
+
+* 192kb的系统内存, 可用内存约40kb
+* 64kb的Lua专属内存,可用内存约40kb
+* 文件系统大小332kb,格式littlefs 2.1,可用空间约200kb
+* 基于Lua 5.3.5, 提供95%的原生库支持
+* 适配LuaTask,提供极为友好的`sys.lua`
+* `gpio`库提供GPIO管脚控制功能(映射表后面有提供)
+* `uart`库提供串口输入输出功能,支持uart1(调试/刷机)/uart2(用户可用)/uart0(芯片日志)
+* `i2c`库提供iic总线master功能
+* `disp`库提供基于i2c的显示屏支持,当前支持SSD1306
+* `nbiot`库提供与nbiot相关的支持
+* `json`库,提供lua对象与json字符串的双向转换
+* `socket`库,提供异步Socket接口,用于与服务器的通信
+* `log`库提供简洁的日志功能
+* `libcoap`库提供coap消息处理所需方法
+* `mqtt`库提供连接到mqtt服务器的功能,结合`crypto`加密库,可连接到阿里云
+* `pwm`库提供多个PWM输出管脚,存在复用关系
+* `adc`库提供外部电平检测,内部温度检测,供电电压检测
+
+## 管脚映射表
+
+管脚编号对应w600芯片的管脚编号, 与GPIO编号有对应关系, 请查阅Air302硬件设计手册
+
+特别提示:
+
+1. ADC0 实际上对应通道2, 读取时应使用 `adc.read(2)`. 通道0为CPU温度, 通道1为VBAT电压.
+2. AON_GPIO2 对应 GPIO24 `gpio.setup(21,0)`
+3. AON_GPIO3 对应 GPIO23 `gpio.setup(23,0)`
+4. SPI功能暂不可用(截止到20200627,尚未支持)
+
+## 刷机工具
+
+1. LuaTools 最新版, 支持ec后缀的固件下载, 也支持lua脚本下载
+2. windows命令行工具链
+
+## 已知限制
+
+1. 鉴于较少的系统内存,请合理分配资源
+2. 尚未支持加密连接(TCP/UDP)
+3. 尚未支持远程升级功能
+
+## 注意事项
+
+部分管脚的电压是1.8v,请勿超电压范围使用.
+
+## 模块购买
+
+1. 骑士智能Air302开发板 https://item.taobao.com/item.htm?id=621910075534
+
+## 详细教程
+
+TODO 敬请期待

+ 298 - 0
bsp/air302/air302.py

@@ -0,0 +1,298 @@
+#!/usr/bin/python3
+# -*- coding: UTF-8 -*-
+
+import os.path
+import shutil
+import zipfile
+import time
+import subprocess
+import sys
+import json
+
+#------------------------------------------------------------------
+# 读取配置信息
+import configparser
+config = configparser.ConfigParser()
+config['air302'] = {
+    "PLAT_ROOT" : "D:\\github\\air302\\sdk\\PLAT\\", # 仅开发者配置
+    "FTC_PATH" : ".\\FlashToolCLI\\",
+    "EC_PATH" : ".\\Air302_dev.ec",
+    "USER_PATH": ".\\user\\",
+    "LIB_PATH" : ".\\lib\\",
+    "DEMO_PATH": ".\\demo\\",
+    "MAIN_LUA_DEBUG" : "true",
+    "LUA_DEBUG" : "false",
+    "COM_PORT" : "COM56"
+}
+if os.path.exists("local.ini") :
+    config.read("local.ini")
+if os.path.exists(config["air302"]["PLAT_ROOT"]):
+    PLAT_ROOT = os.path.abspath(config["air302"]["PLAT_ROOT"]) + os.sep # 源码地址
+else:
+    PLAT_ROOT = config["air302"]["PLAT_ROOT"] 
+FTC_PATH = os.path.abspath(config["air302"]["FTC_PATH"])  + os.sep   # FlashToolCLI刷机工具的目录
+EC_PATH = os.path.abspath(config["air302"]["EC_PATH"])               # EC后缀的固件路径
+USER_PATH = os.path.abspath(config["air302"]["USER_PATH"]) + os.sep  # 用户脚本所在的目录
+LIB_PATH = os.path.abspath(config["air302"]["LIB_PATH"])  + os.sep   # 用户脚本所在的目录
+DEMO_PATH = os.path.abspath(config["air302"]["DEMO_PATH"])  + os.sep # 用户脚本所在的目录
+MAIN_LUA_DEBUG = config["air302"]["MAIN_LUA_DEBUG"] == "true"
+LUA_DEBUG = config["air302"]["LUA_DEBUG"] == "true"
+COM_PORT = config["air302"]["COM_PORT"]
+
+# TODO 从环境变量获取上述参数
+
+'''
+获取git库的当前版本号,为一个hash值
+'''
+def get_git_revision_short_hash():
+    try :
+        return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
+    except:
+        return ""
+
+'''
+打印帮助信息
+'''
+def usage():
+    print('''
+    python air302.py [action]
+
+    lfs   - 编译文件系统
+    dlrom - 下载底层固件
+    dlfs  - 下载lua脚本(即整个文件系统)
+    dlfull- 下载底层和lua脚本
+    pkg   - 生成发布用的压缩包
+    build - 构建源码(仅内部使用)
+
+    用例1, 生成文件系统并下载到开发板
+    python air302.py lfs dlfs
+
+    用例2, 生成文件系统,并下载固件和文件系统到开发板
+    python air302.py lfs dlfull
+
+    用例3, 仅下载底层固件
+    python air302.py dlrom
+    
+    用例四, 编译,打包,构建文件系统,全量下载
+    python air302.py build lfs pkg dlfull
+    ''')
+
+'''
+FlashToolCLI所需要的配置信息
+'''
+FTC_CNF_TMPL = '''
+[config]
+line_0_com = ${COM}
+agbaud = 921600
+
+;bootloader.bin file infomation
+[bootloader]
+blpath = .\\image\\bootloader.bin
+blloadskip = 0
+
+;system.bin file infomation
+[system]
+syspath = .\\system.bin
+sysloadskip = 0
+
+;control such as reset before download
+[control]
+reset = 0
+
+[flexfile0]
+filepath = .\\rfCaliTb\\MergeRfTable.bin
+burnaddr = 0x3A4000
+
+[flexfile1]
+filepath = .\\rfCaliTb\\MergeRfTable.bin
+burnaddr = 0x16000
+
+[flexfile2]
+filepath = .\\disk.fs
+burnaddr = 0x350000
+'''.replace("${COM}", COM_PORT)
+
+'''
+执行打包程序,内部使用
+'''
+def _pkg():
+    # TODO 扩展为用户可用的打包ec固件的工具
+    if os.path.exists("tmp"):
+        shutil.rmtree("tmp")
+
+    _tag = time.strftime("%Y%m%d%H%M%S", time.localtime())
+    _tag = _tag + "-" + get_git_revision_short_hash().decode()
+
+    os.mkdir("tmp")
+    os.mkdir("tmp/ec")
+    # 拷贝固件文件
+    if os.path.exists(PLAT_ROOT) :
+        shutil.copy(PLAT_ROOT + "out/ec616_0h00/air302/air302.bin", "tmp/ec/luatos.bin")
+        shutil.copy(PLAT_ROOT + "out/ec616_0h00/air302/comdb.txt", "tmp/ec/comdb.txt")
+        shutil.copy(FTC_PATH + "image/bootloader.bin", "tmp/ec/bootloader.bin")
+        shutil.copy(FTC_PATH + "image/bootloader_head.bin", "tmp/ec/bootloader_head.bin")
+    elif os.path.exists(EC_PATH) and EC_PATH.endswith(".ec") :
+        with zipfile.ZipFile(EC_PATH) as zip :
+            zip.extractall(path="tmp/ec/")
+    # 拷贝库文件和demo
+    shutil.copytree(LIB_PATH, "tmp/lib")
+    shutil.copytree(DEMO_PATH, "tmp/demo")
+    
+    #拷贝自身
+    shutil.copy(sys.argv[0], "tmp/air302.py")
+    # 写入默认配置文件
+    with open("tmp/local.ini", "w") as f:
+        f.write('''
+[air302]
+; PLAT_ROOT for factory only
+PLAT_ROOT = D:\\gitee\\air302
+FTC_PATH = FlashToolCLI\\
+EC_PATH = ${EC}
+USER_PATH = user\\
+LIB_PATH = lib\\
+DEMO_PATH = demo\\
+MAIN_LUA_DEBUG = true
+LUA_DEBUG = false
+COM_PORT = COM56
+'''.replace("${EC}", "Air302_V0001_"+_tag+".ec"))
+
+    if os.path.exists("userdoc") :
+        shutil.copytree("userdoc", "tmp/userdoc")
+    if os.path.exists(USER_PATH):
+        shutil.copytree(USER_PATH, "tmp/user")
+
+    with open("tmp/文档在userdoc目录.txt", "w") as f:
+        f.write("QQ群: 1061642968")
+
+    with zipfile.ZipFile("tmp/Air302_V0001_"+_tag+".ec", mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zip :
+        zip.write("tmp/ec/luatos.bin", "luatos.bin")                   # 底层固件
+        zip.write("tmp/ec/comdb.txt", "comdb.txt")                     # uart0输出的unilog所需要的数据库文件,备用
+        zip.write("tmp/ec/bootloader.bin", "bootloader.bin")           # bootloader,备用
+        zip.write("tmp/ec/bootloader_head.bin", "bootloader_head.bin") # bootloader_header,备用
+        zip.write(FTC_PATH + "disk.fs", "disk.bin")                    # 默认磁盘镜像
+
+    shutil.rmtree("tmp/ec/")
+    if os.path.exists(FTC_PATH):
+        shutil.copytree(FTC_PATH, "tmp/FlashToolCLI")
+
+    pkg_name = "Air302_V0001_"+_tag + ".zip"
+    shutil.make_archive("Air302_V0001_"+_tag, 'zip', "tmp")
+
+    ## 拷贝一份固定路径的
+    shutil.copy("tmp/Air302_V0001_"+_tag+".ec", "tmp/Air302_dev.ec")
+
+    print("ALL DONE===================================================")
+    print("Package Name", pkg_name)
+
+'''
+下载底层或脚本
+'''
+def _dl(tp, _path=None):
+    with open(FTC_PATH + "config.ini", "w") as f :
+        f.write(FTC_CNF_TMPL)
+    cmd = [FTC_PATH + "FlashToolCLI.exe", "-p", COM_PORT, "burnbatch", "--imglist"]
+    if tp == "rom" or tp == "full":
+        if EC_PATH.endswith(".ec") :
+            import zipfile
+            with zipfile.ZipFile(EC_PATH) as zip :
+                with open(FTC_PATH + "system.bin", "wb") as f:
+                    f.write(zip.read("luatos.bin"))
+        elif EC_PATH.endswith(".bin"):
+            shutil.copy(EC_PATH, FTC_PATH + "system.bin")
+        else:
+            print("Bad EC_PATH")
+            return
+        cmd += ["system"]
+    if tp == "fs" or tp == "full" :
+        cmd += ["flexfile2"]
+    print("CALL", " ".join(cmd))
+    subprocess.check_call(cmd, cwd=FTC_PATH)
+
+'''
+生成文件系统镜像
+'''
+def _lfs(_path=None):
+    _disk = FTC_PATH + "disk"
+    if os.path.exists(_disk) :
+        shutil.rmtree(_disk)
+    os.mkdir(_disk)
+
+    if not _path:
+        _path = USER_PATH
+    # 收集需要处理的文件列表
+    _paths = []
+    # 首先,遍历lib目录
+    if os.path.exists(LIB_PATH) :
+        for name in os.listdir(LIB_PATH) :
+            _paths.append(LIB_PATH + name)
+    # 然后遍历user目录
+    for name in os.listdir(_path) :
+        _paths.append(_path + name)
+    for name in _paths :
+        # 如果是lua文件, 编译之
+        if name.endswith(".lua") :
+            cmd = [FTC_PATH + "luac.exe"]
+            if name == "main.lua" :
+                if not MAIN_LUA_DEBUG :
+                    cmd += ["-s"]
+            elif not LUA_DEBUG :
+                cmd += ["-s"]
+            else:
+                print("LUA_DEBUG", LUA_DEBUG, "False" == LUA_DEBUG)
+            cmd += ["-o", FTC_PATH + "disk/" + os.path.basename(name) + "c", name]
+            print("CALL", " ".join(cmd))
+            subprocess.check_call(cmd)
+        # 其他文件直接拷贝
+        else:
+            print("COPY", name, FTC_PATH + "disk/" + os.path.basename(name))
+            shutil.copy(name, FTC_PATH + "disk/" + os.path.basename(name))
+    print("CALL mklfs")
+    subprocess.check_call([FTC_PATH + "mklfs.exe"], cwd=FTC_PATH)
+
+def main():
+    argc = 1
+    while len(sys.argv) > argc :
+        if sys.argv[argc] == "build" :
+            print("Action Build ----------------------------------")
+            subprocess.check_call([PLAT_ROOT + "build.bat"])
+        elif sys.argv[argc] == "lfs" :
+            print("Action mklfs ----------------------------------")
+            _lfs()
+        elif sys.argv[argc] == "pkg" :
+            print("Action pkg ------------------------------------")
+            _pkg()
+        elif sys.argv[argc] == "dlrom": #下载底层
+            print("Action download ROM ---------------------------")
+            if len(sys.argv) > argc + 1 and sys.argv[argc+1].startsWith("-path="):
+                _dl("rom", sys.argv[argc+1][6:])
+                argc += 1
+            else:
+                _dl("rom")
+        elif sys.argv[argc] == "dlfs":
+            print("Action download FS  ---------------------------")
+            if len(sys.argv) > argc + 1 and sys.argv[argc+1].startsWith("-path="):
+                _dl("fs", sys.argv[argc+1][6:])
+                argc += 1
+            else:
+                _dl("fs")
+        elif sys.argv[argc] == "dlfull":
+            if len(sys.argv) > argc + 1 and sys.argv[argc+1].startsWith("-path="):
+                _dl("full", sys.argv[argc+1][6:])
+                argc += 1
+            else:
+                _dl("full")
+        elif sys.argv[argc] == "clean":
+            if os.path.exists("tmp"):
+                shutil.rmtree("tmp")
+        else:
+            usage()
+            return
+        argc += 1
+    print("============================================================================")
+    print("every done, bye")
+
+
+if len(sys.argv) == 1 :
+    usage()
+else :
+    main()

+ 37 - 0
bsp/air302/demo/adc/main.lua

@@ -0,0 +1,37 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_adc_demo"
+VERSION = "1.0.0"
+
+-- 一定要添加sys.lua !!!!
+local sys = require "sys"
+
+-- 网络灯 GPIO19, NETLED脚
+local NETLED = gpio.setup(19, 0)     -- 初始化GPIO19, 并设置为低电平
+
+adc.open(0) -- CPU温度
+adc.open(1) -- VBAT电压
+adc.open(2) -- 模块上的ADC0脚, 0-1.8v,不要超过范围使用!!!
+
+sys.taskInit(function()
+    while 1 do
+        log.info("LED", "Go Go Go")
+        NETLED(0) -- 低电平,熄灭
+        sys.wait(1000)
+        NETLED(1) -- 高电平,亮起
+        sys.wait(1000)
+        log.debug("adc", "adc0", adc.read(0)) -- adc.read 会返回两个值
+        log.debug("adc", "adc1", adc.read(1))
+        log.debug("adc", "adc2", adc.read(2))
+    end
+    -- 支持close操作,按需使用,close后read的返回值无效,再次open后可以read
+    -- adc.close(0)
+    -- adc.close(1)
+    -- adc.close(2)
+end)
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 111 - 0
bsp/air302/demo/aliyun/main.lua

@@ -0,0 +1,111 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_aliyun_demo"
+VERSION = "1.0.0"
+
+-- 引入必要的库文件(lua编写), 内部库不需要require
+local sys = require "sys"
+local mqtt = require "mqtt"
+
+
+log.info("version", _VERSION, VERSION)
+
+--- UART 相关------------------------------
+-- 配置uart2, 115200 8 N 1
+uart.on(2, "receive", function(id, len)
+     log.info("uart", "receive", uart.read(id, 1024))
+end)
+uart.setup(2, 115200)
+
+-- GPIO 和 PWM 相关 -------------------------------
+-- 网络灯 GPIO19/PWM5
+gpio.setup(19, 0)     -- 初始化GPIO19, 并设置为低电平
+gpio.set(19, 1)       -- 设置为高电平
+-- pwm.open(5, 1000, 50) -- 初始化PWM5, 频率1000hz, 占空比50%
+
+-- GPIO18/PWM4
+-- GPIO17/PWM3
+
+-- 低功耗sleep2模式下依然能输出电平的AON系列GPIO
+-- AON_GPIO2 --> GPIO22
+-- AON_GPIO3 --> GPIO23
+
+-- ADC相关---------------------------------------
+-- 通道 0-内部温度, 1-供电电压, 2-5 外部ADC管脚
+-- adc.open(5)
+-- adc.read(5)
+-- adc.close(5)
+
+-- 连接到阿里云物联网的Task
+sys.taskInit(function()
+    sys.wait(2000)
+    -- 阿里云物联网的设备信息
+    -- https://help.aliyun.com/document_detail/73742.html?spm=a2c4g.11186623.6.593.11a22cf0rGX1bC
+    -- deviceName 可以是imei, 也可以自定义, 填写正确才能连接上
+    local productKey,deviceName,deviceSecret = "a1YFuY6OC1e","azNhIbNNTdsVwY2mhZno","5iRxTePbEMguOuZqltZrJBR0JjWJSdA7" 
+    local host, port, selfid = productKey .. ".iot-as-mqtt.cn-shanghai.aliyuncs.com", 1883, nbiot.imei()
+    local mqttClientId = selfid  .. "|securemode=3,signmethod=hmacsha1,timestamp=132323232|"
+    local mqttUsername = deviceName .. "&" .. productKey
+    local signstr = "clientId"..selfid.."deviceName"..deviceName.."productKey"..productKey.."timestamp".."132323232"
+    local mqttPassword = crypto.hmac_sha1(signstr, deviceSecret)
+    --log.info("aliiot", "mqttClientId", mqttClientId)
+    --log.info("aliiot", "mqttUsername", mqttUsername)
+    --log.info("aliiot", "signstr", signstr)
+    --log.info("aliiot", "mqttPassword", mqttPassword)
+    local topic_get = string.format("/%s/%s/user/get", productKey, deviceName)
+    local topic_update = string.format("/%s/%s/user/update", productKey, deviceName)
+    while true do
+        -- 等待联网成功
+        while not socket.isReady() do 
+            log.info("net", "wait for network ready")
+            sys.waitUntil("NET_READY", 1000)
+        end
+        log.info("main", "net is ready!!")
+        sys.wait(1000) -- 稍等一会
+        
+        -- 清理内存, 啊啊啊啊
+        collectgarbage("collect")
+        collectgarbage("collect")
+
+        -- 开始连接到阿里云物联网
+        local mqttc = mqtt.client(mqttClientId, 240, mqttUsername, mqttPassword)
+        -- 等待底层tcp连接完成
+        while not mqttc:connect(host, port) do sys.wait(15000) end
+        -- 连接成功, 开始订阅
+        log.info("mqttc", "mqtt seem ok", "try subscribe", topic_req)
+        if mqttc:subscribe(topic_get) then
+            -- 订阅完成, 发布业务数据
+            log.info("mqttc", "mqtt subscribe ok", "try publish")
+            if mqttc:publish(topic_update, "test publish " .. selfid, 1) then
+                -- 发布也ok了, 等待数据下发或数据上传
+                while true do
+                    log.info("mqttc", "wait for new msg")
+                    local r, data, param = mqttc:receive(120000, "pub_msg")
+                    log.info("mqttc", "mqttc:receive", r, data, param)
+                    if r then -- 有下发的数据
+                        log.info("mqttc", "get message from server", data.payload or "nil", data.topic)
+                    elseif data == "pub_msg" then -- 需要上报数据
+                        log.info("mqttc", "send message to server", data, param)
+                        mqttc:publish(topic_update, "response " .. param)
+                    elseif data == "timeout" then -- 无交互,发个定时report也行
+                        log.info("mqttc", "wait timeout, send custom report")
+                        mqttc:publish(topic_update, "test publish " .. os.date() .. nbiot.imei())
+                    else -- 其他情况不太可能,退出连接吧
+                        log.info("mqttc", "ok, something happen", "close connetion")
+                        break
+                    end
+                end
+            end
+        end
+        -- 关掉连接,清理资源
+        mqttc:disconnect()
+        -- 避免频繁重连, 必须加延时
+        sys.wait(30000)
+    end
+
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 69 - 0
bsp/air302/demo/disp/main.lua

@@ -0,0 +1,69 @@
+
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_disp_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+-- 网络灯
+local NETLED = gpio.setup(19, 0)
+
+----------------------------------------------------------------------
+-- 对接SSD1306
+function display_str(str)
+    disp.clear()
+    disp.drawStr(str, 1, 18)
+    disp.update()
+end
+
+function ui_update()
+    disp.clear() -- 清屏
+
+    disp.drawStr(os.date("%Y-%m-%d %H:%M:%S"), 1, 12) -- 写日期
+
+    disp.drawStr("Luat@Air302" .. " " .. _VERSION, 1, 24) -- 写版本号
+    if socket.isReady() then
+        disp.drawStr("net ready", 1, 36) -- 写网络状态
+    else
+        disp.drawStr("net not ready", 1, 36)
+    end
+    --disp.drawStr("rssi: " .. tostring(nbiot.rssi()), 1, 36)
+
+    disp.update()
+end
+
+-- 初始化显示屏
+log.info(TAG, "init ssd1306") -- log库是内置库,内置库均不需要require
+disp.init({mode="i2c_sw", pin0=17, pin1=18}) -- 通过GPIO17/GPIO18模拟, 也可以用硬件i2c脚
+display_str("Booting ...")
+
+sys.taskInit(function()
+    while 1 do
+        sys.wait(1000)
+        log.info("disp", "ui update", rtos.meminfo()) -- rtos是也是内置库
+        ui_update()
+    end
+end)
+
+sys.taskInit(function()
+    while 1 do
+        if socket.isReady() then
+            NETLED(1)
+            sys.wait(100)
+            NETLED(0)
+            sys.wait(1900)
+        else
+            NETLED(1)
+            sys.wait(500)
+            NETLED(0)
+            sys.wait(500)
+        end
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 52 - 0
bsp/air302/demo/fs/main.lua

@@ -0,0 +1,52 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_fs_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+-- 日志TAG, 非必须
+local NETLED = gpio.setup(19, 0)
+
+function fs_test()
+    local f = io.open("boot_time", "rb")
+    local c = 0
+    if f then
+        local data = f:read("*a")
+        log.info("fs", "data", data, data:toHex())
+        c = tonumber(data)
+        f:close()
+    end
+    log.info("fs", "boot count", c)
+    c = c + 1
+    f = io.open("boot_time", "wb")
+    --if f ~= nil then
+    log.info("fs", "write c to file", c, tostring(c))
+    f:write(tostring(c))
+    f:close()
+    --end
+end
+
+fs_test() -- 每次开机,把记录的数值+1
+
+sys.taskInit(function()
+    while 1 do
+        if socket.isReady() then
+            NETLED(1)
+            sys.wait(100)
+            NETLED(0)
+            sys.wait(1900)
+        else
+            NETLED(1)
+            sys.wait(500)
+            NETLED(0)
+            sys.wait(500)
+        end
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 29 - 0
bsp/air302/demo/gpio/main.lua

@@ -0,0 +1,29 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_gpio_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+local NETLED = gpio.setup(19, 0) -- 输出模式
+local G18 = gpio.setup(18, nil) -- 输入模式
+local G1 = gpio.setup(1, function() -- 中断模式, 下降沿
+    log.info("gpio", "BOOT button release")
+end)
+
+sys.taskInit(function()
+    while 1 do
+        -- 一闪一闪亮晶晶
+        NETLED(0)
+        sys.wait(500)
+        NETLED(1)
+        sys.wait(500)
+        log.info("gpio", "18", G18())
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 106 - 0
bsp/air302/demo/i2c/main.lua

@@ -0,0 +1,106 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_i2c_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+local sys = require "sys"
+
+-- 当前仅支持i2c0哦
+i2c.setup(0)
+
+-- 读取 OPT3001 光亮度传感器的数据
+
+-- OPT3001 device addr
+local OPT3001_DEVICE_ADDR           = 0x44
+
+-- OPT3001 register addr
+local OPT3001_REG_RESULT            = 0x00
+local OPT3001_REG_CONFIGURATION     = 0x01
+local OPT3001_REG_LOW_LIMIT         = 0x02
+local OPT3001_REG_HIGH_LIMIT        = 0x03
+local OPT3001_REG_MANUFACTURE_ID    = 0x7E
+local OPT3001_REG_DEVICE_ID         = 0x7F
+
+--OPT3001 CONFIG resister bit map
+local CONFIG_RN_Pos         = (12)
+local CONFIG_RN_Msk         = (0xF << CONFIG_RN_Pos) -- Lua 5.3支持各种位运算符, 这在Lua 5.1是没有的
+
+local CONFIG_CT_Pos         = (11)
+local CONFIG_CT_Msk         = (0x1 << CONFIG_CT_Pos)
+
+local CONFIG_M_Pos          = (9)
+local CONFIG_M_Msk          = (0x3 << CONFIG_M_Pos)
+
+local CONFIG_OVF_Pos        = (8)
+local CONFIG_OVF_Msk        = (0x1 << CONFIG_OVF_Pos)
+
+local CONFIG_CRF_Pos        = (7)
+local CONFIG_CRF_Msk        = (0x1 << CONFIG_CRF_Pos)
+
+local CONFIG_FH_Pos         = (6)
+local CONFIG_FH_Msk         = (0x1 << CONFIG_FH_Pos)
+
+local CONFIG_FL_Pos         = (5)
+local CONFIG_FL_Msk         = (0x1 << CONFIG_FL_Pos)
+
+local CONFIG_L_Pos          = (4)
+local CONFIG_L_Msk          = (0x1 << CONFIG_L_Pos)
+
+local CONFIG_POL_Pos        = (3)
+local CONFIG_POL_Msk        = (0x1 << CONFIG_POL_Pos)
+
+local CONFIG_ME_Pos         = (2)
+local CONFIG_ME_Msk         = (0x1 << CONFIG_ME_Pos)
+
+local CONFIG_FC_Pos         = (0)
+local CONFIG_FC_Msk         = (0x3 << CONFIG_L_Pos)
+
+
+-- OPT3001 CONFIG setting macro
+local CONFIG_CT_100         = 0x0000                           -- conversion time set to 100ms
+local CONFIG_CT_800         = CONFIG_CT_Msk                    -- conversion time set to 800ms
+
+local CONFIG_M_CONTI        = (0x2 << CONFIG_M_Pos)            -- continuous conversions
+local CONFIG_M_SINGLE       = (0x1 << CONFIG_M_Pos)            -- single-shot
+local CONFIG_M_SHUTDOWN     = 0x0000                           -- shutdown
+
+
+local CONFIG_RN_RESET       = (0xC << CONFIG_RN_Pos)
+local CONFIG_CT_RESET       = CONFIG_CT_800
+local CONFIG_L_RESET        = CONFIG_L_Msk
+local CONFIG_DEFAULTS       = (CONFIG_RN_RESET | CONFIG_CT_RESET | CONFIG_L_RESET)
+
+local CONFIG_ENABLE_CONTINUOUS     = (CONFIG_M_CONTI | CONFIG_DEFAULTS)
+local CONFIG_ENABLE_SINGLE_SHOT    = (CONFIG_M_SINGLE | CONFIG_DEFAULTS)
+local CONFIG_DISABLE    =  CONFIG_DEFAULTS
+
+sys.taskInit(function()
+    -- 读取device id, 应该是0x3001 = 12289
+    local devid = i2c.readReg(0, OPT3001_DEVICE_ADDR, OPT3001_REG_DEVICE_ID)
+    log.info("i2c", "opt3001", "device id", devid)
+    -- 设置为持续转换
+    i2c.writeReg(0, OPT3001_DEVICE_ADDR, OPT3001_REG_CONFIGURATION, CONFIG_ENABLE_CONTINUOUS)
+    local regVal = 0
+    while 1 do
+        while 1 do
+            log.info("i2c", "check sensor data ready")
+            regVal = i2c.readReg(0, OPT3001_DEVICE_ADDR, OPT3001_REG_CONFIGURATION)
+            if (regVal & CONFIG_CRF_Msk) then
+                log.info("i2c", "sensor data ready")
+                break
+            end
+            sys.wait(3000)
+        end
+        regVal = i2c.readReg(0, OPT3001_DEVICE_ADDR, OPT3001_REG_RESULT)
+        local fraction = regVal & 0xFFF
+        local exponent = 1 << (regVal >> 12)
+        log.info("i2c", "read lux=", fraction * exponent, "/100")
+        sys.wait(3000)
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 62 - 0
bsp/air302/demo/libcoap/main.lua

@@ -0,0 +1,62 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "luatos_air302_libcoap"
+VERSION = "1.0.0"
+
+-- sys库是标配
+local sys = require "sys"
+
+sys.taskInit(function()
+    while 1 do
+        if socket.isReady() then
+            sys.wait(2000)
+            local netc = socket.udp()
+            netc:host("coap.vue2.cn")
+            netc:port(5683)
+            netc:on("connect", function(id, re)
+                log.info("udp", "connect ok", id, re)
+                if re then
+                    local data = libcoap.new(libcoap.GET, "time"):rawdata()
+                    log.info("coap", "send", data:toHex())
+                    netc:send(data)
+                end
+            end)
+            netc:on("recv", function(id, data)
+                log.info("udp", "recv", data:toHex())
+                if #data >= 4 then
+                    local _coap = libcoap.parse(data)
+                    if _coap then
+                        log.info("coap", "type", _coap:tp(), "code", _coap:code(), "msgid", _coap:msgid())
+                        log.info("coap", "data", _coap:data())
+                        log.info("coap", "http statue code", _coap:hcode())
+                        if _coap:tp() == 2 and _coap:hcode() == 205 then
+                            log.info("coap", "http resp ACK and 205")
+                        end
+                    end
+                end
+            end)
+            if netc:start() == 0 then
+                while netc:closed() == 0 do
+                    sys.waitUntil("NETC_END_" .. netc:id(), 30000)
+                    if netc:closed() == 0 then
+                        log.info("udp", "send heartbeat")
+                        local data = libcoap.new(libcoap.GET, "time"):rawdata()
+                        log.info("coap", "send", data:toHex())
+                        netc:send(data)
+                    end
+                end
+            end
+            netc:clean()
+            netc:close()
+            log.info("udp", "all close, sleep 30s")
+            sys.wait(30000)
+        else
+            sys.wait(1000)
+        end
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 122 - 0
bsp/air302/demo/modbus/main.lua

@@ -0,0 +1,122 @@
+
+PROJECT = "air302demo"
+VERSION = "1.0.0"
+
+local sys = require "sys"
+--local console = require "console"
+--console.setup(2, 115200)
+
+
+log.info("hi", _VERSION, VERSION)
+log.info("hi", "from main.lua file --------")
+
+-- uart.on(2, "receive", function(id, len)
+--     log.info("uart", "receive", uart.read(id, 1024))
+-- end)
+-- uart.setup(2, 115200)
+-- _G.tled = 0
+gpio.setup(1, function()
+    log.info("gpio", "BOOT button release")
+end)
+-- gpio.setup(2, 0)
+-- gpio.setup(3, 0)
+-- gpio.setup(9, 0)
+-- gpio.setup(17, 0)
+-- gpio.setup(18, 0)
+-- gpio.setup(19, 0)
+sys.taskInit(function()
+    while 1 do
+        --log.info("gpio", "LED UP")
+        --log.info("gpio", "gpio1", gpio.get(1) == 1)
+        --log.info("gpio", "gpio2", gpio.get(2) == 1)
+        --log.info("gpio", "gpio3", gpio.get(3) == 1)
+        -- log.info("gpio", "gpio9", gpio.get(9, 1) == 1)
+        -- log.info("gpio", "gpio17", gpio.get(17, 1) == 1)
+        -- log.info("gpio", "gpio18", gpio.get(18, 1) == 1)
+        -- log.info("gpio", "gpio19", gpio.get(19, 1) == 1)
+        --sys.wait(3000)
+
+        --log.info("gpio", "fuck", "0")
+        -- gpio.set(1, 0)
+        -- gpio.set(19, 0)
+        --sys.wait(1000)
+        --log.info("gpio", "fuck", "1")
+        -- gpio.set(1, 1)
+        -- gpio.set(19, 1)
+        --sys.wait(1000)
+        -- log.info("pwm", ">>>>>")
+        -- for i = 10,1,-1 do 
+        --     pwm.open(5, 1000, i*9)
+        --     sys.wait(200 + i*10)
+        -- end
+        -- for i = 10,1,-1 do 
+        --     pwm.open(5, 1000, 100 - i*9)
+        --     sys.wait(200 + i*10)
+        -- end
+        -- adc.open(0)
+        -- adc.open(1)
+        -- adc.open(2)
+        -- log.info("adc", "adc0", adc.read(0))
+        -- log.info("adc", "adc0", adc.read(1))
+        -- log.info("adc", "adc0", adc.read(2))
+        -- adc.close(0)
+        -- adc.close(1)
+        -- adc.close(2)
+        --print(1)
+        sys.wait(5000)
+    end
+end)
+
+sys.taskInit(function()
+    while 1 do
+        log.info("netc", "test loop ...")
+        while not nbiot.isReady() do
+            log.info("netc", "wait for ready ...")
+            sys.wait(1000)
+        end
+        log.info("netc", "call socket.tcp()")
+        local netc = socket.tcp()
+        netc:host("39.105.203.30") -- modbus server
+        netc:port(19001)
+        netc:on("connect", function(id, re)
+            log.info("netc", "connect", id, re)
+            local data = json.encode({imei=nbiot.imei(),rssi=nbiot.rssi(),iccid=nbiot.iccid()})
+            log.info("netc", "send reg package", data)
+            netc:send(data)
+        end)
+        netc:on("recv", function(id, data)
+            log.info("netc", "recv", id, data)
+            netc:send(data)
+        end)
+        if netc:start() == 0 then
+            log.info("netc", "netc start successed!!!")
+            local tcount = 0
+            while netc:closed() == 0 do
+                tcount = tcount + 1
+                if tcount > 60 then
+                    netc:send(string.char(0))
+                    tcount = 0
+                end
+                sys.wait(1000)
+                log.debug("netc", "wait for closed", netc:closed() == 0)
+            end
+            log.info("netc", "socket closed?")
+        end
+        log.info("netc", "socket is closed, clean up")
+        netc:clean()
+        netc:close()
+        sys.wait(10000)
+    end
+end)
+--[[
+sys.timerLoopStart(function()
+    log.info("hi", _VERSION)
+    log.info("nbiot1", "rssi", nbiot.rssiStr())
+    log.info("nbiot2", "imsi", nbiot.imsi())
+    log.info("nbiot3", "iccid", nbiot.iccid())
+    log.info("nbiot4", "imei", nbiot.imei())
+    log.info("nbiot5", "net ready?", nbiot.isReady())
+end, 10000)
+]]
+
+sys.run()

+ 59 - 0
bsp/air302/demo/mqtt/main.lua

@@ -0,0 +1,59 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_mqtt_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+local mqtt = require "mqtt"
+
+sys.taskInit(function()
+    -- 服务器配置信息
+    local host, port, selfid = "lbsmqtt.airm2m.com", 1884, nbiot.imei()
+    -- 等待联网成功
+    while true do
+        while not socket.isReady() do 
+            log.info("net", "wait for network ready")
+            sys.waitUntil("NET_READY", 1000)
+        end
+        log.info("main", "Airm2m mqtt loop")
+        
+        local mqttc = mqtt.client(selfid, nil, nil, false)
+        while not mqttc:connect(host, port) do sys.wait(2000) end
+        local topic_req = string.format("/device/%s/req", selfid)
+        local topic_report = string.format("/device/%s/report", selfid)
+        local topic_resp = string.format("/device/%s/resp", selfid)
+        log.info("mqttc", "mqtt seem ok", "try subscribe", topic_req)
+        if mqttc:subscribe(topic_req) then
+            log.info("mqttc", "mqtt subscribe ok", "try publish")
+            if mqttc:publish(topic_report, "test publish " .. os.date()  .. crypto.md5("12345"), 1) then
+                while true do
+                    log.info("mqttc", "wait for new msg")
+                    local r, data, param = mqttc:receive(120000, "pub_msg")
+                    log.info("mqttc", "mqttc:receive", r, data, param)
+                    if r then
+                        log.info("mqttc", "get message from server", data.payload or "nil", data.topic)
+                    elseif data == "pub_msg" then
+                        log.info("mqttc", "send message to server", data, param)
+                        mqttc:publish(topic_resp, "response " .. param)
+                    elseif data == "timeout" then
+                        log.info("mqttc", "wait timeout, send custom report")
+                        mqttc:publish(topic_report, "test publish " .. os.date() .. nbiot.imei())
+                    else
+                        log.info("mqttc", "ok, something happen", "close connetion")
+                        break
+                    end
+                end
+            end
+        end
+        mqttc:disconnect()
+        sys.wait(5000) -- 等待一小会, 免得疯狂重连
+    end
+
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 33 - 0
bsp/air302/demo/pwm/main.lua

@@ -0,0 +1,33 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_pwm_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+-- PWM5 --> NETLED, GPIO19
+-- PWM4 --> GPIO18
+-- PWM2 --> GPIO17
+
+-- gpio.setup(19, 0)
+sys.taskInit(function()
+    while 1 do
+        -- 仿呼吸灯效果
+        log.info("pwm", ">>>>>")
+        for i = 10,1,-1 do 
+            pwm.open(5, 1000, i*9) -- 5 通道, 频率1000hz, 占空比0-100
+            sys.wait(200 + i*10)
+        end
+        for i = 10,1,-1 do 
+            pwm.open(5, 1000, 100 - i*9)
+            sys.wait(200 + i*10)
+        end
+        sys.wait(5000)
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 30 - 0
bsp/air302/demo/uart/main.lua

@@ -0,0 +1,30 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_uart_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+sys.subscribe("uart_write", function(data)
+    uart.write(2, data)
+end)
+
+uart.on(2, "receive", function(id, len)
+    local data = uart.read(id, 1024)
+    log.info("uart", "receive", data)
+    sys.publish("uart_write", data) -- 或者调用uart.write(2, data)也可以的
+end)
+uart.setup(2, 115200)
+
+
+gpio.setup(1, function()
+    -- 按一下boot按键试试
+    log.info("gpio", "BOOT button release")
+    uart.write(2, "boot button release")
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 44 - 0
bsp/air302/demo/udp/main.lua

@@ -0,0 +1,44 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_default_demo"
+VERSION = "1.0.0"
+
+local sys = require "sys"
+
+sys.taskInit(function()
+    while 1 do
+        if socket.isReady() then
+            sys.wait(2000)
+            local netc = socket.udp()
+            netc:host("nutz.cn")
+            netc:port(17888)
+            netc:on("connect", function(id, re)
+                log.info("udp", "connect ok", id, re)
+                if re then
+                    netc:send("IMEI:" .. nbiot.imei())
+                end
+            end)
+            netc:on("recv", function(id, data)
+                log.info("udp", "recv", #data, data)
+            end)
+            if netc:start() == 0 then
+                while netc:closed() == 0 do
+                    sys.waitUntil("NETC_END_" .. netc:id(), 30000)
+                    if netc:closed() == 0 then
+                        log.info("udp", "send heartbeat")
+                        netc:send("heartbeat:" .. nbiot.imei() .. " " .. os.date())
+                    end
+                end
+            end
+            netc:clean()
+            netc:close()
+            log.info("udp", "all close, sleep 30s")
+            sys.wait(300000)
+        else
+            sys.wait(1000)
+        end
+    end
+end)
+
+-- 结尾总是这一句哦
+sys.run()

+ 514 - 0
bsp/air302/lib/mqtt.lua

@@ -0,0 +1,514 @@
+
+--- 模块功能:MQTT客户端
+-- @module mqtt
+-- @author openLuat
+-- @license MIT
+-- @copyright openLuat
+-- @release 2017.10.24
+
+local mqtt = {}
+
+-- MQTT 指令id
+local CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
+local CLIENT_COMMAND_TIMEOUT = 60000
+
+local sys = require "sys"
+local pack = _G.pack
+local string = _G.string
+local encodeLen = mqttcore.encodeLen
+--local encodeUTF8 = mqttcore.encodeUTF8
+-- local function encodeLen(len)
+--     local s = ""
+--     local digit
+--     repeat
+--         digit = len % 128
+--         len = (len - digit) / 128
+--         if len > 0 then
+--             --digit = bit.bor(digit, 0x80)
+--             digit = digit | 0x80
+--         end
+--         s = s .. string.char(digit)
+--     until (len <= 0)
+--     return s
+-- end
+
+local function encodeUTF8(s)
+    if not s or #s == 0 then
+        return ""
+    else
+        return pack.pack(">P", s)
+    end
+end
+
+local function packCONNECT(clientId, keepAlive, username, password, cleanSession, will, version)
+    local content = pack.pack(">PbbHPAAAA",
+        version == "3.1" and "MQIsdp" or "MQTT",
+        version == "3.1" and 3 or 4,
+        (#username == 0 and 0 or 1) * 128 + (#password == 0 and 0 or 1) * 64 + will.retain * 32 + will.qos * 8 + will.flag * 4 + cleanSession * 2,
+        keepAlive,
+        clientId,
+        encodeUTF8(will.topic),
+        encodeUTF8(will.payload),
+        encodeUTF8(username),
+        encodeUTF8(password))
+    return pack.pack(">bAA",
+        CONNECT * 16,
+        encodeLen(string.len(content)),
+        content)
+end
+
+local function packSUBSCRIBE(dup, packetId, topics)
+    local header = SUBSCRIBE * 16 + dup * 8 + 2
+    local data = pack.pack(">H", packetId)
+    for topic, qos in pairs(topics) do
+        data = data .. pack.pack(">Pb", topic, qos)
+    end
+    return pack.pack(">bAA", header, encodeLen(#data), data)
+end
+
+local function packUNSUBSCRIBE(dup, packetId, topics)
+    local header = UNSUBSCRIBE * 16 + dup * 8 + 2
+    local data = pack.pack(">H", packetId)
+    for k, topic in pairs(topics) do
+        data = data .. pack.pack(">P", topic)
+    end
+    return pack.pack(">bAA", header, encodeLen(#data), data)
+end
+
+local function packPUBLISH(dup, qos, retain, packetId, topic, payload)
+    local header = PUBLISH * 16 + dup * 8 + qos * 2 + retain
+    local len = 2 + #topic + #payload
+    if qos > 0 then
+        return pack.pack(">bAPHA", header, encodeLen(len + 2), topic, packetId, payload)
+    else
+        return pack.pack(">bAPA", header, encodeLen(len), topic, payload)
+    end
+end
+
+local function packACK(id, dup, packetId)
+    return pack.pack(">bbH", id * 16 + dup * 8 + (id == PUBREL and 1 or 0) * 2, 0x02, packetId)
+end
+
+local function packZeroData(id, dup, qos, retain)
+    dup = dup or 0
+    qos = qos or 0
+    retain = retain or 0
+    return pack.pack(">bb", id * 16 + dup * 8 + qos * 2 + retain, 0)
+end
+
+local function unpack(s)
+    if #s < 2 then return end
+    log.debug("mqtt.unpack", #s, string.toHex(string.sub(s, 1, 50)))
+    
+    -- read remaining length
+    local len = 0
+    local multiplier = 1
+    local pos = 2
+    
+    repeat
+        if pos > #s then return end
+        local digit = string.byte(s, pos)
+        len = len + ((digit % 128) * multiplier)
+        multiplier = multiplier * 128
+        pos = pos + 1
+    until digit < 128
+    
+    if #s < len + pos - 1 then return end
+    
+    local header = string.byte(s, 1)
+    
+    --local packet = {id = (header - (header % 16)) / 16, dup = ((header % 16) - ((header % 16) % 8)) / 8, qos = bit.band(header, 0x06) / 2, retain = bit.band(header, 0x01)}
+    local packet = {id = (header - (header % 16)) / 16, dup = ((header % 16) - ((header % 16) % 8)) / 8, qos = (header & 0x06) / 2, retain = (header & 0x01)}
+    local nextpos
+    
+    if packet.id == CONNACK then
+        nextpos, packet.ackFlag, packet.rc = pack.unpack(s, "bb", pos)
+    elseif packet.id == PUBLISH then
+        nextpos, packet.topic = pack.unpack(s, ">P", pos)
+        if packet.qos > 0 then
+            nextpos, packet.packetId = pack.unpack(s, ">H", nextpos)
+        end
+        packet.payload = string.sub(s, nextpos, pos + len - 1)
+    elseif packet.id ~= PINGRESP then
+        if len >= 2 then
+            nextpos, packet.packetId = pack.unpack(s, ">H", pos)
+        else
+            packet.packetId = 0
+        end
+    end
+    
+    return packet, pos + len
+end
+
+local mqttc = {}
+mqttc.__index = mqttc
+
+--- 创建一个mqtt client实例
+-- @string clientId
+-- @number[opt=300] keepAlive 心跳间隔(单位为秒),默认300秒
+-- @string[opt=""] username 用户名,用户名为空配置为""或者nil
+-- @string[opt=""] password 密码,密码为空配置为""或者nil
+-- @number[opt=1] cleanSession 1/0
+-- @table[opt=nil] will 遗嘱参数,格式为{qos=, retain=, topic=, payload=}
+-- @string[opt="3.1.1"] version MQTT版本号
+-- @return table mqttc client实例
+-- @usage
+-- mqttc = mqtt.client("clientid-123")
+-- mqttc = mqtt.client("clientid-123",200)
+-- mqttc = mqtt.client("clientid-123",nil,"user","password")
+-- mqttc = mqtt.client("clientid-123",nil,"user","password",nil,nil,"3.1")
+function mqtt.client(clientId, keepAlive, username, password, cleanSession, will, version)
+    local o = {}
+    local packetId = 1
+    
+    if will then
+        will.flag = 1
+    else
+        will = {flag = 0, qos = 0, retain = 0, topic = "", payload = ""}
+    end
+    
+    o.clientId = clientId
+    o.keepAlive = keepAlive or 300
+    o.username = username or ""
+    o.password = password or ""
+    o.cleanSession = cleanSession or 1
+    o.version = version or "3.1.1"
+    o.will = will
+    o.commandTimeout = CLIENT_COMMAND_TIMEOUT
+    o.cache = {}-- 接收到的mqtt数据包缓冲
+    o.inbuf = "" -- 未完成的数据缓冲
+    o.connected = false
+    o.getNextPacketId = function()
+        packetId = packetId == 65535 and 1 or (packetId + 1)
+        return packetId
+    end
+    o.lastOTime = 0
+    
+    setmetatable(o, mqttc)
+    
+    return o
+end
+
+-- 检测是否需要发送心跳包
+function mqttc:checkKeepAlive()
+    if self.keepAlive == 0 then return true end
+    if os.time() - self.lastOTime >= self.keepAlive then
+        if not self:write(packZeroData(PINGREQ)) then
+            log.info("mqtt.client:", "pingreq send fail")
+            return false
+        end
+    end
+    return true
+end
+
+-- 发送mqtt数据
+function mqttc:write(data)
+    log.debug("mqtt.client:write", string.toHex(string.sub(data, 1, 50)))
+    local r = self.io:send(data)
+    if r then self.lastOTime = os.time() end
+    return r
+end
+
+-- 接收mqtt数据包
+function mqttc:read(timeout, msg, msgNoResume)
+    if not self:checkKeepAlive() then
+        log.warn("mqtt.read checkKeepAlive fail")
+        return false
+    end
+
+    local topic = "MQTTC_PKG_" .. tostring(self.io:id())
+    local result, data = sys.waitUntil(topic, timeout)
+    --log.info("mqtt.read", result, data)
+    if result then -- 收到topic消息
+        return true, data
+    else
+        if self.io:closed() == 1 then
+            return false
+        else
+            return false, "timeout"
+        end
+    end
+end
+
+local function update_resp(_self, data)
+    if #data > 0 then
+        if #_self.inbuf > 0 then
+            _self.inbuf = _self.inbuf .. data
+        else
+            _self.inbuf = data
+        end
+    end
+    --log.debug("mqttc", "data recv to unpack", _self.inbuf:toHex())
+    local packet, nextpos = unpack(_self.inbuf)
+    if packet then
+        log.info("mqttc", "msg unpack ok", packet.id)
+        _self.inbuf = string.sub(_self.inbuf, nextpos)
+        sys.publish("MQTTC_PKG_" .. tostring(_self.io:id()), packet)
+        if #_self.inbuf > 0 then
+            update_resp(_self, "")
+        end
+    else
+        log.info("mqttc", "data not full")
+    end
+
+    return true
+end
+
+-- 等待接收指定的mqtt消息
+function mqttc:waitfor(id, timeout, msg, msgNoResume)
+    for index, packet in ipairs(self.cache) do
+        if packet.id == id then
+            return true, table.remove(self.cache, index)
+        end
+    end
+    
+    while true do
+        local insertCache = true
+        local r, data, param = self:read(timeout, msg, msgNoResume)
+        if r then
+            if data.id == PUBLISH then
+                if data.qos > 0 then
+                    if not self:write(packACK(data.qos == 1 and PUBACK or PUBREC, 0, data.packetId)) then
+                        log.info("mqtt.client:waitfor", "send publish ack failed", data.qos)
+                        return false
+                    end
+                end
+            elseif data.id == PUBREC or data.id == PUBREL then
+                if not self:write(packACK(data.id == PUBREC and PUBREL or PUBCOMP, 0, data.packetId)) then
+                    log.info("mqtt.client:waitfor", "send ack fail", data.id == PUBREC and "PUBREC" or "PUBCOMP")
+                    return false
+                end
+                insertCache = false
+            end
+            
+            if data.id == id then
+                return true, data
+            end
+            if insertCache then table.insert(self.cache, data) end
+        else
+            return false, data, param
+        end
+    end
+end
+
+--- 连接mqtt服务器
+-- @string host 服务器地址
+-- @param port string或者number类型,服务器端口
+-- @string[opt="tcp"] transport "tcp"或者"tcp_ssl"
+-- @table[opt=nil] cert,table或者nil类型,ssl证书,当transport为"tcp_ssl"时,此参数才有意义。cert格式如下:
+-- {
+--     caCert = "ca.crt", --CA证书文件(Base64编码 X.509格式),如果存在此参数,则表示客户端会对服务器的证书进行校验;不存在则不校验
+--     clientCert = "client.crt", --客户端证书文件(Base64编码 X.509格式),服务器对客户端的证书进行校验时会用到此参数
+--     clientKey = "client.key", --客户端私钥文件(Base64编码 X.509格式)
+--     clientPassword = "123456", --客户端证书文件密码[可选]
+-- }
+-- @number timeout, 链接服务器最长超时时间
+-- @return result true表示成功,false或者nil表示失败
+-- @usage mqttc = mqtt.client("clientid-123", nil, nil, false); mqttc:connect("mqttserver.com", 1883, "tcp", 5)
+function mqttc:connect(host, port, transport, cert, timeout)
+    if self.connected then
+        log.info("mqtt.client:connect", "has connected")
+        return false
+    end
+    
+    if self.io then
+        self.io:clean()
+        self.io:close()
+        self.io = nil
+    end
+    
+    if transport and transport ~= "tcp" and transport ~= "tcp_ssl" then
+        log.info("mqtt.client:connect", "invalid transport", transport)
+        return false
+    end
+
+    self.io = socket.tcp(transport == "tcp_ssl" or type(cert) == "table", cert)
+    self.io:host(host)
+    self.io:port(port)
+    local connect_topic = "NETC_CONNECT_" .. tostring(self.io:id())
+    self.io:on("connect", function(id, re)
+        log.info("mqtt", "connect result", re, re == 1 and "OK" or "FAIL")
+        sys.publish(connect_topic, re == 1)
+        if re == 0 then
+            self.io:clean()
+            self.io:close()
+        end
+    end)
+    self.io:on("recv", function(id, data)
+        if not update_resp(self, data) then
+            log.info("mqtt", "close connect for bad data")
+            self.io:clean()
+            self.io:close()
+        end
+    end)
+    if not self.io:start() then
+        self.io:clean()
+        self.io:close()
+        log.info("mqtt", "fail to start socket thread")
+        return false
+    end
+    --log.info("mqtt", "wait for connect")
+    local result, linked = sys.waitUntil(connect_topic, 15000)
+    if not result then
+        log.info("mqtt", "connect timeout")
+        return false
+    end
+    if not linked  or self.io:closed() == 1 then
+        log.info("mqtt", "connect fail", result, linked, self.io:closed() == 1)
+        return false
+    end
+    --log.info("mqtt", "send packCONNECT")
+    if not self:write(packCONNECT(self.clientId, self.keepAlive, self.username, self.password, self.cleanSession, self.will, self.version)) then
+        log.info("mqtt.client:connect", "send fail")
+        return false
+    end
+    --log.info("mqtt", "waitfor CONNACK")
+    local r, packet = self:waitfor(CONNACK, self.commandTimeout, nil, true)
+    if not r or packet.rc ~= 0 then
+        log.info("mqtt.client:connect", "connack error", r and packet.rc or -1)
+        return false
+    end
+    
+    self.connected = true
+    --log.info("mqtt", "connected!~!")
+    
+    return true
+end
+
+--- 订阅主题
+-- @param topic,string或者table类型,一个主题时为string类型,多个主题时为table类型,主题内容为UTF8编码
+-- @param[opt=0] qos,number或者nil,topic为一个主题时,qos为number类型(0/1/2,默认0);topic为多个主题时,qos为nil
+-- @return bool true表示成功,false或者nil表示失败
+-- @usage
+-- mqttc:subscribe("/abc", 0) -- subscribe topic "/abc" with qos = 0
+-- mqttc:subscribe({["/topic1"] = 0, ["/topic2"] = 1, ["/topic3"] = 2}) -- subscribe multi topic
+function mqttc:subscribe(topic, qos)
+    if not self.connected then
+        log.info("mqtt.client:subscribe", "not connected")
+        return false
+    end
+    
+    local topics
+    if type(topic) == "string" then
+        topics = {[topic] = qos and qos or 0}
+    else
+        topics = topic
+    end
+    
+    if not self:write(packSUBSCRIBE(0, self.getNextPacketId(), topics)) then
+        log.info("mqtt.client:subscribe", "send failed")
+        return false
+    end
+    
+    if not self:waitfor(SUBACK, self.commandTimeout, nil, true) then
+        log.info("mqtt.client:subscribe", "wait ack failed")
+        return false
+    end
+    
+    return true
+end
+
+--- 取消订阅主题
+-- @param topic,string或者table类型,一个主题时为string类型,多个主题时为table类型,主题内容为UTF8编码
+-- @return bool true表示成功,false或者nil表示失败
+-- @usage
+-- mqttc:unsubscribe("/abc") -- unsubscribe topic "/abc"
+-- mqttc:unsubscribe({"/topic1", "/topic2", "/topic3"}) -- unsubscribe multi topic
+function mqttc:unsubscribe(topic)
+    if not self.connected then
+        log.info("mqtt.client:unsubscribe", "not connected")
+        return false
+    end
+    
+    local topics
+    if type(topic) == "string" then
+        topics = {topic}
+    else
+        topics = topic
+    end
+    
+    if not self:write(packUNSUBSCRIBE(0, self.getNextPacketId(), topics)) then
+        log.info("mqtt.client:unsubscribe", "send failed")
+        return false
+    end
+    
+    if not self:waitfor(UNSUBACK, self.commandTimeout, nil, true) then
+        log.info("mqtt.client:unsubscribe", "wait ack failed")
+        return false
+    end
+    
+    return true
+end
+
+--- 发布一条消息
+-- @string topic UTF8编码的字符串
+-- @string payload 用户自己控制payload的编码,mqtt.lua不会对payload做任何编码转换
+-- @number[opt=0] qos 0/1/2, default 0
+-- @number[opt=0] retain 0或者1
+-- @return bool 发布成功返回true,失败返回false
+-- @usage
+-- mqttc = mqtt.client("clientid-123", nil, nil, false)
+-- mqttc:connect("mqttserver.com", 1883, "tcp")
+-- mqttc:publish("/topic", "publish from luat mqtt client", 0)
+function mqttc:publish(topic, payload, qos, retain)
+    if not self.connected then
+        log.info("mqtt.client:publish", "not connected")
+        return false
+    end
+    
+    qos = qos or 0
+    retain = retain or 0
+    
+    if not self:write(packPUBLISH(0, qos, retain, qos > 0 and self.getNextPacketId() or 0, topic, payload)) then
+        log.info("mqtt.client:publish", "socket send failed")
+        return false
+    end
+    
+    if qos == 0 then return true end
+    
+    if not self:waitfor(qos == 1 and PUBACK or PUBCOMP, self.commandTimeout, nil, true) then
+        log.warn("mqtt.client:publish", "wait ack timeout")
+        return false
+    end
+    
+    return true
+end
+
+--- 接收消息
+-- @number timeout 接收超时时间,单位毫秒
+-- @string[opt=nil] msg 可选参数,控制socket所在的线程退出recv阻塞状态
+-- @return result 数据接收结果,true表示成功,false表示失败
+-- @return data 如果result为true,表示服务器发过来的包;如果result为false,表示错误信息,超时失败时为"timeout"
+-- @return param msg控制退出时,返回msg的字符串
+-- @usage
+-- true, packet = mqttc:receive(2000)
+-- false, error_message = mqttc:receive(2000)
+-- false, msg, para = mqttc:receive(2000)
+function mqttc:receive(timeout, msg)
+    if not self.connected then
+        log.info("mqtt.client:receive", "not connected")
+        return false
+    end
+    
+    return self:waitfor(PUBLISH, timeout, msg)
+end
+
+--- 断开与服务器的连接
+-- @return nil
+-- @usage
+-- mqttc = mqtt.client("clientid-123", nil, nil, false)
+-- mqttc:connect("mqttserver.com", 1883, "tcp")
+-- process data
+-- mqttc:disconnect()
+function mqttc:disconnect()
+    if self.io then
+        if self.connected then self:write(packZeroData(DISCONNECT)) end
+        self.io:close()
+        self.io = nil
+    end
+    self.cache = {}
+    self.inbuf = ""
+    self.connected = false
+end
+
+return mqtt

+ 363 - 0
bsp/air302/lib/sys.lua

@@ -0,0 +1,363 @@
+--- 模块功能:Luat协程调度框架
+--module(..., package.seeall)
+
+local sys = {}
+
+local table = _G.table
+local unpack = table.unpack
+local rtos = _G.rtos
+local coroutine = _G.coroutine
+local log = _G.log
+
+-- lib脚本版本号,只要lib中的任何一个脚本做了修改,都需要更新此版本号
+SCRIPT_LIB_VER = "1.0.0"
+
+-- TaskID最大值
+local TASK_TIMER_ID_MAX = 0x1FFFFF
+-- msgId 最大值(请勿修改否则会发生msgId碰撞的危险)
+local MSG_TIMER_ID_MAX = 0x7FFFFF
+
+-- 任务定时器id
+local taskTimerId = 0
+-- 消息定时器id
+local msgId = TASK_TIMER_ID_MAX
+-- 定时器id表
+local timerPool = {}
+local taskTimerPool = {}
+--消息定时器参数表
+local para = {}
+--定时器是否循环表
+local loop = {}
+--lua脚本运行出错时,是否回退为本地烧写的版本
+local sRollBack = true
+
+--- Task任务延时函数,只能用于任务函数中
+-- @number ms  整数,最大等待126322567毫秒
+-- @return 定时结束返回nil,被其他线程唤起返回调用线程传入的参数
+-- @usage sys.wait(30)
+function sys.wait(ms)
+    -- 参数检测,参数不能为负值
+    --assert(ms > 0, "The wait time cannot be negative!")
+    -- 选一个未使用的定时器ID给该任务线程
+    if taskTimerId >= TASK_TIMER_ID_MAX then taskTimerId = 0 end
+    taskTimerId = taskTimerId + 1
+    local timerid = taskTimerId
+    taskTimerPool[coroutine.running()] = timerid
+    timerPool[timerid] = coroutine.running()
+    -- 调用core的rtos定时器
+    if 1 ~= rtos.timer_start(timerid, ms) then log.debug("rtos.timer_start error") return end
+    -- 挂起调用的任务线程
+    local message = {coroutine.yield()}
+    if #message ~= 0 then
+        rtos.timer_stop(timerid)
+        taskTimerPool[coroutine.running()] = nil
+        timerPool[timerid] = nil
+        return unpack(message)
+    end
+end
+
+--- Task任务的条件等待函数(包括事件消息和定时器消息等条件),只能用于任务函数中。
+-- @param id 消息ID
+-- @number ms 等待超时时间,单位ms,最大等待126322567毫秒
+-- @return result 接收到消息返回true,超时返回false
+-- @return data 接收到消息返回消息参数
+-- @usage result, data = sys.waitUntil("SIM_IND", 120000)
+function sys.waitUntil(id, ms)
+    sys.subscribe(id, coroutine.running())
+    local message = ms and {sys.wait(ms)} or {coroutine.yield()}
+    sys.unsubscribe(id, coroutine.running())
+    return message[1] ~= nil, unpack(message, 2, #message)
+end
+
+--- Task任务的条件等待函数扩展(包括事件消息和定时器消息等条件),只能用于任务函数中。
+-- @param id 消息ID
+-- @number ms 等待超时时间,单位ms,最大等待126322567毫秒
+-- @return message 接收到消息返回message,超时返回false
+-- @return data 接收到消息返回消息参数
+-- @usage result, data = sys.waitUntilExt("SIM_IND", 120000)
+function sys.waitUntilExt(id, ms)
+    sys.subscribe(id, coroutine.running())
+    local message = ms and {sys.wait(ms)} or {coroutine.yield()}
+    sys.unsubscribe(id, coroutine.running())
+    if message[1] ~= nil then return unpack(message) end
+    return false
+end
+
+--- 创建一个任务线程,在模块最末行调用该函数并注册模块中的任务函数,main.lua导入该模块即可
+-- @param fun 任务函数名,用于resume唤醒时调用
+-- @param ... 任务函数fun的可变参数
+-- @return co  返回该任务的线程号
+-- @usage sys.taskInit(task1,'a','b')
+function sys.taskInit(fun, ...)
+    local co = coroutine.create(function(...)
+        local result, err = pcall(fun, ...)
+        if not result then 
+            print(err)
+            error(debug.traceback())
+        end
+    end)
+    coroutine.resume(co, ...)
+    return co
+end
+
+------------------------------------------ rtos消息回调处理部分 ------------------------------------------
+--[[
+函数名:cmpTable
+功能  :比较两个table的内容是否相同,注意:table中不能再包含table
+参数  :
+t1:第一个table
+t2:第二个table
+返回值:相同返回true,否则false
+]]
+local function cmpTable(t1, t2)
+    if not t2 then return #t1 == 0 end
+    if #t1 == #t2 then
+        for i = 1, #t1 do
+            if unpack(t1, i, i) ~= unpack(t2, i, i) then
+                return false
+            end
+        end
+        return true
+    end
+    return false
+end
+
+--- 关闭定时器
+-- @param val 值为number时,识别为定时器ID,值为回调函数时,需要传参数
+-- @param ... val值为函数时,函数的可变参数
+-- @return 无
+-- @usage timerStop(1)
+function sys.timerStop(val, ...)
+    -- val 为定时器ID
+    if type(val) == 'number' then
+        timerPool[val], para[val] = nil
+        rtos.timer_stop(val)
+    else
+        for k, v in pairs(timerPool) do
+            -- 回调函数相同
+            if type(v) == 'table' and v.cb == val or v == val then
+                -- 可变参数相同
+                if cmpTable({...}, para[k]) then
+                    rtos.timer_stop(k)
+                    timerPool[k], para[k] = nil
+                    break
+                end
+            end
+        end
+    end
+end
+
+--- 关闭同一回调函数的所有定时器
+-- @param fnc 定时器回调函数
+-- @return 无
+-- @usage timerStopAll(cbFnc)
+function sys.timerStopAll(fnc)
+    for k, v in pairs(timerPool) do
+        if type(v) == "table" and v.cb == fnc or v == fnc then
+            rtos.timer_stop(k)
+            timerPool[k], para[k] = nil
+        end
+    end
+end
+
+function sys.timerAdvStart(fnc, ms, _repeat, ...)
+    --回调函数和时长检测
+    --assert(fnc ~= nil, "sys.timerStart(first param) is nil !")
+    --assert(ms > 0, "sys.timerStart(Second parameter) is <= zero !")
+    -- 关闭完全相同的定时器
+    local arg = {...}
+    if #arg == 0 then
+        sys.timerStop(fnc)
+    else
+        sys.timerStop(fnc, ...)
+    end
+    -- 为定时器申请ID,ID值 1-20 留给任务,20-30留给消息专用定时器
+    while true do
+        if msgId >= MSG_TIMER_ID_MAX then msgId = TASK_TIMER_ID_MAX end
+        msgId = msgId + 1
+        if timerPool[msgId] == nil then
+            timerPool[msgId] = fnc
+            break
+        end
+    end
+    --调用底层接口启动定时器
+    if rtos.timer_start(msgId, ms, _repeat) ~= 1 then return end
+    --如果存在可变参数,在定时器参数表中保存参数
+    if #arg ~= 0 then
+        para[msgId] = arg
+    end
+    --返回定时器id
+    return msgId
+end
+
+--- 开启一个定时器
+-- @param fnc 定时器回调函数
+-- @number ms 整数,最大定时126322567毫秒
+-- @param ... 可变参数 fnc的参数
+-- @return number 定时器ID,如果失败,返回nil
+function sys.timerStart(fnc, ms, ...)
+    return sys.timerAdvStart(fnc, ms, 0, ...)
+end
+
+--- 开启一个循环定时器
+-- @param fnc 定时器回调函数
+-- @number ms 整数,最大定时126322567毫秒
+-- @param ... 可变参数 fnc的参数
+-- @return number 定时器ID,如果失败,返回nil
+function sys.timerLoopStart(fnc, ms, ...)
+    return sys.timerAdvStart(fnc, ms, -1, ...)
+end
+
+--- 判断某个定时器是否处于开启状态
+-- @param val 有两种形式
+--一种是开启定时器时返回的定时器id,此形式时不需要再传入可变参数...就能唯一标记一个定时器
+--另一种是开启定时器时的回调函数,此形式时必须再传入可变参数...才能唯一标记一个定时器
+-- @param ... 可变参数
+-- @return number 开启状态返回true,否则nil
+function sys.timerIsActive(val, ...)
+    if type(val) == "number" then
+        return timerPool[val]
+    else
+        for k, v in pairs(timerPool) do
+            if v == val then
+                if cmpTable({...}, para[k]) then return true end
+            end
+        end
+    end
+end
+
+
+------------------------------------------ LUA应用消息订阅/发布接口 ------------------------------------------
+-- 订阅者列表
+local subscribers = {}
+--内部消息队列
+local messageQueue = {}
+
+--- 订阅消息
+-- @param id 消息id
+-- @param callback 消息回调处理
+-- @usage subscribe("NET_STATUS_IND", callback)
+function sys.subscribe(id, callback)
+    if not id or type(id) == "boolean" or (type(callback) ~= "function" and type(callback) ~= "thread") then
+        log.warn("warning: sys.subscribe invalid parameter", id, callback)
+        return
+    end
+    if not subscribers[id] then subscribers[id] = {} end
+    subscribers[id][callback] = true
+end
+--- 取消订阅消息
+-- @param id 消息id
+-- @param callback 消息回调处理
+-- @usage unsubscribe("NET_STATUS_IND", callback)
+function sys.unsubscribe(id, callback)
+    if not id or type(id) == "boolean" or (type(callback) ~= "function" and type(callback) ~= "thread") then
+        log.warn("warning: sys.unsubscribe invalid parameter", id, callback)
+        return
+    end
+    if subscribers[id] then subscribers[id][callback] = nil end
+    -- 判断消息是否无其他订阅
+    for k, _ in pairs(subscribers[id]) do
+        return
+    end
+    subscribers[id] = nil
+end
+
+--- 发布内部消息,存储在内部消息队列中
+-- @param ... 可变参数,用户自定义
+-- @return 无
+-- @usage publish("NET_STATUS_IND")
+function sys.publish(...)
+    table.insert(messageQueue, {...})
+end
+
+-- 分发消息
+local function dispatch()
+    while true do
+        if #messageQueue == 0 then
+            break
+        end
+        local message = table.remove(messageQueue, 1)
+        if subscribers[message[1]] then
+            for callback, _ in pairs(subscribers[message[1]]) do
+                if type(callback) == "function" then
+                    callback(unpack(message, 2, #message))
+                elseif type(callback) == "thread" then
+                    coroutine.resume(callback, unpack(message))
+                end
+            end
+        end
+    end
+end
+
+-- rtos消息回调
+--local handlers = {}
+--setmetatable(handlers, {__index = function() return function() end end, })
+
+--- 注册rtos消息回调处理函数
+-- @number id 消息类型id
+-- @param handler 消息处理函数
+-- @return 无
+-- @usage rtos.on(rtos.MSG_KEYPAD, function(param) handle keypad message end)
+--function sys.on(id, handler)
+--    handlers[id] = handler
+--end
+
+------------------------------------------ Luat 主调度框架  ------------------------------------------
+local function safeRun()
+    -- 分发内部消息
+    dispatch()
+    -- 阻塞读取外部消息
+    local msg, param, exparam = rtos.receive(rtos.INF_TIMEOUT)
+    --log.info("sys", msg, param, exparam, tableNSize(timerPool), tableNSize(para), tableNSize(taskTimerPool), tableNSize(subscribers))
+    -- 空消息?
+    if not msg or msg == 0 then
+        -- 无任何操作
+    -- 判断是否为定时器消息,并且消息是否注册
+    elseif msg == rtos.MSG_TIMER and timerPool[param] then
+        if param < TASK_TIMER_ID_MAX then
+            local taskId = timerPool[param]
+            timerPool[param] = nil
+            if taskTimerPool[taskId] == param then
+                taskTimerPool[taskId] = nil
+                coroutine.resume(taskId)
+            end
+        else
+            local cb = timerPool[param]
+            --如果不是循环定时器,从定时器id表中删除此定时器
+            if exparam == 0 then timerPool[param] = nil end
+            if para[param] ~= nil then
+                cb(unpack(para[param]))
+                if exparam == 0 then para[param] = nil end
+            else
+                cb()
+            end
+            --如果是循环定时器,继续启动此定时器
+            --if loop[param] then rtos.timer_start(param, loop[param]) end
+        end
+    --其他消息(音频消息、充电管理消息、按键消息等)
+    --elseif type(msg) == "number" then
+    --    handlers[msg](param, exparam)
+    --else
+    --    handlers[msg.id](msg)
+    end
+end
+
+--- run()从底层获取core消息并及时处理相关消息,查询定时器并调度各注册成功的任务线程运行和挂起
+-- @return 无
+-- @usage sys.run()
+function sys.run()
+    local result, err
+    while true do
+        --if sRollBack then
+            safeRun()
+        --else
+        --    result, err = pcall(safeRun)
+        --    if not result then rtos.restart(err) end
+        --end
+    end
+end
+
+_G.sys_pub = sys.publish
+
+return sys
+----------------------------

+ 70 - 0
bsp/air302/mklfs/Makefile

@@ -0,0 +1,70 @@
+CFLAGS		?= -std=gnu99 -Os -Wall
+CXXFLAGS	?= -std=gnu++11 -Os -Wall
+
+VERSION ?= $(shell git describe --always)
+
+ifeq ($(OS),Windows_NT)
+	TARGET_OS := WINDOWS
+	DIST_SUFFIX := windows
+	ARCHIVE_CMD := 7z a
+	ARCHIVE_EXTENSION := zip
+	TARGET := mklfs.exe
+	TARGET_CFLAGS := -O2 -mno-ms-bitfields -Ilfs -I. -DVERSION=\"$(VERSION)\"
+	TARGET_LDFLAGS := -Wl,-static -static-libgcc
+	TARGET_CXXFLAGS := -Ilfs -I. -DVERSION=\"$(VERSION)\" -D__NO_INLINE__
+	CC=gcc
+	CXX=g++
+else
+	UNAME_S := $(shell uname -s)
+	ifeq ($(UNAME_S),Linux)
+		TARGET_OS := LINUX
+		UNAME_P := $(shell uname -p)
+		ifeq ($(UNAME_P),x86_64)
+			DIST_SUFFIX := linux64
+		endif
+		ifneq ($(filter %86,$(UNAME_P)),)
+			DIST_SUFFIX := linux32
+		endif
+		CC=gcc
+		CXX=g++
+		TARGET_CFLAGS   = -std=gnu99 -O2 -Wall -Ilfs -I. -D$(TARGET_OS) -DVERSION=\"$(VERSION)\" -D__NO_INLINE__
+		TARGET_CXXFLAGS = -std=gnu++11 -O2 -Wall -Ilfs -I. -D$(TARGET_OS) -DVERSION=\"$(VERSION)\" -D__NO_INLINE__
+	endif
+	ifeq ($(UNAME_S),Darwin)
+		TARGET_OS := OSX
+		DIST_SUFFIX := osx
+		CC=clang
+		CXX=clang++
+		TARGET_CFLAGS   = -std=gnu99 -O2 -Wall -Ilfs -I. -D$(TARGET_OS) -DVERSION=\"$(VERSION)\" -D__NO_INLINE__ -mmacosx-version-min=10.7 -arch x86_64
+		TARGET_CXXFLAGS = -std=gnu++11 -O2 -Wall -Ilfs -I. -D$(TARGET_OS) -DVERSION=\"$(VERSION)\" -D__NO_INLINE__ -mmacosx-version-min=10.7 -arch x86_64 -stdlib=libc++
+		TARGET_LDFLAGS  = -arch x86_64 -stdlib=libc++
+	endif
+	ARCHIVE_CMD := tar czf
+	ARCHIVE_EXTENSION := tar.gz
+	TARGET := mklfs
+endif
+
+OBJ             := mklfs.o \
+                   lfs/lfs.o \
+                   lfs/lfs_util.o \
+				   
+VERSION ?= $(shell git describe --always)
+
+.PHONY: all clean
+
+all: $(TARGET)
+
+$(TARGET):
+	@echo "Building mklfs ..."
+	$(CC) $(TARGET_CFLAGS) -c lfs/lfs.c -o lfs/lfs.o
+	$(CC) $(TARGET_CFLAGS) -c lfs/lfs_util.c -o lfs/lfs_util.o
+	$(CC) $(TARGET_CFLAGS) -c mklfs.c -o mklfs.o
+	$(CC) $(TARGET_CFLAGS) -c unlfs.c -o unlfs.o
+	$(CC) $(TARGET_CFLAGS) -o $(TARGET) $(OBJ) $(TARGET_LDFLAGS)
+	$(CC) $(TARGET_CFLAGS) -o unlfs.exe unlfs.o lfs/lfs.o lfs/lfs_util.o $(TARGET_LDFLAGS)
+
+clean:
+	@rm -f *.o
+	@rm -f lfs/*.o
+	@rm -f $(TARGET)
+	@rm -f unlfs.exe

+ 13 - 0
bsp/air302/mklfs/disk/lua/main.lua

@@ -0,0 +1,13 @@
+
+print(_VERSION)
+print("from main.lua file")
+
+rtos.timer_start(1, 3000)
+local t = 1
+while 1 do
+    rtos.receive(0)
+    print("timer trigger, 3000ms" .. tostring(t))
+    t = t + 1
+    rtos.timer_stop(1)
+    rtos.timer_start(1, 3000)
+end

+ 46 - 0
bsp/air302/mklfs/lfs/Makefile.inc

@@ -0,0 +1,46 @@
+LFS_DIR        := $(TOP)/PLAT/middleware/thirdparty/littlefs
+
+CFLAGS_INC     +=  -I $(LFS_DIR) \
+                   -I $(LFS_DIR)/port \
+              
+
+LFS_SRC_DIRS += $(LFS_DIR) \
+                $(LFS_DIR)/port
+LFS_EXCLUDE_FILES :=
+LFS_CSRC = $(foreach dir, $(LFS_SRC_DIRS), $(wildcard $(dir)/*.c))
+LFS_CFILES = $(filter-out $(LFS_EXCLUDE_FILES), $(LFS_CSRC))
+LFS_COBJSTEMP := $(patsubst %.c, %.o, $(LFS_CFILES))
+LFS_COBJSTEMP :=$(subst $(LFS_DIR),PLAT/middleware/thirdparty/littlefs,$(LFS_COBJSTEMP))
+LFS_COBJS := $(addprefix $(BUILDDIR)/, $(LFS_COBJSTEMP))
+
+CFLAGS += -DLFS_NAME_MAX=63 -DLFS_DEBUG_TRACE -DLFS_THREAD_SAFE_MUTEX
+
+-include $(LFS_COBJS:.o=.d)
+
+
+ifeq ($(TOOLCHAIN),GCC)
+
+CFLAGS_INC += -I $(TOP)/PLAT/os/freertos/portable/gcc
+
+lib-y += liblfs.a
+
+$(BUILDDIR)/lib/liblfs.a: $(LFS_COBJS)
+	@mkdir -p $(dir $@)
+	$(ECHO) AR $@
+	$(Q)$(AR) -cr $@ $^
+
+endif
+
+ifeq ($(TOOLCHAIN),ARMCC)
+
+CFLAGS_INC += -I $(TOP)/PLAT/os/freertos/portable/keil
+
+
+lib-y += liblfs.lib
+
+$(BUILDDIR)/lib/liblfs.lib: $(LFS_COBJS)
+	@mkdir -p $(dir $@)
+	$(ECHO) AR $@
+	$(Q)$(AR) $(ARFLAGS) $@ $^
+
+endif

+ 4762 - 0
bsp/air302/mklfs/lfs/lfs.c

@@ -0,0 +1,4762 @@
+/*
+ * The little filesystem
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#include "lfs.h"
+#include "lfs_util.h"
+
+#define LFS_BLOCK_NULL ((lfs_block_t)-1)
+#define LFS_BLOCK_INLINE ((lfs_block_t)-2)
+
+/// Caching block device operations ///
+static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) {
+    // do not zero, cheaper if cache is readonly or only going to be
+    // written with identical data (during relocates)
+    (void)lfs;
+    rcache->block = LFS_BLOCK_NULL;
+}
+
+static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) {
+    // zero to avoid information leak
+    memset(pcache->buffer, 0xff, lfs->cfg->cache_size);
+    pcache->block = LFS_BLOCK_NULL;
+}
+
+static int lfs_bd_read(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
+        lfs_block_t block, lfs_off_t off,
+        void *buffer, lfs_size_t size) {
+    uint8_t *data = buffer;
+    LFS_ASSERT(block != LFS_BLOCK_NULL);
+    if (off+size > lfs->cfg->block_size) {
+        return LFS_ERR_CORRUPT;
+    }
+
+    while (size > 0) {
+        lfs_size_t diff = size;
+
+        if (pcache && block == pcache->block &&
+                off < pcache->off + pcache->size) {
+            if (off >= pcache->off) {
+                // is already in pcache?
+                diff = lfs_min(diff, pcache->size - (off-pcache->off));
+                memcpy(data, &pcache->buffer[off-pcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // pcache takes priority
+            diff = lfs_min(diff, pcache->off-off);
+        }
+
+        if (block == rcache->block &&
+                off < rcache->off + rcache->size) {
+            if (off >= rcache->off) {
+                // is already in rcache?
+                diff = lfs_min(diff, rcache->size - (off-rcache->off));
+                memcpy(data, &rcache->buffer[off-rcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // rcache takes priority
+            diff = lfs_min(diff, rcache->off-off);
+        }
+
+        // load to cache, first condition can no longer fail
+        LFS_ASSERT(block < lfs->cfg->block_count);
+        rcache->block = block;
+        rcache->off = lfs_aligndown(off, lfs->cfg->read_size);
+        rcache->size = lfs_min(
+                lfs_min(
+                    lfs_alignup(off+hint, lfs->cfg->read_size),
+                    lfs->cfg->block_size)
+                - rcache->off,
+                lfs->cfg->cache_size);
+        int err = lfs->cfg->read(lfs->cfg, rcache->block,
+                rcache->off, rcache->buffer, rcache->size);
+        LFS_ASSERT(err <= 0);
+        if (err) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+enum {
+    LFS_CMP_EQ = 0,
+    LFS_CMP_LT = 1,
+    LFS_CMP_GT = 2,
+};
+
+static int lfs_bd_cmp(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
+        lfs_block_t block, lfs_off_t off,
+        const void *buffer, lfs_size_t size) {
+    const uint8_t *data = buffer;
+
+    for (lfs_off_t i = 0; i < size; i++) {
+        uint8_t dat;
+        int err = lfs_bd_read(lfs,
+                pcache, rcache, hint-i,
+                block, off+i, &dat, 1);
+        if (err) {
+            return err;
+        }
+
+        if (dat != data[i]) {
+            return (dat < data[i]) ? LFS_CMP_LT : LFS_CMP_GT;
+        }
+    }
+
+    return LFS_CMP_EQ;
+}
+
+static int lfs_bd_flush(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) {
+    if (pcache->block != LFS_BLOCK_NULL && pcache->block != LFS_BLOCK_INLINE) {
+        LFS_ASSERT(pcache->block < lfs->cfg->block_count);
+        lfs_size_t diff = lfs_alignup(pcache->size, lfs->cfg->prog_size);
+        int err = lfs->cfg->prog(lfs->cfg, pcache->block,
+                pcache->off, pcache->buffer, diff);
+        LFS_ASSERT(err <= 0);
+        if (err) {
+            return err;
+        }
+
+        if (validate) {
+            // check data on disk
+            lfs_cache_drop(lfs, rcache);
+            int res = lfs_bd_cmp(lfs,
+                    NULL, rcache, diff,
+                    pcache->block, pcache->off, pcache->buffer, diff);
+            if (res < 0) {
+                return res;
+            }
+
+            if (res != LFS_CMP_EQ) {
+                return LFS_ERR_CORRUPT;
+            }
+        }
+
+        lfs_cache_zero(lfs, pcache);
+    }
+
+    return 0;
+}
+
+static int lfs_bd_sync(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) {
+    lfs_cache_drop(lfs, rcache);
+
+    int err = lfs_bd_flush(lfs, pcache, rcache, validate);
+    if (err) {
+        return err;
+    }
+
+    err = lfs->cfg->sync(lfs->cfg);
+    LFS_ASSERT(err <= 0);
+    return err;
+}
+
+static int lfs_bd_prog(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate,
+        lfs_block_t block, lfs_off_t off,
+        const void *buffer, lfs_size_t size) {
+    const uint8_t *data = buffer;
+    LFS_ASSERT(block != LFS_BLOCK_NULL);
+    LFS_ASSERT(off + size <= lfs->cfg->block_size);
+
+    while (size > 0) {
+        if (block == pcache->block &&
+                off >= pcache->off &&
+                off < pcache->off + lfs->cfg->cache_size) {
+            // already fits in pcache?
+            lfs_size_t diff = lfs_min(size,
+                    lfs->cfg->cache_size - (off-pcache->off));
+            memcpy(&pcache->buffer[off-pcache->off], data, diff);
+
+            data += diff;
+            off += diff;
+            size -= diff;
+
+            pcache->size = lfs_max(pcache->size, off - pcache->off);
+            if (pcache->size == lfs->cfg->cache_size) {
+                // eagerly flush out pcache if we fill up
+                int err = lfs_bd_flush(lfs, pcache, rcache, validate);
+                if (err) {
+                    return err;
+                }
+            }
+
+            continue;
+        }
+
+        // pcache must have been flushed, either by programming and
+        // entire block or manually flushing the pcache
+        LFS_ASSERT(pcache->block == LFS_BLOCK_NULL);
+
+        // prepare pcache, first condition can no longer fail
+        pcache->block = block;
+        pcache->off = lfs_aligndown(off, lfs->cfg->prog_size);
+        pcache->size = 0;
+    }
+
+    return 0;
+}
+
+static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) {
+    LFS_ASSERT(block < lfs->cfg->block_count);
+    int err = lfs->cfg->erase(lfs->cfg, block);
+    LFS_ASSERT(err <= 0);
+    return err;
+}
+
+
+/// Small type-level utilities ///
+// operations on block pairs
+static inline void lfs_pair_swap(lfs_block_t pair[2]) {
+    lfs_block_t t = pair[0];
+    pair[0] = pair[1];
+    pair[1] = t;
+}
+
+static inline bool lfs_pair_isnull(const lfs_block_t pair[2]) {
+    return pair[0] == LFS_BLOCK_NULL || pair[1] == LFS_BLOCK_NULL;
+}
+
+static inline int lfs_pair_cmp(
+        const lfs_block_t paira[2],
+        const lfs_block_t pairb[2]) {
+    return !(paira[0] == pairb[0] || paira[1] == pairb[1] ||
+             paira[0] == pairb[1] || paira[1] == pairb[0]);
+}
+
+static inline bool lfs_pair_sync(
+        const lfs_block_t paira[2],
+        const lfs_block_t pairb[2]) {
+    return (paira[0] == pairb[0] && paira[1] == pairb[1]) ||
+           (paira[0] == pairb[1] && paira[1] == pairb[0]);
+}
+
+static inline void lfs_pair_fromle32(lfs_block_t pair[2]) {
+    pair[0] = lfs_fromle32(pair[0]);
+    pair[1] = lfs_fromle32(pair[1]);
+}
+
+static inline void lfs_pair_tole32(lfs_block_t pair[2]) {
+    pair[0] = lfs_tole32(pair[0]);
+    pair[1] = lfs_tole32(pair[1]);
+}
+
+// operations on 32-bit entry tags
+typedef uint32_t lfs_tag_t;
+typedef int32_t lfs_stag_t;
+
+#define LFS_MKTAG(type, id, size) \
+    (((lfs_tag_t)(type) << 20) | ((lfs_tag_t)(id) << 10) | (lfs_tag_t)(size))
+
+static inline bool lfs_tag_isvalid(lfs_tag_t tag) {
+    return !(tag & 0x80000000);
+}
+
+static inline bool lfs_tag_isdelete(lfs_tag_t tag) {
+    return ((int32_t)(tag << 22) >> 22) == -1;
+}
+
+static inline uint16_t lfs_tag_type1(lfs_tag_t tag) {
+    return (tag & 0x70000000) >> 20;
+}
+
+static inline uint16_t lfs_tag_type3(lfs_tag_t tag) {
+    return (tag & 0x7ff00000) >> 20;
+}
+
+static inline uint8_t lfs_tag_chunk(lfs_tag_t tag) {
+    return (tag & 0x0ff00000) >> 20;
+}
+
+static inline int8_t lfs_tag_splice(lfs_tag_t tag) {
+    return (int8_t)lfs_tag_chunk(tag);
+}
+
+static inline uint16_t lfs_tag_id(lfs_tag_t tag) {
+    return (tag & 0x000ffc00) >> 10;
+}
+
+static inline lfs_size_t lfs_tag_size(lfs_tag_t tag) {
+    return tag & 0x000003ff;
+}
+
+static inline lfs_size_t lfs_tag_dsize(lfs_tag_t tag) {
+    return sizeof(tag) + lfs_tag_size(tag + lfs_tag_isdelete(tag));
+}
+
+// operations on attributes in attribute lists
+struct lfs_mattr {
+    lfs_tag_t tag;
+    const void *buffer;
+};
+
+struct lfs_diskoff {
+    lfs_block_t block;
+    lfs_off_t off;
+};
+
+#define LFS_MKATTRS(...) \
+    (struct lfs_mattr[]){__VA_ARGS__}, \
+    sizeof((struct lfs_mattr[]){__VA_ARGS__}) / sizeof(struct lfs_mattr)
+
+// operations on global state
+static inline void lfs_gstate_xor(struct lfs_gstate *a,
+        const struct lfs_gstate *b) {
+    for (int i = 0; i < 3; i++) {
+        ((uint32_t*)a)[i] ^= ((const uint32_t*)b)[i];
+    }
+}
+
+static inline bool lfs_gstate_iszero(const struct lfs_gstate *a) {
+    for (int i = 0; i < 3; i++) {
+        if (((uint32_t*)a)[i] != 0) {
+            return false;
+        }
+    }
+    return true;
+}
+
+static inline bool lfs_gstate_hasorphans(const struct lfs_gstate *a) {
+    return lfs_tag_size(a->tag);
+}
+
+static inline uint8_t lfs_gstate_getorphans(const struct lfs_gstate *a) {
+    return lfs_tag_size(a->tag);
+}
+
+static inline bool lfs_gstate_hasmove(const struct lfs_gstate *a) {
+    return lfs_tag_type1(a->tag);
+}
+
+static inline bool lfs_gstate_hasmovehere(const struct lfs_gstate *a,
+        const lfs_block_t *pair) {
+    return lfs_tag_type1(a->tag) && lfs_pair_cmp(a->pair, pair) == 0;
+}
+
+static inline void lfs_gstate_xororphans(struct lfs_gstate *a,
+        const struct lfs_gstate *b, bool orphans) {
+    a->tag ^= LFS_MKTAG(0x800, 0, 0) & (b->tag ^ ((uint32_t)orphans << 31));
+}
+
+static inline void lfs_gstate_xormove(struct lfs_gstate *a,
+        const struct lfs_gstate *b, uint16_t id, const lfs_block_t pair[2]) {
+    a->tag ^= LFS_MKTAG(0x7ff, 0x3ff, 0) & (b->tag ^ (
+            (id != 0x3ff) ? LFS_MKTAG(LFS_TYPE_DELETE, id, 0) : 0));
+    a->pair[0] ^= b->pair[0] ^ ((id != 0x3ff) ? pair[0] : 0);
+    a->pair[1] ^= b->pair[1] ^ ((id != 0x3ff) ? pair[1] : 0);
+}
+
+static inline void lfs_gstate_fromle32(struct lfs_gstate *a) {
+    a->tag     = lfs_fromle32(a->tag);
+    a->pair[0] = lfs_fromle32(a->pair[0]);
+    a->pair[1] = lfs_fromle32(a->pair[1]);
+}
+
+static inline void lfs_gstate_tole32(struct lfs_gstate *a) {
+    a->tag     = lfs_tole32(a->tag);
+    a->pair[0] = lfs_tole32(a->pair[0]);
+    a->pair[1] = lfs_tole32(a->pair[1]);
+}
+
+// other endianness operations
+static void lfs_ctz_fromle32(struct lfs_ctz *ctz) {
+    ctz->head = lfs_fromle32(ctz->head);
+    ctz->size = lfs_fromle32(ctz->size);
+}
+
+static void lfs_ctz_tole32(struct lfs_ctz *ctz) {
+    ctz->head = lfs_tole32(ctz->head);
+    ctz->size = lfs_tole32(ctz->size);
+}
+
+static inline void lfs_superblock_fromle32(lfs_superblock_t *superblock) {
+    superblock->version     = lfs_fromle32(superblock->version);
+    superblock->block_size  = lfs_fromle32(superblock->block_size);
+    superblock->block_count = lfs_fromle32(superblock->block_count);
+    superblock->name_max    = lfs_fromle32(superblock->name_max);
+    superblock->file_max    = lfs_fromle32(superblock->file_max);
+    superblock->attr_max    = lfs_fromle32(superblock->attr_max);
+}
+
+static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) {
+    superblock->version     = lfs_tole32(superblock->version);
+    superblock->block_size  = lfs_tole32(superblock->block_size);
+    superblock->block_count = lfs_tole32(superblock->block_count);
+    superblock->name_max    = lfs_tole32(superblock->name_max);
+    superblock->file_max    = lfs_tole32(superblock->file_max);
+    superblock->attr_max    = lfs_tole32(superblock->attr_max);
+}
+
+
+/// Internal operations predeclared here ///
+static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir,
+        const struct lfs_mattr *attrs, int attrcount);
+static int lfs_dir_compact(lfs_t *lfs,
+        lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
+        lfs_mdir_t *source, uint16_t begin, uint16_t end);
+static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file);
+static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file);
+static void lfs_fs_preporphans(lfs_t *lfs, int8_t orphans);
+static void lfs_fs_prepmove(lfs_t *lfs,
+        uint16_t id, const lfs_block_t pair[2]);
+static int lfs_fs_pred(lfs_t *lfs, const lfs_block_t dir[2],
+        lfs_mdir_t *pdir);
+static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t dir[2],
+        lfs_mdir_t *parent);
+static int lfs_fs_relocate(lfs_t *lfs,
+        const lfs_block_t oldpair[2], lfs_block_t newpair[2]);
+static int lfs_fs_forceconsistency(lfs_t *lfs);
+static int lfs_deinit(lfs_t *lfs);
+#ifdef LFS_MIGRATE
+static int lfs1_traverse(lfs_t *lfs,
+        int (*cb)(void*, lfs_block_t), void *data);
+#endif
+
+/// Block allocator ///
+static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
+    lfs_t *lfs = (lfs_t*)p;
+    lfs_block_t off = ((block - lfs->free.off)
+            + lfs->cfg->block_count) % lfs->cfg->block_count;
+
+    if (off < lfs->free.size) {
+        lfs->free.buffer[off / 32] |= 1U << (off % 32);
+    }
+
+    return 0;
+}
+
+static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
+    while (true) {
+        while (lfs->free.i != lfs->free.size) {
+            lfs_block_t off = lfs->free.i;
+            lfs->free.i += 1;
+            lfs->free.ack -= 1;
+
+            if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) {
+                // found a free block
+                *block = (lfs->free.off + off) % lfs->cfg->block_count;
+
+                // eagerly find next off so an alloc ack can
+                // discredit old lookahead blocks
+                while (lfs->free.i != lfs->free.size &&
+                        (lfs->free.buffer[lfs->free.i / 32]
+                            & (1U << (lfs->free.i % 32)))) {
+                    lfs->free.i += 1;
+                    lfs->free.ack -= 1;
+                }
+
+                return 0;
+            }
+        }
+
+        // check if we have looked at all blocks since last ack
+        if (lfs->free.ack == 0) {
+            LFS_ERROR("No more free space %"PRIu32,
+                    lfs->free.i + lfs->free.off);
+            return LFS_ERR_NOSPC;
+        }
+
+        lfs->free.off = (lfs->free.off + lfs->free.size)
+                % lfs->cfg->block_count;
+        lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size, lfs->free.ack);
+        lfs->free.i = 0;
+
+        // find mask of free blocks from tree
+        memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size);
+        int err = lfs_fs_traverse(lfs, lfs_alloc_lookahead, lfs);
+        if (err) {
+            return err;
+        }
+    }
+}
+
+static void lfs_alloc_ack(lfs_t *lfs) {
+    lfs->free.ack = lfs->cfg->block_count;
+}
+
+
+/// Metadata pair and directory operations ///
+static lfs_stag_t lfs_dir_getslice(lfs_t *lfs, const lfs_mdir_t *dir,
+        lfs_tag_t gmask, lfs_tag_t gtag,
+        lfs_off_t goff, void *gbuffer, lfs_size_t gsize) {
+    lfs_off_t off = dir->off;
+    lfs_tag_t ntag = dir->etag;
+    lfs_stag_t gdiff = 0;
+
+    if (lfs_gstate_hasmovehere(&lfs->gstate, dir->pair) &&
+            lfs_tag_id(gtag) <= lfs_tag_id(lfs->gstate.tag)) {
+        // synthetic moves
+        gdiff -= LFS_MKTAG(0, 1, 0);
+    }
+
+    // iterate over dir block backwards (for faster lookups)
+    while (off >= sizeof(lfs_tag_t) + lfs_tag_dsize(ntag)) {
+        off -= lfs_tag_dsize(ntag);
+        lfs_tag_t tag = ntag;
+        int err = lfs_bd_read(lfs,
+                NULL, &lfs->rcache, sizeof(ntag),
+                dir->pair[0], off, &ntag, sizeof(ntag));
+        if (err) {
+            return err;
+        }
+
+        ntag = (lfs_frombe32(ntag) ^ tag) & 0x7fffffff;
+
+        if (lfs_tag_id(gmask) != 0 &&
+                lfs_tag_type1(tag) == LFS_TYPE_SPLICE &&
+                lfs_tag_id(tag) <= lfs_tag_id(gtag - gdiff)) {
+            if (tag == (LFS_MKTAG(LFS_TYPE_CREATE, 0, 0) |
+                    (LFS_MKTAG(0, 0x3ff, 0) & (gtag - gdiff)))) {
+                // found where we were created
+                return LFS_ERR_NOENT;
+            }
+
+            // move around splices
+            gdiff += LFS_MKTAG(0, lfs_tag_splice(tag), 0);
+        }
+
+        if ((gmask & tag) == (gmask & (gtag - gdiff))) {
+            if (lfs_tag_isdelete(tag)) {
+                return LFS_ERR_NOENT;
+            }
+
+            lfs_size_t diff = lfs_min(lfs_tag_size(tag), gsize);
+            err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, diff,
+                    dir->pair[0], off+sizeof(tag)+goff, gbuffer, diff);
+            if (err) {
+                return err;
+            }
+
+            memset((uint8_t*)gbuffer + diff, 0, gsize - diff);
+
+            return tag + gdiff;
+        }
+    }
+
+    return LFS_ERR_NOENT;
+}
+
+static lfs_stag_t lfs_dir_get(lfs_t *lfs, const lfs_mdir_t *dir,
+        lfs_tag_t gmask, lfs_tag_t gtag, void *buffer) {
+    return lfs_dir_getslice(lfs, dir,
+            gmask, gtag,
+            0, buffer, lfs_tag_size(gtag));
+}
+
+static int lfs_dir_getread(lfs_t *lfs, const lfs_mdir_t *dir,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
+        lfs_tag_t gmask, lfs_tag_t gtag,
+        lfs_off_t off, void *buffer, lfs_size_t size) {
+    uint8_t *data = buffer;
+    if (off+size > lfs->cfg->block_size) {
+        return LFS_ERR_CORRUPT;
+    }
+
+    while (size > 0) {
+        lfs_size_t diff = size;
+
+        if (pcache && pcache->block == LFS_BLOCK_INLINE &&
+                off < pcache->off + pcache->size) {
+            if (off >= pcache->off) {
+                // is already in pcache?
+                diff = lfs_min(diff, pcache->size - (off-pcache->off));
+                memcpy(data, &pcache->buffer[off-pcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // pcache takes priority
+            diff = lfs_min(diff, pcache->off-off);
+        }
+
+        if (rcache->block == LFS_BLOCK_INLINE &&
+                off < rcache->off + rcache->size) {
+            if (off >= rcache->off) {
+                // is already in rcache?
+                diff = lfs_min(diff, rcache->size - (off-rcache->off));
+                memcpy(data, &rcache->buffer[off-rcache->off], diff);
+
+                data += diff;
+                off += diff;
+                size -= diff;
+                continue;
+            }
+
+            // rcache takes priority
+            diff = lfs_min(diff, rcache->off-off);
+        }
+
+        // load to cache, first condition can no longer fail
+        rcache->block = LFS_BLOCK_INLINE;
+        rcache->off = lfs_aligndown(off, lfs->cfg->read_size);
+        rcache->size = lfs_min(lfs_alignup(off+hint, lfs->cfg->read_size),
+                lfs->cfg->cache_size);
+        int err = lfs_dir_getslice(lfs, dir, gmask, gtag,
+                rcache->off, rcache->buffer, rcache->size);
+        if (err < 0) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+static int lfs_dir_traverse_filter(void *p,
+        lfs_tag_t tag, const void *buffer) {
+    lfs_tag_t *filtertag = p;
+    (void)buffer;
+
+    // which mask depends on unique bit in tag structure
+    uint32_t mask = (tag & LFS_MKTAG(0x100, 0, 0))
+            ? LFS_MKTAG(0x7ff, 0x3ff, 0)
+            : LFS_MKTAG(0x700, 0x3ff, 0);
+
+    // check for redundancy
+    if ((mask & tag) == (mask & *filtertag) ||
+            lfs_tag_isdelete(*filtertag) ||
+            (LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) == (
+                LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) |
+                    (LFS_MKTAG(0, 0x3ff, 0) & *filtertag))) {
+        return true;
+    }
+
+    // check if we need to adjust for created/deleted tags
+    if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE &&
+            lfs_tag_id(tag) <= lfs_tag_id(*filtertag)) {
+        *filtertag += LFS_MKTAG(0, lfs_tag_splice(tag), 0);
+    }
+
+    return false;
+}
+
+static int lfs_dir_traverse(lfs_t *lfs,
+        const lfs_mdir_t *dir, lfs_off_t off, lfs_tag_t ptag,
+        const struct lfs_mattr *attrs, int attrcount, bool hasseenmove,
+        lfs_tag_t tmask, lfs_tag_t ttag,
+        uint16_t begin, uint16_t end, int16_t diff,
+        int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) {
+    // iterate over directory and attrs
+    while (true) {
+        lfs_tag_t tag;
+        const void *buffer;
+        struct lfs_diskoff disk;
+        if (off+lfs_tag_dsize(ptag) < dir->off) {
+            off += lfs_tag_dsize(ptag);
+            int err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, sizeof(tag),
+                    dir->pair[0], off, &tag, sizeof(tag));
+            if (err) {
+                return err;
+            }
+
+            tag = (lfs_frombe32(tag) ^ ptag) | 0x80000000;
+            disk.block = dir->pair[0];
+            disk.off = off+sizeof(lfs_tag_t);
+            buffer = &disk;
+            ptag = tag;
+        } else if (attrcount > 0) {
+            tag = attrs[0].tag;
+            buffer = attrs[0].buffer;
+            attrs += 1;
+            attrcount -= 1;
+        } else if (!hasseenmove &&
+                lfs_gstate_hasmovehere(&lfs->gpending, dir->pair)) {
+            // Wait, we have pending move? Handle this here (we need to
+            // or else we risk letting moves fall out of date)
+            tag = lfs->gpending.tag & LFS_MKTAG(0x7ff, 0x3ff, 0);
+            buffer = NULL;
+            hasseenmove = true;
+        } else {
+            return 0;
+        }
+
+        lfs_tag_t mask = LFS_MKTAG(0x7ff, 0, 0);
+        if ((mask & tmask & tag) != (mask & tmask & ttag)) {
+            continue;
+        }
+
+        // do we need to filter? inlining the filtering logic here allows
+        // for some minor optimizations
+        if (lfs_tag_id(tmask) != 0) {
+            // scan for duplicates and update tag based on creates/deletes
+            int filter = lfs_dir_traverse(lfs,
+                    dir, off, ptag, attrs, attrcount, hasseenmove,
+                    0, 0, 0, 0, 0,
+                    lfs_dir_traverse_filter, &tag);
+            if (filter < 0) {
+                return filter;
+            }
+
+            if (filter) {
+                continue;
+            }
+
+            // in filter range?
+            if (!(lfs_tag_id(tag) >= begin && lfs_tag_id(tag) < end)) {
+                continue;
+            }
+        }
+
+        // handle special cases for mcu-side operations
+        if (lfs_tag_type3(tag) == LFS_FROM_NOOP) {
+            // do nothing
+        } else if (lfs_tag_type3(tag) == LFS_FROM_MOVE) {
+            uint16_t fromid = lfs_tag_size(tag);
+            uint16_t toid = lfs_tag_id(tag);
+            int err = lfs_dir_traverse(lfs,
+                    buffer, 0, LFS_BLOCK_NULL, NULL, 0, true,
+                    LFS_MKTAG(0x600, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, 0, 0),
+                    fromid, fromid+1, toid-fromid+diff,
+                    cb, data);
+            if (err) {
+                return err;
+            }
+        } else if (lfs_tag_type3(tag) == LFS_FROM_USERATTRS) {
+            for (unsigned i = 0; i < lfs_tag_size(tag); i++) {
+                const struct lfs_attr *a = buffer;
+                int err = cb(data, LFS_MKTAG(LFS_TYPE_USERATTR + a[i].type,
+                        lfs_tag_id(tag) + diff, a[i].size), a[i].buffer);
+                if (err) {
+                    return err;
+                }
+            }
+        } else {
+            int err = cb(data, tag + LFS_MKTAG(0, diff, 0), buffer);
+            if (err) {
+                return err;
+            }
+        }
+    }
+}
+
+static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs,
+        lfs_mdir_t *dir, const lfs_block_t pair[2],
+        lfs_tag_t fmask, lfs_tag_t ftag, uint16_t *id,
+        int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) {
+    // we can find tag very efficiently during a fetch, since we're already
+    // scanning the entire directory
+    lfs_stag_t besttag = -1;
+
+    // find the block with the most recent revision
+    uint32_t revs[2] = {0, 0};
+    int r = 0;
+    for (int i = 0; i < 2; i++) {
+        int err = lfs_bd_read(lfs,
+                NULL, &lfs->rcache, sizeof(revs[i]),
+                pair[i], 0, &revs[i], sizeof(revs[i]));
+        revs[i] = lfs_fromle32(revs[i]);
+        if (err && err != LFS_ERR_CORRUPT) {
+            return err;
+        }
+
+        if (err != LFS_ERR_CORRUPT &&
+                lfs_scmp(revs[i], revs[(i+1)%2]) > 0) {
+            r = i;
+        }
+    }
+
+    dir->pair[0] = pair[(r+0)%2];
+    dir->pair[1] = pair[(r+1)%2];
+    dir->rev = revs[(r+0)%2];
+    dir->off = 0; // nonzero = found some commits
+
+    // now scan tags to fetch the actual dir and find possible match
+    for (int i = 0; i < 2; i++) {
+        lfs_off_t off = 0;
+        lfs_tag_t ptag = LFS_BLOCK_NULL;
+
+        uint16_t tempcount = 0;
+        lfs_block_t temptail[2] = {LFS_BLOCK_NULL, LFS_BLOCK_NULL};
+        bool tempsplit = false;
+        lfs_stag_t tempbesttag = besttag;
+
+        dir->rev = lfs_tole32(dir->rev);
+        uint32_t crc = lfs_crc(LFS_BLOCK_NULL, &dir->rev, sizeof(dir->rev));
+        dir->rev = lfs_fromle32(dir->rev);
+
+        while (true) {
+            // extract next tag
+            lfs_tag_t tag;
+            off += lfs_tag_dsize(ptag);
+            int err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, lfs->cfg->block_size,
+                    dir->pair[0], off, &tag, sizeof(tag));
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    // can't continue?
+                    dir->erased = false;
+                    break;
+                }
+                return err;
+            }
+
+            crc = lfs_crc(crc, &tag, sizeof(tag));
+            tag = lfs_frombe32(tag) ^ ptag;
+
+            // next commit not yet programmed or we're not in valid range
+            if (!lfs_tag_isvalid(tag) ||
+                    off + lfs_tag_dsize(tag) > lfs->cfg->block_size) {
+                dir->erased = (lfs_tag_type1(ptag) == LFS_TYPE_CRC &&
+                        dir->off % lfs->cfg->prog_size == 0);
+                break;
+            }
+
+            ptag = tag;
+
+            if (lfs_tag_type1(tag) == LFS_TYPE_CRC) {
+                // check the crc attr
+                uint32_t dcrc;
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, lfs->cfg->block_size,
+                        dir->pair[0], off+sizeof(tag), &dcrc, sizeof(dcrc));
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                    return err;
+                }
+                dcrc = lfs_fromle32(dcrc);
+
+                if (crc != dcrc) {
+                    dir->erased = false;
+                    break;
+                }
+
+                // reset the next bit if we need to
+                ptag ^= (lfs_tag_t)(lfs_tag_chunk(tag) & 1U) << 31;
+
+                // toss our crc into the filesystem seed for
+                // pseudorandom numbers
+                lfs->seed ^= crc;
+
+                // update with what's found so far
+                besttag = tempbesttag;
+                dir->off = off + lfs_tag_dsize(tag);
+                dir->etag = ptag;
+                dir->count = tempcount;
+                dir->tail[0] = temptail[0];
+                dir->tail[1] = temptail[1];
+                dir->split = tempsplit;
+
+                // reset crc
+                crc = LFS_BLOCK_NULL;
+                continue;
+            }
+
+            // crc the entry first, hopefully leaving it in the cache
+            for (lfs_off_t j = sizeof(tag); j < lfs_tag_dsize(tag); j++) {
+                uint8_t dat;
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, lfs->cfg->block_size,
+                        dir->pair[0], off+j, &dat, 1);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                    return err;
+                }
+
+                crc = lfs_crc(crc, &dat, 1);
+            }
+
+            // directory modification tags?
+            if (lfs_tag_type1(tag) == LFS_TYPE_NAME) {
+                // increase count of files if necessary
+                if (lfs_tag_id(tag) >= tempcount) {
+                    tempcount = lfs_tag_id(tag) + 1;
+                }
+            } else if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE) {
+                tempcount += lfs_tag_splice(tag);
+
+                if (tag == (LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) |
+                        (LFS_MKTAG(0, 0x3ff, 0) & tempbesttag))) {
+                    tempbesttag |= 0x80000000;
+                } else if (tempbesttag != -1 &&
+                        lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) {
+                    tempbesttag += LFS_MKTAG(0, lfs_tag_splice(tag), 0);
+                }
+            } else if (lfs_tag_type1(tag) == LFS_TYPE_TAIL) {
+                tempsplit = (lfs_tag_chunk(tag) & 1);
+
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, lfs->cfg->block_size,
+                        dir->pair[0], off+sizeof(tag), &temptail, 8);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                }
+                lfs_pair_fromle32(temptail);
+            }
+
+            // found a match for our fetcher?
+            if ((fmask & tag) == (fmask & ftag)) {
+                int res = cb(data, tag, &(struct lfs_diskoff){
+                        dir->pair[0], off+sizeof(tag)});
+                if (res < 0) {
+                    if (res == LFS_ERR_CORRUPT) {
+                        dir->erased = false;
+                        break;
+                    }
+                    return res;
+                }
+
+                if (res == LFS_CMP_EQ) {
+                    // found a match
+                    tempbesttag = tag;
+                } else if ((LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) ==
+                        (LFS_MKTAG(0x7ff, 0x3ff, 0) & tempbesttag)) {
+                    // found an identical tag, but contents didn't match
+                    // this must mean that our besttag has been overwritten
+                    tempbesttag = -1;
+                } else if (res == LFS_CMP_GT &&
+                        lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) {
+                    // found a greater match, keep track to keep things sorted
+                    tempbesttag = tag | 0x80000000;
+                }
+            }
+        }
+
+        // consider what we have good enough
+        if (dir->off > 0) {
+            // synthetic move
+            if (lfs_gstate_hasmovehere(&lfs->gstate, dir->pair)) {
+                if (lfs_tag_id(lfs->gstate.tag) == lfs_tag_id(besttag)) {
+                    besttag |= 0x80000000;
+                } else if (besttag != -1 &&
+                        lfs_tag_id(lfs->gstate.tag) < lfs_tag_id(besttag)) {
+                    besttag -= LFS_MKTAG(0, 1, 0);
+                }
+            }
+
+            // found tag? or found best id?
+            if (id) {
+                *id = lfs_min(lfs_tag_id(besttag), dir->count);
+            }
+
+            if (lfs_tag_isvalid(besttag)) {
+                return besttag;
+            } else if (lfs_tag_id(besttag) < dir->count) {
+                return LFS_ERR_NOENT;
+            } else {
+                return 0;
+            }
+        }
+
+        // failed, try the other block?
+        lfs_pair_swap(dir->pair);
+        dir->rev = revs[(r+1)%2];
+    }
+
+    LFS_ERROR("Corrupted dir pair at %"PRIx32" %"PRIx32,
+            dir->pair[0], dir->pair[1]);
+    return LFS_ERR_CORRUPT;
+}
+
+static int lfs_dir_fetch(lfs_t *lfs,
+        lfs_mdir_t *dir, const lfs_block_t pair[2]) {
+    // note, mask=-1, tag=-1 can never match a tag since this
+    // pattern has the invalid bit set
+    return (int)lfs_dir_fetchmatch(lfs, dir, pair,
+            (lfs_tag_t)-1, (lfs_tag_t)-1, NULL, NULL, NULL);
+}
+
+static int lfs_dir_getgstate(lfs_t *lfs, const lfs_mdir_t *dir,
+        struct lfs_gstate *gstate) {
+    struct lfs_gstate temp;
+    lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x7ff, 0, 0),
+            LFS_MKTAG(LFS_TYPE_MOVESTATE, 0, sizeof(temp)), &temp);
+    if (res < 0 && res != LFS_ERR_NOENT) {
+        return res;
+    }
+
+    if (res != LFS_ERR_NOENT) {
+        // xor together to find resulting gstate
+        lfs_gstate_fromle32(&temp);
+        lfs_gstate_xor(gstate, &temp);
+    }
+
+    return 0;
+}
+
+static int lfs_dir_getinfo(lfs_t *lfs, lfs_mdir_t *dir,
+        uint16_t id, struct lfs_info *info) {
+    if (id == 0x3ff) {
+        // special case for root
+        strcpy(info->name, "/");
+        info->type = LFS_TYPE_DIR;
+        return 0;
+    }
+
+    lfs_stag_t tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x780, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_NAME, id, lfs->name_max+1), info->name);
+    if (tag < 0) {
+        return (int)tag;
+    }
+
+    info->type = lfs_tag_type3(tag);
+
+    struct lfs_ctz ctz;
+    tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz);
+    if (tag < 0) {
+        return (int)tag;
+    }
+    lfs_ctz_fromle32(&ctz);
+
+    if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) {
+        info->size = ctz.size;
+    } else if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) {
+        info->size = lfs_tag_size(tag);
+    }
+
+    return 0;
+}
+
+struct lfs_dir_find_match {
+    lfs_t *lfs;
+    const void *name;
+    lfs_size_t size;
+};
+
+static int lfs_dir_find_match(void *data,
+        lfs_tag_t tag, const void *buffer) {
+    struct lfs_dir_find_match *name = data;
+    lfs_t *lfs = name->lfs;
+    const struct lfs_diskoff *disk = buffer;
+
+    // compare with disk
+    lfs_size_t diff = lfs_min(name->size, lfs_tag_size(tag));
+    int res = lfs_bd_cmp(lfs,
+            NULL, &lfs->rcache, diff,
+            disk->block, disk->off, name->name, diff);
+    if (res != LFS_CMP_EQ) {
+        return res;
+    }
+
+    // only equal if our size is still the same
+    if (name->size != lfs_tag_size(tag)) {
+        return (name->size < lfs_tag_size(tag)) ? LFS_CMP_LT : LFS_CMP_GT;
+    }
+
+    // found a match!
+    return LFS_CMP_EQ;
+}
+
+static lfs_stag_t lfs_dir_find(lfs_t *lfs, lfs_mdir_t *dir,
+        const char **path, uint16_t *id) {
+    // we reduce path to a single name if we can find it
+    const char *name = *path;
+    if (id) {
+        *id = 0x3ff;
+    }
+
+    // default to root dir
+    lfs_stag_t tag = LFS_MKTAG(LFS_TYPE_DIR, 0x3ff, 0);
+    dir->tail[0] = lfs->root[0];
+    dir->tail[1] = lfs->root[1];
+
+    while (true) {
+nextname:
+        // skip slashes
+        name += strspn(name, "/");
+        lfs_size_t namelen = strcspn(name, "/");
+
+        // skip '.' and root '..'
+        if ((namelen == 1 && memcmp(name, ".", 1) == 0) ||
+            (namelen == 2 && memcmp(name, "..", 2) == 0)) {
+            name += namelen;
+            goto nextname;
+        }
+
+        // skip if matched by '..' in name
+        const char *suffix = name + namelen;
+        lfs_size_t sufflen;
+        int depth = 1;
+        while (true) {
+            suffix += strspn(suffix, "/");
+            sufflen = strcspn(suffix, "/");
+            if (sufflen == 0) {
+                break;
+            }
+
+            if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) {
+                depth -= 1;
+                if (depth == 0) {
+                    name = suffix + sufflen;
+                    goto nextname;
+                }
+            } else {
+                depth += 1;
+            }
+
+            suffix += sufflen;
+        }
+
+        // found path
+        if (name[0] == '\0') {
+            return tag;
+        }
+
+        // update what we've found so far
+        *path = name;
+
+        // only continue if we hit a directory
+        if (lfs_tag_type3(tag) != LFS_TYPE_DIR) {
+            return LFS_ERR_NOTDIR;
+        }
+
+        // grab the entry data
+        if (lfs_tag_id(tag) != 0x3ff) {
+            lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), dir->tail);
+            if (res < 0) {
+                return res;
+            }
+            lfs_pair_fromle32(dir->tail);
+        }
+
+        // find entry matching name
+        while (true) {
+            tag = lfs_dir_fetchmatch(lfs, dir, dir->tail,
+                    LFS_MKTAG(0x780, 0, 0),
+                    LFS_MKTAG(LFS_TYPE_NAME, 0, namelen),
+                     // are we last name?
+                    (strchr(name, '/') == NULL) ? id : NULL,
+                    lfs_dir_find_match, &(struct lfs_dir_find_match){
+                        lfs, name, namelen});
+            if (tag < 0) {
+                return tag;
+            }
+
+            if (tag) {
+                break;
+            }
+
+            if (!dir->split) {
+                return LFS_ERR_NOENT;
+            }
+        }
+
+        // to next name
+        name += namelen;
+    }
+}
+
+// commit logic
+struct lfs_commit {
+    lfs_block_t block;
+    lfs_off_t off;
+    lfs_tag_t ptag;
+    uint32_t crc;
+
+    lfs_off_t begin;
+    lfs_off_t end;
+};
+
+static int lfs_dir_commitprog(lfs_t *lfs, struct lfs_commit *commit,
+        const void *buffer, lfs_size_t size) {
+    int err = lfs_bd_prog(lfs,
+            &lfs->pcache, &lfs->rcache, false,
+            commit->block, commit->off ,
+            (const uint8_t*)buffer, size);
+    if (err) {
+        return err;
+    }
+
+    commit->crc = lfs_crc(commit->crc, buffer, size);
+    commit->off += size;
+    return 0;
+}
+
+static int lfs_dir_commitattr(lfs_t *lfs, struct lfs_commit *commit,
+        lfs_tag_t tag, const void *buffer) {
+    // check if we fit
+    lfs_size_t dsize = lfs_tag_dsize(tag);
+    if (commit->off + dsize > commit->end) {
+        return LFS_ERR_NOSPC;
+    }
+
+    // write out tag
+    lfs_tag_t ntag = lfs_tobe32((tag & 0x7fffffff) ^ commit->ptag);
+    int err = lfs_dir_commitprog(lfs, commit, &ntag, sizeof(ntag));
+    if (err) {
+        return err;
+    }
+
+    if (!(tag & 0x80000000)) {
+        // from memory
+        err = lfs_dir_commitprog(lfs, commit, buffer, dsize-sizeof(tag));
+        if (err) {
+            return err;
+        }
+    } else {
+        // from disk
+        const struct lfs_diskoff *disk = buffer;
+        for (lfs_off_t i = 0; i < dsize-sizeof(tag); i++) {
+            // rely on caching to make this efficient
+            uint8_t dat;
+            err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, dsize-sizeof(tag)-i,
+                    disk->block, disk->off+i, &dat, 1);
+            if (err) {
+                return err;
+            }
+
+            err = lfs_dir_commitprog(lfs, commit, &dat, 1);
+            if (err) {
+                return err;
+            }
+        }
+    }
+
+    commit->ptag = tag & 0x7fffffff;
+    return 0;
+}
+
+static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) {
+    // align to program units
+    const lfs_off_t off1 = commit->off + sizeof(lfs_tag_t);
+    const lfs_off_t end = lfs_alignup(off1 + sizeof(uint32_t),
+            lfs->cfg->prog_size);
+
+    // create crc tags to fill up remainder of commit, note that
+    // padding is not crcd, which lets fetches skip padding but
+    // makes committing a bit more complicated
+    while (commit->off < end) {
+        lfs_off_t off = commit->off + sizeof(lfs_tag_t);
+        lfs_off_t noff = lfs_min(end - off, 0x3fe) + off;
+        if (noff < end) {
+            noff = lfs_min(noff, end - 2*sizeof(uint32_t));
+        }
+
+        // read erased state from next program unit
+        lfs_tag_t tag = LFS_BLOCK_NULL;
+        int err = lfs_bd_read(lfs,
+                NULL, &lfs->rcache, sizeof(tag),
+                commit->block, noff, &tag, sizeof(tag));
+        if (err && err != LFS_ERR_CORRUPT) {
+            return err;
+        }
+
+        // build crc tag
+        bool reset = ~lfs_frombe32(tag) >> 31;
+        tag = LFS_MKTAG(LFS_TYPE_CRC + reset, 0x3ff, noff - off);
+
+        // write out crc
+        uint32_t footer[2];
+        footer[0] = lfs_tobe32(tag ^ commit->ptag);
+        commit->crc = lfs_crc(commit->crc, &footer[0], sizeof(footer[0]));
+        footer[1] = lfs_tole32(commit->crc);
+        err = lfs_bd_prog(lfs,
+                &lfs->pcache, &lfs->rcache, false,
+                commit->block, commit->off, &footer, sizeof(footer));
+        if (err) {
+            return err;
+        }
+
+        commit->off += sizeof(tag)+lfs_tag_size(tag);
+        commit->ptag = tag ^ ((lfs_tag_t)reset << 31);
+        commit->crc = LFS_BLOCK_NULL; // reset crc for next "commit"
+    }
+
+    // flush buffers
+    int err = lfs_bd_sync(lfs, &lfs->pcache, &lfs->rcache, false);
+    if (err) {
+        return err;
+    }
+
+    // successful commit, check checksums to make sure
+    lfs_off_t off = commit->begin;
+    lfs_off_t noff = off1;
+    while (off < end) {
+        uint32_t crc = LFS_BLOCK_NULL;
+        for (lfs_off_t i = off; i < noff+sizeof(uint32_t); i++) {
+            // leave it up to caching to make this efficient
+            uint8_t dat;
+            err = lfs_bd_read(lfs,
+                    NULL, &lfs->rcache, noff+sizeof(uint32_t)-i,
+                    commit->block, i, &dat, 1);
+            if (err) {
+                return err;
+            }
+
+            crc = lfs_crc(crc, &dat, 1);
+        }
+
+        // detected write error?
+        if (crc != 0) {
+            return LFS_ERR_CORRUPT;
+        }
+
+        // skip padding
+        off = lfs_min(end - noff, 0x3fe) + noff;
+        if (off < end) {
+            off = lfs_min(off, end - 2*sizeof(uint32_t));
+        }
+        noff = off + sizeof(uint32_t);
+    }
+
+    return 0;
+}
+
+static int lfs_dir_alloc(lfs_t *lfs, lfs_mdir_t *dir) {
+    // allocate pair of dir blocks (backwards, so we write block 1 first)
+    for (int i = 0; i < 2; i++) {
+        int err = lfs_alloc(lfs, &dir->pair[(i+1)%2]);
+        if (err) {
+            return err;
+        }
+    }
+
+    // rather than clobbering one of the blocks we just pretend
+    // the revision may be valid
+    int err = lfs_bd_read(lfs,
+            NULL, &lfs->rcache, sizeof(dir->rev),
+            dir->pair[0], 0, &dir->rev, sizeof(dir->rev));
+    dir->rev = lfs_fromle32(dir->rev);
+    if (err && err != LFS_ERR_CORRUPT) {
+        return err;
+    }
+
+    // make sure we don't immediately evict
+    dir->rev += dir->rev & 1;
+
+    // set defaults
+    dir->off = sizeof(dir->rev);
+    dir->etag = LFS_BLOCK_NULL;
+    dir->count = 0;
+    dir->tail[0] = LFS_BLOCK_NULL;
+    dir->tail[1] = LFS_BLOCK_NULL;
+    dir->erased = false;
+    dir->split = false;
+
+    // don't write out yet, let caller take care of that
+    return 0;
+}
+
+static int lfs_dir_drop(lfs_t *lfs, lfs_mdir_t *dir, lfs_mdir_t *tail) {
+    // steal state
+    int err = lfs_dir_getgstate(lfs, tail, &lfs->gdelta);
+    if (err) {
+        return err;
+    }
+
+    // steal tail
+    lfs_pair_tole32(tail->tail);
+    err = lfs_dir_commit(lfs, dir, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_TAIL + tail->split, 0x3ff, 8), tail->tail}));
+    lfs_pair_fromle32(tail->tail);
+    if (err) {
+        return err;
+    }
+
+    return 0;
+}
+
+static int lfs_dir_split(lfs_t *lfs,
+        lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
+        lfs_mdir_t *source, uint16_t split, uint16_t end) {
+    // create tail directory
+    lfs_alloc_ack(lfs);
+    lfs_mdir_t tail;
+    int err = lfs_dir_alloc(lfs, &tail);
+    if (err) {
+        return err;
+    }
+
+    tail.split = dir->split;
+    tail.tail[0] = dir->tail[0];
+    tail.tail[1] = dir->tail[1];
+
+    err = lfs_dir_compact(lfs, &tail, attrs, attrcount, source, split, end);
+    if (err) {
+        return err;
+    }
+
+    dir->tail[0] = tail.pair[0];
+    dir->tail[1] = tail.pair[1];
+    dir->split = true;
+
+    // update root if needed
+    if (lfs_pair_cmp(dir->pair, lfs->root) == 0 && split == 0) {
+        lfs->root[0] = tail.pair[0];
+        lfs->root[1] = tail.pair[1];
+    }
+
+    return 0;
+}
+
+static int lfs_dir_commit_size(void *p, lfs_tag_t tag, const void *buffer) {
+    lfs_size_t *size = p;
+    (void)buffer;
+
+    *size += lfs_tag_dsize(tag);
+    return 0;
+}
+
+struct lfs_dir_commit_commit {
+    lfs_t *lfs;
+    struct lfs_commit *commit;
+};
+
+static int lfs_dir_commit_commit(void *p, lfs_tag_t tag, const void *buffer) {
+    struct lfs_dir_commit_commit *commit = p;
+    return lfs_dir_commitattr(commit->lfs, commit->commit, tag, buffer);
+}
+
+static int lfs_dir_compact(lfs_t *lfs,
+        lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
+        lfs_mdir_t *source, uint16_t begin, uint16_t end) {
+    // save some state in case block is bad
+    const lfs_block_t oldpair[2] = {dir->pair[1], dir->pair[0]};
+    bool relocated = false;
+    bool exhausted = false;
+
+    // should we split?
+    while (end - begin > 1) {
+        // find size
+        lfs_size_t size = 0;
+        int err = lfs_dir_traverse(lfs,
+                source, 0, LFS_BLOCK_NULL, attrs, attrcount, false,
+                LFS_MKTAG(0x400, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_NAME, 0, 0),
+                begin, end, -begin,
+                lfs_dir_commit_size, &size);
+        if (err) {
+            return err;
+        }
+
+        // space is complicated, we need room for tail, crc, gstate,
+        // cleanup delete, and we cap at half a block to give room
+        // for metadata updates.
+        if (end - begin < 0xff &&
+                size <= lfs_min(lfs->cfg->block_size - 36,
+                    lfs_alignup(lfs->cfg->block_size/2,
+                        lfs->cfg->prog_size))) {
+            break;
+        }
+
+        // can't fit, need to split, we should really be finding the
+        // largest size that fits with a small binary search, but right now
+        // it's not worth the code size
+        uint16_t split = (end - begin) / 2;
+        err = lfs_dir_split(lfs, dir, attrs, attrcount,
+                source, begin+split, end);
+        if (err) {
+            // if we fail to split, we may be able to overcompact, unless
+            // we're too big for even the full block, in which case our
+            // only option is to error
+            if (err == LFS_ERR_NOSPC && size <= lfs->cfg->block_size - 36) {
+                break;
+            }
+            return err;
+        }
+
+        end = begin + split;
+    }
+
+    // increment revision count
+    dir->rev += 1;
+    if (lfs->cfg->block_cycles > 0 &&
+            (dir->rev % (lfs->cfg->block_cycles+1) == 0)) {
+        if (lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) {
+            // oh no! we're writing too much to the superblock,
+            // should we expand?
+            lfs_ssize_t res = lfs_fs_size(lfs);
+            if (res < 0) {
+                return res;
+            }
+
+            // do we have extra space? littlefs can't reclaim this space
+            // by itself, so expand cautiously
+            if ((lfs_size_t)res < lfs->cfg->block_count/2) {
+                LFS_DEBUG("Expanding superblock at rev %"PRIu32, dir->rev);
+                int err = lfs_dir_split(lfs, dir, attrs, attrcount,
+                        source, begin, end);
+                if (err && err != LFS_ERR_NOSPC) {
+                    return err;
+                }
+
+                // welp, we tried, if we ran out of space there's not much
+                // we can do, we'll error later if we've become frozen
+                if (!err) {
+                    end = begin;
+                }
+            }
+#ifdef LFS_MIGRATE
+        } else if (lfs->lfs1) {
+            // do not proactively relocate blocks during migrations, this
+            // can cause a number of failure states such: clobbering the
+            // v1 superblock if we relocate root, and invalidating directory
+            // pointers if we relocate the head of a directory. On top of
+            // this, relocations increase the overall complexity of
+            // lfs_migration, which is already a delicate operation.
+#endif
+        } else {
+            // we're writing too much, time to relocate
+            exhausted = true;
+            goto relocate;
+        }
+    }
+
+    // begin loop to commit compaction to blocks until a compact sticks
+    while (true) {
+        {
+            // There's nothing special about our global delta, so feed it into
+            // our local global delta
+            int err = lfs_dir_getgstate(lfs, dir, &lfs->gdelta);
+            if (err) {
+                return err;
+            }
+
+            // setup commit state
+            struct lfs_commit commit = {
+                .block = dir->pair[1],
+                .off = 0,
+                .ptag = LFS_BLOCK_NULL,
+                .crc = LFS_BLOCK_NULL,
+
+                .begin = 0,
+                .end = lfs->cfg->block_size - 8,
+            };
+
+            // erase block to write to
+            err = lfs_bd_erase(lfs, dir->pair[1]);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // write out header
+            dir->rev = lfs_tole32(dir->rev);
+            err = lfs_dir_commitprog(lfs, &commit,
+                    &dir->rev, sizeof(dir->rev));
+            dir->rev = lfs_fromle32(dir->rev);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // traverse the directory, this time writing out all unique tags
+            err = lfs_dir_traverse(lfs,
+                    source, 0, LFS_BLOCK_NULL, attrs, attrcount, false,
+                    LFS_MKTAG(0x400, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_NAME, 0, 0),
+                    begin, end, -begin,
+                    lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){
+                        lfs, &commit});
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // commit tail, which may be new after last size check
+            if (!lfs_pair_isnull(dir->tail)) {
+                lfs_pair_tole32(dir->tail);
+                err = lfs_dir_commitattr(lfs, &commit,
+                        LFS_MKTAG(LFS_TYPE_TAIL + dir->split, 0x3ff, 8),
+                        dir->tail);
+                lfs_pair_fromle32(dir->tail);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+            }
+
+            if (!relocated && !lfs_gstate_iszero(&lfs->gdelta)) {
+                // commit any globals, unless we're relocating,
+                // in which case our parent will steal our globals
+                lfs_gstate_tole32(&lfs->gdelta);
+                err = lfs_dir_commitattr(lfs, &commit,
+                        LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff,
+                            sizeof(lfs->gdelta)), &lfs->gdelta);
+                lfs_gstate_fromle32(&lfs->gdelta);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+            }
+
+            err = lfs_dir_commitcrc(lfs, &commit);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            // successful compaction, swap dir pair to indicate most recent
+            LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0);
+            lfs_pair_swap(dir->pair);
+            dir->count = end - begin;
+            dir->off = commit.off;
+            dir->etag = commit.ptag;
+            // note we able to have already handled move here
+            if (lfs_gstate_hasmovehere(&lfs->gpending, dir->pair)) {
+                lfs_gstate_xormove(&lfs->gpending,
+                    &lfs->gpending, 0x3ff, NULL);
+            }
+        }
+        break;
+
+relocate:
+        // commit was corrupted, drop caches and prepare to relocate block
+        relocated = true;
+        lfs_cache_drop(lfs, &lfs->pcache);
+        if (!exhausted) {
+            LFS_DEBUG("Bad block at %"PRIx32, dir->pair[1]);
+        }
+
+        // can't relocate superblock, filesystem is now frozen
+        if (lfs_pair_cmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) {
+            LFS_WARN("Superblock %"PRIx32" has become unwritable", oldpair[1]);
+            return LFS_ERR_NOSPC;
+        }
+
+        // relocate half of pair
+        int err = lfs_alloc(lfs, &dir->pair[1]);
+        if (err && (err != LFS_ERR_NOSPC && !exhausted)) {
+            return err;
+        }
+        continue;
+    }
+
+    if (!relocated) {
+        lfs->gstate = lfs->gpending;
+        lfs->gdelta = (struct lfs_gstate){0};
+    } else {
+        // update references if we relocated
+        LFS_DEBUG("Relocating %"PRIx32" %"PRIx32" -> %"PRIx32" %"PRIx32,
+                oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]);
+        int err = lfs_fs_relocate(lfs, oldpair, dir->pair);
+        if (err) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir,
+        const struct lfs_mattr *attrs, int attrcount) {
+    // check for any inline files that aren't RAM backed and
+    // forcefully evict them, needed for filesystem consistency
+    for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
+        if (dir != &f->m && lfs_pair_cmp(f->m.pair, dir->pair) == 0 &&
+                f->type == LFS_TYPE_REG && (f->flags & LFS_F_INLINE) &&
+                f->ctz.size > lfs->cfg->cache_size) {
+            int err = lfs_file_outline(lfs, f);
+            if (err) {
+                return err;
+            }
+
+            err = lfs_file_flush(lfs, f);
+            if (err) {
+                return err;
+            }
+        }
+    }
+
+    // calculate changes to the directory
+    lfs_tag_t deletetag = LFS_BLOCK_NULL;
+    lfs_tag_t createtag = LFS_BLOCK_NULL;
+    for (int i = 0; i < attrcount; i++) {
+        if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE) {
+            createtag = attrs[i].tag;
+            dir->count += 1;
+        } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE) {
+            deletetag = attrs[i].tag;
+            LFS_ASSERT(dir->count > 0);
+            dir->count -= 1;
+        } else if (lfs_tag_type1(attrs[i].tag) == LFS_TYPE_TAIL) {
+            dir->tail[0] = ((lfs_block_t*)attrs[i].buffer)[0];
+            dir->tail[1] = ((lfs_block_t*)attrs[i].buffer)[1];
+            dir->split = (lfs_tag_chunk(attrs[i].tag) & 1);
+            lfs_pair_fromle32(dir->tail);
+        }
+    }
+
+    // do we have a pending move?
+    if (lfs_gstate_hasmovehere(&lfs->gpending, dir->pair)) {
+        deletetag = lfs->gpending.tag & LFS_MKTAG(0x7ff, 0x3ff, 0);
+        LFS_ASSERT(dir->count > 0);
+        dir->count -= 1;
+
+        // mark gdelta so we reflect the move we will fix
+        lfs_gstate_xormove(&lfs->gdelta, &lfs->gpending, 0x3ff, NULL);
+    }
+
+    // should we actually drop the directory block?
+    if (lfs_tag_isvalid(deletetag) && dir->count == 0) {
+        lfs_mdir_t pdir;
+        int err = lfs_fs_pred(lfs, dir->pair, &pdir);
+        if (err && err != LFS_ERR_NOENT) {
+            return err;
+        }
+
+        if (err != LFS_ERR_NOENT && pdir.split) {
+            return lfs_dir_drop(lfs, &pdir, dir);
+        }
+    }
+
+    if (dir->erased || dir->count >= 0xff) {
+        // try to commit
+        struct lfs_commit commit = {
+            .block = dir->pair[0],
+            .off = dir->off,
+            .ptag = dir->etag,
+            .crc = LFS_BLOCK_NULL,
+
+            .begin = dir->off,
+            .end = lfs->cfg->block_size - 8,
+        };
+
+        // traverse attrs that need to be written out
+        lfs_pair_tole32(dir->tail);
+        int err = lfs_dir_traverse(lfs,
+                dir, dir->off, dir->etag, attrs, attrcount, false,
+                0, 0, 0, 0, 0,
+                lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){
+                    lfs, &commit});
+        lfs_pair_fromle32(dir->tail);
+        if (err) {
+            if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) {
+                goto compact;
+            }
+            return err;
+        }
+
+        // commit any global diffs if we have any
+        if (!lfs_gstate_iszero(&lfs->gdelta)) {
+            err = lfs_dir_getgstate(lfs, dir, &lfs->gdelta);
+            if (err) {
+                return err;
+            }
+
+            lfs_gstate_tole32(&lfs->gdelta);
+            err = lfs_dir_commitattr(lfs, &commit,
+                    LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff,
+                        sizeof(lfs->gdelta)), &lfs->gdelta);
+            lfs_gstate_fromle32(&lfs->gdelta);
+            if (err) {
+                if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) {
+                    goto compact;
+                }
+                return err;
+            }
+        }
+
+        // finalize commit with the crc
+        err = lfs_dir_commitcrc(lfs, &commit);
+        if (err) {
+            if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) {
+                goto compact;
+            }
+            return err;
+        }
+
+        // successful commit, update dir
+        LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0);
+        dir->off = commit.off;
+        dir->etag = commit.ptag;
+
+        // note we able to have already handled move here
+        if (lfs_gstate_hasmovehere(&lfs->gpending, dir->pair)) {
+            lfs_gstate_xormove(&lfs->gpending, &lfs->gpending, 0x3ff, NULL);
+        }
+
+        // update gstate
+        lfs->gstate = lfs->gpending;
+        lfs->gdelta = (struct lfs_gstate){0};
+    } else {
+compact:
+        // fall back to compaction
+        lfs_cache_drop(lfs, &lfs->pcache);
+
+        int err = lfs_dir_compact(lfs, dir, attrs, attrcount,
+                dir, 0, dir->count);
+        if (err) {
+            return err;
+        }
+    }
+
+    // update any directories that are affected
+    lfs_mdir_t copy = *dir;
+
+    // two passes, once for things that aren't us, and one
+    // for things that are
+    for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) {
+        if (lfs_pair_cmp(d->m.pair, copy.pair) == 0) {
+            d->m = *dir;
+            if (d->id == lfs_tag_id(deletetag)) {
+                d->m.pair[0] = LFS_BLOCK_NULL;
+                d->m.pair[1] = LFS_BLOCK_NULL;
+            } else if (d->id > lfs_tag_id(deletetag)) {
+                d->id -= 1;
+                if (d->type == LFS_TYPE_DIR) {
+                    ((lfs_dir_t*)d)->pos -= 1;
+                }
+            } else if (&d->m != dir && d->id >= lfs_tag_id(createtag)) {
+                d->id += 1;
+                if (d->type == LFS_TYPE_DIR) {
+                    ((lfs_dir_t*)d)->pos += 1;
+                }
+            }
+
+            while (d->id >= d->m.count && d->m.split) {
+                // we split and id is on tail now
+                d->id -= d->m.count;
+                int err = lfs_dir_fetch(lfs, &d->m, d->m.tail);
+                if (err) {
+                    return err;
+                }
+            }
+        }
+    }
+
+    return 0;
+}
+
+
+/// Top level directory operations ///
+int lfs_mkdir(lfs_t *lfs, const char *path) {
+    LFS_TRACE("lfs_mkdir(%p, \"%s\")", (void*)lfs, path);
+    // deorphan if we haven't yet, needed at most once after poweron
+    int err = lfs_fs_forceconsistency(lfs);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    lfs_mdir_t cwd;
+    uint16_t id;
+    err = lfs_dir_find(lfs, &cwd, &path, &id);
+    if (!(err == LFS_ERR_NOENT && id != 0x3ff)) {
+        LFS_TRACE("lfs_mkdir -> %d", (err < 0) ? err : LFS_ERR_EXIST);
+        return (err < 0) ? err : LFS_ERR_EXIST;
+    }
+
+    // check that name fits
+    lfs_size_t nlen = strlen(path);
+    if (nlen > lfs->name_max) {
+        LFS_TRACE("lfs_mkdir -> %d", LFS_ERR_NAMETOOLONG);
+        return LFS_ERR_NAMETOOLONG;
+    }
+
+    // build up new directory
+    lfs_alloc_ack(lfs);
+    lfs_mdir_t dir;
+    err = lfs_dir_alloc(lfs, &dir);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    // find end of list
+    lfs_mdir_t pred = cwd;
+    while (pred.split) {
+        err = lfs_dir_fetch(lfs, &pred, pred.tail);
+        if (err) {
+            LFS_TRACE("lfs_mkdir -> %d", err);
+            return err;
+        }
+    }
+
+    // setup dir
+    lfs_pair_tole32(pred.tail);
+    err = lfs_dir_commit(lfs, &dir, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pred.tail}));
+    lfs_pair_fromle32(pred.tail);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    // current block end of list?
+    if (cwd.split) {
+        // update tails, this creates a desync
+        lfs_fs_preporphans(lfs, +1);
+        lfs_pair_tole32(dir.pair);
+        err = lfs_dir_commit(lfs, &pred, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair}));
+        lfs_pair_fromle32(dir.pair);
+        if (err) {
+            LFS_TRACE("lfs_mkdir -> %d", err);
+            return err;
+        }
+        lfs_fs_preporphans(lfs, -1);
+    }
+
+    // now insert into our parent block
+    lfs_pair_tole32(dir.pair);
+    err = lfs_dir_commit(lfs, &cwd, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL},
+            {LFS_MKTAG(LFS_TYPE_DIR, id, nlen), path},
+            {LFS_MKTAG(LFS_TYPE_DIRSTRUCT, id, 8), dir.pair},
+            {!cwd.split
+                ? LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8)
+                : LFS_MKTAG(LFS_FROM_NOOP, 0, 0), dir.pair}));
+    lfs_pair_fromle32(dir.pair);
+    if (err) {
+        LFS_TRACE("lfs_mkdir -> %d", err);
+        return err;
+    }
+
+    LFS_TRACE("lfs_mkdir -> %d", 0);
+    return 0;
+}
+
+int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) {
+    LFS_TRACE("lfs_dir_open(%p, %p, \"%s\")", (void*)lfs, (void*)dir, path);
+    lfs_stag_t tag = lfs_dir_find(lfs, &dir->m, &path, NULL);
+    if (tag < 0) {
+        LFS_TRACE("lfs_dir_open -> %"PRId32, tag);
+        return tag;
+    }
+
+    if (lfs_tag_type3(tag) != LFS_TYPE_DIR) {
+        LFS_TRACE("lfs_dir_open -> %d", LFS_ERR_NOTDIR);
+        return LFS_ERR_NOTDIR;
+    }
+
+    lfs_block_t pair[2];
+    if (lfs_tag_id(tag) == 0x3ff) {
+        // handle root dir separately
+        pair[0] = lfs->root[0];
+        pair[1] = lfs->root[1];
+    } else {
+        // get dir pair from parent
+        lfs_stag_t res = lfs_dir_get(lfs, &dir->m, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair);
+        if (res < 0) {
+            LFS_TRACE("lfs_dir_open -> %"PRId32, res);
+            return res;
+        }
+        lfs_pair_fromle32(pair);
+    }
+
+    // fetch first pair
+    int err = lfs_dir_fetch(lfs, &dir->m, pair);
+    if (err) {
+        LFS_TRACE("lfs_dir_open -> %d", err);
+        return err;
+    }
+
+    // setup entry
+    dir->head[0] = dir->m.pair[0];
+    dir->head[1] = dir->m.pair[1];
+    dir->id = 0;
+    dir->pos = 0;
+
+    // add to list of mdirs
+    dir->type = LFS_TYPE_DIR;
+    dir->next = (lfs_dir_t*)lfs->mlist;
+    lfs->mlist = (struct lfs_mlist*)dir;
+
+    LFS_TRACE("lfs_dir_open -> %d", 0);
+    return 0;
+}
+
+int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) {
+    LFS_TRACE("lfs_dir_close(%p, %p)", (void*)lfs, (void*)dir);
+    // remove from list of mdirs
+    for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) {
+        if (*p == (struct lfs_mlist*)dir) {
+            *p = (*p)->next;
+            break;
+        }
+    }
+
+    LFS_TRACE("lfs_dir_close -> %d", 0);
+    return 0;
+}
+
+int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) {
+    LFS_TRACE("lfs_dir_read(%p, %p, %p)",
+            (void*)lfs, (void*)dir, (void*)info);
+    memset(info, 0, sizeof(*info));
+
+    // special offset for '.' and '..'
+    if (dir->pos == 0) {
+        info->type = LFS_TYPE_DIR;
+        strcpy(info->name, ".");
+        dir->pos += 1;
+        LFS_TRACE("lfs_dir_read -> %d", true);
+        return true;
+    } else if (dir->pos == 1) {
+        info->type = LFS_TYPE_DIR;
+        strcpy(info->name, "..");
+        dir->pos += 1;
+        LFS_TRACE("lfs_dir_read -> %d", true);
+        return true;
+    }
+
+    while (true) {
+        if (dir->id == dir->m.count) {
+            if (!dir->m.split) {
+                LFS_TRACE("lfs_dir_read -> %d", false);
+                return false;
+            }
+
+            int err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail);
+            if (err) {
+                LFS_TRACE("lfs_dir_read -> %d", err);
+                return err;
+            }
+
+            dir->id = 0;
+        }
+
+        int err = lfs_dir_getinfo(lfs, &dir->m, dir->id, info);
+        if (err && err != LFS_ERR_NOENT) {
+            LFS_TRACE("lfs_dir_read -> %d", err);
+            return err;
+        }
+
+        dir->id += 1;
+        if (err != LFS_ERR_NOENT) {
+            break;
+        }
+    }
+
+    dir->pos += 1;
+    LFS_TRACE("lfs_dir_read -> %d", true);
+    return true;
+}
+
+int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) {
+    LFS_TRACE("lfs_dir_seek(%p, %p, %"PRIu32")",
+            (void*)lfs, (void*)dir, off);
+    // simply walk from head dir
+    int err = lfs_dir_rewind(lfs, dir);
+    if (err) {
+        LFS_TRACE("lfs_dir_seek -> %d", err);
+        return err;
+    }
+
+    // first two for ./..
+    dir->pos = lfs_min(2, off);
+    off -= dir->pos;
+
+    // skip superblock entry
+    dir->id = (off > 0 && lfs_pair_cmp(dir->head, lfs->root) == 0);
+
+    while (off > 0) {
+        int diff = lfs_min(dir->m.count - dir->id, off);
+        dir->id += diff;
+        dir->pos += diff;
+        off -= diff;
+
+        if (dir->id == dir->m.count) {
+            if (!dir->m.split) {
+                LFS_TRACE("lfs_dir_seek -> %d", LFS_ERR_INVAL);
+                return LFS_ERR_INVAL;
+            }
+
+            err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail);
+            if (err) {
+                LFS_TRACE("lfs_dir_seek -> %d", err);
+                return err;
+            }
+
+            dir->id = 0;
+        }
+    }
+
+    LFS_TRACE("lfs_dir_seek -> %d", 0);
+    return 0;
+}
+
+lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir) {
+    LFS_TRACE("lfs_dir_tell(%p, %p)", (void*)lfs, (void*)dir);
+    (void)lfs;
+    LFS_TRACE("lfs_dir_tell -> %"PRId32, dir->pos);
+    return dir->pos;
+}
+
+int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) {
+    LFS_TRACE("lfs_dir_rewind(%p, %p)", (void*)lfs, (void*)dir);
+    // reload the head dir
+    int err = lfs_dir_fetch(lfs, &dir->m, dir->head);
+    if (err) {
+        LFS_TRACE("lfs_dir_rewind -> %d", err);
+        return err;
+    }
+
+    dir->id = 0;
+    dir->pos = 0;
+    LFS_TRACE("lfs_dir_rewind -> %d", 0);
+    return 0;
+}
+
+
+/// File index list operations ///
+static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) {
+    lfs_off_t size = *off;
+    lfs_off_t b = lfs->cfg->block_size - 2*4;
+    lfs_off_t i = size / b;
+    if (i == 0) {
+        return 0;
+    }
+
+    i = (size - 4*(lfs_popc(i-1)+2)) / b;
+    *off = size - b*i - 4*lfs_popc(i);
+    return i;
+}
+
+static int lfs_ctz_find(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache,
+        lfs_block_t head, lfs_size_t size,
+        lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) {
+    if (size == 0) {
+        *block = LFS_BLOCK_NULL;
+        *off = 0;
+        return 0;
+    }
+
+    lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
+    lfs_off_t target = lfs_ctz_index(lfs, &pos);
+
+    while (current > target) {
+        lfs_size_t skip = lfs_min(
+                lfs_npw2(current-target+1) - 1,
+                lfs_ctz(current));
+
+        int err = lfs_bd_read(lfs,
+                pcache, rcache, sizeof(head),
+                head, 4*skip, &head, sizeof(head));
+        head = lfs_fromle32(head);
+        if (err) {
+            return err;
+        }
+
+        LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count);
+        current -= 1 << skip;
+    }
+
+    *block = head;
+    *off = pos;
+    return 0;
+}
+
+static int lfs_ctz_extend(lfs_t *lfs,
+        lfs_cache_t *pcache, lfs_cache_t *rcache,
+        lfs_block_t head, lfs_size_t size,
+        lfs_block_t *block, lfs_off_t *off) {
+    while (true) {
+        // go ahead and grab a block
+        lfs_block_t nblock;
+        int err = lfs_alloc(lfs, &nblock);
+        if (err) {
+            return err;
+        }
+        LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count);
+
+        {
+            err = lfs_bd_erase(lfs, nblock);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+
+            if (size == 0) {
+                *block = nblock;
+                *off = 0;
+                return 0;
+            }
+
+            size -= 1;
+            lfs_off_t index = lfs_ctz_index(lfs, &size);
+            size += 1;
+
+            // just copy out the last block if it is incomplete
+            if (size != lfs->cfg->block_size) {
+                for (lfs_off_t i = 0; i < size; i++) {
+                    uint8_t data;
+                    err = lfs_bd_read(lfs,
+                            NULL, rcache, size-i,
+                            head, i, &data, 1);
+                    if (err) {
+                        return err;
+                    }
+
+                    err = lfs_bd_prog(lfs,
+                            pcache, rcache, true,
+                            nblock, i, &data, 1);
+                    if (err) {
+                        if (err == LFS_ERR_CORRUPT) {
+                            goto relocate;
+                        }
+                        return err;
+                    }
+                }
+
+                *block = nblock;
+                *off = size;
+                return 0;
+            }
+
+            // append block
+            index += 1;
+            lfs_size_t skips = lfs_ctz(index) + 1;
+
+            for (lfs_off_t i = 0; i < skips; i++) {
+                head = lfs_tole32(head);
+                err = lfs_bd_prog(lfs, pcache, rcache, true,
+                        nblock, 4*i, &head, 4);
+                head = lfs_fromle32(head);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+
+                if (i != skips-1) {
+                    err = lfs_bd_read(lfs,
+                            NULL, rcache, sizeof(head),
+                            head, 4*i, &head, sizeof(head));
+                    head = lfs_fromle32(head);
+                    if (err) {
+                        return err;
+                    }
+                }
+
+                LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count);
+            }
+
+            *block = nblock;
+            *off = 4*skips;
+            return 0;
+        }
+
+relocate:
+        LFS_DEBUG("Bad block at %"PRIx32, nblock);
+
+        // just clear cache and try a new block
+        lfs_cache_drop(lfs, pcache);
+    }
+}
+
+static int lfs_ctz_traverse(lfs_t *lfs,
+        const lfs_cache_t *pcache, lfs_cache_t *rcache,
+        lfs_block_t head, lfs_size_t size,
+        int (*cb)(void*, lfs_block_t), void *data) {
+    if (size == 0) {
+        return 0;
+    }
+
+    lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
+
+    while (true) {
+        int err = cb(data, head);
+        if (err) {
+            return err;
+        }
+
+        if (index == 0) {
+            return 0;
+        }
+
+        lfs_block_t heads[2];
+        int count = 2 - (index & 1);
+        err = lfs_bd_read(lfs,
+                pcache, rcache, count*sizeof(head),
+                head, 0, &heads, count*sizeof(head));
+        heads[0] = lfs_fromle32(heads[0]);
+        heads[1] = lfs_fromle32(heads[1]);
+        if (err) {
+            return err;
+        }
+
+        for (int i = 0; i < count-1; i++) {
+            err = cb(data, heads[i]);
+            if (err) {
+                return err;
+            }
+        }
+
+        head = heads[count-1];
+        index -= count;
+    }
+}
+
+
+/// Top level file operations ///
+int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags,
+        const struct lfs_file_config *cfg) {
+    LFS_TRACE("lfs_file_opencfg(%p, %p, \"%s\", %x, %p {"
+                 ".buffer=%p, .attrs=%p, .attr_count=%"PRIu32"})",
+            (void*)lfs, (void*)file, path, flags,
+            (void*)cfg, cfg->buffer, (void*)cfg->attrs, cfg->attr_count);
+
+    // deorphan if we haven't yet, needed at most once after poweron
+    if ((flags & 3) != LFS_O_RDONLY) {
+        int err = lfs_fs_forceconsistency(lfs);
+        if (err) {
+            LFS_TRACE("lfs_file_opencfg -> %d", err);
+            return err;
+        }
+    }
+
+    // setup simple file details
+    int err;
+    file->cfg = cfg;
+    file->flags = flags | LFS_F_OPENED;
+    file->pos = 0;
+    file->off = 0;
+    file->cache.buffer = NULL;
+
+    // allocate entry for file if it doesn't exist
+    lfs_stag_t tag = lfs_dir_find(lfs, &file->m, &path, &file->id);
+    if (tag < 0 && !(tag == LFS_ERR_NOENT && file->id != 0x3ff)) {
+        err = tag;
+        goto cleanup;
+    }
+
+    // get id, add to list of mdirs to catch update changes
+    file->type = LFS_TYPE_REG;
+    file->next = (lfs_file_t*)lfs->mlist;
+    lfs->mlist = (struct lfs_mlist*)file;
+
+    if (tag == LFS_ERR_NOENT) {
+        if (!(flags & LFS_O_CREAT)) {
+            err = LFS_ERR_NOENT;
+            goto cleanup;
+        }
+
+        // check that name fits
+        lfs_size_t nlen = strlen(path);
+        if (nlen > lfs->name_max) {
+            err = LFS_ERR_NAMETOOLONG;
+            goto cleanup;
+        }
+
+        // get next slot and create entry to remember name
+        err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_CREATE, file->id, 0), NULL},
+                {LFS_MKTAG(LFS_TYPE_REG, file->id, nlen), path},
+                {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), NULL}));
+        if (err) {
+            err = LFS_ERR_NAMETOOLONG;
+            goto cleanup;
+        }
+
+        tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, 0);
+    } else if (flags & LFS_O_EXCL) {
+        err = LFS_ERR_EXIST;
+        goto cleanup;
+    } else if (lfs_tag_type3(tag) != LFS_TYPE_REG) {
+        err = LFS_ERR_ISDIR;
+        goto cleanup;
+    } else if (flags & LFS_O_TRUNC) {
+        // truncate if requested
+        tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0);
+        file->flags |= LFS_F_DIRTY;
+    } else {
+        // try to load what's on disk, if it's inlined we'll fix it later
+        tag = lfs_dir_get(lfs, &file->m, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, file->id, 8), &file->ctz);
+        if (tag < 0) {
+            err = tag;
+            goto cleanup;
+        }
+        lfs_ctz_fromle32(&file->ctz);
+    }
+
+    // fetch attrs
+    for (unsigned i = 0; i < file->cfg->attr_count; i++) {
+        if ((file->flags & 3) != LFS_O_WRONLY) {
+            lfs_stag_t res = lfs_dir_get(lfs, &file->m,
+                    LFS_MKTAG(0x7ff, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_USERATTR + file->cfg->attrs[i].type,
+                        file->id, file->cfg->attrs[i].size),
+                        file->cfg->attrs[i].buffer);
+            if (res < 0 && res != LFS_ERR_NOENT) {
+                err = res;
+                goto cleanup;
+            }
+        }
+
+        if ((file->flags & 3) != LFS_O_RDONLY) {
+            if (file->cfg->attrs[i].size > lfs->attr_max) {
+                err = LFS_ERR_NOSPC;
+                goto cleanup;
+            }
+
+            file->flags |= LFS_F_DIRTY;
+        }
+    }
+
+    // allocate buffer if needed
+    if (file->cfg->buffer) {
+        file->cache.buffer = file->cfg->buffer;
+    } else {
+        file->cache.buffer = lfs_malloc(lfs->cfg->cache_size);
+        if (!file->cache.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // zero to avoid information leak
+    lfs_cache_zero(lfs, &file->cache);
+
+    if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) {
+        // load inline files
+        file->ctz.head = LFS_BLOCK_INLINE;
+        file->ctz.size = lfs_tag_size(tag);
+        file->flags |= LFS_F_INLINE;
+        file->cache.block = file->ctz.head;
+        file->cache.off = 0;
+        file->cache.size = lfs->cfg->cache_size;
+
+        // don't always read (may be new/trunc file)
+        if (file->ctz.size > 0) {
+            lfs_stag_t res = lfs_dir_get(lfs, &file->m,
+                    LFS_MKTAG(0x700, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, file->id,
+                        lfs_min(file->cache.size, 0x3fe)),
+                    file->cache.buffer);
+            if (res < 0) {
+                err = res;
+                goto cleanup;
+            }
+        }
+    }
+
+    LFS_TRACE("lfs_file_opencfg -> %d", 0);
+    return 0;
+
+cleanup:
+    // clean up lingering resources
+    file->flags |= LFS_F_ERRED;
+    lfs_file_close(lfs, file);
+    LFS_TRACE("lfs_file_opencfg -> %d", err);
+    return err;
+}
+
+int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags) {
+    LFS_TRACE("lfs_file_open(%p, %p, \"%s\", %x)",
+            (void*)lfs, (void*)file, path, flags);
+    static const struct lfs_file_config defaults = {0};
+    int err = lfs_file_opencfg(lfs, file, path, flags, &defaults);
+    LFS_TRACE("lfs_file_open -> %d", err);
+    return err;
+}
+
+int lfs_file_close(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_close(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    int err = lfs_file_sync(lfs, file);
+
+    // remove from list of mdirs
+    for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) {
+        if (*p == (struct lfs_mlist*)file) {
+            *p = (*p)->next;
+            break;
+        }
+    }
+
+    // clean up memory
+    if (!file->cfg->buffer) {
+        lfs_free(file->cache.buffer);
+    }
+
+    file->flags &= ~LFS_F_OPENED;
+    LFS_TRACE("lfs_file_close -> %d", err);
+    return err;
+}
+
+static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) {
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    while (true) {
+        // just relocate what exists into new block
+        lfs_block_t nblock;
+        int err = lfs_alloc(lfs, &nblock);
+        if (err) {
+            return err;
+        }
+
+        err = lfs_bd_erase(lfs, nblock);
+        if (err) {
+            if (err == LFS_ERR_CORRUPT) {
+                goto relocate;
+            }
+            return err;
+        }
+
+        // either read from dirty cache or disk
+        for (lfs_off_t i = 0; i < file->off; i++) {
+            uint8_t data;
+            if (file->flags & LFS_F_INLINE) {
+                err = lfs_dir_getread(lfs, &file->m,
+                        // note we evict inline files before they can be dirty
+                        NULL, &file->cache, file->off-i,
+                        LFS_MKTAG(0xfff, 0x1ff, 0),
+                        LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0),
+                        i, &data, 1);
+                if (err) {
+                    return err;
+                }
+            } else {
+                err = lfs_bd_read(lfs,
+                        &file->cache, &lfs->rcache, file->off-i,
+                        file->block, i, &data, 1);
+                if (err) {
+                    return err;
+                }
+            }
+
+            err = lfs_bd_prog(lfs,
+                    &lfs->pcache, &lfs->rcache, true,
+                    nblock, i, &data, 1);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                return err;
+            }
+        }
+
+        // copy over new state of file
+        memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->cache_size);
+        file->cache.block = lfs->pcache.block;
+        file->cache.off = lfs->pcache.off;
+        file->cache.size = lfs->pcache.size;
+        lfs_cache_zero(lfs, &lfs->pcache);
+
+        file->block = nblock;
+        file->flags |= LFS_F_WRITING;
+        return 0;
+
+relocate:
+        LFS_DEBUG("Bad block at %"PRIx32, nblock);
+
+        // just clear cache and try a new block
+        lfs_cache_drop(lfs, &lfs->pcache);
+    }
+}
+
+static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file) {
+    file->off = file->pos;
+    lfs_alloc_ack(lfs);
+    int err = lfs_file_relocate(lfs, file);
+    if (err) {
+        return err;
+    }
+
+    file->flags &= ~LFS_F_INLINE;
+    return 0;
+}
+
+static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) {
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    if (file->flags & LFS_F_READING) {
+        if (!(file->flags & LFS_F_INLINE)) {
+            lfs_cache_drop(lfs, &file->cache);
+        }
+        file->flags &= ~LFS_F_READING;
+    }
+
+    if (file->flags & LFS_F_WRITING) {
+        lfs_off_t pos = file->pos;
+
+        if (!(file->flags & LFS_F_INLINE)) {
+            // copy over anything after current branch
+            lfs_file_t orig = {
+                .ctz.head = file->ctz.head,
+                .ctz.size = file->ctz.size,
+                .flags = LFS_O_RDONLY | LFS_F_OPENED,
+                .pos = file->pos,
+                .cache = lfs->rcache,
+            };
+            lfs_cache_drop(lfs, &lfs->rcache);
+
+            while (file->pos < file->ctz.size) {
+                // copy over a byte at a time, leave it up to caching
+                // to make this efficient
+                uint8_t data;
+                lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1);
+                if (res < 0) {
+                    return res;
+                }
+
+                res = lfs_file_write(lfs, file, &data, 1);
+                if (res < 0) {
+                    return res;
+                }
+
+                // keep our reference to the rcache in sync
+                if (lfs->rcache.block != LFS_BLOCK_NULL) {
+                    lfs_cache_drop(lfs, &orig.cache);
+                    lfs_cache_drop(lfs, &lfs->rcache);
+                }
+            }
+
+            // write out what we have
+            while (true) {
+                int err = lfs_bd_flush(lfs, &file->cache, &lfs->rcache, true);
+                if (err) {
+                    if (err == LFS_ERR_CORRUPT) {
+                        goto relocate;
+                    }
+                    return err;
+                }
+
+                break;
+
+relocate:
+                LFS_DEBUG("Bad block at %"PRIx32, file->block);
+                err = lfs_file_relocate(lfs, file);
+                if (err) {
+                    return err;
+                }
+            }
+        } else {
+            file->pos = lfs_max(file->pos, file->ctz.size);
+        }
+
+        // actual file updates
+        file->ctz.head = file->block;
+        file->ctz.size = file->pos;
+        file->flags &= ~LFS_F_WRITING;
+        file->flags |= LFS_F_DIRTY;
+
+        file->pos = pos;
+    }
+
+    return 0;
+}
+
+int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_sync(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    while (true) {
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            file->flags |= LFS_F_ERRED;
+            LFS_TRACE("lfs_file_sync -> %d", err);
+            return err;
+        }
+
+        if ((file->flags & LFS_F_DIRTY) &&
+                !(file->flags & LFS_F_ERRED) &&
+                !lfs_pair_isnull(file->m.pair)) {
+            // update dir entry
+            uint16_t type;
+            const void *buffer;
+            lfs_size_t size;
+            struct lfs_ctz ctz;
+            if (file->flags & LFS_F_INLINE) {
+                // inline the whole file
+                type = LFS_TYPE_INLINESTRUCT;
+                buffer = file->cache.buffer;
+                size = file->ctz.size;
+            } else {
+                // update the ctz reference
+                type = LFS_TYPE_CTZSTRUCT;
+                // copy ctz so alloc will work during a relocate
+                ctz = file->ctz;
+                lfs_ctz_tole32(&ctz);
+                buffer = &ctz;
+                size = sizeof(ctz);
+            }
+
+            // commit file data and attributes
+            err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS(
+                    {LFS_MKTAG(type, file->id, size), buffer},
+                    {LFS_MKTAG(LFS_FROM_USERATTRS, file->id,
+                        file->cfg->attr_count), file->cfg->attrs}));
+            if (err) {
+                if (err == LFS_ERR_NOSPC && (file->flags & LFS_F_INLINE)) {
+                    goto relocate;
+                }
+                file->flags |= LFS_F_ERRED;
+                LFS_TRACE("lfs_file_sync -> %d", err);
+                return err;
+            }
+
+            file->flags &= ~LFS_F_DIRTY;
+        }
+
+        LFS_TRACE("lfs_file_sync -> %d", 0);
+        return 0;
+
+relocate:
+        // inline file doesn't fit anymore
+        err = lfs_file_outline(lfs, file);
+        if (err) {
+            file->flags |= LFS_F_ERRED;
+            LFS_TRACE("lfs_file_sync -> %d", err);
+            return err;
+        }
+    }
+}
+
+lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file,
+        void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_file_read(%p, %p, %p, %"PRIu32")",
+            (void*)lfs, (void*)file, buffer, size);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    LFS_ASSERT((file->flags & 3) != LFS_O_WRONLY);
+
+    uint8_t *data = buffer;
+    lfs_size_t nsize = size;
+
+    if (file->flags & LFS_F_WRITING) {
+        // flush out any writes
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            LFS_TRACE("lfs_file_read -> %d", err);
+            return err;
+        }
+    }
+
+    if (file->pos >= file->ctz.size) {
+        // eof if past end
+        LFS_TRACE("lfs_file_read -> %d", 0);
+        return 0;
+    }
+
+    size = lfs_min(size, file->ctz.size - file->pos);
+    nsize = size;
+
+    while (nsize > 0) {
+        // check if we need a new block
+        if (!(file->flags & LFS_F_READING) ||
+                file->off == lfs->cfg->block_size) {
+            if (!(file->flags & LFS_F_INLINE)) {
+                int err = lfs_ctz_find(lfs, NULL, &file->cache,
+                        file->ctz.head, file->ctz.size,
+                        file->pos, &file->block, &file->off);
+                if (err) {
+                    LFS_TRACE("lfs_file_read -> %d", err);
+                    return err;
+                }
+            } else {
+                file->block = LFS_BLOCK_INLINE;
+                file->off = file->pos;
+            }
+
+            file->flags |= LFS_F_READING;
+        }
+
+        // read as much as we can in current block
+        lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off);
+        if (file->flags & LFS_F_INLINE) {
+            int err = lfs_dir_getread(lfs, &file->m,
+                    NULL, &file->cache, lfs->cfg->block_size,
+                    LFS_MKTAG(0xfff, 0x1ff, 0),
+                    LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0),
+                    file->off, data, diff);
+            if (err) {
+                LFS_TRACE("lfs_file_read -> %d", err);
+                return err;
+            }
+        } else {
+            int err = lfs_bd_read(lfs,
+                    NULL, &file->cache, lfs->cfg->block_size,
+                    file->block, file->off, data, diff);
+            if (err) {
+                LFS_TRACE("lfs_file_read -> %d", err);
+                return err;
+            }
+        }
+
+        file->pos += diff;
+        file->off += diff;
+        data += diff;
+        nsize -= diff;
+    }
+
+    LFS_TRACE("lfs_file_read -> %"PRId32, size);
+    return size;
+}
+
+lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
+        const void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_file_write(%p, %p, %p, %"PRIu32")",
+            (void*)lfs, (void*)file, buffer, size);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    LFS_ASSERT((file->flags & 3) != LFS_O_RDONLY);
+
+    const uint8_t *data = buffer;
+    lfs_size_t nsize = size;
+
+    if (file->flags & LFS_F_READING) {
+        // drop any reads
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            LFS_TRACE("lfs_file_write -> %d", err);
+            return err;
+        }
+    }
+
+    if ((file->flags & LFS_O_APPEND) && file->pos < file->ctz.size) {
+        file->pos = file->ctz.size;
+    }
+
+    if (file->pos + size > lfs->file_max) {
+        // Larger than file limit?
+        LFS_TRACE("lfs_file_write -> %d", LFS_ERR_FBIG);
+        return LFS_ERR_FBIG;
+    }
+
+    if (!(file->flags & LFS_F_WRITING) && file->pos > file->ctz.size) {
+        // fill with zeros
+        lfs_off_t pos = file->pos;
+        file->pos = file->ctz.size;
+
+        while (file->pos < pos) {
+            lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1);
+            if (res < 0) {
+                LFS_TRACE("lfs_file_write -> %"PRId32, res);
+                return res;
+            }
+        }
+    }
+
+    if ((file->flags & LFS_F_INLINE) &&
+            lfs_max(file->pos+nsize, file->ctz.size) >
+            lfs_min(0x3fe, lfs_min(
+                lfs->cfg->cache_size, lfs->cfg->block_size/8))) {
+        // inline file doesn't fit anymore
+        int err = lfs_file_outline(lfs, file);
+        if (err) {
+            file->flags |= LFS_F_ERRED;
+            LFS_TRACE("lfs_file_write -> %d", err);
+            return err;
+        }
+    }
+
+    while (nsize > 0) {
+        // check if we need a new block
+        if (!(file->flags & LFS_F_WRITING) ||
+                file->off == lfs->cfg->block_size) {
+            if (!(file->flags & LFS_F_INLINE)) {
+                if (!(file->flags & LFS_F_WRITING) && file->pos > 0) {
+                    // find out which block we're extending from
+                    int err = lfs_ctz_find(lfs, NULL, &file->cache,
+                            file->ctz.head, file->ctz.size,
+                            file->pos-1, &file->block, &file->off);
+                    if (err) {
+                        file->flags |= LFS_F_ERRED;
+                        LFS_TRACE("lfs_file_write -> %d", err);
+                        return err;
+                    }
+
+                    // mark cache as dirty since we may have read data into it
+                    lfs_cache_zero(lfs, &file->cache);
+                }
+
+                // extend file with new blocks
+                lfs_alloc_ack(lfs);
+                int err = lfs_ctz_extend(lfs, &file->cache, &lfs->rcache,
+                        file->block, file->pos,
+                        &file->block, &file->off);
+                if (err) {
+                    file->flags |= LFS_F_ERRED;
+                    LFS_TRACE("lfs_file_write -> %d", err);
+                    return err;
+                }
+            } else {
+                file->block = LFS_BLOCK_INLINE;
+                file->off = file->pos;
+            }
+
+            file->flags |= LFS_F_WRITING;
+        }
+
+        // program as much as we can in current block
+        lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off);
+        while (true) {
+            int err = lfs_bd_prog(lfs, &file->cache, &lfs->rcache, true,
+                    file->block, file->off, data, diff);
+            if (err) {
+                if (err == LFS_ERR_CORRUPT) {
+                    goto relocate;
+                }
+                file->flags |= LFS_F_ERRED;
+                LFS_TRACE("lfs_file_write -> %d", err);
+                return err;
+            }
+
+            break;
+relocate:
+            err = lfs_file_relocate(lfs, file);
+            if (err) {
+                file->flags |= LFS_F_ERRED;
+                LFS_TRACE("lfs_file_write -> %d", err);
+                return err;
+            }
+        }
+
+        file->pos += diff;
+        file->off += diff;
+        data += diff;
+        nsize -= diff;
+
+        lfs_alloc_ack(lfs);
+    }
+
+    file->flags &= ~LFS_F_ERRED;
+    LFS_TRACE("lfs_file_write -> %"PRId32, size);
+    return size;
+}
+
+lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file,
+        lfs_soff_t off, int whence) {
+    LFS_TRACE("lfs_file_seek(%p, %p, %"PRId32", %d)",
+            (void*)lfs, (void*)file, off, whence);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+
+    // write out everything beforehand, may be noop if rdonly
+    int err = lfs_file_flush(lfs, file);
+    if (err) {
+        LFS_TRACE("lfs_file_seek -> %d", err);
+        return err;
+    }
+
+    // find new pos
+    lfs_off_t npos = file->pos;
+    if (whence == LFS_SEEK_SET) {
+        npos = off;
+    } else if (whence == LFS_SEEK_CUR) {
+        npos = file->pos + off;
+    } else if (whence == LFS_SEEK_END) {
+        npos = file->ctz.size + off;
+    }
+
+    if (npos > lfs->file_max) {
+        // file position out of range
+        LFS_TRACE("lfs_file_seek -> %d", LFS_ERR_INVAL);
+        return LFS_ERR_INVAL;
+    }
+
+    // update pos
+    file->pos = npos;
+    LFS_TRACE("lfs_file_seek -> %"PRId32, npos);
+    return npos;
+}
+
+int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) {
+    LFS_TRACE("lfs_file_truncate(%p, %p, %"PRIu32")",
+            (void*)lfs, (void*)file, size);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    LFS_ASSERT((file->flags & 3) != LFS_O_RDONLY);
+
+    if (size > LFS_FILE_MAX) {
+        LFS_TRACE("lfs_file_truncate -> %d", LFS_ERR_INVAL);
+        return LFS_ERR_INVAL;
+    }
+
+    lfs_off_t pos = file->pos;
+    lfs_off_t oldsize = lfs_file_size(lfs, file);
+    if (size < oldsize) {
+        // need to flush since directly changing metadata
+        int err = lfs_file_flush(lfs, file);
+        if (err) {
+            LFS_TRACE("lfs_file_truncate -> %d", err);
+            return err;
+        }
+
+        // lookup new head in ctz skip list
+        err = lfs_ctz_find(lfs, NULL, &file->cache,
+                file->ctz.head, file->ctz.size,
+                size, &file->block, &file->off);
+        if (err) {
+            LFS_TRACE("lfs_file_truncate -> %d", err);
+            return err;
+        }
+
+        file->ctz.head = file->block;
+        file->ctz.size = size;
+        file->flags |= LFS_F_DIRTY | LFS_F_READING;
+    } else if (size > oldsize) {
+        // flush+seek if not already at end
+        if (file->pos != oldsize) {
+            lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_END);
+            if (res < 0) {
+                LFS_TRACE("lfs_file_truncate -> %"PRId32, res);
+                return (int)res;
+            }
+        }
+
+        // fill with zeros
+        while (file->pos < size) {
+            lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1);
+            if (res < 0) {
+                LFS_TRACE("lfs_file_truncate -> %"PRId32, res);
+                return (int)res;
+            }
+        }
+    }
+
+    // restore pos
+    lfs_soff_t res = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET);
+    if (res < 0) {
+      LFS_TRACE("lfs_file_truncate -> %"PRId32, res);
+      return (int)res;
+    }
+
+    LFS_TRACE("lfs_file_truncate -> %d", 0);
+    return 0;
+}
+
+lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_tell(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    (void)lfs;
+    LFS_TRACE("lfs_file_tell -> %"PRId32, file->pos);
+    return file->pos;
+}
+
+int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_rewind(%p, %p)", (void*)lfs, (void*)file);
+    lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET);
+    if (res < 0) {
+        LFS_TRACE("lfs_file_rewind -> %"PRId32, res);
+        return (int)res;
+    }
+
+    LFS_TRACE("lfs_file_rewind -> %d", 0);
+    return 0;
+}
+
+lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) {
+    LFS_TRACE("lfs_file_size(%p, %p)", (void*)lfs, (void*)file);
+    LFS_ASSERT(file->flags & LFS_F_OPENED);
+    (void)lfs;
+    if (file->flags & LFS_F_WRITING) {
+        LFS_TRACE("lfs_file_size -> %"PRId32,
+                lfs_max(file->pos, file->ctz.size));
+        return lfs_max(file->pos, file->ctz.size);
+    } else {
+        LFS_TRACE("lfs_file_size -> %"PRId32, file->ctz.size);
+        return file->ctz.size;
+    }
+}
+
+
+/// General fs operations ///
+int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) {
+    LFS_TRACE("lfs_stat(%p, \"%s\", %p)", (void*)lfs, path, (void*)info);
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0) {
+        LFS_TRACE("lfs_stat -> %"PRId32, tag);
+        return (int)tag;
+    }
+
+    int err = lfs_dir_getinfo(lfs, &cwd, lfs_tag_id(tag), info);
+    LFS_TRACE("lfs_stat -> %d", err);
+    return err;
+}
+
+int lfs_remove(lfs_t *lfs, const char *path) {
+    LFS_TRACE("lfs_remove(%p, \"%s\")", (void*)lfs, path);
+    // deorphan if we haven't yet, needed at most once after poweron
+    int err = lfs_fs_forceconsistency(lfs);
+    if (err) {
+        LFS_TRACE("lfs_remove -> %d", err);
+        return err;
+    }
+
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0 || lfs_tag_id(tag) == 0x3ff) {
+        LFS_TRACE("lfs_remove -> %"PRId32, (tag < 0) ? tag : LFS_ERR_INVAL);
+        return (tag < 0) ? (int)tag : LFS_ERR_INVAL;
+    }
+
+    lfs_mdir_t dir;
+    if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
+        // must be empty before removal
+        lfs_block_t pair[2];
+        lfs_stag_t res = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair);
+        if (res < 0) {
+            LFS_TRACE("lfs_remove -> %"PRId32, res);
+            return (int)res;
+        }
+        lfs_pair_fromle32(pair);
+
+        err = lfs_dir_fetch(lfs, &dir, pair);
+        if (err) {
+            LFS_TRACE("lfs_remove -> %d", err);
+            return err;
+        }
+
+        if (dir.count > 0 || dir.split) {
+            LFS_TRACE("lfs_remove -> %d", LFS_ERR_NOTEMPTY);
+            return LFS_ERR_NOTEMPTY;
+        }
+
+        // mark fs as orphaned
+        lfs_fs_preporphans(lfs, +1);
+    }
+
+    // delete the entry
+    err = lfs_dir_commit(lfs, &cwd, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(tag), 0), NULL}));
+    if (err) {
+        LFS_TRACE("lfs_remove -> %d", err);
+        return err;
+    }
+
+    if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
+        // fix orphan
+        lfs_fs_preporphans(lfs, -1);
+
+        err = lfs_fs_pred(lfs, dir.pair, &cwd);
+        if (err) {
+            LFS_TRACE("lfs_remove -> %d", err);
+            return err;
+        }
+
+        err = lfs_dir_drop(lfs, &cwd, &dir);
+        if (err) {
+            LFS_TRACE("lfs_remove -> %d", err);
+            return err;
+        }
+    }
+
+    LFS_TRACE("lfs_remove -> %d", 0);
+    return 0;
+}
+
+int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
+    LFS_TRACE("lfs_rename(%p, \"%s\", \"%s\")", (void*)lfs, oldpath, newpath);
+
+    // deorphan if we haven't yet, needed at most once after poweron
+    int err = lfs_fs_forceconsistency(lfs);
+    if (err) {
+        LFS_TRACE("lfs_rename -> %d", err);
+        return err;
+    }
+
+    // find old entry
+    lfs_mdir_t oldcwd;
+    lfs_stag_t oldtag = lfs_dir_find(lfs, &oldcwd, &oldpath, NULL);
+    if (oldtag < 0 || lfs_tag_id(oldtag) == 0x3ff) {
+        LFS_TRACE("lfs_rename -> %"PRId32, (oldtag < 0) ? oldtag : LFS_ERR_INVAL);
+        return (oldtag < 0) ? (int)oldtag : LFS_ERR_INVAL;
+    }
+
+    // find new entry
+    lfs_mdir_t newcwd;
+    uint16_t newid;
+    lfs_stag_t prevtag = lfs_dir_find(lfs, &newcwd, &newpath, &newid);
+    if ((prevtag < 0 || lfs_tag_id(prevtag) == 0x3ff) &&
+            !(prevtag == LFS_ERR_NOENT && newid != 0x3ff)) {
+        LFS_TRACE("lfs_rename -> %"PRId32, (prevtag < 0) ? prevtag : LFS_ERR_INVAL);
+        return (prevtag < 0) ? (int)prevtag : LFS_ERR_INVAL;
+    }
+
+    lfs_mdir_t prevdir;
+    if (prevtag == LFS_ERR_NOENT) {
+        // check that name fits
+        lfs_size_t nlen = strlen(newpath);
+        if (nlen > lfs->name_max) {
+            LFS_TRACE("lfs_rename -> %d", LFS_ERR_NAMETOOLONG);
+            return LFS_ERR_NAMETOOLONG;
+        }
+    } else if (lfs_tag_type3(prevtag) != lfs_tag_type3(oldtag)) {
+        LFS_TRACE("lfs_rename -> %d", LFS_ERR_ISDIR);
+        return LFS_ERR_ISDIR;
+    } else if (lfs_tag_type3(prevtag) == LFS_TYPE_DIR) {
+        // must be empty before removal
+        lfs_block_t prevpair[2];
+        lfs_stag_t res = lfs_dir_get(lfs, &newcwd, LFS_MKTAG(0x700, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_STRUCT, newid, 8), prevpair);
+        if (res < 0) {
+            LFS_TRACE("lfs_rename -> %"PRId32, res);
+            return (int)res;
+        }
+        lfs_pair_fromle32(prevpair);
+
+        // must be empty before removal
+        err = lfs_dir_fetch(lfs, &prevdir, prevpair);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+
+        if (prevdir.count > 0 || prevdir.split) {
+            LFS_TRACE("lfs_rename -> %d", LFS_ERR_NOTEMPTY);
+            return LFS_ERR_NOTEMPTY;
+        }
+
+        // mark fs as orphaned
+        lfs_fs_preporphans(lfs, +1);
+    }
+
+    // create move to fix later
+    uint16_t newoldtagid = lfs_tag_id(oldtag);
+    if (lfs_pair_cmp(oldcwd.pair, newcwd.pair) == 0 &&
+            prevtag == LFS_ERR_NOENT && newid <= newoldtagid) {
+        // there is a small chance we are being renamed in the same directory
+        // to an id less than our old id, the global update to handle this
+        // is a bit messy
+        newoldtagid += 1;
+    }
+
+    lfs_fs_prepmove(lfs, newoldtagid, oldcwd.pair);
+
+    // move over all attributes
+    err = lfs_dir_commit(lfs, &newcwd, LFS_MKATTRS(
+            {prevtag != LFS_ERR_NOENT
+                ? LFS_MKTAG(LFS_TYPE_DELETE, newid, 0)
+                : LFS_MKTAG(LFS_FROM_NOOP, 0, 0), NULL},
+            {LFS_MKTAG(LFS_TYPE_CREATE, newid, 0), NULL},
+            {LFS_MKTAG(lfs_tag_type3(oldtag), newid, strlen(newpath)),
+                newpath},
+            {LFS_MKTAG(LFS_FROM_MOVE, newid, lfs_tag_id(oldtag)), &oldcwd}));
+    if (err) {
+        LFS_TRACE("lfs_rename -> %d", err);
+        return err;
+    }
+
+    // let commit clean up after move (if we're different! otherwise move
+    // logic already fixed it for us)
+    if (lfs_pair_cmp(oldcwd.pair, newcwd.pair) != 0) {
+        err = lfs_dir_commit(lfs, &oldcwd, NULL, 0);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+    }
+
+    if (prevtag != LFS_ERR_NOENT && lfs_tag_type3(prevtag) == LFS_TYPE_DIR) {
+        // fix orphan
+        lfs_fs_preporphans(lfs, -1);
+
+        err = lfs_fs_pred(lfs, prevdir.pair, &newcwd);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+
+        err = lfs_dir_drop(lfs, &newcwd, &prevdir);
+        if (err) {
+            LFS_TRACE("lfs_rename -> %d", err);
+            return err;
+        }
+    }
+
+    LFS_TRACE("lfs_rename -> %d", 0);
+    return 0;
+}
+
+lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path,
+        uint8_t type, void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_getattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")",
+            (void*)lfs, path, type, buffer, size);
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0) {
+        LFS_TRACE("lfs_getattr -> %"PRId32, tag);
+        return tag;
+    }
+
+    uint16_t id = lfs_tag_id(tag);
+    if (id == 0x3ff) {
+        // special case for root
+        id = 0;
+        int err = lfs_dir_fetch(lfs, &cwd, lfs->root);
+        if (err) {
+            LFS_TRACE("lfs_getattr -> %d", err);
+            return err;
+        }
+    }
+
+    tag = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x7ff, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_USERATTR + type,
+                id, lfs_min(size, lfs->attr_max)),
+            buffer);
+    if (tag < 0) {
+        if (tag == LFS_ERR_NOENT) {
+            LFS_TRACE("lfs_getattr -> %d", LFS_ERR_NOATTR);
+            return LFS_ERR_NOATTR;
+        }
+
+        LFS_TRACE("lfs_getattr -> %"PRId32, tag);
+        return tag;
+    }
+
+    size = lfs_tag_size(tag);
+    LFS_TRACE("lfs_getattr -> %"PRId32, size);
+    return size;
+}
+
+static int lfs_commitattr(lfs_t *lfs, const char *path,
+        uint8_t type, const void *buffer, lfs_size_t size) {
+    lfs_mdir_t cwd;
+    lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL);
+    if (tag < 0) {
+        return tag;
+    }
+
+    uint16_t id = lfs_tag_id(tag);
+    if (id == 0x3ff) {
+        // special case for root
+        id = 0;
+        int err = lfs_dir_fetch(lfs, &cwd, lfs->root);
+        if (err) {
+            return err;
+        }
+    }
+
+    return lfs_dir_commit(lfs, &cwd, LFS_MKATTRS(
+            {LFS_MKTAG(LFS_TYPE_USERATTR + type, id, size), buffer}));
+}
+
+int lfs_setattr(lfs_t *lfs, const char *path,
+        uint8_t type, const void *buffer, lfs_size_t size) {
+    LFS_TRACE("lfs_setattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")",
+            (void*)lfs, path, type, buffer, size);
+    if (size > lfs->attr_max) {
+        LFS_TRACE("lfs_setattr -> %d", LFS_ERR_NOSPC);
+        return LFS_ERR_NOSPC;
+    }
+
+    int err = lfs_commitattr(lfs, path, type, buffer, size);
+    LFS_TRACE("lfs_setattr -> %d", err);
+    return err;
+}
+
+int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type) {
+    LFS_TRACE("lfs_removeattr(%p, \"%s\", %"PRIu8")", (void*)lfs, path, type);
+    int err = lfs_commitattr(lfs, path, type, NULL, 0x3ff);
+    LFS_TRACE("lfs_removeattr -> %d", err);
+    return err;
+}
+
+
+/// Filesystem operations ///
+static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
+    lfs->cfg = cfg;
+    int err = 0;
+
+    // validate that the lfs-cfg sizes were initiated properly before
+    // performing any arithmetic logics with them
+    LFS_ASSERT(lfs->cfg->read_size != 0);
+    LFS_ASSERT(lfs->cfg->prog_size != 0);
+    LFS_ASSERT(lfs->cfg->cache_size != 0);
+
+    // check that block size is a multiple of cache size is a multiple
+    // of prog and read sizes
+    LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->read_size == 0);
+    LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->prog_size == 0);
+    LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->cache_size == 0);
+
+    // check that the block size is large enough to fit ctz pointers
+    LFS_ASSERT(4*lfs_npw2(LFS_BLOCK_NULL / (lfs->cfg->block_size-2*4))
+            <= lfs->cfg->block_size);
+
+    // block_cycles = 0 is no longer supported.
+    //
+    // block_cycles is the number of erase cycles before littlefs evicts
+    // metadata logs as a part of wear leveling. Suggested values are in the
+    // range of 100-1000, or set block_cycles to -1 to disable block-level
+    // wear-leveling.
+    LFS_ASSERT(lfs->cfg->block_cycles != 0);
+
+
+    // setup read cache
+    if (lfs->cfg->read_buffer) {
+        lfs->rcache.buffer = lfs->cfg->read_buffer;
+    } else {
+        lfs->rcache.buffer = lfs_malloc(lfs->cfg->cache_size);
+        if (!lfs->rcache.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // setup program cache
+    if (lfs->cfg->prog_buffer) {
+        lfs->pcache.buffer = lfs->cfg->prog_buffer;
+    } else {
+        lfs->pcache.buffer = lfs_malloc(lfs->cfg->cache_size);
+        if (!lfs->pcache.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // zero to avoid information leaks
+    lfs_cache_zero(lfs, &lfs->rcache);
+    lfs_cache_zero(lfs, &lfs->pcache);
+
+    // setup lookahead, must be multiple of 64-bits, 32-bit aligned
+    LFS_ASSERT(lfs->cfg->lookahead_size > 0);
+    LFS_ASSERT(lfs->cfg->lookahead_size % 8 == 0 &&
+            (uintptr_t)lfs->cfg->lookahead_buffer % 4 == 0);
+    if (lfs->cfg->lookahead_buffer) {
+        lfs->free.buffer = lfs->cfg->lookahead_buffer;
+    } else {
+        lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead_size);
+        if (!lfs->free.buffer) {
+            err = LFS_ERR_NOMEM;
+            goto cleanup;
+        }
+    }
+
+    // check that the size limits are sane
+    LFS_ASSERT(lfs->cfg->name_max <= LFS_NAME_MAX);
+    lfs->name_max = lfs->cfg->name_max;
+    if (!lfs->name_max) {
+        lfs->name_max = LFS_NAME_MAX;
+    }
+
+    LFS_ASSERT(lfs->cfg->file_max <= LFS_FILE_MAX);
+    lfs->file_max = lfs->cfg->file_max;
+    if (!lfs->file_max) {
+        lfs->file_max = LFS_FILE_MAX;
+    }
+
+    LFS_ASSERT(lfs->cfg->attr_max <= LFS_ATTR_MAX);
+    lfs->attr_max = lfs->cfg->attr_max;
+    if (!lfs->attr_max) {
+        lfs->attr_max = LFS_ATTR_MAX;
+    }
+
+    // setup default state
+    lfs->root[0] = LFS_BLOCK_NULL;
+    lfs->root[1] = LFS_BLOCK_NULL;
+    lfs->mlist = NULL;
+    lfs->seed = 0;
+    lfs->gstate = (struct lfs_gstate){0};
+    lfs->gpending = (struct lfs_gstate){0};
+    lfs->gdelta = (struct lfs_gstate){0};
+#ifdef LFS_MIGRATE
+    lfs->lfs1 = NULL;
+#endif
+
+    return 0;
+
+cleanup:
+    lfs_deinit(lfs);
+    return err;
+}
+
+static int lfs_deinit(lfs_t *lfs) {
+    // free allocated memory
+    if (!lfs->cfg->read_buffer) {
+        lfs_free(lfs->rcache.buffer);
+    }
+
+    if (!lfs->cfg->prog_buffer) {
+        lfs_free(lfs->pcache.buffer);
+    }
+
+    if (!lfs->cfg->lookahead_buffer) {
+        lfs_free(lfs->free.buffer);
+    }
+
+    return 0;
+}
+
+int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
+    LFS_TRACE("lfs_format(%p, %p {.context=%p, "
+                ".read=%p, .prog=%p, .erase=%p, .sync=%p, "
+                ".read_size=%"PRIu32", .prog_size=%"PRIu32", "
+                ".block_size=%"PRIu32", .block_count=%"PRIu32", "
+                ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", "
+                ".lookahead_size=%"PRIu32", .read_buffer=%p, "
+                ".prog_buffer=%p, .lookahead_buffer=%p, "
+                ".name_max=%"PRIu32", .file_max=%"PRIu32", "
+                ".attr_max=%"PRIu32"})",
+            (void*)lfs, (void*)cfg, cfg->context,
+            (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
+            (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
+            cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
+            cfg->block_cycles, cfg->cache_size, cfg->lookahead_size,
+            cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer,
+            cfg->name_max, cfg->file_max, cfg->attr_max);
+    int err = 0;
+    {
+        err = lfs_init(lfs, cfg);
+        if (err) {
+            LFS_TRACE("lfs_format -> %d", err);
+            return err;
+        }
+
+        // create free lookahead
+        memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size);
+        lfs->free.off = 0;
+        lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size,
+                lfs->cfg->block_count);
+        lfs->free.i = 0;
+        lfs_alloc_ack(lfs);
+
+        // create root dir
+        lfs_mdir_t root;
+        err = lfs_dir_alloc(lfs, &root);
+        if (err) {
+            goto cleanup;
+        }
+
+        // write one superblock
+        lfs_superblock_t superblock = {
+            .version     = LFS_DISK_VERSION,
+            .block_size  = lfs->cfg->block_size,
+            .block_count = lfs->cfg->block_count,
+            .name_max    = lfs->name_max,
+            .file_max    = lfs->file_max,
+            .attr_max    = lfs->attr_max,
+        };
+
+        lfs_superblock_tole32(&superblock);
+        err = lfs_dir_commit(lfs, &root, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL},
+                {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"},
+                {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+                    &superblock}));
+        if (err) {
+            goto cleanup;
+        }
+
+        // sanity check that fetch works
+        err = lfs_dir_fetch(lfs, &root, (const lfs_block_t[2]){0, 1});
+        if (err) {
+            goto cleanup;
+        }
+
+        // force compaction to prevent accidentally mounting any
+        // older version of littlefs that may live on disk
+        root.erased = false;
+        err = lfs_dir_commit(lfs, &root, NULL, 0);
+        if (err) {
+            goto cleanup;
+        }
+    }
+
+cleanup:
+    lfs_deinit(lfs);
+    LFS_TRACE("lfs_format -> %d", err);
+    return err;
+}
+
+int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
+    LFS_TRACE("lfs_mount(%p, %p {.context=%p, "
+                ".read=%p, .prog=%p, .erase=%p, .sync=%p, "
+                ".read_size=%"PRIu32", .prog_size=%"PRIu32", "
+                ".block_size=%"PRIu32", .block_count=%"PRIu32", "
+                ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", "
+                ".lookahead_size=%"PRIu32", .read_buffer=%p, "
+                ".prog_buffer=%p, .lookahead_buffer=%p, "
+                ".name_max=%"PRIu32", .file_max=%"PRIu32", "
+                ".attr_max=%"PRIu32"})",
+            (void*)lfs, (void*)cfg, cfg->context,
+            (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
+            (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
+            cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
+            cfg->block_cycles, cfg->cache_size, cfg->lookahead_size,
+            cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer,
+            cfg->name_max, cfg->file_max, cfg->attr_max);
+    int err = lfs_init(lfs, cfg);
+    if (err) {
+        LFS_TRACE("lfs_mount -> %d", err);
+        return err;
+    }
+
+    // scan directory blocks for superblock and any global updates
+    lfs_mdir_t dir = {.tail = {0, 1}};
+    while (!lfs_pair_isnull(dir.tail)) {
+        // fetch next block in tail list
+        lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail,
+                LFS_MKTAG(0x7ff, 0x3ff, 0),
+                LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8),
+                NULL,
+                lfs_dir_find_match, &(struct lfs_dir_find_match){
+                    lfs, "littlefs", 8});
+        if (tag < 0) {
+            err = tag;
+            goto cleanup;
+        }
+
+        // has superblock?
+        if (tag && !lfs_tag_isdelete(tag)) {
+            // update root
+            lfs->root[0] = dir.pair[0];
+            lfs->root[1] = dir.pair[1];
+
+            // grab superblock
+            lfs_superblock_t superblock;
+            tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+                    &superblock);
+            if (tag < 0) {
+                err = tag;
+                goto cleanup;
+            }
+            lfs_superblock_fromle32(&superblock);
+
+            // check version
+            uint16_t major_version = (0xffff & (superblock.version >> 16));
+            uint16_t minor_version = (0xffff & (superblock.version >>  0));
+            if ((major_version != LFS_DISK_VERSION_MAJOR ||
+                 minor_version > LFS_DISK_VERSION_MINOR)) {
+                LFS_ERROR("Invalid version %"PRIu16".%"PRIu16,
+                        major_version, minor_version);
+                err = LFS_ERR_INVAL;
+                goto cleanup;
+            }
+
+            // check superblock configuration
+            if (superblock.name_max) {
+                if (superblock.name_max > lfs->name_max) {
+                    LFS_ERROR("Unsupported name_max (%"PRIu32" > %"PRIu32")",
+                            superblock.name_max, lfs->name_max);
+                    err = LFS_ERR_INVAL;
+                    goto cleanup;
+                }
+
+                lfs->name_max = superblock.name_max;
+            }
+
+            if (superblock.file_max) {
+                if (superblock.file_max > lfs->file_max) {
+                    LFS_ERROR("Unsupported file_max (%"PRIu32" > %"PRIu32")",
+                            superblock.file_max, lfs->file_max);
+                    err = LFS_ERR_INVAL;
+                    goto cleanup;
+                }
+
+                lfs->file_max = superblock.file_max;
+            }
+
+            if (superblock.attr_max) {
+                if (superblock.attr_max > lfs->attr_max) {
+                    LFS_ERROR("Unsupported attr_max (%"PRIu32" > %"PRIu32")",
+                            superblock.attr_max, lfs->attr_max);
+                    err = LFS_ERR_INVAL;
+                    goto cleanup;
+                }
+
+                lfs->attr_max = superblock.attr_max;
+            }
+        }
+
+        // has gstate?
+        err = lfs_dir_getgstate(lfs, &dir, &lfs->gpending);
+        if (err) {
+            goto cleanup;
+        }
+    }
+
+    // found superblock?
+    if (lfs_pair_isnull(lfs->root)) {
+        err = LFS_ERR_INVAL;
+        goto cleanup;
+    }
+
+    // update littlefs with gstate
+    lfs->gpending.tag += !lfs_tag_isvalid(lfs->gpending.tag);
+    lfs->gstate = lfs->gpending;
+    if (lfs_gstate_hasmove(&lfs->gstate)) {
+        LFS_DEBUG("Found move %"PRIx32" %"PRIx32" %"PRIx16,
+                lfs->gstate.pair[0],
+                lfs->gstate.pair[1],
+                lfs_tag_id(lfs->gstate.tag));
+    }
+
+    // setup free lookahead
+    lfs->free.off = lfs->seed % lfs->cfg->block_size;
+    lfs->free.size = 0;
+    lfs->free.i = 0;
+    lfs_alloc_ack(lfs);
+
+    LFS_TRACE("lfs_mount -> %d", 0);
+    return 0;
+
+cleanup:
+    lfs_unmount(lfs);
+    LFS_TRACE("lfs_mount -> %d", err);
+    return err;
+}
+
+int lfs_unmount(lfs_t *lfs) {
+    LFS_TRACE("lfs_unmount(%p)", (void*)lfs);
+    int err = lfs_deinit(lfs);
+    LFS_TRACE("lfs_unmount -> %d", err);
+    return err;
+}
+
+
+/// Filesystem filesystem operations ///
+int lfs_fs_traverse(lfs_t *lfs,
+        int (*cb)(void *data, lfs_block_t block), void *data) {
+    LFS_TRACE("lfs_fs_traverse(%p, %p, %p)",
+            (void*)lfs, (void*)(uintptr_t)cb, data);
+    // iterate over metadata pairs
+    lfs_mdir_t dir = {.tail = {0, 1}};
+
+#ifdef LFS_MIGRATE
+    // also consider v1 blocks during migration
+    if (lfs->lfs1) {
+        int err = lfs1_traverse(lfs, cb, data);
+        if (err) {
+            LFS_TRACE("lfs_fs_traverse -> %d", err);
+            return err;
+        }
+
+        dir.tail[0] = lfs->root[0];
+        dir.tail[1] = lfs->root[1];
+    }
+#endif
+
+    while (!lfs_pair_isnull(dir.tail)) {
+        for (int i = 0; i < 2; i++) {
+            int err = cb(data, dir.tail[i]);
+            if (err) {
+                LFS_TRACE("lfs_fs_traverse -> %d", err);
+                return err;
+            }
+        }
+
+        // iterate through ids in directory
+        int err = lfs_dir_fetch(lfs, &dir, dir.tail);
+        if (err) {
+            LFS_TRACE("lfs_fs_traverse -> %d", err);
+            return err;
+        }
+
+        for (uint16_t id = 0; id < dir.count; id++) {
+            struct lfs_ctz ctz;
+            lfs_stag_t tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x700, 0x3ff, 0),
+                    LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz);
+            if (tag < 0) {
+                if (tag == LFS_ERR_NOENT) {
+                    continue;
+                }
+                LFS_TRACE("lfs_fs_traverse -> %"PRId32, tag);
+                return tag;
+            }
+            lfs_ctz_fromle32(&ctz);
+
+            if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) {
+                err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache,
+                        ctz.head, ctz.size, cb, data);
+                if (err) {
+                    LFS_TRACE("lfs_fs_traverse -> %d", err);
+                    return err;
+                }
+            }
+        }
+    }
+
+    // iterate over any open files
+    for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
+        if (f->type != LFS_TYPE_REG) {
+            continue;
+        }
+
+        if ((f->flags & LFS_F_DIRTY) && !(f->flags & LFS_F_INLINE)) {
+            int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache,
+                    f->ctz.head, f->ctz.size, cb, data);
+            if (err) {
+                LFS_TRACE("lfs_fs_traverse -> %d", err);
+                return err;
+            }
+        }
+
+        if ((f->flags & LFS_F_WRITING) && !(f->flags & LFS_F_INLINE)) {
+            int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache,
+                    f->block, f->pos, cb, data);
+            if (err) {
+                LFS_TRACE("lfs_fs_traverse -> %d", err);
+                return err;
+            }
+        }
+    }
+
+    LFS_TRACE("lfs_fs_traverse -> %d", 0);
+    return 0;
+}
+
+static int lfs_fs_pred(lfs_t *lfs,
+        const lfs_block_t pair[2], lfs_mdir_t *pdir) {
+    // iterate over all directory directory entries
+    pdir->tail[0] = 0;
+    pdir->tail[1] = 1;
+    while (!lfs_pair_isnull(pdir->tail)) {
+        if (lfs_pair_cmp(pdir->tail, pair) == 0) {
+            return 0;
+        }
+
+        int err = lfs_dir_fetch(lfs, pdir, pdir->tail);
+        if (err) {
+            return err;
+        }
+    }
+
+    return LFS_ERR_NOENT;
+}
+
+struct lfs_fs_parent_match {
+    lfs_t *lfs;
+    const lfs_block_t pair[2];
+};
+
+static int lfs_fs_parent_match(void *data,
+        lfs_tag_t tag, const void *buffer) {
+    struct lfs_fs_parent_match *find = data;
+    lfs_t *lfs = find->lfs;
+    const struct lfs_diskoff *disk = buffer;
+    (void)tag;
+
+    lfs_block_t child[2];
+    int err = lfs_bd_read(lfs,
+            &lfs->pcache, &lfs->rcache, lfs->cfg->block_size,
+            disk->block, disk->off, &child, sizeof(child));
+    if (err) {
+        return err;
+    }
+
+    lfs_pair_fromle32(child);
+    return (lfs_pair_cmp(child, find->pair) == 0) ? LFS_CMP_EQ : LFS_CMP_LT;
+}
+
+static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t pair[2],
+        lfs_mdir_t *parent) {
+    // use fetchmatch with callback to find pairs
+    parent->tail[0] = 0;
+    parent->tail[1] = 1;
+    while (!lfs_pair_isnull(parent->tail)) {
+        lfs_stag_t tag = lfs_dir_fetchmatch(lfs, parent, parent->tail,
+                LFS_MKTAG(0x7ff, 0, 0x3ff),
+                LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 0, 8),
+                NULL,
+                lfs_fs_parent_match, &(struct lfs_fs_parent_match){
+                    lfs, {pair[0], pair[1]}});
+        if (tag && tag != LFS_ERR_NOENT) {
+            return tag;
+        }
+    }
+
+    return LFS_ERR_NOENT;
+}
+
+static int lfs_fs_relocate(lfs_t *lfs,
+        const lfs_block_t oldpair[2], lfs_block_t newpair[2]) {
+    // update internal root
+    if (lfs_pair_cmp(oldpair, lfs->root) == 0) {
+        LFS_DEBUG("Relocating root %"PRIx32" %"PRIx32,
+                newpair[0], newpair[1]);
+        lfs->root[0] = newpair[0];
+        lfs->root[1] = newpair[1];
+    }
+
+    // update internally tracked dirs
+    for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) {
+        if (lfs_pair_cmp(oldpair, d->m.pair) == 0) {
+            d->m.pair[0] = newpair[0];
+            d->m.pair[1] = newpair[1];
+        }
+
+        if (d->type == LFS_TYPE_DIR &&
+                lfs_pair_cmp(oldpair, ((lfs_dir_t*)d)->head) == 0) {
+            ((lfs_dir_t*)d)->head[0] = newpair[0];
+            ((lfs_dir_t*)d)->head[1] = newpair[1];
+        }
+    }
+
+    // find parent
+    lfs_mdir_t parent;
+    lfs_stag_t tag = lfs_fs_parent(lfs, oldpair, &parent);
+    if (tag < 0 && tag != LFS_ERR_NOENT) {
+        return tag;
+    }
+
+    if (tag != LFS_ERR_NOENT) {
+        // update disk, this creates a desync
+        lfs_fs_preporphans(lfs, +1);
+
+        lfs_pair_tole32(newpair);
+        int err = lfs_dir_commit(lfs, &parent, LFS_MKATTRS({tag, newpair}));
+        lfs_pair_fromle32(newpair);
+        if (err) {
+            return err;
+        }
+
+        // next step, clean up orphans
+        lfs_fs_preporphans(lfs, -1);
+    }
+
+    // find pred
+    int err = lfs_fs_pred(lfs, oldpair, &parent);
+    if (err && err != LFS_ERR_NOENT) {
+        return err;
+    }
+
+    // if we can't find dir, it must be new
+    if (err != LFS_ERR_NOENT) {
+        // replace bad pair, either we clean up desync, or no desync occured
+        lfs_pair_tole32(newpair);
+        err = lfs_dir_commit(lfs, &parent, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_TAIL + parent.split, 0x3ff, 8), newpair}));
+        lfs_pair_fromle32(newpair);
+        if (err) {
+            return err;
+        }
+    }
+
+    return 0;
+}
+
+static void lfs_fs_preporphans(lfs_t *lfs, int8_t orphans) {
+    lfs->gpending.tag += orphans;
+    lfs_gstate_xororphans(&lfs->gdelta,   &lfs->gpending,
+            lfs_gstate_hasorphans(&lfs->gpending));
+    lfs_gstate_xororphans(&lfs->gpending, &lfs->gpending,
+            lfs_gstate_hasorphans(&lfs->gpending));
+}
+
+static void lfs_fs_prepmove(lfs_t *lfs,
+        uint16_t id, const lfs_block_t pair[2]) {
+    lfs_gstate_xormove(&lfs->gdelta,   &lfs->gpending, id, pair);
+    lfs_gstate_xormove(&lfs->gpending, &lfs->gpending, id, pair);
+}
+
+
+static int lfs_fs_demove(lfs_t *lfs) {
+    if (!lfs_gstate_hasmove(&lfs->gstate)) {
+        return 0;
+    }
+
+    // Fix bad moves
+    LFS_DEBUG("Fixing move %"PRIx32" %"PRIx32" %"PRIx16,
+            lfs->gstate.pair[0],
+            lfs->gstate.pair[1],
+            lfs_tag_id(lfs->gstate.tag));
+
+    // fetch and delete the moved entry
+    lfs_mdir_t movedir;
+    int err = lfs_dir_fetch(lfs, &movedir, lfs->gstate.pair);
+    if (err) {
+        return err;
+    }
+
+    // rely on cancel logic inside commit
+    err = lfs_dir_commit(lfs, &movedir, NULL, 0);
+    if (err) {
+        return err;
+    }
+
+    return 0;
+}
+
+static int lfs_fs_deorphan(lfs_t *lfs) {
+    if (!lfs_gstate_hasorphans(&lfs->gstate)) {
+        return 0;
+    }
+
+    // Fix any orphans
+    lfs_mdir_t pdir = {.split = true};
+    lfs_mdir_t dir = {.tail = {0, 1}};
+
+    // iterate over all directory directory entries
+    while (!lfs_pair_isnull(dir.tail)) {
+        int err = lfs_dir_fetch(lfs, &dir, dir.tail);
+        if (err) {
+            return err;
+        }
+
+        // check head blocks for orphans
+        if (!pdir.split) {
+            // check if we have a parent
+            lfs_mdir_t parent;
+            lfs_stag_t tag = lfs_fs_parent(lfs, pdir.tail, &parent);
+            if (tag < 0 && tag != LFS_ERR_NOENT) {
+                return tag;
+            }
+
+            if (tag == LFS_ERR_NOENT) {
+                // we are an orphan
+                LFS_DEBUG("Fixing orphan %"PRIx32" %"PRIx32,
+                        pdir.tail[0], pdir.tail[1]);
+
+                err = lfs_dir_drop(lfs, &pdir, &dir);
+                if (err) {
+                    return err;
+                }
+
+                break;
+            }
+
+            lfs_block_t pair[2];
+            lfs_stag_t res = lfs_dir_get(lfs, &parent,
+                    LFS_MKTAG(0x7ff, 0x3ff, 0), tag, pair);
+            if (res < 0) {
+                return res;
+            }
+            lfs_pair_fromle32(pair);
+
+            if (!lfs_pair_sync(pair, pdir.tail)) {
+                // we have desynced
+                LFS_DEBUG("Fixing half-orphan %"PRIx32" %"PRIx32,
+                        pair[0], pair[1]);
+
+                lfs_pair_tole32(pair);
+                err = lfs_dir_commit(lfs, &pdir, LFS_MKATTRS(
+                        {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pair}));
+                lfs_pair_fromle32(pair);
+                if (err) {
+                    return err;
+                }
+
+                break;
+            }
+        }
+
+        memcpy(&pdir, &dir, sizeof(pdir));
+    }
+
+    // mark orphans as fixed
+    lfs_fs_preporphans(lfs, -lfs_gstate_getorphans(&lfs->gstate));
+    lfs->gstate = lfs->gpending;
+    return 0;
+}
+
+static int lfs_fs_forceconsistency(lfs_t *lfs) {
+    int err = lfs_fs_demove(lfs);
+    if (err) {
+        return err;
+    }
+
+    err = lfs_fs_deorphan(lfs);
+    if (err) {
+        return err;
+    }
+
+    return 0;
+}
+
+static int lfs_fs_size_count(void *p, lfs_block_t block) {
+    (void)block;
+    lfs_size_t *size = p;
+    *size += 1;
+    return 0;
+}
+
+lfs_ssize_t lfs_fs_size(lfs_t *lfs) {
+    LFS_TRACE("lfs_fs_size(%p)", (void*)lfs);
+    lfs_size_t size = 0;
+    int err = lfs_fs_traverse(lfs, lfs_fs_size_count, &size);
+    if (err) {
+        LFS_TRACE("lfs_fs_size -> %d", err);
+        return err;
+    }
+
+    LFS_TRACE("lfs_fs_size -> %d", err);
+    return size;
+}
+
+#ifdef LFS_MIGRATE
+////// Migration from littelfs v1 below this //////
+
+/// Version info ///
+
+// Software library version
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS1_VERSION 0x00010007
+#define LFS1_VERSION_MAJOR (0xffff & (LFS1_VERSION >> 16))
+#define LFS1_VERSION_MINOR (0xffff & (LFS1_VERSION >>  0))
+
+// Version of On-disk data structures
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS1_DISK_VERSION 0x00010001
+#define LFS1_DISK_VERSION_MAJOR (0xffff & (LFS1_DISK_VERSION >> 16))
+#define LFS1_DISK_VERSION_MINOR (0xffff & (LFS1_DISK_VERSION >>  0))
+
+
+/// v1 Definitions ///
+
+// File types
+enum lfs1_type {
+    LFS1_TYPE_REG        = 0x11,
+    LFS1_TYPE_DIR        = 0x22,
+    LFS1_TYPE_SUPERBLOCK = 0x2e,
+};
+
+typedef struct lfs1 {
+    lfs_block_t root[2];
+} lfs1_t;
+
+typedef struct lfs1_entry {
+    lfs_off_t off;
+
+    struct lfs1_disk_entry {
+        uint8_t type;
+        uint8_t elen;
+        uint8_t alen;
+        uint8_t nlen;
+        union {
+            struct {
+                lfs_block_t head;
+                lfs_size_t size;
+            } file;
+            lfs_block_t dir[2];
+        } u;
+    } d;
+} lfs1_entry_t;
+
+typedef struct lfs1_dir {
+    struct lfs1_dir *next;
+    lfs_block_t pair[2];
+    lfs_off_t off;
+
+    lfs_block_t head[2];
+    lfs_off_t pos;
+
+    struct lfs1_disk_dir {
+        uint32_t rev;
+        lfs_size_t size;
+        lfs_block_t tail[2];
+    } d;
+} lfs1_dir_t;
+
+typedef struct lfs1_superblock {
+    lfs_off_t off;
+
+    struct lfs1_disk_superblock {
+        uint8_t type;
+        uint8_t elen;
+        uint8_t alen;
+        uint8_t nlen;
+        lfs_block_t root[2];
+        uint32_t block_size;
+        uint32_t block_count;
+        uint32_t version;
+        char magic[8];
+    } d;
+} lfs1_superblock_t;
+
+
+/// Low-level wrappers v1->v2 ///
+static void lfs1_crc(uint32_t *crc, const void *buffer, size_t size) {
+    *crc = lfs_crc(*crc, buffer, size);
+}
+
+static int lfs1_bd_read(lfs_t *lfs, lfs_block_t block,
+        lfs_off_t off, void *buffer, lfs_size_t size) {
+    // if we ever do more than writes to alternating pairs,
+    // this may need to consider pcache
+    return lfs_bd_read(lfs, &lfs->pcache, &lfs->rcache, size,
+            block, off, buffer, size);
+}
+
+static int lfs1_bd_crc(lfs_t *lfs, lfs_block_t block,
+        lfs_off_t off, lfs_size_t size, uint32_t *crc) {
+    for (lfs_off_t i = 0; i < size; i++) {
+        uint8_t c;
+        int err = lfs1_bd_read(lfs, block, off+i, &c, 1);
+        if (err) {
+            return err;
+        }
+
+        lfs1_crc(crc, &c, 1);
+    }
+
+    return 0;
+}
+
+
+/// Endian swapping functions ///
+static void lfs1_dir_fromle32(struct lfs1_disk_dir *d) {
+    d->rev     = lfs_fromle32(d->rev);
+    d->size    = lfs_fromle32(d->size);
+    d->tail[0] = lfs_fromle32(d->tail[0]);
+    d->tail[1] = lfs_fromle32(d->tail[1]);
+}
+
+static void lfs1_dir_tole32(struct lfs1_disk_dir *d) {
+    d->rev     = lfs_tole32(d->rev);
+    d->size    = lfs_tole32(d->size);
+    d->tail[0] = lfs_tole32(d->tail[0]);
+    d->tail[1] = lfs_tole32(d->tail[1]);
+}
+
+static void lfs1_entry_fromle32(struct lfs1_disk_entry *d) {
+    d->u.dir[0] = lfs_fromle32(d->u.dir[0]);
+    d->u.dir[1] = lfs_fromle32(d->u.dir[1]);
+}
+
+static void lfs1_entry_tole32(struct lfs1_disk_entry *d) {
+    d->u.dir[0] = lfs_tole32(d->u.dir[0]);
+    d->u.dir[1] = lfs_tole32(d->u.dir[1]);
+}
+
+static void lfs1_superblock_fromle32(struct lfs1_disk_superblock *d) {
+    d->root[0]     = lfs_fromle32(d->root[0]);
+    d->root[1]     = lfs_fromle32(d->root[1]);
+    d->block_size  = lfs_fromle32(d->block_size);
+    d->block_count = lfs_fromle32(d->block_count);
+    d->version     = lfs_fromle32(d->version);
+}
+
+
+///// Metadata pair and directory operations ///
+static inline lfs_size_t lfs1_entry_size(const lfs1_entry_t *entry) {
+    return 4 + entry->d.elen + entry->d.alen + entry->d.nlen;
+}
+
+static int lfs1_dir_fetch(lfs_t *lfs,
+        lfs1_dir_t *dir, const lfs_block_t pair[2]) {
+    // copy out pair, otherwise may be aliasing dir
+    const lfs_block_t tpair[2] = {pair[0], pair[1]};
+    bool valid = false;
+
+    // check both blocks for the most recent revision
+    for (int i = 0; i < 2; i++) {
+        struct lfs1_disk_dir test;
+        int err = lfs1_bd_read(lfs, tpair[i], 0, &test, sizeof(test));
+        lfs1_dir_fromle32(&test);
+        if (err) {
+            if (err == LFS_ERR_CORRUPT) {
+                continue;
+            }
+            return err;
+        }
+
+        if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) {
+            continue;
+        }
+
+        if ((0x7fffffff & test.size) < sizeof(test)+4 ||
+            (0x7fffffff & test.size) > lfs->cfg->block_size) {
+            continue;
+        }
+
+        uint32_t crc = LFS_BLOCK_NULL;
+        lfs1_dir_tole32(&test);
+        lfs1_crc(&crc, &test, sizeof(test));
+        lfs1_dir_fromle32(&test);
+        err = lfs1_bd_crc(lfs, tpair[i], sizeof(test),
+                (0x7fffffff & test.size) - sizeof(test), &crc);
+        if (err) {
+            if (err == LFS_ERR_CORRUPT) {
+                continue;
+            }
+            return err;
+        }
+
+        if (crc != 0) {
+            continue;
+        }
+
+        valid = true;
+
+        // setup dir in case it's valid
+        dir->pair[0] = tpair[(i+0) % 2];
+        dir->pair[1] = tpair[(i+1) % 2];
+        dir->off = sizeof(dir->d);
+        dir->d = test;
+    }
+
+    if (!valid) {
+        LFS_ERROR("Corrupted dir pair at %" PRIx32 " %" PRIx32 ,
+                tpair[0], tpair[1]);
+        return LFS_ERR_CORRUPT;
+    }
+
+    return 0;
+}
+
+static int lfs1_dir_next(lfs_t *lfs, lfs1_dir_t *dir, lfs1_entry_t *entry) {
+    while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) {
+        if (!(0x80000000 & dir->d.size)) {
+            entry->off = dir->off;
+            return LFS_ERR_NOENT;
+        }
+
+        int err = lfs1_dir_fetch(lfs, dir, dir->d.tail);
+        if (err) {
+            return err;
+        }
+
+        dir->off = sizeof(dir->d);
+        dir->pos += sizeof(dir->d) + 4;
+    }
+
+    int err = lfs1_bd_read(lfs, dir->pair[0], dir->off,
+            &entry->d, sizeof(entry->d));
+    lfs1_entry_fromle32(&entry->d);
+    if (err) {
+        return err;
+    }
+
+    entry->off = dir->off;
+    dir->off += lfs1_entry_size(entry);
+    dir->pos += lfs1_entry_size(entry);
+    return 0;
+}
+
+/// littlefs v1 specific operations ///
+int lfs1_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
+    if (lfs_pair_isnull(lfs->lfs1->root)) {
+        return 0;
+    }
+
+    // iterate over metadata pairs
+    lfs1_dir_t dir;
+    lfs1_entry_t entry;
+    lfs_block_t cwd[2] = {0, 1};
+
+    while (true) {
+        for (int i = 0; i < 2; i++) {
+            int err = cb(data, cwd[i]);
+            if (err) {
+                return err;
+            }
+        }
+
+        int err = lfs1_dir_fetch(lfs, &dir, cwd);
+        if (err) {
+            return err;
+        }
+
+        // iterate over contents
+        while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) {
+            err = lfs1_bd_read(lfs, dir.pair[0], dir.off,
+                    &entry.d, sizeof(entry.d));
+            lfs1_entry_fromle32(&entry.d);
+            if (err) {
+                return err;
+            }
+
+            dir.off += lfs1_entry_size(&entry);
+            if ((0x70 & entry.d.type) == (0x70 & LFS1_TYPE_REG)) {
+                err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache,
+                        entry.d.u.file.head, entry.d.u.file.size, cb, data);
+                if (err) {
+                    return err;
+                }
+            }
+        }
+
+        // we also need to check if we contain a threaded v2 directory
+        lfs_mdir_t dir2 = {.split=true, .tail={cwd[0], cwd[1]}};
+        while (dir2.split) {
+            err = lfs_dir_fetch(lfs, &dir2, dir2.tail);
+            if (err) {
+                break;
+            }
+
+            for (int i = 0; i < 2; i++) {
+                err = cb(data, dir2.pair[i]);
+                if (err) {
+                    return err;
+                }
+            }
+        }
+
+        cwd[0] = dir.d.tail[0];
+        cwd[1] = dir.d.tail[1];
+
+        if (lfs_pair_isnull(cwd)) {
+            break;
+        }
+    }
+
+    return 0;
+}
+
+static int lfs1_moved(lfs_t *lfs, const void *e) {
+    if (lfs_pair_isnull(lfs->lfs1->root)) {
+        return 0;
+    }
+
+    // skip superblock
+    lfs1_dir_t cwd;
+    int err = lfs1_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1});
+    if (err) {
+        return err;
+    }
+
+    // iterate over all directory directory entries
+    lfs1_entry_t entry;
+    while (!lfs_pair_isnull(cwd.d.tail)) {
+        err = lfs1_dir_fetch(lfs, &cwd, cwd.d.tail);
+        if (err) {
+            return err;
+        }
+
+        while (true) {
+            err = lfs1_dir_next(lfs, &cwd, &entry);
+            if (err && err != LFS_ERR_NOENT) {
+                return err;
+            }
+
+            if (err == LFS_ERR_NOENT) {
+                break;
+            }
+
+            if (!(0x80 & entry.d.type) &&
+                 memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+/// Filesystem operations ///
+static int lfs1_mount(lfs_t *lfs, struct lfs1 *lfs1,
+        const struct lfs_config *cfg) {
+    int err = 0;
+    {
+        err = lfs_init(lfs, cfg);
+        if (err) {
+            return err;
+        }
+
+        lfs->lfs1 = lfs1;
+        lfs->lfs1->root[0] = LFS_BLOCK_NULL;
+        lfs->lfs1->root[1] = LFS_BLOCK_NULL;
+
+        // setup free lookahead
+        lfs->free.off = 0;
+        lfs->free.size = 0;
+        lfs->free.i = 0;
+        lfs_alloc_ack(lfs);
+
+        // load superblock
+        lfs1_dir_t dir;
+        lfs1_superblock_t superblock;
+        err = lfs1_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1});
+        if (err && err != LFS_ERR_CORRUPT) {
+            goto cleanup;
+        }
+
+        if (!err) {
+            err = lfs1_bd_read(lfs, dir.pair[0], sizeof(dir.d),
+                    &superblock.d, sizeof(superblock.d));
+            lfs1_superblock_fromle32(&superblock.d);
+            if (err) {
+                goto cleanup;
+            }
+
+            lfs->lfs1->root[0] = superblock.d.root[0];
+            lfs->lfs1->root[1] = superblock.d.root[1];
+        }
+
+        if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) {
+            LFS_ERROR("Invalid superblock at %d %d", 0, 1);
+            err = LFS_ERR_CORRUPT;
+            goto cleanup;
+        }
+
+        uint16_t major_version = (0xffff & (superblock.d.version >> 16));
+        uint16_t minor_version = (0xffff & (superblock.d.version >>  0));
+        if ((major_version != LFS1_DISK_VERSION_MAJOR ||
+             minor_version > LFS1_DISK_VERSION_MINOR)) {
+            LFS_ERROR("Invalid version %d.%d", major_version, minor_version);
+            err = LFS_ERR_INVAL;
+            goto cleanup;
+        }
+
+        return 0;
+    }
+
+cleanup:
+    lfs_deinit(lfs);
+    return err;
+}
+
+static int lfs1_unmount(lfs_t *lfs) {
+    return lfs_deinit(lfs);
+}
+
+/// v1 migration ///
+int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg) {
+    LFS_TRACE("lfs_migrate(%p, %p {.context=%p, "
+                ".read=%p, .prog=%p, .erase=%p, .sync=%p, "
+                ".read_size=%"PRIu32", .prog_size=%"PRIu32", "
+                ".block_size=%"PRIu32", .block_count=%"PRIu32", "
+                ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", "
+                ".lookahead_size=%"PRIu32", .read_buffer=%p, "
+                ".prog_buffer=%p, .lookahead_buffer=%p, "
+                ".name_max=%"PRIu32", .file_max=%"PRIu32", "
+                ".attr_max=%"PRIu32"})",
+            (void*)lfs, (void*)cfg, cfg->context,
+            (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
+            (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
+            cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
+            cfg->block_cycles, cfg->cache_size, cfg->lookahead_size,
+            cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer,
+            cfg->name_max, cfg->file_max, cfg->attr_max);
+    struct lfs1 lfs1;
+    int err = lfs1_mount(lfs, &lfs1, cfg);
+    if (err) {
+        LFS_TRACE("lfs_migrate -> %d", err);
+        return err;
+    }
+
+    {
+        // iterate through each directory, copying over entries
+        // into new directory
+        lfs1_dir_t dir1;
+        lfs_mdir_t dir2;
+        dir1.d.tail[0] = lfs->lfs1->root[0];
+        dir1.d.tail[1] = lfs->lfs1->root[1];
+        while (!lfs_pair_isnull(dir1.d.tail)) {
+            // iterate old dir
+            err = lfs1_dir_fetch(lfs, &dir1, dir1.d.tail);
+            if (err) {
+                goto cleanup;
+            }
+
+            // create new dir and bind as temporary pretend root
+            err = lfs_dir_alloc(lfs, &dir2);
+            if (err) {
+                goto cleanup;
+            }
+
+            dir2.rev = dir1.d.rev;
+            dir1.head[0] = dir1.pair[0];
+            dir1.head[1] = dir1.pair[1];
+            lfs->root[0] = dir2.pair[0];
+            lfs->root[1] = dir2.pair[1];
+
+            err = lfs_dir_commit(lfs, &dir2, NULL, 0);
+            if (err) {
+                goto cleanup;
+            }
+
+            while (true) {
+                lfs1_entry_t entry1;
+                err = lfs1_dir_next(lfs, &dir1, &entry1);
+                if (err && err != LFS_ERR_NOENT) {
+                    goto cleanup;
+                }
+
+                if (err == LFS_ERR_NOENT) {
+                    break;
+                }
+
+                // check that entry has not been moved
+                if (entry1.d.type & 0x80) {
+                    int moved = lfs1_moved(lfs, &entry1.d.u);
+                    if (moved < 0) {
+                        err = moved;
+                        goto cleanup;
+                    }
+
+                    if (moved) {
+                        continue;
+                    }
+
+                    entry1.d.type &= ~0x80;
+                }
+
+                // also fetch name
+                char name[LFS_NAME_MAX+1];
+                memset(name, 0, sizeof(name));
+                err = lfs1_bd_read(lfs, dir1.pair[0],
+                        entry1.off + 4+entry1.d.elen+entry1.d.alen,
+                        name, entry1.d.nlen);
+                if (err) {
+                    goto cleanup;
+                }
+
+                bool isdir = (entry1.d.type == LFS1_TYPE_DIR);
+
+                // create entry in new dir
+                err = lfs_dir_fetch(lfs, &dir2, lfs->root);
+                if (err) {
+                    goto cleanup;
+                }
+
+                uint16_t id;
+                err = lfs_dir_find(lfs, &dir2, &(const char*){name}, &id);
+                if (!(err == LFS_ERR_NOENT && id != 0x3ff)) {
+                    err = (err < 0) ? err : LFS_ERR_EXIST;
+                    goto cleanup;
+                }
+
+                lfs1_entry_tole32(&entry1.d);
+                err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS(
+                        {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL},
+                        {LFS_MKTAG(
+                            isdir ? LFS_TYPE_DIR : LFS_TYPE_REG,
+                            id, entry1.d.nlen), name},
+                        {LFS_MKTAG(
+                            isdir ? LFS_TYPE_DIRSTRUCT : LFS_TYPE_CTZSTRUCT,
+                            id, sizeof(entry1.d.u)), &entry1.d.u}));
+                lfs1_entry_fromle32(&entry1.d);
+                if (err) {
+                    goto cleanup;
+                }
+            }
+
+            if (!lfs_pair_isnull(dir1.d.tail)) {
+                // find last block and update tail to thread into fs
+                err = lfs_dir_fetch(lfs, &dir2, lfs->root);
+                if (err) {
+                    goto cleanup;
+                }
+
+                while (dir2.split) {
+                    err = lfs_dir_fetch(lfs, &dir2, dir2.tail);
+                    if (err) {
+                        goto cleanup;
+                    }
+                }
+
+                lfs_pair_tole32(dir2.pair);
+                err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS(
+                        {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8),
+                            dir1.d.tail}));
+                lfs_pair_fromle32(dir2.pair);
+                if (err) {
+                    goto cleanup;
+                }
+            }
+
+            // Copy over first block to thread into fs. Unfortunately
+            // if this fails there is not much we can do.
+            LFS_DEBUG("Migrating %"PRIx32" %"PRIx32" -> %"PRIx32" %"PRIx32,
+                    lfs->root[0], lfs->root[1], dir1.head[0], dir1.head[1]);
+
+            err = lfs_bd_erase(lfs, dir1.head[1]);
+            if (err) {
+                goto cleanup;
+            }
+
+            err = lfs_dir_fetch(lfs, &dir2, lfs->root);
+            if (err) {
+                goto cleanup;
+            }
+
+            for (lfs_off_t i = 0; i < dir2.off; i++) {
+                uint8_t dat;
+                err = lfs_bd_read(lfs,
+                        NULL, &lfs->rcache, dir2.off,
+                        dir2.pair[0], i, &dat, 1);
+                if (err) {
+                    goto cleanup;
+                }
+
+                err = lfs_bd_prog(lfs,
+                        &lfs->pcache, &lfs->rcache, true,
+                        dir1.head[1], i, &dat, 1);
+                if (err) {
+                    goto cleanup;
+                }
+            }
+
+            err = lfs_bd_flush(lfs, &lfs->pcache, &lfs->rcache, true);
+            if (err) {
+                goto cleanup;
+            }
+        }
+
+        // Create new superblock. This marks a successful migration!
+        err = lfs1_dir_fetch(lfs, &dir1, (const lfs_block_t[2]){0, 1});
+        if (err) {
+            goto cleanup;
+        }
+
+        dir2.pair[0] = dir1.pair[0];
+        dir2.pair[1] = dir1.pair[1];
+        dir2.rev = dir1.d.rev;
+        dir2.off = sizeof(dir2.rev);
+        dir2.etag = LFS_BLOCK_NULL;
+        dir2.count = 0;
+        dir2.tail[0] = lfs->lfs1->root[0];
+        dir2.tail[1] = lfs->lfs1->root[1];
+        dir2.erased = false;
+        dir2.split = true;
+
+        lfs_superblock_t superblock = {
+            .version     = LFS_DISK_VERSION,
+            .block_size  = lfs->cfg->block_size,
+            .block_count = lfs->cfg->block_count,
+            .name_max    = lfs->name_max,
+            .file_max    = lfs->file_max,
+            .attr_max    = lfs->attr_max,
+        };
+
+        lfs_superblock_tole32(&superblock);
+        err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS(
+                {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL},
+                {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"},
+                {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+                    &superblock}));
+        if (err) {
+            goto cleanup;
+        }
+
+        // sanity check that fetch works
+        err = lfs_dir_fetch(lfs, &dir2, (const lfs_block_t[2]){0, 1});
+        if (err) {
+            goto cleanup;
+        }
+
+        // force compaction to prevent accidentally mounting v1
+        dir2.erased = false;
+        err = lfs_dir_commit(lfs, &dir2, NULL, 0);
+        if (err) {
+            goto cleanup;
+        }
+    }
+
+cleanup:
+    lfs1_unmount(lfs);
+    LFS_TRACE("lfs_migrate -> %d", err);
+    return err;
+}
+
+#endif

+ 651 - 0
bsp/air302/mklfs/lfs/lfs.h

@@ -0,0 +1,651 @@
+/*
+ * The little filesystem
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#ifndef LFS_H
+#define LFS_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+/// Version info ///
+
+// Software library version
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS_VERSION 0x00020001
+#define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16))
+#define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >>  0))
+
+// Version of On-disk data structures
+// Major (top-nibble), incremented on backwards incompatible changes
+// Minor (bottom-nibble), incremented on feature additions
+#define LFS_DISK_VERSION 0x00020000
+#define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16))
+#define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >>  0))
+
+
+/// Definitions ///
+
+// Type definitions
+typedef uint32_t lfs_size_t;
+typedef uint32_t lfs_off_t;
+
+typedef int32_t  lfs_ssize_t;
+typedef int32_t  lfs_soff_t;
+
+typedef uint32_t lfs_block_t;
+
+// Maximum name size in bytes, may be redefined to reduce the size of the
+// info struct. Limited to <= 1022. Stored in superblock and must be
+// respected by other littlefs drivers.
+#ifndef LFS_NAME_MAX
+#define LFS_NAME_MAX 255
+#endif
+
+// Maximum size of a file in bytes, may be redefined to limit to support other
+// drivers. Limited on disk to <= 4294967296. However, above 2147483647 the
+// functions lfs_file_seek, lfs_file_size, and lfs_file_tell will return
+// incorrect values due to using signed integers. Stored in superblock and
+// must be respected by other littlefs drivers.
+#ifndef LFS_FILE_MAX
+#define LFS_FILE_MAX 2147483647
+#endif
+
+// Maximum size of custom attributes in bytes, may be redefined, but there is
+// no real benefit to using a smaller LFS_ATTR_MAX. Limited to <= 1022.
+#ifndef LFS_ATTR_MAX
+#define LFS_ATTR_MAX 1022
+#endif
+
+// Possible error codes, these are negative to allow
+// valid positive return values
+enum lfs_error {
+    LFS_ERR_OK          = 0,    // No error
+    LFS_ERR_IO          = -5,   // Error during device operation
+    LFS_ERR_CORRUPT     = -84,  // Corrupted
+    LFS_ERR_NOENT       = -2,   // No directory entry
+    LFS_ERR_EXIST       = -17,  // Entry already exists
+    LFS_ERR_NOTDIR      = -20,  // Entry is not a dir
+    LFS_ERR_ISDIR       = -21,  // Entry is a dir
+    LFS_ERR_NOTEMPTY    = -39,  // Dir is not empty
+    LFS_ERR_BADF        = -9,   // Bad file number
+    LFS_ERR_FBIG        = -27,  // File too large
+    LFS_ERR_INVAL       = -22,  // Invalid parameter
+    LFS_ERR_NOSPC       = -28,  // No space left on device
+    LFS_ERR_NOMEM       = -12,  // No more memory available
+    LFS_ERR_NOATTR      = -61,  // No data/attr available
+    LFS_ERR_NAMETOOLONG = -36,  // File name too long
+};
+
+// File types
+enum lfs_type {
+    // file types
+    LFS_TYPE_REG            = 0x001,
+    LFS_TYPE_DIR            = 0x002,
+
+    // internally used types
+    LFS_TYPE_SPLICE         = 0x400,
+    LFS_TYPE_NAME           = 0x000,
+    LFS_TYPE_STRUCT         = 0x200,
+    LFS_TYPE_USERATTR       = 0x300,
+    LFS_TYPE_FROM           = 0x100,
+    LFS_TYPE_TAIL           = 0x600,
+    LFS_TYPE_GLOBALS        = 0x700,
+    LFS_TYPE_CRC            = 0x500,
+
+    // internally used type specializations
+    LFS_TYPE_CREATE         = 0x401,
+    LFS_TYPE_DELETE         = 0x4ff,
+    LFS_TYPE_SUPERBLOCK     = 0x0ff,
+    LFS_TYPE_DIRSTRUCT      = 0x200,
+    LFS_TYPE_CTZSTRUCT      = 0x202,
+    LFS_TYPE_INLINESTRUCT   = 0x201,
+    LFS_TYPE_SOFTTAIL       = 0x600,
+    LFS_TYPE_HARDTAIL       = 0x601,
+    LFS_TYPE_MOVESTATE      = 0x7ff,
+
+    // internal chip sources
+    LFS_FROM_NOOP           = 0x000,
+    LFS_FROM_MOVE           = 0x101,
+    LFS_FROM_USERATTRS      = 0x102,
+};
+
+// File open flags
+enum lfs_open_flags {
+    // open flags
+    LFS_O_RDONLY = 1,         // Open a file as read only
+    LFS_O_WRONLY = 2,         // Open a file as write only
+    LFS_O_RDWR   = 3,         // Open a file as read and write
+    LFS_O_CREAT  = 0x0100,    // Create a file if it does not exist
+    LFS_O_EXCL   = 0x0200,    // Fail if a file already exists
+    LFS_O_TRUNC  = 0x0400,    // Truncate the existing file to zero size
+    LFS_O_APPEND = 0x0800,    // Move to end of file on every write
+
+    // internally used flags
+    LFS_F_DIRTY   = 0x010000, // File does not match storage
+    LFS_F_WRITING = 0x020000, // File has been written since last flush
+    LFS_F_READING = 0x040000, // File has been read since last flush
+    LFS_F_ERRED   = 0x080000, // An error occured during write
+    LFS_F_INLINE  = 0x100000, // Currently inlined in directory entry
+    LFS_F_OPENED  = 0x200000, // File has been opened
+};
+
+// File seek flags
+enum lfs_whence_flags {
+    LFS_SEEK_SET = 0,   // Seek relative to an absolute position
+    LFS_SEEK_CUR = 1,   // Seek relative to the current file position
+    LFS_SEEK_END = 2,   // Seek relative to the end of the file
+};
+
+
+// Configuration provided during initialization of the littlefs
+struct lfs_config {
+    // Opaque user provided context that can be used to pass
+    // information to the block device operations
+    void *context;
+
+    // Read a region in a block. Negative error codes are propogated
+    // to the user.
+    int (*read)(const struct lfs_config *c, lfs_block_t block,
+            lfs_off_t off, void *buffer, lfs_size_t size);
+
+    // Program a region in a block. The block must have previously
+    // been erased. Negative error codes are propogated to the user.
+    // May return LFS_ERR_CORRUPT if the block should be considered bad.
+    int (*prog)(const struct lfs_config *c, lfs_block_t block,
+            lfs_off_t off, const void *buffer, lfs_size_t size);
+
+    // Erase a block. A block must be erased before being programmed.
+    // The state of an erased block is undefined. Negative error codes
+    // are propogated to the user.
+    // May return LFS_ERR_CORRUPT if the block should be considered bad.
+    int (*erase)(const struct lfs_config *c, lfs_block_t block);
+
+    // Sync the state of the underlying block device. Negative error codes
+    // are propogated to the user.
+    int (*sync)(const struct lfs_config *c);
+
+    // Minimum size of a block read. All read operations will be a
+    // multiple of this value.
+    lfs_size_t read_size;
+
+    // Minimum size of a block program. All program operations will be a
+    // multiple of this value.
+    lfs_size_t prog_size;
+
+    // Size of an erasable block. This does not impact ram consumption and
+    // may be larger than the physical erase size. However, non-inlined files
+    // take up at minimum one block. Must be a multiple of the read
+    // and program sizes.
+    lfs_size_t block_size;
+
+    // Number of erasable blocks on the device.
+    lfs_size_t block_count;
+
+    // Number of erase cycles before littlefs evicts metadata logs and moves 
+    // the metadata to another block. Suggested values are in the
+    // range 100-1000, with large values having better performance at the cost
+    // of less consistent wear distribution.
+    //
+    // Set to -1 to disable block-level wear-leveling.
+    int32_t block_cycles;
+
+    // Size of block caches. Each cache buffers a portion of a block in RAM.
+    // The littlefs needs a read cache, a program cache, and one additional
+    // cache per file. Larger caches can improve performance by storing more
+    // data and reducing the number of disk accesses. Must be a multiple of
+    // the read and program sizes, and a factor of the block size.
+    lfs_size_t cache_size;
+
+    // Size of the lookahead buffer in bytes. A larger lookahead buffer
+    // increases the number of blocks found during an allocation pass. The
+    // lookahead buffer is stored as a compact bitmap, so each byte of RAM
+    // can track 8 blocks. Must be a multiple of 8.
+    lfs_size_t lookahead_size;
+
+    // Optional statically allocated read buffer. Must be cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
+    void *read_buffer;
+
+    // Optional statically allocated program buffer. Must be cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
+    void *prog_buffer;
+
+    // Optional statically allocated lookahead buffer. Must be lookahead_size
+    // and aligned to a 32-bit boundary. By default lfs_malloc is used to
+    // allocate this buffer.
+    void *lookahead_buffer;
+
+    // Optional upper limit on length of file names in bytes. No downside for
+    // larger names except the size of the info struct which is controlled by
+    // the LFS_NAME_MAX define. Defaults to LFS_NAME_MAX when zero. Stored in
+    // superblock and must be respected by other littlefs drivers.
+    lfs_size_t name_max;
+
+    // Optional upper limit on files in bytes. No downside for larger files
+    // but must be <= LFS_FILE_MAX. Defaults to LFS_FILE_MAX when zero. Stored
+    // in superblock and must be respected by other littlefs drivers.
+    lfs_size_t file_max;
+
+    // Optional upper limit on custom attributes in bytes. No downside for
+    // larger attributes size but must be <= LFS_ATTR_MAX. Defaults to
+    // LFS_ATTR_MAX when zero.
+    lfs_size_t attr_max;
+};
+
+// File info structure
+struct lfs_info {
+    // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR
+    uint8_t type;
+
+    // Size of the file, only valid for REG files. Limited to 32-bits.
+    lfs_size_t size;
+
+    // Name of the file stored as a null-terminated string. Limited to
+    // LFS_NAME_MAX+1, which can be changed by redefining LFS_NAME_MAX to
+    // reduce RAM. LFS_NAME_MAX is stored in superblock and must be
+    // respected by other littlefs drivers.
+    char name[LFS_NAME_MAX+1];
+};
+
+// Custom attribute structure, used to describe custom attributes
+// committed atomically during file writes.
+struct lfs_attr {
+    // 8-bit type of attribute, provided by user and used to
+    // identify the attribute
+    uint8_t type;
+
+    // Pointer to buffer containing the attribute
+    void *buffer;
+
+    // Size of attribute in bytes, limited to LFS_ATTR_MAX
+    lfs_size_t size;
+};
+
+// Optional configuration provided during lfs_file_opencfg
+struct lfs_file_config {
+    // Optional statically allocated file buffer. Must be cache_size.
+    // By default lfs_malloc is used to allocate this buffer.
+    void *buffer;
+
+    // Optional list of custom attributes related to the file. If the file
+    // is opened with read access, these attributes will be read from disk
+    // during the open call. If the file is opened with write access, the
+    // attributes will be written to disk every file sync or close. This
+    // write occurs atomically with update to the file's contents.
+    //
+    // Custom attributes are uniquely identified by an 8-bit type and limited
+    // to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller
+    // than the buffer, it will be padded with zeros. If the stored attribute
+    // is larger, then it will be silently truncated. If the attribute is not
+    // found, it will be created implicitly.
+    struct lfs_attr *attrs;
+
+    // Number of custom attributes in the list
+    lfs_size_t attr_count;
+};
+
+
+/// internal littlefs data structures ///
+typedef struct lfs_cache {
+    lfs_block_t block;
+    lfs_off_t off;
+    lfs_size_t size;
+    uint8_t *buffer;
+} lfs_cache_t;
+
+typedef struct lfs_mdir {
+    lfs_block_t pair[2];
+    uint32_t rev;
+    lfs_off_t off;
+    uint32_t etag;
+    uint16_t count;
+    bool erased;
+    bool split;
+    lfs_block_t tail[2];
+} lfs_mdir_t;
+
+// littlefs directory type
+typedef struct lfs_dir {
+    struct lfs_dir *next;
+    uint16_t id;
+    uint8_t type;
+    lfs_mdir_t m;
+
+    lfs_off_t pos;
+    lfs_block_t head[2];
+} lfs_dir_t;
+
+// littlefs file type
+typedef struct lfs_file {
+    struct lfs_file *next;
+    uint16_t id;
+    uint8_t type;
+    lfs_mdir_t m;
+
+    struct lfs_ctz {
+        lfs_block_t head;
+        lfs_size_t size;
+    } ctz;
+
+    uint32_t flags;
+    lfs_off_t pos;
+    lfs_block_t block;
+    lfs_off_t off;
+    lfs_cache_t cache;
+
+    const struct lfs_file_config *cfg;
+} lfs_file_t;
+
+typedef struct lfs_superblock {
+    uint32_t version;
+    lfs_size_t block_size;
+    lfs_size_t block_count;
+    lfs_size_t name_max;
+    lfs_size_t file_max;
+    lfs_size_t attr_max;
+} lfs_superblock_t;
+
+// The littlefs filesystem type
+typedef struct lfs {
+    lfs_cache_t rcache;
+    lfs_cache_t pcache;
+
+    lfs_block_t root[2];
+    struct lfs_mlist {
+        struct lfs_mlist *next;
+        uint16_t id;
+        uint8_t type;
+        lfs_mdir_t m;
+    } *mlist;
+    uint32_t seed;
+
+    struct lfs_gstate {
+        uint32_t tag;
+        lfs_block_t pair[2];
+    } gstate, gpending, gdelta;
+
+    struct lfs_free {
+        lfs_block_t off;
+        lfs_block_t size;
+        lfs_block_t i;
+        lfs_block_t ack;
+        uint32_t *buffer;
+    } free;
+
+    const struct lfs_config *cfg;
+    lfs_size_t name_max;
+    lfs_size_t file_max;
+    lfs_size_t attr_max;
+
+#ifdef LFS_MIGRATE
+    struct lfs1 *lfs1;
+#endif
+} lfs_t;
+
+
+/// Filesystem functions ///
+
+// Format a block device with the littlefs
+//
+// Requires a littlefs object and config struct. This clobbers the littlefs
+// object, and does not leave the filesystem mounted. The config struct must
+// be zeroed for defaults and backwards compatibility.
+//
+// Returns a negative error code on failure.
+int lfs_format(lfs_t *lfs, const struct lfs_config *config);
+
+// Mounts a littlefs
+//
+// Requires a littlefs object and config struct. Multiple filesystems
+// may be mounted simultaneously with multiple littlefs objects. Both
+// lfs and config must be allocated while mounted. The config struct must
+// be zeroed for defaults and backwards compatibility.
+//
+// Returns a negative error code on failure.
+int lfs_mount(lfs_t *lfs, const struct lfs_config *config);
+
+// Unmounts a littlefs
+//
+// Does nothing besides releasing any allocated resources.
+// Returns a negative error code on failure.
+int lfs_unmount(lfs_t *lfs);
+
+/// General operations ///
+
+// Removes a file or directory
+//
+// If removing a directory, the directory must be empty.
+// Returns a negative error code on failure.
+int lfs_remove(lfs_t *lfs, const char *path);
+
+// Rename or move a file or directory
+//
+// If the destination exists, it must match the source in type.
+// If the destination is a directory, the directory must be empty.
+//
+// Returns a negative error code on failure.
+int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);
+
+// Find info about a file or directory
+//
+// Fills out the info structure, based on the specified file or directory.
+// Returns a negative error code on failure.
+int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info);
+
+// Get a custom attribute
+//
+// Custom attributes are uniquely identified by an 8-bit type and limited
+// to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller than
+// the buffer, it will be padded with zeros. If the stored attribute is larger,
+// then it will be silently truncated. If no attribute is found, the error
+// LFS_ERR_NOATTR is returned and the buffer is filled with zeros.
+//
+// Returns the size of the attribute, or a negative error code on failure.
+// Note, the returned size is the size of the attribute on disk, irrespective
+// of the size of the buffer. This can be used to dynamically allocate a buffer
+// or check for existance.
+lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path,
+        uint8_t type, void *buffer, lfs_size_t size);
+
+// Set custom attributes
+//
+// Custom attributes are uniquely identified by an 8-bit type and limited
+// to LFS_ATTR_MAX bytes. If an attribute is not found, it will be
+// implicitly created.
+//
+// Returns a negative error code on failure.
+int lfs_setattr(lfs_t *lfs, const char *path,
+        uint8_t type, const void *buffer, lfs_size_t size);
+
+// Removes a custom attribute
+//
+// If an attribute is not found, nothing happens.
+//
+// Returns a negative error code on failure.
+int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type);
+
+
+/// File operations ///
+
+// Open a file
+//
+// The mode that the file is opened in is determined by the flags, which
+// are values from the enum lfs_open_flags that are bitwise-ored together.
+//
+// Returns a negative error code on failure.
+int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags);
+
+// Open a file with extra configuration
+//
+// The mode that the file is opened in is determined by the flags, which
+// are values from the enum lfs_open_flags that are bitwise-ored together.
+//
+// The config struct provides additional config options per file as described
+// above. The config struct must be allocated while the file is open, and the
+// config struct must be zeroed for defaults and backwards compatibility.
+//
+// Returns a negative error code on failure.
+int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file,
+        const char *path, int flags,
+        const struct lfs_file_config *config);
+
+// Close a file
+//
+// Any pending writes are written out to storage as though
+// sync had been called and releases any allocated resources.
+//
+// Returns a negative error code on failure.
+int lfs_file_close(lfs_t *lfs, lfs_file_t *file);
+
+// Synchronize a file on storage
+//
+// Any pending writes are written out to storage.
+// Returns a negative error code on failure.
+int lfs_file_sync(lfs_t *lfs, lfs_file_t *file);
+
+// Read data from file
+//
+// Takes a buffer and size indicating where to store the read data.
+// Returns the number of bytes read, or a negative error code on failure.
+lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file,
+        void *buffer, lfs_size_t size);
+
+// Write data to file
+//
+// Takes a buffer and size indicating the data to write. The file will not
+// actually be updated on the storage until either sync or close is called.
+//
+// Returns the number of bytes written, or a negative error code on failure.
+lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
+        const void *buffer, lfs_size_t size);
+
+// Change the position of the file
+//
+// The change in position is determined by the offset and whence flag.
+// Returns the new position of the file, or a negative error code on failure.
+lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file,
+        lfs_soff_t off, int whence);
+
+// Truncates the size of the file to the specified size
+//
+// Returns a negative error code on failure.
+int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size);
+
+// Return the position of the file
+//
+// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR)
+// Returns the position of the file, or a negative error code on failure.
+lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file);
+
+// Change the position of the file to the beginning of the file
+//
+// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_SET)
+// Returns a negative error code on failure.
+int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file);
+
+// Return the size of the file
+//
+// Similar to lfs_file_seek(lfs, file, 0, LFS_SEEK_END)
+// Returns the size of the file, or a negative error code on failure.
+lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file);
+
+
+/// Directory operations ///
+
+// Create a directory
+//
+// Returns a negative error code on failure.
+int lfs_mkdir(lfs_t *lfs, const char *path);
+
+// Open a directory
+//
+// Once open a directory can be used with read to iterate over files.
+// Returns a negative error code on failure.
+int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path);
+
+// Close a directory
+//
+// Releases any allocated resources.
+// Returns a negative error code on failure.
+int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir);
+
+// Read an entry in the directory
+//
+// Fills out the info structure, based on the specified file or directory.
+// Returns a positive value on success, 0 at the end of directory,
+// or a negative error code on failure.
+int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info);
+
+// Change the position of the directory
+//
+// The new off must be a value previous returned from tell and specifies
+// an absolute offset in the directory seek.
+//
+// Returns a negative error code on failure.
+int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off);
+
+// Return the position of the directory
+//
+// The returned offset is only meant to be consumed by seek and may not make
+// sense, but does indicate the current position in the directory iteration.
+//
+// Returns the position of the directory, or a negative error code on failure.
+lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir);
+
+// Change the position of the directory to the beginning of the directory
+//
+// Returns a negative error code on failure.
+int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir);
+
+
+/// Filesystem-level filesystem operations
+
+// Finds the current size of the filesystem
+//
+// Note: Result is best effort. If files share COW structures, the returned
+// size may be larger than the filesystem actually is.
+//
+// Returns the number of allocated blocks, or a negative error code on failure.
+lfs_ssize_t lfs_fs_size(lfs_t *lfs);
+
+// Traverse through all blocks in use by the filesystem
+//
+// The provided callback will be called with each block address that is
+// currently in use by the filesystem. This can be used to determine which
+// blocks are in use or how much of the storage is available.
+//
+// Returns a negative error code on failure.
+int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
+
+#ifdef LFS_MIGRATE
+// Attempts to migrate a previous version of littlefs
+//
+// Behaves similarly to the lfs_format function. Attempts to mount
+// the previous version of littlefs and update the filesystem so it can be
+// mounted with the current version of littlefs.
+//
+// Requires a littlefs object and config struct. This clobbers the littlefs
+// object, and does not leave the filesystem mounted. The config struct must
+// be zeroed for defaults and backwards compatibility.
+//
+// Returns a negative error code on failure.
+int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg);
+#endif
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif

+ 33 - 0
bsp/air302/mklfs/lfs/lfs_util.c

@@ -0,0 +1,33 @@
+/*
+ * lfs util functions
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#include "lfs_util.h"
+
+// Only compile if user does not provide custom config
+#ifndef LFS_CONFIG
+
+
+// Software CRC implementation with small lookup table
+uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) {
+    static const uint32_t rtable[16] = {
+        0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
+        0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
+        0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
+        0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c,
+    };
+
+    const uint8_t *data = buffer;
+
+    for (size_t i = 0; i < size; i++) {
+        crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 0)) & 0xf];
+        crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 4)) & 0xf];
+    }
+
+    return crc;
+}
+
+
+#endif

+ 230 - 0
bsp/air302/mklfs/lfs/lfs_util.h

@@ -0,0 +1,230 @@
+/*
+ * lfs utility functions
+ *
+ * Copyright (c) 2017, Arm Limited. All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#ifndef LFS_UTIL_H
+#define LFS_UTIL_H
+
+// Users can override lfs_util.h with their own configuration by defining
+// LFS_CONFIG as a header file to include (-DLFS_CONFIG=lfs_config.h).
+//
+// If LFS_CONFIG is used, none of the default utils will be emitted and must be
+// provided by the config file. To start, I would suggest copying lfs_util.h
+// and modifying as needed.
+#ifdef LFS_CONFIG
+#define LFS_STRINGIZE(x) LFS_STRINGIZE2(x)
+#define LFS_STRINGIZE2(x) #x
+#include LFS_STRINGIZE(LFS_CONFIG)
+#else
+
+// System includes
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <inttypes.h>
+
+#ifndef LFS_NO_MALLOC
+#include <stdlib.h>
+#endif
+#ifndef LFS_NO_ASSERT
+#include <assert.h>
+#endif
+#if !defined(LFS_NO_DEBUG) || \
+        !defined(LFS_NO_WARN) || \
+        !defined(LFS_NO_ERROR) || \
+        defined(LFS_YES_TRACE)
+#include <stdio.h>
+#endif
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+// Macros, may be replaced by system specific wrappers. Arguments to these
+// macros must not have side-effects as the macros can be removed for a smaller
+// code footprint
+
+// Logging functions
+#ifdef LFS_YES_TRACE
+#define LFS_TRACE(fmt, ...) \
+    printf("lfs_trace:%d: " fmt "\n", __LINE__, __VA_ARGS__)
+#else
+#define LFS_TRACE(fmt, ...)
+#endif
+
+#ifndef LFS_NO_DEBUG
+#define LFS_DEBUG(fmt, ...) \
+    printf("lfs_debug:%d: " fmt "\n", __LINE__, __VA_ARGS__)
+#else
+#define LFS_DEBUG(fmt, ...)
+#endif
+
+#ifndef LFS_NO_WARN
+#define LFS_WARN(fmt, ...) \
+    printf("lfs_warn:%d: " fmt "\n", __LINE__, __VA_ARGS__)
+#else
+#define LFS_WARN(fmt, ...)
+#endif
+
+#ifndef LFS_NO_ERROR
+#define LFS_ERROR(fmt, ...) \
+    printf("lfs_error:%d: " fmt "\n", __LINE__, __VA_ARGS__)
+#else
+#define LFS_ERROR(fmt, ...)
+#endif
+
+// Runtime assertions
+#ifndef LFS_NO_ASSERT
+#define LFS_ASSERT(test) assert(test)
+#else
+#define LFS_ASSERT(test)
+#endif
+
+
+// Builtin functions, these may be replaced by more efficient
+// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more
+// expensive basic C implementation for debugging purposes
+
+// Min/max functions for unsigned 32-bit numbers
+static inline uint32_t lfs_max(uint32_t a, uint32_t b) {
+    return (a > b) ? a : b;
+}
+
+static inline uint32_t lfs_min(uint32_t a, uint32_t b) {
+    return (a < b) ? a : b;
+}
+
+// Align to nearest multiple of a size
+static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) {
+    return a - (a % alignment);
+}
+
+static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) {
+    return lfs_aligndown(a + alignment-1, alignment);
+}
+
+// Find the next smallest power of 2 less than or equal to a
+static inline uint32_t lfs_npw2(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM))
+    return 32 - __builtin_clz(a-1);
+#else
+    uint32_t r = 0;
+    uint32_t s;
+    a -= 1;
+    s = (a > 0xffff) << 4; a >>= s; r |= s;
+    s = (a > 0xff  ) << 3; a >>= s; r |= s;
+    s = (a > 0xf   ) << 2; a >>= s; r |= s;
+    s = (a > 0x3   ) << 1; a >>= s; r |= s;
+    return (r | (a >> 1)) + 1;
+#endif
+}
+
+// Count the number of trailing binary zeros in a
+// lfs_ctz(0) may be undefined
+static inline uint32_t lfs_ctz(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__)
+    return __builtin_ctz(a);
+#else
+    return lfs_npw2((a & -a) + 1) - 1;
+#endif
+}
+
+// Count the number of binary ones in a
+static inline uint32_t lfs_popc(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM))
+    return __builtin_popcount(a);
+#else
+    a = a - ((a >> 1) & 0x55555555);
+    a = (a & 0x33333333) + ((a >> 2) & 0x33333333);
+    return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24;
+#endif
+}
+
+// Find the sequence comparison of a and b, this is the distance
+// between a and b ignoring overflow
+static inline int lfs_scmp(uint32_t a, uint32_t b) {
+    return (int)(unsigned)(a - b);
+}
+
+// Convert between 32-bit little-endian and native order
+static inline uint32_t lfs_fromle32(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && ( \
+    (defined(  BYTE_ORDER  ) && defined(  ORDER_LITTLE_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_LITTLE_ENDIAN  ) && __BYTE_ORDER   == __ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__))
+    return a;
+#elif !defined(LFS_NO_INTRINSICS) && ( \
+    (defined(  BYTE_ORDER  ) && defined(  ORDER_BIG_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_BIG_ENDIAN  ) && __BYTE_ORDER   == __ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__))
+    return __builtin_bswap32(a);
+#else
+    return (((uint8_t*)&a)[0] <<  0) |
+           (((uint8_t*)&a)[1] <<  8) |
+           (((uint8_t*)&a)[2] << 16) |
+           (((uint8_t*)&a)[3] << 24);
+#endif
+}
+
+static inline uint32_t lfs_tole32(uint32_t a) {
+    return lfs_fromle32(a);
+}
+
+// Convert between 32-bit big-endian and native order
+static inline uint32_t lfs_frombe32(uint32_t a) {
+#if !defined(LFS_NO_INTRINSICS) && ( \
+    (defined(  BYTE_ORDER  ) && defined(  ORDER_LITTLE_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_LITTLE_ENDIAN  ) && __BYTE_ORDER   == __ORDER_LITTLE_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__))
+    return __builtin_bswap32(a);
+#elif !defined(LFS_NO_INTRINSICS) && ( \
+    (defined(  BYTE_ORDER  ) && defined(  ORDER_BIG_ENDIAN  ) &&   BYTE_ORDER   ==   ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER  ) && defined(__ORDER_BIG_ENDIAN  ) && __BYTE_ORDER   == __ORDER_BIG_ENDIAN  ) || \
+    (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__))
+    return a;
+#else
+    return (((uint8_t*)&a)[0] << 24) |
+           (((uint8_t*)&a)[1] << 16) |
+           (((uint8_t*)&a)[2] <<  8) |
+           (((uint8_t*)&a)[3] <<  0);
+#endif
+}
+
+static inline uint32_t lfs_tobe32(uint32_t a) {
+    return lfs_frombe32(a);
+}
+
+// Calculate CRC-32 with polynomial = 0x04c11db7
+uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size);
+
+// Allocate memory, only used if buffers are not provided to littlefs
+// Note, memory must be 64-bit aligned
+static inline void *lfs_malloc(size_t size) {
+#ifndef LFS_NO_MALLOC
+    return malloc(size);
+#else
+    (void)size;
+    return NULL;
+#endif
+}
+
+// Deallocate memory, only used if buffers are not provided to littlefs
+static inline void lfs_free(void *p) {
+#ifndef LFS_NO_MALLOC
+    free(p);
+#else
+    (void)p;
+#endif
+}
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif
+#endif

+ 286 - 0
bsp/air302/mklfs/mklfs.c

@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2015 - 2018, IBEROXARXA SERVICIOS INTEGRALES, S.L.
+ * Copyright (C) 2015 - 2018, Jaume Olivé Petrus (jolive@whitecatboard.org)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *     * The WHITECAT logotype cannot be changed, you can remove it, but you
+ *       cannot change it in any way. The WHITECAT logotype is:
+ *
+ *          /\       /\
+ *         /  \_____/  \
+ *        /_____________\
+ *        W H I T E C A T
+ *
+ *     * Redistributions in binary form must retain all copyright notices printed
+ *       to any local or remote output device. This include any reference to
+ *       Lua RTOS, whitecatboard.org, Lua, and other copyright notices that may
+ *       appear in the future.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Lua RTOS, a tool for make a LFS file system image
+ *
+ */
+
+#include "lfs/lfs.h"
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <limits.h>
+#include <dirent.h>
+#include <sys/types.h>
+
+static struct lfs_config cfg;
+static lfs_t lfs;
+static uint8_t *data;
+
+static int lfs_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) {
+    memcpy(buffer, data + (block * c->block_size) + off, size);
+    return 0;
+}
+
+static int lfs_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) {
+	memcpy(data + (block * c->block_size) + off, buffer, size);
+    return 0;
+}
+
+static int lfs_erase(const struct lfs_config *c, lfs_block_t block) {
+    memset(data + (block * c->block_size), 0, c->block_size);
+    return 0;
+}
+
+static int lfs_sync(const struct lfs_config *c) {
+	return 0;
+}
+
+static void create_dir(char *src) {
+    char *path;
+    int ret;
+
+    path = strchr(src, '/');
+    if (path) {
+        fprintf(stdout, "%s\r\n", path);
+
+		if ((ret = lfs_mkdir(&lfs, path)) < 0) {
+			fprintf(stderr,"can't create directory %s: error=%d\r\n", path, ret);
+			exit(1);
+		}
+	}
+}
+
+static void create_file(char *src) {
+    char *path;
+    int ret;
+
+    path = strchr(src, '/');
+    if (path) {
+        fprintf(stdout, "%s\r\n", path);
+
+        // Open source file
+        FILE *srcf = fopen(src,"rb");
+        if (!srcf) {
+            fprintf(stderr,"can't open source file %s: errno=%d (%s)\r\n", src, errno, strerror(errno));
+            exit(1);
+        }
+
+        // Open destination file
+        lfs_file_t dstf;
+        if ((ret = lfs_file_open(&lfs, &dstf, path, LFS_O_WRONLY | LFS_O_CREAT)) < 0) {
+            fprintf(stderr,"can't open destination file %s: error=%d\r\n", path, ret);
+            exit(1);
+        }
+
+		char c = fgetc(srcf);
+		while (!feof(srcf)) {
+			ret = lfs_file_write(&lfs, &dstf, &c, 1);
+			if (ret < 0) {
+				fprintf(stderr,"can't write to destination file %s: error=%d\r\n", path, ret);
+				exit(1);
+			}
+			c = fgetc(srcf);
+		}
+
+        // Close destination file
+		ret = lfs_file_close(&lfs, &dstf);
+		if (ret < 0) {
+			fprintf(stderr,"can't close destination file %s: error=%d\r\n", path, ret);
+			exit(1);
+		}
+
+        // Close source file
+        fclose(srcf);
+    }
+}
+
+static void compact(char *src) {
+    DIR *dir;
+    struct dirent *ent;
+    char curr_path[PATH_MAX];
+
+    dir = opendir(src);
+    if (dir) {
+        while ((ent = readdir(dir))) {
+            // Skip . and .. directories
+            if ((strcmp(ent->d_name,".") != 0) && (strcmp(ent->d_name,"..") != 0)) {
+                // Update the current path
+                strcpy(curr_path, src);
+                strcat(curr_path, "/");
+                strcat(curr_path, ent->d_name);
+
+                if (ent->d_type == DT_DIR) {
+                    create_dir(curr_path);
+                    compact(curr_path);
+                } else if (ent->d_type == DT_REG) {
+                    create_file(curr_path);
+                }
+            }
+        }
+
+        closedir(dir);
+    }
+}
+
+void usage() {
+	fprintf(stdout, "usage: mklfs -c <pack-dir> -b <block-size> -r <read-size> -p <prog-size> -s <filesystem-size> -i <image-file-path>\r\n");
+}
+
+static int is_number(const char *s) {
+	const char *c = s;
+
+	while (*c) {
+		if ((*c < '0') || (*c > '9')) {
+			return 0;
+		}
+		c++;
+	}
+
+	return 1;
+}
+
+static int is_hex(const char *s) {
+	const char *c = s;
+
+	if (*c++ != '0') {
+		return 0;
+	}
+
+	if (*c++ != 'x') {
+		return 0;
+	}
+
+	while (*c) {
+		if (((*c < '0') || (*c > '9')) && ((*c < 'A') || (*c > 'F')) && ((*c < 'a') || (*c > 'f'))) {
+			return 0;
+		}
+		c++;
+	}
+
+	return 1;
+}
+
+static int to_int(const char *s) {
+	if (is_number(s)) {
+		return atoi(s);
+	} else if (is_hex(s)) {
+		return (int)strtol(s, NULL, 16);
+	}
+
+	return -1;
+}
+
+#define FLASH_FS_REGION_OFFSET          0x350000
+#define FLASH_FS_REGION_END             0x3A4000
+#define FLASH_FS_REGION_SIZE            (FLASH_FS_REGION_END-FLASH_FS_REGION_OFFSET)      // 336KB
+
+#define LFS_BLOCK_DEVICE_READ_SIZE      (256)
+#define LFS_BLOCK_DEVICE_PROG_SIZE      (256)
+#define LFS_BLOCK_DEVICE_ERASE_SIZE     (4096)       // one sector 4KB
+#define LFS_BLOCK_DEVICE_TOTOAL_SIZE    (FLASH_FS_REGION_SIZE)
+#define LFS_BLOCK_DEVICE_LOOK_AHEAD     (16)
+#define LFS_BLOCK_DEVICE_CACHE_SIZE     (256)
+
+int main(int argc, char **argv) {
+    char *src = "disk";   // Source directory
+    char *dst = "disk.fs";   // Destination image
+	int err = 0;
+	int fs_size = LFS_BLOCK_DEVICE_TOTOAL_SIZE;
+
+    // Mount the file system
+    cfg.read  = lfs_read;
+    cfg.prog  = lfs_prog;
+    cfg.erase = lfs_erase;
+    cfg.sync  = lfs_sync;
+
+    cfg.read_size = LFS_BLOCK_DEVICE_READ_SIZE;
+    cfg.prog_size = LFS_BLOCK_DEVICE_PROG_SIZE;
+    cfg.block_size = LFS_BLOCK_DEVICE_ERASE_SIZE;
+    cfg.block_count = LFS_BLOCK_DEVICE_TOTOAL_SIZE / LFS_BLOCK_DEVICE_ERASE_SIZE;
+    cfg.block_cycles = 200;
+    cfg.cache_size = LFS_BLOCK_DEVICE_CACHE_SIZE;
+    cfg.lookahead_size = LFS_BLOCK_DEVICE_LOOK_AHEAD;
+
+    // cfg.read_buffer = lfs_read_buf,
+    // cfg.prog_buffer = lfs_prog_buf,
+    // cfg.lookahead_buffer = lfs_lookahead_buf,
+    cfg.name_max = 63;
+    cfg.file_max = 0;
+    cfg.attr_max = 0;
+
+	data = calloc(1, fs_size);
+	if (!data) {
+		fprintf(stderr, "no memory for mount\r\n");
+		return -1;
+	}
+
+	err = lfs_format(&lfs, &cfg);
+	if (err < 0) {
+		fprintf(stderr, "format error: error=%d\r\n", err);
+		return -1;
+	}
+
+	err = lfs_mount(&lfs, &cfg);
+	if (err < 0) {
+		fprintf(stderr, "mount error: error=%d\r\n", err);
+		return -1;
+	}
+
+	compact(src);
+
+	FILE *img = fopen(dst, "wb");
+
+	if (!img) {
+		fprintf(stderr, "can't create image file: errno=%d (%s)\r\n", errno, strerror(errno));
+		return -1;
+	}
+
+	fwrite(data, 1, fs_size, img);
+
+	fclose(img);
+
+	return 0;
+}

+ 268 - 0
bsp/air302/mklfs/unlfs.c

@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2015 - 2018, IBEROXARXA SERVICIOS INTEGRALES, S.L.
+ * Copyright (C) 2015 - 2018, Jaume Olivé Petrus (jolive@whitecatboard.org)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *     * The WHITECAT logotype cannot be changed, you can remove it, but you
+ *       cannot change it in any way. The WHITECAT logotype is:
+ *
+ *          /\       /\
+ *         /  \_____/  \
+ *        /_____________\
+ *        W H I T E C A T
+ *
+ *     * Redistributions in binary form must retain all copyright notices printed
+ *       to any local or remote output device. This include any reference to
+ *       Lua RTOS, whitecatboard.org, Lua, and other copyright notices that may
+ *       appear in the future.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Lua RTOS, a tool for make a LFS file system image
+ *
+ */
+
+#include "lfs/lfs.h"
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <limits.h>
+#include <dirent.h>
+#include <sys/types.h>
+
+static struct lfs_config cfg;
+static lfs_t lfs;
+static uint8_t *data;
+
+static int lfs_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) {
+    memcpy(buffer, data + (block * c->block_size) + off, size);
+    return 0;
+}
+
+static int lfs_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) {
+	memcpy(data + (block * c->block_size) + off, buffer, size);
+    return 0;
+}
+
+static int lfs_erase(const struct lfs_config *c, lfs_block_t block) {
+    memset(data + (block * c->block_size), 0, c->block_size);
+    return 0;
+}
+
+static int lfs_sync(const struct lfs_config *c) {
+	return 0;
+}
+
+static void create_dir(char *src) {
+    char *path;
+    int ret;
+
+    path = strchr(src, '/');
+    if (path) {
+        fprintf(stdout, "%s\r\n", path);
+
+		if ((ret = lfs_mkdir(&lfs, path)) < 0) {
+			fprintf(stderr,"can't create directory %s: error=%d\r\n", path, ret);
+			exit(1);
+		}
+	}
+}
+
+static void create_file(char *src) {
+    char *path;
+    int ret;
+
+    path = strchr(src, '/');
+    if (path) {
+        fprintf(stdout, "%s\r\n", path);
+
+        // Open source file
+        FILE *srcf = fopen(src,"rb");
+        if (!srcf) {
+            fprintf(stderr,"can't open source file %s: errno=%d (%s)\r\n", src, errno, strerror(errno));
+            exit(1);
+        }
+
+        // Open destination file
+        lfs_file_t dstf;
+        if ((ret = lfs_file_open(&lfs, &dstf, path, LFS_O_WRONLY | LFS_O_CREAT)) < 0) {
+            fprintf(stderr,"can't open destination file %s: error=%d\r\n", path, ret);
+            exit(1);
+        }
+
+		char c = fgetc(srcf);
+		while (!feof(srcf)) {
+			ret = lfs_file_write(&lfs, &dstf, &c, 1);
+			if (ret < 0) {
+				fprintf(stderr,"can't write to destination file %s: error=%d\r\n", path, ret);
+				exit(1);
+			}
+			c = fgetc(srcf);
+		}
+
+        // Close destination file
+		ret = lfs_file_close(&lfs, &dstf);
+		if (ret < 0) {
+			fprintf(stderr,"can't close destination file %s: error=%d\r\n", path, ret);
+			exit(1);
+		}
+
+        // Close source file
+        fclose(srcf);
+    }
+}
+
+static void compact(char *src) {
+    DIR *dir;
+    struct dirent *ent;
+    char curr_path[PATH_MAX];
+
+    dir = opendir(src);
+    if (dir) {
+        while ((ent = readdir(dir))) {
+            // Skip . and .. directories
+            if ((strcmp(ent->d_name,".") != 0) && (strcmp(ent->d_name,"..") != 0)) {
+                // Update the current path
+                strcpy(curr_path, src);
+                strcat(curr_path, "/");
+                strcat(curr_path, ent->d_name);
+
+                if (ent->d_type == DT_DIR) {
+                    create_dir(curr_path);
+                    compact(curr_path);
+                } else if (ent->d_type == DT_REG) {
+                    create_file(curr_path);
+                }
+            }
+        }
+
+        closedir(dir);
+    }
+}
+
+void usage() {
+	fprintf(stdout, "usage: mklfs -c <pack-dir> -b <block-size> -r <read-size> -p <prog-size> -s <filesystem-size> -i <image-file-path>\r\n");
+}
+
+static int is_number(const char *s) {
+	const char *c = s;
+
+	while (*c) {
+		if ((*c < '0') || (*c > '9')) {
+			return 0;
+		}
+		c++;
+	}
+
+	return 1;
+}
+
+static int is_hex(const char *s) {
+	const char *c = s;
+
+	if (*c++ != '0') {
+		return 0;
+	}
+
+	if (*c++ != 'x') {
+		return 0;
+	}
+
+	while (*c) {
+		if (((*c < '0') || (*c > '9')) && ((*c < 'A') || (*c > 'F')) && ((*c < 'a') || (*c > 'f'))) {
+			return 0;
+		}
+		c++;
+	}
+
+	return 1;
+}
+
+static int to_int(const char *s) {
+	if (is_number(s)) {
+		return atoi(s);
+	} else if (is_hex(s)) {
+		return (int)strtol(s, NULL, 16);
+	}
+
+	return -1;
+}
+
+#define FLASH_FS_REGION_OFFSET          0x350000
+#define FLASH_FS_REGION_END             0x3A4000
+#define FLASH_FS_REGION_SIZE            (FLASH_FS_REGION_END-FLASH_FS_REGION_OFFSET)      // 336KB
+
+#define LFS_BLOCK_DEVICE_READ_SIZE      (256)
+#define LFS_BLOCK_DEVICE_PROG_SIZE      (256)
+#define LFS_BLOCK_DEVICE_ERASE_SIZE     (4096)       // one sector 4KB
+#define LFS_BLOCK_DEVICE_TOTOAL_SIZE    (FLASH_FS_REGION_SIZE)
+#define LFS_BLOCK_DEVICE_LOOK_AHEAD     (64)
+
+int main(int argc, char **argv) {
+	int err = 0;
+	int fs_size = LFS_BLOCK_DEVICE_TOTOAL_SIZE;
+    lfs_dir_t dir;
+    struct lfs_info info;
+
+    // Mount the file system
+    cfg.read  = lfs_read;
+    cfg.prog  = lfs_prog;
+    cfg.erase = lfs_erase;
+    cfg.sync  = lfs_sync;
+
+    cfg.block_size  = LFS_BLOCK_DEVICE_ERASE_SIZE;
+    cfg.read_size   = LFS_BLOCK_DEVICE_READ_SIZE;
+    cfg.prog_size   = LFS_BLOCK_DEVICE_PROG_SIZE;
+    cfg.block_count = LFS_BLOCK_DEVICE_TOTOAL_SIZE / LFS_BLOCK_DEVICE_ERASE_SIZE;
+    cfg.lookahead_size   = LFS_BLOCK_DEVICE_LOOK_AHEAD;
+    cfg.context     = NULL;
+
+	FILE* fimg = fopen("disk.fs", "r");
+	data = calloc(1, fs_size);
+	fread(data, LFS_BLOCK_DEVICE_ERASE_SIZE, cfg.block_count, fimg);
+	fclose(fimg);
+	err = lfs_mount(&lfs, &cfg);
+	if (err < 0) {
+		fprintf(stderr, "mount error: error=%d\r\n", err);
+		return -1;
+	}
+
+    lfs_dir_open(&lfs, &dir, "/lua/");
+	while (lfs_dir_read(&lfs, &dir, &info) == 1) {
+        fprintf(stdout, "path=/lua/%s\r\n", info.name);
+    }
+    lfs_dir_close(&lfs, &dir);
+
+    lfs_dir_open(&lfs, &dir, "/");
+	while (lfs_dir_read(&lfs, &dir, &info) == 1) {
+        fprintf(stdout, "path=/%s\r\n", info.name);
+    }
+    lfs_dir_close(&lfs, &dir);
+
+	return 0;
+}

+ 29 - 0
bsp/air302/user/main.lua

@@ -0,0 +1,29 @@
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "air302_gpio_demo"
+VERSION = "1.0.0"
+
+-- sys库是标配
+_G.sys = require("sys")
+
+local NETLED = gpio.setup(19, 0) -- 输出模式
+local G18 = gpio.setup(18, nil) -- 输入模式
+local G1 = gpio.setup(1, function() -- 中断模式, 下降沿
+    log.info("gpio", "BOOT button release")
+end)
+
+sys.taskInit(function()
+    while 1 do
+        -- 一闪一闪亮晶晶
+        NETLED(0)
+        sys.wait(500)
+        NETLED(1)
+        sys.wait(500)
+        log.info("gpio", "18", G18())
+    end
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 11 - 0
bsp/air302/userdoc/user_readme.txt

@@ -0,0 +1,11 @@
+# Air302 固件压缩包
+
+1. Air302_XXXX.ec 固件文件, 用于Luatools刷机
+2. demo 示例文件夹
+3. lib 库文件
+
+
+各uart的功能
+uart0, 波特率6M 8N1, 用于读取底层日志
+uart1, 波特率921600 8N1, 用于下载固件/脚本/输出日志
+uart2, 用户使用