搜索
bottom↓
回复: 16

开源PLC学习笔记16(FREEMODBUS片段串口收发中断)——2013_12_5

[复制链接]

出0入0汤圆

发表于 2013-12-5 15:47:17 | 显示全部楼层 |阅读模式
本帖最后由 oldbeginner 于 2013-12-6 15:07 编辑

上节与组态软件通讯的实例还不够丰富,因为组态软件也是刚接触,需要花精力才能作出一些例子来,因此暂且先跳过组态软件方面的建模,继续MODBUS的学习。不过,最后会把和组态软件或触摸屏通讯加入到开源PLC中。

FREEMODBUS这个词是上周第一次见到,搜了一下,资料非常丰富,因为对开源PLC来说,串口收发和中断是非常重要的功能,因此决定学习FREEMODBUS这方面的内容。

搜索资料,决定从
http://wenku.baidu.com/view/b43039d626fff705cc170a1c.html
FreeModbus学习笔记
入手。

再利用
http://www.amobbs.com/forum.php? ... t=freemodbus&page=6
STM32上的移植

http://www.amobbs.com/forum.php? ... highlight=freemodbu
51上的移植(感觉硬件不完全FREEMODBUS符合要求)
来辅助测试。





本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入85汤圆

发表于 2013-12-5 16:50:00 | 显示全部楼层
再次支持

出0入0汤圆

发表于 2013-12-5 17:56:55 | 显示全部楼层
支持楼主

出0入0汤圆

 楼主| 发表于 2013-12-6 11:12:30 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-6 11:34 编辑

直接解读FreeModbus学习笔记,只关心感兴趣的串口收发。那么多功能,只关心下面的默认从机。


一、    FreeModbus的移植
1、  物理层接口文件的修改
在物理层,用户只需完成串行口及超时定时器的配置即可。具体应修改接口文件portserial.cporttimer.c

这里的命名和我之前所用的不一致,例如把uart写成了portserial,为了容易理解和记忆,很多名字都会改成和开源PLC类似的名称。

*************************************************************************************
这样理解,
对外有两个接口文件,uart.c进行串口配置,和timer.c进行定时器配置。(portserial ---> uart            porttimer ----> timer)
****************************************************************************************


   uart.c中函数的修改:
1)     voidvMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
此函数的功能为设置串口状态。有两个参数:xRxEnable及xTxEnable。当xRxEnable为真时,应使能串口接收及接收中断。在RS485通讯系统中,还要注意将RS485接口芯片设为接收使能状态;当xTxEnable为真时,应使能串口发送及发送中断。在RS485通讯系统中,还要注意将RS485接口芯片设为发送使能状态。

首先理解为何起名vMBPortSerialEnable,
v表示返回值,MB应该是MODBUS缩写,后面就表示使能串口。 如果把FREEModbus移植到某个系统里,那时会有非常多函数和变量,这些命名是有意义的,方便识别。但是这里只研究MODBUS,所以不会混淆,为了利于入门把名字改掉,以后移植的时候还是要用原来的名字。
**********************************
void UartEnable ( bool Rx, bool Tx)
**********************************

因为函数比较简单,就在这里理解,发现在原版FREEMODBUS V1.5中这个函数的写法有19种,还不包括51和STM32。先读一下代码再说,
http://www.amobbs.com/forum.php? ... highlight=freemodbu
void
UartEnable( BOOL Rx, BOOL Tx )
{
    /* If RXEnable enable serial receive interrupts. If TxENable enable
     * transmitter empty interrupts.
     */
    if(Rx && Tx)
    {
        TxEnable = 1;
        REN = 1;//接收使能
        ES = 1;//中断使能
    } else if(!Rx && Tx) {
        TxEnable = 1;
        REN = 0;//接收使能
        ES = 1;//中断使能
    } else if(Rx && !Tx) {
        TxEnable = 0;
        REN = 1;//接收使能
        ES = 1;//中断使能
    } else {
        TxEnable = 0;
        REN = 0;//接收使能
        ES = 0;//中断使能
    }
}

整一张表看清楚点


可以猜出,TxEnable是用来控制485的那根CTRL_PIN的(笔记14)。
51的代码可以理解,是不是正确,还要对照一下。
Rx为真时,应使能串口接收及接收中断。(对照第1列)
Tx为真时,应使能串口发送及发送中断。(对照第1行)

看起来没问题。

再看
http://www.amobbs.com/forum.php? ... eemodbus&page=6
  * @brief  控制接收和发送状态
  * @param  xRxEnable 接收使能、
  *         xTxEnable 发送使能
  * @retval None
  */
