Improving SNR and Mitigating Ground Bounce during CPA on I2C Devices (CW313 + Husky)

Hello everyone,

I am a beginner in the SCA field, currently conducting academic research on I2C state-machine behavior using the ChipWhisperer-Husky Starter Kit. I am working with a custom breadboard setup alongside the CW313 Interposer Board.

My research involves analyzing the power consumption of an EEPROM (AT88SC family) during specific I2C transactions. To study the reset state of the chip’s internal logic, my experiment requires power-cycling the target device abruptly before an I2C STOP condition is generated.

While the logic side of my Python script works, I am facing severe analog and synchronization issues (poor SNR and ghost peaks) and would appreciate some guidance from the community on how to improve my hardware setup.

1. Hardware Setup (CW313 ↔ Breadboard)

Since I am bit-banging the I2C protocol, I am using the CW313 to bridge the Husky and my breadboard:

  • Target: 8-lead SOIC connected via micro test clips to a breadboard.

  • I2C Comm: CW313 GPIO01 (Husky TIO1) → SDA | CW313 GPIO02 (Husky TIO2) → SCL.

  • Pull-up Resistors: Two 4.7kΩ pull-up resistors on SDA and SCL. I am using CW313 GPIO03 (driven HIGH via Husky TIO3) to feed the 3.3V rail powering these pull-ups.

  • Power Measurement: I am using the CW313’s onboard shunt. The SHUNTL pin goes directly to the target’s VCC. I am using Husky’s 3.3V Target Power (scope.io.target_pwr) to power the board. Toggling this pin allows me to power-cycle the target.

2. The Analog Problem

Because I am abruptly power-cycling the target during the experiment:

  1. Ground Bounce / PLL Issues: When target_pwr is set to False, the resulting ground bounce causes the Husky’s ADC/Glitch LEDs to blink. The FPGA reports target clock issues (even though clkgen_src = 'system').

  2. Correlation Issues: I am applying FFT alignment on the traces. However, the CPA (Pearson Correlation with Hamming Weight) models are returning erratic peaks (e.g., 0x12, 0x7A). I suspect the power cut transient is completely destroying my SNR, or the bit-banged I2C clock pauses are misaligning the capture window.

3. The Implementation

Here is the core logic I am using. I am arming the scope at the 8th bit of the payload, right before the ACK cycle.

import chipwhisperer as cw
import time
import numpy as np
import scipy.signal

1. SETUP

scope = cw.scope()
scope.default_setup()
time.sleep(0.5)

Shielding the Husky clock for I2C Bit-Banging

scope.clock.clkgen_src = ‘system’
scope.clock.clkgen_freq = 7372800
scope.clock.adc_mul = 4
scope.glitch.enabled = False
scope.clock.reset_adc()
time.sleep(0.5)
scope.errors.clear()

scope.trigger.triggers = “tio4”
scope.trigger.module = “basic”
scope.gain.db = 25
scope.adc.samples = 15000
scope.adc.offset = 0

Power the pull-ups via CW313 GPIO03

scope.io.tio3 = “gpio_high”

def sda(bit): scope.io.tio1 = “high_z” if bit else “gpio_low”
def scl(bit): scope.io.tio2 = “high_z” if bit else “gpio_low”

def i2c_experiment_capture(data_byte):
for i in range(7):
sda((data_byte >> (7 - i)) & 1)
scl(1); scl(0)

sda(data_byte & 1)
scl(0) 

# Arm the scope right before the operation
scope.arm()      
scope.io.tio4 = "gpio_high"
scope.io.tio4 = "gpio_low"

scl(1)
scl(0) 
time.sleep(0.002) # Wait for recording to finish

sda(1); scl(1)
ack = scope.io.tio_states[0]

# Hold SCL low to prevent I2C STOP condition before power cycle
scl(0) 
return ack

2. CAPTURE LOOP

num_iterations = 256
traces = np.zeros((num_iterations, scope.adc.samples))

for iteration in range(num_iterations):
temp_traces = 


for _ in range(10): # Captures per iteration
    scope.io.target_pwr = True
    time.sleep(0.05) 
    sda(1); scl(1)
    time.sleep(0.01)

    # Standard I2C Start and setup bytes
    sda(0); scl(0)             
    # ... (sending setup bytes) ...
    
    i2c_experiment_capture(iteration)
    
    ret = scope.capture()
    if not ret:
        trace = scope.get_last_trace()
        if trace is not None and (np.max(trace) - np.min(trace)) > 0.01:
            temp_traces.append(trace)
    
    scope.errors.clear() 
    
    # Power cycle target to study state machine reset
    scope.io.target_pwr = False     
    time.sleep(0.05) 
        
if len(temp_traces) > 0:
    traces[iteration] = np.mean(temp_traces, axis=0)

(Standard FFT alignment and CPA math follows…)



My Question: Is this reasoning appropriate for measuring analog data during state-machine interruptions? I would greatly appreciate suggestions from experienced members on how to adjust the Husky ADC settings or improve the hardware measurement to prevent the ground bounce from corrupting the SNR.

Thanks in advance!

Hello and welcome.

This doesn’t give me a full picture of your setup; a schematic would be useful.

What exactly is the error? Run print(scope.errors) to find out.

What exactly are the “target clock issues”?

Hello, and thank you for taking the time to reply!

