目录

引言

1、Freemodbus简介

1.1 如何使用库文件

1.2 工程建立详解

​编辑 1.3 Keil内需要的初步设置

 2、Freemodbus-RTU相关代码详解

2.1 主机没有发送数据时的程序流程-modbus部分

2.2 主机发送报文:01 06 00 02 00 01

2.3 其它

3、总结


引言

        利用Freemodbus从机开源库实现一主多从,主机是串口模拟的上位机,从机1是一块GD32的开发板,控制项(寄存器){热敏电阻、湿敏电阻、LED1、LED2};从机2是自己焊的一块STM32洞洞板,控制项(寄存器){热敏电阻、湿敏电阻、LED1、LED2};

        使用到的器件,GD32开发板(自带RS485转TTL电路),STM32洞洞板,USB转RS485模块,RS485转TTL模块,ST-Link下载器。

        从机1(gd32)和从机2(stm32)的代码以及Freemodbus库都放在了下方的链接:

通过网盘分享的文件:代码包.zip
链接: https://pan.baidu.com/s/10x1x7Z0F80pnuMuBRs9zdw?pwd=GD32 提取码: GD32

1、Freemodbus简介

        我的这篇文章介绍了Modbus协议内容:RS485电气协议和Modbus软件协议-CSDN博客,Modbus协议本身简单,但是想用软件实现是很复杂的,而Freemodbus是一个开源的库(只有从机代码开源),这个库已经实现了Modbus通信协议的大部分内容,比如3.5个字符空闲中断等等。我们只需要学会移植这个库,实现自己的逻辑代码即可。

        FreeMODBUS Downloads - Embedded Experts这是下载Freemodbus库的官方网址,代码在GitHub上,国内可以使用加速器访问,比如Steam加速器(现在叫Watt Toolkit)将里面的GitHub加速勾选就可以访问了。

1.1 如何使用库文件

        如下图所示就是下载好的库文件,我们只关心两个文件夹,demo和modbus

        那怎么在我们自己的工程中添加库文件呢?下图是软件架构图,APP是业务应用层;Mid是中间层,主要是移植的库文件中实现具体协议内容的那部分文件;DRV就是驱动层,细分成两部分,一部分就是普通片上外设的驱动实现,一部分就是Modbus需要使用的片上外设的驱动实现,485电路也是串口通信,那就需要串口初始化,3.5个字符的空闲时间需要用定时器 ,那就需要定时器驱动初始化。

1.2 工程建立详解

        FreeModbus库\freemodbus-master\demo\BARE\port将这个路径下的四个文件,复制放到我们工程文件的Drv_modbus文件夹中

        FreeModbus库\freemodbus-master\modbus将这个路径下的,下图中选中的文件,复制到工程文件的Mid_modbus文件夹下。 

        由于我们只会用到Modbus读写寄存器的三个功能,03、06、16,所以打开我们的工程文件,打开Mid_modbus文件,打开functions文件夹,只留下图中的两个.c文件即可,下图中的第一个就是功能实现代码,第二个是一些通用接口函数的实现。

 1.3 Keil内需要的初步设置

        把这些文件添加进Keil工程内之后,记得包含这些新添加的文件夹的路径和头文件的途径。之后还需要对库文件的代码进行一些设置:

        想办法找到并打开Freemodbus库文件的mbconfig.h这个头文件,由于我们使用RTU报文,所以只需使能MB_RTU_ENABLED的(1),其它两个失能为0。

        然后这部分代码是功能使能区,由于我们只使用03、06、16三个功能,所以只需使能下图中的三个1。

 

        之后再编译只会显示一个错误,说有一个函数eMBRegHoldingCB没有实现定义,这就是我们自己需要实现的函数,后面再讲。我们在工程Mid_midbus下新加modbus_slave的.c和.h文件,用于实现eMBRegHoldingCB这个函数。

 2、Freemodbus-RTU相关代码详解

        我写这部分的思路就是,先用已经写好的GD32的代码,把上位机主机串口发送字符帧,从机返回字符帧这个过程,手动过一遍代码,这样就可以知道Freemodbus库文件各函数的调用关系。

        还是要解释一下各个文件,不然之后跳来跳去容易混乱,portserial.c串口驱动代码和串口中断服务函数的实现,当然还有一些其它调用函数;porttimer.c是定时器中断驱动及其中断服务函数的实现,同样也有一些其它调用函数;portevent.c提供从机事件状态,比如没有接收到主机字符帧是一个状态码,接收完一包数据又是一个状态码....

        mbcrc.c就是crc循环校验的实现,可以不用细看;mbrtu.c很重要,代码可以细看,是RTU报文内容的实现;mbfuncholding.c是功能码的视线;mbutils.c是一些通用接口函数的实现;modbus_slave.c是我们自己加入的文件,用于实现eMBRegHoldingCB这个函数。

        modbus_app.c是我们自己实现的业务层代码文件。

        值得一提的是,freemodbus库文件中的驱动文件有三个,但是却只有一个port.h文件,因为这三个.c文件的函数声明被集成放在了一个port.h文件中。

