天天看点

ModBus-RTU通讯协议编程

编者说:

ModBus通信协议结构简单,编程方便,在工业应用现场被广泛使用,特别是PLC应用场合。需要指出的是,ModBus只是一种通信协议,即设备之间的数据约束方式,使用时需要有底层的驱动程序支持,例如,串口通讯。串口通信使用简单,在ModBus协议中应用广泛。在信号的传输方式上又分为RS-232通信,RS-485通信,这种区分只是在数据的传输方式方作划分,底层的驱动程序完全一样。需要长距离、长距离、可靠性高的传输方式时,我们就选择RS-485通信,需要短距离、高速率通信时,我们就用RS-232通信。

这篇博客主要讲述ModBus-RTU通信协议的编程方法,实现时采用串口通信,RS-232的传输方式,采用的单片机为TMS320F280049C。

(1)ModBus通信协议简介

Modbus协议可以说是工业自动化领域应用最为广泛的通讯协议,因为他的开放性、可扩充性和标准化使它成为一个通用工业标准。有了它,不同厂商的产品可以简单可靠的接入网络,实现系统的集中监控,分散控制功能。

目前Modbus规约主要使用的是ASCII, RTU, TCP等,并没有规定物理层。目前Modbus常用的接口形式主要有RS-232C,RS485,RS422,也有使用RJ45接口的,ModBus的ASCII, RTU协议则在此基础上规定了消息、数据的结构、命令和应答的方式。ModBus数据通信采用Master/Slave方式(主/从),即Master端发出数据请求消息,Slave端接收到正确消息后就可以发送数据到Master端以响应请求;Master端也可以直接发消息修改Slave端的数据,实现双向读写。

Modbus协议需要对数据进行校验,串行协议中除有奇偶校验外,ASCII模式采用LRC校验,RTU模式采用16位CRC校验,但TCP模式没有额外规定校验,因为TCP协议是一个面向连接的可靠协议。另外,Modbus采用主从方式定时收发数据,在实际使用中如果某Slave站点断开后(如故障或关机),Master端可以诊断出来,而当故障修复后,网络又可自动接通。因此,Modbus协议的可靠性较好。

(2)ModBus信息帧结构

当设备之间进行通信时,都是以信息帧的结构进行数据之间的传送。信息帧即数据的组成方式,一个信息帧由多位字节数据组成,具体格式如下所示(这里主要讲述RTU格式的信息帧):

开始 设备地址 功能码 数据位 校验位 终止
T1-T2-T3-T4 8bit 8bit N * 8bit 16bit T1-T2-T3-T4

RTU模式中,信息开始至少需要有3.5个字符的静止时间,依据使用的波特率,很容易计算这个静止的时间(如下图中的T1-T2-T3-T4)。接着,第一个区的数据为设备地址。

设备地址:当与主机进行通信的设备有多个时,主机通过设备地址号来选择与哪个设备进行通信。设备地址号为1字节。

功能码:当主机与从机通信时,主机通过功能码来选择对从机进行什么操作。部分功能码列出如下表所示,详细的功能码列表请查阅相关资料。功能码数据长度为1字节

功能码 名称 作用
0x03 读取保持寄存器 读取保持寄存器数据
0x06 预置单寄存器 将数据写入到某寄存器

数据位:要传送的具体数据,由多个字节组成。

校验位:主机或从机可用校验码进行判别接收信息是否出错。错误校验采用CRC-16校验方法。CRC校验的算法后边讲解。

(3)ModBus功能码详解

1. 0x03号命令,读可读写模拟量寄存器(保持寄存器):

主机发送命令格式:

设备号 功能码 起始寄存器地址高八位 起始寄存器地址低八位 读取数高八位 读取数低八位 CRC H CRC L

例:【01】【03】【00】【12】【00】【03】【CRC高】【CRC低】

1)设备地址即为0x01。

2)功能码0x03。

3)0x00 0x12,要读取寄存器的高八位地址和低八位地址。

4)0x00 0x03,读取寄存器的数量,此处为读取3个寄存器。

5)CRC H,CRC L。

从机响应格式:

设备号 功能码 返回字节个数 数据1 .... 数据N CRC H CRC L

例:【01】【03】【03】【12】【01】【03】【CRC高】【CRC低】

