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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Maya---骨骼绑定

调节骨骼大小 回车键确认骨骼 FK子集跟父集走 IK子集不跟父集走 前视图中按shift键添加骨骼 清零、删除历史记录&#xff0c;创建新的物体

多元线性回归【正规方程/sklearn】

多元线性回归【正规方程/sklearn】 1. 基本概念1.1 线性回归1.2 一元简单线性回归1.3 最优解1.4 多元线性回归 2. 正规方程求最优解2.1 线性回归的损失函数&#xff08;最小二乘法&#xff09;2.2 推导正规方程2.3 正规方程练习2.4 使用sklearn计算多元线性方程2.5 凸函数 3. 线…

哪款宠物空气净化器噪音低?希喂、美的、安德迈测评分享

今年双11&#xff0c;宠物空气净化器到底应该如何选&#xff1f;在所有的家电品类里&#xff0c;宠物空气净化器算是比较特殊的那个&#xff0c;产品迭代太快&#xff0c;我们把今年双11在售的各大主流品牌的宠物空气净化器统一汇总整理&#xff0c;发现基本一多半都是24年下半…

【Unity 安装教程】

Unity 中国官网地址链接 Unity - 实时内容开发平台 | 3D、2D、VR & AR可视化https://unity.cn/首先我们想要安装Unity之前&#xff0c;需要安装Unity Hub&#xff1a; Unity Hub 是 Unity Technologies 开发的一个集成软件&#xff0c;它为使用 Unity 引擎的开发者提供了一…

使用Angular构建动态Web应用

&#x1f496; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4bb; Gitee主页&#xff1a;瑕疵的gitee主页 &#x1f680; 文章专栏&#xff1a;《热点资讯》 使用Angular构建动态Web应用 1 引言 2 Angular简介 3 安装Angular CLI 4 创建Angular项目 5 设计应用结构 6 创建组件…

Nature Communications|一种3D打印和激光诱导协同策略用于定制功能化器件(3D打印/激光直写/柔性电子/人机交互/柔性电路)

美国密苏里大学机械与航天工程系Jian Lin团队,在《Nature Communications》上发布了一篇题为“Programmed multimaterial assembly by synergized 3D printing and freeform laser induction”的论文。论文内容如下: 一、 摘要 在自然界中,结构和功能材料经常形成程序化的三…

《面向对象系统分析与设计》三级项目

文章目录 一、项目总体要求<设计模式应用>第一次文档要求第二次文档要求 二、示例第一次文档 - 设计原则应用1、题目描述2、设计:(1&#xff09;优化理由陈述(2&#xff09;类图(3)实现代码 第二次文档 - 设计模式应用1、题目描述2、设计:(1) 优化理由陈述(2&#xff09;…

【虚幻引擎UE】UE5 音频共振特效制作

UE5 音频共振特效制作 一、基础准备1.插件准备2.音源准备 二、创建共感NRT解析器和设置1.解析器选择依据2. 创建解析器3. 创建解析器设置&#xff08;和2匹配&#xff09;4.共感NRT解析器设置参数调整5.为共感NRT解析器关联要解析的音频和相应设置 三、蓝图控制1.创建Actor及静…

短剧AI突围战,百度跑偏了

“ 百度短剧的Agent对话功能并不属于颠覆性创新&#xff0c;只是新插件&#xff0c;对短剧行业市场格局影响不大&#xff0c;最多只能算用户痒点。 ” 转载&#xff1a;科技新知 原创 作者丨晓伊 编辑丨蕨影 你是否有过这样的体验&#xff1f; 刷短剧时&#xff0c;因剧情曲…

解决运行jar错误: 缺少 JavaFX 运行时组件, 需要使用该组件来运行此应用程序

报错 众所周知jdk8以上都没有Javafx java -jar target/myyscan-1.0-SNAPSHOT.jar 错误: 缺少 JavaFX 运行时组件, 需要使用该组件来运行此应用程序解决 https://gluonhq.com/products/javafx/ 去下载/javafx/到本地&#xff0c;选择自己的型号 然后记得指定路径 java --m…

TPLCM柔性屏自动化贴合应用

在当前的显示屏制造领域&#xff0c;TP&LCM贴合技术是推动产品升级和满足市场需求的关键环节。随着技术的不断进步&#xff0c;全贴合技术因其卓越的显示效果和用户体验&#xff0c;逐渐成为中高端产品的标配。然而&#xff0c;这一技术的高精度要求和复杂工艺也带来了诸多…

【C++ 算法进阶】算法提升七

目录 正数数组中那两个数&结果最大 &#xff08;贪心&#xff09;题目题目分析代码详解 最小相机覆盖问题 &#xff08;二叉树递归套路&#xff09;题目题目分析代码详解 拼接字符串 &#xff08;动态规划 前缀树&#xff09;题目题目分析代码详解 正数数组中那两个数&…

SpringBoot实现 License 认证(只校验有效期)

文章目录 一、License介绍二、授权者生成密钥对三、授权者生成license.lic证书3.1、 配置pom.xml3.2 、License生成类3.3 、License生成类需要的参数类3.4、自定义KeyStoreParam3.5、main方法生成license.lic注意事项 四、使用者配置4.1、配置pom.xml4.2、License校验类4.3、Li…

室内地图制作-电子地图管理系统源代码公开-室内地图 开源-SDK调用指南(二)

一、室内外电子地图可视化制图项目需求 室内外地图开发需满足开发者可以在Android、iOs、web应用中加入地图相关的功能&#xff0c;包括&#xff1a;地图展示、地图交互、在地图上绘制路线、POI点、搜索、AR导航、蓝牙点位、离线地图等功能。 在开源室内地图编辑-电子地图管理…

Docker安装Mysql5.7,解决无法访问DockerHub问题

Docker安装Mysql5.7&#xff0c;解决无法访问DockerHub问题 简介 Docker Hub 无法访问&#xff0c;应用安装失败&#xff0c;镜像拉取超时的解决方案。 摘要 &#xff1a; 当 Docker Hub 无法访问时&#xff0c;可以通过配置国内镜像加速来解决应用安装失败和镜像拉取超时的…

Apple Vision Pro市场表现分析:IDC最新数据揭示的真相

随着AR/VR技术逐渐成熟并被更多消费者接受,2024年第二季度(Q2)成为这一领域的一个重要转折点。根据国际数据公司(IDC)发布的最新报告,整个AR/VR市场在本季度经历了显著的增长。接下来,我们将深入探讨Apple Vision Pro在这股增长浪潮中的具体表现。 市场背景 2024年Q2,…

第五届光学与图像处理国际学术会议(ICOIP 2025)征稿中版面有限!

第五届光学与图像处理国际学术会议&#xff08;ICOIP 2025&#xff09; 2025 5th International Conference on Optics and Image Processing (ICOIP 2025&#xff09; 重要信息 时间地点&#xff1a;2025年4月25-27日丨中国西安 截稿日期&#xff1a;2024年12月16日23:59 …

maven本地打jar包依赖

本地工程的pom文件中引入了mysql依赖&#xff0c;但是在maven库中没有拉下来&#xff0c;可以到mysql官网下载jar包&#xff0c;使用maven手动打包到本地仓库中&#xff1a; 官网地址&#xff1a;MySQL :: Download MySQL Connector/J (Archived Versions) 在jar包所在位置的路…

揭开C++ STL的神秘面纱之string:提升编程效率的秘密武器

目录 &#x1f680;0.前言 &#x1f688;1.string 构造函数 &#x1f69d;1.1string构造函数 &#x1f69d;1.2string拷贝构造函数 &#x1f688;2.string类的使用 &#x1f69d;2.1.查询元素个数或空间 返回字符串中有效字符的个数&#xff1a;size lenth 返回字符串目…

AI赋能R-Meta分析核心技术:从热点挖掘到高级模型、助力高效科研与论文发表

Meta分析是针对某一科研问题&#xff0c;根据明确的搜索策略、选择筛选文献标准、采用严格的评价方法&#xff0c;对来源不同的研究成果进行收集、合并及定量统计分析的方法&#xff0c;现已广泛应用于农林生态&#xff0c;资源环境等方面&#xff0c;成为Science、Nature论文的…