I’d like to spend some time on this forum discussing how to debug an FPGA. Indeed, I might wish to spend a lot of time discussing how to debug an FPGA. I’ve done it several ways, but for this post I’d like to discuss a vision for how I like to debug FPGA’s.
Memory Mapped I/O
We’ll start with the concept of Memory Mapped I/O. The idea is simply this: peripherals can be connected to a bus, together with whatever memory is on that bus, so that they can be accessed like memory. Like memory, the peripheral will have an address. Like memory, the bus controller, whether it be a CPU or DMA peripheral, can write to the peripheral and read from it. Unlike memory, however, reading or writing to a peripheral can have side effects.
We’ll use the term “register” to describe a single address within a peripheral.
Register’s can be read only, write only, or read/write. Further, peripherals may have many addresses. Finally, peripherals may take different amounts of time to access. For example, a block RAM peripheral can typically be accessed in a single cycle, whereas an SDRAM peripheral may take longer, and a flash peripheral may take much longer.
Perhaps a perfect example of this concept is that of a serial port, such as the wbuart32 serial port the ZipCPU uses. Reading from the receiver address of this serial port returns the next character from the receiver queue, advancing the queue to the next character. Writing to the transmit register queue’s a character for sending over the port. A third register controls the speed and configuration of the port, whereas a fourth register is used when the serial port has a queue to indicate how much of the queue is used.
But how does this apply to controlling an FPGA? Simple! We’ll implement our logic as peripherals on an FPGA bus, and then control those peripherals by simply reading from or writing to that bus. By having a common standard for accessing peripherals, we only have to build our peripherals to meet that standard once.
Perhaps the vision I’m about to share will make more sense if I explain that I first came up with it while trying to figure out how to control an FPGA on a PCIe card. Indeed, I was trying to figure out how I would control a VC707 card made by Xilinx.
The PCIe bus allows for several different areas or sections of address space which get assigned to each card. These can include both memory address space, and peripheral address space, but every thing is still accessed on a bus. You can read from any given addresses on that bus, or write to a given addresses, as with any other bus.
Since I wanted to build an FPGA capability that could eventually be used on an FPGA over a PCIe interface, I wanted to create something that would work on a PCIe bus eventually, but that could be used over some other transport prior to that point. Further, when plugging the FPGA into the PCIe bus, I already knew that I would need to use some sort of alternative transport in order to debug whatever was going on.
So, here was my vision: I would use the serial port on the VC707 to command the VC707 as though it were connected via PCIe. Then, over this same serial port, I could get the debugging information I needed to know if the PCIe bus were working, or if not … why not?
If you are going to create a memory mapped bus, that you are then going to work with, you need to know what operations you want to do on this bus. I captured the interface for, what I called a device bus, in a C++ header file, devbus.h. Operations on this interface include:
readio(): Reads a 32-bit value from any individual register
readz(): Reading multiple 32-bit values from the same register. While most memories have one address per memory location, peripherals aren’t necessarily like that. This mode captures some of that difference. Some examples where this would be useful include reading from a FIFO, such as the serial port input FIFO, and reading from my debugging scope, where multiple reads from the data register read subsequent values from the scope’s memory.
readi(): Reading a series of 32-bit values from consecutive registers. This is akin to a memcpy command, where one end is coming from the device.
The same operations are then mirrored for writing to registers.
This bus interface also allows for some minimal interrupt capability. For example, you can wait() for an interrupt to be created within the device, or poll() to see if an interrupt has taken place. How these interactions take place is … up to whatever transport implements the interface.
Back when I started working on computers decades ago, you could Peek or Poke an addresses within your Apple IIe computer, and so control the hardware. While I thought of repeating this approach with FPGA’s, I also like the thought of being able to name the peripheral register, rather than trying to remembering all the register numbers (OUCH!). This would allow an FPGA to be reconfigured so the addresses might be moved around, and yet you could still read and write the same peripherals via the same names. I like this idea so much, that all of my projects have a wbregs program within them to control peripherals over some command line.
Rather than getting into details regarding how such an interface might be built (yet), I’d like to add one more component to the vision: I want to be able to control my FPGA devices from anywhere on my local network. If I’m working on my laptop, I’d like to be able to control them from my laptop. If I’m working on my desktop, I’d like to be able to control them from my desktop. If I have the device located next to an antenna in the attic, I still want to be able to control it.
For this purpose, I like to forward all of my transport connections over a TCP/IP link. While this would probably never be appropriate for an FPGA with a PCIe connection, it has worked for all of my FPGA’s with slower transport connections.
Lest you be confused, this is different from having an FPGA with an Ethernet port that can be accessed from your network. We want to use our network interface to command, control, and debug our FPGA before the network controller connected to the FPGA works.
This vision for FPGA control leads nicely into a vision for building and debugging an FPGA. We’ll get more into that idea as this blog progresses, but for now … we’ll just use this idea to keep things simple.
Where there is no vision, the people perish: but he that keepeth the law, happy is he. (Prov 29:18)