发布时间:2025-12-09 18:01:38 浏览次数:4
目录
一、主从模式
二、modbus协议
1、modbus的两种数据帧格式
2、modbus在rs485上的实现
三、总结
在rs485的通信方式中,485总线上可以挂载多个设备,但是485是一种半双工的通信方式(在一个时间段只能与一个设备通信),如果不对挂载的节点设备加以限制,会引起通信紊乱的问题。为了解决数据传输紊乱的问题,我们得对485通信加以规则限制,来保证通信的稳定性和可靠性。举个例子来说明rs485与modbus的关系:我们把城市A看做4主机,城市B看做从机;如果两个城市之间要想进行经济往来(数据传输),那么第一件事就是修路,而这里的公路就是rs485总线;既然公路修好了,就可以通车了,但如果我们不对路上的车辆加以限制的话,那发生交通事故的可能性就会很大,所以我们制定了一套交通规则去限制车辆的行驶,而这个"交通规则"就是modbus。
rs485总线示意图485是一种半双工通信方式(设备发送数据的时候不能接收数据),主从模式时为了解决485与多个设备通信出现通信数据紊乱的问题。在主从模式里,有这样几个规定:
1、系统中的主机和所有从机在上电之后都处于监听状态。
2、从机不可以主动向主机发送数据,同一时刻只能有一个从机与主机通信。
3、任何一次通信(数据交换)都由主机发起。主机发送数据请求之后就转为接收数据状态。主机按照预先约定好的格式发出寻址数据帧。
在约定好主从模式后,485通信主机和多从机的通信就可以互不干扰,485就可以选择任意一个节点设备进行通信,但我们还得考虑一个问题,主机和节点设备该用怎么的数据帧来传输数据呢?这里可以有两种方式:1.自定义数据帧格式;2.使用现有的通信协议(modbus)。很显然自定义的数据帧格式的兼容性不强,只能用于自己设计的485通信设备,而modbus有更强的兼容性。
modbus是一种串行,modbus协议目前存在用于串口、以太网、485以及Tcp/IP等,modbus是一个请求/应答协议,并且提供功能码规定的服务。modbus功能码是 modbus请求/应答 PDU(协议数据单元)的元素。本文件的作用是描述 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手册。
主机用的是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};//从机数据//**************************功能码******************************************************//功能码请求PDUvoid 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 、beepuint16_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;//重装载ARRBase_InitStructure.TIM_Prescaler = 72-1;//预分频psc,usTIM_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;}}elseprintf("not my data\r\n");}}//复位接收缓冲区:为了接收下一组数据RS485_DataBuf.Flag_over = 0;RS485_DataBuf.Buf_count = 0;memset(RS485_DataBuf.Get_buf,0,256);}}//**************************功能码******************************************************///*功能码0x03resPDU:功能码(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]);}/*功能码0x06resPDU:功能码(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;//自己设备IDSend_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的兼容性更强。