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 deviceWB_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)
);
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.
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! */
}