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