发新帖本帖赏金 100.00元(功能说明)我要提问
返回列表

[MM32生态] MM32F3270通讯板之通过RS485接口实现Modbus协议传输

[复制链接]
249|9
手机看帖
扫描二维码
随时随地手机跟帖
xld0932|  楼主 | 2022-5-7 16:57 | 显示全部楼层 |阅读模式
本帖最后由 xld0932 于 2022-5-7 17:03 编辑

#申请原创#   @21小跑堂

RS485接口是我们常用的通讯接口之一,被广泛应用在工业自动化、过程控制、楼宇自动化、视频监控、销售终端等众多领域。在通讯时的逻辑低电平电压在+1.5V~+6.0V之间,逻辑高电平在-6.0V~+1.5V之间,这种平衡差分电平数据通讯方式极大的降低了噪声,增强了抗干扰性。同时为了通讯达到更好的效果,常使用屏蔽或非屏蔽的双绞线来作为通讯线缆。根据通讯线缆的长度,通常其通讯数据速率可达到100kbps至10Mbps之间,还可以选择增强型的IC驱动器,可以将通讯速率提升到35Mbps及以上,但最终还是建议速度(bps)乘以通讯线缆长度(m)不应该超过10的8次方。

RS485是一种半双工通讯,在同一时刻收发操作不能同时进行,只能进行发送或者接收数据。所以一般在RS485的收发控制驱动芯片上有DE/RE这两个控制端口引脚来控制当前是处于发送还是接收状态。因为是半双工通讯,在多机通讯的时候就需要有相应的通讯协议来进行约束,否则大家都同时向总线上发送数据就会乱套了;而Modbus协议就是基于RS485或者RS232接口被应用最多的通讯协议之一。

Modbus协议是一个主/从(Master/Slave)或客户端/服务器(Client/Server)架构的协议,以请求—应答的方式进行数据传输;在一个Modbus通讯组网中,最多可挂载247台通讯节点设备,但有且仅有一台主机(服务器)节点设备;根据传输数据的字节在表示上的不同,可以将其分为ASCII模式和RTU模式这两种,简单来理解就是ASCII模式传输的是ASCII码可见字符,而RTU模式传输的则是十六进制数据。

Modbus协议的数据帧结构根据其模式来区分,格式约定有所不同。在ASCII模式中,以冒号(0x3A)表示数据帧的开始,以回车换行(0x0D  0x0A)来表示数据帧的结束;对于其它字段,允许发送的字符为ASCII码字符0~9和A~F。字符之间的最大间隔时间为1秒,如果大于1秒则会接收设备出现了错误。而在RTU模式中,数据帧的开始和结束则是需要至少达到3.5个字节的通讯时长,而字节的通讯时长则依据通讯的波特率来决定。具体的数据帧结构定义如下表所示:
ASCII模式数据帧结构:
开始
地址码
功能码
数据
LRC校验
结束
1 CHAR
2 CHAR
2 CHAR
N CHAR
2 CHAR
2 CHAR
:
CR LF
RTU模式数据帧结构:
开始
地址码
功能码
数据
CRC校验
结束
T1-T2-T3-T4
1 BYTE
1 BYTE
N BYTE
2 BYTE
T1-T2-T3-T4

地址码由2个字符(ASCII模式)或1个字节(RTU模式)构成,有效的从机(客户端)地址范围为[0, 247],其中0用于广播地址,所有的从机(客户端)节点设备均能收到。

功能码由2个字符(ASCII模式)或1个字节(RTU模式)构成,数据有效值在1~255。在标准的Modbus通讯协议中,只规定了为数不多的公共功能码定义,当然也可以根据需要用户自定义除公共功能码之外的用户自定义功能码。

校验码根据传输模式的不同,在ASCII模式中使用纵向冗余校验(LRC),计算的字段中不包括开始的冒号和结束的回车换行这些字符;而在RTU模式中则是使用循环冗余校验,计算的内容包括校验字段之前的所有数据内容。

