# Interfacing a MCP23S17 (SPI) with a FPGA

I am working with a MCP23S17 SPI I/O expander chip in a VHDL project on my Basys 2.

At first glance I thought this was just a simple SPI interface where I put the chip select low and it will give me the data on the MISO line but it looks like it is bit more complicated with commands and initialization needed.

I added some setup bits ("0100" & "000" & "1") that pop out on the MOSI line once when you try to read data. but nothing has changed. There seems to be a lot of registers to hold settings but I have no clue on how to set these.

Here is a diagram of how I have it all hooked up. The testing I/O is only to make sure I have some known bits that should show up if the transaction succeeds. I will be using the B side of the chip so if something special needs to happen to read that then please explain.

What needs to happen to read data from the chip?

Here is the SPI module (SPI.vhd) that I have written so far.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

-- Uncomment the following library declaration if using
-- arithmetic functions with Signed or Unsigned values
--use IEEE.NUMERIC_STD.ALL;

-- Uncomment the following library declaration if instantiating
-- any Xilinx primitives in this code.
--library UNISIM;
--use UNISIM.VComponents.all;

entity SPI is
Generic (
dataWidthN : positive := 8
);
port(
sck: in std_logic; -- clock
mosi: out std_logic; -- data going into slave
miso: in std_logic; -- data coming out of slave
cs: in std_logic; -- chip select

address: in std_logic_vector(2 downto 0); -- 0 - 7

data: out std_logic_vector(dataWidthN-1 downto 0);

debug: out std_logic_vector(1 downto 0)
);
end SPI;

architecture Behavioral of SPI is
signal data_reg : STD_LOGIC_VECTOR (dataWidthN-1 downto 0);

begin

data <= data_reg;

process (sck)
variable isSetup: std_logic := '0';
variable setupBits: std_logic_vector(7 downto 0) := "0100" & address & "1";
variable setupBitCount: natural := 0;
begin
if rising_edge(sck) then  -- rising edge of SCK
if (cs = '0') then -- SPI CS must be selected

if (isSetup = '0' and setupBitCount < 7) then
mosi <= setupBits(7-setupBitCount);
setupBitCount := setupBitCount + 1;
else
isSetup := '1';
setupBitCount := 0;
end if;

if isSetup = '1' then
debug <= "11";

-- shift serial data into dat_reg on each rising edge
-- of SCK, MSB first
data_reg <= data_reg(dataWidthN-2 downto 0) & miso;
else
debug <= "10";
end if;

end if;
end if;
end process;

end Behavioral;


I have not found many articles talking about this chip using code. I found some Arduino stuff but they all use the SPI library which doesn't help explain what exactly is happening. Here are the few links that I have found:

## Edit:

Alright after working on what Dave Tweed said to do. I am able to send and produce the commands on MOSI but nothing comes back on the MISO line. Keep in mind that the FPGA should get the data and I have a logic analyzer that will show the bits if something comes out and my FPGA code is wrong.

CS:   1111000000000000000000000000
MOSI: xxxx0100aaa10000110000000000
MISO: xxxxxxxxxxxxxxxxxxxxxxxxxxxx


Here is the simulation in ISim. **This will not return data on MISO because it is just a simulation with no chip to actually send back proper data.*

And from a logic analyzer in the real world:

Here is the update SPI.vhd module code:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity SPI is
Generic (
dataWidthN : integer := 8
);
port(
sck: in std_logic; -- clock
mosi: out std_logic; -- data going into slave
miso: in std_logic; -- data coming out of slave
cs: in std_logic; -- chip select

address: in std_logic_vector(2 downto 0); -- 0 - 7

data: out std_logic_vector(dataWidthN-1 downto 0);

debug: out std_logic_vector(1 downto 0)
);
end SPI;

architecture Behavioral of SPI is

signal data_reg : STD_LOGIC_VECTOR (dataWidthN-1 downto 0);
begin

data <= data_reg;

variable transactionComplete: std_logic := '0';
variable setupBits: std_logic_vector(15 downto 0);
variable setupCmdBitCount: natural := 0;  -- setup command is 16 in length
variable readCmdBitCount: natural := 0;  -- A command is same as dataWidthN

variable currState: state_type := idle;
begin
setupBits := "0100" & address & "1" & "00001100";

if falling_edge(sck) then  -- rising edge of SCK

case currState is
if (cs = '0') then -- SPI CS must be selected
debug <= "10";

