關鍵字:LCD12864 數字示波器
本文針對LCD12864 特性,完成了數字示波器顯示必須的繪圖驅動程序設計,這個教程定位給初學者使用,我立足從簡單到復雜一步一步介紹設計過程,甚至是調試的過程,還包括一些經驗總結,特別是提供了完整的keil 工程附件。希望讀者立足示波器項目,學到更多軟硬件設計經驗技巧。
一、簡易數字示波器原理
數字示波器基本原理可以簡單理解為:數據采集+ 圖形顯示,該過程循環(huán)進行,如圖1 所示。
圖1 簡易數字示波器流程圖
LCD 圖形顯示需要根據LCD 特性設計,不同LCD驅動程序不同,本篇將結合不帶字庫的LCD12864 設計顯示程序。
二、圖形液晶LCD12864繪圖驅動設計基礎
關于LCD 的硬件接口電路,在其他教程中有詳細介紹,涉及單片機總線知識和CPLD 內部電路,需要認真學習,這里借助現成的驅動函數,重點講解LCD繪圖程序設計。
LCD12864 的電路接口在頭文件中定義:
#define LCD_LCW XBYTE[0xf4ea]
// 左屏命令寫入
#define LCD_LDW XBYTE[0xf5ea]
// 左屏數據寫入
#define LCD_LCR XBYTE[0xf6ea]
// 左屏命令讀出
#define LCD_LDR XBYTE[0xf7ea]
// 左屏數據讀出
#define LCD_RCW XBYTE[0xf8ea]
// 右屏命令寫入
#define LCD_RDW XBYTE[0xf9ea]
// 右屏數據寫入
#define LCD_RCR XBYTE[0xfaea]
// 右屏命令讀出
#define LCD_RDR XBYTE[0xfbea]
// 右屏數據讀出
后面所有對LCD 的編程操作都是基于以上接口定義進行的各種讀寫操作。
首先來看LCD12864 的點陣結構圖,如圖2 所示。
圖2 LCD點陣分布結構圖
此LCD 屏由水平128 列,垂直64 行組成。水平128 列分左右各64 列兩個半屏構成。垂直64 行又分8 頁,每頁8 行(1 列8 點剛好1 字節(jié))。程序每次對LCD 的繪圖操作就是以最小單位1 字節(jié)進行操作的。
理解這點至關重要。也就是每次只能針對8 點進行操作,而不是1 點進行操作。左右屏由單獨地址線控制(前面的接口定義就是分左右屏定義的)。實際打點只需往指定“位置”寫入數據,“1”亮,“0”暗。
LCD 驅動忙檢測函數void loop_lcd12864_is_busy(unsigned char right)。
void loop_lcd12864_is_busy(unsigned char right)
{
unsigned char tmp,counter=0;
do {
if(right) tmp = LCD_RCR;
else tmp = LCD_LCR;
if(counter++>50) break; // 超時跳出
}
while ((tmp|0x7f)==0xff); //bit7 為1 則表示LCD 內部執(zhí)行命令,處于“忙”狀態(tài)
}
對LCD 進行讀寫操作時,需要進行“忙”檢測,LCD 內部也是由控制器來完成一系列刷屏操作的,執(zhí)行各種操作都是需要一定的時間,也就是說不是任何時候外部控制器都可以對LCD 發(fā)操作指令的,只有LCD為空閑狀態(tài)時才可以操作,忙檢測就是循環(huán)讀取LCD狀態(tài)標志位,判斷是否空閑,關于命令的細節(jié)請參考數據手冊。
命令寫入函數void lcd_cmd_wr(unsigned char cmd,right)。
void lcd_cmd_wr(unsigned char cmd, right)
{
loop_lcd12864_is_busy(right); // 忙檢測
if(right) LCD_RCW = cmd; // 右屏命令寫入
else LCD_LCW = cmd; // 左屏命令寫入
}
數據寫入函數void lcd_dat_wr(unsigned char data,right)。
void lcd_dat_wr(unsigned char data,right)
{
loop_lcd12864_is_busy(right);
if(right) LCD_RDW = data;
else LCD_LDW = data;
}
lcd_cmd_wr() 和lcd_dat_wr() 兩個函數分別是給LCD 寫命令和寫數據函數,通過寫命令函數設定地址。每個函數都分左右屏,“right”參數選擇,“0”選左屏,“非0”選右屏。
讀數據函數unsigned char lcd_dat_rd(unsigned char right)。
unsigned char lcd_dat_rd(unsigned char right)
{
loop_lcd12864_is_busy(right);
if(right) return(LCD_RDR);
else retuen(LCD_LDR);
}
該函數可以讀出LCD 當前顯示的數據,首次操作需要讀2次才有效。
LCD 清屏函數void lcd12864_clr(void)。
void lcd12864_clr(void)
{
unsigned char i,j;
for(i=0;i<8;i++) { // 從0 到7 共8 頁
lcd_cmd_wr(ORGX,0); // 分頁設定左屏0 點地址
lcd_cmd_wr(ORGY+i,0);
lcd_cmd_wr(ORGX,1); // 分頁設定右屏0 點地址
lcd_cmd_wr(ORGY+i,1);
for(j=0;j<64;j++) {
lcd_data_wr(0,0);
lcd_data_wr(0,1);
}
}
}
該函數對LCD 所有點陣寫0,完成一次清屏操作。這里的ORGY,PRGX 是設定光標的命令,光標指向(0,0)字節(jié),是一個固定值。實際在執(zhí)行數據寫入的時,x 坐標范圍從0 到63,在連續(xù)寫入過程中能夠實現自動加1,y 軸頁地址范圍從0 到7,需要逐頁設定。
LCD 初始化函數void lcd12864_init(void)。
void lcd12864_init(void)
{
lcd_cmd_wr(DISPON,0); // 顯示開啟
lcd_cmd_wr(DISPFIRST,0); // 設定顯示首行地址,修改首行地址可以實現屏幕滾動顯示效果
lcd_cmd_wr(ORGY,0); // 設定初始光標
lcd_cmd_wr(ORGX,0);
lcd_cmd_wr(DISPON,1); // 初始另外一半
lcd_cmd_wr(DISPFIRST,1);
lcd_cmd_wr(ORGY,1);
lcd_cmd_wr(ORG,1);
lcd12864_clr(); // 執(zhí)行清屏,非必須操作
}
該函數用來初始化LCD,設置顯示模式,光標位置等,在對LCD 繪圖時,最多的命令就是設定當前光標位置,通過光標位置來指定將要操作的LCD 顯示點。
在對LCD 編程操作以前,一定要執(zhí)行此函數對LCD 進行初始化操作。
從驅動函數可見,一次對LCD 寫入數據是以字節(jié)為單位,通過寫命令設定坐標,y 坐標從0 頁到7 頁,x 坐標從0 列到63 列,分左右屏,左上角為坐標(0,0)點,這和我們習慣的左下角為(0,0)坐標軸是不一樣的。
因為每次操作LCD 是一個字節(jié)為單位,對應8 點,如果我們希望以任意點為坐標顯示,還得另外尋找別的辦法編程實現真正“點”顯示。
如圖3 所示,在屏幕上指定位置畫點,水平軸就是x,與LCD 坐標一致,垂直軸需要將點坐標變成字節(jié)為單位的坐標,我們先按習慣將y 軸64 點從下至上編號0 到63,其中0 到7 點為字節(jié)0,8 到15 點為字節(jié)1,依此類推對應8 字節(jié)。
圖3 LCD“點”顯示示例
第一點y 軸為30,應該對應垂直哪個字節(jié)的哪個比特呢?
實際30 點應該在第4 字節(jié)(24 到31)的Bit 6 上,拿30/8 取整為3,剛好是應該跳過的前3 字節(jié)(對應0 到23),那么30%8(30 除8 取余數)呢,余數是6,不是剛好是Bit 位嗎?所以可以這樣將y 值映射到某字節(jié)的某點上,如果y 軸64 點對應8 字節(jié)變量Da[n],n從0 到7,則:
da[y/8] = 1 《 (y%8);或da[y》3]=0x01《(y&0x07);后一種算法更優(yōu)。
關鍵字:LCD12864 數字示波器
通過總結規(guī)律,用以上算法可以將任意0 到63 之間的數據作為坐標描點到對應的8 個字節(jié)中,然后將8個字節(jié)全部寫入LCD,則通過剛才算法就會有一點與所給坐標一致。
第一點:da[30/8] = 1 《 30%8; 即da[3] = 0x40;
第二點:da[10/8] = 1 《10%8; 即da[1] = 0x04;
首先給出列顯示子函數,在任意列顯示y 值對應點。
{
unsigned char j;
if(x<64) { // 根據列坐標選擇左右半屏
for(j=0;j<8;j++) { // 寫左半屏
lcd_cmd_wr(ORGY+j,0);
lcd_cmd_wr(ORGX+x,0);
lcd_data_wr(da[j],0);
}
}
else {
x-=64;// 坐標調整
for(j=0;j<8;j++) { // 寫右半屏
lcd_cmd_wr(ORGY+j,1);
lcd_cmd_wr(ORGX+x,1);
lcd_data_wr(da[j],1);
}
}
}
有了列顯示函數,LCD 任何坐標位置上描點繪圖函數為:
void lcd_disp(unsigned char x,y) //x 水平坐標,y 垂直坐標
{
unsigned char dat[8];
unsigned char j;
y=63-y;// 使xy 坐標符合習慣
for(j=0;j<8;j++) dat[j] = 0x0;
dat[y/8] |= 0x01《(y&0x07);
lcd_row_wr(x,dat);
}
以上函數能夠在指定坐標(x,y)上描點,下面嘗試將ADC 的值采集后送LCD 顯示。再按時間軸x 軸順序將不同時刻采集到的y 值順序寫入LCD,這是我們就可以在LCD 上看到隨時間變化的電壓曲線了。主程序為:
void main()
{
unsigned char i;
lcd12864_init();
for(i=0;i<128;i++) {
lcd_disp(i,read_adc(0)/16);// 從10bit 映射到6bit,要除16
}
while(1);
}
調整輸入給ADC 的信號頻率,可以得到滿意的波形圖了,效果如圖4 所示。
圖4 LCD實時顯示ADC圖
如果你成功做到了這一步,可喜可賀,已經掌握了繪圖基礎了,不過程序還要繼續(xù)完善。
三、圖形液晶LCD12864繪圖驅動設計提高
如何在現實波形顯示的基礎上,同時將定標網格也顯示出來呢?
首先我們看一種C 語法“A = 0x05; A |= 0x50;”運行以上指令后,A = 0x55 ;也可以說第二個數據0x50 是疊加到第一個數據上的,我們可以用這種算法把需要顯示的亮點(也就是“1”)按一定的算法疊加在一起,送LCD 顯示,就出現了我們希望的在波形上增加背景網格的效果。
因為網格與水平x 軸是嚴格關聯的,所以我們可以對x 軸數據進行判斷,有規(guī)律的將邊框和背景格點加入。
改進帶背景格的lcd_disp() 函數。
void lcd_disp(unsigned char x,unsigned char y)
{
unsigned char da[8];
unsigned char j;
y = 63-y;
for(j=1;j<7;j++) da[j] = 0x0;
da[0]=0x01;
da[7]=0x80;
if(x%5==0) {
da[21/8] |= 0x01《(21%8);
da[42/8] |= 0x01《(42%8);
}
if((x==0)||(x==127)) {// 加兩端邊框
for(j=0;j<8;j++) da[j] = 0xff;
}
da[y/8] |= 0x01《(y%8);
lcd_row_wr(x,da);
}
效果如圖5 所示。
圖5 帶網格的LCD顯示圖
四、圖形液晶LCD12864移動游標線繪圖驅動設計
聲明控制水平線的變量“unsigned charpointY=0;” 范圍0 到63, 聲明控制垂直線的變量“unsigned char pointX=0;”范圍0 到127。
void lcd_disp(unsigned char x,unsigned char y)
{
unsigned char da[8];
unsigned char j;
y = 63-y;
for(j=1;j<7;j++) da[j] = 0x0;
{// 繪制邊框
da[0]=0x01;
da[7]=0x80;
if((x==0)||(x==127)) {
for(j=0;j<8;j++) da[j] = 0xff;
}
}
if(x%5==0) da[pointY>>3] |= 0x01 《 (pointY&0x07);
// 繪制由變量pointY 控制的水平游標線
if(x==pointX) // 繪制由變量pointX 控制的垂直游標線
for(j=0;j<64;j++)
if(j%5==0) da[j>>3] |= 0x01 《(j&0x07);
da[y/8] |= 0x01《(y%8); // 繪制信號波形
lcd_row_wr(x,da);
}
運行效果如圖6 所示。
圖6 水平垂直移動游標線示例
五、圖形液晶LCD12864數字符號顯示
圖形點陣LCD 顯示數字,原理是把數字以點陣的形式取模,再把點陣模寫入特定的LCD 空間即可,首先來看數字取模,如圖7 所示,對數字“0”按8×5點取模。
圖7 數字取模示例圖
縱向看,8 點一列,從上至下對應bit0 到bit7,我們用1 表示“亮”,0 表示“暗”,從左至右,依次確定為0111 1100,即0x7c ;1000 0010,即0x82 ;10000010, 即0x82 ;1000 0010, 即0x82 ;0111 1100,即0x7c ;如果我們依次將這5 個字節(jié)寫入LCD 某頁連續(xù)5個地址空間,LCD 上就會顯示“0”。
下面我們把數字變量在LCD 上動態(tài)顯示,就是數值變了,顯示跟著變。
字符顯示LCD 驅動函數,實現8×n 點陣字符寫入函數。
void lcd_put_xyns(unsigned char x,y,n,unsigned char *s)
{
unsigned char i;
for(i=0;i
if((x+i)>63) {
lcd_cmd_wr(ORGY+y,1);
lcd_cmd_wr(ORGX+x+i-64,1);
lcd_dat_wr(s[i],1);
}
else {
lcd_cmd_wr(ORGY+y,0);
lcd_cmd_wr(ORGX+x+i,0);
lcd_dat_wr(s[i],0);
}
}
}
參數:“x, y”是坐標,這里y 是頁坐標,取值從0 到7,“n”是點陣模字節(jié)數,“*s”是點陣模起始地址。
將字模生成字模表:
unsigned char code number[]={
0x7C,0x82,0x82,0x7C,0x84,0xFE,0x80,0x00,0xCC,0xA2,0x
92,0x8C,0x44,0x92,0x92,0x6C,
0x38,0x24,0xFE,0x20,0x9E,0x92,0x92,0x62,0x7C,0x92,0x9
2,0x64,0x06,0xF2,0x0E,0x02,
0x6C,0x92,0x92,0x6C,0x4C,0x92,0x92,0x7C,0x80,
};
lcd_put_xyns(0,0,4,number+0*4); // 直接顯示字符0
lcd_put_xyns(4,0,4,number+1*4); // 直接顯示字符1
……
tmp=3421; // 以下代碼顯示變量tmp
lcd_put_xyns(80,2,4,number+(tmp/1000)*4);
lcd_put_xyns(84,2,4,number+(tmp/100%10)*4);
lcd_put_xyns(88,2,4,number+(tmp/10%10)*4);
lcd_put_xyns(92,2,4,number+(tmp%10)*4);
關鍵字:LCD12864 數字示波器
運行效果如圖8 所示。
圖8 LCD顯示數字圖
基于以上原理,我們還可以將其他字符取模顯示。
最終完成的效果如圖9 所示。
圖9 完整示波器效果圖
六、數字示波器幾個參數
1. 數據采樣率與時間軸定標
數據采樣率也就是每秒鐘連續(xù)采集到的數據個數,或者說兩個有效數據之間的時間間隔。為了在LCD 上還原波形圖,時間軸與采樣率之間要有嚴格的換算關系。
為了還原波形圖,采樣率與被測量信號頻率之間有一定的關聯,不同的采樣率,對應不同頻段的信號,高采樣率適合高頻信號,低采樣率適合變化緩慢的低頻信號。
從顯示效果來看,為了在屏幕上再現信號的頻率特征,根據Nyquist 頻率特征定理,一般要保證信號的每個周期內至少有2 點,用LCD 再現,2 點效果已經很差了。最高采樣率一般由ADC 器件決定,這里用的TLC1549 最快只能做到88μs 采集一點,按2 點每周期計算的話,可以做到對5.6K(1000000/88/2) 信號采樣,實際效果已經很差了,1K 以內效果最好。
實際在設計采樣率檔位時,最快檔按100μs 每點,屏幕每5 點(500μs)一格,第一檔就是0.5ms/div,這一檔位需要連續(xù)采樣,用少量延時控制采樣率為10k,第二檔就是1ms/div,第三檔2ms/div,第四檔5ms/div,第五檔10ms/div,后面四檔分別用定時器控制完成。
2. 電壓軸分檔定標
由于這版沒做模擬電路,沒有信號電調理電路,所謂電壓檔定標是純軟件算法實現,屏幕同樣取5 點一格,共10 格,當輸入信號從0 到5V 變化時,LCD 剛好滿屏顯示,每格0.5V,另外還設計了0.1v/div,0.2v/div,1v/div,2v/div,5v/div。共6 檔。
3. 交流直流切換
功能完善的示波器應該從硬件上實現交直流切換,本版先從軟件上實現交流直流顯示,設計思想是根據采樣得到的數據的最大值和最小值確定交流信號幅度和中值,把直流部分減掉,顯示只顯示交流部分,這就是交流檔,直流檔就是不做去直流處理。
4. 運行停止切換
該功能實現起來很容易,所謂停止,就是停止新的數據采集,重復顯示同一幀數據,顯示的效果就是波形穩(wěn)定無抖動,便于對信號電壓和頻率進行測量。
5. 電壓測量
根據當前電壓檔位(每格表示多少伏),計算出兩測量線之間的電壓值,在LCD 上顯示。
6. 頻率測量
根據當前時間軸檔位(每格表示多長時間),計算出兩測量線之間的時間,假定兩測量線之間剛好是一個周期,轉換成頻率值在LCD 上顯示。
7. 波形平移
用變量控制波形在LCD 上顯示的相對位置,實現波形上下左右平移。
8. 幀同步
我們使用的數字示波器,對有規(guī)律的周期信號能夠穩(wěn)定顯示,要想實現穩(wěn)定顯示,需要在起始點顯示信號的不同周期的同一點,如何做到這點呢?
如圖10 所示,我們對比兩幀數據,波形起點不在同一點,我們沒辦法保證每次數據采樣剛好在同一點(硬件觸發(fā)的除外),我需要按一定的規(guī)律,在LCD 上繪圖時,總是從同一點開始,例如過零點,這里我們首先用統(tǒng)計的辦法找出信號的最大值和最小值,計算出信號中交流部分的中點,也就是過零點,然后逐點比較,搜尋過零點。
圖10 不同幀數據起點不同
程序設計思想是逐點比較過濾,我們取中值,如圖7-1 中第5 點所示,信號從小于中值到大于中值的上升沿為起點,不同幀信號可能從1、2、3、4 任何一點開始,這就要用程序判斷把設定的起點5 以前的數據丟掉。
程序是這樣的:
while(da_buffer[i] > dam) if(++i > (DATA_SIZE/2)) break;
while(da_buffer[i] <= dam) if(++i > (DATA_SIZE/2)) break;
while(da_buffer[i] > dam) if(++i > (DATA_SIZE/2)) break;
這里的dam 就是信號交流部分的中值。
程序用幾個while 語句,看上去很呆板,實際運行效果很好喔。第一句把比設定值大的數據過濾,如圖10 中的1、2、3 點,如果數據是從4 開始的,第一句會直接跳過;第二句把比設定值小的數據跳過,找出過零點的上升沿5 點。
9. 矢量繪圖
前面我們介紹的LCD 描點繪圖不是矢量繪圖,圖形由一系列的“點”組成,在波形繪圖區(qū),每列只有1個點,拿方波繪圖來看,描點繪圖圖形如圖12 所示,矢量繪圖在點與點之間填充“直線”,方波繪圖效果如圖11 所示。對比兩種繪圖效果,最好選擇矢量繪圖,LCD 波形顯示效果較點繪圖好很多。
圖11 點繪圖
圖12 矢量繪圖
矢量繪圖需要按時間軸連續(xù)兩點同時考慮,根據兩點之間的差值補點,這就是繪圖程序disp(x, y, l)中參數l 的作用。
七、數字示波器程序流程圖設計分析
表達程序設計思想的關鍵就是程序流程圖,下面將重點分析本程序設計的幾個關鍵流程圖。
主程序流程圖如圖13 所示,初始化完成系統(tǒng)初始化設置,包括全局變量初始化,紅外按鍵中斷、定時器中斷初始化,在主循環(huán)程序中,處理外部按鍵輸入,由于數據采集是在定時中斷中完成的,主循環(huán)中需要等待一幀數據采集完成后,才能對數據進行同步處理,包括直流濾波,數據到LCD 映射,調用LCD 顯示函數完成波形繪圖,一幀數據處理完成后,重新開中斷,等待下一幀數據采集。
圖13 主程序流程圖圖
LCD 波形顯示流程圖如圖14 所示,LCD 波形顯示區(qū)需要根據新采集到的數據不斷刷新,這里以LCD列為單位,一次繪圖一個(x, y)坐標點,這里的編程思想是定義7 字節(jié)內存變量,與LCD 的第x 列對應,首先對參數進行檢查,例如y 參數就不能超過LCD 顯示去最大值,超過需要修正,確保點到點映射不出錯,需要顯示的內容除了波形信號外,還有窗口邊框,背景格,游標線等,都要同時考慮。這里的l 參數是兩點之間的間隔,采用矢量顯示,兩點之間用直線矢量繪圖代替描點,顯示效果要好很多。
圖14 LCD波形顯示
數據采集流程圖如圖15 所示,數據采集需要嚴格等時,而且根據不同頻率的信號,從低頻到高頻需要用不同的采樣率,所以這里用定時器控制采樣時間間隔。
圖15 數據采集流程圖
由于定時器中斷任務調用需要額外時間,對于高速中斷是有一定時間間隔限制的,通過計算,100μs 周期采樣用定時器實現不了,所以這里定時數據采樣分高速和低速兩檔,高速檔一次一幀,低速檔一次一點,一幀數據采集完成后,回到主程序對數據進行處理。
按鍵任務流程圖如圖16 所示,由于示波器控制需要很多按鍵,涉及電壓檔位,采樣時間檔位,游標線等,這些檔位都由一個全局變量控制,在按鍵任務模塊,就是根據有效按鍵,對這些檔位全局變量進行加減調整,變量調整后,有些效果會在波形顯示刷新中體現出來,還有的需要單獨刷新顯示。
圖16 按鍵任務流程圖
八、經驗總結與心得體會
STC89C52RC 內部擴展RAM 只有256 字節(jié),不像數據手冊上介紹的512 字節(jié),外部RAM 超量使用會導致意想不到的后果,經常莫名其妙的死機。
窗口波形顯示有兩種模式,逐點顯示(采集一個數據馬上刷新顯示)和逐幀顯示(采集完成一幀數據后刷新顯示),經過測試論證,低頻逐點視覺效果好,高頻沒區(qū)別,但是逐點沒辦法同步。
由于版面限制,這里發(fā)表的只是一個壓縮版,詳細資料見“大學生電子實驗室論壇” http://www.ceet.hbnu.edu.cn/bbs/。
初學者在學習他人程序時,不要僅僅對程序代碼下功夫看懂,其實真正學習的捷徑是看懂程序流程圖,學習他人的程序設計思想,在吸收他人程序設計思想的前提下,具體編程實現特定的功能,程序可以有多種“寫法”。
作為終端輸出顯示裝置,并不需要很高的刷新速度,因為它是給“人”看的,每秒超過10 次的變化,對人眼睛產生不了有用的效果。
我寫這篇教程,特別注重解決問題方法的總結,我認為掌握一種解決問題的方法比掌握一種具體的知識要重要得多。