Prism_tomor_night

2018年9月24日 星期一

[8051] 以I2C介面驅動LCD模組(LCM1602)

主題


透過I2C通訊介面,驅動常見市售的整合式LCD模組LCM1602。


材料


LCM1602顯示器模組(與PCF8574T整合之I2C介面晶片),外觀如下,若擔心相容性,請確認背後標記為Arduino的晶片型號是否與本例相同;本例中使用模組在蝦米拍賣中購得。
https://i1.wp.com/henrysbench.capnfatz.com/wp-content/uploads/2015/10/YWRobot-LCM1602-I2C-V1-Pinouts.png

Datasheet下載:
PCF8574T: https://www.nxp.com/docs/en/data-sheet/PCF8574_PCF8574A.pdf
LCD1602: https://cdn-shop.adafruit.com/datasheets/TC1602A-01T.pdf


動機


液晶顯示器是相當便宜且廣泛應用的簡易顯示器,但由於其太過廣泛被使用,市面上有相當多以不同晶片整合的模組販售,故常找不到正確的datasheet來了解如何驅動。

另外,整合式模組使用I2C通訊介面,僅需要2個GPIO腳位來傳輸訊號,相對於未模組化前的液晶顯示器,需要至少12支腳來進行8位元並列通訊(或8支腳來進行4位元通訊模式),I2C介面具有節省GPIO腳位與可以同時和多個存在此IIC線路上的機器通訊的優點,故廣泛被使用,相當方便。

相較於Arduino方便的函式庫,使用8051更可學習如何寫程式驅動裝置。


說明


LCD本身僅接收並列訊號,而PCF8574T則是支援接收串列訊號並轉發成並列、或是收並列發串列,在本例中,我們主要利用8051送串列訊號給PCF8574T,並讓PCF8574T轉發成並列訊號至LCD模組。因此,我們必須先知道在LCM1602中,LCD是怎麼和PCF8574T連接的。
LCM1602結構圖:LCD與PCF8574T連接

由上圖可以看到,僅有D4-D7四支資料腳連接到PCF8574T,其餘的四支腳則是連接到LCD的控制腳RS/RW/E/BL,D0-D3經實測會永遠高電壓,並不具有功能,換言之,我們無法使用LCD預設的八支腳資料傳輸模式來下指令,只能用四支腳傳輸兩次來取代八支腳傳輸:

LCM1602, LCD1602 功能設定

知道LCD和PCF8574T是怎麼連接之後,也必須知道PCF8574T是怎麼理解上位機8051的串列指令、並把這些指令轉發到LCD去。
說來也不難,對於PCF8574T接收8051的指令,只有兩個格式:address和data。每個I2C網路上的裝置本身都有一個編號(address),用來讓網路上所有的裝置知道現在上位機正在向誰(address)說話,又是說些什麼內容(data)。格式如下:
PCF8574T/PCF8574AT時序圖
藍色即是address,綠色是傳輸的資料,其中可以看到傳輸的資料依序是從高位的P7到低位的P0,對應到接線結構圖;傳完最後一位元的P0後,A訊號會讓PCF8574T的實體P7-P0腳依收到的指令改變狀態。圖上有一些狀態表達的訊號,一併說明:
1) S(START):用來跟網路上的所有裝置宣告,上位機要開始下指令了。
2) R/W:請別和LCD上的R/W搞混,這是指上位機要對裝置下指令(R: READ)或是要求裝置傳資料回上位機(W: WRITE)以便知道裝置現在的狀態。
3) A(ACK):接收方用來告訴傳送方「接收完成」的訊號。若是8051下指令給PCF8574T,接收方就是PCF8574T,傳送方就是8051,當8051傳完8位元的值後,PCF8574T會回覆ACK代表理解;若當8051下命令叫PCF8574T傳送資料回8051,接收方就變成了8051,收完8位元資料後必須告訴PCF8574T接收完成,ACK!


START與ACK訊號格式
START:在SCL訊號為1時將SDA訊號拉為0。
ACK:接收完第8位元資料後,下位裝置會強制把SDA的腳位拉低,此時上位機只要把SCL腳位升高再拉低,就代表上位機知道下位機收完資料。在寫程式的時候,當8051送完第8位元資料,就該將SDA腳位設成1去讀取SDA的狀態,若讀到0,代表是下位機在傳ACK,就再送一個先高後低的CLK訊號至SCL腳上,SCL就會從0被釋放。

