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寄存器地址值,组成指令应答
- 小于0x1000:读、写指令传到回调函数
- 写应答:启用写应答,收到写指令会自动回发应答指令
--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进行读写测试,如下图所示:
