Terasic provides a useful tool, called SystemBuilder, to generate the pin-allocation files for the different peripherals on the board. You select the peripherals in a GUI, and the tool creates the corresponding file. This replaces Digilent's "master xdc" file approach. The SystemBuilder can be used incrementally, so you can start small, and then just generate the additional file entries later when you add more peripherals.
To configure the FPGA, Quartus Prime replaces Vivado for the Xilinx FPGAs. There's a free version, Quartus Prime Lite [2]. In general, Quartus is easy to use. It's pretty fast, compiling this configuration in under two and a half minutes on my machine. Volatile configuration of the FPGA is also fast, but programming the non-volatile configuration device on the board takes longer compared to Vivado. However, you can continue using the current configuration during the programming process.
This FPGA configuration boots the EPO disk image for the SD card as provided with version 8.0 of Astrobe for RISC5 (but check out the remark about I2C). It is a simplified version of my "standard platform", which also includes devices such as stack overflow monitor, watchdog, logging, calltrace, and more. The structure of the RISC5Top.v file is therefore a bit different, as are the different modules for the processor environment.
RISCTop.v: due to the increasing complexity of my platform, I had to find a more systematic approach to structure the system. I have taken inspiration from THM Oberon [3]. All functionality is implemented strictly in modules, there's no register logic on the top level. All modules have a uniform interface (apart from module-specifics), which allows me to systematically add or remove devices, as well as design them. Hardware functions and features are encapsulated within modules.
Xilinx-specifics: (Embedded) Project Oberon uses three Xilinx-specific components, namely for the clocks, the CPU registers, and the IO tri-state buffers. I have replaced the registers and the IO buffers with generic modules. The clock PLL is always FPGA-specific, but it's now encapsulated within a module. EPO uses specific buffers for the clocks, but I have found that this is not required, as Quartus and also Vivado will recognise the clocks and use the clock distribution infrastructure available on the specific FPGA. The same holds true for the GPIO tri-state buffers, for which the tools will select appropriate building blocks, and place the devices physically near the corresponding IO pins.
RAM: unfortunately, the Cyclone V GX FPGA offers fewer on-chip RAM blocks compared to the Artix-7, I was only able to get 416 kB maximum. (See below about using the SRAM instead.) As with the Artix-7 FPGA, the critical timings of the design are between the CPU registers and the RAM (via the bus), since the latter is implemented on the FPGA as several cell columns across the chip, which results in long paths. When I ran into "timing requirements not met", I had to reduce the system clock frequency to 38 MHz. If you reduce the amount of RAM to, say, 320 kB, you can increase the clock frequency. The RAM module allows to adjust the RAM size in steps of one kB via a parameter. Don't forget to adjust the bootloader accordingly.
RS232: the transmitter and receiver are combined into a single module, including buffers for both directions. The size of the buffers is parameterised. The buffered devices behave as unbuffered ones with EPO's software. To use the, say, buffered transmitting, just act on the “buffer not full” hardware signal in lieu of "buffer empty".
SPI: the SPI device is an extended version of EPO's. It is compatible with EPO's bootloader, Kernel, SPI device driver, and the Real-time clock. I had to introduce a 'epo_compat' parameter, as otherwise the additional configuration bits, which EPO obviously cannot "know", get zeroed out. The extensions include selectable SPI mode (clock polarity and clock phase, ie. CPOL and CPHA), as well as transmission modes for 16 bits and MSByte first, which are useful with some peripherals such as displays.
I2C: since I needed to write a "bus adapter" for the I2C device anyway, I have reduced the number of IO addresses used from six to four in the process. This required two small corresponding changes in the software module I2C, which is provided in the 'oberon/lib' directory. As I had to make these changes anyway, I have also defined a CONST for the system clock frequency, so the I2C serial clock will use the correct frequency as intended via I2C.Init().
IO: the IO address range is extended to 256 bytes, hence allowing for 64 device addresses, while keeping compatibility with the traditional IO addresses of (Embedded) Project Oberon. The overall address map has reserved space for three additional 256 IO address blocks should that be required (cf. bottom of RISC5Top.v for the map). There's a PDF outlining the currently implemented 256 bytes IO address space in the 'doc' directory.
LEDs, buttons, switches, 7-seg displays: the eight green LEDs are operated via Oberon's LED() procedures. The red LEDs can be switched by software as well as direct input signals from devices in the FPGA. The state of the buttons and switches can be read from software, but there are also direct output signals to other devices directly in the hardware. There's a module LSB in 'oberon/lib'.
HSMC connector: this connector can be used for different types of expansion cards. I mostly use it to extend the number of directly usable IO pins using the GPIO-HSTC card [4]. I have the 16 digital channels of my oscilloscope wired up to this card, as well as some other test peripherals, and just move it to my other Terasic board DE2-115 when I work there, without the need to rewire these connections again and again. Very handy indeed. Quartus allows to integrate an external logic analyser interface directly on the chip, which can connect to any signal inside. The connector is not wired for this configuration/project, but I can provide the assignment file if needed.
FPGA pin allocations: refer to the file RISC5Top.qsf in the build directory for how the input/output ports as defined in RISC5Top.v are allocated to the peripherals on the board. Obviously, the connections to the LEDs, switches, buttons, the 7-segment displays, the SD card, and the RS232 transmitter/receiver are given by the board design. GPIO, SPI (chip select 1 and 2), and I2C are allocated on the 40 pin expansion header. There's also a corresponding PDF in the 'doc' folder for the expansion header.
Quartus project: in the directory 'build' is a Quartus project file to build the configuration.
SRAM: I have experimented with using the 512 kB SRAM instead of the FPGA's RAM blocks, but only have a version that runs with a 20 MHz system clock. The SRAM is only 16 bit wide, so two read/write cycles are required. As of now, I have tried to get it running with the two memory cycles happening within one CPU clock cycle, ie. without stalling the CPU. A 40 MHz CPU clock leaves only 12.5 ns per memory cycle, so with the SRAM address set-up time of 10 ns for reading, and combinatorial gate delays through the bus and CPU logic of about 8 ns things are getting (too) tight. I will try to get a version running that stalls the CPU. SRAM should be less prone to path timing issues than the FPGA RAM.
Repository: please find the Verilog and Oberon modules as well as the other files as described above in this GitHub repository [5].
Here's the RISC5Top.v file if you want to get a quick overview without exploring the repo.
Code: Select all
/**
RISC5 CPU and environment definition for Embedded Project Oberon
--
Board and technology: Terasic Cyclone V Starter Kit, Altera Cyclone V GX
--
Base/origin:
* Embedded Project Oberon
* THM-oberon
--
2023 Gray, gray@grayraven.org
https://oberon-rts.org/licences
**/
`timescale 1ns / 1ps
`default_nettype none
module RISC5Top #(
parameter
clock_freq = 38_000_000, // as set in module 'clocks'
prom_file = "../bootload/BootLoad-416k-64k.mem",
rs232_buf_slots = 256, // RS232 buffer size in bytes, same for for tx and rx
num_gpio = 26 // number of GPIO pins
)(
// clock
input wire clk_in,
// RS-232
input wire rs232_0_rxd,
output wire rs232_0_txd,
// SD card (SPI CS = 0)
output wire sdcard_cs_n,
output wire sdcard_sclk,
output wire sdcard_mosi,
input wire sdcard_miso,
// SPI CS = 1 and 2
output wire [2:1] spi_cs_n,
output wire [2:1] spi_sclk,
output wire [2:1] spi_mosi,
input wire [2:1] spi_miso,
// LEDs, switches, buttons, 7-segment
output wire [7:0] led_g,
output wire [9:0] led_r,
output wire [6:0] hex1_n,
output wire [6:0] hex0_n,
input wire [3:0] btn_in_n,
input wire [9:0] swi_in,
// GPIO
inout wire [num_gpio-1:0] gpio,
// I2C
inout wire i2c_scl,
inout wire i2c_sda
);
// clk
wire clk_ok; // clocks stable
wire clk;
wire clk_rst;
// reset
wire rst; // active high
// cpu
wire [23:0] adr; // address bus
wire [31:0] inbus; // data to RISC core from RAM or IO
wire [31:0] codebus; // code to RISC core from RAM or ROM
wire [31:0] outbus; // data from RISC core
wire rd; // CPU read
wire wr; // CPU write
wire ben; // CPU byte enable
wire irq; // interrupt request to CPU
// prom
wire prom_stb;
wire [31:0] prom_dout;
// ram
wire ram_stb;
wire [31:0] ram_dout;
// i/o
wire io_en; // i/o enable
wire [31:0] io_out; // io devices output
// ms timer
wire tmr_stb;
wire [31:0] tmr_dout; // running milliseconds since reset
wire tmr_ms_tick; // millisecond timer tick signal
wire tmr_ack;
// lsb
wire lsb_stb;
wire [9:0] lsb_leds_r_in; // direct signals in for red LEDs
wire [31:0] lsb_dout; // buttons, switches status
wire [3:0] lsb_btn; // button signals out, 'clk' synced
wire [9:0] lsb_swi; // switch signals out, 'clk' synced
wire lsb_ack;
// rs232
wire rs232_0_stb;
wire [31:0] rs232_0_dout; // received data, status
wire rs232_0_ack;
// 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 signal to device
wire [2:0] spi_0_cs_n_d; // chip selects from device
wire spi_0_ack;
// gpio
wire gpio_stb;
wire [31:0] gpio_dout; // pin data, in/out control status
wire gpio_ack;
// i2c
wire i2c_stb;
wire [31:0] i2c_dout;
wire i2c_ack;
// clocks
clocks clocks_0 (
// in
.rst(clk_rst),
.clk_in(clk_in),
//out
.clk_ok(clk_ok),
.clk(clk)
);
// reset
reset reset_0 (
// in
.clk(clk),
.clk_ok(clk_ok),
.rst_in_n(btn_in_n[3]),
// out
.rst_out(rst)
);
// CPU
RISC5 #(.start_addr(24'hFFE000)) risc5_0 (
// in
.clk(clk),
.rst(~rst),
.irq(irq),
.codebus(codebus[31:0]),
.inbus(inbus[31:0]),
// out
.rd(rd),
.wr(wr),
.ben(ben),
.adr(adr[23:0]),
.outbus(outbus[31:0])
);
// boot ROM
prom #(.mem_file(prom_file)) prom_0 (
// in
.clk(~clk),
.en(prom_stb),
.addr(adr[10:2]),
// out
.data_out(prom_dout[31:0])
);
// RAM 416k
ramg5 #(.num_kbytes(416)) ram_0 (
// in
.clk(clk),
.en(ram_stb),
.we(wr),
.be(ben),
.addr(adr[18:0]),
.data_in(outbus[31:0]),
// out
.data_out(ram_dout[31:0])
);
// ms timer
// one IO address
ms_timer #(.clock_freq(clock_freq)) tmr_0 (
// in
.clk(clk),
.rst(rst),
.stb(tmr_stb),
// out
.data_out(tmr_dout[31:0]),
.ms_tick(tmr_ms_tick),
.ack(tmr_ack)
);
// LEDs, switches, buttons, 7-seg displays
// one IO address
assign lsb_leds_r_in[9:0] = 10'b0; // only 'clk' synced signals
lsb_s lsb_0 (
// in
.clk(clk),
.rst(rst),
.stb(lsb_stb),
.we(wr),
.leds_r_in(lsb_leds_r_in[9:0]),
.data_in(outbus[31:0]),
// out
.data_out(lsb_dout[31:0]),
.ack(lsb_ack),
.btn_out(lsb_btn[3:0]),
.swi_out(lsb_swi[9:0]),
// external in
.btn_in_n(btn_in_n[3:0]),
.swi_in(swi_in[9:0]),
// external out
.leds_g(led_g[7:0]),
.leds_r(led_r[9:0]),
.hex1_n(hex1_n[6:0]),
.hex0_n(hex0_n[6:0])
);
// RS232 buffered
// two consecutive IO addresses
rs232 #(.clock_freq(clock_freq), .buf_slots(rs232_buf_slots)) rs232_0 (
// in
.clk(clk),
.rst(rst),
.stb(rs232_0_stb),
.we(wr),
.addr(adr[2]),
.data_in(outbus[7:0]),
// out
.data_out(rs232_0_dout[31:0]),
.ack(rs232_0_ack),
// external
.rxd(rs232_0_rxd),
.txd(rs232_0_txd)
);
// SPI
// two consecutive IO addresses
spie #(.epo_compat(1'b1), .slow_div(80)) spie_0 (
// in
.clk(clk),
.rst(rst),
.stb(spi_0_stb),
.we(wr),
.addr(adr[2]),
.data_in(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)
);
assign sdcard_cs_n = spi_0_cs_n_d[0];
assign sdcard_sclk = spi_0_sclk_d;
assign sdcard_mosi = spi_0_mosi_d;
assign spi_cs_n[1] = spi_0_cs_n_d[1];
assign spi_sclk[1] = spi_0_sclk_d;
assign spi_mosi[1] = spi_0_mosi_d;
assign spi_cs_n[2] = spi_0_cs_n_d[2];
assign spi_sclk[2] = spi_0_sclk_d;
assign spi_mosi[2] = spi_0_mosi_d;
assign spi_0_miso_d = sdcard_miso & spi_miso[1] & spi_miso[2];
// GPIO
// two consecutive IO addresses
gpio #(.num_gpio(num_gpio)) gpio_0 (
// in
.clk(clk),
.rst(rst),
.stb(gpio_stb),
.we(wr),
.addr(adr[2]),
.data_in(outbus[num_gpio-1:0]),
// out
.data_out(gpio_dout),
.ack(gpio_ack),
// external
.io_pin(gpio[num_gpio-1:0])
);
// I2C
// four consecutive IO addresses
i2ce i2ce_0 (
// in
.clk(clk),
.rst(rst),
.stb(i2c_stb),
.we(wr),
.addr(adr[3:2]),
.data_in(outbus[31:0]),
// out
.data_out(i2c_dout),
.ack(i2c_ack),
// external
.scl(i2c_scl),
.sda(i2c_sda)
);
// address decoding
// ----------------
// cf. memory map below
// max RAM address space at 000000H to 0FFE000H (16 MB - 8 kB)
// adr[23:0] = 0FFE000H => adr[23:13] = 11'h7FF
assign ram_stb = (adr[23:13] != 11'h7FF);
// codebus multiplexer: RAM or PROM
// PROM: 2 kB at 0FFE000H => initial code address for CPU
// PROM uses adr[10:2] (word address)
assign prom_stb = (adr[23:12] == 12'hFFE && adr[11] == 1'b0);
assign codebus[31:0] = ~prom_stb ? ram_dout[31:0] : prom_dout[31:0];
// inbus multiplexer: RAM or IO
// IO block: 256 bytes (64 words) at 0FFFF00H
// there's space reserved for three more 256 bytes IO blocks
// at: 0FFFE00H, 0FFFD00, 0FFFC00
assign io_en = (adr[23:8] == 16'hFFFF);
assign inbus[31:0] = ~io_en ? ram_dout[31:0] : io_out[31:0];
// the traditional 16 IO word addresses of (Embedded) Project Oberon
assign i2c_stb = (io_en && adr[7:4] == 4'b1111); // -16, -12, -8, -4
assign gpio_stb = (io_en && adr[7:3] == 5'b11100); // -32 (data), -28 (ctrl/status)
assign spi_0_stb = (io_en && adr[7:3] == 5'b11010); // -48 (data), -44 (ctrl/status)
assign rs232_0_stb = (io_en && adr[7:3] == 5'b11001); // -56 (data), -52 (ctrl/status)
assign lsb_stb = (io_en && adr[7:2] == 6'b110001); // -60 note: system LEDs via LED()
assign tmr_stb = (io_en && adr[7:2] == 6'b110000); // -64
// extended IO address range
// eg: calltrace (not implemented)
// assign cts_stb = (io_en && adr[7:3] == 5'b10110); // -80, -76 (ctrl/status)
// IO data out multiplexing
// ------------------------
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] :
32'h0;
endmodule
`resetall
/**
FFFFFF +---------------------------+
| 64 dev addr (1 word each) | 256 Bytes
FFFF00 +---------------------------+
| 64 dev addr (unused) | 256 Bytes
FFFE00 +---------------------------+
| 64 dev addr (unused) | 256 Bytes
FFFD00 +---------------------------+
| 64 dev addr (unused) | 256 Bytes
FFFC00 +---------------------------+
| |
| -- unused -- | 3 kB
| !
FFF000 +---------------------------+
| !
| PROM (2k used) | 4 KB
| |
FFE000 +---------------------------+
| |
| |
| |
| |
| |
| max |
| RAM | 16 MB - 8 kB
| space |
| |
| |
| |
| |
| |
000000 +---------------------------+
**/
[1] https://www.terasic.com.tw/cgi-bin/page ... 0#contents
[2] https://www.intel.com/content/www/us/en ... ource.html
[3] https://github.com/hgeisse/THM-Oberon
[4] https://www.terasic.com.tw/cgi-bin/page ... 2#contents
[5] https://github.com/ygrayne/oberon-epo/t ... cv-sk-base