STM32-Modbus协议(一文通)

Modbus协议原理

RT-Thread官网开源modbus

                RT-Thread官方提供 FreeModbus开源。

                野火有移植的例程。

                QT经常用 libModbus库。

Modbus是什么?

        Modbus协议,从字面理解它包括ModBus两部分,首先它是一种bus,即总线协议,和I2C、SPI类似,总线就意味着有主机,有从机,这些设备在同一条总线上。

        Modbus支持单主机,多从机,最多支持247个从机设备

        Mod协议最早用在PLC产品上,后来被其他工业控制器厂商广泛接收,成为了一种主流的通讯协议,用于控制器和外围设备通信。

        Modbus在7层OSI参考模型中属于第七层应用层

        数据链路层有两种:基于标准串口协议TCP协议,物理层可使用3线232、2线485、4线422,或光纤、网线、无线等多种传输介质。

        Modbus协议是一请求/应答方式的交互过程,主机主动发起通讯请求,从机响应主机的请求,从机在没有收到主机的请求时,不会主动发送数据,从机之间不会进行通讯。

        Modbus官方标准文档可以直接在野火官网下载到。

        Modbus协议在STM32上面就是把串口引脚接到 MAX485 芯片(RS485电平)/或者MAX3232芯片(RS232电平)

        注意这是个协议,主要规定了数据帧的传输格式和数据交互方法。

Modbus RTU和Modbus extended

        Modbus、Modbus RTU和Modbus Extended之间的区别可以精简地归纳如下:

定义与范围:

        Modbus:是一种通信协议,定义了数据传输的格式和规则。

        Modbus RTU:是Modbus协议的一种实现方式,采用二进制编码,通常用于串行通信。

        Modbus Extended(或称为Modbus RTU Extend):是Modbus RTU的扩展版本,提供了更多高级功能和更大的数据集支持。

数据集大小:

        Modbus RTU支持最多1024个数据项(从机),但每次通信量少。

        Modbus Extended是Modbus RTU的扩展,虽支持数据项可能较少,通常256个数据项(从机),但每次可传输更多数据(也就是单个数据项更大,可能32字节),处理更复杂操作。

功能特点:

        Modbus RTU:提供基本的数据读写功能,适用于简单自动化需求。

        Modbus Extended:在Modbus RTU基础上增加了高级特性,如可变长度字符串(VLS)错误检测和纠正(EDC),增强了处理复杂数据的能力。

应用场景:

        Modbus RTU:常用于小型、简单的自动化系统,如工厂控制或楼宇管理。

        Modbus Extended:更适合大型、复杂的自动化系统,特别是对数据量、性能和可靠性要求较高的场景。

3 种协议模式

基于串口的 ASCII码模式RTU模式

        ASCII码模式采用 LRC 校验,RTU模式采用 16位 CRC 校验

基于以太网的 TCP 模式

        TCP 模式不使用校验,因为TCP自带校验和

        Modbus总线上所有的设备传输模式必须相同。
        实际使用要根据设备使用手册来选择采用哪种模式。

1. ASCII模式数据帧例子

主机发送请求(读取从机地址为1的保持寄存器0x0405的值)

:010304050001CRCLF
  • : 起始字符
  • 01 从机地址
  • 03 功能码(读取保持寄存器)
  • 0405 寄存器地址
  • 0001 读取长度
  • CRC LRC校验码(由数据计算得出,此处为占位符)
  • LF 换行符(结束字符)

从机响应

:010302XXXXCRCLF
  • : 起始字符
  • 01 从机地址
  • 03 功能码(读取保持寄存器)
  • 02 数据长度
  • XXXX 寄存器数据(实际数据,此处为占位符)
  • CRC LRC校验码
  • LF 换行符

2. RTU模式数据帧例子

从站地址功能码起始(高)起始(低)数量(高)数量(低)校验

主机发送请求(写入从机地址为1的保持寄存器0x0405的值0x1234)

01 06 04 05 12 34 CRC
  • 01 从机地址
  • 06 功能码(写入单个保持寄存器)
  • 0405 寄存器地址
  • 1234 写入的数据
  • CRC CRC校验码(由数据计算得出,此处为占位符)

从机响应

01 06 04 05 12 34 CRC
  • 内容与请求相同,表示写入成功

3. TCP模式数据帧例子

主机发送请求(读取从机地址为1的输入寄存器,起始地址0x0000,读取2个字)

        注意 PLC通常是x86架构,字长(机器位数)16位,因此一个字是16位。

Transaction Identifier: 0x0001  
Protocol Identifier: 0x0000  
Length Field: 0x0006  
Unit Identifier: 0x01  
Function Code: 0x04  
Starting Address: 0x0000  
Quantity of Registers: 0x0002
  • 该数据帧为 Modbus TCP的 ADU(应用数据单元),其中包含了 7个字段,用于标识交易、协议、长度、单元(从机地址)、功能码、起始地址和读取长度。

从机响应

Transaction Identifier: 0x0001  
Protocol Identifier: 0x0000  
Length Field: 0x0005  
Unit Identifier: 0x01  
Function Code: 0x04  
Byte Count: 0x04  
Data: 0x1234 0x5678
  • 响应中包含了请求中的交易标识符、协议标识符等,以及数据字段,表示读取到的寄存器值。

Modbus协议应用技巧

        首先,Modbus协议经常被拿来跟 PLC、传感器通讯,PLC属于x86架构或者AMD架构,用的CISC指令集。这是 PLC和 STM32的区别,STM是 RISC指令集。

        其次,modbus只是个协议,规定了数据帧的格式,你能满足它的数据帧,就能通信。

功能码

modbus协议功能码

读取操作:

        读线圈(0x01)

发送请求帧格式:
[从站地址] [0x01] [起始地址高] [起始地址低] [读取数量高] [读取数量低] [校验码]
01 01 00 00 00 01 CRC(假设从站地址为01,读取起始地址为0000,数量为1个线圈)

返回响应帧格式:
[从站地址] [0x01] [字节数] [线圈状态数据...] [校验码]
(字节数通常为读取数量,线圈状态数据为每个线圈的状态,通常为00或FF表示OFF或ON)
01 01 01 00 CRC
(假设读取的线圈状态为ON/开,状态字节为01,后续字节为数据值,
但在此例中只有一个线圈,所以数据值为00)

        读离散量输入(0x02)

                 数据帧和读线圈类似,但功能码为0x02。

        读保持寄存器(0x03)

发送请求帧:
[从站地址] [0x03] [起始地址高] [起始地址低] [读取数量高] [读取数量低] [校验码]
01 03 00 00 00 02 CRC(假设从站地址为01,读取起始地址为0000,数量为2个寄存器)

返回响应帧:
[从站地址] [0x03] [字节数] [寄存器数据...] [校验码]
01 03 04 00 01 00 02 CRC
(假设读取的两个寄存器值分别为0001和0002,每个寄存器值占两个字节,所以总字节数为4)

        读输入寄存器(0x04)

        请求帧格式与读保持寄存器类似,但功能码为0x04。

写入操作:

        写单个线圈(0x05)

发送请求帧格式:
[从站地址] [0x05] [目标地址高] [目标地址低] [要写入的值] [校验码]
(要写入的值通常为00或FF表示OFF或ON)
01 05 00 00 FF 00 CRC
(假设从站地址为01,目标地址为0000,写入的值为ON/开)

返回响应帧格式:
[从站地址] [0x05] [目标地址高] [目标地址低] [写入的值] [校验码]
(写入成功后,从站通常返回与请求相同的帧,但实际应用中可能返回其他格式的响应帧)
01 05 00 00 FF 00 CRC
(写入成功后,从站通常返回与请求相同的帧作为响应,但实际应用中可能有所不同)

        写单个寄存器(0x06)

[从站地址] [0x06] [目标地址高] [目标地址低] [要写入的数据高] [要写入的数据低] [校验码]
发送请求帧:01 06 00 00 00 13 CRC
(假设从站地址为01,目标地址为0000,写入的数据值为0013)

[从站地址] [0x06] [目标地址高] [目标地址低] [写入的数据高] [写入的数据低] [校验码]
返回响应帧:01 06 00 00 00 13 CRC
(写入成功后,从站通常返回与请求相同的帧作为响应,但实际应用中可能有所不同)

        写多个线圈(0x0F)

[从站地址] [0x0F] [起始地址高] [起始地址低] 
              [要写入的线圈数量高] [要写入的线圈数量低] [字节数] [线圈状态数据...] [校验码]
发送请求帧:01 0F 00 00 00 02 01 01 CRC
(假设从站地址为01,起始地址为0000,写入2个线圈,第一个线圈ON,第二个线圈OFF)

[从站地址] [0x0F] [起始地址高] [起始地址低] [写入的线圈数量高] [写入的线圈数量低] [校验码]
返回响应帧:01 0F 00 00 00 02 CRC
(写入成功后,从站返回包含起始地址和写入数量的响应帧,但实际应用中可能有所不同)

        写多个寄存器(0x10)

[从站地址] [0x10] [起始地址高] [起始地址低] 
            [要写入的寄存器数量高] [要写入的寄存器数量低] 
                [字节数] [寄存器数据...] [校验码]
发送请求帧:01 10 00 00 00 02 04 00 01 00 02 CRC
(假设从站地址为01,起始地址为0000,写入2个寄存器,第一个寄存器值为0001,第二个寄存器值为0002)

[从站地址] [0x10] [起始地址高] [起始地址低] 
                [写入的寄存器数量高] [写入的寄存器数量低] [校验码]
返回响应帧:01 10 00 00 00 02 CRC
(写入成功后,从站返回包含起始地址和写入数量的响应帧,但实际应用中可能有所不同)

源码移植

        下面看一下野火移植的源码:

        main函数

/* Private user code ---------------------------------------------------------*/
/* 离散输入变量 */
extern UCHAR    ucSDiscInBuf[S_DISCRETE_INPUT_NDISCRETES/8]  ;
/* 线圈 */
extern UCHAR    ucSCoilBuf[S_COIL_NCOILS/8];
/* 输入寄存器 */
extern USHORT   usSRegInBuf[S_REG_INPUT_NREGS];
/* 保持寄存器 */
extern USHORT   usSRegHoldBuf[S_REG_HOLDING_NREGS];


int main(void){

    /* 串口2初始化在portserial.c中 */
    ...
    /* 定时器4初始化 */
    MX_TIM4_Init();
    ...
    /* Modbus初始化 */
	eMBInit(  
        MB_RTU,                 // 传输模式:RTU (Remote Terminal Unit),即Modbus RTU模式  
        MB_SAMPLE_TEST_SLAVE_ADDR,// 从站地址:在此示例中使用的测试从站地址  
        MB_MASTER_USARTx,       // 串口配置:指定用于Modbus通信的USART(串行通讯接口)  
        MB_MASTER_USART_BAUDRATE, // 波特率:设置USART的波特率,用于Modbus通信的速率  
        MB_PAR_NONE             // 校验位和停止位配置:无校验,通常表示8位数据位,1个停止位  
    );
	
	/* 启动Mdobus */
	eMBEnable();
    while (1)
    {
		/* 更新保持寄存器值 */
		usSRegHoldBuf[0] =  HAL_GetTick() & 0xff;		           //获取时间戳 提出1至8位
		usSRegHoldBuf[1] = (HAL_GetTick() & 0xff00) >> 8;      //获取时间戳 提出9至16位
		usSRegHoldBuf[2] = (HAL_GetTick() & 0xff0000) >> 16 ;  //获取时间戳 提出17至24位
		usSRegHoldBuf[3] = (HAL_GetTick() & 0xff000000) >> 24; //获取时间戳 提出25至32位
		
		/* 更新输入寄存器值 */
		usSRegInBuf[0] =  HAL_GetTick() & 0xff;		             //获取时间戳 提出1至8位
		usSRegInBuf[1] = (HAL_GetTick() & 0xff00) >> 8;        //获取时间戳 提出9至16位
		usSRegInBuf[2] = (HAL_GetTick() & 0xff0000) >> 16 ;    //获取时间戳 提出17至24位
		usSRegInBuf[3] = (HAL_GetTick() & 0xff000000) >> 24;   //获取时间戳 提出25至32位
		
		/* 更新线圈 */
		ucSCoilBuf[0] =  HAL_GetTick() & 0xff;		             //获取时间戳 提出1至8位
		ucSCoilBuf[1] = (HAL_GetTick() & 0xff00) >> 8;         //获取时间戳 提出9至16位
		ucSCoilBuf[2] = (HAL_GetTick() & 0xff0000) >> 16 ;     //获取时间戳 提出17至24位
		ucSCoilBuf[3] = (HAL_GetTick() & 0xff000000) >> 24;    //获取时间戳 提出25至32位
		
		/* 离散输入变量 */
		ucSDiscInBuf[0] =  HAL_GetTick() & 0xff;		           //获取时间戳 提出1至8位
		ucSDiscInBuf[1] = (HAL_GetTick() & 0xff00) >> 8;       //获取时间戳 提出9至16位
		
		/* 可以不用延时,如果延时时间过长主机会timeout */
		HAL_Delay(200);		
		
		/*从机轮询*/
		( void )eMBPoll(  );
  }
}

        主要有

eMBInit
eMBInit(  
    MB_RTU,                 // 传输模式:RTU (Remote Terminal Unit),即Modbus RTU模式  
    MB_SAMPLE_TEST_SLAVE_ADDR, // 从站地址:在此示例中使用的测试从站地址  
    MB_MASTER_USARTx,       // 串口配置:指定用于Modbus通信的USART(串行通讯接口)  
    MB_MASTER_USART_BAUDRATE, // 波特率:设置USART的波特率,用于Modbus通信的速率  
    MB_PAR_NONE             // 校验位和停止位配置:无校验,通常表示8位数据位,1个停止位  
);

/*
eMBInit 函数功能简述:

参数验证:检查从设备地址是否有效。
模式选择:根据通信模式设置函数指针。
初始化:调用对应模式的初始化函数配置通信参数。
事件初始化:初始化端口事件模块以处理通信事件。
状态设置:成功初始化后,设置模块为禁用状态。
返回状态:返回初始化结果的状态码。
*/
/*eMBInit内部的传输模式初始化*/
#if MB_RTU_ENABLED > 0  
            case MB_RTU:  // RTU模式  
                // 设置RTU模式相关的函数指针  
                pvMBFrameStartCur = eMBRTUStart;  
                pvMBFrameStopCur = eMBRTUStop;  
                peMBFrameSendCur = eMBRTUSend;  
                peMBFrameReceiveCur = eMBRTUReceive;  
                pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;  
                pxMBFrameCBByteReceived = xMBRTUReceiveFSM;  
                pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;  
                pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;  
  
                 // 初始化RTU  
                eStatus = eMBRTUInit(ucMBAddress, ucPort, ulBaudRate, eParity); 
                break;  
#endif 

/*
eMBRTUInit 函数的功能是初始化 Modbus RTU 通信模式,具体包括:
        串口配置:设置指定端口的波特率、8个数据位和校验位。
        定时器设置:根据波特率计算并设置定时器T35的值,以确保正确的通信时序。
        错误处理:在初始化过程中,如遇到任何失败,则返回相应的错误状态。
*/
eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )  
{  
    eMBErrorCode    eStatus = MB_ENOERR; // 初始化状态为无错误  
    ULONG           usTimerT35_50us; // 定时器T35的50微秒单位值  
  
    ( void )ucSlaveAddress; // 目前未使用从设备地址参数 
 
    ENTER_CRITICAL_SECTION(  ); // 进入临界区,保护共享资源  
    //__set_PRIMASK(1),设置PRIMASK寄存器,由CMSIS库提供
    //屏蔽除 NMI 和 HardFalut 外的所有异常和中断。
  
    // Modbus RTU使用8个数据位  
    if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )  
    {  
        eStatus = MB_EPORTERR; // 串口初始化失败,设置错误状态  
    }  
    else  
    {  
        // 根据波特率设置定时器T35的值  
        if( ulBaudRate > 19200 )  
        {  
            usTimerT35_50us = 35; // 波特率大于19200时使用固定值  
        }  
        else  
        {  
            // 计算T35的值为3.5个字符时间  
            usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );  
        }  
          
        // 初始化定时器  
        if( xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) != TRUE )  
        {  
            eStatus = MB_EPORTERR; // 定时器初始化失败,设置错误状态  
        }  
    }  
    EXIT_CRITICAL_SECTION(  ); // 退出临界区
    //__set_PRIMASK(0) 设置Primask寄存器 
  
    return eStatus; // 返回初始化状态  
}

        上面可以看到,modbus模块的初始化,根据波特率设置了所谓Timer35定时器的值,        

        但这个定时器其实是我们自己在 main里设置的(示例用的TIM4),这里定时器初始化直接返回了True。

BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )		//定时器初始化直接返回TRUE,已经在mian函数初始化过
{
    return TRUE;
}

        实际的设置代码,野火原版是hal库的,我这里给个标准库的参考版本

void MX_TIM4_Init(void)    
{    
    // 开启TIM4时钟  
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);    
    
    // 初始化定时器基础配置  
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;    
    TIM_TimeBaseStruct.TIM_Prescaler = 4200 - 1;  // 设置预分频器  
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数  
    TIM_TimeBaseStruct.TIM_Period = 35;  // 设置周期  
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;  // 时钟不分频  
    TIM_TimeBaseStruct.TIM_RepetitionCounter = 0;  // 重复计数器为0(通常不需要)  
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStruct);  // 初始化TIM4  
    
    // 启用TIM4更新中断  
    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);    
    // 启动TIM4  
    TIM_Cmd(TIM4, ENABLE);    
    
    // 配置NVIC以启用TIM4中断  
    NVIC_InitTypeDef NVIC_InitStruct;    
    NVIC_InitStruct.NVIC_IRQChannel = TIM4_IRQn;  // 设置中断通道  
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  // 设置抢占优先级  
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;  // 设置子优先级  
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;  // 启用中断  
    NVIC_Init(&NVIC_InitStruct);  // 初始化NVIC  
}
/*TIM4的中断服务函数*/
void TIM4_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim4);
}

/**stm32f4xx_it.c中的溢出回调函数**/
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)	//定时器中断回调函数,用于连接porttimer.c文件的函数
{
  /* NOTE : This function Should not be modified, when the callback is needed,
            the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
   */
  	prvvTIMERExpiredISR( );//freemodbus移植过来的函数
}

/*定时器中调用freemodbus移植过来的函数*/
void prvvTIMERExpiredISR( void )	//modbus定时器动作,需要在中断内使用
{
    ( void )pxMBPortCBTimerExpired(  );//这个函数其实是指向 xMBRTUTimerT35Expired()
}

//定时器最终调用的函数在下个代码块给出

         xMBRTUTimerT35Expired 函数是 Modbus RTU 通信协议中的一部分,用于处理接收状态定时器 T35 到期时的逻辑。

        它首先初始化一个轮询标志 xNeedPoll,然后根据当前接收状态 eRcvState 执行不同操作:

        在启动阶段结束时发布“准备就绪”事件,

        在接收到完整帧时发布“帧接收”事件,

        若发生错误则跳过。

        无论状态如何,都会禁用并重置定时器并将接收状态设置为空闲。

        最后,函数返回是否需要轮询的标志。

        简而言之,该函数根据 T35 定时器的到期情况更新接收状态、模拟时间队列发布相应事件,并禁用计时器。

BOOL xMBRTUTimerT35Expired( void )  
{  
    BOOL xNeedPoll = FALSE;  
  
    switch (eRcvState)  
    {  
        // Timer t35到期,启动阶段结束  
    case STATE_RX_INIT:  
        xNeedPoll = xMBPortEventPost(EV_READY);  
        break;  
  
        // 接收到帧且t35到期,通知监听器收到新帧  
    case STATE_RX_RCV:  
        xNeedPoll = xMBPortEventPost(EV_FRAME_RECEIVED);  
        break;  
  
        // 接收帧时发生错误  
    case STATE_RX_ERROR:  
        break;  
  
        // 函数在非法状态下被调用  
    default:  
        assert((eRcvState == STATE_RX_INIT) || (eRcvState == STATE_RX_RCV) || (eRcvState == STATE_RX_ERROR));  
    }  
  
    // 禁用端口计时器  
    vMBPortTimersDisable();  
    // 设置接收状态为空闲  
    eRcvState = STATE_RX_IDLE;  
  
    return xNeedPoll;  
}
/*模拟事件上报*/
BOOL xMBPortEventPost( eMBEventType eEvent )  
{  
    // 设置事件在队列中的标志为TRUE  
    xEventInQueue = TRUE;        //注意这里不是真实的队列,只是个bool模拟队列状态
    // 保存传入的事件类型  
    eQueuedEvent = eEvent;  
    // 返回TRUE表示事件成功发布  
    return TRUE;  
}
eMBpoll 

        main函数while里面还有个 eMBpoll()从机轮询。

        此函数是Modbus协议栈中的轮询函数,负责处理协议栈中的事件。

        它首先检查协议栈是否准备就绪,然后检查是否有事件可用(参考定时器回调的模拟事件)。

        若有事件,将根据事件类型执行相应的操作,如接收帧、执行功能码处理或发送回复帧等。

        函数通过静态变量和局部变量来存储和处理接收到的帧、地址、功能码、异常等信息,并根据需要调用其他函数来执行具体的操作。

        最后,函数返回无错误状态。

/*从机轮询*/
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;  
  
    // 检查协议栈是否准备就绪  
    if( eMBState != STATE_ENABLED )  
    {  
        return MB_EILLSTATE;  // 如果未就绪,则返回非法状态错误  
    }  
  
    // 检查是否有事件可用  
    if( xMBPortEventGet( &eEvent ) == TRUE )  
    {  
        switch ( eEvent )  
        {  
        case EV_READY:  
            // 准备就绪事件,无需特殊处理  
            break;  
  
        case EV_FRAME_RECEIVED:  
            // 接收到帧事件  
            eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );  
            if( eStatus == MB_ENOERR )  
            {  
                // 如果帧是发送给我们的或者是广播帧,则发布执行事件  
                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++ )  
            {  
                if( xFuncHandlers[i].ucFunctionCode == 0 )  
                {  
                    break;  // 没有更多的函数处理器,退出循环  
                }  
                else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )  
                {  
                    eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );  
                    break;  // 找到匹配的功能码并执行处理函数,退出循环  
                }  
            }  
  
            // 如果接收地址不是广播地址,则发送回复帧  
            if( ucRcvAddress != MB_ADDRESS_BROADCAST )  
            {  
                if( eException != MB_EX_NONE )  
                {  
                    // 如果发生异常,构建错误帧  
                    usLength = 0;  
                    ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );  
                    ucMBFrame[usLength++] = eException;  
                }  
  
                // (可选)在发送前延迟一段时间(仅适用于ASCII模式)  
                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;  // 函数返回无错误状态  
}
串口数据帧接收/发送
void USART2_IRQHandler(void)
{
    ...
	if(__HAL_UART_GET_IT_SOURCE(&huart2, UART_IT_RXNE)!= RESET) 
		{
			prvvUARTRxISR();//接收,函数指针
		}

	if(__HAL_UART_GET_IT_SOURCE(&huart2, UART_IT_TXE)!= RESET) 
		{
			prvvUARTTxReadyISR();//发送,函数指针
		}
    ...
}
/*真实的发送*/
BOOL xMBRTUTransmitFSM( void )  
{  
    BOOL xNeedPoll = FALSE;  // 初始化轮询需求为不需要  
  
    assert( eRcvState == STATE_RX_IDLE );  // 断言接收状态应为空闲  
  
    switch ( eSndState )  // 根据发送状态进行处理  
    {  
        case STATE_TX_IDLE:  
            // 如果发送状态为空闲  
            vMBPortSerialEnable( TRUE, FALSE );  // 启用接收器,禁用发送器  
            break;  
  
        case STATE_TX_XMIT:  
            // 如果发送状态为正在发送  
            if( usSndBufferCount != 0 )  // 检查发送缓冲区是否还有数据  
            {  
                xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );  // 发送当前字节  
                pucSndBufferCur++;  // 移动到缓冲区中的下一个字节  
                usSndBufferCount--;  // 减少缓冲区计数  
            }  
            else  
            {  
                xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );  // 发布帧发送完成事件,可能需要轮询  
                vMBPortSerialEnable( TRUE, FALSE );  // 禁用发送器,防止再次发送缓冲区空中断  
                eSndState = STATE_TX_IDLE;  // 将发送状态设置为空闲  
            }  
            break;  
    }  
  
    return xNeedPoll;  // 返回是否需要轮询的标志  
}

        最后被串口中断调用的,串口接收函数。 

BOOL xMBRTUReceiveFSM( void )  
{  
    BOOL xTaskNeedSwitch = FALSE;  // 初始化任务切换需求标志为FALSE  
    UCHAR ucByte;  // 用于存储接收到的字节  
  
    assert( eSndState == STATE_TX_IDLE );  // 确保发送状态为空闲  
  
    /*串口读取字符*/
    // 总是读取字符(无论当前接收状态如何)  
    ( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );  
  
    switch ( eRcvState )  // 根据接收状态进行处理  
    {  
    case STATE_RX_INIT:  
        // 如果在初始化状态接收到字符,等待帧结束  
        vMBPortTimersEnable(  );  // 启用定时器  
        break;  
  
    case STATE_RX_ERROR:  
        // 在错误状态,等待损坏帧的所有字符传输完毕  
        vMBPortTimersEnable(  );  // 启用定时器  
        break;  
  
    case STATE_RX_IDLE:  
        // 在空闲状态,等待新字符。接收到字符后,启动定时器,并进入接收状态  
        usRcvBufferPos = 0;  // 重置接收缓冲区位置  
        ucRTUBuf[usRcvBufferPos++] = ucByte;  // 将接收到的字节存入缓冲区  
        eRcvState = STATE_RX_RCV;  // 更改接收状态为正在接收  
        vMBPortTimersEnable(  );  // 启用定时器 
        break;  
  
    case STATE_RX_RCV:  
        // 正在接收帧。每接收到一个字符,重置定时器。
        // 如果接收到的字节数超过Modbus帧的最大可能大小,则忽略该帧  
        if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )  
        {  
            ucRTUBuf[usRcvBufferPos++] = ucByte;  // 将接收到的字节存入缓冲区  
        }  
        else  
        {  
            eRcvState = STATE_RX_ERROR;  // 接收字节数超标,更改接收状态为错误  
        }  
        vMBPortTimersEnable(  );  // 启用定时器(为了保持接收超时检测)  
        break;  
    }  
  
    return xTaskNeedSwitch;  // 返回任务切换需求标志(在此函数中始终为FALSE)  
}

        每一次定时器溢出,都将 eRcvState转变为STATE_RX_IDLE状态,然后 接收,

        一次性接受完全部数据帧。

        再重启定时器,又是 IDLE状态。

modbus帧解析

        在临界区内接收并处理一个Modbus RTU帧,进行长度和CRC校验,如果校验通过,则提取并返回地址、长度和PDU数据,否则设置错误码。

#define MB_SER_PDU_SIZE_MIN     4       // Modbus RTU 帧的最小大小  
#define MB_SER_PDU_SIZE_MAX     256     // Modbus RTU 帧的最大大小  
#define MB_SER_PDU_SIZE_CRC     2       // PDU 中 CRC 字段的大小  
#define MB_SER_PDU_ADDR_OFF     0       // Ser-PDU 中从站地址的偏移量  
#define MB_SER_PDU_PDU_OFF      1       // Ser-PDU 中 Modbus-PDU 的偏移量
/*该函数将数据存放在数组中,并返回从站存储位置,帧存储位置,帧长度*/
eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength ); 
/*RTU帧解析*/
eMBErrorCode eMBRTUReceive( UCHAR * pucRcvAddress,  // 接收到的从站地址存储位置  
                            UCHAR ** pucFrame,      // 接收到的帧数据存储位置  
                            USHORT * pusLength )   // 接收到的帧数据长度存储位置
{  
    BOOL            xFrameReceived = FALSE;  // 帧接收标志  
    eMBErrorCode    eStatus = MB_ENOERR;     // 初始化错误码为无错误  
  
    ENTER_CRITICAL_SECTION(  );  // 进入临界区  
  
    assert( usRcvBufferPos < MB_SER_PDU_SIZE_MAX );  // 断言:接收缓冲区位置应小于最大PDU大小  
  
    // 长度和CRC校验  
    if( ( usRcvBufferPos >= MB_SER_PDU_SIZE_MIN )  
        && ( usMBCRC16( ( UCHAR * ) ucRTUBuf, usRcvBufferPos ) == 0 ) )  
    {  
        // 保存地址字段  
        *pucRcvAddress = ucRTUBuf[MB_SER_PDU_ADDR_OFF];  
  
        // 计算Modbus-PDU总长度 = 接收缓冲区位置-从站地址偏移-校验偏移  
        *pusLength = 
            ( USHORT )( usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC );  
  
        // 返回Modbus PDU的起始位置  
        *pucFrame = ( UCHAR * ) & ucRTUBuf[MB_SER_PDU_PDU_OFF];  
        xFrameReceived = TRUE;  // 标记帧已接收  
    }  
    else  
    {  
        eStatus = MB_EIO;  // 设置错误码为输入/输出错误  
    }  
  
    EXIT_CRITICAL_SECTION(  );  // 退出临界区  
    return eStatus;  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/900879.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

监督学习之逻辑回归

逻辑回归&#xff08;Logistic Regression&#xff09; 逻辑回归是一种用于二分类&#xff08;binary classification&#xff09;问题的统计模型。尽管其名称中有“回归”二字&#xff0c;但逻辑回归实际上用于分类任务。它的核心思想是通过将线性回归的输出映射到一个概率值…

如何限制电脑软件的安装?

1.修改注册表&#xff08;需谨慎操作&#xff0c;建议备份注册表&#xff09;&#xff1a; 打开“运行”对话框&#xff0c;输入 regedit 打开注册表编辑器。 导航到 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer。 创建新的DWORD值&…

2024双11买什么东西比较好?双十一购物清单,双十一囤货清单排名

今年双十一好价确实多&#xff0c;一方面是年底促销&#xff0c;一方面国补也很给力&#xff0c;种草很久的产品趁着这个时间下单最好不过了&#xff0c;不知道各位有哪些心水好物&#xff0c;我今年入手了不少生活用品和数码类产品&#xff0c;下文就挑选几款我觉得特别值得入…

基于Multisim的四人智力竞赛抢答器设计与仿真

1&#xff09;设计任务 设计一台可供 4 名选手参加比赛的智力竞赛抢答器。 用数字显示抢答倒计时间&#xff0c;由“9”倒计到“0”时&#xff0c;无人抢答&#xff0c;蜂鸣器连续响 1 秒。选手抢答时&#xff0c;数码显示选手组号&#xff0c;同时蜂鸣器响 1 秒&#xff0c;倒…

使用Prometheus对微服务性能自定义指标监控

背景 随着云计算和容器化技术的不断发展&#xff0c;微服务架构逐渐成为现代软件开发的主流趋势。微服务架构将大型应用程序拆分成多个小型、独立的服务&#xff0c;每个服务都可以独立开发、部署和扩展。这种架构模式提高了系统的可伸缩性、灵活性和可靠性&#xff0c;但同时…

Appium中的api(一)

目录 1.基础python代码准备 1--参数的一些说明 2--python内所要编写的代码 解释 2.如何获取包名和界面名 1-api 2-完整代码 代码解释 3.如何关闭驱动连接 4.安装卸载app 1--卸载 2--安装 5.判断app是否安装 6.将应用放到后台在切换为前台的时间 7.UIAutomatorViewer的使用 1--找…

学习笔记——路由——IP组播-PIM-DM(密集模式)前言概述

7、PIM-DM(密集模式) (1)前言 PIM-DM(PIM Dense Mode)使用“推(Push)模式”转发组播报文&#xff0c;一般应用于组播组成员规模相对较小、相对密集的网络。 在实现过程中&#xff0c;它会假设网络中的组成员分布非常稠密&#xff0c;每个网段都可能存在组成员。当有活跃的组…

TLS协议基本原理与Wireshark分析

01背 景 随着车联网的迅猛发展&#xff0c;汽车已经不再是传统的机械交通工具&#xff0c;而是智能化、互联化的移动终端。然而&#xff0c;随之而来的是对车辆通信安全的日益严峻的威胁。在车联网生态系统中&#xff0c;车辆通过无线网络与其他车辆、基础设施以及云端服务进行…

JavaEE----多线程(四)----阻塞队列的介绍和初步实现

文章目录 1.阻塞队列1.1作用一&#xff1a;解耦合1.2作用二&#xff1a;削峰填谷1.3系统里面的阻塞队列的使用1.4实现普通队列1.5在普通队列的基础上面实现阻塞队列1.6设计优化1.7实现初步的生产者消费者模型 1.阻塞队列 阻塞队列的最大意义&#xff1a;就是实现“生产者消费者…

Pyramidal Flow使用指南:快手、北大、北邮,开源可免费商用视频生成模型,快速上手教程

什么是 Pyramidal Flow&#xff1f; Pyramidal Flow 是由快手科技、北京大学和北京邮电大学联合推出的开源视频生成模型&#xff0c;它是完全开源的&#xff0c;发布在 MIT 许可证下&#xff0c;允许商业使用、修改和再分发。该模型能够通过文本描述生成最高10秒、分辨率为128…

铜业机器人剥片 - SNK施努卡

SNK施努卡有色行业电解车间铜业机器人剥片 铜业机器人剥片技术是针对传统人工剥片效率低下、工作环境恶劣及生产质量不稳定的痛点而发展起来的自动化解决方案。 面临人工剥片的诸多挑战&#xff0c;包括低效率、工作环境差、人员流动大以及产品质量控制不精确等问题。 人工剥片…

Idea基于JRbel实现项目热部署修改Java、Xml文件无需重启项目

Idea基于JRbel实现项目热部署修改Java、Xml文件无需重启项目 1.JRbel服务安装2.JRbel插件安装3.JRbel配置 1.JRbel服务安装 直接装插件的话&#xff0c;需要用到一个服务地址&#xff0c;服务下载链接&#xff1a;&#xff08;现在没时间搞&#xff0c;会尽快加上&#xff09;…

合合信息亮相PRCV大会,探讨生成式AI时代的内容安全与系统构建加速

一、前言 在人工智能技术的飞速发展下&#xff0c;生成式AI已经成为推动社会进步的重要力量。然而&#xff0c;随着技术的不断进步&#xff0c;内容安全问题也日益凸显。如何确保在享受AI带来的便利的同时&#xff0c;保障信息的真实性和安全性&#xff0c;已经成为整个行业待解…

Jmeter自动化实战

前言 由于系统业务流程很复杂,在不同的阶段需要不同的数据,且数据无法重复使用,每次造新的数据特别繁琐,故想着能不能使用jmeter一键造数据 创建录制模板 录制模板参考 首先创建一个录制模板 因为会有各种请求头,cookies,签名,认证信息等原因,导致手动复制粘贴的的全面导致接…

Flutter TextField和Button组件开发登录页面案例

In this section, we’ll go through building a basic login screen using the Button and TextField widgets. We’ll follow a step-bystep approach, allowing you to code along and understand each part of the process. Let’s get started! 在本节中&#xff0c;我们…

NVIDIA发布Nemotron-70B-Instruct,超越GPT-4o和Claude 3.5的AI模型

一、Nemotron-70B-Instruct 是什么 Nemotron-70B-Instruct 是由 NVIDIA 基于 Meta 的 Llama 3.1-70B 模型开发的先进大语言模型&#xff08;LLM&#xff09;。该模型采用了新颖的神经架构搜索&#xff08;Neural Architecture Search&#xff0c;NAS&#xff09;方法和知识蒸馏…

【华为HCIP实战课程二十】OSPF特殊区域NSSA配置详解,网络工程师

一、NSSA&#xff08;Not So Stubby Area&#xff09;区域 在NSSA区域内可以拥有ASBR&#xff0c;并且重分发进入OSPF的路由是以7类LSA形式存在&#xff0c;该类型的LSA只能存在于NSSA区域内不接收5类LSA&#xff0c;ABR过滤外部进入该区域的4 5类LSA&#xff0c;可以引入外部…

题解 力扣 LeetCode 739 每日温度 C++

题目传送门&#xff1a; 739. 每日温度 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/daily-temperatures/description/ 思路&#xff1a; 就是单调栈的思路&#xff0c;具体见代码 不知道单调栈的&#xff0c;可以看我的这篇文章&#xff1a; 数…

web3对象如何连接以太网络节点

实例化web3对象 当我们实例化web3对象&#xff0c;我们一般开始用本地址&#xff0c;如下 import Web3 from web3 var web3 new Web3(Web3.givenProvider || ws://localhost:5173)我们要和以太网进行交互&#xff0c;所以我们要将’ws://localhost:5173’的本地地址换成以太…

【Linux学习】(6)编译器gcc/g++

前言 本节重点&#xff1a;掌握gcc/g编译器的使用&#xff0c;并了解其过程&#xff0c;原理 一、Linux编译器-gcc/g使用 1. gcc/g的基本使用 在前面我们学习了vim&#xff0c;知道如何在Linux中编写代码。但又是如何编译代码的&#xff1f;——在Linux中我们编译代码使用的是…