当前位置: 首页 > news >正文

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

目录

  • 一、背景
  • 二、代码理解
    • (一)main()函数
      • 例程代码
      • 功能
        • 遇到的问题
        • 解决方式
        • 分析
    • (二)eMBMasterPoll( void )函数
      • 例程代码
      • 1. 变量声明
      • 2. 协议栈状态检查
      • 3. 获取事件
      • 4. 事件处理(switch-case)
        • 4.1 `EV_MASTER_READY`事件
        • 4.2 `EV_MASTER_FRAME_RECEIVED`事件(接收到一帧数据)
        • 4.3 `EV_MASTER_EXECUTE`事件(执行功能)
        • 4.4 `EV_MASTER_FRAME_SENT`事件(一帧数据发送完成)
        • 4.5 `EV_MASTER_ERROR_PROCESS`事件(处理错误)
        • 4.6 默认情况
      • 5. 返回状态
      • 总结
    • (三)void test(char MB)函数
      • 例程代码
      • 关键点说明:
    • (四)test(MB_USER_HOLD);
      • 函数作用
      • 详细执行流程
        • 1. 准备写入数据
        • 2. 执行 Modbus 写操作
      • Modbus 协议层行为
      • 为什么用保持寄存器?
      • modbus slave的通信现象
      • 通信数据解析(第一条记录为例)
        • 主站请求(Rx 表示从站接收到的数据)
        • 从站响应(Tx 表示从站发送的数据)
      • 时间戳数据分析
        • 时间戳还原示例
      • 通信流程正确性验证
      • 特别注意事项
  • 我的
    • 目的
    • 思路
      • 关键实现细节:
      • 从机端还原数据:
      • 重要注意事项:
    • 结果

一、背景

   继上篇成功移植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_buffHlod_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 字节数据
数据 100 27寄存器 40002 = 0x0027 (39)
数据 200 30寄存器 40003 = 0x0030 (48)
数据 300 37寄存器 40004 = 0x0037 (55)
数据 400 00寄存器 40005 = 0x0000 (0)
CRCED 71校验正确
从站响应(Tx 表示从站发送的数据)
Tx: 01 10 00 01 00 04 90 0A
字段说明
从站地址01设备地址 1
功能码10写多个保持寄存器 (0x10)
起始地址00 01寄存器 40002 (0x0001)
寄存器数00 04成功写入 4 个寄存器
CRC90 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)时间间隔
10x0027, 0x0030, 0x0037, 0x00000x0000373014,112基准
20x00F8, 0x0037, 0x0037, 0x00000x0000373714,135+23ms
30x00C9, 0x003F, 0x0037, 0x00000x0000373F14,143+8ms
40x009A, 0x0047, 0x0037, 0x00000x0000374714,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 * 1000vi_config.I_value = 5000;   // 5.00A * 1000Hlod_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 寄存器映射

    寄存器地址对应值说明
    40002vi_buff[0]电压值低16位
    40003vi_buff[1]电压值高16位
    40004vi_buff[2]电流值低16位
    40005vi_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
在这里插入图片描述

在这里插入图片描述

http://www.dtcms.com/a/278800.html

相关文章:

  • argus/nvarguscamerasrc 远程显示报错
  • 项目一第一天
  • 纯数学专业VS应用数学专业:这两个哪个就业面更广?
  • C++后端面试八股文
  • Linux 基础命令详解:从入门到实践(1)
  • JAVA 并发 ThreadLocal
  • RestAssured(Java)使用详解
  • 19.数据增强技术
  • 管程! 解决互斥,同步问题的现代化手段(操作系统os)
  • Java行为型模式---模板方法模式
  • Imx6ull用网线与电脑连接
  • SpringBoot JAR 反编译替换文件
  • 【嵌入式汇编基础】-操作系统基础(三)
  • 【每日刷题】移动零
  • LabVIEW-Origin 船模数据处理系统
  • 【爬虫】Python实现爬取京东商品信息(超详细)
  • 期权和期货的区别主要是什么?
  • [论文阅读] 人工智能 | 用大型语言模型玩转多语言主观性检测:CheckThat! 2025赛事中的亮眼表现
  • Unity3D + VS2022连接雷电模拟器调试
  • 【PTA数据结构 | C语言版】字符串连接操作(不限长)
  • 分布式一致性协议
  • Android动画:属性动画以及实现点击图标缩放的动画效果
  • Relocations in generic ELF (EM: 40)
  • “国乙黑月光”指的是谁?
  • YOLOv11调参指南
  • Maven 依赖原则和依赖冲突
  • Docker入门指南(超详细)
  • Jetpack Compose 重组陷阱:一个“乌龙”带来的启示
  • yolo8+声纹识别(实时字幕)
  • 从“炼丹”到“流水线”——如何用Prompt Engineering把LLM微调成本打下来?