RS485:可以买个USB的rs485模块,也就几块钱。
模块空闲时:A: +4.57V B: 0V(GND)
A: +4.58v(逻辑1) 低于2伏(逻辑0) 和TTL串口一样。但接收有区别,发送的时候不接收。
B: 参考线GND
TTL电平:逻辑0:0v 逻辑1:+5v (要用2根线:GND和信号)
rs485的逻辑:+2~+6V(逻辑1); <2V(逻辑0)
注:不要和普通RS232弄混了,rs232由于历史原因,有几个版本。主要在于电压不一样。它是DC12v的(逻辑是反的。逻辑1是负12V)。还有一种是DC5V的,常见的是usb TTL串口模块,逻辑是正的,空闲是+4.6V的样子。电压可以用跳线帽选择3.3v和5v。不能混用,会烧坏。
1.1.1.1. 电平标准
根据通讯使用的电平标准不同,串口通讯可分为 TTL 标准及 RS-232 标准,见下表。
通讯标准 | 电平标准(发送端) |
---|---|
5V TTL | 逻辑1:2.4V ~ 5V |
逻辑0:0 ~ 0.5V | |
RS-232 | 逻辑1:-15 ~ -3V |
逻辑0:+3V ~ +15V |
我们知道常见的电子电路中常使用 TTL 的电平标准,理想状态下,使用 5V 表示二进制逻辑 1, 使用 0V 表示逻辑 0;而为了增加串口通讯的远距离传输及抗干扰能力,它使用-15V 表示逻辑 1, +15V 表示逻辑 0。使用 RS232 与 TTL 电平校准表示同一个信号时的对比见图1 - 2,
图1 - 2 RS-232与TTL电平标准下表示同一个信号
因为控制器一般使用 TTL 电平标准,所以常常会使用 MA3232 芯片对 TTL 及 RS-232 电平的信号进行互相转换。
rs485数据帧:以 波特率9600为例:A+B+C+D
A:起始位:下降沿开始,保持104ms低电平(在起始位前要保持高电平)
B:数据位D0,D1,D2,D3,D4,D5,D6,D7,(每个位时间104ms)
C:奇偶校验位。如果有:保持104ms电平(如果没有校验,就没有该电平)
D:停止位保持高电平104ms;(可选1,1.5,2停止位,例1.5x104ms)
注:校验位104ms的电平状态(由D0到D7的高电平总个数决定。(例:奇校验,d0到d7的高电平个数加校验位电平的和是奇数,例0x0F是4个高电平,那么4是偶数,要奇校验,那校验位电平就要是高电平,并保持104ms,4+1是5 五是奇数)如果是无校验,就没有104ms这个电平。
MODBUS:ModBus
地址:0x00是广播地址(从机不回应)。 地址范围0~247
0xFF地址,有的设备是回应地址
数据帧模式:RTU和ASC模式
RTU: A+B+C+D
A:地址号bit8,(地址范围0~247:0x00~0xF7)
B:功能码bit8,(有128个功能码0x00~0x80)
C:数据段,受功能码变化而改变。(一般是bit16一组 高字节+低字节)
D:CRC16校验码;低bit8+高bit8
分4个区:
0XXXX 输出线圈(接触器输出)0x01读输出线圈 0x05强制单线圈 0x0F写出线圈
1XXXX 开关输入(开关量输入)0x02读开关量
3XXXX 输入寄存器(模拟量采集)0x04模拟量采集
4XXXX 保持寄存器(通电保持寄存器)0x03读保持寄存器 0x06写单寄存器 0x10批量写
比如模块的寄存器配置 40003 是设置波特率的。那直接用03功能码就能去4区读取:从站+03+0003+0001+CRC 所以功能码也决定是去操作的哪个区。比如04功能码就是操作3区的寄存器。
注:
功能码0x01读取输出线圈的输出状态( DO )
01 01 0000 0018 3C 00 //表示01站 ,01功能码,第0000点开始 ,0018表示读24个线圈,3C00表示CRC校验 可以用串口助手16进制发送看效果
返回01 01 03 FF 8F 00 68 4E //表示01站,01功能码,03表示返回3个字节,FF表示byte[0]值,8F表示byte[1]值,00表示byte[2]值, 684E表示校验码。
功能码0x02读取输入开关状态量 ( DI )
01 02 00 00 00 14 78 05//表示01站 ,02功能码,第0000点开始 ,0014表示读20个开关,7805表示CRC校验 可以用串口助手16进制发送看效果
01 02 03 00 00 00 78 4E //表示01站,01功能码,03表示返回3个字节,00表示byte[0]值,00表示byte[1]值,00表示byte[2]值, 784E表示校验码。
功能码0x03读寄存器值
01 03 00 00 00 1D 85 C3 //表示01站 ,03功能码,第0000个寄存器开始 ,001D表示读29个寄存器,85C3表示CRC校验
寄存器定义会因模块厂家不同而不同。
0000寄存器:《 DI 》 8bit
0001寄存器:《 DO 》 8bit
0002寄存器:从站编号,默认值0001,范围1~247(改后重启)
0003寄存器:波特率1:4800,2:9600,3:19200,4:38400,5:57600
0004寄存器:校验位1:无,2:奇校验,3:偶校验 // 96n81
0005寄存器:断网保护(秒),大于1800秒为失效。比如设置3秒,通讯断开3秒,所有线圈复位
0006寄存器:产品版本 4B10是19216表示19年2月16日;这个只读的,修改不了,除非刷固件。
0007寄存器:及以后,都是用户寄存器RAM,断电丢失(无断电保存功能)。
功能码0x04读输入寄存器 (AD模块的模拟量值)
01 04 00 13 00 01 C0 0F // 0013表示第19个寄存器开始,0001表示读取1个寄存器值
功能码0x05设置单个线圈输出状态 (开 FF00 关 0000 )
01 05 00 01 FF 00 DD FA //0001表示D1线圈,FF00表示打开(0000表示关闭)
01 05 00 01 00 00 9C 0A //关闭D1线圈
功能码0x06设置单个寄存器值
01 06 00 01 AB CD 66 AF //设置0001寄存器值是0xABCD
发01 03 00 01 00 01 D5 CA 收 01 03 02 AB CD 06 E1 //返回2字节 AB CD
功能码0x0F设置多个线圈
01 0F 00 00 00 04 01 F0 3E D2 // 第0000个线圈开始,0004表示只修改4个线圈,01表示发送1个字节 F0表示字节值 3ED2表示CRC校验码。
01 0F 00 00 00 0F 02 FF FF E4 44 //第0000个线圈开始,000F表示只修改15个线圈,02表示发送1个字节 FF表示字节值 E444表示CRC校验码。
(批量设置多个线圈起始点)+bit16(byte字节数量)+byte【0】+byte【1】+。。。
功能码0x10设置多个寄存器
01 10 0005 0001 02 00 03 E6 04 // 从 0005寄存器开始(断网保护寄存器)0001表示只修改一个寄存器,02表示要发送2字节 0003表示2字节的值,这样断网超过3秒线圈就复位。
01 10 00 05 00 01 02 07 08 A5 F3 //4区0005寄存器断网保护超过1800秒就失效。0708表示1800秒。
//===================
数据帧之间是有间隔的。是3.5个字符(38.5个电平)
注意: 针对 3.5 个字符周期, 其实是一个具体时间,但是这个时间长度跟波特率有关。
在串口通信中, 1 个字符包括 1 位起始位、 8 位数据位(一般情况)、 1 位校验位(或者没有)、 1 位
停止位(一般情况下),因此 1 个字符包括 11 个位, 那么 3.5 个字符就是 38.5 个位, 波特率表示的
含 义 是 每 秒 传 输 的 二 进 制 位 的 个 位 , 因 此 如 果 是 9600 波 特 率 , 3.5 个 字 符 周 期
=1000/9600*38.5=4.01ms。
//
数据帧配好后,重点是CRC校验了,校验码16bit正确就能看见效果
如果配置错误,从机会返回功能码+0x80的值;(相当于+127,可以if(i<127)来判断是否成功)
public class CRC
{
#region CRC16
public static byte[] CRC16(byte[] data)
{
int len = data.Length;
if (len > 0)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc = (ushort)(crc ^ (data[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置
return new byte[] { hi, lo };
}
return new byte[] { 0, 0 };
}
#endregion
//
ASC:A+B+C+D
A:前导码,表示开始,8bit:(‘;’)
B:地址16bit,比如0x03,要拆成0x30和0x33发送
C:和上面一样,都需要拆解后发送。主要用于显示。但效率低。
//
可以用串口助手来调试:调试之前先确保从站是01,
串口助手发:01 05 0000 FF00 8C3A就能看到站1的bit0这个点输出。
发01 05 00 01 FF 00 DD FA就能看见从站1的bit1这个线圈输出。
发01 05 00 07 FF 00 3D FB就能看见从站1的bit7这个线圈输出。
/
只要把从站设备看成是byte【】数组就行。实际就是在操作这个数组。不同的下标代表不同的定义byte[0]和byte[1]一般表示线圈量或开关量,主要看你买的模块是8bit线圈还是32bit线圈或者16bit开关量。比如01 06 00 05 xxxx xxxx 我这个模块是断网保护(秒)byte[5]。比如设置3秒。那就是超过3秒没有通信,所有输出线圈会被复位。这个数据帧 01代表1号从站,06代表写一个寄存器,0005表示byte[5]这个寄存器。xxxx 是bit16设定值 ,最后xxxx表示CRC16校验码(这个校验码是多少,要用程序去计算出来,可以复制下面C#的程序来参考)。
//
调用示例:
textBox2.Text = textBox1.Text.Trim()+ CRC.ToModbusCRC16(CRC.StringToHexByte(textBox1.Text));
CRC.ToCRC16("012345678", true); //结果为:C3CD
CRC.ToCRC16("012345678", false); //结果为:CDC3
CRC.ToModbusCRC16("012345678", true); //结果为:2801
CRC.ToCRC16("你好,我们测试一下CRC16算法", true); //结果为:0182
/// <summary>
/// CRC校验
/// </summary>
public class CRC
{
#region CRC16
public static byte[] CRC16(byte[] data)
{
int len = data.Length;
if (len > 0)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc = (ushort)(crc ^ (data[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置
return new byte[] { hi, lo };
}
return new byte[] { 0, 0 };
}
#endregion
#region ToCRC16
public static string ToCRC16(string content)
{
return ToCRC16(content, Encoding.UTF8);
}
public static string ToCRC16(string content, bool isReverse)
{
return ToCRC16(content, Encoding.UTF8, isReverse);
}
public static string ToCRC16(string content, Encoding encoding)
{
return ByteToString(CRC16(encoding.GetBytes(content)), true);
}
public static string ToCRC16(string content, Encoding encoding, bool isReverse)
{
return ByteToString(CRC16(encoding.GetBytes(content)), isReverse);
}
public static string ToCRC16(byte[] data)
{
return ByteToString(CRC16(data), true);
}
public static string ToCRC16(byte[] data, bool isReverse)
{
return ByteToString(CRC16(data), isReverse);
}
#endregion
#region ToModbusCRC16
public static string ToModbusCRC16(string s)
{
return ToModbusCRC16(s, true);
}
public static string ToModbusCRC16(string s, bool isReverse)
{
return ByteToString(CRC16(StringToHexByte(s)), isReverse);
}
public static string ToModbusCRC16(byte[] data)
{
return ToModbusCRC16(data, true);
}
public static string ToModbusCRC16(byte[] data, bool isReverse)
{
return ByteToString(CRC16(data), isReverse);
}
#endregion
#region ByteToString
public static string ByteToString(byte[] arr, bool isReverse)
{
try
{
byte hi = arr[0], lo = arr[1];
return Convert.ToString(isReverse ? hi + lo * 0x100 : hi * 0x100 + lo, 16).ToUpper().PadLeft(4, '0');
}
catch (Exception ex) { throw (ex); }
}
public static string ByteToString(byte[] arr)
{
try
{
return ByteToString(arr, true);
}
catch (Exception ex) { throw (ex); }
}
#endregion
#region StringToHexString
public static string StringToHexString(string str)
{
StringBuilder s = new StringBuilder();
foreach (short c in str.ToCharArray())
{
s.Append(c.ToString("X4"));
}
return s.ToString();
}
#endregion
#region StringToHexByte
private static string ConvertChinese(string str)
{
StringBuilder s = new StringBuilder();
foreach (short c in str.ToCharArray())
{
if (c <= 0 || c >= 127)
{
s.Append(c.ToString("X4"));
}
else
{
s.Append((char)c);
}
}
return s.ToString();
}
private static string FilterChinese(string str)
{
StringBuilder s = new StringBuilder();
foreach (short c in str.ToCharArray())
{
if (c > 0 && c < 127)
{
s.Append((char)c);
}
}
return s.ToString();
}
/// <summary>
/// 字符串转16进制字符数组
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public static byte[] StringToHexByte(string str)
{
return StringToHexByte(str, false);
}
/// <summary>
/// 字符串转16进制字符数组
/// </summary>
/// <param name="str"></param>
/// <param name="isFilterChinese">是否过滤掉中文字符</param>
/// <returns></returns>
public static byte[] StringToHexByte(string str, bool isFilterChinese)
{
string hex = isFilterChinese ? FilterChinese(str) : ConvertChinese(str);
//清除所有空格
hex = hex.Replace(" ", "");
//若字符个数为奇数,补一个0
hex += hex.Length % 2 != 0 ? "0" : "";
byte[] result = new byte[hex.Length / 2];
for (int i = 0, c = result.Length; i < c; i++)
{
result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return result;
}
#endregion
}