MODBUS移植STM32,STM32做從機

MODBUS學習日誌

一、MODBUS通信協議

1、通信協議

  1. 硬件層協議:解決傳輸問題,相當於路
  2. 串口通信協議 : RS232、RS485、CAN總線

1.1、三種通信方式

1.1.1、單工方式(simplex)

單工通信只支持信號在一個方向上傳輸(正向或反向),任何時候不能改變信號的傳輸方向。爲保證正確傳送數據信號,接收端要對接收的數據進行校驗,若校驗出錯,則通過監控信道發送請求重發的信號。此種方式適用於數據收集系統,如氣象數據的收集、電話費的集中計算等。例如計算機和打印機之間的通信是單工模式,因爲只有計算機向打印機傳輸數據,而沒有相反方向的數據傳輸。還有在某些通信信道中,如單工無線發送等。

1.1.2、半雙工方式(需要上層軟件做協議)(half-duplex)

半雙工通信允許信號在兩個方向上傳輸,但某一時刻只允許信號在一個信道上單向傳輸。因此,半雙工通信實際上是一種可切換方向的單工通信。此種方式適用於問訊、檢索、科學計算等數據通信系統;傳統的對講機使用的就是半雙工通信方式。由於對講機傳送及接收使用相同的頻率,不允許同時進行。因此一方講完後,需設法告知另一方講話結束(例如講完後加上’OVER’),另一方纔知道可以開始講話。

1.1.3、全雙工方式(full-duplex)

全雙工通信允許數據同時在兩個方向上傳輸,即有兩個信道,因此允許同時進行雙向傳輸。全雙工通信是兩個單工通信方式的結合,要求收發雙方都有獨立的接收和發送能力。全雙工通信效率高,控制簡單,但造價高。計算機之間的通信是全雙工方式。一般的電話、手機也是全雙工的系統,因爲在講話時可以聽到對方的聲音。

參考鏈接: https://blog.csdn.net/iningwei/article/details/100134783

1.2、主從模式:

主從模式,是數據庫設計模式中最常見、也是大家日常設計工作中用的最多的一種模式,它描述了兩個表之間的主從關係,是典型的“一對多”關係。
規定要求:

  1. 系統中只有一個設備時主機
  2. 系統中的所有從機不可以主動向主機發數據
  3. 系統中的主機和所有從機上電後都處於監聽狀態
  4. 任何一次的數據交換都要由主機發起
     4.1、將自己轉爲發送狀態
     4.2、主機按照預先約定的格式,發出尋址數據幀
     4.3、恢復自己的接受狀態,等待所尋址的從機響應

1.3、軟件層協議:解決傳輸的目的

1.3.1、主從模式
  1. 整個系統只能有一個主機,每個從機必須有一個唯一的地址(0~247)

  2. 其中0號地址位廣播地址:主機向0號地址的設備發數據包,也就是要把該數據包發給所有的從設備。0號地址的數據包所有從機是不迴應的。

2、MODBUS的主機尋址幀的格式

>MODBUS的兩種傳輸方式:RTU方式和ASC方式
>
>RTU方式:也叫十六進制 例如:發送0x03:0000 0011
>
>RTU方式:也叫十六進制 例如:發送0x03:0000 0011
>
>ASC方式:0x03  {發送0 :0x30:0011 0000 }{ 發送3:0x33:0011 0011}
>
>所以ASC的通信效率低,但是方便調試,使用實驗;工業上都採用RTU方式,效率高

2.3、RTU方式

​ 1、從機地址 2、功能碼(127個) 3、數據1~數據n 4、校驗碼(CRCL、CRCH)

其中: 1~3參與CRC16校驗

從機是以接收數據停止時間達到3.5個字節以上,那麼就認爲主機的尋址幀完成,並開始處理。

例如:波特率:9600bt/s

所以每位數據傳輸的時間T=1000000us/9600=104us

