stm32-Modbus主机移植程序理解以及实战

发布于:2025-07-16 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、背景

   继上篇成功移植freemodbus主机例程之后,我要尝试运用它来实现自己想要的功能。
上篇:stm32-modbus-rs485程序移植过程

二、代码理解

(一)main()函数

例程代码

int main(void)
 {
     /* HAL库初始化 */
     HAL_Init();

     /* 系统时钟初始化 */
     SystemClock_Config();

     /* 管脚时钟初始化 */
     MX_GPIO_Init();

     /* 定时器4初始化 */
     MX_TIM4_Init();

     /* 串口2初始化在portserial.c中 */

     /* FreeModbus主机初始化 */
     eMBMasterInit(MB_RTU, MB_MASTER_USARTx, MB_MASTER_USART_BAUDRATE, MB_MASTER_USART_PARITY);

     /* 启动FreeModbus主机 */
     eMBMasterEnable();

     while (1)
     {
         /* 主机轮训 */
         eMBMasterPoll();

         /* 测试函数 通过宏定义选择哪种操作 函数在modbus_master_test.c中*/
         test(MB_USER_INPUT_REG);

         /* 延时1秒 */
         HAL_Delay(MB_POLL_CYCLE_MS);
     }
 }

功能

  在main函数中需要先初始化HAL库、系统时钟,然后初始化管脚及定时器,初始化完FreeModbus主机后就可以启动主机。 最后再循环中不断轮训主机及测试函数。

遇到的问题

  由于我的while循环中还要进行按键扫描,程序中的延时一秒导致按键不能及时响应。

解决方式

使用状态机:非阻塞方式轮询,避免 HAL_Delay 占用 CPU。

uint32_t lastPollTime = 0;
while (1) {
    if (HAL_GetTick() - lastPollTime >= MB_POLL_CYCLE_MS) {
        eMBMasterPoll();
        test(MB_USER_INPUT_REG);
        lastPollTime = HAL_GetTick();
    }
    // 其他任务...
}
分析
关键部分 作用
HAL_GetTick() 获取系统当前时间(毫秒级,通常由 SysTick 中断维护)
lastPollTime 记录上一次执行 eMBMasterPoll 的时间戳
HAL_GetTick() - lastPollTime 计算距离上次执行的时间差
>= MB_POLL_CYCLE_MS 检查是否达到设定的轮询周期(如 1000ms)
  1. 不卡死CPU
      HAL_Delay(1000) 会让 CPU 空转 1000ms,期间无法做任何事情。
      而 if (HAL_GetTick() - lastPollTime >= 1000) 只是 快速检查时间是否到期,如果没有到期,CPU 可以继续执行其他任务。
  2. 允许并行处理其他任务
  3. 适用于 RTOS 或裸机系统
      这种模式在 裸机(无操作系统) 下非常常见,可以模拟多任务。
      在 RTOS(如 FreeRTOS) 里,通常会直接用任务(Task)和定时器(Timer),但原理类似。

(二)eMBMasterPoll( void )函数

例程代码

