MATLAB笔记[6]-Modbus-RTU通信

保命声明:笔者代码能力有限,若行文中有错漏之处欢迎大家指出。

RS485总线

工业现场经常要采集多点数据,模拟信号或开关信号,一般用到RS485总线,RS-485采用半双工工作方式,支持多点数据通信。RS-485总线网络拓扑一般采用终端匹配的总线型结构。即采用一条总线将各个节点串接起来,不支持环形或星型网络。


RS485无具体的物理形状,根据工程的实际情况而采用的接口,RS485采用差分信号负逻辑,+2V~+6V表示"0",- 6V~- 2V表示"1"。
RS485有两线制和四线制两种接线,四线制只能实现点对点的通信方式,现很少采用,现在多采用的是两线制接线方式,这种接线方式为总线式拓朴结构在同一总线上最多可以挂接32个结点。
485总线的通讯距离可以达到1200米。根据485总线结构理论,在理想环境的前提下,485总线传输距离可以达到1200米。其条件是通讯线材优质达标,波特率为9600,只负载一台485设备,才能使得通讯距离达到1200米,所以通常485总线实际的稳定的通讯距离往往达不到1200米。如果负载485设备多,线材阻抗不合乎标准,线径过细,转换器品质不良,设备防雷保护复杂和波特率的提高等等因素都会降低通讯距离。

二进制表示

[https://www.cnblogs.com/phyger/p/14060343.html]

  1. A高B低:1
  2. B高A低:0
  3. 起始信号: 由1变0,一个bit时间
  4. 停止信号: 由0变1,一个bit时间
  5. 空闲态: 一直是1(A高B低)
  6. 发送顺序:先发送低位再发送高位,比如发送0x53(01010011),先发送低四位,再发送高四位,并且低四位发送也是先从低到高发,所以示波器看到的应该是(11001010)

数据格式示例

  1. 波特率:9600
  2. 起始位:1位
  3. 数据位:8位
  4. 校验位: 无
  5. 停止位:1位(无校验位时应该为2个停止位)

Modbus协议

[https://mp.weixin.qq.com/s/WlHnfaPcfbGCX-OyjSBycA]
[https://www.bilibili.com/video/BV1GQ4y1Q7hW]

标准文档下载:[https://www.qsbye.cn/files/Modbus_standard.rar]
(包含:基于Modbus协议的工业自动化网络规范 GB-T19582.1-2008.pdf,modbus_application_protocol_specification_v1.1b3.pdf)
参考代码:[[https://github.com/foxclever/Modbus]

  • Modbus RTU(常用)
  • Modbus ASCII
  • Modbus TCP

Modbus协议,从字面理解它包括Mod和Bus两部分,首先它是一种bus,即总线协议,和I2C、SPI类似,总线就意味着有主机,有从机,这些设备在同一条总线上,最多支持247个从机设备.
Modbus在7层OSI参考模型中属于第七层应用层,数据链路层有两种:基于标准串口协议和TCP协议,物理层可使用3线232、2线485、4线422,或光纤、网线、无线等多种传输介质。

Modbus协议是一种请求/应答方式的交互过程,主机主动发起通讯请求,从机响应主机的请求,从机在没有收到主机的请求时,不会主动发送数据,从机之间不会进行通讯。

总体状态机:

发送接收状态机:

主机状态机:

从机状态机:

Modbus数据帧格式

地址域 功能码 数据 差错校验
1byte 1byte 0-252byte 2byte

差错校验计算地址域,功能码,数据(对报文帧全部数据进行计算)



总结:RS485是小端,Modbus是大端.

差错校验算法

  • CRC
  • LRC

(常用的21个标准CRC参数模型如图,CRC算法大全[https://github.com/whik/crc-lib-c])

Modbus RTU使用CRC-16校验算法(低8位在前,高8位在后)
在线计算CRC16
CRC-16-Modbus.c

unsigned int CRC16_2(unsigned char *buf, int len)
{
    unsigned int crc = 0xFFFF;
    for (int pos = 0; pos < len; pos++)
    {
        crc ^= (unsigned int)buf[pos]; // XOR byte into least sig. byte of crc
        for (int i = 8; i != 0; i--)   // Loop over each bit
        {
            if ((crc & 0x0001) != 0)   // If the LSB is set
            {
                crc >>= 1; // Shift right and XOR 0xA001
                crc ^= 0xA001;
            }
            else // Else LSB is not set
            {
                crc >>= 1;    // Just shift right
            }
        }
    }

    //高低字节转换
    crc = ((crc & 0x00ff) << 8) | ((crc & 0xff00) >> 8);
    return crc;
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Modbus主/从机地址

主机没有地址,从机有一个固定地址(需要提前设置)
Modbus协议中主机可以以两种模式对从机设备发出请求:单播和广播
在单播模式下,从机地址必须唯一,地址范围1-247。主机以特定地址访问指定的某个从机,发出一个请求数据帧,这个数据帧功能可以是读取或写入数据,从机接收到并处理完成后,会回报一个应答数据帧,以表示读取或写入成功。
在广播模式下,主机向所有的从机发出请求数据帧,所有的从机都会处理这条命令,对于广播请求,所有的从机无需做出应答操作。一般地址0表示广播地址。

C语言仿真Modbus-RTU的读指令

// Modbus硬编码版
#include <stdio.h>
#include <stdlib.h>
//#include <malloc/malloc.h>
#include <string.h>
#ifndef uint8_t
#include <stdint.h>
#endif

void Modbus_RTU_master(uint8_t * input,uint8_t len_input,uint8_t * output,uint8_t len_output);//主机自身状态机
void Modbus_RTU_slave(uint8_t * input,uint8_t len_input,uint8_t * output,uint8_t len_output);//从机自身状态机
void Modbus_RTU_master_to_slave(uint8_t *message,uint8_t message_len);//主机发送从机,单向
void Modbus_RTU_slave_to_master(uint8_t *message,uint8_t message_len);//从机发送主机,单向
uint8_t Check_CRC(uint8_t* message,uint8_t message_len);//校验CRC,返回1正确,返回0错误
uint16_t CRC16(uint8_t *buf,uint8_t len);//CRC16计算
uint8_t* _16bit_to_LSB_8bit_array(uint16_t input);//返回uint8_t数组,低字节在前
uint8_t* Make_load_to_message(uint8_t *load,uint8_t load_len);//制作载荷为消息(添加CRC)

uint8_t constant_from_slave3=0;//获取到的3号从机数据


int main(int argc, const char * argv[]) {
    //主机开始请求
    //slave_id:0x03,Function:0x03,Starting address Hi:0x00,Starting address Lo:0x00,No of Registers Hi:0x00,No of Registers Lo:0x01
    uint8_t __message[]={0x03,0x03,0x00,0x00,0x00,0x01};
    uint8_t *message=Make_load_to_message(__message, 6);
    Modbus_RTU_master_to_slave(message, 8);
    printf("constant of slave 3:%d\n",constant_from_slave3);
    printf("Hello, World!\n");
    return 0;
}


uint16_t CRC16(uint8_t *buf, uint8_t len){
    //len有效载荷长度,buf报文指针(报文大小:4-256字节)
    /*
     |地址1byte|功能码1byte|数据0-252byte|CRC16 2byte|
     */
    unsigned int crc = 0xFFFF;
    for (int pos = 0; pos < len; pos++){
        crc ^= (unsigned int)buf[pos]; // XOR byte into least sig. byte of crc
        for (int i = 8; i != 0; i--){   // Loop over each bit
            if ((crc & 0x0001) != 0){   // If the LSB is set
                crc >>= 1; // Shift right and XOR 0xA001
                crc ^= 0xA001;
            }
            else{ // Else LSB is not set
                crc >>= 1;    // Just shift right
            }
        }
    }

    //高低字节转换,低字节在前
    crc = ((crc & 0x00ff) << 8) | ((crc & 0xff00) >> 8);
    return crc;
}
/*
 CRC校验总体函数
 功能:message就是接收到的Modbus报文,从中分离出有效载荷和CRC16计算比对;
 返回:1正确,0错误
 Modbus报文:|地址1byte|功能码1byte|数据0-252byte|CRC16 2byte|
 */
uint8_t Check_CRC(uint8_t* message,uint8_t message_len){
    uint8_t is_valid=0;
    //计算整体载荷长度
    uint8_t len_load=message_len-2;
    //CRC计算
    uint8_t *CRC_t=_16bit_to_LSB_8bit_array(CRC16(message,len_load));
    /*
    {//测试
        for(uint8_t i=0;i<message_len;i++){
            printf("message[%d]:%x\n",i,message[i]);
        }
    }
    */
    printf("CRC_t[0]:%x\n",CRC_t[0]);
    printf("CRC_t[1]:%x\n",CRC_t[1]);
    //分离报文的CRC
    uint8_t CRC_from_message[2];
    //Modbus是大端
        //CRC_from_message=0x0000 | (message[len_t-2]<<8);
        //CRC_from_message=CRC_from_message | (message[len_t-1]);
    CRC_from_message[0]=message[message_len-2];
    printf("crc_from_message[0]:%x\n",CRC_from_message[0]);
    CRC_from_message[1]=message[message_len-1];
    printf("crc_from_message[1]:%x\n",CRC_from_message[1]);
    //判断
    if(CRC_t[0]==CRC_from_message[0] && CRC_t[1]==CRC_from_message[1]) is_valid=1;
    else is_valid=0;
    
    return is_valid;
}
/*
功能:CRC16计算,返回uint8_t数组,低字节在前
 */
uint8_t* _16bit_to_LSB_8bit_array(uint16_t input){
    static uint8_t output[2];
    output[0]=input>>8;
    //printf("%x,",output[0]);
    output[1]=input;
    //printf("%x\n",output[1]);
    return output;
}
/*
 功能:给载荷添加CRC信息制作为完整message
 */
uint8_t * Make_load_to_message(uint8_t *load,uint8_t load_len){
    //static uint8_t message[load_len];//malloc
    uint8_t *message;
    message=(uint8_t*)malloc((unsigned long)load_len);
    uint8_t *crc;
    crc=_16bit_to_LSB_8bit_array(CRC16(load, load_len));
    //memmove
    printf("load_len:%d\n",load_len);
    for(uint8_t i=0;i<load_len;i++){
        message[i]=load[i];
    }
    message[load_len]=crc[0];
    message[load_len+1]=crc[1];
    printf("message_len:%d\n",load_len+2);
    return message;
}
/*
 功能:主机请求和接收的状态机;
 输入:接收
 输出:请求
 */
void Modbus_RTU_master(uint8_t * input,uint8_t len_input,uint8_t * output,uint8_t len_output){
    if(input!=NULL){
        //获取应答
        if(Check_CRC(input, len_input)){//应答crc有效
            printf("主机接收正确数据\n");
            
            
            if(input[0]==0x03){//是3号从机
                constant_from_slave3=input[4];//直接赋值了
            }
        }
    }
}
/*
 功能:从机发送和接收的状态机
 输入:接收
 输出:应答
 假设:16bit寄存器低8位存放8bit数据,高8位空置
 */
void Modbus_RTU_slave(uint8_t * input,uint8_t len_input,uint8_t * output,uint8_t len_output){
    uint8_t slave_id=0x03;//设定从机地址
    uint8_t constant1=0x04;//从机的数据
    
    //CRC校验
    if(Check_CRC(input, len_input)){
        printf("从机接收正确数据\n");
    if(input[0]==0x00){//广播
        //do something
    }
    if(input[0]==slave_id){//本机
        if(input[1]==0x03){//0x03 读
            static uint8_t message1[5]={0x03,0x03,0x02,0x00,0x04};//应答时带上自己的地址
            uint8_t * _message1=Make_load_to_message(message1, 5);
            Modbus_RTU_slave_to_master(_message1, 7);
        }
        if(input[1]==0x10){//0x10 写
            //pass
        }
    //TODO:写入
    }
  }
    else{//crc校验不通过
        Modbus_RTU_slave_to_master(NULL, NULL);
    }
}
/*
 功能:主机发送从机,单向
 */
void Modbus_RTU_master_to_slave(uint8_t *message,uint8_t message_len){
    if(message==NULL) return;
    
    Modbus_RTU_slave(message,message_len,NULL,NULL);
}
/*
 功能:从机发送主机,单向
 */
void Modbus_RTU_slave_to_master(uint8_t *message,uint8_t message_len){
    if(message==NULL) return;
    
    Modbus_RTU_master(message,message_len,NULL,NULL);
}

结果:

load_len:6
message_len:8
CRC_t[0]:85
CRC_t[1]:e8
crc_from_message[0]:85
crc_from_message[1]:e8
从机接收正确数据
load_len:5
message_len:7
CRC_t[0]:c0
CRC_t[1]:47
crc_from_message[0]:c0
crc_from_message[1]:47
主机接收正确数据
constant of slave 3:4
Hello, World!
Program ended with exit code: 0

MATLAB的Modbus Explorer应用实战Modbus

[https://gitee.com/wllis121/normal_proj/tree/master/Arduino_Serial_cmd]
[https://www.bilibili.com/video/BV1Ei4y117yR]
[https://ww2.mathworks.cn/help/supportpkg/nucleo/ug/MODBUS-Communication-Example-client-server.html]
Industrial Communication Toolbox™ provides access to live and historical industrial plant data directly from MATLAB® and Simulink®. You can read, write, and log OPC Unified Architecture (UA) data from devices such as distributed control systems, supervisory control and data acquisition systems, and programmable logic controllers. You can also access plant and manufacturing data directly from OSIsoft® PI servers, and use this data for process monitoring, process improvement, and predictive maintenance applications.

You can work with data from live servers and data historians that conform to the OPC UA, OPC Data Access (DA), and OPC Classic Historical Data Access (HDA) standards. When communicating over OPC UA, you can securely connect to OPC UA servers using a variety of security modes, encryption algorithms, and user authentication methods.

The toolbox includes Simulink blocks that let you model online supervisory control and perform hardware-in-the-loop controller testing. In both MATLAB and Simulink, you can verify algorithms by establishing a secure OPC UA connection to your plant and build connected digital twin models for IIoT applications. The toolbox also supports communication with edge devices and cloud servers over Modbus® and MQTT protocols.
You can read and write to coils and registers using the Modbus Explorer app.
This example creates a Modbus object using Serial RTU, with an increased Timeout of 20 seconds.

m = modbus('serialrtu','COM3','Timeout'=20)
Modbus Serial RTU with properties:

             Port: 'COM3'
         BaudRate: 9600
         DataBits: 8
           Parity: 'none'
         StopBits: 1
           Status: 'open'
       NumRetries: 1
          Timeout: 20 (seconds)
        ByteOrder: 'big-endian'
        WordOrder: 'big-endian'

The object display in the output shows the specified Timeout property value.

参考框图


创建主机(Master,Client)

创建从机(Slave,Server)

Arduino Uno
ModbusRTUServerLED.ino

#include <ArduinoRS485.h> // ArduinoModbus depends on the ArduinoRS485 library
#include <ArduinoModbus.h>

const int ledPin = LED_BUILTIN;

void setup() {
  Serial.begin(9600);

  Serial.println("Modbus RTU Server LED");
  delay(1000);
  Serial.println("Modbus RTU Server LED");
  // start the Modbus RTU server, with (slave) id 1
  if (!ModbusRTUServer.begin(1, 9600)) {
    Serial.println("Failed to start Modbus RTU Server!");
    delay(1000);
    Serial.println("Failed to start Modbus RTU Server!");
    while (1);
  }
  
  // configure the LED
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  //Serial.println("Succeed to start Modbus RTU Server!");
   //delay(1000);
   //Serial.println("Succeed to start Modbus RTU Server!");
  // configure a single coil at address 0x01
  ModbusRTUServer.configureCoils(0x01, 1);
}

void loop() {
  // poll for Modbus RTU requests
  ModbusRTUServer.poll();

  // read the current value of the coil
  int coilValue = ModbusRTUServer.coilRead(0x01);
  //设置线圈数值
  ModbusRTUServer.coilWrite(0x01,1);
  if (coilValue!=0) {
    // coil value set, turn LED on
    digitalWrite(ledPin, HIGH);
  } else {
    // coil value clear, turn LED off
    digitalWrite(ledPin, LOW);
  }
}

结果

posted @ 2023-01-29 17:54  qsBye  阅读(1720)  评论(0编辑  收藏  举报