中国肩章,成都有实力的seo团队,装饰设计有限公司经营范围,建设通手机版Modbus协议原理 RT-Thread官网开源modbus RT-Thread官方提供 FreeModbus开源。 野火有移植的例程。 QT经常用 libModbus库。
Modbus是什么#xff1f; Modbus协议#xff0c;从字面理解它包括Mod和Bus两部分#xff0c;首先它是一种bus#xff0c;即总线协议#xff0c;和…Modbus协议原理 RT-Thread官网开源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只是个协议规定了数据帧的格式你能满足它的数据帧就能通信。
功能码 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;
}