This page is the third in a series of five pages about becoming a professional FPGA designer. On this page, I'll try to outline the most important practical skills required for being an FPGA designer. I'll also try to clarify why each skill is important. Don't skip the Verilog part, even if it seems obvious — I might have a few non-obvious things to say about it.
And before I start, I'll explain why I only mention Verilog and not VHDL: Simply because Verilog is recommended, as it seems it's going to be the dominant HDL language in the future. This is, among other things, because VHDL is rarely heard of in China. But everything said below is true for VHDL as well.
Verilog's two sides
Why knowing Verilog is required hardly needs an explanation. In fact, a lot of people out there think that knowing Verilog equals being an FPGA designer. One or two project managers have sent their junior programmers to a Verilog course, and were disappointed when they didn't come back as FPGA champions.
So first of all, knowing Verilog's syntax is far from knowing how to use this language. This is true for any language, but with Verilog the gap is much wider. Secondly, it's important to understand that Verilog code isn't a computer program. It describes hardware. Therefore, in principle, everything happens in parallel. This is the mental shift that many software developers have trouble with, in particular those who aren't used to multi-threaded programming.
The programming language yes-or-no question is actually a bit more complicated, because Verilog is used for two different purposes: Simulation of hardware and creation of logic for an FPGA or an ASIC (synthesis).
The purpose of a simulation is often to check Verilog code before synthesizing it into logic in the FPGA. To do this, we write a testbench, which is essentially a computer program that artificially creates stimulus signals, and does something with the output signals coming from the piece of logic we're testing: Usually writing the signals' values to a file, or maybe checking that they are as expected.
When Verilog is used for simulation, it actually is a programming language, but not anything like C, Python or JavaScript. Yet, the computer does exactly what we asked for: When the simulation runs, the testbench as well as the code we intend to synthesize behave exactly as defined by the syntax.
And here comes the huge catch: When the same Verilog code we tested is used for synthesis, Verilog isn't a programming language anymore. It describes logic, in the same way that HTML describes what should be displayed on a web page. Even worse, and unlike HTML, the interpretation of the Verilog code can be quite fuzzy.
It's important to understand that synthesizing Verilog code is a kind of magic. We humans describe the behavior we want, and the synthesizer somehow implements this with the help of logic resources. Therefore, we need to define this behavior with specific coding patterns that the synthesizer recognizes and for which it generates the logic we intended. Unlike regular programming, we can't just write whatever we want. We need to think about the logic elements that the synthesizer will generate as a result of our requests. The Verilog code is just hints to the synthesizer about what logic to generate.
In the AI era, I also need to add: Verilog isn't vibe coding. It's a formal language, and the synthesizer isn't an LLM that we sweet-talk into doing what we want. Synthesizers existed long before LLMs were a thing, and they interpret our code according to strict rules, and not a trained neural network.
And most important of all: If we write poor Verilog code, the logic will not behave like the simulation. The simulator will do exactly what we want, because the simulator does what the code says. The synthesizer, on the other hand, can silently produce logic that does something different, if we're not careful to write the Verilog code properly.
This point is crucial and worth repeating: When Verilog is used for simulation, it's a programming language, even if a rather lame one. All you need to do is get the syntax right, and the simulator does exactly what you want. The synthesizer, on the other hand, might generate logic completely different from the simulation, even if the Verilog syntax is perfectly correct. If you're lucky, the synthesizer will issue a warning that you might not get what you expected. Hopefully, you'll see that warning, despite being buried among hundreds of others. If you're even luckier, the synthesizer will stop with an error. But all too often, you get a silent bug. To avoid this, you must know which coding patterns are OK for synthesis. And not try anything else.
And now comes the question: So how do you learn proper Verilog?
Basic Verilog skills
The first step is learning the syntax, of course. There are a lot of Verilog books and tutorials out there. Unfortunately a whole lot of them go through all and everything, which isn't just unnecessary, but can tempt you into using coding patterns that synthesizers will mess up.
I suggest the following minimal set of topics. Learn them from any source you can find, and you're good to go as far as syntax is concerned. If you bump into something you don't know in Verilog code you come across, ask your favorite AI prompt what it means. It's not harder than that. So here's my shopping list:
- Modules and instances, ports (input, output, output reg, inout).
- Wires and registers. Registers that create flip-flops and those that don't (with always @(*) and similar).
- Assignments with "assign" vs. with always @(posedge clk) vs. always @(*). '=' vs '<=' within always blocks.
- if-statements
- case statements and their use for state machines, muxes and ROMs.
- Constants in Verilog, e.g. 16'h1234 and 4'b1001. Also understand x and z as values.
- Arithmetic and logic operations in Verilog. Understanding the difference between & and &&, for example. Unary reduction operators, e.g. &myreg. True/false as a logic value, e.g. myreg <= (this == 1).
- Bit manipulations: Selection of bits (e.g. myreg[3:2]) as well as concatenation (e.g. { myreg1, myreg2 }) and duplication (e.g. { 8{myreg} } ).
- Module parameters (parameter and localparam).
- Using IP and instantiating them in your Verilog module. This is not a purely Verilog skill, as it involves using the development tool for creating and configuring an IP block (a FIFO, or a PLL, for example). And yet, most of the work is instantiating and connecting the ports in your design, and this is a Verilog skill.
- The "initial" block for initializing registers at powerup as well as setting variables in simulation.
- Commands used for simulation only: $stop, $finish, $display, $readmemh, and the extensive use of "initial".
- Time delays with "#", and their use in simulations.
- The `timescale directive (and its lack of importance in many cases).
- generate statements — less important to start with, but used widely.
- Synthesis attributes. These are direct instructions to the synthesizer, and their syntax varies from one vendor to another. For example, DONT_TOUCH = "TRUE" is often used to tell the synthesizer not to optimize away a register, even if that would save logic. So be sure to read through the list of such attributes that your synthesizer supports, as they can be very useful.
So far, the easy part. The real issue is getting the synthesizer to generate the logic we actually want by writing Verilog code correctly. Actually, to get any synthesizer in the world to generate this exact same logic.
I go by a simple principle, which is Rule #4 in my own list of Golden Rules: Make sure to use coding patterns that are very common. The idea is that if the synthesizer misinterprets my code, it makes the same mistake with many other people's code as well. And that won't happen, because all these people would soon pick another synthesizer. So I look at the code I wrote, and ask: "How many other people would be affected if the synthesizer messes this up?" If the answer is "many", I'm probably on the safe side. Emphasis on "probably".
But how do all these other people write Verilog, then? That's actually a hard nut to crack, because the vast majority of Verilog is written inside companies and is never published. And different people write with different coding styles. To make things worse, examples on the Internet are often written by inexperienced people. Even if their code works, it's not necessarily something to imitate: A different synthesizer might mess up the same code, possibly because synthesis tricks are pushed to their limits. Verilog code from OpenCores varies in quality from excellent to code that only works in simulation.
So how does one know? The best suggestion I can come up with is to read the Verilog code generated by AMD's IP tools. Some of these IPs, for example the DDR memory controllers, generate synthesizable Verilog, and it's written by people who know what they're doing. Note, however, that automatically generated Verilog code often contains a lot of pointless and unused code. This is typical of code generated by scripts. There are often modules that just instantiate other modules, which in turn instantiate other modules, and so on. Wrapping modules indefinitely like this is not something to imitate; it's just a way to keep the hierarchy structured as necessary to allow for different configurations of the same code. Automatically generated Verilog code also tends to have a lot of parameters, which shouldn't necessarily be imitated either. Remember that your goal is to imitate their way of expressing functionality, not the messy hierarchy.
A word about SystemVerilog: It's quite popular, but I've personally never written code in this language dialect. This is mainly because I want my Verilog to be as simple as possible. The less I need to trust the synthesizer, the better. When I need complicated structures, I write a script in Perl that creates simple Verilog. Feed the synthesizer with a teaspoon, or it will puke on you.
Tools for implementation
Mastering this skill means working efficiently with the tools, having good control over them, understanding their warning messages, allowing reproducible bitstream generation, and keeping the project manageable as it grows. In short, controlling your work on the project.
Vivado is the preferred tool to work with. Not only does AMD dominate the market, but other vendors, in particular the new Chinese FPGA vendors, tend to design their tools in a compatible way. Aside from knowing how to use these tools as an IDE, the process of turning the Verilog code and IP into a bitstream should be understood. It begins with synthesis, and is followed by several other steps which are vendor-dependent, but all do the same thing in principle: Gradually transforming the synthesizer's output into a bitstream that you can load into the FPGA. This sequence of execution steps is not a black box, and it shouldn't be treated as one.
The main reason for understanding how the tools work is to respond correctly to errors and warnings. During a project implementation, a lot of warnings are produced, and it's important to distinguish which ones are important and which can be ignored. And if an error occurs, the process fails, and a problem must be solved. The skill of solving such problems comes from understanding the theory behind the tools' actions, as well as accumulated experience.
Trying to ask AI how to solve a problem works sometimes, but often AI takes you through a long journey of futile debugging. And if you listen to AI without any judgment of your own, you might end up doing something that apparently solves the problem, when you've actually just gotten rid of an error message and created a real problem instead. In short: There is no substitute for your own brain, and there never will be.
Another important point is that each tool has its quirks, for example, misleading error messages. Or even worse: The tools silently ignore code, settings or constraints. Learning these things is only a matter of experience, and part of gaining that experience is actually reading the reports and figuring out what these messages mean, even if they have no concrete relevance.
This is a list of concepts and routines I suggest as a checklist. These relate only to synthesis and obtaining a bitstream. I'll leave simulation, verification and debugging for later.
- Synthesis and generation of netlists (edif in particular)
- Technology mapping (even though this is done by the synthesizer in Vivado).
- Inclusion of IPs in the design.
- Place & route
- Post-route timing optimizations
- Bitstream generation
- Using JTAG to load the bitstream or program flash memory
- Viewing and analyzing the synthesized and/or placed & routed design with Vivado's tools.
If you try out an example project, you're very likely to go through everything mentioned in this list, except for the last item. Using the tools when someone has already prepared an example project for you is easy. In real life, projects are rarely organized so neatly, and you will be responsible for making the tools work correctly and for getting the best results possible. If you don't understand how the machinery works, you'll have a hard time fixing it when it gets stuck or doesn't do what you want. Remember, weird things happen all the time, even if you do everything right.
An ocean of files
The development tools create files, and lots of them. Each step the tools run, from synthesis to the finalized bitstream, is like a computer program that reads some files and outputs its results as other files.
To make things even more difficult, the tools often create copies of source files and rely on these copies instead of the originals. The tools also generate intermediate files, and rely on them instead of anything looking like a source file. Keep this in mind, in particular when you make changes in the sources and nothing changes in the results.
I wouldn't prioritize understanding what every single file does and what it's for, as the first step in learning. It's pointless to attempt to master them all, but it's important to know which files should be considered "source" and which ones are "generated". The better you swim in this ocean of files, the better your chances to keep your head above water. You'll understand what I mean the first time you try to create an independent copy of an entire project, for the purpose of developing it in a separate direction. Or move it to a different computer.
The best method is to maintain a minimal set of files that define the FPGA project in a Git repository. Delete all other files every now and then, and rebuild the project from this minimal set. For an example of how a project can be bootstrapped from a minimal set of files, download and try to implement one of Xillybus' demo bundles (available for both Vivado and Quartus). Note that you don't import the sources normally into Vivado to start off, but rather run a Tcl script. It may sound like a scary solution, but this script is easy to modify even if you don't know any Tcl.
One thing worth knowing about Vivado is that the DCP is a compressed file, containing netlists in edif, constraints, and other information. For example, when the DCP is the result of place and route or later stages, it contains the exact placements of the logic elements. It's a snapshot of the design after completing a specific stage in the process. I suggest uncompressing a DCP and looking into it, just for the heck of it.
Verification and simulation
In a perfect world, the logic design works from the first attempt, and there are no bugs to fix. The reality is almost always different, of course.
In every beginner's course on Verilog, they'll tell you this: First, write the Verilog code that you want as logic in the FPGA. We call this "code for synthesis" or "synthesizable code". Then write a testbench in Verilog, and use it in a simulation so you can check that the code for synthesis works correctly. Or actually, see how it doesn't work correctly and fix the bugs. The testbench is Verilog code written only for the purpose of simulation, and that never gets near a synthesizer.
Indeed, the usual working method is to write a Verilog module, or a few Verilog modules, and then write a testbench for verifying that they work correctly. This is repeated as the project gets bigger, and in the end, possibly the whole project is simulated. For such a large simulation, there are often several different testbenches, each intended to check different functionalities.
But not all simulations are done in the same way. There are three main approaches to simulation.
The first approach is simulating for the sake of obtaining waveforms. In this approach, the testbench only creates signals that feed the inputs of the module we want to simulate. This includes the clocks and resets, as well as other signals that mimic the behavior of real physical signals or of signals generated by other modules. After the simulation is completed, a GUI interface is used to view the waveforms created by the logic under test, in order to see if it works correctly, or why it doesn't. This method is suitable for simpler logic. For example, the state machine that implements a video output pattern can be simulated this way, as the correct repeated output pattern is easily verified by looking only at the waveforms.
The second approach is reading input from files and writing output to files. Here, the testbench creates a few simple signals, but the important signals are read from a file instead. The values at the outputs, or a few selected outputs, from the tested module are written to another file. It's quite common to write a computer program or script that generates the file that the testbench reads, as well as the file containing the expected output from the testbench. After running the simulation, a simple text diff can be done between the expected output and what the testbench actually wrote. There are of course endless variants on this: The testbench can make the comparison with the expected outputs, or possibly the other way around: Computer software reads the testbench' output and analyzes it.
This simulation method is particularly suitable for what I called "processing logic", i.e. logic that implements some kind of data processing. It's also useful for regression testing, i.e. simulations that check that nothing has changed functionally from one version of the code to another.
The third approach is to let the testbench check the correctness by itself, and stop with an error if something bad happens. This method requires writing meaningful test patterns in Verilog, which is often more difficult than getting it done in a scripting language. On the other hand, the FPGA project is easier to maintain when there's a self-contained testbench that gives a pass or fail indication. Whoever runs the regression suite will be happy working this way.
These are the three main approaches. I've made it sound as if only the Verilog we humans write is simulated, but that isn't true:
Post-synthesis simulations
As I mentioned before, the synthesizer may produce logic that is different from the behavior described by the Verilog code. One way to tackle this issue is to simulate the output (i.e. the netlist) produced by the synthesizer. This is called post-synthesis simulation.
To run a post-synthesis simulation, we ask the development tools to create a Verilog model of the synthesized code. This is a huge Verilog module that has the same ports as the one we wrote for synthesis. But inside, it consists of small modules (called simulation primitives) that represent the actual logic elements of the targeted FPGA. So it's a really messy Verilog file, but we can refer to it in the testbench instead of the original Verilog module we wrote.
The classic method is to run a simulation on the human-written Verilog code, and then on the post-synthesis model, and compare the outputs. The second method mentioned above (with files as input and output) is best, because we expect the logic design to behave exactly the same after synthesis. If it doesn't, consider that as a huge blow to your Verilog coding style. Actually, if you're writing Verilog correctly, you shouldn't need a post-synthesis simulation. This type of simulation is more common in the ASIC industry, where they are paranoid about ending up with a bug in their final chip. Also worth noting: Even if your post-synthesis simulation gives exactly the same output as the original, this still doesn't guarantee that the synthesizer did what you expected.
There's also post-place-and-route simulation. As its name implies, this simulates the logic elements as they are placed and connected inside the FPGA. It takes the propagation delays inside the FPGA into account, but in a very limited manner. There is still a huge difference between the simulation and what happens in real life. This is because the actual delays inside the FPGA depend on a lot of physical factors, for example temperature and supply voltages. These delays are also somewhat random, because of contaminations in the chip's silicon, which are scattered all over. These contaminations change the physical properties, so the electrical delays are slightly off. Each physical FPGA therefore has different delays, yet within specification. When the simulation runs, only a specific delay is applied to each path, which is usually the largest allowed delay. If your design works perfectly in a post-place-and-route simulation, it still doesn't mean it will work on a real physical FPGA.
So all in all, simulations have quite serious limitations. For one, the simulation obeys your wishes where the synthesizer won't. And even post-synthesis simulations can't cover many real-life effects inside the FPGA: Glitches, tolerances of the physical logic elements due to temperature, supply voltage or manufacturing tolerances. Neither can the simulation reproduce the logic's behavior as a result of timing violations, for example when crossing clock domains unsafely.
If the design is correctly written and constrained, simulation and reality behave the same. Otherwise, the simulation is worthless. It's important to keep that in mind.
Another limitation is that the simulation can only cover a very short segment in time. If the simulation runs for one million clock cycles, which can take a long time, this covers only 10 ms of real time when the clock runs at 100 MHz. Rare issues and corner cases are easily missed, simply because they didn't happen to fall inside the simulated window.
My suggestion on simulations
What do I recommend for a newbie? Knowing how to simulate is a must, and it's something to learn along with learning the other Verilog-related skills. In particular, be sure to be comfortable with the second simulation method, using files for input and output. This is the method considered serious, among other things because it's common in regression tests. Try it out with post-synthesis and post-place-and-route simulations as well, at least for the sake of a job interview.
But: Don't get addicted to simulations. Slap yourself on the hand every time you find a bug with a simulation. If you find a bug with the first method (looking at waveforms from a simulation), slap yourself even harder. The ultimate goal is to write code that works on the first attempt. Simulations catch the simple bugs, not the ones that cause something weird every billion clock cycles. Those are the ones you don't want to chase forever.
Finally, a little secret about myself: I rarely run any simulation. I make sure to write Verilog code carefully and thoughtfully, so that it works correctly right away, or at least with few enough bugs to fix directly on the FPGA. But I don't know about anyone else working this way, and I'm not sure I'd recommend it as a general approach.
Actually, I'd like to add an advanced bonus tip, to keep in mind later on: When people run simulations, they often add resets to their logic, because in a simulation all registers begin with the unknown state, marked "X". As a result, nothing valuable comes out, as the entire system remains in the "X" state. The common mistake is to quickly put asynchronous resets everywhere to get rid of these X's. And then forget that this may be a very bad idea, as explained on a separate page. So yes, definitely solve the problem with the X's, but do it wisely: Maybe with a synchronous reset, or maybe with an "initial" block in the synthesizable code? Most synthesizers implement this block, even though it's originally intended only for simulation. Don't let the simulation control how you write code.
Testing and debugging
After all is said and done, you'll end up testing your design and finding the reasons why it doesn't work as expected. There is a saying that debugging is like a detective story, where the same person is the victim, the detective, and the culprit.
And the truth is that in this detective story, there is no single best way to solve the mystery. It's a matter of being creative in finding the correct approach, tools and methods every time, in order to close in on the source of the bug. Some people stick to the same debugging methods every time. They find the problem fairly quickly sometimes, and sometimes it takes forever for them.
It's quite common to develop a set of specialized debugging tools and methods for a specific project. This happens with large software projects too, but with FPGA design it's often inevitable. It's like developing a testbench for testing, but in hardware.
But even though there is no single optimal method for finding a bug, there are still certain tools that are commonly used. I'll mention a few of them.
The tool that most people prefer to start off with is an on-chip logic analyzer. Depending on which development suite you have, this tool is called ILA, ChipScope, SignalTap, or something similar, and they all do the same thing: They turn your computer into a logic analyzer. With this tool, you can view waveforms captured inside the FPGA in the same way that you watch waveforms from simulations. You choose which signals to watch and the condition for triggering a capture of these signals.
The communication between the computer and the FPGA happens on the JTAG interface, which is the same one used to send the bitstream file to the FPGA. So there is no additional hardware required, and many FPGA designers who don't like electronics appreciate this fact. The FPGA and its already existing JTAG link are also the debugging tool.
This method is so convenient that a lot of FPGA designers get addicted to it, and forget other methods that may be more suitable in some situations.
The main drawback of this method is that JTAG has a relatively low data bandwidth, so ILA and similar tools are not suitable when large amounts of data are required for investigation. It also takes a little time for the data to arrive through the JTAG link. This can be problematic when we want an immediate response to events, so that we can correlate them with other things happening.
When the JTAG link's limitations become an issue, faster interfaces are required. If the FPGA board has a PCIe interface, it can be used to transport data at a high data rate to and from a computer. Implementing a PCIe interface and the computer's driver can be projects in themselves, but with Xillybus it's quick and easy to get such a link up and running. A similar solution with Xillybus is also possible with Zynq-7000 boards working with Xillinux.
A high-bandwidth connection is often required when there is a pinpoint issue hidden among a lot of data. For example, in image processing, in which case the image is transmitted to the computer through Xillybus and viewed on the computer's monitor with the help of a dedicated computer program.
Whether you choose an on-chip logic analyzer or Xillybus, these are sterile debugging tools. No hardware is touched. But sometimes we need that simple and immediate response from the hardware in order to figure out what's going on.
So first, allow me to suggest the simplest debugging tool of all: The LED. Surprisingly often, the quickest method to find a bug is to define different logic conditions, and make sure that a LED flashes when they are true. In particular, flashing LEDs when something that should never happen actually does. The more LEDs, the merrier. This is a bit like inserting a "printf" in C code for debugging. By the same token, add a little Verilog code for debugging. Load the bitstream into the FPGA, try some random foolish things, and maybe these LEDs will reveal what the bug is related to.
There is one important thing to remember when you use this method: If the condition is met, be sure that the LED is lit at least 20 ms in response to that. Otherwise, the human eye won't see it.
A more sophisticated version of the LED method is using an oscilloscope. This is why I suggested buying and getting acquainted with an oscilloscope in the first page of this series. The idea is to connect several signals from within the FPGA to its output pins that are reachable with an oscilloscope probe. In real projects, it's not unusual to have a dedicated connector on a PCB, where the board designer has gathered a lot of unused pins from the FPGA, so they can be used for debugging.
Compared with the on-chip logic analyzer, the oscilloscope may appear lame: It can monitor two signals at a time, maybe a few more with more expensive oscilloscopes. Its bandwidth is limited, so really fast signals can be missed. And yet, sometimes it is the strongest tool at hand. In particular when there are repeated signal patterns, watching them with an oscilloscope can help catch what goes wrong.
And sometimes the oscilloscope is required to understand what happens at the plain hardware level. Are the voltages correct? Do the digital signals look like they should? Is there some crazy noise on the voltage signal?
And this brings me to the most down-to-earth kind of debugging. More often than we want to believe, the problem is a really silly hardware issue, in particular with PCBs developed for a specific project. One of the supply voltages may be unstable, maybe with occasional short spikes or dips. The clock oscillator may not behave as expected, generating an inadequate clock signal. It's always a good idea to look at these before developing sophisticated theories.
So if you want to become good at FPGA design, you need a bit of both: Sterile methods for fetching data from within the FPGA as well as getting your hands dirty with hardware.
And never forget: There is only one debugging tool that is really efficient, if and when used correctly: Your own brain.
Programming languages
Even though programming isn't directly related to FPGA design, a minimal set of programming skills is needed to get the job done. For example, I mentioned above that test data for simulation is often created with a specially written program or script.
I'll mention a few programming languages that might be worth mastering. Among all the things I suggest learning, this is actually the easier part, and there's definitely no need to know all of this from day one. Or ever, for that matter.
- Tcl: This language is commonly used to write scripts run by Vivado and other FPGA development tools. One-liners are often useful as well. Learning this language's full syntax and all features is pointless however. You will probably never do real programming in this language, but mastering the basics of its syntax is recommended. In particular, learn the different types of quotes and brackets, and what they mean.
- C: This language is so widespread, that I would consider it a bit of a handicap not knowing it. For FPGA development, it's often a good candidate for creating large amounts of test data.
- Python: This is a popular scripting language, which can be useful for generating test data, but even more important: Generating Verilog that involves repetitive code that isn't neatly expressed with Verilog's own syntactic capabilities. It's often easier to write a script that writes Verilog code based on code templates than doing it with Verilog directly.
- Perl: This language can be used for the same purposes as I mentioned for Python above. In my opinion, Perl is definitely better at doing virtually everything, but Python is more popular because it was adopted by universities: Python's syntax is what programming language academics like, and Perl has a very neat and pragmatic syntax, but it breaks the academic rules on how a programming language should look. So Perl is great for getting the job done, but it's considered an outdated language, because most people consider Python the primary scripting language. If you're not already into Python, consider Perl instead. You won't regret it.
- Makefile and Bash shell scripting: If you are already familiar with these, keep them in mind for building some projects. Otherwise, I wouldn't put these at high priority. It's very difficult to write a proper Makefile for an FPGA project, because the dependencies aren't always as straightforward as in a plain software compilation. And yet, I use Makefiles when the build process is complex and I want to build only the parts that require recompilation.
There are several other languages, for example MATLAB, that can be useful too, in particular for creating test data for simulations.
Another aspect of knowing programming languages is that they often create a common language with the software team. This is relevant for FPGA projects that involve some kind of embedded processor or even a computer. So being a programmer at some level yourself helps you communicate with the software guys. Also, it's often easier to write the low-level program communicating with your FPGA design than to get someone else to do it based on a specification.
And as a general recommendation, learn how to use Git and use it, even if you're working on a project alone. Version control is not only to keep managers happy, but it's a valuable tool that allows you to try something, and then go back, and then regret that you went back. And after you've finished fooling around, none of that mess is visible.
I use gitk for a graphical representation of the version tree.
This wraps up the third page in this series. The next page also discusses practical skills, but those of less importance to start off with. The main point is to explain how and when they can be important.