注意1:在RTU模式时,一帧数据包中必须以连续的数据流发送整个数据包,如果两个字节之间的间隔大于1.5个字节时长,则会认为数据包不完整,从而将该数据包丢弃。为了实现RTU通讯中的时间间隔管理,一般有两种方式:一是UART自带的空闲中断功能,另外一个就是通过定时器来实现计时;在高速通讯波特率下,使用定时器将给MCU带来沉重的负担,所以协议就另外规定了,当通讯波特率小于等于19200bps时,需要严格按照间隔时间来进行数据传输,当通讯波特率大于19200bps时,则采用固定的1.5字节时长750us,数据包间隔时间为1750us。

注意2:在使用Modbus协议进行通讯时,字节序和大小端是一个非常容易忽视而又容易出错的问题。对于一个2字节的16位整数,在内存中存储有两种方式:一种是将低8位数据存放在起始地址,这种称为小端模式(LITTLE-ENDIAN);另外一种就是将高8位数据存放在起始地址,这种称为大端模式(BIG-ENDIAN),所以在开发之前需要先搞清楚基于MCU系统的大小端模式和字节序。

公共功能码
功能码
名称
寄存器PLC地址
位/字操作
操作数量
01
读线圈状态
00001~09999
位操作
单个或多个
02
读离散输入状态
10001~19999
位操作
单个或多个
03
读保持寄存器
40001~49999
字操作
单个或多个
04
读输入寄存器
30001~39999
字操作
单个或多个
05
写单个线圈
00001~09999
位操作
单个
06
写单个保持寄存器
40001~49999
字操作
单个
15
写多个线圈
00001~09999
位操作
多个
16
写多个保持寄存器
40001~49999
字操作
多个
功能码可以分为位操作和字操作两类,位操作的最小单位是一位,字操作的最小单位是两个字节。具体的功能码的操作说明可以参考Modbus协议或者是《Modbus软件开发实战指南》第4章节的Modbus功能码详解。

Modbus协议是一个在工业制造领域中得到广泛应用的一个通讯协议,几乎被所有的设备所支持;对于Modbus软件开发来说,可以对照Modbus协议自己编写主/从机代码和应用功能,当然也可以直接使用开源的代码库,其中libmodbus(http://www.libmodbus.org)和FreeMODBUS(http://www.freemodbus.org)这两个开源代码库算是比较常用的了,也值得开发者去认真的分析和学习。所以下面本文会基于freemodbus的移植过程和示例演示进行分享。

将FreeMODBUS源码添加到工程当中,详细参考附件中的软件工程源代码;我们在Port中的demo.c中实现FreeMODBUS的初始化、运行调度和应用实现和测试。

FreeMODBUS的初始化
在demo.c文件中的eMBDemoInit函数中,通过调试eMBInit函数对FreeMODBUS进行初始化,调用eMBInit函数的同时,对Modbus的传输模式、从机地址、以及串口通讯的参数进行设定;初始化完成后会返回初始化结果,如果初始化没有错误的情况下,我们将再调用eMBEnable函数来使能FreeMODBUS功能。在初始化过程当中,我们需要对底层的UART串口进行初始化配置、在通讯时需要对帧间隔时间进行管理,所以需要使用到一个定时器,也要对其进行初始化配置,参考代码如下所示:
int eMBDemoInit(void)
{
    eMBErrorCode eStatus;

    eStatus = eMBInit(MB_RTU, 0x0A, 0, 115200, MB_PAR_NONE);

    /* Enable the Modbus Protocol Stack. */
    if(eStatus == MB_ENOERR)
    {
        eStatus = eMBEnable();
    }

    return eStatus;
}

底层串口移植:
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{
    if(xRxEnable) UART_ITConfig(UART4, UART_IT_RXIEN, ENABLE);
    else          UART_ITConfig(UART4, UART_IT_RXIEN, DISABLE);

    if(xTxEnable) UART_ITConfig(UART4, UART_IT_TXIEN, ENABLE);
    else          UART_ITConfig(UART4, UART_IT_TXIEN, DISABLE);
}

BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    UART_InitTypeDef UART_InitStructure;

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOC,   ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1ENR_UART4, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = UART4_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    UART_StructInit(&UART_InitStructure);
    UART_InitStructure.UART_BaudRate = ulBaudRate;

    switch(ucDataBits)
    {
        case 5 : UART_InitStructure.UART_WordLength = UART_WordLength_5b; break;
        case 6 : UART_InitStructure.UART_WordLength = UART_WordLength_6b; break;
        case 7 : UART_InitStructure.UART_WordLength = UART_WordLength_7b; break;
        case 8 : UART_InitStructure.UART_WordLength = UART_WordLength_8b; break;
        default: UART_InitStructure.UART_WordLength = UART_WordLength_8b; break;
    }

    UART_InitStructure.UART_StopBits = UART_StopBits_1;

    switch(eParity)
    {
        case MB_PAR_NONE: UART_InitStructure.UART_Parity = UART_Parity_No;   break;
        case MB_PAR_ODD : UART_InitStructure.UART_Parity = UART_Parity_Odd;  break;
        case MB_PAR_EVEN: UART_InitStructure.UART_Parity = UART_Parity_Even; break;
        default         : UART_InitStructure.UART_Parity = UART_Parity_No;   break;
    }

    UART_InitStructure.UART_HardwareFlowControl = UART_HardwareFlowControl_None;
    UART_InitStructure.UART_Mode = UART_Mode_Rx | UART_Mode_Tx;
    UART_Init(UART4, &UART_InitStructure);

    UART_Cmd(UART4, ENABLE);

    GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_8);
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_8);

    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    return TRUE;
}

BOOL xMBPortSerialPutByte(CHAR ucByte)
{
    UART_SendData(UART4, ucByte);

    return TRUE;
}

BOOL xMBPortSerialGetByte(CHAR * pucByte)
{
    *pucByte = UART_ReceiveData(UART4);

    return TRUE;
}

static void prvvUARTTxReadyISR(void)
{
    pxMBFrameCBTransmitterEmpty();
}

static void prvvUARTRxISR(void)
{
    pxMBFrameCBByteReceived();
}

void UART4_IRQHandler(void)
{
    if(UART_GetITStatus(UART4, UART_IT_RXIEN) != RESET)
    {
        prvvUARTRxISR();

        UART_ClearITPendingBit(UART4, UART_IT_RXIEN);
    }

    if(UART_GetITStatus(UART4, UART_IT_TXIEN) != RESET)
    {
        prvvUARTTxReadyISR();

        UART_ClearITPendingBit(UART4, UART_IT_TXIEN);
    }
}

底层定时器移植:
BOOL xMBPortTimersInit(USHORT usTim1Timerout50us)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    NVIC_InitTypeDef        NVIC_InitStructure;

    RCC_ClocksTypeDef  RCC_Clocks;
    RCC_GetClocksFreq(&RCC_Clocks);

    RCC_APB1PeriphClockCmd(RCC_APB1ENR_TIM2, ENABLE);

    TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure);
    TIM_TimeBaseInitStructure.TIM_Prescaler = (RCC_Clocks.PCLK1_Frequency / 1000000 - 1);
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 500 - 1;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_ClearFlag(TIM2, TIM_FLAG_Update);
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    return TRUE;
}

inline void vMBPortTimersEnable(void)
{
    TIM_SetCounter(TIM2, 0);
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    TIM_Cmd(TIM2, ENABLE);
}

inline void vMBPortTimersDisable(void)
{
    TIM_Cmd(TIM2, DISABLE);
    TIM_SetCounter(TIM2, 0);
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

}

static void prvvTIMERExpiredISR(void)
{
    (void)pxMBPortCBTimerExpired();
}

void TIM2_IRQHandler(void)
{
    prvvTIMERExpiredISR();
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}


