本文隸屬於AVR單片機教程系列。
顯示屏
開發板套件里有兩塊屏幕,大的是LCD(液晶顯示),小的是OLED(有機發光二極管)。正與你所想的相反,短小精悍的比較貴,而本講的主題——LCD1602——功能較少,使用起來也簡單很多。
這塊屏幕的顯示是以字符為單位的。每個字符都是8像素高,5像素寬。1602這個名字,來源於顯示字符的數量,共2行,每行16個字符。出售1602的商家提供了一份文檔:提取碼8c1u。
硬件
一個典型的1602顯示屏有16個引腳(還有些模塊是用串行總線驅動的):
名稱 | 功能 | 連接 |
---|---|---|
VSS | 電源地 | GND |
VDD | 正電源 | VCC(5V) |
VO | 對比度調整 | 左側的電位器,其左端接GND,右端接VCC |
RS | 數據/指令選擇 | PB0 |
R/W | 讀/寫選擇 | PB1 |
E | 使能 | PB2 |
D0~D7 | 雙向並行數據線 | 74HC595的輸出,通過exout_write 輸出 |
A | 背光LED正極 | NPN三極管的發射極,其集電極接VCC,基極接BAK |
K | 背光LED負極 | GND |
很復雜吧?好在開發板已經處理好了這些,我們只需要關注2組線:RS
、R/W
、E
這3根控制線,通過DDRB
和PORTB
來操控,和D0
~D7
這8跟數據線,通過exout_write
來以字節為單位輸出(在這第一期的最后一篇中,我終於成功地將“盡量減少接線”的原則在第一期中貫徹到底了)。其他的還有對比度CON
接口,可以不外接,還有背光BAK
接口,可以接5V或3.3V以開啟背光。當然,如果你是非主流,你可以把CON
接到DAC上,把BAK
接到PWM上。
此外,還需要把擴展輸出的最高位(標注Ext Out 7
)接到一個單片機引腳上。關於為什么會有這種詭異的接法,這是設計時的失誤(也可能是不得已吧,畢竟單片機的32個IO已經占滿了),參見:一個低電平引發的思考。
協議
1602與單片機之間是通過並行總線通信的。AVR單片機硬件上不支持並行總線,需要通過軟件模擬時序來實現。
寫操作的時序如下:
進行一個寫操作,需要先讓RS
根據寫的類型設置電平,R/W
輸出低電平,D0
~D7
輸出要發送的數據,然后在E
的上升沿數據被對方讀取,並保持R/W
與D0
~D7
電平不變,直到E
的下降沿之后。兩次E
的上升沿之間至少需要400us時間間隔。
1602共有8條指令,都是一字節長度的。從高位到低位,每一條指令都由若干個0
、一個1
和有效指令組成,使得沒有兩條指令會有相同的二進制表示。這種編碼方式成為前綴碼,UTF-8也用了類似的編碼。
前導0個數 | 功能 |
---|---|
0 | 設置DDRAM(顯示存儲器)地址 |
1 | 設置CGRAM(字符發生存儲器)地址 |
2 | 設置總線寬度、字符行數、字符大小 |
3 | 設置滾動對象(光標或畫面)、滾動方向 |
4 | 設置畫面、光標、閃爍的開關 |
5 | 設置AC(地址計數器)是否變化與變化方向 |
6 | 將AC清零 |
7 | 清屏 |
1602插上開發板后只有第一行有黑色背景。我們先把它設置為2行輸出,這是最簡單的有視覺效果的指令。
#include <avr/io.h>
#include <ee1/lcd.h>
inline void short_delay()
{
asm("nop");
asm("nop");
asm("nop");
asm("nop");
}
int main()
{
DDRB |= 0b111; // PB2:0 for LCD control: E, R/W, RS
exout_init(); // Ext Out 7:0 for LCD instructions/data
#define LCD_CONTROL(x) (PORTB = (PORTB & ~0b111) | (x))
LCD_CONTROL(0b000);
short_delay();
LCD_CONTROL(0b100);
short_delay();
exout_write(0b00111000); // 8-bit, 2 lines, 5x7 font
short_delay();
LCD_CONTROL(0b000);
}
在這段代碼中,asm("nop")
是一句匯編語句nop
,代表這個CPU周期內不執行操作,可以用來短時間而精確地延時。在控制信號與數據信號之間加入短暫的延時,使時序符合1602的要求。
其他指令見數據手冊,這份數據手冊本身就是很好的教程。需要提醒的是,顯示屏內的控制器執行指令需要一定時間,程序在發送下一指令之前,應通過RS為低電平的讀取操作來獲取其狀態,並等待直到狀態為空閑。
軟件
開發板的庫不僅包裝了這些硬件操作,還添加了對字符串的支持,包括對\b
、\t
、\n
和\r
等不可打印字符的處理。
下面我們用1602顯示屏來完成一個小項目,在之前用按鍵控制蜂鳴器的基礎上,加入LCD顯示與背光動態效果。
#include <string.h>
#include <ee1/lcd.h>
#include <ee1/pwm.h>
#include <ee1/button.h>
#include <ee1/tone.h>
#include <ee1/delay.h>
int main()
{
lcd_init(PIN_3);
lcd_set_status(true, false, false);
button_init(PIN_6, PIN_7);
wave_mode(WAVE_0, WAVE_MODE_PWM);
wave_mode(WAVE_1, WAVE_MODE_TONE);
char display[6] = {[0] = '\r', [5] = '\0'}; // 1. 為什么要開6字節字符數組?
char* notes = display + 1; // 2. display數組是如何使用的?
uint8_t brightness; // 3. 這一行有什么問題?
uint16_t frequency[] = {262, 330, 392, 523};
uint16_t buzzer = 0, tone;
while (1)
{
memset(notes, ' ', 4); // 4. memset是什么函數?
tone = 0;
for (uint8_t i = 0; i != 4; ++i)
if (button_down(i))
{
notes[i] = 0xFF; // 5. 0xFF是什么字符?
brightness = 255;
tone = frequency[i]; // 6. 如果有多個按鍵按下,蜂鳴器會響什么音?
}
lcd_print_string(display);
pwm_set(WAVE_0, brightness * brightness >> 8); // 7. 為什么第二個參數不直接寫brightness?
if (tone != buzzer) // 8. 為什么必須引入buzzer變量並在這里作判斷?
{
buzzer = tone;
tone_set(WAVE_1, buzzer);
}
if (brightness)
brightness -= 5;
delay(10);
}
}
在這AVR單片機教程第一期最后一講中,我在代碼中留了8個問題。如果你能全部答上來,說明你對C語言已經有一些了解,熟悉了單片機開發中的部分細節,並具備初步的工程素養了。
作業
思考題:
-
明明可以選擇4位總線來節省IO,為什么開發板上還是8位連接?
-
實現讀取1602空閑狀態。
-
在1602上畫一顆心。
-
修改例程,使1602以滾動的橫線指示按下的按鍵與發出的音符(類似於你玩過或看過的音游)。
擴展閱讀: