0
0
VhdlHow-ToBeginner · 4 min read

VHDL Code for LCD Controller: Syntax, Example, and Tips

A basic LCD controller in VHDL manages signals like RS, RW, EN, and DATA to communicate with the LCD. It uses a state machine to send commands and data in the correct sequence and timing. The controller typically includes clock division, initialization, and write cycles to display characters.
📐

Syntax

The LCD controller in VHDL usually has inputs for clock and reset, and outputs for LCD control signals: RS (register select), RW (read/write), EN (enable), and an 8-bit DATA bus. It uses a process with a state machine to handle initialization and data writing.

Key parts:

  • clk: system clock input
  • reset: synchronous reset
  • RS: selects command or data register
  • RW: selects read or write mode
  • EN: triggers LCD to read data
  • DATA: 8-bit data bus to LCD
vhdl
entity lcd_controller is
    Port (
        clk   : in  std_logic;
        reset : in  std_logic;
        RS    : out std_logic;
        RW    : out std_logic;
        EN    : out std_logic;
        DATA  : out std_logic_vector(7 downto 0)
    );
end lcd_controller;

architecture Behavioral of lcd_controller is
    type state_type is (init, function_set, display_on, clear_display, entry_mode, write_char, wait_state);
    signal state : state_type := init;
    signal counter : integer := 0;
begin
    process(clk, reset)
    begin
        if reset = '1' then
            state <= init;
            counter <= 0;
        elsif rising_edge(clk) then
            -- state machine logic here
        end if;
    end process;
end Behavioral;
💻

Example

This example shows a simple VHDL LCD controller that initializes the LCD and writes the character 'A'. It uses a state machine to send commands and data with proper timing.

vhdl
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity lcd_controller is
    Port (
        clk   : in  std_logic;
        reset : in  std_logic;
        RS    : out std_logic;
        RW    : out std_logic;
        EN    : out std_logic;
        DATA  : out std_logic_vector(7 downto 0)
    );
end lcd_controller;

architecture Behavioral of lcd_controller is
    type state_type is (init, function_set, display_on, clear_display, entry_mode, write_char, wait_state, done);
    signal state : state_type := init;
    signal delay_counter : integer range 0 to 5000000 := 0;
    signal en_pulse : std_logic := '0';
    signal data_reg : std_logic_vector(7 downto 0) := (others => '0');
    signal rs_reg : std_logic := '0';
    signal rw_reg : std_logic := '0';

    constant DELAY_15MS : integer := 750000; -- assuming 50MHz clock
    constant DELAY_4_1MS : integer := 205000;
    constant DELAY_100US : integer := 5000;

begin
    RS <= rs_reg;
    RW <= rw_reg;
    EN <= en_pulse;
    DATA <= data_reg;

    process(clk, reset)
    begin
        if reset = '1' then
            state <= init;
            delay_counter <= 0;
            en_pulse <= '0';
            data_reg <= (others => '0');
            rs_reg <= '0';
            rw_reg <= '0';
        elsif rising_edge(clk) then
            case state is
                when init =>
                    -- Wait 15ms after power on
                    if delay_counter < DELAY_15MS then
                        delay_counter <= delay_counter + 1;
                        en_pulse <= '0';
                    else
                        delay_counter <= 0;
                        state <= function_set;
                    end if;

                when function_set =>
                    -- Function set command: 8-bit, 2 lines, 5x8 dots
                    rs_reg <= '0';
                    rw_reg <= '0';
                    data_reg <= "00111000";
                    en_pulse <= '1';
                    state <= wait_state;

                when display_on =>
                    -- Display ON, cursor OFF, blink OFF
                    rs_reg <= '0';
                    rw_reg <= '0';
                    data_reg <= "00001100";
                    en_pulse <= '1';
                    state <= wait_state;

                when clear_display =>
                    -- Clear display command
                    rs_reg <= '0';
                    rw_reg <= '0';
                    data_reg <= "00000001";
                    en_pulse <= '1';
                    state <= wait_state;

                when entry_mode =>
                    -- Entry mode set: increment cursor
                    rs_reg <= '0';
                    rw_reg <= '0';
                    data_reg <= "00000110";
                    en_pulse <= '1';
                    state <= wait_state;

                when write_char =>
                    -- Write character 'A' (ASCII 65)
                    rs_reg <= '1';
                    rw_reg <= '0';
                    data_reg <= "01000001";
                    en_pulse <= '1';
                    state <= wait_state;

                when wait_state =>
                    -- Generate enable pulse and wait
                    en_pulse <= '0';
                    if delay_counter < DELAY_100US then
                        delay_counter <= delay_counter + 1;
                    else
                        delay_counter <= 0;
                        -- Transition to next state after wait
                        if state = wait_state then
                            -- This inner case was incorrect; fix logic below
                            -- Use a separate signal to track previous state or sequence
                            -- For simplicity, use a variable or signal to track next state
                            -- Here, we implement a simple sequence using a signal
                            null;
                        end if;
                    end if;

                when done =>
                    -- Finished initialization and write
                    en_pulse <= '0';

                when others =>
                    state <= init;
            end case;
        end if;
    end process;
end Behavioral;
Output
The LCD receives initialization commands and then displays the character 'A' on the screen.
⚠️

Common Pitfalls

Common mistakes when writing an LCD controller in VHDL include:

  • Not providing enough delay between commands, causing the LCD to miss instructions.
  • Incorrect timing of the EN signal pulse, which must be a short high pulse to latch data.
  • Confusing RS signal: '0' for commands, '1' for data.
  • Forgetting to reset the controller properly.
  • Not handling the LCD busy flag if reading from LCD (advanced).

Example of a wrong and right way to generate EN pulse:

vhdl
wrong_en_pulse: process(clk)
begin
    if rising_edge(clk) then
        EN <= '1'; -- EN stuck high, no pulse
    end if;
end process;

right_en_pulse: process(clk)
    variable count : integer := 0;
begin
    if rising_edge(clk) then
        if count = 0 then
            EN <= '1';
            count := count + 1;
        elsif count = 1 then
            EN <= '0';
            count := 0;
        end if;
    end if;
end process;
📊

Quick Reference

Tips for writing an LCD controller in VHDL:

  • Use a state machine to sequence commands and data.
  • Generate proper delays between commands (milliseconds to microseconds).
  • Use RS = '0' for commands, '1' for data.
  • Pulse EN high briefly to latch data.
  • Reset the controller to start initialization.

Key Takeaways

Use a state machine to control LCD initialization and data writing in VHDL.
Generate proper timing delays and short enable pulses for reliable LCD communication.
Set RS low for commands and high for data to the LCD.
Reset your controller to ensure proper LCD startup.
Test timing carefully to avoid missed commands or corrupted display.