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.
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.
La synchronisation des données sur l’horloge série est définie par deux paramètres.
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.
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.
Dans Vivado :
SPIDevice
et sélectionnez Edit in IP Packager.Dans le fichier SPIDevice_v1_0_S00_AXI.vhd
:
Dans le fichier SPIDevice_v1_0.vhd
:
SPIDevice_v1_0_S00_AXI
.SPIDevice_v1_0_S00_AXI
.SPITestbench_v1_0.vhd
. Pressez le bouton Finish.É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
Mettez à jour l’IP dans le catalogue :
Dans la fenêtre Vivado du projet principal,
SPIDevice
et choisisez Create Port.ZYBO_Master.xdc
.ja_p[0]
à ja_p[3]
(lignes 163 à 190).ja_p[0]
à ja_p[3]
en spi_cs
, spi_mosi
, spi_miso
et spi_sck
(dans cet ordre).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.
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