Kampis Elektroecke

HD44780 LCD-Interface

Displays mit einem HD44780 LCD-Controller (oder einer kompatiblen Alternative) sind weit verbreitet und finden sich in vielen Bereichen wieder. In diesem Artikel möchte ich die Grundlagen des Display-Controllers näher bringen und zeigen, wie ein einfaches Interface in VHDL entworfen werden kann.

Bei dem HD44780 handelt es sich um einen De-facto- oder Quasi-Standard für die Ansteuerung von alphanumerischen Dot-Matrix-LCDs, welcher vor etlichen Jahren von der Firma Hitachi entwickelt worden ist. Der Controller besitzt ein Display data RAM (DDRAM) das bis zu 80 8-Bit Zeichen speichern kann. Der Inhalt des RAM wird über einen 16-Zeilen, 40-Spalten LCD-Treiber, der mit zusätzlichen Display-Controllern oder entsprechenden Treibern (z. B. HD44100) erweitert werden kann, auf einem Dot-Matrix-Display dargestellt.

Der Display-Controller besitzt zusätzlich ein integriertes ROM mit einem vollständigen ASCII-Zeichensatz und ein RAM, das Character generator RAM (CGRAM), welches bis zu 8 zusätzliche und frei definierbare Zeichen, speichern kann. Für die Adressierung der einzelnen Zeichen im DDRAM werden die folgenden Adressen genutzt:

Displaygröße 1. Zeile 2. Zeile 3. Zeile 4. Zeile
1×20 0x00 – 0x13      
2×20 0x00 – 0x13 0x40 – 0x53    
4×20 0x00 – 0x13 0x40 – 0x53 0x14 – 0x27 0x54 – 0x67

Bei Displaygrößen über 2×8 Zeichen werden außerdem zusätzliche Treiber benötigt und wenn zudem mehr als 80 Zeichen dargestellt werden sollen, so muss auch noch ein weiterer Display-Controller verwendet werden.

Die Kommunikation zwischen MCU und Display-Controller findet über ein paralleles Businterface statt, wobei zwei verschiedene Datenübertragungsmodi genutzt werden können:

  • 4-Bit Datenbus
  • 8-Bit Datenbus

Info:

Der Display-Controller verfügt zudem noch über zwei verschiedene Darstellungsmodi:

  • 5×8-Pixel
  • 5×10-Pixel

Der Einfachheit halber verwende ich in diesem Artikel ausschließlich den 8-Bit Modus mit 5×8-Pixel großen Zeichen. Soll der 4-Bit und/oder der 5×10-Pixel Modus verwendet werden, so muss die Initialisierung und die Datenübertragung zum Display entsprechend angepasst werden.


Hinzu kommen drei Steuersignale für den Display-Controller:

  • Register Select (RS) – Wählt das Zielregister aus.
    • Low – Befehlsregister
    • High – Datenregister
  • Read/Write (RW) – Wählt zwischen einem Lese- und einem Schreibzugriff.
    • Low – Schreibzugriff
    • High – Lesezugriff
  • Enable (E) – Kopiert die Daten in den Display-Controller.
    • Low/High Übergang – Der Display-Controller ließt den Status von RS und RW ein
    • High/Low Übergang – Daten werden ausgegeben oder Daten werden eingelesen (je nach Zustand von RW)

Weiterhin verfügt der Display-Controller über einen kleinen Satz an Kommandos um z. B. den Cursor zu bewegen oder die RAM-Adresse zu setzen.

Befehl RS RW D7 D6 D5 D4 D3 D2  D1 D0 
Clear display 0 0 0 0 0 0 0 0 0 1
Return home 0 0 0 0 0 0 0 0 1 X
Entry mode set 0 0 0 0 0 0 0 1 I/D S
Display on/off 0 0 0 0 0 0 1 D C B
Cursor or display shift
0 0 0 0 0 1 S/C R/L 0* 0*
G/C* PWR* 1* 1*
Function set 0 0 0 0 1 DL N F FT0* FT1*
Set CGRAM address 0 0 0 1 ACG ACG ACG ACG ACG ACG
Set DDRAM address 0 0 1 ADD ADD ADD ADD ADD ADD ADD
Read busy flag & address 0 1 BF AC AC AC AC AC AC AC
Write data to CG or DDRAM 1 0 Datenbyte
Read data from CG or DDRAM 1 1 Datenbyte
*Diese Funktionen stehen bei einigen HD44780 kompatiblen Display-Controllern, nicht aber beim Original HD44780 zur Verfügung.
Abkürzung Beschreibung
I/D Increment (1) / Decrement (0)
S Shift Enable (1) / Disable (0)
D Display On (1) / Off (0)
C Cursor On (1) / Off (0)
B Cursor blink On (1) / Off (0)
S/C Display Shift (1) / Cursor Move (0)
R/L Shift Right (1) / Left (0)
DL 8-Bit Datenbus (1) / 4-Bit (0)
N 2 Zeilen (1) / 1 Zeile (0)
F 5×10 Pixel Zeichen (1) / 5×8 Pixel (0)
ACG CGRAM Adresse
ADD DDRAM Adresse
BF Busy Flag
AC Adresszähler
G/C Grafik Modus (1) / Zeichen Modus (0)
PWR Interner DC/DC On (1) / Off (0)
FT Auswahl des Zeichensatzes.

