Alp Bolukbasi

M.Sc. Computer Science @ KIT : hardware-aware systems engineer

Karlsruhe, DE — somewhere between the debugger and the datasheet


SRAM Weak PUF Readout on iCE40

puf

I read an SRAM PUF response from a Lattice iCE40 board for the first task in a hardware security course. I expected to spend most of the time on security, but the work went into proving that the response was real. Before I could discuss any security property, I had to make sure every byte left the FPGA and reached the PC without the measurement pipeline inventing an artifact.

Lattice iCE40HX8K board used for the weak SRAM PUF task
The Lattice iCE40HX8K board used for the weak SRAM PUF readout.

A physical unclonable function, or PUF, derives a device-specific response from manufacturing variations already present inside the chip. The value comes from the physical device rather than non-volatile memory that stores a fixed secret.

Two chips made from the same design still differ slightly at the physical level. A PUF tries to turn those differences into a reproducible digital fingerprint.

What a weak PUF is

A weak PUF exposes one or a small number of challenge-response pairs. This makes it useful as a device fingerprint: power up the device, read the response, and use it to identify or characterize that physical instance.

A strong PUF instead supports a large challenge-response space. I was not building an authentication protocol in this task. I first needed to learn whether I could read a hardware-derived startup pattern reliably, then analyse whether it behaved like a useful fingerprint.

Why SRAM

SRAM cells are built from cross-coupled inverters. Ideally, an uninitialized cell could power up as either 0 or 1. In real silicon, tiny mismatches between transistors make some cells slightly prefer one state over the other.

A cell that tends to power up to the same value across repeated cycles can contribute to a device-specific response. A cell that changes randomly is less useful for identification, although it still describes the physical behaviour of the memory.

SRAM, BRAM, and the FPGA detail

In a microcontroller or processor, SRAM usually means general on-chip static memory. On an FPGA, the name can be confusing because the fabric exposes embedded block memories, or BRAMs.

The naming difference did not affect the measurement. What mattered was that the memory had a power-up state before the design intentionally overwrote it. The readout circuit had to capture that state and stream it to the host before I treated the data as a PUF response.

What this task does

The implementation reads startup memory on a Lattice iCE40 FPGA, sends the raw response to a PC over UART, and analyses data collected across repeated power cycles.

The analysis covers the usual first-order PUF metrics: uniformity, bit-aliasing, uniqueness, and reliability. Those metrics only mean something if the readout path is trustworthy. A broken UART transfer or an off-by-one address bug can look like a bad PUF even when the physical source behaves correctly.

During bring-up, I recorded each UART and FSM failure beside the PUF measurement it affected. That made the boundary clear: the startup value came from the memory, while the address logic and transport decided whether I ever observed that value correctly.

Setup

The task collected SRAM startup data from the FPGA and sent it to the host. The circuit was small, but the measurement crossed state transitions, counters, UART handshakes, and host-side validation.

Build workflow

Before taking measurements, I made the design build, flash, and run in a repeatable way. I initially treated the process as "compile the Verilog and flash the bitstream." After repeating the same Yosys, nextpnr, icepack, and programmer commands, I moved them into a small Makefile to avoid copy-paste mistakes while debugging.

FPGA build output for the PUF task
Build output after synthesizing the PUF readout design.
Makefile workflow for the FPGA PUF task
The Makefile used for the build and flashing loop.

Trusting the measurement

Turning the FPGA into a measurement instrument took more work than the PUF concept itself. Until I verified the readout path, every PUF metric remained suspect.

Before analysing uniqueness, stability, entropy, or security properties, I had to answer a narrower question: was I receiving the bytes that the FPGA intended to send?

The data path crossed FPGA memory, read address generation, byte selection, UART transmission, host-side capture, and Python analysis. I checked each boundary before using the final data.

Readout pipeline

The design follows a small FSM. It waits for a command from the host, decides whether this is a normal PUF readout or a diagnostic request, reads memory words, selects the correct byte, hands that byte to the UART transmitter, waits until the transmitter really finished, and then moves to the next byte.

The PC only sees a serial byte stream. The FPGA has to maintain the current byte index, derive the RAM address, select the low or high byte of a 16-bit word, and avoid sending the same byte twice.

Registers used inside the PUF readout module
The internal registers made the readout state explicit.
Byte index to SRAM read address mapping
The byte index mapped to the SRAM read address.

Debugging the readout path

I added a diagnostic mode that did not depend on the RAM contents. It returned a known pattern, which let me debug the transport path before blaming the PUF data.

Normal PUF output is hard to debug at first because it is supposed to look somewhat random. If the output is strange, it is not immediately obvious whether the problem comes from the PUF source, the address mapping, the UART transmitter, or the host-side capture script.

To separate these problems, I added a debug request path. Instead of reading SRAM data, the FPGA could send a known byte pattern. This reduced the problem to a much simpler question:

if I ask the FPGA to send known bytes, do I receive exactly those bytes?

The expected diagnostic marker was:

44 42

which is ASCII for:

D B

At one point, however, the PC was receiving:

44 44 42 42

