目录

一、主从模式

二、modbus协议

1、modbus的两种数据帧格式

2、modbus在rs485上的实现

 三、总结


        在rs485的通信方式中,485总线上可以挂载多个设备,但是485是一种半双工的通信方式(在一个时间段只能与一个设备通信),如果不对挂载的节点设备加以限制,会引起通信紊乱的问题。为了解决数据传输紊乱的问题,我们得对485通信加以规则限制,来保证通信的稳定性和可靠性。举个例子来说明rs485与modbus的关系:我们把城市A看做4主机,城市B看做从机;如果两个城市之间要想进行经济往来(数据传输),那么第一件事就是修路,而这里的公路就是rs485总线;既然公路修好了,就可以通车了,但如果我们不对路上的车辆加以限制的话,那发生交通事故的可能性就会很大,所以我们制定了一套交通规则去限制车辆的行驶,而这个"交通规则"就是modbus。

rs485通信模型
rs485总线示意图

一、主从模式

        485是一种半双工通信方式(设备发送数据的时候不能接收数据),主从模式时为了解决485与多个设备通信出现通信数据紊乱的问题。在主从模式里,有这样几个规定:

        1、系统中的主机和所有从机在上电之后都处于监听状态。

        2、从机不可以主动向主机发送数据,同一时刻只能有一个从机与主机通信。

        3、任何一次通信(数据交换)都由主机发起。主机发送数据请求之后就转为接收数据状态。主机按照预先约定好的格式发出寻址数据帧。

        在约定好主从模式后,485通信主机和多从机的通信就可以互不干扰,485就可以选择任意一个节点设备进行通信,但我们还得考虑一个问题,主机和节点设备该用怎么的数据帧来传输数据呢?这里可以有两种方式:1.自定义数据帧格式;2.使用现有的通信协议(modbus)。很显然自定义的数据帧格式的兼容性不强,只能用于自己设计的485通信设备,而modbus有更强的兼容性。

二、modbus协议

modbus是一种串行,modbus协议目前存在用于串口、以太网、485以及Tcp/IP等,modbus是一个请求/应答协议,并且提供功能码规定的服务。modbus功能码是 modbus请求/应答 PDU(协议数据单元)的元素。本文件的作用是描述 modbus事务处理框架内使用的功能码。

1、modbus的两种数据帧格式

1 RTU 传输模式:当设备使用 RTU (Remote Terminal Unit) 模式在 Modbus 串行链路通信, 报文中每个 8 位字节含有两个 4 位十六进制字符。这种模式的主要优点是较高的数据密度,在相同的波特率下比 ASCII 模式有更高的吞吐率。每个报文必须以连续的字符流传送。

Modbus 报文 RTU 帧:由发送设备将 Modbus 报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。在 RTU 模式,报文帧由时长至少为 3.5 个字符时间的空闲间隔区分。在后续的部分,这个时间区间被称作 t3.5

 注:整个报文帧必须以连续的字符流发送。(定时器定时)如果两个字符之间的空闲间隔大于 1.5 个字符时间,则报文帧被认为不完整应该被接收节点丢弃。对于波特率大于 19200 Bps 的情形,应该使用 2 个定时的固定值:建议的字符间超时时间(t1.5)为750µs,帧间的超时时间 (t1.5) 为1.750ms。

2 ASCII 传输模式:由发送设备将 Modbus 报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。ASCII详情见modbus手册。

2、modbus在rs485上的实现

主机用的是stm32f407,从机用的stm32f103。

主机的modbus实现

//主机向从机发送数据交换请求后,接收到从机的数据时,主机没有做CRC校验
#include "modbus.h"
#include "sp3485.h"
#include "CRC16.h"

extern u8 flag;

温度、湿度、热释电、光照、烟雾、有毒气体
//uint16_t ModBus_Buf[6] = {0,0,0,0,0,0};//从机数据
//**************************功能码******************************************************
//功能码请求PDU
void ModBus_Send03Req(uint8_t DeviceID,uint16_t StartAddr,uint16_t Count)
{
    uint8_t Send_Buf[16]  = "\0";
	uint16_t Temp_crc = 0;
	Send_Buf[0] = DeviceID;
	Send_Buf[1] = 03;
	Send_Buf[2] = (StartAddr>>8);
	Send_Buf[3] = (StartAddr&0XFF);
	Send_Buf[4] = (Count>>8);
	Send_Buf[5] = (Count&0XFF);
	Temp_crc = CRC16_Fanction(Send_Buf,6);
	Send_Buf[6] = (Temp_crc&0XFF);
	Send_Buf[7] = (Temp_crc>>8);
	RS485_Send_Data(Send_Buf,8);
}

