1. 程式人生 > >點陣屏上繪圖——基於LCD12864 控制詳解

點陣屏上繪圖——基於LCD12864 控制詳解

— 前言 — 



   前言往往要解釋寫文章的動機和原因,同時給作者一個正題以外灌水的機會——本文也不例外。 



1、為什麼我要寫這篇文章。 

不可否認,我的確受到了Armok的利誘影響,但是最近發生的一些事情卻使我覺得寫這篇文章是非常有必要的。在OurAVR上看到很多版本的LCD驅動程式,幾乎每一個版本都只是簡單的將全部或部分的顯示資料Cover到LCD的視訊記憶體上,完成一個字或者是圖片的顯示就等著大家喊“牛”了。其實要走的路還很遠。對一個工程專案來說,增加n多的成本來提供一個點陣屏作為使用者介面,不是一兩幅歡迎圖片和Now Loading...Please Standy By的提示能糊弄的過去的。使用者希望你提供的是友好的圖形介面GUI,雖然比不過XP和Apple的華麗,但是由各種基本圖形組成的視窗介面還是需要的。 

當我們真的想實現一個圖形介面的時候,很快就會發現,我們需要的不僅僅是一個被喊了“牛”的初級驅動,我們需要的是一個圖形引擎——一個自定義的圖形函式包,沒有DirectX的華麗,但是能繪製一個任意的直線或是矩形就夠了——結果往往發現無所適從。這個時候,我們遇到的就是一個門檻,真正的嵌入式工程師和一個業餘電子愛好者之間的門檻。 



2、我如何寫這篇文章 

考慮到本人老王賣瓜的習慣,所以請大家一定無比在吃飯前看本人寫的技術文章,同時保持耐心等待續集(絕對有續集)。本人現單身,個人問題眾多,學習任務重,所以可能有時候寫文章象羊拉屎,不對大家胃口,請見諒。 

硬體平臺:AVR   Mega8級 

LCD:     不帶字型檔的12864 

軟體平臺:ICC 

規範: 符合基本的C程式設計規範 



3、何時開始正文 

廣告之後,馬上回來……

怎樣在點陣屏上繪圖——基於LCD12864

Chapter Zero  
   — 預備知識 — 
其實,本文應該算是計算機圖形學的一個具體分支,所以,計算機圖形學的基本要求就是本文的基本要求,考慮到各位兄弟的胃口,我就多羅嗦下。 

1、位操作 
向LCD12864這種二值螢幕,我們習慣於用1個位元組表示連續的8個點,1對應對應位被點亮,0表示不亮,所以對圖形的操作最基本的手段就是位操作。 
複習下,常用的位操作,假設Dis表示某一個現存地址的內容 
Dis = Dis~     黑白顛倒 
Dis &= ~(1<<n) 第n處被擦去, 
Dis |= (1<<n)   第n處被畫了一個點 
Dis ^= (1<<n)   如果n處是亮的,就變被擦掉;如果n處是空白的,就被點亮…… 
…… 差不多就這些 

2、作圖原理 
點是一切光柵顯示裝置的基本要素,所有的操作都是以點為基礎的,所以學會如何利用點構成線、圓、填充就是必須掌握的——幾何不能太差哦。 
還有,結合螢幕的硬體特點,對演算法進行優化的一些方法也是需要掌握的。比方說,如何填充之類的……後面會針對LCD12864作詳細介紹的。 

3、人機互動學 
雖然很多人都沒有實實在在學過這門功課,但是多多少少對於介面應該怎樣有些許瞭解。如何利用手中的基本操作函式做出一些特效,如何安排窗體,如何繪製圖形介面的一些基本元素如按鈕,甚至如何顯示漢字,都是人際互動學需要教會你的——總之,如果你沒有學過這門課程,你的產品只有你自己用的話——跟著感覺走,沒錯的。 

4、最最重要的物質基礎 
你要掌握一種微控制器,掌握一種點陣螢幕。 
AVR Inside 
Not Only For LCD12864

怎樣在點陣屏上繪圖——基於LCD12864