These bytes were constants generated by the debug case statement, so the duplicated values could not come from SRAM/BRAM. The fault had to be in the transmit path.

At that point I had no evidence of a PUF problem. I first had to prove that the measurement infrastructure was not creating the duplicated bytes.

The UART module exposes a small ready/enable style interface. The PUF controller presents one byte on uart_data_to_tx, then asserts uart_tx_enable for one clock cycle when the transmitter is ready.

There is one timing trap in that interface: seeing uart_tx_ready high does not necessarily mean that the byte you just requested has already been transmitted. It may still be high from before the transmission started.

During bring-up, the FSM treated this immediately-high ready signal as if the transfer had already completed. The result was that the same byte could be accepted twice, which showed up as paired bytes in the debug stream.

The fix was to make the transmit pulse registered and explicit, and then wait for the UART transmitter to actually become busy before accepting completion. The final sequence became:

  1. wait until uart_tx_ready is high;
  2. place the next byte on uart_data_to_tx;
  3. assert uart_tx_enable for exactly one clock;
  4. enter a wait state;
  5. observe uart_tx_ready going low;
  6. only then wait for uart_tx_ready to return high;
  7. increment the byte index and continue.

The uart_seen_busy register prevents the FSM from confusing "the transmitter was already ready" with "the byte has finished transmitting."

UART handshake logic for the PUF readout
The important part was waiting for ready-high, then confirming that the transmitter actually entered the busy phase.

How the PC reads the response

The PC does not directly read individual PUF bits from the FPGA. It talks to the FPGA through a serial UART link.

The host script sends a one-byte command such as s for a normal SRAM PUF readout or d for diagnostic mode. After receiving the command, the FPGA starts streaming bytes back over UART.

The full readout is:

16 KiB = 16384 bytes = 131072 bits

On the host side, the capture script collects these bytes and writes them as hexadecimal text. The final file shape is:

512 lines × 64 hex characters

Each line is then interpreted as one 256-bit response block. That format made the later analysis easier: the same bit position can be compared across repeated power cycles for reliability, while the bit distribution inside a response can be used for uniformity-style checks.

The capture script is therefore part of the measurement infrastructure. If it silently receives fewer bytes, repeats stale data, or writes malformed lines, the statistical analysis becomes meaningless.

From raw bytes to PUF metrics

After the UART path started behaving, the task became more like a data analysis problem. The capture script produced hexadecimal readout files. I converted those hex strings back into bit vectors and then computed the first-order PUF metrics from those vectors.

Uniformity checks whether a response has a balanced number of zeros and ones. For each captured response, I counted the number of one bits and divided it by the response length. A perfectly balanced response would be close to 50%. If the value is far away from that, the memory startup pattern is biased toward either zero or one.

uniformity = number_of_ones / number_of_bits

Bit-aliasing looks at the same idea from the bit-position side. For a given bit position, I checked how often that bit was one across multiple captured responses. If every bit position behaves independently and without global bias, these values should also be around 50% on average.

bit_aliasing[i] = number_of_responses_where_bit_i_is_1 / number_of_responses

Reliability is about stability across repeated measurements. For this, I compared responses captured after repeated power cycles and counted how many bit positions changed. A reliable PUF response should not flip too much between repeated readouts of the same device.

The Python analysis was not complicated, but it only became meaningful after the FPGA readout path returned the expected diagnostic bytes every time.

Small Verilog lessons from this task

The Verilog part looked small, but it forced a few useful habits.

I made transmit pulses explicit. I cleared uart_tx_enable by default on every clock and asserted it only in the state that intentionally sends a byte. This avoids accidentally holding a send request high for multiple cycles.

The byte index and RAM address are different concepts. The RAM returns 16-bit words, while UART sends 8-bit bytes. The controller therefore uses a byte index for the outgoing stream and derives the RAM word address from it:

raddr = i_r[13:1]

The lowest bit selects which half of the 16-bit word is sent:

i_r[0] = 0  ->  rdata[7:0]
i_r[0] = 1  ->  rdata[15:8]
Byte index to SRAM read address mapping
The readout is byte-oriented from the UART perspective, but word-oriented from the RAM perspective.

Debug output should not depend on the thing being debugged. The diagnostic stream was useful precisely because it bypassed the PUF data path and sent known constants. That made it possible to isolate the UART/FSM issue before trusting the SRAM readout.

Before measuring the physical effect, I needed enough independent checks to show that the pipeline was not inventing artifacts. The diagnostic stream provided that check for the UART and FSM path.

Measurements

Once the readout path became reliable, I collected repeated measurements and analysed the PUF response itself.

Measurements collected from the weak SRAM PUF readout
Repeatable measurements after the UART path worked.

Where the effort went

In this task, hardware security depended more on bring-up and measurement engineering than I expected. Most of the effort went into digital design, UART communication, host-side capture, verification, and small debugging loops.

The PUF idea had to pass through a real hardware data path before it became a plot or metric. I could not separate the security claim from the measurement path that produced its data.

Next steps

I still need to analyse stability across resets and uniqueness across devices, if I can access more than one. I also do not yet know how much post-processing the response would need before it could serve as a reliable fingerprint.

back to notes