[MCU] [N32L43X评测] 6.USART实现ModbusRTU从站

805721366   2022-7-31 01:00 楼主

MODBUS协议是一种已广泛应用于当今工业控制领域的通用通讯协议。通过此协议,控制器相互之间、或控制器经由网络、总线可以和其它设备之间进行通信。Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。一般将主控设备方所使用的协议称为MODBUS Master,从设备方使用的协议称为MODBUS Slave。典型的主设备包括工控机和工业控制器等;典型的从设备如PLC可编程控制器等。MODBUS通讯物理接口可以选用串口(包括RS232、RS485和RS422),也可以选择以太网口

通信遵循以下的过程:

主设备向从设备发送请求

从设备分析并处理主设备的请求,然后向主设备发送结果

如果出现任何差错,从设备将返回一个异常功能码

MODBUS的工作方式是请求/应答,每次通讯都是主站先发送指令,可以是广播,或是向特定从站的单播;从站响应指令,并按要求应答,或者报告异常。当主站不发送请求时,从站不会自己发出数据,从站和从站之间不能直接通讯

MODBUS有三种通信方式:

以太网:对应的通信模式是MODBUS TCP/IP

异步串行传输(各种介质如有线RS-232-/422/485/;光纤、无线等):对应的通信模式是MODBUS RTU或MODBUS ASCII

高速令牌传递网络:对应的通信模式是MODBUS PLUS

MODBUS协议的报文(或帧)的基本格式是:表头 + 功能码 + 数据区 + 校验码

功能码和数据区在不同类型的网络都是固定不变的,表头和校验码则因网络底层的实现方式不同而有所区别。表头包含了从站的地址,功能码告诉从站要执行何种功能,数据区是具体的信息

对于不同类型的网络,MODBUS的协议层实现是一样的,区别在于下层的实现方式,常见的有TCP/IP和串行通讯两种

MODBUS TCP基于以太网和TCP/IP协议,MODBUS RTU和MODBUS ASCII则是使用异步串行传输(通常是RS-232/422/485)

在工业控制领域,工业仪表间的通信比较常用的是基于MODBUS RTU的485口通信,MODBUS RTU协议需要用时间间隔来判断一帧报文的开始和结束,协议规定的时间为3.5个字符周期,就是说一帧报文开始前,必须有大于3.5个字符周期的空闲时间,一帧报文结束后,也必须要有3.5个字符周期的空闲时间;同时一帧报文中,字符间空闲时间大于1.5字符周期

针对3.5个字符周期,其实是一个具体时间,但是这个时间跟波特率相关。在串口通信中,1个字符包括1位起始位、8位数据位(一般情况)、1位校验位(或者没有)、1位停止位(一般情况下),因此1个字符包括11个位,那么3.5个字符就是38.5个位,波特率表示的含义是每秒传输的二进制位的个位,在波特率为9600的情况下,3.5个字符周期=1000ms/9600bit*38.5bit=4.0104167ms

MODBUS RTU详解可参考:

MODBUS APPLICATION PROTOCOL SPECIFICATION V1.1b3.pdf (1.67 MB)
(下载次数: 4, 2022-7-30 23:42 上传)

Modbus 协议.pdf (3.27 MB)
(下载次数: 5, 2022-7-30 23:42 上传)

此篇主要介绍USART1实现MODBUS RTU从站功能

硬件连接

GND ——  GND

TXD ——  PA10(RX)

RXD ——  PA9(TX)

微信图片_20220730235256.jpg

软件代码

USART代码:

void USART_Initial(void)
{
    NVIC_InitType NVIC_InitStructure;
    GPIO_InitType GPIO_InitStructure;
    USART_InitType USART_InitStructure;
    USART_ClockInitType  USART_ClockStructure;

    /* Enable GPIO clock */
    RCC_EnableAPB2PeriphClk(USART_GPIO_CLK, ENABLE);
    /* Enable USART Clock */
    RCC_EnableAPB2PeriphClk(USART_CLK, ENABLE);

    /* Configure the GPIO ports */
    /* Initialize GPIO_InitStructure */
    GPIO_InitStruct(&GPIO_InitStructure);
    /* Configure USART Tx as alternate function push-pull */
    GPIO_InitStructure.Pin            = USART_TxPin;
    GPIO_InitStructure.GPIO_Mode      = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Alternate = USART_Tx_GPIO_AF;
    GPIO_InitPeripheral(USART_GPIO, &GPIO_InitStructure);
    /* Configure USART Rx as alternate function push-pull and pull-up */
    GPIO_InitStructure.Pin            = USART_RxPin;
    GPIO_InitStructure.GPIO_Mode      = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Pull      = GPIO_Pull_Up;
    GPIO_InitStructure.GPIO_Alternate = USART_Rx_GPIO_AF;
    GPIO_InitPeripheral(USART_GPIO, &GPIO_InitStructure);

    /* USART configuration ------------------------------------------------------*/
    USART_StructInit(&USART_InitStructure);
    USART_InitStructure.BaudRate            = System.Com_Baud.UWD;//115200;
    USART_InitStructure.WordLength          = USART_WL_8B;
    USART_InitStructure.StopBits            = USART_STPB_1;
    USART_InitStructure.Parity              = USART_PE_NO;
    USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE;
    USART_InitStructure.Mode                = USART_MODE_RX | USART_MODE_TX;
    /* Configure USART */
    USART_Init(USART, &USART_InitStructure);

    USART_ClockStructure.Clock = USART_CLK_DISABLE;
    USART_ClockStructure.Polarity = USART_CLKPOL_LOW;
    USART_ClockStructure.Phase = USART_CLKPHA_2EDGE;
    USART_ClockStructure.LastBit = USART_CLKLB_DISABLE;
    USART_ClockInit(USART, &USART_ClockStructure);

    USART_ClrFlag(USART, USART_FLAG_TXDE);
    USART_ClrFlag(USART, USART_FLAG_RXDNE);
    /* Enable USART Receive interrupts */
    USART_ConfigInt(USART, USART_INT_TXDE, DISABLE);
    USART_ConfigInt(USART, USART_INT_RXDNE, ENABLE);

    /* NVIC configuration */
    /* Configure the NVIC Preemption Priority Bits */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
    /* Enable the USART Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel            = USART_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd         = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    UART_STR.Send_Flag = 0;
    UART_STR.Read_Flag = 0;
    UART_STR.Send_Dly = 0;
    UART_STR.In_Num = 0;
    UART_STR.Out_Num = 0;
    UART_STR.Start = 0;
    UART_STR.Read_Dly = 0;
    UART_STR.Read_All = 0;

    /* Enable the USART */
    USART_Enable(USART, ENABLE);
}

MODBUS RTU从站代码:

void Serial_Set_Time(void)
{
    switch(System.Com_Baud.UWD)
    {
    case 2400:
        System.Delay_Time = 6;
        System.Stop_Time = 7;
        break;

    case 4800:
        System.Delay_Time = 3;
        System.Stop_Time = 7;
        break;

    case 9600:
        System.Delay_Time = 2;
        System.Stop_Time = 4;
        break;

    case 19200:
    case 38400:
        System.Delay_Time = 1;
        System.Stop_Time = 2;
        break;

    default:
        System.Delay_Time = 1;
        System.Stop_Time = 2;
        break;
    }
}


//发送接收延时,每毫秒运算一次,放在1ms定时器中断函数中
void USART_SendRec_Dly(void)
{
    if(UART_STR.Send_Flag)
    {
        if(UART_STR.Send_Dly == 0)
        {
            UART_STR.Send_Flag = 0;
            USART_ConfigInt(USART, USART_INT_TXDE, ENABLE);
            USART_ConfigInt(USART, USART_INT_RXDNE, DISABLE);
            USART1->STS |= USART_INT_TXDE;
        }
        else
        {
            UART_STR.Send_Dly--;
        }
    }

    if(UART_STR.Read_Dly    >=  System.Delay_Time) //2400:Read_Dly>=6;4800:Read_Dly>=3;9600:Read_Dly>=2;19200:Read_Dly>=1
    {
        UART_STR.Read_Flag = 1;

        if(UART_STR.Start   >   System.Stop_Time) //2400:Read_Start>14;4800:Read_Start>7,9600:Read_Start>4,19200:Read_Start>2
        {
            UART_STR.Read_All = 1;
        }
    }
    else
    {
        UART_STR.Read_Dly++;
        UART_STR.Read_Flag = 0;
    }
}