Chapter One  

— 從點滴開始 —  



前面已經說過,對於柵格顯示裝置來說,點是一切圖形的基礎,說白了,不會在液晶螢幕的任意位置隨心所欲的畫點就不能說你已經掌握了某種液晶螢幕的使用了。事實上,根據筆者的經驗,嘗試畫點往往能暴露出很多問題。就拿LCD12864來說吧,一段時間,筆者總是發現,在片切換的地方有一點點錯誤…… 



在說明如何畫點之前先說明一個概念:顯示緩衝區。 

所謂顯示緩衝區,顧名思義,就是顯示資料的一個緩衝地帶。一般情況下,我們繪圖也是直接對這塊緩衝區域進行操作的。 

很多朋友就要提問了:我們為什麼不直接對螢幕操作呢? 

事實上,LCD本身存在一塊顯示儲存器,也可以被認為是一個顯示快取,直接寫在這個儲存器的資料並非直接顯示出來,而多半需要一個顯示指令來影射一下。我們有時候也通過這種類似的技術來實現很大的圖片的顯示——先畫好,再拿給大家看,讓人以為你是一下就畫好的,當然如果你刻意需要那種圖片被“畫”出來的效果,則不在討論之列。問題就在於,LCD是片外資源,對其儲存器的訪問可以被認為是對片外儲存器的訪問,其速度顯然沒有對片內SRAM的操作速度要快。如果我們使用那種常用的序列方式來作圖(所謂序列方式作圖,就是繪圖指令的執行和系統的其他操作時序列的,指令不完成,其它操作就不回被執行),那麼對於一些實時性要求高的系統來說就會造成重大隱患——甚至是不符合要求的;如果開闢一段片記憶體儲空間和LCD的儲存器一一對應,在相同的時間段內,花費相同的資源來保持著兩個儲存空間的一致性,那麼就可以保證實時系統的穩定和可靠,保證畫面顯示的正常(因為允許跳幀嘛^_^)。這就是我們為什麼需要另外在片內選取一個顯示緩衝區的原因。 

以後,我們的操作都是針對顯示緩衝區的。顯示緩衝區將成為一個概念,無論這個緩衝區位於SRAM內還是LCD內部。比方說,我們需要一個低成本的遊戲機 ——如做一個貪食蛇遊戲機送給老師作為課程設計,或者送給小侄子,那麼,M8甚至是M48成為首選,問題是,M8絕對無法開闢出一個8 * 128 = 1K大小的陣列,所以,這個時候,認定LCD的片記憶體儲器作為顯示緩衝區就是不二的選擇——好在貪食蛇速度太快了也沒有辦法玩哈。 



繞了這麼多的彎子,究竟如何畫點呢?我還想多補充一個原因,關於,為什麼需要顯示緩衝區,由於點陣螢幕使用的是1個位元組來表示8個點的,如果你想使得一個位元組中的某一個點被操作而不影響別的點,你起碼要知道原來這個位置是什麼內容,對於有些使用595級聯的LCD裝置來說,無法直接從LCD讀取現存資料,所以開闢一個緩衝區,從中獲得資訊加以加工以後再放回去,是這種裝置處理顯示圖形的唯一方法——當然我們沒有那麼慘,但是從LCD中獲取所需點所在位元組的內容還是必須的,不然你畫一個點就會破壞一條線上所有的資料。 



   好的,那麼我們實際上已經明確瞭如何去畫一個點。 

   首先,根據使用者給出的座標計算出所要畫的點所在現存內的地址偏移量; 

   然後,我們讀出該地址內的資料; 

   再次,根據使用者所需畫點的型別(擦除、畫點、反相點)來進行相應的操作獲得一個新的資料; 

   最後,將該資料寫回原來的地址。 



   不是很難吧? 

--------------------------------------------- 

LCD12864補充知識 

1、關於座標系。很多人,包括筆者,一開始都看不懂LCD12864的記憶體影射方式,感覺X、Y似乎不是那麼回事。後來才發現,只要把螢幕豎著放一切就好懂了。X還是橫軸,Y還是豎軸……但是這顯然不符合我們的習慣,我們習慣於長的那個邊作為橫軸,所以需要一點點座標之間的轉換。 