***注意:當8051以SDA腳傳送各位元的值時,務必先拉低SCL、待SDA狀態變化完之後,再升高SCL。你可以想像成SCL從0變成1的瞬間是喚醒網路上的裝置來讀現在SDA上的值。若在SCL=1時任意改變SDA上的值,會造成錯誤,例如裝置會誤判成START/STOP指令而重新或停止接收命令。

如果I2C網路上只有一台裝置,例如本實例中僅有一台LCM1602,8051僅需要重覆傳送這裝置的編號(address),預設編號值如下圖:

LCM1602中PCF8574T的預設位址
要留意,在本例中只讓8051下指令給裝置,並不會讓8051讀取裝置狀態值,故最後一位元的R/W=0即可,此時在時序圖上,LCM1602的藍色方框address就是0x4E。
而PCF8574AT的唯一的差別就是預設位址為0x7E,依datasheet上的說明如下:
接下來處理數據傳輸的問題,依LCD的datasheet指示,必須依照下方流程圖來初始化,在左側我們把它翻譯成8051對PCF8574T的指令:


為什麼上面的資料被標示成資料「組」(Set)呢?因為,按照LCD的datasheet說明,只要是寫入指令或資料至D7-D0,都必須照著時序圖一步一步送指令,這不是單一指令,而是多指令組合:
1) 先設定RS(指令/資料模式)和R/W(讀/寫模式)
2) 隔一個最小時間間隔後拉高E(E=1)喚醒LCD(由於此例中8051作動時間遠大於datasheet中的最小時間間隔需求,故可以忽略其影響)
3) 將欲傳送的數值上傳至D7-D0(8bits模式)或D7-D4(4bits模式)
4) E拉至0的瞬間,下緣觸發,LCD讀取D7-D0(或D7-D4)值
故Data Set 1至3必須照下方表格送出四個指令才能讓LCD讀到要求的D7-D4值

LCDD7D6D5D4BLERWRS
Data
PCF8574TP7P6P5P4P3P2P1P0
Step 1000010000x08
Step 2000011000x0C
Step 3001111000x3C
Step 4001110000x38

請注意,在每一個Step之間,也就是每一筆8bits資料,都需有ACK訊號來宣告傳輸完成,為方便閱讀在這邊不標註出來;而每一個位元的傳輸,都得按照PCF8574T的datasheet中註明的格式:




本例中預設啟動背光,所以每一步的BL都是1;Data Set 4 只要依樣畫葫蘆改掉,使D4=0即可。


從Data Set 5的功能設定開始,LCD正式在4位元傳輸模式下工作,也就是說,8位元的資料必須分兩次傳,以Set5為例,流程修改如下:
LCDD7D6D5D4BLERWRS
Data
PCF8574TP7P6P5P4P3P2P1P0
Step 1000010000x08
Step 2000011000x0C
Step 3001011000x2C
Step 4001010000x28
Step 5000011000x0C
Step 6100011000x8C
Step 7100010000x88

第4步結束之後,直接重覆第2步至第4步一遍,上傳第二次的4bits數值,在此例中設定N=1且F=0。

流程搞清楚之後,就可以把所有在8bits傳輸下的數值轉化成上述的兩次4bits流程,只要把8bits的D7-D0資料拆成兩筆D7-D4填入第3、4、6、7步即可。Set 6的關閉螢幕、Set 7清除螢幕與Set 8的移動模式都請參照。

完成初始化之後,別忘了設定Display ON,如下圖所示

現在,可以開始顯示字元在LCD螢幕上了,你必須先設定DDRAM來告訴LCD你要在哪個位置(位址)顯示(不需要每次都設定位址,若你要顯示相鄰連續的字元,LCD會自動幫你加1移到下個位置),具體位置與址址配置如datasheet所示:


想顯示什麼字元(流程相同,但記得把RS改成1變成下「資料」而不是上述那些「指令」),請參考datasheet第15頁的代碼。也可以自創字元(請參照程式碼示範自設的歐元符號)。



程式碼


//LCM1602 + PCF8574T I2C 介面

#include "reg51.h"

sbit sda = P2^0;
sbit scl = P2^1;

#define SLAVE 0x4E //LCM1602位址定義

void delay(unsigned int dl)
{
 while (dl>0)
  dl--;
}

void start()
{
 scl = 1;
 delay(5);
 sda = 1;
 delay(5);
 sda = 0;
 delay(4);
}

void send_8bits(unsigned char strg)
{
 unsigned char sf;
 
 for (sf=0; sf<8; sf++)
 {
  scl = 0;
  sda =(bit)(strg & (0x80>>sf));
  delay(5);
  scl = 1;
  delay(4);
 }
 scl = 0;
 delay(5);
}