void
UartEnable( BOOL Rx, BOOL Tx)
{
  if(Rx)
  {
    //使能接收和接收中断
    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(Tx)
  {
    //使能发送完成中断
    USART_ITConfig(USART1, USART_IT_TC, ENABLE);
  }
  else
  {
    //禁止发送完成中断
    USART_ITConfig(USART1, USART_IT_TC, DISABLE);
  }
}

用同样方法,这里有点区别,在STM32写法中,Rx和Tx不相关,他俩可以分开写也可以并在一起(直接叠加就可以了)。


对比上面两张表,发现不一致的地方,就是485控制线的电平,
C51
1   1
0   0

STM32
0    1
0    1

出现不一致的地方在于Rx和Tx同时为1或同时为0时,485控制线电平不一致,
找了一下,http://zhidao.baidu.com/link?url ... yHcEOPSARDzE7heRXEK
485是半双工,不能同时收发的。485就有两根线,一根正,一根负,通过两线之间电平判断逻辑0或者1,同一时间内只能接收或者发送,。

我感觉这两种写法都没问题,但是在程序运用时,就要分别出,选择正确组合,使用时肯定要相应变化。

先继续,知道485是半双工,不能同时满足收发,在这个地方只能进行人工调整。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-12-6 13:13:21 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-6 14:54 编辑
oldbeginner 发表于 2013-12-6 11:12
直接解读FreeModbus学习笔记,只关心感兴趣的串口收发。那么多功能,只关心下面的默认从机。

2)        void vMBPortClose( void )
此函数的功能是关闭Modbus通讯端口,具体的,应在此函数中关闭通讯端口的发送使能及接收使能。

*****************
void UartClose(void)
*****************


这个没有例子,是空的,继续。

*****************************************************************

3)        BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity)
此函数的功能是初始化串行通讯端口。有四个参数:ucPORT、ulBaudRate、ucDataBits及eParity。参数ucPORT可以忽略;参数ulBaudRate是通讯端口的波特率,应根据此数值设置所使用硬件端口的波特率;参数ucDataBits为通讯时所使用的数据位宽,注意,若使用RTU模式,则有ucDataBits=8,若使用ASCII模式,则有ucDataBits=7,应根据此参数设置所使用硬件端口的数据位宽;eParity为校验方式,eParity=MB_PAR_NONE为无校验,此时硬件端口应设置为无校验方式及两个停止位,eParity=MB_PAR_ODD为奇校验,此时硬件端口应设置为奇校验方式及一个停止位,eParity= MB_PAR_EVEN为偶校验,此时硬件端口应设置为偶校验方式及一个停止位。函数返回值务必为TRUE。

上面证实了自幼是有代价的,为了简便,1个参数不带。

***********************
bool UartInit(void)
***********************

直接默认
9600,8,n,1  RTU
复制上节的UartInit(void)函数
void UartInit (void)
{
         IE=0x90;
        TMOD = (TMOD & 0X0F) | 0X20;        //串口工作在方式1
        TH1 = -22118400L/12/32/9600;    //求当波特率是9600时定时器的初值
        TL1 = -22118400L/12/32/9600;
        TR1 = 1;
        SCON = 0X50;                    //01010000;
        PCON |= 0X80;                   //波特率加倍
        return true;
}

两个例子,
C51的串口初始化感觉不好,暂时略过。
STM32要配置RX TX 485端口,所以冗长。


**************************************************************

4)        BOOL xMBPortSerialPutByte(CHAR ucByte)
此函数的功能为通讯端口发送一字节数据。参数为:ucByte,待发送的数据。应在此函数中编写发送一字节数据的函数。注意,由于使用的是中断发送,故只需将数据放到发送寄存器即可。函数返回值务必为TRUE。

*****************************
void UartSendByte(char b)
*****************************

void
UartSendByte(char b )
{
    SBUF = b;
    return;
}

要看具体用法。

***************************************************************

5)        BOOL xMBPortSerialGetByte( CHAR * pucByte )
此函数的功能为通讯端口接收一字节数据。参数为:* pucByte,接收到的数据。应在此函数中编写接收的函数。注意,由于使用的是中断接收,故只需将接收寄存器的值放到* pucByte即可。函数返回值务必为TRUE。

*******************
void UartGetByte (char * b)
*******************

void
UartSendByte(char b )
{
    *b=SBUF ;
    return;
}


**********************************************

6)        void prvvUARTTxReadyISR(void)
发送中断函数。此函数无需修改。只需在用户的发送中断函数中调用此函数即可,同时,用户应在调用此函数后,清除发送中断标志位。

***************
static void UartTxReadyISR(void)
*****************


static void UartTxReadyISR( void )
{
//    pxMBFrameCBTransmitterEmpty(  );
        
        RTUTransmitFSM();
}
调用了状态机函数,后面会专门理解一下状态机。

**********************************************

7)        void prvvUARTRxISR(void)
发送中断函数。此函数无需修改。只需在用户的接收中断函数中调用此函数即可,同时,用户应在调用此函数后,清除接收中断标志位。

