ModBus通信协议结构简单,编程方便,在工业应用现场被广泛使用,特别是PLC应用场合。需要指出的是,ModBus只是一种通信协议,即设备之间的数据约束方式,使用时需要有底层的驱动程序支持,例如,串口通讯。串口通信使用简单,在ModBus协议中应用广泛。在信号的传输方式上又分为RS-232通信,RS-485通信,这种区分只是在数据的传输方式方作划分,底层的驱动程序完全一样。需要长距离、长距离、可靠性高的传输方式时,我们就选择RS-485通信,需要短距离、高速率通信时,我们就用RS-232通信。
这篇博客主要讲述ModBus-RTU通信协议的编程方法,实现时采用串口通信,RS-232的传输方式,采用的单片机为TMS320F280049C。
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协议的可靠性较好。
当设备之间进行通信时,都是以信息帧的结构进行数据之间的传送。信息帧即数据的组成方式,一个信息帧由多位字节数据组成,具体格式如下所示(这里主要讲述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校验的算法后边讲解。
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。
从机响应格式:
如果成功把计算机发送的命令原样返回,否则不响应。
串口通信程序即为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
}
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中断
}
协议层代码执行流程如下所述:
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>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
提问方式:以上程序有啥不懂的可以随时向我提问哈,用微信扫描下方二维码我会在第一时间给大家回复的,谢谢。