一字節時間位=10T=1004us(起始位 8位 停止位)(串口格式)

所以時間爲:3.5*10T=3645us

2.4、ASC方式

1、: 2、地址 3、功能碼 4、數據1~數據n 5、(地址數據)採用LRC校驗=((地址+功能碼+數據1數據n)%256)+1=(0255) 6、13 10(回車 換行)

2.5、CRC簡述

1.將一個 16 位寄存器裝入十六進制 FFFF (全 1). 將之稱作 CRC 寄存器.
2.將報文的第一個 8 位字節與 16 位 CRC 寄存器的低字節異或,結果置於 CRC 寄存器.
3.將 CRC 寄存器右移 1 位 (向 LSB 方向), MSB 充零. 提取並檢測 LSB.
4.(如果 LSB 爲 0): 重複步驟 3 (另一次移位).
(如果 LSB 爲 1): 對 CRC 寄存器異或多項式值 0xA001 (1010 0000 0000 0001).
5.重複步驟 3 和 4,直到完成 8 次移位。當做完此操作後,將完成對 8 位字節的完整操作。
6.對報文中的下一個字節重複步驟 2 到 5,繼續此操作直至所有報文被處理完畢。
7.CRC 寄存器中的最終內容爲 CRC 值.
8.當放置 CRC 值於報文時,高低字節必須交換。

3、從設備的迴應數據包格式

  1. 迴應數據包和主機查詢的數據包格式包是一致的
  2. 正常回應時,功能碼與主機發的功能碼一致(1~127)
  3. 異常的迴應,功能碼要在收到的功能碼基礎上加上128 例如:發 0x03 收:0x03 +128

4、MODBUS從機協議實現

  1. 硬件上具備串口
  2. 硬件上需要定時器(精確到毫秒級)

二、MODBUS移植STM32流程

1、系統初始化設計流程

開始
配置系統時鐘爲72MHZ
配置基本定時器爲1MS
配置串口爲9600bts,並開啓接受中斷
使能定時器和串口中斷,串口中斷優先級>定時器中斷優先級

1.1、配置系統時鐘

SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9);//設置系統時鐘,外部設置爲72MHZ,內部設置爲64MHZ

1.2、配置基本定時器的步驟

void BASIC_TIM_Config(void)
{
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  BASIC_TIM_APBxClock_FUN(BASIC_TIM_CLK, ENABLE);      //開啓定時器時鐘,即內部時鐘CK_INT=72M
  TIM_TimeBaseStructure.TIM_Period=TIM6_Period;        //自動重裝載寄存器周的值(計數值)
  // 累計TIM_Period 個頻率後產生一個更新或者中斷
  // 時鐘預分頻數爲71,則驅動計數器的時鐘CK_CNT = CK_INT / (71+1)=1M
  TIM_TimeBaseStructure.TIM_Prescaler= TIM6_Prescaler;
 //TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;     // 時鐘分頻因子 ,基本定時器沒有,不用管
 //TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; // 計數器計數模式,基本定時器只能向上計數,沒有計數模式的設置
 //TIM_TimeBaseStructure.TIM_RepetitionCounter=0;            // 重複計數器的值,基本定時器沒有,不用管
 TIM_TimeBaseInit(BASIC_TIM, &TIM_TimeBaseStructure);        // 初始化定時器
 TIM_ClearFlag(BASIC_TIM, TIM_FLAG_Update);                  // 清除計數器中斷標誌位
 TIM_ITConfig(BASIC_TIM,TIM_IT_Update,ENABLE);               // 開啓計數器中斷
 TIM_Cmd(BASIC_TIM, ENABLE);                                 // 使能計數器
 //BASIC_TIM_APBxClock_FUN(BASIC_TIM_CLK, DISABLE);          // 暫時關閉定時器的時鐘,等待使用
 }

基本定時器頭文件

