一文看懂Modbus通信協議(下)

目錄

前言

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通信接口標準介紹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章