题外话,用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中断接收和中断发送存在误解,也希望路过的小伙伴及时指出