(ON/OFF)
(ON/OFF)
8個內部線圈的通斷狀態,這8個線圈的地址由控制器決定,用戶邏輯可以將這些線圈定義,以說明從機狀態,短報文適宜於迅速讀取狀態
484
PC從機邏輯
484
9的報文發送後,本功能碼才發送
ModBus事務處理通信事件記錄。如果某項事務處理完成,記錄會給出有關錯誤
PC從機邏輯
13的報文發送後,本功能碼才得發送
和MICRO84
PC狀態邏輯
程序對功能碼的處理,就是來檢測這個字節的數值,然後根據其數值來做相應的功能處理。
數據:跟在功能代碼後邊的是n個8bit的數據。這個n值的到底是多少,是功能代碼來確定的,不同的功能代碼後邊跟的數據數量不同。舉個例子,如果功能碼是0x03,也就是讀保持寄存器,那麼主機發送數據n的組成部分就是:2個字節的寄存器起始地址,加2個字節的寄存器數量N。從機數據n的組成部分是:1個字節的字節數,因為我們的寄存器的值是2個字節,所以這個字節數也就是2N個,再加上2N個寄存器的值,如圖18-6所示。
圖18-6讀保持寄存器數據結構
CRC校驗:CRC校驗是一種數據算法,是用來校驗數據對錯的。CRC校驗函數把一幀數據除最後兩個字節外,前邊所有的字節進行特定的算法計算,計算完後生成了一個16bit的數據,作為CRC校驗碼,添加在一幀數據的最後。接收方接收到數據後,同樣會把前邊的字節進行CRC計算,計算完了再和發過來的16bit的CRC數據進行比較,如果相同則認為數據正常,沒有出錯,如果比較不相同,則說明數據在傳輸中發生了錯誤,這幀數據將被丟棄,就像沒收到一樣,而發送方會在得不到回應後做相應的處理錯誤處理。
RTU模式的每個字節的位是這樣分布的:1個起始位、8個數據位,最小有效位先發送、1個奇偶校驗位(如果無校驗則沒有這一位)、1位停止位(有校驗位時)或者2個停止位(無校驗位時)。
給從機下發不同的指令,從機去執行不同的操作,這個就是判斷一下功能碼即可,和我們前邊學的實用串口例程是類似的。多機通信,無非就是添加了一個設備地址判斷而已,難度也不大。我們找了一個Modbus調試精靈,通過設置設備地址,讀寫寄存器的地址以及數值數量等參數,可以直接替代串口調試助手,比較方便的下發多個字節的數據,如圖18-7所示。我們先來就圖中的設置和數據來對Modbus做進一步的分析,圖中的數據來自於調試精靈與我們接下來要講的例程之間的交互。
圖18-7Modbus調試精靈
如圖,我們的USB轉RS485模塊虛擬出的是COM5,波特率9600,無校驗位,數據位是8位,1位停止位,設備地址假設為1。
寫寄存器的時候,如果我們要把01寫到一個地址是0000的寄存器地址裡,點一下「寫入」,就會出現發送指令:010600000001480A。我們來分析一下這幀數據,其中01是設備地址,06是功能碼,代表寫寄存器這個功能,後邊跟0000表示的是要寫入的寄存器的地址,0001就是要寫入的數據,480A就是CRC校驗碼,這是軟體自動算出來的。而根據Modbus協議,當寫寄存器的時候,從機成功完成該指令的操作後,會把主機發送的指令直接返回,我們的調試精靈會接收到這樣一幀數據:010600000001480A。
假如我們現在要從寄存器地址0002開始讀取寄存器,並且讀取的數量是2個。點一下「讀出」,就會出現發送指令:01030002000265CB。其中01是設備地址,03是功能碼,代表讀寄存器這個功能,0002就是讀寄存器的起始地址,後一個0002就是要讀取2個寄存器的數值,65CB就是CRC校驗。而接收到的數據是:01030400000000FA33。其中01是設備地址,03是功能碼,04代表的是後邊讀到的數據字節數是4個,00000000分別是地址為0002和0003的寄存器內部的數據,而FA33就是CRC校驗了。
似乎越來越明朗了,所謂的Modbus通信協議,無非就是主機下發了不同的指令,從機根據指令的判斷來執行不同的操作而已。由於我們的開發板沒有Modbus功能碼那麼多相應的功能,我們在程序中定義了一個數組regGroup[5],相當於5個寄存器,此外又定義了第6個寄存器,控制蜂鳴器,通過下發不同的指令我們改變寄存器組的數據或者改變蜂鳴器的開關狀態。在Modbus協議裡寄存器的地址和數值都是16位的,即2個字節,我們默認高字節是0x00,低字節就是數組regGroup對應的值。其中地址0x0000到0x0004對應的就是regGroup數組中的元素,我們寫入的同時把數字又顯示到1602液晶上,而0x0005這個地址,寫入0x00,蜂鳴器就不響,寫入任何其它數值,蜂鳴器就報警。我們單片機的主要工作也就是解析串口接收的數據執行不同操作。
/*Lcd1602.c文件程序原始碼*/
(此處省略,可參考之前章節的代碼)
/RS485.c文件程序原始碼*/
(此處省略,可參考之前章節的代碼)
/CRC16.c文件程序原始碼/
/*CRC16計算函數,ptr-數據指針,len-數據長度,返回值-計算出的CRC16數值*/
unsignedintGetCRC16(unsignedchar*ptr,unsignedcharlen)
{
unsignedintindex;
unsignedcharcrch=0xFF;//高CRC字節
unsignedcharcrcl=0xFF;//低CRC字節
unsignedcharcodeTabH[]={//CRC高位字節值表
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,
0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,
0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,
0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,
0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,
0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,
0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,
0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,
0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,
0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,
0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,
0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,
0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,
0x80,0x41,0x00,0xC1,0x81,0x40
};
unsignedcharcodeTabL[]={//CRC低位字節值表
0x00,0xC0,0xC1,0x01,0xC3,0x03,0x02,0xC2,0xC6,0x06,
0x07,0xC7,0x05,0xC5,0xC4,0x04,0xCC,0x0C,0x0D,0xCD,
0x0F,0xCF,0xCE,0x0E,0x0A,0xCA,0xCB,0x0B,0xC9,0x09,
0x08,0xC8,0xD8,0x18,0x19,0xD9,0x1B,0xDB,0xDA,0x1A,
0x1E,0xDE,0xDF,0x1F,0xDD,0x1D,0x1C,0xDC,0x14,0xD4,
0xD5,0x15,0xD7,0x17,0x16,0xD6,0xD2,0x12,0x13,0xD3,
0x11,0xD1,0xD0,0x10,0xF0,0x30,0x31,0xF1,0x33,0xF3,
0xF2,0x32,0x36,0xF6,0xF7,0x37,0xF5,0x35,0x34,0xF4,
0x3C,0xFC,0xFD,0x3D,0xFF,0x3F,0x3E,0xFE,0xFA,0x3A,
0x3B,0xFB,0x39,0xF9,0xF8,0x38,0x28,0xE8,0xE9,0x29,
0xEB,0x2B,0x2A,0xEA,0xEE,0x2E,0x2F,0xEF,0x2D,0xED,
0xEC,0x2C,0xE4,0x24,0x25,0xE5,0x27,0xE7,0xE6,0x26,
0x22,0xE2,0xE3,0x23,0xE1,0x21,0x20,0xE0,0xA0,0x60,
0x61,0xA1,0x63,0xA3,0xA2,0x62,0x66,0xA6,0xA7,0x67,
0xA5,0x65,0x64,0xA4,0x6C,0xAC,0xAD,0x6D,0xAF,0x6F,
0x6E,0xAE,0xAA,0x6A,0x6B,0xAB,0x69,0xA9,0xA8,0x68,
0x78,0xB8,0xB9,0x79,0xBB,0x7B,0x7A,0xBA,0xBE,0x7E,
0x7F,0xBF,0x7D,0xBD,0xBC,0x7C,0xB4,0x74,0x75,0xB5,
0x77,0xB7,0xB6,0x76,0x72,0xB2,0xB3,0x73,0xB1,0x71,
0x70,0xB0,0x50,0x90,0x91,0x51,0x93,0x53,0x52,0x92,
0x96,0x56,0x57,0x97,0x55,0x95,0x94,0x54,0x9C,0x5C,
0x5D,0x9D,0x5F,0x9F,0x9E,0x5E,0x5A,0x9A,0x9B,0x5B,
0x99,0x59,0x58,0x98,0x88,0x48,0x49,0x89,0x4B,0x8B,
0x8A,0x4A,0x4E,0x8E,0x8F,0x4F,0x8D,0x4D,0x4C,0x8C,
0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86,0x82,0x42,
0x43,0x83,0x41,0x81,0x80,0x40
};
while(len--)//計算指定長度的CRC
{
index=crch^*ptr++;
crch=crcl^TabH[index];
crcl=TabL[index];
}
return((crch<<8)|crcl);
}
關於CRC校驗的算法,如果不是專門學習校驗算法本身,大家可以不去研究這個程序的細節,直接使用現成的函數即可。
/*main.c文件程序原始碼/
#include
sbitBUZZ=P1^6;
bitflagBuzzOn=0;//蜂鳴器啟動標誌
unsignedcharT0RH=0;//T0重載值的高字節
unsignedcharT0RL=0;//T0重載值的低字節
unsignedcharregGroup[5];//Modbus寄存器組,地址為0x00~0x04
voidConfigTimer0(unsignedintms);
externvoidUartDriver();
externvoidConfigUART(unsignedintbaud);
externvoidUartRxMonitor(unsignedcharms);
externvoidUartWrite(unsignedchar*buf,unsignedcharlen);
externunsignedintGetCRC16(unsignedchar*ptr,unsignedcharlen);
externvoidInitLcd1602();
externvoidLcdShowStr(unsignedcharx,unsignedchary,unsignedchar*str);
voidmain()
{
EA=1;//開總中斷
ConfigTimer0(1);//配置T0定時1ms
ConfigUART(9600);//配置波特率為9600
InitLcd1602();//初始化液晶
while(1)
{
UartDriver();//調用串口驅動
}
}
/*串口動作函數,根據接收到的命令幀執行響應的動作
buf-接收到的命令幀指針,len-命令幀長度*/
voidUartAction(unsignedchar*buf,unsignedcharlen)
{
unsignedchari;
unsignedcharcnt;
unsignedcharstr[4];
unsignedintcrc;
unsignedcharcrch,crcl;
if(buf[0]!=0x01)//本例中的本機地址設定為0x01,
{//如數據幀中的地址字節與本機地址不符,
return;//則直接退出,即丟棄本幀數據不做任何處理
}
//地址相符時,再對本幀數據進行校驗
crc=GetCRC16(buf,len-2);//計算CRC校驗值
crch=crc>>8;
crcl=crc&0xFF;
if((buf[len-2]!=crch)||(buf[len-1]!=crcl))
{
return;//如CRC校驗不符時直接退出
}
//地址和校驗字均相符後,解析功能碼,執行相關操作
switch(buf[1])
{
case0x03://讀取一個或連續的寄存器
if((buf[2]==0x00)&&(buf[3]<=0x05))//只支持0x0000~0x0005
{
if(buf[3]<=0x04)
{
i=buf[3];//提取寄存器地址
cnt=buf[5];//提取待讀取的寄存器數量
buf[2]=cnt*2;//讀取數據的字節數,為寄存器數*2
len=3;//幀前部已有地址、功能碼、字節數共3個字節
while(cnt--)
{
buf[len++]=0x00;//寄存器高字節補0
buf[len++]=regGroup[i++];//寄存器低字節
}
}
else//地址0x05為蜂鳴器狀態
{
buf[2]=2;//讀取數據的字節數
buf[3]=0x00;
buf[4]=flagBuzzOn;
len=5;
}
break;
}
else//寄存器地址不被支持時,返回錯誤碼
{
buf[1]=0x83;//功能碼最高位置1
buf[2]=0x02;//設置異常碼為02-無效地址
len=3;
break;
}
case0x06://寫入單個寄存器
if((buf[2]==0x00)&&(buf[3]<=0x05))//只支持0x0000~0x0005
{
if(buf[3]<=0x04)
{
i=buf[3];//提取寄存器地址
regGroup[i]=buf[5];//保存寄存器數據
cnt=regGroup[i]>>4;//顯示到液晶上
if(cnt>=0xA)
str[0]=cnt-0xA+A;
else
str[0]=cnt+0;
cnt=regGroup[i]&0x0F;
if(cnt>=0xA)
str[1]=cnt-0xA+A;
else
str[1]=cnt+0;
str[2]=;
LcdShowStr(i*3,0,str);
}
else//地址0x05為蜂鳴器狀態
{
flagBuzzOn=(bit)buf[5];//寄存器值轉為蜂鳴器的開關
}
len-=2;//長度-2以重新計算CRC並返回原幀
&nb
break;
}
else//寄存器地址不被支持時,返回錯誤碼
{
buf[1]=0x86;//功能碼最高位置1
buf[2]=0x02;//設置異常碼為02-無效地址
len=3;
break;
}
default://其它不支持的功能碼
buf[1]|=0x80;//功能碼最高位置1
buf[2]=0x01;//設置異常碼為01-無效功能
len=3;
break;
}
crc=GetCRC16(buf,len);//計算返回幀的CRC校驗值
buf[len++]=crc>>8;//CRC高字節
buf[len++]=crc&0xFF;//CRC低字節
UartWrite(buf,len);//發送返回幀
}
/*配置並啟動T0,ms-T0定時時間*/
voidConfigTimer0(unsignedintms)
{
unsignedlongtmp;//臨時變量
tmp=11059200/12;//定時器計數頻率
tmp=(tmp*ms)/1000;//計算所需的計數值
tmp=65536-tmp;//計算定時器重載值
tmp=tmp+33;//補償中斷響應延時造成的誤差
T0RH=(unsignedchar)(tmp>>8);//定時器重載值拆分為高低字節
T0RL=(unsignedchar)tmp;
TMOD&=0xF0;//清零T0的控制位
TMOD|=0x01;//配置T0為模式1
TH0=T0RH;//加載T0重載值
TL0=T0RL;
ET0=1;//使能T0中斷
TR0=1;//啟動T0
}
/*T0中斷服務函數,執行串口接收監控和蜂鳴器驅動*/
voidInterruptTimer0()interrupt1
{
TH0=T0RH;//重新加載重載值
TL0=T0RL;
if(flagBuzzOn)//執行蜂鳴器鳴叫或關閉
BUZZ=~BUZZ;
else
BUZZ=1;
UartRxMonitor(1);//串口接收監控
}
大家可以看到負責解析協議的UartAction函數很長,因為協議解析本來就是一件很繁瑣的事情。我們的例程僅解析執行了兩個功能命令,就已經有近百行程序了,如果你需要解析更多的功能命令的話,那麼建議把每個功能都做一個函數,然後在相應的case分支裡調用即可,這樣就不會使單個函數過於龐大而難以維護。
1、了解RS485通信以及和RS232的不同用法。
2、了解Modbus協議以及RTU數據幀的規則。
3、寫一個電子鐘程序,並且可以通過485調試器校時。