Part VIThe Room

Programming and Tooling

July 31, 2026·45 min read·advanced

Most such roles carry the same two bullets. "Experience in C or C++ programming" and "Experience using an interpretive language such as Perl or Python."

01.Part 1, what is actually being asked

1.1 The exact wording, and where it sits on the page

Most such roles carry the same two bullets. "Experience in C or C++ programming" and "Experience using an interpretive language such as Perl or Python."

Two facts about those bullets matter more than the bullets themselves.

They are under Preferred Qualifications, not Minimum Qualifications. The minimum qualifications in those same role descriptions are RTL design, microarchitecture, verification, and timing. Nobody is hiring an RTL engineer for their C++ skills.

They are listed last, under everything else, which is the recruiter's way of signalling relative weight without saying so.

So calibrate accordingly. This is the shortest note in the set and it should get the least study time. It will not decide the outcome of an interview. It can, however, produce a bad five minutes if you fumble it, and a confident specific answer here is cheap to acquire.

1.2 Why the bullets exist at all

The reason a hardware role asks about programming is not that hardware engineers occasionally write scripts. It is that a modern RTL engineer spends a large fraction of the working week writing and reading software, and a candidate who cannot will simply be slower at the actual job.

Count the software surfaces an RTL engineer touches in a normal week. The testbench is SystemVerilog, which is a programming language with classes and inheritance before it is a hardware language. The reference model against which the DUT is checked is often C. The synthesis, STA, and place-and-route tools are driven by Tcl scripts. The regression is launched by a Makefile and a farm submission script. The results come back as gigabytes of text that somebody has to reduce to a table, and that somebody writes Python. The register map that the RTL, the firmware header, the UVM register model, and the documentation all describe is generated from one specification file by a script. The lint waivers are a data file processed by a script.

None of that is optional infrastructure. It is how the work gets done.

1.3 Your position, stated honestly up front

That covers the interpretive-language bullet completely and the C half of the first bullet.

The genuine gaps are C++ and Perl. Both are narrow, both have a clean honest answer, and Part 3 and Part 6 give you those answers. Do not overclaim either. A hardware interviewer who hears "yes I know C++" will ask one follow-up about RAII or virtual destructors, and being caught bluffing on a preferred qualification costs far more than the qualification was worth.


02.Part 2, C in a hardware organization

2.1 The three places C actually appears

Reference models. The scoreboard described in Verification Methodology needs to know what the correct answer is. Something has to compute it, independently of the RTL, from the specification. That something is frequently a C model, and in many organizations the C model is the executable form of the specification, written by the architect and handed to both the design team and the verification team as the arbiter of intent. The independence matters. If the C model is written by reading the RTL, it will faithfully reproduce the RTL's bugs and check nothing.

Embedded firmware. Power management controllers, security processors, sequencer microcontrollers, and bring-up code are C. This is exactly the mechanism-versus-policy boundary in SoC Integration and Interfaces, where the hardware implements a mechanism such as a voltage-frequency change and the firmware implements the policy that decides when to make one. For a power management role (P) this is directly relevant, since a large part of a modern power-management subsystem is firmware running on a small core, and knowing where the line sits between what the RTL does and what the firmware does is a real interview topic.

DPI. The bridge that lets a SystemVerilog testbench call the C model. Section 2.8.

2.2 Pointers and memory layout, built from zero

A pointer is a variable whose value is a memory address. That is the whole definition and everything else follows from it.

Concretely, suppose an int is 4 bytes and you declare

