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.rhkb.cn/news/456754.html

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

相关文章

学习threejs,利用THREE.ExtrudeGeometry拉伸几何体实现svg的拉伸

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.ExtrudeGeometry拉伸…

通过ssh端口反向通道建立并实现linux系统的xrdp以及web访问

Content 1 问题描述2 原因分析3 解决办法3.1 安装x11以及gnome桌面环境查看是否安装x11否则使用下面指令安装x11组件查看是否安装gnome否则使用下面指令安装gnome桌面环境 3.2 安装xrdp使用下面指令安装xrdp&#xff08;如果安装了则跳过&#xff09;启动xrdp服务 3.3 远程服务…

C2W4.LAB.Word_Embedding.Part1

理论课&#xff1a;C2W4.Word Embeddings with Neural Networks 文章目录 Word Embeddings First Steps: Data PreparationCleaning and tokenizationSliding window of wordsTransforming words into vectors for the training setMapping words to indices and indices to w…

七,Linux基础环境搭建(CentOS7)- 安装Scala和Spark

Linux基础环境搭建&#xff08;CentOS7&#xff09;- 安装Scala和Spark 大家注意以下的环境搭建版本号&#xff0c;如果版本不匹配有可能出现问题&#xff01; 一、Scala下载及安装 Scala是一门多范式的编程语言&#xff0c;一种类似java的编程语言&#xff0c;设计初衷是实现…

合并数组的两种常用方法比较

在 JavaScript 中&#xff0c;合并数组的两种常用方法是使用扩展运算符 (...) 和使用 push 方法。 使用扩展运算符 this.items [...this.items, ...data.items]; 优点&#xff1a; 易于理解&#xff1a;使用扩展运算符的语法非常直观&#xff0c;表达了“将两个数组合并成一个…

24.redis高性能

Redis的单线程和高性能 Redis是单线程吗&#xff1f; Redis 的单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的&#xff0c;这也是 Redis 对外 提供键值存储服务的主要流程。 Redis 的多线程部分&#xff0c;比如持久化、异步删除、集群数据同步等&#xff…

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

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

C#/.NET/.NET Core全面的自学入门指南

自学入门建议 确认学习目标&#xff1a;自学C#/.NET首先你需要大概了解该门语言和框架的发展、前景和基本特点&#xff0c;从自身实际情况和方向出发确认学习的必要性。 制定学习计划&#xff1a;制定一个详细的学习计划&#xff08;比如每天学习一个C#/.NET知识点、小技能&am…

【web安全】缓慢的HTTP拒绝服务攻击详解

文章目录 前言一、攻击原理二、攻击类型三、攻击特点四、HTTP慢速攻击实战工具简介使用参数介绍五、修复建议前言 缓慢的HTTP拒绝服务攻击是一种专门针对于Web的应用层拒绝服务攻击,攻击者操纵网络上的肉鸡,对目标Web服务器进行海量http request攻击,直到服务器带宽被打满,造成…

微服务网关Zuul

一、Zuul简介 Zuul是Netflix开源的微服务网关&#xff0c;包含对请求的路由和过滤两个主要功能。 1&#xff09;路由功能&#xff1a;负责将外部请求转发到具体的微服务实例上&#xff0c;是实现外部访问统一入口的基础。 2&#xff09;过滤功能&#xff1a;负责对请求的过程…

入侵检测算法平台部署LiteAIServer视频智能分析平台行人入侵检测算法

在当今科技日新月异的时代&#xff0c;行人入侵检测技术作为安全防护的重要组成部分&#xff0c;正经历着前所未有的发展。入侵检测算法平台部署LiteAIServer作为这一领域的佼佼者&#xff0c;凭借其卓越的技术实力与广泛的应用价值&#xff0c;正逐步成为守护公共安全的新利器…

R5:天气预测-探索式数据分析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、实验目的&#xff1a; 根据数据对 RainTomorrow 进行预测&#xff0c;熟悉探索式数据分析&#xff08;EDA&#xff09; 二、实验环境&#xff1a; 语言环境…

QT:MaintenanceTool 模块安装工具

QT的MaintenanceTool 工具对已安装的 Qt 进行卸载、修复等其他操作时提示At least one valid and enabled repository required for this action to succeed 解决方式&#xff1a;在设置中添加一个临时的仓库 https://mirrors.tuna.tsinghua.edu.cn/qt/online/qtsdkrepositor…

LeetCode: 3274. 检查棋盘方格颜色是否相同

一、题目 给你两个字符串 coordinate1 和 coordinate2&#xff0c;代表 8 x 8 国际象棋棋盘上的两个方格的坐标。   以下是棋盘的参考图。   如果这两个方格颜色相同&#xff0c;返回 true&#xff0c;否则返回 false。   坐标总是表示有效的棋盘方格。坐标的格式总是先…

【数据分享】全国各省份农业-瓜果类面积(1993-2018年)

数据介绍 一级标题指标名称指标全称单位指标解释农业瓜果类面积农业-瓜果类面积-瓜果类面积千公顷根据第三次全国农业普查结果&#xff0c;对2007年-2017年农业生产有关数据进行了修正。农业西瓜面积农业-瓜果类面积-西瓜面积千公顷根据第三次全国农业普查结果&#xff0c;对2…

守护头顶安全——AI高空抛物监测,让悲剧不再重演

在城市的喧嚣中&#xff0c;我们享受着高楼林立带来的便捷与繁华&#xff0c;却往往忽视了那些隐藏在高空中的危险。近日&#xff0c;震惊全国的高空抛物死刑案件被最高院核准并执行。案件中被告人多次高空抛物的举动&#xff0c;夺去了无辜者的生命&#xff0c;也让自己付出了…

django5入门【03】新建一个hello界面

文章目录 1、前提条件⭐2、操作步骤总结3、实际操作示例 1、前提条件⭐ 将上一节创建的 Django 项目导入到 PyCharm 中。 2、操作步骤总结 &#xff08;1&#xff09;在 HelloDjango/HelloDjango 目录下&#xff0c;新建一个 views.py 文件。 &#xff08;2&#xff09;在 H…

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

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

arcgis中dem转模型导入3dmax

文末分享素材 效果 1、准备数据 (1)DEM (2)DOM 2、打开arcscene软件 3、加载DEM、DOM数据 4、设置DOM的高度为DEM

yub‘s Algorithm exercise Day13

用栈实现队列 link&#xff1a;232. 用栈实现队列 - 力扣&#xff08;LeetCode&#xff09; 思路分析 首先理清楚栈和队列的异同. 队列是先进先出 栈先进后出【两者都能存储元素】 再来看peek()和poll(). 栈和队列都有peek() 可以称之为“瞄一眼”只是看一下当前栈顶/队头元…