困惑了將近一年多的ModbusRTU在我昨天窮極無聊給自己定目標的情況下搞出來了,以前移植不出來主要原因就是基本功不紮實,沒有進一步理解串口和定時器配置的原理,一通操作,移植完之後就Timeout,接下來就分享一下我是怎麼從0開始移植這個協議的。
項目已上傳碼雲,文章底部有鏈接!
1.需要的材料
- STM32開發板一塊,不限型號
- freeModbus包可進入後方鏈接下載(Modbus官方源碼包)
- STM32CubeMX
2.操作步驟
操作之前先講兩個主要問題
1.串口設置問題
MoubusRTU移植到stm32平臺通信是通過串口進行通信,主要是需要串口進行收發,所以發送中斷時必須的,在波特率設置問題上是和定時器相關聯,在mbrtu.c文件的eMBRTUInit函數裏具體說明了串口波特率和定時器設置的關係
eMBErrorCode
eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
eMBErrorCode eStatus = MB_ENOERR;
ULONG usTimerT35_50us;
( void )ucSlaveAddress;
ENTER_CRITICAL_SECTION( );
/* Modbus RTU uses 8 Databits. */
if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )
{
eStatus = MB_EPORTERR;
}
else
{
/* If baudrate > 19200 then we should use the fixed timer values
* t35 = 1750us. Otherwise t35 must be 3.5 times the character time.
*/
if( ulBaudRate > 19200 )
{
usTimerT35_50us = 35; /* 1750us. */
}
else
{
/* The timer reload value for a character is given by:
*
* ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
* = 11 * Ticks_per_1s / Baudrate
* = 220000 / Baudrate
* The reload for t3.5 is 1.5 times this value and similary
* for t3.5.
*/
usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
}
if( xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) != TRUE )
{
eStatus = MB_EPORTERR;
}
}
EXIT_CRITICAL_SECTION( );
return eStatus;
}
從上面代碼的註釋中可以看出,當波特率大於19200時,超時時間固定位爲1750us,當波特率小於19200時,超時時間爲3.5個字符時間,具體計算公式在代碼註釋裏已經有了,這裏我就不多贅述。本人波特率使用115200,所以按照1750us來。
2.定時器設置問題
ModbusRTU是通過定時器和串口配合來實現Modbus通信的,所以定時器是決定有沒有超時的一大關鍵問題,由串口設置部分可知,定時器設置是要配合串口設置的波特率食用比較香,所以根據我使用的115200波特率可以得到我定時器設置。首先是APB1的主頻率獲取到,modbus要求通過預分配後得到的週期爲50us,對應頻率爲20KHz。根據rtu初始化代碼得到自動重載值設置爲35。
具體操作:
熟悉stm32cubemx的老司機可以直接從15步看起
1.選擇MCU型號
2.使能時鐘源RCC爲外部時鐘
3.配置時鐘樹,記錄APB1頻率,我這裏是36MHz
4.使能定時器4,預分頻係數爲1800-1,對應的分頻頻率爲20KHz,不懂的回到上面去看定時器設置解析,自動重載值設置爲35,得到超時時間1750us。
5.使能定時器中斷
6.配置串口2,選擇異步通信後參數設置爲115200,8,NONE,1
7.使能串口中斷
8.配置中斷優先級,定時器中斷優先級低於串口中斷即可
9.配置項目參數並分離頭文件和c文件後生成代碼。
10.打開freeModbus代碼包的demo文件夾,新建一個名爲STM32MB的文件夾,之後將BARE文件夾內所有內容複製到STM32MB文件夾下,複製完成如圖
11.回到freeModbus代碼包,複製整個modbus文件夾也粘貼到STM32MB文件夾內,完成效果如圖
12.將STM32MB文件夾移動到stm32cubeMX生成的工程目錄下,如圖
13.打開工程,引入STM32MB內的所有頭文件,並新建名爲MB和MB_Port的組,MB內添加STM32MB文件夾下modbus文件夾內所有c文件以及根目錄的demo.c文件,MB_Port內添加STM32MB文件夾下port文件夾內所有c文件,如圖所示
14.修改demo.c文件夾的main函數名爲host,編譯不報錯即可開始修改,如圖所示
以下爲正式修改Modbus代碼,上面比較繁瑣,熟悉stm32cubemx的老司機可以直接從15步看起
15.修改MB_Port下的portserial.c文件(串口設置)
我直接貼代碼,自己對比我的代碼和源碼差距,關鍵地方我會在後邊標註
#include "port.h"
#include "stm32f7xx_hal.h"
#include "usart.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
//static void prvvUARTTxReadyISR( void );
//static void prvvUARTRxISR( void );
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if (xRxEnable) //將串口收發中斷和modbus聯繫起來,下面的串口改爲自己使能的串口
{
__HAL_UART_ENABLE_IT(&huart2,UART_IT_RXNE); //我用的是串口2,故爲&huart2
}
else
{
__HAL_UART_DISABLE_IT(&huart2,UART_IT_RXNE);
}
if (xTxEnable)
{
__HAL_UART_ENABLE_IT(&huart2,UART_IT_TXE);
}
else
{
__HAL_UART_DISABLE_IT(&huart2,UART_IT_TXE);
}
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
return TRUE; //改爲TURE,串口初始化在usart.c定義,mian函數已完成
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
if(HAL_UART_Transmit (&huart2 ,(uint8_t *)&ucByte,1,0x01) != HAL_OK ) //添加發送一位代碼
return FALSE ;
else
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
/* Return the byte in the UARTs receive buffer. This function is called
* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
*/
if(HAL_UART_Receive (&huart2 ,(uint8_t *)pucByte,1,0x01) != HAL_OK )//添加接收一位代碼
return FALSE ;
else
return TRUE;
}
/* Create an interrupt handler for the transmit buffer empty interrupt
* (or an equivalent) for your target processor. This function should then
* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
* a new character can be sent. The protocol stack will then call
* xMBPortSerialPutByte( ) to send the character.
*/
//static
void prvvUARTTxReadyISR( void ) //刪去前面的static,方便在串口中斷使用
{
pxMBFrameCBTransmitterEmpty( );
}
/* Create an interrupt handler for the receive interrupt for your target
* processor. This function should then call pxMBFrameCBByteReceived( ). The
* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
* character.
*/
//static
void prvvUARTRxISR( void ) //刪去前面的static,方便在串口中斷使用
{
pxMBFrameCBByteReceived( );
}
16.修改MB_Port下的porttimer.c文件(定時器設置)
我直接貼代碼,自己對比我的代碼和源碼差距,關鍵地方我會在後邊標註
#include "port.h"
#include "stm32f7xx_hal.h"
#include "tim.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
//static void prvvTIMERExpiredISR( void );
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us ) //定時器初始化直接返回TRUE,已經在mian函數初始化過
{
return TRUE;
}
inline void
vMBPortTimersEnable( ) //使能定時器中斷,我用的是定時器4,所以爲&htim4
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
__HAL_TIM_CLEAR_IT(&htim4,TIM_IT_UPDATE);
__HAL_TIM_ENABLE_IT(&htim4,TIM_IT_UPDATE);
__HAL_TIM_SetCounter(&htim4,0);
__HAL_TIM_ENABLE(&htim4);
}
inline void
vMBPortTimersDisable( ) //取消定時器中斷
{
/* Disable any pending timers. */
__HAL_TIM_DISABLE(&htim4);
__HAL_TIM_SetCounter(&htim4,0);
__HAL_TIM_DISABLE_IT(&htim4,TIM_IT_UPDATE);
__HAL_TIM_CLEAR_IT(&htim4,TIM_IT_UPDATE);
}
/* Create an ISR which is called whenever the timer has expired. This function
* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
* the timer has expired.
*/
//static
void prvvTIMERExpiredISR( void ) //modbus定時器動作,需要在中斷內使用
{
( void )pxMBPortCBTimerExpired( );
}
17.修改完Modbus與stm32的接口文件之後要在port.h文件內定義總中斷
位置在port.h文件的32行和33行,修改爲如下所示,並在port.h前包含上stm32的hal庫,如圖所示
#define ENTER_CRITICAL_SECTION( ) __set_PRIMASK(1) //關總中斷
#define EXIT_CRITICAL_SECTION( ) __set_PRIMASK(0) //開總中斷
#include "stm32f7xx_hal.h"
modbus端口函數到此修改完成,接下來是中斷函數
18.串口及定時器中斷修改
打開工程內的中斷文件,是在Application/User–>stm32f7xx_it.c
根據板子不同而不同,區別是stm32f後面的數字。知道是中斷管理文件就行
在/* USER CODE BEGIN PFP */後添加以下代碼,用於和modbus的串口和定時器功能代碼聯繫
extern void prvvUARTTxReadyISR(void);
extern void prvvUARTRxISR(void);
extern void prvvTIMERExpiredISR( void );
找到自己設置的串口中斷處理函數,添加如下代碼,用於將串口收到的內容移動到modbus功能函數進行處理
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
/* USER CODE END USART2_IRQn 0 */
HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
if(__HAL_UART_GET_IT_SOURCE(&huart2, UART_IT_RXNE)!= RESET)
{
prvvUARTRxISR();//接收中斷
}
if(__HAL_UART_GET_IT_SOURCE(&huart2, UART_IT_TXE)!= RESET)
{
prvvUARTTxReadyISR();//發送中斷
}
HAL_NVIC_ClearPendingIRQ(USART2_IRQn);
HAL_UART_IRQHandler(&huart2);
/* USER CODE END USART2_IRQn 1 */
}
在Application/User–>stm32f7xx_it.c末尾的/* USER CODE BEGIN 1 */添加定時器中斷回調函數如下:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定時器中斷回調函數,用於連接porttimer.c文件的函數
{
/* NOTE : This function Should not be modified, when the callback is needed,
the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
*/
prvvTIMERExpiredISR( );
}
到此,串口和定時器的問題已經處理完畢,接下來是modbus的配置
19.modbus功能處理
硬件接口方面結束之後就可以開始寫功能了,在MB–>demo.c中有功能示例,我們根據功能示例來修改對應的功能並使能modbus,這裏我只說輸入寄存器功能,其它的一次類推,就不多贅述。
這裏也是直接貼代碼,大概說一下,就是自己設置一個數組,將數據放到數組內,並在被讀取時根據數據位置將數據返回去
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- Defines ------------------------------------------*/
#define REG_INPUT_START 0
#define REG_INPUT_NREGS 5
/* ----------------------- Static variables ---------------------------------*/
static USHORT usRegInputStart = REG_INPUT_START;
//static
uint16_t usRegInputBuf[REG_INPUT_NREGS];
uint16_t InputBuff[5];
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
int i;
InputBuff[0] = 0x11;
InputBuff[1] = 0x22;
InputBuff[2] = 0x33;
InputBuff[3] = 0x44;
if( ( usAddress >= REG_INPUT_START )
&& ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
{
iRegIndex = ( int )( usAddress - usRegInputStart );
for(i=0;i<usNRegs;i++)
{
*pucRegBuffer=InputBuff[i+usAddress-1]>>8;
pucRegBuffer++;
*pucRegBuffer=InputBuff[i+usAddress-1]&0xff;
pucRegBuffer++;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
eMBRegisterMode eMode )
{
return MB_ENOREG;
}
eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,
eMBRegisterMode eMode )
{
return MB_ENOREG;
}
eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
return MB_ENOREG;
}
20.modbus啓動
啓動modbus需要在main函數進行初始化、開啓偵聽操作,需要添加以下代碼,對應位置可在mian函數找到
/* USER CODE BEGIN Includes */
#include "mb.h"
#include "mbport.h"
/* USER CODE END Includes */
/* USER CODE BEGIN 2 */
eMBInit( MB_RTU, 0x01, 1, 115200, MB_PAR_NONE);//初始化modbus,走modbusRTU,從站地址爲0x01,端口爲1。
eMBEnable( );//使能modbus
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
( void )eMBPoll( );//啓動modbus偵聽
}
/* USER CODE END 3 */
至此修改完畢,編譯下載之後即可使用modbus poll進行連接測試。
21.modbus測試
將上述代碼編譯下載到板子,用TTL轉USB接入PC,找到在PC的對應端口即可打開ModbusPoll進行通信測試
代碼下載成功後打開ModbusPoll,打開讀寫定義並設置爲從站地址1,功能04讀輸入寄存器,起始地址0,長度爲4,如圖所示
按F3進行連接,連接設置如圖,串口所在位置會顯示TTL轉串口的芯片型號,按照如下設定後確定。
即可得到下圖,由於我們輸入寄存器存放的是16進制數,所以要將ModbusPoll顯示模式改爲16進制才能顯示相同內容
最終效果如下圖,ModbusPoll讀取的值與STM32內寄存器內的值一致,讀取成功!
至此,freeModbusRTU移植成功!,具體代碼已上傳,詳見我的碼雲F7_ModbusRTU
做完事情就要去打一下游戲!
希望大家不要白piao,點贊評論打賞素質三連!!!