************************
static void RxReadyISR(void)
************************
和上一个函数的名字太接近,区别一下去掉了前缀Uart

static void prvvUARTRxISR( void )
{               
//    pxMBFrameCBByteReceived(  );           
        
        RTUReceiveFSM();         
}
到状态机时理解。


这样,uart.c (portserial.c)就先这样理解。

补充,重要的中断函数
void UartISR(void) interrupt 4
{
        if(RI){
                RI = 0;
                UartRxISR();         
        }
   
        if(TI){
                TI = 0;
                if(TxEnable){
                        UartTxReadyISR();
                }
        }
}
中断函数里调用了函数,应该会有仿错处理。

出0入0汤圆

 楼主| 发表于 2013-12-6 14:00:27 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-6 14:54 编辑
oldbeginner 发表于 2013-12-6 13:13
2)        void vMBPortClose( void )
此函数的功能是关闭Modbus通讯端口,具体的,应在此函数中关闭通讯端口的 ...

二、     timer.c中函数的修改:


1)     BOOLxMBPortTimersInit( USHORT usTim1Timerout50us )此函数的功能为初始化超时定时器。参数为:usTim1Timerout50us,50us的个数。用户应根据所使用的硬件初始化超时定时器,使之能产生中断时间为usTim1Timerout50us*50us 中断。函数返回值务必为TRUE。
一眼看上去,无法马上知道这个函数的功能是定时器初始化改一下*********************void TimerInit(void)********************STM32中使用了定时4,这里改为定时器0。
利用C51中的函数
bool TimerInit(void)
{
    TMOD &= ~GATE0;      
    TMOD &= ~C_T0;
    TMOD |= T0_M1;
    return ture;
}
其中,#define GATE0       0x08        //定时器1启动控制位,0:TR0为1时启动定时器1 1:TR0为1且INT0为高电平时才启动定时器0
#define C_T0        0x04        //定时器/计数器选择位,0:定时器(时钟源为内部时钟) 1:计数器(时钟源为T0引脚的外部时钟)
#define T0_M0       0x00        //定时器0操作模式0:13位定时器
#define T0_M1       0x01        //定时器0操作模式1:16位定时器
#define T0_M2       0x02        //定时器0操作模式2:8位自动重载定时器
#define T0_M3       0x03        //定时器0操作模式3:TL0和TH0为两组独立8位定时器
******************************************************

2)     voidvMBPortTimersEnable(  )此函数的功能为使能超时定时器。用户需在此函数中清除中断标志位、清零定时器计数值,并重新使能定时器中断。

**********************void TimerEnable(void)**********************
借用C51
void
TimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
    TH0 = U16_HI(0-(F_MCU/12/20000)*n);
    TL0 = U16_LO(0-(F_MCU/12/20000)*n);

    TF0 = 0;
    ET0 = 1;
    TR0 = 1;
}
其中,
#define U16_HI(d)(TO(u16,(d))>>8)
#define U16_LO(d)(TO(u16,(d))&0x00FF)
#define F_MCU (110592000)

*************************************************************

3)     voidvMBPortTimersDisable(  )此函数的功能为关闭超时定时器。用户需在此函数中清零定时器计数值,并关闭定时器中断。

*****************
void TimerDisable(void)
*****************
void
    TimerDisable(  )
{
    /* Disable any pending timers. */
   ET0 = 0;
   TR0 = 0;
}

*************************************************************

4)     voidTIMERExpiredISR( void )定时器中断函数。此函数无需修改。只需在用户的定时器中断中调用此函数即可,同时,用户应在调用此函数后清除中断标志位。

***********************
void TimerExpiredISR(void)
*************************

void TimerExpiredISR( void )  interrupt 1
{
//    ( void )pxMBPortCBTimerExpired(  );        
   
             RTUTimerT35Expired();
  }
调用了状态机相关的函数。
等到状态机部分解决。

************************************************
这样timer.c的函数先这样理解。

从uart.c和timer.c中可以看出和以前的区别,这里都没有标志位,应该都放在状态机相关部分了。


出0入0汤圆

 楼主| 发表于 2013-12-6 14:52:01 | 显示全部楼层
本帖最后由 oldbeginner 于 2013-12-6 17:09 编辑
oldbeginner 发表于 2013-12-6 14:00
二、     timer.c中函数的修改:

1)     BOOLxMBPortTimersInit( USHORT usTim1Timerout50us )此函数的 ...

状态机也是第一次接触,虽然看键盘代码时有点了解,但是没有专门理解过。
http://labviewnote.weebly.com/441-2936624577264263161620171.html

