目錄
前言
首先回顧一下上一節介紹的Modbus通信協議基本理論,首先介紹了Modbus通信協議的主從通信模式特點,分析了Modbus通信的傳輸特點;其次介紹了兩種Modbus通信協議基本的數據格式:Modbus-RTU協議和Modbus-ASCLL協議。Modbus通信協議是在RS-485串口實驗的基礎上實現的,簡單說就是首先要實現RS-485的串口通信,對所收發的數據串按照Modbus的規則編寫(比作數據的加密處理)因此在程序編寫上主要分爲3個步驟:1.實現1ms中斷計時的定時器;2.實現發送和接收數據的串口;3.Modbus程序編寫。本節將本着從理論落實到實踐的角度對Modbus通信協議進行代碼實現。
1. 硬件介紹
1.2 硬件電路介紹
微處理器選用:STM32F103;
RS485_IN和RS485_OUT爲收發引腳:選用MCU的PB10和PB11引腳作爲RS-485的接收引腳和發送引腳;
RS485_DE爲收發狀態控制引腳:當RS485_DE爲高電平時,芯片處於發送狀態;當RS485_DE爲低電平時,芯片處於接收狀態;
阻抗匹配電路:R44、R45、R46爲阻抗匹配電阻
1.2 硬件通信平臺
Modbus通信實驗平臺搭建如下:將USB串口準換成TTL電平,再將TTL電平轉換爲RS-485差分信號之後連接於STM32從機1。
Modbus通信實驗調試軟件如下:
下載鏈接鏈接:https://pan.baidu.com/s/1ccJkBmZJQhuChypKy-qoug
提取碼:ppe1
2. 軟件介紹
要想實現Modbus的程序,首先應當完成三件事:
(1)實現1ms中斷計時的定時器;
(2)實現發送和接收數據的串口;
(3)Modbus程序編寫。
2.1 定時器程序設計
利用TIM2實現1ms的定時中斷功能。
2.1.1 配置時鐘函數
/******************************************************************
功能: 配置時鐘函數
******************************************************************/
//通用定時器 2 中斷初始化
void Timer2_Init() //1ms產生1次更新事件
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//結構體類型初始化:包含自動重裝載值,分頻係數,計數方式
//①TIM2時鐘使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_DeInit(TIM2);
//定時器TIM2初始化
TIM_TimeBaseStructure.TIM_Period=1000-1; // 自動重裝載週期 1ms
TIM_TimeBaseStructure.TIM_Prescaler=72-1; // 分頻係數72M/72=1MHZ-->1us
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //設置時鐘分頻因子爲不分頻
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //計數方式爲向上計數
//②初始化定時器參數
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
//③設置TIM2允許更新中斷
TIM_ITConfig(TIM2, TIM_IT_Update,ENABLE);//使能TIM2更新中斷
//④使能TIM2
TIM_Cmd(TIM2,ENABLE);
}
2.1.2 定時器中斷服務子程序
/******************************************************************
功能: 定時器中斷服務子程序
******************************************************************/
void TIM2_IRQHandler() //定時器2的中斷服務子函數 1ms一次中斷
{
u8 st;
st= TIM_GetFlagStatus(TIM2, TIM_FLAG_Update); //檢測TIM2中斷更新標誌位
if(st==SET) //如果TIM2滿足中斷標誌
{
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除TIM2中斷更新標誌位
//每一毫秒所要執行的任務
if(modbus.timrun!=0)
{
modbus.timout++;
if(modbus.timout>=4) //間隔時間達到了4毫秒時間
{
modbus.timrun=0;//關閉定時器--停止定時
modbus.reflag=1; //收到一幀數據
}
}
}
}
2.2 串口程序設計
從庫函數操作層面結合寄存器的描述來設置串口,以達到我們最基本的通信功能。串口設置的一般步驟可以總結爲如下幾個步驟:
(1)串口時鐘使能, GPIO 時鐘使能;
(2)串口復位;
(3)GPIO 端口模式設置;
(4)串口參數初始化;
(5)開啓中斷並且初始化 NVIC;
(6)使能串口;
(7)編寫中斷處理函數。
2.2.1 配置串口函數:
void RS485_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
//①串口時鐘使能, GPIO 時鐘使能,複用時鐘使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE); //開啓USART2時鐘
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); //開啓USART2時鐘
//②串口復位
USART_DeInit(USART2); //串口2復位
//RS485DE引腳初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP; //複用推輓輸出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.5
RS485_RT_0; //使MAX485芯片處於接收狀態(收發控制引腳)
//③GPIO端口模式設置
//初始化485串口引腳以及串口配置
//USART1_TX PB.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //複用推輓輸出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.2
//USART1_RX PB.11
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空輸入
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.3
//④串口參數初始化、結構體指針成員變量
USART_InitStructure.USART_BaudRate = 9600; //設置波特率爲9600;
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(USART2, &USART_InitStructure); //串口參數初始化
//⑤開啓中斷
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE); //開啓串口響應中斷,USART_IT_RXNE接收到數據中斷
//⑥使能串口
USART_Cmd(USART2, ENABLE);//串口使能
USART_ClearFlag(USART2,USART_FLAG_TC ); //清除串口TC發送完成中斷標誌
2.2.2 初始化中斷服務子程序:
/******************************************************************
功能: 初始化NVIC
******************************************************************/
//⑤初始化NVIC(定時器中斷+串口中斷)
void NVIC_Init()
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 中斷優先級
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //定時器產生更新事件中斷
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ; //搶佔優先級1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子優先級2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ; //搶佔優先級1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子優先級0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure);
}
2.2.3 串口中斷響應事件:
/******************************************************************
功能: Modbus3字節接收中斷處理
******************************************************************/
void USART2_IRQHandler() //MODBUS字節接收中斷
{
u8 st,sbuf;
st=USART_GetITStatus(USART2, USART_IT_RXNE); //判斷讀寄存器是否非空(RXNE)
if(st==SET) //返回值是 SET,說明是串口接收到數據完成中斷髮生
{
sbuf=USART2->DR;
if( modbus.reflag==1) //有數據包正在處理
{
return ;
}
modbus.rcbuf[modbus.recount++]=sbuf;//利用數組存放接收的數據
modbus.timout=0;
if(modbus.recount==1) //收到主機發來的一幀數據的第一字節
{
modbus.timrun=1; //啓動定時
}
}
}
2.3 modbus程序編寫
2.2.1 crc16較驗程序
根據crc16的高位字節值表和低位字節值表編寫校驗程序。
/******************************************************************
功能: CRC16校驗
******************************************************************/
uint crc16( uchar *puchMsg, uint usDataLen )
{
uchar uchCRCHi = 0xFF ; // 高CRC字節初始化
uchar uchCRCLo = 0xFF ; // 低CRC 字節初始化
unsigned long uIndex ; // CRC循環中的索引
while ( usDataLen-- ) // 傳輸消息緩衝區
{
uIndex = uchCRCHi ^ *puchMsg++ ; // 計算CRC
uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
uchCRCLo = auchCRCLo[uIndex] ;
}
return ( uchCRCHi << 8 | uchCRCLo ) ;
}
2.3.2 Modbus宏定義
#define RS485_RT_1 GPIO_SetBits(GPIOA, GPIO_Pin_5) //485發送狀態
#define RS485_RT_0 GPIO_ResetBits(GPIOA, GPIO_Pin_5) //485置接收狀態
typedef struct
{
u8 myadd; //本設備的地址
u8 rcbuf[64]; //Modbus接收緩衝區64個字節
u16 timout; //Modbus的數據斷續時間
u8 recount; //Modbus端口已經收到的數據個數
u8 timrun; //Modbus定時器是否計時的標誌
u8 reflag; //收到一幀數據的標誌
u8 Sendbuf[64]; //Modbus發送緩衝區
}
MODBUS;
2.3.3 Modbus函數初始化
/******************************************************************
功能: Modbus函數初始化
******************************************************************/
void Modbus_Init()
{
modbus.myadd=4; //本從設備的地址
modbus.timrun=0; //Modbus定時器停止計時
RS485_Init();
}
2.3.4 Modbus事件函數
/******************************************************************
功能: Modbus事件函數
******************************************************************/
void Mosbus_Event()
{
u16 crc;
u16 rccrc;
if(modbus.reflag==0) //沒有收到Modbus的數據
{
return ;
}
crc= crc16(&modbus.rcbuf[0], modbus.recount-2); //計算校驗碼,-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) //如果是廣播地址則不處理
{
}
}
modbus.recount=0;
modbus.reflag=0;
}
2.3.5 Modbus讀功能碼處理
/******************************************************************
功能: Modbus3號功能碼處理
******************************************************************/
void Modbud_fun3() //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;
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);
modbus.Sendbuf[i++]=crc/256; //
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1;
for(j=0;j<i;j++)
{
RS485_byte(modbus.Sendbuf[j]);
}
RS485_RT_0;
}
2.3.6 Modbus寫單個寄存器功能碼處理
/******************************************************************
功能: Modbus6寫單個寄存器功能碼處理
******************************************************************/
void Modbud_fun6() //6號功能碼處理
{
u16 Regadd;
u16 val;
u16 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; //
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1;
for(j=0;j<i;j++)
{
RS485_byte(modbus.Sendbuf[j]);
}
RS485_RT_0;
}
2.3.7 主函數
/******************************************************************
功能: 主函數
******************************************************************/
u16 Reg[]={0x0000, //本設備寄存器中的值
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
};
int main()
{
Timer2_Init(); //初始化定時器Timer2
Mosbus_Init(); //初始化定時器中斷
Isr_Init();
while(1)
{
Mosbus_Event(); //處理MODbus數據
}
}
往期博客: