專注電子技術(shù)學(xué)習(xí)與研究
當前位置:單片機教程網(wǎng) >> MCU設(shè)計實例 >> 瀏覽文章

FreeModbus移植經(jīng)驗分享

作者:佚名   來源:本站原創(chuàng)   點擊數(shù):  更新時間:2013年06月12日   【字體:

   為什么要移植Freemodbus

         為什么要移植Freemodbus,這個問題需要從兩個方面來回答。第一,modbus是一個非常好的應(yīng)用層協(xié)議,它很簡潔也相對完善。對于還沒有接觸過modbus的朋友來說,我非常不建議直接移植freemodbus,應(yīng)該耐心的從modbus文檔入手,并充分把握身邊的所有資源,例如PLC的中modbus部分。第二,其實嵌入式系統(tǒng)的通信協(xié)議可以自己制定,但是通過實踐發(fā)現(xiàn)自己定制的協(xié)議漏洞百出,尤其是擴展極為困難。我始終認為借鑒他人的經(jīng)驗是很好的途徑。借鑒他人成熟的代碼,可以減少調(diào)試的時間,實現(xiàn)的功能也多了不少。

         個人觀點,僅供參考。

         freemodbus小提示

         freemodbus只能使用從機功能。freemodbus更適合嵌入式系統(tǒng),雖然例子中也有WIN32的例子,如果想要做PC機程序并實現(xiàn)主機功能,推薦使用另一個modbus庫——NMODBUS,使用C#開發(fā)。同樣WINFORM也可以通過自己編寫串口代碼實現(xiàn)modbus功能,但是這會花費很長的時間,可能是一周也可能是一個月,如果使用現(xiàn)成的代碼庫,那么開發(fā)時間可能只有10分鐘。

  freeemodbus中如何通過串口發(fā)送和接收數(shù)據(jù)

         freemodbus通過串口中斷的方式接收和發(fā)送數(shù)據(jù)。采用這種做法我想可以節(jié)省程序等待的時間,并且也短充分使用CPU的資源。串口中斷接收毋庸置疑,在中斷服務(wù)函數(shù)中把數(shù)據(jù)保存在數(shù)組中,以便稍后處理。但是串口發(fā)送中斷使用哪種形式?串口發(fā)送中斷至少有兩種方式,第一種,數(shù)據(jù)寄存器空中斷,只要數(shù)據(jù)寄存器為空并且中斷屏蔽位置位,那么中斷就會發(fā)生;第二種,發(fā)送完成中斷,若數(shù)據(jù)寄存器的數(shù)據(jù)發(fā)送完成并且中斷屏蔽位置位,那么中斷也會發(fā)送。我非常建議各位使用串口發(fā)送完成中斷。freemodbus多使用RS485通信中,從機要么接收要么發(fā)送,多數(shù)情況下從機處于接收狀態(tài),要有數(shù)據(jù)發(fā)送時才進入發(fā)送狀態(tài)。進入發(fā)送狀態(tài)時,數(shù)據(jù)被一個一個字節(jié)發(fā)送出去,當最后一個字節(jié)被發(fā)送出去之后,從機再次進入接收狀態(tài)。如果使用發(fā)送寄存器為空中斷,還需要使用其他的方法才可以判斷最后一個字節(jié)的數(shù)據(jù)是否發(fā)送完成。如果使用數(shù)據(jù)寄存器為空中斷,那么將很有可能丟失最后一個字節(jié)。(馬潮老師的AVR圖書中也推薦使用發(fā)送完成中斷,交流性質(zhì)的文章,就沒有參考文獻了。)
 

  freemodbus中如何判斷幀結(jié)束

         大家應(yīng)該清楚,modbus協(xié)議中沒有明顯的開始符和結(jié)束符,而是通過幀與幀之間的間隔時間來判斷的。如果在指定的時間內(nèi),沒有接收到新的字符數(shù)據(jù),那么就認為收到了新的幀。接下來就可以處理數(shù)據(jù)了,首當其沖的就是判斷幀的合法性。Modbus通過時間來判斷幀是否接受完成,自然需要單片機中的定時器配合。

   整體代碼

下面給出一個STM32平臺上使用FREEMODBUS最簡單的例子,操作保持寄存器,此時操作指令可以為03,0616

  • <FONT size=3>#include "stm32f10x.h"
  • #include <stdio.h>
  • #include "mb.h"
  • #include "mbutils.h"
  • //保持寄存器起始地址
  • #define REG_HOLDING_START 0x0000
  • //保持寄存器數(shù)量
  • #define REG_HOLDING_NREGS 8
  • //保持寄存器內(nèi)容
  • uint16_t usRegHoldingBuf[REG_HOLDING_NREGS]
  • = {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};
  • int main(void)
  • {
  • //初始化 RTU模式 從機地址為1 USART1 9600 無校驗
  • eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
  • //啟動FreeModbus
  • eMBEnable();
  • while (1)
  • {
  • //FreeMODBUS不斷查詢
  • eMBPoll();
  • }
  • }
  • /**
  • * @brief 保持寄存器處理函數(shù),保持寄存器可讀,可讀可寫
  • * @param pucRegBuffer 讀操作時--返回數(shù)據(jù)指針,寫操作時--輸入數(shù)據(jù)指針
  • * usAddress 寄存器起始地址
  • * usNRegs 寄存器長度
  • * eMode 操作方式,讀或者寫
  • * @retval eStatus 寄存器狀態(tài)
  • */
  • eMBErrorCode
  • eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
  • eMBRegisterMode eMode )
  • {
  • //錯誤狀態(tài)
  • eMBErrorCode eStatus = MB_ENOERR;
  • //偏移量
  • int16_t iRegIndex;
  • //判斷寄存器是不是在范圍內(nèi)
  • if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
  • && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
  • {
  • //計算偏移量
  • iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
  • switch ( eMode )
  • {
  • //讀處理函數(shù)
  • case MB_REG_READ:
  • while( usNRegs > 0 )
  • {
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • //寫處理函數(shù)
  • case MB_REG_WRITE:
  • while( usNRegs > 0 )
  • {
  • usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
  • usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • }
  • }
  • else
  • {
  • //返回錯誤狀態(tài)
  • eStatus = MB_ENOREG;
  • }
  • return eStatus;
  • }
  • </FONT>

先給大家一個整體的印象,先讓大家會使用FREEMODBUS,再詳細描述細節(jié)

//保持寄存器起始地址

#define REG_HOLDING_START     0x0000

//保持寄存器數(shù)量

#define REG_HOLDING_NREGS     8

這兩個宏定義,決定了保持寄存器的起始地址和總個數(shù)。需要強調(diào)的是,modbus寄存器的地址有兩套規(guī)則,一套稱為PLC地址,為5位十進制數(shù),例如40001。另一套是協(xié)議地址,PLC地址40001意味著該參數(shù)類型為保持寄存器,協(xié)議地址為0x0000,這里面有對應(yīng)關(guān)系,去掉PLC地址的最高位,然后剩下的減1即可。這會存在一個問題,PLC地址30002PLC地址40002的協(xié)議地址同為0x0001,此時訪問時是不是會沖突呢。親們,當然不會了,30001為輸入寄存器,需要使用04指令訪問,而40001為保持寄存器,可以使用03、0616指令訪問。所以,用好modbus還是要熟悉協(xié)議本生,切不可著急。

//保持寄存器內(nèi)容

uint16_t usRegHoldingBuf[REG_HOLDING_NREGS]

= {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};

接下來定義了保持寄存器的內(nèi)容,在這里請大家注意了,保持寄存器為無符號16位數(shù)據(jù)。在測試的情況下,我隨便找了一些數(shù)據(jù)進行測試?磾(shù)據(jù)的本質(zhì)似乎看不出說明規(guī)律,但是usRegHoldingBuf卻是以16進制保存了浮點數(shù)。

  • int main(void)
  • {
  • //初始化 RTU模式 從機地址為1 USART1 9600 無校驗
  • eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
  • //啟動FreeModbus
  • eMBEnable();
  • while (1)
  • {
  • //FreeMODBUS不斷查詢
  • eMBPoll();
  • }
  • }



接下來就進入主函數(shù)部分。有三個FREEMODBUS提供的函數(shù),eMBIniteMBEnableeMBPolleMBInitmodbus的初始化函數(shù),eMBEnablemodbus的使能函數(shù),而eMBPollmodbus的查詢函數(shù),eMBPoll也是非常單純的函數(shù),查詢是否有數(shù)據(jù)幀到達,如果有數(shù)據(jù)到達,便進行相依的處理。再次觀察這幾個函數(shù),只有eMBInit有很多的參數(shù),這些參數(shù)和位于系統(tǒng)底層的硬件有關(guān),這個應(yīng)該引起移植過程的更多關(guān)注。下面幾個章節(jié)再議。

  • <FONT size=3>eMBErrorCode
  • eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
  • eMBRegisterMode eMode )
  • {
  • //錯誤狀態(tài)
  • eMBErrorCode eStatus = MB_ENOERR;
  • //偏移量
  • int16_t iRegIndex;
  • //判斷寄存器是不是在范圍內(nèi)
  • if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
  • && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
  • {
  • //計算偏移量
  • iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
  • switch ( eMode )
  • {
  • //讀處理函數(shù)
  • case MB_REG_READ:
  • while( usNRegs > 0 )
  • {
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );
  • *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • //寫處理函數(shù)
  • case MB_REG_WRITE:
  • while( usNRegs > 0 )
  • {
  • usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
  • usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
  • iRegIndex++;
  • usNRegs--;
  • }
  • break;
  • }
  • }
  • else
  • {
  • //返回錯誤狀態(tài)
  • eStatus = MB_ENOREG;
  • }
  • return eStatus;
  • }
  • </FONT>
     

最后,如果收到一個有效的數(shù)據(jù)幀,那么就可以開始處理了。

第一步,判斷寄存器的地址是否在合法的范圍內(nèi)。

  if( ( (int16_t)usAddress >= REG_HOLDING_START ) \

     && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )

第二步,判斷需要操作寄存器的偏移地址。

         給個例子可以迅速的說明問題,例如訪問寄存器的起始地址為0x0002,保持寄存器的起始地址為0x0000,那么這個訪問的偏移量為2,程序就從保持寄存器數(shù)組的第2個(從0開始)開始操作。

第三步,讀寫操作分開處理

      case MB_REG_READ:

        while( usNRegs > 0 )

        {

          *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );

          *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );

          iRegIndex++;

          usNRegs--;

        }

        break;

         以讀操作為例,代碼不多說了,請大家注意操作的順序。保持寄存器以16位形式保存,但是modbus通信時以字節(jié)為單位,高位字節(jié)數(shù)據(jù)在前,低位數(shù)據(jù)字節(jié)在后。

   串口相關(guān)部分代碼編寫

         串口部分的代碼編寫比較常規(guī),主要有三個函數(shù),串口初始化,串口數(shù)據(jù)發(fā)送和串口數(shù)據(jù)接收。除了以上三個函數(shù)之外,還有串口中斷服務(wù)函數(shù)。

  • /**
  • * @brief 串口初始化
  • * @param ucPORT 串口號
  • * ulBaudRate 波特率
  • * ucDataBits 數(shù)據(jù)位
  • * eParity 校驗位
  • * @retval None
  • */
  • BOOL
  • xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
  • {
  • (void)ucPORT; //不修改串口
  • (void)ucDataBits; //不修改數(shù)據(jù)位長度
  • (void)eParity; //不修改校驗格式
  • GPIO_InitTypeDef GPIO_InitStructure;
  • USART_InitTypeDef USART_InitStructure;
  • //使能USART1,GPIOA
  • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
  • RCC_APB2Periph_USART1, ENABLE);
  • //GPIOA9 USART1_Tx
  • GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  • GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  • GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //推挽輸出
  • GPIO_Init(GPIOA, &GPIO_InitStructure);
  • //GPIOA.10 USART1_Rx
  • GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  • GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  • GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮動輸入
  • GPIO_Init(GPIOA, &GPIO_InitStructure);
  • USART_InitStructure.USART_BaudRate = ulBaudRate; //只修改波特率
  • 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(USART1, &USART_InitStructure);
  • //使能USART1
  • USART_Cmd(USART1, ENABLE);
  • NVIC_InitTypeDef NVIC_InitStructure;
  • NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
  • //設(shè)定USART1 中斷優(yōu)先級
  • NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  • NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  • NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  • NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  • NVIC_Init(&NVIC_InitStructure);
  • //最后配置485發(fā)送和接收模式
  • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
  • //GPIOD.8
  • GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
  • GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  • GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  • GPIO_Init(GPIOD, &GPIO_InitStructure);
  • return TRUE;
  • }

  •  


 

