VisualHMI - 自由串口协议(主动)

1.概述

ViualHMI 系统原生集集成多种主流工业通信协议的驱动模块,包括 DCBUS、XGUS、Modbus(RTU,支持主/从模式)、三菱 FX2N、台达 DVP 以及西门子 PPI 等。用户在开发过程中,仅需在 工程配置 → 通讯协议 中选择对应协议类型并完成参数设置,无需编写底层通信代码

然而,在实际工业应用中,仍普遍存在私有协议设备或需通过第二路(或多路)串口与额外外设通信的场景(适用于支持多串口的 HMI 硬件型号)。对此,VisualHMI 提供基于 Lua 脚本的自由串口控制能力,允许开发者直接操作串口收发缓冲区,实现完全自定义的协议解析与交互逻辑。

本例程以自定义协议为例,结合 Lua 的 协程元表 机制,构建了一套非阻塞、流程可控、结构清晰的通信框架。该方案确保主程序的响应流畅,还能精确管理通信环节,充分释放 HMI 在异构系统集成中的灵活性与扩展性。如下所示:

💡特别注意:

主动读串口数据需要用到API: uart_recv(),需要在工程属性中:串口设置→接收回调→禁用(新版本软件有该参数选择)

适用范围:VisualHMI - HMI&M系列&Dx系列

例程下载链接:ViusalHMI - 自由串口协议(主动)(点击下载)

2.API说明

2.1.uart_send(ch,packet)

向指定串口通道发送字节数组

参数 类型 说明
ch integer 串口通道号
0:主串口(默认) • 1, 2, ...:副串口(具体可用通道依硬件型号而定)
⚠️ 该通道必须处于自由协议模式(即未被 Modbus/XGUS 等系统协议占用)。
packet table 待发送的字节数组
以 Lua 表形式传入,下标从 1 开始。 每个元素应为 0~255 的整数。 示例:{0xAA, 0x55, 0x01, 0x02}

2.2.uart_rxsize(ch)

读取串口接收缓冲区数据字节数函数uart_rxsize(ch) 是 HMI 系统提供的串口通信状态查询接口,用于实时获取指定串口通道接收缓冲区中待读取的数据字节数。该函数专为主动式串口协议解析(即脚本轮询读取),用来实现自定义协议功能

📊 参数说明

项目 类型 说明
参数
ch number 串口号
• 通常取值:0,具体编号依 HMI 硬件配置而定
返回值
size number 接收缓冲区中当前可用的字节数
• 返回 N 表示有N个字节
• 返回 0 表示无新数据

2.3.uart_recv(ch, size)

串口主动接收指定字节数函数uart_recv(ch, size)是 HMI 系统提供的串口数据读取接口,用于从指定串口的接收缓冲区中主动读取指定数量的字节。该函数需配合 uart_rxsize 使用,构成“查询-读取”主动通信模型,适用于自定义协议解。

📊 参数说明

项目 类型 说明
参数
ch number 串口号
• 通常:0, 依硬件配置而定
size number 期望读取的字节数
• 必须 ≥1 • 建议 ≤ uart_rxsize(ch) 返回值
返回值
data table 接收到的原始字节数据
接收到的原始字节数据数组,以 Lua 表形式传递,下标从 1 开始。 示例:{0xAA, 0x55, 0x01, 0x02}

2.4.uart_rxclear(ch)

清空串口接收缓冲区函数uart_rxclear(ch) 是 HMI 系统提供的串口接收缓冲区管理接口,用于立即清空指定串口通道(ch)的接收缓冲区中所有未读取的数据。该函数在自定义串口通信协议开发中至关重要,常用于通信初始化、错误恢复、协议同步等场景,避免残留数据干扰后续帧解析。

📊 参数说明

参数 类型 说明
ch number 串口号
• 通常:0 ,具体编号依 HMI 硬件配置而定

2.5.calc_crc16(data)

Modbus CRC-16 校验计算函数calc_crc16(data) 是 HMI 系统内置的标准 Modbus CRC-16(CRC-16-MODBUS)校验算法实现,用于对指定数据块计算 16 位循环冗余校验码。该函数广泛应用于 Modbus RTU协议的帧完整性验证或组帧,确保串口通信中数据的可靠性。

📊 参数说明

项目 类型 说明
参数
data table 待校验的原始数据
• 以 Lua 表形式传递,下标从 1 开始。 示例:{0xAA, 0x55, 0x01, 0x02}
返回值
crc number 计算得到的 CRC-16 校验值
• 范围:0 ~ 65535 • 低字节在前,高字节在后(符合 Modbus 小端序)

3.协议举例

本例程以XGUS协议举例,将XGUS协议进行读写地址(大于0x1000)的内容映射到HMI的系统寄存器地址,演示如何在 VisualHMI 中使用 LUA 脚 本实现自定义串口协议通信。系统与外设通过主串口或副串口进行双向交互。