void USART_Send_Ready(void)
{
    UART_STR.Send_Flag = 1;
    UART_STR.Send_Dly = 1;
    UART_STR.In_Num = 0;
    UART_STR.Start = 0;
    UART_STR.Read_All = 0;
    UART_STR.SendBuf = UART_STR.OutBuf;
}


/*-------------------分割线-------------------*/
//Function name:    GetCRC16
//Descriptions:     计算CRC16校验码
//input parameters: 指针地址:puchMsg,数据长度:usDataLen!!!注意usDataLen>2!!!
//output parameters:输出CRC16
//Returned value:   无
//MARK:             无
/*-------------------分割线-------------------*/
unsigned short int GetCRC16(unsigned char *puchMsg, unsigned short int usDataLen)
{
    unsigned char uchCRCHi = 0xFF; /* 高CRC字节初始化 */
    unsigned char uchCRCLo = 0xFF; /* 低CRC 字节初始化 */
    unsigned int uIndex = 0; /* CRC循环中的索引 */

    while (usDataLen--) /* 传输消息缓冲区 */
    {
        uIndex = uchCRCHi ^ *puchMsg++; /* 计算CRC */
        uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex];
        uchCRCLo = auchCRCLo[uIndex];
    }

    return (unsigned short int)((unsigned short int)uchCRCHi << 8 | uchCRCLo);
}


//0x06、0x10写寄存器命令,控制板载LED灯亮灭测试
void Function_Write_Address(unsigned short int Addr, unsigned short int Temp)
{
    UNION_WD_2BY Temp_Data;
    Temp_Data.HUWD = Temp;

    switch(Addr)
    {
    case 0x00:
        if(Temp_Data.HUWD)
        {
            GPIO_SetBits(LED1_PORT, LED1_PIN);
        }
        else
        {
            GPIO_ResetBits(LED1_PORT, LED1_PIN);
        }

        break;

    case 0x01:
        if(Temp_Data.HUWD)
        {
            GPIO_SetBits(LED2_PORT, LED2_PIN);
        }
        else
        {
            GPIO_ResetBits(LED2_PORT, LED2_PIN);
        }

        break;

    case 0x02:
        if(Temp_Data.HUWD)
        {
            GPIO_SetBits(LED3_PORT, LED3_PIN);
        }
        else
        {
            GPIO_ResetBits(LED3_PORT, LED3_PIN);
        }

        break;

    default:
        break;
    }
}


