1)實驗平臺:正點原子stm32f103戰(zhàn)艦開發(fā)板V4
2)平臺購買地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套實驗源碼+手冊+視頻下載地址: http://www.openedv.com/thread-340252-1-1.html
第二十五章 TFT-LCD(MCU屏)實驗
前面我們介紹了OLED模塊及其顯示,但是該模塊只能顯示單色/雙色,不能顯示彩色,而且尺寸也較小。本章我們將介紹正點原子的TFT-LCD模塊(MCU屏),該模塊采用TFT-LCD面板,可以顯示16位色的真彩圖片。在本章中,我們將使用開發(fā)板底板上的TFTLCD接口(僅支持MCU屏,本章僅介紹MCU屏的使用),來點亮TFT-LCD,并實現(xiàn)ASCII字符和彩色的顯示等功能,并在串口打印LCD控制器ID,同時在LCD上面顯示。
本章分為如下幾個小節(jié):
25.1 TFTLCD和FSMC簡介
25.2 硬件設(shè)計
25.3 程序設(shè)計
25.4 下載驗證
25.1 TFT-LCD簡介
本章我們將通過STM32F103的FSMC外設(shè)來控制TFT-LCD的顯示,這樣我們就可以用STM32輸出一些信息到顯示屏上了。
25.1.1 TFT-LCD簡介
液晶顯示器,即Liquid Crystal Display,利用了液晶導電后透光性可變的特性,配合顯示器光源、彩色濾光片和電壓控制等工藝,最終可以在液晶陣列上顯示彩色的圖像。目前液晶顯示技術(shù)以TN、STN、TFT三種技術(shù)為主,TFT-LCD即采用了TFT(Thin Film Transistor)技術(shù)的液晶顯示器,也叫薄膜晶體管液晶顯示器。
TFT-LCD與無源TN-LCD、STN-LCD的簡單矩陣不同的是,它在液晶顯示屏的每一個象素上都設(shè)置有一個薄膜晶體管(TFT),可有效地克服非選通時的串擾,使顯示液晶屏的靜態(tài)特性與掃描線數(shù)無關(guān),因此大大提高了圖像質(zhì)量。TFT式顯示器具有很多優(yōu)點:高響應度,高亮度,高對比度等等。TFT式屏幕的顯示效果非常出色,廣泛應用于手機屏幕、筆記本電腦和臺式機顯示器上。
由于液晶本身不會發(fā)光,加上液晶本身的特性等原因,使得液晶屏的成像角受限,我們從屏幕的的一側(cè)可能無法看清液晶的顯示內(nèi)容。液晶顯示器的成像角的大小也是評估一個液晶顯示器優(yōu)劣的指標,目前,規(guī)格較好的液晶顯示器成像角一般在120°~160°之間。
正點原子TFT-LCD模塊(MCU屏)有如下特點:
1,2.8’/3.5’/4.3’/7’等4種大小的屏幕可選。
2,320×240的分辨率(3.5’分辨率為:320480,4.3’和7’分辨率為:800480)。
3,16位真彩顯示。
4,自帶觸摸屏,可以用來作為控制輸入。
本章,我們以正點原子2.8寸(此處的寸是代表英寸,下同)的TFT-LCD模塊為例介紹,(其他尺寸的LCD可參考具體的LCD型號的資料,也比較類似),該模塊支持65K色顯示,顯示分辨率為320×240,接口為16位的8080并口,自帶觸摸功能。
該模塊的外觀如圖25.1.1.1所示:
圖25.1.1.1 正點原子2.8寸TFTLCD外觀圖
模塊原理圖如圖25.1.1.2所示:
圖25.1.1.2 正點原子TFTLCD模塊原理圖
TFTLCD模塊采用2*17的2.54公排針與外部連接,即圖中TFT_LCD部分。從圖25.1.1.2可以看出,正點原子TFTLCD模塊采用16位的并方式與外部連接。圖25.1.1.2還列出了觸摸控制的接口,但觸摸控制是在顯示的基礎(chǔ)上疊加的一個控制功能,不配置也不會對顯示造成影響,我們放到以后的章節(jié)再介紹觸摸的用法。該模塊與顯示功能的相關(guān)的信號線如表25.1.1.1:
表25.1.1.1 TFT-LCD接口信號線
上述的接口線實際是對應到液晶顯示控制器上的,這個芯片位于液晶屏的下方,所以我們從外觀圖上看不到??刂芁CD顯示的過程,就是按其顯示驅(qū)動芯片的時序,把色彩和位置信息正確地寫入對應的寄存器。
25.1.2 液晶顯示控制器
正點原子提供2.8/3.5/4.3/7寸等4種不同尺寸和分辨率的TFTLCD模塊,其驅(qū)動芯片為:ILI9341/ST7789/NT35310/NT35510/SSD1963等(具體的型號,大家可以通過下載本章實驗代碼,通過串口或者LCD顯示查看),這里我們僅以ILI9341控制器為例進行介紹,其他的控制基本都類似,我們就不詳細闡述了。
ILI9341液晶控制器自帶顯存,可配置支持8/9/16/18位的總線中的一種,可以通過3/4線串行協(xié)議或8080并口驅(qū)動。正點原子的TFT-LCD模塊上的電路配置為8080并口方式,其顯存總大小為172800(24032018/8),即18位模式(26萬色)下的顯存量。在16位模式下,ILI9341采用RGB565格式存儲顏色數(shù)據(jù),此時ILI9341的18位顯存與MCU的16位數(shù)據(jù)線以及RGB565的對應關(guān)系如圖25.1.2.1所示:
圖25.1.2.1 16位數(shù)據(jù)與顯存對應關(guān)系圖
從圖中可以看出,ILI9341在16位模式下面,18位顯存的B0和B12并沒有用到,對外的數(shù)據(jù)線使用DB0-DB15連接MCU的D0-D15實現(xiàn)16位顏色的傳輸(使用8080 MCU 16bit I型接口,詳見9341數(shù)據(jù)手冊7.1.1節(jié))。
這樣MCU的16位數(shù)據(jù),最低5位代表藍色,中間6位為綠色,最高5位為紅色。數(shù)值越大,表示該顏色越深。另外,特別注意ILI9341所有的指令都是8位的(高8位無效),且參數(shù)除了讀寫GRAM的時候是16位,其他操作參數(shù),都是8位的。
知道了屏幕的顯色信息后,我們?nèi)绾悟?qū)動它呢?OLED的章節(jié)我們已經(jīng)描述過8080方式操作的時序,我們通過《ILI9341_DS.pdf》來加深一下在8080并口方式下如何操作這個芯片。
以寫周期為例,8080方式下的操作時序如圖25.1.2.2所示。
圖25.1.2.2 8080方式下對液晶控制器的寫操作
上圖中的各個控制線與我們在表25.1.1.1提到的命名有些許差異,因為我們在原理圖時往往為了方便自己記憶會對命名進行微調(diào),為了方便讀者對照,我們把圖25.1.2.2中列出的引腳引腳與我們的TFTLCD模塊的的對應關(guān)系再列出,如表25.1.2.1所示。
表25.1.2.1 TFT-LCD引腳與液晶控制器的對應關(guān)系
這下我們再來分析一下圖25.1.2.2所示的寫操作的時序,控制液晶的主機,在整個寫周期內(nèi)需要控制片選CSX拉低(標注為①),之后對其它的控制線的電平才有效。在標號②表示的這個寫命令周期中,D/CX被位低(參考ILI9341的引腳定義),同時把命令碼通過數(shù)據(jù)線D[17:0](我們實際只用了16個引腳)按位編碼。注意到③處,需要數(shù)據(jù)線在入電平拉高后再操持一段時間以便數(shù)據(jù)被正確采樣。
圖25.1.2.2中⑤表示寫數(shù)據(jù)操作,與前面描述的寫命令操作只有D/CX的操作不同,讀者們可以嘗試自己分析一下。更多的關(guān)于ILI9341的讀寫操作時序則參考《ILI9341_DS.pdf》。
通過前述的時序分析,我們知道了對于ILI9341來說,控制命令有命令碼、數(shù)據(jù)碼之分,接下來,我們介紹一下ILI9341的幾個重要命令。因為ILI9341的命令很多,我們這里就不全部介紹了,有興趣的大家可以找到ILI9341的datasheet看看。里面對這些命令有詳細的介紹。我們將介紹:0xD3,0x36,0x2A,0x2B,0x2C,0x2E等6條指令。
指令0xD3,是讀ID4指令,用于讀取LCD控制器的ID,該指令如表25.1.2.2所示:
表25.1.2.2 0xD3指令描述
從上表可以看出,0xD3指令后面跟了4個參數(shù),最后2個參數(shù),讀出來是0x93和0x41,剛好是我們控制器ILI9341的數(shù)字部分,從而,通過該指令,即可判別所用的LCD驅(qū)動器是什么型號,這樣,我們的代碼,就可以根據(jù)控制器的型號去執(zhí)行對應驅(qū)動IC的初始化代碼,從而兼容不同驅(qū)動IC的屏,使得一個代碼支持多款LCD。
接下來看指令:0x36,這是存儲訪問控制指令,可以控制ILI9341存儲器的讀寫方向,簡單的說,就是在連續(xù)寫GRAM的時候,可以控制GRAM指針的增長方向,從而控制顯示方式(讀GRAM也是一樣)。該指令如表25.1.2.3所示:
表25.1.2.3 0x36指令描述
從上表可以看出,0x36指令后面,緊跟一個參數(shù),這里主要關(guān)注:MY、MX、MV這三個位,通過這三個位的設(shè)置,我們可以控制整個ILI9341的全部掃描方向,如表25.1.2.4所示:
表25.1.2.4 MY、MX、MV設(shè)置與LCD掃描方向關(guān)系表
這樣,我們在利用ILI9341顯示內(nèi)容的時候,就有很大靈活性了,比如顯示BMP圖片,BMP解碼數(shù)據(jù),就是從圖片的左下角開始,慢慢顯示到右上角,如果設(shè)置LCD掃描方向為從左到右,從下到上,那么我們只需要設(shè)置一次坐標,然后就不停的往LCD填充顏色數(shù)據(jù)即可,這樣可以大大提高顯示速度。
實驗中,我們默認使用從左到右,從上到下的掃描方式。
接下來看指令:0x2A,這是列地址設(shè)置指令,在從左到右,從上到下的掃描方式(默認)下面,該指令用于設(shè)置橫坐標(x坐標),該指令如表25.1.2.5所示:
在默認掃描方式時,該指令用于設(shè)置x坐標,該指令帶有4個參數(shù),實際上是2個坐標值:SC和EC,即列地址的起始值和結(jié)束值,SC必須小于等于EC,且0≤SC/EC≤239。一般在設(shè)置x坐標的時候,我們只需要帶2個參數(shù)即可,也就是設(shè)置SC即可,因為如果EC沒有變化,我們只需要設(shè)置一次即可(在初始化ILI9341的時候設(shè)置),從而提高速度。
與0X2A指令類似,指令:0X2B,是頁地址設(shè)置指令,在從左到右,從上到下的掃描方式(默認)下面,該指令用于設(shè)置縱坐標(y坐標)。該指令如表25.1.2.6所示:
表25.1.2.6 0X2B指令描述
在默認掃描方式時,該指令用于設(shè)置y坐標,該指令帶有4個參數(shù),實際上是2個坐標值:SP和EP,即頁地址的起始值和結(jié)束值,SP必須小于等于EP,且0≤SP/EP≤319。一般在設(shè)置y坐標的時候,我們只需要帶2個參數(shù)即可,也就是設(shè)置SP即可,因為如果EP沒有變化,我們只需要設(shè)置一次即可(在初始化ILI9341的時候設(shè)置),從而提高速度。
接下來看指令:0X2C,該指令是寫GRAM指令,在發(fā)送該指令之后,我們便可以往LCD的GRAM里面寫入顏色數(shù)據(jù)了,該指令支持連續(xù)寫,指令描述如表25.1.2.7所示。
表25.1.2.7 0X2C指令描述
由表25.1.2.7可知,在收到指令0X2C之后,數(shù)據(jù)有效位寬變?yōu)?6位,我們可以連續(xù)寫入LCD GRAM值,而GRAM的地址將根據(jù)MY/MX/MV設(shè)置的掃描方向進行自增。例如:假設(shè)設(shè)置的是從左到右,從上到下的掃描方式,那么設(shè)置好起始坐標(通過SC,SP設(shè)置)后,每寫入一個顏色值,GRAM地址將會自動自增1(SC++),如果碰到EC,則回到SC,同時SP++,一直到坐標:EC,EP結(jié)束,期間無需再次設(shè)置的坐標,從而大大提高寫入速度。
最后,來看看指令:0X2E,該指令是讀GRAM指令,用于讀取ILI9341的顯存(GRAM),該指令在ILI9341的數(shù)據(jù)手冊上面的描述是有誤的,真實的輸出情況如表25.1.2.8所示:
表25.1.2.8 0X2E指令描述
該指令用于讀取GRAM,如表25.1.2.7所示,ILI9341在收到該指令后,第一次輸出的是dummy數(shù)據(jù),也就是無效的數(shù)據(jù),第二次開始,讀取到的才是有效的GRAM數(shù)據(jù)(從坐標:SC,SP開始),輸出規(guī)律為:每個顏色分量占8個位,一次輸出2個顏色分量。比如:第一次輸出是R1G1,隨后的規(guī)律為:B1R2G2B2R3G3B3R4G4B4R5G5…以此類推。如果我們只需要讀取一個點的顏色值,那么只需要接收到參數(shù)3即可,如果要連續(xù)讀?。ɡ肎RAM地址自增,方法同上),那么就按照上述規(guī)律去接收顏色數(shù)據(jù)。
以上,就是操作ILI9341常用的幾個指令,通過這幾個指令,我們便可以很好的控制ILI9341顯示我們所要顯示的內(nèi)容了。
25.1.3 FSMC簡介
ILI9341的8080通訊接口時序可以由STM32使用GPIO接口進行模擬,但這樣效率太低,STM32提供了一種更高效的控制方法——使用FSMC接口實現(xiàn)8080時序,但FSMC是STM32片上外設(shè)的一種,并非所有的STM32都擁有這種硬件接口,使用何種方式驅(qū)動需要在芯片選型時就確定好。我們的開發(fā)板支持FSMC接口,下面我們來了解一下這個接口的功能。
FSMC,即靈活的靜態(tài)存儲控制器,能夠與同步或異步存儲器和16位PC存儲器卡連接,F(xiàn)SMC接口可以通過地址信號,快速地找到存儲器對應存儲塊上的數(shù)據(jù)。STM32F1的FSMC接口支持包括SRAM、NAND FLASH、NOR FLASH和PSRAM等存儲器。F1系列的大容量型號,且引腳數(shù)目在100腳及以上的STM32F103芯片都帶有FSMC接口,正點原子戰(zhàn)艦STM32F103的主芯片為STM32F103ZET6,是帶有FSMC接口的。
FSMC接口的結(jié)構(gòu)如圖25.1.3.1所示:
圖25.1.3.1 FSMC框圖
從圖25.1.3.1我們可以看出,STM32的FSMC可以驅(qū)動NOR/PSRAM、NAND、PC卡這3類設(shè)備,他們具有不同的CS以區(qū)分不同的設(shè)備。本部分我們要用到的是NOR/PSRAM的功能。①為FSMC的總線和時鐘源,②為STM32內(nèi)部的FSMC控制單元,③是連接硬件的引腳,這里的“公共信號”表示不論我們驅(qū)動前面提到的3種設(shè)備中的哪種,這些IO是共享的,所以如果需要用到多種功能的情況,程序上還要考慮分時復用。④是NOR/PSRAM會使用到的信號控制線,③和④這些信號比較重要,它們的功能如表25.1.3.1:
表25.1.3.1 FSMC信號線的的功能
在數(shù)電的課程中有介紹過存儲器的知識,它是可以存儲數(shù)據(jù)的器件。復雜的存儲器為了存儲更多的數(shù)據(jù),常常通過地址線來管理數(shù)據(jù)存儲的位置,這樣只要先找到需要讀寫數(shù)據(jù)的位置,然后對進行數(shù)據(jù)讀寫的操作。由于存儲器的這種數(shù)據(jù)和地址對應關(guān)系,采用FSMC這種專門硬件接口就能加快對存儲器的數(shù)據(jù)訪問。
STM32F1的FSMC將外部存儲器劃分為固定大小為256M字節(jié)的四個存儲塊,F(xiàn)SMC的外部設(shè)備地址映像如圖25.1.3.2所示:
圖25.1.3.2 FSMC存儲塊地址映像
從上圖可以看出,F(xiàn)SMC總共管理1GB空間,擁有4個存儲塊(Bank),F(xiàn)SMC各Bank配置寄存器如表25.1.3.2:
表25.1.3.2 FSMC各Bank配置寄存器表
本章,我們用到的是塊1,所以在本章我們僅討論塊1的相關(guān)配置,其他塊的配置,請參考《STM32F10xxx參考手冊_V10(中文版).pdf》第19章(324頁)的相關(guān)介紹。
STM32F1的FSMC存儲塊1(Bank1)被分為4個區(qū),每個區(qū)管理64M字節(jié)空間,每個區(qū)都有獨立的寄存器對所連接的存儲器進行配置。Bank1的256M字節(jié)空間可以通過28根地址線(HADDR[27:0])尋址后訪問。這里HADDR是內(nèi)部AHB地址總線,其中HADDR[25:0]來自外部存儲器地址FSMC_A[25:0],而HADDR[26:27]對4個區(qū)進行尋址。如表25.1.3.3所示:
表25.1.3.3中,我們要特別注意HADDR[25:0]的對應關(guān)系:
當Bank1接的是16位寬度存儲器的時候:HADDR[25:1]→FSMC_A[24:0]。
當Bank1接的是8位寬度存儲器的時候:HADDR[25:0] →FSMC_A[25:0]。
不論外部接8位/16位寬設(shè)備,F(xiàn)SMC_A[0]永遠接在外部設(shè)備地址A[0]。這里,TFTLCD使用的是16位數(shù)據(jù)寬度,所以HADDR[0]并沒有用到,只有HADDR[25:1]是有效的,對應關(guān)系變?yōu)椋篐ADDR[25:1]FSMC_A[24:0],相當于右移了一位,具體來說,比如地址:0x7E,對應二進制是:01111110,此時FSMC_A6是0而不是1,因為要右移一位,這里請?zhí)貏e注意。
另外,HADDR[27:26]的設(shè)置,是不需要我們干預的,比如:當你選擇使用Bank1的第三個區(qū),即使用FSMC_NE3來連接外部設(shè)備的時候,即對應了HADDR[27:26]=10,我們要做的就是配置對應第3區(qū)的寄存器組,來適應外部設(shè)備即可。對于NOR FLASH控制器,主要是通過FSMC_BCRx、FSMC_BTRx和FSMC_BWTRx寄存器設(shè)置(其中x=1~4,對應4個區(qū))。通過這3個寄存器,可以設(shè)置FSMC訪問外部存儲器的時序參數(shù),拓寬了可選用的外部存儲器的速度范圍。FSMC的NORFLASH控制器支持同步和異步突發(fā)兩種訪問方式。選用同步突發(fā)訪問方式時,F(xiàn)SMC將HCLK(系統(tǒng)時鐘)分頻后,發(fā)送給外部存儲器作為同步時鐘信號FSMC_CLK。此時需要的設(shè)置的時間參數(shù)有2個:
1,HCLK與FSMC_CLK的分頻系數(shù)(CLKDIV),可以為2~16分頻;
2,同步突發(fā)訪問中獲得第1個數(shù)據(jù)所需要的等待延遲(DATLAT)。
對于異步突發(fā)訪問方式,F(xiàn)SMC主要設(shè)置3個時間參數(shù):地址建立時間(ADDSET)、數(shù)據(jù)建立時間(DATAST)和地址保持時間(ADDHLD)。FSMC綜合了SRAM/ROM、PSRAM和NOR Flash產(chǎn)品的信號特點,定義了4種不同的異步時序模型。選用不同的時序模型時,需要設(shè)置不同的時序參數(shù),如表25.1.3.4所列:
表25.1.3.4 NOR FLASH控制器支持的時序模型
在實際擴展時,根據(jù)選用存儲器的特征確定時序模型,從而確定各時間參數(shù)與存儲器讀/寫周期參數(shù)指標之間的計算關(guān)系;利用該計算關(guān)系和存儲芯片數(shù)據(jù)手冊中給定的參數(shù)指標,可計算出FSMC所需要的各時間參數(shù),從而對時間參數(shù)寄存器進行合理的配置。
模式A支持獨立的讀寫時序控制。這個對我們驅(qū)動TFTLCD來說非常有用,因為TFTLCD在讀的時候,一般比較慢,而在寫的時候可以比較快,如果讀寫用一樣的時序,那么只能以讀的時序為基準,從而導致寫的速度變慢,或者在讀數(shù)據(jù)的時候,重新配置FSMC的延時,在讀操作完成的時候,再配置回寫的時序,這樣雖然也不會降低寫的速度,但是頻繁配置,比較麻煩。而如果有獨立的讀寫時序控制,那么我們只要初始化的時候配置好,之后就不用再配置,既可以滿足速度要求,又不需要頻繁改配置。模式A的寫操作及讀操作時序分別如圖25.1.3.3和圖25.1.3.4所示:
圖25.1.3.3 模式A寫操作時序
圖25.1.3.4 模式A讀操作時序圖
圖25.1.3.3和圖25.1.3.4中的ADDSET與DATAST,是通過不同的寄存器設(shè)置的。
以圖25.1.3.3所示的寫操作時序為例,該圖表示一個存儲器操作周期由地址建立周期(ADDSET)、數(shù)據(jù)建立周期(DATAST)組成。在地址建立周期中,數(shù)據(jù)建立周期期間NWE信號拉低發(fā)出寫信號,接著FSMC把數(shù)據(jù)通過數(shù)據(jù)線傳輸?shù)酱鎯ζ髦?。注意:NEW拉高后的那1個HCLK是必要的,以保證數(shù)據(jù)線上的信號被準確采樣。
讀操作模式時序類似,區(qū)別是它的一個存儲器操作周期由地址建立周期(ADDSET)和數(shù)據(jù)建立周期(DATAST)以及2個HCLK周期組成,且在數(shù)據(jù)建立周期期間地址線發(fā)出要訪問的地址,數(shù)據(jù)掩碼信號線指示出要讀取地址的高、低字節(jié)部分,片選信號使能存儲器芯片;地址建立周期結(jié)束后讀使能信號線發(fā)出讀使能信號,接著存儲器通過數(shù)據(jù)信號線把目標數(shù)據(jù)傳輸給FSMC,F(xiàn)SMC把它交給內(nèi)核。
當FSMC外設(shè)被配置成正常工作,并且外部接了PSRAM,若向0x60000000地址寫入數(shù)據(jù)如0xABCD,F(xiàn)SMC會自動在各信號線上產(chǎn)生相應的電平信號,寫入數(shù)據(jù)。FSMC會控制片選信號NE1輸出低電平,相應的PSRAM芯片被片選激活,然后使用地址線A[25:0]輸出0x60000000,在NWE寫使能信號線上發(fā)出低電平的寫使能信號,而要寫入的數(shù)據(jù)信號0xABCD則從數(shù)據(jù)線D[15:0]輸出,然后數(shù)據(jù)就被保存到PSRAM中了。
到這里大家發(fā)現(xiàn)沒有,之前講的液晶控制器的8080并口模式與FSMC接口很像,區(qū)別是FSMC通過地址訪問設(shè)備數(shù)據(jù),并且可以自動控制相應電平,而8080方式則是直接控制,且沒有地址線。對比圖25.1.2.2和圖25.1.3.3,不難發(fā)現(xiàn)它們的相似點,我們概括如表25.1.3.5:
表25.1.3.5 FSMC(NOR/PSRAM)方式和8080并口對比
如果能用某種方式把FSMC的地址線和8080方式下的等效起來,那不就可以直接用FSMC等效8080方式操作LCD屏的顯存了?FSMC利用地址線訪問數(shù)據(jù),并自動設(shè)置地址線和相關(guān)控制信號線的電平,如果我們對命令操作和數(shù)據(jù)操作采用不同的地址來訪問,同時使得操作數(shù)據(jù)時,地址線上的一個引腳的電平為高,操作命令時,同一個引腳的電平為低的話,就可以完美解決這個問題了!
戰(zhàn)艦STM32開發(fā)板把TFT-LCD就是用的FSMC_NE4做片選,把RS連接在A10上面的。我們來分析一下要讓實現(xiàn)上面的通過地址自動切換命令和數(shù)據(jù)的實現(xiàn)方式。
首先NOR/PSRAM儲塊地址范圍:0X6000 0000 ~ 0X6FFF FFFF,基地址是0X6000 0000,每個存儲塊是64MB,那么這時候我們訪問LCD的地址應該是第4個存儲塊,編號從1開始,訪問LCD的起始地址就是 0x6000 0000 + (0x400 0000 * (x - 1)) = 0x6C00 0000,即從0x6C00 0000起的64MB內(nèi)存地址都可以去訪問LCD。
FSMC_A10對應地址值:2^10 * 2 = 0x800(16位模式時,參考表25.1.3.3及之后對HADDR和FSMC地址線對應關(guān)系的描述:HADDR[25:1]FSMC_A[24:0],所以這里計算時還需要乘2);則寫命令時的地址為:0x6C00 0000 + 2^10 * 2 = 0x6C00 0800。寫數(shù)據(jù)的地址就是使FSMC_A10為0的其它任意地址。(大家不要被地址訪問的思路帶進去了,以為接下來就是用FSMC的地址偏移來操作顯存了,實際顯存的操作還是歸MCU屏管理。我們使能了FSMC功能后,就可以直接在我們設(shè)置的地址讀寫數(shù)據(jù)。實際上我們只用到了兩個固定的地址:一個地址把FSMC_A10位置1,另一個把該位置0,但要保證這兩個地址在各個BANK的管理范圍內(nèi))。
STM32F1的FSMC支持8/16位數(shù)據(jù)寬度,我們這里用到的LCD是16位寬度的,所以在設(shè)置的時候,選擇16位寬就OK了。向這兩個地址寫的16進制數(shù)據(jù)會被直接送到數(shù)據(jù)線上,根據(jù)地址自動解析為命令或者數(shù)據(jù),通過這樣一個過程,我們就完成了用FSMC模擬8080并口的操作,最終完成對液晶控制器的控制。
25.1.4 FSMC關(guān)聯(lián)寄存器簡介
接下來我們講解一下Bank1的幾個控制寄存器。
首先,我們介紹SRAM/NOR閃存片選控制寄存器:FSMC_BCRx(x=1~4),該寄存器各位描述如圖25.1.4.1所示:
圖25.1.4.1 FSMC_BCRx寄存器各位描述
該寄存器我們在本章用到的設(shè)置有:EXTMOD、WREN、MWID、MTYP和MBKEN這幾個設(shè)置,我們將逐個介紹。
EXTMOD:擴展模式使能位,也就是是否允許讀寫不同的時序,很明顯,我們本章需要讀寫不同的時序,故該位需要設(shè)置為1。
WREN:寫使能位。我們需要向TFTLCD寫數(shù)據(jù),故該位必須設(shè)置為1。
MWID[1:0]:存儲器數(shù)據(jù)總線寬度。我們的TFTLCD是16位數(shù)據(jù)線,所以設(shè)置該值為01。
MTYP[1:0]:存儲器類型。前面提到,我們把TFTLCD當成SRAM用,所以需要設(shè)置該值為00。
MBKEN:存儲塊使能位。這個容易理解,我們需要用到該存儲塊控制TFTLCD,當然要使能這個存儲塊了。
接下來,我們看看SRAM/NOR閃存片選時序寄存器:FSMC_BTRx(x=1~4),該寄存器各位描述如圖25.1.4.2所示:
圖25.1.4.2 FSMC_BTRx寄存器各位描述
這個寄存器包含了每個存儲器塊的控制信息,可以用于SRAM、ROM和NOR閃存存儲器。如果FSMC_BCRx寄存器中設(shè)置了EXTMOD位,則有兩個時序寄存器分別對應讀(本寄存器)和寫操作(FSMC_BWTRx寄存器)。因為我們要求讀寫分開時序控制,所以EXTMOD是使能了的,也就是本寄存器是讀操作時序寄存器,控制讀操作的相關(guān)時序。本章我們要用到的設(shè)置有:ACCMOD、DATAST和ADDSET這三個設(shè)置。
ACCMOD[1:0]:訪問模式。本章我們用到模式A,故設(shè)置為00。
DATAST[7:0]:數(shù)據(jù)保持時間。對ILI9341來說,其實就是RD低電平持續(xù)時間,一般為355ns。而一個HCLK時鐘周期為13.9ns左右(1/72Mhz),為了兼容其他屏,我們這里設(shè)置DATAST為15,也就是16個HCLK周期,時間大約是222ns(未計算數(shù)據(jù)存儲的2個HCLK時間,對9341來說超頻了,但是實際上是可以正常使用的)。
ADDSET[3:0]:地址建立時間。其建立時間為:ADDSET個HCLK周期,最大為15個HCLK周期。對ILI9341來說,這里相當于RD高電平持續(xù)時間,為90ns,本來這里我們應該設(shè)置和DATAST一樣,但是由于STM32F103 FSMC的性能問題,就算設(shè)置ADDSET為0,RD的高電平持續(xù)時間也超過90ns。所有,我們這里可以設(shè)置ADDSET為較小的值,本章我們設(shè)置ADDSET為1,即2個HCLK周期。
最后,我們再來看看SRAM/NOR閃寫時序寄存器:FSMC_BWTRx(x=1~4),該寄存器各位描述如圖25.1.4.3所示:
圖25.1.4.3 FSMC_BWTRx寄存器各位描述
該寄存器在本章用作寫操作時序控制寄存器,需要用到的設(shè)置同樣是:ACCMOD、DATAST和ADDSET這三個設(shè)置。這三個設(shè)置的方法同F(xiàn)SMC_BTRx一模一樣,只是這里對應的是寫操作的時序,ACCMOD設(shè)置同F(xiàn)SMC_BTRx一模一樣,同樣是選擇模式A,另外DATAST和ADDSET則對應低電平和高電平持續(xù)時間,對ILI9341來說,這兩個時間只需要15ns就夠了,比讀操作快得多。所以我們這里設(shè)置DATAST為1,即2個HCLK周期,時間約為28ns。然后ADDSET(也存在性能問題)設(shè)置為0,即1個HCLK周期,實際WR高電平時間就可以滿足。
至此,我們對STM32F1的FSMC介紹就差不多了,通過以上兩個小節(jié)的了解,我們可以開始寫LCD的驅(qū)動代碼了。不過,這里還要給大家做下科普,在MDK的寄存器定義里面,并沒有定義FSMC_BCRx、FSMC_BTRx、FSMC_BWTRx等這個單獨的寄存器,而是將他們進行了一些組合。
FSMC_BCRx和FSMC_BTRx,組合成BTCR[8]寄存器組,他們的對應關(guān)系如下:
BTCR[0]對應FSMC_BCR1,
BTCR[1]對應FSMC_BTR1
BTCR[2]對應FSMC_BCR2
BTCR[3]對應FSMC_BTR2
BTCR[4]對應FSMC_BCR3
BTCR[5]對應FSMC_BTR3
BTCR[6]對應FSMC_BCR4
BTCR[7]對應FSMC_BTR4
FSMC_BWTRx則組合成BWTR[7],他們的對應關(guān)系如下:
BWTR[0]對應FSMC_BWTR1,
BWTR[2]對應FSMC_BWTR2,
BWTR[4]對應FSMC_BWTR3,
BWTR[6]對應FSMC_BWTR4,
BWTR[1]、BWTR[3]和BWTR[5]保留,沒有用到。
通過上面的講解,通過對FSMC相關(guān)的寄存器的描述,大家對FSMC的原理有了一個初步的認識,如果還不熟悉的朋友,請一定要搜索網(wǎng)絡(luò)資料理解FSMC的原理。只有理解了原理,編程時才會得心應手。
25.2 硬件設(shè)計
- 例程功能
使用開發(fā)板的MCU屏接口連接正點原子 TFTLCD模塊(僅限MCU屏模塊),實現(xiàn)TFTLCD模塊的顯示。通過把LCD模塊插入底板上的TFTLCD模塊接口,按下復位之后,就可以看到LCD模塊不停的顯示一些信息并不斷切換底色。同時該實驗會顯示LCD驅(qū)動器的ID,并且會在串口打?。ò磸臀灰淮危蛴∫淮危?。LED0閃爍用于提示程序正在運行。 - 硬件資源
1)LED燈
LED0 – PB5
2)串口1(PA9/PA10連接在板載USB轉(zhuǎn)串口芯片CH340上面)
3)正點原子 2.8/3.5/4.3/7/10寸TFTLCD模塊(僅限MCU屏,16位8080并口驅(qū)動) - 原理圖
TFTLCD模塊的電路見圖25.1.1.2,而開發(fā)板的LCD接口和正點原子 TFTLCD模塊直接可以對插,開發(fā)板上的LCD接口如圖25.2.1所示:
圖25.2.1 TFTLCD模塊與開發(fā)板對接的LCD接口示意圖
圖25.2.2 TFTLCD模塊與開發(fā)板的連接原理圖
在硬件上,TFTLCD模塊與開發(fā)板的IO口對應關(guān)系如下:
LCD_BL(背光控制)對應PB0;
LCD_CS對應PG12即FSMC_NE4;
LCD_RS對應PG0即FSMC_A10;
LCD_WR對應PD5即FSMC_NWE;
LCD_RD對應PD4即FSMC_NOE;
LCD _D[15:0]則直接連接在FSMC_D15~FSMC_D0;
這些線的連接,開發(fā)板的內(nèi)部已經(jīng)連接好了,我們只需要將TFTLCD模塊插上去就好了。
需要說明的是,開發(fā)板上設(shè)計的TFT-LCD模塊插座,已經(jīng)把模塊模塊的RST信號線直接接到我們開發(fā)板的復位腳上,所以不需要軟件控制,這樣可以省下來一個IO口。另外我們還需要一個背光控制線來控制LCD的背光燈,因為LCD不會自發(fā)光,沒有背光燈的情況下我們是看不到LCD上顯示的內(nèi)容的。所以,我們總共需要的IO口數(shù)目為22個。
25.3 程序設(shè)計
25.3.1 FSMC和SRAM的HAL庫驅(qū)動
SRAM和FMC在HAL庫中的驅(qū)動代碼在stm32f1xx_ll_fsmc.c/stm32f1xx_hal_sram.c以及stm32f1xx_ll_fsmc.h/ stm32f1xx_hal_sram.h中。
- HAL_SRAM_Init函數(shù)
SRAM的初始化函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_SRAM_Init(SRAM_HandleTypeDef *hsram,
FSMC_NORSRAM_TimingTypeDef *Timing, FSMC_NORSRAM_TimingTypeDef *ExtTiming);
?函數(shù)描述:
用于初始化 SRAM,注意這個函數(shù)不限制一定是SRAM,只要時序類似,均可使用。前面說過,這里我們把LCD當作SRAM使用,因為他們時序類似。
?函數(shù)形參:
形參1是SRAM_HandleTypeDef結(jié)構(gòu)體類型指針變量,其定義如下:
typedef struct
{
FSMC_NORSRAM_TypeDef Instance; / 寄存器基地址 */
FSMC_NORSRAM_EXTENDED_TypeDef Extended; / 擴展模式寄存器基地址 /
FSMC_NORSRAM_InitTypeDef Init; / SRAM初始化結(jié)構(gòu)體 /
HAL_LockTypeDef Lock; / SRAM鎖對象結(jié)構(gòu)體 /
__IO HAL_SRAM_StateTypeDef State; / SRAM設(shè)備訪問狀態(tài) */
DMA_HandleTypeDef hdma; / DMA結(jié)構(gòu)體 */
} SRAM_HandleTypeDef;
1)Instance:指向FSMC寄存器基地址。我們直接寫FSMC_NORSRAM_DEVICE即可,因為HAL庫定義好了宏定義FSMC_NORSRAM_DEVICE,也就是如果是SRAM設(shè)備,直接填寫這個宏定義標識符即可。
2)Extended:指向FSMC擴展模式寄存器基地址,因為我們要配置的讀寫時序是不一樣的。前面講的FSMC_BCRx寄存器的EXTMOD位,我們會配置為1允許讀寫不同的時序,所以還要指定寫操作時序寄存器地址,也就是通過參數(shù)Extended來指定的,這里設(shè)置為FSMC_NORSRAM_EXTENDED_DEVICE。
3)Init:用于對FSMC的初始化配置,這個比較重要,后面再來講解。
4)Lock:用于配置鎖狀態(tài)。
5)State:SRAM設(shè)備訪問狀態(tài)。
6)hdma:在使用DMA時候才使用,這里就先不講解了。
成員變量Init是FSMC_NORSRAM_InitTypeDef結(jié)構(gòu)體指針類型,該變量才是真正用來設(shè)置SRAM控制接口參數(shù)的。下面詳細了解這個結(jié)構(gòu)體定義:
typedef struct
{
uint32_t NSBank; /* 存儲區(qū)塊號 */
uint32_t DataAddressMux; /* 地址/數(shù)據(jù)復用使能 */
uint32_t MemoryType; /* 存儲器類型 */
uint32_t MemoryDataWidth; /* 存儲器數(shù)據(jù)寬度 */
uint32_t BurstAccessMode; /* 突發(fā)模式配置 */
uint32_t WaitSignalPolarity; /* 設(shè)置等待信號的極性 */
uint32_t WrapMode; /* 突發(fā)下存儲器傳輸使能*/
uint32_t WaitSignalActive; /* 等待狀態(tài)之前或等待狀態(tài)期間 */
uint32_t WriteOperation; /* 存儲器寫使能 */
uint32_t WaitSignal; /* 使能或者禁止通過等待信號來插入等待狀態(tài) */
uint32_t ExtendedMode; /* 使能或者禁止使能擴展模式 */
uint32_t AsynchronousWait; /* 用于異步傳輸期間,使能或者禁止等待信號 */
uint32_t WriteBurst; /* 用于使能或者禁止異步的寫突發(fā)操作 */
uint32_t PageSize; /* 設(shè)置頁大小 */
}FSMC_NORSRAM_InitTypeDef;
NSBank用來指定使用到的存儲塊區(qū)號,我們硬件設(shè)計時使用的存儲塊區(qū)號4,所以選擇值為FSMC_NORSRAM_BANK4。
DataAddressMux用來設(shè)置是否使能地址/數(shù)據(jù)復用,該變量僅對NOR/PSRAM有效,所以這里我們選擇不使能地址/數(shù)據(jù)復用值FSMC_DATA_ADDRESS_MUX_DISABLE 即可。
MemoryType用來設(shè)置存儲器類型,這里我們把LCD當SRAM使用,所以設(shè)置為FSMC_MEMORY_TYPE_SRAM 即可。
MemoryDataWidth用來設(shè)置存儲器數(shù)據(jù)總線寬度,可選8位還是16位,這里我們選擇16位數(shù)據(jù)寬度FSMC_NORSRAM_MEM_BUS_WIDTH_16。
WriteOperation用來設(shè)置存儲器寫使能,也就是是否允許寫入。毫無疑問我們會進行存儲器寫操作,所以這里設(shè)置為FSMC_WRITE_OPERATION_ENABLE。
ExtendedMode用來設(shè)置是否使能擴展模式,也就是是否允許讀寫使用不同時序,前面講解過本實驗讀寫采用不同時序,所以設(shè)置值為使能值FSMC_EXTENDED_MODE_ENABLE。
其他參數(shù)WriteBurst,BurstAccessMode,WaitSignalPolarity,WaitSignalActive,WaitSignal,AsynchronousWait等是用在突發(fā)訪問和異步時序情況下,這里我們不做過多講解。
形參2 Timing和形參3 ExtTiming都是FSMC_NORSRAM_TimingTypeDef結(jié)構(gòu)體類型指針變量,其定義如下:
typedef struct
{
uint32_t AddressSetupTime; /* 地址建立時間 */
uint32_t AddressHoldTime; /* 地址保持時間 */
uint32_t DataSetupTime; /* 數(shù)據(jù)建立時間 */
uint32_t BusTurnAroundDuration; /* 總線周轉(zhuǎn)階段的持續(xù)時間 */
uint32_t CLKDivision; /* CLK時鐘輸出信號的周期 */
uint32_t DataLatency; /* 同步突發(fā)NOR FLASH的數(shù)據(jù)延遲 */
uint32_t AccessMode; /* 異步模式配置 */
}FSMC_NORSRAM_TimingTypeDef;
對于本實驗,讀速度比寫速度慢得多,因此讀寫時序不一樣,所以對于Timing和ExtTiming要設(shè)置了不同的值,其中Timing設(shè)置寫時序參數(shù),ExtTiming設(shè)置讀時序參數(shù)。
下面解析一下結(jié)構(gòu)體的成員變量:
AddressSetupTime用來設(shè)置地址建立時間,可以理解為RD/WR的高電平時間。
AddressHoldTime用來設(shè)置地址保持時間,模式A并沒有用到。
DataSetupTime用來設(shè)置數(shù)據(jù)建立時間,可以理解為RD/WR的低電平時間。
BusTurnAroundDuration用來配置總線周轉(zhuǎn)階段的持續(xù)時間,NOR FLASH用到。
CLKDivision用來配置CLK時鐘輸出信號的周期,以HCLK周期數(shù)表示。若控制異步存儲器,該參數(shù)無效。
DataLatency用來設(shè)置同步突發(fā)NOR FLASH的數(shù)據(jù)延遲。若控制異步存儲器,該參數(shù)無效。
AccessMode用來設(shè)置異步模式,HAL庫允許其取值范圍為FSMC_ACCESS_MODE_A、FSMC_ACCESS_MODE_B、FSMC_ACCESS_MODE_C和FSMC_ACCESS_MODE_D,這里我們用是異步模式A,所以取值為FSMC_ACCESS_MODE_A。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。
?注意事項:
和其他外設(shè)一樣,HAL庫也提供了SRAM的初始化MSP回調(diào)函數(shù),函數(shù)聲明如下:
void HAL_SRAM_MspInit(SRAM_HandleTypeDef *hsram) ;
2. FSMC_NORSRAM_Extended_Timing_Init函數(shù)
FSMC_NORSRAM_Extended_Timing_Init函數(shù)是初始化擴展時序模式函數(shù)。其聲明如下:
HAL_StatusTypeDef FSMC_NORSRAM_Extended_Timing_Init(
FSMC_NORSRAM_EXTENDED_TypeDef *Device, FSMC_NORSRAM_TimingTypeDef *Timing,
uint32_t Bank, uint32_t ExtendedMode);
?函數(shù)描述:
該函數(shù)用于初始化擴展時序模式。
?函數(shù)形參:
形參1是FSMC_NORSRAM_EXTENDED_TypeDef結(jié)構(gòu)體類型指針變量,擴展模式寄存器基地址選擇。
形參2是FSMC_NORSRAM_TimingTypeDef結(jié)構(gòu)體類型指針變量,可以是讀或者寫時序結(jié)構(gòu)體。
形參3是儲存區(qū)塊號。
形參4是使能或者禁止擴展模式。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。
?注意事項:
該函數(shù)我們用于重新配置寫或者讀時序。
FSMC驅(qū)動LCD顯示配置步驟
1)使能FSMC和相關(guān)GPIO時鐘,并設(shè)置好GPIO工作模式
我們通過FSMC控制LCD,所以先需要使能FSMC以及相關(guān)GPIO口的時鐘,并設(shè)置好GPIO的工作模式。
2)設(shè)置FSMC參數(shù)
這里我們需要設(shè)置FSMC的相關(guān)訪問參數(shù)(數(shù)據(jù)位寬、訪問時序、工作模式等),以匹配液晶驅(qū)動IC,這里我們通過HAL_SRAM_Init函數(shù)完成FSMC參數(shù)配置,詳見本例程源碼。
3)初始化LCD
由于我們例程兼容了很多種液晶驅(qū)動IC,所以先要讀取對應IC的驅(qū)動型號,然后根據(jù)不同的IC型號來調(diào)用不同的初始化函數(shù),完成對LCD的初始化。
注意:這些初始化函數(shù)里面的代碼,都是由LCD廠家提供,一般不需要改動,也不需要深究,我們直接照抄即可。
4)實現(xiàn)LCD畫點&讀點函數(shù)
在初始化LCD完成以后,我們就可以控制LCD顯示了,而最核心的一個函數(shù),就是畫點和讀點函數(shù),只要實現(xiàn)這兩個函數(shù),后續(xù)的各種LCD操作函數(shù),都可以基于這兩個函數(shù)實現(xiàn)。
5)實現(xiàn)其他LCD操作函數(shù)
在完成畫點和讀點兩個最基礎(chǔ)的LCD操作函數(shù)以后,我們就可以基于這兩個函數(shù)實現(xiàn)各種LCD操作函數(shù)了,比如畫線、畫矩形、顯示字符、顯示字符串、顯示數(shù)字等,如果不夠用還可以根據(jù)自己需要來添加。詳見本例程源碼。
25.3.2 程序流程圖
圖25.3.2.1 TFTLCD(MCU屏)實驗程序流程圖
25.3.2 程序解析
- LCD驅(qū)動代碼
這里我們只講解核心代碼,詳細的源碼請大家參考光盤本實驗對應源碼。液晶(LCD)驅(qū)動源碼包括四個文件:lcd.c、lcd.h、lcd_ex.c和lcdfont.h。
lcd.c和lcd.h文件是驅(qū)動函數(shù)和引腳接口宏定義以及函數(shù)聲明等。lcd_ex.c存放各個LCD驅(qū)動IC的寄存器初始化部分代碼,是lcd.c文件的補充文件,起到簡化lcd.c文件的作用。lcdfont.h頭文件存放了4種字體大小不一樣的ASCII字符集(1212、1616、2424和3232)。這個跟oledfont.h頭文件一樣的,只是這里多了3232的ASCII字符集,制作方法請回顧OLED實驗。
下面我們還是先介紹lcd.h文件,首先是LCD的引腳定義:
/ LCD RST/WR/RD/BL/CS/RS 引腳 定義
- LCD_D0~D15,由于引腳太多,就不在這里定義了,直接在lcd_init里面修改.所以在移植的時候,除了
- 改這6個IO口, 還得改LCD_Init里面的D0~D15所在的IO口.
*/
/* RESET 和系統(tǒng)復位腳共用 所以這里不用定義 RESET引腳 */
#define LCD_WR_GPIO_PORT GPIOD
#define LCD_WR_GPIO_PIN GPIO_PIN_5
#define LCD_WR_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE();}while(0)
#define LCD_RD_GPIO_PORT GPIOD
#define LCD_RD_GPIO_PIN GPIO_PIN_4
#define LCD_RD_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE();}while(0)
#define LCD_BL_GPIO_PORT GPIOB
#define LCD_BL_GPIO_PIN GPIO_PIN_0
#define LCD_BL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE();}while(0)
/* LCD_CS(需要根據(jù)LCD_FSMC_NEX設(shè)置正確的IO口) 和 LCD_RS(需要根據(jù)LCD_FSMC_AX設(shè)置正確的IO口) 引腳 定義 */
#define LCD_CS_GPIO_PORT GPIOG
#define LCD_CS_GPIO_PIN GPIO_PIN_12
#define LCD_CS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE();}while(0)
#define LCD_RS_GPIO_PORT GPIOG
#define LCD_RS_GPIO_PIN GPIO_PIN_0
#define LCD_RS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE();}while(0)
第一部分的宏定義是LCD WR/RD/BL/CS/RS/DATA 引腳定義,需要注意的是:LCD的RST引腳和系統(tǒng)復位腳連接在一起,所以不用單獨使用一個IO口(節(jié)省一個IO口)。而DATA引腳直接用的是FSMC_D[x]引腳,具體可以查看前面的描述。
下面介紹我們在lcd.h里面定義的一個重要的結(jié)構(gòu)體:
/* LCD重要參數(shù)集 */
typedef struct
{
uint16_t width; /* LCD 寬度 */
uint16_t height; /* LCD 高度 */
uint16_t id; /* LCD ID */
uint8_t dir; /* 橫屏還是豎屏控制:0,豎屏;1,橫屏。 */
uint16_t wramcmd; /* 開始寫gram指令 */
uint16_t setxcmd; /* 設(shè)置x坐標指令 */
uint16_t setycmd; /* 設(shè)置y坐標指令 */
} _lcd_dev;
extern _lcd_dev lcddev; /* 管理LCD重要參數(shù) /
/ LCD的畫筆顏色和背景色 /
extern uint32_t g_point_color; / 默認紅色 /
extern uint32_t g_back_color; / 背景顏色.默認為白色 */
該結(jié)構(gòu)體用于保存一些LCD重要參數(shù)信息,比如LCD的長寬、LCD ID(驅(qū)動IC型號)、LCD橫豎屏狀態(tài)等,這個結(jié)構(gòu)體雖然占用了十幾個字節(jié)的內(nèi)存,但是卻可以讓我們的驅(qū)動函數(shù)支持不同尺寸的LCD,同時可以實現(xiàn)LCD橫豎屏切換等重要功能,所以還是利大于弊的。最后聲明_lcd_dev結(jié)構(gòu)體類型變量lcddev,lcddev在lcd.c中定義。
緊接著就是g_point_color和g_back_color變量的聲明,它們也是在lcd.c中被定義。g_point_color變量用于保存LCD的畫筆顏色,g_back_color則是保存LCD的背景色。
下面是LCD背光控制IO口的宏定義:
/* LCD背光控制 */
#define LCD_BL(x) do{ x ? \
HAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_RESET); \
}while(0)
本實驗,我們用到FSMC驅(qū)動LCD,通過前面的介紹,我們知道TFTLCD的RS接在FSMC的A10上面,CS接在FSMC_NE4上,并且是16位數(shù)據(jù)總線。即我們使用的是FSMC存儲器1的第4區(qū),我們定義如下LCD操作結(jié)構(gòu)體(在lcd.h里面定義):
/* LCD地址結(jié)構(gòu)體 */
typedef struct
{
volatile uint16_t LCD_REG;
volatile uint16_t LCD_RAM;
} LCD_TypeDef;
/* LCD_BASE的詳細解算方法:
* 我們一般使用FSMC的塊1(BANK1)來驅(qū)動TFTLCD液晶屏(MCU屏), 塊1地址范圍總大小為256MB,均分成4塊:
* 存儲塊1(FSMC_NE1)地址范圍: 0X6000 0000 ~ 0X63FF FFFF
* 存儲塊2(FSMC_NE2)地址范圍: 0X6400 0000 ~ 0X67FF FFFF
* 存儲塊3(FSMC_NE3)地址范圍: 0X6800 0000 ~ 0X6BFF FFFF
* 存儲塊4(FSMC_NE4)地址范圍: 0X6C00 0000 ~ 0X6FFF FFFF
*
* 我們需要根據(jù)硬件連接方式選擇合適的片選(連接LCD_CS)和地址線(連接LCD_RS)
* 戰(zhàn)艦F103開發(fā)板使用FSMC_NE4連接LCD_CS,FSMC_A10連接LCD_RS,16位數(shù)據(jù)線,計算方法如下:
* 首先FSMC_NE4的基地址為: 0X6C00 0000; NEx的基址為(x=1/2/3/4): 0X6000 0000 + (0X400 0000 * (x - 1))
* FSMC_A10對應地址值: 2^10 * 2 = 0X800; FSMC_Ay對應的地址為(y = 0 ~ 25): 2^y * 2
*
* LCD->LCD_REG,對應LCD_RS = 0(LCD寄存器); LCD->LCD_RAM,對應LCD_RS = 1(LCD數(shù)據(jù))
* 則 LCD->LCD_RAM的地址為: 0X6C00 0000 + 2^10 * 2 = 0X6C00 0800
* LCD->LCD_REG的地址可以為 LCD->LCD_RAM之外的任意地址.
* 由于我們使用結(jié)構(gòu)體管理LCD_REG 和 LCD_RAM(REG在前,RAM在后,均為16位數(shù)據(jù)寬度)
* 因此 結(jié)構(gòu)體的基地址(LCD_BASE) = LCD_RAM - 2 = 0X6C00 0800 -2
*
* 更加通用的計算公式為((片選腳FSMC_NEx)x=1/2/3/4, (RS接地址線FSMC_Ay)y=0~25):
* LCD_BASE = (0X6000 0000 + (0X400 0000 * (x - 1))) | (2^y * 2 -2)
* 等效于(使用移位操作)
* LCD_BASE = (0X6000 0000 + (0X400 0000 * (x - 1))) | ((1 << y) * 2 -2)
*/
#define LCD_BASE (uint32_t)((0X60000000 + (0X4000000 * (LCD_FSMC_NEX - 1))) |
(((1 << LCD_FSMC_AX) * 2) -2))
#define LCD ((LCD_TypeDef *) LCD_BASE)
其中LCD_BASE,必須根據(jù)我們外部電路的連接來確定,我們使用BANK1的存儲塊4的尋址范圍為0X6C000000~6FFFFFFF,我們需要在這個地址范圍內(nèi)找到兩個地址,實現(xiàn)對RS位(FSMC_A10位)的0和1的控制。這兩個地址的取值方法,我們在前面的25.1.3的末尾已經(jīng)詳細說明了。為了方便控制和節(jié)省內(nèi)存,我們使這兩個地址變成相鄰的兩個16進制指針,這樣就可以用前面定義的LCD_TypeDef來管理這兩個地址了。
根據(jù)我們的算法和定義,我們將這個地址強制轉(zhuǎn)換為LCD_TypeDef結(jié)構(gòu)體地址,那么可以得到LCD->LCD_REG的地址就是0X6C00 07FE,對應A10的狀態(tài)為0(即RS=0),而LCD->LCD_RAM的地址就是0X6C00 0800(結(jié)構(gòu)體地址自增),對應A10的狀態(tài)為1(即RS=1)。
所以,有了這個定義,當我們要往LCD寫命令/數(shù)據(jù)的時候,可以這樣寫:
LCD->LCD_REG = CMD; /* 寫命令 */
LCD->LCD_RAM = DATA; /* 寫數(shù)據(jù) */
而讀的時候反過來操作就可以了,如下所示:
CMD = LCD->LCD_REG; /* 讀LCD寄存器 */
DATA = LCD->LCD_RAM; /* 讀LCD數(shù)據(jù) */
這其中,CS、WR、RD和IO口方向都是由FSMC硬件自動控制,不需要我們手動設(shè)置了。
最后是一些其他的宏定義,包括LCD掃描方向和顏色,以及SSD1963相關(guān)配置參數(shù)。
下面開始對lcd.c文件介紹,先看LCD初始化函數(shù),其定義如下:
/**
* @brief 初始化LCD
* @note 該初始化函數(shù)可以初始化各種型號的LCD(詳見本.c文件最前面的描述)
*
* @param 無
* @retval 無
*/
void lcd_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
FSMC_NORSRAM_TimingTypeDef fsmc_read_handle;
FSMC_NORSRAM_TimingTypeDef fsmc_write_handle;
LCD_CS_GPIO_CLK_ENABLE(); /* LCD_CS腳時鐘使能 */
LCD_WR_GPIO_CLK_ENABLE(); /* LCD_WR腳時鐘使能 */
LCD_RD_GPIO_CLK_ENABLE(); /* LCD_RD腳時鐘使能 */
LCD_RS_GPIO_CLK_ENABLE(); /* LCD_RS腳時鐘使能 */
LCD_BL_GPIO_CLK_ENABLE(); /* LCD_BL腳時鐘使能 */
gpio_init_struct.Pin = LCD_CS_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 推挽復用 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(LCD_CS_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_CS引腳 */
gpio_init_struct.Pin = LCD_WR_GPIO_PIN;
HAL_GPIO_Init(LCD_WR_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_WR引腳 */
gpio_init_struct.Pin = LCD_RD_GPIO_PIN;
HAL_GPIO_Init(LCD_RD_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_RD引腳 */
gpio_init_struct.Pin = LCD_RS_GPIO_PIN;
HAL_GPIO_Init(LCD_RS_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_RS引腳 */
gpio_init_struct.Pin = LCD_BL_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(LCD_BL_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_BL引腳 */
g_sram_handle.Instance = FSMC_NORSRAM_DEVICE;
g_sram_handle.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
g_sram_handle.Init.NSBank = FSMC_NORSRAM_BANK4; /* 使用NE4 */
/*地址/數(shù)據(jù)線不復用*/
g_sram_handle.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
/*16位數(shù)據(jù)寬度*/
g_sram_handle.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
/*是否使能突發(fā)訪問,僅對同步突發(fā)存儲器有效,此處未用到*/
g_sram_handle.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
/*等待信號的極性,僅在突發(fā)模式訪問下有用*/
g_sram_handle.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
/* 存儲器是在等待周期之前的一個時鐘周期還是等待周期期間使能NWAIT */
g_sram_handle.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
/* 存儲器寫使能 */
g_sram_handle.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
/* 等待使能位,此處未用到 */
g_sram_handle.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
/* 讀寫使用不同的時序 */
g_sram_handle.Init.ExtendedMode = FSMC_EXTENDED_MODE_ENABLE;
/* 是否使能同步傳輸模式下的等待信號,此處未用到 */
g_sram_handle.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
g_sram_handle.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; /* 禁止突發(fā)寫 */
/*FSMC讀時序控制寄存器*/
/* 地址建立時間(ADDSET)為1個HCLK 1/72M=13.9ns */
fsmc_read_handle.AddressSetupTime = 0;
fsmc_read_handle.AddressHoldTime = 0;
/* 數(shù)據(jù)保存時間(DATAST)為16個HCLK 13.9*16=222.4ns */
fsmc_read_handle.DataSetupTime = 15; /* 部分液晶驅(qū)動IC讀數(shù)據(jù)時,速度不能太快 */
fsmc_read_handle.AccessMode = FSMC_ACCESS_MODE_A; /* 模式A */
/*FSMC寫時序控制寄存器*/
/* 地址建立時間(ADDSET)為1個HCLK 13.9ns */
fsmc_write_handle.AddressSetupTime = 0;
fsmc_write_handle.AddressHoldTime = 0;
/* 數(shù)據(jù)保存時間(DATAST)為2個HCLK 13.9*2= 27.8ns */
fsmc_write_handle.DataSetupTime = 1;
fsmc_write_handle.AccessMode = FSMC_ACCESS_MODE_A; /* 模式A */
HAL_SRAM_Init(&g_sram_handle, &fsmc_read_handle, &fsmc_write_handle);
delay_ms(50);
/* 嘗試9341 ID的讀取 */
lcd_wr_regno(0XD3);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 讀到0X00 */
lcddev.id = lcd_rd_data(); /* 讀取93 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 讀取41 */
if (lcddev.id != 0X9341) /* 不是 9341 , 嘗試看看是不是 ST7789 */
{
lcd_wr_regno(0X04);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 讀到0X85 */
lcddev.id = lcd_rd_data(); /* 讀取0X85 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 讀取0X52 */
if (lcddev.id == 0X8552) /* 將8552的ID轉(zhuǎn)換成7789 */
{
lcddev.id = 0x7789;
}
if (lcddev.id != 0x7789) /* 也不是ST7789, 嘗試是不是 NT35310 */
{
lcd_wr_regno(0XD4);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 讀回0X01 */
lcddev.id = lcd_rd_data(); /* 讀回0X53 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 這里讀回0X10 */
if (lcddev.id != 0X5310) /* 也不是NT35310,嘗試看看是不是NT35510 */
{
/* 發(fā)送秘鑰(廠家提供,照搬即可) */
lcd_write_reg(0xF000, 0x0055);
lcd_write_reg(0xF001, 0x00AA);
lcd_write_reg(0xF002, 0x0052);
lcd_write_reg(0xF003, 0x0008);
lcd_write_reg(0xF004, 0x0001);
lcd_wr_regno(0xC500); /* 讀取ID高8位 */
lcddev.id = lcd_rd_data(); /* 讀回0X55 */
lcddev.id <<= 8;
lcd_wr_regno(0xC501); /* 讀取ID低8位 */
lcddev.id |= lcd_rd_data(); /* 讀回0X10 */
delay_ms(5);
if (lcddev.id != 0X5510) /* 也不是NT5510,嘗試看看是不是SSD1963 */
{
lcd_wr_regno(0XA1);
lcddev.id = lcd_rd_data();
lcddev.id = lcd_rd_data(); /* 讀回0X57 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 讀回0X61 */
/* SSD1963讀回的ID是5761H,為方便區(qū)分,我們強制設(shè)置為1963 */
if (lcddev.id == 0X5761)lcddev.id = 0X1963;
}
}
}
}
/* 特別注意, 如果在main函數(shù)里面屏蔽串口1初始化, 則會卡死在printf
* 里面(卡死在f_putc函數(shù)), 所以, 必須初始化串口1, 或者屏蔽掉下面
* 這行 printf 語句 !!!!!!!
*/
printf("LCD ID:%x\r\n", lcddev.id); /* 打印LCD ID */
if (lcddev.id == 0X7789)
{
lcd_ex_st7789_reginit(); /* 執(zhí)行ST7789初始化 */
}
else if (lcddev.id == 0X9341)
{
lcd_ex_ili9341_reginit(); /* 執(zhí)行ILI9341初始化 */
}
else if (lcddev.id == 0x5310)
{
lcd_ex_nt35310_reginit(); /* 執(zhí)行NT35310初始化 */
}
else if (lcddev.id == 0x5510)
{
lcd_ex_nt35510_reginit(); /* 執(zhí)行NT35510初始化 */
}
else if (lcddev.id == 0X1963)
{
lcd_ex_ssd1963_reginit(); /* 執(zhí)行SSD1963初始化 */
lcd_ssd_backlight_set(100); /* 背光設(shè)置為最亮 */
}
lcd_display_dir(0); /* 默認為豎屏 */
LCD_BL(1); /* 點亮背光 */
lcd_clear(WHITE);
}
該函數(shù)先對FSMC相關(guān)IO進行初始化,然后使用HAL_SRAM_Init函數(shù)初始化FSMC控制器,同時我們使用HAL_SRAM_MspInit回調(diào)函數(shù)來初始化相應的IO口,最后讀取LCD控制器的型號,根據(jù)控制IC的型號執(zhí)行不同的初始化代碼,這樣提高了整個程序的通用性。為了簡化lcd.c的初始化程序,不同控制IC的芯片對應的初始化程序(如:lcd_ex_st7789_reginit()、lcd_ex_ili9341_reginit()等)我們放在lcd_ex.c文件中,這些初始化代碼完成對LCD寄存器的初始化,由LCD廠家提供,一般是不需要做任何修改的,我們直接調(diào)用就可以了。
下面是6個簡單,但是很重要的函數(shù):
/**
* @brief LCD寫數(shù)據(jù)
* @param data: 要寫入的數(shù)據(jù)
* @retval 無
*/
void lcd_wr_data(volatile uint16_t data)
{
data = data; /* 使用-O2優(yōu)化的時候,必須插入的延時 */
LCD->LCD_RAM = data;
}
/**
* @brief LCD寫寄存器編號/地址函數(shù)
* @param regno: 寄存器編號/地址
* @retval 無
*/
void lcd_wr_regno(volatile uint16_t regno)
{
regno = regno; /* 使用-O2優(yōu)化的時候,必須插入的延時 */
LCD->LCD_REG = regno; /* 寫入要寫的寄存器序號 */
}
/**
* @brief LCD寫寄存器
* @param regno:寄存器編號/地址
* @param data:要寫入的數(shù)據(jù)
* @retval 無
*/
void lcd_write_reg(uint16_t regno, uint16_t data)
{
LCD->LCD_REG = regno; /* 寫入要寫的寄存器序號 */
LCD->LCD_RAM = data; /* 寫入數(shù)據(jù) */
}
/**
* @brief LCD延時函數(shù),僅用于部分在mdk -O1時間優(yōu)化時需要設(shè)置的地方
* @param t:延時的數(shù)值
* @retval 無
*/
static void lcd_opt_delay(uint32_t i)
{
while (i--); /*使用AC6時空循環(huán)可能被優(yōu)化,可使用while(1) __asm volatile(""); */
}
/**
* @brief LCD讀數(shù)據(jù)
* @param 無
* @retval 讀取到的數(shù)據(jù)
*/
static uint16_t lcd_rd_data(void)
{
volatile uint16_t ram; /* 防止被優(yōu)化 */
lcd_opt_delay(2);
ram = LCD->LCD_RAM;
return ram;
}
/**
* @brief 準備寫GRAM
* @param 無
* @retval 無
*/
void lcd_write_ram_prepare(void)
{
LCD->LCD_REG = lcddev.wramcmd;
}
因為FSMC自動控制了WR/RD/CS等這些信號,所以這6個函數(shù)實現(xiàn)起來都非常簡單,我們就不多說,注意,上面有幾個函數(shù),我們添加了一些對MDK –O2優(yōu)化的支持,去掉的話,在-O2優(yōu)化的時候會出問題。這些函數(shù)實現(xiàn)功能見函數(shù)前面的備注,通過這幾個簡單函數(shù)的組合,我們就可以對LCD進行各種操作了。
下面要介紹的函數(shù)是坐標設(shè)置函數(shù),該函數(shù)代碼如下:
/**
* @brief 設(shè)置光標位置(對RGB屏無效)
* @param x,y: 坐標
* @retval 無
*/
void lcd_set_cursor(uint16_t x, uint16_t y)
{
if (lcddev.id == 0X1963)
{
if (lcddev.dir == 0) /* 豎屏模式, x坐標需要變換 */
{
x = lcddev.width - 1 - x;
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(0);
lcd_wr_data(0);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0XFF);
}
else /* 橫屏模式 */
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0XFF);
lcd_wr_data((lcddev.width - 1) >> 8);
lcd_wr_data((lcddev.width - 1) & 0XFF);
}
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_data(y & 0XFF);
lcd_wr_data((lcddev.height - 1) >> 8);
lcd_wr_data((lcddev.height - 1) & 0XFF);
}
else if (lcddev.id == 0X5510)
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_regno(lcddev.setxcmd + 1);
lcd_wr_data(x & 0XFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_regno(lcddev.setycmd + 1);
lcd_wr_data(y & 0XFF);
}
else /* 9341/5310/7789 等 設(shè)置坐標 */
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0XFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_data(y & 0XFF);
}
}
該函數(shù)實現(xiàn)將LCD的當前操作點設(shè)置到指定坐標(x,y)。因為9341/5310/1963/5510等的設(shè)置有些不太一樣,所以進行了區(qū)別對待。
接下來介紹畫點函數(shù),其定義如下:
/**
* @brief 畫點
* @param x,y: 坐標
* @param color: 點的顏色(32位顏色,方便兼容LTDC)
* @retval 無
*/
void lcd_draw_point(uint16_t x, uint16_t y, uint32_t color)
{
lcd_set_cursor(x, y); /* 設(shè)置光標位置 */
lcd_write_ram_prepare(); /* 開始寫入GRAM */
LCD->LCD_RAM = color;
}
該函數(shù)實現(xiàn)比較簡單,就是先設(shè)置坐標,然后往坐標寫顏色。lcd_draw_point函數(shù)雖然簡單,但是至關(guān)重要,其他幾乎所有上層函數(shù),都是通過調(diào)用這個函數(shù)實現(xiàn)的。
下面介紹讀點函數(shù),用于讀取LCD的GRAM,這里說明一下,為什么OLED模塊沒做讀GRAM的函數(shù),而這里做了。因為OLED模塊是單色的,所需要全部GRAM也就1K個字節(jié),而TFTLCD模塊為彩色的,點數(shù)也比OLED模塊多很多,以16位色計算,一款320×240的液晶,需要320×240×2個字節(jié)來存儲顏色值,也就是也需要150K字節(jié),這對任何一款單片機來說,都不是一個小數(shù)目了。而且我們在圖形疊加的時候,可以先讀回原來的值,然后寫入新的值,在完成疊加后,我們又恢復原來的值。這樣在做一些簡單菜單的時候,是很有用的。這里我們讀取TFTLCD模塊數(shù)據(jù)的函數(shù)為LCD_ReadPoint,該函數(shù)直接返回讀到的GRAM值。該函數(shù)使用之前要先設(shè)置讀取的GRAM地址,通過lcd_set_cursor函數(shù)來實現(xiàn)。lcd_read_point的代碼如下:
/**
* @brief 讀取個某點的顏色值
* @param x,y:坐標
* @retval 此點的顏色(32位顏色,方便兼容LTDC)
*/
uint32_t lcd_read_point(uint16_t x, uint16_t y)
{
uint16_t r = 0, g = 0, b = 0;
if (x >= lcddev.width || y >= lcddev.height)return 0; /* 超過了范圍,直接返回 */
lcd_set_cursor(x, y); /* 設(shè)置坐標 */
if (lcddev.id == 0X5510)
{
lcd_wr_regno(0X2E00); /* 5510 發(fā)送讀GRAM指令 */
}
else
{
lcd_wr_regno(0X2E); /* 9341/5310/1963/7789 等發(fā)送讀GRAM指令 */
}
r = lcd_rd_data(); /* 假讀(dummy read) */
if (lcddev.id == 0X1963)return r; /* 1963 直接讀就可以 */
r = lcd_rd_data(); /* 實際坐標顏色 */
/* 9341/5310/5510/7789 要分2次讀出 */
b = lcd_rd_data();
/* 對于 9341/5310/5510/7789, 第一次讀取的是RG的值,R在前,G在后,各占8位 */
g = r & 0XFF;
g <<= 8;
/* 9341/5310/5510/7789 需要公式轉(zhuǎn)換一下 */
return (((r >> 11) << 11) | ((g >> 10) << 5) | (b >> 11));
}
在lcd_read_point函數(shù)中,因為我們的代碼不止支持一種LCD驅(qū)動器,所以,我們根據(jù)不同的LCD驅(qū)動器((lcddev.id)型號,執(zhí)行不同的操作,以實現(xiàn)對各個驅(qū)動器兼容,提高函數(shù)的通用性。
第十個要介紹的是字符顯示函數(shù)lcd_show_char,該函數(shù)同前面OLED模塊的字符顯示函數(shù)差不多,但是這里的字符顯示函數(shù)多了1個功能,就是可以以疊加方式顯示,或者以非疊加方式顯示。疊加方式顯示多用于在顯示的圖片上再顯示字符。非疊加方式一般用于普通的顯示。該函數(shù)實現(xiàn)代碼如下:
/**
* @brief 在指定位置顯示一個字符
* @param x,y : 坐標
* @param chr : 要顯示的字符:" "--->"~"
* @param size : 字體大小 12/16/24/32
* @param mode : 疊加方式(1); 非疊加方式(0);
* @retval 無
*/
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size,
uint8_t mode, uint16_t color)
{
uint8_t temp, t1, t;
uint16_t y0 = y;
uint8_t csize = 0;
uint8_t *pfont = 0;
/* 得到字體一個字符對應點陣集所占的字節(jié)數(shù) */
csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2);
/* 得到偏移后的值(ASCII字庫是從空格開始取模,所以-' '就是對應字符的字庫) */
chr = chr - ' ';
switch (size)
{
case 12:
pfont = (uint8_t *)asc2_1206[chr]; /* 調(diào)用1206字體 */
break;
case 16:
pfont = (uint8_t *)asc2_1608[chr]; /* 調(diào)用1608字體 */
break;
case 24:
pfont = (uint8_t *)asc2_2412[chr]; /* 調(diào)用2412字體 */
break;
case 32:
pfont = (uint8_t *)asc2_3216[chr]; /* 調(diào)用3216字體 */
break;
default:
return ;
}
for (t = 0; t < csize; t++)
{
temp = pfont[t]; /* 獲取字符的點陣數(shù)據(jù) */
for (t1 = 0; t1 < 8; t1++) /* 一個字節(jié)8個點 */
{
if (temp & 0x80) /* 有效點,需要顯示 */
{
lcd_draw_point(x, y, color); /* 畫點出來,要顯示這個點 */
}
else if (mode == 0) /* 無效點,不顯示 */
{
/* 畫背景色,相當于這個點不顯示(注意背景色由全局變量控制) */
lcd_draw_point(x, y, g_back_color);
}
temp <<= 1; /* 移位, 以便獲取下一個位的狀態(tài) */
y++;
if (y >= lcddev.height)return; /* 超區(qū)域了 */
if ((y - y0) == size) /* 顯示完一列了? */
{
y = y0; /* y坐標復位 */
x++; /* x坐標遞增 */
if (x >= lcddev.width)return; /* x坐標超區(qū)域了 */
break;
}
}
}
}
在lcd_show_char函數(shù)里面,我們用到了四個字符集點陣數(shù)據(jù)數(shù)組asc2_1206、asc2_1608、asc2_2412和asc2_3216。
lcd.c的函數(shù)比較多,其他的函數(shù)請大家自行查看源碼,都有詳細的注釋。
2. main.c代碼
在main.c里面編寫如下代碼:
int main(void)
{
uint8_t x = 0;
uint8_t lcd_id[12];
HAL_Init(); /* 初始化HAL庫 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 設(shè)置時鐘, 72Mhz */
delay_init(72); /* 延時初始化 */
usart_init(115200); /* 串口初始化為115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
g_point_color = RED;
sprintf((char *)lcd_id, "LCD ID:%04X", lcddev.id); /* 將id打印到lcd_id數(shù)組 */
while (1)
{
switch (x)
{
case 0: lcd_clear(WHITE); break;
case 1: lcd_clear(BLACK); break;
case 2: lcd_clear(BLUE); break;
case 3: lcd_clear(RED); break;
case 4: lcd_clear(MAGENTA); break;
case 5: lcd_clear(GREEN); break;
case 6: lcd_clear(CYAN); break;
case 7: lcd_clear(YELLOW); break;
case 8: lcd_clear(BRRED); break;
case 9: lcd_clear(GRAY); break;
case 10: lcd_clear(LGRAY); break;
case 11: lcd_clear(BROWN); break;
}
lcd_show_string(10, 40, 240, 32, 32, "STM32", RED);
lcd_show_string(10, 80, 240, 24, 24, "TFTLCD TEST", RED);
lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(10, 130, 240, 16, 16,(char *)lcd_id, RED); /*顯示LCD_ID*/
x++;
if (x == 12)
x = 0;
LED0_TOGGLE(); /* 紅燈閃爍 */
delay_ms(1000);
}
}
main函數(shù)功能主要是顯示一些固定的字符,字體大小包括3216、2412、168和126四種,同時顯示LCD驅(qū)動IC的型號,然后不停的切換背景顏色,每1s切換一次。而LED0也會不停的閃爍,指示程序已經(jīng)在運行了。其中我們用到一個sprintf的函數(shù),該函數(shù)用法同printf,只是sprintf把打印內(nèi)容輸出到指定的內(nèi)存區(qū)間上,最終在死循環(huán)中通過lcd_show_strinig函數(shù)進行屏幕顯示,sprintf的詳細用法,請百度學習。
特別注意:
usart_init函數(shù),不能去掉,因為在lcd_init函數(shù)里面調(diào)用了printf,所以一旦去掉這個初始化,就會死機!實際上,只要你的代碼有用到printf,就必須初始化串口,否則都會死機,停在usart.c里面的fputc函數(shù)出不來。
25.4 下載驗證
下載代碼后,LED0不停的閃爍,提示程序已經(jīng)在運行了。同時可以看到TFTLCD模塊的顯示背景色不停切換,如圖25.4.1所示:文章來源:http://www.zghlxwxcb.cn/news/detail-680606.html
圖25.4.1 TFTLCD顯示效果圖
此外,為了讓大家能直觀的了解LCD屏的掃描方式,我們額外編寫了兩個main.c文件(main1.c和main2.c,放到User文件夾中),方便大家編譯下載,觀察現(xiàn)象。
使用方法:關(guān)閉工程后,先把原實驗中的main.c改成其他名字,然后把main1.c重命名為main.c,雙擊keilkill.bat清理編譯的中間文件,最后打開工程重新編譯下載,就可以觀察實驗現(xiàn)象。觀察了main1.c,可以再觀察main2.c,main2.c文件的操作方法類似。這兩個main.c文件的程序非常簡單,這里就不講解,具體請看源碼。文章來源地址http://www.zghlxwxcb.cn/news/detail-680606.html
到了這里,關(guān)于【正點原子STM32連載】 第二十五章 TFT-LCD(MCU屏)實驗 摘自【正點原子】STM32F103 戰(zhàn)艦開發(fā)指南V1.2的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!