├── .gitattributes ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── package.json ├── src └── main.ts ├── tsconfig.json └── typings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # enforce unix style line endings 2 | *.ts text eol=lf 3 | *.tsx text eol=lf 4 | *.md text eol=lf 5 | *.txt text eol=lf 6 | *.js text eol=lf 7 | *.json text eol=lf 8 | *.xml text eol=lf 9 | *.svg text eol=lf 10 | *.yaml text eol=lf 11 | *.css text eol=lf 12 | *.html text eol=lf 13 | *.py text eol=lf 14 | *.exp text eol=lf 15 | *.manifest text eol=lf 16 | 17 | # do not enforce text for everything - it causes issues with random binary files 18 | 19 | *.sln text eol=crlf 20 | 21 | *.png binary 22 | *.jpg binary 23 | *.jpeg binary 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | typings 3 | built 4 | *.sw? 5 | *.ts.new 6 | *.tgz 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dapjs/4aab051ca3c927f82e8d4b60388b2f5a29ef802d/LICENSE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | node node_modules/typescript/bin/tsc 3 | #node built/main.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DAP.JS 2 | 3 | **Note: this package is now maintained by ARM at https://github.com/ARMmbed/dapjs** 4 | 5 | Node.js interface to DAP-CMSIS over USB/HID 6 | 7 | This package is meant to provide a subset of functionality of [pyOCD](https://github.com/mbedmicro/pyOCD). 8 | 9 | It's currently only being tested with BBC micro:bit devices. 10 | 11 | The is explicitly not using node.js features not present in browsers (in particular `Buffer` type) 12 | so it's easier to refactor it to use different USB/HID providers (WinRT, Chrome Apps etc). 13 | 14 | ## Build process 15 | 16 | ``` 17 | typings install 18 | npm install 19 | make 20 | ``` 21 | 22 | ## License 23 | 24 | MIT 25 | 26 | ## Code of Conduct 27 | 28 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dapjs", 3 | "version": "0.3.0", 4 | "description": "JavaScript interface to on-chip debugger (CMSIS-DAP)", 5 | "dependencies": { 6 | "bluebird": "^3.3.5", 7 | "node-hid": "^0.5.1" 8 | }, 9 | "devDependencies": { 10 | "typescript": "^1.8.10" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Microsoft/dapjs.git" 15 | }, 16 | "author": "Michal Moskal ", 17 | "license": "MIT", 18 | "homepage": "https://github.com/Microsoft/pxt#readme", 19 | "files": [ 20 | "README.md", 21 | "built/main.js", 22 | "built/main.d.ts" 23 | ], 24 | "typings": "built/main", 25 | "main": "built/main.js" 26 | } 27 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as Promise from "bluebird" 4 | 5 | const STACK_BASE = 0x20004000; 6 | const PAGE_SIZE = 0x400; 7 | 8 | function readUInt32LE(b: Uint8Array, idx: number) { 9 | return (b[idx] | 10 | (b[idx + 1] << 8) | 11 | (b[idx + 2] << 16) | 12 | (b[idx + 3] << 24)) >>> 0; 13 | } 14 | 15 | function bufferConcat(bufs: Uint8Array[]) { 16 | let len = 0 17 | for (let b of bufs) { 18 | len += b.length 19 | } 20 | let r = new Uint8Array(len) 21 | len = 0 22 | for (let b of bufs) { 23 | r.set(b, len) 24 | len += b.length 25 | } 26 | return r 27 | } 28 | 29 | export const enum DapCmd { 30 | DAP_INFO = 0x00, 31 | DAP_LED = 0x01, 32 | DAP_CONNECT = 0x02, 33 | DAP_DISCONNECT = 0x03, 34 | DAP_TRANSFER_CONFIGURE = 0x04, 35 | DAP_TRANSFER = 0x05, 36 | DAP_TRANSFER_BLOCK = 0x06, 37 | DAP_TRANSFER_ABORT = 0x07, 38 | DAP_WRITE_ABORT = 0x08, 39 | DAP_DELAY = 0x09, 40 | DAP_RESET_TARGET = 0x0a, 41 | DAP_SWJ_PINS = 0x10, 42 | DAP_SWJ_CLOCK = 0x11, 43 | DAP_SWJ_SEQUENCE = 0x12, 44 | DAP_SWD_CONFIGURE = 0x13, 45 | DAP_JTAG_SEQUENCE = 0x14, 46 | DAP_JTAG_CONFIGURE = 0x15, 47 | DAP_JTAG_IDCODE = 0x16, 48 | DAP_VENDOR0 = 0x80, 49 | } 50 | 51 | const enum Csw { 52 | CSW_SIZE = 0x00000007, 53 | CSW_SIZE8 = 0x00000000, 54 | CSW_SIZE16 = 0x00000001, 55 | CSW_SIZE32 = 0x00000002, 56 | CSW_ADDRINC = 0x00000030, 57 | CSW_NADDRINC = 0x00000000, 58 | CSW_SADDRINC = 0x00000010, 59 | CSW_PADDRINC = 0x00000020, 60 | CSW_DBGSTAT = 0x00000040, 61 | CSW_TINPROG = 0x00000080, 62 | CSW_HPROT = 0x02000000, 63 | CSW_MSTRTYPE = 0x20000000, 64 | CSW_MSTRCORE = 0x00000000, 65 | CSW_MSTRDBG = 0x20000000, 66 | CSW_RESERVED = 0x01000000, 67 | 68 | CSW_VALUE = (CSW_RESERVED | CSW_MSTRDBG | CSW_HPROT | CSW_DBGSTAT | CSW_SADDRINC) 69 | } 70 | 71 | const enum DapVal { 72 | AP_ACC = 1 << 0, 73 | DP_ACC = 0 << 0, 74 | READ = 1 << 1, 75 | WRITE = 0 << 1, 76 | VALUE_MATCH = 1 << 4, 77 | MATCH_MASK = 1 << 5 78 | } 79 | 80 | const enum Info { 81 | VENDOR_ID = 0x01, 82 | PRODUCT_ID = 0x02, 83 | SERIAL_NUMBER = 0x03, 84 | CMSIS_DAP_FW_VERSION = 0x04, 85 | TARGET_DEVICE_VENDOR = 0x05, 86 | TARGET_DEVICE_NAME = 0x06, 87 | CAPABILITIES = 0xf0, 88 | PACKET_COUNT = 0xfe, 89 | PACKET_SIZE = 0xff 90 | } 91 | 92 | export const enum Reg { 93 | DP_0x0 = 0, 94 | DP_0x4 = 1, 95 | DP_0x8 = 2, 96 | DP_0xC = 3, 97 | AP_0x0 = 4, 98 | AP_0x4 = 5, 99 | AP_0x8 = 6, 100 | AP_0xC = 7, 101 | 102 | IDCODE = Reg.DP_0x0, 103 | ABORT = Reg.DP_0x0, 104 | CTRL_STAT = Reg.DP_0x4, 105 | SELECT = Reg.DP_0x8, 106 | 107 | } 108 | 109 | export const enum ApReg { 110 | CSW = 0x00, 111 | TAR = 0x04, 112 | DRW = 0x0C, 113 | IDR = 0xFC 114 | } 115 | 116 | function apReg(r: ApReg, mode: DapVal) { 117 | let v = r | mode | DapVal.AP_ACC 118 | return (4 + ((v & 0x0c) >> 2)) as Reg 119 | } 120 | 121 | const enum CortexM { 122 | // Debug Fault Status Register 123 | DFSR = 0xE000ED30, 124 | DFSR_EXTERNAL = (1 << 4), 125 | DFSR_VCATCH = (1 << 3), 126 | DFSR_DWTTRAP = (1 << 2), 127 | DFSR_BKPT = (1 << 1), 128 | DFSR_HALTED = (1 << 0), 129 | 130 | // Debug Exception and Monitor Control Register 131 | DEMCR = 0xE000EDFC, 132 | // DWTENA in armv6 architecture reference manual 133 | DEMCR_TRCENA = (1 << 24), 134 | DEMCR_VC_HARDERR = (1 << 10), 135 | DEMCR_VC_BUSERR = (1 << 8), 136 | DEMCR_VC_CORERESET = (1 << 0), 137 | 138 | // Debug Core Register Selector Register 139 | DCRSR = 0xE000EDF4, 140 | DCRSR_REGWnR = (1 << 16), 141 | DCRSR_REGSEL = 0x1F, 142 | 143 | // Debug Halting Control and Status Register 144 | DHCSR = 0xE000EDF0, 145 | C_DEBUGEN = (1 << 0), 146 | C_HALT = (1 << 1), 147 | C_STEP = (1 << 2), 148 | C_MASKINTS = (1 << 3), 149 | C_SNAPSTALL = (1 << 5), 150 | S_REGRDY = (1 << 16), 151 | S_HALT = (1 << 17), 152 | S_SLEEP = (1 << 18), 153 | S_LOCKUP = (1 << 19), 154 | 155 | // Debug Core Register Data Register 156 | DCRDR = 0xE000EDF8, 157 | 158 | // Coprocessor Access Control Register 159 | CPACR = 0xE000ED88, 160 | CPACR_CP10_CP11_MASK = (3 << 20) | (3 << 22), 161 | 162 | NVIC_AIRCR = (0xE000ED0C), 163 | NVIC_AIRCR_VECTKEY = (0x5FA << 16), 164 | NVIC_AIRCR_VECTRESET = (1 << 0), 165 | NVIC_AIRCR_SYSRESETREQ = (1 << 2), 166 | 167 | CSYSPWRUPACK = 0x80000000, 168 | CDBGPWRUPACK = 0x20000000, 169 | CSYSPWRUPREQ = 0x40000000, 170 | CDBGPWRUPREQ = 0x10000000, 171 | 172 | TRNNORMAL = 0x00000000, 173 | MASKLANE = 0x00000f00, 174 | 175 | DBGKEY = (0xA05F << 16), 176 | 177 | // FPB (breakpoint) 178 | FP_CTRL = (0xE0002000), 179 | FP_CTRL_KEY = (1 << 1), 180 | FP_COMP0 = (0xE0002008), 181 | 182 | // DWT (data watchpoint & trace) 183 | DWT_CTRL = 0xE0001000, 184 | DWT_COMP_BASE = 0xE0001020, 185 | DWT_MASK_OFFSET = 4, 186 | DWT_FUNCTION_OFFSET = 8, 187 | DWT_COMP_BLOCK_SIZE = 0x10, 188 | } 189 | 190 | export const enum CortexReg { 191 | R0 = 0, 192 | R1 = 1, 193 | R2 = 2, 194 | R3 = 3, 195 | R4 = 4, 196 | R5 = 5, 197 | R6 = 6, 198 | R7 = 7, 199 | R8 = 8, 200 | R9 = 9, 201 | R10 = 10, 202 | R11 = 11, 203 | R12 = 12, 204 | SP = 13, 205 | LR = 14, 206 | PC = 15, 207 | XPSR = 16, 208 | MSP = 17, // Main Stack Pointer 209 | PSP = 18, // Process Stack Pointer 210 | PRIMASK = 20, // &0xff 211 | CONTROL = 20, // &0xff000000 >> 24 212 | } 213 | 214 | function bank(addr: number) { 215 | const APBANKSEL = 0x000000f0 216 | return (addr & APBANKSEL) | (addr & 0xff000000) 217 | } 218 | 219 | let HID = require('node-hid'); 220 | 221 | function error(msg: string, reconnect = false, wait = false): any { 222 | let err = new Error(msg); 223 | if (reconnect) (err as any).dapReconnect = true; 224 | if (wait) (err as any).dapWait = true; 225 | throw err; 226 | } 227 | 228 | function info(msg: string) { 229 | // console.log(msg) 230 | } 231 | 232 | function addInt32(arr: number[], val: number) { 233 | if (!arr) arr = [] 234 | arr.push(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff) 235 | return arr 236 | } 237 | 238 | function hex(v: number) { 239 | return "0x" + v.toString(16) 240 | } 241 | 242 | function rid(v: number) { 243 | let m = [ 244 | "DP_0x0", 245 | "DP_0x4", 246 | "DP_0x8", 247 | "DP_0xC", 248 | "AP_0x0", 249 | "AP_0x4", 250 | "AP_0x8", 251 | "AP_0xC", 252 | ] 253 | 254 | return m[v] || "?" 255 | } 256 | 257 | interface CmdEntry { 258 | resolve: (v: Uint8Array) => void; 259 | data: number[]; 260 | } 261 | 262 | export class Dap { 263 | dev: any; 264 | 265 | private sent: CmdEntry[] = []; 266 | private waiting: CmdEntry[] = []; 267 | private maxSent = 1; 268 | 269 | constructor(path: string) { 270 | this.dev = new HID.HID(path) 271 | 272 | this.dev.on("data", (buf: Buffer) => { 273 | let c = this.sent.shift() 274 | if (!c) { 275 | console.log("DROP", buf) 276 | } else { 277 | //console.log("GOT", buf) 278 | c.resolve(buf) 279 | this.pokeWaiting() 280 | } 281 | }) 282 | 283 | this.dev.on("error", (err: Error) => { 284 | console.log(err.message) 285 | }) 286 | } 287 | 288 | private pokeWaiting() { 289 | if (this.sent.length < this.maxSent && this.waiting.length > 0) { 290 | let w = this.waiting.shift() 291 | this.sent.push(w) 292 | //console.log(`SEND ${this.waiting.length} -> ${this.sent.length} ${w.data.join(",")}`) 293 | this.sendNums(w.data) 294 | } 295 | } 296 | 297 | private sendNums(lst: number[]) { 298 | lst.unshift(0) 299 | while (lst.length < 64) 300 | lst.push(0) 301 | this.dev.write(lst) 302 | } 303 | 304 | private jtagToSwdAsync() { 305 | let arrs = [ 306 | [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 307 | [0x9e, 0xe7], 308 | [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 309 | [0x00] 310 | ] 311 | return promiseIterAsync(arrs, a => this.swjSequenceAsync(a)) 312 | } 313 | 314 | private swjSequenceAsync(data: number[]) { 315 | data.unshift(data.length * 8) 316 | return this.cmdNumsAsync(DapCmd.DAP_SWJ_SEQUENCE, data).then(() => { }) 317 | } 318 | 319 | cmdNumsAsync(op: DapCmd, data: number[]) { 320 | data.unshift(op) 321 | return new Promise((resolve, reject) => { 322 | this.waiting.push({ resolve, data }) 323 | this.pokeWaiting() 324 | }).then(buf => { 325 | if (buf[0] != op) error(`Bad response for ${op} -> ${buf[0]}`) 326 | switch (op) { 327 | case DapCmd.DAP_CONNECT: 328 | case DapCmd.DAP_INFO: 329 | case DapCmd.DAP_TRANSFER: 330 | break; 331 | default: 332 | if (buf[1] != 0) 333 | error(`Bad status for ${op} -> ${buf[1]}`) 334 | } 335 | return buf 336 | }) 337 | } 338 | 339 | private infoAsync(id: Info) { 340 | return this.cmdNumsAsync(DapCmd.DAP_INFO, [id]) 341 | .then(buf => { 342 | if (buf[1] == 0) return null 343 | switch (id) { 344 | case Info.CAPABILITIES: 345 | case Info.PACKET_COUNT: 346 | case Info.PACKET_SIZE: 347 | if (buf[1] == 1) return buf[2] 348 | if (buf[1] == 2) return buf[3] << 8 | buf[2] 349 | } 350 | return buf.slice(2, buf[1] + 2 - 1); // .toString("utf8") 351 | }) 352 | } 353 | 354 | resetTargetAsync() { 355 | return this.cmdNumsAsync(DapCmd.DAP_RESET_TARGET, []) 356 | } 357 | 358 | disconnectAsync() { 359 | return this.cmdNumsAsync(DapCmd.DAP_DISCONNECT, []) 360 | } 361 | 362 | connectAsync() { 363 | info("Connecting...") 364 | return this.infoAsync(Info.PACKET_COUNT) 365 | .then((v: number) => { 366 | this.maxSent = v 367 | }) 368 | .then(() => this.cmdNumsAsync(DapCmd.DAP_SWJ_CLOCK, addInt32(null, 1000000))) 369 | .then(() => this.cmdNumsAsync(DapCmd.DAP_CONNECT, [1])) 370 | .then(buf => { 371 | if (buf[1] != 1) error("Non SWD") 372 | // 1MHz 373 | return this.cmdNumsAsync(DapCmd.DAP_SWJ_CLOCK, addInt32(null, 1000000)) 374 | }) 375 | .then(() => this.cmdNumsAsync(DapCmd.DAP_TRANSFER_CONFIGURE, [0, 0x50, 0, 0, 0])) 376 | .then(() => this.cmdNumsAsync(DapCmd.DAP_SWD_CONFIGURE, [0])) 377 | .then(() => this.jtagToSwdAsync()) 378 | .then(() => info("Connected.")) 379 | } 380 | } 381 | 382 | function promiseWhileAsync(fnAsync: () => Promise) { 383 | let loopAsync = (cond: boolean): Promise => 384 | cond ? fnAsync().then(loopAsync) : Promise.resolve() 385 | return loopAsync(true) 386 | } 387 | 388 | function promiseIterAsync(elts: T[], f: (v: T, idx: number) => Promise): Promise { 389 | let i = -1 390 | let loop = (): Promise => { 391 | if (++i >= elts.length) return Promise.resolve() 392 | return f(elts[i], i).then(loop) 393 | } 394 | return loop() 395 | } 396 | 397 | function promiseMapSeqAsync(elts: T[], f: (v: T) => Promise): Promise { 398 | let res: S[] = [] 399 | return promiseIterAsync(elts, v => f(v).then(z => { res.push(z) })) 400 | .then(() => res) 401 | } 402 | 403 | function range(n: number) { 404 | let r: number[] = [] 405 | for (let i = 0; i < n; ++i)r.push(i) 406 | return r 407 | } 408 | 409 | export class Breakpoint { 410 | public lastWritten: number; 411 | constructor(public parent: Device, public index: number) { 412 | } 413 | 414 | readAsync() { 415 | return this.parent.readMemAsync(CortexM.FP_COMP0 + this.index * 4) 416 | .then(n => { 417 | console.log(`idx=${this.index}, CURR=${n}, LAST=${this.lastWritten}`) 418 | }) 419 | } 420 | 421 | writeAsync(num: number) { 422 | // Doesn't seem to work 423 | //if (num == this.lastWritten) return Promise.resolve() 424 | this.lastWritten = num 425 | return this.parent.writeMemAsync(CortexM.FP_COMP0 + this.index * 4, num) 426 | //.then(() => this.readAsync()) 427 | } 428 | } 429 | 430 | function assert(cond: any) { 431 | if (!cond) { 432 | throw new Error("assertion failed"); 433 | } 434 | } 435 | 436 | export class Device { 437 | private dpSelect: number; 438 | private csw: number; 439 | idcode: number; 440 | dap: Dap; 441 | breakpoints: Breakpoint[]; 442 | 443 | constructor(private path: string) { 444 | this.dap = new Dap(path) 445 | } 446 | 447 | private clearCaches() { 448 | delete this.dpSelect 449 | delete this.csw 450 | for (let b of this.breakpoints) 451 | delete b.lastWritten 452 | } 453 | 454 | reconnectAsync() { 455 | this.clearCaches() 456 | 457 | return this.dap.disconnectAsync() 458 | .then(() => Promise.delay(100)) 459 | .then(() => this.initAsync()) 460 | 461 | // it seems we do not have to actually close the USB connection 462 | /* 463 | let dev = this.dap.dev 464 | // see https://github.com/node-hid/node-hid/issues/61 465 | dev.removeAllListeners() // unregister on(data) event 466 | dev.write([0, 0, 7]) // write something so that it responds 467 | dev.close() // now can close the device 468 | this.dap = null 469 | return Promise.delay(2000) 470 | .then(() => { 471 | this.dap = new Dap(this.path) 472 | return this.initAsync() 473 | }) 474 | */ 475 | } 476 | 477 | initAsync() { 478 | return this.dap.connectAsync() 479 | .then(() => this.readDpAsync(Reg.IDCODE)) 480 | .then(n => { this.idcode = n }) 481 | .then(() => this.writeRegAsync(Reg.DP_0x0, 1 << 2)) // clear sticky error 482 | .then(() => this.writeDpAsync(Reg.SELECT, 0)) 483 | .then(() => this.writeDpAsync(Reg.CTRL_STAT, CortexM.CSYSPWRUPREQ | CortexM.CDBGPWRUPREQ)) 484 | .then(() => { 485 | let m = CortexM.CDBGPWRUPACK | CortexM.CSYSPWRUPACK 486 | return promiseWhileAsync(() => 487 | this.readDpAsync(Reg.CTRL_STAT) 488 | .then(v => (v & m) != m)) 489 | }) 490 | .then(() => this.writeDpAsync(Reg.CTRL_STAT, CortexM.CSYSPWRUPREQ | CortexM.CDBGPWRUPREQ | CortexM.TRNNORMAL | CortexM.MASKLANE)) 491 | .then(() => this.writeDpAsync(Reg.SELECT, 0)) 492 | .then(() => this.readApAsync(ApReg.IDR)) 493 | .then(() => this.setupFpbAsync()) 494 | .then(() => info("Initialized.")) 495 | } 496 | 497 | writeRegAsync(regId: Reg, val: number) { 498 | if (val === null) error("bad val") 499 | info(`writeReg(${rid(regId)}, ${hex(val)})`) 500 | return this.regOpAsync(regId, val) 501 | .then(() => { 502 | }) 503 | } 504 | 505 | readRegAsync(regId: Reg) { 506 | return this.regOpAsync(regId, null) 507 | .then(buf => { 508 | let v = readUInt32LE(buf, 3) 509 | info(`readReg(${rid(regId)}) = ${hex(v)}`) 510 | return v 511 | }) 512 | } 513 | 514 | readDpAsync(addr: Reg) { 515 | return this.readRegAsync(addr) 516 | } 517 | 518 | readApAsync(addr: ApReg) { 519 | return this.writeDpAsync(Reg.SELECT, bank(addr)) 520 | .then(() => this.readRegAsync(apReg(addr, DapVal.READ))) 521 | } 522 | 523 | writeDpAsync(addr: Reg, data: number) { 524 | if (addr == Reg.SELECT) { 525 | if (data === this.dpSelect) return Promise.resolve() 526 | this.dpSelect = data 527 | } 528 | return this.writeRegAsync(addr, data) 529 | } 530 | 531 | writeApAsync(addr: ApReg, data: number) { 532 | return this.writeDpAsync(Reg.SELECT, bank(addr)) 533 | .then(() => { 534 | if (addr == ApReg.CSW) { 535 | if (data === this.csw) return Promise.resolve() 536 | this.csw = data 537 | } 538 | return this.writeRegAsync(apReg(addr, DapVal.WRITE), data) 539 | }) 540 | } 541 | 542 | writeMemAsync(addr: number, data: number) { 543 | //console.log(`wr: ${addr.toString(16)} := ${data}`) 544 | return this.writeApAsync(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32) 545 | .then(() => this.writeApAsync(ApReg.TAR, addr)) 546 | .then(() => this.writeApAsync(ApReg.DRW, data)) 547 | } 548 | 549 | readMemAsync(addr: number): Promise { 550 | return this.writeApAsync(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32) 551 | .then(() => this.writeApAsync(ApReg.TAR, addr)) 552 | .then(() => this.readApAsync(ApReg.DRW)) 553 | .catch(e => { 554 | if (e.dapWait) { 555 | console.log(`transfer wait, read at 0x${addr.toString(16)}`) 556 | return Promise.delay(100).then(() => this.readMemAsync(addr)) 557 | } 558 | else return Promise.reject(e) 559 | }) 560 | } 561 | 562 | haltAsync() { 563 | return this.writeMemAsync(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN | CortexM.C_HALT) 564 | } 565 | 566 | isHaltedAsync() { 567 | return this.statusAsync().then(s => s.isHalted) 568 | } 569 | 570 | statusAsync() { 571 | return this.readMemAsync(CortexM.DHCSR) 572 | .then(dhcsr => this.readMemAsync(CortexM.DFSR) 573 | .then(dfsr => ({ 574 | dhcsr: dhcsr, 575 | dfsr: dfsr, 576 | isHalted: !!(dhcsr & CortexM.S_HALT) 577 | }))) 578 | } 579 | 580 | debugEnableAsync() { 581 | return this.writeMemAsync(CortexM.DHCSR, CortexM.DBGKEY | CortexM.C_DEBUGEN) 582 | } 583 | 584 | resumeAsync() { 585 | return this.isHaltedAsync() 586 | .then(halted => { 587 | if (halted) 588 | return this.writeMemAsync(CortexM.DFSR, CortexM.DFSR_DWTTRAP | CortexM.DFSR_BKPT | CortexM.DFSR_HALTED) 589 | .then(() => this.debugEnableAsync()) 590 | }) 591 | } 592 | 593 | snapshotMachineStateAsync() { 594 | let state: MachineState = { 595 | stack: null, 596 | registers: [] 597 | } 598 | return promiseIterAsync(range(16), regno => this.readCpuRegisterAsync(regno) 599 | .then(v => { 600 | state.registers[regno] = v 601 | })) 602 | .then(() => this.readStackAsync()) 603 | .then(stack => { 604 | state.stack = stack 605 | return state 606 | }) 607 | } 608 | 609 | restoreMachineState(state: MachineState) { 610 | return promiseIterAsync(state.registers, 611 | (val, idx) => val === null 612 | ? Promise.resolve() 613 | : this.writeCpuRegisterAsync(idx, val)) 614 | .then(() => this.writeBlockAsync(STACK_BASE - state.stack.length * 4, state.stack)) 615 | } 616 | 617 | waitForHaltAsync() { 618 | return promiseWhileAsync(() => this.isHaltedAsync().then(v => { 619 | if (v) return false 620 | return Promise.delay(50).then(() => true) 621 | })) 622 | } 623 | 624 | executeCodeAsync(code: number[], args: number[], quick = false) { 625 | code = code.concat([0xbe2a]) // 'bkpt 42'; possible zero-padding will be interpreted as 'movs r0, r0' 626 | let baseAddr = STACK_BASE - code.length * 4; 627 | let state: MachineState = { 628 | stack: code, 629 | registers: args.slice() 630 | } 631 | while (state.registers.length < 16) { 632 | state.registers.push(quick ? null : 0) 633 | } 634 | state.registers[CortexReg.LR] = STACK_BASE - 4 + 1; // 'bkpt' instruction we added; +1 for Thumb state 635 | state.registers[CortexReg.SP] = baseAddr; 636 | state.registers[CortexReg.PC] = baseAddr; 637 | if (quick) state.stack = [] 638 | return this.restoreMachineState(state) 639 | //.then(() => this.snapshotMachineStateAsync()) 640 | //.then(logMachineState("beforecode")) 641 | .then(() => this.resumeAsync()) 642 | } 643 | 644 | writePagesAsync(info: FlashData) { 645 | let currBuf = 0 646 | let bufPtr = 0 647 | let dstAddr = info.flashAddr 648 | let waitForStopAsync = () => promiseWhileAsync(() => 649 | this.isHaltedAsync() 650 | .then(h => !h)) 651 | let quickRun = false 652 | let loopAsync = (): Promise => { 653 | return Promise.resolve() 654 | .then(() => { 655 | let nextPtr = bufPtr + PAGE_SIZE / 4 656 | let sl = info.flashWords.slice(bufPtr, nextPtr) 657 | bufPtr = nextPtr 658 | return this.writeBlockAsync(info.bufferAddr + currBuf * PAGE_SIZE, sl) 659 | }) 660 | .then(waitForStopAsync) 661 | .then(() => this.executeCodeAsync(info.flashCode, [dstAddr, info.bufferAddr + currBuf * PAGE_SIZE], quickRun)) 662 | .then(() => { 663 | quickRun = true 664 | currBuf++ 665 | dstAddr += PAGE_SIZE 666 | if (currBuf >= info.numBuffers) currBuf = 0 667 | if (bufPtr < info.flashWords.length) return loopAsync(); 668 | else return waitForStopAsync() 669 | }) 670 | } 671 | return this.haltAsync() 672 | .then(loopAsync) 673 | .then(() => Promise.delay(200)) 674 | .then(() => this.resetCoreAsync()) 675 | } 676 | 677 | isThreadHaltedAsync() { 678 | return this.isHaltedAsync() 679 | .then(v => { 680 | if (!v) return false 681 | return this.readCpuRegisterAsync(CortexReg.PRIMASK) 682 | .then(v => { 683 | if (v & 1) return false 684 | else 685 | return this.readCpuRegisterAsync(CortexReg.XPSR) 686 | .then(v => { 687 | if (v & 0x3f) return false 688 | else return true 689 | }) 690 | }) 691 | }) 692 | } 693 | 694 | safeHaltAsync() { 695 | return this.isThreadHaltedAsync() 696 | .then(halted => { 697 | if (!halted) { 698 | return promiseWhileAsync(() => this.haltAsync() 699 | .then(() => this.isThreadHaltedAsync()) 700 | .then(safe => { 701 | if (safe) 702 | return false 703 | else 704 | return this.resumeAsync().then(() => true) 705 | })) 706 | } 707 | }) 708 | } 709 | 710 | setBreakpointsAsync(addrs: number[]) { 711 | function mapAddr(addr: number) { 712 | if (addr === null) return 0 713 | if ((addr & 3) == 2) 714 | return 0x80000001 | (addr & ~3) 715 | else if ((addr & 3) == 0) 716 | return 0x40000001 | (addr & ~3) 717 | else error("uneven address"); 718 | } 719 | if (addrs.length > this.breakpoints.length) 720 | error("not enough hw breakpoints"); 721 | return this.debugEnableAsync() 722 | .then(() => this.setFpbEnabledAsync(true)) 723 | .then(() => { 724 | while (addrs.length < this.breakpoints.length) 725 | addrs.push(null) 726 | return promiseIterAsync(addrs, (addr, i) => 727 | this.breakpoints[i].writeAsync(mapAddr(addr))) 728 | }) 729 | } 730 | 731 | setFpbEnabledAsync(enabled = true) { 732 | return this.writeMemAsync(CortexM.FP_CTRL, CortexM.FP_CTRL_KEY | (enabled ? 1 : 0)) 733 | } 734 | 735 | setupFpbAsync() { 736 | // Reads the number of hardware breakpoints available on the core 737 | // and disable the FPB (Flash Patch and Breakpoint Unit) 738 | // which will be enabled when a first breakpoint will be set 739 | 740 | // setup FPB (breakpoint) 741 | return this.readMemAsync(CortexM.FP_CTRL) 742 | .then(fpcr => { 743 | let nb_code = ((fpcr >> 8) & 0x70) | ((fpcr >> 4) & 0xF) 744 | let nb_lit = (fpcr >> 7) & 0xf 745 | if (nb_code == 0) error("invalid initialization") 746 | info(`${nb_code} hardware breakpoints, ${nb_lit} literal comparators`) 747 | this.breakpoints = range(nb_code).map(i => new Breakpoint(this, i)) 748 | return this.setFpbEnabledAsync(false) 749 | }) 750 | .then(() => Promise.map(this.breakpoints, b => b.writeAsync(0))) 751 | } 752 | 753 | resetCoreAsync() { 754 | return this.writeMemAsync(CortexM.NVIC_AIRCR, CortexM.NVIC_AIRCR_VECTKEY | CortexM.NVIC_AIRCR_SYSRESETREQ) 755 | .then(() => { }) 756 | } 757 | 758 | readCpuRegisterAsync(no: CortexReg) { 759 | return this.writeMemAsync(CortexM.DCRSR, no) 760 | .then(() => this.readMemAsync(CortexM.DHCSR)) 761 | .then(v => assert(v & CortexM.S_REGRDY)) 762 | .then(() => this.readMemAsync(CortexM.DCRDR)) 763 | } 764 | 765 | writeCpuRegisterAsync(no: CortexReg, val: number) { 766 | return this.writeMemAsync(CortexM.DCRDR, val) 767 | .then(() => this.writeMemAsync(CortexM.DCRSR, no | CortexM.DCRSR_REGWnR)) 768 | .then(() => this.readMemAsync(CortexM.DHCSR)) 769 | .then(v => { 770 | assert(v & CortexM.S_REGRDY) 771 | }) 772 | } 773 | 774 | readStateAsync(): Promise { 775 | let r: CpuState = { 776 | pc: 0, 777 | lr: 0, 778 | stack: [] 779 | } 780 | return this.readStackAsync() 781 | .then(s => r.stack = s) 782 | .then(() => this.readCpuRegisterAsync(CortexReg.PC)) 783 | .then(v => r.pc = v) 784 | .then(() => this.readCpuRegisterAsync(CortexReg.LR)) 785 | .then(v => r.lr = v) 786 | .then(() => r) 787 | } 788 | 789 | private regOpAsync(regId: Reg, val: number) { 790 | let request = regRequest(regId, val !== null) 791 | let sendargs = [0, 1, request] 792 | if (val !== null) 793 | addInt32(sendargs, val) 794 | return this.dap.cmdNumsAsync(DapCmd.DAP_TRANSFER, sendargs) 795 | .then(buf => { 796 | if (buf[1] != 1) error("Bad #trans " + buf[1], true) 797 | if (buf[2] != 1) { 798 | if (buf[2] == 2) 799 | error("Transfer wait", true, true) 800 | error("Bad transfer status " + buf[2], true) 801 | } 802 | return buf 803 | }) 804 | } 805 | 806 | readRegRepeatAsync(regId: Reg, cnt: number) { 807 | assert(cnt <= 15) 808 | let request = regRequest(regId) 809 | let sendargs = [0, cnt] 810 | for (let i = 0; i < cnt; ++i) sendargs.push(request) 811 | return this.dap.cmdNumsAsync(DapCmd.DAP_TRANSFER, sendargs) 812 | .then(buf => { 813 | if (buf[1] != cnt) error("(many) Bad #trans " + buf[1]) 814 | if (buf[2] != 1) error("(many) Bad transfer status " + buf[2]) 815 | return buf.slice(3, 3 + cnt * 4) 816 | }) 817 | } 818 | 819 | writeRegRepeatAsync(regId: Reg, data: number[]) { 820 | assert(data.length <= 15) 821 | let request = regRequest(regId, true) 822 | let sendargs = [0, data.length] 823 | for (let i = 0; i < data.length; ++i) { 824 | sendargs.push(request) 825 | addInt32(sendargs, data[i]) 826 | } 827 | return this.dap.cmdNumsAsync(DapCmd.DAP_TRANSFER, sendargs) 828 | .then(buf => { 829 | if (buf[2] != 1) error("(many-wr) Bad transfer status " + buf[2], true, true) 830 | }) 831 | } 832 | 833 | readBlockAsync(addr: number, words: number) { 834 | let funs = [() => Promise.resolve()] 835 | let bufs: Uint8Array[] = [] 836 | let end = addr + words * 4 837 | let ptr = addr 838 | while (ptr < end) { 839 | let nextptr = ptr + PAGE_SIZE 840 | if (ptr == addr) { 841 | nextptr &= ~(PAGE_SIZE - 1) 842 | } 843 | (() => { 844 | let len = Math.min(nextptr - ptr, end - ptr) 845 | let ptr0 = ptr 846 | assert((len & 3) == 0) 847 | funs.push(() => 848 | this.readBlockCoreAsync(ptr0, len >> 2) 849 | .then(b => { 850 | bufs.push(b) 851 | })) 852 | })() 853 | ptr = nextptr 854 | } 855 | return promiseIterAsync(funs, f => f()) 856 | .then(() => bufferConcat(bufs)) 857 | } 858 | 859 | private readBlockCoreAsync(addr: number, words: number) { 860 | return this.writeApAsync(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32) 861 | .then(() => this.writeApAsync(ApReg.TAR, addr)) 862 | .then(() => { 863 | let blocks = range(Math.ceil(words / 15)) 864 | let lastSize = words % 15 865 | if (lastSize == 0) lastSize = 15 866 | let bufs: Uint8Array[] = [] 867 | return Promise.map(blocks, no => this.readRegRepeatAsync(apReg(ApReg.DRW, DapVal.READ), 868 | no == blocks.length - 1 ? lastSize : 15)) 869 | .then(bufs => bufferConcat(bufs)) 870 | }) 871 | } 872 | 873 | writeBlockAsync(addr: number, words: number[]) { 874 | if (words.length == 0) 875 | return Promise.resolve() 876 | console.log(`write block: 0x${addr.toString(16)} ${words.length} len`) 877 | if (1 > 0) 878 | return this.writeBlockCoreAsync(addr, words) 879 | .then(() => console.log("written")) 880 | let blSz = 10 881 | let blocks = range(Math.ceil(words.length / blSz)) 882 | return promiseIterAsync(blocks, no => 883 | this.writeBlockCoreAsync(addr + no * blSz * 4, words.slice(no * blSz, no * blSz + blSz))) 884 | .then(() => console.log("written")) 885 | } 886 | 887 | writeBlockCoreAsync(addr: number, words: number[]): Promise { 888 | return this.writeApAsync(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32) 889 | .then(() => this.writeApAsync(ApReg.TAR, addr)) 890 | .then(() => { 891 | let blSz = 12 // with 15 we get strange errors 892 | let blocks = range(Math.ceil(words.length / blSz)) 893 | let reg = apReg(ApReg.DRW, DapVal.WRITE) 894 | return Promise.map(blocks, no => this.writeRegRepeatAsync(reg, words.slice(no * blSz, no * blSz + blSz))) 895 | .then(() => { }) 896 | }) 897 | .catch(e => { 898 | if (e.dapWait) { 899 | console.log(`transfer wait, write block`) 900 | return Promise.delay(100).then(() => this.writeBlockCoreAsync(addr, words)) 901 | } 902 | else return Promise.reject(e) 903 | }) 904 | } 905 | 906 | snapshotHexAsync() { 907 | return this.readBlockAsync(0, 256 * 1024 / 4) 908 | .then(buf => { 909 | let upper = -1 910 | let addr = 0 911 | let myhex: string[] = [] 912 | while (addr < buf.length) { 913 | if ((addr >> 16) != upper) { 914 | upper = addr >> 16 915 | myhex.push(hexBytes([0x02, 0x00, 0x00, 0x04, upper >> 8, upper & 0xff])) 916 | } 917 | let bytes = [0x10, (addr >> 8) & 0xff, addr & 0xff, 0] 918 | for (let i = 0; i < 16; ++i) 919 | bytes.push(buf[addr + i]) 920 | myhex.push(hexBytes(bytes)) 921 | addr += 16 922 | } 923 | 924 | myhex.push(":020000041000EA") 925 | myhex.push(":0410140000C0030015") 926 | myhex.push(":040000050003C0C173") 927 | myhex.push(":00000001FF") 928 | myhex.push("") 929 | 930 | return myhex.join("\r\n") 931 | }) 932 | } 933 | 934 | readIdCodeAsync() { 935 | return this.readDpAsync(Reg.IDCODE) 936 | } 937 | 938 | readStackAsync() { 939 | return this.readCpuRegisterAsync(CortexReg.SP) 940 | .then(sp => { 941 | let size = STACK_BASE - sp 942 | if ((size & 3) || size < 0 || size > 8 * 1024) error("Bad SP: " + hex(sp)); 943 | return this.readBlockAsync(sp, size / 4) 944 | }) 945 | .then(bufToUint32Array) 946 | } 947 | } 948 | 949 | function hexBytes(bytes: number[]) { 950 | var chk = 0 951 | var r = ":" 952 | bytes.forEach(b => chk += b) 953 | bytes.push((-chk) & 0xff) 954 | bytes.forEach(b => r += ("0" + b.toString(16)).slice(-2)) 955 | return r.toUpperCase(); 956 | } 957 | 958 | function arrToString(arr: number[]) { 959 | let r = "" 960 | for (let i = 0; i < arr.length; ++i) { 961 | r += ("0000" + i).slice(-4) + ": " + ("00000000" + (arr[i] >>> 0).toString(16)).slice(-8) + "\n" 962 | } 963 | return r 964 | } 965 | 966 | function machineStateToString(s: MachineState) { 967 | return "\n\nREGS:\n" + arrToString(s.registers) + "\n\nSTACK:\n" + arrToString(s.stack) + "\n" 968 | } 969 | 970 | export interface FlashData { 971 | flashCode: number[]; 972 | flashWords: number[]; 973 | numBuffers: number; 974 | bufferAddr: number; 975 | flashAddr: number; 976 | } 977 | 978 | export interface CpuState { 979 | pc: number; 980 | lr: number; 981 | stack: number[]; 982 | } 983 | 984 | export interface MachineState { 985 | registers: number[]; 986 | stack: number[]; 987 | } 988 | 989 | function bufToUint32Array(buf: Uint8Array) { 990 | assert((buf.length & 3) == 0) 991 | let r: number[] = [] 992 | if (!buf.length) return r 993 | r[buf.length / 4 - 1] = 0 994 | for (let i = 0; i < r.length; ++i) 995 | r[i] = readUInt32LE(buf, i << 2) 996 | return r 997 | } 998 | 999 | function regRequest(regId: number, isWrite = false) { 1000 | let request = !isWrite ? DapVal.READ : DapVal.WRITE 1001 | if (regId < 4) 1002 | request |= DapVal.DP_ACC 1003 | else 1004 | request |= DapVal.AP_ACC 1005 | request |= (regId & 3) << 2 1006 | return request 1007 | } 1008 | 1009 | function timeAsync(lbl: string, f: () => Promise): () => Promise { 1010 | return () => { 1011 | let n = Date.now() 1012 | return f().then(v => { 1013 | let d = Date.now() - n 1014 | console.log(`${lbl}: ${d}ms`) 1015 | return v 1016 | }) 1017 | } 1018 | } 1019 | 1020 | export interface HidDevice { 1021 | product: string; 1022 | path: string; 1023 | } 1024 | 1025 | export function getMbedDevices() { 1026 | let devices = HID.devices() as HidDevice[] 1027 | return devices.filter(d => /MBED CMSIS-DAP/.test(d.product)) 1028 | } 1029 | 1030 | export interface Map { 1031 | [n: string]: T; 1032 | } 1033 | 1034 | let devices: Map> = {} 1035 | 1036 | function getDeviceAsync(path: string) { 1037 | if (devices[path]) return devices[path] 1038 | let d = new Device(path) 1039 | return (devices[path] = d.initAsync().then(() => d)) 1040 | } 1041 | 1042 | function handleDevMsgAsync(msg: any): Promise { 1043 | if (!msg.path) error("path missing"); 1044 | // TODO enforce queue per path? 1045 | return getDeviceAsync(msg.path) 1046 | .then(dev => { 1047 | switch (msg.op) { 1048 | case "halt": return dev.safeHaltAsync().then(() => ({})) 1049 | case "snapshot": return dev.snapshotMachineStateAsync() 1050 | .then(v => ({ state: v })); 1051 | case "restore": return dev.restoreMachineState(msg.state); 1052 | case "resume": return dev.resumeAsync(); 1053 | case "reset": return dev.resetCoreAsync(); 1054 | case "breakpoints": return dev.setBreakpointsAsync(msg.addrs); 1055 | case "status": return dev.statusAsync(); 1056 | case "bgexec": 1057 | return dev.executeCodeAsync(msg.code, msg.args || []) 1058 | case "exec": 1059 | return dev.executeCodeAsync(msg.code, msg.args || []) 1060 | .then(() => dev.waitForHaltAsync()) 1061 | case "wrpages": 1062 | return dev.writePagesAsync(msg) 1063 | case "wrmem": 1064 | return dev.writeBlockAsync(msg.addr, msg.words) 1065 | case "mem": 1066 | return dev.readBlockAsync(msg.addr, msg.words) 1067 | .then(buf => { 1068 | let res: number[] = [] 1069 | for (let i = 0; i < buf.length; i += 4) 1070 | res.push(readUInt32LE(buf, i)) 1071 | return { data: res } 1072 | }) 1073 | } 1074 | }) 1075 | } 1076 | 1077 | export function handleMessageAsync(msg: any): Promise { 1078 | switch (msg.op) { 1079 | case "list": return Promise.resolve({ devices: getMbedDevices() }) 1080 | default: 1081 | return handleDevMsgAsync(msg) 1082 | .then(v => v, err => { 1083 | if (!err.dapReconnect) return Promise.reject(err) 1084 | console.log("re-connecting, ", err.message) 1085 | return getDeviceAsync(msg.path) 1086 | .then(dev => dev.reconnectAsync()) 1087 | .then(() => handleDevMsgAsync(msg)) 1088 | }) 1089 | } 1090 | } 1091 | 1092 | let code = [0x4770b403, 0xb500bc03, 0x219620ff, 0x47984b01, 0xbd00, 0x18451] 1093 | 1094 | function logMachineState(lbl: string) { 1095 | return (s: MachineState) => { 1096 | //console.log(machineStateToString(s).replace(/^/gm, lbl + ": ")) 1097 | return s 1098 | } 1099 | } 1100 | 1101 | function main() { 1102 | let mydev = getMbedDevices()[0] 1103 | let d = new Device(mydev.path) 1104 | let st: MachineState; 1105 | d.initAsync() 1106 | .then(() => d.haltAsync()) 1107 | .then(() => d.snapshotHexAsync()) 1108 | .then(h => { 1109 | require("fs").writeFileSync("microbit.hex", h) 1110 | process.exit(0) 1111 | }) 1112 | .then(() => d.snapshotMachineStateAsync()) 1113 | //.then(logMachineState("init")) 1114 | .then(s => st = s) 1115 | .then(() => d.executeCodeAsync(code, [0xbeef, 0xf00, 0xf00d0, 0xffff00])) 1116 | //.then(() => Promise.delay(100)) 1117 | //.then(() => d.haltAsync()) 1118 | .then(() => d.waitForHaltAsync()) 1119 | .then(() => d.snapshotMachineStateAsync()) 1120 | .then(logMachineState("final")) 1121 | .then(st => { console.log(hex(st.stack[0])) }) 1122 | .then(() => d.restoreMachineState(st)) 1123 | .then(() => d.resumeAsync()) 1124 | .then(() => process.exit(0)) 1125 | /* 1126 | .then(() => d.haltAsync()) 1127 | .then(() => d.readStackAsync()) 1128 | .then(arr => { 1129 | for (let i = 0; i < arr.length; ++i) 1130 | console.log(i, hex(arr[i])) 1131 | }) 1132 | .then(timeAsync("readmem", () => d.readBlockAsync(0x20000000, 16 / 4 * 1024))) 1133 | .then(v => console.log(v.length, v)) 1134 | */ 1135 | /* 1136 | .then(() => promiseIterAsync(range(16), k => 1137 | d.readCpuRegisterAsync(k) 1138 | .then(v => console.log(`r${k} = ${hex(v)}`)))) 1139 | */ 1140 | } 1141 | 1142 | if (require.main === module) { 1143 | main(); 1144 | } 1145 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "built", 6 | "rootDir": "src", 7 | "noImplicitAny": true, 8 | "declaration": true, 9 | "sourceMap": false 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "built", 14 | "typings" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bluebird": "registry:npm/bluebird#3.3.4+20160531224558" 4 | }, 5 | "globalDependencies": { 6 | "node": "registry:dt/node#6.0.0+20160621231320" 7 | } 8 | } 9 | --------------------------------------------------------------------------------