一文看懂Modbus通信协议(下)

偶尔善良 提交于 2020-05-02 16:05:04

目录

前言

1. 硬件介绍

1.2 硬件电路介绍

1.2 硬件通信平台

2. 软件介绍

2.1 定时器程序设计

2.1.1 配置时钟函数

2.1.2 定时器中断服务子程序

2.2 串口程序设计

2.2.1 配置串口函数:

2.2.2 初始化中断服务子程序:

2.2.3 串口中断响应事件:

2.3 modbus程序编写

2.2.1 crc16较验程序

2.3.2 Modbus宏定义

2.3.3 Modbus函数初始化

2.3.4 Modbus事件函数

2.3.5 Modbus读功能码处理

2.3.6 Modbus写单个寄存器功能码处理

2.3.7 主函数

前言

首先回顾一下上一节介绍的Modbus通信协议基本理论,首先介绍了Modbus通信协议的主从通信模式特点,分析了Modbus通信的传输特点;其次介绍了两种Modbus通信协议基本的数据格式:Modbus-RTU协议和Modbus-ASCLL协议。Modbus通信协议是在RS-485串口实验的基础上实现的,简单说就是首先要实现RS-485的串口通信,对所收发的数据串按照Modbus的规则编写(比作数据的加密处理)因此在程序编写上主要分为3个步骤:1.实现1ms中断计时的定时器;2.实现发送和接收数据的串口;3.Modbus程序编写。本节将本着从理论落实到实践的角度对Modbus通信协议进行代码实现。

1. 硬件介绍

1.2 硬件电路介绍

微处理器选用:STM32F103;

RS485_INRS485_OUT为收发引脚:选用MCU的PB10和PB11引脚作为RS-485的接收引脚和发送引脚;

RS485_DE为收发状态控制引脚:当RS485_DE为高电平时,芯片处于发送状态;当RS485_DE为低电平时,芯片处于接收状态;

阻抗匹配电路:R44、R45、R46为阻抗匹配电阻

1.2 硬件通信平台

Modbus通信实验平台搭建如下:将USB串口准换成TTL电平,再将TTL电平转换为RS-485差分信号之后连接于STM32从机1。

Modbus通信实验调试软件如下:

下载链接链接:https://pan.baidu.com/s/1ccJkBmZJQhuChypKy-qoug 
        提取码:ppe1 

2. 软件介绍

要想实现Modbus的程序,首先应当完成三件事:

(1)实现1ms中断计时的定时器;

(2)实现发送和接收数据的串口;

(3)Modbus程序编写。

2.1 定时器程序设计

利用TIM2实现1ms的定时中断功能。

2.1.1 配置时钟函数

/******************************************************************
功能: 配置时钟函数
******************************************************************/
//通用定时器 2 中断初始化
void Timer2_Init()  //1ms产生1次更新事件
{
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;//结构体类型初始化:包含自动重装载值,分频系数,计数方式
    //①TIM2时钟使能
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); 
	TIM_DeInit(TIM2);

//定时器TIM2初始化
	TIM_TimeBaseStructure.TIM_Period=1000-1;  // 自动重装载周期  1ms
	TIM_TimeBaseStructure.TIM_Prescaler=72-1;  // 分频系数72M/72=1MHZ-->1us
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;  //设置时钟分频因子为不分频
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;  //计数方式为向上计数
//②初始化定时器参数
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); 
//③设置TIM2允许更新中断
    TIM_ITConfig(TIM2, TIM_IT_Update,ENABLE);//使能TIM2更新中断
//④使能TIM2
	TIM_Cmd(TIM2,ENABLE);
}

2.1.2 定时器中断服务子程序

/******************************************************************
功能: 定时器中断服务子程序
******************************************************************/
void TIM2_IRQHandler()    //定时器2的中断服务子函数  1ms一次中断
{
  u8 st;
  st= TIM_GetFlagStatus(TIM2, TIM_FLAG_Update);	//检测TIM2中断更新标志位
	if(st==SET)  //如果TIM2满足中断标志
	{
	  TIM_ClearFlag(TIM2, TIM_FLAG_Update);  //清除TIM2中断更新标志位
		//每一毫秒所要执行的任务
        if(modbus.timrun!=0)
		{
		  modbus.timout++; 
		  if(modbus.timout>=4)  //间隔时间达到了4毫秒时间
			{
				modbus.timrun=0;//关闭定时器--停止定时
				modbus.reflag=1;  //收到一帧数据
			}
		}  		
	}	
}

2.2 串口程序设计

从库函数操作层面结合寄存器的描述来设置串口,以达到我们最基本的通信功能。串口设置的一般步骤可以总结为如下几个步骤:

(1)串口时钟使能, GPIO 时钟使能;

(2)串口复位;

(3)GPIO 端口模式设置;