Bevor der Display-Controller verwendet werden kann, muss dieser Initialisiert werden. Dies erfolgt in zwei Schritten:

  • Initialisierung des Datenbuses. Hier wird festgelegt ob die Kommunikation mit dem Display-Controller über einen 8-Bit Bus (Default) oder über einen 4-Bit Bus stattfinden soll.
  • Initialisierung des Display-Controllers. In diesem Schritt wird der Controller initialisiert. Es werden die Displayparameter übertragen und die gewünschte Betriebsart eingestellt.

Für die Initialisierung des Datenbuses im 8-Bit Modus muss der nachfolgende Ablauf eingehalten werden:

Anschließend geht es mit der Initialisierung des Display-Controllers weiter, bei der verschiedene Parameter, wie z. B. die Einstellungen zum Cursor oder die Anzahl der Zeilen vom Display, gesetzt werden. Das verwendete Display (hier ein EA W162) soll folgendermaßen konfiguriert werden:

Befehl Datenwort Funktion
Function set 0x39
  • Busbreite 8 Bit (DL = 1)
  • 2 Zeilen (N = 1)
  • 5×8 Pixel pro Zeichen (F = 1)
  • Europäischer Zeichensatz (FT1 = 0, FT0 = 1)
Display on/off control 0x0F
  • Display an (D = 1)
  • Cursor an (C = 1)
  • Blinkender Cursor an (B = 1)
Entry mode set 0x06
  • Increment cursor position (I/D = 1)
  • Shift deaktivieren (S = 0)
Cursor or display shift 0x17
  • Zeichen Modus
  • Interner DC/DC an

Das komplette LCD-Interface wird mit Hilfe eines Zustandsautomaten entworfen, wobei der Ablauf folgendermaßen definiert ist:

Reset
  • Zurücksetzen aller Signale
  • µs-Zähler zurücksetzen
  • LCD-Bus zurücksetzen
Initialize
  • LCD-Bus in den 8-Bit Modus setzen
  • Default-Konfiguration in den Display-Controller laden
Busy
  • Busy-Flag des Display-Controllers abfragen
Idle
  • Ready Signal setzen
  • Auf Valid Signal warten und Daten speichern
Transmit Daten an den LCD-Controller senden

Nach einem Reset wechselt der Zustandsautomat in den entsprechenden Zustand, setzt alle Signale zurück und verbleibt dort 50 ms. Am Ende des Resets wird der Wert 0x30 auf den Datenbus geschrieben und in den nächsten Zustand gewechselt.

process(Clock, nReset)
    variable usCounter  : INTEGER := 0;
begin
    if(nReset = '0') then
        usCounter := 0;
        Ready <= '0';
        LCD_E <= '0';
        LCD_RS <= '0';
        LCD_RW <= '0';
        LCD_Data <= (others => '0');
        Data_Int <= (others => '0');
        CurrentState <= Reset;
    elsif(rising_edge(Clock)) then
        case CurrentState is
            when Reset =>
                usCounter := usCounter + 1;
                if(usCounter < (RESET_DELAY_1 * CLOCK_FREQ)) then
                    CurrentState <= Reset;
                else
                    usCounter := 0;
                    LCD_Data <= x"30";
                    CurrentState <= Initialize;
                end if;

Im nächsten Zustand wird die Initialisierung des LCD-Bus und die Initialisierung des Display-Controllers vorgenommen. Dazu werden zuerst die drei notwendigen Datenbytes für die Busbreite und dann die entsprechende Konfiguration übertragen. Wenn alle Daten übertragen wurden