/*-------------------分割线-------------------*/
//Function name:    ModBus_Process
//Descriptions:     ModBus_处理函数
//input parameters: 无
//output parameters:
//Returned value:   无
//MARK:             无
/*-------------------分割线-------------------*/
unsigned int USART_Deal(unsigned char *inbuff, unsigned short int Incount, unsigned char *outbuff)
{
    static union
    {
        short int IWD;
        unsigned short int UWD;
        unsigned char UBY[2];
    } SSADR, SSDAT, SSEND;
    static unsigned char CommFun;
    static unsigned short int Temp_OutNum;
    static unsigned char *sbuff;
    static unsigned short int Cal_CRC16, Tem_CRC16;
    static unsigned short int Temp_Cnt;
    UNION_WD_2BY DO_Temp, Temp_Data;
    unsigned char i;

    CommFun = *(inbuff + 1);
    SSADR.UBY[INTER_B_H] = *(inbuff + 2);
    SSADR.UBY[INTER_B_L] = *(inbuff + 3);
    SSDAT.UBY[INTER_B_H] = *(inbuff + 4);
    SSDAT.UBY[INTER_B_L] = *(inbuff + 5);
    SSEND.UWD = SSADR.UWD + SSDAT.UWD;
    Temp_OutNum = 0;

    if(Incount > 2)
    {
        Cal_CRC16 = GetCRC16(UART_STR.InBuf, Incount - 2);
    }

    Tem_CRC16 = *(inbuff + Incount - 2);
    Tem_CRC16 <<= 8;
    Tem_CRC16 += *(inbuff + Incount - 1);

    if(Cal_CRC16 == Tem_CRC16)
    {
        switch(CommFun)
        {
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
        case 6:
        case 8:
            if( Incount == 8 ) break;
            else return 0;

        case 7:
        case 11:
        case 12:
        case 17:
            if( Incount == 12 ) break;
            else return 0;

        case 15:
        case 16:
            if( Incount == (*(inbuff + 6) + 9) ) break;
            else return 0;

        case 20:
        case 21:
            if( (Incount == (*(inbuff + 2) + 5)) && (*(inbuff + 3) == 6) ) break;
            else return 0;

        case 22:
            if( Incount == 10 ) break;
            else return 0;

        case 23:
            if( Incount == (*(inbuff + 10) + 13) ) break;
            else return 0;

        case 24:
            if( Incount == 6 ) break;
            else return 0;

        case 43:
            if( Incount == 7 ) break;
            else return 0;

        default:
            if( CommFun < 0x80 ) break;
            else return 0;
        }
    }
    else    return 0;

    sbuff = outbuff;
    *sbuff++ = System.Com_Addr.UBY[0]; //通讯地址
    *sbuff = CommFun;

    if(CommFun == 3 || CommFun == 4) //0x03、0x04命令读寄存器
    {
        if(SSDAT.UWD == 0 || SSDAT.UWD > 0X007D) //查询最大数据不能超过125,异常码0x03
        {
            (*sbuff) += 0x80;
            *(++sbuff) = 0x03;
            Temp_OutNum = 3;
        }
        else if( SSADR.UWD >= 250 || (SSEND.UWD >= 251) ) //读取范围设定值,异常码0x02
        {
            (*sbuff) += 0x80;
            *(++sbuff) = 0x02;
            Temp_OutNum = 3;
        }
        else
        {
            *(++sbuff) = SSDAT.UWD * 2;
            sbuff = outbuff + 3;

            for(Temp_Cnt = SSADR.UWD; Temp_Cnt < SSEND.UWD; Temp_Cnt++) //100~240
            {
                if(Temp_Cnt >= 0x00 && Temp_Cnt < 0x31)
                {
                    switch(Temp_Cnt)
                    {
                    //LED1状态显示
                    case 0x00:
                        LED1_State = GPIO_ReadOutputDataBit(LED1_PORT, LED1_PIN);
                        *sbuff++ = 0x00;
                        *sbuff++ = LED1_State;
                        break;

                    //LED2状态显示
                    case 0x01:
                        LED2_State = GPIO_ReadOutputDataBit(LED2_PORT, LED2_PIN);
                        *sbuff++ = 0x00;
                        *sbuff++ = LED2_State;
                        break;

                    //LED3状态显示
                    case 0x02:
                        LED3_State = GPIO_ReadOutputDataBit(LED3_PORT, LED3_PIN);
                        *sbuff++ = 0x00;
                        *sbuff++ = LED3_State;
                        break;

                    //KEY1状态显示
                    case 0x03:
                        KEY1_State = GPIO_ReadInputDataBit(GPIOA, GPIO_PIN_4);
                        *sbuff++ = 0x00;
                        *sbuff++ = KEY1_State;
                        break;

                    //KEY2状态显示
                    case 0X04:
                        KEY2_State = GPIO_ReadInputDataBit(GPIOA, GPIO_PIN_5);
                        *sbuff++ = 0x00;
                        *sbuff++ = KEY2_State;
                        break;

                    //KEY3状态显示
                    case 0X05:
                        KEY3_State = GPIO_ReadInputDataBit(GPIOA, GPIO_PIN_6);
                        *sbuff++ = 0x00;
                        *sbuff++ = KEY3_State;
                        break;

                    //...

                    default:
                        *sbuff++ =  0;
                        *sbuff++ = 0;
                        break;
                    }
                }
                else
                {
                    *sbuff++ =  0;
                    *sbuff++ = 0;
                }
            }

            Temp_OutNum = *(outbuff + 2) + 3;
        }
    }//03读寄存器命令结束

    //06写单个寄存器命令
    else if(CommFun == 6)
    {
//				if(SSDAT.UWD >= 0x00 && SSDAT.UWD <= 0xFFFF)
//        {
//            (*sbuff) += 0x80;
//            *(++sbuff) = 0x03;
//            Temp_OutNum = 3;
//        }
//        else
        {
            if((SSADR.UWD >= 0x03 && SSADR.UWD <= 0X05) || SSADR.UWD >= 250 )//0x06写入命令地址设定
            {
                (*sbuff) += 0x80;
                *(++sbuff) = 0x02;
                Temp_OutNum = 3;
            }
            else
            {
                Function_Write_Address(SSADR.UWD, SSDAT.UWD);

                sbuff = outbuff + 2;
                *sbuff++ = SSADR.UBY[INTER_B_H];
                *sbuff++ = SSADR.UBY[INTER_B_L];
                *sbuff++ = SSDAT.UBY[INTER_B_H];
                *sbuff++ = SSDAT.UBY[INTER_B_L];
                Temp_OutNum = 6;
            }
        }
    }
    //写多个寄存器命令
    else if(CommFun == 16)
    {
        if(SSDAT.UWD == 0 || SSDAT.UWD > 0x7b || SSDAT.UWD * 2 != *(inbuff + 6) )
        {
            (*sbuff) += 0x80;
            *(++sbuff) = 0x03;
            Temp_OutNum = 3;
        }
        else if( SSADR.UWD >= 250 || (SSEND.UWD >= 251)) //约束写入地址
        {
            (*sbuff) += 0x80;
            *(++sbuff) = 0x02;
            Temp_OutNum = 3;
        }
        else
        {
            if(*(inbuff + 6) > 0x07) //判断寄存器数量
            {
                (*sbuff) += 0x80;
                *(++sbuff) = 0x03;
                Temp_OutNum = 3;
            }
            else
            {
                ModBus_CodeAddress.UBY[1] = *(inbuff + 2);
                ModBus_CodeAddress.UBY[0] = *(inbuff + 3); //根据通讯地址来写入数据
                ModBus_DataLength.UBY[1] = *(inbuff + 4);
                ModBus_DataLength.UBY[0] = *(inbuff + 5);
                ModBus_DataEnd = ModBus_CodeAddress.UWD + ModBus_DataLength.UWD;

                for(i = 7, ModBus_Temp = ModBus_CodeAddress.UWD; ModBus_Temp < ModBus_DataEnd; ModBus_Temp++, i = i + 2)                    //100~240
                {
                    Temp_Data.UBY[1] = *(inbuff + i);
                    Temp_Data.UBY[0] = *(inbuff + i + 1);
                    Function_Write_Address(ModBus_Temp, Temp_Data.HUWD); //写入的地址及数据(对应0X06命令)
                }

                sbuff = outbuff + 2;
                *sbuff++ = SSADR.UBY[INTER_B_H];
                *sbuff++ = SSADR.UBY[INTER_B_L];
                *sbuff++ = SSDAT.UBY[INTER_B_H];
                *sbuff++ = SSDAT.UBY[INTER_B_L];
                Temp_OutNum = 6;
            }
        }
    }

    else
    {
        (*sbuff) += 0x80;
        *(++sbuff) = 0x01;
        Temp_OutNum = 3;
    }

    sbuff = outbuff;

    if(Temp_OutNum > 2)
        Cal_CRC16 = GetCRC16(sbuff, Temp_OutNum);

    sbuff = outbuff + Temp_OutNum;
    *(sbuff++) = (Cal_CRC16 >> 8);
    *(sbuff++) = Cal_CRC16;
    Temp_OutNum += 2;
    return Temp_OutNum;

}