摘录一下,
状态机的实质就是:状态、状态的转换和处理。状态的转换来自于某些事件发生或状态结束来触发。状态的处理就是你想要解决的问题的程序代码。
假如,我们早上被闹钟叫起,起床后要把被子叠好,穿好衣服,拉开窗帘打开窗户置换室内的空气;然后开始刮胡须、洗脸、刷牙;之后到餐厅边吃早餐边听广播或看电视新闻节目;早餐后穿好外衣出门上班。基本上就是这么个程序(也可能比这个还复杂随你想象:比如把手机、钱包、驾照带好等等)。
      问题是:有没有这样的时候,起来晚了,连洗脸、刷牙都顾不上了,吃点东西赶紧出门上班,可能会有这样的情况。
      问题是:有没有这样的时候,起来晚了,连洗脸、刷牙、吃点东西都顾不上了,赶紧出门上班,也可能会有这样的情况。
      看到了吧,整个过程是多么的复杂!其实将我们所关心的事情进行抽象化处理后是很简单的,也就是这么几件事(状态及状态的改变):起床、洗漱、早餐、上班。例图给出了上述状态的状态图


      如果“出门上班”是我们最终的目的,从例图上可以看出共有三种途径能够达到这个目的。
一是:按部就班的“起床”-“洗漱”-“早餐”-“出门上班”;
二是:时间较紧“起床”-“洗漱”-“出门上班”;
三是:时间更紧“起床”-“出门上班”。
注意:这是抽象出来的状态图,根据不同的需求,抽象的结果也不同。为了使大家有更清晰的理解,有必要再重申一下:状态机不是程序,是从程序中抽象出来的程序构架,真正的程序代码应该在存在于例图中的状态椭圆。

*******************************************************

先理解一个出错标示,
typedef enum
{
    MB_ENOERR,                  /*!< no error. */
    MB_ENOREG,                  /*!< illegal register address. */
    MB_EINVAL,                  /*!< illegal argument. */
    MB_EPORTERR,                /*!< porting layer error. */
    MB_ENORES,                  /*!< insufficient resources. */
    MB_EIO,                     /*!< I/O error. */
    MB_EILLSTATE,               /*!< protocol stack in illegal state. */
    MB_ETIMEDOUT                /*!< timeout error occurred. */
} eMBErrorCode;

简化成ErrCode,用来表示不同出错类型。

然后,
开始理解状态机部分,
首先是标志位

typedef enum
{
    STATE_RX_INIT,              /*!< Receiver is in initial state. */
    STATE_RX_IDLE,              /*!< Receiver is in idle state. */
    STATE_RX_RCV,               /*!< Frame is beeing received. */
    STATE_RX_ERROR              /*!< If the frame is invalid. */
} eMBRcvState;  (改成 ReceiveState记忆)

typedef enum
{
    STATE_TX_IDLE,              /*!< Transmitter is in idle state. */
    STATE_TX_XMIT               /*!< Transmitter is in transfer state. */
} eMBSndState;     (改成 SendState记忆)


然后,看一下变量定义,
volatile UCHAR  ucRTUBuf[MB_SER_PDU_SIZE_MAX];

static volatile UCHAR *pucSndBufferCur;
static volatile USHORT usSndBufferCount;

static volatile USHORT usRcvBufferPos;
首先在宏定义中,指明了该模式下所支持的最小请求帧长度为4(1字节地址+1字节命令+2字节校验),最大请求帧长度为256,CRC为两字节,地址为第一字节,PDU开始于第二字节。
在全局变量中,只定义了一个串口缓存数组ucRTUBuf[MB_SER_PDU_SIZE_MAX]。由于发送与接收不是同步的,故可采用该缓存数组实现Modbus协议。在接收过程中,将所接收到的数据直接存放于缓存ucRTUBuf中,在发送过程中,通过指针*pucSndBufferCur来访问该数组。

**************************************************

1)        eMBErrorCode eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
此函数为RTU模式的初始化函数。此函数中判断串行口初始化是否成功(通过判断串行口初始化函数的返回值实现。当然,查看返回值必然先调用该函数,从而完成端口初始化),如果成功,则根据波特率计算T35,初始化超时定时器。

******************
ErrCode RtuInit(void)
******************


ErrorCode
RtuInit(void )
{
    ErrorCode    Status = MB_ENOERR;
    ULONG           usTimerT35_50us;

     ENTER_CRITICAL_SECTION(  );

      if( UartInit( void ) != TRUE )
    {        
        Status = MB_EPORTERR;
    }
    else
    {
        if( TimersInit( ) != TRUE )
        {
            Status = MB_EPORTERR;
        }
    }
    EXIT_CRITICAL_SECTION(  );
        SendState = STATE_TX_IDLE;

    return Status;
}

感觉这次是个总的初始化,其中如果uart初始化出错,或timer初始化出错,都要记录下来。所以uart初始化或timer初始化都要返回bool值。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-12-6 16:21:02 | 显示全部楼层
oldbeginner 发表于 2013-12-6 14:52
状态机也是第一次接触,虽然看键盘代码时有点了解,但是没有专门理解过。
http://labviewnote.weebly.com ...

        void eMBRTUStart( void )
