UCSBarchlab / PyRTL

A collection of classes providing simple hardware specification, simulation, tracing, and testing suitable for teaching and research. Simplicity, usability, clarity, and extensibility are the overarching goals, rather than performance or optimization.
http://ucsbarchlab.github.io/PyRTL
BSD 3-Clause "New" or "Revised" License
254 stars 76 forks source link

Example project with Xilinx Zynq 7010 or similar SoC hardware #272

Closed lneuhaus closed 4 years ago

lneuhaus commented 4 years ago

Hi,

I am not sure if there is a better place to post this question. If so, please let me know and I will move it.

As the creator of www.pyrpl.org (DSP tools for test and measurement applications running on RedPitayas, which are cheap Zynq7010 board with analog IO) I am currently getting interested in porting PyRPL from implementing the FPGA design in Verilog to doing that with PyRTL. For that, it would be helpful to have an example project that uses PyRTL to design the FPGA logic running on a Zynq7010 or similar SoC hardware. Is there any such project?

Thanks in advance, Leo

timsherwood commented 4 years ago

Hi Leo, I don't have such a project in here right now, but it is a good question. I think one of my Ph.D. student might have something that would help -- let me talk to them. It should be easy to replace any Verilog you right with PyRTL, the only issue is the automatically generated logic the tools make for you (e.g. AXI-bus interface) but that is on the list of things we are considering.

lneuhaus commented 4 years ago

Thanks a lot. In the meantime I figured out that the easiest approach would indeed be to integrate pyrtl-generated verilog logic into an existing Zynq project. This leaves the implementation of register read/write access from python the main remaining task for me. My approach would be to inherit from pyrtl's register classes and to supplement read-write functionality from Python. If you have working examples in this direction, or even some better solution, which would save me the beginner's mistakes, that would be really great!

lneuhaus commented 4 years ago

Just to be clear: I am thinking about something in the lines of this:

import io
import pyrtl

class ReadWriteRegister(pyrtl.Register):
    def __init__(self, bitwidth=None, read_only=False):
        super().__init__(bitwidth=bitwidth)
        self.read_only = read_only

    def __get__(self, instance, cls):
        if instance is None:
            return self
        print(f"Reading of register {self.name} would happen here.")
        return 123

    def __set__(self, instance, value):
        if self.read_only:
            raise ValueError(f"{self.name} is a read-only register.")
        print(f"Setting register {self.name} to value {value} would happen here.")

    def __set_name__(self, cls, name):
        self.name = name
        cls._read_write_registers.add(name)

class FpgaBaseClass:
    pyrtl.reset_working_block()

    _read_write_registers = set()

    fpga_revision = ReadWriteRegister(8, read_only=True)

    def __init__(self):
        print("Connecting to board and loading of the bitstream would happen here.")

    @classmethod
    def _programmable_logic(cls):
        cls.fpga_revision.next <<= 12  # some constant that can be read from the FPGA
        read_write_bus_i = pyrtl.Input(16, 'read_write_bus_i')
        read_write_bus_o = pyrtl.Output(16, 'read_write_bus_o')
        for i, name in enumerate(sorted(cls._read_write_registers)):
            print(f"Adding read-write logic for register {name} at address {i} would happen here.")
        cls._custom_programmable_logic()

    @classmethod
    def _custom_programmable_logic(cls):
        """
        Placeholder for custom PL.
        """ 

    @classmethod
    def generate_verilog_code(cls):
        print("\nGenerating verilog output\n=========================")
        cls._programmable_logic()
        with io.StringIO() as vfile:
            pyrtl.output_to_verilog(vfile)
            print(vfile.getvalue())

# this is the only code required for a custom board implementation
class MyFpgaBoard(FpgaBaseClass):
    counter_value = ReadWriteRegister(8, read_only=True)
    some_setting = ReadWriteRegister(10)

    @classmethod
    def _custom_programmable_logic(cls):
        cls.counter_value.next <<= cls.counter_value + 1

MyFpgaBoard.generate_verilog_code()  # prints the verilog code to the screen

print()
print("Showing usage once a bitstream is generated from Verilog, e.g. using Vivado")
print("===========================================================================")
board = MyFpgaBoard()
print(f"Counter value: {board.counter_value}")
board.some_setting = 10
print(f"Some setting: {board.some_setting}")
print(f"FPGA revision: {board.fpga_revision}")
board.fpga_revision = 10  # this will fail, as this is a read-only register

which evaluates to

Generating verilog output
=========================
Adding read-write logic for register counter_value at address 0 would happen here.
Adding read-write logic for register fpga_revision at address 1 would happen here.
Adding read-write logic for register some_setting at address 2 would happen here.
// Generated automatically via PyRTL
// As one initial test of synthesis, map to FPGA with:
//   yosys -p "synth_xilinx -top toplevel" thisfile.v

module toplevel(clk, read_write_bus_i, read_write_bus_o);
    input clk;
    input[15:0] read_write_bus_i;
    output[15:0] read_write_bus_o;

    reg[7:0] fpga_revision;
    reg[7:0] counter_value;
    reg[9:0] some_setting;

    wire const_65_0;
    wire[7:0] tmp154;
    wire[7:0] const_63_12;
    wire[8:0] tmp153;
    wire const_64_1;
    wire[7:0] tmp152;
    wire[6:0] tmp151;

    // Combinational
    assign const_65_0 = 0;
    assign const_63_12 = 12;
    assign const_64_1 = 1;
    assign tmp151 = {const_65_0, const_65_0, const_65_0, const_65_0, const_65_0, const_65_0, const_65_0};
    assign tmp153 = counter_value + tmp152;
    assign tmp154 = {tmp153[7], tmp153[6], tmp153[5], tmp153[4], tmp153[3], tmp153[2], tmp153[1], tmp153[0]};
    assign tmp152 = {tmp151, const_64_1};

    // Registers
    always @( posedge clk )
    begin
        fpga_revision <= const_63_12;
        counter_value <= tmp154;
    end

endmodule

Showing usage once a bitstream is generated from Verilog, e.g. using Vivado
===========================================================================
Connecting to board and loading of the bitstream would happen here.
Reading of register counter_value would happen here.
Counter value: 123
Setting register some_setting to value 10 would happen here.
Reading of register some_setting would happen here.
Some setting: 123
Reading of register fpga_revision would happen here.
FPGA revision: 123
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-73a8efab7420> in <module>
     80 print(f"Some setting: {board.some_setting}")
     81 print(f"FPGA revision: {board.fpga_revision}")
---> 82 board.fpga_revision = 10  # this will fail, as this is a read-only register

<ipython-input-40-73a8efab7420> in __set__(self, instance, value)
     16     def __set__(self, instance, value):
     17         if self.read_only:
---> 18             raise ValueError(f"{self.name} is a read-only register.")
     19         print(f"Setting register {self.name} to value {value} would happen here.")
     20 

ValueError: fpga_revision is a read-only register.

Certainly just a proof of concept with a few issues to iron out, but I guess the next step for this would be to set up the simulation such that the board's behavior could actually be implemented.

Is your solution similar to this, or are you thinking of something completely different?

gtzimpragos commented 4 years ago

Hi Leo,

thanks for your interest in our work! In the past, I have done something similar a couple times. More specifically, I was loading my PyRTL designs to a Xilinx PYNQ board (http://www.pynq.io) and controlling them using the board's ARM cores. We briefly describe this process in our FPL'17 paper (https://sites.cs.ucsb.edu/~sherwood/pubs/FPL-17-pyrtl.pdf). In a nutshell, we had developed a BRAM interface for the data transfer between the PS and PL parts and then used AXI-Lite for the required control signals. In the PYNQ website, you can also find a tutorial (https://discuss.pynq.io/t/tutorial-creating-a-hardware-design-for-pynq/145) describing how to package, load, and communicate with hardware blocks -- fpga cad tools cannot parse PyRTL code directly so we had to generate its Verilog equivalent (pyrtl.output_to_verilog()). Please let me know if that makes sense or you need more help.

Thanks, George

lneuhaus commented 4 years ago

Hi George,

Thanks for your answer. I think I get the idea. In case the source code of your project is online, that would certainly be helpful. Otherwise I know what to do anyways and we can close this issue.