/*
 * [url=home.php?mod=space&uid=159083]@brief[/url] This function handles USART global interrupt request.
 */
void USART1_IRQHandler(void)
{
    unsigned char TempData;

    if (USART_GetIntStatus(USART, USART_INT_RXDNE) != RESET)
    {
        /* Read one byte from the receive data register */
        TempData = USART_ReceiveData(USART);
        USART_ClrIntPendingBit(USART, USART_INT_RXDNE);

        if(UART_STR.In_Num == 0)
        {
            if(TempData == System.Com_Addr.UBY[0] && UART_STR.Read_All == 0 && UART_STR.Read_Flag == 1) //接受第一个字符时T3.5有没有到 没有到当前字符取消
            {
                UART_STR.InBuf[UART_STR.In_Num++] = TempData;
                UART_STR.Start = 1;
            }
        }
        else if(UART_STR.Read_Flag == 0) //接受其他字符时候没有超过T1.5的延时
        {
            if(UART_STR.In_Num >= 260)
            {
                UART_STR.In_Num = 0;    //接受长度不对  重新接受
                UART_STR.Start = 0;
            }
            else
            {
                UART_STR.Start++;     //等待3.5个字符的延时表示接受结束
                UART_STR.InBuf[UART_STR.In_Num++] = TempData;
            }
        }
        else
        {
            UART_STR.In_Num = 0;
        }

        UART_STR.Read_Dly = 0;
        UART_STR.Read_Flag = 0; //防止在帧前格外加的字符,如果是第1个字符不是地址重新等待1.5个字符
    }

    else if (USART_GetIntStatus(USART, USART_INT_TXDE) != RESET)
    {
        if(UART_STR.Out_Num != 0)
        {
            UART_STR.Out_Num--;
            USART_SendData(USART, *UART_STR.SendBuf);
            UART_STR.SendBuf++;
        }
        else
        {
            USART_ConfigInt(USART, USART_INT_TXDE, DISABLE);
        }

        USART_ClrIntPendingBit(USART, USART_INT_TXDE); //Clear the Uart_Hard transmit interrupt

        if(UART_STR.Out_Num <= 1)
            USART_ConfigInt(USART, USART_INT_RXDNE, ENABLE);
    }
    else
    {
        USART_ClrFlag(USART, USART_FLAG_OREF | USART_FLAG_NEF | USART_FLAG_FEF | USART_FLAG_PEF); //注意,清除出错标志
    }

}

