關於FSMC+LCD第一次學習完時候,自己也還是對這個不清不白,時而清楚,時而糊塗。這一次再次學習的話,不能在這樣了,仔仔細細把STM32參考手冊,原子的LCD實驗看了一遍,又在網上找了好些關於FSMC+LCD的資料,終於徹底明白了,,,當然,叫我完全一個人獨立的把這個LCD顯示實驗程序寫出來還是不可能的,C語言還是有待提高,實戰還是太少,看到別人寫的代碼,有些細節根本一點都想不到,更何況讓自己去寫。。。。
收集的資料:
LCD有如下控制線:
CS:Chip Select 片選,低電平有效
RS:Register Select 寄存器選擇
WR:Write 寫信號,低電平有效
RD:Read 讀信號,低電平有效
RESET:重啟信號,低電平有效
DB0-DB15:數據線
假如這些線,全部用普通IO口控制。根據LCD控制芯片手冊(大部分控制芯片時序差不多):
如果情況如下:
DB0-DB15的IO全部為1(表示數據0xff),也可以為其他任意值,這里以0xff為例。
CS為0(表示選上芯片,CS拉低時,芯片對傳入的數據才會有效)
RS為1(表示DB0-15上傳遞的是要被寫到寄存器的值),如果為0,表示傳遞的是數據。
WR為0,RD為1(表示是寫動作),反過來就是讀動作。
RESET一直為高,如果RESET為低,會導致芯片重啟。
這種情況,會導致一個值0xff被傳入芯片,被LCD控制芯片當作寫寄存器值去解析。LCD控制芯片收到DB0-15上的值之后,根據其他控制線的情況,它得出結論,這個0xff是用來設置寄存器的。一般情況下,LCD控制芯片會把傳入的寄存器值的高8位當做寄存器地址(因為芯片內部肯定不止一個寄存器),低8位當做真正的要賦給對應寄存器值。這樣,就完成了一個寫LCD控制芯片內部寄存器的時序。
如果上述情況不變,只將RS置低,那么得到的情況如下:LCD控制芯片會把DB0-15上的數據當做單純的數據值來處理。那么假如LCD處在畫圖狀態,這個傳入的值0xff,就會被顯示到對應的點上,0xffff就表示白色,那么對應的點就是白色。在這個數據值傳遞過來之前,程序肯定會通過設置寄存器值,告訴LCD控制芯片要寫的點的位置在哪里。
如果上述兩種情況都不變,分別把WR和RD的信號反過來(WR=1,RD=0),那么寫信號就會被變成讀信號。讀信號下,主控芯片需要去讀DB0-15的值,而LCD控制芯片就會去設置DB0-15的值,從而完成讀數據的時序。
讀寄存器的時序麻煩一點。第一步,先要將WR和RD都置低,主控芯片通過DB0-15傳入寄存器地址。第二步就和前面讀數據一樣,將WR置高,RD置低,讀出DB0-15的值即可。在這整個的過程中,RS一直為低。
好了,上面就是IO直接控制LCD的方法。假如放到STM32里面,用IO直接控制顯得效率很低。STM32有FSMC(其實其他芯片基本都有類似的總線功能),FSMC的好處就是你一旦設置好之后,WR、RD、DB0-DB15這些控制線和數據線,都是FSMC自動控制的。打個比方,當你在程序中寫到:
*(volatile unsigned short int *)(0x60000000)=val;
那么FSMC就會自動執行一個寫的操作,其對應的主控芯片的WE、RD這些腳,就會呈現出寫的時序出來(即WE=0,RD=1),數據val的值也會通過DB0-15自動呈現出來(即FSMC-D0:FSMC-D15=val)。地址0x60000000會被呈現在數據線上(即A0-A25=0,地址線的對應最麻煩,要根據具體情況來,好好看看FSMC手冊)。
那么在硬件上面,我們需要做的,僅僅是MCU和LCD控制芯片的連接關系:
WE-WR,均為低電平有效
RD-RD,均為低電平有效
FSMC-D0-15接LCD DB0-15
連接好之后,讀寫時序都會被FSMC自動完成。但是還有一個很關鍵的問題,就是RS沒有接,CS沒有接。因為在FSMC里面,根本就沒有對應RS和CS的腳。怎么辦呢?這個時候,有一個好方法,就是用某一根地址線來接RS。比如我們選擇了A16這根地址線來接,那么當我們要寫寄存器的時候,我們需要RS,也就是A16置高。軟件中怎么做呢?也就是將FSMC要寫的地址改成0x60020000,如下:
*(volatile unsigned short int *)(0x60020000)=val;
這個時候,A16在執行其他FSMC的同時會被拉高,因為A0-A18要呈現出地址0x60020000。0x60020000里面的Bit17=1,就會導致A16為1。
當要讀數據時,地址由0x60020000改為了0x60000000,這個時候A16就為0了。
那么有朋友就會有疑問,第一,為什么地址是0x6xxxxxxx而不是0x0xxxxxxx;第二,CS怎么接;第三,為什么Bit17對應A16?
先來看前兩個問題,大家找到STM32的FSMC手冊,在FSMC手冊里面,我們很容易找到,FSMC將0x60000000-0x6fffffff的地址用作NOR/PRAM(共256M地址范圍)。而這個存儲塊,又被分成了四部分,每部分64M地址范圍。當對其中某個存儲塊進行讀寫時,對應的NEx就會置低。這里,就解決了我們兩個問題,第一,LCD的操作時序,和NOR/PRAM是一樣的(為什么一樣自己找找NOR/PRAM的時序看看),所以我們選擇0x6xxxxxxx這個地址范圍(選擇這個地址范圍,操作這個地址時,FSMC就會呈現出NOR/PRAM的時序)。第二,我們可以將NEx連接到LCD的CS,只要我們操作的地址是第一個存儲塊內即可(即0-0x3ffffff地址范圍)。
第三個問題再來看一看FSMC手冊關於存儲器字寬的描述,我們發現,當外部存儲器是16位時,硬件管腳A0-A24表示的是地址線A1-A25的值,所以我們要位移一下,Bit17的值,實際會被反應到A16這根IO來。關於數據寬度及位移的問題,初學的朋友可能會比較疑惑,當你接觸了多NOR/PRAM這樣的器件后,你會發現,很多芯片的總線,都是這樣設計的,為的是節省地址線
第二個角度理解:
FSMC總線上看,LCD只有2個地址.
Bank1_LCD_C是寫寄存器,此時RS=1,告訴LCD我在總線上輸出數據的是寄存器的地址
Bank1_LCD_D是寫數據,此時RS=0,告訴LCD我在總線上輸出地數據是寄存器的數據或者GRAM的數據.
寫寄存器數據按2步來:
第一步先往Bank1_LCD_C (對應RS=1),送寄存器的地址:*(__IO uint16_t *) (Bank1_LCD_C)= index; 接着在Bank1_LCD_D這個地址(對應RS=0),寫入剛指向的寄存器的數據: *(__IO uint16_t *) (Bank1_LCD_D)= val;
為什么*(__IO uint16_t *) (Bank1_LCD_C)= index; 就是往 LCD 寫寄存器呢?
這是一個16位的IO賦值操作,地址是Bank1_LCD_C,這個地址就是指向FSMC的 Bank1的NE1對應的地址空間。而LCD片選正是連接到NE1,具體地址要看RS接到哪一根地址線上。當CPU執行到這一條的時候,就會通過FSMC總線控制器在數據總線上進行一個地址為 Bank1_LCD_C的數據寫操作,此操作自動完成CS信號,
RD信號,WR信號,以及地址總線數據(RS信號)的輸出以及數據總線數據的輸出.
其他的操作都是這兩個操作組合完成。也就是我上面所說的,
"所有的寄存器地址和寄存器數據,以及 GRAM數據都是通過 IO0-IO15完成傳輸的,而不是FSMC的地址.這是容易搞混的一個地方.LCD的FSMC地址只有一根 ,就是RS."。
把TFT看做類似SRAM的存儲器,只能接在 BANK1上。對應基地址是0x60000000.
而BANK1又有划分為四個片選,分別對應基地址:
NE1 0x600000000
NE2 0x640000000
NE3 0x680000000
NE4 0x6C0000000
所以每個NEx能尋址的空間大小為64M,也就是對應了FSMC的A0到A25 共26根地址線.
假如使用NE4接到為LCD的片選CS上,那么就對應基地址 0x6C000000,
如果RS接到地址線的 A0上,那么當 RS為0時對應的地址就是 LCD_REG = 0x6C000000,(其實你用0x6CFFFFF0是一樣的,因為只用到一根地址線).
RS為1時對應的地址就是 LCD_RAM =0x6C000001,(0x6CFFFFF1一樣對應 LCD_RAM,因為它一樣對應 RS=1).
如果 RS接到 其他地址線上,情況是類似的。
比如接到 An上,那么
LCD_REG= 0x6C000000,
LCD_RAM= 0x6C000000 | (1<<n)
注意這個地址不是唯一的,只要這個地址能尋址到 BANK1 的 NE4上而且使 RS=0,那么就是 LCD_REG,使 RS=1,就是LCD_RAM.
對應Bank1_LCD_C 的地址,FSMC總線控制器在RS接的那根地址線輸出的是 1,而對應Bank1_LCD_D,輸出的0.
RS接的可不是GPIO,是FSMC地址總線的一根.FSMC進行讀寫操作的時候會在地址總線根據要讀寫的地址輸出電平的.
RS接哪一根地址線雖然沒有固定要求,但是一旦你確定要接哪一根,那么Bank_LCD_C和Bank_LCD_D也要隨之確定,這可不是“自動的".
雖然沒有手動操作GPIO來操作RS,但是你敲代碼的時候可是手動指定 Bank1_LCD_C 或者 Bank1_LCD_D ,從而確定 RS的電平.
所謂的“自動”是指:不是通過操作GPIO來操作RS,而是直接根據地址總線地址的不同來完成操作RS,這兩種方法的速度差別是非常大的.
如果是GPIO方式,先要通過操作GPIO 分別 輸出 RS,CS,等的電平,然后再通過過GPIO操作輸出數據,然后還要通過GPIO 再操作RD,WR,CS等的電平。
每操作一個GPIO都要好幾個周期,加起來就非常慢了.
而FSMC是在一個FSMC寫周期內就完成了這所有的動作
問題:RS如何選擇:
#define Bank1_LCD_R ((uint32_t)0x60000000) //disp Reg ADDR
#define Bank1_LCD_D ((uint32_t)0x60020000) //disp Data ADDR
這里LCD選取的16位,將RS接在A16,則HADDR[25:1]對應FSMC_A[24:0];關鍵在於為什么???
從上面可以看出,LCD除了需要數據線之外,額外的地址線是不需要的~~~~~但是在STM32在進行FSMC總線操作時,所有的地址線還是會出現時序的,但是操縱LCD 不需要額外的地址線了,也就是FSMC_A[16:25]可以解放了,但是要注意一旦配置了FSMC,這些管腳還是會出現時序的;
現在我們向0x60000000這個塊地址送出數據,當然這些數據肯定是16位的,因為是16位的LCD,由於RS(A16)為0,所以這個讀寫寄存器的操作;當向0x60020000寫數據時,由於總線時序是要有地址寫的,這時bit17就為高了,也就是RS為1了,這時所進行的操作就是讀寫RAM了!!!!
其中RS的選擇可以是任意的,但一般還是選擇,不用的地址線為好~~~~~
程序:(僅僅是FSMC初始化)
void LCD_Init()
{
RCC->AHBENR |= 1 << 8; //開啟FSMC時鍾
RCC->APB2ENR |=1 << 3 | 1 << 5 | 1 << 6 | 1 << 8; //開啟B,D,E,G時鍾
RCC->APB2ENR |= 1 << 0; //開啟AFIO
GPIOB->CRL &= 0xfffffff0;
GPIOB->CRL |= 0x00000003; //GPIOB0 推挽輸出
GPIOD->CRL &= 0xff00ff00;
GPIOD->CRL |= 0x00bb00bb; //設置0,1,4,5為復用推挽輸出
GPIOD->CRH &= 0x00fff000;
GPIOD->CRH |= 0xbb000bbb; //設置8,9,10,14,15為推挽輸出
GPIOE->CRL &= 0x0fffffff;
GPIOE->CRL |= 0xb0000000; // 都是復用推挽輸出
GPIOE->CRH &= 0x00000000;
GPIOE->CRH |= 0xbbbbbbbb; //
GPIOG->CRL &= 0xfffffff0;
GPIOG->CRL |= 0x0000000b;
GPIOG->CRH &= 0xfff0ffff;
GPIOG->CRH |= 0x000b0000;
FSMC_Bank1->BTCR[6] &= 0x00000000;
FSMC_Bank1->BTCR[6] |= 1 << 14| 1 << 12 | 0x01 << 4 | 0x00 << 2 | 1 << 0; //BCR4 擴展模式,寫使能,16位,SRAM,存儲塊使能
FSMC_Bank1->BTCR[7] &= 0x00000000;
FSMC_Bank1->BTCR[7] |= 0x00 << 28 | 0x0000 << 20 | 0x0f << 8 | 0x01; //模式A,不分頻,16hclk的讀數據建立時間,1hclk的地址建立時間
FSMC_Bank1E->BWTR[6] &= 0x00000000; (關於這個時鍾設置我沒找到)
FSMC_Bank1E->BWTR[6] |= 0x00 << 28 | 0x0000 << 20 | 0x03 << 8 | 0x00;//模式A,不分頻,3hclk的寫數據建立時間,0hclk的地址建立時間
然后讀出自己的ID號,根據自己LCD驅動器的ID號選擇LCD初始化序列了,不需要自己寫的
}
其他的就是根據LCD的手冊來讀來寫了,常用的命令也就幾個
//寫寄存器函數
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{
LCD->LCD_REG=regval;//寫入要寫的寄存器序號
}
//寫LCD數據
//data:要寫入的值
void LCD_WR_DATA(u16 data)
{
LCD->LCD_RAM=data;
}
//讀LCD數據
//返回值:讀到的值
u16 LCD_RD_DATA(void)
{
vu16 ram; //防止被優化
ram=LCD->LCD_RAM;
return ram;
}
//寫寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要寫入的數據
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue)
{
LCD->LCD_REG = LCD_Reg; //寫入要寫的寄存器序號
LCD->LCD_RAM = LCD_RegValue;//寫入數據
}
//讀寄存器
//LCD_Reg:寄存器地址
//返回值:讀到的數據
u16 LCD_ReadReg(u16 LCD_Reg)
{
LCD_WR_REG(LCD_Reg); //寫入要讀的寄存器序號
delay_us(5);
return LCD_RD_DATA(); //返回讀到的值
}
typedef struct
{
vu16 LCD_REG; //寫命令
vu16 LCD_RAM; //寫數據
}LCD_TYPE;
#define LCD_BASE (unsigned int)(0x60000000 + 0x0c000000 + 0x000007FE) //這個括號一定要打,否則會出現硬件錯誤(害我花了幾個小時,沒想到這里出錯了)
#define LCD ((LCD_TYPE*)LCD_BASE)
這個是最關鍵的,只要這個龍明白了,就感覺LCD顯示不是很難了,主要是畫一下圖形用C語言難寫,顯示字符串等等,這些都是看C語言學的怎么樣了,,,