/*
open led1    ModBus_Send05Req(1,6,0x01);
open led2    ModBus_Send05Req(1,6,0x10);
open ledall  ModBus_Send05Req(1,6,0x11);
open  beep   ModBus_Send05Req(1,7,1);
close beep   ModBus_Send05Req(1,7,0);*/

void ModBus_Send06Req(uint8_t DeviceID,uint16_t InputAddr,uint16_t OutVlaue)
{
    uint8_t Send_Buf[16]  = "\0";
	uint16_t Temp_crc = 0;
	
	Send_Buf[0] = DeviceID;
	Send_Buf[1] = 06;
	Send_Buf[2] = (InputAddr>>8);
	Send_Buf[3] = (InputAddr&0XFF);
	Send_Buf[4] = (OutVlaue>>8);
	Send_Buf[5] = (OutVlaue&0XFF);
	Temp_crc = CRC16_Fanction(Send_Buf,6);
	Send_Buf[6] = (Temp_crc&0XFF);
	Send_Buf[7] = (Temp_crc>>8);
	
	RS485_Send_Data(Send_Buf,8);
}

从机的modbus实现

#include "modbus.h"
#include "rs485.h"
#include <string.h>
#include <stdio.h>
#include "CRC16.h"
#include "oled.h"
#include "led.h"

//温度、湿度、热释电、光照、烟雾、有毒气体、led 、beep
uint16_t ModBus_Buf[8] = {0,0,0,0,0,0,0,0};
u16 Slave_ID = 0;

