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.pswp.cn/web/88954.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/88954.shtml
英文地址,請注明出處:http://en.pswp.cn/web/88954.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

c++判斷文件或目錄是否存在

#include<sys/stat.h>#include<fstream>#include<string>#include<stdio.h>#include<stdlib.h>#include<vector>#include<io.h>#include<iostream>bool IsFileGood(string strFileName, book bFile){if(bFile) \\文件{ifstrea…

Java設計模式之行為型模式(命令模式)

一、核心定義與設計思想 命令模式通過對象化請求&#xff0c;將操作的具體實現細節封裝在命令對象中&#xff0c;使得調用者&#xff08;Invoker&#xff09;無需直接依賴接收者&#xff08;Receiver&#xff09;&#xff0c;僅需通過命令對象間接調用。這種設計支持以下能力&a…

大數據領域開山鼻祖組件Hadoop核心架構設計

一、Hadoop的整體架構 Hadoop是一個專為大數據設計的架構解決方案&#xff0c;歷經多年開發演進&#xff0c;已逐漸發展成為一個龐大且復雜的系統。其內部工作機制融合了分布式理論與具體工程開發的精髓&#xff0c;構成了一個整體架構。 Hadoop最樸素的原理在于&#xff0c;它…

OneCode3.0 VFS分布式文件管理API速查手冊

&#x1f4da; 前言&#xff1a;OneCode 3.0微內核引擎架構解析 在云原生與分布式系統日益普及的今天&#xff0c;文件管理系統面臨著前所未有的挑戰——海量數據存儲、跨節點協同、多租戶隔離以及彈性擴展等需求推動著傳統文件系統向分布式架構演進。OneCode 3.0作為新一代企業…

UI前端與數字孿生結合實踐探索:智慧物流的倉儲自動化管理系統

hello寶子們...我們是艾斯視覺擅長ui設計、前端開發、數字孿生、大數據、三維建模、三維動畫10年經驗!希望我的分享能幫助到您!如需幫助可以評論關注私信我們一起探討!致敬感謝感恩!一、引言&#xff1a;傳統倉儲的 “效率黑洞” 與數字孿生的破局當倉庫管理員在數萬平的庫房中…

使用layui的前端框架過程中,無法加載css和js怎么辦?

這使用layui的前端框架過程中&#xff0c;無法加載css和js怎么辦&#xff1f;里寫自定義目錄標題已經按要求下載并解壓到指定位置了&#xff0c;但是感覺就是無法加載文件后臺提示如下&#xff1a;那就我清理緩存當再次觀察html頁面時&#xff0c;發現頁面最開始有兩個< htm…

gitlab+TortoiseGit克隆生成ppk方式

1、第一步 2、第二步3、第三步4、第四步&#xff0c;如何使用這個ppk就可以了

VSCode中使用容器及容器編排docker-compose

前面筆者寫了一篇博文&#xff1a;使用容器編排對go項目進行部署、調試&#xff0c;介紹了在Goland中如何使用容器&#xff0c;由于Goland的容器配置是可視化的&#xff0c;使用起來非常方便&#xff0c;VSCode中也有一個容器插件&#xff0c;但是筆者一直未使用過&#xff0c;…

深度學習入門:讓神經網絡變得“深不可測“?(二)

深度學習入門&#xff1a;讓神經網絡變得"深不可測" &#x1f9e0;? 系列課程第二彈&#xff1a;深度學習的奇妙世界 前言&#xff1a;從淺到深的華麗轉身 哈嘍&#xff0c;各位AI探險家&#xff01;&#x1f44b; 歡迎回到我們的"讓機器變聰明"系列課…

硅基計劃2.0 學習總結 捌 異常與常用工具類

文章目錄一、異常1. 防御性編程2. throw關鍵字3. throws關鍵字4. 捕獲5. finally關鍵字二、自定義異常類三、常用工具類1. Date以及相關的類1. 創建時間&#xff08;基本棄用&#xff09;2. 捕獲系統時間3. 獲取當前年月日時分秒4. 日期加減5. 根據字符串創建日期6. 根據當前時…

