1. 程式人生 > 其它 >通訊介面應用筆記3:使用W5500實現Modbus TCP伺服器

通訊介面應用筆記3:使用W5500實現Modbus TCP伺服器

  前面我們設計實現了W5500的驅動程式,也講解了驅動的使用方式。在最近一次的專案應用中,正好有一個使用W5500實現TCP通訊的需求,所以我們就使用該驅動程式輕鬆實現。這一篇中我們就來說一說基於我們W5500通訊驅動程式實現TCP通訊的過程。

1、應用需求

  在本次應用中,要求實現一個基於W5500的Modbus TCP伺服器。這個需求的描述雖然只有一句話,但是這個需求的內容可不簡單。我們首先來分析一下這個需求的具體內容。

  為了實現基於W5500的Modbus TCP伺服器,我們必先須基於W5500實現一個TCP伺服器。W5500本身是帶硬體協議棧的,但卻並不帶TCP伺服器。不過在我們前面的關於外設驅動庫的系列文章中已經封裝了W5500的驅動,其中就帶有一個TCP伺服器,我們可以直接採用就可以了。

  其次我們要在TCP伺服器的基礎上實現Modbus TCP協議。關於Modbus協議棧,我們以前的文章就講述過Modbus通訊協議棧的開發問題。而且我們已經將我們開發的Modbus通訊協議棧開源。其中已經封裝了Modbus TCP伺服器物件,所以我們直接採用這一Modbus通訊協議棧就可以了。

  有了驅動和協議棧,我們還需要考慮應用層面的具體問題,而且也只需要考慮應用層面的具體問題。這裡就看出我們前面封裝外設驅動和Modbus通訊協議棧的價值所在了。關於應用層面的問題我們主要需要重點考慮幾個問題:

  第一,資料的儲存型別及地址範圍。我們知道Modbus協議常見的資料型別有4種。我們需要考慮在系統中需要使用到的型別及地址,這將決定Modbus協議資料處理回撥函式的實現。

  第二,網路配置問題,我們需要通過網路訪問這臺下位機就需要要為其配置網路。這存在靜態配置,動態配置和系統自動分配的問題。作為伺服器,我們一般不會希望讓系統自動分配。所以我們需要考慮的是如何方便使用者為其分配地址的問題。

  第三,併發訪問的問題。掛載在網路上的伺服器肯定面臨多個客戶端來訪問的問題。W5500可以實現8個Socket,而Modbus TCP通用的預設埠號是502,當然也可以使用其它埠,只要不衝突就好。所以我們可以考慮使用不同的Socket和不同的埠號來實現併發訪問。

2、功能設計

  我們分析了基於W5500實現Modbus TCP伺服器的需求。我們現在從硬體和軟體兩個方面來分析器功能的實現。

2.1、硬體功能設計

  我們知道W5500帶有硬體協議棧,整合有乙太網控制器和物理層,所以對外我們只需要實現乙太網變壓器和硬體介面就好了。但與控制器部分的連線則採用SPI介面,除此之外還需要提供中斷輸入和模式設定的相關介面。在這裡我們設計器硬體連線如下:

  在上圖中,我們將中斷輸入引入到MCU的GPIO埠,而模式設定PMODE0、PMODE1、PMODE2均通過電阻上拉到電源。對於W5500來說PMODE0、PMODE1、PMODE2均為高電平表示開啟全部功能,所以我們直接拉高而不是引入到MCU引腳來控制。

2.2、軟體功能設計

  從需求來說,軟體的功能非常簡單,就是實現一個Modbus TCP伺服器。但實際上,如我們前面所描述的那樣,軟體需要考慮的問題還是比較多的。從功能實現上主要有3個方面需要考慮:

  第一,實現TCP伺服器,這個伺服器用於在系統中輪詢處理,從W5500獲取資料和傳送資料給W5500都需要通過這部分來實現。

  第二,TCP伺服器得到資料後,我們需要解析資料,並根據解析的上位資料決定進一步的動作,還需要生成返回資訊。這部分對應功能就是Modbus TCP伺服器的實現。

  第三,根據Modbus TCP伺服器解析出的Modbus訊息,需要決定下一步的動作,這個具體動作根據功能碼的不同可能有不同需求,所以我們需要根據具體的要求實現不同功能碼的動作。

  根據上述的設計,我們可以簡單的將需要實現的軟體功能圖示如下:

  上圖中,因為W5500的TCP伺服器以及Modbus TCP協議棧的相關函式我們都做了封裝,所以它們之間的呼叫都將以回撥函式的方式實現。除了上述的軟體實現外,還需要注意必要的初始化配置。

