Développement d'un contrôleur de bus SPI

Fonctionnement

Un bus SPI (Serial Peripheral Interface) est un bus série synchrone souvent utilisé pour échanger des données entre un microcontrôleur et un ou plusieurs périphériques.

Le maître est le composant qui a l’initiative des communications. L’esclave répond aux demandes du maître. Dans le cas du SPI, le maître produit le signal d’horloge qui servira à cadencer l’émission et la réception des bits de données.

Maître et esclave SPI

La figure ci-dessus illustre le fonctionnement typique de deux composants SPI. Le maître et l’esclave possèdent chacun un registre à décalage qui sert à la fois à l’émission et à la réception. Les deux registres à décalage sont reliés de manière à former un anneau. Si on se place du point de vue du maître, la transmission d’un octet se passe de la manière suivante. À chaque période de l’horloge série :

Dans une transmission SPI, l’envoi et la réception des données se font en parallèle. Au bout de 8 périodes d’horloge, le maître et l’esclave ont simplement échangé les contenus de leur registres respectifs.

Le protocole SPI

La synchronisation des données sur l’horloge série est définie par deux paramètres.

Protocole SPI

Ces deux paramètres autorisent quatre variantes du protocole SPI. Pour que la communication ait lieu sans erreur, le maître et l’esclave doivent avoir les mêmes réglages de polarité et de phase.

Réalisation d’un contrôleur SPI simple

L’interface SPI de notre IP sera composée des ports suivants :

Nom du port Type Direction Rôle
spi_cs std_logic out Chip Select, activation du périphérique SPI.
spi_mosi std_logic out Master Out Slave In, données série destinées au périphérique SPI.
spi_miso std_logic in Master In Slave Out, données série en provenance du périphérique SPI.
spi_sck std_logic out Serial Clock, horloge de la communication série.

En interne, la communication sur le bus SPI sera pilotée par les registres suivants :

Registre Alias Rôle
data_reg0(0) spi_start_reg Commande de démarrage d’un transfert SPI. Revient à zéro automatiquement.
data_reg0(1) spi_select_reg Commande de sélection du périphérique SPI.
data_reg0(2) spi_polarity_reg La polarité de l’horloge SPI.
data_reg0(3) spi_phase_reg La phase de l’horloge SPI.
data_reg0(4) spi_done_reg Indicateur de fin de transfert SPI. Revient à zéro au début d’un nouveau transfert.
data_reg1 spi_division_reg La division de fréquence à appliquer pour générer l’horloge série.
data_reg2(7 downto 0) spi_data_reg En écriture, l’octet à envoyer au périphérique SPI, en lecture, l’octet reçu du périphérique SPI.

Le chronogramme ci-dessous détaille le fonctionnement interne du contrôleur SPI. Il met en évidence les registres à utiliser pour compter le temps et le nombre de bits.

Chronogramme détaillé du contrôleur SPI

Dans Vivado :

  1. Ouvrez le schéma de votre architecture matérielle.
  2. Cliquez avec le bouton droit sur le composant SPIDevice et sélectionnez Edit in IP Packager.

Dans le fichier SPIDevice_v1_0_S00_AXI.vhd :

  1. Dans l’entité, ajoutez les ports du bus SPI.
  2. Dans l’architecture, ajoutez le code de gestion du bus SPI de manière à respecter le chronogramme.
  3. Corrigez les erreurs de syntaxe éventuelles et enregistrez le fichier.

Dans le fichier SPIDevice_v1_0.vhd :

  1. Dans l’entité, ajoutez les ports du bus SPI.
  2. Dans l’architecture, ajoutez les ports du bus SPI à la déclaration du composant SPIDevice_v1_0_S00_AXI.
  3. Dans l’architecture, ajoutez les associations des ports du bus SPI à l’instanciation du composant SPIDevice_v1_0_S00_AXI.
  4. Corrigez les erreurs de syntaxe éventuelles et enregistrez le fichier.