(4)串口参数初始化;

(5)开启中断并且初始化 NVIC;

(6)使能串口;

(7)编写中断处理函数。

2.2.1 配置串口函数:

void RS485_Init()
{
GPIO_InitTypeDef  GPIO_InitStructure;
USART_InitTypeDef  USART_InitStructure;
//①串口时钟使能, GPIO 时钟使能,复用时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE); //开启USART2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);  //开启USART2时钟
//②串口复位
USART_DeInit(USART2);  //串口2复位
//RS485DE引脚初始化
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;  //复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure);  //初始化 GPIOA.5
    RS485_RT_0;  //使MAX485芯片处于接收状态(收发控制引脚)
//③GPIO端口模式设置
    //初始化485串口引脚以及串口配置
    //USART1_TX   PB.10
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure);  //初始化 GPIOA.2
    //USART1_RX	PB.11
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);  //初始化 GPIOA.3
//④串口参数初始化、结构体指针成员变量
    USART_InitStructure.USART_BaudRate = 9600;  //设置波特率为9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;  //字长为 8 位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;  //一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;  //无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  //无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;  //收发模式
    USART_Init(USART2, &USART_InitStructure);  //串口参数初始化
//⑤开启中断
    USART_ITConfig(USART2,USART_IT_RXNE,ENABLE); //开启串口响应中断,USART_IT_RXNE接收到数据中断
//⑥使能串口
    USART_Cmd(USART2, ENABLE);//串口使能
    USART_ClearFlag(USART2,USART_FLAG_TC ); //清除串口TC发送完成中断标志

2.2.2 初始化中断服务子程序

/******************************************************************
功能: 初始化NVIC
******************************************************************/
//⑤初始化NVIC(定时器中断+串口中断)
void NVIC_Init()
{
    NVIC_InitTypeDef  NVIC_InitStructure;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //  中断优先级

    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;  //定时器产生更新事件中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;  //抢占优先级1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;  //子优先级2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  //IRQ 通道使能
    NVIC_Init(&NVIC_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;  //抢占优先级1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //子优先级0
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  //IRQ 通道使能
    NVIC_Init(&NVIC_InitStructure);
}

2.2.3 串口中断响应事件:

/******************************************************************
功能: Modbus3字节接收中断处理
******************************************************************/
void USART2_IRQHandler() //MODBUS字节接收中断
{
    u8 st,sbuf;
    st=USART_GetITStatus(USART2, USART_IT_RXNE);  //判断读寄存器是否非空(RXNE)
    if(st==SET)  //返回值是 SET,说明是串口接收到数据完成中断发生
   {   		 
	 sbuf=USART2->DR;
     if( modbus.reflag==1)  //有数据包正在处理
		 {
		   return ;
		 }			 
		  modbus.rcbuf[modbus.recount++]=sbuf;//利用数组存放接收的数据
      modbus.timout=0;  
      if(modbus.recount==1)  //收到主机发来的一帧数据的第一字节
			{
			  modbus.timrun=1;  //启动定时
			}
   } 
}

2.3 modbus程序编写

2.2.1 crc16较验程序

根据crc16的高位字节值表低位字节值表编写校验程序。

/******************************************************************
功能: CRC16校验
******************************************************************/
uint crc16( uchar *puchMsg, uint usDataLen )
{
    uchar uchCRCHi = 0xFF ; // 高CRC字节初始化
    uchar uchCRCLo = 0xFF ; // 低CRC 字节初始化
    unsigned long uIndex ; 		// CRC循环中的索引

    while ( usDataLen-- ) 	// 传输消息缓冲区
    {
        uIndex = uchCRCHi ^ *puchMsg++ ; 	// 计算CRC
        uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
        uchCRCLo = auchCRCLo[uIndex] ;
    }

    return ( uchCRCHi << 8 | uchCRCLo ) ;
}

2.3.2 Modbus宏定义

#define RS485_RT_1 GPIO_SetBits(GPIOA, GPIO_Pin_5)     //485发送状态
#define RS485_RT_0 GPIO_ResetBits(GPIOA, GPIO_Pin_5)   //485置接收状态
typedef struct
{
    u8 myadd;  //本设备的地址
    u8 rcbuf[64];  //Modbus接收缓冲区64个字节
    u16 timout;  //Modbus的数据断续时间	
    u8 recount;  //Modbus端口已经收到的数据个数
    u8 timrun;  //Modbus定时器是否计时的标志
    u8 reflag;  //收到一帧数据的标志
    u8 Sendbuf[64];  //Modbus发送缓冲区	
}
MODBUS;

2.3.3 Modbus函数初始化

/******************************************************************
功能: Modbus函数初始化
******************************************************************/
void Modbus_Init()
{
	modbus.myadd=4;  //本从设备的地址
	modbus.timrun=0;  //Modbus定时器停止计时
    RS485_Init();
}

2.3.4 Modbus事件函数

/******************************************************************
功能: Modbus事件函数
******************************************************************/
void Mosbus_Event()
{
	u16 crc;
	u16 rccrc;
  if(modbus.reflag==0)  //没有收到Modbus的数据
	{
	  return ;
	}
  crc= crc16(&modbus.rcbuf[0], modbus.recount-2);   //计算校验码,-2去除两位校验码
  rccrc=modbus.rcbuf[modbus.recount-2]*256 + modbus.rcbuf[modbus.recount-1];  //收到的校验码
  if(crc == rccrc)  //数据包符号CRC校验规则
	{ 
	  if(modbus.rcbuf[0] == modbus.myadd)  //确认数据包是否是发给本设备的 确认接收的地址是本机地址
		{
		  switch(modbus.rcbuf[1])  //分析功能码
			{
			  case 0:     break;
			  case 1:     break;
		      case 2:     break;
		      case 3:     Modbud_fun3(); break;   //3号功能码处理
		      case 4:     break;
		      case 5:     break;
		      case 6:     Modbud_fun6(); break;   //6号功能码处理
	          case 7:     break;			
        //....				
			}
		}
		else if(modbus.rcbuf[0] == 0)   //如果是广播地址则不处理
		{
		}
	}
	modbus.recount=0;  
    modbus.reflag=0;	
}

2.3.5 Modbus读功能码处理

/******************************************************************
功能: Modbus3号功能码处理
******************************************************************/
void Modbud_fun3()  //3号功能码处理---主机要读取本从机的寄存器
{
    u16 Regadd;  //寄存器起始地址
	u16 Reglen;  //寄存器个数
	u16 byte;
	u16 i,j;
	u16 crc;
	Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];  //得到要读取的寄存器的首地址
	Reglen=modbus.rcbuf[4]*256+modbus.rcbuf[5];  //得到要读取的寄存器的数量
	i=0;
	modbus.Sendbuf[i++]=modbus.myadd;  //本设备地址
    modbus.Sendbuf[i++]=0x03;  //功能码      
    byte=Reglen*2;   //要返回的数据字节数
	modbus.Sendbuf[i++]=byte%256;
	for(j=0;j<Reglen;j++)
	{
	  modbus.Sendbuf[i++]=Reg[Regadd+j]/256;
		modbus.Sendbuf[i++]=Reg[Regadd+j]%256;		
	}
	crc=crc16(modbus.Sendbuf,i);
	modbus.Sendbuf[i++]=crc/256;  //
	modbus.Sendbuf[i++]=crc%256;
	RS485_RT_1; 
	for(j=0;j<i;j++)
	{
	 RS485_byte(modbus.Sendbuf[j]);
	}
	RS485_RT_0;
}

