RS485+ModBus-RTU协议

文章推荐

MODBUS_RTU通讯规约
Modbus通信协议详解
modbus slave和modbus poll使用说明(含软件下载地址)

RS485简介

RS485:串行、半双工。

物理层

串口是一种接口标准,它规定了接口的电气标准,简单说只是物理层的一个标准。
在串口接口标准上使用各种协议进行通讯及设备控制。
典型的串行通讯标准是RS232和RS485,它们定义了电压,阻抗等,没有定义协议。

1
在这里插入图片描述
RE

差分信号

两条线传输一个信号,使用一对双绞线,其中一根线定义为A,另一个定义为B。两个485接口连接,A连接A,B连接B。
长距离布线会有信号衰减,而且引入噪声和干扰的可能性更大,在线缆A和B上的表现就是电压幅度的变化,但是,采用差分线的好处就是,差值相减就会忽略掉干扰依旧能输出正常的信号,把这种差分接收器忽略两条信号线上相同电压的能力称为共模抑制。
在这里插入图片描述

拓扑结构

RS485有两线制和四线制两种接线,四线制只能实现点对点的通信方式,现很少采用,多采用的是两线制接线方式,这种接线方式为总线拓扑结构,在同一总线上最多可以挂接32个节点。

RS-485总线同I2C,也是主从模式,支持点对点单从机模式,也支持多从机模式,不支持多主机模式。
理想情况下RS485需要2个匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为120Ω)
在这里插入图片描述

引脚连接

在这里插入图片描述
RE引脚在低电平时表示接收数据,高电平时表示发送数据。

ModBus通讯协议

简介

Modbus是一种串行通信协议。

Modbus是一种一主一从的一对一通信方式(主机发一帧,从机回一帧的形式),当然也一主多从,但实际也是一对一通信,同一时刻只能有一个从机进行响应。如果需要和多个从机同时通信,这里也支持使用广播,即主机发送指令,所有从机接收指令并执行,但不进行应答。

当进行一主多从通信时,主机通过从机ID号来区分要通信的从机设备。从机ID范围为1~ 247,0为广播地址,248~255为用户自定义地址。
222
目前总共有4种通信形式,RTU、ASCII、TCP、Plus。

RTU

其报文格式是十六进制的,由Slave ID+数据+CRC校验三部分组成。
要注意的是,数据部分高位数据在前,低位数据在后,而CRC校验则是低位在前,高位在后。即数据存储为小端模式,CRC校验码存储为大端模式

报文解析功能码03

在这里插入图片描述

代码

.c文件

#include <stdio.h>
#include <string.h>
#include "stm32f10x.h"
#include "modbus.h"
#include "lcd.h"
#include "delay.h"

u16 	UART4_RX_STA;
u16		UART4_TX_STA;
u8 		UART4_RX_BUF[UART4_REC_LEN];
u8 		UART4_TX_BUF[UART4_REC_LEN];

int sendsta=1;


modbus_receive				poll;				//poll软件发送(单片机做从机)
modbus_receive				slave;			//slave软件反馈(单片机做主机)

modbus_send 					slav;				//单片机做从机发送数据


u16 ModeBus_Data[5]={0x01,0x02,0x03,0x04,0x05};

void RS485_Init(void)
{
	GPIO_InitTypeDef  	GPIO_InitStructure;		//定义结构体类型变量
	USART_InitTypeDef  	USARTInitDef;
	NVIC_InitTypeDef		NVICInitDef;
		
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);	 //使能始终,使能挂载在APB2,的GPIOA
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE);		//开串口的时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;	 //设置引脚PC10			TX
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 		 //设置成通用推挽复用输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //设置传输速率为50MHZ
	GPIO_Init(GPIOC, &GPIO_InitStructure);//调用GPIO初始化函数,进行初始化

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;	 //设置引脚PC11			RX
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; 		 //设置成浮空输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //设置传输速率为50MHZ
	GPIO_Init(GPIOC, &GPIO_InitStructure);//调用GPIO初始化函数,进行初始化
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;	 //设置引脚PC11			RE
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //设置成浮空输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //设置传输速率为50MHZ
	GPIO_Init(GPIOC, &GPIO_InitStructure);//调用GPIO初始化函数,进行初始化

//--------------------------------------------------------------------------------
	USARTInitDef.USART_BaudRate= 9600;//波特率
	USARTInitDef.USART_HardwareFlowControl= USART_HardwareFlowControl_None;  //是否选择硬件流
	USARTInitDef.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
	USARTInitDef.USART_Parity= USART_Parity_No;           //是否奇偶校验
	USARTInitDef.USART_StopBits=USART_StopBits_1;
	USARTInitDef.USART_WordLength=USART_WordLength_8b;
	USART_Init(UART4, &USARTInitDef);
//--------------------------------------------------------------------------------
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);		//分组
	
	NVICInitDef.NVIC_IRQChannel=UART4_IRQn;
	NVICInitDef.NVIC_IRQChannelCmd=ENABLE;
	NVICInitDef.NVIC_IRQChannelPreemptionPriority=1;
	NVICInitDef.NVIC_IRQChannelSubPriority=1;
	NVIC_Init(&NVICInitDef);
