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);
}

結果

2018年8月24日 星期五

[8051] 以UART串列介面實現LCD顯示終端機輸入字元

目標

  
以8051收終端機輸入的字元並發送至液晶顯示器。


材料


  1. LCM1602 x1 (在此使用LCD1602整合PCF8574T模組、I2C介面)
  2. USB Hub x1 (與電腦UART通訊)
  3. Putty (終端機,免費軟體)


應用


  1. I2C(IIC)介面通訊使用
  2. Printf & Scanf debug介面、直接由終端機輸出/輸入觀察變數


功能說明


  由上位終端機(在此使用Putty)鍵入字元,透過UART介面傳送至8051,再由8051透過I2C介面傳送給LCD1602顯示鍵入字元。
  輸入第1-16字元顯示在第一列、第17-31字元顯示在第二列,最多同時顯示31字元,若超過時所有字元會往前移一位擠出第1位字元,且在第31字元顯示新輸入字元。


結果





設定


  依控制台 – 裝置管理員中的COM位址,並設定Baudrate(此以COM4 19200bps為例)


程式碼


//LCM1602 with PCF8574T IIC interface
//realize that LCD shows what PC inputs
//Baudrate 19200

#include <reg51.h>
#include <stdio.h>

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

#define SLAVE 0x4E //slave addr.

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; //read ack on sda
 
 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); //inst 4bits
 ack();
 send_8bits((inst_4b&0xF0)+0x08); //EN=0, read this 4bits
 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); //MSB
 ack();
 send_8bits((inst&0xF0)+0x08); //EN=0, read MSB
 ack();
 
 send_8bits(0x0C);   //EN=1
 ack();
 send_8bits((inst<<4)+0x0C); //LSB
 ack();
 send_8bits((inst<<4)+0x08); //EN=0, read LSB
 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); //MSB
 ack();
 send_8bits((data_&0xF0)+0x09); //EN=0, read MSB
 ack();
 
 send_8bits(0x0D);   //EN=1
 ack();
 send_8bits((data_<<4)+0x0D); //LSB
 ack();
 send_8bits((data_<<4)+0x09); //EN=0, read LSB
 ack();
} 

void WriteString(unsigned char count, unsigned char MSG[])
{
 unsigned char sf;
 
 WriteInst(0x80);   //set DDRAM addr.
 
 if (count <16)
 {
  for (sf=0; sf<count; sf++)
  WriteData(MSG[sf]); 
 }
 else
 {
  for (sf=0; sf<16; sf++)
   WriteData(MSG[sf]); 
 
  WriteInst(0xC0);  //set DDRAM addr.
 
  for (sf=16; sf<count; sf++)
   WriteData(MSG[sf]);
 }
}

void initial(void)
{
 delay(15000);
 
 start();
 send_8bits(SLAVE); //communicate to PCF8574T, request to read
 ack();
 
 WriteInst4bits(0x30); //write 0011 to DB7-4 for initialization
 
 delay(4100);
 
 WriteInst4bits(0x30); //write 0011 to DB7-4 for initialization
 
 delay(100);

 WriteInst4bits(0x30); //write 0011 to DB7-4 for initialization
 WriteInst4bits(0x20); //write 0010 to DB7-4 for initialization
  
 WriteInst(0x28); //function set, DL(DB4)=0(4bits), N(DB3)=1(2 lines), F(DB2)=0(5*7)
 WriteInst(0x08); //display OFF
 WriteInst(0x01); //clear
 WriteInst(0x06); //entry mode
 WriteInst(0x0F); //display ON, D(DB2)=1(disp. ON), C(DB1)=1(cursor ON), B(DB0)=0(blinking ON)
}

void init_uart(void)
{
 SCON = 0x50;  //SM0=0, SM1=1, Mode 1, REN=1
 TMOD = 0x20;  //Timer_1 work in mode 2 (auto_load)
 TCON = 0x40; //TR1=1, turn on timer_1 
 TH1 = 253;
 TI = 1;  //if it is not set to 1, then putchar will wait till 1, then works
 PCON |=0x80; //SMOD=1
}

void main()
{ 
 unsigned char k;
 unsigned char MSG[31];  //MSG contains 31 chars
 unsigned char MSG_count = 0;
 
 init_uart();
 initial();
 
 while(1)
 { 
  if (MSG_count <=30)
  {
   scanf("%c", &MSG[MSG_count]);
   MSG_count++;
  }
  else
  {
   for (k=0; k<30; k++)
    MSG[k] = MSG[k+1];
   
   scanf("%c", &MSG[30]);
  }
  WriteString(MSG_count, MSG);
 }
}

筆記

過程中碰到一些困難點:用scanf(%s,...)來實現此功能時預想以enter送字,但一直無法克服enter帶來的空白字元,改用getchar後沒有太大差別;最後發現用scanf(%c,...)可以更直觀地連續送字且不需要特別清除enter的字元。整理重點如下:
  1. 以getchar實現功能
      i. 在main的while無限迴圈中讀取RI=1後進入串列中斷,再以指令getchar()取得緩衝區內資料
      ii. Putty必須開啟Terminal – Local echo force on,否則無法看到自己輸入的字元
      iii. 必須自己寫清除結束字元(enter鍵、\n、0x0A)的判斷
  2. 以scanf(%c…)實現功能
      i. %c預設只讀入一個字元,故在Putty中不需以enter送出,可連續鍵入字元,鍵入字元後自動會被8051讀取
      ii. 注意,enter本身即為一個字元(0x0A)
 3. 以scanf(%s…)實現功能
      i. %s本身可一次讀取多字元的字串,且會以空白鍵當做字串間隔,意即空白鍵會結束該次scanf
      ii. enter是scanf(%s…)的結束字元,故若使用enter鍵送出字元,必須清除結束字元(\n、0x0A)

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

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