Adc_mul does not work as I expected


Hi,
I tried to use Husky with different adc_mul factor. I found the recorded data with adc_mul=1 and adc_mul=4 are just the same. This is not what I expected: I expected that the lower plot (adc_mul=4) shall be a “zoom-in” of the above one (adc_mul=1) because husky records 4 samples per cycle.

The value of adc_clock is 29MHz and target is 7.37MHz. The clock setting looks good so did I misunderstand anything?

Yes, it should be. Show me your Python code?

The code is modularized so it is hard to paste everything here. I share some parts I think related.

def set_clock(
		self, 
		clock_speed:int=7.38E6, 
		clock_source:str='clkgen_x1',
		husky_clock_src:str='system',
		husky_clock_mul:int=1):
		"""
		Set the clock of CW's scope and the baud rate of 
		the target to read the values from it.
		Currently, there are 2 options of clock_source 
		for CW Lite: clkgen_x1 and clkgen_x4 

		For the Husky scope, it is mainly used 
		scope.clock.clkgen_src and scope.clock.adc_mul for its
		adc_freq; The clock.adc_src is only for backward compatibility.

		2 options for husky_clock_src: system and external.
		"""
		self.scope.clock.clkgen_freq = clock_speed

		if self.name == 'Husky':
			self.scope.clock.clkgen_src = husky_clock_src
			self.scope.clock.adc_mul = husky_clock_mul
			time.sleep(0.1)
			print('adc clock :', self.scope.clock.adc_freq)
			print('target clock:', self.scope.clock.clkgen_freq)
		else:
			self.scope.clock.adc_src = clock_source
		
		self.target.baud = int(clock_speed / 7.37E6 * 38400)

def set_adc(
		self, 
		decimate:int=1, 
		samples:int=20_000, 
		offset:int=0,
		enable_stream:bool=False,
		bits_per_sample:int=12):
		"""Set ADC of the scope"""

		self.scope.default_setup()
		if self.name == 'Husky':
			self.scope.adc.stream_mode = enable_stream
			# self.scope.adc.bits_per_sample = bits_per_sample
		self.scope.adc.decimate = decimate
		self.scope.adc.samples = samples
		self.scope.adc.offset = offset
### instSampleMultiplier decides how many power samples
### I need for capturing for each instruction.  
instSampleMultiplier = 4

### The product of number of instruction number and
### the inst/sample multiplier is the total power samples
### for capturing
targetPowerSamples = instCount * instSampleMultiplier

### The maximum buffer size of ChipWhisperer Husky
### is 100k samples. If the target power samples 
### are larger than the buffer size, we will need
### to repeat the same procedure for multiple times
bufferSize = 100_000
numChunks = targetPowerSamples // bufferSize
numChunks = (
	numChunks+1 
	if targetPowerSamples % bufferSize 
	else numChunks)
cw = CWrapper(
	platform=PLATFORM,
	name='Husky')
cw.reset()

offset = 0
cw.set_clock(
	clock_speed=7.38E6, 
	husky_clock_src='system',
	husky_clock_mul=instSampleMultiplier)

cw.set_adc(
	decimate=1, 
	samples=bufferSize, 
	offset=offset)

cw.program_target(
	hex_name=HEX_FILENAME)

fullPowerTraces = np.zeros(numChunks * bufferSize)
print('fullPowerTraces.shape:', fullPowerTraces.shape)
print('numChunks:', numChunks)

for n in range(numChunks):

	output, powerTrace = cw.capture_by_inference(
		input_data=[1],
		write_input_register_byte='p',
		output_size=4,
		read_output_register_byte='r',
		capture_power=True)
	print(powerTrace.shape)
	fullPowerTraces[offset:offset+bufferSize] = powerTrace

	offset += bufferSize

	cw.set_adc(
		decimate=1, 
		samples=bufferSize, 
		offset=offset)

	## lapse shall set with value at least higher than 0.001
	cw.reset(lapse=0.1)