eMBErrorCode
eMBMasterPoll( void )
{
    static UCHAR   *ucMBFrame;
    static UCHAR    ucRcvAddress;
    static UCHAR    ucFunctionCode;
    static USHORT   usLength;
    static eMBException eException;

    int             i , j;
    eMBErrorCode    eStatus = MB_ENOERR;
    eMBMasterEventType    eEvent;
    eMBMasterErrorEventType errorType;

    /* Check if the protocol stack is ready. */
    if(( eMBState != STATE_ENABLED ) && ( eMBState != STATE_ESTABLISHED))
    {
        return MB_EILLSTATE;
    }

    /* Check if there is a event available. If not return control to caller.
     * Otherwise we will handle the event. */
    if( xMBMasterPortEventGet( &eEvent ) == TRUE )
    {
        switch ( eEvent )
        {
        case EV_MASTER_READY:
            eMBState = STATE_ESTABLISHED;
            break;

        case EV_MASTER_FRAME_RECEIVED:
            eStatus = peMBMasterFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
            /* Check if the frame is for us. If not ,send an error process event. */
            if ( ( eStatus == MB_ENOERR ) && ( ucRcvAddress == ucMBMasterGetDestAddress() ) )
            {
                ( void ) xMBMasterPortEventPost( EV_MASTER_EXECUTE );
            }
            else
            {
                vMBMasterSetErrorType(EV_ERROR_RECEIVE_DATA);
                ( void ) xMBMasterPortEventPost( EV_MASTER_ERROR_PROCESS );
            }
            break;

        case EV_MASTER_EXECUTE:
            ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];
            eException = MB_EX_ILLEGAL_FUNCTION;
            /* If receive frame has exception .The receive function code highest bit is 1.*/
            if(ucFunctionCode >> 7) {
            	eException = (eMBException)ucMBFrame[MB_PDU_DATA_OFF];
            }
			else
			{
				for (i = 0; i < MB_FUNC_HANDLERS_MAX; i++)
				{
					/* No more function handlers registered. Abort. */
					if (xMasterFuncHandlers[i].ucFunctionCode == 0)	{
						break;
					}
					else if (xMasterFuncHandlers[i].ucFunctionCode == ucFunctionCode) {
						vMBMasterSetCBRunInMasterMode(TRUE);
						/* If master request is broadcast,
						 * the master need execute function for all slave.
						 */
						if ( xMBMasterRequestIsBroadcast() ) {
							usLength = usMBMasterGetPDUSndLength();
							for(j = 1; j <= MB_MASTER_TOTAL_SLAVE_NUM; j++){
								vMBMasterSetDestAddress(j);
								eException = xMasterFuncHandlers[i].pxHandler(ucMBFrame, &usLength);
							}
						}
						else {
							eException = xMasterFuncHandlers[i].pxHandler(ucMBFrame, &usLength);
						}
						vMBMasterSetCBRunInMasterMode(FALSE);
						break;
					}
				}
			}
            /* If master has exception ,Master will send error process.Otherwise the Master is idle.*/
            if (eException != MB_EX_NONE) {
            	vMBMasterSetErrorType(EV_ERROR_EXECUTE_FUNCTION);
            	( void ) xMBMasterPortEventPost( EV_MASTER_ERROR_PROCESS );
            }
            else {
            	vMBMasterCBRequestScuuess( );
            	vMBMasterRunResRelease( );
            }
            break;

        case EV_MASTER_FRAME_SENT:
        	/* Master is busy now. */
        	vMBMasterGetPDUSndBuf( &ucMBFrame );
			eStatus = peMBMasterFrameSendCur( ucMBMasterGetDestAddress(), ucMBFrame, usMBMasterGetPDUSndLength() );
            break;

        case EV_MASTER_ERROR_PROCESS:
        	/* Execute specified error process callback function. */
			errorType = eMBMasterGetErrorType();
			vMBMasterGetPDUSndBuf( &ucMBFrame );
			switch (errorType) {
			case EV_ERROR_RESPOND_TIMEOUT:
				vMBMasterErrorCBRespondTimeout(ucMBMasterGetDestAddress(),
						ucMBFrame, usMBMasterGetPDUSndLength());
				break;
			case EV_ERROR_RECEIVE_DATA:
				vMBMasterErrorCBReceiveData(ucMBMasterGetDestAddress(),
						ucMBFrame, usMBMasterGetPDUSndLength());
				break;
			case EV_ERROR_EXECUTE_FUNCTION:
				vMBMasterErrorCBExecuteFunction(ucMBMasterGetDestAddress(),
						ucMBFrame, usMBMasterGetPDUSndLength());
				break;
			}
			vMBMasterRunResRelease();
        	break;
            
        default:
            break;
        }

    }
    return MB_ENOERR;

1. 变量声明

static UCHAR   *ucMBFrame;           // 指向当前处理的Modbus帧的指针
static UCHAR    ucRcvAddress;        // 接收到的帧的从站地址
static UCHAR    ucFunctionCode;      // 接收到的功能码
static USHORT   usLength;            // 帧长度
static eMBException eException;      // 异常代码
int             i , j;               // 循环变量
eMBErrorCode    eStatus = MB_ENOERR; // 错误状态,初始为无错误
eMBMasterEventType    eEvent;        // 事件类型
eMBMasterErrorEventType errorType;   // 错误事件类型
  • 静态变量用于在多次调用之间保持状态,例如帧指针、地址、功能码等。
  • 局部变量用于临时存储和循环。

2. 协议栈状态检查

if(( eMBState != STATE_ENABLED ) && ( eMBState != STATE_ESTABLISHED))
{
    return MB_EILLSTATE;
}
  • 检查主站状态(eMBState),如果不在ENABLEDESTABLISHED状态,则返回错误MB_EILLSTATE(非法状态)。

3. 获取事件

if( xMBMasterPortEventGet( &eEvent ) == TRUE )
{
    // 事件处理
}
  • 调用xMBMasterPortEventGet获取事件,如果有事件,则进入事件处理分支。

4. 事件处理(switch-case)

