Implementing or porting IO devices

This document should serve as a base for you to implement or port IO devices to work in ZPUino

This document is still under development!

Overview of IO devices

Signal description

WB_CLK_O

This is the main clock signal. The IO clock is the same as the core clock.

WB_RST_O

The global reset signal (high). This is a synchronous signal and is implemented as a "soft" reset.

WB_DAT_O

Data write from core to IO device. This is a 32-bit signal, in big-endian order.

WB_DAT_I

Data read from IO device to core. This is a 32-bit signal, in big-endian order.

WB_ADDR_O

IO address from core to IO device. This is a 9-bit signal, word-aligned (10 downto 2).

WB_CYC_O

Cycle start signal from core to IO device

WB_STB_O

Strobe signal from core to IO device WB_WE_O Write-Enable signal from core to IO device.

WB_ACK_I

ACK signal from IO device to core.

WB_INT_I

INTERRUPT signal from IO device to core. This signal should stay asserted until a IO operation to device clears it explicitly. This signal might have other name, like WB_INTA_I or WB_INTB_I. This allows mapping more than one interrupt line.

SPP_EN

SPP (Special Purpose Pin) enable signal. If device uses PPS, it should assert this to use GPIO pins.

SPP_WRITE

SPP write signals. These will be mapped to GPIO pins using the PPS multiplexer. These signals are optional.

SPP_READ

SPP read signals. These will be mapped to GPIO pins using the PPS demultiplexer. These signals are optional.

Timing diagrams

Example IO device: a 64-bit counter

The following examples implement a 64-bit counter. We decided to design this counter with the following assumptions:

  • The counter shall be 64-bit, and we must be able to read the low-part (lower 32bit) and the high-part (upper 32-bit) as if they were two 32-bit registers.
  • It should have an enable register. When enable is one, the counter shall count upwards, incrementing one at each clock cycle. When zero, it shall keep its previous value.
  • We should be able to write the counter value itself.
-- 64-bit counter example IO module for ZPUino library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; library work; use work.zpu_config.all; use work.zpupkg.all; use work.zpuinopkg.all; entity counter is port ( wb_clk_i: in std_logic; wb_rst_i: in std_logic; wb_dat_o: out std_logic_vector(wordSize-1 downto 0); wb_dat_i: in std_logic_vector(wordSize-1 downto 0); wb_addr_i: in std_logic_vector(maxIOBit downto minIOBit); wb_we_i: in std_logic; wb_cyc_i: in std_logic; wb_stb_i: in std_logic; wb_ack_o: out std_logic; wb_inta_o: out std_logic ); end entity counter; architecture behave of counter is signal count_q: unsigned(63 downto 0); -- The main register signal counter_enabled_q: std_logic; -- Counter enable/disable begin -- Acknowledge all tranfers wb_ack_o <= wb_stb_i and wb_cyc_i; -- Tie interrupt to '0', we never interrupt wb_inta_o <= '0'; -- Read multiplexer. Note we don't need to use 're' signal here -- unless we want to modify anything when 're' is '1'. process(wb_adr_i,count_q,counter_enabled_q) begin case address(minIOBit+1 downto minIOBit) is when "00" => -- Address 0 -- Read counter enabled wb_dat_o <= (others => '0'); -- All bits read as '0'... wb_dat_o(0) <= counter_enabled_q; -- except the first one when "10" => -- Address 2 -- Read low-part of counter wb_dat_o <= std_logic_vector(count_q(31 downto 0)); when "11" => -- Address 3 -- Read high-part of counter wb_dat_o <= std_logic_vector(count_q(63 downto 32)); when others => wb_dat_o <= (others => DontCareValue); -- No value to output end case; end process; -- Main process process(wb_clki) begin if rising_edge(wb_clk_i) then if wb_rst_i='1' then -- Reset counter enabled. counter_enabled_q <= '0'; else if wb_cyc_i='1' and wb_stb_i='1' and wb_we_i='1' then case wb_addr_i(minIOBit+1 downto minIOBit) is when "00" => counter_enabled_q <= wb_dat_i(0); when "10" => count_q(31 downto 0) <= unsigned(wb_dat_i); when "11" => count_q(63 downto 32) <= unsigned(wb_dat_i); when others => end case; else if counter_enabled_q='1' then count_q <= count_q + 1; end if; end if; end if; end if; end process; end behave;

The registers

So we have implemented our counter now, let's take a closer look at its address space:

Address 0

In address 0 we map a single-bit register (counter_enabled_q). All other bits read as '0', and are ignored during writes.

