本教材現以連載的方式由網絡發布,并將于2014年由清華大學出版社出版最終完整版,版權歸作者和清華大學出版社所有。本著開源、分享的理念,本教材可以自由傳播及學習使用,但是務必請注明出處來自金沙灘工作室
理論上的內容要想逐步消化掌握,必須得通過大量的實踐進行鞏固,否則時間一長,極容易忘掉。尤其是一些編程的算法相關的技巧,就是靠不停的寫程序,不停的參考別人的程序慢慢積累成長起來的。這節課帶著大家學習一下1602的例程和實際開發中比較實用的串口程序。
13.1 通信時序解析 隨著我們對通信技術的深入學習,大家要逐漸在頭腦中建立起時序這種概念。所謂“時序”從字面意義上來理解,一是“時間問題”,二是“順序問題”。
先說“順序問題”,這個相對簡單一些。我們在學UART串口通信的時候,先1位起始位,再8位數據位,最后1位停止位,這個先后順序不能錯。我們在學 1602液晶的時候,比如寫指令,RS=L,R/W=L,D0~D7=指令碼,這三者的順序是無所謂的,但是最終的E=高脈沖,必須是在這三條程序之后,這個順序一旦錯誤,寫的數據也可能出錯。
“時間問題”內容相對復雜。比如UART通信,每一位的時間寬度是1/baud。我們初中就學過一個概念,世界上沒有絕對的準確。那我們這個每一位的時間寬度1/baud要求精確到什么范圍內呢?
前邊教程我提到過,單片機讀取UART的RXD引腳數據的時候,一位數據,單片機平均分成了16份,取其中的7、8、9三次讀到的結果,這三次中有2次是高電平那這一位就是1,有2次是低電平,那這一次就是0。如果我們的波特率稍微有些偏差,只要累計下來到最后一位停止位,這7、8、9還在范圍內即可。如圖 13-1所示。
2.JPG (16.66 KB, 下載次數: 245)
下載附件
2013-9-28 15:04 上傳
圖13-1 UART信號采集時序圖
我用三個箭頭來表示7、8、9這三次的采集位置,大家可以注意到,當采集到D7的時候,已經有一次采集偏差出去了,但是我們采集到的數據還是不會錯,因為有 2次采集正確。至于這個偏差允許多大,大家自己可以詳細算一下。實際上UART通信的波特率是允許一定范圍內誤差存在的,但是不能過大,否則就會采集錯誤。大家在計算波特率的時候,發現沒有整除,有小數部分的時候,就要特別小心了,因為小數部分是一概被舍掉的,于是計算誤差就產生了。 我們用 11.0592M晶振計算的過程中,11059200/12/32/9600得到的是一個整數,如果用12M晶振計算12000000/12/32 /9600就會得到一個小數,大家可以算一下誤差多少,是否在誤差范圍內。
1602的時序問題,大家要學會通過LCD1602的數據手冊提供的時序圖和時序參數表格來進行研究,而且看懂時序圖是學習單片機必須學會的一項技能,如圖12-2所示。
3.JPG (95.7 KB, 下載次數: 259)
下載附件
2013-9-28 15:05 上傳
圖13-2 1602時序圖
大家看到這種圖的時候,不要覺得害怕。說句不過分的話,單片機這些邏輯上的問題,只要小學畢業就可以理解的,很多時候是因為大家把問題想象的太難才學不下去的。
我們先來看一下讀操作時序的RS引腳和R/W引腳,這兩個引腳先進行變化,因為是讀操作,所以R/W引腳首先要置為高電平,而不管他原來是什么。讀指令還是讀數據,都是讀操作,而且都有可能,所以RS引腳既有可能是置為高電平,也有可能是置為低電平,大家注意圖上的畫法。而RS和R/W變化了經過 Tsp1這么長時間后,使能引腳E才能從低電平到高電平發生變化。
而使能引腳E拉高了經過了tD這么長時間后,LCD1602輸出DB的數據就是有效數據了,我們就可以來讀取DB的數據了。讀完了之后,我們要先把使能E拉低,經過一段時間后RS、R/W和DB才可以變化繼續為下一次讀寫做準備了。
而寫操作時序和讀操作時序的差別,就是寫操作時序,DB的改變是我們單片機來完成的,因此要放到使能引腳E的變化之前進行操作,其他區別大家可以自行對比一下。
細心的同學會發現,這個時序圖上還有很多時間標簽。比如E的上升時間tR,下降時間時間tF,使能引腳E從一個上升沿到下一個上升沿之間的長度周期 tC,使能E下降沿后,R/W和RS變化時間間隔tHD1等等很多時間要求,這些要求怎么看呢?放心,只要是正規的數據手冊,都會把這些時間要求給大家標記出來的。我們來看一下表13-1所示。
表13-1 1602時序參數
時序參數 | 符號 | 極限值 | 單位 | 測試條件 | 最小值 | 典型值 | 最大值 | E信號周期 | tC | 400 | -- | -- | ns | 引腳E | E脈沖寬度 | tPW | 150 | -- | -- | ns | E上升沿/下降沿時間 | tR, tF | -- | -- | 25 | ns | 地址建立時間 | tSP1 | 30 | -- | -- | ns | 引腳E、RS、R/W | 地址保持時間 | tHD1 | 10 | -- | -- | ns | 數據建立時間(讀) | tD | -- | -- | 100 | ns | 引腳DB0~DB7 | 數據保持時間(讀) | tHD2 | 20 | -- | -- | ns | 數據建立時間(寫) | tSP2 | 40 | -- | -- | ns | 數據保持時間(寫) | tHD2 | 10 | -- | -- | ns | 大家要善于把手冊中的這個表格和時序圖結合起來看。表12-1中的數據,都是時序參數,本節課的所有的時序參數,我都一點點的給大家講出來,以后遇到同類時序圖,我就不再講了,只是提一下,但是大家務必要學會自己看時序圖,這個很重要,此外,看以下解釋需要結合圖13-2來看。
tC:指的是使能引腳E從本次上升沿到下次上升沿的最短時間是400ns,而我們單片機因為速度較慢,一個機器周期就是1us多,而一條C語言指令肯定是一個或者幾個機器周期的,所以這個條件完全滿足。
tPW:指的是使能引腳E高電平的持續時間最短是150ns,由于我們的單片機比較慢,這個條件也完全滿足。
tR, tF:指的是使能引腳E的上升沿時間和下降沿時間,不能超過25ns,這個時間限值空間很大,我們用示波器測了一下我們開發板的這個引腳上升沿和下降沿時間大概是10ns到15ns之間,完全滿足。
tSP1:指的是RS和R/W引腳使能后至少保持30ns,使能引腳E才可以變成高電平,這個條件完全滿足。
tHD1:指的是使能引腳E變成低電平后,至少保持10ns之后,RS和R/W才能進行變化,這個條件完全滿足。
tD:指的是我們的使能引腳E變成高電平后,最多100ns后,1602就把數據送出來了,那么我們就可以正常去讀取狀態或者數據了。
tHD2:指的是讀操作過程中,使能引腳E變成低電平后,至少保持20ns,DB數據總線才可以進行變化,這個條件完全滿足。
tSP2:指的是DB數據總線準備好后,至少保持40ns,使能引腳E才可以從低到高進行使能變化,這個條件完全滿足。
tHD2:指的是寫操作過程中,只能引腳E變成低電平后,至少保持10ns,DB數據總線才可以變化,這個條件完全滿足。
好了,表13-1這個LCD1602的時序參數表已經解析完成了,看完之后,是不是感覺比你想象的要簡單,沒有你想的那么困難。大家自己也得慢慢學會看這種時序圖和表格,在今后的學習中,這方面的能力尤為重要。如果以后換用了其它型號的單片機,那么就根據單片機的執行速度來評估你的程序是否滿足時序要求,整體上來說器件都是有一個最快速度的限制,而沒有最慢限制,所以當換用高速的單片機后通常都是靠在各步驟間插入軟件延時來滿足較慢的時序要求。
13.2 1602整屏移動 我們前邊學第七章點陣LED的時候,可以實現上下移動,左右移動等。而對于1602液晶來說,也可以進行屏幕移動,實現我們想要的一些效果,那我們來用一個例程實現字符串在1602液晶上的左移。每個人都不要只瞪著眼看,一定要認真抄下來,甚至抄幾遍,邊抄遍理解,要想真正學好,一定要根據我的方法來做。
#include <reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
bit flagT0 = 0; //T0中斷產生標志
unsigned char T0RH = 0; //T0重載值的高字節
unsigned char T0RL = 0; //T0重載值的低字節
unsigned char code str1[] = "Kingst Studio"; //待顯示的第一行字符串
unsigned char code str2[] = "Let's move..."; //待顯示的第二行字符串,需保持與第一行字符串等長,較短的行可用空格補齊
void ConfigTimer0(unsigned int ms);
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len);
void LcdInit();
void main ()
{
unsigned char i;
unsigned char iMove = 0; //移動索引
unsigned int tmrMove = 0; //屏幕移動定時器
unsigned char pdata bufMove1[16 + sizeof(str1) + 16]; //移動顯示的緩沖區
unsigned char pdata bufMove2[16 + sizeof(str1) + 16]; //移動顯示的緩沖區
EA = 1; //開總中斷
ConfigTimer0(10); //配置T0定時10ms
LcdInit(); //初始化液晶
for (i=0; i<16; i++) //緩沖區開頭一段填充為空格
{
bufMove1[ i] = ' ';
bufMove2[ i] = ' ';
}
for (i=0; i<(sizeof(str1)-1); i++) //待顯示字符串拷貝到緩沖區中間位置
{
bufMove1[16+i] = str1[ i];
bufMove2[16+i] = str2[ i];
}
for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++) //緩沖區結尾一段也填充為空格
{
bufMove1[ i] = ' ';
bufMove2[ i] = ' ';
}
while(1)
{
if (flagT0)
{
flagT0 = 0;
tmrMove += 10;
if (tmrMove >= 500) //每500ms移動一次屏幕
{
tmrMove = 0;
LcdShowStr(0, 0, bufMove1+iMove, 16); //從緩沖區抽出需顯示的一段字符顯示到液晶上
LcdShowStr(0, 1, bufMove2+iMove, 16);
iMove++; //移動索引遞增,實現左移
if (iMove >= (16+sizeof(str1)-1)) //起始位置達到字符串尾部后即返回從頭開始
{
iMove = 0;
}
}
}
}
}
void ConfigTimer0(unsigned int ms) //T0配置函數
{
unsigned long tmp;
tmp = 11059200 / 12; //定時器計數頻率
tmp = (tmp * ms) / 1000; //計算所需的計數值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 12; //修正中斷響應延時造成的誤差
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
}
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為止
}
void LcdWriteCmd(unsigned char cmd) //寫入命令函數
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_E = 1;
LCD1602_DB = cmd;
LCD1602_E = 0;
}
void LcdWriteDat(unsigned char dat) //寫入數據函數
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_E = 1;
LCD1602_DB = dat;
LCD1602_E = 0;
}
void LcdInit() //液晶初始化函數
{
LcdWriteCmd(0x38); //16*2顯示,5*7點陣,8位數據接口
LcdWriteCmd(0x0C); //顯示器開,光標關閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len) // 顯示字符串,屏幕起始坐標(x,y),字符串指針str,需顯示長度len
{
unsigned char addr;
//由輸入的顯示坐標計算顯示RAM的地址
if (y == 0)
{
addr = 0x00 + x; //第一行字符地址從0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址從0x40起始
}
//由起始顯示RAM地址連續寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (len--) //連續寫入字符串數據
{
LcdWriteDat(*str);
str++;
}
}
void InterruptTimer0() interrupt 1 //T0中斷服務函數
{
TH0 = T0RH; //定時器重新加載重載值
TL0 = T0RL;
flagT0 = 1;
}
通過這個程序,大家首先要學會for語句在數組中的靈活應用,這個其實在數碼管顯示有效位的例程中已經有所體現了。其次,隨著我們后邊程序量的增大,大家得學會多個函數之間的相互調用的靈活應用,體會其中的奧妙。
13.3 多.c文件的初步認識 我們上一節的這個液晶滾屏移動程序,大概有150行左右。隨著我們硬件模塊使用的增多,程序量的增大,我們往往要把程序寫到多個文件里,方便代碼的編寫、維護和移植。
比如這個液晶滾屏程序,我們就可以把1602底層的功能函數專門寫到一個.C文件內,如LcdWaitReady、LcdWriteCmd、 LcdWriteDat、LcdInit、LcdShowStr 這幾個函數,都是屬于液晶底層驅動的程序代碼,我們要使用液晶功能的時候,只有兩個函數對我們實際功能實現部分有用,一個是LcdInit這個函數,需要先初始化液晶,另外一個就是LcdShowStr這個函數,我們只需要把我們要顯示的內容通過參數傳遞給這個函數,這個函數就可以實現我們想要的顯示效果,所以我們把這幾個底層的液晶驅動程序都放到另外一個文件Lcd1602.c文件中,而我們想實現的一些比如滾動實現、中斷等上層功能程序全部都放到main.c中,但是main.c文件如何調用Lcd1602.c文件中的函數呢?
C語言中,有一個extern關鍵字,他有兩個基本作用。
1、當一個變量的聲明不在文件的開頭,在它聲明之前的函數想要引用的話,則應該用extern進行“外部變量”聲明。用一個簡單的程序給大家介紹一下,知道這么回事,能看懂別人寫的就行,自己寫就別這么用了。
#include<reg52.h> //包含寄存器的庫文件
sbit LED = P0 ^ 0; //位地址聲明 注意:sbit必須小寫!
void main()
{
extern unsigned int i;
while(1) //程序死循環
{
LED = 0; //點亮小燈
for(i=0;i<30000;i++); //延時
LED = 1; //熄滅小燈
for(i=0;i<30000;i++); //延時
}
}
unsigned int i = 0;
... ...
我們變量的作用域,是從聲明這個變量開始往后所有的程序,如果我們調用在前,聲明在后,那么就是這么用。但是實際開發過程中,我們一般都不會這樣做,所以僅僅是表達一下extern的這個用法,但它并不實用。
2、在一個工程中,我們為了方便管理維護代碼,所以用了多個.C源文件,如果其中一個main.c文件要調用Lcd1602.c文件里的變量或者函數的時候,我們就必須得在main.c里邊進行以下外部聲明,告訴編譯器這個變量或者函數是在其他文件中定義的,可以直接在這個文件中進行調用,我們用上一節的程序代碼試試看。
多.c文件的編程方式,大家不要想象的太復雜。首先新建一個工程,一個工程代表一個完整的單片機程序,只能生成一個hex,但是一個工程可以有很多個.c源文件組成共同參與編譯。工程建立好之后,新建文件并且保存稱為main.c文件,再新建一個文件并且保存稱為Lcd1602.c文件,下面我們就可以在兩個不同文件中分別編寫代碼了。當然,在編寫程序的過程中,不是說我們要先把main.c的文件全部寫完,再進行1602.c程序的編寫,而往往是交互的。比如我們先寫Lcd1602.c文件中部分Lcd1602液晶的底層函數LcdWaitReady、LcdWriteCmd、 LcdWriteDat、LcdInit,然后編寫main.c文件中的功能程序,在編寫main.c文件中程序時,又有對Lcd1602.c底層程序的綜合調用,這個時候需要Lcd1602.c文件提供一個被調用的函數比如LcdShowStr,我們就可以再到Lcd1602.c中把這個函數完成。當然了,這僅僅是一個說明例子而已,順序完全是沒有一個標準的,實際過程我們如果對程序邏輯需求了解透徹,根據我們自己的理解去寫程序即可。那我們把1602 整屏移動的程序改造成為多文件的程序,大家先初步認識一下。
/*************************1602.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為止
}
void LcdWriteCmd(unsigned char cmd) //寫入命令函數
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_E = 1;
LCD1602_DB = cmd;
LCD1602_E = 0;
}
void LcdWriteDat(unsigned char dat) //寫入數據函數
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
void LcdInit() //液晶初始化函數
{
LcdWriteCmd(0x38); //16*2顯示,5*7點陣,8位數據接口
LcdWriteCmd(0x0C); //顯示器開,光標關閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len) // 顯示字符串,屏幕起始坐標(x,y),字符串指針str,需顯示長度len
{
unsigned char addr;
//由輸入的顯示坐標計算顯示RAM的地址
if (y == 0)
{
addr = 0x00 + x; //第一行字符地址從0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址從0x40起始
}
//由起始顯示RAM地址連續寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (len--) //連續寫入字符串數據
{
LcdWriteDat(*str);
str++;
}
}
/*************************main.c文件程序源代碼**************************/
#include <reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
bit flagT0 = 0;
unsigned char T0RH = 0; //T0重載值的高字節
unsigned char T0RL = 0; //T0重載值的低字節
unsigned char code str1[] = "Kingst Studio"; //待顯示的第一行字符串
unsigned char code str2[] = "Let's move..."; //待顯示的第二行字符串,需保持與第一行字符串等長,較短的行可用空格補齊
void ConfigTimer0(unsigned int ms);
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len);
void main ()
{
unsigned char i;
unsigned char iMove = 0; //移動索引
unsigned int tmrMove = 0; //屏幕移動定時器
unsigned char pdata bufMove1[16 + sizeof(str1) + 16]; //移動顯示的緩沖區
unsigned char pdata bufMove2[16 + sizeof(str1) + 16]; //移動顯示的緩沖區
EA = 1; //開總中斷
ConfigTimer0(10); //配置T0定時10ms
LcdInit(); //初始化液晶
for (i=0; i<16; i++) //緩沖區開頭一段填充為空格
{
bufMove1[ i] = ' ';
bufMove2[ i] = ' ';
}
for (i=0; i<(sizeof(str1)-1); i++) //待顯示字符串拷貝到緩沖區中間位置
{
bufMove1[16+i] = str1[ i];
bufMove2[16+i] = str2[ i];
}
for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++) //緩沖區結尾一段也填充為空格
{
bufMove1[ i] = ' ';
bufMove2[ i] = ' ';
}
while(1)
{
if (flagT0)
{
flagT0 = 0;
tmrMove += 10;
if (tmrMove >= 500) //每500ms移動一次屏幕
{
tmrMove = 0;
LcdShowStr(0, 0, bufMove1+iMove, 16); //從緩沖區抽出需顯示的一段字符顯示到液晶上
LcdShowStr(0, 1, bufMove2+iMove, 16);
iMove++; //移動索引遞增,實現左移
if (iMove >= (16+sizeof(str1)-1)) //起始位置達到字符串尾部后即返回從頭開始
{
iMove = 0;
}
}
}
}
}
void ConfigTimer0(unsigned int ms) //T0配置函數
{
unsigned long tmp;
tmp = 11059200 / 12; //定時器計數頻率
tmp = (tmp * ms) / 1000; //計算所需的計數值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 12; //修正中斷響應延時造成的誤差
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
}
void InterruptTimer0() interrupt 1 //T0中斷服務函數
{
TH0 = T0RH; //定時器重新加載重載值
TL0 = T0RL;
flagT0 = 1;
}
我們在main.c要調用Lcd1602.c文件中的LcdInit()和LcdShowStr這兩個函數,只需要在main.c中進行extern 聲明即可。大家用keil軟件編程試試,真正的感覺一下多.c源文件的好處。如果這個你的感覺還不夠深刻,那下面我們來做一個稍微大點的程序來體會一下。
13.4 計算器程序 按鍵和液晶,可以組成我們最簡易的計算器。下面我們來寫一個簡易整數計算器提供給大家學習。為了讓程序不過于復雜,我們這個計算器不考慮連加,連減等連續計算,不考慮小數情況。加減乘除分別用上下左右來替代,回車表示等于,ESC表示歸0。程序共分為三部分,一部分是1602液晶顯示,一部分是按鍵動作和掃描,一部分是主函數功能。
/*************************1602.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為止
}
void LcdWriteCmd(unsigned char cmd) //寫入命令函數
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
void LcdWriteDat(unsigned char dat) //寫入數據函數
{
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地址連續寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (*str != '\0') //連續寫入字符串數據,直到檢測到結束符
{
LcdWriteDat(*str);
str++;
}
}
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len) //區域清除,清除從(x,y)坐標起始的len個字符位
{
unsigned char addr;
//由輸入的顯示坐標計算顯示RAM的地址
if (y == 0)
addr = 0x00 + x; //第一行字符地址從0x00起始
else
addr = 0x40 + x; //第二行字符地址從0x40起始
//由起始顯示RAM地址連續寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (len--) //連續寫入空格
{
LcdWriteDat(' ');
}
}
void LcdFullClear()
{
LcdWriteCmd(0x01); //清屏
}
void LcdInit() //液晶初始化函數
{
LcdWriteCmd(0x38); //16*2顯示,5*7點陣,8位數據接口
LcdWriteCmd(0x0C); //顯示器開,光標關閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
LcdShowStr(15, 1, "0");
}
/***********************keyboard.c文件程序源代碼************************/
#include <reg52.h>
sbit KEY_IN_1 = P2^4; //矩陣按鍵的掃描輸入引腳1
sbit KEY_IN_2 = P2^5; //矩陣按鍵的掃描輸入引腳2
sbit KEY_IN_3 = P2^6; //矩陣按鍵的掃描輸入引腳3
sbit KEY_IN_4 = P2^7; //矩陣按鍵的掃描輸入引腳4
sbit KEY_OUT_1 = P2^3; //矩陣按鍵的掃描輸出引腳1
sbit KEY_OUT_2 = P2^2; //矩陣按鍵的掃描輸出引腳2
sbit KEY_OUT_3 = P2^1; //矩陣按鍵的掃描輸出引腳3
sbit KEY_OUT_4 = P2^0; //矩陣按鍵的掃描輸出引腳4
const unsigned char code KeyCodeMap[4][4] = { //矩陣按鍵編號到PC標準鍵盤鍵碼的映射表
{ '1', '2', '3', 0x26 }, //數字鍵1、數字鍵2、數字鍵3、向上鍵
{ '4', '5', '6', 0x25 }, //數字鍵4、數字鍵5、數字鍵6、向左鍵
{ '7', '8', '9', 0x28 }, //數字鍵7、數字鍵8、數字鍵9、向下鍵
{ '0', 0x1B, 0x0D, 0x27 } //數字鍵0、ESC鍵、 回車鍵、 向右鍵
};
unsigned char pdata KeySta[4][4] = { //全部矩陣按鍵的當前狀態
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1}
};
unsigned char step = 0; //操作步驟
unsigned char oprt = 0; //運算類型
signed long num1 = 0; //操作數1
signed long num2 = 0; //操作數2
signed long result = 0; //運算結果
extern void LcdFullClear();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);
unsigned char NumToString(unsigned char *str, signed long num) //整型數轉換為字符串,字符串指針str,待轉換數num,返回值為字符串長度
{
unsigned char i, len;
unsigned char buf[12];
if (num < 0) //如果為負數,則首先輸出符號到指針上,并取其絕對值
{
*str = '-';
str++;
num = -num;
}
i = 0; //先轉換為低位在前的十進制數組
do {
buf[ i] = num % 10;
num /= 10;
i++;
} while (num > 0);
len = i; //i最后的值就是有效字符的個數
while (i > 0) //然后將數組值轉換為ASCII碼反向拷貝到接收指針上
{
i--;
*str = buf[ i] + '0';
str++;
}
return len; //返回轉換后的字符串長度
}
void ShowOprt(unsigned char y, unsigned char type) //顯示運算符,顯示位置y,運算符類型type
{
switch (type)
{
case 0: LcdShowStr(0, y, "+"); break;
case 1: LcdShowStr(0, y, "-"); break;
case 2: LcdShowStr(0, y, "*"); break;
case 3: LcdShowStr(0, y, "/"); break;
default: break;
}
}
void Reset() //計算器復位函數
{
num1 = 0;
num2 = 0;
step = 0;
LcdFullClear();
}
void NumKeyAction(unsigned char n) //數字鍵動作函數,按鍵輸入的數值n
{
unsigned char len;
unsigned char str[12];
if (step > 1) //如計算已完成,則重新開始新的計算
{
Reset();
}
if (step == 0) //輸入第一操作數
{
num1 = num1*10 + n; //輸入數值累加到原操作數上
len = NumToString(str, num1); //新數值轉換為字符串
LcdShowStr(16-len, 1, str); //顯示到液晶第二行上
}
else //輸入第二操作數
{
num2 = num2*10 + n;
len = NumToString(str, num2);
LcdShowStr(16-len, 1, str);
}
}
void OprtKeyAction(unsigned char type) //運算符按鍵動作函數,運算符類型type
{
unsigned char len;
unsigned char str[12];
if (step == 0) //第二操作數尚未輸入時響應,即不支持連續操作
{
len = NumToString(str, num1); //第一操作數轉換為字符串
LcdAreaClear(0, 0, 16-len); //清除第一行左邊的字符位
LcdShowStr(16-len, 0, str); //字符串靠右顯示在第一行
ShowOprt(1, type); //在第二行顯示操作符
LcdAreaClear(1, 1, 14); //清除第二行中間的字符位
LcdShowStr(15, 1, "0"); //在第二行最右端顯示0
oprt = type; //記錄操作類型
step = 1;
}
}
void GetResult() //計算結果
{
unsigned char len;
unsigned char str[12];
if (step == 1) //第二操作數已輸入時才執行計算
{
step = 2;
switch (oprt) //根據運算符類型計算結果,未考慮溢出問題
{
case 0: result = num1 + num2; break;
case 1: result = num1 - num2; break;
case 2: result = num1 * num2; break;
case 3: result = num1 / num2; break;
default: break;
}
len = NumToString(str, num2); //原第二操作數和運算符顯示在第一行
ShowOprt(0, oprt);
LcdAreaClear(1, 0, 16-1-len);
LcdShowStr(16-len, 0, str);
len = NumToString(str, result); //計算結果和等號顯示在第二行
LcdShowStr(0, 1, "=");
LcdAreaClear(1, 1, 16-1-len);
LcdShowStr(16-len, 1, str);
}
}
void KeyAction(unsigned char keycode) //按鍵動作函數,根據鍵碼執行相應動作
{
if ((keycode>='0') && (keycode<='9')) //顯示輸入的字符
{
NumKeyAction(keycode - '0');
}
else if (keycode == 0x26) //向上鍵,+
{
OprtKeyAction(0);
}
else if (keycode == 0x28) //向下鍵,-
{
OprtKeyAction(1);
}
else if (keycode == 0x25) //向左鍵,*
{
OprtKeyAction(2);
}
else if (keycode == 0x27) //向右鍵,÷
{
OprtKeyAction(3);
}
else if (keycode == 0x0D) //回車鍵,計算結果
{
GetResult();
}
else if (keycode == 0x1B) //Esc鍵,清除
{
Reset();
LcdShowStr(15, 1, "0");
}
}
void KeyDrive() //按鍵動作驅動函數
{
unsigned char i, j;
static unsigned char pdata backup[4][4] = { //按鍵值備份,保存前一次的值
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1}
};
for (i=0; i<4; i++) //循環掃描4*4的矩陣按鍵
{
for (j=0; j<4; j++)
{
if (backup[ i][j] != KeySta[ i][j]) //檢測按鍵動作
{
if (backup[ i][j] != 0) //按鍵按下時執行動作
{
KeyAction(KeyCodeMap[ i][j]); //調用按鍵動作函數
}
backup[ i][j] = KeySta[ i][j];
}
}
}
}
void KeyScan() //按鍵掃描函數
{
unsigned char i;
static unsigned char keyout = 0; //矩陣按鍵掃描輸出計數器
static unsigned char keybuf[4][4] = { //按鍵掃描緩沖區,保存一段時間內的掃描值
{0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}
};
//將一行的4個按鍵值移入緩沖區
keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;
keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;
keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;
keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;
//消抖后更新按鍵狀態
for (i=0; i<4; i++) //每行4個按鍵,所以循環4次
{
if ((keybuf[keyout][ i] & 0x0F) == 0x00)
{ //連續4次掃描值為0,即16ms(4*4ms)內都只檢測到按下狀態時,可認為按鍵已按下
KeySta[keyout][ i] = 0;
}
else if ((keybuf[keyout][ i] & 0x0F) == 0x0F)
{ //連續4次掃描值為1,即16ms(4*4ms)內都只檢測到彈起狀態時,可認為按鍵已彈起
KeySta[keyout][ i] = 1;
}
}
//執行下一次的掃描輸出
keyout++;
keyout &= 0x03;
switch (keyout)
{
case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;
case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;
case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;
case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;
default: break;
}
}
/*************************main.c文件程序源代碼**************************/
#include <reg52.h>
void ConfigTimer0(unsigned int ms);
extern void KeyScan();
extern void KeyDrive();
extern void LcdInit();
unsigned char T0RH = 0; //T0重載值的高字節
unsigned char T0RL = 0; //T0重載值的低字節
void main(void)
{
EA = 1; //開總中斷
ConfigTimer0(1); //配置T0定時1ms
LcdInit(); //初始化液晶
while(1)
{
KeyDrive();
}
}
void ConfigTimer0(unsigned int ms) //T0配置函數
{
unsigned long tmp;
tmp = 11059200 / 12; //定時器計數頻率
tmp = (tmp * ms) / 1000; //計算所需的計數值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 18; //修正中斷響應延時造成的誤差
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
}
void InterruptTimer0() interrupt 1 //T0中斷服務函數
{
TH0 = T0RH; //定時器重新加載重載值
TL0 = T0RL;
KeyScan(); //按鍵掃描
}
通過這樣一個程序,大家一方面學習如何進行多個.c文件編程,另外一個方面學會多個函數之間的靈活調用。可以把這個程序看成是一個簡單的小項目,學習一下項目編程都是如何進行和布局的。不要把項目想象的太難,再復雜的項目也是這種簡單程序的組合而已。
13.5 串口通信機制和實用的串口例程 我們前邊學串口通信的時候,比較注重的是串口底層時序上的操作過程,所以例程都是簡單的收發字符或者字符串。在我們實際應用中,往往串口還要和電腦上的上位機軟件進行交互,實現電腦軟件發送不同的指令,單片機可以對應執行不同的操作,這就要求我們組織一個比較合理的通信機制邏輯關系,用來實現我們想要的結果。
程序的功能是,通過我們電腦的串口調試助手下發三個不同的命令,第一條指令:buzz on可以讓蜂鳴器響;第二條指令:buzz off可以讓蜂鳴器不響;第三條指令:showstr ,這個命令空格后邊,可以添加任何字符串,讓后邊的字符串在1602液晶上顯示出來,同時不管發送什么命令,單片機收到后把命令原封不動的再通過串口發送給電腦,以表示“我收到了……你可以檢查下對不對”。這樣的感覺是不是更像是一個小項目了呢?
對于串口通信部分來說,單片機給電腦發字符串好說,有多大的數組,我們就發送多少個字節即可,但是單片機接收數據,接收多少個才應該是一幀數據呢?數據接收起始頭在哪里,結束在哪里?這些我們在接收到數據前都是無從得知的。那怎么辦呢?
我們的編程思路基于這樣一種通常的事實:當需要發送一幀(多個字節)數據時,這些數據都是連續不斷的發送的,即發送完一個字節后會緊接著發送下一個字節,期間沒有間隔或間隔很短,而當這一幀數據都發送完畢后,就會間隔很長一段時間(相對于連續發送時的間隔來講)不再發送數據,也就是通信總線上會空閑一段較長的時間。于是我們就建立這樣一種程序機制:設置一個軟件的總線空閑定時器,這個定時器在有數據傳輸時(從單片機接收角度來說就是接收到數據時)清零,而在總線空閑時(也就是沒有接收到數據時)時累加,當它累加到一定時間(例程里是30ms)后,我們就可以認定一幀完整的數據已經傳輸完畢了,于是告訴其它程序可以來處理數據了,本次的數據處理完后就恢復到初始狀態,再準備下一次的接收。那么這個用于判定一幀結束的空閑時間取多少合適呢?它取決于多個條件,并沒有一個固定值,我們這里介紹幾個需要考慮的原則:第一,這個時間必須大于波特率周期,很明顯我們的單片機接收中斷產生是在一個字節接收完畢后,也就是一個時刻點,而其接收過程我們的程序是無從知曉的,因此在至少一個波特率周期內你覺不能認為空閑已經時間達到了。第二,要考慮發送方的系統延時,因為不是所有的發送方都能讓數據嚴格無間隔的發送,因為軟件響應、關中斷、系統臨界區等等操作都會引起延時,所以還得再附加幾個到十幾個ms的時間。我們選取的30ms是一個折中的經驗值,它能適應大部分的波特率(大于1200)和大部分的系統延時(PC機或其它單片機系統)情況。
我先把這個程序最重要的UART.c文件中的程序貼出來,一點點給大家解析,這個是實際項目開發常用的用法,大家一定要認真弄明白。
#include <reg52.h>
bit flagOnceTxd = 0; //單次發送完成標志,即發送完一個字節
bit cmdArrived = 0; //命令到達標志,即接收到上位機下發的命令
unsigned char cntRxd = 0;
unsigned char pdata bufRxd[40]; //串口接收緩沖區
extern bit flagBuzzOn;
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);
void ConfigUART(unsigned int baud) //串口配置函數,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
}
unsigned char UartRead(unsigned char *buf, unsigned char len) //串口數據讀取函數,數據接收指針buf,讀取數據長度len,返回值為實際讀取到的數據長度
{
unsigned char i;
if (len > cntRxd) //讀取長度大于接收到的數據長度時,
{
len = cntRxd; //讀取長度設置為實際接收到的數據長度
}
for (i=0; i<len; i++) //拷貝接收到的數據
{
*buf = bufRxd[ i];
buf++;
}
cntRxd = 0; //清零接收計數器
return len; //返回實際讀取長度
}
void UartWrite(unsigned char *buf, unsigned char len) //串口數據寫入函數,即串口發送函數,待發送數據指針buf,數據長度len
{
while (len--)
{
flagOnceTxd = 0;
SBUF = *buf;
buf++;
while (!flagOnceTxd);
}
}
bit CmdCompare(unsigned char *buf, const unsigned char *cmd) //命令比較函數,緩沖區數據與指定命令比較,相同返回1,不同返回0
{
while (*cmd != '\0')
{
if (*cmd != *buf) //遇到不相同字符時即刻返回0
{
return 0;
}
else //當前字符相等時,指針遞增準備比較下一字符
{
cmd++;
buf++;
}
}
return 1; //到命令字符串結束時字符都相等則返回1
}
void UartDriver() //串口驅動函數,檢測接收到的命令并執行相應動作
{
unsigned char i;
unsigned char len;
unsigned char buf[30];
const unsigned char code cmd0[] = "buzz on";
const unsigned char code cmd1[] = "buzz off";
const unsigned char code cmd2[] = "showstr ";
const unsigned char code *cmdList[] = {cmd0, cmd1, cmd2};
if (cmdArrived) //有命令到達時,讀取處理該命令
{
cmdArrived = 0;
for (i=0; i<sizeof(buf); i++) //清零命令接收緩沖區
{
buf[ i] = 0;
}
len = UartRead(buf, sizeof(buf)); //將接收到的命令讀取到緩沖區中
for (i=0; i<sizeof(cmdList)/sizeof(cmdList[0]); i++) //與所支持的命令列表逐一進行比較
{
if (CmdCompare(buf, cmdList[ i]) == 1) //檢測到相符命令時退出循環,此時的i值就是該命令在列表中的下標值
{
break;
}
}
switch (i) //根據比較結果執行相應命令
{
case 0:
flagBuzzOn = 1; //開啟蜂鳴器
break;
case 1:
flagBuzzOn = 0; //關閉蜂鳴器
break;
case 2:
buf[len] = '\0'; //為接收到的字符串添加結束符
i = sizeof(cmd2) - 1;
LcdShowStr(0, 0, buf+i); //顯示字符串
i = len - i; //計算有效字符個數
if (i < 16) //有效字符少于16時,清楚液晶上的后續字符位
{
LcdAreaClear(i, 0, 16-i);
}
break;
default: //i大于命令列表最大下標時,即表示沒有相符的命令,給上機發送“錯誤命令”的提示
UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
return;
}
buf[len++] = '\r'; //有效命令被執行后,在原命令幀之后添加回車換行符后返回給上位機,表示已執行
buf[len++] = '\n';
UartWrite(buf, len);
}
}
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) //接收計數器未改變,即總線空閑時,累積空閑時間
{
idletmr += ms;
if (idletmr >= 30) //空閑時間超過30ms即認為一幀命令接收完畢
{
cmdArrived = 1; //設置命令到達標志
}
}
}
}
else
{
cntbkp = 0;
}
}
void InterruptUART() interrupt 4 //UART中斷服務函數
{
if (RI) //接收到字節
{
RI = 0; //手動清零接收中斷標志位
if (cntRxd < sizeof(bufRxd)) //接收緩沖區尚未用完時,
{
bufRxd[cntRxd++] = SBUF; //保存接收字節,并遞增計數器
}
}
if (TI) //字節發送完畢
{
TI = 0; //手動清零發送中斷標志位
flagOnceTxd = 1; //設置單次發送完成標志
}
}
我對照著程序,把重點部分給大家分析一下。
bit變量flagOnceTxd:單片機接收到串口下發命令后回發給調試助手程序中所用到的變量。
bit變量cmdArrived:單片機接收完整一幀數據后,通過這個變量指示接收一個命令完畢。
變量cntRxd:用來記錄一幀數據中,實際接收了多少個字節。
數組bufRxd[40]:用來存放接收到的數據。
聲明外部Bit變量flagBuzzOn:蜂鳴器響和不響的標志位,串口接收指令,讓蜂鳴器響,那就flagBuzzOn=1,如果讓它不響,那就flagBuzzOn=0。主程序main.c里進行判斷,如果是1則蜂鳴器響,如果是0則蜂鳴器不響。
聲明外部函數LcdShowStr:串口接收到讓液晶顯示字符的程序,調用此函數。
聲明外部函數LcdAreaClear:串口接收到讓液晶顯示字符的程序后,顯示有效字符,后面的液晶顯示清空。
下面對函數逐個進行分析。
ConfigUART函數不需要多說,配置串口波特率的。
函數unsigned char UartRead(unsigned char *buf, unsigned char len):
串口數據讀取函數,數據接收指針buf,讀取數據長度len,返回值為實際讀取到的數據長度。當其他函數要調用這個函數的時候,調用之前是不知道串口讀到字節長度,所以調用之前定義一個足夠大的緩沖數組比如buf[30],調用這個函數后,通過這個函數把串口讀到的數據全部復制到buf[30]這個數組中。這里有兩個長度,第一個長度是調用UartRead函數的形參len,這個長度用的是buf數組的長度(實際上是30);第二個長度是接收到的字符串的實際長度,UartRead函數不再是void類型,而是unsigned char類型,因此有一個返回值,返回值就是實際接收到的字符串的長度。
進入這個函數后,我們首先判斷一下30是否比接收到的字符串大,如果大的話,則獲取實際長度,如果不大于的話,則直接返回30。然后用一個for循環,通過數組元素的指針,把串口接收緩沖區的所有的數據全部傳遞到調用UartRead的那個函數的數組buf里。最后清掉UART接收數據個數的計數器,返回剛才這一幀的數據長度。
也許你會說,既然數據都已經接收到bufRxd[40]中了,那我直接從這里面拿出來用不就行了嘛,何必還得再拷貝到另一個地方去呢?我們設計這種雙緩沖的機制,主要是為了提高串口接收到響應效率:首先如果你在bufRxd[40]中處理數據,那么這時機不能再接收任何數據,因為新接收的數據會破壞原來的數據,造成其不完整和混亂;其次,你這個處理過程可能會耗費較長的時間,比如說上位機現在就給你發來一個延時顯示的命令,那么在這個延時的過程中你都無法去接收新的命令,在上位機看來就是你暫時失去響應了。而使用這種雙緩沖機制就可以大大改善這個問題,因為數據拷貝所需的時間是相當短的,而只要拷貝出去后,bufRxd[40]就可以馬上準備去接收新數據了。
函數void UartWrite(unsigned char *buf, unsigned char len) :
串口數據寫入函數,即串口發送函數,待發送數據指針buf,數據長度len。這個函數要和串口中斷函數結合來看,每次進入串口中斷后,把變量 flagOnceTxd置1,其實相當于把flagOnceTxd認為和TI一樣的效果,只是TI是給串口中斷用的,而flagOnceTxd是給 UartWrite函數作為標志位用的。當我們接收到電腦下發下來的指令后,我們通過這個函數把指令再返回到電腦串口調試助手。
函數bit CmdCompare(unsigned char *buf, const unsigned char *cmd) :
命令比較函數,緩沖區數據與指定命令比較,相同返回1,不同返回0。這是字符串比較函數,傳遞進來的是2個字符串的指針,因為我們單片機中的命令是完整的字符串格式,都擁有一個結束符——’\0’,而上位機下發的字符串是沒有結束符的,所以我們以單片機中的cmd為基準,檢測cmd直到遇到’\0’為止,對2個字符串進行比較,如果遇到不同的字符,則返回0,比較到最后都沒有發現不同的,則認定相同就返回1。
函數void UartDriver() :
串口驅動函數,檢測接收到的命令并執行相應動作。這個函數是放在主循環里檢測掃描的函數,要講這個函數之前,先來了解一下指針數組。
因為指針(指針變量)本身也是個變量,因此用指向同一種數據類型的幾個指針來構成一個數組,就形成了指針數組。我們程序之前,只講了RAM里邊的變量的地址,寫到FLASH里邊的數組以及所有的程序代碼,實際上都是有地址的,都可以使用指針訪問。那這個函數我們定義到FLASH里三個字符串數組,并且定義了一個指針數組,我寫出來,大家重點再認識一下。
const unsigned char code cmd0[] = "buzz on";
const unsigned char code cmd1[] = "buzz off";
const unsigned char code cmd2[] = "showstr ";
const unsigned char code *cmdList[] = {cmd0, cmd1, cmd2};
我們這個程序,字符串命令數越多,指針數組的優勢會越明顯,這就是我前邊提到的,指針的意義體現在復雜程序上,而且越復雜,指針的優勢越明顯。
這個函數判斷到了命令到達標志位變1后,首先將命令從串口接收緩沖區中把接收到的數據全部讀過來,然后和我們之前協議好的指令一一進行對比。這段程序的技巧,大家要學會。尤其這一句i<sizeof(cmdList)/sizeof(cmdList[0]),是我們求一個數組元素個數的一個常用方法。sizeof(cmdList)是數組所占的總空間大小,sizeof(cmdList[0])是第0個元素所占的空間大小,因為數組元素類型一致,所以每個元素所占空間大小肯定是一樣的。我們程序現在用了3個命令,這個位置我們不寫成3,而寫成這樣的表達式,后邊如果你想添加更多的命令,只需要添加一個命令數組cmd3[],并把它同時添加到cmdList[]中去就行了,而程序主體中不需要做任何改動,這就是程序易維護性的一種體現!
讀到指令后,根據判斷到的指令執行相應的動作,最后在字符串后加個尾巴,再發到串口調試助手,以便于調試助手把一條命令顯示為一行,而不是連續的不分行的顯示。
函數:void UartRxMonitor(unsigned char ms):
這是串口接收的監控程序,我們把它放在了1ms定時器中斷里,即每隔1ms調用一次,如果你使用的定時不是1ms,那么只需要修改調用時的參數就行了。這個函數就用來完成我們上面說的通過總線空閑定時器來判定一幀數據接收完畢的任務,所以它必須被不停的固定的間隔來調用。
剩下的程序相信大家都可以獨立研究明白,不懂的多和同學討論一下,我把剩下的程序貼出來。
/*************************main.c文件程序源代碼**************************/
#include <reg52.h>
sbit BUZZ = P1^6; //蜂鳴器控制引腳
bit flagBuzzOn = 0; //蜂鳴器啟動標志
unsigned char T0RH = 0; //T0重載值的高字節
unsigned char T0RL = 0; //T0重載值的低字節
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配置函數
{
unsigned long tmp;
tmp = 11059200 / 12; //定時器計數頻率
tmp = (tmp * ms) / 1000; //計算所需的計數值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 18; //修正中斷響應延時造成的誤差
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
}
void InterruptTimer0() interrupt 1 //T0中斷服務函數
{
TH0 = T0RH; //定時器重新加載重載值
TL0 = T0RL;
if (flagBuzzOn) //執行蜂鳴器鳴叫或關閉
BUZZ = ~BUZZ;
else
BUZZ = 1;
UartRxMonitor(1); //串口接收監控
}
/***********************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為止
}
void LcdWriteCmd(unsigned char cmd) //寫入命令函數
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_E = 1;
LCD1602_DB = cmd;
LCD1602_E = 0;
}
void LcdWriteDat(unsigned char dat) //寫入數據函數
{
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地址連續寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (*str != '\0') //連續寫入字符串數據,直到檢測到結束符
{
LcdWriteDat(*str);
str++;
}
}
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len) //區域清除,清除從(x,y)坐標起始的len個字符位
{
unsigned char addr;
//由輸入的顯示坐標計算顯示RAM的地址
if (y == 0)
addr = 0x00 + x; //第一行字符地址從0x00起始
else
addr = 0x40 + x; //第二行字符地址從0x40起始
//由起始顯示RAM地址連續寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (len--) //連續寫入空格
{
LcdWriteDat(' ');
}
}
void LcdInit() //液晶初始化函數
{
LcdWriteCmd(0x38); //16*2顯示,5*7點陣,8位數據接口
LcdWriteCmd(0x0C); //顯示器開,光標關閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
}
大家是否發現,現在模塊化編程后,很多函數,甚至.c文件,如果我們有需要,都可以直接復制添加到我們新的工程中來用,非常方便功能程序移植,這樣隨著實踐積累的增加,你會發現工作效率變得越來越高了。
13.6 作業1、將通信時序的邏輯完全理解透徹,并且能夠自己獨立看懂其他器件的時序圖。
2、根據1602整屏移動程序,改寫成右移以及先左移后右移的程序。
3、掌握多.c源文件編寫代碼的方法以及調用其他文件中變量和函數的用法。
4、徹底理解比較實用的串口通信機制程序,能夠完全解析明白實用串口通信例程,為今后自己獨立編寫類似程序打下基礎。
|