Recently, I've been working on repurposing some FPGA-based devices. Since the devices in question can only be programmed via their JTAG interfaces, I needed an FPGA JTAG programmer. Unfortunately, loading FPGA bitstreams with OpenOCD using SEGGER J-Link turned out to be less than trivial. I'm sure others will run into this issue in the future so to avoid duplicate effort, I've written this post detailing my experiences.
Defining the Problem
The normal flow for Xilinx FPGA configuration over JTAG is very easy:
- Generate the bitstream
- Set up the boundary scan chain in Xilinx iMPACT
- Generate an SVF file
- Program the device using iMPACT and your Xilinx Platform Cable
Unfortunately (or maybe fortunately) for me, I did not have a Xilinx Platform Cable, so I could not perform Step 4. However, I did have a SEGGER J-Link JTAG cable and I'd read that OpenOCD could use it to play the SVF files iMPACT generates, so I thought I was good-to-go. Unfortunately again, weird things kept going wrong between OpenOCD's SVF player and its J-Link driver so I was still unable to program my FPGA. Seeking a potentially-easy solution, I turned to UrJTAG.
That was a mistake. While UrJTAG also has SVF playback support, its J-Link driver had not been touched in five years so, naturally, it did not work with my J-Link's new firmware.
For some reason, I decided that it would be easier to fix UrJTAG's J-Link driver than it would be to figure out why OpenOCD wasn't working (this was another mistake). To help get myself started, I asked some questions in the #openocd channel on Freenode. The people there were friendly and eager to help, but they correctly pointed out that I would get more help if I tried porting UrJTAG's features to OpenOCD. After wasting a day messing with UrJTAG, and after learning that OpenOCD had all of the capabilities I thought only UrJTAG had plus a TLC RPC server, I decided to try my hand at writing a Python-based Spartan-6 programmer. This would effectively bypass steps 2-4, which would have been nice, but I didn't have enough experience to do it properly. Putting that project aside, I decided to fix OpenOCD's SVF player.
A person in the channel going by the handle "PaulFertser" helped me debug the issues I was having. As it turned out, the problem wasn't really in the SVF player, but in OpenOCD's J-Link driver. Specifically, it lacked support for the STABLECLOCKS command, which was causing OpenOCD to crash whenever it encountered an SVF RUNTEST command.
After reading the code in
jlink.c, I wrote a patch to add
the necessary functionality to the J-Link driver. However, I wasn't done yet.
After solving that problem, I ran into an issue with the way the J-Link driver
handled command buffering. Basically, the driver only buffers commands up to the
J-Link device's internal limit instead of buffering indefinitely and filling the
J-Link's buffer as-needed. This problem was easily solved with a small hack, but
that hack broke some of the SVF player's TDO-checking functionality. Thankfully,
I didn't need that functionality, so I just left it at that.
Adding Support for the Digilent Analog Discovery
Since I wasn't really sure where to go from there, I decided to work on a related side-project involving the Digilent Analog Discovery MSO/AWG. Basically, I wanted to be able to program its FPGA using OpenOCD so work could begin on getting it supported in sigrok.
This turned out to be a lot easier than I thought since the OpenOCD FTDI driver is configured using TCL for each FTDI-based JTAG device. Fortunately, someone else had already done the hard work of getting it supported in UrJTAG, so I was able to take the relevant configuration parameters and use them to create another patch for Openocd.
Writing a Programmer in Python
With the previous issues settled and the knowledge I gained, I was able to move forward with my plans to cut iMPACT out of the programming process.
To start, I had to read up on how Spartan-6 configuration works. Thankfully,
Xilinx has a user guide (UG380) with information on all of the
configuration interfaces for Spartan-6 devices. From that guide, I was able to
write some code to interact with OpenOCD and attempt to program my FPGA.
However, due to my inexperience with JTAG, I didn't know that both the
drscan commands shifted data into the chip LSB-first, so when I tried to
send the configuration packets (which UG380 specified as needing to be sent
MSB-first), nothing happened. It was only after I observed how the SVF file was
sending commands that I realized that all of the configuration packets needed to
be bit-flipped left-to-right (i.e., 0x2000 -> 0x0004). Despite making that
change, my code still didn't work.
After doing a lot of reading on the JTAG state machine, I realized why I was
still unable to program my FPGA: In each version of my code, I had been shifting
the configuration packets into the device one-by-one, separating each packet
with either a DRPAUSE state or the default RUN/TEST state. What I ended up
learning, though, was that all the configuration packets needed to be shifted in
continuously by using
-endstate DRSHIFT between each
drscan command. In
hindsight, this explains the name of the bitstream (.bit) file since it
literally contains a stream of bits that get sent to the FPGA over a
single wire (TDI).
After some fiddling, my code started working, and after a few optimizations, I was able to get it to program my Spartan-6 FPGAs despite the 2 KiB limit of the J-Link's command buffer.
What I Learned
Don't always trust datasheets
While UG380 had nearly all the information I needed, it would have been nice for it to have some explanations as to why things needed to be done the way they were done instead of just presenting the instructions. I mention this because presenting instructions on how to do something without explaining the motivations behind each instruction leads to cargo cult programming, where no one really understands why certain things are done, only that trying to modify those voodoo-magic sequences doesn't work.
Observe what the vendor tools are doing
The vendor tools always need to work, so if you think you're following the vendor's instructions properly but not getting the expected results, try seeing what the vendor tools are doing differently and do that instead.
IRC is your friend
Typically, the kinds of projects I'm working on are not the kinds of things the people around me have any experience with, so the only way I can get help is to find like-minded people on the Internet to talk to. While web forums are great, nothing beats real-time communication, and IRC excels in that regard. If you ever find yourself in a position where you're walking seemingly-uncharted ground and you need help, it would be a really good idea to learn IRC if you haven't already done so.
I'd like to give a big thanks to everyone in the #openocd and ##fpga channels for attempting to answer all of my JTAG and FPGA questions, no matter how unusual they were. I'd also like to thank PaulFertser specifically for encouraging me to continue with the project despite the number of roadblocks I was encountering.
I've uploaded my loader code here. It requires Python 3 and OpenOCD to work.