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
the data_o
bus of the download FIFO.
To pop 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
dnEmpty_o
, dnFull_o
, and 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 data_i
bus
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:
Address | R/W | Function |
---|---|---|
0 | R | Pop and return data from the upload FIFO |
0 | W | Push data onto the download FIFO |
1 | R |
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.
For the null-modem emulator on linux, socat seems to be the most mentioned tool. For Windows, I had to try several before eventually settling on com0com because it is:
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.
Comments
Thank you Dave for documenting this; are we restricted to ZPUino; I wished to use this feature with a different SOC system.
Link / ReplyCan you share the UCF file ? and how to instantiate the module that interface the PC to the FPGA application logic ?
For now, I will use the com0com and python script, but it would be nice if the installation of the XSTOOLs also installed drivers that would automatically create the COM port when the Xula is plugged in.
William, the VHDL for the HostIoComm module is all available so you can use it with anything you want.
Link / ReplyThere isn't any specific UCF file for this module. It's all internal to the FPGA and communicates through the JTAG port out to the USB link. The only pin assignments you'll need are for whatever I/O your application logic has.
I'm not sure about creating a COMx port whenever the XuLA is plugged in. Many people will never use the HostIoComm module, so that would be a waste for them. And for those that do, a single execution of com0com will setup a virtual null modem that they can use forever until they manually delete it using the Device Manager.
Interesting concept indeed. How does you python module behave with this constant polling of the USB port and checking for the availability of data? I mean, CPU usage....because from what I can see you're just looping around and around waiting for data. For the serial port, that's not a problem since the kernel handles it, but I'm wondering how efficient the rest of it is.
Link / ReplyYour idea got me to think about implementing a fake serial interface for the ZPUino where I could write a functions similar to serial.print() and serial.read() that talk to a fifo over wishbone. Then, by pushing and pulling data back via your jtag user interface to a terminal python script, I could void the need for serial communication altogether. Anyway, it wouldn't be that different from your project but i'd prefer to bypass the whole putty and null-modem situation.
I'm just still not entirely sure it's a good idea.
By the way, I will start posting some of my work soon too, I just got a website going pretty much just for that purpose.
Thanks
Francois, I don't know how efficient the looping on the USB port is, but I don't know any way to avoid it. I'm not aware of any USB control structures that alert the host when a peripheral device has data it wants to offload.
Link / ReplyIt's definitely possible to implement your fake serial interface. All you would need to do add a Wishbone interface to the HostIoComm (that's already been done), set the device id to the same one used by ZPUino UART modules, and then use the serial.* routines already in the library.
Within the PC, you can use the USB-to-serial server and strip-off the serial link interface. Then insert your Python code to work with the data directly.
Looking forward to seeing more of your work!
Hi Dave! I want to do the echo test on my XuLa2 LX9 board so I downloaded the EchoTest project file you made on GitHub. I followed the procedure to install the VHDL_Lib but somehow I'm still getting error '<ctrl_i> is not declared' when I try to synthesize the project. Can you tell me to fix this error, please?
Link / ReplyGarry, I've cloned the VHDL_Lib repo and re-built the EchoTest design without any problems. I can't even find a ctrl_i signal in the top-level module so I suspect it is buried in a lower-level module. I don't know why you would get an error about that if the library is installed correctly.
Link / ReplyMake sure you create the XESS library in your project. Then set the EchoTest module as your top-level module. Then add the XuLA2.vhd and XuLA2.ucf files to your project. Check the option that allows unmatched LOC constraints or else the XuLA2.ucf file will create a lot of errors. Make sure the pullups are off on the TCK pin. Set JTAG clock for the startup clock. Then you should be able to compile it.
Thank you very much, Dave. Now it's working perfectly.. :)
Link / ReplyNew Comment