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

무료무료 기술 자료

이 글은 바로 읽을 수 있으며 기초 학습과 검색 유입에 적합합니다.

文件读写 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
内部 Flash3:/1.txt/데이터/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_열기(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(데이터)바이트 table,1 ≤ #데이터 ≤ 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_데이터(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_데이터(1, g_byte_buffer)
  end

5. 写文件

封装 file.write_chunk(filepath, 데이터, mode),mode 1=覆盖写、2=追加写,데이터 支持 string 或바이트 table(≤2048)。

function file.write_chunk(filepath, 데이터, mode)
  if mode ~= file.mode.create and mode ~= file.mode.append then return false end
  local t = type(데이터)
  local len = (t == "string" or t == "table") and #데이터 or -1
  if len  CHUNK_SIZE then return false end   -- 防内存溢出
  local ok = file_열기(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(데이터, i) end
  else
    for i = 1, len do buf[i] = 데이터[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_열기(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 튜토리얼「文件读写」,版权归大彩科技所有。

실제 프로젝트에 이 자료를 활용하시겠습니까?

도구 센터에서 프레임 분석, CRC 검증, 장비 디버깅을 진행하거나 요청을 제출하여 선정 및 연동 제안을 받아보세요.

엔지니어 회원

이 글을 실행 가능한 디버깅 자료로 활용하세요

가입하면 고급 프레임 분석, 자료 패키지 다운로드, 코드 예제, 엔지니어링 사례, 우선 기술 지원을 이용할 수 있어 실제 프로젝트 납품에 적합합니다.

고급 도구 무제한 이용
자료 패키지와 코드 패키지
완전한 엔지니어링 사례 라이브러리
우선 기술 지원 채널

답글 작성

이메일 주소는 공개되지 않습니다. 必填项已用 * 标注