单片机
返回首页

MSP430中MODBUS-RTU的程序编写方式

2022-09-13 来源:csdn

**MSP430中MODBUS-RTU的程序编写方式**

MODBUS RTU简单介绍

MODBUS 轮询程序,此函数持续在while中循环

定时器定时卡3.5字符时间,置标志位给轮询函数

03功能码的处理函数,此函数通过for语句持续将寄存器中数据打包发送,并添加CRC校验

06功能码解析函数,此函数用于将发送进来的数据解析后根据地址写入寄存器

通过串口发送一串数据,并在此数据后自动追加CRC校验码

此函数是正确应答函数,在03/06功能码解析函数中用到

写到这里依然没有出现对应寄存器读写的函数,其实读写的函数很简单,读取的函数只是输入寄存器的地址,即可将读取到的结果return给函数本身,在03功能码的解析函数中将数据读取出来并追加CRC通过串口发送出去,就达到了读取寄存器的效果

大家可以根据自己的要求在本函数中添加相应的地址和数据,自行增加case语句即可;如果要读取32位数据,需要把32位数据拆分成两个16位数据,需要占用两个寄存器。


下面为06功能码写保持寄存器的函数,可以根据自己要求自行添加内部函数,增加case语句即可


最后分享一个将2字节的数组(小端Little Endian,低字节在前)转换为16位整数的程序

注明:以上的函数适用于MSP430F5438A单片机,程序来源是参考安富莱的MODBUS例程,本人将程序重新优化和改写,目前MODBUS响应速度小于20MS即可响应


MODBUS RTU简单介绍

Modbus 一个工业上常用的通讯协议、一种通讯约定。Modbus协议包括RTU、ASCII、TCP。其中MODBUS-RTU最常用,比较简单,在单片机上很容易实现。虽然RTU比较简单,但是看协议资料、手册说得太专业了,起初很多内容都很难理解。


首先第一步是串口的初始化以及定时器,MODBUS是传输协议,依赖于串口,本质就是通过串口对数据以规定的格式进行收发并判断,RTU为16进制传输。所以首先要对串口进行初始化,初始化串口波特率、数据位。校验位以及停止位,在编写MODBUS协议之前首先保证自己串口已经调通,定时器已经开启并初始化,这些是前提。


当上位机发来请求命令,比方说要读我们设备1个寄存器,地址为1,那么上位机就会发送01 03 00 00 00 01 xx xx


01 表示从机地址

03 表示modbus03功能码

00 第一组表示寄存器起始地址高8位

00 第二组表示寄存器起始地址低8位

00 第三组表示读取的寄存器数量高8位

01 表示读取的寄存器数量低8位,即这条数据帧代表从第0个地址开始,读取一个寄存器

XX 第一组表示CRC校验高8位

XX 第二组表示CRC校验低8位


MODBUS 轮询程序,此函数持续在while中循环

///*超过3.5个字符时间后执行函数,通知主函数开始解码*/

        if(gtModbusTime.u8ModbusTimeOutFlag == ENUM_FALSE)       

        {

            return;                                 

        }  

        u16Addr = gtModbusData.u8RxBuf[0];

        if(u16Addr != gtFramData.u8FramDeviceAddr)

        {

            gtModbusData.u8RxCount = 0;

            return; 

        } 

        gtModbusTime.u8ModbusTimeOutFlag = ENUM_FALSE;   

        /*接收的数据小于4个字节就认为是错误的*/

        if(gtModbusData.u8RxCount < 4)     

        {

            gtModbusData.u8RxCount = 0;

            return; 

        }    

        /*计算CRC校验和*/

        u16Crc = Crc16(gtModbusData.u8RxBuf,gtModbusData.u8RxCount);

        if(u16Crc != 0)

        {

            gtModbusData.u8RxCount = 0;

            return; 

        }    

        /*如果标志位不是0,则说明是有效数据,进入分析应用层协议*/

        if(gtModbusData.u8RxCount != 0)

        {

            /*进入应用解析代码*/

            ModBusApplication();          

        }


那么我们的串口接受到这样的请求数据就需要对数据进行处理,处理方式如下:

首先对定时器的标志位进行判断,如果检测到接受数据完成标志位没有被清零,那么证明数据还在接受中,所以直接break,判定数据有没有接受完成是计算一帧数据接受完成的时间,加入波特率为9600,那么证明一秒发送9600个位,9600除以8就是多少个字节,发送请求的命令是8个字节,MODBUS定义3.5个字符间隔就可以确定数据是否接收完成,3.5个字符不是3.5个字节,而是完整的一帧数据的3.5倍时间,9600意味着一秒发送9600个位,那么也就是一秒发送1200字节,这样就可以算出8个字节需要多少时间,并且计算8个字节的3.5倍大约是多少时间从而控制定时器去定这么长时间。


定时器定时卡3.5字符时间,置标志位给轮询函数

switch(TA0IV)

    {

        case 0x02:

        {

            /*关闭定时器中断,目的防止不接收数据时定时器一直在工作*/ 

            TA0CCTL1 &= ~CCIE;      

            /*接收一个字节置位*/

            gtModbusTime.u8ModbusTimeOutFlag = ENUM_TRUE;     

            if(gtModbusTime.u8ModbusFlag == ENUM_FALSE)

            {

                gtModbusTime.u8ModbusFlag = ENUM_TRUE;      

            }   

            break;

        }

在代码解析中就可以获取到MODBUS的命令位,根据命令就可以进入不同的协议解析:


static void ModBusApplication(void)

{

    switch (gtModbusData.u8RxBuf[1])       

    {

        /*01功能读取线圈输出状态,本程序未用*/ 

        case 0x01:         

        {

            break; 

        }  

        /*02功能读取线圈输入状态,本程序未用*/

        case 0x02:         

        {

            break; 

        }  

        /*03功能读取保持寄存器*/         

        case 0x03:         

        {

            Modbus03H();    

            break;

        }  

        /*04功能读取输入寄存器,本程序未用*/

        case 0x04:         

        {

            break;   

        } 

        /*05功能写开关量输出*/

        case 0x05:         

        {

            break; 

        }  

        /*06功能写单路寄存器*/          

        case 0x06:         

        {

            Modbus06H();

            break;

        } 

        /*10功能写多个寄存器*/

        case 0x10:          

        {

            Modbus10H();

            break;

        }           

        default:     

        {

            break;

        }                               

    }

}


在不同的协议解析中,根据MODBUS的要求当传输回来是03指令时,需要将寄存器的数据按照要求的长度和CRC校验后发送出去,如果是06指令,那么就返回响应成功的指令。


03功能码的处理函数,此函数通过for语句持续将寄存器中数据打包发送,并添加CRC校验

static void Modbus03H(void)

{

    uint16_t u16Register = 0;

    uint16_t u16Number = 0;

    uint16_t u16LoopData = 0;

    /*64大小计算出来最多32个寄存器*/    

    uint8_t  u8RegisterValue[128];   

    /*查询错误标志正确*/

    gtModbusData.u8RspCode = RSP_OK;               

    if(gtModbusData.u8RxCount != 8)          

    {

        /*数值域错误,数据位数不对就直接返回*/

        gtModbusData.u8RspCode = RSP_ERR_VALUE;      

        return;

    }

    /*取寄存器的首地址*/

    u16Register = u16BeBufToUint16(& gtModbusData.u8RxBuf[2]);    

    /*取多少位的长度*/

    u16Number = u16BeBufToUint16(& gtModbusData.u8RxBuf[4]);   

    /*如果读到数据长度不对*/

    if(u16Number > sizeof(u8RegisterValue) / 2)                  

    {

        /*则数值域错误*/

        gtModbusData.u8RspCode = RSP_ERR_VALUE;                  

        return;

    }   

    for(u16LoopData = 0; u16LoopData < u16Number; u16LoopData++)

    {

        if(ModBusReadRegisterValue(u16Register,& u8RegisterValue[2 * u16LoopData]) == 0)

        {

            /*如果读到的数据长度为0,则寄存器地址错误,查询错误标志为寄存器地址错误*/

            gtModbusData.u8RspCode = RSP_ERR_REG_ADDR;  

            break;          

        }

        u16Register++;

    }

    /*如果查询标志正确*/

    if(gtModbusData.u8RspCode == RSP_OK)               

    {

        gtModbusData.u8TxCount = 0;

        /*之前获取到的地址码*/

        gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = gtModbusData.u8RxBuf[0];  

        /*功能码*/

        gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = gtModbusData.u8RxBuf[1];   

        gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = u16Number * 2;             

        for(u16LoopData = 0; u16LoopData < u16Number; u16LoopData++)

        {

            gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = u8RegisterValue[2 * u16LoopData];      

            gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = u8RegisterValue[2 * u16LoopData + 1];  

        }    

        /*计算要发送的CRC*/     

        gtUnionUword.u16word = Crc16(gtModbusData.u8TxBuf,gtModbusData.u8TxCount); 

        /*将原有数据右移8位在尾部添加CRC*/

        gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = gtUnionUword.u8byte[1];   

        gtModbusData.u8TxBuf[gtModbusData.u8TxCount++] = gtUnionUword.u8byte[0];   

        /*通过串口发送出去*/    

        UartA3_SendBuf(gtModbusData.u8TxBuf,gtModbusData.u8TxCount);               

    }

}


06功能码解析函数,此函数用于将发送进来的数据解析后根据地址写入寄存器

void Modbus06H(void)

{

    uint16_t u16Register = 0;

    uint16_t u16Value = 0;

    /*如果查询标志正确*/

    gtModbusData.u8RspCode = RSP_OK;                 

    

    if(gtModbusData.u8RxCount != 8)

    {

        /*数值域错误*/

        gtModbusData.u8RspCode = RSP_ERR_VALUE;      

        return;

    }

    /*取出寄存器的初始地址码*/

    u16Register = u16BeBufToUint16(& gtModbusData.u8RxBuf[2]);  

    /*取要写寄存器的长度*/

    u16Value = u16BeBufToUint16(& gtModbusData.u8RxBuf[4]);       

    /*返回是1代表写成功*/

    if(ModBusWriteRegisterValue(u16Register,u16Value) == 1)       

    {

         /*如果查询标志正确*/

        if(gtModbusData.u8RspCode == RSP_OK)                          

        {

            /*直接返回应答帧*/

            ModBusSendAckOK();                                         

        }

    }

    else

    {

        /*否则则认为寄存器地址错误*/

        gtModbusData.u8RspCode = RSP_ERR_REG_ADDR;                 

    }

}


通过串口发送一串数据,并在此数据后自动追加CRC校验码

void ModBusSendWithCrc(uint8_t *u8pBuf,uint8_t u8DataLen)

{   

    uint8_t u8Buf[S_TX_BUF_SIZE];

    memcpy(u8Buf,u8pBuf,u8DataLen);

    gtUnionUword.u16word = Crc16(u8pBuf,u8DataLen);

    /*将原有数据右移8位在尾部添加CRC*/

    u8Buf[u8DataLen++] = gtUnionUword.u8byte[1];      

    u8Buf[u8DataLen++] = gtUnionUword.u8byte[0];               

    UartA3_SendBuf(u8Buf,u8DataLen);       

}


此函数是正确应答函数,在03/06功能码解析函数中用到

static void ModBusSendAckOK(void)

{

    uint8_t u8TxBuf[6];

    uint8_t u8DataLoop = 0;        

    for(u8DataLoop = 0; u8DataLoop < 6; u8DataLoop++)          

    {

        u8TxBuf[u8DataLoop] = gtModbusData.u8RxBuf[u8DataLoop];    /*将获取到的数据依次发送出去*/    

    }

    ModBusSendWithCrc(u8TxBuf,6);        /*添加CRC发送出去*/     

}


写到这里依然没有出现对应寄存器读写的函数,其实读写的函数很简单,读取的函数只是输入寄存器的地址,即可将读取到的结果return给函数本身,在03功能码的解析函数中将数据读取出来并追加CRC通过串口发送出去,就达到了读取寄存器的效果

static uint8_t ModBusReadRegisterValue(uint16_t u16RegisterAddr,uint8_t *u8pRegisterValue)

{

    uint16_t u16Value = 0;  

    uint32_t u32Value = 0;

    switch (u16RegisterAddr)

    {      

        /********************可读可写FRAM**********************/   

        /*上行波特率地址*/

        case UP_BAUD_ADD:      

        {

            u16Value = gtFramData.u16FramUpBaud;   

            break;

        }

       default:

        {

           // break;

        }

    }

        u8pRegisterValue[0] = u16Value >> 8;      /*将u16分成两个u8,将取到的数据返回给输入参数*/

        u8pRegisterValue[1] = u16Value;      

        return 1;                             /*表示成功*/

}


大家可以根据自己的要求在本函数中添加相应的地址和数据,自行增加case语句即可;如果要读取32位数据,需要把32位数据拆分成两个16位数据,需要占用两个寄存器。


下面为06功能码写保持寄存器的函数,可以根据自己要求自行添加内部函数,增加case语句即可

static uint8_t ModBusWriteRegisterValue(uint16_t u16RegisterAddr,uint16_t u16RegisterValue)

{  

    switch (u16RegisterAddr)        

    {     

        case VALVE_SWITCH_ADD: 

        {

            break;

        }

                default:

        {

            return 0;  

        }                 

    }

    return 1;


最后分享一个将2字节的数组(小端Little Endian,低字节在前)转换为16位整数的程序

uint16_t u16BeBufToUint16(uint8_t *u8pBuf)

{

    return (((uint16_t)u8pBuf[0] << 8) | u8pBuf[1]);

}


注明:以上的函数适用于MSP430F5438A单片机,程序来源是参考安富莱的MODBUS例程,本人将程序重新优化和改写,目前MODBUS响应速度小于20MS即可响应

进入单片机查看更多内容>>
相关视频
  • RISC-V嵌入式系统开发

  • SOC系统级芯片设计实验

  • 云龙51单片机实训视频教程(王云,字幕版)

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

精选电路图
  • 家用电源无载自动断电装置的设计与制作

  • 用数字电路CD4069制作的万能遥控轻触开关

  • 使用ESP8266从NTP服务器获取时间并在OLED显示器上显示

  • 开关电源的基本组成及工作原理

  • 用NE555制作定时器

  • 带有短路保护系统的5V直流稳压电源电路图

    相关电子头条文章