Thursday, March 27, 2014

Giao tiếp MSP430 và LCD Nokia 5110


Đặng Hồng Luật -CN-ĐTVT 03 – K55 Bách Khoa Hà Nội

1.    Giới thiệu tổng quan về LCD Nokia 5110

LCD Nokia 5110 thuộc dòng sản phầm Graphic LCD 84x48.

LCD Nokia 5110 là màn hình LCD Graphic đơn giản với nhiều ứng dụng khác nhau. Nguồn gốc màn hinh này thực chất là từ các thế hệ điện thoại di động trước đây. Nó đã được gắn trên một bô PCB dễ dàng hàn gắn với các board khác.
LCD Nokia 5110 sử dụng vi điều khiển PCD8544, được sử dụng cùng với Nokia 3310 LCD trước đây. PCD8544 là dạng low power CMOS controller/driver, được thiết kế vơi chế độ hiển thị màn hình graphic là 84 cột và 48 hàng. Tất cả các chưc năng cơ bản đã được tích hợp sẵn trên chip, từ đó cho ta hiệu quả về một thiết bị ngoại vi chiếm ít nguồn tiêu thụ. PCD8544 giao tiếp với vi điều khiển của chúng ta qua một loạt các chân bus đã được cung cấp sẵn. 

2.    Giao tiếp với LCD Nokia 5110

2.1. Cấu tạo – Sơ đồ nguyên lý của LCD Nokia 5110

Sơ đồ nguyên lý của LCD Nokia 5110
Qua sơ đồ trên, ta thấy được LCD Nokia 5110 được điều khiển toàn bộ bởi 5 cổng đầu vào, và 3 cổng nguồn, dưới dạng giao tiếp SPI, gồm:
-          RST – Tín hiệu Reset, giúp khởi động, reset màn hình
-          CE – Tín hiệu Chip Select, giúp đưa ra tín hiệu chọn chip hoạt động ở mức tích cực thấp
-          DC – Tín hiệu Data/Command giúp ta chọn chế độ đưa lệnh vào PCD8544 là lệnh điều khiển, hay dữ liệu hiển thị
-          DIN – Dữ liệu đầu vào có thể là lệnh điều khiển hoặc dữ liệu hiển thị màn hình
-          CLK – Xung Clock điều khiển
-          VCC – Nguồn nuôi
-          LIGHT – Đất cho đèn LED Backlight
-          GND – Đất cho nguồn nuôi

2.2.         Lập trình giao tiếp với LCD Nokia 5110

2.2.1.      Thư viện và các lệnh định nghĩa cơ bản