1)设备地址即为0x01。

2)功能码0x03,功能为读取保持寄存器值。

3)0x03,要返回的数据数量,此处为3。

4)0x12 0x01 0x03,返回的数据。

5)CRC H,CRC L。

2. 0x06号命令,写单个模拟量寄存器(预置寄存器):

主机发送命令格式:

设备号 功能码 预置寄存器地址高八位 预置寄存器地址低八位 预置数据高八位 预置数据低八位 CRC H CRC L

例:【01】【06】【00】【01】【00】【03】【CRC高】【CRC低】

1)设备地址即为0x01。

2)功能码0x06,预置单个模拟量寄存器。

3)0x00 0x01,要预置的寄存器的高八位地址和低八位地址。

4)0x00 0x03,预置的数据,此处为一个16bit的数据。

5)CRC H,CRC L。

从机响应格式:

如果成功把计算机发送的命令原样返回,否则不响应。

(4)串口通信程序配置

串口通信程序即为ModBus协议的物理层代码。串口发送程序时,每次发送的数据量为1个字节,发送方式为9600,N,8,1。对应的DSP TMS320F280049C的串口配置程序如下所示:

1)接收RX和发送TX引脚配置程序

void SCI_GPIO_Init(void)
{
    EALLOW;

    GpioCtrlRegs.GPADIR.bit.GPIO11 = 0;                 //配置为输入
    GpioCtrlRegs.GPAPUD.bit.GPIO11 = 1;                 //配置为推挽输出

    GpioCtrlRegs.GPAMUX1.bit.GPIO11 = 2;                //配置SCI-GPIO作为使用
    GpioCtrlRegs.GPAGMUX1.bit.GPIO11 = 0x01;

    GpioCtrlRegs.GPADIR.bit.GPIO12 = 1;                 //配置为输出
    GpioCtrlRegs.GPAPUD.bit.GPIO12 = 1;                 //配置为推挽输出

    GpioCtrlRegs.GPAMUX1.bit.GPIO12 = 2;                //配置SCI-GPIO作为使用
    GpioCtrlRegs.GPAGMUX1.bit.GPIO12 = 0x01;

    EDIS;
}
           

2)串口外设配置程序

void InitSCI(void)
{
    SCI_GPIO_Init();

//    ScibRegs.SCIFFRX.bit.RXFFIENA = 1;      //接收FIFO中断使能
//    ScibRegs.SCIFFRX.bit.RXFFIL = 1;        //接收FIFO深度
//    ScibRegs.SCIFFRX.bit.RXFFINTCLR = 1;    //接收FIFO中断清零
//    ScibRegs.SCIFFRX.bit.RXFIFORESET = 1;   //复位FIFO

    ScibRegs.SCICCR.all = 0x0007;           // 1 stop bit,  No loopback
                                                // No parity, 8 char bits,
                                                // async mode, idle-line protocol
    ScibRegs.SCICTL1.bit.RXENA = 1;
    ScibRegs.SCICTL1.bit.TXENA = 1;

    ScibRegs.SCICTL2.bit.RXBKINTENA = 1;        //接收中断使能
    //
    // SCIA at 9600 baud
    // @LSPCLK = 25 MHz (100 MHz SYSCLK) HBAUD = 0x01  and LBAUD = 0x44.
    //
    ScibRegs.SCIHBAUD.all = 0x0001;
    ScibRegs.SCILBAUD.all = 0x0044;

    ScibRegs.SCICTL1.all = 0x0023;          // Relinquish SCI from Reset
}
           

(5)ModBus协议层程序配置

ModBus协议层程序主要是进行数据的处理,处理方式主要按照协议的要求来配置。此博客所写的协议为ModBus-RTU协议。

1)数据的接收处理

数据的接收主要采用中断的方式,使用的是串口的接收中断。串口中断是逐字节进行数据接收的,而设备之间每次通信是多字节的,因此,在每次接收设备传来的数据时,需要设置定时器计数,以保证每次接收数据的完整性。数据接收部分的代码如下所示:

__interrupt void SCIbISR(void)
{
    PieCtrlRegs.PIEIER1.bit.INTx1 = 0;      //关闭ADC中断

    if(Rx_Stop_Flag == 0)
    {
        CpuCounter = CpuTimer0.RegsAddr->TIM.all;             //读取定时器0的计数值
        CpuTimer0.RegsAddr->TCR.bit.TSS = 0;                  //开启定时器计数
    }

    ScibRegs.SCIFFRX.bit.RXFFINTCLR = 1;    //接收FIFO中断清零

    unsigned char res = ScibRegs.SCIRXBUF.bit.SAR;

    if((CpuCounter>=50000000)&&(Rx_Stop_Flag == 0))                 //500mS
    {
        Rx_Stop_Flag = 0;

        RS232_RXBuf[RS232_RXCount++] = res;
    }

    else
    {
        CpuTimer0.RegsAddr->TCR.bit.TSS = 1;            //停止定时器计数

        CpuTimer0.RegsAddr->TIM.all = CpuTimer0.RegsAddr->PRD.all;       //重装载计数器

        Rx_Stop_Flag = 1;                               //标志置一,停止接收数据

        GpioDataRegs.GPBCLEAR.bit.GPIO33 = 1;
    }

    PieCtrlRegs.PIEACK.bit.ACK9 = 1;

    PieCtrlRegs.PIEIER1.bit.INTx1 = 1;
}
           

程序中Rx_Stop_Flag为数据接收标志,一次数据接收完成后,此标志会置1。下一步,程序会按照该标志是否置1来进行ModBus-RTU程序的执行。

2)协议层程序的处理

如果Rx_Stop_Flag标志置1。程序会进行数据处理,此部分的程序为ModBus协议层的程序。协议层部分代码如下所示:

if(Rx_Stop_Flag)
           {
               PieCtrlRegs.PIEIER1.bit.INTx1 = 0;  //关闭ADC中断
               if(RS232_RXBuf[0] == RS232_addr)    //地址码判断
               {
                   CRC_Cal = CRC16(RS232_RXBuf,RS232_RXCount-2);                                       //根据接收到的数据计算得到的CRC
                   RX_CRC = RS232_RXBuf[RS232_RXCount-1]|(((Uint16)RS232_RXBuf[RS232_RXCount-2])<<8);  //接收数据最后两位的CRC

                   if(CRC_Cal == RX_CRC)           //CRC检验正确
                   {
                       switch(RS232_RXBuf[1])
                       {
                           case 0x03:              //功能码0x03
                           {
                               ModBus_Solve_03();
                               break;
                           }
                           case 0x06:              //功能码0x06
                           {
                               ModBus_Solve_06();
                               break;
                           }
                       }
                   }
                   else        //CRC校验出错
                   {

                   }
               }
               RS232_RXCount=0;        //接收计数清零
               Rx_Stop_Flag=0;         //停止接收标志清零

               PieCtrlRegs.PIEIER1.bit.INTx1 = 1;   //开启ADC中断
           }
           

协议层代码执行流程如下所述:

  1. 根据接收数据的第一位来判断设备地址是否匹配。
  2. 进行CRC校验,判断数据通信是否出错
  3. 根据接收数据的第二位(功能码)来确定要执行的操作。0x03 or 0x06。
  4. 根据相应的功能码来执行相应的处理函数。

3)功能函数代码的编写

此部分的代码主要根据功能码由设备执行相应的功能。例如,0x03要求设备返回一些数据(返回当前电压、电流等),0x06要求设备执行一些操作(继电器动作,输出电压、电流的调整等)。

0x03功能码处理函数如下:

void ModBus_Solve_03(void)      //上位机读取指令
{
    Uint16 RX_Number;
    Uint16 RX_Command;
    Uint16 i;

    RX_Command = ((Uint16)RS232_RXBuf[2]<<8)|RS232_RXBuf[3];        //要读取寄存器的地址
    RX_Number = ((Uint16)RS232_RXBuf[4]<<8)|RS232_RXBuf[5];         //读取字节个数

//    RS232_TXBuf[0] = RS232_RXBuf[0];    //地址码
//    RS232_TXBuf[1] = RS232_RXBuf[1];    //功能码
//    RS232_TXBuf[2] = RS232_RXBuf[2];    //寄存器地址高位
//    RS232_TXBuf[3] = RS232_RXBuf[3];    //寄存器地址低位
//    RS232_TXBuf[4] = RS232_RXBuf[4];    //读取字节个数  高位
//    RS232_TXBuf[5] = RS232_RXBuf[5];    //读取字节个数  低位

    RS232_TXBuf[0] = RS232_RXBuf[0];    //地址码
    RS232_TXBuf[1] = RS232_RXBuf[1];    //功能码
    RS232_TXBuf[2] = RX_Number;         //返回字节个数为2

    for(i=0;i<RX_Number;i++)
    {
        RS232_TXBuf[3+i] = i;    //返回寄存器值
    }

    Uint16 CRC_Cal = CRC16(RS232_TXBuf,3+i);

    RS232_TXBuf[3+i] = (CRC_Cal>>8)&0xFF;
    RS232_TXBuf[4+i] = (CRC_Cal)&0xFF;

    RS232_SendBuf(RS232_TXBuf,5+i);
}
           

 0x06功能码处理函数如下:

void ModBus_Solve_06(void)      //上位机写入指令       ModBus--预置单寄存器    [地址码]  [功能码]  [寄存器高位地址]  [寄存器低位地址]  [高八位数据]  [低八位数据]  [CRC H]  [CRC L]
{
    Uint16 RX_Command;

    RX_Command = ((Uint16)RS232_RXBuf[2]<<8)|RS232_RXBuf[3];

    switch(RX_Command)
    {
        case 0x0001:            //设置Buck电压
        {
            break;
        }
        case 0x0002:            //继电器切换
        {

            break;
        }
        case 0x0003:            //Buck输出禁止
        {
            break;
        }
        case 0x0004:            //输出双脉冲
        {
            break;
        }
        case 0x0005:            //IGBT PWM信号禁止
        {
            break;
        }
        case 0x0006:            //
        {
            break;
        }
    }

    RS232_TXBuf[0] = RS232_RXBuf[0];
    RS232_TXBuf[1] = RS232_RXBuf[1];
    RS232_TXBuf[2] = RS232_RXBuf[2];
    RS232_TXBuf[3] = RS232_RXBuf[3];
    RS232_TXBuf[4] = RS232_RXBuf[4];
    RS232_TXBuf[5] = RS232_RXBuf[5];

    Uint16 CRC_Cal = CRC16(RS232_TXBuf,6);

    RS232_TXBuf[6] = (CRC_Cal>>8)&0xFF;
    RS232_TXBuf[7] = (CRC_Cal)&0xFF;

    RS232_SendBuf(RS232_TXBuf,8);
}
           

4)CRC校验函数的编写

CRC校验的原理为:当从机发送数据时,从机会根据发送的数据,采用CRC校验算法计算出一个16bit的数据,CRC_H、CRC_H,然后从机设备会将16位的CRC数据附带在要发送的数据后边,将此数据作为一个数据帧发送给主机。主机接收到数据后,将CRC数据位前面的数据采用相同的CRC校验算法进行计算,将计算得到的16bit的数据与接收到的CRC位的数据作比较,比较结果一致即为数据传输正确,否则,数据传输失败。

CRC校验算法为:

unsigned short CRC16(unsigned char *puchMsg, unsigned short usDataLen) //puchMsg ; /* 要进行CRC校验的消息 //usDataLen ; /* 消息中字节数 */
{
    unsigned char uchCRCHi = 0xFF ; /* 高CRC字节初始化 */
    unsigned char uchCRCLo = 0xFF ; /* 低CRC 字节初始化 */
    unsigned uIndex ; /* CRC循环中的索引 */
    while (usDataLen--) /* 传输消息缓冲区 */
    {
        uIndex = uchCRCHi ^ *puchMsg++ ; /* 计算CRC */
        uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex];
        uchCRCLo = auchCRCLo[uIndex];
    }
    return ((uchCRCHi << 8) | uchCRCLo);
}
           

完整的实验代码我已上传到CSDN,需要的可自行下载哈,下载链接如下所示:

https://download.csdn.net/download/fanxianyan1993/12058062

提问方式:以上程序有啥不懂的可以随时向我提问哈,用微信扫描下方二维码我会在第一时间给大家回复的,谢谢。 

ModBus-RTU通讯协议编程

继续阅读