2.3.6 Modbus写单个寄存器功能码处理

/******************************************************************
功能: Modbus6写单个寄存器功能码处理
******************************************************************/
void Modbud_fun6()  //6号功能码处理
{
    u16 Regadd;
	u16 val;
	u16 i,crc,j;
	i=0;
    Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];  //得到要修改的地址 
	val=modbus.rcbuf[4]*256+modbus.rcbuf[5];     //修改后的值
	Reg[Regadd]=val;  //修改本设备相应的寄存器
	//以下为回应主机
	modbus.Sendbuf[i++]=modbus.myadd;//本设备地址
    modbus.Sendbuf[i++]=0x06;        //功能码 
    modbus.Sendbuf[i++]=Regadd/256;
	modbus.Sendbuf[i++]=Regadd%256;
	modbus.Sendbuf[i++]=val/256;
	modbus.Sendbuf[i++]=val%256;
	crc=crc16(modbus.Sendbuf,i);
	modbus.Sendbuf[i++]=crc/256;  //
	modbus.Sendbuf[i++]=crc%256;
	RS485_RT_1;
	for(j=0;j<i;j++)
	{
	 RS485_byte(modbus.Sendbuf[j]);
	}
	RS485_RT_0;
}

2.3.7 主函数

/******************************************************************
功能: 主函数
******************************************************************/
u16 Reg[]={0x0000,   //本设备寄存器中的值
           0x0001,
           0x0002,
           0x0003,
           0x0004,
           0x0005,
           0x0006,
           0x0007,
           0x0008,
           0x0009,
           0x000A,	
          };	
int main()
{
  Timer2_Init();  //初始化定时器Timer2
  Mosbus_Init();   //初始化定时器中断
  Isr_Init();
  while(1)
	{
		Mosbus_Event();  //处理MODbus数据
	}
}

往期博客:

一文看懂Modbus通信协议(上)

通信的硬件层协议和软件层协议

RS-232、RS-485、RS-422通信接口标准介绍

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!