void ack(void)
{
 sda = 1; 
 
 if(sda == 0)
 {  
  scl = 1;
  delay(4);
  scl = 0;
  delay(5);
 }
}

void stop(void)
{
 sda = 0;
 scl = 1;
 delay(5);
 sda = 1;
}

void WriteInst4bits(unsigned char inst_4b)
{
 send_8bits(0x08);         //RS=0, RW=0
 ack();
 send_8bits(0x0C);         //EN=1
 ack();
 send_8bits((inst_4b&0xF0)+0x0C); //送出D7-D4
 ack();
 send_8bits((inst_4b&0xF0)+0x08); //EN=0 讀四位元值
 ack();
}

void WriteInst(unsigned char inst)
{ 
 send_8bits(0x08);         //RS=0, RW=0
 ack();
 send_8bits(0x0C);         //EN=1
 ack();
 send_8bits((inst&0xF0)+0x0C);   //高四位
 ack();
 send_8bits((inst&0xF0)+0x08);   //EN=0 讀高四位
 ack();
 
 send_8bits(0x0C);         //EN=1
 ack();
 send_8bits((inst<<4)+0x0C);    //低四位
 ack();
 send_8bits((inst<<4)+0x08);    //EN=0 讀低四位
 ack();
} 

void WriteData(unsigned char data_)
{
 send_8bits(0x09);         //RS=1, RW=0
 ack();
 send_8bits(0x0D);         //EN=1
 ack();
 send_8bits((data_&0xF0)+0x0D);  //高四位
 ack();
 send_8bits((data_&0xF0)+0x09);  //EN=0 讀高四位
 ack();
 
 send_8bits(0x0D);         //EN=1
 ack();
 send_8bits((data_<<4)+0x0D);   //低四位
 ack();
 send_8bits((data_<<4)+0x09);   //EN=0 讀低四位
 ack();
} 

void WriteString(unsigned char count, unsigned char MSG[])
{
 unsigned char sf;
 unsigned char move = 0;
 
 for (sf=0; sf<count; sf++)
   WriteData(MSG[sf]);
}

void initial(void)
{
 delay(15000);
 
 start();
 send_8bits(SLAVE);  //傳送LCM1602位址
 ack();
 
 WriteInst4bits(0x30); //寫入0011至DB7-4
 
 delay(4100);
 
 WriteInst4bits(0x30); //寫入0011至DB7-4
 
 delay(100);

 WriteInst4bits(0x30); //寫入0011至DB7-4
 WriteInst4bits(0x20); //寫入0010至DB7-4
  
 WriteInst(0x28); //功能設定 function set, DL(DB4)=0(4位元傳輸), N(DB3)=1(2列顯示), F(DB2)=0(5*7解析度)
 WriteInst(0x08); //關閉螢幕 display off
 WriteInst(0x01); //清除螢幕 clear display
 WriteInst(0x06); //移動模式 entry mode
 WriteInst(0x0E); //開啟螢幕 display ON, D(DB2)=1(開), C(DB1)=1(游標開), B(DB0)=0(閃爍關)
}

void main()
{
 unsigned char Euro[] = {0x07, 0x08, 0x1F, 0x08, 0x1F, 0x08, 0x07, 0x00}; //歐元符號
 unsigned char MSG_1[] = "Start frm Garage";
 unsigned char MSG_2[] = {'I','t',' ','c','o','s','t','s',' ','1','7','9',',','8','9',0,' '};
 
 initial();
 
 WriteInst(0x40);  //設定CGRAM位址0x00給客製符號
 WriteString(sizeof(Euro), Euro); //寫入歐元符號
 
 WriteInst(0x80);  //設定DDRAM位址給顯示位置(第一行第一位)
 WriteString(sizeof(MSG_1)-1, MSG_1);
 
 WriteInst(0xC0);  //設定DDRAM位址給顯示位置(第二行第一位)
 WriteString(sizeof(MSG_2)-1, MSG_2);
 
 while(1);
}

結果

沒有留言:

張貼留言

[8051] 以I2C介面驅動LCD模組(LCM1602)

主題 透過I2C通訊介面,驅動常見市售的整合式LCD模組LCM1602。 材料 LCM1602顯示器模組(與PCF8574T整合之I2C介面晶片),外觀如下,若擔心相容性,請確認背後標記為Arduino的晶片型號是否與本例相同;本例中使用模組在蝦米拍賣中購得...