目录
一、背景
继上篇成功移植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_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
向从站写入多个保持寄存器。 - 参数说明:
- 从站地址:
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 * 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();
}
// 其他任务...
}
关键实现细节:
数据结构拆分:
// 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