目錄
- 一、背景
- 二、代碼理解
- (一)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