//--------------------------------------------------------------------------------
	USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);		//配置中断
	USART_ITConfig(UART4, USART_IT_IDLE, ENABLE);		//配置中断,中断源一般分开写
	
	USART_Cmd(UART4, ENABLE);//使能串口
	
	RS485_TX_RX=0;
		
}

//中断接收:方法二:利用中断标志位(RXNE.IDLE)注意不要在串口加\r\n
void UART4_IRQHandler() 
{	
	u8 res;
	
	if(USART_GetITStatus(UART4,USART_IT_RXNE))	//SR寄存器的  RXNE位,读寄存器非空,判断是否有数据
	{
		res = USART_ReceiveData(UART4);									//读取数据相当于清除中断
		UART4_RX_BUF[UART4_RX_STA&0X3FFF]=res;						//将接收到数据存入数组
		UART4_RX_STA++;																		//数组长度变量++
		if(UART4_RX_STA>(UART4_REC_LEN-1))UART4_RX_STA=0;//接受的数据长度超过数组最大值,重新开始
	}
	
	if(USART_GetITStatus(UART4,USART_IT_IDLE))	//SR寄存器的  IDLE位,判断总线是否空闲,也就是判断是否接受完数据
	{
		res = UART4->SR;
		res = UART4->DR;	//清空闲中断标志位,看中文数据操作手册:先读SR,再读DR,后清除中断
		UART4_RX_STA|=0x8000;//第十五位置一
	}
	
}


void ModeBus_Send_Data(u8* Buffer,u8 len)			//发送数据	
{
	u8 i;
	RS485_TX_RX=1;
	delay_ms(10);
	for(i=0;i<len;i++)
	{
		while(USART_GetFlagStatus(UART4,USART_FLAG_TC)==RESET);		//判断是否发送完成
		USART_SendData(UART4,Buffer[i]);
	}
	while(USART_GetFlagStatus(UART4,USART_FLAG_TC)==RESET);
	delay_ms(10);
	RS485_TX_RX=0;
}

//使用查表法计算CRC16   高效
//CRC高位字节值表
const u8 auchCRCHi[] = {  
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 
} ; 
/* CRC低位字节值表*/ 
const u8 auchCRCLo[] = {  
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 
0x43, 0x83, 0x41, 0x81, 0x80, 0x40 
} ;

//CRC16计算
u16 crc16(u8 *puchMsg, u16 usDataLen) 
{ 
    u8 uchCRCHi = 0xFF ; 
    u8 uchCRCLo = 0xFF ;  
    u32 uIndex ; 
    while (usDataLen--) 
    { 
        uIndex = uchCRCHi ^ *puchMsg++ ; 
        uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ; 
        uchCRCLo = auchCRCLo[uIndex] ; 
    } 
    return (uchCRCHi << 8 | uchCRCLo) ; 
}
//------------------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------单片机做从机--------------------------------------------------------------

/*
电脑poll软件做主机
配置好后,poll软件自动发送
*/
void ModBus485_Poll(void)				
{
	if(UART4_RX_STA & 0x8000)
	{
		poll.receive_length=UART4_RX_STA & 0X3FFF;				//获取STA的大小,有几次RXNE的中断,就有多少数据传输
		poll.calculate_crc=crc16(UART4_RX_BUF,poll.receive_length-2);																//	计算,求出poll软件发来的校验码(根据前面的数据,计算出主机发送报文格式的校验码)
		poll.receive_crc=((UART4_RX_BUF[poll.receive_length-2]<<8)|(UART4_RX_BUF[poll.receive_length-1]));			//将发来的检验码存入到(主机报文发送的校验码)
		
		if(UART4_RX_BUF[0]==addr)			//地址相同
		{	
			if(poll.calculate_crc==poll.receive_crc)			//检验码相同
			{
				switch(UART4_RX_BUF[1])			//判断是那个功能码
				{
					case 1:
					case 2:break;
					case 3:poll_mod3();break;
				}
				ModeBus_Send_Data(UART4_TX_BUF,UART4_TX_STA);
			}
			memset(UART4_RX_BUF,0,UART4_REC_LEN);			//清零
			memset(UART4_TX_BUF,0,UART4_REC_LEN);
			UART4_RX_STA=0;
			UART4_TX_STA=0;
		}
		sendsta=0;
	}else return;
}
/*
单片机做从机模式3
向poll软件返回数据
*/
void poll_mod3(void)							
{
	u8 i;
	slav.start_addr=((UART4_RX_BUF[2]<<8)|(UART4_RX_BUF[3]));
	slav.num=((UART4_RX_BUF[4]<<8)|(UART4_RX_BUF[5]));
	UART4_TX_BUF[UART4_TX_STA++]=addr;				//从机地址
	UART4_TX_BUF[UART4_TX_STA++]=0x03;							//功能吗
	UART4_TX_BUF[UART4_TX_STA++]=(slav.num*2);		//一个寄存器占两个字节
	for(i=0;i<slav.num;i++)												//传输的寄存器数值
	{
			UART4_TX_BUF[UART4_TX_STA++]=ModeBus_Data[slav.start_addr+i]/256;
			UART4_TX_BUF[UART4_TX_STA++]=ModeBus_Data[slav.start_addr+i];
	}
	slav.send_crc=crc16(UART4_TX_BUF,UART4_TX_STA);						//检验码
	UART4_TX_BUF[UART4_TX_STA++]=slav.send_crc/256;
	UART4_TX_BUF[UART4_TX_STA++]=slav.send_crc;
}


