VisualHMI 文件 IO:LUA 读写 SD 卡/U 盘/Flash 与目录列举

免费免费技术资料

这篇内容可直接阅读,适合用于基础学习和搜索引流。

本文目录
  1. 1. 1. 存储设备标识(M 系列 vs DH 系列)
  2. 2. 2. 文件 API 总表
  3. 3. 3. 🔴 内存安全铁律(不看必翻车)
  4. 4. 4. 读文件:流式分块 + 回调
  5. 5. 4.1 读 txt 逐行显示
  6. 6. 4.2 读 bin 按 16 字节一行
  7. 7. 5. 写文件
  8. 8. 6. 遍历 + 拷贝删除
  9. 9. 6.1 遍历 SD 根目录显示到数据记录控件
  10. 10. 6.2 复制 + 删除(带进度条)

文件读写 API 全套:读 txt/csv/bin、写文件、遍历目录、拷贝/删除。核心思想就一条——流式处理,永远不要让内存里同时存在超过 2KB 的缓冲区。HMI 内存极小,违反这条看门狗直接复位,设备无预警重启。

1. 存储设备标识(M 系列 vs DH 系列)

设备 M 系列 DH 系列
SD 卡 1:/1.txt /sdcard/1.txt
U 盘 2:/1.txt /udisk/1.txt
内部 Flash 3:/1.txt /data/1.txt

路径分隔符必须用 /。

2. 文件 API 总表

函数 参数 说明
dofile(name) lua 文件路径 加载执行 .lua 模块,大型工程拆模块
list_dir(path) 目录路径 异步扫描目录,结果逐项回调 on_list_dir
on_list_dir(path,filename,type,fsize) type 0=文件夹/1=文件;fsize 字节 遍历结果回调,每发现一个条目调一次
file_open(path,mode) mode 0=FA_READ 只读/1=FA_WRITE 覆盖写/2=FA_WRITE_ADD 追加写 打开文件,返回布尔
file_close() 关闭文件,写入后必须调,防丢数据
file_size() 当前打开文件的总字节数
file_seek(offset) 字节偏移,0=文件开头 定位读写位置
file_read(count) 1 ≤ count ≤ 2048 读字节数组(table,下标从 1),失败返回 nil
file_write(data) 字节 table,1 ≤ #data ≤ 2048 写字节数组,返回布尔
file_delete(path) 完整路径 删除文件,不可恢复
file_copy(src,dst) 源/目标完整路径 复制文件,进度回调 on_copy_file_process
on_copy_file_process(status,filesize,transfersize) status 0失败/1进行中/2成功 拷贝进度回调,transfersize 已复制字节
mkdir(dir) 完整目录路径 同步创建文件夹

3. 🔴 内存安全铁律(不看必翻车)

严禁把多次读取的数据累积存进全局变量或长生命周期缓冲区。典型错误:第一次读 2KB 存全局 table,后面每次读再追加——处理大文件时内存持续增长,直接触发看门狗复位。

正确姿势:流式处理——每次 file_read(2048) 后立即解析/校验/写入/显示,处理完就释放,任何时刻内存里只有一块 ≤2KB 的临时缓冲。写文件同理:每次只准备 ≤2048 字节的块立即 file_write,不要先拼一个几 MB 的大 table 再分段写(数据准备阶段就内存溢出了)。

4. 读文件:流式分块 + 回调

官方封装了 file.read_stream(filepath, on_chunk):打开 → 取大小 → 循环 seek+read(≤2048) → 每块调 on_chunk(chunk, offset, is_last) → 关文件。回调返回 false 可提前终止,大文件循环里调 feed_dog() 喂狗。

4.1 读 txt 逐行显示

if addr == 0x1501 then
  local idx = get_uint16(vtype, addr)
  if idx == 0 then return end
  local path = (idx == 1) and "1:/test1.txt" or "1:/test2.txt"
  record_clear(0)
  g_line_buffer = ""                       -- 行缓冲
  file.read_stream(path, function(chunk, offset, is_last)
    local str = ""
    for _, b in ipairs(chunk) do str = str .. string.char(b) end
    g_line_buffer = g_line_buffer .. str
    local lines = my_split(g_line_buffer, "n")  -- 按行分割
    g_line_buffer = lines[#lines]          -- 保留不完整行(可能跨块)
    for i = 1, #lines - 1 do
      if #lines[i] > 0 then record_write_string(0, lines[i]) end
    end
    return true
  end)
  if string.len(g_line_buffer) > 0 then    -- 最后一行
    record_write_string(0, g_line_buffer)
  end
end

注意跨块行缓冲:换行符可能正好落在两块之间,所以要维护 g_line_buffer 把不完整行留到下一块。

4.2 读 bin 按 16 字节一行

elseif addr == 0x1502 then
  local idx = get_uint16(vtype, addr)
  if idx == 0 then return end
  local path = (idx == 1) and "1:/test1.bin" or "1:/test2.bin"
  record_clear(1)
  g_byte_buffer = {}
  file.read_stream(path, function(chunk, offset, is_last)
    for _, b in ipairs(chunk) do
      table.insert(g_byte_buffer, b)
      if #g_byte_buffer >= 16 then        -- 每满 16 字节提交一行
        local line = {}
        for i = 1, 16 do line[i] = g_byte_buffer[i] end
        record_write_data(1, line)
        for i = 1, 16 do table.remove(g_byte_buffer, 1) end
      end
    end
    return true
  end)
  if #g_byte_buffer > 0 then              -- 末尾不足 16 字节补 0
    while #g_byte_buffer < 16 do table.insert(g_byte_buffer, 0) end
    record_write_data(1, g_byte_buffer)
  end

5. 写文件

封装 file.write_chunk(filepath, data, mode),mode 1=覆盖写、2=追加写,data 支持 string 或字节 table(≤2048)。

function file.write_chunk(filepath, data, mode)
  if mode ~= file.mode.create and mode ~= file.mode.append then return false end
  local t = type(data)
  local len = (t == "string" or t == "table") and #data or -1
  if len  CHUNK_SIZE then return false end   -- 防内存溢出
  local ok = file_open(filepath, mode)
  if not ok then return false end
  if mode == file.mode.append then            -- 追加模式必须手动定位!
    local size = file_size()
    if not file_seek(size) then file_close(); return false end
  end
  local buf = {}
  if t == "string" then
    for i = 1, len do buf[i] = string.byte(data, i) end
  else
    for i = 1, len do buf[i] = data[i] end
  end
  local ret = file_write(buf)
  file_close()
  return ret
end

-- 场景1:新建写
local name = get_string(VT_LW, 0x1510)
local msg  = get_string(VT_LW, 0x1520)
if not name or #name == 0 or not msg then return end
file.write_chunk("1:/"..name..".txt", msg.."n", 1)

-- 场景2:追加写
file.write_chunk("1:/"..name..".txt", msg.."n", 2)

⚠️ 追加写必须显式 file_seek(file_size())。HMI 的 file_open(mode=2) 不等于 POSIX 的 O_APPEND,不会自动定位到末尾——不 seek 就从偏移 0 开始写,把原文件开头覆盖掉。

6. 遍历 + 拷贝删除

6.1 遍历 SD 根目录显示到数据记录控件

function get_extension(filename)
  local dot = string.find(filename, ".", 1, true)
  if dot then
    return string.sub(filename, 1, dot-1), string.sub(filename, dot+1):lower()
  end
  return filename, "未知"
end

function on_list_dir(path, filename, type, fsize)
  local msg
  if type == 0 then
    msg = filename..";文件夹;;"
  else
    local sz
    if fsize >= 1024^3 then sz = string.format("%.2fG", fsize/1024^3)
    elseif fsize >= 1024^2 then sz = string.format("%.2fM", fsize/1024^2)
    elseif fsize >= 1024 then sz = string.format("%.2fKB", fsize/1024)
    else sz = fsize.."byte" end
    local name, ext = get_extension(filename)
    msg = name..";"..ext..";"..sz..";"
  end
  record_write_string(2, msg)
end

-- 刷新按钮 LW1504=1
function on_update(slave, vtype, addr)
  if vtype == VT_LW then
    if addr == 0x1504 and get_uint16(vtype, addr) == 1 then
      record_clear(2)
      list_dir("1:")          -- 遍历 SD 根目录
    end
  end
end

6.2 复制 + 删除(带进度条)

function on_copy_file_process(status, filesize, transfersize)
  if status == 0 then
    set_uint16(VT_LW, 0x1507, 3)           -- 失败
  elseif status == 1 then
    local p = math.floor(transfersize * 100 / filesize)
    set_uint16(VT_LW, 0x1506, p)           -- 进度条
    refresh_screen()
  elseif status == 2 then
    set_uint16(VT_LW, 0x1506, 100)
    set_uint16(VT_LW, 0x1507, 2)           -- 成功
    refresh_screen()
  end
end

function on_update(slave, vtype, addr)
  if vtype == VT_LW and addr == 0x1505 then
    local mode = get_uint16(vtype, addr)
    if mode == 1 then                      -- 复制
      if file.exists("1:/1.txt") then
        file_copy("1:/1.txt", "1:/1_copy.txt")
      else
        set_uint16(VT_LW, 0x1507, 1)
      end
      mkdir("1:/test")
    elseif mode == 2 then                  -- 删除
      file_delete("1:/test")
      if file.exists("1:/1_copy.txt") then
        file_delete("1:/1_copy.txt")
        if not file.exists("1:/1_copy.txt") then
          set_uint16(VT_LW, 0x1507, 4)
        else
          set_uint16(VT_LW, 0x1507, 5)
        end
      else
        set_uint16(VT_LW, 0x1507, 1)
      end
    end
    refresh_screen()
  end
end
五个必踩的坑:①内存铁律——严禁全局累积大缓冲,一次只留 ≤2KB 块,否则看门狗复位;②追加写必须 file_seek(file_size()),HMI 的 append 不是 O_APPEND;③路径必须带设备前缀和 /——「1:/config.txt」对、「1:config.txt」错;④大文件读取循环里要 feed_dog(),回调里别干耗时的事;⑤写文件后立即 file_close(),断电会损坏文件或部分写入。

整理自 广州大彩科技 VisualHMI 开发文档(hmi-doc.gz-dc.com)LUA 教程「文件读写」,版权归大彩科技所有。

来源/工具信息 —— 点击展开
来源 Modbus中文网(modbus.cn) —— 国内领先的Modbus通信协议技术社区 分类 LUA 脚本 / 串口屏/HMI 开发 字数 5246 字 · 阅读约 14 分钟 更新 2026-08-05 永久链接 https://www.modbus.cn/51508.html
推荐工具:Modbus调试助手 微信小程序
Modbus中文网官方推出的Modbus调试工具,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 无需安装,微信搜索「Modbus调试助手」即可使用。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
📝 作者声明
本文由 Modbus中文网技术团队 原创撰写,内容基于实际项目案例与技术文档,力求为读者提供准确、实用的参考信息。
把这篇资料用于真实项目?

进入工具中心进行报文解析、CRC 校验和设备调试,或提交需求获取选型与接入建议。

工程师会员

把这篇文章变成可执行的调试资料

开通后可使用高级报文解析、资料包下载、代码示例、工程案例和优先技术支持,适合真实项目交付。

高级工具不限次
资料包与代码包
完整工程案例库
优先技术支持入口

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注