Modbus多機通信例程
給從機下發(fā)不同的指令,從機去執(zhí)行不同的操作,這個就是判斷一下功能碼即可,和我們前邊學的實用串口例程是類似的。多機通信,無非就是添加了一個設(shè)備地址判斷而已,難度也不是很大。我們找了一個Modbus調(diào)試精靈,通過設(shè)置設(shè)備地址,讀寫寄存器的地址以及數(shù)值數(shù)量等參數(shù),可以直接替代串口調(diào)試助手,比較方便的下發(fā)多個字節(jié)的數(shù)據(jù),如圖1所示。我們先來就圖中的設(shè)置和數(shù)據(jù)來對Modbus做進一步的分析,圖中的數(shù)據(jù)來自于調(diào)試精靈與我們接下來要講的例程之間的交互。
圖1 Modbus調(diào)試精靈
如圖:我們的USB轉(zhuǎn)485模塊虛擬出的是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é)議,當寫寄存器的時候,從機成功完成該指令的操作后,會把主機發(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功能碼那么多相應的功能,我們在程序中定義了一個數(shù)組regGroup[5],相當于5個寄存器,此外又定義了第6個寄存器,控制蜂鳴器,通過下發(fā)不同的指令我們改變寄存器組的數(shù)據(jù)或者改變蜂鳴器的開關(guān)狀態(tài)。在Modbus協(xié)議里寄存器的地址和數(shù)值都是16位的,即2個字節(jié),我們默認高字節(jié)是0x00,低字節(jié)就是數(shù)組regGroup對應的值。其中地址0x0000到0x0004對應的就是regGroup數(shù)組中的元素,我們寫入的同時把數(shù)字又顯示到我們的LCD1602液晶上,而0x0005這個地址,寫入0x00,蜂鳴器就不響,寫入任何其他數(shù)字,蜂鳴器就報警。我們單片機的主要工作也就是解析串口接收的數(shù)據(jù)執(zhí)行不同操作,也就是主要在RS485.C這個文件中了
/***********************RS485.c文件程序源代碼*************************/
#include <reg52.h>
#include <intrins.h>
sbit RS485_DIR = P1^7; //RS485方向選擇引腳
bit flagOnceTxd = 0; //單次發(fā)送完成標志,即發(fā)送完一個字節(jié)
bit cmdArrived = 0; //命令到達標志,即接收到上位機下發(fā)的命令
unsigned char cntRxd = 0;
unsigned char pdata bufRxd[40]; //串口接收緩沖區(qū)
unsigned char regGroup[5]; //Modbus寄存器組,地址為0x00~0x04
extern bit flagBuzzOn;
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern unsigned int GetCRC16(unsigned char *ptr, unsigned char len);
void ConfigUART(unsigned int baud) //串口配置函數(shù),baud為波特率
{
RS485_DIR = 0; //RS485設(shè)置為接收方向
SCON = 0x50; //配置串口為模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1為模式2
TH1 = 256 - (11059200/12/32) / baud; //計算T1重載值
TL1 = TH1; //初值等于重載值
ET1 = 0; //禁止T1中斷
ES = 1; //使能串口中斷
TR1 = 1; //啟動T1
}
unsigned char UartRead(unsigned char *buf, unsigned char len) //串口數(shù)據(jù)讀取函數(shù),數(shù)據(jù)接收指針buf,讀取數(shù)據(jù)長度len,返回值為實際讀取到的數(shù)據(jù)長度
{
unsigned char i;
if (len > cntRxd) //讀取長度大于接收到的數(shù)據(jù)長度時,
{
len = cntRxd; //讀取長度設(shè)置為實際接收到的數(shù)據(jù)長度
}
for (i=0; i<len; i++) //拷貝接收到的數(shù)據(jù)
{
*buf = bufRxd[i];
buf++;
}
cntRxd = 0; //清零接收計數(shù)器
return len; //返回實際讀取長度
}
void DelayX10us(unsigned char t) //軟件延時函數(shù),延時時間(t*10)us
{
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (--t);
}
void UartWrite(unsigned char *buf, unsigned char len) //串口數(shù)據(jù)寫入函數(shù),即串口發(fā)送函數(shù),待發(fā)送數(shù)據(jù)指針buf,數(shù)據(jù)長度len
{
RS485_DIR = 1; //RS485設(shè)置為發(fā)送
while (len--) //發(fā)送數(shù)據(jù)
{
flagOnceTxd = 0;
SBUF = *buf;
buf++;
while (!flagOnceTxd);
}
DelayX10us(5); //等待最后的停止位完成,延時時間由波特率決定
RS485_DIR = 0; //RS485設(shè)置為接收
}
void UartDriver() //串口驅(qū)動函數(shù),檢測接收到的命令并執(zhí)行相應動作
{
unsigned char i;
unsigned char cnt;
unsigned char len;
unsigned char buf[30];
unsigned char str[4];
unsigned int crc;
unsigned char crch, crcl;
if (cmdArrived) //有命令到達時,讀取處理該命令
{
cmdArrived = 0;
len = UartRead(buf, sizeof(buf)); //將接收到的命令讀取到緩沖區(qū)中
if (buf[0] == 0x01) //核對地址以決定是否響應命令,本例本機地址為0x01
{
crc = GetCRC16(buf, len-2); //計算CRC校驗值
crch = crc >> 8;
crcl = crc & 0xFF;
if ((buf[len-2] == crch) && (buf[len-1] == crcl)) //判斷CRC校驗是否正確
{
switch (buf[1]) //按功能碼執(zhí)行操作
{
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,因Modbus定義的寄存器為16位
len = 3;
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';
str[2] = '\0';
LcdShowStr(i*3, 0, str);
}
else //地址0x05為蜂鳴器狀態(tài)
{
flagBuzzOn = (bit)buf[5]; //寄存器值轉(zhuǎn)換為蜂鳴器的開關(guān)
}
len -= 2; //長度-2以重新計算CRC并返回原幀
break;
}
else //寄存器地址不被支持時,返回錯誤碼
{
buf[1] = 0x86; //功能碼最高位置1
buf[2] = 0x02; //設(shè)置異常碼為02-無效地址
len = 3;
break;
}
default: //其它不支持的功能碼
buf[1] |= 0x80; //功能碼最高位置1
buf[2] = 0x01; //設(shè)置異常碼為01-無效功能
len = 3;
break;
}
crc = GetCRC16(buf, len); //計算CRC校驗值
buf[len++] = crc >> 8; //CRC高字節(jié)
buf[len++] = crc & 0xFF; //CRC低字節(jié)
UartWrite(buf, len); //發(fā)送響應幀
}
}
}
}
void UartRxMonitor(unsigned char ms) //串口接收監(jiān)控函數(shù)
{
static unsigned char cntbkp = 0;
static unsigned char idletmr = 0;
if (cntRxd > 0) //接收計數(shù)器大于零時,監(jiān)控總線空閑時間
{
if (cntbkp != cntRxd) //接收計數(shù)器改變,即剛接收到數(shù)據(jù)時,清零空閑計時
{
cntbkp = cntRxd;
idletmr = 0;
}
else
{
if (idletmr < 5) //接收計數(shù)器未改變,即總線空閑時,累積空閑時間
{
idletmr += ms;
if (idletmr >= 5) //空閑時間超過4個字節(jié)傳輸時間即認為一幀命令接收完畢
{
cmdArrived = 1; //設(shè)置命令到達標志
}
}
}
}
else
{
cntbkp = 0;
}
}
void InterruptUART() interrupt 4 //UART中斷服務(wù)函數(shù)
{
if (RI) //接收到字節(jié)
{
RI = 0; //手動清零接收中斷標志位
if (cntRxd < sizeof(bufRxd)) //接收緩沖區(qū)尚未用完時,
{
bufRxd[cntRxd++] = SBUF; //保存接收字節(jié),并遞增計數(shù)器
}
}
if (TI) //字節(jié)發(fā)送完畢
{
TI = 0; //手動清零發(fā)送中斷標志位
flagOnceTxd = 1; //設(shè)置單次發(fā)送完成標志
}
}
/***********************lcd1602.c文件程序源代碼*************************/
#include <reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
void LcdWaitReady() //等待液晶準備好
{
unsigned char sta;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do
{
LCD1602_E = 1;
sta = LCD1602_DB; //讀取狀態(tài)字
LCD1602_E = 0;
} while (sta & 0x80); //bit7等于1表示液晶正忙,重復檢測直到其等于0為止
}
void LcdWriteCmd(unsigned char cmd) //寫入命令函數(shù)
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
void LcdWriteDat(unsigned char dat) //寫入數(shù)據(jù)函數(shù)
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str) //顯示字符串,屏幕起始坐標(x,y),字符串指針str
{
unsigned char addr;
//由輸入的顯示坐標計算顯示RAM的地址
if (y == 0)
addr = 0x00 + x; //第一行字符地址從0x00起始
else
addr = 0x40 + x; //第二行字符地址從0x40起始
//由起始顯示RAM地址連續(xù)寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (*str != '\0') //連續(xù)寫入字符串數(shù)據(jù),直到檢測到結(jié)束符
{
LcdWriteDat(*str);
str++;
}
}
void LcdInit() //液晶初始化函數(shù)
{
LcdWriteCmd(0x38); //16*2顯示,5*7點陣,8位數(shù)據(jù)接口
LcdWriteCmd(0x0C); //顯示器開,光標關(guān)閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
}
關(guān)于CRC校驗的算法,如果不是專門學習校驗算法本身,大家可以不去研究這個程序的細節(jié),文檔直接給我們提供了函數(shù),我們直接調(diào)用即可。
/***********************CRC16.c文件程序源代碼*************************/
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,
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);
}
/***********************main.c文件程序源代碼*************************/
void ConfigTimer0(unsigned int ms);
extern void LcdInit();
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartDriver();
void main ()
{
EA = 1; //開總中斷
ConfigTimer0(1); //配置T0定時1ms
ConfigUART(9600); //配置波特率為9600
LcdInit(); //初始化液晶
while(1)
{
UartDriver();
}
}
void ConfigTimer0(unsigned int ms) //T0配置函數(shù)
{
unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率
tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 34; //修正中斷響應延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié)
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0為模式1
TH0 = T0RH; //加載T0重載值
TL0 = T0RL;
ET0 = 1; //使能T0中斷
TR0 = 1; //啟動T0
}
void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù)
{
TH0 = T0RH; //定時器重新加載重載值
TL0 = T0RL;
if (flagBuzzOn) //蜂鳴器鳴叫或關(guān)閉
BUZZ = ~BUZZ;
else
BUZZ = 1;
UartRxMonitor(1); //串口接收監(jiān)控
}
編輯:admin 最后修改時間:2018-05-08