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