运行测试

从站地址设为0x01,波特率设为115200,初始设置板载LED1、LED3亮,LED2灭,KEY1、KEY2、KEY3按键松开(GPIO设置为上拉输入,按下时为0低电平,松开时为1高电平)

0x03读寄存器命令

使用modscan32测试,设置从站地址、0x03读命令

微信图片_20220731002041.png

点击连接设置-->连接,选择调试串口,设置串口参数,点击确认

微信图片_20220731002426.png

微信图片_20220731002628.png

连接成功后,显示LED1-3,KEY1-3当前状态

微信图片_20220731003049.png

微信图片_20220731003731.jpg

0x06写寄存器命令

双击更新设置LED1、LED2、LED3寄存器值,LED1、LED3灭,LED2亮

微信图片_20220731004023.png

微信图片_20220731004140.png

微信图片_20220731004144.png

更新后,显示

微信图片_20220731004223.png

微信图片_20220731004347.jpg

测试代码

N32L43x_USART_MODBUS-RTU_Slave.rar (483.49 KB)
(下载次数: 10, 2022-7-31 00:50 上传)

modscan32软件

Modscan32.rar (1.72 MB)
(下载次数: 8, 2022-7-31 00:54 上传)

回复评论 (1)

感谢分享, modbus写得非常好,学习了。
点赞  2022-7-31 11:11
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复