Simuler le fonctionnement du contrôleur SPI

  1. Dans le panneau Sources, cliquez avec le bouton droit sur Simulation Sources. Choisissez Add Sources.
  2. Sélectionnez Add or create simulation sources et pressez le bouton Next.
  3. Pressez le bouton Create File.
  4. Le fichier à créer sera un fichier source VHDL nommé SPITestbench_v1_0.vhd. Pressez le bouton Finish.
  5. Dans la fenêtre Define Module, pressez OK sans rien changer, puis pressez Yes dans la boîte de dialogue de confirmation.

Éditez le fichier SPITestbench_v1_0.vhd et remplacez son contenu par :

entity SPITestbench_v1_0 is
end SPITestbench_v1_0;

library ieee;
use ieee.std_logic_1164.all;

architecture Simulation of SPITestbench_v1_0 is
    constant C_S00_AXI_DATA_WIDTH : integer := 32;
    constant C_S00_AXI_ADDR_WIDTH : integer := 4;

    subtype Addr_t is std_logic_vector(C_S00_AXI_ADDR_WIDTH-1 downto 0);
    subtype Data_t is std_logic_vector(C_S00_AXI_DATA_WIDTH-1 downto 0);

    constant CLK_PERIOD    : time := 10 ns;
    constant DATA_TO_SPI   : std_logic_vector(7 downto 0) := x"AC";
    constant DATA_FROM_SPI : std_logic_vector(7 downto 0) := x"35";

    constant DIVISION : Data_t    := x"00000010";
    constant PHASE    : std_logic := '1';
    constant POLARITY : std_logic := '1';

    signal spi_cs      : std_logic;
    signal spi_mosi    : std_logic;
    signal spi_miso    : std_logic;
    signal spi_sck     : std_logic;
    signal axi_aclk	   : std_logic := '0';
    signal axi_aresetn : std_logic;
    signal axi_awaddr  : Addr_t;
    signal axi_awprot  : std_logic_vector(2 downto 0);
    signal axi_awvalid : std_logic;
    signal axi_awready : std_logic;
    signal axi_wdata   : Data_t;
    signal axi_wstrb   : std_logic_vector((C_S00_AXI_DATA_WIDTH/8)-1 downto 0);
    signal axi_wvalid  : std_logic;
    signal axi_wready  : std_logic;
    signal axi_bresp   : std_logic_vector(1 downto 0);
    signal axi_bvalid  : std_logic;
    signal axi_bready  : std_logic;
    signal axi_araddr  : Addr_t;
    signal axi_arprot  : std_logic_vector(2 downto 0);
    signal axi_arvalid : std_logic;
    signal axi_arready : std_logic;
    signal axi_rdata   : Data_t;
    signal axi_rresp   : std_logic_vector(1 downto 0);
    signal axi_rvalid  : std_logic;
    signal axi_rready  : std_logic;