mosi <= setupBits(setupBits'length-1-setupCmdBitCount);

setupCmdBitCount := setupCmdBitCount + 1;

-- Move to the next state
if setupCmdBitCount >= setupBits'length then
setupCmdBitCount := 0;
end if;

else
currState := idle;
end if;

if (cs = '0') then -- SPI CS must be selected
debug <= "11";

-- shift serial data into dat_reg on each rising edge
-- of SCK, MSB first
data_reg <= data_reg(dataWidthN-2 downto 0) & miso;

transactionComplete := '1';
currState := idle;
end if;

else
currState := idle;
end if;

-- Idle state: if the state is unknown then we just go idle
when others =>
debug <= "00";

setupCmdBitCount := 0;
mosi <= '0';

if cs = '0' and transactionComplete = '0'  then
mosi <= setupBits(setupBits'length-1-setupCmdBitCount);
setupCmdBitCount := setupCmdBitCount + 1;

elsif cs = '1' and transactionComplete = '1' then
transactionComplete := '0';
end if;

end case;

end if;

end process;

end Behavioral;


The MCP23S17 is really meant to be connected to a microcontroller. I have used it successfully in a Blackfin-based project. It has a number of internal registers, just like the GPIO ports on a typical microcontroller. Each 8-bit port has a direction register, an input register and an output register, plus registers for input polarity and interrupt-on-change. There's also a global configuration register.

It does default to all-inputs at power up, so if that's all you need, then you just need to create a state machine that reads the two input registers. Note that you need to supply both a chip address byte and then a register address byte for each read cycle.

Also, you need to be aware that this chip has the funky feature of having two different address maps for the registers, depending on the setting of the "BANK" bit. Study this part carefully; it's pretty confusing.

The BANK bit is zero on power-up, so the two registers you want, GPIOA and GPIOB are found at addresses 12 and 13, respectively. Therefore, to read them both, you need to do two 24-clock SPI cycles:

CS:   1111000000000000000000000000111111110000000000000000000000001111
MOSI: xxxx0100aaa10000110000000000xxxxxxxx0100aaa10000110100000000xxxx
MISO: xxxx0000000000000000AAAAAAAAxxxxxxxx0000000000000000BBBBBBBBxxxx

• "aaa" represents the chip address.
• "AAAAAAAA" represents the data from port A
• "BBBBBBBB" represents the data from port B

Note that everything is MSB-first.

• Could you give me an example of trying to read A and B sides (what I should pump down MOSI)? Do I need to make this command everytime I want the data? – MLM May 30 '13 at 2:01
• I have updated my code so that can pipe out those commands correctly (works in simulation and and real world - logic analyzer). You can see my results in the op. Although I am sending these commands on MOSI nothing comes back on MISO. – MLM Jun 2 '13 at 0:14
• You're using chip address aaa=000. Are you sure the corresponding pins (A2, A1, A0) on the 23S17 are grounded? Also, you seem to have the MOSI data changing on the rising edge of the clock. You should be changing the data on the falling edge, since the 23S17 setup and hold times are relative to the rising edge. – Dave Tweed Jun 2 '13 at 0:47
• Yep, all A pins are grounded. No change when changing to falling edge: i.imgur.com/ua54TsF.png – MLM Jun 2 '13 at 2:27

Have you simulated your VHDL to verify that it is doing what you expect it to?

Your FPGA should be acting as the SPI master but it does not generate the SPI CLK signal. The VHDL process is also clocked from the same SPI CLK signal (sck) and because there is no clock on this signal your process doesn't do anything.

• The clock is generated on the top level module and then fed into the SPI module as well as the spi_sck line going to the chip. The simulation looks okay. SW0 goes low and the setup bits pop out. i.imgur.com/irxxmrg.png – MLM May 30 '13 at 1:50
• That sim picture doesn't prove that your SPI master on the FPGA is working. In fact it doesn't even show it shifting out 8 bits correctly, after 4 bits mosi goes 'U'. To my eyes your VHDL looks logically incorrect. I think you need a more comprehensive test bench to prove that your SPI master is working. – Amoch May 30 '13 at 2:33
• I see, it seems to shift out the first 4 correct and the debug shows it is setup after 8 clocks but there does indeed seem to be missing 4 bits. You can see the setupBits in the code above: variable setupBits: std_logic_vector(7 downto 0) := "0100" & address & "1"; – MLM May 30 '13 at 2:46
• Additional note, there is an errata for this part that you may want to pay attention to, IIRC it impacts the address you need to use. – Dan Mills Nov 29 '17 at 17:50