Custom FSM Core in ss2_aes_wrapper Producing Inconsistent Single Traces (but Average Trace Looks Fine)

I am experimenting with replacing the aes_core in the ss2_aes_wrapper Vivado project with my own custom FSM-based arithmetic core. I kept the same load_i and busy_o handshake logic as the AES core.

Setup details:

  • Inputs are fixed (two constant values).

  • Verified outputs are correct.

  • Collected 1000 traces with CW312T_A35, sample rate 88 MHz, 120 samples/trace.

  • Randomly picked two individual traces and plotted them alongside the average trace with confidence interval.

Issue:

  • The average trace shows a clear and stable pattern.

  • However, individual traces look visually very different from one another, unlike the AES case where single traces look consistent.

Reference:
When I run the original AES core in the same setup, the single traces are visually consistent, as expected. With my FSM core, only the average shows the pattern, while single traces look noisy or misaligned.

Question:
What could be causing this inconsistency in individual traces?

  • Is it possible my FSM introduces variable latency or timing jitter?

  • Could it be related to trigger alignment with load_i/busy_o?

  • Or maybe something about uninitialized states or different toggling across runs?

I’ve attached both the single-trace plots and the averaged trace for reference. Any insights on what might cause this difference compared to the AES core would be very helpful.

Thanks in advance!

Here is the verilog of aes_core and fsm_core: `timescale 1ns / 1ps//====================================================== - Pastebin.com

Did you set scope.clock.pll._allow_rdiv = True? Because that’s kind of what I would expect to see.

Otherwise your traces don’t look too bad. The peaks are well aligned. There will always be noise. Double-check that your target is running in constant time, and make its starting conditions identical.

Here is my code for target setup:

# ============================================================================

# SCOPE AND TARGET SETUP

# ============================================================================

print("Setting up ChipWhisperer scope...")

scope = cw.scope()

scope.default_setup()

# Configure sample settings

scope.adc.samples = 200 # More samples for detailed analysis

scope.adc.offset = 0

scope.adc.basic_mode = "rising_edge"

scope.trigger.triggers = "tio4"

scope.io.tio1 = "serial_rx"

scope.io.tio2 = "serial_tx"

# Target platform configuration for CW312T_A35

TARGET_PLATFORM = 'CW312T_A35'

print(f"Target platform: {TARGET_PLATFORM}")

scope.io.hs2 = 'clkgen'

scope.gain.db = 25

platform = 'ss2_a35'

print("Connecting to target...")

target = cw.target(scope, cw.targets.CW305, force=False, fpga_id=None, platform=platform, program=False)

# Clock configuration

scope.clock.clkgen_freq = 7.37e6

scope.io.hs2 = 'clkgen'

if scope._is_husky:

scope.clock.clkgen_src = 'system'

scope.clock.adc_mul = 12

scope.clock.reset_dcms()

else:

scope.clock.adc_src = "clkgen_x4"

time.sleep(0.1)

# Test connection

try:

target._ss2_test_echo()

print("Target connection test passed")

except:

print("Warning: Target echo test failed, continuing anyway...")

# ============================================================================

# PROGRAM FPGA FROM BITSTREAM

# ============================================================================

print(f"\nProgramming FPGA from bitstream...")

print(f"Bitstream path: {BITSTREAM_PATH}")

if os.path.exists(BITSTREAM_PATH):

try:

# Import the FPGA programmer

from chipwhisperer.hardware.naeusb.programmer_targetfpga import CW312T_XC7A35T

# Create FPGA programmer object

fpga = CW312T_XC7A35T(scope)

# Temporarily disable HS2 during programming

scope.io.hs2 = None

# Program the FPGA with the bitstream

fpga.program(BITSTREAM_PATH, sck_speed=10e6)

print("FPGA programming successful!")

# Re-enable HS2 after programming

scope.io.hs2 = 'clkgen'

# Wait for programming to complete

time.sleep(2)

# Test if target is responsive after programming

try:

target._ss2_test_echo()

print("FPGA programmed and responsive")

except:

print("Warning: FPGA programmed but echo test failed")

except Exception as e:

print(f"Error programming FPGA: {e}")

print("Continuing with default firmware...")

# Make sure HS2 is still enabled even if programming failed

scope.io.hs2 = 'clkgen'

else:

print(f"Bitstream file not found: {BITSTREAM_PATH}")

print("Continuing with default firmware...")

# Lock ADC

print("Locking ADC...")

for i in range(5):

scope.clock.reset_adc()

time.sleep(1)

if scope.clock.adc_locked:

break

if scope.clock.adc_locked:

print("ADC locked successfully")

else:

print("Warning: ADC failed to lock")

print("Hardware setup complete!")

  • I did not use scope.clock.pll._allow_rdiv = True
  • Also, can you please explain “Double-check that your target is running in constant time, and make its starting conditions identical.”?

Thanks for your reply.

If your target code doesn’t run in constant time, then its power traces won’t overlay nicely, and that would explain why your traces look so different from trace to trace. You can do this by simulating your Verilog code.

You can also look at scope.adc.trig_count after each trace capture and make sure that it’s constant, assuming that your target holds the trigger line high for the duration of its operation.

Starting conditions: I haven’t analyzed your code, but are there for example internal values that affect the target execution, and do these vary from trace to trace?

Thanks for the valuable replies. Here is the debug result. This shows trigger count is alway constant for all traces.