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

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

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即可响应

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值