MODBUS學習日誌
一、MODBUS通信協議
1、通信協議
- 硬件層協議:解決傳輸問題,相當於路
- 串口通信協議 : RS232、RS485、CAN總線
1.1、三種通信方式
1.1.1、單工方式(simplex)
單工通信只支持信號在一個方向上傳輸(正向或反向),任何時候不能改變信號的傳輸方向。爲保證正確傳送數據信號,接收端要對接收的數據進行校驗,若校驗出錯,則通過監控信道發送請求重發的信號。此種方式適用於數據收集系統,如氣象數據的收集、電話費的集中計算等。例如計算機和打印機之間的通信是單工模式,因爲只有計算機向打印機傳輸數據,而沒有相反方向的數據傳輸。還有在某些通信信道中,如單工無線發送等。
1.1.2、半雙工方式(需要上層軟件做協議)(half-duplex)
半雙工通信允許信號在兩個方向上傳輸,但某一時刻只允許信號在一個信道上單向傳輸。因此,半雙工通信實際上是一種可切換方向的單工通信。此種方式適用於問訊、檢索、科學計算等數據通信系統;傳統的對講機使用的就是半雙工通信方式。由於對講機傳送及接收使用相同的頻率,不允許同時進行。因此一方講完後,需設法告知另一方講話結束(例如講完後加上’OVER’),另一方纔知道可以開始講話。
1.1.3、全雙工方式(full-duplex)
全雙工通信允許數據同時在兩個方向上傳輸,即有兩個信道,因此允許同時進行雙向傳輸。全雙工通信是兩個單工通信方式的結合,要求收發雙方都有獨立的接收和發送能力。全雙工通信效率高,控制簡單,但造價高。計算機之間的通信是全雙工方式。一般的電話、手機也是全雙工的系統,因爲在講話時可以聽到對方的聲音。
參考鏈接: https://blog.csdn.net/iningwei/article/details/100134783
1.2、主從模式:
主從模式,是數據庫設計模式中最常見、也是大家日常設計工作中用的最多的一種模式,它描述了兩個表之間的主從關係,是典型的“一對多”關係。
規定要求:
1. 系統中只有一個設備時主機
2. 系統中的所有從機不可以主動向主機發數據
3. 系統中的主機和所有從機上電後都處於監聽狀態
4. 任何一次的數據交換都要由主機發起
4.1、將自己轉爲發送狀態
4.2、主機按照預先約定的格式,發出尋址數據幀
4.3、恢復自己的接受狀態,等待所尋址的從機響應
1.3、軟件層協議:解決傳輸的目的
1.3.1、主從模式
-
整個系統只能有一個主機,每個從機必須有一個唯一的地址(0~247)
-
其中0號地址位廣播地址:主機向0號地址的設備發數據包,也就是要把該數據包發給所有的從設備。0號地址的數據包所有從機是不迴應的。
2、MODBUS的主機尋址幀的格式
>MODBUS的兩種傳輸方式:RTU方式和ASC方式
>
>RTU方式:也叫十六進制 例如:發送0x03:0000 0011
>
>RTU方式:也叫十六進制 例如:發送0x03:0000 0011
>
>ASC方式:0x03 {發送0 :0x30:0011 0000 }{ 發送3:0x33:0011 0011}
>
>所以ASC的通信效率低,但是方便調試,使用實驗;工業上都採用RTU方式,效率高
2.3、RTU方式
1、從機地址 2、功能碼(127個) 3、數據1~數據n 4、校驗碼(CRCL、CRCH)
其中: 1~3參與CRC16校驗
從機是以接收數據停止時間達到3.5個字節以上,那麼就認爲主機的尋址幀完成,並開始處理。
例如:波特率:9600bt/s
所以每位數據傳輸的時間T=1000000us/9600=104us
一字節時間位=10T=1004us(起始位 8位 停止位)(串口格式)
所以時間爲:3.5*10T=3645us
2.4、ASC方式
1、: 2、地址 3、功能碼 4、數據1~數據n 5、(地址數據)採用LRC校驗=((地址+功能碼+數據1數據n)%256)+1=(0255) 6、13 10(回車 換行)
2.5、CRC簡述
1.將一個 16 位寄存器裝入十六進制 FFFF (全 1). 將之稱作 CRC 寄存器.
2.將報文的第一個 8 位字節與 16 位 CRC 寄存器的低字節異或,結果置於 CRC 寄存器.
3.將 CRC 寄存器右移 1 位 (向 LSB 方向), MSB 充零. 提取並檢測 LSB.
4.(如果 LSB 爲 0): 重複步驟 3 (另一次移位).
(如果 LSB 爲 1): 對 CRC 寄存器異或多項式值 0xA001 (1010 0000 0000 0001).
5.重複步驟 3 和 4,直到完成 8 次移位。當做完此操作後,將完成對 8 位字節的完整操作。
6.對報文中的下一個字節重複步驟 2 到 5,繼續此操作直至所有報文被處理完畢。
7.CRC 寄存器中的最終內容爲 CRC 值.
8.當放置 CRC 值於報文時,高低字節必須交換。
3、從設備的迴應數據包格式
- 迴應數據包和主機查詢的數據包格式包是一致的
- 正常回應時,功能碼與主機發的功能碼一致(1~127)
- 異常的迴應,功能碼要在收到的功能碼基礎上加上128 例如:發 0x03 收:0x03 +128
4、MODBUS從機協議實現
- 硬件上具備串口
- 硬件上需要定時器(精確到毫秒級)
二、MODBUS移植STM32流程
1、系統初始化設計流程
1.1、配置系統時鐘
SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9);//設置系統時鐘,外部設置爲72MHZ,內部設置爲64MHZ
1.2、配置基本定時器的步驟
void BASIC_TIM_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
BASIC_TIM_APBxClock_FUN(BASIC_TIM_CLK, ENABLE); //開啓定時器時鐘,即內部時鐘CK_INT=72M
TIM_TimeBaseStructure.TIM_Period=TIM6_Period; //自動重裝載寄存器周的值(計數值)
// 累計TIM_Period 個頻率後產生一個更新或者中斷
// 時鐘預分頻數爲71,則驅動計數器的時鐘CK_CNT = CK_INT / (71+1)=1M
TIM_TimeBaseStructure.TIM_Prescaler= TIM6_Prescaler;
//TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; // 時鐘分頻因子 ,基本定時器沒有,不用管
//TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; // 計數器計數模式,基本定時器只能向上計數,沒有計數模式的設置
//TIM_TimeBaseStructure.TIM_RepetitionCounter=0; // 重複計數器的值,基本定時器沒有,不用管
TIM_TimeBaseInit(BASIC_TIM, &TIM_TimeBaseStructure); // 初始化定時器
TIM_ClearFlag(BASIC_TIM, TIM_FLAG_Update); // 清除計數器中斷標誌位
TIM_ITConfig(BASIC_TIM,TIM_IT_Update,ENABLE); // 開啓計數器中斷
TIM_Cmd(BASIC_TIM, ENABLE); // 使能計數器
//BASIC_TIM_APBxClock_FUN(BASIC_TIM_CLK, DISABLE); // 暫時關閉定時器的時鐘,等待使用
}
基本定時器頭文件
#ifdef BASIC_TIM6 // 使用基本定時器TIM6
#define BASIC_TIM TIM6
#define BASIC_TIM_APBxClock_FUN RCC_APB1PeriphClockCmd
#define BASIC_TIM_CLK RCC_APB1Periph_TIM6
#define BASIC_TIM_IRQ TIM6_IRQn
#define BASIC_TIM_IRQHandler TIM6_IRQHandler
#define TIM6_Period (1000)
#define TIM6_Prescaler (72-1)
定時器中斷函數
void BASIC_TIM_IRQHandler (void) //定時器中斷函數
{
if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET )
{
TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
}
}
配置定時器中斷使能
void ALL_NVIC_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 設置中斷組爲1
NVIC_InitStructure.NVIC_IRQChannel = BASIC_TIM_IRQ ; // 設置中斷來源
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 設置主優先級爲 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 設置搶佔優先級爲3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);C
}
主程序結構
int main(void)
{
SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9);//設置系統時鐘,外部設置爲72MHZ,內部設置爲64MHZ
BASIC_TIM_Config(); //定時器配置爲1MS
ALL_NVIC_Init(); //配置中斷優先級
}
運行程序,判斷是否到定時器中斷中的斷點完成定時器1MS定時
1.3、配置串口GPIO口
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE); // 打開串口GPIO 的時鐘
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE); // 打開串口外設的時鐘
// 將USART1 Tx 的GPIO 配置爲推輓複用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 將USART Rx 的GPIO 配置爲浮空輸入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作參數
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; // 配置波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 配置 針數據字長
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(DEBUG_USART, &USART_InitStructure); // 完成串口的初始化配置
USART_ITConfig(DEBUG_USART, USART_IT_RXNE, ENABLE); // 使能串口接收中斷
USART_Cmd(DEBUG_USART, ENABLE); // 使能串口
}
串口頭文件
// 串口2-USART1
#define DEBUG_USART USART2
#define DEBUG_USART_CLK RCC_APB1Periph_USART2
#define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd
#define DEBUG_USART_BAUDRATE 9600
// USART GPIO 引腳宏定義
#define DEBUG_USART_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_2
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_3
// USART GPIO 中斷
#define DEBUG_USART_IRQ USART2_IRQn
#define DEBUG_USART_IRQHandler USART2_IRQHandler
串口中斷函數
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp;
if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET) //判斷是否有數據接收
{
ucTemp = USART_ReceiveData( DEBUG_USART ); //將接收的一個字節保存
USART_SendData(DEBUG_USART,ucTemp); //保存後發送調試助手,
}
}
串口中斷使能函數
void ALL_NVIC_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 設置中斷組爲1
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ ; // 設置中斷來源
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 設置主優先級爲 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 設置搶佔優先級爲0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
主函數結構
int main(void)
{
SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9);//設置系統時鐘,外部設置爲72MHZ,內部設置爲64MHZ
USART_Config();
ALL_NVIC_Init();
}
運行程序,判斷是否到串口中斷中的斷點完成串口配置
1.4、配置定時器作用於串口
當串口接受完數據,開啓定時器計數,當時間>8T就開始處理數據
此時需要配置MODBUS的參數,如下
typedef struct
{
unsigned char myadd; //本設備的地址
unsigned char rcbuf[100]; //MODBUS接收緩衝區
unsigned int timout; //MODbus的數據斷續時間
unsigned char recount; //MODbus端口已經收到的數據個數
unsigned char timrun; //MODbus定時器是否計時的標誌
unsigned char reflag; //收到一幀數據的標誌
unsigned char Sendbuf[100]; //MODbus發送緩衝區
}MODBUS;
extern MODBUS modbus; //聲明全局變量,然後在C文件中調用
串口中斷配置如下
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp;
if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET) //判斷是否有數據接收
{
ucTemp = USART_ReceiveData( DEBUG_USART ); //將接收的一個字節保存
modbus.rcbuf[modbus.recount++]=ucTemp; //保存到MODBUS的接收緩存區
modbus.timout=0; //串口接收數據的過程中,定時器不計時
if(modbus.recount==1) //收到主機發來的一幀數據的第一字節
{
modbus.timrun=1; //啓動定時
}
}
}
定時器中斷配置如下
void BASIC_TIM_IRQHandler (void) //定時器中斷函數
{
if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET )
{
if(modbus.timrun!=0) //串口發送數據是否結束,結束就讓定時器定時
{
modbus.timout++; //定時器定時1毫秒,並開始記時
if(modbus.timout>=8) //間隔時間達到了時間,假設爲8T,實際3.5T即可
{
modbus.timrun=0;//關閉定時器--停止定時
modbus.reflag=1;//收到一幀數據,開始處理數據
}
}
TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
}
}
運行程序,判斷是否進入到定時器處理modbus.reflag=1;
1.5、配置處理數據包程序
void Mosbus_Event(void)
{
u16 crc;
u16 rccrc;
if(modbus.reflag==0) //沒有收到MODbus的數據包
{
return ; //沒有收到處理指令,繼續等待下一條數據
}
crc= crc16(&modbus.rcbuf[0], modbus.recount-2); //計算校驗碼
rccrc=modbus.rcbuf[modbus.recount-2]*256 + modbus.rcbuf[modbus.recount-1]; //收到的校驗碼
if(crc == rccrc) //數據包符合CRC校驗規則
{
if(modbus.rcbuf[0] == modbus.myadd) //確認數據包是否是發給本設備的
{
switch(modbus.rcbuf[1]) //分析功能碼
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbud_fun3(); break; //3號功能碼處理
case 4: break;
case 5: break;
case 6: Modbud_fun6(); break; //6號功能碼處理
case 7: break;
}
}
else if(modbus.rcbuf[0] == 0) //廣播地址,不處理
{
}
} //數據包不符合CRC校驗規則
modbus.recount=0; //清除緩存計數
modbus.reflag=0; //重新開始執行處理函數C
}
處理流程圖
1.6、功能碼程序
6號功能碼程序
void Modbud_fun6() //6號功能碼處理,寫寄存器
{
unsigned int Regadd;
unsigned int val;
unsigned int i,crc,j;
i=0;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要修改的地址
val=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //修改後的值
Reg[Regadd]=val; //修改本設備相應的寄存器
//以下爲迴應主機
modbus.Sendbuf[i++]=modbus.myadd; //發送本設備地址
modbus.Sendbuf[i++]=0x06; //發送功能碼
modbus.Sendbuf[i++]=Regadd/256; //發送修改地址高位
modbus.Sendbuf[i++]=Regadd%256; //發送修改地址低位
modbus.Sendbuf[i++]=val/256; //發送修改的值高位
modbus.Sendbuf[i++]=val%256; //發送修改的值低位
crc=crc16(modbus.Sendbuf,i); //校驗地址、功能碼、地址、數據
modbus.Sendbuf[i++]=crc/256; //發送CRC的值高位
modbus.Sendbuf[i++]=crc%256; //發送CRC的值低位
for(j=0;j<i;j++) //通過串口逐個發送
Usart_SendByte( DEBUG_USART,modbus.Sendbuf[j]);
}
3號功能碼程序
void Modbud_fun3(void) //3號功能碼處理 ---主機要讀取本從機的寄存器
{
u16 Regadd;
u16 Reglen;
u16 byte;
u16 i,j;
u16 crc;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要讀取的寄存器的首地址
Reglen=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //得到要讀取的寄存器的數量
i=0;
modbus.Sendbuf[i++]=modbus.myadd; //發送本設備地址
modbus.Sendbuf[i++]=0x03; //發送功能碼
byte=Reglen*2; //要返回的數據字節數
//modbus.Sendbuf[i++]=byte/256;
modbus.Sendbuf[i++]=byte%256; //發送要返回的數據字節數
for(j=0;j<Reglen;j++)
{
modbus.Sendbuf[i++]=Reg[Regadd+j]/256; //發送讀取數據字節數的高位
modbus.Sendbuf[i++]=Reg[Regadd+j]%256; //發送讀取數據字節數的低位
}
crc=crc16(modbus.Sendbuf,i); //CRC校驗
modbus.Sendbuf[i++]=crc/256; //發送CRC的值高位
modbus.Sendbuf[i++]=crc%256; //發送CRC的值低位
for(j=0;j<i;j++) //通過串口逐個發送
Usart_SendByte( DEBUG_USART,modbus.Sendbuf[j]);
}