MODBUS协议是一种已广泛应用于当今工业控制领域的通用通讯协议。通过此协议,控制器相互之间、或控制器经由网络、总线可以和其它设备之间进行通信。Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。一般将主控设备方所使用的协议称为MODBUS Master,从设备方使用的协议称为MODBUS Slave。典型的主设备包括工控机和工业控制器等;典型的从设备如PLC可编程控制器等。MODBUS通讯物理接口可以选用串口(包括RS232、RS485和RS422),也可以选择以太网口
通信遵循以下的过程:
主设备向从设备发送请求
从设备分析并处理主设备的请求,然后向主设备发送结果
如果出现任何差错,从设备将返回一个异常功能码
MODBUS的工作方式是请求/应答,每次通讯都是主站先发送指令,可以是广播,或是向特定从站的单播;从站响应指令,并按要求应答,或者报告异常。当主站不发送请求时,从站不会自己发出数据,从站和从站之间不能直接通讯
MODBUS有三种通信方式:
以太网:对应的通信模式是MODBUS TCP/IP
异步串行传输(各种介质如有线RS-232-/422/485/;光纤、无线等):对应的通信模式是MODBUS RTU或MODBUS ASCII
高速令牌传递网络:对应的通信模式是MODBUS PLUS
MODBUS协议的报文(或帧)的基本格式是:表头 + 功能码 + 数据区 + 校验码
功能码和数据区在不同类型的网络都是固定不变的,表头和校验码则因网络底层的实现方式不同而有所区别。表头包含了从站的地址,功能码告诉从站要执行何种功能,数据区是具体的信息
对于不同类型的网络,MODBUS的协议层实现是一样的,区别在于下层的实现方式,常见的有TCP/IP和串行通讯两种
MODBUS TCP基于以太网和TCP/IP协议,MODBUS RTU和MODBUS ASCII则是使用异步串行传输(通常是RS-232/422/485)
在工业控制领域,工业仪表间的通信比较常用的是基于MODBUS RTU的485口通信,MODBUS RTU协议需要用时间间隔来判断一帧报文的开始和结束,协议规定的时间为3.5个字符周期,就是说一帧报文开始前,必须有大于3.5个字符周期的空闲时间,一帧报文结束后,也必须要有3.5个字符周期的空闲时间;同时一帧报文中,字符间空闲时间大于1.5字符周期
针对3.5个字符周期,其实是一个具体时间,但是这个时间跟波特率相关。在串口通信中,1个字符包括1位起始位、8位数据位(一般情况)、1位校验位(或者没有)、1位停止位(一般情况下),因此1个字符包括11个位,那么3.5个字符就是38.5个位,波特率表示的含义是每秒传输的二进制位的个位,在波特率为9600的情况下,3.5个字符周期=1000ms/9600bit*38.5bit=4.0104167ms
MODBUS RTU详解可参考:
(下载次数: 4, 2022-7-30 23:42 上传)
(下载次数: 5, 2022-7-30 23:42 上传)
此篇主要介绍USART1实现MODBUS RTU从站功能
硬件连接
GND —— GND
TXD —— PA10(RX)
RXD —— PA9(TX)
软件代码
USART代码:
void USART_Initial(void)
{
NVIC_InitType NVIC_InitStructure;
GPIO_InitType GPIO_InitStructure;
USART_InitType USART_InitStructure;
USART_ClockInitType USART_ClockStructure;
/* Enable GPIO clock */
RCC_EnableAPB2PeriphClk(USART_GPIO_CLK, ENABLE);
/* Enable USART Clock */
RCC_EnableAPB2PeriphClk(USART_CLK, ENABLE);
/* Configure the GPIO ports */
/* Initialize GPIO_InitStructure */
GPIO_InitStruct(&GPIO_InitStructure);
/* Configure USART Tx as alternate function push-pull */
GPIO_InitStructure.Pin = USART_TxPin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Alternate = USART_Tx_GPIO_AF;
GPIO_InitPeripheral(USART_GPIO, &GPIO_InitStructure);
/* Configure USART Rx as alternate function push-pull and pull-up */
GPIO_InitStructure.Pin = USART_RxPin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pull = GPIO_Pull_Up;
GPIO_InitStructure.GPIO_Alternate = USART_Rx_GPIO_AF;
GPIO_InitPeripheral(USART_GPIO, &GPIO_InitStructure);
/* USART configuration ------------------------------------------------------*/
USART_StructInit(&USART_InitStructure);
USART_InitStructure.BaudRate = System.Com_Baud.UWD;//115200;
USART_InitStructure.WordLength = USART_WL_8B;
USART_InitStructure.StopBits = USART_STPB_1;
USART_InitStructure.Parity = USART_PE_NO;
USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE;
USART_InitStructure.Mode = USART_MODE_RX | USART_MODE_TX;
/* Configure USART */
USART_Init(USART, &USART_InitStructure);
USART_ClockStructure.Clock = USART_CLK_DISABLE;
USART_ClockStructure.Polarity = USART_CLKPOL_LOW;
USART_ClockStructure.Phase = USART_CLKPHA_2EDGE;
USART_ClockStructure.LastBit = USART_CLKLB_DISABLE;
USART_ClockInit(USART, &USART_ClockStructure);
USART_ClrFlag(USART, USART_FLAG_TXDE);
USART_ClrFlag(USART, USART_FLAG_RXDNE);
/* Enable USART Receive interrupts */
USART_ConfigInt(USART, USART_INT_TXDE, DISABLE);
USART_ConfigInt(USART, USART_INT_RXDNE, ENABLE);
/* NVIC configuration */
/* Configure the NVIC Preemption Priority Bits */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
/* Enable the USART Interrupt */
NVIC_InitStructure.NVIC_IRQChannel = USART_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
UART_STR.Send_Flag = 0;
UART_STR.Read_Flag = 0;
UART_STR.Send_Dly = 0;
UART_STR.In_Num = 0;
UART_STR.Out_Num = 0;
UART_STR.Start = 0;
UART_STR.Read_Dly = 0;
UART_STR.Read_All = 0;
/* Enable the USART */
USART_Enable(USART, ENABLE);
}
MODBUS RTU从站代码:
void Serial_Set_Time(void)
{
switch(System.Com_Baud.UWD)
{
case 2400:
System.Delay_Time = 6;
System.Stop_Time = 7;
break;
case 4800:
System.Delay_Time = 3;
System.Stop_Time = 7;
break;
case 9600:
System.Delay_Time = 2;
System.Stop_Time = 4;
break;
case 19200:
case 38400:
System.Delay_Time = 1;
System.Stop_Time = 2;
break;
default:
System.Delay_Time = 1;
System.Stop_Time = 2;
break;
}
}
//发送接收延时,每毫秒运算一次,放在1ms定时器中断函数中
void USART_SendRec_Dly(void)
{
if(UART_STR.Send_Flag)
{
if(UART_STR.Send_Dly == 0)
{
UART_STR.Send_Flag = 0;
USART_ConfigInt(USART, USART_INT_TXDE, ENABLE);
USART_ConfigInt(USART, USART_INT_RXDNE, DISABLE);
USART1->STS |= USART_INT_TXDE;
}
else
{
UART_STR.Send_Dly--;
}
}
if(UART_STR.Read_Dly >= System.Delay_Time) //2400:Read_Dly>=6;4800:Read_Dly>=3;9600:Read_Dly>=2;19200:Read_Dly>=1
{
UART_STR.Read_Flag = 1;
if(UART_STR.Start > System.Stop_Time) //2400:Read_Start>14;4800:Read_Start>7,9600:Read_Start>4,19200:Read_Start>2
{
UART_STR.Read_All = 1;
}
}
else
{
UART_STR.Read_Dly++;
UART_STR.Read_Flag = 0;
}
}
void USART_Send_Ready(void)
{
UART_STR.Send_Flag = 1;
UART_STR.Send_Dly = 1;
UART_STR.In_Num = 0;
UART_STR.Start = 0;
UART_STR.Read_All = 0;
UART_STR.SendBuf = UART_STR.OutBuf;
}
/*-------------------分割线-------------------*/
//Function name: GetCRC16
//Descriptions: 计算CRC16校验码
//input parameters: 指针地址:puchMsg,数据长度:usDataLen!!!注意usDataLen>2!!!
//output parameters:输出CRC16
//Returned value: 无
//MARK: 无
/*-------------------分割线-------------------*/
unsigned short int GetCRC16(unsigned char *puchMsg, unsigned short int usDataLen)
{
unsigned char uchCRCHi = 0xFF; /* 高CRC字节初始化 */
unsigned char uchCRCLo = 0xFF; /* 低CRC 字节初始化 */
unsigned int uIndex = 0; /* CRC循环中的索引 */
while (usDataLen--) /* 传输消息缓冲区 */
{
uIndex = uchCRCHi ^ *puchMsg++; /* 计算CRC */
uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex];
uchCRCLo = auchCRCLo[uIndex];
}
return (unsigned short int)((unsigned short int)uchCRCHi << 8 | uchCRCLo);
}
//0x06、0x10写寄存器命令,控制板载LED灯亮灭测试
void Function_Write_Address(unsigned short int Addr, unsigned short int Temp)
{
UNION_WD_2BY Temp_Data;
Temp_Data.HUWD = Temp;
switch(Addr)
{
case 0x00:
if(Temp_Data.HUWD)
{
GPIO_SetBits(LED1_PORT, LED1_PIN);
}
else
{
GPIO_ResetBits(LED1_PORT, LED1_PIN);
}
break;
case 0x01:
if(Temp_Data.HUWD)
{
GPIO_SetBits(LED2_PORT, LED2_PIN);
}
else
{
GPIO_ResetBits(LED2_PORT, LED2_PIN);
}
break;
case 0x02:
if(Temp_Data.HUWD)
{
GPIO_SetBits(LED3_PORT, LED3_PIN);
}
else
{
GPIO_ResetBits(LED3_PORT, LED3_PIN);
}
break;
default:
break;
}
}
/*-------------------分割线-------------------*/
//Function name: ModBus_Process
//Descriptions: ModBus_处理函数
//input parameters: 无
//output parameters:
//Returned value: 无
//MARK: 无
/*-------------------分割线-------------------*/
unsigned int USART_Deal(unsigned char *inbuff, unsigned short int Incount, unsigned char *outbuff)
{
static union
{
short int IWD;
unsigned short int UWD;
unsigned char UBY[2];
} SSADR, SSDAT, SSEND;
static unsigned char CommFun;
static unsigned short int Temp_OutNum;
static unsigned char *sbuff;
static unsigned short int Cal_CRC16, Tem_CRC16;
static unsigned short int Temp_Cnt;
UNION_WD_2BY DO_Temp, Temp_Data;
unsigned char i;
CommFun = *(inbuff + 1);
SSADR.UBY[INTER_B_H] = *(inbuff + 2);
SSADR.UBY[INTER_B_L] = *(inbuff + 3);
SSDAT.UBY[INTER_B_H] = *(inbuff + 4);
SSDAT.UBY[INTER_B_L] = *(inbuff + 5);
SSEND.UWD = SSADR.UWD + SSDAT.UWD;
Temp_OutNum = 0;
if(Incount > 2)
{
Cal_CRC16 = GetCRC16(UART_STR.InBuf, Incount - 2);
}
Tem_CRC16 = *(inbuff + Incount - 2);
Tem_CRC16 <<= 8;
Tem_CRC16 += *(inbuff + Incount - 1);
if(Cal_CRC16 == Tem_CRC16)
{
switch(CommFun)
{
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 8:
if( Incount == 8 ) break;
else return 0;
case 7:
case 11:
case 12:
case 17:
if( Incount == 12 ) break;
else return 0;
case 15:
case 16:
if( Incount == (*(inbuff + 6) + 9) ) break;
else return 0;
case 20:
case 21:
if( (Incount == (*(inbuff + 2) + 5)) && (*(inbuff + 3) == 6) ) break;
else return 0;
case 22:
if( Incount == 10 ) break;
else return 0;
case 23:
if( Incount == (*(inbuff + 10) + 13) ) break;
else return 0;
case 24:
if( Incount == 6 ) break;
else return 0;
case 43:
if( Incount == 7 ) break;
else return 0;
default:
if( CommFun < 0x80 ) break;
else return 0;
}
}
else return 0;
sbuff = outbuff;
*sbuff++ = System.Com_Addr.UBY[0]; //通讯地址
*sbuff = CommFun;
if(CommFun == 3 || CommFun == 4) //0x03、0x04命令读寄存器
{
if(SSDAT.UWD == 0 || SSDAT.UWD > 0X007D) //查询最大数据不能超过125,异常码0x03
{
(*sbuff) += 0x80;
*(++sbuff) = 0x03;
Temp_OutNum = 3;
}
else if( SSADR.UWD >= 250 || (SSEND.UWD >= 251) ) //读取范围设定值,异常码0x02
{
(*sbuff) += 0x80;
*(++sbuff) = 0x02;
Temp_OutNum = 3;
}
else
{
*(++sbuff) = SSDAT.UWD * 2;
sbuff = outbuff + 3;
for(Temp_Cnt = SSADR.UWD; Temp_Cnt < SSEND.UWD; Temp_Cnt++) //100~240
{
if(Temp_Cnt >= 0x00 && Temp_Cnt < 0x31)
{
switch(Temp_Cnt)
{
//LED1状态显示
case 0x00:
LED1_State = GPIO_ReadOutputDataBit(LED1_PORT, LED1_PIN);
*sbuff++ = 0x00;
*sbuff++ = LED1_State;
break;
//LED2状态显示
case 0x01:
LED2_State = GPIO_ReadOutputDataBit(LED2_PORT, LED2_PIN);
*sbuff++ = 0x00;
*sbuff++ = LED2_State;
break;
//LED3状态显示
case 0x02:
LED3_State = GPIO_ReadOutputDataBit(LED3_PORT, LED3_PIN);
*sbuff++ = 0x00;
*sbuff++ = LED3_State;
break;
//KEY1状态显示
case 0x03:
KEY1_State = GPIO_ReadInputDataBit(GPIOA, GPIO_PIN_4);
*sbuff++ = 0x00;
*sbuff++ = KEY1_State;
break;
//KEY2状态显示
case 0X04:
KEY2_State = GPIO_ReadInputDataBit(GPIOA, GPIO_PIN_5);
*sbuff++ = 0x00;
*sbuff++ = KEY2_State;
break;
//KEY3状态显示
case 0X05:
KEY3_State = GPIO_ReadInputDataBit(GPIOA, GPIO_PIN_6);
*sbuff++ = 0x00;
*sbuff++ = KEY3_State;
break;
//...
default:
*sbuff++ = 0;
*sbuff++ = 0;
break;
}
}
else
{
*sbuff++ = 0;
*sbuff++ = 0;
}
}
Temp_OutNum = *(outbuff + 2) + 3;
}
}//03读寄存器命令结束
//06写单个寄存器命令
else if(CommFun == 6)
{
// if(SSDAT.UWD >= 0x00 && SSDAT.UWD <= 0xFFFF)
// {
// (*sbuff) += 0x80;
// *(++sbuff) = 0x03;
// Temp_OutNum = 3;
// }
// else
{
if((SSADR.UWD >= 0x03 && SSADR.UWD <= 0X05) || SSADR.UWD >= 250 )//0x06写入命令地址设定
{
(*sbuff) += 0x80;
*(++sbuff) = 0x02;
Temp_OutNum = 3;
}
else
{
Function_Write_Address(SSADR.UWD, SSDAT.UWD);
sbuff = outbuff + 2;
*sbuff++ = SSADR.UBY[INTER_B_H];
*sbuff++ = SSADR.UBY[INTER_B_L];
*sbuff++ = SSDAT.UBY[INTER_B_H];
*sbuff++ = SSDAT.UBY[INTER_B_L];
Temp_OutNum = 6;
}
}
}
//写多个寄存器命令
else if(CommFun == 16)
{
if(SSDAT.UWD == 0 || SSDAT.UWD > 0x7b || SSDAT.UWD * 2 != *(inbuff + 6) )
{
(*sbuff) += 0x80;
*(++sbuff) = 0x03;
Temp_OutNum = 3;
}
else if( SSADR.UWD >= 250 || (SSEND.UWD >= 251)) //约束写入地址
{
(*sbuff) += 0x80;
*(++sbuff) = 0x02;
Temp_OutNum = 3;
}
else
{
if(*(inbuff + 6) > 0x07) //判断寄存器数量
{
(*sbuff) += 0x80;
*(++sbuff) = 0x03;
Temp_OutNum = 3;
}
else
{
ModBus_CodeAddress.UBY[1] = *(inbuff + 2);
ModBus_CodeAddress.UBY[0] = *(inbuff + 3); //根据通讯地址来写入数据
ModBus_DataLength.UBY[1] = *(inbuff + 4);
ModBus_DataLength.UBY[0] = *(inbuff + 5);
ModBus_DataEnd = ModBus_CodeAddress.UWD + ModBus_DataLength.UWD;
for(i = 7, ModBus_Temp = ModBus_CodeAddress.UWD; ModBus_Temp < ModBus_DataEnd; ModBus_Temp++, i = i + 2) //100~240
{
Temp_Data.UBY[1] = *(inbuff + i);
Temp_Data.UBY[0] = *(inbuff + i + 1);
Function_Write_Address(ModBus_Temp, Temp_Data.HUWD); //写入的地址及数据(对应0X06命令)
}
sbuff = outbuff + 2;
*sbuff++ = SSADR.UBY[INTER_B_H];
*sbuff++ = SSADR.UBY[INTER_B_L];
*sbuff++ = SSDAT.UBY[INTER_B_H];
*sbuff++ = SSDAT.UBY[INTER_B_L];
Temp_OutNum = 6;
}
}
}
else
{
(*sbuff) += 0x80;
*(++sbuff) = 0x01;
Temp_OutNum = 3;
}
sbuff = outbuff;
if(Temp_OutNum > 2)
Cal_CRC16 = GetCRC16(sbuff, Temp_OutNum);
sbuff = outbuff + Temp_OutNum;
*(sbuff++) = (Cal_CRC16 >> 8);
*(sbuff++) = Cal_CRC16;
Temp_OutNum += 2;
return Temp_OutNum;
}
/*
* [url=home.php?mod=space&uid=159083]@brief[/url] This function handles USART global interrupt request.
*/
void USART1_IRQHandler(void)
{
unsigned char TempData;
if (USART_GetIntStatus(USART, USART_INT_RXDNE) != RESET)
{
/* Read one byte from the receive data register */
TempData = USART_ReceiveData(USART);
USART_ClrIntPendingBit(USART, USART_INT_RXDNE);
if(UART_STR.In_Num == 0)
{
if(TempData == System.Com_Addr.UBY[0] && UART_STR.Read_All == 0 && UART_STR.Read_Flag == 1) //接受第一个字符时T3.5有没有到 没有到当前字符取消
{
UART_STR.InBuf[UART_STR.In_Num++] = TempData;
UART_STR.Start = 1;
}
}
else if(UART_STR.Read_Flag == 0) //接受其他字符时候没有超过T1.5的延时
{
if(UART_STR.In_Num >= 260)
{
UART_STR.In_Num = 0; //接受长度不对 重新接受
UART_STR.Start = 0;
}
else
{
UART_STR.Start++; //等待3.5个字符的延时表示接受结束
UART_STR.InBuf[UART_STR.In_Num++] = TempData;
}
}
else
{
UART_STR.In_Num = 0;
}
UART_STR.Read_Dly = 0;
UART_STR.Read_Flag = 0; //防止在帧前格外加的字符,如果是第1个字符不是地址重新等待1.5个字符
}
else if (USART_GetIntStatus(USART, USART_INT_TXDE) != RESET)
{
if(UART_STR.Out_Num != 0)
{
UART_STR.Out_Num--;
USART_SendData(USART, *UART_STR.SendBuf);
UART_STR.SendBuf++;
}
else
{
USART_ConfigInt(USART, USART_INT_TXDE, DISABLE);
}
USART_ClrIntPendingBit(USART, USART_INT_TXDE); //Clear the Uart_Hard transmit interrupt
if(UART_STR.Out_Num <= 1)
USART_ConfigInt(USART, USART_INT_RXDNE, ENABLE);
}
else
{
USART_ClrFlag(USART, USART_FLAG_OREF | USART_FLAG_NEF | USART_FLAG_FEF | USART_FLAG_PEF); //注意,清除出错标志
}
}
运行测试
从站地址设为0x01,波特率设为115200,初始设置板载LED1、LED3亮,LED2灭,KEY1、KEY2、KEY3按键松开(GPIO设置为上拉输入,按下时为0低电平,松开时为1高电平)
0x03读寄存器命令
使用modscan32测试,设置从站地址、0x03读命令
点击连接设置-->连接,选择调试串口,设置串口参数,点击确认
连接成功后,显示LED1-3,KEY1-3当前状态
0x06写寄存器命令
双击更新设置LED1、LED2、LED3寄存器值,LED1、LED3灭,LED2亮
更新后,显示
测试代码
modscan32软件