2.1 主机没有发送数据时的程序流程-modbus部分

        提示:整个第二章都是用的GD32的代码。

(1)main.c中的main函数调用初始化函数

AppInit();

 这个函数又调用modbus应用层初始化函数

ModbusAppInit();

 这个函数又调用从机初始化函数

ModbusSlaveInit(&mbInstace); 

(2)从机初始化函数内会调用两个函数

void ModbusSlaveInit(ModbusSlaveInstance_t *mbInstance)
{
    eMBInit(MB_RTU, mbInstance->slaveAddr, 0, mbInstance->baudRate, MB_PAR_NONE);//串口号在portserial中已经写固定了,所以写啥都无所谓
    eMBEnable();
    g_modbusFuncCb = mbInstance->cb;//结构体直赋值

【2.1】eMBInit(MB_RTU, mbInstance->slaveAddr, 0, mbInstance->baudRate, MB_PAR_NONE);

        函数的参数中,串口号给的是0,这一点之后解释。

/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode
eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
    eMBErrorCode    eStatus = MB_ENOERR;

    /* check preconditions */
    //从机地址范围是1~247,这里做检查判断
    if( ( ucSlaveAddress == MB_ADDRESS_BROADCAST ) ||
        ( ucSlaveAddress < MB_ADDRESS_MIN ) || ( ucSlaveAddress > MB_ADDRESS_MAX ) )
    {
        eStatus = MB_EINVAL;
    }
    else
    {
        ucMBAddress = ucSlaveAddress;//全局变量接收表示从机地址的字节

        switch ( eMode )
        {
#if MB_RTU_ENABLED > 0
        case MB_RTU://下面这些代码主要是给函数指针变量赋值
            pvMBFrameStartCur = eMBRTUStart;
            pvMBFrameStopCur = eMBRTUStop;
            peMBFrameSendCur = eMBRTUSend;
            peMBFrameReceiveCur = eMBRTUReceive;
            pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
            pxMBFrameCBByteReceived = xMBRTUReceiveFSM;
            pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;
            pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;

            eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );
            break;
#endif
#if MB_ASCII_ENABLED > 0
        case MB_ASCII:
            pvMBFrameStartCur = eMBASCIIStart;
            pvMBFrameStopCur = eMBASCIIStop;
            peMBFrameSendCur = eMBASCIISend;
            peMBFrameReceiveCur = eMBASCIIReceive;
            pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
            pxMBFrameCBByteReceived = xMBASCIIReceiveFSM;
            pxMBFrameCBTransmitterEmpty = xMBASCIITransmitFSM;
            pxMBPortCBTimerExpired = xMBASCIITimerT1SExpired;

            eStatus = eMBASCIIInit( ucMBAddress, ucPort, ulBaudRate, eParity );
            break;
#endif
        default:
            eStatus = MB_EINVAL;
        }

        if( eStatus == MB_ENOERR )
        {
            if( !xMBPortEventInit(  ) )
            {
                /* port dependent event module initalization failed. */
                eStatus = MB_EPORTERR;
            }
            else
            {
                eMBCurrentMode = eMode;//全局变量接收报文类型码
                eMBState = STATE_DISABLED;//注意现在这个全局变量已被赋值
            }
        }
    }
    return eStatus;
}

        在函数 eMBInit中会调这个函数

 eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );

/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode
eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
    eMBErrorCode    eStatus = MB_ENOERR;
    ULONG           usTimerT35_50us;

    ( void )ucSlaveAddress;
    ENTER_CRITICAL_SECTION(  );

    /* Modbus RTU uses 8 Databits. */
    if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )
    {
        eStatus = MB_EPORTERR;
    }
    else
    {
        /* If baudrate > 19200 then we should use the fixed timer values
         * t35 = 1750us. Otherwise t35 must be 3.5 times the character time.
         */
        if( ulBaudRate > 19200 )
        {
            usTimerT35_50us = 35;       /* 1800us. */
        }
        else
        {
            /* The timer reload value for a character is given by:
             *
             * ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
             *             = 11 * Ticks_per_1s / Baudrate
             *             = 220000 / Baudrate
             * The reload for t3.5 is 1.5 times this value and similary
             * for t3.5.
             */
            usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
        }
        if( xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) != TRUE )
        {
            eStatus = MB_EPORTERR;
        }
    }
    EXIT_CRITICAL_SECTION(  );

    return eStatus;
}

        在函数xMBPortSerialInit内可以看到几个参数前面加了void,意在告诉编译器我不使用这些参数,不用给我报警告提示,也意在告诉阅读代码的读者,我故意忽略这些参数的,并不是我忘记使用了,这也是最开始   eMBInit函数中,串口号给0的原因,因为调用到这,根本不会用到,我已经在驱动函数中把串口号写死了,所以参数位置写啥都可以。

        不得不说多看看这些开源库代码还是很有好处的,虽然很难,但是能学到很多东西,比如说看到现在,你也该发现了,库代码中有很多变量前缀什么uc, us, eMB, xMB, vMB等等,冠以uc开头的变量是表示这个变量是无符号char类型的,uc就是unsigned short;eMB表示此变量或函数与Modbus协议有关,并且类型或返回值是枚举类型;xMB表示返回值或类型是BOOL类型;vMB表示是void类型等等。

        就说eMBRTUInit这个函数,其使用串口和定时器初始化的方式也很巧妙,用的if和else。

        定时器初始化函数xMBPortTimersInit( USHORT usTim1Timerout50us )的参数表示50us的倍数,定时器驱动初始化中把分频系数设为了5999,也就是说频率是20kHZ,那么周期就是50us,如果参数给2,则重装载值为2 - 1,那2*50 = 100us, 100us进入一次中断。为什么要这样设计呢?这和Modbus协议规定有关,因为它规定接收完一包数据之后需要有3.5T时间的空闲位(假设传输一个字符所需时间为T)。

      

        当波特率大于19200时,重装载值为35,35*50=1750us;当为其它波特率时则采用计算的方式,波特率表示每秒传输的二进制位数,波特率为9600那么传输一个bit位需要1000/9600ms,而传输一个字节包括起始位、数据位、停止位 共10bit,那么传输一个字符所需的时间就是10*1000/9600ms, 传输3.5个字符就是3.5*10*1000/9600,转换为50us的倍数就是3.5*10*1000*1000/9600/50us,为了避免浮点数的运算,写成7*10*1000*1000/2/9600/50,也就是

 7*10*20*1000/(2*9600)=7*200000/(2*9600),我们写的驱动代码中是没有奇偶校验位的,但是库代码中意思是有奇偶校验位,即传输一个字符是11bit,因为差别不大所以也就没有改。

        if( ulBaudRate > 19200 )
        {
            usTimerT35_50us = 35;       /* 1800us. */
        }
        else
        {
            /* The timer reload value for a character is given by:
             *
             * ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
             *             = 11 * Ticks_per_1s / Baudrate
             *             = 220000 / Baudrate
             * The reload for t3.5 is 1.5 times this value and similary
             * for t3.5.
             */
            usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
        }

 #define ENTER_CRITICAL_SECTION( )  //这个是进出临界的宏定义,没定义的话空着就可以了;
                                   //不同平台这个宏定义可以设计成不同的