3、應用實現

  根據我們前面的設計,接下來我們考慮一下這一需求的具體實現過程。我們將這一過程分為4個部分來分別描述。

3.1、系統的初始化

  在實現具體的功能之前,我們需要對硬體以及軟體環境做必要的初始化配置。具體到這裡就是對W5500作必要的軟硬體配置,包括介面、網路以及回撥函式等。具體例項程式碼如下:

/* 乙太網通訊配置 */
void McEthernetConfiguration(void)
{
  uint8_t mac[6]={0x01, 0x08, 0xdc,0x00, 0xab, 0xcd}; //本地Mac地址
  uint8_t ip[4]={192, 168, 1, 190};          //本地IP地址
  uint8_t sn[4]={255,255,255,0};           //子網掩碼
  uint8_t gw[4]={192, 168, 1, 1};           //閘道器地址
  uint8_t dns[4]={0,0,0,0};              //DNS伺服器地址
  
  /* 乙太網使用GPIO初始化 */
  GPIO_Init_Configuration();
  
  /* SPI1埠初始化 */
  SPI1_Init_Configuration();
  
  /*W5500物件初始化函式*/
  W5500Initialization(&w5500,      //W5500物件
                      mac,        //本地Mac地址
                      ip,        //本地IP地址
                      sn,        //子網掩碼
                      gw,        //閘道器地址
                      dns,        //DNS伺服器地址
                      NETINFO_STATIC,  //DHCP型別
                      EnterCritical,   //進入臨界區
                      ExitCritical,   //退出臨界區
                      EnableChipSelect, //片選使能
                      DisableChipSelect, //片選失能
                      ReadByteBySPI,   //SPI讀位元組
                      WriteByteBySPI,  //SPI寫位元組
                      W5500DataParsing, //報文解析函式
                      NULL        //資料請求函式
                      );
}

  在這個例項中,我們對網路部分採用的是靜態配置,就是說網路引數是固定不變的,而且我們的測試環境只限於區域網內。

3.2、資料處理函式

  資料處理函式是最靈活的,因為每個專案及每個人對資料處理的要求都是不一樣的,只要能符合應用要求就沒問題。需要說一下的是,這部分是Modbus協議棧對處理資料的要求,想要詳細瞭解的話,可以看我們以前關於Modbus協議站的文章。對於這個例項,資料處理函式如下:

/*獲取想要讀取的Coil量的值*/
void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList)
{
 uint16_t start;
 uint16_t count;

 /*先判斷地址是否處於合法範圍*/
 start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;
 count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);
 
 for(int i=0;i<count;i++)
 {
  statusList[i]=dPara.coil[start+i];
 }
}
 
/*獲取想要讀取的保持暫存器的值*/
void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{
 uint16_t start;
 uint16_t count;

 /*先判斷地址是否處於合法範圍*/
 start=(startAddress>HoldingRegisterStartAddress)?((startAddress<=HoldingRegisterEndAddress)?startAddress:HoldingRegisterEndAddress):HoldingRegisterStartAddress;
 count=((start+quantity-1)<=HoldingRegisterEndAddress)?quantity:(HoldingRegisterEndAddress-start);
 
 for(int i=0;i<count;i++)
 {
  registerValue[i]=aPara.holdingRegister[start+i];
 }
}
 
/*設定單個線圈的值*/
void SetSingleCoil(uint16_t coilAddress,bool coilValue)
{
 /*先判斷地址是否處於合法範圍*/
 if(coilAddress<=12)
 {
  dPara.coil[coilAddress]=coilValue;
 }
}
 
/*設定多個線圈的值*/
void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue)
{
 uint16_t endAddress=startAddress+quantity-1;
 if((startAddress<=12)&&(endAddress<=12))
 {
  for(int i=0;i<quantity;i++)
  {
   dPara.coil[i+startAddress]=statusValue[i];
  }
 }
}
 
/*設定單個暫存器的值*/
void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue)
{
 bool noError=(bool)(((50<=registerAddress)&&(registerAddress<=59))
                   ||((73<=registerAddress)&&(registerAddress<=74))
                   ||((90<=registerAddress)&&(registerAddress<=91)));

 if(noError)
 {
  aPara.holdingRegister[registerAddress]=registerValue;
 }
 
}
 
/*設定多個暫存器的值*/
void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{
 uint16_t endAddress=startAddress+quantity-1;
 
 bool noError=(bool)(((18<=startAddress)&&(startAddress<=28)&&(18<=endAddress)&&(endAddress<=28))
                   ||((50<=startAddress)&&(startAddress<=59)&&(50<=endAddress)&&(endAddress<=59))
                   ||((73<=startAddress)&&(startAddress<=74)&&(73<=endAddress)&&(endAddress<=74))
                   ||((90<=startAddress)&&(startAddress<=91)&&(90<=endAddress)&&(endAddress<=91)));
 if(noError)
 {
  for(int i=0;i<quantity;i++)
  {
   aPara.holdingRegister[startAddress+i]=registerValue[i];
  }
 }
 
}