傳入的參數(shù)有端口號,波特率,數(shù)據(jù)位和校驗位,可以根據(jù)實際的情況修改代碼。在這里我并沒有修改其他參數(shù),至于傳入的波特率是有效的。除了配置串口的相關(guān)參數(shù)之外,還需要配置串口的中斷優(yōu)先級。最后,由于使用485模式,還需要一個發(fā)送接收控制端,該IO配置為推挽輸出模式。

  • <FONT size=3>/**
  • * @brief 控制接收和發(fā)送狀態(tài)
  • * @param xRxEnable 接收使能、
  • * xTxEnable 發(fā)送使能
  • * @retval None
  • */
  • void
  • vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
  • {
  • if(xRxEnable)
  • {
  • //使能接收和接收中斷
  • USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  • //MAX485操作 低電平為接收模式
  • GPIO_ResetBits(GPIOD,GPIO_Pin_8);
  • }
  • else
  • {
  • USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
  • //MAX485操作 高電平為發(fā)送模式
  • GPIO_SetBits(GPIOD,GPIO_Pin_8);
  • }
  • if(xTxEnable)
  • {
  • //使能發(fā)送完成中斷
  • USART_ITConfig(USART1, USART_IT_TC, ENABLE);
  • }
  • else
  • {
  • //禁止發(fā)送完成中斷
  • USART_ITConfig(USART1, USART_IT_TC, DISABLE);
  • }
  • }
  • </FONT>
     

 