此函数为RTU模式开始函数。函数主要功能是,将接收状态ReceiveState设为STATE_RX_INIT(Receiver is in initial state),使能接收同时关闭发送,使能超时定时器。

***************
void RtuStart(void)
***************


void
RtuStart( void )
{
    ENTER_CRITICAL_SECTION(  );
    ReceiveState = STATE_RX_INIT;

    UartEnable( TRUE, FALSE );
    TimersEnable(  );

    EXIT_CRITICAL_SECTION(  );
}

其中,
#define ENTER_CRITICAL_SECTION( ) EA = 0;  
#define EXIT_CRITICAL_SECTION( ) EA = 1;

用法和上节一致,初始化时,禁止中断。

感觉,已经看了一些代码,感觉这些变量名或函数名起得质量真心一般,最起码不优秀。比如这个中断,起名进入关键部分,什么也不能说明。

***************************************************************

        void eMBRTUStop( void )
此函数为RTU模式终止函数。函数主要功能是,关闭接收与发送,关闭超时定时器。

*******************
void RtuStop(void)
*******************


void
RtuStop( void )
{
    ENTER_CRITICAL_SECTION(  );

    UartEnable( FALSE, FALSE );
    TimerDisable(  );
    EXIT_CRITICAL_SECTION(  );
}
容易理解。

************************************************************

        eMBErrorCode eMBRTUReceive( UCHAR * pucRcvAddress, UCHAR ** pucFrame, USHORT * pusLength )
此函数为RTU接收数据帧信息提取函数。函数主要功能是,将接收帧(存放于缓存)的地址指针赋给指针变量pucRcvAddress,将PDU编码首地址赋给指针* pucFrame,将PDU长度地址赋给指针变量pusLength。使用指针访问缓存数组,而不是额外开辟缓存存放帧信息,大大减少了内存的开支。

************************
ErrCode RtuReceive ( uchar * addr , uchar * frame , uchar * length)
************************


ErrCode
RtuReceive( uchar * addr , uchar * frame , uchar * length )
{
    BOOL        FrameReceived = FALSE;
    ErrCode    Status = MB_ENOERR;

    ENTER_CRITICAL_SECTION(  );
    assert( usRcvBufferPos < MB_SER_PDU_SIZE_MAX );

    if( ( usRcvBufferPos >= MB_SER_PDU_SIZE_MIN )
        && ( usMBCRC16( ( UCHAR * ) ucRTUBuf, usRcvBufferPos ) == 0 ) )
    {
   
        *addr = ucRTUBuf[MB_SER_PDU_ADDR_OFF];

          *length = ( USHORT )( usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC );

        *frame = ( UCHAR * ) & ucRTUBuf[MB_SER_PDU_PDU_OFF];
        FrameReceived = TRUE;       
    }
    else
    {
        Status = MB_EIO;
    }

    EXIT_CRITICAL_SECTION(  );
    return Status;
}
函数主要功能是,将接收帧(存放于缓存)的地址指针赋给指针变量addr,将PDU编码首地址赋给指针frame,将PDU长度地址赋给指针变量length。


****************************************************
        eMBErrorCode eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )
此函数为RTU回复帧信息组织函数。函数的功能是,此函数首先使发送内容指针pucSndBufferCur指向pucFrame之前的一个地址,并将该地址内容填充为ucSlaveAddress,并使用直接访问方式向缓存数组ucRTUBuf的相应地址内存入CRC校验值。注意,此函数中,对ucRTUBuf的访问既有间接方式(指针pucSndBufferCur与pucFrame),又有直接方式(直接向相应地址内写值),比较难理解。
回复帧组织完后,将发送状态eSndState设为STATE_TX_XMIT(Transmitter is in transfer state),并禁止接收使能发送。发送一旦使能,就会进入发送中断,完成相应字符的发送

**********************************
ErrCode RtuSend(uchar addr , const uchar * frame , ushort length)
**********************************


ErrCode
RtuSend( uchar addr , const uchar * frame , ushort length)
{
    ErrCode    Status = MB_ENOERR;
    ushort          usCRC16;

    ENTER_CRITICAL_SECTION(  );

    if( ReceiveState == STATE_RX_IDLE )
    {
        pucSndBufferCur = ( UCHAR * ) pucFrame - 1;
        usSndBufferCount = 1;

        pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
        usSndBufferCount += usLength;

        usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 & 0xFF );
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 );

        SendState = STATE_TX_XMIT;
        UartEnable( FALSE, TRUE );
                RutTransmitFSM();
    }
    else
    {
        Status = MB_EIO;
    }
    EXIT_CRITICAL_SECTION(  );
    return Status;
}
功能是,此函数首先使发送内容指针pucSndBufferCur指向frame之前的一个地址,并将该地址内容填充为addr,并使用直接访问方式向缓存数组ucRTUBuf的相应地址内存入CRC校验值。

*************************************************

出0入0汤圆

 楼主| 发表于 2013-12-6 18:36:28 | 显示全部楼层
oldbeginner 发表于 2013-12-6 16:21
        void eMBRTUStart( void )
此函数为RTU模式开始函数。函数主要功能是,将接收状态ReceiveState设为STA ...

        BOOL xMBRTUReceiveFSM( void )
此函数描述了一个接收状态机,供接收中断调用。状态机中,首先完成串口接收寄存器读取,然后判断相应接收状态eRcvState,实现接收。在STATE_RX_INIT状态,重置超时定时器,等待超时中断(超时中断会把eRcvState设为STATE_RX_IDLE);在STATE_RX_ERROR状态,同样会重置超时定时器等待超时中断;在STATE_RX_IDLE状态,会将接收字符个数置零,同时向缓存数组ucRTUBuf中存入接收到的字符,跳入状态STATE_RX_RCV,并使重置超时定时器;在STATE_RX_RCV状态,不断将接收到的字符存入缓存,并统计接收计数,重置超时定时器,接收计数大于帧最大长度时,会跳入STATE_RX_ERROR状态。
在任何一处发生超时中断,都会将状态eRcvState置为STATE_RX_IDLE。在接收过程(STATE_RX_RCV)中,发生超时中断,指示着一帧数据接收完成。
接收状态机如图所示(重新画了一下图,有点感觉了)



*********************************
bool RtuReceiveFSM(void)
*********************************



BOOL
RtuReceiveFSM( void ) reentrant
{
    BOOL            TaskNeedSwitch = FALSE;
    UCHAR           ucByte;

    assert( SendState == STATE_TX_IDLE );

     ( void )UartGetByte( ( CHAR * ) & ucByte );

    switch ( ReceiveState )
    {
      case STATE_RX_INIT:       
        TimerEnable(  );
        break;

    case STATE_RX_ERROR:
        TimerEnable(  );
        break;

    case STATE_RX_IDLE:         
        usRcvBufferPos = 0;
        ucRTUBuf[usRcvBufferPos++] = ucByte;
        ReceiveState = STATE_RX_RCV;

        TimerEnable(  );
        break;
    case STATE_RX_RCV:         
        if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
        {
            ucRTUBuf[usRcvBufferPos++] = ucByte;
        }
        else
        {
            ReceiveState = STATE_RX_ERROR;
        }
        TimerEnable(  );
        break;
    }
    return TaskNeedSwitch;
}

一个知识点reentrant(可重入函数声明关键字),解决L15警告(笔记14)
http://wenku.baidu.com/view/b5cd861614791711cc7917da.html
感觉这篇文章还可以,值得理解一下。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-12-7 12:14:27 | 显示全部楼层
oldbeginner 发表于 2013-12-6 18:36
        BOOL xMBRTUReceiveFSM( void )
此函数描述了一个接收状态机,供接收中断调用。状态机中,首先完成串 ...

        BOOL xMBRTUTransmitFSM( void )
此函数描述了一个发送状态机,供发送中断调用。状态机中,判断相应发送状态SendState,实现发送。在STATE_TX_IDLE状态,使能接收关闭发送;在STATE_TX_XMIT状态,调用底层串口发送函数将缓存中的字符发送出去,并使发送指针加1,待发送字符数减1,待发送数为0时,将向系统发送事件EV_FRAME_SENT(Frame sent),同时使能接收关闭发送,并转向STATE_TX_IDLE状态。



*************************
bool RtuTransmitFSM(void)
*************************


BOOL
RtuTransmitFSM( void ) reentrant
{
    BOOL            NeedPoll = FALSE;

    assert( ReceiveState == STATE_RX_IDLE );

    switch ( SendState )
    {   
    case STATE_TX_IDLE:
        UartEnable( TRUE, FALSE );
        break;

    case STATE_TX_XMIT:
        if( usSndBufferCount != 0 )
        {
            UartPutByte( ( CHAR )*pucSndBufferCur );
            pucSndBufferCur++;  
            usSndBufferCount--;
        }
        else
        {
            NeedPoll = UartEventPost( EV_FRAME_SENT );
            UartEnable( TRUE, FALSE );
            SendState = STATE_TX_IDLE;
        }
        break;
    }
    return NeedPoll;
}

*****************************************************************

u      BOOL xMBRTUTimerT35Expired(void )
此函数描述了发生超时中断时应处理的事务,供超时中断调用。通过判读接收状态eRcvState来决定要处理的事务,思想上有点像摩尔类型的FSM的输出逻辑。若中断发生于STATE_RX_INIT,则向系统发送事件EV_READY(Startupfinished);若中断发生于STATE_RX_RCV,则向系统发送事件EV_FRAME_RECEIVED(Framereceived);若中断发生于STATE_RX_ERROR,则跳出,不执行。在每个执行分支结束后,均关闭超时定时器,并将eRcvState转为STATE_RX_IDLE。当然,这儿不像FSM的输出逻辑。