//modbus中断
void TIM4_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM4,TIM_IT_Update) == SET)
	{
		TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
		
		RS485_DataBuf.Flag_over = 1;//一帧接收完成
        
		TIM_Cmd(TIM4,DISABLE);//关闭定时器
	}
}
/* Modbus数据帧格式(3.5t=3.5个字符时间间隔)
    起始位 + 设备地址 + 功能代码 + 数据 + CRC校验 + 结束符
     3.5t      8Bit      8Bit     n*8Bit  16Bit     3.5t
*/
void Modbus_Init(u16 id, u32 brr)
{
    u32 time_arr = 0;
    Slave_ID = id;
    
    time_arr = (44000000/brr);//至少t3.5,我们使用4个字符间隔(单位us)
    ModBus_TIM4_Config(time_arr);
}
//modbos定时器,定时周期为4个字符时间
void ModBus_TIM4_Config(uint32_t arr)
{
	TIM_TimeBaseInitTypeDef Base_InitStructure = {0};
	NVIC_InitTypeDef NVIC_InitStructure = {0};
	//时钟使能
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
	//配置定时器:
	//配置基本定时器:时钟预分频、ARR(LOAD)、CNT(VAL)、计数方式、分频因子
	Base_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//分频因子
	Base_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
	Base_InitStructure.TIM_Period = arr-1;//重装载ARR
	Base_InitStructure.TIM_Prescaler = 72-1;//预分频psc,us
	TIM_TimeBaseInit(TIM4,&Base_InitStructure);
	TIM_SetCounter(TIM4,0);		//计数器清零
	TIM_Cmd(TIM4,DISABLE);
	
	TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE);//打开定时器更新中断
	
	NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	//定时器使能
	TIM_Cmd(TIM4,DISABLE);
}
//ModBus 获取数据并解析(接收到的数据)
void ModBus_GetData(void)
{
    u16 CRC_Get = 0,CRC_Temp = 0;
    if(RS485_DataBuf.Flag_over == 1)
    {
        //如果接收到的数据帧里面有数据
        if(RS485_DataBuf.Buf_count > 4)//设备地址 + 功能代码 + CRC校验 = 4Bit
        {
            //获取数据最后两位的CRC校验码
            CRC_Get = (RS485_DataBuf.Get_buf[RS485_DataBuf.Buf_count-1]<<8)|(RS485_DataBuf.Get_buf[RS485_DataBuf.Buf_count-2]);
            //验证接收到数据的CRC校验码
            CRC_Temp = CRC16_Fanction(RS485_DataBuf.Get_buf,RS485_DataBuf.Buf_count-2);
            if(CRC_Get == CRC_Temp)//CRC校验通过,数据无错误
            {
                //判断是不是给我的数据
				if(RS485_DataBuf.Get_buf[0] == 0X00)
					ModBus_Rsp41(); //广播
				else if(RS485_DataBuf.Get_buf[0] == Slave_ID)
				{
					switch(RS485_DataBuf.Get_buf[1])
					{
						case 0X03:ModBus_Rsp03();break;//读保持寄存器
                        case 0x06:ModBus_Rsp06();break;//写单个线圈
                        default:break;
					}
				}
				else
					printf("not my data\r\n");
            }
        }
        //复位接收缓冲区:为了接收下一组数据
		RS485_DataBuf.Flag_over = 0;
		RS485_DataBuf.Buf_count = 0;
		memset(RS485_DataBuf.Get_buf,0,256);
    }
}
//**************************功能码******************************************************//
/*
功能码0x03
resPDU:功能码(1B)+起始地址(2B)+寄存器数量(2B)
rspPUD:功能码(1B)+字节数(1B)2*N  +寄存器值(N*2B)
*/
void ModBus_Rsp03(void)
{
    uint8_t Send_Buf[256]  = "\0";
	uint16_t i = 0,Temp_addr=0;
	uint16_t Temp_crc = 0;
    
    Send_Buf[0] = Slave_ID; //设备地址
    Send_Buf[1] = 0x03;     //功能代码
    Send_Buf[2] = ((RS485_DataBuf.Get_buf[4]<<8)|(RS485_DataBuf.Get_buf[5]))*2;       //字节数
    //res的请求地址
    Temp_crc = (RS485_DataBuf.Get_buf[2]<<8)|(RS485_DataBuf.Get_buf[3]);
    for(i=0; i<Send_Buf[2]/2; i++)
    {   //温度、湿度、光照、烟雾、灯、按键、蜂鸣器
        Send_Buf[3+2*i] = (ModBus_Buf[Temp_addr+i]>>8);
        Send_Buf[4+2*i] = (ModBus_Buf[Temp_addr+i]&0XFF);
    }
    Temp_crc = CRC16_Fanction(Send_Buf,3+Send_Buf[2]);
	Send_Buf[3+2*i] = (Temp_crc&0XFF);
	Send_Buf[4+2*i] = (Temp_crc>>8);
	
	RS485_Send_Data(Send_Buf,5+Send_Buf[2]);
}
/*
功能码0x06
resPDU:功能码(1B)+起始地址(2B)+寄存器数量(2B)
rspPUD:功能码(1B)+起始地址(2B)+寄存器数量(2B)*/
void ModBus_Rsp06(void)
{
    uint8_t Send_Buf[256]  = "\0";
	uint16_t i = 0;
    u16 index = 0,value = 0;
    
    for(i=0; i<16; i++)
    {
        Send_Buf[i] = RS485_DataBuf.Get_buf[i];
    }
    index = Send_Buf[2]<<8 | Send_Buf[3];
    value = Send_Buf[4]<<8 | Send_Buf[5];
    
    if(index == 0x06)//led
    {
        if(value == 0x11)
        {
            LED1_H;LED2_H;
        }
        else if(value == 0x01)
        {
            LED2_H;LED1_L;
        }
        else if(value == 0x10)
        {
            LED2_L;LED1_H;
        }
        else{LED1_L;LED2_L;}    
    }
    if(index == 0x07)//beep
    {
        if(value == 0x01)
            BEEP_ON;
        else 
            BEEP_OFF;
    }
	RS485_Send_Data(Send_Buf,8);
}
//广播回应函数
void ModBus_Rsp41(void)
{
    uint8_t Send_Buf[256]  = "\0";
	uint16_t Temp_crc = 0;

	Slave_ID = RS485_DataBuf.Get_buf[2];
	
	Send_Buf[0] = Slave_ID;//自己设备ID
	Send_Buf[1] = 0X41;
	Temp_crc = CRC16_Fanction(Send_Buf,2);
	Send_Buf[2] = (Temp_crc&0XFF);
	Send_Buf[3] = (Temp_crc>>8);
	
	RS485_Send_Data(Send_Buf,4);
}

 三、总结

        1、rs485总线是一种半双工通信总线,在同一时间只能接受或发送,可以挂载多个节点设备。

        2、为了解决主机与从机通信的稳定性和可靠性,我们规定了主从模式,同一时刻只能一主一从进行通信。但主机和从机用什么样的数据帧进行通信还没有确定。

        3、modbus规定了主机与从机通信的数据格式,相对于自定义的数据格式,modbus的兼容性更强。

物联沃分享整理
物联沃-IOTWORD物联网 » rs485与modbus协议

发表评论