RS485使用Modbus原理

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 标准,见下表。

SPI 的四种模式

通讯标准

电平标准(发送端)

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,

RS-232与TTL

图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

}

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值