when Initialize =>
    usCounter := usCounter + 1;
    if(usCounter < (RESET_DELAY_2 * CLOCK_FREQ)) then
        LCD_E <= '1';
    elsif(usCounter < ((RESET_DELAY_2 + 10) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY_2 + RESET_DELAY_3 + 10) * CLOCK_FREQ)) then
        LCD_E <= '1';
    elsif(usCounter < ((RESET_DELAY_2 + RESET_DELAY_3 + 20) * CLOCK_FREQ)) then
        LCD_E <= '0';         
    elsif(usCounter < ((RESET_DELAY_2 + RESET_DELAY_3 + RESET_DELAY_4 + 20) * CLOCK_FREQ)) then
        LCD_E <= '1';
    elsif(usCounter < ((RESET_DELAY_3 + RESET_DELAY_3 + RESET_DELAY_4 + 30) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY + 40) * CLOCK_FREQ)) then
        LCD_E <= '1';
        LCD_Data <= Configs(CONFIG, 0);
    elsif(usCounter < ((RESET_DELAY + 50) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY + 60) * CLOCK_FREQ)) then
        LCD_E <= '1';
        LCD_Data <= Configs(CONFIG, 1);
    elsif(usCounter < ((RESET_DELAY + 70) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY + 80) * CLOCK_FREQ)) then
        LCD_E <= '1';
        LCD_Data <= Configs(CONFIG, 2);
    elsif(usCounter < ((RESET_DELAY + 90) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY + 100) * CLOCK_FREQ)) then
        LCD_E <= '1';
        LCD_Data <= Configs(CONFIG, 3);
    elsif(usCounter < ((RESET_DELAY + 110) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY + 120) * CLOCK_FREQ)) then
        LCD_E <= '1';
        LCD_Data <= Configs(CONFIG, 4);
    elsif(usCounter < ((RESET_DELAY + 130) * CLOCK_FREQ)) then
        LCD_E <= '0';
    elsif(usCounter < ((RESET_DELAY + 140) * CLOCK_FREQ)) then
        LCD_E <= '1';
        LCD_Data <= Configs(CONFIG, 5);
    elsif(usCounter < ((RESET_DELAY + 150) * CLOCK_FREQ)) then
        LCD_E <= '0';
    else
        usCounter := 0;
        CurrentState <= WaitBusy;
    end if;

Am Ende der Konfiguration wird das Busy-Flag des Display-Controllers abgefragt. Dazu wird das RW-Signal gesetzt und das gesendete Datenbyte eingelesen. Der Zustandsautomat verbleibt so lange in dem Zustand WaitBusy bis das Busy-Flag vom Display-Controller gelöscht wurde und er die interne Befehlsverarbeitung abgeschlossen hat. Erst dann wechselt der Zustandsautomat in den Zustand Idle.

when WaitBusy =>
    usCounter := usCounter + 1;
    if(usCounter < (10 * CLOCK_FREQ)) then
        LCD_RS <= '0';
        LCD_RW <= '1';
        LCD_E <= '1';
        LCD_Data <= (others => 'Z');
    elsif(usCounter < (20 * CLOCK_FREQ)) then
        LCD_E <= '0';
    else
        usCounter := 0;
        if(LCD_Data(7) = '1') then
            Ready <= '0';
            CurrentState <= WaitBusy;
        else
            Ready <= '1';
            CurrentState <= Idle;
        end if;
    end if;

Im Zustand Idle wartet der Zustandsautomat dann so lange bis das Valid-Signal auf High gezogen wird. Erst dann kopiert der Zustandsautomat den Zustand des Data-Buses in einen internen Speicher, setzt den RS-Pin entsprechend des Signals SendCommand und löscht das Ready-Signal.

when Idle =>
    if(Valid = '1') then
        Ready <= '0';
        Data_Int <= Data;
        if(SendCommand = '1') then
            LCD_RS <= '0';
        else
            LCD_RS <= '1';
        end if;

        CurrentState <= Transmit;
    else
        Ready <= '1';
        CurrentState <= Idle;
    end if;

Danach wechselt der Zustandsautomat in den Zustand Transmit und beginnt die Datenübertragung an den Display-Controller. 

when Transmit =>
    usCounter := usCounter + 1;
    if(usCounter < (10 * CLOCK_FREQ)) then
        LCD_RW <= '0';
        LCD_E <= '1';
        LCD_Data <= Data_Int;
    elsif(usCounter < (20 * CLOCK_FREQ)) then
        LCD_E <= '0';
    else
        usCounter := 0;
        CurrentState <= WaitBusy;
    end if;