4.1 EV_MASTER_READY事件
case EV_MASTER_READY:
    eMBState = STATE_ESTABLISHED;
    break;
  • 当主站准备好时,将状态设置为ESTABLISHED(已建立连接)。
4.2 EV_MASTER_FRAME_RECEIVED事件(接收到一帧数据)
case EV_MASTER_FRAME_RECEIVED:
    eStatus = peMBMasterFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
    if ( ( eStatus == MB_ENOERR ) && ( ucRcvAddress == ucMBMasterGetDestAddress() ) )
    {
        ( void ) xMBMasterPortEventPost( EV_MASTER_EXECUTE );
    }
    else
    {
        vMBMasterSetErrorType(EV_ERROR_RECEIVE_DATA);
        ( void ) xMBMasterPortEventPost( EV_MASTER_ERROR_PROCESS );
    }
    break;
  • 调用peMBMasterFrameReceiveCur接收当前帧,获取从站地址、帧数据和长度。
  • 如果接收成功且地址匹配(是发给本主站的),则发送EV_MASTER_EXECUTE事件(执行功能)。
  • 否则,设置错误类型为EV_ERROR_RECEIVE_DATA(接收数据错误),并发送EV_MASTER_ERROR_PROCESS事件(错误处理)。
4.3 EV_MASTER_EXECUTE事件(执行功能)
case EV_MASTER_EXECUTE:
    ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF]; // 从帧中获取功能码
    eException = MB_EX_ILLEGAL_FUNCTION; // 默认异常为非法功能
    // 检查功能码最高位是否为1(表示从站返回异常)
    if(ucFunctionCode >> 7) {
        eException = (eMBException)ucMBFrame[MB_PDU_DATA_OFF]; // 异常码在数据区第一个字节
    }
    else
    {
        // 遍历已注册的功能处理函数
        for (i = 0; i < MB_FUNC_HANDLERS_MAX; i++)
        {
            if (xMasterFuncHandlers[i].ucFunctionCode == 0) {
                break; // 遇到0表示结束,没有找到对应的功能码处理函数
            }
            else if (xMasterFuncHandlers[i].ucFunctionCode == ucFunctionCode) {
                vMBMasterSetCBRunInMasterMode(TRUE); // 设置回调运行在主站模式
                // 检查当前请求是否是广播(广播地址为0)
                if ( xMBMasterRequestIsBroadcast() ) {
                    usLength = usMBMasterGetPDUSndLength(); // 获取发送PDU长度
                    // 遍历所有从站(从1到最大从站数)
                    for(j = 1; j <= MB_MASTER_TOTAL_SLAVE_NUM; j++){
                        vMBMasterSetDestAddress(j); // 设置目标从站地址
                        eException = xMasterFuncHandlers[i].pxHandler(ucMBFrame, &usLength); // 执行处理函数
                    }
                }
                else {
                    eException = xMasterFuncHandlers[i].pxHandler(ucMBFrame, &usLength); // 执行处理函数
                }
                vMBMasterSetCBRunInMasterMode(FALSE); // 清除主站模式标志
                break;
            }
        }
    }
    // 根据执行结果处理异常
    if (eException != MB_EX_NONE) {
        vMBMasterSetErrorType(EV_ERROR_EXECUTE_FUNCTION); // 设置错误类型为执行功能错误
        ( void ) xMBMasterPortEventPost( EV_MASTER_ERROR_PROCESS ); // 发送错误处理事件
    }
    else {
        vMBMasterCBRequestScuuess( ); // 请求成功回调
        vMBMasterRunResRelease( );   // 释放资源
    }
    break;
  • 从接收到的帧中提取功能码。
  • 如果功能码最高位为1,表示从站返回异常,从数据区读取异常码。
  • 否则,在注册的功能处理函数中查找匹配的功能码。
    • 如果找到,则根据请求类型(广播/单播)执行处理函数:
      • 广播:遍历所有从站地址,对每个从站执行处理函数。
      • 单播:执行一次处理函数。
  • 如果执行过程中出现异常(eException != MB_EX_NONE),则触发错误处理流程。
  • 如果成功,则调用成功回调和释放资源。
4.4 EV_MASTER_FRAME_SENT事件(一帧数据发送完成)
case EV_MASTER_FRAME_SENT:
    vMBMasterGetPDUSndBuf( &ucMBFrame ); // 获取发送缓冲区指针
    eStatus = peMBMasterFrameSendCur( ucMBMasterGetDestAddress(), ucMBFrame, usMBMasterGetPDUSndLength() ); // 发送当前帧
    break;
  • 获取发送缓冲区的指针,然后调用发送函数发送数据。