begin
    spi_device_inst : entity work.SPIDevice_v1_0
        generic map(
            C_S00_AXI_DATA_WIDTH => C_S00_AXI_DATA_WIDTH,
            C_S00_AXI_ADDR_WIDTH => C_S00_AXI_ADDR_WIDTH
        )
        port map(
            spi_cs          => spi_cs,
            spi_mosi        => spi_mosi,
            spi_miso        => spi_miso,
            spi_sck         => spi_sck,
    		s00_axi_aclk	=> axi_aclk,
    		s00_axi_aresetn	=> axi_aresetn,
    		s00_axi_awaddr	=> axi_awaddr,
    		s00_axi_awprot	=> axi_awprot,
    		s00_axi_awvalid	=> axi_awvalid,
    		s00_axi_awready	=> axi_awready,
    		s00_axi_wdata	=> axi_wdata,
    		s00_axi_wstrb	=> axi_wstrb,
    		s00_axi_wvalid	=> axi_wvalid,
    		s00_axi_wready	=> axi_wready,
    		s00_axi_bresp	=> axi_bresp,
    		s00_axi_bvalid	=> axi_bvalid,
    		s00_axi_bready	=> axi_bready,
    		s00_axi_araddr	=> axi_araddr,
    		s00_axi_arprot	=> axi_arprot,
    		s00_axi_arvalid	=> axi_arvalid,
    		s00_axi_arready	=> axi_arready,
    		s00_axi_rdata	=> axi_rdata,
    		s00_axi_rresp	=> axi_rresp,
    		s00_axi_rvalid	=> axi_rvalid,
    		s00_axi_rready	=> axi_rready
        );

    axi_aclk    <= not axi_aclk after CLK_PERIOD / 2;
    axi_aresetn <= '0', '1' after CLK_PERIOD;

    axi_master : process
        procedure axi_write32(addr : Addr_t; data : Data_t) is
        begin
            axi_wdata   <= data;
            axi_wvalid  <= '1';
            axi_awaddr  <= addr;
            axi_awvalid <= '1';
            axi_wstrb   <= "1111";
            wait until rising_edge(axi_aclk) and axi_awready = '1' and axi_wready = '1';
            axi_wvalid  <= '0';
            axi_awvalid <= '0';
        end axi_write32;

        procedure axi_read32(addr : Addr_t; data : out Data_t) is
        begin
            axi_araddr  <= addr;
            axi_arvalid <= '1';
            wait until rising_edge(axi_aclk) and axi_arready = '1';
            axi_arvalid <= '0';
            if axi_rvalid = '0' then
                wait until rising_edge(axi_aclk) and axi_rvalid = '1';
            end if;
            data := axi_rdata;
            axi_rready <= '1';
            wait until rising_edge(axi_aclk);
            axi_rready  <= '0';
        end axi_read32;

        variable rdata : Data_t;
    begin
        axi_awvalid <= '0';
        axi_wvalid  <= '0';
        axi_rready  <= '0';
        axi_arvalid <= '0';

        wait until rising_edge(axi_aclk) and axi_aresetn = '1';

        axi_write32(x"0", x"0000000" & PHASE & POLARITY & "00");
        axi_write32(x"4", DIVISION);                             -- Division de fréquence
        axi_write32(x"0", x"0000000" & PHASE & POLARITY & "10"); -- Select
        axi_write32(x"8", x"000000"  & DATA_TO_SPI);             -- Écriture du registre de données
        axi_write32(x"0", x"0000000" & PHASE & POLARITY & "11"); -- Start

        -- Lecture du registre d'état jusqu'à l'activation du bit "done".
        loop
            axi_read32(x"0", rdata);
            exit when rdata(4) = '1';
        end loop;

        axi_write32(x"0", x"0000000" & PHASE & POLARITY & "00"); -- Arrêt
        axi_read32(x"8", rdata);                                 -- Lecture du registre de données.

        assert rdata(7 downto 0) = DATA_FROM_SPI
            report "Wrong data from SPI"
            severity FAILURE;

        report "Success";
        wait;
    end process axi_master;

    spi_slave : process
        variable data : std_logic_vector(7 downto 0);
        constant SPI_SCK_AFTER_EDGE : std_logic := not (POLARITY xor PHASE);
    begin
        wait until spi_cs = '0';

        for i in 7 downto 0 loop
            if PHASE = '0' then
                spi_miso <= DATA_FROM_SPI(i);
            end if;
            wait until spi_sck'event and spi_sck /= POLARITY;
            if PHASE = '0' then
                data(i) := spi_mosi;
            else
                spi_miso <= DATA_FROM_SPI(i);
            end if;
            wait until spi_sck'event and spi_sck = POLARITY;
            if PHASE = '1' then
                data(i) := spi_mosi;
            end if;
        end loop;

        assert data = DATA_TO_SPI
            report "Wrong data to SPI"
            severity FAILURE;

        report "Success";
        wait;
    end process spi_slave;
end Simulation;

Dans le panneau Flow Navigator, pressez Run Simulation puis Run Behavioral Simulation.

