MODBUS主機協議移植
最近由於專案需求需要移植的modbus主機協議,由於MODBUS主機的原始碼不開源,網上的例程也不多,在百度苦苦找尋一番沒有什麼結果。最後參考了安富萊的例程,將其修改並移植到自己的工程中。我通過USB轉串列埠連線到電腦,用虛擬裝置測試,讀寫正常。以下是我的移植過程。下面有stm32F1和msp430F5的原始碼與MODBUS虛擬裝置
下面使用的晶片是stn32f103zet6,IDE為MDK
1,首先將下面幾個檔案新增到工程中
2,接下來只需要新增幾個介面函式就能正常執行
//需要連線的外部介面函式 extern void (*RS485_ReceiveData)(uint8_t); //接受資料,在串列埠中斷接受到的數傳遞到此函式即可 extern void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen); //傳送資料 extern void bsp_StartHardTimer(uint32_t timeout, void * _pCallBack); //開始計時3.5個位元組的時間,單位為us //以下兩個函式用來測量間隔時間 extern uint32_t bsp_GetRunTime(void); //返回當前時間,單位ms extern uint32_t bsp_CheckRunTime(int32_t _LastTime); //返回與之前間隔的時間
這兩個函式由於需要使用到串列埠所以需要先初始化串列埠
extern void (*RS485_ReceiveData)(uint8_t); //接受資料,在串列埠中斷接受到的數傳遞到此函式即可
extern void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen); //傳送資料
下面是串列埠初始化函式,這裡使用串列埠3
myusart.c檔案下
void MODBUS_USART3_Init(u32 bound) { //GPIO埠設定 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); //使能USART1,GPIOA時鐘 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //USART1_TX GPIOB.10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //PB.10 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //複用推輓輸出 GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO1.10 //USART1_RX GPIOB.11初始化 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//PB11 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空輸入 GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB.11 //Usart1 NVIC 配置 NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//搶佔優先順序3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子優先順序2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能 NVIC_Init(&NVIC_InitStructure); //根據指定的引數初始化VIC暫存器 //USART 初始化設定 USART_InitStructure.USART_BaudRate = bound;//串列埠波特率 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字長為8位資料格式 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一個停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//無奇偶校驗位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//無硬體資料流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收發模式 USART_Init(USART3, &USART_InitStructure); //初始化串列埠1 USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);//開啟串列埠接受中斷 USART_Cmd(USART3, ENABLE); //使能串列埠1 }
串列埠初始化成功後,可以新增上面那幾個介面函式
串列埠3中斷中新增RS485_ReceiDate()函式指標
void USART3_IRQHandler(void) //串列埠3中斷服務程式,modbus串列埠接受介面
{
uint8_t ch;
if(USART_GetITStatus(USART3,USART_IT_RXNE) == SET)
{
ch = USART_ReceiveData(USART3);
RS485_ReceiveData(ch);
}
USART_ClearITPendingBit(USART3,USART_IT_RXNE);
}
新增函式void RS485_SendBuf(uint8_t * _ucaBuf,uint16_t _usLen)
void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
{
uint16_t num;
for(num = 0;num<_usLen;num++)
{
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE) == RESET);
USART_SendData(USART3,*_ucaBuf);
_ucaBuf++;
}
}
串列埠部分已經配置完成,接下來是定時器的部分
需要配置以下幾個函式
extern void bsp_StartHardTimer(uint32_t timeout, void * _pCallBack); //開始計時3.5個位元組的時間,單位為us
//以下兩個函式用來測量間隔時間
extern uint32_t bsp_GetRunTime(void); //返回當前時間,單位ms
extern uint32_t bsp_CheckRunTime(int32_t _LastTime); //返回與之前間隔的時間
同樣的需要先初始化定時器,這裡使用定時器2和定時器3
在/timer.c裡檔案下
void MODBUS_Tim2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
uint32_t usPeriod;
uint16_t usPrescaler;
uint32_t uiTIMxCLK;
/* 使能TIM時鐘 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
uiTIMxCLK = SystemCoreClock / 2;
usPrescaler = uiTIMxCLK / 1000000 ; /* 分頻到週期 1us */
usPeriod = 0xFFFF; /* 103支援16位 */
/* Time base configuration */
TIM_TimeBaseStructure.TIM_Period = usPeriod;
TIM_TimeBaseStructure.TIM_Prescaler = usPrescaler;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
/* TIMx enable counter */
TIM_Cmd(TIM2, DISABLE);
/* 配置TIM定時中斷 (Update) */
{
NVIC_InitTypeDef NVIC_InitStructure; /* 中斷結構體在 misc.h 中定義 */
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4; /* 比串列埠優先順序低 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_CC3) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC3);
TIM_ITConfig(TIM2, TIM_IT_CC3, DISABLE); /* 禁能CC3中斷 */
s_TIM_CallBack(); //將標誌位置1
TIM_Cmd(TIM2, DISABLE);
}
}
void MODBUS_Tim3_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
/* 使能TIM時鐘 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
/* Time base configuration */
TIM_TimeBaseStructure.TIM_Period = 999;
TIM_TimeBaseStructure.TIM_Prescaler = 35;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
/* TIMx enable counter */
TIM_Cmd(TIM3, ENABLE);
/* 配置TIM定時中斷 (Update) */
{
NVIC_InitTypeDef NVIC_InitStructure; /* 中斷結構體在 misc.h 中定義 */
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4; /* 比串列埠優先順序低 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
}
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
g_iRunTime++;
if(g_iRunTime > 0x7FFFFFFF) g_iRunTime = 0;
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
定時器2用來檢測3.5個位元組的時間來判斷一幀資料是夠結束
定時器3用於計數,只要要1ms的記一次數就可以
接下來是void bsp_StartHardTimer(uint32_t _uiTimeOut,void * _pCallBack)函式
在這個函式中開啟中斷,在3.5個位元組的時間後會進入中斷,具體時間要根據波特率設定
void bsp_StartHardTimer(uint32_t _uiTimeOut, void * _pCallBack)
{
uint32_t cnt_now;
uint32_t cnt_tar;
if (_uiTimeOut < 5)
{
;
}
else
{
_uiTimeOut -= 5;
}
TIM_SetCounter(TIM2,0);
TIM_Cmd(TIM2, ENABLE);
cnt_now = TIM_GetCounter(TIM2); /* 讀取當前的計數器值 */
cnt_tar = cnt_now + _uiTimeOut; /* 計算捕獲的計數器值 */
s_TIM_CallBack = (void (*)(void))_pCallBack;
TIM_SetCompare3(TIM2, cnt_tar); /* 設定捕獲比較計數器CC3 */
TIM_ClearITPendingBit(TIM2, TIM_IT_CC3);
TIM_ITConfig(TIM2, TIM_IT_CC3, ENABLE); /* 使能CC3中斷 */
}
下面是uint32_t bsp_GetRunTime(void)與uint32_t bsp_CheckRunTime(int32_t _LastTime)
這兩個函式是用來測量時間,判斷是否通訊超時
uint32_t bsp_GetRunTime(void)
{
int32_t runtime;
__set_PRIMASK(1); /* 關中斷 */
runtime = g_iRunTime; /* 這個變數在Systick中斷中被改寫,因此需要關中斷進行保護 */
__set_PRIMASK(0); /* 開中斷 */
return runtime;
}
//獲取間隔時間
uint32_t bsp_CheckRunTime(int32_t _LastTime)
{
int32_t now_time;
int32_t time_diff;
__set_PRIMASK(1); /* 關中斷 */
now_time = g_iRunTime; /* 這個變數在Systick中斷中被改寫,因此需要關中斷進行保護 */
__set_PRIMASK(0); /* 開中斷 */
if (now_time >= _LastTime)
{
time_diff = now_time - _LastTime;
}
else
{
time_diff = 0x7FFFFFFF - _LastTime + now_time;
}
return time_diff;
}
到這裡已經基本上移植完成,接下來就是測試
呼叫一下這些函式看看是否移植成功
uint8_t MODH_ReadParam_01H(uint16_t slaveAddr,uint16_t _reg, uint16_t _num); //讀線圈暫存器
uint8_t MODH_ReadParam_02H(uint16_t slaveAddr,uint16_t _reg, uint16_t _num); //讀離散輸入暫存器
uint8_t MODH_ReadParam_03H(uint16_t slaveAddr,uint16_t _reg, uint16_t _num); //讀儲存暫存器
uint8_t MODH_ReadParam_04H(uint16_t slaveAddr,uint16_t _reg, uint16_t _num); //讀輸入暫存器
uint8_t MODH_WriteParam_05H(uint16_t slaveAddr,uint16_t _reg, uint16_t _value); //寫單個線圈暫存器
uint8_t MODH_WriteParam_06H(uint16_t slaveAddr,uint16_t _reg, uint16_t _value); //寫單個儲存暫存器
uint8_t MODH_WriteParam_10H(uint16_t slaveAddr,uint16_t _reg, uint8_t _num, const uint16_t *_buf); //寫多個儲存暫存器
3,測試
首先將硬體連線好,我這裡是用虛擬裝置,將32的串列埠3通過串列埠轉USB連線到電腦
開啟虛擬裝置匯入暫存器,設定好串列埠與波特率
在主函式中呼叫讀寫函式,若讀寫成功則返回1,否則返回0
觀察虛擬機器現象並通過串列埠1列印測試結果
在主函式中
if(MODH_WriteParam_06H(1,4,9))
{
printf("寫入成功06\r\n");
}
else
{
printf("通訊失敗06\r\n");
}
if(MODH_WriteParam_10H(1,0, 3,buff))
{
printf("寫入成功10\r\n");
}
else
{
printf("通訊失敗10\r\n");
}
if(MODH_ReadParam_02H(1,0,10))
{
printf("讀取成功02\r\n");
}
else
{
printf("通訊失敗02\r\n");
}
到這裡我的整個移植過程就結束了
下面是原始碼和除錯用的虛擬裝置。