#define EXIT_CRITICAL_SECTION( )  

【2.2】 ModbusSlaveInit(。。。)调用eMBEnable();

eMBErrorCode
eMBEnable( void )
{
    eMBErrorCode    eStatus = MB_ENOERR;

    if( eMBState == STATE_DISABLED )//之前初始化已经设置变量值了,所以if成立
    {
        /* Activate the protocol stack. */
        pvMBFrameStartCur(  );//调用这个函数指针变量。
        eMBState = STATE_ENABLED;
    }
    else
    {
        eStatus = MB_EILLSTATE;
    }
    return eStatus;
}

// pvMBFrameStartCur(  );//调用这个函数指针变量。

//实际就是调用执行这个函数

void
eMBRTUStart( void )
{
    ENTER_CRITICAL_SECTION(  );
    /* Initially the receiver is in the state STATE_RX_INIT. we start
     * the timer and if no character is received within t3.5 we change
     * to STATE_RX_IDLE. This makes sure that we delay startup of the
     * modbus protocol stack until the bus is free.
     */
    eRcvState = STATE_RX_INIT;
    vMBPortSerialEnable( TRUE, FALSE );//使能串口接收中断,注意是从机,也就是单片机接收
    vMBPortTimersEnable(  );//使能定时器更新中断、使能定时器

    EXIT_CRITICAL_SECTION(  );
}

 (3)while(1)内循环调用的函数说明

【3.1】 由于之前初始化已经使能了定时器跟新中断,时间肯定是<=1.75ms进入一次中断服务函数,所以单片机上电复位后的几乎瞬间就会执行定时器中断服务函数,理解这一点很重要,之后会用到。

【3.2】 单片机每隔1ms调用函数void ModbusTask(void), 而这个函数又会调用(void)eMBPoll();

void ModbusTask(void)
{
	/******************************************************************************************
    在 C 语言中,前面加上 (void) 的作用是显式地表明你不关心函数的返回值。具体来说:
	①避免警告:如果 eMBPoll() 函数有返回值但你没有使用这个返回值,某些编译器可能会发出警告,提示你忽略了返回值。
			   通过将其强制转换为 (void),可以告诉编译器你明确意识到这个返回值并决定不使用它,从而避免警告。

	②代码可读性:使用 (void) 可以提高代码的可读性,让其他开发者清楚地知道你故意忽略了这个返回值,而不是无意中遗漏。
    ******************************************************************************************/
	(void)eMBPoll();
}

【3.3】 函数eMBPoll();由于主机没有发送数据,所以到第一个case就会break出去。

eMBErrorCode
eMBPoll( void )
{
    static UCHAR   *ucMBFrame;//这里不用数组形式表示,因为长度是未知的
    static UCHAR    ucRcvAddress;
    static UCHAR    ucFunctionCode;
    static USHORT   usLength;
    static eMBException eException;

    int             i;
    eMBErrorCode    eStatus = MB_ENOERR;
    eMBEventType    eEvent;

    /* Check if the protocol stack is ready. */
    if( eMBState != STATE_ENABLED )//初始化已经ENABLE了,所以这个if不成立
    {
        return MB_EILLSTATE;
    }

    /* Check if there is a event available. If not return control to caller.
     * Otherwise we will handle the event. */
    if( xMBPortEventGet( &eEvent ) == TRUE )
    {
        switch ( eEvent )
        {
        case EV_READY://由于主机没有发送命令报文,所以到这个case就会跳出去
            break;

        case EV_FRAME_RECEIVED:
            eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
            if( eStatus == MB_ENOERR )
            {
                /* Check if the frame is for us. If not ignore the frame. */
                if( ( ucRcvAddress == ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
                {
                    ( void )xMBPortEventPost( EV_EXECUTE );
                }
            }
            break;

        case EV_EXECUTE:
            ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];
            eException = MB_EX_ILLEGAL_FUNCTION;
            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;
                }
            }

            /* If the request was not sent to the broadcast address we
             * return a reply. */
            if( ucRcvAddress != MB_ADDRESS_BROADCAST )
            {
                if( eException != MB_EX_NONE )
                {
                    /* An exception occured. Build an error frame. */
                    usLength = 0;
                    ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
                    ucMBFrame[usLength++] = eException;
                }
                if( ( eMBCurrentMode == MB_ASCII ) && MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS )
                {
                    vMBPortTimersDelay( MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS );
                }                
                eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );
            }
            break;

        case EV_FRAME_SENT:
            break;
        }
    }
    return MB_ENOERR;
}