//------------------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------单片机做主机--------------------------------------------------------------
/*
单片机做主机发送(模式3)
参数:
slave:从机的地址
start_addr:起始地址
num:寄存器个数
*/
void master_mode_3(u8 slave,u16 start_addr,u8 num)							
{
	u16 master_crc;
	UART4_TX_BUF[0]=slave;				//从机地址
	UART4_TX_BUF[1]=0x03;							//功能吗
	UART4_TX_BUF[2]=start_addr/256;									//起始地址高位
 	UART4_TX_BUF[3]=start_addr%256;									//起始地址低位
 	UART4_TX_BUF[4]=num/256;												//寄存器个数高位
	UART4_TX_BUF[5]=num%256;												//寄存器个数低位
	
 	master_crc=crc16(UART4_TX_BUF,6); //获取CRC校验位
	UART4_TX_BUF[6]=master_crc/256;
	UART4_TX_BUF[7]=master_crc;
	
	ModeBus_Send_Data(UART4_TX_BUF,8);
	delay_ms(100);
	memset(UART4_TX_BUF,0,UART4_REC_LEN);
}

/*
单片机做主机	处理从机(slave软件)响应数据(模式3)
*/

void ModBus485_Slave(void)				
{
	if(UART4_RX_STA & 0x8000)
	{
		slave.receive_length=UART4_RX_STA & 0X3FFF;				//获取STA的大小,有几次RXNE的中断,就有多少数据传输
		slave.calculate_crc=crc16(UART4_RX_BUF,slave.receive_length-2);																//	计算,求出poll软件发来的校验码(根据前面的数据,计算出主机发送报文格式的校验码)
		slave.receive_crc=((UART4_RX_BUF[slave.receive_length-2]<<8)|(UART4_RX_BUF[slave.receive_length-1]));			//将发来的检验码存入到(主机报文发送的校验码)
		
		if(UART4_RX_BUF[0]==addr)			//地址相同
		{	
			if(slave.calculate_crc==slave.receive_crc)			//检验码相同
			{
				switch(UART4_RX_BUF[1])			//判断是那个功能码
				{
					case 1:
					case 2:break;
					case 3:slave_mod3();break;
				}
			}
			memset(UART4_RX_BUF,0,UART4_REC_LEN);			//清零
			UART4_RX_STA=0;
			
		}
	}else return;
}

/*
单片机做主机
打印出poll软件(从机)返回的数据
*/
void slave_mod3(void)
{
 	int count=(int)UART4_RX_BUF[2];//这是数据个数(不是寄存器个数)
 	
 	printf("从机返回 %d 个寄存器数据\n",count/2);

 	printf("第一个寄存器的值= %d\n",(UART4_RX_BUF[3]<<8)|UART4_RX_BUF[4]);
	printf("第二个寄存器的值= %d\n",(UART4_RX_BUF[5]<<8)|UART4_RX_BUF[6]);
	printf("第三个寄存器的值= %d\n",(UART4_RX_BUF[7]<<8)|UART4_RX_BUF[8]);
	printf("\n");
	delay_ms(1000);
}

.h文件

#ifndef MODBUS_H
#define MODBUS_H

#include <stdio.h>
#include "stm32f10x.h"
#include "stdint.h"
#include "sys.h"

#define 	UART4_REC_LEN 			200
#define 	RS485_TX_RX 				PCout(8)				//RE引脚
#define 	addr						1

extern int sendsta;

/*
返回的数据
单片机做从机时,接收poll软件自动发送的数据
单片机做主机时,接收slave软件返回的数据
*/
typedef struct receive
{
	u16 	calculate_crc;				//计算出主机发送的校验位	
	u16 	receive_crc;							//主机接收到的校验位
	u8 		receive_length;						//接受的数据的长度
}modbus_receive;																					//接收poll软件的发送



/*
发送的数据
*/

typedef struct	send
{
	u16 send_crc;			//从机的发送的校验码
	u16	start_addr;			//起始地址
	u16 num;					//发送寄存器的数目
}modbus_send;													//单片机做主机		的模式3发送



void RS485_Init(void);
void ModeBus_Send_Data(u8* Buffer,u8 len);

void ModBus485_Poll(void);
void poll_mod3(void);

void master_mode_3(u8 slave,u16 start_addr,u8 num);
void ModBus485_Slave(void);
void slave_mod3(void);
#endif
  • 6
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

好好睡觉好好吃饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值