STM32-Modbus协议(一文通)
Modbus协议原理
RT-Thread官方提供 FreeModbus开源。
野火有移植的例程。
QT经常用 libModbus库。
Modbus是什么?
Modbus协议,从字面理解它包括Mod和Bus两部分,首先它是一种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只是个协议,规定了数据帧的格式,你能满足它的数据帧,就能通信。
功能码
读取操作:
读线圈(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;
}
相关文章:
STM32-Modbus协议(一文通)
Modbus协议原理 RT-Thread官网开源modbus RT-Thread官方提供 FreeModbus开源。 野火有移植的例程。 QT经常用 libModbus库。 Modbus是什么? Modbus协议,从字面理解它包括Mod和Bus两部分,首先它是一种bus,即总线协议,和…...
100. 不同方向的投影视图
本节课给大家讲解,通过UI按钮界面交互改变threejs相机的观察视角。 x轴方向观察 // 通过UI按钮改变相机观察角度 document.getElementById(x).addEventListener(click, function () {camera.position.set(500, 0, 0); //x轴方向观察camera.lookAt(0, 0, 0); //重新…...
Appium中的api(三)
目录 Appium中的api(三) 1.输入和清空内容 1--输入内容 2--清空内容 2.获取文本内容 3.获取文本位置 4.获取文本的大小(即获取控件的宽和高) 5.滑动api 6.拖拽api 7.如何获取手机分辨率 8.如何截图 9.模拟按键事件api 10.操作通知栏 案例:App自动化模拟 …...
踩坑:关于使用ceph pg repair引发的业务阻塞
概述 在某次故障回溯中,发现引发集群故障,slow io,pg stuck的罪魁祸首竟是做了一次ceph pg repair $pgid。然而ceph pg repair作为使用频率极高的,用来修复pg不一致的常用手段,平时可能很少注意其使用规范和可能带来的…...
瞬间升级!电子文档华丽变身在线题库,效率翻倍✨
👋嘿小伙伴们,有个超赞的秘籍要告诉你们——土著刷题能将你的电子文档一键变身在线题库!😉 你还没发现这个宝藏功能吗?快来瞧瞧! 🌟是不是常被一堆电子版的学习资料搞得头昏脑涨,学习…...
如何动态改变本地的ip
在当今数字化时代,网络连接已成为我们日常生活和工作中不可或缺的一部分。无论是出于隐私保护、突破地域限制,还是为了测试和优化网络应用,动态改变本地IP地址的需求日益增多。本文将详细介绍如何安全、有效地实现这一目标,旨在帮…...
Spring Boot框架在中小企业设备管理中的创新应用
4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式,是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示: 图4-1系统工作原理…...
Ceph入门到精通-Osd db扩容
ceph-bluestore-tool 是一个在 BlueStore 实例上执行低级管理操作的实用程序。 以下命令可用于 ceph-bluestore-tool 语法 ceph-bluestore-tool COMMAND [ --dev DEVICE … ] [ -i OSD_ID ] [ --path OSD_PATH ] [ --out-dir DIR ] [ --log-file | -l filename ] [ --deep ]c…...
windows msvc2017 x64编译AWS SDK CPP库
在本文中,我们将介绍如何编译AWS SDK C库,以便在您的项目中使用。AWS SDK C库提供了与Amazon Web Services交互的接口,允许您在C应用程序中使用AWS服务。 一、准备工作 在开始编译AWS SDK C库之前,请确保您的系统已经安装了以下…...
铜业机器人剥片 - SNK施努卡
SNK施努卡有色行业电解车间铜业机器人剥片 铜业机器人剥片技术是针对传统人工剥片效率低下、工作环境恶劣及生产质量不稳定的痛点而发展起来的自动化解决方案。 面临人工剥片的诸多挑战,包括低效率、工作环境差、人员流动大以及产品质量控制不精确等问题。 人工剥片…...
非接触式竖向位移、水平位移视频实时在线监测的设备分类及选型
前言 视觉是人工智能正在快速发展的一个分支,简单说来,机器视觉就是用机器代替人眼来做测量和判断。在结构健康自动化监测方面,机器视觉采用光学图像结合智能算法和物联网技术,利用先进的智能靶标识别及亚像素处理等技术ÿ…...
Svelte 5 正式发布:新一代前端框架!
10 月 22 日,Svelte 5 正式发布!该版本带来的更新主要包括: 重写框架:Svelte 5 是从头开始重写的,使得应用更快、更小、更可靠,并且代码更一致和符合习惯。 向后兼容:Svelte 5 几乎完全向后兼容…...
85.【C语言】数据结构之顺序表的中间插入和删除及遍历查找
目录 3.操作顺序表 1.分析中间插入函数 函数的参数 代码示例 图片分析 main.c部分改为 在SeqList.h添加SLInsert函数的声明 运行结果 2.分析中间删除函数 函数的参数 代码示例 图片分析 main.c部分改为 在SeqList.h添加SLErase函数的声明 运行结果 承接84.【C语…...
触觉智能Purple Pi OH鸿蒙开发板成功适配OpenHarmony5.0 Release,开启新征程!
10月22日,触觉智能Purple Pi OH鸿蒙开发板迎来了重大系统版本升级,成功适配OpenHarmony5.0 Release,为嵌入式开发者和科技爱好者们带来了全新的机遇与挑战! 触觉智能 Purple Pi OH 开发板一直以来都以其高品质和超高性价比而著称。…...
分布式解决方案---分布式ID
目录 是什么 特点 全局唯一 高并发 高可用 怎么做 实现方案 是什么 分布式ID是指在分布式系统中生成的唯一标识符。由于分布式系统的特点,多个节点可能会同时生成ID,因此需要确保每个ID在整个系统中是唯一的。 重点就是唯一性!&#x…...
httpd服务
文章目录 1、搭建一个网络yum源2、基于域名访问的虚拟主机3、基于端口来访问域名4、搭建个人网站5、加密访问显示自定义网页内容 1、搭建一个网络yum源 [roottest01 conf.d]# cat repo.conf <virtualhost *:80>documentroot /var/www/html/ServerName 10.104.43.154ali…...
Linux系统安装Redis详细操作步骤(二进制发布包安装方式)
安装方式介绍 在Linux系统中,安装软件的方式主要有四种,这四种安装方式的特点如下: 安装方式特点二进制发布包安装软件已经针对具体平台编译打包发布,只要解压,修改配置即可rpm安装软件已经按照redhat的包管理规范进…...
Jenkins和Gitlab整合构建CI/CD流水线
配置环境 虚拟机建议4G起步 192.168.58.199 192.168.58.200 部署Jenkins 部署Jenkins参考这篇文章:Jenkins安装部署_connecting to pkg.jenkins.io (pkg.jenkins.io)|151.-CSDN博客 安装完毕之后根据下图操作 选择git,添加git仓库克隆url,选…...
14 C语言中的关键字
C语言中的关键字 在C语言中,关键字是一些预定义的单词,它们具有特殊的意义和用途。这些关键字不能用作标识符,比如变量名、函数名等,因为它们被保留用于特定的语言结构和操作。 关键字的分类 C语言的关键字可以分为几个主要类别…...
(11)(2.1.7) FETtec OneWire ESCs(一)
文章目录 前言 1 去哪里买 2 连接 3 组态 前言 !Note 此功能在固件版本 4.1.1 及更高版本上可用。 OneWire 是 FETtec 的 ESC 双向通信协议(ESC bi-directional communication protocol)。 FETtec OneWire ESC 的遥测信息被发送回自动驾驶仪: 电…...
Python 异步编程:使用 `asyncio.to_thread` 和 `asyncio.Queue` 处理任务队列
Python 异步编程:使用 asyncio.to_thread 和 asyncio.Queue 处理任务队列 1. 什么是 asyncio.to_thread?2. 什么是 asyncio.Queue?3. 示例代码:使用 asyncio.to_thread 和 asyncio.Queue 处理任务队列示例代码代码解释运行结果 4.…...
【问题解决】Flink在linux上运行成功但是无法访问webUI界面
一,问题 在搭建Flink的时候,已经在linux服务器上运行了./start-cluster.sh, 而且日志显示已经成功了。 服务器上也没有开启防火墙 正常来说应该能通过ip:8081来访问(8081是Flink WebUI的默认端口),但是访问的时候,显示…...
【问题解决】pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
今天配置完poetry环境变量之后pnpm不能用了 具体报错 pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 所在位置 行:1 字符: 1pnpm run dev~~~~ Ca…...
微信网页授权回调地址放多个参数的方法
https://open.weixin.qq.com/connect/oauth2/authorize?appidAPPID&redirect_uriREDIRECT_URI&response_typecode&scopeSCOPE&stateSTATE#wechat_redirect 跳转后地址 redirect_uri/?codeCODE&stateSTATE。 redirect_uri如果不进行urlencode编码, 跳转后…...
相机工作距离计算
镜头 基础参数 焦距 例如,使用1英寸靶面(12.8mm x 9.6mm)的相机,工作距离WD是300mm,视野FOV的高度是120mm,那么光学放大倍率𝛽 9.6𝑚𝑚/120𝑚𝑚0.08 ,…...
Pandas模块之垂直或水平交错条形图
目录 df.plot() 函数Pandas模块之垂直条形图Pandas模块之水平交错条形图 df.plot() 函数 df.plot() 是 Pandas 中的一个函数,用于绘制数据框中的数据。它是基于 Matplotlib 库构建的,可以轻松地创建各种类型的图表,包括折线图、柱状图、散点…...
ArcGIS必会的选择要素方法(AND、OR、R、IN等)位置选择等
今天来看看ArcGIS中的几个选择的重要使用方法 1、常规选择、 2、模糊查询、 3、组合复合条件查询(AND、OR、IN), 4、空值NULL查询 5、位置选择 推荐学习: 以梦为马,超过万名学员学习ArcGIS入门到实战的应用课程…...
快速创建一个微信小程序,详细步骤以及示范程序代码
创建一个微信小程序涉及前端和后端的搭建与联调。以下是一个快速创建微信小程序的详细步骤以及示范程序代码。 一、注册微信小程序账号 前往微信公众平台,注册一个小程序账号并完成相关设置。注册完成后,获取小程序的AppID,这是后续开发过程…...
【继承】讲解
访问控制 传递下去可以一共分为四个特性 公有保护私有存在但不可见 虽然它们各自的特性不同,能不能使用也另说,但是在建立类对象的时候,系统都会申请相应的内存,也就是说,无论它们能不能用,它们都存在。 …...
无人机之低空管控技术
无人机的低空管控技术是对低空飞行活动进行管理和控制的一系列措施和技术的总称,旨在确保低空飞行活动的安全、有序和高效。 一、主要技术手段 雷达系统监测 原理:雷达是利用电磁波探测目标的电子设备,通过发射电磁波对目标进行照射并接收…...
做网站买别人的服务器/网络推广岗位职责和任职要求
已读完,待扫描笔记内容复制上来。并找到一个手机应用程序:aTimelogger非常好用。朋友写的文章:给你的时间记记账-aTimeLogger2软件使用心得试着记录自己的主要工作和生活学习内容的用时。此图做的非常不满意。不好。...
中国建设网官方网站客服电话/北京seo网站推广
用css样式,为表格加入边框 Table 表格在没有添加 css 样式之前,是没有边框的。这样不便于我们后期合并单元格知识点的讲解,所以在这一节中我们为表格添加一些样式,为它添加边框。 在右侧代码编辑器中添加如下代码: <…...
高端做网站/上海优化公司有哪些
以前一直以为IIS应用程序的默认文档只能设置根目录下的文件,像index.html,default.aspx等,后来经同事指点,原来子目录或者子应用程序下的文件也可以添加到根应用程序的默认文档列表中。之 前为了实现这样的需求,当用户…...
武汉专业做网站/怎么才能让百度收录网站
我们有时可能需要在Java类中调用Servlet从而实现某些特殊的功能,在JavaAPI中提供了一个URL的类,其中openStream()方法可以打开URL的连接,并返回一个用于该连接读入的InputStream。二、Java中调用servlet应用举例:package com.soli…...
wordpress手机版网页/关键词首页排名代发
作为开发人员,我们依赖于静态分析工具来检查、lint(分析)和转换我们的代码。我们使用这些工具来帮助我们提高生产效率并生成更好的代码。然而,当我们使用markdown编写内容时,可用的工具就很少。在本文中,我们将介绍如何开发一个Ma…...
网站商城建设合同免费下载/建站工具
题目 一个IP地址是用四个字节(每个字节8个位)的二进制码组成。请将32位二进制码表示的IP地址转换为十进制格式表示的IP地址输出。 输入格式: 输入在一行中给出32位二进制字符串。 输出格式: 在一行中输出十进制格式的IP地址&…...