FreeMODBUS运行调度
在demo.c文件中的eMBPollHandler函数中,我们通讯调用eMBPoll函数来实现FreeMODBUS的运行调度功能,我们会先判断当前FreeMODBUS是否被使能,如果没有被使能,会直接返回状态;若FreeMODBUS被使能,则会根据当前系统的事件来做不同的响应和处理。当为EV_RDY事件时,表示FreeMODBUS进入侦听状态,则什么都不需要做;当为EV_FRAME_RECEIVED事件时,表示接收到了完整的数据帧,然后会接收当前的数据帧内容,再根据数据帧中的地址码与自身地址或者是广播地址进行比较,待地址码匹配上后将会发送EV_EXECUTE事件,等待下一次去执行;当为EV_EXECUTE事件时,通过查询功能函数列表的方式,检查是否有与之功能相对应的可执行函数,并返回相应的状态,并对广播地址作特殊的处理。参考代码如下所示:
void eMBPollHandler(void)
{
    (void)eMBPoll();
}

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 )
    {
        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:
            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;
}


FreeMODBUS示例演示
为了方便增加新的Modbus功能,FreeMODBUS在应用层提供了Hooks处理方式。对于指定功能代码,只需要通过实现这个功能的回调函数即可,可以参考demo.c中的最后几个函数;也可以通过eMBRegisterCB函数来进行注册,将功能码跟与之相匹配的功能实现函数绑定在一起,待产生EV_EXECUTE事件后来调用处理。在demo.c文件中我们定义了输入寄存器和保持寄存器,对寄存器的起始地址和寄存器数量进行了宏定义,最后对功能调用函数进行实现,具体的参考代码如下所示:
#define REG_INPUT_START     10001
#define REG_INPUT_NREGS     4

static USHORT usRegInputBuffer[REG_INPUT_NREGS] =
{
    0x0123, 0x4567, 0x89AB, 0xCDEF,
};

#define REG_HOLDING_START   40001
#define REG_HOLDING_NREGS   16

static USHORT usRegHoldingBuffer[REG_HOLDING_NREGS] =
{
    0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777,
    0x8888, 0x9999, 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD, 0xEEEE, 0xFFFF,
};

eMBErrorCode eMBRegInputCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs)
{
    eMBErrorCode eStatus = MB_ENOERR;
    int iRegIndex;

    usAddress -= 1;

    if((usAddress           >= REG_INPUT_START) &&
       (usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS))
    {
        iRegIndex = (int)(usAddress - REG_INPUT_START);

        while(usNRegs-- > 0)
        {
            *pucRegBuffer++ = (unsigned char)(usRegInputBuffer[iRegIndex] >> 0x08);
            *pucRegBuffer++ = (unsigned char)(usRegInputBuffer[iRegIndex]  & 0xFF);
            iRegIndex++;
        }
    }
    else
    {
        eStatus = MB_ENOREG;
    }

    return eStatus;
}


eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode)
{
    eMBErrorCode eStatus = MB_ENOERR;
    USHORT usData;
    int iRegIndex;

    usAddress -= 1;

    if((usAddress           >= REG_HOLDING_START) &&
       (usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS))
    {
        iRegIndex = (int)(usAddress - REG_HOLDING_START);

        switch(eMode)
        {
            case MB_REG_READ:
                while(usNRegs-- > 0)
                {
                    *pucRegBuffer++ = (unsigned char)(usRegHoldingBuffer[iRegIndex] >> 0x08);
                    *pucRegBuffer++ = (unsigned char)(usRegHoldingBuffer[iRegIndex]  & 0xFF);
                    iRegIndex++;
                }
                break;

            case MB_REG_WRITE:
                while(usNRegs-- > 0)
                {
                    usData   = *pucRegBuffer++;
                    usData <<= 0x08;
                    usData  |= *pucRegBuffer++;

                    usRegHoldingBuffer[iRegIndex++] = usData;
                }
                break;

            default:
                break;
        }
    }
    else
    {
        eStatus = MB_ENOREG;
    }

    return eStatus;
}


