Skip to content

FPGA and MCU Co-design

FPGA and MCU Co-design hero image
Modified:
Published:

Having built a CPU, you now know something uncomfortable: the processor you spent a lesson on is worse than the one you can buy for two dollars. That is not a failure, it is the point. An FPGA earns its place doing what a microcontroller cannot, and the skill worth having is knowing where that line falls. #fpga #mcu #codesign

Learning Objectives

By the end of this lesson, you will be able to:

  1. Compare FPGAs, microcontrollers and DSPs against the properties that actually decide a design.
  2. Explain what a soft core and a system-on-chip are, and when either is worth it.
  3. Partition a real system between an FPGA and an MCU, with reasoning you can defend.
  4. Build a register interface between the two over SPI.
  5. Recognise when an FPGA is the wrong answer, and say so.

What We Are Building



An SPI register interface, and the reasoning behind the split

A worked partition of a motor-control system, then the FPGA side of an SPI slave that exposes a small bank of registers, so a microcontroller can configure the hardware and read results back. Plus the honest test for whether you needed an FPGA in the first place.

FPGA versus MCU versus DSP



The three are not competitors so much as answers to different questions. What separates them is how they spend time.

FPGAMCUDSP
ConcurrencyGenuine. Every block runs every cycleOne thing at a time, plus interruptsOne thing at a time, but very wide per step
Timing determinismCycle exactGood, until an interrupt landsGood
Best atMany parallel channels, custom protocols, nanosecond timingDecisions, sequencing, connectivity, anything with a libraryRegular heavy maths on streams
ReprogrammingRebuild the bitstreamReflash, secondsReflash, seconds
Unit costHighestLowestMiddle
PowerHighestLowestMiddle
Development effortHighestLowestMiddle
EcosystemSparseEnormousGood in its niche
  • You need many channels at once with identical timing. Eight quadrature encoders, sixteen PWM outputs, a hundred parallel comparisons.
  • The timing is hard real time in nanoseconds, not microseconds. Interrupt latency would be a large fraction of your budget.
  • The interface does not exist as a peripheral. An unusual protocol, a camera bus, a legacy parallel interface no vendor supports.
  • The data rate is high and the processing is simple. Filtering a stream as it arrives, where a CPU would be swamped just fetching it.

The heuristic engineers actually use, stated plainly: start with a microcontroller, and move a function to an FPGA only when you can name the specific requirement the MCU misses. Parallelism you do not need is cost, power and development time you paid for nothing.

System-on-Chip and Soft Cores



There is a third option between “FPGA” and “FPGA plus MCU”: put the processor inside the FPGA.

A soft core is a CPU described in Verilog and synthesised into the fabric, exactly like the mini CPU you built last lesson, only competently. Real ones are readily available: PicoRV32, VexRiscv and Neorv32 are all RISC-V cores that fit comfortably on a mid-range FPGA and run a real toolchain, meaning you can compile C for them with standard GCC.

A system-on-chip is that core plus memory plus peripherals plus your custom logic, all on one device, communicating over an internal bus.

Soft core inside the FPGASeparate MCU alongside
Chip countOneTwo
Interface between themAn internal bus, no wiresSPI or a parallel bus, and its timing
CPU performanceModest. Tens of MHz, no cacheHundreds of MHz, real cache, FPU
Cost of the CPUFabric you could have used for logicA few dollars of separate silicon
Software ecosystemBare metal or a small RTOSFull vendor SDKs, networking stacks
Best whenThe logic dominates and the software is simpleThe software is substantial

The honest summary: a soft core is excellent when you need a little sequencing next to a lot of custom logic, and a poor deal when you need serious software. A 2 USD microcontroller outperforms most soft cores by a wide margin and comes with a networking stack. This lesson builds the two-chip version, because that is what most real designs look like.

Partitioning a System



Take a concrete example: closed-loop control of four brushless motors, with a web interface for monitoring and tuning.

Write down every function, then ask of each one what it actually demands.

FunctionDemandWhereWhy
Read 4 quadrature encoders4 channels, edges up to 1 MHz eachFPGAFour counters running genuinely in parallel. An MCU would be interrupt-saturated
Generate 12 PWM outputs, 6-step commutation12 channels, exact phase, dead-timeFPGADead-time errors destroy transistors. Cycle-exact timing is not optional
Over-current tripReact within 1 microsecondFPGAMust not wait on software. Combinational comparison plus latch
PID loop, 1 kHz per motorModest maths, 4 instancesEitherSee below
Read setpoints from a web requestTCP, TLS, JSONMCUThe libraries exist. Building this in Verilog would be perverse
Log to an SD cardFilesystemMCUSame reason
Tuning, calibration, error recoveryComplex, changeable logicMCUWill be rewritten many times. Firmware updates are cheap

Two observations about how that table was filled in.

The trip is on the FPGA for a reason that is not performance. Over-current protection must work even if the software has crashed. Putting a safety interlock behind a scheduler is a design error regardless of how fast the scheduler is.

The PID loop could go either way, and that is the interesting one. At 1 kHz an MCU has a millisecond per iteration, which is an eternity. Put it on the MCU: it is easier to write, easier to tune, and trivially changeable. Move it to the FPGA only if you later need 50 kHz, or if jitter from other MCU activity starts showing up in the control response.

The principle underneath: put a function on the FPGA when it needs parallelism, hard timing, or independence from software. Put it on the MCU otherwise. Then resist moving anything else.

This mirrors the reasoning in local versus cloud control: the fast loop stays as close to the hardware as it must be, and the layer above earns its place by doing what the layer below cannot.



The two chips need to talk. The pattern that works, and that almost every real peripheral uses, is a register interface: the FPGA exposes a small bank of addressable registers, and the MCU reads and writes them.

That is worth pausing on. It is exactly what you have been doing to microcontroller peripherals all along, from the other side. Now you are the peripheral.

The protocol

Keep it as simple as possible. Two bytes per transaction:

A minimal register protocol over SPI
Byte 1: [7] read/write flag, 1 = write
[6:0] register address
Byte 2: data in (on a write) or data out (on a read)
cs_n frames the transaction. Deassert between transactions.

The FPGA side

Lesson 5 built an SPI master. Here you need the other end, an SPI slave, because the microcontroller drives the clock.

spi_reg_slave.v
module spi_reg_slave (
input wire sclk,
input wire cs_n,
input wire mosi,
output wire miso,
// register file exposed to the rest of the FPGA design
input wire [7:0] status, // read-only, driven by your logic
input wire [7:0] result, // read-only
output reg [7:0] control, // written by the MCU
output reg [7:0] setpoint // written by the MCU
);
reg [4:0] bit_cnt;
reg [7:0] shift_in;
reg [7:0] shift_out;
reg [7:0] cmd;
wire is_write = cmd[7];
wire [6:0] addr = cmd[6:0];
assign miso = shift_out[7];
// Read the addressed register into the outgoing shifter
always @(*) begin
case (addr)
7'd0: shift_out_next = status;
7'd1: shift_out_next = result;
7'd2: shift_out_next = control;
7'd3: shift_out_next = setpoint;
default: shift_out_next = 8'h00;
endcase
end
reg [7:0] shift_out_next;
always @(posedge sclk or posedge cs_n) begin
if (cs_n) begin
bit_cnt <= 0;
end else begin
shift_in <= {shift_in[6:0], mosi};
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 5'd7) begin
cmd <= {shift_in[6:0], mosi}; // first byte complete
shift_out <= shift_out_next; // present the read value
end
if (bit_cnt == 5'd15) begin // second byte complete
if (is_write) begin
case (addr)
7'd2: control <= {shift_in[6:0], mosi};
7'd3: setpoint <= {shift_in[6:0], mosi};
default: ; // reads and unknown addresses do nothing
endcase
end
end
end
end
endmodule

Two things to note honestly about this block.

It is clocked by sclk, which belongs to the MCU. That makes the whole module a foreign clock domain inside your design. Anything in your FPGA logic that reads control or setpoint must treat them as crossing a domain boundary, using the discipline from the previous lesson: synchronise single-bit flags, and for multi-byte values either use a Gray-coded handshake or only sample them when a synchronised “updated” flag says they are stable. Getting this wrong is the classic integration bug, and it is intermittent, so it will pass your bench test.

Read-only and writable registers are deliberately separated. status and result are inputs to this module, driven by your logic. control and setpoint are outputs, driven by the MCU. Mixing the two directions in one register is how you get a value that neither side believes it wrote.

The MCU side

On the microcontroller it is ordinary SPI code. This is the pattern for any of the boards covered in the STM32 or ESP32 courses:

Register access from the MCU
#define REG_STATUS 0x00
#define REG_RESULT 0x01
#define REG_CONTROL 0x02
#define REG_SETPOINT 0x03
uint8_t fpga_read(uint8_t addr) {
uint8_t tx[2] = { addr & 0x7F, 0x00 }; // top bit clear = read
uint8_t rx[2];
spi_select();
spi_transfer(tx, rx, 2);
spi_deselect();
return rx[1]; // data arrives in the second byte
}
void fpga_write(uint8_t addr, uint8_t value) {
uint8_t tx[2] = { addr | 0x80, value }; // top bit set = write
spi_select();
spi_transfer(tx, NULL, 2);
spi_deselect();
}

Start every integration by reading a register whose value you know. Put a fixed identifying constant at address 0 and read it first. If it comes back right, your wiring, clock polarity, bit order and framing are all correct, and every later problem is protocol rather than physical. If it comes back wrong, you have learned that before writing anything that could move a motor.

When an FPGA Is the Wrong Answer



This section exists because the failure mode of a course like this one is enthusiasm.

An FPGA is the wrong choice when:

  • A microcontroller already meets the requirement. If a 2 USD part hits your timing with margin, using an FPGA costs more money, more power, more board area and considerably more of your time, in exchange for nothing.
  • The requirements are still moving. Firmware iterates in seconds. A bitstream change is a synthesis run, a timing check and a reflash.
  • Nobody on the team can maintain it. A design only you can modify is a liability, and this is a real constraint rather than a soft one.
  • The volume is high and the margin is thin. At scale the unit cost difference dominates everything else. This is the same non-recurring versus per-unit argument that decides FPGA against ASIC, one step down the ladder.
  • The problem is really about software. Networking, file formats, user interfaces, protocols with libraries. Verilog is a bad language for all of it.

The corresponding test for when it is right: can you name the specific requirement a microcontroller misses, and is that requirement real rather than anticipated? “We might need more channels later” is not a requirement. “We need sixteen encoder channels at 1 MHz today” is.

Application Questions and Solutions



Question 1: Should this go on the FPGA or the MCU?

A design needs to timestamp incoming pulses to within 10 nanoseconds and also format the results into JSON for a web request. How would you partition it?

Click to reveal the solution
  1. Separate the two needs. Nanosecond timestamping is hard real-time and parallel. JSON formatting and networking are flexible, sequential, library-heavy tasks. ✅

  2. Put timing on the FPGA. Capture and timestamp the pulses in hardware, where the timing is deterministic and independent of any software load. ✅

  3. Put formatting on the MCU. Hand the timestamps to the microcontroller over SPI, and let it build the JSON and handle the network, where a rich software stack already exists. ✅

  4. Size the buffer between them. The FPGA captures at hardware rates and the MCU reads when it can, so a FIFO absorbs the mismatch. If the MCU falls behind, decide deliberately whether to drop oldest, drop newest, or assert back-pressure, and expose an overflow counter so the choice is visible. ✅

Question 2: The MCU reads a setpoint the FPGA never wrote

An MCU writes a 16-bit setpoint as two consecutive single-byte register writes. The FPGA logic occasionally acts on a value that is neither the old setpoint nor the new one. Explain, and fix it.

Click to reveal the solution
  1. Notice the value is split. Two bytes are written in two separate SPI transactions, so between them the register pair holds the new low byte and the old high byte. Any logic sampling in that window sees a value that never existed. ✅

  2. See that this is not a clock-domain problem. Synchronisers would not help. The corruption is at the protocol level: the update is not atomic. ✅

  3. Add a commit mechanism. Have the MCU write both bytes into shadow registers, then write a separate apply bit. The FPGA copies shadow to live in one clock when it sees apply, so the transition is atomic. ✅

  4. Then handle the clock domain as well. The apply bit is a single bit crossing from the SPI domain into your logic, so synchronise it with two flip-flops and act on its rising edge, exactly as in the debouncer from Lesson 4. ✅

The general pattern: multi-byte configuration needs a shadow-and-commit protocol. This is why real peripherals have “update” or “latch” bits that always seemed like clutter.

Question 3: Talk your colleague out of an FPGA

A colleague proposes an FPGA for a datalogger that samples four thermocouples once per second, timestamps the readings, and uploads them over WiFi. They argue it is more professional. Respond.

Click to reveal the solution
  1. Test the timing requirement. One sample per second, on four channels. A microcontroller has hundreds of millions of cycles between samples. There is no timing pressure of any kind. ✅

  2. Test the parallelism requirement. Four channels at 1 Hz is four reads per second, which any MCU does sequentially without noticing. Genuine parallelism buys nothing. ✅

  3. Weigh what the FPGA costs here. WiFi means a TCP and TLS stack, which on an FPGA means adding a soft core and porting a network stack to it. On an ESP32 it is a library call. The FPGA route is more expensive, higher power, and much slower to build. ✅

  4. Answer plainly. An ESP32 is the right tool: it samples, timestamps, and uploads with existing libraries, for a couple of dollars. The professional choice is the one that meets the requirement with the least complexity, not the one with the most impressive silicon. ✅

Summary



ConceptKey Takeaway
The defaultStart with an MCU. Move a function to an FPGA only when you can name what the MCU misses
FPGA strengthsReal parallelism, nanosecond determinism, custom interfaces, independence from software
MCU strengthsDecisions, connectivity, libraries, cost, power, and cheap changes after shipping
Soft coresPicoRV32, VexRiscv, Neorv32. Good for a little sequencing beside a lot of logic
Safety functionsPut interlocks in hardware. They must work when the software does not
Register interfaceThe FPGA becomes a peripheral. Exactly what you have been configuring all along
The SPI domainAn MCU-driven sclk is a foreign clock domain inside your design. Synchronise accordingly
Atomic updatesMulti-byte configuration needs shadow registers plus a commit bit
Start with a known valueRead a fixed constant register first to prove wiring, polarity and framing
Wrong toolIf a 2 USD MCU meets the requirement, an FPGA costs money, power and time for nothing

You can now build digital hardware, verify it, run it on a board, make peripherals, cross clock domains, execute a program, and place a function on the right side of a system boundary. One thing remains: taking a design past configurable silicon and into a chip of its own.

Comments

Loading comments...


© 2021-2026 SiliconWit®. All rights reserved.