#ifdef  BASIC_TIM6                                           // 使用基本定時器TIM6
#define BASIC_TIM                   TIM6
#define BASIC_TIM_APBxClock_FUN     RCC_APB1PeriphClockCmd
#define BASIC_TIM_CLK               RCC_APB1Periph_TIM6
#define BASIC_TIM_IRQ               TIM6_IRQn
#define BASIC_TIM_IRQHandler        TIM6_IRQHandler
#define TIM6_Period                 (1000)
#define TIM6_Prescaler              (72-1)

定時器中斷函數

void BASIC_TIM_IRQHandler (void)                             //定時器中斷函數
{
	if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET )
		{
	   TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
		}
}

配置定時器中斷使能

void ALL_NVIC_Init(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);             // 設置中斷組爲1
	
	NVIC_InitStructure.NVIC_IRQChannel = BASIC_TIM_IRQ ;         // 設置中斷來源
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;    // 設置主優先級爲 1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;           // 設置搶佔優先級爲3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);C
}

主程序結構

int main(void)
{ 

	SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9);//設置系統時鐘,外部設置爲72MHZ,內部設置爲64MHZ
	BASIC_TIM_Config();  //定時器配置爲1MS
     ALL_NVIC_Init();     //配置中斷優先級
	
}

運行程序,判斷是否到定時器中斷中的斷點完成定時器1MS定時

1.3、配置串口GPIO口

void USART_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	
	DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);	  // 打開串口GPIO 的時鐘	
	DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);	          // 打開串口外設的時鐘	
	// 將USART1 Tx 的GPIO 配置爲推輓複用模式
  GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);		
	// 將USART Rx 的GPIO 配置爲浮空輸入模式
  GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);	
	// 配置串口的工作參數
	USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;	  // 配置波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;	  // 配置 針數據字長
	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(DEBUG_USART, &USART_InitStructure);	             // 完成串口的初始化配置	
	USART_ITConfig(DEBUG_USART, USART_IT_RXNE, ENABLE);	         // 使能串口接收中斷
	USART_Cmd(DEBUG_USART, ENABLE);	                             // 使能串口
}

串口頭文件

// 串口2-USART1
#define DEBUG_USART                            USART2
#define DEBUG_USART_CLK                        RCC_APB1Periph_USART2
#define DEBUG_USART_APBxClkCmd                 RCC_APB1PeriphClockCmd
#define DEBUG_USART_BAUDRATE                   9600
// USART GPIO 引腳宏定義
#define DEBUG_USART_GPIO_CLK                   RCC_APB2Periph_GPIOA
#define DEBUG_USART_GPIO_APBxClkCmd            RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT               GPIOA
#define DEBUG_USART_TX_GPIO_PIN                GPIO_Pin_2
#define DEBUG_USART_RX_GPIO_PORT               GPIOA
#define DEBUG_USART_RX_GPIO_PIN                GPIO_Pin_3
// USART GPIO 中斷
#define DEBUG_USART_IRQ                        USART2_IRQn
#define DEBUG_USART_IRQHandler                 USART2_IRQHandler

串口中斷函數

  void DEBUG_USART_IRQHandler(void)
{
  uint8_t ucTemp;
  if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET)  //判斷是否有數據接收
	{
		  ucTemp = USART_ReceiveData( DEBUG_USART ); //將接收的一個字節保存
		  USART_SendData(DEBUG_USART,ucTemp);        //保存後發送調試助手,		
  }
}

串口中斷使能函數

void ALL_NVIC_Init(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);             // 設置中斷組爲1
    NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ ;      // 設置中斷來源
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;   // 設置主優先級爲 1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;          // 設置搶佔優先級爲0
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);	
}

主函數結構

int main(void)
{ 
	SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9);//設置系統時鐘,外部設置爲72MHZ,內部設置爲64MHZ
    USART_Config();	
    ALL_NVIC_Init();
}

運行程序,判斷是否到串口中斷中的斷點完成串口配置

1.4、配置定時器作用於串口