4.5 EV_MASTER_ERROR_PROCESS事件(处理错误)
case EV_MASTER_ERROR_PROCESS:
    errorType = eMBMasterGetErrorType(); // 获取错误类型
    vMBMasterGetPDUSndBuf( &ucMBFrame ); // 获取发送缓冲区指针
    // 根据错误类型调用不同的错误回调函数
    switch (errorType) {
    case EV_ERROR_RESPOND_TIMEOUT:
        vMBMasterErrorCBRespondTimeout(ucMBMasterGetDestAddress(),
                ucMBFrame, usMBMasterGetPDUSndLength());
        break;
    case EV_ERROR_RECEIVE_DATA:
        vMBMasterErrorCBReceiveData(ucMBMasterGetDestAddress(),
                ucMBFrame, usMBMasterGetPDUSndLength());
        break;
    case EV_ERROR_EXECUTE_FUNCTION:
        vMBMasterErrorCBExecuteFunction(ucMBMasterGetDestAddress(),
                ucMBFrame, usMBMasterGetPDUSndLength());
        break;
    }
    vMBMasterRunResRelease(); // 释放资源
    break;
  • 根据错误类型(响应超时、接收数据错误、执行功能错误)调用相应的错误处理回调函数。
  • 最后释放资源。
4.6 默认情况
default:
    break;
  • 对于其他未处理的事件,不进行任何操作。

5. 返回状态

return MB_ENOERR;
  • 函数最后返回无错误状态(MB_ENOERR),即使之前处理中可能有错误,但错误已经通过事件处理,所以这里总是返回成功。

总结

