一 為什么要移植Freemodbus
為什么要移植Freemodbus,這個問題需要從兩個方面來回答。第一,modbus是一個非常好的應用層協議,它很簡潔也相對完善。對于還沒有接觸過modbus的朋友來說,我非常不建議直接移植freemodbus,應該耐心的從modbus文檔入手,并充分把握身邊的所有資源,例如PLC的中modbus部分。第二,其實嵌入式系統的通信協議可以自己制定,但是通過實踐發現自己定制的協議漏洞百出,尤其是擴展極為困難。我始終認為借鑒他人的經驗是很好的途徑。借鑒他人成熟的代碼,可以減少調試的時間,實現的功能也多了不少。
個人觀點,僅供參考。
freemodbus小提示
freemodbus只能使用從機功能。freemodbus更適合嵌入式系統,雖然例子中也有WIN32的例子,如果想要做PC機程序并實現主機功能,推薦使用另一個modbus庫——NMODBUS,使用C#開發。同樣WINFORM也可以通過自己編寫串口代碼實現modbus功能,但是這會花費很長的時間,可能是一周也可能是一個月,如果使用現成的代碼庫,那么開發時間可能只有10分鐘。
二 freeemodbus中如何通過串口發送和接收數據
freemodbus通過串口中斷的方式接收和發送數據。采用這種做法我想可以節省程序等待的時間,并且也短充分使用CPU的資源。串口中斷接收毋庸置疑,在中斷服務函數中把數據保存在數組中,以便稍后處理。但是串口發送中斷使用哪種形式?串口發送中斷至少有兩種方式,第一種,數據寄存器空中斷,只要數據寄存器為空并且中斷屏蔽位置位,那么中斷就會發生;第二種,發送完成中斷,若數據寄存器的數據發送完成并且中斷屏蔽位置位,那么中斷也會發送。我非常建議各位使用串口發送完成中斷。freemodbus多使用RS485通信中,從機要么接收要么發送,多數情況下從機處于接收狀態,要有數據發送時才進入發送狀態。進入發送狀態時,數據被一個一個字節發送出去,當最后一個字節被發送出去之后,從機再次進入接收狀態。如果使用發送寄存器為空中斷,還需要使用其他的方法才可以判斷最后一個字節的數據是否發送完成。如果使用數據寄存器為空中斷,那么將很有可能丟失最后一個字節。(馬潮老師的AVR圖書中也推薦使用發送完成中斷,交流性質的文章,就沒有參考文獻了。)
二 freemodbus中如何判斷幀結束
大家應該清楚,modbus協議中沒有明顯的開始符和結束符,而是通過幀與幀之間的間隔時間來判斷的。如果在指定的時間內,沒有接收到新的字符數據,那么就認為收到了新的幀。接下來就可以處理數據了,首當其沖的就是判斷幀的合法性。Modbus通過時間來判斷幀是否接受完成,自然需要單片機中的定時器配合。
三 整體代碼
下面給出一個STM32平臺上使用FREEMODBUS最簡單的例子,操作保持寄存器,此時操作指令可以為03,06和16;
- <FONT size=3>#include "stm32f10x.h"
- #include <stdio.h>
- #include "mb.h"
- #include "mbutils.h"
- //保持寄存器起始地址
- #define REG_HOLDING_START 0x0000
- //保持寄存器數量
- #define REG_HOLDING_NREGS 8
- //保持寄存器內容
- 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 保持寄存器處理函數,保持寄存器可讀,可讀可寫
- * @param pucRegBuffer 讀操作時--返回數據指針,寫操作時--輸入數據指針
- * usAddress 寄存器起始地址
- * usNRegs 寄存器長度
- * eMode 操作方式,讀或者寫
- * @retval eStatus 寄存器狀態
- */
- eMBErrorCode
- eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
- eMBRegisterMode eMode )
- {
- //錯誤狀態
- eMBErrorCode eStatus = MB_ENOERR;
- //偏移量
- int16_t iRegIndex;
- //判斷寄存器是不是在范圍內
- if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
- && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
- {
- //計算偏移量
- iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
- switch ( eMode )
- {
- //讀處理函數
- case MB_REG_READ:
- while( usNRegs > 0 )
- {
- *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );
- *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );
- iRegIndex++;
- usNRegs--;
- }
- break;
- //寫處理函數
- case MB_REG_WRITE:
- while( usNRegs > 0 )
- {
- usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
- usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
- iRegIndex++;
- usNRegs--;
- }
- break;
- }
- }
- else
- {
- //返回錯誤狀態
- eStatus = MB_ENOREG;
- }
- return eStatus;
- }
- </FONT>
先給大家一個整體的印象,先讓大家會使用FREEMODBUS,再詳細描述細節
//保持寄存器起始地址
#define REG_HOLDING_START 0x0000
//保持寄存器數量
#define REG_HOLDING_NREGS 8
這兩個宏定義,決定了保持寄存器的起始地址和總個數。需要強調的是,modbus寄存器的地址有兩套規則,一套稱為PLC地址,為5位十進制數,例如40001。另一套是協議地址,PLC地址40001意味著該參數類型為保持寄存器,協議地址為0x0000,這里面有對應關系,去掉PLC地址的最高位,然后剩下的減1即可。這會存在一個問題,PLC地址30002和PLC地址40002的協議地址同為0x0001,此時訪問時是不是會沖突呢。親們,當然不會了,30001為輸入寄存器,需要使用04指令訪問,而40001為保持寄存器,可以使用03、06和16指令訪問。所以,用好modbus還是要熟悉協議本生,切不可著急。
//保持寄存器內容
uint16_t usRegHoldingBuf[REG_HOLDING_NREGS]
= {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};
接下來定義了保持寄存器的內容,在這里請大家注意了,保持寄存器為無符號16位數據。在測試的情況下,我隨便找了一些數據進行測試。看數據的本質似乎看不出說明規律,但是usRegHoldingBuf卻是以16進制保存了浮點數。
- int main(void)
- {
- //初始化 RTU模式 從機地址為1 USART1 9600 無校驗
- eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
- //啟動FreeModbus
- eMBEnable();
- while (1)
- {
- //FreeMODBUS不斷查詢
- eMBPoll();
- }
- }
接下來就進入主函數部分。有三個FREEMODBUS提供的函數,eMBInit,eMBEnable和eMBPoll。eMBInit為modbus的初始化函數,eMBEnable為modbus的使能函數,而eMBPoll為modbus的查詢函數,eMBPoll也是非常單純的函數,查詢是否有數據幀到達,如果有數據到達,便進行相依的處理。再次觀察這幾個函數,只有eMBInit有很多的參數,這些參數和位于系統底層的硬件有關,這個應該引起移植過程的更多關注。下面幾個章節再議。
- <FONT size=3>eMBErrorCode
- eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
- eMBRegisterMode eMode )
- {
- //錯誤狀態
- eMBErrorCode eStatus = MB_ENOERR;
- //偏移量
- int16_t iRegIndex;
- //判斷寄存器是不是在范圍內
- if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
- && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
- {
- //計算偏移量
- iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
- switch ( eMode )
- {
- //讀處理函數
- case MB_REG_READ:
- while( usNRegs > 0 )
- {
- *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] >> 8 );
- *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf[iRegIndex] & 0xFF );
- iRegIndex++;
- usNRegs--;
- }
- break;
- //寫處理函數
- case MB_REG_WRITE:
- while( usNRegs > 0 )
- {
- usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
- usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
- iRegIndex++;
- usNRegs--;
- }
- break;
- }
- }
- else
- {
- //返回錯誤狀態
- eStatus = MB_ENOREG;
- }
- return eStatus;
- }
- </FONT>
最后,如果收到一個有效的數據幀,那么就可以開始處理了。
第一步,判斷寄存器的地址是否在合法的范圍內。
if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
&& ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
第二步,判斷需要操作寄存器的偏移地址。
給個例子可以迅速的說明問題,例如訪問寄存器的起始地址為0x0002,保持寄存器的起始地址為0x0000,那么這個訪問的偏移量為2,程序就從保持寄存器數組的第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通信時以字節為單位,高位字節數據在前,低位數據字節在后。
四 串口相關部分代碼編寫
串口部分的代碼編寫比較常規,主要有三個函數,串口初始化,串口數據發送和串口數據接收。除了以上三個函數之外,還有串口中斷服務函數。
- /**
- * @brief 串口初始化
- * @param ucPORT 串口號
- * ulBaudRate 波特率
- * ucDataBits 數據位
- * eParity 校驗位
- * @retval None
- */
- BOOL
- xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
- {
- (void)ucPORT; //不修改串口
- (void)ucDataBits; //不修改數據位長度
- (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);
- //設定USART1 中斷優先級
- 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發送和接收模式
- 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;
- }
傳入的參數有端口號,波特率,數據位和校驗位,可以根據實際的情況修改代碼。在這里我并沒有修改其他參數,至于傳入的波特率是有效的。除了配置串口的相關參數之外,還需要配置串口的中斷優先級。最后,由于使用485模式,還需要一個發送接收控制端,該IO配置為推挽輸出模式。
- <FONT size=3>/**
- * @brief 控制接收和發送狀態
- * @param xRxEnable 接收使能、
- * xTxEnable 發送使能
- * @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操作 高電平為發送模式
- GPIO_SetBits(GPIOD,GPIO_Pin_8);
- }
- if(xTxEnable)
- {
- //使能發送完成中斷
- USART_ITConfig(USART1, USART_IT_TC, ENABLE);
- }
- else
- {
- //禁止發送完成中斷
- USART_ITConfig(USART1, USART_IT_TC, DISABLE);
- }
- }
- </FONT>
由于485使用半雙工模式,從機一般處于接收狀態,有數據發送時才會進入發送模式。在FreeModbus中有專門的控制接收和發送狀態的函數,在這里不但可以打開或關閉接收和發送中斷,還可以控制485收發芯片的發送接收端口。代碼非常簡單,但是還是建議各位使用發送完成中斷。
- <FONT size=3>BOOL
- xMBPortSerialPutByte( CHAR ucByte )
- {
- //發送數據
- USART_SendData(USART1, ucByte);
- return TRUE;
- }
- BOOL
- xMBPortSerialGetByte( CHAR * pucByte )
- {
- //接收數據
- *pucByte = USART_ReceiveData(USART1);
- return TRUE;
- }
- xMBPortSerialPutByte和xMBPortSerialGetByte兩個函數用于串口發送和接收數據,在這里只要調用STM32的庫函數即可。
- static void prvvUARTTxReadyISR( void )
- {
- //mb.c eMBInit函數中
- //pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
- //發送狀態機
- pxMBFrameCBTransmitterEmpty();
- }
- static void prvvUARTRxISR( void )
- {
- //mb.c eMBInit函數中
- //pxMBFrameCBByteReceived = xMBRTUReceiveFSM
- //接收狀態機
- pxMBFrameCBByteReceived();
- }
- void USART1_IRQHandler(void)
- {
- //發生接收中斷
- if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
- {
- prvvUARTRxISR();
- //清除中斷標志位
- USART_ClearITPendingBit(USART1, USART_IT_RXNE);
- }
- //發生完成中斷
- if(USART_GetITStatus(USART1, USART_IT_TC) == SET)
- {
- prvvUARTTxReadyISR();
- //清除中斷標志
- USART_ClearITPendingBit(USART1, USART_IT_TC);
- }
- }
- </FONT>
若進入串口中斷服務函數,則要調用FreeModbus中響應的函數,串口接收中斷服務函數對應prvvUARTRxISR(),其代碼如下
- <FONT size=3>static void prvvUARTRxISR( void )
- {
- //mb.c eMBInit函數中
- //pxMBFrameCBByteReceived = xMBRTUReceiveFSM
- //接收狀態機
- pxMBFrameCBByteReceived();
- }
- </FONT>
在prvvUARTRxISR中又調用了pxMBFrameCBByteReceived(),其實pxMBFrameCBTransmitterEmpty()并不是一個函數,而是一個函數指針。其定義如下,請注意函數指針的聲明和函數聲明的區別。
BOOL( *pxMBFrameCBTransmitterEmpty ) ( void );
在mb.c文件的eMBInit函數完成賦值。一般情況下都會選擇RTU模式,那么pxMBFrameCBByteReceived就和xMBRTUReceiveFSM等價了,
pxMBFrameCBByteReceived = xMBRTUReceiveFSM;
同理,若發生串口發送完成中斷,該中斷服務函數對應prvvUARTTxReadyISR,其代碼如下
- <FONT size=3>static void prvvUARTTxReadyISR( void )
- {
- //mb.c eMBInit函數中
- //pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
- //發送狀態機
- pxMBFrameCBTransmitterEmpty();
- }
- </FONT>
在prvvUARTTxReadyISR中又調用了pxMBFrameCBTransmitterEmpty(),pxMBFrameCBTransmitterEmpty也是函數指針,在eMBInit函數完成賦值,它等價于xMBRTUTransmitFSM。
特別提醒,由于我使用的是串口發送完成中斷,想要進入該中斷服務函數,需要發送一個字節的數據并啟動串口發送中斷,代碼還需要少許修改。在mbRTU.c的eMBRTUSend中稍作修改,代碼如下。
- <P style="MARGIN: 0cm 0cm 0pt" class=MsoNormal> </P>
復制代碼
- /* 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. */
- //發送狀態轉換,在中斷中不斷發送
- eSndState = STATE_TX_XMIT;
- //插入代碼 啟動第一次發送,這樣才可以進入發送完成中斷
- xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
- pucSndBufferCur++;
- usSndBufferCount--;
- //使能發送狀態,禁止接收狀態
- vMBPortSerialEnable( FALSE, TRUE );
寫到這里給位可能看的不是很明白,建議研究一下FreeModbus的源碼,稍作一些修改使用起來才會更加方便。