STM32-Modbus协议(一文通)

Modbus协议原理

RT-Thread官网开源modbus

RT-Thread官方提供 FreeModbus开源。

野火有移植的例程。

QT经常用 libModbus库。

Modbus是什么?

Modbus协议,从字面理解它包括ModBus两部分,首先它是一种bus ,即总线协议,和I2CSPI类似,总线就意味着有主机,有从机,这些设备在同一条总线上。

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)

cpp 复制代码
发送请求帧格式:
[从站地址] [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)

cpp 复制代码
发送请求帧:
[从站地址] [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)

cpp 复制代码
发送请求帧格式:
[从站地址] [0x05] [目标地址高] [目标地址低] [要写入的值] [校验码]
(要写入的值通常为00或FF表示OFF或ON)
01 05 00 00 FF 00 CRC
(假设从站地址为01,目标地址为0000,写入的值为ON/开)

返回响应帧格式:
[从站地址] [0x05] [目标地址高] [目标地址低] [写入的值] [校验码]
(写入成功后,从站通常返回与请求相同的帧,但实际应用中可能返回其他格式的响应帧)
01 05 00 00 FF 00 CRC
(写入成功后,从站通常返回与请求相同的帧作为响应,但实际应用中可能有所不同)

写单个寄存器(0x06)

cpp 复制代码
[从站地址] [0x06] [目标地址高] [目标地址低] [要写入的数据高] [要写入的数据低] [校验码]
发送请求帧:01 06 00 00 00 13 CRC
(假设从站地址为01,目标地址为0000,写入的数据值为0013)

[从站地址] [0x06] [目标地址高] [目标地址低] [写入的数据高] [写入的数据低] [校验码]
返回响应帧:01 06 00 00 00 13 CRC
(写入成功后,从站通常返回与请求相同的帧作为响应,但实际应用中可能有所不同)

写多个线圈(0x0F)

cpp 复制代码
[从站地址] [0x0F] [起始地址高] [起始地址低] 
              [要写入的线圈数量高] [要写入的线圈数量低] [字节数] [线圈状态数据...] [校验码]
发送请求帧:01 0F 00 00 00 02 01 01 CRC
(假设从站地址为01,起始地址为0000,写入2个线圈,第一个线圈ON,第二个线圈OFF)

[从站地址] [0x0F] [起始地址高] [起始地址低] [写入的线圈数量高] [写入的线圈数量低] [校验码]
返回响应帧:01 0F 00 00 00 02 CRC
(写入成功后,从站返回包含起始地址和写入数量的响应帧,但实际应用中可能有所不同)

写多个寄存器(0x10)

cpp 复制代码
[从站地址] [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函数

cpp 复制代码
/* 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
cpp 复制代码
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 函数功能简述:

参数验证:检查从设备地址是否有效。
模式选择:根据通信模式设置函数指针。
初始化:调用对应模式的初始化函数配置通信参数。
事件初始化:初始化端口事件模块以处理通信事件。
状态设置:成功初始化后,设置模块为禁用状态。
返回状态:返回初始化结果的状态码。
*/
cpp 复制代码
/*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 
cpp 复制代码
/*
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。

cpp 复制代码
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )		//定时器初始化直接返回TRUE,已经在mian函数初始化过
{
    return TRUE;
}

实际的设置代码,野火原版是hal库的,我这里给个标准库的参考版本:

cpp 复制代码
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  
}
cpp 复制代码
/*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 定时器的到期情况更新接收状态、模拟时间队列发布相应事件,并禁用计时器。

cpp 复制代码
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;  
}
cpp 复制代码
/*模拟事件上报*/
BOOL xMBPortEventPost( eMBEventType eEvent )  
{  
    // 设置事件在队列中的标志为TRUE  
    xEventInQueue = TRUE;        //注意这里不是真实的队列,只是个bool模拟队列状态
    // 保存传入的事件类型  
    eQueuedEvent = eEvent;  
    // 返回TRUE表示事件成功发布  
    return TRUE;  
}
eMBpoll

main函数while里面还有个 eMBpoll()从机轮询。

此函数是Modbus协议栈中的轮询函数,负责处理协议栈中的事件。

它首先检查协议栈是否准备就绪,然后检查是否有事件可用(参考定时器回调的模拟事件)。

若有事件,将根据事件类型执行相应的操作,如接收帧、执行功能码处理或发送回复帧等。

函数通过静态变量和局部变量来存储和处理接收到的帧、地址、功能码、异常等信息,并根据需要调用其他函数来执行具体的操作。

最后,函数返回无错误状态。

cpp 复制代码
/*从机轮询*/
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;  // 函数返回无错误状态  
}
串口数据帧接收/发送
cpp 复制代码
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();//发送,函数指针
		}
    ...
}
cpp 复制代码
/*真实的发送*/
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;  // 返回是否需要轮询的标志  
}

最后被串口中断调用的,串口接收函数。

cpp 复制代码
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数据,否则设置错误码。

cpp 复制代码
#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 的偏移量
cpp 复制代码
/*该函数将数据存放在数组中,并返回从站存储位置,帧存储位置,帧长度*/
eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength ); 
cpp 复制代码
/*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;  
}
相关推荐
致奋斗的我们7 分钟前
RHCE的学习(7)
linux·服务器·网络·学习·redhat·rhce·rhcsa
Chris-zz1 小时前
Linux:磁盘深潜:探索文件系统、连接之道与库的奥秘
linux·网络·c++·1024程序员节
TeYiToKu1 小时前
笔记整理—linux驱动开发部分(1)驱动梗概
linux·c语言·arm开发·驱动开发·嵌入式硬件
黑龙江亿林等级保护测评1 小时前
等保行业如何面对新兴安全威胁
网络·安全·金融·智能路由器·ddos
墨染新瑞1 小时前
C语言——网络编程(下)
网络
Whappy0012 小时前
3. STM32之TIM实验--输出比较(PWM输出,电机,四轴飞行器,智能车,机器人)--(实验1:PWM驱动LED呼吸灯)
stm32·嵌入式硬件·机器人
菜林子2 小时前
【Linux | 网络I/O模型】五种网络I/O模型详解
linux·服务器·网络
IT·小灰灰2 小时前
Python——自动化发送邮件
运维·网络·后端·python·自动化
很透彻2 小时前
【网络】传输层协议UDP
网络·网络协议·udp
想拿 0day 的脚步小子2 小时前
6.stm32 OLED显示屏
stm32