主題
透過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時序圖 |
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訊號格式 |
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的預設位址 |
而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值請注意,在每一個Step之間,也就是每一筆8bits資料,都需有ACK訊號來宣告傳輸完成,為方便閱讀在這邊不標註出來;而每一個位元的傳輸,都得按照PCF8574T的datasheet中註明的格式:
本例中預設啟動背光,所以每一步的BL都是1;Data Set 4 只要依樣畫葫蘆改掉,使D4=0即可。
從Data Set 5的功能設定開始,LCD正式在4位元傳輸模式下工作,也就是說,8位元的資料必須分兩次傳,以Set5為例,流程修改如下:
第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);
}
沒有留言:
張貼留言