Như thường lệ, bước đầu của một chương trình điều khiển, luôn là bước nhập thư viện, cũng như cung cấp các định nghĩa cơ bản cho toàn bộ chương trình sau này sử dụng.
Đầu tiên, ta gọi thư viện của chip MSP430G2553 đầu tiên, do ta đang sử dụng IC này để làm vi điều khiển hiện tại:
#include <msp430g2553.h>         //Call header for MSP430G2553
Thư viện được gọi dưới lệnh include và thư viện gọi ra ở dạng file header.
Tiếp theo sau đó, ta định nghĩa lại một vài toán tử chúng ta sẽ thường xuyên sử dụng trong suốt chương trình sau này, điều này sẽ khiến cho chương trình của chúng ta về sau trông sẽ dễ hiểu hơn với ngôn ngữ thường ngày chúng ta quen thuộc, và quan trọng nhất là dễ dàng đọc hiểu nhanh chóng trong quá trình sửa chữa sau này, tránh tình trạng nhầm lẫn ký tự gây khó khăn sau này:
#define set |=            //Set 1 bit to 1
#define clr &=~           //Set all bits to 0
#define rev ^=            //Reverse all bits
Toán hạng “set’ sử dụng phép OR để nâng 1 bit duy nhất lên trạng thái “1”.
Toán hạng “clr” sử dụng toán hạng AND với NOT để biến tất cả các bit hiện tại về “0”.
Toán hạng “rev” sử dụng toán hạng XOR để đảo tất cả các bit hiện tại.
Tiếp sau đó, ta định nghĩa các cổng nối bus tín hiệu từ LCD tới vi điều khiển của chúng ta sẽ nối với các cổng nào trên vi điều khiển:
#define RST BIT0          //P1.0
#define CE BIT1           //P1.1
#define DC BIT2           //P1.2
#define DIN BIT3          //P1.3
#define CLK BIT4          //P1.4
Qua việc định nghĩa như vậy, ta đã set các bit hiện tại RST cho tới CLK lần lượt vào các Port 1.0 cho tới 1.4 trên vi điều khiển. Việc định nghĩa các cổng như vậy sẽ giúp ta tốn ít thời gian cho việc lập trình sau này hơn, do sau này ta sẽ chỉ việc gọi tên các cổng chúng ta quan tâm tới trên LCD, sau đó chương trình định nghĩa sẽ tự tham chiếu lại địa chỉ các port giúp chúng ta.
Tiếp sau đó, do Port 1 được sử dụng như là Port giao tiếp với LCD, nên ta định nghĩa tiếp:
#define LCD_OUT P1OUT            //Output of LCD is marked to values of P1
#define LCD_DIR P1DIR            //Direction of LCD is direction of P1
Do cổng ra tới LCD cũng chính là Port 1 ở vi điều khiển, nên ta cho chúng tham chiếu tương ứng với nhau.
Cuối cùng, ta tham chiếu định nghĩa nốt một biến nữa là biến phân biệt DIN dạng Command hay Data:
#define LCD_CMD 0         //When DC=0 bits comes to command
#define LCD_DATA 1        //When DC=1 bits comes to data
Điều này sẽ giúp chúng ta đỡ băn khoăn và nhầm lẫn hơn sau này do chỉ việc phải xem với giá trị nào thì LCD sẽ lấy tín hiệu là Command/Data, và trong tương lai, ta chỉ cần viết lệnh do chúng ta định nghĩa từ trước là được. Điều này sẽ giúp chúng ta tiết kiệm được rất nhiều thời gian, đồng bộ định nghĩa chương trình và tránh nhầm lẫn tương lai.

2.2.2.      Định nghĩa các biến, tham số sử dụng trong chương trình

