|
| 1 | +# pc-nrfconnect-ppk 源码解析——PPK2功能概述 |
| 2 | + |
| 3 | +pc-nrfconnect-ppk 是 Power Profiler Kit II 的御用上位机,所有的界面功能都是围绕 PPK2 的实际能力开发的,因此对于 PPK2 的硬件理解也是读懂源码的一个关键环节。 |
| 4 | + |
| 5 | +## PPK2 概述 |
| 6 | + |
| 7 | +这里引述 [PPK2 产品介绍](https://www.nordicsemi.cn/tools/ppk2/)中的描述; |
| 8 | + |
| 9 | +Power Profiler Kit II (PPK2)是一个独立的单元,可以测量(同时也可以选择供电)外部硬件以及所有Nordic DK低于uA至高达1A的电流。 |
| 10 | + |
| 11 | +PPK2通过一根标准的5V USB电缆供电,可提供高达500mA的电流。如需提供高达1A的电流,需要两根USB电缆。 |
| 12 | + |
| 13 | +支持安培表模式以及电源模式(PCB上分别显示为AMP和源测量单元(SMU))。对于安培表模式,外部电源必须向被测设备(DUT)提供0.8-5V的VCC电平。对于电源模式,PPK2提供0.8-5V的VCC电平,板载稳压器为外部应用提供最高1A的电流。除了外部硬件外,还可以测量所有Nordic DK的低睡眠电流、高工作电流以及短路电流峰值。 |
| 14 | + |
| 15 | +PPK2可以对nRF51、nRF52和nRF53系列的短距离应用提供支持,除此之外,它在支持nRF91系列蜂窝系统级封装(SiP)外还可以提供额外的电流,例如带有传感器的nRF9160 DK。 |
| 16 | + |
| 17 | +PPK2具有一个高动态测量范围的先进模拟测量单元。这使得它可以在低功耗嵌入式应用中的整个范围内精确测量功耗,范围可从μA到1A。根据测量范围的不同,分辨率在100nA到1mA之间,其精度足可以检测在低功耗优化系统中经常看到的小峰值。 |
| 18 | + |
| 19 | +PPK2还可以使用数字输入作为简易逻辑分析仪,实现代码同步测量。可以通过将数字输入连接到外部被测设备(DUT)上的I/O引脚来实现。使用此功能前,被测设备必须采用1.6-5.5V的VCC电压供电。然后,数字输入可以显示不同时间点被测设备中执行的代码。 |
| 20 | + |
| 21 | +与上一代的长周期采样窗口相比,快10倍的采样率(即100ksps)能够在任何时候实现最大的连续分辨率。这使得用户能够使用同一个采样窗口收集平均采集数据并放大获得高分辨率数据。 |
| 22 | + |
| 23 | +PC软件是一个运行在nRF Connect for Desktop框架中的独立Power Profiler应用。此应用既支持原始PPK,也支持PPK2。使用PPK2,Power Profiler应用可以在同一采样窗口中显示平均采集时间和高分辨率事件。测量数据可以导出进行后期处理。 |
| 24 | + |
| 25 | +## 软件描述 |
| 26 | + |
| 27 | +在 pc-nrfconnect-ppk 源码中,使用 SerialDevice 类对 PPK2 进行描述,我们将其按照参数定义和功能接口两部分进行逐一介绍。 |
| 28 | + |
| 29 | +### 参数定义 |
| 30 | + |
| 31 | +SerialDevice 的配置参数定义如下所示: |
| 32 | + |
| 33 | +```typescript |
| 34 | +class SerialDevice extends Device { |
| 35 | + public modifiers: modifiers = { |
| 36 | + r: [1031.64, 101.65, 10.15, 0.94, 0.043], |
| 37 | + gs: [1, 1, 1, 1, 1], |
| 38 | + gi: [1, 1, 1, 1, 1], |
| 39 | + o: [0, 0, 0, 0, 0], |
| 40 | + s: [0, 0, 0, 0, 0], |
| 41 | + i: [0, 0, 0, 0, 0], |
| 42 | + ug: [1, 1, 1, 1, 1], |
| 43 | + }; |
| 44 | + |
| 45 | + public adcSamplingTimeUs = 10; |
| 46 | + public resistors = { hi: 1.8, mid: 28, lo: 500 }; |
| 47 | + public vdd = 5000; |
| 48 | + public vddRange = { min: 800, max: 5000 }; |
| 49 | + public triggerWindowRange = { min: 1, max: 100 }; |
| 50 | + public isRunningInitially = false; |
| 51 | + |
| 52 | + private adcMult = 1.8 / 163840; |
| 53 | + |
| 54 | + // This are all declared to make typescript aware of their existence. |
| 55 | + private spikeFilter; |
| 56 | + private path; |
| 57 | + private child; |
| 58 | + private parser: any; |
| 59 | + private expectedCounter: null | number; |
| 60 | + private dataLossCounter: number; |
| 61 | + private corruptedSamples: { value: number; bits: number }[]; |
| 62 | + private rollingAvg: undefined | number; |
| 63 | + private rollingAvg4: undefined | number; |
| 64 | + private prevRange: undefined | number; |
| 65 | + private afterSpike: undefined | number; |
| 66 | + private consecutiveRangeSample: undefined | number; |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +其中前半部分自带初值的参数为 PPK2 硬件性能参数,后半部分为软件实现中设定的参数,这里我们着重对前半部分进行简要说明。 |
| 71 | + |
| 72 | +- modifiers:用于电路参数的微调 |
| 73 | + - r:5个采样电阻的阻值 |
| 74 | + - gs、gi、o、s、i、ug:不知道是啥,但是估计和上面的5个采样电阻相关 |
| 75 | +- adcSamplingTimeUs:adc 采样时间,10us 这也是采样率 100K 的来源(100K*10us = 1s) |
| 76 | +- resistors:也是个电阻,里面记录了三个阻值,不知道干嘛的 |
| 77 | +- vdd:电源电压 5V(默认值?) |
| 78 | +- vddRange:输出电压的工作范围(800mV~5000mV) |
| 79 | +- triggerWindowRange:出发窗口的范围(1~100),不知道干啥的 |
| 80 | +- isRunningInitially:无限运行? |
| 81 | +- adcMult:1.8 / 163840 (16384 = 2^14)这里猜测是一个14位的 ADC |
| 82 | + |
| 83 | +### 功能接口 |
| 84 | + |
| 85 | +SerialDevice 的功能接口如下所示: |
| 86 | + |
| 87 | +```typescript |
| 88 | +class SerialDevice extends Device { |
| 89 | + // 构造函数,传入 device 和采样回调函数 |
| 90 | + constructor( device: SharedDevice, onSampleCallback: (values: SampleValues) => void); |
| 91 | + |
| 92 | + // 真正意义上的对外接口,有实际的外部调用者 |
| 93 | + start(); |
| 94 | + stop(); |
| 95 | + parseMeta(meta: any): any; |
| 96 | + // Capability methods |
| 97 | + ppkSetPowerMode(isSmuMode: boolean): Promise<unknown>; |
| 98 | + ppkSetUserGains(range: number, gain: number): Promise<unknown>; |
| 99 | + ppkSetSpikeFilter(spikeFilter: SpikeFilter): void; |
| 100 | + ppkAverageStart(); |
| 101 | + |
| 102 | + // 同样被申明在 Device基类 中,但是并没有外部调用者 |
| 103 | + sendCommand(...args: PPKCmd[]): Promise<unknown> | undefined; |
| 104 | + |
| 105 | + // 仅内部调用的函数 |
| 106 | + getMetadata(); |
| 107 | + // parser |
| 108 | + getAdcResult(range: number, adcVal: number): number; |
| 109 | + handleRawDataSet(adcValue: number); |
| 110 | + parseMeasurementData(buf: Buffer); |
| 111 | + // other |
| 112 | + resetDataLossCounter(); |
| 113 | + dataLossReport(missingSamples: number); |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +接口说明已在上面的注释中有所体现,这里不过多赘述。 |
| 118 | + |
| 119 | +## 功能实现 |
| 120 | + |
| 121 | +### 骨架功能的实现 |
| 122 | + |
| 123 | +```typescript |
| 124 | +class SerialDevice extends Device { |
| 125 | + |
| 126 | + private child; |
| 127 | + private parser: any; |
| 128 | + |
| 129 | + constructor( |
| 130 | + device: SharedDevice, |
| 131 | + onSampleCallback: (values: SampleValues) => void |
| 132 | + ) { |
| 133 | + super(onSampleCallback); |
| 134 | + |
| 135 | + this.parser = null; |
| 136 | + this.child = fork( |
| 137 | + path.resolve(getAppDir(), 'worker', 'serialDevice.js') |
| 138 | + ); |
| 139 | + this.child.on('message', (message: serialDeviceMessage) => { |
| 140 | + this.parser(Buffer.from(message.data)); |
| 141 | + }); |
| 142 | + } |
| 143 | + |
| 144 | + start() { |
| 145 | + this.child.send({ open: this.path }); |
| 146 | + return this.getMetadata(); |
| 147 | + } |
| 148 | + |
| 149 | + stop() { |
| 150 | + this.child.kill(); |
| 151 | + } |
| 152 | + |
| 153 | + sendCommand(cmd: PPKCmd) { |
| 154 | + this.child.send({ write: cmd }); |
| 155 | + return Promise.resolve(cmd.length); |
| 156 | + } |
| 157 | + |
| 158 | + handleRawDataSet(adcValue: number) { |
| 159 | + let value = 0, bits = 0; |
| 160 | + this.onSampleCallback({ value, bits }); |
| 161 | + } |
| 162 | + |
| 163 | + parseMeasurementData(buf: Buffer) { |
| 164 | + this.handleRawDataSet(buf); |
| 165 | + } |
| 166 | + |
| 167 | + getMetadata() { |
| 168 | + let metadata = ''; |
| 169 | + return ( |
| 170 | + new Promise(resolve => { |
| 171 | + this.parser = (data: Buffer) => { |
| 172 | + metadata = `${metadata}${data}`; |
| 173 | + this.parser = this.parseMeasurementData.bind(this); |
| 174 | + resolve(metadata); |
| 175 | + }; |
| 176 | + this.sendCommand([PPKCmd.GetMetadata]); |
| 177 | + }).then(meta); |
| 178 | + ) |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +骨架功能的实现围绕 worker 和 parser 展开:worker 负责创建一个独立线程,下发控制命令并接收原始采样数据,parser 负责解析原始采样数据并输送给上层应用; |
| 184 | + |
| 185 | +#### worker |
| 186 | + |
| 187 | +TODO:使用流程图说明 cmd 和 message 的流转; |
| 188 | + |
| 189 | +精简过后的 serialDevice.js 代码: |
| 190 | + |
| 191 | +```javascript |
| 192 | +const { SerialPort } = require('serialport'); |
| 193 | + |
| 194 | +let port = null; |
| 195 | +process.on('message', msg => { |
| 196 | + if (msg.open) { |
| 197 | + console.log('\x1b[2J'); // ansi clear screen |
| 198 | + process.send({ opening: msg.open }); |
| 199 | + port = new SerialPort({ |
| 200 | + path: msg.open, |
| 201 | + autoOpen: false, |
| 202 | + baudRate: 115200, |
| 203 | + }); |
| 204 | + |
| 205 | + let data = Buffer.alloc(0); |
| 206 | + port.on('data', buf => { |
| 207 | + data = Buffer.concat([data, buf]); |
| 208 | + }); |
| 209 | + setInterval(() => { |
| 210 | + if (data.length === 0) return; |
| 211 | + process.send(data.slice(), err => { |
| 212 | + if (err) console.log(err); |
| 213 | + }); |
| 214 | + data = Buffer.alloc(0); |
| 215 | + }, 30); |
| 216 | + port.open(err => { |
| 217 | + if (err) { |
| 218 | + process.send({ error: err.toString() }); |
| 219 | + } |
| 220 | + process.send({ started: msg.open }); |
| 221 | + }); |
| 222 | + } |
| 223 | + if (msg.write) { |
| 224 | + port.write(msg.write, err => { |
| 225 | + if (err) { |
| 226 | + process.send({ error: 'PPK command failed' }); |
| 227 | + } |
| 228 | + }); |
| 229 | + } |
| 230 | +}); |
| 231 | + |
| 232 | +process.on('disconnect', () => { |
| 233 | + console.log('parent process disconnected, cleaning up'); |
| 234 | + if (port) { |
| 235 | + port.close(process.exit); |
| 236 | + } else { |
| 237 | + process.exit(); |
| 238 | + } |
| 239 | +}); |
| 240 | + |
| 241 | +``` |
| 242 | + |
| 243 | +#### parser |
| 244 | + |
| 245 | +TODO:分为几个部分进行: |
| 246 | + |
| 247 | +- meta |
| 248 | +- value |
| 249 | + - get raw data(adc value) |
| 250 | + - concat into raw data set |
| 251 | + - convert adc value to current value |
| 252 | +- bits |
| 253 | + |
| 254 | +### 其它功能 |
| 255 | + |
| 256 | +#### data loss |
| 257 | + |
| 258 | +TODO |
0 commit comments