單片機 Modbus 多機通信程序設(shè)計
給從機下發(fā)不同的指令,從機去執(zhí)行不同的操作,這個就是判斷一下功能碼即可,和我們前邊學(xué)的實用串口例程是類似的。多機通信,無非就是添加了一個設(shè)備地址判斷而已,難度也不大。我們找了一個 Modbus 調(diào)試精靈,通過設(shè)置設(shè)備地址,讀寫寄存器的地址以及數(shù)值數(shù)量等參數(shù),可以直接替代串口調(diào)試助手,比較方便的下發(fā)多個字節(jié)的數(shù)據(jù),如圖18-7所示。我們先來就圖中的設(shè)置和數(shù)據(jù)來對 Modbus 做進一步的分析,圖中的數(shù)據(jù)來自于調(diào)試精靈與我們接下來要講的例程之間的交互。
圖18-7 Modbus 調(diào)試精靈
如圖,我們的 USB 轉(zhuǎn) RS485 模塊虛擬出的是 COM5,波特率9600,無校驗位,數(shù)據(jù)位是8位,1位停止位,設(shè)備地址假設(shè)為1。
寫寄存器的時候,如果我們要把01寫到一個地址是0000的寄存器地址里,點一下“寫入”,就會出現(xiàn)發(fā)送指令:01 06 00 00 00 01 48 0A。我們來分析一下這幀數(shù)據(jù),其中01是設(shè)備地址,06是功能碼,代表寫寄存器這個功能,后邊跟00 00表示的是要寫入的寄存器的地址,00 01就是要寫入的數(shù)據(jù),48 0A就是 CRC 校驗碼,這是軟件自動算出來的。而根據(jù) Modbus 協(xié)議,當(dāng)寫寄存器的時候,從機成功完成該指令的操作后,會把主機發(fā)送的指令直接返回,我們的調(diào)試精靈會接收到這樣一幀數(shù)據(jù):01 06 00 00 00 01 48 0A。
假如我們現(xiàn)在要從寄存器地址0002開始讀取寄存器,并且讀取的數(shù)量是2個。點一下“讀出”,就會出現(xiàn)發(fā)送指令:01 03 00 02 00 02 65 CB。其中01是設(shè)備地址,03是功能碼,代表讀寄存器這個功能,00 02就是讀寄存器的起始地址,后一個00 02就是要讀取2個寄存器的數(shù)值,65 CB就是 CRC 校驗。而接收到的數(shù)據(jù)是:01 03 04 00 00 00 00 FA 33。其中01是設(shè)備地址,03是功能碼,04代表的是后邊讀到的數(shù)據(jù)字節(jié)數(shù)是4個,00 00 00 00分別是地址00 02和00 03的寄存器內(nèi)部的數(shù)據(jù),而 FA 33 就是 CRC 校驗了。
似乎越來越明朗了,所謂的 Modbus 通信協(xié)議,無非就是主機下發(fā)了不同的指令,從機根據(jù)指令的判斷來執(zhí)行不同的操作而已。由于我們的開發(fā)板沒有 Modbus 功能碼那么多相應(yīng)的功能,我們在程序中定義了一個數(shù)組 regGroup[5],相當(dāng)于5個寄存器,此外又定義了第6個寄存器,控制蜂鳴器,通過下發(fā)不同的指令我們改變寄存器組的數(shù)據(jù)或者改變蜂鳴器的開關(guān)狀態(tài)。在 Modbus 協(xié)議里寄存器的地址和數(shù)值都是16位的,即2個字節(jié),我們默認高字節(jié)是 0x00,低字節(jié)就是數(shù)組 regGroup 對應(yīng)的值。其中地址 0x0000 到 0x0004 對應(yīng)的就是 regGroup數(shù)組中的元素,我們寫入的同時把數(shù)字又顯示到 1602 液晶上,而 0x0005 這個地址,寫入 0x00,蜂鳴器就不響,寫入任何其它數(shù)值,蜂鳴器就報警。我們單片機的主要工作也就是解析串口接收的數(shù)據(jù)執(zhí)行不同操作。 /*Lcd1602.c 文件程序源代碼***/ (此處省略,可參考之前章節(jié)的代碼) /****RS485.c 文件程序源代碼*****/ (此處省略,可參考之前章節(jié)的代碼) /****CRC16.c 文件程序源代碼****/
/* CRC16 計算函數(shù),ptr-數(shù)據(jù)指針,len-數(shù)據(jù)長度,返回值-計算出的 CRC16 數(shù)值 */
unsigned int GetCRC16(unsigned char *ptr, unsigned char len){
unsigned int index;
unsigned char crch = 0xFF; //高 CRC 字節(jié)
unsigned char crcl = 0xFF; //低 CRC 字節(jié)
unsigned char code TabH[] = { //CRC 高位字節(jié)值表
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
} ;
unsigned char code TabL[] = { //CRC 低位字節(jié)值表
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,[!--empirenews.page--]
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);
}
關(guān)于 CRC 校驗的算法,如果不是專門學(xué)習(xí)校驗算法本身,大家可以不去研究這個程序的細節(jié),直接使用現(xiàn)成的函數(shù)即可。 /*****main.c 文件程序源代碼**/
#include
sbit BUZZ = P1^6;
bit flagBuzzOn = 0; //蜂鳴器啟動標(biāo)志
unsigned char T0RH = 0; //T0 重載值的高字節(jié)
unsigned char T0RL = 0; //T0 重載值的低字節(jié)
unsigned char regGroup[5]; //Modbus 寄存器組,地址為 0x00~0x04
void ConfigTimer0(unsigned int ms);
extern void UartDriver();
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartWrite(unsigned char *buf, unsigned char len);
extern unsigned int GetCRC16(unsigned char *ptr, unsigned char len);
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
void main(){
EA = 1; //開總中斷
ConfigTimer0(1); //配置 T0 定時 1ms
ConfigUART(9600); //配置波特率為 9600
InitLcd1602(); //初始化液晶
while (1){
UartDriver(); //調(diào)用串口驅(qū)動
}
}
/* 串口動作函數(shù),根據(jù)接收到的命令幀執(zhí)行響應(yīng)的動作
buf-接收到的命令幀指針,len-命令幀長度 */
void UartAction(unsigned char *buf, unsigned char len){
unsigned char i;
unsigned char cnt;
unsigned char str[4];
unsigned int crc;
unsigned char crch, crcl;
/* 本例中的本機地址設(shè)定為 0x01,
如數(shù)據(jù)幀中的地址字節(jié)與本機地址不符,
則直接退出,即丟棄本幀數(shù)據(jù)不做任何處理 */
if (buf[0] != 0x01){
return;
}
//地址相符時,再對本幀數(shù)據(jù)進行校驗
crc = GetCRC16(buf, len-2); //計算 CRC 校驗值
crch = crc >> 8;
crcl = crc & 0xFF;
if ((buf[len-2]!=crch) || (buf[len-1]!=crcl)){
return; //如 CRC 校驗不符時直接退出
}
//地址和校驗字均相符后,解析功能碼,執(zhí)行相關(guān)操作
switch (buf[1]){
case 0x03: //讀取一個或連續(xù)的寄存器
if ((buf[2]==0x00) && (buf[3]<=0x05)){ //只支持 0x0000~0x0005
if (buf[3] <= 0x04){
i = buf[3]; //提取寄存器地址
cnt = buf[5]; //提取待讀取的寄存器數(shù)量
buf[2] = cnt*2; //讀取數(shù)據(jù)的字節(jié)數(shù),為寄存器數(shù)*2
len = 3; //幀前部已有地址、功能碼、字節(jié)數(shù)共 3 個字節(jié)
while (cnt--){
buf[len++] = 0x00; //寄存器高字節(jié)補 0
buf[len++] = regGroup[i++]; //寄存器低字節(jié)
}
}else{ //地址 0x05 為蜂鳴器狀態(tài)
buf[2] = 2; //讀取數(shù)據(jù)的字節(jié)數(shù)
buf[3] = 0x00;
buf[4] = flagBuzzOn;
len = 5;
}
break;
}else{ //寄存器地址不被支持時,返回錯誤碼
buf[1] = 0x83; //功能碼最高位置 1
buf[2] = 0x02; //設(shè)置異常碼為 02-無效地址
len = 3;
break;
}
case 0x06: //寫入單個寄存器
if ((buf[2]==0x00) && (buf[3]<=0x05)){ //只支持 0x0000~0x0005
if (buf[3] <= 0x04){
i = buf[3]; //提取寄存器地址
regGroup[i] = buf[5]; //保存寄存器數(shù)據(jù)
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‘;
}
[!--empirenews.page--]str[2] = ‘