3.1.协议帧格式

XGUS协议格式如下所示:

定义 帧头 长度 指令 数据 CRC校验(可选)
长度 2 byte 1 byte 1 byte 最大249byte(包含地址长度) 2 byte
说明 0x5AA5 0x00~0xFF (指令+数据+校验的字节数目) 0x80/0x81/0x82/0x83 ... CRC-16(x16+x15+x2+1)

📌 说明

  • 数据长度 = 地址(2 byte) + 数据内容(n byte) = n + 2
  • 指令:0x82(写),0x83(读)

以向变量地址0x1000写入数值2为例,不启用CRC校验时,指令为:5A A5 05 82 1000 0002

5AA5 帧头
05 指令长度,单位字节(bytes)
82 写入变量地址指令
1000 变量地址
0002 被写入的数据2,按字(word)写入

4.应用

4.1.工程设置

使用主动读取,在工程设置中,要禁用接收回调

4.2. 画面设置

在画面设置数值空间,关联系统地址LW1000~LW100F,LW2000和LW3000,用于指令中的数据映射

5.LUA设置

5.1.初始化

在脚本初始化中,加载XGUS.lua文件,创建新的实例,参数设置:串口号(默认0),帧头格式(默认0x5AA5)。是否开启CRC(默认关闭),是否开启写回调(默认关闭)

--main.lua

function on_init()
    dofile('XGUS.lua')
    xgus = XGUS:new(0,0x55AA,true,true)  --串口号,帧头格式,开启crc,开启写回调
end
--XGUS.lua

XGUS = {
    port = 1,       --端口号
    _runner = nil,
    head = 0x5AA5,  --帧头
    func = 0,       --命令
    addr = 0,       --地址
    count = 0,      --个数
    req = nil,      --请求数据
    crc = false,    --crc
    resp = false,   --写应答
}

function XGUS:new(port,head,crc,resp)
    local obj = {}
    setmetatable(obj,self)
    self.__index = self
    obj.port = port or 0
    obj.head = head or 0x5AA5
    obj.crc = crc or false
    obj.resp = resp or false

    obj.recvMsg = self.recv
    obj.sendMsg = self.send

    return obj
end

5.2.串口接收(主动)

on_run 回调中调用 XGUS:run()XGUS:run() 内部使用协程实现非阻塞轮询,持续检查串口是否有新数据,并处理请求。

轮询主动读取串口是否有数据,如果数据达到符合:“帧头+指令+长度+地址”的指令长度,对获取到的指令进行解析,判断判断帧头格式是否合法,记录数据长度,读写指令,数据,XGUS:wait()等待读取,直至获取数据长度的指令数据。在开启CRC情况下对指令进行校验比对。指令正确则将指令传入数据请求处理

--main.lua

_EN_SET_DATA_ = false

function on_run(screen)
    _EN_SET_DATA_ = true
    xgus:run()
    _EN_SET_DATA_ = false
end
--XGUS.lua

--用户定时调用此接口
--使用协程可以打断或恢复任务,防止阻塞主线程
--方便执行耗时的任务流程
function XGUS:run()
    if self.runner==nil then
        self._runner = coroutine.create(function() self:_run() end)
    end
    coroutine.resume(self._runner)
end

--循环取消息执行,仅内部使用
function XGUS:_run()
    while(true) do
        if self:recvMsg() then
          self:processMsg()
        else
          coroutine.yield()
        end
    end
end

--主动读串口数据
function XGUS:recv()

    if uart_rxsize(self.port)<=0 then
        return false
    end

    if self:wait(function() return uart_rxsize(self.port)>=6 end)~=true then
        uart_rxclear(self.port)
        return false
    end

    local req = uart_recv(self.port,6)

    local cmd_head = (req[1]<<8) | req[2]   --帧头
    self.count = req[3]                     --指令长度
    self.func = req[4]                      --命令(读写命令)
    self.addr = (req[5]<<8) | req[6]        --地址

    if cmd_head ~= self.head or (self.func ~= 0x82 and self.func ~= 0x83) then return false end

    local totoalSize = 3+self.count
    local remainSize = totoalSize-6

    if self:wait(function() return uart_rxsize(self.port)>=remainSize end)
    then
        local req_remain = uart_recv(self.port,remainSize)
        local count = 6
        for k,v in ipairs(req_remain) do
            count = count+1
            req[count] = v
        end
    end
    self.req = req

    uart_rxclear(self.port)

    if self.crc == true then
        local crc_req = req[#req]<<8|req[#req-1]

        local str = {}
        for i = 4, #req-2 do str[i-3] = req[i] end

        local crc_check = calc_crc16(str)
        if crc_check ~= crc_req then
            return false
        end
    end

    return true
end

function XGUS:wait(when)
    local tickstart = get_tick_count()
    while when()~=true do
        local diff = get_tick_count()-tickstart
        if (diff<0) then diff = 0xFFFFFFFF-tickstart-diff+1 end
        if diff>self.timeout then
            return false
        end
        coroutine.yield()
    end

    return true
end

5.3.指令解析

对获取到的指令进行解析处理,解析指令读写格式,对应地址,数据,是否应答处理:

  • 数据:
    • 小于0x1000:读、写指令传到回调函数on_cmd_resp_xgus()中,需要在回调函数内自行处理
    • 大于等于0x1000:写指令映射到系统寄存器(LW1000~LWFFFF);读指令读取对应的LW寄存器地址值,组成指令应答
  • 写应答:启用写应答,收到写指令会自动回发应答指令
--XGUS.lua

--指令判断执行(检查)
function XGUS:processMsg()

    local func = self.func
    local write_count = (self.count - ((self.crc == true) and 5 or 3) + ((self.count+1)%2))//2

    if func == XGUSFunction.write then  --写
        local datas = {}

        if self.addr >= 0x1000 then  --用户寄存器
            for i = 1, write_count do
                if self.req[5+i*2] == nil then self.req[5+i*2] = 0 end
                if self.req[6+i*2] == nil then self.req[6+i*2] = 0 end  --单字节补低八位
                datas[i] = self.req[5+i*2]<<8 | self.req[6+i*2] -- 解析写入数据
            end
            set_array(VT_LW, self.addr, datas)
        else    --系统寄存器
            for i = 1, write_count do
                datas[i] = self.req[6+i] -- 解析写入数据
            end
            on_cmd_resp_xgus(self.addr,write_count,func,datas)
        end

        if self.resp == true then  --写应答
            self:sendMsg({((self.head)>>8),(self.head&0xFF),0x03,0x82,0x4F,0x4B})
        end

    elseif func == XGUSFunction.read then  --读

        if self.addr >= 0x1000 then
            local datas = {((self.head)>>8),(self.head&0xFF),(4+self.req[7]*2),0x83,(self.addr>>8),(self.addr&0xFF),((self.req[7]*2)&0xFF)}
            for i = 1, self.req[7] do
                local num = get_uint16(VT_LW, self.addr+(i-1))
                datas[6+i*2] = (num >> 8) & 0xFF
                datas[7+i*2] = num & 0xFF
            end
            self:sendMsg(datas)
        else
            on_cmd_resp_xgus(self.addr,self.req[7],func,{})
        end
    end
end
--main.lua

function on_cmd_resp_xgus(addr,count,wr,data)

    --print('addr = '..(string.format('%04X',addr))..' , count = '..count..' , '..((wr == 0) and 'read' or 'write'))
    if wr == 0x82 then  --写
        print('write addr num : '..count)

    elseif wr == 0x83 then  --读
        print('read addr num : '..count)
    end
end

5.4.数据发送

提供对外客调函数接口XGUS:dataSend(addr,data),作用是主动对外发写指令,数据可以是 'number' 类型或 'table' 类型

发送指令通过函数XGUS:send(resp),计算CRC校验后发送

--XGUS.lua

--写数据
function XGUS:dataSend(addr,data)

    if type(data) ~= 'table' and type(data) ~= 'number' then
        print('set data type error!')
        return
    end

    if type(addr) ~= 'number' then
        print('set addr type error!')
        return
    end

    local send_buff = {}

    send_buff[1] = (self.head>>8) & 0xFF
    send_buff[2] = self.head & 0xFF
    send_buff[3] = 1+2+((type(data) == 'table') and #(data) or 2)  --指令+地址+数据+CRC(选用)
    send_buff[4] = 0x82
    send_buff[5] = (addr>>8) & 0xFF
    send_buff[6] = addr & 0xFF
    if type(data) == 'number' then 
        send_buff[7] = (data>>8) & 0xFF
        send_buff[8] = data & 0xFF
    else
        for i = 1, #data do
            send_buff[5+(i*2)] = (data[i]>>8) & 0xFF
            send_buff[6+(i*2)] = data[i] & 0xFF
        end
    end

    self:sendMsg(send_buff)

end

function XGUS:send(resp)

    if self.crc == true then
        local crc_buff = {}
        resp[3] = resp[3] + 2 -- crc+2字节
        for i = 4, #resp do crc_buff[i - 3] = resp[i] end
        local crc_check = calc_crc16(crc_buff)

        resp[#resp + 1] = (crc_check >> 0) & 0xFF
        resp[#resp + 1] = (crc_check >> 8) & 0xFF
    end

    uart_send(self.port,resp)
end

6. 运行预览

运行虚拟屏,与串口工具进行通讯测试,对地址0x1000进行读写测试,如下图所示:

Copyright ©Dacai all right reserved,powered by Gitbook该文件修订时间: 2026-02-10 11:22:39

results matching ""

    No results matching ""