RS485 Modbus主机教程(繁版)

RS485 Modbus主机教程(繁版)

前言

前言

本文章参考 FlexLua 官网教程。

繁版和简版区别:

  1. 繁版更灵活,使用起来稍微繁琐一点,但能实现更灵活的 Modbus 主机通信功能;
  2. 简版使用起来非常简单,但主要专注于最频繁使用的 Modbus 读寄存器/读线圈的操作(例如 01、02、03和04功能码)

打开网易新闻 查看精彩图片

一、介绍

一、介绍

ShineBlink 提供Modbus主机库函数,方便开发者可以很容易实现Modbus主机功能,因此Core可以很容易扮演Modbus通信网络的主机角色,比如通过RS485串口读写从机设备。

Modbus主机库函数非常简单,仅有如下两个函数:

  1. LIB_MbRtuMasterSendTrans():将主机需要下发的命令转换成符合Modbus协议格式的字节流,该字节流以table数组形式返回。
  2. LIB_MbRtuMasterRecvTrans():将从机发来的应答数据字节流转换成更直观的结果,以方便程序后续处理。

目前支持的功能为:01,02,03,04,05,06,0f,10:

功能码功能介绍

01 | 读线圈 |
02 | 读离散量输入 |
03 | 读保持寄存器 |
04 | 读输入寄存器 |
05 | 写单个线圈 |
06 | 写单个寄存器 |
0F | 写多个线圈 |
10 | 写多个寄存器 |

二、功能码案例教程

二、功能码案例教程

基础演示代码框架:

以下代码是一个完整的演示Modbus主机读线圈功能的代码,可以作为后面其他功能码的代码框架,后面每个案例仅展示关键代码,就不占用篇幅了。

--配置Uart1作为485接口,初始默认波特率9600,并且D6作为自动收发切换引脚
LIB_Uart1Rs485Config("BAUDRATE_9600","D6")
--设置开发板上的"BTN1"(占用D10口)按键以低电平有效的方式检测按键动作
LIB_ButtonConfig("BTN1","D10","L")
--开始大循环
while(GC(1) == true)
do
--轮询按键事件
key_value = LIB_ButtonQuery("BTN1")
--如果开发板上的按键1短按过
if key_value == 1 then
--下发读线圈命令(设备地址=0x03,起始地址=1000,个数=3)
LIB_Uart1BlockSend(LIB_MbRtuMasterSendTrans("01", 0x03, 1000, 3))
--等待0.2秒的应答时间
LIB_DelayMs(200)
--判断刚刚的命令是否收到应答数据
recv_flag,recv_tab = LIB_Uart1Recv()
if recv_flag == 1 then
--解析从机发来的应答数据
result,content=LIB_MbRtuMasterRecvTrans("01",recv_tab)
if result > 0 then --打印content数组中的所有结果
for i, v in ipairs(content) do
print(i, v)
end
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end
end
end
end

(1)功能码"01":读线圈(bit)

(1)功能码"01":读线圈(bit)

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

--读线圈(设备地址=0x03,起始地址=1000,个数=3)
tab = LIB_MbRtuMasterSendTrans("01", 0x03, 1000, 3)
将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("01",recv_tab)
if result > 0 then
--content将会是一个table类型的数组,例如{0,1,0},表示返回的3个线圈的值
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

(2)功能码"02":读离散量输入(bit)

(2)功能码"02":读离散量输入(bit)

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

--读线圈(设备地址=0x03,起始地址=2000,个数=5)
tab = LIB_MbRtuMasterSendTrans("02", 0x03, 2000, 5)
然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("02",recv_tab)
if result > 0 then
--content将会是一个table类型的数组,例如{0,1,0,1,1},表示返回的5个离散输入量的值
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

(3)功能码"03":读保持寄存器(uint16)

(3)功能码"03":读保持寄存器(uint16)

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

--读保持寄存器(设备地址=0x03,起始地址=3500,个数=4)
tab = LIB_MbRtuMasterSendTrans("03", 0x03, 3500, 4)
然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("03",recv_tab)
if result > 0 then
--content将会是一个table类型的数组,例如{0xffff,0xffec,0x4133,0x3333},表示返回的4个保持寄存器的值
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

额外重要知识:

Modbus通讯中,寄存器有可能按照各种不同的方式存储(例如LONG AB CD,LONG CD BA, LONG BA CD,FLOAT AB CD,FLOAT CD AB等等),所以如果需将Modbus多个保持寄存器的原始16位数据合成实际数值,可借用功能强大的LIBBC()函数来实现,这样可避免复杂的Lua代码。LIBBC()的转换功能很多,详细请在API手册中查阅,下面只介绍其中两种常见的:

  1. 例如上面收到的0xffff,0xffec实际为0xffffffec的拆分,如果按照Modbus种常用的数据格式(LONG AB CD)它实际是 "-20" 的补码形式:
--将0xffff和0xffec转换成-20
val = LIB_BC("BYTE16_I32", content[1], content[2])
print(string.format("val=%d", val))--打印结果:val=-20

  1. 例如上面收到的0x4133,0x3333实际为0x41333333的拆分,如果按照Modbus种常用的数据格式(Float AB CD)它实际是IEEE-754格式浮点数 "11.2" 的形式:
--将0x4133和0x3333转换成11.2
val = LIB_BC("BYTE16_F32", content[3], content[4])
print(string.format("val=%f", val))--打印结果:val=11.2

(4)功能码"04":读输入寄存器(uint16)

(4)功能码"04":读输入寄存器(uint16)

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

