13.5 串口通信機制和實用的串口例程
程序的功能是,通過電腦串口調試助手下發三個不同的命令,
- 第一條指令:buzz on 可以讓蜂鳴器響;
- 第二條指令:buzz off 可以讓蜂鳴器不響;
- 第三條指令:showstr ,這個命令空格後邊,可以添加任何字符串,讓後邊的字符串在 1602 液晶上顯示出來,同時不管發送什麼命令,單片機收到後把命令原封不動的再通過串口發送給電腦,以表示“我收到了……你可以檢查下對不對”。
我們就建立這樣一種程序機制:
設置一個軟件的總線空閒定時器,這個定時器在有數據傳輸時(從單片機接收角度來說就是接收到數據時)清零,而在總線空閒時(也就是沒有接收到數據時)時累加,當它累加到一定時間(例程裏是 30ms)後,我們就可以認定一幀完整的數據已經傳輸完畢了,於是告訴其它程序可以來處理數據了,本次的數據處理完後就恢復到初始狀態,再準備下一次的接收。那麼這個用於判定一幀結束的空閒時間取多少合適呢?它取決於多個條件,並沒有一個固定值,
我們這裏介紹幾個需要考慮的原則:
- 第一,這個時間必須大於波特率週期,很明顯我們的單片機接收中斷產生是在一個字節接收完畢後,也就是一個時刻點,而其接收過程我們的程序是無從知曉的,因此在至少一個波特率週期內你絕不能認爲空閒已經時間達到了。
- 第二,要考慮發送方的系統延時,因爲不是所有的發送方都能讓數據嚴格無間隔的發送,因爲軟件響應、關中斷、系統臨界區等等操作都會引起延時,所以還得再附加幾個到十幾個 ms 的時間。我們選取的 30ms 是一個折中的經驗值,它能適應大部分的波特率(大於1200)和大部分的系統延時(PC 機或其它單片機系統)情況。
/*****************************Uart.c 文件程序源代碼*****************************/
#include <reg52.h>
bit flagFrame = 0; //幀接收完成標誌,即接收到一幀新數據
bit flagTxd = 0; //單字節發送完成標誌,用來替代 TXD 中斷標誌位
unsigned char cntRxd = 0; //接收字節計數器
unsigned char pdata bufRxd[64]; //接收字節緩衝區
extern void UartAction(unsigned char *buf, unsigned char len);
/* 串口配置函數,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
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
}
/* 串口數據寫入,即串口發送函數,buf-待發送數據的指針,len-指定的發送長度 */
void UartWrite(unsigned char *buf, unsigned char len)
{
while (len--) //循環發送所有字節
{
flagTxd = 0; //清零發送標誌
SBUF = *buf++; //發送一個字節數據
while (!flagTxd); //等待該字節發送完成
}
}
/* 串口數據讀取函數,buf-接收指針,len-指定的讀取長度,返回值-實際讀到的長度 */
unsigned char UartRead(unsigned char *buf, unsigned char len)
{
unsigned char i;
if (len > cntRxd) //指定讀取長度大於實際接收到的數據長度時,
{ //讀取長度設置爲實際接收到的數據長度
len = cntRxd;
}
for (i=0; i<len; i++) //拷貝接收到的數據到接收指針上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收計數器清零
return len; //返回實際讀取長度
}
/* 串口接收監控,由空閒時間判定幀結束,需在定時中斷中調用,ms-定時間隔 */
void UartRxMonitor(unsigned char ms)
{
static unsigned char cntbkp = 0;
static unsigned char idletmr = 0;
if (cntRxd > 0) //接收計數器大於零時,監控總線空閒時間
{
if (cntbkp != cntRxd) //接收計數器改變,即剛接收到數據時,清零空閒計時
{
cntbkp = cntRxd;
idletmr = 0;
}
else //接收計數器未改變,即總線空閒時,累積空閒時間
{
if (idletmr < 30) //空閒計時小於 30ms 時,持續累加
{
idletmr += ms;
if (idletmr >= 30) //空閒時間達到 30ms 時,即判定爲一幀接收完畢
{
flagFrame = 1; //設置幀接收完成標誌
}
}
}
}
else
{
cntbkp = 0;
}
}
/* 串口驅動函數,監測數據幀的接收,調度功能函數,需在主循環中調用 */
void UartDriver()
{
unsigned char len;
unsigned char pdata buf[40];
if (flagFrame) //有命令到達時,讀取處理該命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)); //將接收到的命令讀取到緩衝區中
UartAction(buf, len); //傳遞數據幀,調用動作執行函數
}
}
/* 串口中斷服務函數 */
void InterruptUART() interrupt 4
{
if (RI) //接收到新字節
{
RI = 0; //清零接收中斷標誌位
if (cntRxd < sizeof(bufRxd)) //接收緩衝區尚未用完時,
{ //保存接收字節,並遞增計數器
bufRxd[cntRxd++] = SBUF;
}
}
if (TI) //字節發送完畢
{
TI = 0; //清零發送中斷標誌位
flagTxd = 1; //設置字節發送完成標誌
}
}
大家可以對照註釋和前面的講解分析下這個 Uart.c 文件,在這裏指出其中的兩個要點希
望大家多注意下。
- 接收數據的處理,在串口中斷中,將接收到的字節都存入緩衝區 bufRxd 中,同時利用另外的定時器中斷通過間隔調用 UartRxMonitor 來監控一幀數據是否接收完畢,判定的原則就是我們前面介紹的空閒時間,當判定一幀數據結束完畢時,設置 flagFrame 標誌,主循環中可以通過調用 UartDriver 來檢測該標誌,並處理接收到的數據。當要處理接收到的數據時,先通過串口讀取函數 UartRead 把接收緩衝區 bufRxd 中的數據讀取出來,然後再對讀到的數據進行判斷處理。也許你會說,既然數據都已經接收到 bufRxd 中了,那我直接在這裏面用不就行了嘛,何必還得再拷貝到另一個地方去呢?我們設計這種雙緩衝的機制,主要是爲了提高串口接收到響應效率:首先如果你在 bufRxd 中處理數據,那麼這時侯就不能再接收任何數據,因爲新接收的數據會破壞原來的數據,造成其不完整和混亂;其次,這個處理過程可能會耗費較長的時間,比如說上位機現在就給你發來一個延時顯示的命令,那麼在這個延時的過程中你都無法去接收新的命令,在上位機看來就是你暫時失去響應了。而使用這種雙緩衝機制就可以大大改善這個問題,因爲數據拷貝所需的時間是相當短的,而只要拷貝出去後,bufRxd 就可以馬上準備去接收新數據了。
- 串口數據寫入函數 UartWrite,它把數據指針 buf 指向的數據塊連續的由串口發送出去。雖然我們的串口程序啓用了中斷,但這裏的發送功能卻沒有在中斷中完成,而是仍然靠查詢發送中斷標誌 flagTxd(因中斷函數內必須清零 TI,否則中斷會重複進入執行,所以另置了一個 flagTxd 來代替 TI)來完成,當然也可以採用先把發送數據拷貝到一個緩衝區中,然後再在中斷中發緩衝區數據發送出去的方式,但這樣一是要耗費額外的內存,二是使程序更復雜。這裏也還是想告訴大家,簡單方式可以解決的問題就不要搞得更復雜。
/*****************************main.c 文件程序源代碼******************************/
buf-接收到的命令幀指針,len-命令幀長度 */
void UartAction(unsigned char *buf, unsigned char len)
{
unsigned char i;
unsigned char code cmd0[] = "buzz on"; //開蜂鳴器命令
unsigned char code cmd1[] = "buzz off"; //關蜂鳴器命令
unsigned char code cmd2[] = "showstr "; //字符串顯示命令
unsigned char code cmdLen[] = { //命令長度彙總表
sizeof(cmd0)-1, sizeof(cmd1)-1, sizeof(cmd2)-1,
};
unsigned char code *cmdPtr[] = { //命令指針彙總表
&cmd0[0], &cmd1[0], &cmd2[0],
};
for (i=0; i<sizeof(cmdLen); i++) //遍歷命令列表,查找相同命令
{
if (len >= cmdLen[i]) //首先接收到的數據長度要不小於命令長度
{
if (CmpMemory(buf, cmdPtr[i], cmdLen[i])) //比較相同時退出循環
{
break;
}
}
}
switch (i) //循環退出時 i 的值即是當前命令的索引值
{
case 0:
flagBuzzOn = 1; //開啓蜂鳴器
break;
case 1:
flagBuzzOn = 0; //關閉蜂鳴器
break;
case 2:
buf[len] = '\0'; //爲接收到的字符串添加結束符
LcdShowStr(0, 0, buf+cmdLen[2]); //顯示命令後的字符串
i = len - cmdLen[2]; //計算有效字符個數
if (i < 16) //有效字符少於 16 時,清除液晶上的後續字符位
{
LcdAreaClear(i, 0, 16-i);
}
break;
default: //未找到相符命令時,給上機發送“錯誤命令”的提示
UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
return;
}
buf[len++] = '\r'; //有效命令被執行後,在原命令幀之後添加
buf[len++] = '\n'; //回車換行符後返回給上位機,表示已執行
UartWrite(buf, len);
}
/* 配置並啓動 T0,ms-T0 定時時間 */
void ConfigTimer0(unsigned int ms)
{
unsigned long tmp; //臨時變量
tmp = 11059200 / 12; //定時器計數頻率
tmp = (tmp * ms) / 1000; //計算所需的計數值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 33; //補償中斷響應延時造成的誤差
T0RH = (unsigned char)(tmp>>8); //定時器重載值拆分爲高低字節
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零 T0 的控制位
TMOD |= 0x01; //配置 T0 爲模式 1
TH0 = T0RH; //加載 T0 重載值
TL0 = T0RL;
ET0 = 1; //使能 T0 中斷
TR0 = 1; //啓動 T0
}
/* T0 中斷服務函數,執行串口接收監控和蜂鳴器驅動 */
void InterruptTimer0() interrupt 1
{
TH0 = T0RH; //重新加載重載值
TL0 = T0RL;
if (flagBuzzOn) //執行蜂鳴器鳴叫或關閉
BUZZ = ~BUZZ;
else
BUZZ = 1;
UartRxMonitor(1); //串口接收監控
}
main 函數和主循環的結構我們已經做過很多了,就不多說了,這裏重點把串口接收數據的具體解析方法給大家分析一下,這種用法具有很強的普遍性,掌握並靈活運用它可以使你將來的開發工作事半功倍。
首先來看 CmpMemory 函數,這個函數很簡單,就是比較兩段內存數據,通常都是數組中的數據,函數接收兩段數據的指針,然後逐個字節比較——if (*ptr1++ != *ptr2++),這行代碼既完成了兩個指針指向的數據的比較,又在比較完後把兩個指針都各自+1,從這裏是不是也能領略到一點 C 語言的簡潔高效的魅力呢。這個函數的用處自然就是用來比較我們接收到的數據和事先放在程序裏的命令字符串是否相同,從而找出相符的命令了。
接下來是 UartAction 函數對接收數據的解析和處理方法,先把接收的數據與所支持的命令字符串逐條比較,這個比較中首先要確保接收的長度大於命令字符串的長度,然後再用上
述的 CmpMemory 函數逐字節比較,如果比較相同就立即退出循環,不同則繼續對比下一條命令。當找到相符的命令字符串時,最終 i 的值就是該命令在其列表中的索引位置,當遍歷完命令列表都沒有找到相符的命令時,最終 i 的值將等於命令總數,那麼接下來就用 switch語句根據 i 的值來執行具體的動作,這個就不需要再詳細說明了。
/***************************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; //讀取狀態字
LCD1602_E = 0;
} while (sta & 0x80); //bit7 等於 1 表示液晶正忙,重複檢測直到其等於 0 爲止
}
/* 向 LCD1602 液晶寫入一字節命令,cmd-待寫入命令值 */
void LcdWriteCmd(unsigned char cmd)
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 向 LCD1602 液晶寫入一字節數據,dat-待寫入數據值 */
void LcdWriteDat(unsigned char dat)
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 設置顯示 RAM 起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */
void LcdSetCursor(unsigned char x, unsigned char y)
{
unsigned char addr;
if (y == 0) //由輸入的屏幕座標計算顯示 RAM 的地址
addr = 0x00 + x; //第一行字符地址從 0x00 起始
else
addr = 0x40 + x; //第二行字符地址從 0x40 起始
LcdWriteCmd(addr | 0x80); //設置 RAM 地址
}
/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,str-字符串指針 */
void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str)
{
LcdSetCursor(x, y); //設置起始地址
while (*str != '\0') //連續寫入字符串數據,直到檢測到結束符
{
LcdWriteDat(*str++);
}
}
/* 區域清除,清除從(x,y)座標起始的 len 個字符位 */
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len)
{
LcdSetCursor(x, y); //設置起始地址
while (len--) //連續寫入空格
{
LcdWriteDat(' ');
}
}
/* 初始化 1602 液晶 */
void InitLcd1602()
{
LcdWriteCmd(0x38); //16*2 顯示,5*7 點陣,8 位數據接口
LcdWriteCmd(0x0C); //顯示器開,光標關閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
}
液晶文件與上一個例程的液晶文件基本是一樣的,唯一的區別是刪掉了一個本例中用不到的全屏清屏函數,其實留着這個函數也沒關係,只是 Keil 會提示一個警告,告訴你有未被調用的函數而已,可以不理會它。 經過這幾個多文件工程的練習後,大家是否發現,在採用多文件模塊化編程後,不光是某些函數,甚至整個 c 文件,如有需要,我們都可以直接複製到其它的新工程中使用,非常方便功能程序的移植,這樣隨着實踐積累的增加,你會發現工作效率變得越來越高了。