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:
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.
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:
Das komplette LCD-Interface wird mit Hilfe eines Zustandsautomaten entworfen, wobei der Ablauf folgendermaßen definiert ist:
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