2.2 主机发送报文:01 06 00 02 00 01

(1)01是从机地址,06是功能码{写单个控制项}, 0002是控制项的地址,0001是数据内容,表示LED亮,如果是0000就表示熄灭LED。

         由于初始化的时候是使能了定时器更新中断和定时器的,那么承接前言,单片机上电复位几乎一瞬间就会调用定时器中断服务函数。

void TIMER3_IRQHandler(void)
{
	if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP) == SET)
	{
		timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
		timer_counter_value_config(TIMER3, 0);
		prvvTIMERExpiredISR();
	}
}

中断服务函数里面会执行函数指针,也就是下面这个函数:

BOOL
xMBRTUTimerT35Expired( void )
{
    BOOL            xNeedPoll = FALSE;

    switch ( eRcvState )
    {
        /* Timer t35 expired. Startup phase is finished. */
    case STATE_RX_INIT:
        xNeedPoll = xMBPortEventPost( EV_READY );
        break;

        /* A frame was received and t35 expired. Notify the listener that
         * a new frame was received. */
    case STATE_RX_RCV:
        xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );
        break;

        /* An error occured while receiving the frame. */
    case STATE_RX_ERROR:
        break;

        /* Function called in an illegal state. */
    default:
        assert( ( eRcvState == STATE_RX_INIT ) ||
                ( eRcvState == STATE_RX_RCV ) || ( eRcvState == STATE_RX_ERROR ) );
    }

    vMBPortTimersDisable(  );
    eRcvState = STATE_RX_IDLE;

    return xNeedPoll;
}

          静态全局枚举类型变量eRcvState 未初始化默认为0,即STATE_RX_INIT,执行第一个case,然后退出switch,失能定时器中断和定时器,eRcvState赋值为STATE_RX_IDLE。

(1)由于初始化的时候,串口使能了接收中断,失能发送中断,所以每来一个字节,触发一次串口接收中断服务函数:

void USART1_IRQHandler(void)
{
    //判断接收还是发送中断
    if (RESET != usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_RBNE))
    {
        //接收完成中断
        prvvUARTRxISR();
        usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_RBNE_ORERR);
    }
    if(RESET != usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_TC))
    {
        //发送完成中断
        prvvUARTTxReadyISR();
        usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_TC);
    }
}

        函数prvvUARTRxISR();会调用函数指针变量pxMBFrameCBByteReceived();也就是执行这个函数:

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;
}

     /* Always read the character. */
    ( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );

        这条语句表示总是会在一开始就获取字节数据。

        由于一开始的定时器中断已经把eRcvState赋值为STATE_RX_IDLE,所以自然会跳动这个case, 将接受到的第一个字节保存到数组【0】里面,然后usRcvBufferPos++为1,注意变量++是先使用后++。 将eRcvState = STATE_RX_RCV; break之前再调用定时器使能函数,在使能函数里面不仅会使能定时器和定时器跟新中断,还会对计数器清零,这样保证在接收完主机发送的报文之前不会产生3.5T的空闲。

        之后再接收下一个字节,case会跳到STATE_RX_RCV,将接收到的字节保存到数组中,并且对定时器计数器清零,避免产生中断。

        就这样将数据全部接收完后,不会再产生串口接收中断,也就不会再对计数器清零,这样定时器计数器到了3.5T的时间就会产生更新中断,由于eRcvState已经被赋值为STATE_RX_RCV,就会执行这个case:

    case STATE_RX_RCV:
        xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );
        break;

跳出Switch后固定执行:

    vMBPortTimersDisable(  );
    eRcvState = STATE_RX_IDLE;


(2)这个时候在每ms执行一次的eMBPoll();函数中,就会执行下述代码,类似状态机,在这个case中,函数指针变量peMBFrameReceiveCur的作用是把从串口中端侧接收到的:代表从机地址的字节数据、存放接收到的数据的数组地址(以元素1开始为首地址)、字节数据减3后的数字(减去从机地址1字节,CRC校验码2字节)分别拷贝给ucRcvAddress、ucMBFrame、usLength。

        case EV_FRAME_RECEIVED:
            eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
            if( eStatus == MB_ENOERR )
            {
                /* Check if the frame is for us. If not ignore the frame. */
                if( ( ucRcvAddress == ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
                {
                    ( void )xMBPortEventPost( EV_EXECUTE );
                }
            }
            break;

         第二次执行的时间自然就会跳转到case EV_EXECUTE:

ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];由于从串口中断侧接收到的数组地址是从1开始的,那么用于保存数据的数组ucMBFrame的第0个元素实际上就是功能码。

        遍历到对应的功能码,就执行函数指针变量。

                else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
                {
                    eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
                    break;
                }

06功能码实际执行的函数就是这个:

eMBException
eMBFuncWriteHoldingRegister( UCHAR * pucFrame, USHORT * usLen )
{
    USHORT          usRegAddress;
    eMBException    eStatus = MB_EX_NONE;
    eMBErrorCode    eRegStatus;

    if( *usLen == ( MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN ) )
    {
        usRegAddress = ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8 );
        usRegAddress |= ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_ADDR_OFF + 1] );
        //usRegAddress++; modify

        /* Make callback to update the value. */
        eRegStatus = eMBRegHoldingCB( &pucFrame[MB_PDU_FUNC_WRITE_VALUE_OFF],
                                      usRegAddress, 1, MB_REG_WRITE );

        /* If an error occured convert it into a Modbus exception. */
        if( eRegStatus != MB_ENOERR )
        {
            eStatus = prveMBError2Exception( eRegStatus );
        }
    }
    else
    {
        /* Can't be a valid request because the length is incorrect. */
        eStatus = MB_EX_ILLEGAL_DATA_VALUE;
    }
    return eStatus;
}

主机发送的字节数据加上两个校验码字节刚好是8个字节,之前在拷贝长度的时候已经减去了3,即减去了1个从机地址码字节,2个校验码字节,所以:

if( *usLen == ( MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN ) )成立。

        //usRegAddress++; modify这里把这个注释掉了,其实modbus中规定主机发送的控制项地址应该比实际的地址少1,比如我们举的例子是发送:01 06 00 02 00 01,其中0002是控制项地址,按照规定我们应该发送01 06 00 01 00 01,然后代码用usRegAddress++;得到实际的地址,但是我们把库文件中的这条代码注释掉,我们发送的时候发送实际地址也是可以使用的,至于Modbus为啥有这种规定我也不知道,可能和PLC有关,毕竟是西门子公司发明的Modbus。

        然后调用这个需要我们自己实现的函数,MB_PDU_FUNC_WRITE_VALUE_OFF值是3,时刻记住当初从串口中断拷贝的数组地址是从1开始的,所示这里是3,实际就是从“寄存器个数高字节”地址起。usRegAddress是上几条语句获得的控制项地址字节(高位低位),1表示只有一个控制项(因为给的功能码是写单寄存器),MB_REG_WRITE表示是主机向从机发送报文数据。

        eRegStatus = eMBRegHoldingCB( &pucFrame[MB_PDU_FUNC_WRITE_VALUE_OFF],
                                      usRegAddress, 1, MB_REG_WRITE );

        后面的内容可已试着自己分析了, 不管是Freemodbus库文件中的代码还是自己写的逻辑代码,都使用到了大量的结构体和函数指针变量,需要有扎实的C语言基本功才能读懂。

2.3 其它

        其它的从机返回数据,主机发送写多寄存器报文,主机发送读多寄存器功能报文,都可以试着自己分析一下。

eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )

          值得一提的是,在函数eMBRTUSend中,手动加入了以下代码。 

		//函数xMBRTUTransmitFSM是在中断函数USART1_IRQHandler中调用的
		//TC上电默认为0进不了中断,手册436,图17-3
	    //插入代码 启动第一次发送,这样才可以进入发送完成中断,在中断中调用xMBRTUTransmitFSM函数   modify
		xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
		pucSndBufferCur++; 
		usSndBufferCount--;

         因为从机串口发送中断采用的是TC标志位,而不是是TXE标志位,有什么区别呢?

 

         假如串口发送中断采用TXE标志位会怎样。处理最后一个字节,发送数据寄存器中的数据空了,TXE标志位硬件置1,调用函数xMBRTUTransmitFSM(),xMBPortSerialPutByte()将数据从移位寄存器发送到数组中,退出中断。

        此时串口发送中断还是使能的,由于发送数据寄存器中已经没有数据了,自然TXE置位1,调用函数xMBRTUTransmitFSM(),执行下面的代码,失能定时器发送,使能定时器接收,而如果移位寄存器中最后一个字节的数据还没有全移动数组中,那么就会导致最后一个字节发送错误,由于最后一个字节是校验码,那么就会导致错误。所以采用TC标志位。

        else
        {
            xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
            /* Disable transmitter. This prevents another transmit buffer
             * empty interrupt. */
            vMBPortSerialEnable( TRUE, FALSE );
            eSndState = STATE_TX_IDLE;
        }

        那么采用TC标志位和我们在 eMBRTUSend函数中加的代码有什么关系呢?由于TC标志位是需要先发送数据,等TXE置1,移位寄存器空了之后才会置1。而发送数据的函数又是在发送中断中调用的,而没有TC标志位又根本不会触发发送中断。所以需要在eMBRTUSend,先手动发送一个字节数据,触发TC标志位,相当于“启动中断”的作用。

特别提示,有一个地方容易理解错:

那就是mbfuncholding.c中的函数eMBFuncReadHoldingRegister的这段代码:

            pucFrameCur = &pucFrame[MB_PDU_FUNC_OFF];
            *usLen = MB_PDU_FUNC_OFF;

            /* First byte contains the function code. */
            *pucFrameCur++ = MB_FUNC_READ_HOLDING_REGISTER;
            *usLen += 1;

            /* Second byte in the response contain the number of bytes. */
            *pucFrameCur++ = ( UCHAR ) ( usRegCount * 2 );
            *usLen += 1;

            /* Make callback to fill the buffer. */
            eRegStatus = eMBRegHoldingCB( pucFrameCur, usRegAddress, usRegCount, MB_REG_READ );
			//这个地方坑了我好久啊,我草还是代码看少了,我说咋一直不对
			//最后一个*pucFrameCur++之后,地址已经是指向“数据内容1高字节这个位置了”
			//也就是说传给函数eMBRegHoldingCB的参数pucFrameCur地址不再是最初的 pucFrameCur = &pucFrame[0];
			//而是&pucFrame[2]
			//这也就是为什么modbus_app.c中的函数ReadRegsCb
			//是吧获取到的内容字节数据保存在buf[2*i]而不是buf[2*(i+1)]中
            /* If an error occured convert it into a Modbus exception. */

3、总结

 

 

 4、演示视频

【Modbus协议实现一主多从】 https://www.bilibili.com/video/BV1cnwHeoEFY/?share_source=copy_web&vd_source=c9e86bee3ac4892e3670e748240943c6

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