--读输入寄存器(设备地址=0x03,起始地址=4500,个数=4)
tab = LIB_MbRtuMasterSendTrans("04", 0x03, 4500, 4)
然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("04",recv_tab)
if result > 0 then
--content将会是一个table类型的数组,例如{0xffff,0xffec,0x4133,0x3333},表示返回的4个保持寄存器的值
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

额外重要知识:

Modbus通讯中,寄存器有可能按照各种不同的方式存储(例如LONG AB CD,LONG CD BA, LONG BA CD,FLOAT AB CD,FLOAT CD AB等等),所以如果需将Modbus多个输入寄存器的原始16位数据合成实际数值,可借用功能强大的LIBBC()函数来实现,这样可避免复杂的Lua代码。LIBBC()的转换功能很多,详细请在API手册中查阅,下面只介绍其中两种常见的:

  1. 例如上面收到的0xffff,0xffec实际为0xffffffec的拆分,如果按照Modbus种常用的数据格式(LONG AB CD)它实际是 "-20" 的补码形式:
--将0xffff和0xffec转换成-20
val = LIB_BC("BYTE16_I32", content[1], content[2])
print(string.format("val=%d", val))--打印结果:val=-20

  1. 例如上面收到的0x4133,0x3333实际为0x41333333的拆分,如果按照Modbus种常用的数据格式(Float AB CD)它实际是IEEE-754格式浮点数 "11.2" 的形式:
--将0x4133和0x3333转换成11.2
val = LIB_BC("BYTE16_F32", content[3], content[4])
print(string.format("val=%f", val))--打印结果:val=11.2

(5)功能码"05":写单个线圈(bit)

(5)功能码"05":写单个线圈(bit)

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

--写单个线圈(设备地址=0x03,地址=0x0000的线圈)的值为0
tab = LIB_MbRtuMasterSendTrans("05", 0x03, 0x0000, 0)
--写单个线圈(设备地址=0x03,地址=0x0000的线圈)的值为1
tab = LIB_MbRtuMasterSendTrans("05", 0x03, 0x0000, 1)
然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("05",recv_tab)
if result > 0 then
print("Write single coil ok")
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

(6)功能码"06":写单个寄存器(uint16)

(6)功能码"06":写单个寄存器(uint16)

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

--写单个寄存器(设备地址=0x03,地址=0x0000的寄存器)的值为0x1234
tab = LIB_MbRtuMasterSendTrans("06", 0x03, 0x0000, 0x1234)
然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("06",recv_tab)
if result > 0 then
print("Write single register ok")
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

(7)功能码"0F":写多个线圈

(7)功能码"0F":写多个线圈

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

data = {1,0,1,1}
--将data数组中的4个线圈值写入设备地址为0x03,线圈地址为0x0000~0x0003的四个位置中。注:函数中会根据起始地址0x0000,以及data数组中的元素个数,自动计算线圈地址范围为0x0000~0x0003
tab = LIB_MbRtuMasterSendTrans("0F", 0x03, 0x0000, data)
然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:
--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("0F",recv_tab)
if result > 0 then
print("Write multiple coils ok")
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

(8)功能码"10":写多个寄存器

(8)功能码"10":写多个寄存器

首先生成主机需要发送的modbus命令字节流,以table形式返回,以供类似LIB_Uart0Send()这种串口发送函数使用:

data = {0xffff,0xffec,0x4133,0x3333}
--将data数组中的4个16bit值写入设备地址为0x03,寄存器地址为3500~3503的四个位置中。注:函数中会根据起始地址3500,以及data数组中的元素个数,自动计算寄存器的地址范围为3500~3503
tab = LIB_MbRtuMasterSendTrans("10", 0x03, 3500, data))

然后将接收到的从机应答字节流(recv_tab)进行解析,并返回结果:

--解析从机发来的应答字节流
result,content=LIB_MbRtuMasterRecvTrans("10",recv_tab)
if result > 0 then
print("Write multiple registers ok")
else
err_code = -128 - result --转换成实际modbus异常码
print(string.format("Fail,err_code = %d",err_code))
end

额外重要知识:

Modbus通讯中,寄存器有可能按照各种不同的方式存储(例如LONG AB CD,LONG CD BA, LONG BA CD,FLOAT AB CD,FLOAT CD AB等等),所以开发者在Lua编程中如果需要将某个带符号整型数据或浮点数据拆分成适合Modbus传输的原始16位数据,可借用功能强大的LIBBC()函数来实现,这样可避免复杂的Lua代码。LIBBC()的转换功能很多,详细请在API手册中查阅,下面只介绍其中两种常见的:

  1. 例如需要将“-20”这个带符号整型数据以(LONG AB CD)形式通过Modbus字节流发送,并写入从机
  2. 例如需要将“11.2”这个浮点小数以(FLOAT AB CD)形式通过Modbus字节流发送,并写入从机
data = {} --定义一个空数组,用来缓存下面这些需要写入的16bit寄存器值
aa = -20
--由于-20的(LONG AB CD)形式为32位补码0xffffffec,所以下面的函数会返回val1=0xffff,val2=0xffec
val1, val2 = LIB_BC("I32_BYTE16", aa)
data[1] = val1
data[2] = val2
--由于11.2的(FLOAT AB CD)形式为32位0x41333333,所以下面的函数会返回val1=0x4133,val2=0x3333
ff = 11.2
val1, val2 = LIB_BC("F32_BYTE16", ff)
data[3] = val1
data[4] = val2
--将data数组中的4个16bit值写入设备地址为0x03,寄存器地址为3500~3503的四个位置中。注:函数中会根据起始地址3500,以及data数组中的元素个数,自动计算寄存器的地址范围为3500~3503
tab = LIB_MbRtuMasterSendTrans("10", 0x03, 3500, data)