With processor-based systems, many of us like the simplicity and flexibility of communicating with our software applications through a terminal (like PuTTY).
Taken to an extreme, some of us even want to communicate with our FPGA applications through a terminal. But that can be difficult with something like the XuLA FPGA board, which doesn't even have a serial port. So what do you do?
For the XuLA, I've beaten problems like this before by building a data pipeline that goes through the USB link to talk with application logic in the FPGA as shown here and here. But those examples use a Python API on the host computer side and any data buffering in the FPGA is implemented using the SDRAM, which is "complicated".
Sometimes I'd like a simpler system with a couple of FIFOs: one that the application logic can dump data into so that it will eventually appear back at the PC, and another that receives data from the PC which the application logic can read.
Of course, the PC needs a way to access the FIFOs, so I can just add a HostIoToRam interface on that side. While technically not RAMs, the FIFOs can be read and written using a RAM-like interface. A write from the PC over the USB link will push data into the download FIFO, while a read will pop data from the upload FIFO.
That still leaves the problem of providing a terminal-like interface on the PC side. For that, I'll build a server in Python that mirrors any data coming or going through the USB link onto a serial link.
But how does that help? Sending data to a serial interface on a PC typically means sending it to an external serial port. (Do PCs even have those anymore?) But my server is running as software inside the PC. How do I get access to the serial stream of data using PuTTY?
The answer is to use a null modem emulator (also called a virtual null modem) which is a software process that mimics two connected, physical serial I/O ports. So my server transfers data from the USB link to one serial port while the PuTTY talks to the other serial port. The end result is that PuTTY thinks it it talking directly to the application on the FPGA through a serial interface.
Putting it all together, this is what the complete link looks like.
This is all great in theory, but how do you actually do it? There are a few issues that need more explanation:
The application logic hooks to the FIFOs as follows:
The current piece of data received from the PC (call it
data[T]) is always available on
data_o bus of the download FIFO.
data[T] from the FIFO and get the next data word, the
rmv_i input is raised.
On the next rising edge of
clk_i (edge #2 in the example below),
the new data (
data[T+1]) appears on the
data_o bus and the
dnLevel_o outputs are updated to
reflect the empty, full and occupancy level status of the FIFO, respectively.
For sending data to the PC, a new data word is placed on the
of the upload FIFO and the
add_i input is raised.
The new data is accepted on the next rising edge (edge #2 in the example below)
and the status outputs are updated.
Moving over to the PC-facing side, the FIFOs connect to a HostIoToRam module through a piece of shim logic.
The shim logic maps the FIFO interfaces into a set of read/write registers with the following functions:
|0||R||Pop and return data from the upload FIFO|
|0||W||Push data onto the download FIFO|
Read the empty/full status of both FIFOs:
|1||W||Reset both FIFOs to the empty state|
|2||R||Get the # of words in the download FIFO|
|3||R||Get the # of words in the upload FIFO|
|4||W||Outputs an active-high signal to indicate a BREAK condition to the FPGA application logic.|
To make it easier to use, the VHDL for the HostIoToRam module, shim logic and FIFOs are packaged together as the HostIoComm module. Included in the VHDL file is an example FPGA design that accepts characters from a host PC and echoes them back. The section of the EchoTest design that interacts with the HostIoComm module is reproduced below:
-- Instantiate the HostIoComm communication interface. u2 : HostIoComm generic map( SIMPLE_G => true ) port map( reset_i => reset_s, clk_i => clk_s, rmv_i => rmv_r, -- Pull high to remove data received from the host. data_o => dataFromHost_s, -- Data from the host. dnEmpty_o => empty_s, -- False if the download FIFO has data from the host. add_i => add_r, -- Pull high to add data to FIFO going back to host. data_i => std_logic_vector(dataToHost_r), -- Data to host. upFull_o => full_s -- False if the upload FIFO has room for more data. ); -- This process scans the incoming FIFO for characters received from the host. -- When found, it removes the character from the incoming FIFO and places it in the FIFO that -- transmits back to the host. This process works on the falling clock edge -- while the HostIoComm works on the rising clock edge. So the control signals -- for the echo transfer are set up on the falling edge and the actual transfer -- between FIFOs occurs on the next rising edge. The FIFO statuses are also updated -- on the rising edge so they can have an effect on this process on the next falling edge. echoProcess : process(clk_s) begin if falling_edge(clk_s) then -- By default, don't add or remove characters (no echo). rmv_r <= LO; add_r <= LO; if (reset_s = LO) and (empty_s = NO) and (full_s = NO) then -- If there is no reset and the incoming FIFO has data while -- the outgoing FIFO has room for it, then transfer the -- character from the incoming to the outgoing FIFO. rmv_r <= HI; -- Removes char received from host. dataToHost_r <= unsigned(dataFromHost_s); -- Echo the char. add_r <= HI; -- Places char on FIFO back to host. end if; end if; end process;
Since USB data transfers are always initiated by the host device (the PC, in this case), most of the responsibility for the control of data flowing over the link resides with the USB-to-serial server Using the XSTOOLs Python package, the server performs simple register reads/writes to transfer data to/from the FIFOs in the FPGA. The server implements flow control by querying the FIFO status and level registers and acting accordingly, i.e. don't send more data if the download FIFO is full or it will be lost, and don't read data from the upload FIFO if it is empty or else it will be garbage. The FPGA application logic must also abide by these rules when it sends and receives data.
This video shows how to download the EchoTest bitstream into a XuLa board, create a null-modem interface, get the USB-to-serial server running, and then connect PuTTY to it for sending/receiving characters.
Now that terminal-like communications with an FPGA are possible, it should be relatively
easy to add this feature to the ZPUino and ditch the FTDI-to-UART cable.