由于485使用半雙工模式,從機一般處于接收狀態(tài),有數(shù)據(jù)發(fā)送時才會進入發(fā)送模式。在FreeModbus中有專門的控制接收和發(fā)送狀態(tài)的函數(shù),在這里不但可以打開或關(guān)閉接收和發(fā)送中斷,還可以控制485收發(fā)芯片的發(fā)送接收端口。代碼非常簡單,但是還是建議各位使用發(fā)送完成中斷。

  • <FONT size=3>BOOL
  • xMBPortSerialPutByte( CHAR ucByte )
  • {
  • //發(fā)送數(shù)據(jù)
  • USART_SendData(USART1, ucByte);
  • return TRUE;
  • }
  • BOOL
  • xMBPortSerialGetByte( CHAR * pucByte )
  • {
  • //接收數(shù)據(jù)
  • *pucByte = USART_ReceiveData(USART1);
  • return TRUE;
  • }
  • xMBPortSerialPutByte和xMBPortSerialGetByte兩個函數(shù)用于串口發(fā)送和接收數(shù)據(jù),在這里只要調(diào)用STM32的庫函數(shù)即可。
  • static void prvvUARTTxReadyISR( void )
  • {
  • //mb.c eMBInit函數(shù)中
  • //pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
  • //發(fā)送狀態(tài)機
  • pxMBFrameCBTransmitterEmpty();
  • }
  • static void prvvUARTRxISR( void )
  • {
  • //mb.c eMBInit函數(shù)中
  • //pxMBFrameCBByteReceived = xMBRTUReceiveFSM
  • //接收狀態(tài)機
  • pxMBFrameCBByteReceived();
  • }
  • void USART1_IRQHandler(void)
  • {
  • //發(fā)生接收中斷
  • if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
  • {
  • prvvUARTRxISR();
  • //清除中斷標志位
  • USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  • }
  • //發(fā)生完成中斷
  • if(USART_GetITStatus(USART1, USART_IT_TC) == SET)
  • {
  • prvvUARTTxReadyISR();
  • //清除中斷標志
  • USART_ClearITPendingBit(USART1, USART_IT_TC);
  • }
  • }
  • </FONT>

     

