Overview
A platform’s top module instantiates and connects the functional modules, and creates the IO ports to the board’s components and connectors. Module variants across different platforms, or further extensions and improvements thereof, usually use the same designator, in order to make experimentation as well as maintaining the different platforms easier. However, they are defined in files using different names, or stored in different directories. The specific modules used by a platform top module are defined in the corresponding Vivado (Xilinx FPGA) or Quartus (Altera FPGA) project file, respectively.
Locations
The top Verilog modules are in the lib
directory local to the platform directories, together with any other platform-specific modules.
The project files are in the build
directory in the platform directories.
General
The top modules do not contain any register logic, ie. there are no always
blocks. They exclusively serve to instantiate and connect the platform’s functional modules.
Platform-wide parameters are defined as module parameters, avoiding define
statements.
Like for all Verilog modules, the default_nettype
is none
. Also, the width of all wires and registers is usually indicated in all statements, unless it’s one.
The system reset signal is active high.
Wires
The wires for all IO modules follow the same pattern:
// spi
wire spi_0_stb;
wire [31:0] spi_0_dout; // received data, status
wire spi_0_sclk_d; // sclk signal from device
wire spi_0_mosi_d; // mosi signal from device
wire spi_0_miso_d; // miso signals to device
wire [2:0] spi_0_cs_n_d; // chip selects from device
wire spi_0_ack;
// log buffer
wire log_stb;
wire [31:0] log_dout; // log data output, stored log indices
wire log_ack
There’s always the strobe (enable) signal, the always 32 bits wide output wire, any device-specific connections, and the ack signal, prefixed by the corresponding module’s identifier. The ack signal is not used with the ETH architecture, and will be removed by the FPGA synthesiser as dangling logic.
IO Devices
For example:
// SPI
// two consecutive IO addresses
spie spie_0 (
// in
.clk(clk),
.rst(rst),
.stb(spi_0_stb),
.we(cpu_wr),
.addr(cpu_adr[2]),
.data_in(cpu_outbus[31:0]),
// out
.data_out(spi_0_dout[31:0]),
.ack(spi_0_ack),
// external out
.cs_n(spi_0_cs_n_d[2:0]),
.sclk(spi_0_sclk_d),
.mosi(spi_0_mosi_d),
// external in
.miso(spi_0_miso_d)
);
IO devices get allocated one or more consecutive IO addresses. The addr
signal together with we
(write enable) enables the module to decode the address internally, for example the SPI device with its two allocated IO addresses:
wire rd_data = stb & ~we & ~addr; // read received data
wire wr_data = stb & we & ~addr; // write data to transmit
wire rd_ctrl = stb & ~we & addr; // read status
wire wr_ctrl = stb & we & addr; // write control
See the I2C module for an example with four allocated IO addresses. This addressing scheme obviously requires the allocation of IO addresses in suitable blocks.
For uniformity, all IO devices have exactly one data output connection, which is always 32 bits wide. The muxing and any padding required is done within the module, for example the SPI module:
assign data_out[31:0] =
rd_data ? data_rx[31:0] :
rd_ctrl ? {22'h0, spi_ctrl[13:12], 7'b0, spi_rdy} :
32'h0;
Memory Map
All platforms use the same basic memory map.
FFFFFF +---------------------------+
| 64 dev IO addresses | 256 Bytes
FFFF00 +---------------------------+
| 64 dev addr (reserved) | 256 Bytes
FFFE00 +---------------------------+
| 64 dev addr (reserved) | 256 Bytes
FFFD00 +---------------------------+
| 64 dev addr (reserved) | 256 Bytes
FFFC00 +---------------------------+
| unused | 3 kB
FFF000 +---------------------------+
| PROM (reserved) | 2 kB
FFE800 +---------------------------+
| PROM | 2 kB
FFE000 +---------------------------+
| |
... ...
| |
| max |
| RAM | 16 MB - 8 kB
| space |
| |
... ...
| |
000000 +---------------------------+
**/
Each platform uses a different slice of the available RAM address space, depending on the hardware available in the FPGA itself, or on the board.
The original (Embedded) Project Oberon’s IO address range is extended to 256 bytes, hence allowing for 64 device addresses, while keeping compatibility with the traditional IO addresses. The overall address map has reserved space for three additional 256 IO address blocks should that be required.
For now, all platforms use the same specific IO address for the same IO device for easier SW compatibility.
Address Decoding
The three main memory areas, namely RAM, PROM, and IO, are selectively enabled accordingly:
assign ram_stb = (cpu_adr[23:13] != 11'h7FF);
assign prom_stb = (cpu_adr[23:12] == 12'hFFE && cpu_adr[11] == 1'b0);
assign io_en = (cpu_adr[23:8] == 16'hFFFF);
cpu_codebus
(from RAM or PROM) and cpu_inbus
(from RAM or IO) are multiplexed thusly:
assign cpu_codebus[31:0] = ~prom_stb ? ram_dout[31:0] : prom_dout[31:0];
assign cpu_inbus[31:0] = ~io_en ? ram_dout[31:0] : io_out[31:0];
The different IO devices are enabled (strobed) as follows (example):
// the traditional 16 IO addresses of (Embedded) Project Oberon
assign i2c_stb = (io_en && cpu_adr[7:4] == 4'b1111); // -16, -12, -8, -4
assign gpio_stb = (io_en && cpu_adr[7:3] == 5'b11100); // -32, -28
assign spi_0_stb = (io_en && cpu_adr[7:3] == 5'b11010); // -48, -44
assign rs232_0_stb = (io_en && cpu_adr[7:3] == 5'b11001); // -56, -52
assign lsb_stb = (io_en && cpu_adr[7:2] == 6'b110001); // -60
assign tmr_stb = (io_en && cpu_adr[7:2] == 6'b110000); // -64
// extended IO address range
assign scs_stb = (io_en && cpu_adr[7:3] == 5'b10111); // -72, -68
assign cts_stb = (io_en && cpu_adr[7:3] == 5'b10110); // -80, -76
assign stm_stb = (io_en && cpu_adr[7:4] == 4'b1010); // -96, -92, -88, -84
assign scfg_stb = (io_en && cpu_adr[7:2] == 6'b100101); // -108
assign wd_stb = (io_en && cpu_adr[7:2] == 6'b100100); // -112
assign ptmr_stb = (io_en && cpu_adr[7:2] == 6'b011111); // -132
assign start_stb = (io_en && cpu_adr[7:2] == 6'b010001); // -188
assign log_stb = (io_en && cpu_adr[7:3] == 5'b00100); // -224, -220
Note the IO address assignment blocks (one, two, or four addresses) and their alignment as mentioned above.
IO Data Multiplexing
Finally, the outputs from the IO devices are multiplexed onto io_out
based on their strobe signals:
assign io_out[31:0] =
i2c_stb ? i2c_dout[31:0] :
gpio_stb ? gpio_dout[31:0] :
spi_0_stb ? spi_0_dout[31:0] :
rs232_0_stb ? rs232_0_dout[31:0] :
lsb_stb ? lsb_dout[31:0] :
tmr_stb ? tmr_dout[31:0] :
scs_stb ? scs_dout[31:0] :
cts_stb ? cts_dout[31:0] :
stm_stb ? stm_dout[31:0] :
scfg_stb ? scfg_dout[31:0] :
wd_stb ? wd_dout[31:0] :
ptmr_stb ? ptmr_dout[31:0] :
start_stb ? start_dout[31:0] :
log_stb ? log_dout[31:0] :
32'h0;