*****************************
bool RtuTimerT35Expired(void)
*****************************


BOOL
RtuTimerT35Expired( void )  reentrant
{
    BOOL           xNeedPoll = FALSE;

    switch ( ReceiveState )
    {
    case STATE_RX_INIT:
        NeedPoll = UartEventPost( EV_READY );
        break;
    case STATE_RX_RCV:
        NeedPoll = UartEventPost( EV_FRAME_RECEIVED );
        break;
    case STATE_RX_ERROR:
        break;
    default:
        assert( ( ReceiveState== STATE_RX_INIT ) ||
                ( ReceiveState== STATE_RX_RCV ) || ( ReceiveState== STATE_RX_ERROR ) );   
        break;
    }

    TimerDisable(  );
    ReceiveState = STATE_RX_IDLE;

    return NeedPoll;
}

做了这些准备工作,就可以开始理解工作流程了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-12-7 14:30:49 | 显示全部楼层
学习速度神速啊

出0入0汤圆

 楼主| 发表于 2013-12-7 17:14:12 | 显示全部楼层
oldbeginner 发表于 2013-12-7 12:14
        BOOL xMBRTUTransmitFSM( void )
此函数描述了一个发送状态机,供发送中断调用。状态机中,判 ...


看一下主函数,
void
main( void )
{
    eMBErrorCode    Status;

        usRegInputBuf[0] = 1;
        usRegInputBuf[1] = 2;
        usRegInputBuf[2] = 3;
        usRegInputBuf[3] = 4;

    Status = MBInit(  );

    Status =MBEnable(  );

    for( ;; )
    {
        ( void )MBPoll(  );

        usRegInputBuf[0]++;
    }
}

************************************************************
http://blog.sina.com.cn/s/blog_4aca3d5d01011a1d.html
MBInit()函数的源码分析

以RTU方式为例,首先,检查调用的地址是否合法。如不合法,返回错误。如果合法则继续执行,

首先,针对RTU方式还是ASCII方式,选择不同的编译模块。

对需要调用的函数指针进行复制。如果移植需要改变其他用途,则要修改相应的指针,包括如下赋值:

            pvMBFrameStartCur =RtuStart;

            pvMBFrameStopCur = RtuStop;

            peMBFrameSendCur = RtuSend;

            peMBFrameReceiveCur = RtuReceive;

            pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? UartClose : NULL;

            pxMBFrameCBByteReceived = RtuReceiveFSM;

            pxMBFrameCBTransmitterEmpty = RtuTransmitFSM;

            pxMBPortCBTimerExpired = RtuTimerT35Expired;

然后调用Status = RtuInit( );具体初始化通讯端口。


RtuInit这个函数主要干两件事:

第一,   初始化串口:   

if(UartInit( ) != TRUE )

{

   Status = MB_EPORTERR;

}

这个函数在uart.c中,需要用户在移植的时候根据自己的处理器编写。

第二,   初始化计时器:首先要根据波特率计算一下是3.5~5.0个字节周期的时间,然后再调用TimerInit(  ),初始化计时器。这个函数在timer.c中,需要用户在移植的时候根据自己的处理器编写。
******************************************************

MBEnable源码分析


首先,看看Modbus功能是否是被关闭的,如果不是被关闭(可能是没有被初始化或者已经打开),就返回错误。

如果是disable状态,就干下面两件事:

l  调用pvMBFrameStartCur()。由于这是个函数指针,在模块MBInit中,指向了RtuStart函数

n  在源代码中有这样一段注释:,意思是,首先设置成STATE_RX_INIT,然后打开计时器,等待t3.5以后,进入STATE_RX_IDLE状态。

n  看源代码中,首先有设置Receive的状态,后调用UartEnable,设置接收状态,然后打开定时器。

n  当定时器中断后,自动调用中断服务程序,在中断服务程序中,只调用了pxMBPortCBTimerExpired,而这是一个函数指针,在RTU方式初始化时,被指向了RtuTimerT35Expired()函数。

n  xMBRTUTimerT35Expired函数在mbrtu.c中,在这里,我们只看第一种方式,就是进入初始化状态,在t35时间以后,只调用了一个NeedPoll =UartEventPost( EV_READY );

n  UartEventPost函数就是在事件队列里加了一个EV_RDY事件。

l  然后,将MB状态改为使能状态,

l  初始化结束。

***************************************************************************

总线侦听MBPoll()

首先,判断系统是否被使能,如果没有,则返回错误值。

然后,检查是否有事件发生,如果有,则根据不同类型的事件响应:

l  如果是EV_RDY,表示系统刚刚进入侦听状态,则什么都不做;

l  如果状态为EV_FRAME_RECEIVED,也就是接收到完整的帧,做下面两件事情:

n  调用Status=peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength )。这是一个函数指针,MBInit中,被初始化指向RtuReceive。

n  RtuReceive这个函数首先校验帧的长度和CRC,然后从协议中解析出地址、数据和长度。

n  然后检查地址,如果是广播地址或者是本机地址,就调用UartEventPost( EV-EXECUTE),将接收器的状态更改为EV_EXECUTE。

l  如果状态为EV_EXECUTE,就在函数列表中检查,有没有与命令字段相符合的函数来解析相应则执行该函数,否则返回非法功能代码。


*************************************************************
循环侦听

主函数调用MBPoll( void )检测事件。
6)        若发生串口接收中断,且ReceiveState为STATE_RX_IDLE(4中已将ReceiveState设为STATE_RX_IDLE),则向接收缓存中存入接收到的字符,同时将ReceiveState设为STATE_RX_RCV状态,并清零超时定时器。在下一个数据来到时,不断将数据存入接收缓存,并清零超时定时器。

7)        如果没有接收完成,则不可能发生超时中断。发生超时中断,说明T35时间内未收到新的串口数据,根据Modbus协议的规定,这指示着一帧请求数据接收完成。在中断中,向协议栈发送消息EV_FRAME_RECEIVED(Frame received),等待协议栈处理此消息。

8)        MBPoll( void )检测到事件EV_FRAME_RECEIVED后,调用static peMBFrameReceive peMBFrameReceiveCur简单判断请求帧数据,并向协议栈发送消息EV_EXECUTE(Execute function)。

9)        MBpoll( void )检测到事件EV_EXECUTE后,根据相应的请求代码查找处理该功能的函数指针来处理该功能。若不是广播消息,则调用static peMBFrameSend peMBFrameSendCur发送回复消息,在此函数中,只把要回复的数据复制到了串口缓存中,同时将SendState设为STATE_TX_XMIT(Transmitter is in transfer state),并通过调用UartEnable( BOOL Rx, BOOL Tx )使能发送中断。注意,发送中断使能后,由于串口发送寄存器本来就是空的,故在使能后将进入发送中断中。

10)        发送中断中,且SendState为STATE_TX_XMIT(9中已将SendState设为STATE_TX_XMIT),则将串口缓存中的数据发送出去,同时不断对发送字符个数统计,当发送完成后,向协议栈发送消息EV_FRAME_SENT(Frame sent)。

11)        MBPoll( void )检测到事件EV_FRAME_SENT后,不处理此消息。

12)        当串口接收到数据后,协议栈将重复6-11处理消息

*********************************************
只是大致浏览了一下,还没有细化,已经能感觉到比较复杂了,需要把通路整理完整。

出0入13汤圆

发表于 2014-3-12 21:47:14 | 显示全部楼层
最近调试发现一个问题,就是接收函数的RtuReceive( uchar * addr , uchar * frame , uchar * length )中这一句话明显有问题:
    if( ( usRcvBufferPos >= MB_SER_PDU_SIZE_MIN )
        && ( usMBCRC16( ( UCHAR * ) ucRTUBuf, usRcvBufferPos ) == 0 ) )
其中usMBCRC16并不判断CRC的结果啊!
freemodbus源码中是如下的CRC函数:
USHORT
usMBCRC16( UCHAR * pucFrame, USHORT usLen )
{
    UCHAR           ucCRCHi = 0xFF;
    UCHAR           ucCRCLo = 0xFF;
    int             iIndex;

    while( usLen-- )
    {
        iIndex = ucCRCLo ^ *( pucFrame++ );
        ucCRCLo = ucCRCHi ^ pgm_read_byte( &aucCRCHi[iIndex] );
        ucCRCHi = pgm_read_byte( &aucCRCLo[iIndex] );;
    }
    return ucCRCHi << 8 | ucCRCLo;
}

返回的是CRC啊,这里却要( usMBCRC16( ( UCHAR * ) ucRTUBuf, usRcvBufferPos ) == 0 ) 实在不理解其中的含义啊!

出0入0汤圆

发表于 2014-3-16 20:15:57 | 显示全部楼层
不错。研究很透彻。这个东西很久以前研究过了。

出0入0汤圆

发表于 2014-11-27 21:33:41 | 显示全部楼层
学习了,楼主讲解的不错

出0入0汤圆

发表于 2015-6-23 15:02:27 | 显示全部楼层
讲得很好,很详细

出0入0汤圆

发表于 2015-6-23 15:49:36 | 显示全部楼层
PLC 数字量输入口滤波的程序在哪里,谁能给我说一下
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-26 18:44

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表