Files
mixly3-server/mixly/common/modules/web-modules/microbit/dap-wrapper.js
2026-01-24 16:12:04 +08:00

528 lines
17 KiB
JavaScript

(() => {
/**
* (c) 2021, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
// https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/constants.ts
// https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/constants.ts
// CRA's build tooling doesn't support const enums so we've converted them to prefixed constants here.
// If we move this to a separate library then we can replace them.
// In the meantime we should prune the list below to what we actually use.
// FICR Registers
const FICR = {
CODEPAGESIZE: 0x10000000 | 0x10,
CODESIZE: 0x10000000 | 0x14,
};
const DapCmd = {
DAP_INFO: 0x00,
DAP_CONNECT: 0x02,
DAP_DISCONNECT: 0x03,
DAP_TRANSFER: 0x05,
DAP_TRANSFER_BLOCK: 0x06,
// Many more.
};
const Csw = {
CSW_SIZE: 0x00000007,
CSW_SIZE32: 0x00000002,
CSW_ADDRINC: 0x00000030,
CSW_SADDRINC: 0x00000010,
CSW_DBGSTAT: 0x00000040,
CSW_HPROT: 0x02000000,
CSW_MSTRDBG: 0x20000000,
CSW_RESERVED: 0x01000000,
CSW_VALUE: -1, // see below
// Many more.
};
Csw.CSW_VALUE = Csw.CSW_RESERVED | Csw.CSW_MSTRDBG | Csw.CSW_HPROT | Csw.CSW_DBGSTAT | Csw.CSW_SADDRINC;
const DapVal = {
AP_ACC: 1 << 0,
READ: 1 << 1,
WRITE: 0 << 1,
// More.
};
const ApReg = {
CSW: 0x00,
TAR: 0x04,
DRW: 0x0c,
// More.
};
const CortexSpecialReg = {
// Debug Exception and Monitor Control Register
DEMCR: 0xe000edfc,
// DWTENA in armv6 architecture reference manual
DEMCR_VC_CORERESET: 1 << 0,
// CPUID Register
CPUID: 0xe000ed00,
// Debug Halting Control and Status Register
DHCSR: 0xe000edf0,
S_RESET_ST: 1 << 25,
NVIC_AIRCR: 0xe000ed0c,
NVIC_AIRCR_VECTKEY: 0x5fa << 16,
NVIC_AIRCR_SYSRESETREQ: 1 << 2,
// Many more.
};
const CoreRegister = {
SP: 13,
LR: 14,
PC: 15,
};
// Returns a representation of an Access Port Register.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/util.ts#L63
const apReg = (r, mode) => {
const v = r | mode | DapVal.AP_ACC;
return 4 + ((v & 0x0c) >> 2);
};
// Returns a code representing a request to read/write a certain register.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/util.ts#L92
const regRequest = (regId, isWrite = false) => {
let request = !isWrite ? 1 << 1 /* READ */ : 0 << 1; /* WRITE */
if (regId < 4) {
request |= 0 << 0 /* DP_ACC */;
} else {
request |= 1 << 0 /* AP_ACC */;
}
request |= (regId & 3) << 2;
return request;
};
const bufferConcat = (bufs) => {
let len = 0;
for (const b of bufs) {
len += b.length;
}
const r = new Uint8Array(len);
len = 0;
for (const b of bufs) {
r.set(b, len);
len += b.length;
}
return r;
};
class DAPWrapper {
constructor(device, logging) {
this.device = device;
this.logging = logging;
this.initialConnectionComplete = true;
this.loggedBoardSerialInfo = null;
this._pageSize = 0;
this._numPages = 0;
this.transport = new DAPjs.WebUSB(this.device);
this.daplink = new DAPjs.DAPLink(this.transport);
this.cortexM = new DAPjs.CortexM(this.transport);
}
log(v) {
//console.log(v);
}
/**
* The page size. Throws if we've not connected.
*/
pageSize() {
if (this._pageSize === undefined) {
throw new Error("pageSize not defined until connected");
}
return this._pageSize;
}
/**
* The number of pages. Throws if we've not connected.
*/
numPages() {
if (this._numPages === undefined) {
throw new Error("numPages not defined until connected");
}
return this._numPages;
}
boardSerialInfo() {
return BoardSerialInfo.parse(
this.device,
this.logging.log.bind(this.logging)
);
}
async connectAsync() {
await this.daplink.connect();
await this.daplink.setSerialBaudrate(115200);
await this.cortexM.connect();
this.logging.event({
type: "WebUSB-info",
message: "connected",
});
const serialInfo = this.boardSerialInfo();
this.log(`Detected board ID ${serialInfo.id}`);
if (
!this.loggedBoardSerialInfo ||
!this.loggedBoardSerialInfo.eq(this.boardSerialInfo())
) {
this.loggedBoardSerialInfo = this.boardSerialInfo();
this.logging.event({
type: "WebUSB-info",
message: "board-id/" + this.boardSerialInfo().id,
});
this.logging.event({
type: "WebUSB-info",
message:
"board-family-hic/" +
this.boardSerialInfo().familyId +
this.boardSerialInfo().hic,
});
}
this._pageSize = await this.cortexM.readMem32(FICR.CODEPAGESIZE);
this._numPages = await this.cortexM.readMem32(FICR.CODESIZE);
}
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L119
async reconnectAsync() {
if (this.initialConnectionComplete) {
await this.disconnectAsync();
this.transport = new DAPjs.WebUSB(this.device);
this.daplink = new DAPjs.DAPLink(this.transport);
this.cortexM = new DAPjs.CortexM(this.transport);
} else {
this.initialConnectionComplete = true;
}
await this.daplink.connect();
await this.cortexM.connect();
this.logging.event({
type: "WebUSB-info",
message: "connected",
});
const serialInfo = this.boardSerialInfo();
this.log(`Detected board ID ${serialInfo.id}`);
if (
!this.loggedBoardSerialInfo ||
!this.loggedBoardSerialInfo.eq(this.boardSerialInfo())
) {
this.loggedBoardSerialInfo = this.boardSerialInfo();
this.logging.event({
type: "WebUSB-info",
message: "board-id/" + this.boardSerialInfo().id,
});
this.logging.event({
type: "WebUSB-info",
message:
"board-family-hic/" +
this.boardSerialInfo().familyId +
this.boardSerialInfo().hic,
});
}
this._pageSize = await this.cortexM.readMem32(FICR.CODEPAGESIZE);
this._numPages = await this.cortexM.readMem32(FICR.CODESIZE);
}
async startSerial(listener) {
const currentBaud = await this.daplink.getSerialBaudrate();
if (currentBaud !== 115200) {
// Changing the baud rate causes a micro:bit reset, so only do it if necessary
await this.daplink.setSerialBaudrate(115200);
}
this.daplink.on(DAPjs.DAPLink.EVENT_SERIAL_DATA, listener);
await this.daplink.startSerialRead(1);
}
stopSerial(listener) {
this.daplink.stopSerialRead();
this.daplink.removeListener(DAPjs.DAPLink.EVENT_SERIAL_DATA, listener);
}
async disconnectAsync() {
if (this.device.opened && this.transport.interfaceNumber !== undefined) {
return this.daplink.disconnect();
}
}
// Send a packet to the micro:bit directly via WebUSB and return the response.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/transport/cmsis_dap.ts#L161
async send(packet) {
const array = Uint8Array.from(packet);
await this.transport.write(array.buffer);
const response = await this.transport.read();
return new Uint8Array(response.buffer);
}
// Send a command along with relevant data to the micro:bit directly via WebUSB and handle the response.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/transport/cmsis_dap.ts#L74
async cmdNums(
op,
data
) {
data.unshift(op);
const buf = await this.send(data);
if (buf[0] !== op) {
throw new Error(`Bad response for ${op} -> ${buf[0]}`);
}
switch (op) {
case DapCmd.DAP_CONNECT:
case DapCmd.DAP_INFO:
case DapCmd.DAP_TRANSFER:
case DapCmd.DAP_TRANSFER_BLOCK:
break;
default:
if (buf[1] !== 0) {
throw new Error(`Bad status for ${op} -> ${buf[1]}`);
}
}
return buf;
}
// Read a certain register a specified amount of times.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/dap.ts#L117
async readRegRepeat(regId, cnt) {
const request = regRequest(regId);
const sendargs = [0, cnt];
for (let i = 0; i < cnt; ++i) {
sendargs.push(request);
}
// Transfer the read requests to the micro:bit and retrieve the data read.
const buf = await this.cmdNums(DapCmd.DAP_TRANSFER, sendargs);
if (buf[1] !== cnt) {
throw new Error("(many) Bad #trans " + buf[1]);
} else if (buf[2] !== 1) {
throw new Error("(many) Bad transfer status " + buf[2]);
}
return buf.subarray(3, 3 + cnt * 4);
}
// Write to a certain register a specified amount of data.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/dap.ts#L138
async writeRegRepeat(
regId,
data
) {
const request = regRequest(regId, true);
const sendargs = [0, data.length, 0, request];
data.forEach((d) => {
// separate d into bytes
sendargs.push(
d & 0xff,
(d >> 8) & 0xff,
(d >> 16) & 0xff,
(d >> 24) & 0xff
);
});
// Transfer the write requests to the micro:bit and retrieve the response status.
const buf = await this.cmdNums(DapCmd.DAP_TRANSFER_BLOCK, sendargs);
if (buf[3] !== 1) {
throw new Error("(many-wr) Bad transfer status " + buf[2]);
}
}
// Core functionality reading a block of data from micro:bit RAM at a specified address.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/memory/memory.ts#L181
async readBlockCore(
addr,
words
) {
// Set up CMSIS-DAP to read/write from/to the RAM address addr using the register
// ApReg.DRW to write to or read from.
await this.cortexM.writeAP(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32);
await this.cortexM.writeAP(ApReg.TAR, addr);
let lastSize = words % 15;
if (lastSize === 0) {
lastSize = 15;
}
const blocks = [];
for (let i = 0; i < Math.ceil(words / 15); i++) {
const b = await this.readRegRepeat(
apReg(ApReg.DRW, DapVal.READ),
i === blocks.length - 1 ? lastSize : 15
);
blocks.push(b);
}
return bufferConcat(blocks).subarray(0, words * 4);
}
// Core functionality writing a block of data to micro:bit RAM at a specified address.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/memory/memory.ts#L205
async writeBlockCore(
addr,
words
) {
try {
// Set up CMSIS-DAP to read/write from/to the RAM address addr using the register ApReg.DRW to write to or read from.
await this.cortexM.writeAP(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32);
await this.cortexM.writeAP(ApReg.TAR, addr);
await this.writeRegRepeat(apReg(ApReg.DRW, DapVal.WRITE), words);
} catch (e) {
if (e.dapWait) {
// Retry after a delay if required.
this.log(`Transfer wait, write block`);
await new Promise((resolve) => setTimeout(resolve, 100));
return await this.writeBlockCore(addr, words);
} else {
throw e;
}
}
}
// Reads a block of data from micro:bit RAM at a specified address.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/memory/memory.ts#L143
async readBlockAsync(addr, words) {
const bufs = [];
const end = addr + words * 4;
let ptr = addr;
// Read a single page at a time.
while (ptr < end) {
let nextptr = ptr + this.pageSize();
if (ptr === addr) {
nextptr &= ~(this.pageSize() - 1);
}
const len = Math.min(nextptr - ptr, end - ptr);
bufs.push(await this.readBlockCore(ptr, len >> 2));
ptr = nextptr;
}
const result = bufferConcat(bufs);
return result.subarray(0, words * 4);
}
// Writes a block of data to micro:bit RAM at a specified address.
async writeBlockAsync(address, data) {
let payloadSize = this.transport.packetSize - 8;
if (data.buffer.byteLength > payloadSize) {
let start = 0;
let end = payloadSize;
// Split write up into smaller writes whose data can each be held in a single packet.
while (start !== end) {
let temp = new Uint32Array(data.buffer.slice(start, end));
await this.writeBlockCore(address + start, temp);
start = end;
end = Math.min(data.buffer.byteLength, end + payloadSize);
}
} else {
await this.writeBlockCore(address, data);
}
}
// Execute code at a certain address with specified values in the registers.
// Waits for execution to halt.
async executeAsync(address, code, sp, pc, lr, registers) {
if (registers.length > 12) {
throw new Error(`Only 12 general purpose registers but got ${registers.length} values`);
}
await this.cortexM.halt(true);
await this.writeBlockAsync(address, code);
await this.cortexM.writeCoreRegister(CoreRegister.PC, pc);
await this.cortexM.writeCoreRegister(CoreRegister.LR, lr);
await this.cortexM.writeCoreRegister(CoreRegister.SP, sp);
for (var i = 0; i < registers.length; ++i) {
await this.cortexM.writeCoreRegister(i, registers[i]);
}
await this.cortexM.resume(true);
return this.waitForHalt();
}
// Checks whether the micro:bit has halted or timeout has been reached.
// Recurses otherwise.
async waitForHaltCore(halted, deadline) {
if (new Date().getTime() > deadline) {
throw new Error("timeout");
}
if (!halted) {
const isHalted = await this.cortexM.isHalted();
// NB this is a Promise so no stack risk.
return this.waitForHaltCore(isHalted, deadline);
}
}
// Initial function to call to wait for the micro:bit halt.
async waitForHalt(timeToWait = 10000) {
const deadline = new Date().getTime() + timeToWait;
return this.waitForHaltCore(false, deadline);
}
// Resets the micro:bit in software by writing to NVIC_AIRCR.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/cortex.ts#L347
async softwareReset() {
await this.cortexM.writeMem32(
CortexSpecialReg.NVIC_AIRCR,
CortexSpecialReg.NVIC_AIRCR_VECTKEY |
CortexSpecialReg.NVIC_AIRCR_SYSRESETREQ
);
// wait for the system to come out of reset
let dhcsr = await this.cortexM.readMem32(CortexSpecialReg.DHCSR);
while ((dhcsr & CortexSpecialReg.S_RESET_ST) !== 0) {
dhcsr = await this.cortexM.readMem32(CortexSpecialReg.DHCSR);
}
}
// Reset the micro:bit, possibly halting the core on reset.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/cortex.ts#L248
async reset(halt = false) {
if (halt) {
await this.cortexM.halt(true);
// VC_CORERESET causes the core to halt on reset.
const demcr = await this.cortexM.readMem32(CortexSpecialReg.DEMCR);
await this.cortexM.writeMem32(
CortexSpecialReg.DEMCR,
CortexSpecialReg.DEMCR | CortexSpecialReg.DEMCR_VC_CORERESET
);
await this.softwareReset();
await this.waitForHalt();
// Unset the VC_CORERESET bit
await this.cortexM.writeMem32(CortexSpecialReg.DEMCR, demcr);
} else {
await this.softwareReset();
}
}
}
window.DAPWrapper = DAPWrapper;
})();