3.2、資料解析函式

  大家可能在前面的初始化函式中發現有一個名為W5500DataParsing的資料解析函式。這個函式是W5500驅動中,TCP伺服器的要求,實現對資料的解析。因為具體的應用層協議解析多不勝數,所以設計成了回撥函式,其函式原型如下:

/*解析接收到的資料*/
typedef uint16_t (*W5500DataParsingType)(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer);

  對於我們來說,我們需要根據具體的應用層協議來實現這一函式。不過我們採用的Modbus TCP協議,在我們的Modbus協議棧中已經實現瞭解析函式,所以我們呼叫如下:

/*報文解析函式*/
static uint16_t W5500DataParsing(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer)
{
  /*解析接收到的資訊,返回響應命令的長度*/
  return ParsingClientAccessCommand(rxBuffer,txBuffer);
}

3.3、TCP伺服器

  我們在前面已經說過了,需要對伺服器進行輪詢。所以我們需要在一個程序中輪詢訪問W5500的TCP伺服器。同樣我們也要考慮多客戶端同時訪問的問題,我們將輪詢函式實現如下:

/* 乙太網通訊處理 */
void McEthernetProcess(void)
{
  /*TCP伺服器資料通訊*/
  W5500TCPServer(&w5500,Socket0,502);
  
  W5500TCPServer(&w5500,Socket1,503);
  
  W5500TCPServer(&w5500,Socket2,504);
  
  W5500TCPServer(&w5500,Socket3,505);
  
  W5500TCPServer(&w5500,Socket4,506);
  
  W5500TCPServer(&w5500,Socket5,507);
  
  W5500TCPServer(&w5500,Socket6,508);
  
  W5500TCPServer(&w5500,Socket7,509);
}

  事實上使用同一個Socket和不同的埠也是可以實現多客戶端訪問的,但既然有8個Socket,用起來自然更好一點。

4、應用驗證

  我們已經根據需求實現了一個Modbus TCP伺服器,究竟效果如何呢?我們還需要測試一下,以確認設計的正確性。

4.1、通訊測試

  我們將目標板連線到區域網中,使用著名的Modbus Poll軟體來測試一下我們設計的程式是否符合要求。

  我們首先在一臺機器上連線埠為504的Modbus TCP伺服器,連線正常且資料獲取也完全正確。具體如下圖所示:


  同時,我們採用區域網內的另一臺機器連線埠為502的Modbus TCP伺服器,連線正常且資料獲取也完全正確。具體如下圖所示:

  經過上述測試,我們可以確定我們實現的Modbus TCP伺服器是可行的,而且在多客戶端並行訪問下也可以正確工作。

4.2、小結

  這一篇中,我們實現了可以支援多客戶端訪問的Modbus TCP伺服器,經測試執行也符合設計預期。這裡我們將需要考慮的幾個問題總結如下:

  關於初始化配置的問題,在這個例子中,我們對網路的配置是直接在軟體上固定死的,這樣做雖然簡單直接但並不是一個好的選擇。更好的辦法是可以讓使用者自己配置,方法有多種,可以根據自己的實際情況,在軟體上進一步的考慮。

  關於資料處理的問題,具體的資料處理與實際的應用需求有關,也與應用層協議的要求有關,這個例子中實現的Modbus的資料處理函式並不是唯一的,但可參考其思路。

  關於資料解析的問題,在本例中實現的是Modbus TCP伺服器的解析函式。對於不同的應用協議需要編寫不同的解析函式,這部分是靈活性最大的,支援所有可運行於TCP應用層的通訊協議。

  關於多客戶端訪問的問題,W5500可以實現8個Socket,而Modbus TCP預設埠號是502,當然也可以使用其它埠。所以我們可以考慮使用不同的Socket和不同的埠號來實現併發訪問。事實上,經過我們測試使用同一個Socket和不同的埠也是可以實現多客戶端訪問的,有興趣的同仁可以試試。

歡迎關注:

如果閱讀這篇文章讓您略有所得,還請點選下方的【好文要頂】按鈕。

當然,如果您想及時瞭解我的部落格更新,不妨點選下方的【關注我】按鈕。

如果您希望更方便且及時的閱讀相關文章,也可以掃描上方二維碼關注我的微信公眾號【木南創智