Sobald die Daten übertragen wurden wechselt der Zustandsautomat zurück in den Zustand WaitBusy um auf das Ende der Datenverarbeitung durch den Display-Controller zu warten.

Damit ist das Interface zum Ansteuern des Display-Controllers fertig und kann verwendet werden. Dazu wird ein weiterer Zustandsautomat definiert, der nach und nach alle Daten aus einem Speicher in den Display-Controller kopiert.

entity Top is
    Port (  Clock   : in STD_LOGIC;
            ResetN  : in STD_LOGIC;
            LCD_RS  : out STD_LOGIC;
            LCD_E   : out STD_LOGIC;
            LCD_RW  : out STD_LOGIC;
            LCD_Data: inout STD_LOGIC_VECTOR(7 downto 0)
            ); 
end Top;

architecture Top_Arch of Top is
    type LCD_Data_t is record
        Data        : STD_LOGIC_VECTOR(7 downto 0);
        IsCommand   : STD_LOGIC;
    end record;

    type RAM_t is array(0 to 13) of LCD_Data_t;
    type State_t is (Reset, WaitDisplay, Send, Finish);

    signal CurrentState     : State_t := Reset;
    signal RAM              : RAM_t := ((x"48", '0'), (x"65", '0'), (x"6C", '0'), (x"6C", '0'), (x"6F", '0'), 
                                        (x"2C", '0'), (x"20", '0'), (x"57", '0'), (x"6F", '0'), (x"72", '0'), 
                                        (x"6C", '0'), (x"64", '0'), (x"21", '0'), (x"02", '1'));
    signal Data             : STD_LOGIC_VECTOR(7 downto 0);
    signal Address          : UNSIGNED(6 downto 0) := "0000010";
    signal Ready            : STD_LOGIC;
    signal Valid            : STD_LOGIC := '0';
    signal SendCommand      : STD_LOGIC := '0';
    signal AddressSet       : STD_LOGIC := '0';

    component LCD_Controller is
        Generic (   CLOCK_FREQ  : INTEGER := 125
                    );
        Port (  Clock   : in STD_LOGIC;
                ResetN  : in STD_LOGIC;
                Data    : in STD_LOGIC_VECTOR(7 downto 0) := (others => '0');
                Ready   : out STD_LOGIC;
                Valid   : in STD_LOGIC;
                SendCommand : in STD_LOGIC;
                LCD_RS  : out STD_LOGIC;
                LCD_E   : out STD_LOGIC;
                LCD_RW  : out STD_LOGIC;
                LCD_Data: inout STD_LOGIC_VECTOR(7 downto 0)
                );
    end component;
begin
    LCD : LCD_Controller generic map (  CLOCK_FREQ => 125
                                        )
                         port map(  Clock => Clock,
                                    ResetN => ResetN,
                                    Data => Data,
                                    Ready => Ready,
                                    Valid => Valid,
                                    SendCommand => SendCommand,
                                    LCD_RS => LCD_RS,
                                    LCD_E => LCD_E,
                                    LCD_RW => LCD_RW,
                                    LCD_Data => LCD_Data
                                    );
    process(Clock)
        variable Index : INTEGER := 0;
    begin 
        if(ResetN = '0') then
            CurrentState <= Reset;
        elsif(rising_edge(Clock)) then
            case CurrentState is
                when Reset =>
                    Valid <= '0';
                    Data <= (others => '0');
                    CurrentState <= WaitDisplay;
                when WaitDisplay =>
                    Valid <= '0';
                    if(Ready = '1') then
                        if(AddressSet = '0') then
                            AddressSet <= '1';
                            SendCommand <= '1';
                            Data <= '1' & STD_LOGIC_VECTOR(Address);
                        else
                            SendCommand <= RAM(Index).IsCommand;
                            Data <= RAM(Index).Data;
                            Index := Index + 1;
                        end if;
                        Valid <= '1';
                        CurrentState <= Send;
                    else
                        CurrentState <= WaitDisplay;
                    end if;
                when Send =>
                    if(Ready <= '0') then
                        if(Index < RAM'length) then
                            CurrentState <= WaitDisplay;
                        else
                            CurrentState <= Finish;
                        end if;
                    else
                        CurrentState <= Send;
                    end if;
                 when Finish =>
                    Valid <= '0';
                end case;
            end if;
        end if;
    end process;
end Top_Arch;

In diesem Beispiel wird der Text „Hello, World!“ auf dem angeschlossenen Display ausgegeben und anschließend der Cursor zurück auf die Home-Position gesetzt.

Das komplette Projekt steht in meinem GitHub-Repository zum Download bereit.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert