1 Modbus协议介绍及解析
1.1 Modbus简介
Modbus是一种广泛应用于工业控制领域串行通信协议,以其开放性、高可靠性、高效简单性、免费等优点,成为了工业领域通信协议的业界标准,是工业现场电子设备之间常用的连接方式。Modbus按其格式可分为Modbus-RTU,Modbus-ASCII,Modbus-TCP
,其中前两者适用于串行通信控制网络中,例如RS485,RS232等,而Modbus-TCP
主要应用于基于以太网TCP/IP通信的控制网络中。通过此协议,控制器相互之间、或控制器和其它设备之间进行通信。Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。一般将主控设备方所使用的协议称为Modbus Master
,从设备方使用的协议称为Modbus Slave
。典型的主设备包括工控机和工业控制器等;典型的从设备如PLC可编程控制器等。Modbus通讯物理接口可以选用串口(包括RS232和RS485),也可以选择以太网口。
1.2 报文类型及格式
1.2.1 协议描述
Modbus协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。特定总线或网络上的 Modbus协议映射能够在应用数据单元(ADU)上引入一些附加域,启动Modbus事务处理的客户机创建Modbus 应用数据单元。如图1所示。Modbus-RTU方式的通讯数据帧格式如图2所示。
1.2.2 通讯信息传输过程
当命令由主机发送至从机时,符合相应地址码的从机处理命令,根据功能码作相应任务。如果CRC校验无误,则执行相应的任务,然后把数据返送给主机。如果CRC校验出错就不返回任何信息,主机应当有相应的超时处理。如果接收正确,但不能处理,返回异常报文。对字型数据发送顺序为先高字节后低字节;对浮点数按照正常的顺序发送;信文总长度(包括地址码和CRC校验码)不超过256字节。
1.2.3 应用数据单元
应用数据单元(ADU)由地址码、功能码、数据区、错误校验码构成。
-
地址码
地址码是通讯信息帧的第一字节,从0到255。每个从机有唯一的地址码,并且只有符合地址码的从机才能响应回送信息。0xFF为广播地址。 -
功能码
功能码向服务器指示将执行哪种操作。Modbus协议建立了客户机启动的请求格式,用一个字节编码 Modbus数据单元的功能码域,当从客户机向服务器设备发送报文时,功能码域通知服务器执行哪种操作。 -
数据区
数据区可以是数据(如:开关量输入/输出、模拟量输入/输出、寄存器等等)、参考地址等。均为二进制数。各种数据参考地址在综合控制装置中均从1开始,在通讯过程中则从0开始,所以读写地址N时使用的地址数据为N-1。 -
错误校验码(CRC校验)
由于电子噪声或一些其它干扰,信息在传输过程中有时会发生错误,CRC校验可以检验主机或从机在通讯数据传送过程中的信息是否有误,错误的数据可以放弃(无论是发送还是接收),这样增加了系统的安全和效率。Modbus通讯协议的CRC(冗余循环码)包含2个字节,即16位二进制数。CRC码由发送设备(主机)计算,放置于发送信息帧的尾部。接收信息的设备(从机)再重新计算接收到信息的CRC,比较计算得到的CRC是否与接收到的相符,如果两者不相符,则表明出错。在进行CRC计算时只用8个数据位,起始位及停止位,如有奇偶校验位也包括奇偶校验位,都不参与CRC计算。扫描二维码关注公众号,回复: 15421949 查看本文章
1.3 主要功能码说明
1.3.1 读取内部线圈(接点)
功能码:0x01
描述:读从机多个内部线圈数据。不支持广播命令。(地址0XXXX)
查询
查询信息指定要读取的线圈开始地址和线圈数量。线圈地址从0开始。
以读取地址为17的从机中第7~15号内部线圈的值为例:
响应
响应报文格式:
计算机发送命令:[设备地址] [功能码01] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
设备响应:[设备地址] [功能码01] [返回的字节个数][数据1][数据2]…[数据n][CRC校验的低8位] [CRC校验的高8位]
1.3.2 读取开关量输入
功能码:0x02
描述:读从机多个开入。不支持广播命令。(地址1XXXX,只读)
查询和响应同0x01功能码
1.3.3 写数字量(线圈状态)
功能码:0x05
例:[11][05][00][AC][FF][00][CRC低][CRC高]
1.功能码:写数字量的命令号固定为05。
2.需下置的寄存器地址高8位,低8位:表明了需要下置的开关的地址。
3.下置的数据高8位,低8位:表明需要下置的开关量的状态。例子中为把该开关闭合。注意,此处只可以是[FF][00]表示闭合[00][00]表示断开,其他数值非法。
4.此命令一条只能下置一个开关量的状态。
计算机发送命令:[设备地址] [功能码05] [需下置的寄存器地址高8位] [低8位] [下置的数据高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
设备响应:如果成功把计算机发送的命令原样返回,否则不响应。
1.3.4 取多个保持寄存器(数据寄存器)
功能码:0x03
描述:读从机多个保持寄存器二进制数据。不支持广播命令。(地址4XXXX)
查询
查询信息指定要读取的寄存器开始地址和寄存器数量。寄存器地址从0开始。
以读取地址为17的从机中第108~第110号寄存器为例:
查询报文格式
响应
响应报文格式:
计算机发送命令:[设备地址] [功能码03] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
设备响应:[设备地址] [功能码03] [返回的字节个数][数据1][数据2]…[数据n][CRC校验的低8位] [CRC校验的高8位]
1.3.5 读取多个输入寄存器(模拟输入寄存器)
功能码:0x04
描述:读从机多个模拟输入寄存器二进制数据。不支持广播命令。(地址3XXXX,只读)
同0x03功能码
1.3.6 写单个寄存器
功能码:0x06
描述:写从机单个寄存器。(地址4XXXX)
命令: 命令指定要写的寄存器地址和写入的值。地址从900开始。
以将数据1写入地址为1的从机中第900号寄存器为例:
命令报文格式:
响应
响应报文格式:与命令格式相同。
计算机发送命令:[设备地址] [功能码06] [需下置的寄存器地址高8位] [低8位] [下置的数据高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
1.3.7 写多个保持寄存器
功能码:0x10
描述:写从机多个保持寄存器。(地址4XXXX)
命令: 命令指定要写的寄存器地址和写入的值。地址从900开始。
以将数据1写入地址为1的从机中第900、901号寄存器为例:
命令报文格式:
响应
响应报文格式:
计算机发送命令:[设备地址] [功能码10] [需下置的寄存器起始地址高8位] [低8位] [要写的寄存器数量的高字节] [低字节] [要写的字节数(等于寄存器数目*2)] [下置的数据高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
设备响应:如果成功则响应:设备地址,功能码[0x10],寄存器起始地址高字节,低字节,要写的寄存器数量的高字节,低字节,CRC校验低字节,高字节
2 使用python完成Modbus协议从云端获取信息
云端信息请求分为两大类,一是通过TCP协议
进行请求,二是通过UDP协议
进行请求,下面主要分析使用python进行TCP协议
进行请求,UDP协议
除了需要每次请求数据时都要与进行连接以外,其它数据请求格式和解析接收的数据与TCP一致。
2.1 TCP方式请求数据
- 建立tcp连接
tcp = socket.socket(socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP)
tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp.settimeout(5)
try:
tcp.connect(('demo-monitor.igong.com', 8002))
print("TCP连接成功")
except:
print("连接TCP失败")
sys.exit(1)
- 生成crc16校验位
def crc16(string):
# data = bytes.fromhex(string)
data = string
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if ((crc & 1) != 0):
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return hex(((crc & 0xff) << 8) + (crc >> 8))
- 指令发送和接收
def getStain(cmd, num, time):
# print(cmd)
# print(num)
cmd = bytes.fromhex(cmd)
crc = crc16(cmd)
crc = bytes.fromhex(crc[2:])
cmd = cmd + crc
# print(cmd)
# 发送对应的指令
tcp.send(cmd)
try:
data = tcp.recv(8192)
except socket.timeout:
print("超时")
sys.exit(1)
crc = data[-2:]
crc1 = crc16(data[:-2])
crc1 = crc1[2:]
if len(crc1) == 3:
crc1 = '0' + crc1
crc1 = bytes.fromhex(crc1)
if crc != crc1:
print("CRC16校验失败!")
sys.exit(2)
yb, wd = struct.unpack('>ii', data[4:12])
yb = yb / 100.0
wd = wd / 100.0
print("应变:", yb, "温度:", wd)
print(time)
yb = str(yb)
wd = str(wd)
AddData(num, yb, wd, time)
- 数据存储到mysql
# 连接数据
def MySQLConnect():
connection = pymysql.connect(
host='localhost', # IP,MySQL数据库服务器IP地址
port=3306, # 端口,默认3306,可以不输入
user='你的名字', # 数据库用户名
password='你的密码', # 数据库登录密码
database='sensor', # 要连接的数据库
charset='utf8' # 字符集,注意不是'utf-8'
)
return connection
# 插入数据到数据库
def AddData(num, yb, wd, time):
# 连接数据库
conn = MySQLConnect()
# 使用cursor()方法创建一个游标对象cursor
cursor = conn.cursor()
# 插入数据库
sql = "INSERT INTO strain_sensor(id ,mic, strain_temp, time) VALUES (%s,%s,%s,%s); "
cursor.execute(sql, [num, yb, wd, time])
# 提交事务
conn.commit()
# 关闭游标
cursor.close()
# 关闭数据库连接
conn.close()
将数据存入mysql数据库中,Modbus —TCP云端数据采集就完成了,下面是完整Modbus—TCP采集代码:
import socket
import sys
import struct
import time
import tcp
import threading
import _thread
import pymysql
# 本程序是应变传感器采集,可以通过发送ALL指令进行全部传感器的采集
def crc16(string):
# data = bytes.fromhex(string)
data = string
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if ((crc & 1) != 0):
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return hex(((crc & 0xff) << 8) + (crc >> 8))
# 连接数据
def MySQLConnect():
connection = pymysql.connect(
host='localhost', # IP,MySQL数据库服务器IP地址
port=3306, # 端口,默认3306,可以不输入
user='你的用户名', # 数据库用户名
password='你的密码', # 数据库登录密码
database='sensor', # 要连接的数据库
charset='utf8' # 字符集,注意不是'utf-8'
)
return connection
# 插入数据到数据库
def AddData(num, yb, wd, time):
# 连接数据库
conn = MySQLConnect()
# 使用cursor()方法创建一个游标对象cursor
cursor = conn.cursor()
# 插入数据库
sql = "INSERT INTO strain_sensor(id ,mic, strain_temp, time) VALUES (%s,%s,%s,%s); "
cursor.execute(sql, [num, yb, wd, time])
# 提交事务
conn.commit()
# 关闭游标
cursor.close()
# 关闭数据库连接
conn.close()
# 获取一次数据
def getStain(cmd, num, time):
# print(cmd)
# print(num)
cmd = bytes.fromhex(cmd)
crc = crc16(cmd)
crc = bytes.fromhex(crc[2:])
cmd = cmd + crc
# print(cmd)
# 发送对应的指令
tcp.send(cmd)
try:
data = tcp.recv(8192)
except socket.timeout:
print("超时")
sys.exit(1)
crc = data[-2:]
crc1 = crc16(data[:-2])
crc1 = crc1[2:]
if len(crc1) == 3:
crc1 = '0' + crc1
crc1 = bytes.fromhex(crc1)
if crc != crc1:
print("CRC16校验失败!")
sys.exit(2)
yb, wd = struct.unpack('>ii', data[4:12])
yb = yb / 100.0
wd = wd / 100.0
print("应变:", yb, "温度:", wd)
print(time)
yb = str(yb)
wd = str(wd)
AddData(num, yb, wd, time)
def setCircleData(cmd, num):
count = 0
flag = 0
last = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
last1 = time.time()
while True:
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
now1 = time.time()
# print(now)
if (flag == 0):
count = count + 1
flag = 1
getStain(cmd, num, last)
last = now
last1 = now1
if now1 - last1 > 5:
if count >= 5:
str = input("请选择是否继续采集(y表示继续,n表示退出):")
if str == 'y':
count = 0
continue
else:
break
count = count + 1
getStain(cmd, num, now)
last = now
last1 = now1
# 同时采集全部应变传感器
def setCircleAll(cmd):
flag = 0
count = 0
last = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
last1 = time.time()
while True:
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
now1 = time.time()
# count=0
# print(now)
if (flag == 0):
flag = 1
count = count + 1
for cmd1 in cmd:
id = '00' + cmd1[0:2]
# print(id)
# print(cmd1)
getStain(cmd1, id, last)
last = now
last1 = now1
if now1 - last1 > 5:
if count >= 5:
str = input("请选择是否继续采集(y表示继续,n表示退出):")
if str == 'y':
count = 0
continue
else:
break
count = count + 1
for cmd1 in cmd:
id = '00' + cmd1[0:2]
getStain(cmd1, id, now)
last = now
last1 = now1
if __name__ == '__main__':
tcp = socket.socket(socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP)
tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp.settimeout(5)
try:
tcp.connect(('demo-monitor.igong.com', 8002))
print("TCP连接成功")
except:
print("连接TCP失败")
sys.exit(1)
flag = 0
while True:
print("具体指令给格式为000+传感器编号(1,2,3,4,5)")
num = input("请输入采集传感器的编号(All表示采集全部传感器,0表示退出采集):")
if num == '0001':
cmd = '010300010002'
setCircleData(cmd, num)
elif num == '0002':
cmd = '020300010002'
setCircleData(cmd, num)
elif num == '0003':
cmd = '030300010002'
setCircleData(cmd, num)
elif num == '0004':
cmd = '040300010002'
setCircleData(cmd, num)
elif num == '0005':
cmd = '050300010002'
setCircleData(cmd, num)
elif num == 'All':
cmd = {
'010300010002', '020300010002', '030300010002', '040300010002', '050300010002'}
setCircleAll(cmd)
elif num == '0':
break
else:
print("输入信息不合法,请重新输入")
2.2 UDP方式请求数据
UDP连接
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
udp.settimeout(5)
完整代码如下:
import socket
import struct
import sys
import time
import pymysql
#实现采集温度传感器和静力水准仪
def crc16(string):
#data = bytes.fromhex(string)
data=string
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if ((crc & 1) != 0):
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return hex(((crc & 0xff) << 8) + (crc >> 8))
#连接数据库
def MySQLConnect():
connection = pymysql.connect(
host='localhost', # IP,MySQL数据库服务器IP地址
port=3306, # 端口,默认3306,可以不输入
user='你的用户名', # 数据库用户名
password='你的密码', # 数据库登录密码
database='sensor', # 要连接的数据库
charset='utf8' # 字符集,注意不是'utf-8'
)
return connection
#插入温湿度采集到数据库
def AddData1(wd,sd,time):
# 连接数据库
conn = MySQLConnect()
# 使用cursor()方法创建一个游标对象cursor
cursor = conn.cursor()
# 插入数据库
sql = "INSERT INTO temp_hum_sensor(temp, hum, time) VALUES (%s,%s,%s); "
cursor.execute(sql, [wd, sd, time])
# 提交事务
conn.commit()
# 关闭游标
cursor.close()
# 关闭数据库连接
conn.close()
#插入静力水准仪采集到数据库
def AddData2(id,water_level,time):
# 连接数据库
conn = MySQLConnect()
# 使用cursor()方法创建一个游标对象cursor
cursor = conn.cursor()
# 插入数据库
sql = "INSERT INTO static_level(id, water_level, time) VALUES (%s,%s,%s); "
cursor.execute(sql, [id, water_level, time])
# 提交事务
conn.commit()
# 关闭游标
cursor.close()
# 关闭数据库连接
conn.close()
#采集温度传感器的数据
def getDataTemp(cmd):
#flag标志采集的次数
flag=0
last = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(last)
last1 = time.time()
cmd = bytes.fromhex(cmd)
#print(cmd)
crc = crc16(cmd)
crc = bytes.fromhex(crc[2:])
#得到发送的指令(modbus协议定义内容+校验)
cmd = cmd + crc
udp.sendto(cmd, ('demo-monitor.igong.com', 8001))
try:
data, addr = udp.recvfrom(8192)
except socket.timeout:
print("超时")
sys.exit(1)
crc = data[-2:]
crc1 = crc16(data[:-2])
crc1 = crc1[2:]
if (len(crc1) == 3):
crc1 = '0' + crc1
crc1 = bytes.fromhex(crc1)
# print(crc1)
if crc != crc1:
print("CRC16校验失败!")
sys.exit(2)
# 解析数据
wd, sd = struct.unpack('>ii', data[4:12])
wd = wd / 100.
print("温度:", wd, "湿度:", sd)
AddData1(wd, sd, last)
flag=flag+1
while True:
now= time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
#print(s)
now1=time.time()
#每隔5s获取一次数据
if(now1-last1>5):
udp.sendto(cmd, ('demo-monitor.igong.com', 8001))
try:
data, addr = udp.recvfrom(8192)
except socket.timeout:
print("超时")
sys.exit(1)
crc = data[-2:]
crc2=bytes.hex(crc)
#print(crc2)
crc1 = crc16(data[:-2])
crc1=crc1[2:]
if(len(crc1)==3):
crc1='0'+crc1
#print(crc1)
crc1=bytes.fromhex(crc1)
#print(crc1)
if crc != crc1:
print("CRC16校验失败!")
sys.exit(2)
#解析数据
wd, sd = struct.unpack('>ii', data[4:12])
wd = wd / 100.0
#当前时间
print(now)
#获取得到的数据
print("温度:", wd, "湿度:", sd)
last=now
last1=now1
wd=str(wd)
sd=str(sd)
AddData1(wd,sd,now)
flag = flag + 1
if flag >= 5:
str1 = input("请选择是否继续采集(y表示继续,n表示退出):")
if str1 == 'y':
flag = 0
continue
else:
break
def getDataStaticLevel(cmd):
id=cmd[0:2]
#print(id)
if id=='02':
#print("2号")
id='00'+id
getData(id,cmd)
elif id=='03':
#print("3号")
id = '00' + id
getData(id,cmd)
elif id=='04':
#print("4号")
id = '00' + id
getData(id,cmd)
elif id=='05':
#print("5号")
id = '00' + id
getData(id,cmd)
def getData(id,cmd):
flag=0
last = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(last)
last1 = time.time()
cmd = bytes.fromhex(cmd)
crc = crc16(cmd)
crc = crc[2:]
if (len(crc) == 3):
crc = '0' + crc
crc = bytes.fromhex(crc)
cmd = cmd + crc
udp.sendto(cmd, ('demo-monitor.igong.com', 8001))
print("发送数据成功")
try:
data, address = udp.recvfrom(8192)
#print(data)
except socket.timeout:
print("超时")
sys.exit(1)
#print(len(data))
crc = data[-2:]
#print(data[:-2])
crc1 = crc16(data[:-2])
#print(crc1)
crc1=crc1[2:]
if len(crc1) == 3:
crc1 = '0' + crc1
#print(crc1)
crc1 = bytes.fromhex(crc1)
if crc != crc1:
print("CRC16校验失败!")
sys.exit(2)
#print(data[4:8])
nd = struct.unpack('>i', data[4:8])
#print(nd)
nd1 = nd[0]*10.0
nd1=str(nd1)
#print(last)
print("挠度:"+nd1)
AddData2(id,nd1,last)
flag=flag+1
while True:
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# print(s)
now1 = time.time()
# 每隔5s获取一次数据
if (now1 - last1 > 5):
udp.sendto(cmd, ('demo-monitor.igong.com', 8001))
try:
data, addr = udp.recvfrom(8192)
except socket.timeout:
print("超时")
sys.exit(1)
crc = data[-2:]
crc2 = bytes.hex(crc)
# print(crc2)
crc1 = crc16(data[:-2])
crc1 = crc1[2:]
if (len(crc1) == 3):
crc1 = '0' + crc1
# print(crc1)
crc1 = bytes.fromhex(crc1)
# print(crc1)
if crc != crc1:
print("CRC16校验失败!")
sys.exit(2)
# 解析数据
nd = struct.unpack('>i', data[4:8])
nd = nd[0] * 10.0
nd1=str(nd)
print(now)
print("挠度:" + nd1)
nd=str(nd)
AddData2(id, nd1, now)
last=now
last1=now1
flag = flag + 1
if flag >= 5:
str1 = input("请选择是否继续采集(y表示继续,n表示退出):")
if str1 == 'y':
flag = 0
continue
else:
break
if __name__ == '__main__':
print("开始程序")
print("程序相关说明:")
print("本程序采用UDP协议,其中当输入指令为0就退出整个程序。")
print("命令格式类似于地址(01,02,03,04,05)+03+传感器地址(0001)+传感器个数(0001,0002)")
print("例如:010300010002(温度传感器),020300010001(静力水准仪1)")
print("如果出现不合法指令就输出提示信息,并重新输入指令。")
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
udp.settimeout(5)
while True:
cmd = input("请输入相关命令:")
#print(len(cmd))
num = cmd[8:]
#print(num)
if num == '0001' and cmd[0:2] == '01':
print("此处不实现只对温度采集!!!")
elif num == '0001':
print("一个传感器")
getDataStaticLevel(cmd)
elif num == '0002':
print("两个传感器")
getDataTemp(cmd)
elif cmd == '0':
break
else:
print("指令不合法!!!")
2.3 运行结果
TCP传输程序运行结果如下:
数据库存储效果如下:
3 使用c语言完成modbus协议从云端服务器读取信息
下文介绍使用tcp方式请求数据的方法
3.1 新建项目
本文使用小熊猫ide
下载链接
新建一个TCP客户端应用
3.2 修改代码
- 初始化socket dll
通过ip连接服务器对应端口,以便于获取数据
WORD winsock_version = MAKEWORD(2,2);
WSADATA wsa_data;
if (WSAStartup(winsock_version, &wsa_data) != 0) {
printf("Failed to init socket!\n");
return 1;
}
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (client_socket == INVALID_SOCKET) {
printf("Failed to create server socket!\n");
return 2;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.S_un.S_addr = inet_addr(SERVER_IP);
if (connect(client_socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("Failed to connect server: %ld !\n", GetLastError());
return 3;
}
- 生成crc16校验码
uint16_t CRC_16(uint8_t *temp)
{
uint8_t i,j;
uint16_t CRC_1 = 0xFFFF; //声明CRC寄存区,也就是步骤1
for(i = 0;i < 6;i++) //这里的for循环说的是步骤6中的重复步骤 2 到步骤 5
{
CRC_1 ^= temp[i]; //这里就是步骤2,进行异或运算
for(j = 0;j < 8;j++) //用来将异或后的低八位全部移出的for循环
{
if(CRC_1 & 0x01) //判断低八位的最后一位是否为1,为1时执行下列语句,也就是步骤3说的移位判断与步骤5说的右移8次
{
/*一定要先移位,再异或*/
CRC_1 >>=1; //移位后再异或,就是步骤4
CRC_1 ^= 0xA001; //0xA001为0x8005的逆序
}
else //若不为1,则直接移位。
{
CRC_1 >>=1;
}
}
}
// CRC_1 = (((CRC_1 & 0xFF)<<8) + (CRC_1>>8));
// printf("%04x\r\n",CRC_1); //用于打印检测CRC校验码
return(CRC_1);
}
- 分析数据并取出
int ret = recv(client_socket, recv_data, BUFFER_SIZE, 0);
if (ret < 0) {
printf("Failed to receive data!\n");
break;
}
recv_data[ret]=0; // correctly ends received string
char yb[4],wd[4];
for(int i=0;i<4;i++){
//TODO
yb[i] = recv_data[4+i];
wd[i] = recv_data[8+i];
}
float mic = hexToDec(yb)/100.0;
float strain_temp = hexToDec(wd)/100.0;
printf("应变:%f\r\n",mic);
printf("温度:%f\r\n",strain_temp);
完整代码如下:
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <winsock2.h>
#include <math.h>
#include "stdint.h"
#define length_8 8 //定义一个宏,为传入8位16进制数的个数
#define PORT 8002
#define SERVER_IP "123.56.90.74"
#define BUFFER_SIZE 4196
const char* kExitFlag = "exit";
/* 返回ch字符在sign数组中的序号 */
int getIndexOfSigns(char ch)
{
if(ch >= '0' && ch <= '9')
{
return ch - '0';
}
if(ch >= 'A' && ch <='F')
{
return ch - 'A' + 10;
}
if(ch >= 'a' && ch <= 'f')
{
return ch - 'a' + 10;
}
return -1;
}
/* 十六进制数转换为十进制数 */
int hexToDec(char *source)
{
int sum = 0;
int t = 1;
int i, len=4;
char low,high;
for(int i=0,j=7;i<4;i++){
//TODO
high = (source[i] & 0xf0)>>4;
low = source[i] & 0x0f;
sum += high*pow(16,j--)+low*pow(16,j--);
}
return sum;
}
const unsigned char *fromhex(const char *str)
{
static unsigned char buf[512];
size_t len = strlen(str) / 2;
if (len > 512) len = 512;
for (size_t i = 0; i < len; i++) {
unsigned char c = 0;
if (str[i * 2] >= '0' && str[i*2] <= '9')
c += (str[i * 2] - '0') << 4;
if ((str[i * 2] & ~0x20) >= 'A' && (str[i*2] & ~0x20) <= 'F')
c += (10 + (str[i * 2] & ~0x20) - 'A') << 4;
if (str[i * 2 + 1] >= '0' && str[i * 2 + 1] <= '9')
c += (str[i * 2 + 1] - '0');
if ((str[i * 2 + 1] & ~0x20) >= 'A' && (str[i * 2 + 1] & ~0x20) <= 'F')
c += (10 + (str[i * 2 + 1] & ~0x20) - 'A');
buf[i] = c;
}
return buf;
}
uint16_t CRC_16(uint8_t *temp)
{
uint8_t i,j;
uint16_t CRC_1 = 0xFFFF; //声明CRC寄存区,也就是步骤1
for(i = 0;i < 6;i++) //这里的for循环说的是步骤6中的重复步骤 2 到步骤 5
{
CRC_1 ^= temp[i]; //这里就是步骤2,进行异或运算
for(j = 0;j < 8;j++) //用来将异或后的低八位全部移出的for循环
{
if(CRC_1 & 0x01) //判断低八位的最后一位是否为1,为1时执行下列语句,也就是步骤3说的移位判断与步骤5说的右移8次
{
/*一定要先移位,再异或*/
CRC_1 >>=1; //移位后再异或,就是步骤4
CRC_1 ^= 0xA001; //0xA001为0x8005的逆序
}
else //若不为1,则直接移位。
{
CRC_1 >>=1;
}
}
}
// CRC_1 = (((CRC_1 & 0xFF)<<8) + (CRC_1>>8));
// printf("%04x\r\n",CRC_1); //用于打印检测CRC校验码
return(CRC_1);
}
int main() {
// 初始化socket dll。
WORD winsock_version = MAKEWORD(2,2);
WSADATA wsa_data;
if (WSAStartup(winsock_version, &wsa_data) != 0) {
printf("Failed to init socket!\n");
return 1;
}
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (client_socket == INVALID_SOCKET) {
printf("Failed to create server socket!\n");
return 2;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.S_un.S_addr = inet_addr(SERVER_IP);
if (connect(client_socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("Failed to connect server: %ld !\n", GetLastError());
return 3;
}
char recv_data[BUFFER_SIZE+1];
while (true) {
uint8_t data[length_8];
printf("0+传感器编号(1,2,3,4,5)0300010002\r\n");
scanf("%s",data);
uint16_t crc;
unsigned char * cmd;
char crc1[8];
cmd = fromhex(data);
crc = CRC_16(cmd);
uint8_t a = 0xFF;
for(int i=0;i<6;i++){
//TODO
crc1[i] = cmd[i];
}
crc1[6] = a & crc;
crc1[7] = (crc >> 8) & a;
if (send(client_socket, crc1, 8, 0) < 0) {
printf("Failed to send data!\n");
break;
}
int ret = recv(client_socket, recv_data, BUFFER_SIZE, 0);
if (ret < 0) {
printf("Failed to receive data!\n");
break;
}
recv_data[ret]=0; // correctly ends received string
char yb[4],wd[4];
for(int i=0;i<4;i++){
//TODO
yb[i] = recv_data[4+i];
wd[i] = recv_data[8+i];
}
float mic = hexToDec(yb)/100.0;
float strain_temp = hexToDec(wd)/100.0;
printf("应变:%f\r\n",mic);
printf("温度:%f\r\n",strain_temp);
// printf("Receive data from server: \"%x\"\n",recv_data);
if (strcmp(data,kExitFlag)==0) {
printf("Exit!\n");
break;
}
}
closesocket(client_socket);
WSACleanup();
return 0;
}
3.3 实现效果
总结
通过本次实验,了解了如何使用Modbus协议进行通讯。Modbus是一种串行通信协议,其免费、简单,并且方便修改。但Modbus是主从方式通信,也就是说,不能同步进行通信,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。
参考:
https://blog.csdn.net/qinkaiword/article/details/119419055
https://blog.csdn.net/panda5_csdn/article/details/94332166