1+ """
2+ This python API is written for use with the Nordic Semiconductor's Power Profiler Kit II (PPK 2).
3+ The PPK2 uses Serial communication.
4+ The official nRF Connect Power Profiler was used as a reference: https://github.com/NordicSemiconductor/pc-nrfconnect-ppk
5+ """
6+
7+ import serial
8+ import time
9+ import struct
10+
11+
12+ class PPK2_Command ():
13+ """Serial command opcodes"""
14+ NO_OP = 0x00
15+ TRIGGER_SET = 0x01
16+ AVG_NUM_SET = 0x02 # no-firmware
17+ TRIGGER_WINDOW_SET = 0x03
18+ TRIGGER_INTERVAL_SET = 0x04
19+ TRIGGER_SINGLE_SET = 0x05
20+ AVERAGE_START = 0x06
21+ AVERAGE_STOP = 0x07
22+ RANGE_SET = 0x08
23+ LCD_SET = 0x09
24+ TRIGGER_STOP = 0x0a
25+ DEVICE_RUNNING_SET = 0x0c
26+ REGULATOR_SET = 0x0d
27+ SWITCH_POINT_DOWN = 0x0e
28+ SWITCH_POINT_UP = 0x0f
29+ TRIGGER_EXT_TOGGLE = 0x11
30+ SET_POWER_MODE = 0x11
31+ RES_USER_SET = 0x12
32+ SPIKE_FILTERING_ON = 0x15
33+ SPIKE_FILTERING_OFF = 0x16
34+ GET_META_DATA = 0x19
35+ RESET = 0x20
36+ SET_USER_GAINS = 0x25
37+
38+ class PPK2_Modes ():
39+ """PPK2 measurement modes"""
40+ AMPERE_MODE = "AMPERE_MODE"
41+ SOURCE_MODE = "SOURCE_MODE"
42+
43+ class PPK2_API ():
44+ def __init__ (self , port ):
45+
46+ self .ser = serial .Serial (port )
47+ self .ser .baudrate = 9600
48+
49+ self .modifiers = {
50+ "Calibrated" : None ,
51+ "R" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
52+ "GS" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
53+ "GI" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
54+ "O" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
55+ "S" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
56+ "I" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
57+ "UG" : {"0" : None , "1" : None , "2" : None , "3" : None , "4" : None },
58+ "HW" : None ,
59+ "IA" : None
60+ }
61+
62+ self .vdd_low = 800
63+ self .vdd_high = 5000
64+
65+ self .current_vdd = 0
66+
67+ self .adc_mult = 1.8 / 163840
68+
69+ self .MEAS_ADC = self ._generate_mask (14 , 0 )
70+ self .MEAS_RANGE = self ._generate_mask (3 , 14 )
71+ self .MEAS_LOGIC = self ._generate_mask (8 , 24 )
72+
73+ self .prev_rolling_avg = None
74+ self .prev_rolling_avg4 = None
75+ self .prev_range = None
76+
77+ self .mode = None
78+
79+ # adc measurement buffer remainder and len of remainder
80+ self .remainder = {"sequence" : b'' , "len" : 0 }
81+
82+ def _pack_struct (self , cmd_tuple ):
83+ """Returns packed struct"""
84+ return struct .pack ("B" * len (cmd_tuple ), * cmd_tuple )
85+
86+ def _write_serial (self , cmd_tuple ):
87+ """Writes cmd bytes to serial"""
88+ cmd_packed = self ._pack_struct (cmd_tuple )
89+ self .ser .write (cmd_packed )
90+
91+ def _twos_comp (self , val ):
92+ """Compute the 2's complement of int32 value"""
93+ if (val & (1 << (32 - 1 ))) != 0 :
94+ val = val - (1 << 32 ) # compute negative value
95+ return val
96+
97+ def _convert_source_voltage (self , mV ):
98+ """Convert input voltage to device command"""
99+ # minimal possible mV is 800
100+ if mV < self .vdd_low :
101+ mV = self .vdd_low
102+
103+ # maximal possible mV is 5000
104+ if mV > self .vdd_high :
105+ mV = self .vdd_high
106+
107+ offset = 32
108+ # get difference to baseline (the baseline is 800mV but the initial offset is 32)
109+ diff_to_baseline = mV - self .vdd_low + offset
110+ base_b_1 = 3
111+ base_b_2 = 0 # is actually 32 - compensated with above offset
112+
113+ # get the number of times we have to increase the first byte of the command
114+ ratio = int (diff_to_baseline / 256 )
115+ remainder = diff_to_baseline % 256 # get the remainder for byte 2
116+
117+ set_b_1 = base_b_1 + ratio
118+ set_b_2 = base_b_2 + remainder
119+
120+ return set_b_1 , set_b_2
121+
122+ def _read_metadata (self ):
123+ """Read metadata"""
124+ # try to get metadata from device
125+ for _ in range (0 , 5 ):
126+ # it appears the second reading is the metadata
127+ read = self .ser .read (self .ser .in_waiting )
128+ time .sleep (0.1 )
129+
130+ if read != b'' and "END" in read .decode ("utf-8" ):
131+ return read .decode ("utf-8" )
132+
133+ def _parse_metadata (self , metadata ):
134+ """Parse metadata and store it to modifiers"""
135+ data_split = [row .split (": " ) for row in metadata .split ("\n " )]
136+
137+ for key in self .modifiers .keys ():
138+ for data_pair in data_split :
139+ if key == data_pair [0 ]:
140+ self .modifiers [key ] = data_pair [1 ]
141+ for ind in range (0 , 5 ):
142+ if key + str (ind ) == data_pair [0 ]:
143+ self .modifiers [key ][str (ind )] = float (data_pair [1 ])
144+
145+ def _generate_mask (self , bits , pos ):
146+ pos = pos
147+ mask = ((2 ** bits - 1 ) << pos )
148+ mask = self ._twos_comp (mask )
149+ return {"mask" : mask , "pos" : pos }
150+
151+ def _get_masked_value (self , value , meas ):
152+ masked_value = (value & meas ["mask" ]) >> meas ["pos" ]
153+ return masked_value
154+
155+ def _handle_raw_data (self , adc_value ):
156+ """Convert raw value to analog value"""
157+ current_measurement_range = min (self ._get_masked_value (
158+ adc_value , self .MEAS_RANGE ), 5 ) # 5 is the number of parameters
159+ adc_result = self ._get_masked_value (adc_value , self .MEAS_ADC ) * 4
160+ bits = self ._get_masked_value (adc_value , self .MEAS_LOGIC )
161+ analog_value = self .get_adc_result (
162+ current_measurement_range , adc_result ) * 10 ** 6
163+
164+ return analog_value
165+
166+ def get_data (self ):
167+ """Return readings of one sampling period"""
168+ sampling_data = self .ser .read (self .ser .in_waiting )
169+ return sampling_data
170+
171+ def get_modifiers (self ):
172+ """Gets and sets modifiers from device memory"""
173+ self ._write_serial ((PPK2_Command .GET_META_DATA , ))
174+ metadata = self ._read_metadata ()
175+ self ._parse_metadata (metadata )
176+
177+ def start_measuring (self ):
178+ """Start continous measurement"""
179+ self ._write_serial ((PPK2_Command .AVERAGE_START , ))
180+
181+ def stop_measuring (self ):
182+ """Stop continous measurement"""
183+ self ._write_serial ((PPK2_Command .AVERAGE_STOP , ))
184+
185+ def set_source_voltage (self , mV ):
186+ """Inits device - based on observation only REGULATOR_SET is the command.
187+ The other two values correspond to the voltage level.
188+
189+ 800mV is the lowest setting - [3,32] - the values then increase linearly
190+ """
191+ b_1 , b_2 = self ._convert_source_voltage (mV )
192+ self ._write_serial ((PPK2_Command .REGULATOR_SET , b_1 , b_2 ))
193+ #self.current_vdd = mV
194+
195+ def toggle_DUT_power (self , state ):
196+ """Toggle DUT power based on parameter"""
197+ if state == "ON" :
198+ self ._write_serial ((PPK2_Command .DEVICE_RUNNING_SET , PPK2_Command .TRIGGER_SET )) # 12,1
199+
200+ if state == "OFF" :
201+ self ._write_serial ((PPK2_Command .DEVICE_RUNNING_SET , PPK2_Command .NO_OP )) # 12,0
202+
203+ def use_ampere_meter (self ):
204+ """Configure device to use ampere meter"""
205+ self .mode = PPK2_Modes .AMPERE_MODE
206+ self ._write_serial ((PPK2_Command .SET_POWER_MODE , PPK2_Command .TRIGGER_SET )) # 17,1
207+
208+ def use_source_meter (self ):
209+ """Configure device to use source meter"""
210+ self .mode = PPK2_Modes .SOURCE_MODE
211+ self ._write_serial ((PPK2_Command .SET_POWER_MODE , PPK2_Command .AVG_NUM_SET )) # 17,2
212+
213+ def get_adc_result (self , current_range , adc_value ):
214+ """Get result of adc conversion"""
215+ current_range = str (current_range )
216+ result_without_gain = (adc_value - self .modifiers ["O" ][current_range ]) * (
217+ self .adc_mult / self .modifiers ["R" ][current_range ])
218+
219+ adc = self .modifiers ["UG" ][current_range ] * (
220+ result_without_gain *
221+ (self .modifiers ["GS" ][current_range ] *
222+ result_without_gain + self .modifiers ["GI" ][current_range ])
223+ # this part is used only in source meter mode
224+ + (self .modifiers ["S" ][current_range ] +
225+ (self .current_vdd / 1000 ) + self .modifiers ["I" ][current_range ])
226+ )
227+
228+ self .rolling_avg = adc
229+ self .rolling_avg4 = adc
230+
231+ return adc
232+
233+ def _digital_to_analog (self , adc_value ):
234+ """Convert discrete value to analog value"""
235+ return int .from_bytes (adc_value , byteorder = "little" , signed = False ) # convert reading to analog value
236+
237+ def average_of_sampling_period (self , buf ):
238+ """
239+ Calculates the average value of one sampling period.
240+ The number of sampled values depends on the delay between serial reads.
241+ See example for more info.
242+ """
243+
244+ sample_size = 4 # one analog value is 4 bytes in size
245+ offset = self .remainder ["len" ]
246+ measurement_avg = 0
247+ num_samples = 0
248+
249+ first_reading = (self .remainder ["sequence" ] + buf [0 :sample_size - offset ])[:4 ]
250+ adc_val = self ._digital_to_analog (first_reading )
251+ measurement_avg += self ._handle_raw_data (adc_val )
252+ num_samples += 1
253+
254+ offset = sample_size - offset
255+
256+ while offset <= len (buf ) - sample_size :
257+ next_val = buf [offset :offset + sample_size ]
258+ offset += sample_size
259+ adc_val = self ._digital_to_analog (next_val )
260+
261+ measurement_avg += self ._handle_raw_data (adc_val )
262+ num_samples += 1
263+
264+ print ("Avg of {} samples: {} μA" .format (
265+ num_samples , measurement_avg / num_samples ))
266+
267+ self .remainder ["sequence" ] = buf [offset :len (buf )]
268+ self .remainder ["len" ] = len (buf )- offset
269+
270+ return measurement_avg / num_samples
0 commit comments