當串口接受完數據,開啓定時器計數,當時間>8T就開始處理數據

此時需要配置MODBUS的參數,如下

typedef struct
{
 unsigned char   myadd;          //本設備的地址
 unsigned char   rcbuf[100];     //MODBUS接收緩衝區
 unsigned int    timout;         //MODbus的數據斷續時間	
 unsigned char   recount;        //MODbus端口已經收到的數據個數
 unsigned char   timrun;         //MODbus定時器是否計時的標誌
 unsigned char   reflag;         //收到一幀數據的標誌
 unsigned char   Sendbuf[100];   //MODbus發送緩衝區	
}MODBUS;
extern MODBUS modbus;            //聲明全局變量,然後在C文件中調用

串口中斷配置如下

 void DEBUG_USART_IRQHandler(void)
{
  uint8_t ucTemp;
  if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET)  //判斷是否有數據接收
	{
	      ucTemp = USART_ReceiveData( DEBUG_USART ); //將接收的一個字節保存
		  modbus.rcbuf[modbus.recount++]=ucTemp;     //保存到MODBUS的接收緩存區	
		  modbus.timout=0;		  //串口接收數據的過程中,定時器不計時	
		  if(modbus.recount==1)    //收到主機發來的一幀數據的第一字節
			  {
			    modbus.timrun=1;   //啓動定時
			  }
  }
}

定時器中斷配置如下

void BASIC_TIM_IRQHandler (void)     //定時器中斷函數
{
	if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET )
		{
	    if(modbus.timrun!=0)          //串口發送數據是否結束,結束就讓定時器定時
		  {
	     modbus.timout++;             //定時器定時1毫秒,並開始記時
			 if(modbus.timout>=8)    //間隔時間達到了時間,假設爲8T,實際3.5T即可
				{
					modbus.timrun=0;//關閉定時器--停止定時
					modbus.reflag=1;//收到一幀數據,開始處理數據
				}
			}
	   TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
		}
}

運行程序,判斷是否進入到定時器處理modbus.reflag=1;

1.5、配置處理數據包程序

void Mosbus_Event(void)
{
	u16 crc;
	u16 rccrc;
  if(modbus.reflag==0)      //沒有收到MODbus的數據包
	{
	  return ;              //沒有收到處理指令,繼續等待下一條數據
	}
  crc= crc16(&modbus.rcbuf[0], modbus.recount-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)             //廣播地址,不處理
		{
		}
	}                                            //數據包不符合CRC校驗規則
	modbus.recount=0;                            //清除緩存計數
  modbus.reflag=0;	                             //重新開始執行處理函數C
}

處理流程圖

modbus.reflag==0
modbus.reflag==1
不符合
符合
不是
廣播地址
開始
是否接受完數據,並開始處理
計算校驗碼
數據包是否符合CRC校驗規則
確認數據發給本設備地址
數據包是否發給本設備地址
分析功能碼功能
按功能處理數據
結束處理並將計數和使能關掉

1.6、功能碼程序

6號功能碼程序

void Modbud_fun6()                             //6號功能碼處理,寫寄存器
{
  unsigned int Regadd;
	unsigned int val;
	unsigned int 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;                 //發送CRC的值高位
	modbus.Sendbuf[i++]=crc%256;                 //發送CRC的值低位	
	for(j=0;j<i;j++)                             //通過串口逐個發送
   Usart_SendByte( DEBUG_USART,modbus.Sendbuf[j]);
}

3號功能碼程序

void Modbud_fun3(void)                           //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;  
	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);                //CRC校驗
	modbus.Sendbuf[i++]=crc/256;                //發送CRC的值高位
	modbus.Sendbuf[i++]=crc%256;                //發送CRC的值低位	
	for(j=0;j<i;j++)                            //通過串口逐個發送
   Usart_SendByte( DEBUG_USART,modbus.Sendbuf[j]);
}

三、試驗現象

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