eMBErrorCode eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode)
{
    eMBErrorCode eStatus = MB_ENOERR;

    return eStatus;
}


eMBErrorCode eMBRegDiscreteCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNDiscrete)
{
    eMBErrorCode eStatus = MB_ENOERR;

    return eStatus;
}


FreeMODBUS通讯测试
10.jpg

调试Modbus通讯的软件有很多,比如Modbus Poll可以作为Modbus主机去请求从机操作、Modbus Slave可以作为Modbus从机去应答主机请求、其它的还有ModbusScan32等等。我们下面的测试仅仅做一些装简单的测试,用野人的串口调试助手中Modbus指令选项卡中的功能即可实现;我们需要选择协议的类型为Modbus-RTU,从设备ID号根据程序中的配置设置为0x0A,然后选择相应的功能码和对应的寄存器地址进行读写操作。在操作了Modbus操作参数后,点击更新按键,会在生成报文窗口中显示Modbus主机发送的数据,点击发送按键后数据将会被发送到Modbus从机,Modbus从机的回复数据会显示在数据接收窗口。如下图所示:
1.png

1、读取单个输入寄存器数据
2.png

2、读取多个输入寄存器数据
3.png

3、读取多于输入寄存器数量的数据,返回错误提示
4.png

4、读取单个保持寄存器数据
5.png

5、读取多个保持寄存器数据
6.png

6、读取多于保持寄存器数量的数据,返回错误提示
7.png

7、写入单个保持寄存器数据
8.png

8、读取刚刚写入的保持寄存器,核对写入数据是否正确
9.png


附件
软件工程源代码: FreeModbus.zip (2.82 MB)

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 100.00 元 2022-05-11
理由:恭喜通过原创文章审核!请多多加油哦!

xld0932|  楼主 | 2022-5-7 17:18 | 显示全部楼层
本文通过MM32F3270通讯板的RS485接口实现了基于Modbus协议的通讯,介绍了RS485接口、Modbus通讯协议数据帧格式以及在实际应用当中的2点注意事项;然后使用开源的FreeMODBUS软件代码库程序,实现Modbus通讯,讲解了FreeMODBUS的底层移植、初始化、运行调度以及应用实例;与PC端的串口助手软件完成了Modbus通讯的功能实测。

使用特权

评论回复
gouguoccc| | 2022-5-7 18:56 | 显示全部楼层
楼主这个写的详细啊。

使用特权

评论回复
xld0932|  楼主 | 2022-5-7 19:28 | 显示全部楼层
gouguoccc 发表于 2022-5-7 18:56
楼主这个写的详细啊。

使用特权

评论回复
www5911839| | 2022-5-7 19:59 | 显示全部楼层

刚想发帖说为啥没有附上这个经典的表情包,果然它没缺席

使用特权

评论回复
xld0932|  楼主 | 2022-5-7 20:09 | 显示全部楼层
www5911839 发表于 2022-5-7 19:59
刚想发帖说为啥没有附上这个经典的表情包,果然它没缺席

使用特权

评论回复
chenjun89| | 2022-5-8 11:36 | 显示全部楼层
modbus经久不衰啊!

使用特权

评论回复
xld0932|  楼主 | 2022-5-8 11:53 | 显示全部楼层
chenjun89 发表于 2022-5-8 11:36
modbus经久不衰啊!

是的呢

使用特权

评论回复
koala889| | 2022-5-15 08:15 | 显示全部楼层
楼主这系列,能整理保存就好了,
也发现论坛有收藏功能

使用特权

评论回复
xld0932|  楼主 | 2022-5-15 09:48 | 显示全部楼层
koala889 发表于 2022-5-15 08:15
楼主这系列,能整理保存就好了,
也发现论坛有收藏功能

我可以和21ic建议一下,出一个专题

使用特权

评论回复
发新帖 本帖赏金 100.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则