这个函数是Modbus主站的核心事件处理循环,它处理以下事件:

  • 准备就绪(READY
  • 接收到帧(FRAME_RECEIVED
  • 执行功能(EXECUTE
  • 发送完成(FRAME_SENT
  • 错误处理(ERROR_PROCESS
    函数通过状态机和事件驱动机制,实现了Modbus主站的通信流程。注意,函数中使用了多个静态变量来保存帧处理过程中的状态,这些状态在事件之间传递信息。

(三)void test(char MB)函数

例程代码

/**
  * @brief  测试程序
  * @param  功能选择
  * @retval 无
  */
void test(char MB)
{
	USHORT Hlod_buff[4];
	UCHAR	Coils[4]={1,0,1,0};
	
	Hlod_buff[0] = HAL_GetTick() & 0xff;		           //获取时间戳 提出1至8位
	Hlod_buff[1] = (HAL_GetTick() & 0xff00) >> 8;      //获取时间戳 提出9至16位
	Hlod_buff[2] = (HAL_GetTick() & 0xff0000) >> 16 ;  //获取时间戳 提出17至24位
	Hlod_buff[3] = (HAL_GetTick() & 0xff000000) >> 24; //获取时间戳 提出25至32位
	
	/* 注:各操作的API在mb_m.h中 */
	switch(MB)
	{
		case MB_USER_HOLD: 
					/* 写多个保持寄存器值 */
					eMBMasterReqWriteMultipleHoldingRegister(MB_SAMPLE_TEST_SLAVE_ADDR, //从机设备地址
																									 MB_REG_START, 							//数据起始位置
																									 MB_SEND_REG_NUM, 					//写数据总数
																									 Hlod_buff, 								//数据
																									 WAITING_FOREVER);					//永久等待
		break;
		
		case MB_USER_COILS:
					/* 写多个线圈 */
					eMBMasterReqWriteMultipleCoils(MB_SAMPLE_TEST_SLAVE_ADDR, //从机设备地址
																				 MB_REG_START, 							//数据起始位置
																				 MB_SEND_REG_NUM, 					//写数据总数
																				 Coils, 										//数据
																				 WAITING_FOREVER);					//永久等待
		break;
		
		case MB_USER_INPUT_REG:
					/* 读输入寄存器 */
					eMBMasterReqReadInputRegister(MB_SAMPLE_TEST_SLAVE_ADDR,	//从机设备地址
																				MB_REG_START,               //数据起始位置
																				MB_READ_REG_NUM,						//读数据总数
																				WAITING_FOREVER);						//永久等待
		break;
	}
}	

这段代码是一个测试函数,用于演示Modbus主站如何执行不同的Modbus操作。函数根据传入的参数MB选择执行写保持寄存器、写线圈或读输入寄存器操作。下面逐行解释:

void test(char MB)
{
    // 定义数组用于存储保持寄存器数据(每个元素为16位)
    USHORT Hlod_buff[4];
    // 定义线圈数组(每个元素表示一个线圈状态,0或1),初始化为{1,0,1,0}
    UCHAR Coils[4]={1,0,1,0};

变量说明

  • Hlod_buff[4]:用于存储保持寄存器数据的数组,每个元素是一个16位整数。
  • Coils[4]:用于存储线圈状态的数组,每个元素是一个字节(但通常只使用最低位)。

    // 将当前系统时间戳(32位)拆分成4个16位整数存入Hlod_buff
    Hlod_buff[0] = HAL_GetTick() & 0xff;           // 提取最低8位(0-7位)
    Hlod_buff[1] = (HAL_GetTick() & 0xff00) >> 8;      // 提取次低8位(8-15位)
    Hlod_buff[2] = (HAL_GetTick() & 0xff0000) >> 16 ;  // 提取次高8位(16-23位)
    Hlod_buff[3] = (HAL_GetTick() & 0xff000000) >> 24; // 提取最高8位(24-31位)

时间戳拆分

  • HAL_GetTick()返回一个32位无符号整数(毫秒级时间)。
  • 通过位掩码和移位操作,将32位时间戳拆分成4个8位部分,并分别存入Hlod_buff的4个元素中(每个元素为16位,但高8位为0)。

    // 根据传入的MB参数选择操作
    switch(MB)
    {
        case MB_USER_HOLD: 
            // 写多个保持寄存器
            eMBMasterReqWriteMultipleHoldingRegister(
                MB_SAMPLE_TEST_SLAVE_ADDR, // 目标从站地址(宏定义)
                MB_REG_START,              // 起始寄存器地址(宏定义)
                MB_SEND_REG_NUM,           // 要写入的寄存器数量(宏定义)
                Hlod_buff,                 // 数据缓冲区指针
                WAITING_FOREVER            // 超时设置(永久等待)
            );
            break;

写多个保持寄存器(功能码0x10)

  • 调用函数eMBMasterReqWriteMultipleHoldingRegister向从站写入多个保持寄存器。
  • 参数说明:
    1. 从站地址:MB_SAMPLE_TEST_SLAVE_ADDR(通常为1-247)
    2. 起始地址:MB_REG_START(如0表示从0号寄存器开始)
    3. 寄存器数量:MB_SEND_REG_NUM(这里为4,因为Hlod_buff有4个元素)
    4. 数据源:Hlod_buff数组(包含拆分后的时间戳)
    5. 超时:WAITING_FOREVER(无限等待从站响应)

        case MB_USER_COILS:
            // 写多个线圈
            eMBMasterReqWriteMultipleCoils(
                MB_SAMPLE_TEST_SLAVE_ADDR, // 目标从站地址
                MB_REG_START,              // 起始线圈地址
                MB_SEND_REG_NUM,           // 要写入的线圈数量(宏定义,这里为4)
                Coils,                     // 线圈状态数组
                WAITING_FOREVER            // 永久等待
            );
            break;

写多个线圈(功能码0x0F)

  • 调用函数eMBMasterReqWriteMultipleCoils向从站写入多个线圈状态。
  • 参数说明:
    1. 从站地址:同上
    2. 起始地址:MB_REG_START(线圈起始地址)
    3. 线圈数量:MB_SEND_REG_NUM(这里为4)
    4. 数据源:Coils数组(值为{1,0,1,0})
    5. 超时:永久等待

        case MB_USER_INPUT_REG:
            // 读输入寄存器
            eMBMasterReqReadInputRegister(
                MB_SAMPLE_TEST_SLAVE_ADDR, // 目标从站地址
                MB_REG_START,              // 起始输入寄存器地址
                MB_READ_REG_NUM,           // 要读取的寄存器数量(宏定义)
                WAITING_FOREVER            // 永久等待
            );
            break;
    }
}

读输入寄存器(功能码0x04)

  • 调用函数eMBMasterReqReadInputRegister从从站读取输入寄存器。
  • 参数说明:
    1. 从站地址:同上
    2. 起始地址:MB_REG_START
    3. 寄存器数量:MB_READ_REG_NUM(宏定义,未在代码中显示具体值)
    4. 超时:永久等待

关键点说明:

  1. 功能选择:通过传入的MB参数(MB_USER_HOLDMB_USER_COILSMB_USER_INPUT_REG)选择要测试的Modbus功能。
  2. 数据准备
    • 写保持寄存器:使用系统时间戳拆分后的4个16位整数。
    • 写线圈:使用预定义的数组{1,0,1,0}
  3. 超时处理:所有操作都设置为WAITING_FOREVER,这意味着主站会一直等待从站响应,直到收到响应或发生错误(如超时错误)。在实际应用中,可能需要设置合理的超时时间。
  4. 宏定义:代码中使用了多个宏(如MB_SAMPLE_TEST_SLAVE_ADDRMB_REG_START等),这些宏应在其他地方定义,用于配置测试参数。

(四)test(MB_USER_HOLD);

这个 test(MB_USER_HOLD) 函数调用在 Modbus 主站系统中执行一个 写多个保持寄存器(Write Multiple Holding Registers) 操作,具体作用和实现原理如下:


函数作用

test(MB_USER_HOLD) 会向指定的 Modbus 从站设备写入 4 个保持寄存器的值,这些值是当前系统时间戳(HAL_GetTick())的拆分形式。


详细执行流程

1. 准备写入数据
USHORT Hlod_buff[4];
Hlod_buff[0] = HAL_GetTick() & 0xff;          // 时间戳低 8 位
Hlod_buff[1] = (HAL_GetTick() & 0xff00) >> 8; // 时间戳次低 8 位
Hlod_buff[2] = (HAL_GetTick() & 0xff0000) >> 16; // 时间戳次高 8 位
Hlod_buff[3] = (HAL_GetTick() & 0xff000000) >> 24; // 时间戳高 8 位
  • 将 32 位时间戳拆分为 4 个 16 位寄存器值
  • 目的:测试数据随时间变化,便于调试和验证通信正确性
2. 执行 Modbus 写操作
eMBMasterReqWriteMultipleHoldingRegister(
    MB_SAMPLE_TEST_SLAVE_ADDR, // 目标从站地址
    MB_REG_START,              // 起始寄存器地址
    MB_SEND_REG_NUM,           // 写入寄存器数量
    Hlod_buff,                 // 写入的数据
    WAITING_FOREVER            // 超时设置(永久等待)
);
  • 功能码0x10 (写多个保持寄存器)
  • 操作:向从站的保持寄存器区域写入数据
  • 参数解析
    参数 说明
    MB_SAMPLE_TEST_SLAVE_ADDR 目标从站设备地址
    MB_REG_START 写入的起始寄存器地址(如 40001)
    MB_SEND_REG_NUM 写入的寄存器数量(应为 4)
    Hlod_buff 包含时间戳的 4 个寄存器值
    WAITING_FOREVER 无限等待从站响应

Modbus 协议层行为

当调用 test(MB_USER_HOLD) 时,底层会生成并发送以下格式的 Modbus 请求帧:

[从站地址] [功能码 0x10] [起始地址高] [起始地址低] 
[寄存器数高] [寄存器数低] [字节数] [数据1高] [数据1低] ... [CRC16]

示例(假设时间戳 0x12345678):

01 10 00 00 00 04 08 00 78 00 56 00 34 00 12 XX XX
  • 01:从站地址 1
  • 10:写多个保持寄存器功能码
  • 00 00:起始地址 40001
  • 00 04:写入 4 个寄存器
  • 08:后续数据字节数(4寄存器×2字节=8)
  • 00 78:Hlod_buff[0] = 0x0078
  • 00 56:Hlod_buff[1] = 0x0056
  • 00 34:Hlod_buff[2] = 0x0034
  • 00 12:Hlod_buff[3] = 0x0012
  • XX XX:CRC 校验

为什么用保持寄存器?

  • 保持寄存器(Holding Registers) 是 Modbus 中可读可写的寄存器类型(地址范围 4xxxx)
  • 相比线圈(Coils) 只能存储位数据,保持寄存器可存储 16 位数据
  • 相比输入寄存器(Input Registers) 是只读的,保持寄存器允许双向读写

modbus slave的通信现象

通信
从 ModSlave 捕获的通信数据来看,通信完全正常且符合预期。这是典型的 Modbus RTU 主站(STM32)与从站(ModSlave)之间的写保持寄存器操作。以下是详细分析:


通信数据解析(第一条记录为例)

主站请求(Rx 表示从站接收到的数据)
Rx: 01 10 00 01 00 04 08 00 27 00 30 00 37 00 00 ED 71
字段 说明
从站地址 01 设备地址 1
功能码 10 写多个保持寄存器 (0x10)
起始地址 00 01 寄存器 40002 (0x0001)
寄存器数 00 04 写入 4 个寄存器
字节数 08 后续 8 字节数据
数据 1 00 27 寄存器 40002 = 0x0027 (39)
数据 2 00 30 寄存器 40003 = 0x0030 (48)
数据 3 00 37 寄存器 40004 = 0x0037 (55)
数据 4 00 00 寄存器 40005 = 0x0000 (0)
CRC ED 71 校验正确
从站响应(Tx 表示从站发送的数据)
Tx: 01 10 00 01 00 04 90 0A
字段 说明
从站地址 01 设备地址 1
功能码 10 写多个保持寄存器 (0x10)
起始地址 00 01 寄存器 40002 (0x0001)
寄存器数 00 04 成功写入 4 个寄存器
CRC 90 0A 校验正确

响应码 90 0A 表示操作成功(功能码高位未置 1,无异常)


时间戳数据分析

数据中的 00 27 00 30 00 37 00 00 对应 HAL_GetTick() 的拆分值:

Hlod_buff[0] = tick & 0xFF;         // 0x27 (39)  → 时间戳低 8 位
Hlod_buff[1] = (tick >> 8) & 0xFF;  // 0x30 (48)  → 时间戳次低 8 位
Hlod_buff[2] = (tick >> 16) & 0xFF; // 0x37 (55)  → 时间戳次高 8 位
Hlod_buff[3] = (tick >> 24) & 0xFF; // 0x00 (0)   → 时间戳高 8 位
时间戳还原示例
记录 寄存器值 组合值 时间戳 (ms) 时间间隔
1 0x0027, 0x0030, 0x0037, 0x0000 0x00003730 14,112 基准
2 0x00F8, 0x0037, 0x0037, 0x0000 0x00003737 14,135 +23ms
3 0x00C9, 0x003F, 0x0037, 0x0000 0x0000373F 14,143 +8ms
4 0x009A, 0x0047, 0x0037, 0x0000 0x00003747 14,151 +8ms

时间间隔 ≈ 8ms,符合 Modbus RTU 的帧间隔要求(T3.5 ≈ 4ms @9600bps)


通信流程正确性验证

  1. 主从角色正确

    • 主站(STM32)主动发送写请求 (01 10...)
    • 从站(ModSlave)返回成功响应 (01 10 00 01 00 04 90 0A)
  2. 协议完整性

    • 所有请求都有对应响应
    • CRC 校验全部通过(无 15 10 等异常响应)
  3. 数据一致性

    • 请求中的寄存器地址/数量与响应完全一致
    • 从站正确执行了写操作

特别注意事项

  1. 固定高位字节问题

    • 所有记录的 Hlod_buff[2] = 0x0037 (55) 和 Hlod_buff[3] = 0x0000
    • 原因:HAL_GetTick() 值较小(约 14 秒),高位尚未变化
    • 解决方案:长时间运行后会自然变化(如 65 秒后 Hlod_buff[2] 将递增)
  2. 从站寄存器映射验证
    在 ModSlave 中检查寄存器值是否更新:

    40002: 0x0027 → 39
    40003: 0x0030 → 48
    40004: 0x0037 → 55
    40005: 0x0000 → 0
    

    应与主站发送数据完全一致。

我的

目的

  将vi_config.V_value和vi_config.I_value的值发送到从机。

typedef struct {
    int32_t V_value; // 电压值
    int32_t I_value; //电流值
} VIConfig;

思路

要将 vi_config 结构体中的 V_valueI_value 发送到 Modbus 从机,需要将 32 位整数值拆分为两个 16 位寄存器值。以下是完整的实现代码:

/* 新增操作类型 */
#define MB_USER_SEND_VI 4  // 发送电压电流值

/* 在 test() 函数中添加新的 case */
void test(char MB)
{
    USHORT Hlod_buff[4];
    UCHAR   Coils[4] = {1, 0, 1, 0};
    static VIConfig vi_config;  // 存储电压电流配置
    
    // 初始化示例值(实际应用中从传感器获取)
    vi_config.V_value = 22000;  // 22.00V * 1000
    vi_config.I_value = 5000;   // 5.00A * 1000

    Hlod_buff[0] = HAL_GetTick() & 0xff;
    Hlod_buff[1] = (HAL_GetTick() & 0xff00) >> 8;
    Hlod_buff[2] = (HAL_GetTick() & 0xff0000) >> 16;
    Hlod_buff[3] = (HAL_GetTick() & 0xff000000) >> 24;

    switch(MB)
    {
        case MB_USER_HOLD:
            eMBMasterReqWriteMultipleHoldingRegister(MB_SAMPLE_TEST_SLAVE_ADDR,
                                                     MB_REG_START,
                                                     MB_SEND_REG_NUM,
                                                     Hlod_buff,
                                                     WAITING_FOREVER);
            break;
            
        case MB_USER_COILS:
            eMBMasterReqWriteMultipleCoils(MB_SAMPLE_TEST_SLAVE_ADDR,
                                           MB_REG_START,
                                           MB_SEND_REG_NUM,
                                           Coils,
                                           WAITING_FOREVER);
            break;
            
        case MB_USER_INPUT_REG:
            eMBMasterReqReadInputRegister(MB_SAMPLE_TEST_SLAVE_ADDR,
                                          MB_REG_START-1,
                                          MB_READ_REG_NUM-2,
                                          WAITING_FOREVER);
            break;
            
        // 新增的电压电流发送功能
        case MB_USER_SEND_VI:
        {
            USHORT vi_buff[4];  // 4个寄存器:V_value低16位, V_value高16位, I_value低16位, I_value高16位
            
            /* 拆分电压值到两个寄存器 */
            vi_buff[0] = (USHORT)(vi_config.V_value & 0xFFFF);         // 低16位
            vi_buff[1] = (USHORT)((vi_config.V_value >> 16) & 0xFFFF); // 高16位
            
            /* 拆分电流值到两个寄存器 */
            vi_buff[2] = (USHORT)(vi_config.I_value & 0xFFFF);         // 低16位
            vi_buff[3] = (USHORT)((vi_config.I_value >> 16) & 0xFFFF); // 高16位
            
            /* 发送到从站保持寄存器 */
            eMBMasterReqWriteMultipleHoldingRegister(MB_SAMPLE_TEST_SLAVE_ADDR,
                                                     MB_REG_START,  // 起始地址
                                                     4,             // 4个寄存器
                                                     vi_buff,        // 数据
                                                     1000);         // 1秒超时
        }
        break;
    }
}

/* 在主循环中调用 */
uint32_t lastPollTime = 0;
while (1) {
    if (HAL_GetTick() - lastPollTime >= MB_POLL_CYCLE_MS) {
        eMBMasterPoll();
        test(MB_USER_SEND_VI);  // 发送电压电流值
        lastPollTime = HAL_GetTick();
    }
    // 其他任务...
}

关键实现细节:

  1. 数据结构拆分

    // 32位电压值拆分为两个16位寄存器
    vi_buff[0] = (USHORT)(vi_config.V_value & 0xFFFF);         // 低16位
    vi_buff[1] = (USHORT)((vi_config.V_value >> 16) & 0xFFFF); // 高16位
    
    // 32位电流值拆分为两个16位寄存器
    vi_buff[2] = (USHORT)(vi_config.I_value & 0xFFFF);         // 低16位
    vi_buff[3] = (USHORT)((vi_config.I_value >> 16) & 0xFFFF); // 高16位
    
  2. Modbus 寄存器映射

    寄存器地址 对应值 说明
    40002 vi_buff[0] 电压值低16位
    40003 vi_buff[1] 电压值高16位
    40004 vi_buff[2] 电流值低16位
    40005 vi_buff[3] 电流值高16位
  3. 通信示例
    假设:

    • V_value = 22000 (0x000055F0)
    • I_value = 5000 (0x00001388)

    生成的 Modbus 帧:

    01 10 00 01 00 04 08 F0 55 00 00 88 13 00 00 [CRC]
    
    • F0 55:电压低16位 (0x55F0)
    • 00 00:电压高16位 (0x0000)
    • 88 13:电流低16位 (0x1388)
    • 00 00:电流高16位 (0x0000)

从机端还原数据:

在 Modbus 从机(如 ModSlave)中,使用以下逻辑还原数据:

// 伪代码示例
int32_t V_value = (registers[40003] << 16) | registers[40002];
int32_t I_value = (registers[40005] << 16) | registers[40004];

// 转换为实际物理值(假设存储时放大了1000倍)
float actual_voltage = V_value / 1000.0f;  // 22.000 V
float actual_current = I_value / 1000.0f;  // 5.000 A

重要注意事项:

  1. 字节序问题

    • 当前使用小端序(低字节在前)
    • 如果从机使用大端序系统,需要调整解析方式:
      // 大端序解析
      int32_t V_value = (registers[40002] << 16) | registers[40003];
      
  2. 数据类型转换

    • 如果值可能是负数,使用带符号类型:
      int32_t V_value = (int32_t)((registers[40003] << 16) | registers[40002]);
      
  3. 超时处理

    1000 // 1秒超时(优于永久等待)
    
  4. 值范围处理

    • 如果电压/电流值可能超过 16 位范围(±32,767),必须使用 32 位拆分
    • 对于更大的值(如 ±2,147,483,647),当前 32 位格式已足够

结果

  例如,现在uint32_t V_value=58000,转换成16进制0x 0000 E290 , uint32_t I_value=75000,转换成16进制0x 0001 24F8, 32位值拆分为两个16位,发到2-5
在这里插入图片描述

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到