Dans l’onglet Tcl console, en bas de la fenêtre, exécutez cette commande pour continuer la simulation pendant 500 nanosecondes. Répétez l’opération jusqu’à ce qu’un message Success s’affiche, ou jusqu’à ce qu’une erreur se produise.

run 500ns

Compléter l’architecture du système

Mettre à jour l’IP

Mettez à jour l’IP dans le catalogue :

  1. Dans le panneau Flow Navigator, dans la catégorie Project Manager, exécutez l’action Package IP.
  2. Dans les rubriques File Groups et Ports and interfaces exécutez l’action Merge changes …. Les nouveaux fichiers et les nouveaux ports doivent apparaître dans la liste.
  3. Choisissez Review and Package et pressez le bouton Re-Package IP. Acceptez la fermeture du projet.

Dans la fenêtre Vivado du projet principal,

  1. Dans le menu Tools, choisissez Report puis Report IP status.
  2. Dans le panneau en bas de la fenêtre, dans l’onglet IP status, pressez le bouton Upgrade Selected. Les nouveaux ports de votre IP doivent apparaître sur le schéma.
  3. Cliquez avec le bouton droit sur chacun des ports non conecté du composant SPIDevice et choisisez Create Port.

Modifier les affectations de broches

  1. Dans le panneau Sources, depliez l’arborescence Constraints.
  2. Ouvrez le fichier ZYBO_Master.xdc.
  3. Décommentez les contraintes pour les ports ja_p[0] à ja_p[3] (lignes 163 à 190).
  4. Renommez les ports ja_p[0] à ja_p[3] en spi_cs, spi_mosi, spi_miso et spi_sck (dans cet ordre).
  5. Enregistrer le fichier.

Synthétiser

  1. Dans le Flow Navigator, sous Program and Debug, choisissez Generate Bitstream.
  2. À la fin de la synthèse, pressez le bouton Cancel dans la boîte de dialogue.

Mettre à jour le programme d’amorçage

Reconstruisez le fichier boot.bin en exécutant la commande suivante dans le terminal où vous avez lancé Vivado :

bootgen -w -image scripts/zybo-minimal.bif -o fpga/zybo-minimal/boot.bin

Copiez le nouveau fichier fpga/zybo-minimal/boot.bin sur la carte microSD et redémarrez la carte Zybo.

Utiliser l’IP depuis le shell

Comme dans les étapes précédentes, connectez-vous à la carte Zybo à l’aide d’un terminal série.

En utilisant les commandes poke et peek, vérifiez que vous pouvez dialoguer avec votre IP. Par exemple, si votre périphérique SPI est un accéléromètre ADXL345, son registre 0 retourne une valeur prédéfinie égale à 0xe5 :

# Phase = 1, Polarity = 1, Select = 0, Start = 0
poke 0x43c00000 0x0c
# Division de fréquence par 1000
poke 0x43c00004 1000

# -----------------------------------------------------
# Commande de lecture du registre 0 de l'accéléromètre
# -----------------------------------------------------

# Phase = 1, Polarity = 1, Select = 1, Start = 0
poke 0x43c00000 0x0e
# Read = 1, Multibyte = 0, Reg = 000000
poke 0x43c00008 0x80
# Phase = 1, Polarity = 1, Select = 1, Start = 1
poke 0x43c00000 0x0f
# Doit afficher 0x1e (Done = 1, Polarity = 1, Select = 1, Start = 0)
peek 0x43c00000

# ---------------------------------------------------------
# Récupération de la valeur du registre de l'accéléromètre
# ---------------------------------------------------------

# Phase = 1, Polarity = 1, Select = 1, Start = 1
poke 0x43c00000 0x0f
# Doit afficher 0x1e (Done = 1, Polarity = 1, Select = 1, Start = 0)
peek 0x43c00000
# Phase = 1, Polarity = 1, Select = 0, Start = 0
poke 0x43c00000 0x0c
# Doit afficher l'identifiant du périphérique: 0xe5
peek 0x43c00008