Address 1

Address 1 is not used.

Address 2

Address 2 maps the lower 32-bit part of the 64-bit register. It can be read and written.

Address 3

Address 3 maps the upper 32-bit part of the 64-bit register. It can be read and written.

Introducing your device to the IO system

Before you can connect your device to ZPUino you must let it know in advance your device interface. There are two places where you can do that:
  • In zpuino_io.vhd file itself;
  • In zpuinopkg.vhd package file.
On this example we'll add our device to the package file. Just edit the file and add your component declaration, like in the following example:
component zpuino_adc is port ( wb_clk_i: in std_logic; wb_rst_i: in std_logic; wb_dat_o: out std_logic_vector(wordSize-1 downto 0); wb_dat_i: in std_logic_vector(wordSize-1 downto 0); wb_adr_i: in std_logic_vector(maxIObit downto minIObit); wb_we_i: in std_logic; wb_cyc_i: in std_logic; wb_stb_i: in std_logic; wb_ack_o: out std_logic; wb_inta_o:out std_logic; sample: in std_logic; -- GPIO SPI pins mosi: out std_logic; miso: in std_logic; sck: out std_logic; seln: out std_logic; enabled: out std_logic ); end component zpuino_adc; component counter is port ( wb_clk_i: in std_logic; wb_rst_i: in std_logic; wb_dat_o: out std_logic_vector(wordSize-1 downto 0); wb_dat_i: in std_logic_vector(wordSize-1 downto 0); wb_addr_i: in std_logic_vector(maxIOBit downto minIOBit); wb_we_i: in std_logic; wb_cyc_i: in std_logic; wb_stb_i: in std_logic; wb_ack_o: out std_logic; wb_inta_o: out std_logic ); end component counter; end package zpuinopkg;

Connecting your device

In order to connect your device to ZPUino IO you need to attach it to an empty slot. A total of 16 slot exists, some of them are not used and you can easily locate them, because they have a zpuino_empty_device mapped to them. Here's an excerpt of zpuino_io.vhd, where you can see that IO Slot 9 is free to use:
-- -- IO SLOT 9 -- slot9: zpuino_empty_device port map ( wb_clk_i => wb_clk_i, wb_rst_i => wb_rst_i, wb_dat_o => slot_read(9), wb_dat_i => slot_write(9), wb_adr_i => slot_address(9), wb_we_i => slot_we(9), wb_cyc_i => slot_cyc(9), wb_stb_i => slot_stb(9), wb_ack_o => slot_ack(9), wb_inta_o => slot_interrupt(9) );

So let's connect our counter example to this slot, replacing the zpuino_empty_device:

-- -- IO SLOT 9 -- slot9: counter port map ( wb_clk_i => wb_clk_i, wb_rst_i => wb_rst_i, wb_dat_o => slot_read(9), wb_dat_i => slot_write(9), wb_adr_i => slot_address(9), wb_we_i => slot_we(9), wb_cyc_i => slot_cyc(9), wb_stb_i => slot_stb(9), wb_ack_o => slot_ack(9), wb_inta_o => slot_interrupt(9) );
And that's it! The new device is now attached and ready for synthesis.

Accessing our counter.

To access out counter device from the sketch, we ought to declare its registers and map them to the correct IO slot. We can then use them easily across our code:
/* Define our counter base IO address to slot 9 */ #define COUNTERBASE IO_SLOT(9) /* Counter control maps in address 0 of our slot */ #define COUNTER_CTRL REGISTER(COUNTERBASE,0) /* Low 32-bit part, address 2 */ #define COUNTER_LOW REGISTER(COUNTERBASE,2) /* High 32-bit part, address 3 */ #define COUNTER_HIGH REGISTER(COUNTERBASE,3) /* 1st bit (index 0) is out counter enable bit */ #define COUNTERENABLED 0 void setup() { Serial.begin(115200); } void loop() { /* Stop our counter, by masking the COUNTERENABLED bit */ COUNTER_CTRL &= ~(_BV(COUNTERENABLED)); /* Reset high and low part of counter */ COUNTER_LOW = 0; COUNTER_HIGH = 0; /* Start our counter */ COUNTER_CTRL |= _BV(COUNTERENABLED); ... /* Do something here */ /* Stop our counter again */ COUNTER_CTRL &= ~(_BV(COUNTERENABLED)); /* Print it */ Serial.print("Counter high is "); Serial.print(COUNTER_HIGH,10); Serial.print(", low is "); Serial.println(COUNTER_LOW,10); /* That's it! */ }