文章目录
- 一、串口通信与温湿度项目实战
- 1、学习串口通信硬件:巩固RS-485串口硬件和通信基础知识
- 1.1、串行通信的数据流和格式
- 1.2、串口通信参数设置
- 1.3、modbus协议基础
- 1.4、数据存储和功能代码
- 1.5、modbus通信报文分析
- 2、主-从通信仿真测试
- 2.1、组件设计
- 2.2、创建温湿度测试项目
- 2.3、添加控制台项目Modbus_base用于测试
- 2.4、将功能封装成类库moudbusRTU通信库
- 2.5、用通信库完成项目开发
一、串口通信与温湿度项目实战
仿真软件
modbus poll 、modbus slave、虚拟串口软件
下载网址:https://blog.csdn.net/hanhui22/article/details/108344006
xcom串口助手
下载网址:https://blog.csdn.net/mshlc0728/article/details/109472277
1、学习串口通信硬件:巩固RS-485串口硬件和通信基础知识
1.1、串行通信的数据流和格式
原理:串行通信是将字节byte拆分成一个个的位bit后,在传输出去,收到数据的一方,再将这些位组合成原来的字节。要求传输格式必须统一,传输方式(节拍)必须一致。
起始位:通常用0表示,位于字符帧开头
数据位:通常包括5到8位数据,在起始位之后,先发送低位,后发送高位
奇偶校验位:用来检验数据传输过程中的是否有错误,站一位,包括偶校验(O)奇校验(E)无校验(N)
停止位:通常用1表示,便于接收端确定下一帧的起始位
1.2、串口通信参数设置
波特率: 表示每秒传输的位(比特,bit)的个数,也就是没秒传输的0和1的个数。常见波特率:4800b/s、9600b/s、19200b/s、38400b/s、115200b/s
串行通信格式(单字节协议):常用:(9600,N,8,1)波特率9600b/s,无奇偶校验位,8位数据位,1位停止位
1.3、modbus协议基础
通信协议:不同设备之间交换数据要遵循的规范。就好比人与人之间交流用的语言,必须要有语法。通常情况下,串行通信遵守Modbus协议,串口硬件(RS-232、RS-485、RS-422)都是按照Modbus协议。但是Modbus也可以用于以太网TCP/IP通信。
modbus与串口关系:1、串行通信物理接口:RS-232、RS-485、RS-422接口,是硬件。2、Modbus是国际标准的串行通信协议,是软件。
modbus与串行通信的关系:
1、前面所讲串行通信格式:表示一个字节的传输协议(9600,N,8,1),实际应用中数据传送都是多个串行字节组合到一起。问题:如何识别多个字节?也就是对多个串行字节传输和解析的标准怎么规定?Modbus就是这个作用。
2、Modbus:就是如何用串口一次连续传输多个有序字节的协议。它规定了一次发送多少给字节,以及字节顺序如何排列。
modbus网络传输的三种模式:
1、ASCII模式:MG标准信息交换码(0-9,a-z,A-Z),数据中的每8个位的字节都用ASCII码发送。
2、RTU模式:(Remote Terminal Unit,RTU)远程终端单元模式通信,针对通信距离较长和工业现场环境恶劣而设计的通信结构,特点:消息中每个8 bit的字节都包含量4bit的十六进制字符。
3、TCP模式:通过以太网和互联网连接传输数据使用的是TCP/IP协议,称为TCP模式,硬件接口就是以太网接口
Modbus RTU消息帧格式
注意:3.5字符时间间隔是作区别前后两帧数据的分隔符,只针对RTU模式有效。
计算: 串行通信中,一个字符包括:1个起始位+8个数据位+1个校验位(可选)+1个停止位(一般情况)这样,一个字符帧就包括11位,3.5个字符就是3.5*11=38.5位
地址域:用来定位设备。一个字节的从站地址,范围是:1~247
功能码:指定存储区域的操作类型。1个字节构成,取值范围是:1~255(十进制)
1.4、数据存储和功能代码
线圈和寄存器
电气控制中,接触器和中间继电器都是靠得电和失电来控制触点闭合(1)和断开(0)实现两种状态。所以,用线圈表示布尔量(1/0)。寄存器在计算机中就是用来存储数据的,所以,非布尔量的数据,放到寄存器中。
读写类型
以西门子PLC为例,I和Q都表示线圈,但是他们的分工是不同的,I表示输入,Q表示输出,输入意味着该存储区里的值必须由外部设备接入,是只读的;输出表示输出结果给外部设备,是可读可写的。
设备地址:一般采用10进制,一般是5位,第一位表示寄存器类型。
Modbus协议地址:指通信时使用的寄存器寻址地址。注意:设备地址是40001对应寻址地址就是0x0000;40002对应0x0001
功能码
用来表示不同区域,执行不同的读写操作,读写2种操作,存储区4个,但是输入线圈和输入寄存器只能读取,不能写入,出去这两种,会有6种不同的操作,但是对写入操作又做了两种细分,最后形成了8种具体操作
1.5、modbus通信报文分析
读取保持/输出寄存器,功能码:03H用于输出参数或者设备设定的参数,AO模拟量输出 类型:字
说明:一个寄存器就是一个字,一个字包含两个字节(16位11111111 11111111,最大值是65535)
设备文档
Tx:019-01 03 00 00 00 02 C4 0B
Rx:020-01 03 04 00 FE 00 AF DB BF
询问帧:01地址码 03功能码 0000起始地址 0002数据长度 C40B 校验码
应答帧:01地址码 03 功能码 04应答码 00FE温度值 00AF湿度值
串口助手调试:
主站发送:01 03 00 00 00 02 C4 0B 从站响应:01 03 04 00 7B 02 4D 4B 7F
主站发送的消息(01 03 00 00 00 02 C4 0B
):
- 01:设备地址(Slave Address),表示目标从设备的地址是
1
。 - 03:功能码(Function Code),表示读取保持寄存器(Read Holding Registers)。功能码
0x03
通常用于请求从设备的保持寄存器。 - 00 00:起始地址(Starting Address)。表示读取从地址
0x0000
开始的寄存器。 - 00 02:读取的寄存器数量(Number of Registers)。这里请求读取
2
个寄存器。 - C4 0B:校验和(CRC)。这是消息的 CRC 校验码,确保消息在传输中没有错误。
返回的响应(01 03 04 00 7B 02 4D 4B 7F
):
- 01:设备地址(Slave Address)。表示返回响应的从设备是
1
。 - 03:功能码(Function Code)。与请求的功能码一致,表示是 读取保持寄存器 的响应。
- 04:字节数(Byte Count)。这里返回
4
个字节的数据(即 2 个寄存器,每个寄存器占 2 字节)。 - 00 7B:第一个寄存器的数据。表示第一个寄存器的值是
0x007B
,即十进制的123
。 - 02 4D:第二个寄存器的数据。表示第二个寄存器的值是
0x004D
,即十进制的77
。 - 4B 7F:这部分应该是额外的或校验错误的部分,可能与消息的 CRC 校验码或数据一致性有关,具体要看通讯协议的要求。
2、主-从通信仿真测试
只给出大概的步骤,具体步骤看课程
2.1、组件设计
创建一个类库用于组件设计
设置控件的属性:
//设置温湿度显示
private int barHight=188;
public double setValue
{
set{
if (value < 0 || value > 70)
{
MessageBox.Show("温度值必须在0到70之间");
}
else
{
//实际值显示的高度
double realValue = barHight - (barHight / 70.0)*value;
//上面空白部分遮罩高度
this.whiteBar.Height = Convert.ToInt32(realValue);
}
}
}
//设置温湿度背景颜色
public BgColor bgColor;
public BgColor SetBgColor
{
get { return bgColor; }
set
{
if (value == BgColor.Green)
{
this.panel1.BackgroundImage = Properties.Resources.one;
}
else
{
this.panel1.BackgroundImage = Properties.Resources.two;
}
bgColor = value;
}
}
C#使用Resources资源文件,添加图片资源https://blog.csdn.net/weixin_44710358/article/details/144627373
2.2、创建温湿度测试项目
创建windows窗体应用
右键控件项目生成,在温湿度控制项目会生成上面完成的组件
2.3、添加控制台项目Modbus_base用于测试
static void Main(string[] args)
{
SerialPort serialPort = new SerialPort();
//设置96N81
serialPort.PortName="COM2";
serialPort.BaudRate = 9600;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
//打开串口
serialPort.Open();
while (true)
{
Thread.Sleep(2000);
//创建字节数组,拼接报文
List<byte> sendBits = new List<byte>();
sendBits.Add(0x01);//站地址
sendBits.Add(0x03);//功能码
sendBits.Add(0x00);//起始寄存器高位
sendBits.Add(0x00);//低位
sendBits.Add(0x00);//寄存器数量高位
sendBits.Add(0x02);//低位
sendBits.Add(0xC4);//CRC校验
sendBits.Add(0x0B);
//发送报文
serialPort.Write(sendBits.ToArray(), 0, sendBits.Count);//发送字节数组,偏移量为0
//接收报文
Thread.Sleep(100);//延迟100毫秒
byte[] saveBits = new byte[serialPort.BytesToRead];//获取缓冲区字节数
serialPort.Read(saveBits, 0, saveBits.Length);//接收缓冲区全部的字节
//验证接收数据
if (saveBits[0] == 0x01)
{
//观察报文01 03 04 00 7B 02 4D 4B 7F
//接收报文1 3 4 00 123 2 77 75 127
//数据索引0 1 2 3 4 5 6 7 8
//字节到10进制高低位转换:
//123 和 589 如何转换的 123 和 2 77
// 123/256 + 123%256 589/256 + 589%256
//转换成温度、湿度(1个高字节*256+一个低字节)=十进制数
int humidity = saveBits[3] * 256 + saveBits[4];
int temperature = saveBits[5] * 256 + saveBits[6];
Console.WriteLine($"湿度{humidity * 0.1}%" + $"温度{temperature * 0.1}℃");
}
}
}
2.4、将功能封装成类库moudbusRTU通信库
功能实现:将功能的实现分为字段、构造方法、属性、方法
namespace Modbus_RTU
{
/// <summary>
/// moudbusRTU通信库
/// </summary>
public class modbusRTU
{
//串口对象
private SerialPort serialPort = null;
//构造方法
public modbusRTU()
{
}
/// <summary>
/// 接收构造方法
/// </summary>
/// <param name="delay">接收报文延迟</param>
public modbusRTU(int delay)
{
this.ReceiveDelay = delay;
}
//属性
//private int ReceiveDelay { get; set; }没有对数据进行限制
private int _receiveDelay = 100;
public int ReceiveDelay
{
get { return _receiveDelay; }
set
{
if (value < 10||value>2000)//根据实际情况定义
{
_receiveDelay = 100;
}
else
{
_receiveDelay = value;
}
}
}
//方法
//打开串口
/// <summary>
/// 打开串口
/// </summary>
/// <param name="portName">串口号</param>
/// <param name="baudRate">波特率</param>
/// <param name="parity">校验位</param>
/// <param name="dataBits">数据位</param>
/// <param name="stopBits">停止位</param>
public void Connect(string portName,int baudRate=9600,Parity parity=Parity.None,int dataBits=8,StopBits stopBits=StopBits.One)
{
//实例化串口对象
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
try
{
serialPort.Open();
}
catch (Exception e)
{
throw new Exception("串口打开失败"+e);
}
}
//关闭串口
public void DisConnect()
{
if (serialPort!=null && serialPort.IsOpen)
{
serialPort.Close();
}
}
//读取保存寄存器数据
/// <summary>
/// 读取保存寄存器
/// </summary>
/// <param name="slaveId">从站id</param>
/// <param name="startAddress">寄存器起始地址</param>
/// <param name="count">寄存器数量</param>
/// <returns>返回只有数据的数组</returns>
/// <exception cref="Exception"></exception>
public byte[] ReadData(byte slaveId,int startAddress,int count)
{
//第一封装请求报文
byte[] sendBytes = new byte[8];
sendBytes[0] = slaveId;
sendBytes[1] = 0x03;
sendBytes[2] = (byte)(startAddress / 256);
sendBytes[3] = (byte)(startAddress % 256);
sendBytes[4] = (byte)(count / 256);
sendBytes[5] = (byte)(count % 256);
//封装CRC校验
byte[] CRC = CRC16(sendBytes,6);
sendBytes[6] = CRC[0];
sendBytes[7] = CRC[1];//CRC校验有问题,找不到合适的校验
//sendBytes[6] = 0XC4;
//sendBytes[7] = 0X0B;
//第二发送请求报文
byte[] receiveBytes = null;
try
{
serialPort.Write(sendBytes,0,sendBytes.Length );//发送报文
Thread.Sleep(ReceiveDelay);//延时接收
//第三 接收返回报文
receiveBytes=new byte[serialPort.BytesToRead];
serialPort.Read(receiveBytes,0,receiveBytes.Length);//从缓冲区接收所有数据到指定位置
}
catch (Exception e)
{
throw new Exception("发送报文异常" + e);
}
//第四 分析返回数据
int byteLength = count * 2;
if (receiveBytes.Length == 5 + byteLength)//报文长度合理
{
//进一步校验
if (CheckCRC(receiveBytes) && receiveBytes[1] == 0x03 && receiveBytes[2] == byteLength)
{
//截取数据部分
byte[] partData = new byte[byteLength];
//返回截取数组,只包括数据(从索引3开始,去掉地址,功能码,字节数组,目的数组,从0开始,复制数据长度)
Array.Copy(receiveBytes, 3, partData, 0, byteLength);
return partData;
}
else return null;
}
else return null;
}
#region CRC校验
private static readonly byte[] aucCRCHi =
{
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40
};
private static readonly byte[] aucCRCLo =
{
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
0x40
};
private byte[] CRC16(byte[] pucFrame, int usLen)
{
int i = 0;
byte[] res = new byte[2] { 0xFF, 0xFF };
ushort index;
while (usLen-- > 0)
{
index = (ushort)(res[0] ^ pucFrame[i++]);
res[0] = (byte)(res[1]^ aucCRCHi[index]);
res[1] = aucCRCLo[index];
}
return res;
}
private bool CheckCRC(byte[] value)
{
if (value == null) return false;
if (value.Length <= 2) return false;
int length = value.Length;
byte[] buf = new byte[length - 2];
Array.Copy(value, 0, buf, 0, buf.Length);
byte[] CRCbuf = CRC16(buf, buf.Length);
if (CRCbuf[0] == value[length - 2] && CRCbuf[1] == value[length - 1])
return true;
return false;
}
#endregion
}
}
2.5、用通信库完成项目开发
项目总结:首先通过对modbus协议的学习,modbus报文分为地址码、功能码、数据码和校验码、知道了了数据是怎样的发送和接受。同时通过对modbus slave 、modbus poll、串口助手工具的使用,能够更好的测试数据的传输、还学习了一些组件的使用,学会了用Windows Forms设计一个简单的窗口界面。