Interfaçage « rapide » entre la caméra D5M et l’écran LCD LT-24 à l’aide d’un FPGA DE0-Nano

Afin de contrôler complètement la caméra, j’ai dû utiliser le port I2C. Après quelques recherches sur les internets, j’ai trouvé un programme VHDL qui fournit la logique d’interfaçage, prenant en entrée un port parallèle et générant un signal I2C de sortie, avec un clock à la bonne fréquence. Merci à Scott Larson pour cet excellent travail, je n’aurais jamais écrit ça moi-même.

Tout ce qu’il me fallait faire (et c’était quand même passablement compliqué), c’était de créer une machine d’état pour écrire dans les registres souhaités de la caméra. Pour cela, je me suis inspiré de la machine d’état pour contrôler l’écran LCD, ainsi que de l’exemple fournit sur le site donné ci-haut.

———————————
— Data feeder
———————————
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity datafeeder is
    port ( feed_clk : in std_logic;
        i2c_busy : in std_logic;
        i2c_data_rd : in std_logic_vector(7 downto 0);
        feed_rst_n : in std_logic;
        ——————————–
        i2c_reset_n : out std_logic;
        i2c_ena : out std_logic;
        i2c_addr : out std_logic_vector(6 downto 0);
        i2c_rw : out std_logic;
        i2c_data_wr : out std_logic_vector(7 downto 0);
        data : out std_logic_vector (15 downto 0));
end datafeeder;
— architecture
architecture my_datafeeder of datafeeder is
    signal busy_prev : std_logic;
    constant slave_addr : std_logic_vector(6 downto 0) := « 1011101 »; — The address of d5m camera is BA write and BB read, the first 7 bits are 1011101 or 5D
    type state_type is (start,getcmd,delay,writereg,readreg,donewr,done); — All the states of the FSM
    signal state : state_type;
    subtype Cmd_t is std_logic_vector (23 downto 0);
    type Commands_t is array (natural range <>) of Cmd_t;
    constant Commands : Commands_t := (     — 8 MSB bits for register address and 16 LSB bits for data
    x »0B » & x »0003″, –Restart, pause acquisition before changing settings
    — Change pixclock to 96MHz (Nope. Max Throughput of pipeline is at 50Mhz for 320×240 picture on LCD
    –x »10″ & x »0051″, –PLL control, (power on : last bit = 1)
    –x »11″ & x »6018″, –PLL config 1, (m factor : 96, n factor – 1 : 24)
    –x »12″ & x »0001″, –PLL config 2, (p factor – 1 : 1)
    –x »10″ & x »0053″, –PLL control, (power on and enable on, last 2 bits)
    — Change image size to 320×240
    x »01″ & x »0042″, –Row start, add 12 to default 54 = 66
    x »02″ & x »0020″, –Column start, add 16 to default 16 = 32
    x »03″ & x »077F », –Row size, 240*8 – 1 = 1919
    x »04″ & x »09FF », –Column size, 320*8 -1 = 2559
    x »22″ & x »0003″, –Row address mode, bin1, skip4, skip 4 pour une image de 640×480 en pattern bayer
    x »23″ & x »0003″, –Column address mode, bin1, skip4, le bin rajoute du flou en déplacement
    x »0B » & x »0001″);–Restart, clear pause_restart bit
    signal Index : integer range 0 to Commands’length; — Index of the command set
    signal Cmd : Cmd_t;
    signal reg_addr : std_logic_vector(7 downto 0);
    signal reg_data : std_logic_vector(15 downto 0);
    signal rw : std_logic;
begin
    state_machine : process(feed_clk,feed_rst_n)
    variable busy_cnt : integer range 0 to 3;
    variable clockcount : integer range 0 to 49999 := 49999; — Delay 1ms between each command
    begin
        if (feed_rst_n = ‘0’) then
            i2c_reset_n <= '0';
            Index <= 0;
            rw <= '0';
            state <= start;
        elsif rising_edge(feed_clk) then
            Cmd <= Commands(Index);
            case state is
                when start =>
                    i2c_reset_n <= '1';
                    i2c_ena <= '0';
                    state <= getcmd;
                when getcmd =>
                    reg_addr <= Cmd(23 downto 16);
                    reg_data <= Cmd(15 downto 0);
                    state <= delay;
                when delay =>
                    if rw = ‘0’ then
                        if clockcount = 0 then
                            state <= writereg;
                        end if;
                        clockcount := clockcount – 1;
                    else
                        state <= readreg;
                    end if;
                when writereg => –state for conducting this transaction
                    busy_prev <= i2c_busy; --capture the value of the previous i2c busy signal
                    if(busy_prev = ‘0’ AND i2c_busy = ‘1’) then –i2c busy just went high
                    busy_cnt := busy_cnt + 1; –counts the times busy has gone from low to high during transaction
                    end if;
                    case busy_cnt is –busy_cnt keeps track of which command we are on
                        when 0 => –no command latched in yet
                            i2c_ena <= '1'; --initiate the transaction
                            i2c_addr <= slave_addr; --set the address of the slave
                            i2c_rw <= '0'; --command 1 is a write
                            i2c_data_wr <= reg_addr; --write register address
                        when 1 =>
                            i2c_rw <= '0'; --command 2 is a write
                            i2c_data_wr <= reg_data(15 downto 8); --write msb bits of data to register
                        when 2 =>
                            i2c_data_wr <= reg_data(7 downto 0); --write lsb bits of data to register
                        when 3 =>
                            i2c_ena <='0'; --disable i2c module, all commands are passed to latch
                            if (i2c_busy = ‘0’) then –i2c module has finihsed
                                busy_cnt := 0; –reset busy counter
                                state <= done;
                            end if;
                        when others => null;
                    end case;
                when readreg =>
                    busy_prev <= i2c_busy; --capture the value of the previous i2c busy signal
                    if(busy_prev = ‘0’ AND i2c_busy = ‘1’) then –i2c busy just went high
                        busy_cnt := busy_cnt + 1; –counts the times busy has gone from low to high during transaction
                    end if;
                    case busy_cnt is
                        when 0 => –no command latched in yet
                            i2c_ena <= '1'; --initiate the transaction
                            i2c_addr <= slave_addr; --set the address of the slave
                            i2c_rw <= '0'; --command 1 is a write
                            i2c_data_wr <= reg_addr; --data to be written
                        when 1 =>
                            i2c_rw <= '1';
                        when 2 =>
                            i2c_rw <= '1';
                            if (i2c_busy = ‘0’) then
                                data(15 downto 8) <= i2c_data_rd;
                            end if;
                        when 3 =>
                            i2c_ena <= '0';
                            if (i2c_busy = ‘0’) then
                                data(7 downto 0) <= i2c_data_rd;
                                busy_cnt := 0;
                                state <= done;
                            end if;
                        when others => null;
                    end case;
                when done =>
                    if Index=Commands’length then — last command has been sent
                        –state <= donewr;
                        state <= done;
                        data <= x"FF00";
                    else
                        Index <= Index + 1; -- fecth next vector
                        state <= getcmd;
                    end if;
                when donewr => — read in loop all registers written
                    rw <= '1';
                    Index <= 0;
                    state <= getcmd;
            end case;
        end if;
    end process state_machine;
end my_datafeeder;

Par la suite, j’ai modifié le processus choosepix, vu que le format de l’image produit par la caméra est déjà le bon. Je prends une image de 640×480 en bayer, vu que je veux une 320×240 rbg, je dois prendre deux fois plus de pixels sur la caméra pour ramener les couleurs ensemble.

chosepix : process(Xpos_in,Ypos_in)
begin
    if ((Xpos_in < 640) and (Ypos_in < 480)) then -- for a 320x240 picture
        Xposchosen <= Xpos_in mod 2; --Read bayer pattern (skip 1 col)
        Yposchosen <= Ypos_in mod 2; --Read bayer pattern (skip 1 row)
    else
        Xposchosen <= (others=>‘1’);
        Yposchosen <= (others=>‘1’);
    end if;
end process chosepix;

J’ai dû changer pixelmix aussi parce qu’il y avait un bug avec le fifo, qui faisait que je lisais la sortie avant le coup d’horloge, donc l’image rouge était décalé horizontalement d’un pixel. Pour le résoudre, j’ai créé un latch et créé un front montant sur la pin rd du fifo avant la lecture de la mémoire :

pixelmix : process(D5M_PIXCLK,Yposchosen,Xposchosen,q_in)
begin
    if (rising_edge(D5M_PIXCLK) and (Yposchosen = 1) and (Xposchosen = 0)) then
        pixImage_out(4 downto 0) <= pixel_in; -- blue value of output pixel
    end if;
    if ((Yposchosen = 1) and (Xposchosen = 1)) then
        if (rising_edge(D5M_PIXCLK)) then
            rdreq_out <= '1';
            pixImage_out(10 downto 5) <= pixel_in & '0'; -- green value of output pixel
        end if;
    else
        pixImage_out(15 downto 11) <= q_in; -- red value of output pixel
        rdreq_out <= '0';
    end if;
end process pixelmix;

Finalement, je ne sais pas pourquoi, mais tout ça a rajouté un petit délai lors de l’écriture des pixels sur l’écran LCD, je l’ai rajout en faisant un not sur la clock newpixclk pour l’inverser, mais ce n’est vraiment pas très beau. En tout cas, ça fonctionne!!

Connexion directe d’une caméra D5M avec un écran LCD LT-24 à l’aide d’un FPGA DE0-Nano

Pour réussir cet exploit, je suis parti de mes deux réussites précédentes, l’interfaçage de la caméra ainsi que l’interfaçage de l’écran LCD. Ce serait simple de joindre les deux bouts, n’est-ce pas? Eh bien, pas vraiment. La caméra produit une image de 2592×1944 pixels, en patron Bayer (alternance de rouge, vert, bleu). L’écran a 320×240 pixels et son format de couleur est 16bits (5 premiers bits bleus, 6 bits du milieu vert et 5 derniers bits rouges). Il faut donc adapter la taille de l’image, et garder une couleur en mémoire pour toute une rangée, afin de l’appliquer au bon pixel de la ligne suivante.

J’ai donc créé l’entité image_transform afin de faire ces deux étapes, en partant des signaux produits par mon pixel_interface, qui me donne Xpos et Ypos, les signaux donnant la position du pixel sur l’écran.

Un premier processus, choosepix, permet de sauter 7 pixels sur 8 et de produire deux signaux, Xposchosen et Yposchosen correspondant au modulo, ce qui fait que le cycle du compte de 0 à 7 se répète 320×240 fois.

Pour la première ligne du patron de Bayer, j’enregistre seulement le pixel rouge, avec le processus pixelsave. Celui-ci utilise un FIFO créé avec l’utilitaire de Quartus II (megafunction), j’ai essayé de le programmer moi-même, sans grand succès, j’ai donc décidé de remettre cette partie de plaisir à plus tard et de me fier aux ingénieurs d’ALTERA pour cette partie-là. C’est probablement la mémoire la plus simple d’utilisation, elle a très bien fait le travail. Cela m’a permit de découvrir la différence entre les éléments logiques LE (les blocs lego de base pour générer n’importe quel circuit logique) et l’embeded memory (bloc lego de base pour générer n’importe quelle mémoire). J’ai pas trop poussé ma compréhension de cette chose, mais c’est très intéressant. Pour l’instant, c’est simplement bon à savoir, car ça permet d’économiser les registres logiques (mais bon, je suis encore très loin de remplir le FPGA de toute manière).

Dans la deuxième ligne, à l’aide du processus pixelmix, je reconstruis le pixel en 16 bits, en lisant d’abord le bleu, puis le vert, puis la mémoire du rouge correspondant à la première adresse de la valeur précédente. Je vide ainsi toute la mémoire en repassant sur la ligne précédente.

Finalement, l’écran LCD demande une clock à chaque fois que je veux écrire un nouveau pixel dans sa mémoire, je génère la fréquence et la phase appropriée à l’aide du processus pixclkgen, à partir des signaux Xposchosen et Yposchosen. J’ai rajouté l’état suivant à la machine d’état de l’écran LCD, qui apparaît après l’initialisation complète :

when CamLiveFeed =>
    LT_DBi <= pixImage_in; -- Write pixel to LCD data port
    LT_WR_n <= newpixclk_in; -- toggle write pin with pixel clock
    State <= CamLiveFeed; -- deadlock

Voici mon entité image_transform :

———————–
— Image transformation
———————–
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity image_transform is
    port( D5M_PIXCLK : in std_logic;
        pixel_in : in std_logic_vector(4 downto 0);
        Xpos_in : in unsigned(11 downto 0);
        Ypos_in : in unsigned(10 downto 0);
        q_in : in std_logic_vector(4 downto 0);
        ———————————–
        aclr_out : out std_logic;
        data_out : out std_logic_vector(4 downto 0);
        rdclk_out : out std_logic;
        rdreq_out : out std_logic;
        wrclk_out : out std_logic;
        wrreq_out : out std_logic;
        pixImage_out : out std_logic_vector(15 downto 0);
        newpixclk_out : out std_logic);
end image_transform;
— architecture
architecture my_image_transform of image_transform is
    signal Xposchosen : unsigned(11 downto 0);
    signal Yposchosen : unsigned(10 downto 0);
begin
    chosepix : process(Xpos_in,Ypos_in)
    begin
        if ((Xpos_in < 2560) and (Ypos_in < 1920)) then -- for a 320x240 picture
            Xposchosen <= Xpos_in mod 8;
            Yposchosen <= Ypos_in mod 8;
        else
            Xposchosen <= (others=>‘1’);
            Yposchosen <= (others=>‘1’);
        end if;
    end process chosepix;
    wrclk_out <= not D5M_PIXCLK; -- write and read fifo at falling edge of the clock
    rdclk_out <= not D5M_PIXCLK;
    pixelsave : process(D5M_PIXCLK,Yposchosen,Xposchosen) — save first row each 8 rows and skip 8 pixels on x axis to match output resolution
    begin
        if ((Yposchosen = 0) and (Xposchosen = 1)) then
            if (rising_edge(D5M_PIXCLK) ) then
                wrreq_out <= '1';
                data_out <= pixel_in; -- put the first pixel row in memory buffer
            end if;
        else
            wrreq_out <= '0';
        end if;
    end process pixelsave;
    pixelmix : process(D5M_PIXCLK,Yposchosen,Xposchosen)
    begin
        if (rising_edge(D5M_PIXCLK) and (Yposchosen = 1) and (Xposchosen = 0)) then
                pixImage_out(4 downto 0) <= pixel_in; -- blue value of output pixel
        end if;
        if ((Yposchosen = 1) and (Xposchosen = 1)) then
            if (rising_edge(D5M_PIXCLK)) then
                rdreq_out <= '1';
                pixImage_out(10 downto 5) <= pixel_in & '0'; -- green value of output pixel
                pixImage_out(15 downto 11) <= q_in; -- red value of output pixel
            end if;
        else
            rdreq_out <= '0';
        end if;
        if (Yposchosen = 2) then
            aclr_out <= '1';
        else
            aclr_out <= '0';
        end if;
    end process pixelmix;
    pixclckgen : process(D5M_PIXCLK) — generates a clock at the pixel_out rate
    begin
        if (rising_edge(D5M_PIXCLK) and (Xposchosen < 4) and (Yposchosen = 1)) then
            newpixclk_out <= '0';
        elsif (rising_edge(D5M_PIXCLK) and (Xposchosen >= 4) and (Yposchosen = 1)) then
            newpixclk_out <= '1';
        end if;
    end process pixclckgen;
end my_image_transform;

L’entité du top permet de tout connecter en série, je ne l’inclurai pas ici.

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;

Interface d’une caméra TRDB-D5M avec un FPGA DE0-Nano

Ceci est mon premier projet en VHDL, effectué en novembre dernier (désolé, je suis en train de rattraper la documentation des derniers mois 🙂

J’ai d’abord commencé par réussir à amener la clock de 50MHz sur la sortie XCLKIN de la caméra. C’est un simple signal en vhdl :

———————————
— Clock output to the D5M camera
———————————
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
— entity
entity D5M_clock is
    port ( CLOCK_50 : in std_logic;
        D5M_XCLKIN : out std_logic);
end D5M_clock;
— architecture
architecture myD5M_clock of D5M_clock is
begin
    D5M_XCLKIN <= CLOCK_50; -- put 50Mhz clock on the clock input of the camera
end myD5M_clock;

Ensuite, j’ai fait l’interfaçage avec la sortie du pixel sur le port parallèle :

———————
— Pixel Interfacing
———————
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity pixel_interface is
    port ( D5M_PIXCLK : in std_logic;
        D5M_LVAL : in std_logic;
        D5M_FVAL : in std_logic;
        –D5M_D0 : in std_logic;
        –D5M_D1 : in std_logic;
        –D5M_D2 : in std_logic;
        –D5M_D3 : in std_logic;
        –D5M_D4 : in std_logic;
        –D5M_D5 : in std_logic;
        D5M_D6 : in std_logic;
        D5M_D7 : in std_logic;
        D5M_D8 : in std_logic;
        D5M_D9 : in std_logic;
        D5M_D10 : in std_logic;
        D5M_D11 : in std_logic;
———————————————
        –pixel : out std_logic_vector(11 downto 0));
        pixel_out : out std_logic_vector(5 downto 0);
        Xpos_out : out unsigned(11 downto 0);
        Ypos_out : out unsigned(10 downto 0));
end pixel_interface;
— architecture
architecture my_pixel_interface of pixel_interface is
    signal Xpos : unsigned(11 downto 0);
    signal Ypos : unsigned(10 downto 0);
    signal newline : std_logic;
begin
    pixel_latch: process(D5M_PIXCLK) — latch pixel data
    begin
        if (falling_edge(D5M_PIXCLK) and (D5M_LVAL = ‘1’) and (D5M_FVAL = ‘1’)) then — read pixel data when it’s valid
            pixel_out <= D5M_D11 & D5M_D10 & D5M_D9 & D5M_D8 & D5M_D7 & D5M_D6;
        end if;
    end process pixel_latch;
    position_counter : process(D5M_FVAL,D5M_LVAL,D5M_PIXCLK)
    begin
        if (D5M_FVAL = ‘0’) then
            Ypos <= to_unsigned(-1, Ypos'length); -- -1 to start the first count at 0
        elsif (D5M_LVAL = ‘0’) then
            Xpos <= to_unsigned(-1, Xpos'length); -- Reset Xpos counter (-1 to start the first count at 0)
            newline <= '1';
        elsif (falling_edge(D5M_PIXCLK)) then — When a new valid pixel is read
            if (newline = ‘1’) then — Increment Ypos count only once a new line
                Ypos <= Ypos + 1;
                newline <= '0';
            end if;
            Xpos <= Xpos + 1; -- Increment Xpos count
        end if;
    end process position_counter;
    Xpos_out <= Xpos; -- Final output assignement
    Ypos_out <= Ypos;
end my_pixel_interface;

Dans l’architecture, il y a deux process : pixel_latch et position_counter. Pixel_latch est une latch qui permet de lire le port parallèle au bon moment, lorsque la PIXCLOCK est en falling edge (la D5M écrit le pixel sur le port en rising edge) et le met dans un vecteur pixel_out. Position_counter permet de se synchroniser sur les autres sorties de la caméra, soit FVAL (frame valid) et LVAL (line valid) afin de compter les pixels et de savoir la position du pixel actuel en x et en y sur l’image, afin de bien pouvoir reconstruire celle-ci ultérieurement. Le -1 est reconverti en unsigned comme étant la valeur maximale du compteur (2^11-1 pour Xpos et 2^10-1 pour Ypos, ici présent), permettant que la première valeur de position soit 0,0 et non pas 1. Le compteur est incrémenté tant et aussi longtemps que FVAL et LVAL sont à l’état haut. Un signal newline est produit pour compter les lignes (Ypos). La programmation de cette architecture m’a permis de mieux comprendre la différence entre les conditions en programmation classique et celles dans un process de vhdl : il ne peut y avoir qu’une clock que l’on traite, tous les signaux doivent avoir des états définis peu importe ce qu’il se passe, même si on ne s’en sert pas. Bref, la base de la pensée sous-jacente à la conception matérielle des circuits logiques.

Finalement, un dernier composant permet de choisir un seul pixel de l’image et l’afficher sur les DELs de la carte, pour voir si tout fonctionne bien :

—————————-
— Pixel selection to output
—————————-
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity pixel_selection is
    port ( pixel_in : in std_logic_vector(5 downto 0);
        Xpos_in : in unsigned(11 downto 0);
        Ypos_in : in unsigned(10 downto 0);
———————————————
        POUT0 : out std_logic;
        POUT1 : out std_logic;
        POUT2 : out std_logic;
        POUT3 : out std_logic;
        POUT4 : out std_logic;
        POUT5 : out std_logic);
end pixel_selection;
— architecture
architecture my_pixel_selection of pixel_selection is
begin
    pixsel : process(Xpos_in,Ypos_in)
    begin
        if ((Xpos_in = 1296) and (Ypos_in = 972)) then
            POUT0 <= pixel_in(0);
            POUT1 <= pixel_in(1);
            POUT2 <= pixel_in(2);
            POUT3 <= pixel_in(3);
            POUT4 <= pixel_in(4);
            POUT5 <= pixel_in(5);
        end if;
    end process pixsel;
end my_pixel_selection;

C’était avant que je ne comprenne qu’on pouvait directement mapper les vecteurs dans le pin map, sans avoir besoin d’expliciter chaque bit comme dans le code ci-haut. Mais pour la compréhension de ce qu’il se passe, c’est bien de commencer comme ça : le LSB est relié à la DEL0, etc jusqu’à la DEL5. J’aurais pu utiliser les 8 DELs, mais de toute manière, mon but était de l’interfacer avec un écran en RGB16bit, ce qui a été fait (prochain article! 😉

Séparation des soies et des graines d’asclépiade

J’ai réussi, un peu par hasard, à patenter un système pour séparer les soies des graines d’asclépiade plus facilement qu’à la main, en semi-automatisant le processus. J’enlève seulement la coque, puis je place le mélange de soies et de graines dans cette mini-poubelle modifiée pour accueillir un moteur de lave-vaisselle au centre. J’avais imprimé une palette, pensant rendre le tout plus efficace, mais son inertie était trop grande, ce qui maintenait le moteur dans un état statique.

En diminuant le voltage AC à l’aide d’un transformateur variable, on peut introduire des instabilités dans la rotation, ce qui aide à vraiment déloger les graines efficacement, par exemple en inversant le sens de rotation et en produisant des coups aléatoires.

test du nRF51-DK

J’ai ressorti mon nRF51-DK de la poussière en me disant qu’après deux ans d’absence dans ce projet-là, peut-être que les choses auraient changé concernant le monde mystérieux de l’interfaçage BLE. Je dirais oui et non, car si ça marche un peu plus que dans mes souvenirs, c’est encore plus ou moins sur la coche.

J’utilise bluez 5.50 sur Fedora. Pour une raison obscure, l’interface par défaut de KDE de bluetooth est très aléatoire. J’y vais donc avec le terminal. J’ai commencé par faire un restart : service bluetooth restart, ensuite bluetoothctl pour avoir l’interface de contrôle.

Avec power on et scan on/off, on peut pas mal tout faire.

Donc ce qui est bizarre aussi c’est que mon nRF51-DK doit être nouvellement programmé avec le code. J’utilise celui-ci pour tester l’uart : https://os.mbed.com/teams/Bluetooth-Low-Energy/code/BLE_LoopbackUART/

et j’alterne avec BLE puck juste pour être capable de voir ce qu’il se passe. Donc voici toutes les étapes pour arriver à me connecter avec mon nRF en mode uart. (Avec mbed, on peut compiler en ligne et glisser le fichier hex directement par usb)

power off -> loader le firmware puck ->power on -> scan on -> scan off -> power off -> loader le firmware uart ->power on-> scan on

Ensuite on peut se connecter avec l’interface graphique de bluetooth.

J’ai essayé le code d’Adafruit https://github.com/adafruit/Adafruit_Python_BluefruitLE , mais après beaucoup de gossage (j’ai enlevé le bout du code qui flush la cache pour être capable de voir mon périphérique), il arrive à le trouver et à identifier son UUID comme étant du uart, mais il bogue rendu à device.connect() :

dbus.exceptions.DBusException: org.freedesktop.DBus.Error.UnknownObject: Method « Connect » with signature «  » on interface « org.bluez.Device1 » doesn’t exist

Donc ça me semble être un bug dans la librairie d’Adafruit. Avec le connect de fedora, il l’enregistre comme un service uart, je ne sais simplement pas comment y avoir accès par la suite.

Tentative exploratoire de filage de soie d’asclépiade

J’ai fait une première tentative de filage de l’asclépiade en plaçant du médium glacis clair dans un pot en plastique, suivi par des soies. C’était davantage un test exploratoire pour débroussailler la suite des choses.

J’ai tout d’abord essayé de mélanger le tout à l’aide d’un bâton, puis l’idée m’est venue d’enrouler le début de l’accumulation de soie autour, afin d’avoir une base solide. En rajoutant progressivement des soies au mélange et en tournant le fil imbibé d’adhésif dans le pot, j’ai obtenu une manière de filer les soies en un fil potentiellement infini, tant que les paramètres sont bien contrôlés.

Tout porte à croire qu’en contrôlant le flux de soie et d’adhésif, un fil très fin pourrait être produit.
Le bout opposé au bâton, lorsqu’il ne restait plus de médium glacis clair dans le pot.
Le fil dans toute sa longueur, est un peu rigide par endroits à cause de son épaisseur
Les fibres sont bien maintenues ensembles.

En enroulant la fibre autour du bâton, j’ai pu continuer presque indéfiniment, n’étant limité que par la quantité de médium dans le pot. Ensuite, je l’ai déroulé au complet et accroché pour laisser sécher. Toutes ces étapes devront être bien contrôlées dans un prototype éventuel.

Piano midi avec fluidsynth

J’ai installé Ubuntu Studio sur une tour pour faire un piano midi qui s’ouvre automatiquement.

Voici le script bash que j’ai adapté d’ici : https://superuser.com/questions/1291367/bash-condition-not-met-when-executed-by-crontab

#!/bin/bash
sleep 60
echo "Starting"
/usr/bin/fluidsynth -is -a jack -j -m alsa_seq /home/fred/Nice-Keys-Ultimate-V2.3.sf2 &
echo "Fluidsynth started"
while true; do /usr/bin/aconnect -o; if [[ $(/usr/bin/aconnect -o ) = FLUID ]]; then break; fi; sleep 2; done
/usr/bin/aconnect 20:0 128:0
echo "Connected"

J’ai rajouté un sleep 60 pour attendre que l’os ait fini de démarrer comme du monde, sinon il y avait des erreurs bizarres (probablement qu’il faut que la carte de son soit chargée avant d’exécuter le script.

Ensuite, j’ai ajouté ceci dans la crontab :

@reboot bash /home/fred/pianolauncher.sh >> /home/fred/out.log 2>&1

où pianolauncher est mon script ci-haut. Ça fonctionnait lorsque mon écran était connecté, mais sans écran, il y avait un bug. Il faut ajouter la ligne suivante au fichier /etc/environment :

JACK_NO_AUDIO_RESERVATION=1

Eh voilà! Un piano avec une latence très faible grâce au kernel préemptoire d’Ubuntu Studio, qui s’ouvre automatiquement sans besoin d’avoir d’écran, avec une librairie de sons vraiment géniale!

Test de plusieurs adhésifs sur l’asclépiade

En mars dernier, je suis passé dans un magasin d’art pour acheter des échantillons de toutes les colles et adhésifs que j’ai pu trouver, afin de les tester sur la fibre d’asclépiade. La seule contrainte était qu’ils devaient être sous forme liquide et pouvoir sécher à l’air libre. Quelques mois ont passé sans que je tente l’expérience, mais cette semaine fut la bonne! Les résultats m’ont grandement surpris.

Premièrement, je vais commencer par expliquer ma méthodologie. J’ai utilisé des grilles en coton, communément appelées tissu à fromage, comme substrat de base pour emprisonner les fibres, qui sont très volatiles rappelons-le. Une fois les fibres bien placées, j’appliquais la colle généreusement, en tâchant de bien la mélanger aux fibres. Dans mon premier essai, des fibres se sont échappées et j’ai tâché de les rassembler pour former un fil, ce qui a très bien fonctionné. Voyons voir les résultats un par un. J’utilise des critères subjectifs de flexibilité, résistance à l’eau, toxicité et confort afin d’effectuer la comparaison.

Colle à bois

Lorsqu’elle n’est pas sèche, la colle à bois (Colle blanche multi-usage Lepage) semble être l’adhésif idéal. Les fibres s’y mélangent parfaitement, leur comportement est surprenant  puisqu’on est habitué qu’elles soient fortement hydrophobes (mais la colle ne contient visiblement pas d’eau). On peut facilement façonner un fil qui tient bien. Cependant, on déchante un peu une fois séché. Le résultat est extrêmement cassant, comme si la colle a trop attaqué chimiquement les fibres.

Flexibilité : 1/5

Résistance à l’eau : Oui

Toxicité : Non

Confort : 2/5

Mod Podge

Le Mod Podge (Mod Podge fini mat) est beaucoup plus liquide à l’application que la colle, et donne l’impression de moins bien tenir sur la fibre, probablement parce qu’il contient de l’eau. Par contre, une fois séché, le résultat est impressionnant : les fibres conservent leur souplesse et leur brillance, tout en ayant plutôt bien fusionné. Seul hic : il se dissout complètement une fois plongé dans l’eau, ce qui empêche son usage dans le textile.

Flexibilité : 3/5

Résistance à l’eau : Non

Toxicité : Non

Confort : 4/5

Medium glacis clair

Le medium glacis clair (Jo Sonja clear glaze medium surface sealer) a une viscosité qui se situe entre celle de la colle et du modpoge, et ressemble à une peinture transparente. On peut facilement façonner un fil à la manière de la colle. Une fois séché, le fil garde sa souplesse et sa brillance, ce qui signifie que la fibre est peu attaquée. Problème : ne se trouve plus au Omer DeSerre.

Flexibilité : 3/5

Résistance à l’eau : Oui

Toxicité :  contient du propylène glycol et de l’hydroxide d’ammonium (aucune idée de la concentration), mais je ne sais pas si c’est toxique, ça semble être correct.

Confort : 4/5

Médium textile

Le médium textile (Ceramcoat médium textile) ressemble beaucoup au médium glacis clair. Cependant, il ne permet pas de façonner aussi bien un fil à l’application. Une fois séché, la fibre conserve ses propriétés. Il ne résiste pas vraiment bien à l’eau, bien que je n’ai pas suivi le protocole de l’étiquette disant qu’il faut attendre 7 jours avant de laver le tissu (trop long, à un moment donné).

Flexibilité : 3/5

Résistance à l’eau : Non

Toxicité : Non

Confort : 4/5

Peinture acrylique pour cuir

La peinture acrylique pour cuir (DecoArt Patent Leather Paint) a été un essai chanceux puisque visiblement, ce produit est fait pour être utilisé sur du cuir, qui a des propriétés bien différentes des fibres de l’asclépiade. Pourtant, un peu comme avec la colle, les fibres semblaient directement se prêter à la fabrication d’un fil dès l’application. Une fois séché, la couleur bleue reste vraiment tenace. Les fibres conservent une flexibilité et semblent même beaucoup moins cassantes qu’à l’origine.

Flexibilité : 3/5

Résistance à l’eau : Oui

Toxicité : Non

Confort : 3/5

Colle caoutchouc

La colle caoutchouc (Elmers) a été choisie plutôt dans une tentative d’être exhaustif et de ne laisser de côté aucune alternative, bien que son caractère poison et inflammable soit rébarbatif. À l’application, une odeur forte se dégage, mais j’avais prévu le coup en plaçant mes tests dans une pièce à part et bien aérée. Cela vient avec un pinceau et je ne l’ai pas autant manipulé que les autres, ce qui laisse la comparaison un peu délicate. Un fois séché, les fibres sont bien collées, bien qu’il ait été difficile de les enrober complètement d’adhésif. Elles conservent parfaitement leur propriété hydrofuge.

Flexibilité : 4/5

Résistance à l’eau : Oui

Toxicité : Oui ++

Confort : 2/5

Conclusion

Pour mon application, qui est d’éventuellement faire un textile d’asclépiade, seuls deux adhésifs ont passé le test : le médium glacis clair et la peinture acrylique pour cuir. Ils ont en commun de fonctionner sur une base d’acrylique, qui semble avoir une bonne compatibilité chimique avec la cellulose de l’asclépiade. Ce sera donc à investiguer davantage, mais je crois avoir bien cerné mes recherches. Le hic, c’est que ce sont deux produits assez chers (4,5$ pour 60ml), mais probablement qu’une très faible quantité est véritablement nécessaire pour bien coller les fibres, dépendamment de la méthode d’application qui reste bien évidemment à optimiser. Le fil résultant sera donc inévitablement un composite synthétique-asclépiade, avec tous les avantages et inconvénients que cela comporte. La capacité d’utiliser des peintures colorées est tout de même très intéressant, et la résistance accrue aux contraintes mécaniques que confère l’adhésif permet de rendre filable une soie extrêmement fragile et lisse, tout en conservant ses propriétés isolantes et hydrofuges.

Synchronisation des DEL avec le signal vidéo

Afin de bien pouvoir séparer les signaux de ma caméra oxymétrique, je dois trouver un moyen pour synchroniser les DELs avec l’acquisition des images. L’objectif est d’alterner les prises de vue entre un éclairage avec la DEL rouge et avec la DEL infrarouge, sans que les signaux se chevauchent. Le hic, c’est que la caméra que j’utilise n’a aucun trig externe : toute l’électronique de contrôle est intégrée, laissant seulement le signal vidéo analogique en sortie.

Mon premier essai fut avec une esquisse arduinesque d’une redoutable simplicité :

void loop() {
analogWrite(5,10);
delay(33);
delayMicroseconds(325);
analogWrite(5,0);
analogWrite(6,60);
delay(33);
delayMicroseconds(325);
analogWrite(6,0);
}

La broche enable de chaque régulateur de courant des DELs est reliée aux pin 5 et 6 de l’arduino, qui fait un PWM pour éviter la surchauffe. Essentiellement, le programme allume en alternance les deux DEL, avec une période d’environ 33,333ms, pour correspondre au 30 images par secondes du flux vidéo. Il y a deux problèmes principaux à cette approche : l’horloge de l’arduino n’est synchronisée ni en phase, ni en fréquence avec celle de la caméra, ce qui fait que pendant la durée d’une prise d’image, bien souvent les deux DELs sont allumées séquentiellement, donc on se retrouve avec un signal mélangé. La légère différence en fréquence fait que ce décalage en phase se déplace au fil du temps, produisant un battement lent dans le signal qui rend celui-ci inutilisable périodiquement. Bref, ça marche, mais ce n’est vraiment pas fiable et il y a manière de faire mieux.

J’ai donc relégué l’arduino et me suis décidé à résoudre le problème de manière purement électronique. Le schéma est présenté ci-dessous :

J’ai commencé par observer le signal vidéo analogique à l’oscilloscope. Il ressemble à ceci :

À chaque ligne, il y a un pic vers le bas qui indique une nouvelle ligne. La zone au milieu de la photo survient à la fréquence de 60Hz, puisque la vidéo est interlacée, c’est l’indicateur de nouvelle image. Afin de me synchroniser là-dessus, je me sers de la partie distinctive qui est un plat près de 0V. Je passe donc ce signal dans la borne négative d’un premier comparateur, avec un niveau de voltage assez bas sur la borne positive (224mV), ce qui sort uniquement les pics les plus bas (nouvelle ligne et nouvelle image). La sortie de ce comparateur donne ceci :

Les pics plus importants correspondent au plateau près de zéro que je souhaitais isoler. Je passe ce signal dans un filtre passe-bas, qui va garder seulement la composante à 60Hz et retirer les petites fluctuations :

Je passe ensuite ce signal dans un second comparateur, afin d’obtenir un retour parfait au ground et une forme plus abrupte des pics :

Ça marche bien, sauf que c’est en open collector, donc on ne peut pas l’interfacer directement avec une puce en logique CMOS. Le voltage du niveau haut est à 4,3V environ, ce qui est trop loin du 5V. Après un bon bout de gossage, j’ai réussi à brancher un transistor PNP en configuration d’émetteur commun. Le biasing provoquait des effets bizarres, parce que je mettais une résistance de charge trop élevée, ce qui limitait trop le courant dans le collecteur. En le revirant de bord, (l’émetteur et le collecteur sont inversés), j’ai réussi à le faire marcher par magie. En investiguant un peu, je me suis rendu compte que faire ça diminue le facteur beta, soit le gain en courant du transistor. Autrement dit, la région d’amplification est beaucoup plus faible et il sature très rapidement. En diminuant la résistance de charge, le courant augmente drastiquement et on peut se servir du transistor branché normalement, puisqu’il reproduit bien l’onde carrée. Cela donne un pic inversé qui varie entre 4,9V et 0V :

En envoyant cela dans l’entrée d’horloge d’une bascule T, on crée une onde carrée avec un rapport cyclique de 50%, à la moitié de la fréquence des pulses d’entrée :

Donc après toutes ces étapes, on vient de recréer une horloge qui est synchronisée sur celle de la caméra (avec une phase décalée bien sûr), à 30Hz. Afin de contrôler les DELs, je vais la diviser encore en deux avec une autre bascule T, ce qui va produire deux ondes carrées à 15Hz déphasées de 180 degrés, autrement dit, les deux DELs vont clignoter en alternance à la fréquence de la caméra. Cela donne ceci :

Finalement, je me sers de ce signal pour moduler une autre onde carrée générée par un timer 555 avec un duty cycle faible, qui fait office de PWM pour mes DELs. Le résultat, appliqué sur la broche de contrôle des régulateurs de mes DELs, ressemble à ceci :

Et ça marche tout seul! Lorsqu’on allume le tout, les DELs se mettent à clignoter à la bonne fréquence comme par magie!