假設輸入的是正常習慣的座標 X,Y DX DY就是LCD上的座標,那麼轉換關係是 



char DX = (Y >> 3);                               //計算出屬於哪個位元組 

char BX = Y - (DX << 3);                             //屬於該位元組的哪個位 

char DY = X; 



2、讀取12864的資料的時候,一定要注意,E訊號要在一個下降延之後持續拉高,然後才能正常獨處資料;假設直接拉高,的確也能讀出資料,但是,等著抓頭皮,發帖子“[跪求]大俠幫忙關於12864——請使用明確的大標題……”^_^ 



--------------------------------------------- 

廢話少說(說的不少了),看原始碼: 

# define LCD12864_Graphic_Draw          0x01 

# define LCD12864_Graphic_Clear           0x00 

# define LCD12864_Graphic_Not          0x02 



…… 



void LCD12864Draw(char X,char Y,char Type) 



char DX = (Y >> 3);                               //計算出屬於哪個位元組 

       char BX = Y - (DX << 3);                      //計算出屬於位元組哪一位 

       char TempData = 0; 



       LCD12864_ChooseBoth; 

      

       setX(DX); 

       if (X > 63) 

       { 

         LCD12864_ChooseCS2; 

            X -= 64; 

       } 

       else 

       { 

         LCD12864_ChooseCS1; 

       } 

       setY(X); 

      

       TempData = getLCD12864Data(); 

      

       switch (Type) 

       { 

         case LCD12864_Graphic_Clear: 

                   TempData &= ~(1<<BX); 

                   break; 

            case LCD12864_Graphic_Not: 

                   TempData ^= (1 << BX); 

                   break; 

            default: 

                   TempData |= (1 << BX);  

       } 

      

setY(X); 

      

       sendDataToLCD(TempData); 

}

怎樣在點陣屏上繪圖——基於LCD12864

特別說明一下,關於貪食蛇範例的問題,這篇文章裡面只會簡單得提及一下。 

作為嵌入式系統開發的一個範例,我會另外開一個帖子詳細說明開發過程。 
這個範例將作為介紹嵌入式系統開發方法的一個很好的例子,用於解釋一個系統和一段表示您調通了某一個功能的程式碼之間有什麼區別,同時也將介紹嵌入式開發系統的幾種模式(超級迴圈、排程器),順便侃一侃時間驅動的系統RTOS (Real Time Operation System實時作業系統)和RTS(Real Time System)實時系統。

怎樣在點陣屏上繪圖——基於LCD12864

[本章導讀] 

直線由點構成,更精確的說,直線是由靠近這條線的畫素構成。這就引出一個問題,究近那些點算是靠近一條直線;哪些點不算是靠近一條直線,這必須使用一種演算法作為依據。實際上,圖形學演算法和純幾何演算法還是有很大差別的,問題就出在一個離散化上面,說白了,你畫出的直線很可能是一組波動厲害的鋸齒象素群而不是一條看起來有規則變化的直線。 
當然,太過於理論的東西對我們是沒有多少實際價值的。下面,我就介紹兩種常見的畫線思路,一種就是最容易被想到的直線方程計算的方法,另外一種則是被稱為布蘭森漢姆(Bresenham)的計算機圖形學主流演算法。 
   
在介紹完這兩種演算法以後,我們會針對LCD12864的硬體結構為例子,介紹,具體演算法的實現和優化。

怎樣在點陣屏上繪圖——基於LCD12864

首先,我們從最基本的數學演算法說起。 
如果我們使用公式y = kx + b來作為繪圖的依據,那麼就需要分3種情況:水平直線,斜率為0;垂直直線,斜率為五窮達(或者說k不存在);普通直線。 
假設我們已經知道直線的起始座標點(Xbegin,Ybegin)和終點(Xend,Yend),x,y,是當前的座標點,如果我們通過增加x反算出y的方法的話,這個公式就可以很容易轉換為虛擬碼。 
LineMode 為直線的型別:水平,垂直,普通 
if Xbegin == Xend then LineMode = 水平 
elseif Ybegin = Yend   then LineMode = 垂直 
else k = (Yend - Ybegin) / (Xend - Xbegin) 

