前言
因項目需求,我們要從PC端去控制一些外部設備,比如激光器、光放大器等,這些設備一般使用到的都是低速的串口通信,所以我們需要設計一個上下位機串口通信系統來控制這些設備。這篇文章先講如何使用Arduino Mega2560開發板來作爲下位機控制各個外部設備。
上下位機工作原理
- 通常來說,上位機指的是PC端的控制軟件,比如可以設置讀取參數的界面軟件,下位機則是指單片機或者帶微處理器的系統,這裏我們使用Mega2560來作爲下位機控制板。下位機可以把一些模擬信號經過AD採集後轉換爲數字信號,經過處理後通過串口發送給上位機;同理上位機也可以給下位機發送一些指令或者信息。
- 我們這裏需要通過PC端的界面軟件發送命令參數到外部設備上,然後再把外部設備的某些功能參數返回給界面軟件。就以前一篇文章Arduino(4)爲例說明,我們已經把外部設備光開關相關的功能操作寫成了類庫,mega2560作爲下位機控制板直接去調用這個類庫就可以了。
下位機軟件執行流程
在編寫下位機軟件之前,我們可以先自定義一種串口通信的數據幀,來確保上下位機之間的通信不會出現差錯。上下位機串口通信下行協議:PC -> Mega2560,共有8字節構成,採用異或校驗。幀頭可以自定義一字節的數據,例如0xfb;命令字表示自定義一些命令操作,例如我將設置光開關通道號的操作定義爲0x10#define OPTICALSWITCH_CHANNEL_SET 0x10
;4個字節的數據值區域表示攜帶本次命令操作設置的參數值;幀尾自定義爲數據幀的末尾,例如0xfe;校驗碼則是對整個數據幀進行左右異或後得到的一個字節的數據。
幀頭 | 校驗碼 | 命令 | 數據值 | 數據值 | 幀尾 |
---|---|---|---|---|---|
1 byte | 1 byte | 1 byte | 2 byte | 2byte | 1 byte |
接下來就是在Arduino IDE編寫數據幀的處理流程,代碼如下所示:
#include <OpticalSwitch.h>
#define FRAME 8 // frame length for host command
/*********** Module Command Keyword *************/
#define OPTICALSWITCH_CHANNEL_SET 0x10
#define OPTICALSWITCH_CHANNEL_READ 0x11
/*********** Module Name Code *********************/
#define OpticalSwitch 0x03
// global varibles for com
const int baud1 = 9600; // inital baud rate for com with host
byte rxData[FRAME]; // bytes for received frames
int comError = 0; // error count for communications
bool newCMD = false; // flag for incoming new command
bool response = false; // flag for outgoing data
int inbyte; // use it to clear other data in serial buffer
void setup()
{
// put your setup code here, to run once:
Serial.begin(baud1); //connect with host software
delay(10);
modComSetup(); // setup and check communications to modules sequentially
}
void loop()
{
// put your main code here, to run repeatedly:
delay(300);
int rxLength = Serial.available(); //判斷接收數據的長度
if(rxLength > 0)
{
if (rxLength == FRAME) //接收數據的長度等於預設值才爲指定命令幀,並將其接收下來;否則清除串口緩衝區
{
// get CMD in rxData
Serial.readBytes(rxData, FRAME); //接收數據,存進指定數組
newCMD = true;
}
else
{
// clear incoming buffer
clearBUF();
// setup a error flag to notify host to resend command
comError++;
}
}
//當有新的命令幀下發時
if(newCMD)
{
newCMD = false;
//校驗碼通過後才由Arduino繼續下發命令給外部設備
if (!checkCMD(rxData))
{
comError = 0;
//執行命令幀
parseCMD(rxData);
}
else
{
comError++;
}
}
clearBUF();
//有錯則重發命令
if(comError) resendCMD();
routine();
}
//在此函數內循環讀取外部設備的參數
void routine()
{
}
void modComSetup()
{
// setup com ports and registers of modules one by one
if(OpticalSwitch_Setup()!=true) return;
}
void resendCMD()
{
// in case COM error, ask host to resend the last command
response = true;
comError = 0;
// prepare data to be send to host
}
void SendDataToHost(unsigned char ModuleCode)
{
// frame data to fill up txData and send it to host
switch(ModuleCode)
{
case OpticalSwitch:
{
Serial.write('D');
Serial.write(OpticalSwitch);
Serial.write(channel);
break;
}
default:
break;
}
}
void clearBUF()
{
delay(50);
int rxLength = Serial.available();
// clear incoming buffer
for (int i = 0; i < rxLength; i++)
{
inbyte = Serial.read();
}
}
bool checkCMD(unsigned char *rxData)
{
if(rxData[0]==0xfb && rxData[FRAME-1]==0xfe )
{
if(rxData[1] == calcBIP8(rxData))
{
return 0;
}
else
{
return 1;
}
}
else
{
return 1;
}
// calculate to check the correctness of the received command
// correct--> return 0;
// uncorrect--> return 1;
}
void parseCMD(unsigned char *rxData)
{
// parse received command and setup flags accordingly for further processing
switch(rxData[2])
{
case OPTICALSWITCH_CHANNEL_SET:
{
OpticalSwitchChannel(rxData[4]);
break;
}
case OPTICALSWITCH_CHANNEL_READ:
{
SendDataToHost(OpticalSwitch)
break;
}
default:
break;
}
}
//異或校驗,用於校驗下發命令的數據幀是否完整
unsigned char calcBIP8(unsigned char* data)
{
unsigned char bip16 = data[0]^(data[1]&0x00)^data[2]^data[3]^data[4]^data[5]^data[6]^data[7];
unsigned char bip8 = ((bip16&0xf0)>>4)^(bip16&0x0f);
return bip8;
}
- 首先引用我們之前寫好的類庫
#include <OpticalSwitch.h>
;然後void setup()函數內初始化Arduino mega2560和PC電腦相連的串口(該串口由mega2560上自帶串口轉USB線和電腦上USB口連接),然後對外部設備進行初始化,這部分程序已經寫在了光開關的類庫裏了。void loop()函數內則是數據幀的整個處理流程 - 從上位機下發的數據命令幀被與PC相連的串口(也就是Serial)接收後,首先判斷其數據長度是否等於預設值,不然就是傳輸過程出錯了,數據包出現了丟失;接着將接收到的數據存儲到rxData數組內,由異或校驗函數calcBIP8()來檢測數據幀的正確性;然後執行派發函數parseCMD(),這個函數根據數據幀內的命令字來執行相應的命令,調用相應模塊的指定函數;如果這個過程出錯了就重發命令,執行resendCMD()函數,並且清空串口緩衝區,等待下一次數據命令幀;routine()內則循環讀取外部設備的參數,存進相應的數組,等待上位機讀取。整個執行過程放在了loop()函數內,循環執行,有新的命令幀就執行,沒有就等待新的命令到來。
- 這樣整個控制外部設備的下位機軟件就已經做好了,上位機發送帶有命令字的數據幀到下位機,就可以控制各種外部設備了。