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) |
- 不卡死CPU
HAL_Delay(1000) 会让 CPU 空转 1000ms,期间无法做任何事情。
而 if (HAL_GetTick() - lastPollTime >= 1000) 只是 快速检查时间是否到期,如果没有到期,CPU 可以继续执行其他任务。 - 允许并行处理其他任务
- 适用于 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
),如果不在ENABLED
或ESTABLISHED
状态,则返回错误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
向从站写入多个保持寄存器。 - 参数说明:
- 从站地址:
MB_SAMPLE_TEST_SLAVE_ADDR
(通常为1-247) - 起始地址:
MB_REG_START
(如0表示从0号寄存器开始) - 寄存器数量:
MB_SEND_REG_NUM
(这里为4,因为Hlod_buff有4个元素) - 数据源:
Hlod_buff
数组(包含拆分后的时间戳) - 超时:
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
向从站写入多个线圈状态。 - 参数说明:
- 从站地址:同上
- 起始地址:
MB_REG_START
(线圈起始地址) - 线圈数量:
MB_SEND_REG_NUM
(这里为4) - 数据源:
Coils
数组(值为{1,0,1,0}) - 超时:永久等待
case MB_USER_INPUT_REG:// 读输入寄存器eMBMasterReqReadInputRegister(MB_SAMPLE_TEST_SLAVE_ADDR, // 目标从站地址MB_REG_START, // 起始输入寄存器地址MB_READ_REG_NUM, // 要读取的寄存器数量(宏定义)WAITING_FOREVER // 永久等待);break;}
}
读输入寄存器(功能码0x04):
- 调用函数
eMBMasterReqReadInputRegister
从从站读取输入寄存器。 - 参数说明:
- 从站地址:同上
- 起始地址:
MB_REG_START
- 寄存器数量:
MB_READ_REG_NUM
(宏定义,未在代码中显示具体值) - 超时:永久等待
关键点说明:
- 功能选择:通过传入的
MB
参数(MB_USER_HOLD
、MB_USER_COILS
、MB_USER_INPUT_REG
)选择要测试的Modbus功能。 - 数据准备:
- 写保持寄存器:使用系统时间戳拆分后的4个16位整数。
- 写线圈:使用预定义的数组
{1,0,1,0}
。
- 超时处理:所有操作都设置为
WAITING_FOREVER
,这意味着主站会一直等待从站响应,直到收到响应或发生错误(如超时错误)。在实际应用中,可能需要设置合理的超时时间。 - 宏定义:代码中使用了多个宏(如
MB_SAMPLE_TEST_SLAVE_ADDR
、MB_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
:从站地址 110
:写多个保持寄存器功能码00 00
:起始地址 4000100 04
:写入 4 个寄存器08
:后续数据字节数(4寄存器×2字节=8)00 78
:Hlod_buff[0] = 0x007800 56
:Hlod_buff[1] = 0x005600 34
:Hlod_buff[2] = 0x003400 12
:Hlod_buff[3] = 0x0012XX 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)
通信流程正确性验证
-
主从角色正确
- 主站(STM32)主动发送写请求 (
01 10...
) - 从站(ModSlave)返回成功响应 (
01 10 00 01 00 04 90 0A
)
- 主站(STM32)主动发送写请求 (
-
协议完整性
- 所有请求都有对应响应
- CRC 校验全部通过(无
15 10
等异常响应)
-
数据一致性
- 请求中的寄存器地址/数量与响应完全一致
- 从站正确执行了写操作
特别注意事项
-
固定高位字节问题
- 所有记录的
Hlod_buff[2] = 0x0037
(55) 和Hlod_buff[3] = 0x0000
- 原因:
HAL_GetTick()
值较小(约 14 秒),高位尚未变化 - 解决方案:长时间运行后会自然变化(如 65 秒后
Hlod_buff[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_value
和 I_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();}// 其他任务...
}
关键实现细节:
-
数据结构拆分:
// 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位
-
Modbus 寄存器映射:
寄存器地址 对应值 说明 40002 vi_buff[0]
电压值低16位 40003 vi_buff[1]
电压值高16位 40004 vi_buff[2]
电流值低16位 40005 vi_buff[3]
电流值高16位 -
通信示例:
假设: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
重要注意事项:
-
字节序问题:
- 当前使用小端序(低字节在前)
- 如果从机使用大端序系统,需要调整解析方式:
// 大端序解析 int32_t V_value = (registers[40002] << 16) | registers[40003];
-
数据类型转换:
- 如果值可能是负数,使用带符号类型:
int32_t V_value = (int32_t)((registers[40003] << 16) | registers[40002]);
- 如果值可能是负数,使用带符号类型:
-
超时处理:
1000 // 1秒超时(优于永久等待)
-
值范围处理:
- 如果电压/电流值可能超过 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