Tiếp theo sau đó, là phần định nghĩa các biến số, tham số sẽ được sử dụng trong chương trình tương lai của chúng ta.
Đầu tiên, dĩ nhiên là chúng ta sẽ cần một bảng chữ cái để giúp hiển thị các thông điệp lên màn hình. Ở đây, ta sử dụng bảng mã ASCII:
static const char ASCII[][5] =
{
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f
};
Việc nhập bảng chữ cái này sẽ giúp chúng ta có thể hiển thị các chữ cái và ký tự thường sử dụng sau này lên màn hình thông qua các hàm chức năng tương lai sẽ sử dụng.
Tiếp theo sau đó là một vài hình ảnh giúp tăng tính sinh động cho màn hình sau này chúng ta sử dụng. Do màn hình chúng ta đang sử dụng có kích cỡ 48x84, nên các ảnh chúng ta sử dụng cũng sẽ phải ở định dạng như vậy, hoặc nếu không chúng ta sẽ phải chuyển đổi về dạng như vậy để sử dụng.
Ảnh dưới định dạng lớn, sẽ được chuyển dần về dạng bé hơn và đưa tới dạng bitmap, từ đó, ta sẽ convert được ảnh sang dạng bit như sau:
const unsigned char Terran_bm [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

Các bức ảnh còn lại cũng được làm với cách làm tương tự và nhập vào dưới dạng các biến để sử dụng sau này.

2.2.3.      Các hàm chức năng

Trước khi vào chương trình chính, ta sẽ khai báo các chương trình con được sử dụng tương lai trong chương trình chính của chúng ta.
void Delay(volatile long time);
void ShiftOut(unsigned char txdata);
void LCDWrite(char dc,unsigned char data);
void GotoXY(int x,int y);
void LCDClr();
void LCDChar(char character);
void LCDString(char *characters);
void LCDInit(unsigned char contrast,unsigned char coefficent,unsigned char bias);
Các chương trình chức năng con chúng ta cần gồm có:
-          Delay – Giúp chương trình giữ trạng thái đó trong một số lượng clock nhất định chúng ta đưa vào
-          ShiftOut – Giúp đưa dữ liệu từ vi điều khiển tới LCD
-          LCDWrite – Giúp đưa dữ liệu tới LCD, có thể là dạng Command, hay Data
-          GotoXY – Giúp đưa con trỏ tới tọa độ X,Y nào đó trên màn hình LCD
-          LCDClr – Giúp xóa trắng màn hình
-          LCDChar – Giúp đưa một ký tự lên màn hình
-          LCDString – Giúp đưa một chuỗi ký tự lên màn hình
-          LCDInit – Đưa các giá trị khởi tạo LCD

2.2.3.1.            Delay

Chương trình con này giúp giữ trạng thái LCD trong một khoảng thời gian tương ứng với số xung clock nào đó.
void Delay(volatile long time)
{
     while(time>0)
            time--;
}
Hàm chức năng này hoạt động theo nguyên tắc là nhận một giá trị biến “time” ở đầu vào. Biến này sẽ được đưa vào vào lặp while để xét điều kiện, nếu số lượng vẫn lớn hơn 0 thì biến while sẽ tiếp tục xét tiếp liên tục. Sau mỗi lần xét như vậy, biến time sẽ bị giảm giá trị đi 1. Do đó, chương trình sẽ giữ trạng thái LCD trong vòng “time” lần xung clock.

2.2.3.2.            ShiftOut

Hàm chức năng này có nhiệm vụ đưa dữ liệu ra vi điều khiển tới LCD qua port từng bit một. Sử dụng phương pháp dịch bit, đúng như tên gọi của hàm:
void ShiftOut(unsigned char txdata)
{
     unsigned int i;
     LCD_OUT clr CLK;
     LCD_OUT set DIN;
     for(i=8;i>0;i--)
     {
            if((txdata & BIT7) == BIT7)
                   LCD_OUT set DIN;
            else
                   LCD_OUT clr DIN;
            LCD_OUT set CLK;
            LCD_OUT clr CLK;
            txdata<<=1;
     }
}
Hàm nhận dữ liệu đầu vào dạng 2 bit hexa, tương ứng, ta sẽ được dạng 8 bit binary.
Nguyên tắc làm việc của hàm chức năng này là:
-          Đầu tiên, ta hạ bit CLK xuống 0 và nâng mức DIN lên 1 để bắt đầu cho phép quá trình nạp dữ liệu.
-          Sau đó, sử dụng một hàm for, chạy 8 lần, tương ứng 8 bit được truyền. 8 bit này sẽ được AND với BIT7 (0x80 = 1000.0000), tức là chỉ lấy bit MSB để xét, nếu như bit này có giá trị 1, thì sau khi AND sẽ cho ra kết quả là 1, tương ứng, đầu ra LCD_OUT sẽ được set lên mức 1. Tương tự như vậy, ta có truòng hợp ngược lại.
-          Sau khi xét xong một bit, còn 7 bit như vậy nữa, để tiếp tục xét, ta sử dụng toán tử dịch trái bit đi 1, do đó, bit tiếp theo giờ sẽ là MSB, cứ như vậy cho tới hết 8 bit cần xét.

2.2.3.3.            LCDWrite

Hàm chức năng này có nhiệm vụ giúp viết dữ liệu vào LCD, dữ liệu đó vừa có thể là Command, vừa có thể là Data, tùy theo biến số ta đưa vào quy định dạng dữ liệu gửi đi.
void LCDWrite(char dc,unsigned char data)
{
     if(dc)
            LCD_OUT set DC;
     else
            LCD_OUT clr DC;
     LCD_OUT clr CE;
     ShiftOut(data);
     LCD_OUT set CE;
}
Hàm chức năng này có nguyên tắc hoạt động khá đơn giản:
-          Đầu tiên hàm sẽ xét biến dc đưa vào, nếu dc là 1 (ta lúc đầu đã tham chiếu là LCD_DATA) thì dữ liệu ra sẽ là Data, và đầu ra LCD sẽ set bit DC lên mức 1. Tương tự như vậy ta có trường hợp ngược lại
-          Sau đã xác định được dạng dữ liệu, hàm đưa tín hiệu hạ mức CE xuống 0 để chọn chip hoạt động, và bắt đầu gọi hàm truyền dữ liệu ShiftOut với giá trị được đưa vào là dữ liệu cần truyền.
-          Sau hàm ShiftOut đã thực hiện xong nhiệm vụ, ta lại set tín hiệu CE lên mức 1, bỏ chọn chip.

2.2.3.4.            GotoXY

Hàm chức năng này có tác dụng đưa con trỏ ghi dữ liệu ra màn hình tới vị trí cần ghi:
void GotoXY(int x,int y)
{
     LCDWrite(LCD_CMD,0x80 | x);
     LCDWrite(LCD_CMD,0x40 | y);
}
Như được thấy ở trong hàm, ta sử dụng 2 câu lệnh Command gửi tới LCD. Tra trong bảng Command tương ứng, ta được đó là 2 lệnh set giá trị địa chỉ cho RAM, tương ứng với địa chỉ dữ liệu sẽ được hiển thị lên màn hình, ở đây ta hiểu như đó sẽ là con trỏ ghi dữ liệu của ta lên màn hình. Theo như bảng, ta sẽ có được 5 dòng và 84 cột địa chỉ để ghi dữ liệu.

2.2.3.5.            LCDClr

Hàm chức năng này giúp chúng ta xóa sạch dữ liệu trong RAM của LCD, tương đương với việc xóa màn hình LCD.
void LCDClr()
{
     unsigned int i;
     for(i=0;i<504;i++)
     {
            LCDWrite(LCD_DATA,0x00);
     }
     GotoXY(0,0);
}
Nguyên tắc xóa khá đơn giản, ta chỉ việc sử dụng vòng lặp for chạy 504 lần ( (84x4)/8 = 504 ), mỗi lần chạy như vậy, sẽ đưa ra một lệnh nhập dữ liệu Data, nhưng dữ liệu đưa vào là trắng trơn, nên tất cả giá trị trong thanh RAM sẽ về 0 hết sau 504 lần lặp.
Sau khi đã hoàn thành việc xóa RAM, ta đưa lại con trỏ về vị trí khởi điểm là (0,0) với lệnh GotoXY đã viết.

2.2.3.6.            LCDChar

Hàm chức năng LCDChar có nhiệm vụ đưa ra màn hình một ký tự (character). Biến đưa vào sẽ dưới dạng biến địa chỉ, và thông qua địa chỉ bảng mã ASCII chúng ta sử dụng, sẽ cho ra được ký tự trên màn hình.
void LCDChar(char character)
{
     unsigned int i;
     LCDWrite(LCD_DATA,0x00);
     for(i=0;i<5;i++)
            LCDWrite(LCD_DATA,ASCII[character - 0x20][i]);
     LCDWrite(LCD_DATA,0x00);
}
Nguyên tắc thực hiện lệnh:
-          Đầu tiên gọi một lệnh ghi dữ liệu ở cột đầu tiên là dữ liệu trắng, tương ứng với khoảng trống cần thiết để tách biệt với ký tự nếu có phía trước.
-          Sau đó, ta sử dụng vòng lặp for để ghi tiếp 5 cột phía sau cột trắng đó để biểu thị ký tự của chúng ta. Do bảng mã chúng ta có địa chỉ bắt đầu từ 0x20, nên chúng ta chỉ việc lấy biến địa chỉ đưa vào trừ đi 0x20 là ta sẽ có số thứ tự trong mảng ASCII. Tương tự, có biến i là số cột hiển thị, i sẽ có giá trị chạy từ 0 tới 4. Cần để ý thêm là mảng ASCII của chúng ta là mảng ASCII [ ] [5] – tức là một ma trận có nhiều hàng, mỗi hàng có 5 cột, và địa chỉ của ký tự đầu tiên bắt đầu từ 0x20. Như hình bên, ta sẽ thấy rằng RAM của MSP430 có địa chỉ suất phát từ 0x20 (0200h) nên ta sẽ lấy được đại chỉ biến đầu tiên bắt đầu từ đó.
-          Cuối cùng, ta cũng thêm một lệnh đưa dữ liệu trắng vào cột cuối cùng trong 7 cột hiển thị chữ để giúp tách biệt chữ trước với chữ sau trong quá trình hiển thị nội dung nếu có trên màn hình LCD.

2.2.3.7.            LCDString

Hàm LCDString có chức năng hiển thị một xâu ký tự (string) ra màn hình:
void LCDString(char *characters)
{
     while(*characters)
            LCDChar(*characters++);
}
Nguyên tắc của việc nhập một xâu ký tự dựa trên việc tạo một vòng lặp liên tiếp để hiển thị từng ký tự một cho tới khi hoàn thiện được chuỗi ký tự mong muốn của chúng ta.
Biến đưa vào ở đây sẽ là một con trỏ (pointer) và con trỏ này sau mỗi lần đưa ra ký tự sẽ được tăng giá trị lên 1, cho tới khi con trỏ đạt một giá trị rỗng ( =0 ) thì vòng lặp sẽ dừng lại, tươnng ứng với việc kết thúc chuỗi ký tự.

2.2.3.8.            LCDInit

Hàm LCDInit có chức năng khởi tạo LCD, các biến được đưa vào là các tham số để điều chỉnh các giá trị contrast, coefficient, bias.
void LCDInit(unsigned char contrast,unsigned char coefficient,unsigned char bias)
{
     LCD_DIR set RST + CE + DC + CLK + DIN
     LCD_OUT clr RST;
     LCD_OUT set RST;
     LCDWrite(LCD_CMD,0x21);
     LCDWrite(LCD_CMD,0x80 + contrast);
     LCDWrite(LCD_CMD,0x04 + coefficient);
     LCDWrite(LCD_CMD,0x10 + bias);
     LCDWrite(LCD_CMD,0x20);
     LCDWrite(LCD_CMD,0x0C);
}
Bước đầu tiên trong việc thiết lập LCD, như thường lệ là phải xét các bit hoạt động điều kiện cho LCD.
Đầu tiên, ta set các bit RST, CE, DC, CLK, DIN tương ứng với các port thành đầu ra bằng cách thay đổi các bit tương ứng ở LCD_DIR, tương ứng với port 1 của MSP430G2553.
Sau đó, ta hạ mức RST rồi lại nâng lên mức 1 để tạo tín hiệu reset cho LCD.
Tiếp theo, ta đưa ra các lệnh thiết lập cho LCD dưới dạng Command:
-          0x21 – Chọn chế độ active cho chip, chế độ địa chỉ hàng ngang và tập lệnh mở rộng (để chỉnh sửa các tham số hệ thống)
-          0x80 + contrast – Chỉnh sửa độ tương phản cho màn hình
-          0x04 + coefficient – Chỉnh sửa thông số nhiệt cho LCD
-          0x10 + bias – Chỉnh sửa thông số bias cho LCD
-          0x20 – Tương tự như lệnh 0x21, nhưng giờ ta bỏ chế độ tập lệnh mở rộng sau khi đã chỉnh sửa các tham số hệ thống xong.
-          0x0C – Chọn chế độ hiển thị các bit 1 là màu đen và bit 0 là màu trắng trên màn hình LCD, tức là chữ đen và nền trắng. Ngược lại thì ta sẽ có lệnh 0x0D

2.2.4.      Chương trình chính

Cuối cùng, chương trình chính của chúng ta chỉ còn bao gồm những bước cuối là:
-          Dừng Watchdog Timer và thiết lập xung Clock
-          Khởi tạo màn hình LCD
-          Thực hiện các lệnh dữ liệu ra màn hình
void main(void)
{
     WDTCTL = WDTPW + WDTHOLD;
     BCSCTL1 = 0xFF;
     DCOCTL = 0x0F;

     LCDInit(60,0,4);

     while(1)
     {
            LCDClr();
            GotoXY(21,0);
            LCDString("Luatdh");
            GotoXY(24,1);
            LCDString("Hanoi");
            GotoXY(7,2);
            LCDString("University");
            GotoXY(35,3);
            LCDString("of");
            GotoXY(7,4);
            LCDString("Sience and");
            GotoXY(7,5);
            LCDString("Technology");
            Delay(700000);

            LCDClr();
            unsigned int i;
            for(i=0;i<504;i++)
                   LCDWrite(LCD_DATA,logo_footer_blizzard[i]);
            Delay(700000);

            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);

            LCDClr();
            for(i=0;i<504;i++)
                   LCDWrite(LCD_DATA,Terran_bm[i]);
            Delay(700000);

            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);

            LCDClr();
            for(i=0;i<504;i++)
                   LCDWrite(LCD_DATA,Zerg_bm[i]);
            Delay(700000);

            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);

            LCDClr();
            for(i=0;i<504;i++)
                   LCDWrite(LCD_DATA,Protoss_bm[i]);
            Delay(700000);

            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0D);
            Delay(700000);
            LCDWrite(LCD_CMD,0x0C);
            Delay(700000);
     }
}
Các bạn có thể download toàn bộ project mẫu ở đây : MSP430vsLCDNokia5110

1 comment:

END COMMENT FACEBOOK-->