[原創] 我也來講ModBus移植,基於飛思卡爾 K60,中斷接收,中斷髮送
題外話,用K60,其實我是抵觸的,哪有STM32用的舒服,客戶就要汽車級MCU,那就上吧,就是多花點時間唄。移植下來,收穫還很多,記錄下來,或許將來有小夥伴用得上:
在移植MB之前,先理一理MB的實現機理:
首先是三個函式:
1. eMBInit() eMBEnable() 和 eMBPoll()
我倒過來講:
首先 eMBPoll()是在while(1)裡的,本質上是一個狀態機
eEvent包含三種狀態:
EV_READY(什麼也沒幹)
EV_FRAME_RECEIVED(看看CRC對不對以及傳過來的地址,跟我本身的地址是不是一致,一致就跳下一個狀態,不一致就break)
EV_EXECUTE(已經確認就是我的訊息,趕緊執行吧)
EV_FRAME_SENT(什麼也沒幹)
著重看一下EV_EXECUTE
for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )
{
/* No more function handlers registered. Abort. */
if( xFuncHandlers[i].ucFunctionCode == 0 )
{
break;
}
else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
{
eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
break ;
}
}
通過for迴圈來找功能碼在不在本地執行表中,本地執行表是一個結構體陣列,可以通俗理解成字典,一個碼對應一個函式指標,比如:(mb.c)
static xMBFunctionHandler xFuncHandlers[MB_FUNC_HANDLERS_MAX] = {
{MB_FUNC_READ_HOLDING_REGISTER,eMBVendorRead}, //0x03
{MB_FUNC_WRITE_MULTIPLE_REGISTERS,eMBVendorWrite}, //0x06
{MB_VENDOR_READ,eMBVendorRead}, //0x21
{MB_VENDOR_WRITE,eMBVendorFWrite}, //0x22
{MB_VENDOR_IAP,eMBVendorIap}, //0x30
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED > 0
{MB_FUNC_OTHER_REPORT_SLAVEID, eMBFuncReportSlaveID}, //0x17
#endif
};
結構體定義如下:(mbproto.h)
typedef struct
{
UCHAR ucFunctionCode;
pxMBFunctionHandler pxHandler;
} xMBFunctionHandler;
typedef eMBException( *pxMBFunctionHandler ) ( UCHAR * pucFrame, USHORT * pusLength );
由定義可知,pxMBFunctionHandler 是一個函式指標,引數1 為資料指標,引數2為資料長度指標,返回值為eMBException(這是一個列舉型別)
可見,本質上是通過功能碼呼叫函式指標指向的函式
函式執行過程中,需要對傳輸的引數1,按MB幀格式要求進行改造,比如
uLen = pucFrame[4] << 1;
pucFrame[1] = uLen;
for(i = 0;i < uLen;i++)
{
pucFrame[i + 2] = testData++;//測試使用,實際根據情況修改
}
*usLen = 2 + ulen;
資料也更新了,下面該把資料報回去了,執行下一條語句
eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );
peMBFrameSendCur是一個函式指標,在eMBInit()的時候被賦值了eMBRTUSend,
eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )
{
eMBErrorCode eStatus = MB_ENOERR;
USHORT usCRC16;
ENTER_CRITICAL_SECTION( );
/* Check if the receiver is still in idle state. If not we where to
* slow with processing the received frame and the master sent another
* frame on the network. We have to abort sending the frame.
*/
if( eRcvState == STATE_RX_IDLE )
{
/* 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;
vMBPortSerialEnable( FALSE, TRUE );
}
else
{
eStatus = MB_EIO;
}
EXIT_CRITICAL_SECTION( );
return eStatus;
}
總結下來,就是把資料頭也就是自身的地址加上,算上頭 算上功能碼 算上資料 計算一下CRC放在最後兩個位元組,然後開啟發送中斷,就完事了。傳送中斷自動地完成以上資料的傳送,這裡也體現了一個問題:傳送緩衝區就是在接收緩衝區的基礎上改動得到的,記憶體位置一樣。好eMBPoll()結束
下面開始講剩下的兩個函式eMBInit() eMBEnable()
eMBInit()
初始化了一堆函式指標,這裡不細表,然後呼叫了eMBRTUInit完成對硬體串列埠的初始化,包括本地地址,串列埠號,波特率,奇偶校驗四個引數。
eMBEnable()
開啟協議棧,->開啟接收中斷,關閉傳送中斷,開啟定時器中斷
以下是接收狀態機函式
BOOL
xMBRTUReceiveFSM( void )
{
BOOL xTaskNeedSwitch = FALSE;
UCHAR ucByte;
assert( eSndState == STATE_TX_IDLE );
/* Always read the character. */
( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );
switch ( eRcvState )
{
/* If we have received a character in the init state we have to
* wait until the frame is finished.
*/
case STATE_RX_INIT:
vMBPortTimersEnable( );
break;
/* In the error state we wait until all characters in the
* damaged frame are transmitted.
*/
case STATE_RX_ERROR:
vMBPortTimersEnable( );
break;
/* In the idle state we wait for a new character. If a character
* is received the t1.5 and t3.5 timers are started and the
* receiver is in the state STATE_RX_RECEIVCE.
*/
case STATE_RX_IDLE:
usRcvBufferPos = 0;
ucRTUBuf[usRcvBufferPos++] = ucByte;
eRcvState = STATE_RX_RCV;
/* Enable t3.5 timers. */
vMBPortTimersEnable( );
break;
/* We are currently receiving a frame. Reset the timer after
* every character received. If more than the maximum possible
* number of bytes in a modbus frame is received the frame is
* ignored.
*/
case STATE_RX_RCV:
if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
{
ucRTUBuf[usRcvBufferPos++] = ucByte;
}
else
{
eRcvState = STATE_RX_ERROR;
}
vMBPortTimersEnable( );
break;
}
return xTaskNeedSwitch;
}
接收狀態機,無論何種狀態,只要離開狀態機就開啟定時檢查,超時(超時時間有講究,MB技術要求是3.5T,即3.5倍的幀時長)就算通訊終止。
然後告知EV_FRAME_RECEIVED,當然這僅是在STATE_RX_RCV狀態下其他狀態,直接算廢幀了,重新接收。
eMBPoll()收到EV_FRAME_RECEIVED就開始了前面各種操作,就不贅述了。
最後講一下發送狀態機:
BOOL
xMBRTUTransmitFSM( void )
{
BOOL xNeedPoll = FALSE;
assert( eRcvState == STATE_RX_IDLE );
switch ( eSndState )
{
/* We should not get a transmitter event if the transmitter is in
* idle state. */
case STATE_TX_IDLE:
/* enable receiver/disable transmitter. */
vMBPortSerialEnable( TRUE, FALSE );
break;
case STATE_TX_XMIT:
/* check if we are finished. */
if( usSndBufferCount != 0 )
{
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++; /* next byte in sendbuffer. */
usSndBufferCount--;
}
else
{
xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
/* Disable transmitter. This prevents another transmit buffer
* empty interrupt. */
vMBPortSerialEnable( TRUE, FALSE );
eSndState = STATE_TX_IDLE;
}
break;
}
return xNeedPoll;
}
中斷髮送,直到傳送完畢,自己把傳送中斷關了,待在STATE_TX_IDLE狀態。
至此,MB原始碼講解完畢。
/**********************************************************************************************/
針對K60系統移植過程:
1. 配置uart,我用的是UART4,核心程式碼如下:
奇校驗、使能傳送中斷和接收中斷,資料位設定為9位(是因為加入了奇偶校驗導致),1位停止位,也就是說1bit start + 8bit data + 1bit parity + 1 bit stop,共計11bit(這是MB協議要求的,若無parity,停止位應該是2位)
//配置成9位奇校驗模式
UART_C1_REG(UARTN[MB_RXTX_PORT]) |= (0
| UART_C1_M_MASK
| UART_C1_PE_MASK
| UART_C1_PT_MASK
);
2. 另建MBK60Set原始檔(想通過這個檔案將MB移植與平臺無關)
void ABT_MBUartRXIRQEN(void)
{
gpio_set(MB_DIR_PORT, MB_DIR_RX);
uart_rx_irq_en(MB_RXTX_PORT);//recv intr
}
void ABT_MBUartRXIRQDIS(void)
{
gpio_set(MB_DIR_PORT, MB_DIR_TX);
uart_rx_irq_dis(MB_RXTX_PORT);//禁止recv intr
}
void ABT_MBUartTXIRQEN(void)
{
gpio_set(MB_DIR_PORT, MB_DIR_TX);
uart_tx_irq_en(MB_RXTX_PORT);//tx intr
}
void ABT_MBUartTXIRQDIS(void)
{
gpio_set(MB_DIR_PORT, MB_DIR_RX);
uart_tx_irq_dis(MB_RXTX_PORT);//禁止tx intr
}
就是控制一下irq使能與SP3485傳輸方向
3. 配置Timer,也在MBK60Set
void ABT_MBPitExpired_handler(void)
{
prvvTIMERExpiredISR();//-> pxMBPortCBTimerExpired(); == xMBRTUTimerT35Expired();
led_turn(LED0);
PIT_Flag_Clear(PIT0);
}
void ABT_MBTimer35TInit(uint32 timeOut)
{
//pit_init_ms(PIT0,timeOut);
pit_init_ms(PIT0,2);
set_vector_handler(PIT0_VECTORn,ABT_MBPitExpired_handler);
set_irq_priority(PIT0_IRQn,1);
enable_irq(PIT0_IRQn);
printf("timer is ok\n");
}
void ABT_MBTimerEnable(void)
{
PIT_Flag_Clear(PIT0); //清中斷標誌位
PIT_TCTRL(PIT0) &= ~ PIT_TCTRL_TEN_MASK; //禁止PITn定時器(用於清空計數值)
PIT_TCTRL(PIT0) = ( 0
| PIT_TCTRL_TEN_MASK //使能 PITn定時器
| PIT_TCTRL_TIE_MASK //開PITn中斷
);
enable_irq(PIT0_IRQn);
}
void ABT_MBTimerDisable(void)
{
PIT_Flag_Clear(PIT0); //清中斷標誌位
PIT_TCTRL(PIT0) &= ~ PIT_TCTRL_TEN_MASK; //禁止PITn定時器(用於清空計數值)
PIT_TCTRL(PIT0) = ( 0
| PIT_TCTRL_TEN_MASK //使能 PITn定時器
| PIT_TCTRL_TIE_MASK //開PITn中斷
);
disable_irq(PIT0_IRQn);
}
有價值的問題
0: MB通過反覆使能Timer來重啟定時,在K60中,可不能只是簡單地enable一下timer中斷就完事,一定要清計數值
(MB的本意也是如此,只不過命名不是清計數值)
1: K60中斷接收一串16進位制數,總是漏掉第一個位元組
比如 傳送 23 03 00 60 00 02 C2 97
收到 03 00 60 00 02 C2 97
單步看可以完整接收,跑起來就丟,後來發現在STATE_RX_IDLE,會重複進入兩次
(測試方法:放一個全域性變數在STATE_RX_IDLE中由0自加,竟然發現是2,正常情況執行一次就跳轉,應該是1啊)
因此,我對xMBRTUReceiveFSM做如下改動,可以正常接收
case STATE_RX_IDLE:
if(idleFlag == 0)
{
usRcvBufferPos = 0;
ucRTUBuf[usRcvBufferPos++] = rxData;
idleFlag = 1;
}
else
{
ucRTUBuf[usRcvBufferPos++] = rxData;
}
eRcvState = STATE_RX_RCV;
/* Enable t3.5 timers. */
vMBPortTimersEnable( );
break;
2: K60中斷髮送一串16進位制數,總是漏掉最後一個位元組
比如上報 23 03 04 11 12 13 14 7D C7
終端只收到 23 03 04 11 12 13 14 7D
斷點看緩衝區,明明最後一個位元組是C7,卻沒有傳送
因此我對xMBRTUTransmitFSM做如下改動,傳送正常
case STATE_TX_XMIT:
/* check if we are finished. */
if( usSndBufferCount != 0 )
{
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++; /* next byte in sendbuffer. */
usSndBufferCount--;
flag = 0;
}
else
{
if(flag == 0)
{
flag = 1;
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
}
else
{
xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
/* Disable transmitter. This prevents another transmit buffer
* empty interrupt. */
vMBPortSerialEnable( TRUE, FALSE );
eSndState = STATE_TX_IDLE;
}
}
break;
下圖是測試了30000多次的截圖,
最後的話,修改MB官方原始碼實屬無奈,以後用STM32移植MB,應該不會這麼xxx了
當然,或許我對K60中斷接收和中斷髮送存在誤解,也希望路過的小夥伴及時指出