C
int x = 0x11223344; int *p = &x; ```text If `x` happens to live at address `0x1000`, then `p` holds the number `0x1000`. Not the value `0x11223344`, the **address** of where that value is stored. `*p` means "go to the address in `p` and read what is there," which gives back `0x11223344`. Now the part that surprises people. What is `p + 1`? The intuitive answer is `0x1001`. **The correct answer is `0x1004`.** Pointer arithmetic is scaled by the size of the pointed-to type, because the only sensible meaning of "the next `int`" is four bytes further along, not one byte further along into the middle of the current one. <Figure src="/figures/hardware-interview-prep/iv-25-Programming-and-Tooling-fig01.svg" alt="Pointer arithmetic is scaled by the size of the pointed-to type, so incrementing an int pointer at 0x1000 steps past the whole four-byte object to 0x1004 rather than one byte into the middle of it." caption="Pointer arithmetic is scaled by the size of the pointed-to type, so incrementing an int pointer at 0x1000 steps past the whole four-byte object to 0x1004 rather than one byte into the middle of it." id="fig:25-Programming-and-Tooling-1" /> That diagram already shows the byte order used by the machine, which is the subject of 2.5, so ignore the byte ordering for now and note only the scaling. The same scaling explains why `char *` arithmetic moves one byte at a time and why casting a pointer to `char *` is the standard way to walk a structure byte by byte, which is how a testbench serializes a packet. An **array** in C decays to a pointer to its first element in almost every context, so `arr[i]` is defined as `*(arr + i)`, which by the scaling rule is the address of `arr` plus `i` times the element size. That is why array indexing is free in hardware terms, since it is one shift and one add, and it is why the address-generation unit in [Execution Units](/learn/hardware-interview-prep/execution-units) has a scaled-index addressing mode in the first place. ### 2.3 Fixed-width types, and the bug that motivates them Plain C types have implementation-defined widths. `int` is usually 32 bits but is not required to be. `long` is 32 bits on Windows and 64 bits on Linux for the same processor. `char` may be signed or unsigned depending on the compiler. For application software that is a nuisance. For hardware work it is a defect generator, because you are modeling a register that is exactly 32 bits wide and a type that is "at least 16 bits" does not model it. The fix is `<stdint.h>`, which gives exact-width types. | Type | Width | Signed | Use it for | |---|---|---|---| | `uint8_t` | 8 | no | bytes, byte lanes, small fields | | `uint16_t` | 16 | no | half-words, 16-bit registers | | `uint32_t` | 32 | no | the default for a 32-bit register | | `uint64_t` | 64 | no | 64-bit registers, cycle counters, addresses | | `int32_t` | 32 | yes | signed arithmetic models | | `uintptr_t` | pointer-sized | no | an address held as an integer | Here is the bug those types prevent, and it is worth working because it is a real class. ```c unsigned short a = 0xFFFF; unsigned short b = 0x0001; unsigned short c = a + b; ```text What is `c`? The instinct is 0, because 16-bit arithmetic wraps. The actual behavior is that C **promotes** both operands to `int` before the addition, computes 65536 in 32-bit arithmetic, then truncates on the assignment, so `c` is 0 after all. Correct answer, wrong reasoning, and the reasoning matters because a slightly different expression breaks. ```c unsigned short a = 0xFFFF; uint32_t wide = a * a; ```text Here the promotion is to signed `int`. $65535 \times 65535 = 4{,}294{,}836{,}225$, which does not fit in a 32-bit **signed** int, so this is signed overflow, which is undefined behavior, and the compiler is entitled to do anything at all including optimizing away the surrounding code. Writing `uint32_t wide = (uint32_t)a * a;` fixes it. This class of bug is common in reference models that mirror RTL arithmetic, where the RTL does honest unsigned modular arithmetic and the C model quietly does something else. The rule to carry is that in a hardware model you use exact-width unsigned types everywhere and you make every cast explicit, because you are modeling a machine that has no promotion rules. ### 2.4 Bit manipulation, which is most of the actual work A hardware register is not a number. It is a bag of fields packed into a word. Suppose a control register is defined as follows. <Figure src="/figures/hardware-interview-prep/iv-25-Programming-and-Tooling-fig02.svg" alt="A control register is a word carved into named fields, each one identified by the bit range it occupies, so reading or writing one field means shifting and masking rather than touching the word as a number." caption="A control register is a word carved into named fields, each one identified by the bit range it occupies, so reading or writing one field means shifting and masking rather than touching the word as a number." id="fig:25-Programming-and-Tooling-2" /> **Extracting a field** is a shift then a mask. To read THRESHOLD, which is bits 15 down to 8, ```c #define THRESH_SHIFT 8 #define THRESH_WIDTH 8 #define THRESH_MASK ((uint32_t)((1u << THRESH_WIDTH) - 1)) uint32_t thresh = (reg >> THRESH_SHIFT) & THRESH_MASK; ```text Work the mask by hand so it is not magic. `1u << 8` is `0x100`, which is 256. Subtract 1 and you get `0xFF`, which is eight ones. Shifting `reg` right by 8 brings bit 8 down to bit 0, and the mask keeps the low eight bits and discards everything above. **Inserting a field** is a three-step read-modify-write and the middle step is the one people forget. ```c reg = (reg & ~(THRESH_MASK << THRESH_SHIFT)) /* 1. clear the old field */ | ((new_val & THRESH_MASK) << THRESH_SHIFT); /* 2. OR in the new one */ ```text Skip the clear and you get a bug that only shows up when the new value has fewer bits set than the old one, because OR can only set bits and never clear them. Writing `0x0F` over an existing `0xF0` without clearing gives `0xFF`. That bug survives casual testing because the first write after reset always works, since the field was zero. Mask the incoming value too, as the second line does, because a caller passing a value wider than the field will otherwise corrupt neighbouring fields silently. **Two undefined-behavior traps** worth knowing, since both appear in real firmware. `1 << 31` shifts a **signed** 1 into the sign bit, which is undefined behavior. Write `1u << 31`. Every shift constant in hardware code should carry the `u`. Shifting by an amount greater than or equal to the type's width is undefined. `uint32_t x = y >> 32;` is not zero, it is undefined, and on many machines the hardware shifter takes the shift amount modulo 32, so it returns `y` unchanged. That behavior is a direct consequence of the barrel shifter in [Arithmetic Hardware](/learn/hardware-interview-prep/arithmetic-hardware) having only five control bits for a 32-bit shift, which is a nice thing to be able to explain. **Population count and leading zeros** deserve a mention because they connect straight back to hardware. `__builtin_popcount(x)` counts set bits and `__builtin_clz(x)` counts leading zeros. Both map to single instructions on ARM and x86, and both exist as instructions because the hardware to compute them is a small tree, whereas the software loop is tens of cycles. A leading-zero counter is exactly the priority-encoder structure from [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) and is the normalization step of a floating-point adder from [Floating-Point Arithmetic](/learn/computer-architecture/floating-point). If an interviewer asks you to implement `clz` in RTL, that is a real question and the answer is a log-depth tree, not a loop. ### 2.5 Endianness Store the 32-bit value `0x12345678` at address `0x1000`. Which byte goes where? There are two answers and both are used. <Figure src="/figures/hardware-interview-prep/iv-25-Programming-and-Tooling-fig03.svg" alt="The same 32-bit value stored at the same address occupies the same four bytes under both conventions, and the only thing that differs is which end of the value lands at the lowest address." caption="The same 32-bit value stored at the same address occupies the same four bytes under both conventions, and the only thing that differs is which end of the value lands at the lowest address." id="fig:25-Programming-and-Tooling-3" /> Little-endian puts the **least** significant byte at the **lowest** address. x86-64 and ARM in its normal configuration are little-endian, so every Apple product you have used is little-endian. Big-endian is the network byte order used in IP headers, and it survives in some legacy interfaces. Detect it in C with a union, which is a good example of a union having a legitimate use. ```c union { uint32_t w; uint8_t b[4]; } u = { .w = 0x12345678 }; /* u.b[0] == 0x78 on little-endian, 0x12 on big-endian */ ```text Where this bites in hardware work is specific and worth naming. A 32-bit register accessed one byte at a time through a bridge has to define which byte lane carries which bits, and getting that wrong produces a design where full-word accesses work and byte accesses scramble. AMBA buses carry byte strobes exactly so that partial writes are expressible, as described in [Interconnect and AMBA](/learn/hardware-interview-prep/interconnect-and-amba), and the strobe-to-bit-range mapping is an endianness decision baked into the RTL. A testbench that builds a packet in a C model and drives it into an RTL interface has to serialize it in the order the RTL expects, and an endianness mismatch there produces a failure that looks like data corruption and is really a byte-order bug. Firmware that memory-maps a structure over a register block inherits the same problem. ### 2.6 `volatile`, and exactly what breaks without it This is the C question most likely to be asked in a hardware interview, because it is the one that separates people who have written firmware from people who have not. Start with the failure rather than the definition. ```c #define STATUS_REG 0x4000A004u uint32_t *status = (uint32_t *)STATUS_REG; while ((*status & 0x1u) == 0) { /* wait for the DONE bit */ } ```text This code polls a hardware status register until the DONE bit is set. It looks obviously correct and it is broken. The compiler analyzes the loop. It sees a load from `*status`. It sees that nothing inside the loop body writes to that memory. It applies a completely legal optimization called loop-invariant code motion, hoisting the load out of the loop, and produces something equivalent to ```c uint32_t tmp = *status; /* loaded ONCE */ while ((tmp & 0x1u) == 0) { /* forever */ } ```text If DONE was not set at the moment of that single load, **the loop never exits**. The chip is working perfectly. The firmware hangs. The compiler was not wrong under its own rules. Its model of memory is that memory changes only when the program changes it, and a hardware status register violates that model, because the change comes from outside the program entirely. `volatile` is how you tell the compiler that assumption does not hold. ```c volatile uint32_t *status = (volatile uint32_t *)STATUS_REG; ```text The contract is precise. **Every read of a volatile object in the source must produce exactly one load in the generated code, every write must produce exactly one store, and the compiler must not reorder volatile accesses with respect to each other.** The second failure mode is the mirror image and involves writes. ```c uint32_t *fifo = (uint32_t *)0x4000B000u; *fifo = 0xAA; /* push a byte */ *fifo = 0xBB; /* push another */ ```text Without `volatile`, the compiler sees two stores to the same address with no intervening read. It concludes the first store is dead and deletes it. One value is pushed instead of two, and the FIFO is short by one entry forever. Same story for a register where writing a value twice is meaningful, or where writing triggers an action rather than storing a value. Now the caveats, because getting these right is what turns a correct answer into an impressive one. **`volatile` is not atomicity.** A `volatile uint32_t x; x++;` still compiles to a load, an add, and a store, which is three separate accesses. An interrupt between them still corrupts the value. **`volatile` is not a memory barrier.** It orders volatile accesses against each other at the compiler level and does nothing whatsoever about the **processor** reordering them, and nothing about ordering a volatile access against a normal one. On a weakly ordered machine like ARM, described in [Load Store and Memory Ordering](/learn/hardware-interview-prep/load-store-and-memory-ordering), a device write followed by a flag write can still reach the device in the wrong order. You need a barrier instruction, a `DSB` or the compiler's memory-fence intrinsic, and MMIO regions are usually mapped as Device memory precisely so that the hardware does not reorder them. **`volatile` is not for thread synchronization.** That is what atomics are for. This is a well-known misuse and calling it out shows you know the boundary. So the complete answer to the interview question is that `volatile` prevents the compiler from caching, eliminating, or reordering accesses to a location that can change or have side effects outside the program, that omitting it turns polling loops into infinite loops and drops redundant-looking register writes, and that it solves a compiler problem and not a processor-ordering or atomicity problem. ### 2.7 Structure packing and alignment Processors read memory most efficiently when a value's address is a multiple of its size, which is called **natural alignment**. A 4-byte value at an address divisible by 4 is one memory access. The same value straddling a boundary may require two accesses and a merge, which is the misaligned-access handling described in [Load Store and Memory Ordering](/learn/hardware-interview-prep/load-store-and-memory-ordering), and on some architectures it faults outright. C compilers therefore insert **padding** into structures to keep members aligned. That padding is invisible in the source and very visible in `sizeof`. ```c struct bad { uint8_t a; /* offset 0 */ /* 3 bytes of padding at offsets 1, 2, 3 */ uint32_t b; /* offset 4, needs 4-byte alignment */ uint8_t c; /* offset 8 */ /* 3 bytes of tail padding at 9, 10, 11 */ }; /* sizeof == 12 */ ```text <Figure src="/figures/hardware-interview-prep/iv-25-Programming-and-Tooling-fig04.svg" alt="Three bytes of declared data occupy twelve bytes of storage, because the compiler pads before b to give it a four-byte aligned offset and pads again at the tail so that an array of the structure keeps every element aligned." caption="Three bytes of declared data occupy twelve bytes of storage, because the compiler pads before b to give it a four-byte aligned offset and pads again at the tail so that an array of the structure keeps every element aligned." id="fig:25-Programming-and-Tooling-4" /> Three bytes of data in twelve bytes of storage. Now reorder the members largest-first. ```c struct good { uint32_t b; /* offset 0 */ uint8_t a; /* offset 4 */ uint8_t c; /* offset 5 */ /* 2 bytes of tail padding */ }; /* sizeof == 8 */ ```text Twelve bytes becomes eight, a 33 percent saving, for a change that alters nothing about the code that uses it. Tail padding remains because the structure must be sized so that an **array** of them keeps every element aligned, which is why `sizeof` is always a multiple of the largest member's alignment. The hardware-specific trap is overlaying a structure on a register block. ```c struct regs { uint8_t ctrl; /* spec says offset 0x00 */ uint32_t data; /* spec says offset 0x01 */ }; volatile struct regs *r = (volatile struct regs *)0x40000000u; ```text The specification says `data` is at offset `0x01`. The compiler put it at offset `0x04`. Every access to `r->data` goes to the wrong address, and the failure looks like a hardware bug. The fix is `__attribute__((packed))` on GCC and Clang, or `#pragma pack`, which suppresses padding. The cost is that accesses to unaligned members now compile to byte-wise loads and shifts, which is slower and, more importantly for MMIO, may split what the hardware requires to be a single 32-bit bus transaction into four byte transactions that the device does not accept. That is why real register maps are designed on natural alignment in the first place, with explicit reserved fields for the gaps, which is a design rule worth stating if the topic comes up. ### 2.8 DPI, wiring a C model into a SystemVerilog testbench **DPI** stands for Direct Programming Interface. It is the standard SystemVerilog mechanism for calling C functions from SystemVerilog and SystemVerilog tasks from C. Knowing that it exists and roughly how it is wired is worth considerably more than deep C skill for this interview. Here is the shape of the thing. <Figure src="/figures/hardware-interview-prep/iv-25-Programming-and-Tooling-fig05.svg" alt="The C reference model lives outside the testbench but inside the simulator process, and DPI is the only thing joining them, carrying the scoreboard's arguments out and the expected result back." caption="The C reference model lives outside the testbench but inside the simulator process, and DPI is the only thing joining them, carrying the scoreboard's arguments out and the expected result back." id="fig:25-Programming-and-Tooling-5" /> The SystemVerilog side declares the C function. ```systemverilog import "DPI-C" function int unsigned ref_alu( input int unsigned a, input int unsigned b, input byte unsigned op ); // used inside the scoreboard expected = ref_alu(txn.a, txn.b, txn.op); if (expected !== actual) `uvm_error("ALU", $sformatf("a=%0h b=%0h op=%0h exp=%0h got=%0h", txn.a, txn.b, txn.op, expected, actual)) ```text The C side is an ordinary function with a matching signature. ```c #include <stdint.h> unsigned int ref_alu(unsigned int a, unsigned int b, unsigned char op) { switch (op) { case 0: return a + b; case 1: return a - b; case 2: return a & b; case 3: return a | b; default: return 0; } } ```text Compile that to a shared object, point the simulator at it, and the two halves link at elaboration. Four practical points that make the difference between having read about DPI and having used it. **Type mapping is not free.** SystemVerilog `int` is a 32-bit signed 2-state value and maps cleanly to C `int`. 4-state types, meaning anything that can hold X or Z, do not map to a C scalar at all and require the `svLogicVecVal` representation, which is a pair of words per 32 bits encoding the four states. If your DUT can produce X on an output and your scoreboard passes it to a 2-state DPI argument, the X silently becomes 0 and you have a checker that cannot see X propagation. That is a real and nasty class of testbench bug. **Direction matters.** `import "DPI-C"` brings a C function into SystemVerilog. `export "DPI-C"` goes the other way and lets C call a SystemVerilog task, which is how a C-based stimulus generator drives a testbench or how an instruction-set simulator steps in lockstep with an RTL core. **Calls cost time.** Each DPI call has real overhead in the simulator, so calling one per bit of a transaction is a way to make a regression ten times slower. Batch at the transaction level, passing arrays through a pointer rather than one element per call. **Ownership of memory is a trap.** Strings and arrays passed across the boundary have lifetime rules that are easy to get wrong, and a C model that returns a pointer to a local buffer produces intermittent corruption that looks like an RTL bug. The other reason to know DPI exists is that it is the mechanism behind **lockstep co-simulation**, where a full instruction-set simulator such as Spike runs alongside an RTL core and every retirement is compared. That is the industrial way to verify a CPU core, and it is a good thing to be able to name for front-end and execution roles. --- ## Part 3, C++ as an honest gap ### 3.1 Where C++ actually appears in this field Three places, and knowing which three keeps the gap small. **SystemC.** A C++ class library that adds modules, ports, signals, and a discrete-event simulation kernel to C++, used for transaction-level models of an SoC. A TLM model of a subsystem runs thousands of times faster than the RTL and is what firmware teams develop against before silicon. This connects to [SoC Integration and Interfaces](/learn/hardware-interview-prep/soc-integration-and-interfaces), because the virtual platform a firmware team uses is usually SystemC. **Verilator.** Verilator compiles synthesizable Verilog and SystemVerilog into a C++ class, then you write a C++ or SystemC harness that instantiates the class, toggles the clock, drives the inputs, and checks the outputs. It is the fastest open-source simulator by a wide margin and it is how open-source hardware projects run their regressions. Using it means writing C++ whether you want to or not, and the C++ involved is genuinely simple. **Large simulation infrastructure.** gem5 is C++ and is the tool behind everything in [Performance Modeling](/learn/hardware-interview-prep/performance-modeling). Extending it, adding a prefetcher or changing a scheduler policy, means editing C++ classes. ### 3.2 The subset worth having You do not need template metaprogramming, move semantics, or anything from the last three standard revisions. The useful subset is small. **Classes and inheritance**, because UVM's entire structure is classes and inheritance expressed in SystemVerilog, so understanding the concept in C++ makes UVM read naturally and vice versa. A base class with virtual methods that derived classes override is the pattern behind every UVM component. **RAII and destructors**, which is the C++ idea that a resource is acquired in a constructor and released in a destructor, so that scope exit cleans up automatically. It is the one genuinely distinctive C++ concept a C programmer does not have. **Standard containers**, `std::vector` and `std::map` and iteration over them, because that is what every real codebase uses for the things a C programmer would write a linked list for. **References versus pointers**, which is a five-minute topic. A reference is an alias that cannot be null and cannot be reseated, and it is the default way to pass a large object without copying. **Enough comfort to read and modify.** That is the actual bar. Nobody is asking you to architect a C++ system. They are asking whether you can open a 4000-line gem5 source file, find the relevant class, and change a policy without breaking it. ### 3.3 The efficient path, which is a weekend not a semester Do not read a C++ book. Do the two labs. Work through the **Verilator** portion of [Lab --- The Open-Source HDL Workflow](/learn/computer-architecture/lab-hdl-workflow). You write a C++ harness of maybe 60 lines that instantiates the generated model, runs a clock loop, drives stimulus, and dumps a VCD. That is real C++ in exactly the context an interviewer means, and it gives you a concrete thing to describe. Then work through the gem5 lab in [Lab --- gem5 Out-of-Order Modeling](/learn/computer-architecture/lab-gem5-ooo), which you should be doing anyway for the performance-modeling note. Configuring gem5 is Python, but reading and tweaking a model means touching the C++, and even a small change gives you a sentence with substance in it. Two weekends of work turns "I have not used C++" into "I have written Verilator harnesses and modified gem5 models," which is both true and sufficient. ### 3.4 What to say if asked directly Say this, more or less. "I work in C and Python daily. I read C++ comfortably and have written Verilator testbench harnesses and modified gem5 model code, but I have not built a large C++ system from scratch, so if the role needs someone to architect one I would be learning on the job." That answer is credible, checkable, and specific. It also does something useful, which is to name what you **have** done in the same breath as what you have not, so the interviewer's takeaway is a bounded gap rather than an absence. The alternative, claiming C++ fluency and being asked to explain why a base class needs a virtual destructor, is a bad trade for a preferred qualification listed last on the page. And for the record, the answer to that question is that deleting a derived object through a base pointer with a non-virtual destructor is undefined behavior and in practice skips the derived destructor, leaking whatever it owned. --- ## Part 4, Python, which is a strength ### 4.1 What it is actually used for Python is the default automation language in hardware now, and it is the bullet you can answer with real content. The uses divide into five kinds. **Parsing tool output.** Synthesis, STA, lint, DRC, LEC, power, and coverage reports are all text, often hundreds of thousands of lines, and somebody has to reduce them to the ten numbers a human should look at. **Generating RTL.** Regular structures that would be error-prone to type by hand. **Generating collateral from a single source of truth.** Register maps, headers, documentation, and verification models all derived from one specification file. **Driving regressions.** Launching jobs, collecting results, triaging failures, tracking pass rates over time. **Analyzing data.** Performance sweeps, power numbers, timing trends across runs. ### 4.2 A worked parsing example Here is the shape of a real script, small enough to read and close enough to the real thing to describe in an interview. ```python #!/usr/bin/env python3 """Summarize the worst timing paths from an STA report.""" import re import sys from dataclasses import dataclass START = re.compile(r"^\s*Startpoint:\s+(\S+)") END = re.compile(r"^\s*Endpoint:\s+(\S+)") SLACK = re.compile(r"^\s*slack \((MET|VIOLATED)\)\s+(-?\d+\.\d+)") @dataclass class Path: start: str end: str slack: float def parse(path_to_report): paths, cur = [], {} with open(path_to_report) as fh: for line in fh: if m := START.match(line): cur = {"start": m.group(1)} elif m := END.match(line): cur["end"] = m.group(1) elif m := SLACK.match(line): cur["slack"] = float(m.group(2)) paths.append(Path(**cur)) cur = {} return paths if __name__ == "__main__": paths = parse(sys.argv[1]) viol = sorted((p for p in paths if p.slack < 0), key=lambda p: p.slack) print(f"{len(paths)} paths reported, {len(viol)} violating") print(f"WNS = {viol[0].slack:.3f} ns" if viol else "WNS = clean") print(f"TNS = {sum(p.slack for p in viol):.3f} ns") for p in viol[:10]: print(f" {p.slack:8.3f} {p.start:45s} -> {p.end}") ```text WNS is worst negative slack, the single worst path, and TNS is total negative slack, the sum over all violating paths, and both are defined in [STA Synthesis and Physical Design](/learn/hardware-interview-prep/sta-synthesis-and-physical-design). The reason both matter is that a design with WNS of -50 ps and TNS of -60 ps has one problem, while a design with WNS of -50 ps and TNS of -8000 ps has a systemic problem, and no human reading a 400,000-line report spots that distinction quickly. The real value appears when you run it every night and store the numbers, because then you have a **trend**, and a trend answers the question that actually matters, which is whether today's RTL change made timing worse. ### 4.3 Generating RTL, and when not to Some structures are painful to type and easy to generate. A 64-entry CAM comparator array, a one-hot mux tree, a decode table with 200 entries, a crossbar's connectivity. ```python def gen_onehot_mux(name, width, n): lines = [f"module {name} #(parameter W={width}) ("] lines.append(f" input logic [{n-1}:0] sel,") for i in range(n): lines.append(f" input logic [W-1:0] in{i},") lines.append(" output logic [W-1:0] out") lines.append(");") terms = " |\n ".join( f"({{W{{sel[{i}]}}}} & in{i})" for i in range(n)) lines.append(f" assign out = {terms};") lines.append("endmodule") return "\n".join(lines) ```text Now the judgment, because generating RTL is easy to overdo. **If the structure is parameterizable inside SystemVerilog, use SystemVerilog.** A `generate` block with a `for` loop is readable, is what the next engineer expects, and is checked by the compiler. Reach for a Python generator when the structure is **data-driven from something outside the language**, for example a register block described in a YAML file, a decode table maintained in a spreadsheet by the architecture team, or a connectivity map produced by another tool. The cost of generation is real and should be stated. Generated RTL must be regenerated and re-reviewed on every spec change, the generator itself becomes code someone must maintain, and debugging a waveform means reading generated code that nobody wrote by hand. The standard mitigations are to commit the generator and the specification but treat the generated output as build product, to stamp a header into every generated file saying what produced it and from what input, and to make the build fail if a checked-in generated file is out of date with respect to its source. ### 4.4 Register map generation, the highest-value case This one deserves its own section because it is the clearest example of the pattern and it comes up in [SoC Integration and Interfaces](/learn/hardware-interview-prep/soc-integration-and-interfaces). A register block is described once, in a machine-readable file. ```yaml block: pwr_ctrl base: 0x40001000 registers: - name: CTRL offset: 0x00 fields: - {name: MODE, bits: "3:0", access: rw, reset: 0x0} - {name: EN, bits: "4", access: rw, reset: 0x0} - {name: THRESH, bits: "15:8", access: rw, reset: 0x40} - name: STATUS offset: 0x04 fields: - {name: DONE, bits: "0", access: ro, reset: 0x0} - {name: ERRCNT, bits: "11:4", access: rc, reset: 0x0} ```text From that one file, a generator emits the RTL for the register block including the read-mux and the write-decode and the reset values, a C header with the offsets and field masks for firmware, a UVM register model for verification, and the documentation table. The reason this matters is not convenience. It is that the alternative, four hand-maintained artifacts, **guarantees they will eventually disagree**, and the resulting bug is a firmware team writing to an offset the RTL does not implement, which is found late and blamed on the wrong team. Single source of truth removes an entire bug class. If you are asked what you would improve about a flow, this is a strong answer. ### 4.5 The library set, and cocotb | Library | What for | |---|---| | `re` | parsing tool output, which is most of the work | | `argparse` | giving your script a real command-line interface so others use it | | `pathlib` | file paths without string surgery | | `subprocess` | invoking tools and capturing their output | | `dataclasses` | structured records without boilerplate | | `pandas` | tabular analysis of sweep results | | `matplotlib` | the IPC-versus-ROB plot from [Performance Modeling](/learn/hardware-interview-prep/performance-modeling) | | `pytest` | testing your own scripts, which matters once others depend on them | | `jinja2` | templating for RTL and header generation | | `pyyaml` | reading specification files | **cocotb** deserves specific mention. It is a coroutine-based framework that lets you write a testbench in Python against a Verilog or VHDL simulator, driving and sampling signals through the simulator's VPI or VHPI interface. The DUT is real RTL running in a real simulator, and the testbench is Python. ```python import cocotb from cocotb.clock import Clock from cocotb.triggers import RisingEdge @cocotb.test() async def test_add(dut): cocotb.start_soon(Clock(dut.clk, 1, units="ns").start()) dut.rst_n.value = 0 await RisingEdge(dut.clk) dut.rst_n.value = 1 dut.a.value, dut.b.value = 3, 5 await RisingEdge(dut.clk) await RisingEdge(dut.clk) assert dut.sum.value == 8, f"expected 8, got {dut.sum.value}" ```text **When to choose it.** Block-level verification, open-source simulators where no UVM license exists, reusing an existing Python model as the reference, and fast bring-up of a new block where writing a UVM environment would take longer than the block took to design. **When not to.** A large environment with existing UVM verification IP, constrained-random at scale where SystemVerilog's constraint solver is the point, and formal coverage closure flows that expect SystemVerilog. Saying both halves is the good answer, since a candidate who says cocotb is simply better has not run a real UVM environment. --- ## Part 5, Tcl, which is unavoidable ### 5.1 Why every EDA tool speaks Tcl Synopsys, Cadence, and Siemens tools are all Tcl-driven, and so are the open-source flows that imitate them. The historical reason is that Tcl was designed in the late 1980s specifically as an embeddable command language, meaning a C application could link the interpreter and register its own commands, which is exactly what an EDA tool needs. Every vendor did the same thing independently and the result is that the industry standardized on Tcl by accident. Practically this means that scripting a synthesis run, constraining a design, querying a netlist, or automating a place-and-route flow means Tcl, regardless of your opinion of the language. This section is about depth rather than acquisition. ### 5.2 The two rules that make Tcl surprising Tcl has a reputation for weirdness and it comes from exactly two rules. **Rule one, everything is a string.** There are no types. The number 5 and the string "5" are the same thing. A list is a string with spaces in it. A command name is a string. This is why Tcl feels different from every other scripting language. **Rule two, the interpreter performs exactly one substitution pass over a command line before executing it.** One. Not zero, not recursively until nothing changes. One. That second rule explains all the quoting behavior. Double quotes allow substitution. Braces suppress it, deferring the contents to be substituted later by whatever command receives them. ```tcl set a 5 if {$a > 3} { puts "big" } # correct. The braces hand the literal string "$a > 3" to if, # which evaluates it as an expression itself. if "$a > 3" { puts "big" } # substitutes to if "5 > 3" ... which works here by accident # and breaks the moment $a contains a space or an operator. ```text The same rule governs `expr`, and the braced form is the one to use always. ```tcl set x 2 set y [expr $x * 3] ;# 6, but the expression is re-parsed at runtime set y [expr {$x * 3}] ;# 6, braced form, faster and safe ```text The unbraced form is both slower, because the expression string is recompiled on every evaluation, and unsafe, because a variable containing something like `1 + 1` gets substituted into the expression text and changes its meaning. In a flow script where variables come from a configuration file, that is an injection bug. ### 5.3 SDC is Tcl The design constraints file that drives synthesis and STA is not a data format that happens to look like Tcl. **It is a Tcl script**, executed by the tool's interpreter, and it can contain loops, conditionals, and procedure definitions. ```tcl create_clock -name clk -period 1.250 [get_ports clk] set_clock_uncertainty -setup 0.060 [get_clocks clk] set_clock_uncertainty -hold 0.020 [get_clocks clk] set_input_delay -clock clk -max 0.400 [get_ports {data_in[*]}] set_input_delay -clock clk -min 0.050 [get_ports {data_in[*]}] set_output_delay -clock clk -max 0.350 [get_ports {data_out[*]}] set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b] set_multicycle_path -setup 2 -from [get_pins u_mult/*/CK] \ -to [get_pins u_acc/*/D] set_multicycle_path -hold 1 -from [get_pins u_mult/*/CK] \ -to [get_pins u_acc/*/D] ```text Read the last two commands together, because the pairing is the classic trap. A `-setup 2` multicycle path tells the tool the data has two clock periods to arrive, which relaxes setup as intended. What it also does, if you stop there, is move the **hold** check to the same edge, which now demands that the data hold for a full extra cycle, creating a hold requirement that is almost impossible to meet and that place-and-route will try to fix by inserting an enormous amount of delay. **Almost every `-setup N` needs a `-hold N-1` alongside it.** The mechanism is the setup and hold equations in [Digital Logic and Timing](/learn/hardware-interview-prep/digital-logic-and-timing), and being able to explain why the hold check moves is a genuinely good answer. The other thing to say about SDC is that a constraint file is a **claim about the design's intent** and the tool believes it without checking. A false path declared on a path that is not actually false produces a design that closes timing in the report and fails in silicon. Constraints are as much a source of bugs as RTL is, which is why constraint review is a real review. ### 5.4 The query commands Beyond writing constraints, the daily use of Tcl is interrogating the design. ```tcl # how many flops in this hierarchy? llength [get_cells -hier -filter "is_sequential == true" u_core/*] # report the 20 worst endpoints, one path each foreach_in_collection p [get_timing_paths -max_paths 20 -nworst 1] { set slack [get_property $p slack] set ep [get_object_name [get_property $p endpoint]] puts [format "%-60s %8.3f" $ep $slack] } # find every port with no input delay set, a common constraint hole foreach_in_collection port [all_inputs] { if {[sizeof_collection [get_attribute $port input_delay]] == 0} { puts "unconstrained: [get_object_name $port]" } } ```text Note `foreach_in_collection` rather than `foreach`. A tool collection is not a Tcl list and iterating it with plain `foreach` either fails or, worse, iterates once over the collection's string representation. That distinction is the single most common Tcl mistake in EDA scripts. The commands worth recognizing are the object queries `get_cells`, `get_pins`, `get_nets`, `get_ports`, `get_clocks`, and `get_timing_paths`, the property accessors `get_property` and `get_attribute` and `get_object_name`, and the reports `report_timing`, `report_qor`, `report_area`, `report_power`, and `report_constraint`. --- ## Part 6, Perl, where read-only competence is enough ### 6.1 Why it is still there A great deal of EDA flow infrastructure was written between 1995 and 2010, when Perl was the obvious choice for text processing, and that infrastructure still runs because it works and rewriting it has no business case. So you will encounter Perl at a large company, and what you will be asked to do is **modify** a script, not write one. Apple lists Perl as an alternative to Python in the same bullet, which tells you it is not required. Nobody is going to test you on it. ### 6.2 The 90 percent you need Three things get you to competent modification. **Regular expressions**, which are essentially the same as Python's, since Python's `re` module was modeled on Perl's. If you know Python regexes you know Perl regexes. **The sigils**, which is where the confusion lives, and one rule resolves it. **The sigil describes the thing you are getting, not the container you are getting it from.** | Written | Means | |---|---| | `@arr` | the whole array | | `$arr[0]` | one scalar element of the array | | `@arr[1,2]` | a slice, which is several elements, hence `@` | | `%h` | the whole hash | | `$h{key}` | one scalar element of the hash | | `$#arr` | the last valid index of the array | `$arr[0]` starting with `$` looks wrong until you apply the rule, at which point it is obvious, since one element is a scalar. **The line-processing idiom**, which is what 90 percent of flow Perl actually does. ```perl #!/usr/bin/perl use strict; use warnings; my %count; open(my $fh, '<', $ARGV[0]) or die "cannot open $ARGV[0]: $!"; while (my $line = <$fh>) { next unless $line =~ /^\s*WARN-(\d+):\s*(.*)$/; my ($id, $msg) = ($1, $2); $count{$id}++; } close($fh); foreach my $id (sort { $count{$b} <=> $count{$a} } keys %count) { printf("%-10s %6d\n", "WARN-$id", $count{$id}); } ```text Read that and you can modify most flow scripts you will meet. `$_` as the implicit variable, `my` for lexical scoping, `use strict` and `use warnings` at the top of anything sane, and the diamond operator `<$fh>` reading a line at a time. Half a day of reading gets you there, and that is the correct amount of investment. If asked how you would approach modifying a Perl script you did not write, the good answer is procedural rather than linguistic. Read it end to end first without changing anything. Find the input and output contract, meaning what it reads and what it produces. Run it on a known input and save the output as a golden reference. Make the smallest possible change. Diff the new output against the golden reference and confirm only the intended thing changed. That answer works for any language and shows engineering discipline rather than Perl trivia, which is what the question is really testing. --- ## Part 7, the rest of the toolchain ### 7.1 Linux and the shell All EDA runs on Linux, and comfort here is assumed rather than asked. The pieces that matter are process management and knowing how to find what is consuming a machine, the shell text pipeline of `grep`, `sed`, `awk`, `sort`, and `uniq` for the one-off question that does not deserve a script, environment modules or an equivalent for selecting tool versions, and remote execution on a compute farm. The farm part is worth a paragraph because it is invisible until it bites. Jobs are submitted to LSF or Slurm with `bsub` or `sbatch`, they run on a machine you never see, and they write to a shared filesystem. A simulation that writes its waveform dump to NFS in a tight loop will saturate the filer and slow down everyone else's jobs, which is why the convention is to write to node-local scratch and copy results back at the end. Knowing that convention exists marks you as somebody who has worked on a farm. ### 7.2 Git for RTL Version control for hardware has the same commands as version control for software and three differences worth knowing. **Merge conflicts in RTL are more dangerous.** Two engineers edit the same `always_ff` block. Git merges them cleanly at the text level because the edits touch different lines. The result is syntactically valid SystemVerilog that lints clean, compiles, and implements neither engineer's intent. There is no compiler error and no test failure until the specific condition is hit. The mitigation is social rather than technical, meaning small modules, clear ownership, and reviewing the merged result rather than the two sides. **Submodules deliver IP.** A block developed by another team lives in its own repository and is pulled in as a submodule pinned to a specific commit. The parent repository records **which commit** of the submodule it is using, and updating the IP means updating that pointer and committing it. This is exactly the workflow the hub itself uses for the website, and it means you can describe the pattern from experience. The trap everyone hits once is that a submodule checks out in detached HEAD state, so commits made inside it without first checking out a branch are attached to nothing and are lost on the next update. **Large files need special handling.** Hardware repositories accumulate liberty files, GDS, encrypted IP, memory compiler output, and reference waveforms, all of which are large and binary. Git stores every version of every file forever, so a 200 MB binary updated weekly turns into a repository nobody can clone. Git LFS stores a pointer in the repository and the content elsewhere. The stronger discipline is to keep generated collateral out of the repository entirely and regenerate it, committing only the inputs and the generator. ### 7.3 Make and regression infrastructure Builds and regressions are Makefile-driven, and the concept that matters is **dependency-driven rebuild**, meaning a target is rebuilt only if something it depends on is newer than it. ```makefile RTL := $(wildcard rtl/*.sv) TB := $(wildcard tb/*.sv) SIM := build/simv $(SIM): $(RTL) $(TB) @mkdir -p build vcs -sverilog -full64 -o $@ $(RTL) $(TB) run: $(SIM) $(SIM) +seed=$(SEED) +UVM_TESTNAME=$(TEST) .PHONY: run clean clean: rm -rf build ```text Edit one RTL file and `make run` rebuilds and runs. Edit nothing and it runs the existing binary. On a design where compilation takes twenty minutes that distinction is the difference between a productive afternoon and a wasted one. A **regression** is the next layer up. It is a list of tests, each with a seed or a range of seeds, submitted to the farm in parallel, with results collected into a database. The infrastructure question that separates a usable regression from an unusable one is **triage**. A 5000-test nightly with 60 failures is useless if a human must open 60 logs. It is useful if a script buckets the 60 failures by their error signature and reports "48 failures all matching timeout in the arbiter, 11 matching scoreboard mismatch on opcode 0x2C, 1 unique," because that turns 60 investigations into 3. That triage-by-signature idea is worth saying out loud in an interview, because it is the thing an engineer who has actually owned a regression knows and an engineer who has only run one does not. ### 7.4 Waveform tools and debug fluency Verdi, DVE, Questa's visualizer, and on the open-source side GTKWave and Surfer. **Debug efficiency is a genuine differentiator and it is largely tool fluency.** Two engineers of equal design skill can differ by a factor of ten in how long it takes them to localize a failure, and over a project that difference is enormous. It is also almost never taught. The core motion is **backward tracing**. You have a signal with a wrong value at a known time. You find its driver, look at that driver's inputs at the time it computed the wrong value, find which input was wrong, and repeat. Modern tools automate the traversal with a driver-tracing command, and knowing that command exists is most of the skill. The workflow that makes this fast is more specific than "open the waveform." <Figure src="/figures/hardware-interview-prep/iv-25-Programming-and-Tooling-fig06.svg" alt="The waveform is opened late and narrowly, because every earlier step exists to shrink the search before the expensive tool is reached at all." caption="The waveform is opened late and narrowly, because every earlier step exists to shrink the search before the expensive tool is reached at all." id="fig:25-Programming-and-Tooling-6" /> Two practical points sit inside that flow. **Do not dump everything.** A full waveform dump of a large design produces terabytes and slows simulation by an order of magnitude. The standard practice is to run the regression with dumping off, and when a test fails, re-run that specific seed with dumping enabled over a limited time window and a limited hierarchy. That requires the regression to be **reproducible from a seed**, which is why seeds are recorded, and it requires the design to be free of nondeterminism, which is why relying on simulator scheduling order is forbidden in testbench code. **Save your signal groups.** Reconstructing a useful signal set from a 5000-signal hierarchy takes twenty minutes. Saving it takes ten seconds. Engineers who do this debug perceptibly faster than engineers who do not, and it costs nothing. If asked to describe your debug workflow when a regression fails overnight, the diagram above is the answer, and the two points about selective dumping and reproducible seeds are what make it sound like experience rather than theory. --- ## Part 9, check yourself Answer out loud, in full sentences, as if an interviewer asked. If you cannot, reread the section named. 1. Why do hardware roles ask about programming at all, and where do these bullets sit relative to the minimum qualifications? (1.1, 1.2) 2. `int *p` points at address `0x1000`. What is `p + 1` and why is it not `0x1001`? (2.2) 3. Why does a hardware reference model use `uint32_t` rather than `unsigned int`, and give a bug that integer promotion causes. (2.3) 4. Write the read-modify-write sequence to set an 8-bit field inside a 32-bit register, and say what breaks if you skip the clear step. (2.4) 5. Store `0x12345678` at `0x1000` on a little-endian machine and draw the four bytes. Name two places endianness bites in hardware work. (2.5) 6. A polling loop on a status register never exits even though the hardware sets the bit. Explain exactly what the compiler did and how `volatile` fixes it. (2.6) 7. Name three things `volatile` does **not** do. (2.6) 8. A structure with a `uint8_t`, a `uint32_t`, and a `uint8_t` has `sizeof` 12. Explain why, and reorder it to 8. (2.7) 9. What is DPI, what does it connect to what, and name one type-mapping hazard. (2.8) 10. You are asked directly whether you know C++. What do you say? (3.4) 11. Give a specific thing you would automate in Python in a synthesis flow, and say what the output looks like. (4.2) 12. When would you generate RTL with a script instead of using a SystemVerilog `generate` block, and what does generation cost you? (4.3) 13. Why generate the register map, the C header, the UVM model, and the documentation from one file? (4.4) 14. What is cocotb, and when would you choose SystemVerilog and UVM over it? (4.5) 15. Why does `set_multicycle_path -setup 2` almost always need a `-hold 1` alongside it? (5.3) 16. You must modify a Perl flow script you did not write. Describe your approach. (6.2) 17. Why are RTL merge conflicts more dangerous than software merge conflicts? (7.2) 18. A nightly regression comes back with 60 failures. What do you do first, and what makes that tractable? (7.3) 19. Walk through your debug workflow from a failing overnight regression to a root cause. (7.4) --- ## Part 10, related notes - [Lab --- The Open-Source HDL Workflow](/learn/computer-architecture/lab-hdl-workflow) for hands-on Icarus, Yosys, and Verilator, which is also the cheapest C++ on-ramp you have - [Lab --- Toolchain Setup: Spike, QEMU, and Cross-Toolchains](/learn/computer-architecture/lab-toolchain) for compilers, `objdump`, and GDB - [Verification Methodology](/learn/hardware-interview-prep/verification-methodology) for where reference models, scoreboards, and DPI actually sit in a testbench - [STA Synthesis and Physical Design](/learn/hardware-interview-prep/sta-synthesis-and-physical-design) for SDC, WNS and TNS, and the multicycle-path trap in Part 5 - [SoC Integration and Interfaces](/learn/hardware-interview-prep/soc-integration-and-interfaces) for register maps, the firmware-versus-hardware boundary, and generated collateral - [Performance Modeling](/learn/hardware-interview-prep/performance-modeling) for gem5, the other C++ on-ramp, and for the data analysis Python is used for - [DFT and Silicon Debug](/learn/hardware-interview-prep/dft-and-silicon-debug) for the observability infrastructure behind post-silicon debug - [Load Store and Memory Ordering](/learn/hardware-interview-prep/load-store-and-memory-ordering) for misaligned access and for why `volatile` is not a memory barrier - [Digital Logic and Timing](/learn/hardware-interview-prep/digital-logic-and-timing) for the setup and hold equations that explain the multicycle-path hold behavior - [Arithmetic Hardware](/learn/hardware-interview-prep/arithmetic-hardware) for the barrel shifter that explains why shifting by 32 does not give zero
Book mode
hardware-interview-prepinterview-prephardware
Was this helpful?