Interface d’un écran LCD LT-24 avec un FPGA DE0-Nano

J’étais un peu perplexe lors de l’interfaçage de cette partie du projet. La documentation fournie par Terasic était très superficielle, se contentant de montrer les exemples, codés avec un processeur NIOS (donc, par définition, complètement obscur). Pour ma compréhension personnelle du fonctionnement des FPGA, je voulais que toute la logique soit explicitée en VHDL. Après une recherche approfondie des internets, je suis tombé sur cette « Application Note » provenant d’ALSE. Ils ont enlevé quelques parties du code, pour une raison ou une autre, mais avec un peu de déduction, c’est possible de le reconstruire. Sans ce document, je n’aurais pas réussi à faire grand chose. Ils ont déchiffré le datasheet de plus de 200 pages de l’ ILI9341 pour sortir les registres dans lequel il faut absolument écrire pour initialiser l’écran.

Le programme d’ALSE utilise entre autres une machine d’états pour envoyer des commandes par le port d’interface parallèle, en tenant compte de toutes les spécificités du contrôleur. Cela m’a permis tout d’abord de comprendre comment programmer une machine d’état en VHDL (mon cours de circuits logique était loin, et on n’a jamais vu ça!)

Un compteur (tickclk) permet de produire une horloge qui produit un pulse à toutes les 1ms, ce qui permet de passer de commande à commande plutôt lentement.

Un array de commandes contenant à la fois le délai nécessaire après l’écriture de celle-ci sur le port, le contenu (adresse ou data) et la valeur en 16 bits à écrire permet de produire toutes les séquences d’écriture nécessaire pour écrire dans tous les registres souhaités. Ensuite, il suffit de lire le datasheet! Le programme ci-dessous est fortement inspiré de l’exemple d’ALSE et permet d’afficher quelque pixels avec la couleur de notre choix.

— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity lcdtest is
    port ( Clk : in std_logic;
        Rst : in std_logic;
        ——————–
        — LT24 graphic Controller
        LT_DBi : out std_logic_vector(15 downto 0); — we use it as OUT only
        LT_WR_n : out std_logic; — aka WRX
        LT_RD_n : out std_logic; — aka RDX
        LT_RS : out std_logic; — aka D/CX
        LT_CS_n : out std_logic; — aka CSX
        LT_RESET_n : out std_logic; — aka RESX
        LT_LCD_ON : out std_logic); — Transistor to drive LCD lighting (1 = on)
end lcdtest;
— architecture
architecture my_lcdtest of lcdtest is
    subtype Cmd_t is std_logic_vector (28 downto 0);
    — Dly_ms(12) & C/D & Data(16) – 29 bits, could be a record
    type Commands_t is array (natural range <>) of Cmd_t;
    constant Commands : Commands_t := (
    — delay_ms D/Cd_n DB — p.83 datasheet ILI9341
    x »001″ & ‘0’ & x »0011″, — Exit Sleep
    — x »100″ & ‘0’ & x »0001″, — Software reset
    x »800″ & ‘0’ & x »0029″, — Display ON
    x »001″ & ‘0’ & x »003A », — Set COLMOD register
    x »000″ & ‘1’ & x »0055″, — non-default 16 bits RGB data
    x »000″ & ‘0’ & x »0036″, — Memory Access Control
    — x »000″ & ‘1’ & x »0008″, — non-default BGR filter !
    x »000″ & ‘1’ & x »0028″, — non-default BGR filter + row/col inversion for putting the display in 320×240 mode
    x »000″ & ‘0’ & x »00F2″, — Enable 3G register
    x »000″ & ‘1’ & x »0000″, — non-default = disable 3 gamma — à voir plus tard si je veux une correction gamma ou pas, pour l’instant on va laisser ça comme ça
    x »000″ & ‘0’ & x »002A », — Set Column address
    x »000″ & ‘1’ & x »0000″, —
    x »000″ & ‘1’ & x »0010″, — 16
    x »000″ & ‘0’ & x »002B », — Set Page address
    x »000″ & ‘1’ & x »0000″, —
    x »000″ & ‘1’ & x »000A », — 10
    x »000″ & ‘0’ & x »002C », — Memory Write
    x »000″ & ‘1’ & x »FFFF », — WHITE
    x »000″ & ‘1’ & x »FFFF », — WHITE
    x »000″ & ‘1’ & x »FFFF », — WHITE
    x »000″ & ‘1’ & x »FFFF », — WHITE
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »F000″, — color
    x »000″ & ‘1’ & x »F000″, — color
    x »000″ & ‘1’ & x »F000″, — color
    x »000″ & ‘1’ & x »F000″, — color
    x »000″ & ‘1’ & x »F000″, — color
    x »000″ & ‘1’ & x »F000″, — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F », — color
    x »000″ & ‘1’ & x »000F »); — color
    type state_type is (Boot,Idle,GetCmd,Dly,Wr1,Wr2,Done,Done1); — All the states of the FSM
    signal State : state_type;
    signal Index : integer range 0 to Commands’length; — Index of the command set
    signal Cntr : integer range 0 to to_integer(x »FFF »); — Count of delay between instructions
    signal Cycle : integer range 0 to 255; — Counter for the write cycle
    signal Cmd : Cmd_t;
    constant Nsetup : integer := 5; — Write cycle, try to reduce it to speed up
    constant Nhold : integer := 5;
    signal Tick1ms : std_logic;
— —————————
— ILI9341 interface
— —————————
— CSX can be kept low
— Write cycle > 66ns = 100 ns (5c = 2 + 3) can work
— Tdst data setup > 10 ns
— Tdht data hold > 10 ns
— /!\ Data read (not used here) is SLOW (500 ns)
begin
    LT_RD_n <= '1'; -- we don't read
    tickclk : process(Clk)
    variable clockcount : integer range 0 to 49999 := 49999;
    begin
        if falling_edge(Clk) then
            if clockcount = 0 then
                clockcount := 49999;
                Tick1ms <= '1';
            else                 clockcount := clockcount – 1;
                Tick1ms <= '0';
            end if;         end if;
    end process tickclk;
    state_machine : process(Clk,Rst)
    begin
        if Rst=’0′ then
            State <= Boot;
            LT_DBi <= (others=>‘0’);
            LT_WR_n <= '1';
            LT_CS_n <= '1';
            LT_RS <= '0';
            LT_RESET_n <= '0';
            LT_LCD_ON <= '0';
            Index <= 0;
            Cntr <= 15;
            Cycle <= 0;
            Cmd <= (others=>‘0’);
        elsif rising_edge(Clk) then
            Cmd <= Commands(Index);
            case State is
                when Boot =>
                    LT_RESET_n <= '0';
                    Index <= 0;
                    if Tick1ms=’1′ then
                        if Cntr=0 then
                            LT_RESET_n <= '1';
                            Cntr <= 300;
                            State <= Idle;
                        else
                            Cntr <= Cntr - 1;
                        end if;
                    end if;
                when Idle =>
                    if Cntr=0 then
                        LT_LCD_ON <= '1'; -- Light ON
                        LT_CS_n <= '0';
                        State <= GetCmd;
                    elsif Tick1ms=’1′ then
                        Cntr <= Cntr - 1;
                    end if;
                when GetCmd =>
                    Cntr <= to_integer(unsigned(Cmd(Cmd_t'high downto Cmd'high-11)));
                    LT_DBi <= Cmd(LT_DBi'range);
                    LT_RS <= Cmd(LT_DBi'length);
                    State <= Dly;
                when Dly =>
                    if Cntr = 0 then
                        Cycle <= Nsetup-1;
                        LT_WR_n <= '0';
                        State <= Wr1;
                    elsif Tick1ms=’1′ then
                        Cntr <= Cntr-1;
                    end if;
                when Wr1 => — Setup
                    if Cycle = 0 then
                        Cycle <= Nhold-1;
                        LT_WR_n <= '1'; -- rising edge (trig write)
                        State <= Wr2;
                    else
                        Cycle <= Cycle-1;
                    end if;
                when Wr2 => — Hold
                    if Cycle = 0 then
                        State <= Done;
                    else
                        Cycle <= Cycle-1;
                    end if;
                when Done =>
                    if Index=Commands’length-1 then — last command has been sent
                        LT_CS_n <= '1'; -- de-select the interface
                        State <= Done; -- deadlock (default)
                    else
                        Index <= Index + 1; -- fecth next vector
                        State <= Done1;
                    end if;
                when Done1 => — compensate double pipeline
                    State <= GetCmd;
            end case;
        end if;
    end process state_machine;
end my_lcdtest;