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