若進入串口中斷服務(wù)函數(shù),則要調(diào)用FreeModbus中響應(yīng)的函數(shù),串口接收中斷服務(wù)函數(shù)對應(yīng)prvvUARTRxISR(),其代碼如下

  • <FONT size=3>static void prvvUARTRxISR( void )
  • {
  • //mb.c eMBInit函數(shù)中
  • //pxMBFrameCBByteReceived = xMBRTUReceiveFSM
  • //接收狀態(tài)機
  • pxMBFrameCBByteReceived();
  • }
  • </FONT>

     

prvvUARTRxISR中又調(diào)用了pxMBFrameCBByteReceived(),其實pxMBFrameCBTransmitterEmpty()并不是一個函數(shù),而是一個函數(shù)指針。其定義如下,請注意函數(shù)指針的聲明和函數(shù)聲明的區(qū)別。

BOOL( *pxMBFrameCBTransmitterEmpty ) ( void );

mb.c文件的eMBInit函數(shù)完成賦值。一般情況下都會選擇RTU模式,那么pxMBFrameCBByteReceived就和xMBRTUReceiveFSM等價了,

pxMBFrameCBByteReceived = xMBRTUReceiveFSM;


 

同理,若發(fā)生串口發(fā)送完成中斷,該中斷服務(wù)函數(shù)對應(yīng)prvvUARTTxReadyISR,其代碼如下

  • <FONT size=3>static void prvvUARTTxReadyISR( void )
  • {
  • //mb.c eMBInit函數(shù)中
  • //pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
  • //發(fā)送狀態(tài)機
  • pxMBFrameCBTransmitterEmpty();
  • }
  • </FONT>

     

prvvUARTTxReadyISR中又調(diào)用了pxMBFrameCBTransmitterEmpty(),pxMBFrameCBTransmitterEmpty也是函數(shù)指針,在eMBInit函數(shù)完成賦值,它等價于xMBRTUTransmitFSM

         特別提醒,由于我使用的是串口發(fā)送完成中斷,想要進入該中斷服務(wù)函數(shù),需要發(fā)送一個字節(jié)的數(shù)據(jù)并啟動串口發(fā)送中斷,代碼還需要少許修改。在mbRTU.ceMBRTUSend中稍作修改,代碼如下。

  • <P style="MARGIN: 0cm 0cm 0pt" class=MsoNormal> </P>
     


復(fù)制代碼
 

  • /* First byte before the Modbus-PDU is the slave address. */
  • pucSndBufferCur = ( UCHAR * ) pucFrame - 1;
  • usSndBufferCount = 1;
  • /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */
  • pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
  • usSndBufferCount += usLength;
  • /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */
  • usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
  • ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 & 0xFF );
  • ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 );
  • /* Activate the transmitter. */
  • //發(fā)送狀態(tài)轉(zhuǎn)換,在中斷中不斷發(fā)送
  • eSndState = STATE_TX_XMIT;
  • //插入代碼 啟動第一次發(fā)送,這樣才可以進入發(fā)送完成中斷
  • xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
  • pucSndBufferCur++;
  • usSndBufferCount--;
  • //使能發(fā)送狀態(tài),禁止接收狀態(tài)
  • vMBPortSerialEnable( FALSE, TRUE );


  •  

寫到這里給位可能看的不是很明白,建議研究一下FreeModbus的源碼,稍作一些修改使用起來才會更加方便。

關(guān)閉窗口

相關(guān)文章