2025-7-14-C++ 學習 排序(2)

文章目錄2025-7-14-C 學習 排序&#xff08;2&#xff09;P1059 [NOIP 2006 普及組] 明明的隨機數題目描述輸入格式輸出格式輸入輸出樣例 #1輸入 #1輸出 #1說明/提示提交代碼P1093 [NOIP 2007 普及組] 獎學金題目背景題目描述輸入格式輸出格式輸入輸出樣例 #1輸入 #1輸出 #1輸入…

微信131~140

1.在組件中使用store對象的數據 // 要想使用store中的數據以及方法 // 需要從 mobx-miniprogram-bindings 方法將 ComponentWithStore 方法 import { ComponentWithStore } from mobx-miniprogram-bindings // 導入store對象 import { numStore } from ../../../stores/numstor…

微美全息借區塊鏈與DRL算法打造資源管理協同架構,達成邊緣計算與區塊鏈動態適配

在當今數字化浪潮洶涌的時代&#xff0c;邊緣計算與區塊鏈技術正逐步成為驅動技術革新與業務轉型升級的核心動力。當這兩項前沿技術相互融合&#xff0c;一個兼具高效性與安全性的任務處理系統便得以構建。為了充分挖掘邊緣計算系統的性能潛力&#xff0c;避免任務卸載過程中的…

屬性綁定

簡寫模式二.為什么要這樣做布爾型attribute動態綁定多個值

鏈表算法之【獲取鏈表開始入環的節點】

目錄 LeetCode-142題 LeetCode-142題 給定一個鏈表的頭節點head&#xff0c;返回鏈表開始入環的第一個節點&#xff0c;如果鏈表無環&#xff0c;則返回null class Solution {public ListNode detectCycle(ListNode head) {// checkif (head null || head.next null)retur…

【網絡編程】KCP——可靠的 UDP 傳輸協議——的知識匯總

文章目錄前言UDP 協議UDP 的關鍵指標/特性UDP 的典型應用場景KCP 協議的基礎KCP 的構造KCP 協議特性KCP 的可靠傳輸機制——ARQ三種 ARQ 機制對比KCP 的選擇性重傳一、基礎機制&#xff1a;選擇性重傳&#xff08;SR&#xff09;二、KCP 對 SR 的增強策略KCP 的激進重傳策略——…

量子計算新突破!阿里“太章3.0”實現512量子比特模擬(2025中國量子算力巔峰)

??摘要??2025年量子計算競爭進入??實用化臨界點??&#xff0c;阿里達摩院發布“太章3.0”量子模擬器&#xff0c;在全球首次實現??512量子比特全振幅模擬??&#xff0c;較谷歌Sycamore的53比特提升近10倍算力維度。本文深度解析三大技術突破&#xff1a;??張量網…

DOM事件綁定時機:解決腳本提前加載導致的綁定失敗

引言&#xff1a;一個讓無數新手抓狂的常見錯誤在JavaScript開發中&#xff0c;尤其是在前端領域&#xff0c;有一個讓無數新手抓狂的問題&#xff1a;明明寫了事件監聽代碼&#xff0c;點擊按鈕卻沒有任何反應&#xff01;更令人困惑的是&#xff0c;代碼邏輯看起來完全正確&a…

游戲框架筆記

游戲的數據有哪些類型無非是只讀數據&#xff08;各種道具配表里的數據&#xff09;和可讀可寫數據&#xff08;玩家屬性、擁有的物品&#xff09;。游戲框架需要哪些管理器用戶數據管理器負責找到數據持久化文件&#xff0c;從中讀取指定用戶的數據&#xff0c;包括玩家的設置…

【C語言進階】指針面試題詳解(2)

上一期內容&#xff0c;大多數的解題思路寫在代碼中&#xff0c;沒有寫在正文中&#xff0c;這就導致系統判斷文章質量不高&#xff0c;沒有什么數據&#xff0c;這一期將思路寫在正文中。注意&#xff1a;運行環境是x86 1.題目1思路&#xff1a;&a是取到了整個數組的地址&…