switch LineMode 
   case 水平 
      for x = Xbegin to Xend 
         在x,Ybegin處畫點 
   case 垂直 
      for y = Ybegin to Yend 
         在Ebegin,y處畫點 
   default: 
      for x = Xbegin to Xend 
      { 
         y = kx + b 
         在x,y處畫點 
      } 

非常簡單,不是麼?注意,大部分情況下,這個演算法存在很多乘法和除法,對微控制器系統來說,可能不是那麼合適哦。畫出的點也基本令人滿意。

怎樣在點陣屏上繪圖——基於LCD12864

有了上面的程式碼墊底,我想很多人都可以放心了,因為大不了跑一個高速晶振也能快速的畫出直線,否則你還需要耐心的閱讀下面的演算法。 

首先,我們給出這個演算法的虛擬碼。 
在(x1,y1)到(x2,y2)之間畫一條直線 

dx 是x到終點橫座標的距離 
dy 是y到終點縱座標的距離 
ix 是dx的絕對值 
iy 是dy的絕對值 
inc是dx和dy中較大的那個 
plot是是否要畫一個點的標誌位,boolean變數 

plotx 是當前點所在的橫座標 
ploty 是當前點所在的縱座標 
  
plotx = x1 
ploty = y1 
x = 0 
y = 0 

在 plotx,ploty畫一個點——起點 

for i = 0 to inc 增量1 
   x += ix 
   y += iy 
   plot = false 

   if x > inc then 
          plot = true 
          x -= inc 
            if dx > 0 then plotx ++ 
            if dx < 0 then plotx -- 
  
   if y > inc then 
          plot = true 
          y -= inc 
            if dy > 0 then ploty ++ 
            if dy < 0 then ploty -- 

   if plot == true then 在(plotx,ploty)處畫點 
這就是計算機圖形學中流行的布蘭森漢姆(Bresenham)演算法,他的意圖就是採用離散的整數增量來代替斜率增量計算,學習這個演算法,最好的方法不是看多少理論,而是按照上面的虛擬碼自己完成一條直線的繪製工作,你就能心領神會了——不是小弟我偷懶。 

所有的計算都是簡單得整數計算,程式碼效率自然不用小弟我羅嗦哈。

怎樣在點陣屏上繪圖——基於LCD12864

俗語說,巧婦難為無米之炊,有了點再有了直線演算法,距離窗體的繪製不遠了,But Stop! “沒有最好,但求更好”這是我們做系統開發應該謹記的一條準則。 
有了上面的演算法還不夠,畢竟,他們只是一些虛擬碼,針對不同的螢幕——準確地說,是針對不同的視訊記憶體對映方式,有不同的演算法優化方法。究竟有哪些優化方法暫且不論,其實他們都是一個原理,我想先解釋一下,為什麼我們需要優化演算法,或者說,我們需要先弄清楚是什麼地方產生了冗餘。 

還記得黑白點陣螢幕的視訊記憶體對映方式麼?它簡單的使用1個位元組表示8個座標點,同時這1個位元組是沿著螢幕的短邊方向對映的,所以當我們想畫一條垂直的直線時,對於每一個牽涉到的位元組都有可能要重複的操作8次之多,這種操作不是簡單的畫線,而是要先讀取再計算最後再寫這樣的複合操作,重複8次只是為了把整個位元組變黑顯然是一種超級不可容忍的冗餘——大家都知道,直接把這個位元組讀取一次,計算一次,再寫一次就完成了。基於這種思想,我們需要把這種特殊情況單獨提取出來,重新優化程式碼……具體優化程式碼就留給大家做作業了哈。 

同時補充一下,這樣做,不是隻對畫線作了優化,事實上,在後面,矩形的填充裡面,這種優化會極大地提高速度,因為填充的本質就是畫線(點)……