To give you a better picture, my target is an Atmel AT88SC25616C (CryptoMemory). My goal is to extract a 24-bit password by exploiting a timing/power leakage in the EEPROM’s charge pump activation, combined with a “Tear-Off” attack to prevent the internal security counter (PAC) from decrementing.

Here is the breakdown of the setup, the errors, the progress I’ve made, and the analog wall I’ve hit.

1. The Setup & “Schematic”

I am using a custom breadboard connected to the CW313 Interposer.

  • Target: AT88SC25616C (8-lead SOIC).

  • VCC: Routed directly through the CW313 SHUNTL for power analysis. The power is supplied by Husky’s target_pwr.

  • I2C Bus: Bit-banged. TIO1 → SDA, TIO2 → SCL.

  • Pull-ups: Two 4.7kΩ resistors, powered by TIO3 (driven HIGH).

  • Trigger: TIO4 is pulsed HIGH->LOW right before I send the I2C STOP condition.

  • The Tear-Off: Exactly 3.5ms after the STOP condition, I hard-cut the power (scope.io.target_pwr = False) to prevent the EEPROM from finalizing the PAC decrement write cycle.

2. The Errors & Target Clock Issues

When I run print(scope.errors) right after the power cut, I typically get an ADC Error (specifically, ADC clipping or phase errors) and sometimes ExtClk Error.

Because I am abruptly dropping target_pwr while the pull-ups and logic lines are still active, it creates a massive ground bounce/transient. Even though I am using scope.clock.clkgen_src = 'system' (the Husky is generating its own clock, not relying on the target), the electrical shock on the CW313 seems to destabilize the Husky’s ADC PLL or trigger logic momentarily, causing the LEDs to flash red.

3. The Attack Progression & The Core Problem

Despite the transient, I managed to capture clean enough traces before the power cut to successfully extract the 1st byte of the password, but the 2nd and 3rd bytes completely broke my mathematical models.

The State-Machine Leakage: When the chip verifies the 3-byte password, it evaluates them sequentially.

  • If Byte 1 is wrong, it immediately fires up the high-voltage charge pump to write to the EEPROM (decreasing the PAC).

  • If Byte 1 is correct, it delays the charge pump ignition to check Byte 2. This creates a beautiful, measurable delay in the power trace (Timing Leakage).

The Success (Byte 1): Using a simple TVLA and a Single-Point DPA (or SAD), I successfully isolated Byte 1 (0x18). The charge pump ignition happens early enough (around sample 30,000 at 29.5 MS/s) that the traces align well.

The Analog Wall (Bytes 2 & 3): The AT88SC relies on an internal, asynchronous RC oscillator running at ~36 kHz. By the time the execution reaches the Byte 2 decision (around sample 50,000+) and Byte 3 (sample 60,000+), the thermal and voltage variations cause massive Cumulative Phase Drift.

When I visually inspect the traces, I can clearly see that the correct Byte 2 (0xC4) holds the baseline voltage steady while all other 255 incorrect guesses suffer a voltage sag (charge pump activating). However, because of the severe jitter and DC offset between captures, standard Time-Domain Analysis (SAD, Variance, CPA, or Threshold Crossing) completely fails. The mathematical models end up latching onto noise spikes or baseline drifts rather than the actual charge pump delay.

4. What I Need Help With

To make this project successful and reliable, I need guidance on two fronts:

  1. Hardware / Measurement: How can I cleanly cut power to the target (Tear-Off) without causing a transient that crashes the Husky’s ADC? Should I use an external MOSFET circuit triggered by a GPIO instead of dropping scope.io.target_pwr directly?

  2. Software / Signal Processing: Standard FFT alignment on the start of the trace doesn’t fix the asynchronous RC drift 60,000 samples later. What are the best practices within the ChipWhisperer ecosystem for aligning traces with severe internal clock jitter? Is implementing Dynamic Time Warping (DTW) or Elastic Alignment the only way out, or is there a better feature-extraction technique for this kind of asynchronous delay?

Thank you again for the support!

Let me first set expectations: we cannot answer all your questions, particularly on the signal processing side. We answer all questions directly related to using our products. We’re happy to help with questions that fall outside of that, so asking such questions is not a problem, but understand that we may not be able to help you with e.g. aligning your traces. Doing this well can require pretty deep knowledge about your setup, your target, and your goals, and that’s well outside the scope of our free support.

Review our Intro to Husky notebook to understand what the flashing red LEDs mean. With regards to ADC clipping error, you can either:

  • adjust scope.gain so that the ADC doesn’t clip
  • choose to ignore the error (use this)

I have no idea what “phase error” might be; what is the exact message?

extclk_error should not happen; please provide the full output of print(scope) when you see it.

I will also say that toggling TIO4 from Python to then trigger on it is far from ideal, we wouldn’t recommend it unless there is really no other way! Triggering from Python like this means that you will have very inconsistent timing (and therefore potentially tricky trace alignment problems). Why not trigger on the I2C line, using either the basic trigger or the edge counter trigger?

I’m also confused about how you capture the traces. In your code above, you arm the scope once, then call scope.capture() multiple times in a loop: that is not the correct way to do it (and probably the cause of some of the errors you see?). You must arm the scope each time before scope.capture(). Have a look at how cw.capture_trace() works.

Finally, you may be interested in using our brand-new “hardware bitbanger” feature, which was added to the develop branch just last week. We will publish demo notebooks shortly, but for now you can look at the API. We’ve not used it for I2C yet but it should work very well for that. We’ve used it for SWD and 1-wire.