This is a bit hard to follow and yet incomplete (e.g. instSampleMultiplier isn’t defined).
Please reduce this to a minimal reproducible example… something like this:

  • connect to Husky and target
  • capture trace with adc_mul = 1
  • capture trace with adc_mul = 4
  • compare results

I suspect that in doing so you’ll find your issue!

Ok, I will make a simple code to test again

I simplified the code a bit, but the issue remains

import struct
import numpy as np
import chipwhisperer as cw
import chipwhisperer.capture.targets as CWTargets

scope = cw.scope()
target = cw.target(
	scope, 
	CWTargets.SimpleSerial)
mul = 4
scope.clock.clkgen_freq = 7.38e6
scope.clock.clkgen_src = 'system'
scope.clock.adc_mul = mul
target.baud = int(7.38e6 / 7.37E6 * 38400)

scope.default_setup()
scope.adc.decimate = 1
scope.adc.samples = 100_000
scope.adc.offset = 0

cw.program_target(
	scope,
	cw.programmers.STM32FProgrammer,
	'main-CWLITEARM.hex')

inputBytes = bytearray([])
inputData = [1]
for d in inputData:
	d = struct.pack('i', d)
	inputBytes += d

target.simpleserial_write('p', inputBytes)

scope.arm()
scope.capture()
trace = scope.get_last_trace()

outputData = target.simpleserial_read('r', 4, timeout=0)

np.save(f"test_x{mul}.npy", trace)

scope.default_setup() changes a bunch of your scope settings, including scope.clock.adc_mul.
So, no matter what you set mul to, the capture from this code will be with scope.clock.adc_mul = 4.

This is why we made scope.default_setup() tell you what it’s changing:
If you run the code interactively, you would see:

>>> scope.clock.adc_mul = 1
>>> scope.default_setup()
scope.clock.adc_mul                      changed from 1                         to 4                        
scope.clock.adc_freq                     changed from 7370129.87012987          to 29480519.48051948        
scope.clock.extclk_tolerance             changed from 1715241718.189958         to 19629383676.79548       

Hi, thanks for the reply.

I check your code: if I set scope.clock.adc_mul = 4, even the scope.default_setup() is call after, scope.clock.adc_mul remains 4.

In addition, if I comment out the defaut_setup(), the program complains:

OSError: Could not detect STM32F, check connections, BOOT MODE entry setup

Note: My ChipWhisperer version is 5.7 and the result of scope.fw_version is {'major': 1, 'minor': 4, 'debug': 0}.

Hi,
After some changes, I now can get the different result. (Note: not so sure this is the correct or not)

The change I made is as following:

scope = cw.scope()
target = cw.target(
	scope, 
	CWTargets.SimpleSerial)
scope.default_setup()
cw.program_target(
	scope,
	cw.programmers.STM32FProgrammer,
	'main-CWLITEARM.hex')

mul = 4
scope.clock.clkgen_freq = 7.38e6
scope.clock.clkgen_src = 'system'
scope.clock.adc_mul = mul
target.baud = int(7.38e6 / 7.37E6 * 38400)

scope.adc.decimate = 1
scope.adc.samples = 100_000
scope.adc.offset = 0

scope.io.nrst = "low"
time.sleep(0.05)
scope.io.nrst = "high"

I first use default_setup before program_target so that the connection between host and target can work. Then I change the clock and adc speed after the program process is finished. Once this change is made, I reset target by rewriting nrst pin.

However, I am still a bit confused that when the clock update really happen. Does it occur once I assign 7.38e6 to scope.clock.clkgen_freq? Or everything only takes effect when the target is reset by nrst?

scope.default_setup() does a few things that most of our targets need, like turning on the clock to the target on HS2. The idea is to:

  1. connect to the scope
  2. call default_setup()
  3. then customize anything you want.

You can (and should) have a look at any of our Jupyter tutorials if you have a doubt on how to set up your target for capture.

Setting scope.clock.clkgen_freq has an immediate effect. Some targets may require a reset after its input clock is changed.