From 9ff7e3a216f26f592ce366e5e77ad3d7582e383a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Ctefan=20S=CC=8Cimek?= Date: Mon, 7 Oct 2024 16:52:46 +0200 Subject: [PATCH] feat: add SWO support for BMP --- binary_modules/package-lock.json | 55 ++++++++++- binary_modules/package.json | 3 +- debug_attributes.md | 4 +- package-lock.json | 51 ++++++++++ package.json | 5 +- src/bmp.ts | 27 ++++-- src/frontend/configprovider.ts | 6 -- src/frontend/extension.ts | 5 + src/frontend/swo/sources/usb.ts | 158 +++++++++++++++++++++++++++++++ webpack.config.js | 6 +- 10 files changed, 299 insertions(+), 21 deletions(-) create mode 100644 src/frontend/swo/sources/usb.ts diff --git a/binary_modules/package-lock.json b/binary_modules/package-lock.json index 33314022..68b3781c 100644 --- a/binary_modules/package-lock.json +++ b/binary_modules/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "serialport": "^10.4.0" + "serialport": "^10.4.0", + "usb": "^2.14.0" }, "devDependencies": { "electron-rebuild": "^3.2.8" @@ -335,6 +336,12 @@ "@types/node": "*" } }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", + "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2044,6 +2051,30 @@ "node": ">= 10.0.0" } }, + "node_modules/usb": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.14.0.tgz", + "integrity": "sha512-I3lzVOH21BsO6qPYvx1C7Ji08lbuM0qmsEtNGAphqlhNME5cz/vExY+jIXZl+HQIRybI/sTxdyLab5tALsL69w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=12.22.0 <13.0 || >=14.17.0" + } + }, + "node_modules/usb/node_modules/node-addon-api": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", + "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2361,6 +2392,11 @@ "@types/node": "*" } }, + "@types/w3c-web-usb": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", + "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3637,6 +3673,23 @@ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true }, + "usb": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.14.0.tgz", + "integrity": "sha512-I3lzVOH21BsO6qPYvx1C7Ji08lbuM0qmsEtNGAphqlhNME5cz/vExY+jIXZl+HQIRybI/sTxdyLab5tALsL69w==", + "requires": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.5.0" + }, + "dependencies": { + "node-addon-api": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", + "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==" + } + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/binary_modules/package.json b/binary_modules/package.json index cfd83917..e18d1d61 100644 --- a/binary_modules/package.json +++ b/binary_modules/package.json @@ -11,6 +11,7 @@ "electron-rebuild": "^3.2.8" }, "dependencies": { - "serialport": "^10.4.0" + "serialport": "^10.4.0", + "usb": "^2.14.0" } } diff --git a/debug_attributes.md b/debug_attributes.md index 30078505..a3ac46f1 100644 --- a/debug_attributes.md +++ b/debug_attributes.md @@ -88,8 +88,8 @@ If the type is marked as `{...}` it means that it is a complex item can have mul | swoConfig
.enabled | boolean | Both | Enable SWO decoding. | | swoConfig
.source | string | Both | Source for SWO data. Can either be "probe" to get directly from debug probe, or a serial port device to use a serial port external to the debug probe. | | swoConfig
.swoFrequency | number | Both | SWO frequency in Hz. | -| swoConfig
.swoPath | string | Both | Path name when source is "file" or "serial". Typically a /path-name or a serial-port-name | -| swoConfig
.swoPort | string | Both | When server is "external" && source is "socket", port to connect to. Format [host:]port | +| swoConfig
.swoPath | string | Both | Path name when source is "file" or "serial", device name regex match when source is "probe" for BMP. Typically a /path-name or a serial-port-name | +| swoConfig
.swoPort | string | Both | When server is "external" && source is "socket", port to connect to. Format [host:]port. For BMP, specifies the regex match of the USB interface contianing raw SWO data. | | symbolFiles | object[] | Both | Array of ELF files to load symbols from instead of the executable file. Each item in the array cab be a string or an object. Program information is ignored (see `loadFiles`). Can be an empty list to specify none. If this property does not exist, then the executable is used for symbols | | targetId | string | number | Both | On BMP this is the ID number that should be passed to the attach command (defaults to 1); for PyOCD this is the target identifier (only needed for custom hardware) | | targetProcessor | number | Both | The processor you want to debug. Zero based integer index. Must be less than 'numberOfProcessors' | diff --git a/package-lock.json b/package-lock.json index 9959da7d..cd1d7919 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "stream-json": "^1.7.3", "tmp": "^0.2.1", "universal-analytics": "^0.5.3", + "usb": "^2.14.0", "uuid": "^8.3.2", "vscode-jsonrpc": "^6.0.0" }, @@ -530,6 +531,12 @@ "integrity": "sha512-CQcY3+Fe5hNewHnOEAVYj4dd1do/QHliXaknAEYSXx2KEHUzFibDZSKptCon+HPgK55xx20pR+PBJjf0MomnBA==", "dev": true }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", + "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==", + "license": "MIT" + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -2661,6 +2668,15 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", + "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-gyp-build": { "version": "4.8.2", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", @@ -3792,6 +3808,21 @@ "punycode": "^2.1.0" } }, + "node_modules/usb": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.14.0.tgz", + "integrity": "sha512-I3lzVOH21BsO6qPYvx1C7Ji08lbuM0qmsEtNGAphqlhNME5cz/vExY+jIXZl+HQIRybI/sTxdyLab5tALsL69w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=12.22.0 <13.0 || >=14.17.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4467,6 +4498,11 @@ "integrity": "sha512-CQcY3+Fe5hNewHnOEAVYj4dd1do/QHliXaknAEYSXx2KEHUzFibDZSKptCon+HPgK55xx20pR+PBJjf0MomnBA==", "dev": true }, + "@types/w3c-web-usb": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", + "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==" + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -6060,6 +6096,11 @@ "semver": "^7.3.5" } }, + "node-addon-api": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", + "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==" + }, "node-gyp-build": { "version": "4.8.2", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", @@ -6881,6 +6922,16 @@ "punycode": "^2.1.0" } }, + "usb": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.14.0.tgz", + "integrity": "sha512-I3lzVOH21BsO6qPYvx1C7Ji08lbuM0qmsEtNGAphqlhNME5cz/vExY+jIXZl+HQIRybI/sTxdyLab5tALsL69w==", + "requires": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.5.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 22c8bbec..074dfc7e 100644 --- a/package.json +++ b/package.json @@ -2493,12 +2493,12 @@ "swoPath": { "type": "string", "default": "", - "description": "Path name when source is \"file\" or \"serial\". Typically a /path-name or a serial-port-name" + "description": "Path name when source is \"file\" or \"serial\", device name regex match when source is \"probe\" for BMP. Typically a /path-name or a serial-port-name" }, "swoPort": { "type": "string", "default": "", - "description": "When server is \"external\" && source is \"socket\", port to connect to. Format [host:]port" + "description": "When server is \"external\" && source is \"socket\", port to connect to. Format [host:]port. For BMP, specifies the regex match of the USB interface contianing raw SWO data." }, "decoders": { "description": "SWO Decoder Configuration", @@ -3009,6 +3009,7 @@ "stream-json": "^1.7.3", "tmp": "^0.2.1", "universal-analytics": "^0.5.3", + "usb": "^2.14.0", "uuid": "^8.3.2", "vscode-jsonrpc": "^6.0.0" }, diff --git a/src/bmp.ts b/src/bmp.ts index 6b6bf34a..dfb72f0b 100644 --- a/src/bmp.ts +++ b/src/bmp.ts @@ -109,6 +109,10 @@ export class BMPServerController extends EventEmitter implements GDBServerContro ); commands.push(this.args.swoConfig.profile ? 'EnablePCSample' : 'DisablePCSample'); + + if (this.args.swoConfig.source === 'probe') { + commands.push('monitor traceswo'); + } return commands.map((c) => `interpreter-exec console "${c}"`); } @@ -131,13 +135,22 @@ export class BMPServerController extends EventEmitter implements GDBServerContro public serverLaunchStarted(): void {} public serverLaunchCompleted(): void { - if (this.args.swoConfig.enabled && this.args.swoConfig.source !== 'probe') { - this.emit('event', new SWOConfigureEvent({ - type: 'serial', - args: this.args, - device: this.args.swoConfig.source, - baudRate: this.args.swoConfig.swoFrequency - })); + if (this.args.swoConfig.enabled) { + if (this.args.swoConfig.source === 'probe') { + this.emit('event', new SWOConfigureEvent({ + type: 'usb', + args: this.args, + device: this.args.swoConfig.swoPath || 'Black Magic Probe', + port: this.args.swoConfig.swoPort || 'Black Magic Trace Capture' + })); + } else { + this.emit('event', new SWOConfigureEvent({ + type: 'serial', + args: this.args, + device: this.args.swoConfig.source, + baudRate: this.args.swoConfig.swoFrequency + })); + } } } diff --git a/src/frontend/configprovider.ts b/src/frontend/configprovider.ts index 8b359f2a..538c955a 100644 --- a/src/frontend/configprovider.ts +++ b/src/frontend/configprovider.ts @@ -622,12 +622,6 @@ export class CortexDebugConfigurationProvider implements vscode.DebugConfigurati return 'The Black Magic Probe GDB Server does not have support for the rtos option.'; } - if (config.swoConfig.enabled && config.swoConfig.source === 'probe') { - vscode.window.showWarningMessage('SWO support is not available from the probe when using the BMP GDB server. Disabling SWO.'); - config.swoConfig = { enabled: false, ports: [], cpuFrequency: 0, swoFrequency: 0 }; - config.graphConfig = []; - } - return null; } diff --git a/src/frontend/extension.ts b/src/frontend/extension.ts index 9b389bcd..f445f917 100644 --- a/src/frontend/extension.ts +++ b/src/frontend/extension.ts @@ -16,6 +16,7 @@ import { JLinkSocketRTTSource, SocketRTTSource, SocketSWOSource, PeMicroSocketSo import { FifoSWOSource } from './swo/sources/fifo'; import { FileSWOSource } from './swo/sources/file'; import { SerialSWOSource } from './swo/sources/serial'; +import { UsbSWOSource } from './swo/sources/usb'; import { SymbolInformation, SymbolScope } from '../symbols'; import { RTTTerminal } from './rtt_terminal'; import { GDBServerConsole } from './server_console'; @@ -763,6 +764,10 @@ export class CortexDebugExtension { mySession.swoSource = new SerialSWOSource(e.body.device, e.body.baudRate); Reporting.sendEvent('SWO', 'Source', 'Serial'); } + else if (e.body.type === 'usb') { + mySession.swoSource = new UsbSWOSource(e.body.device, e.body.port); + Reporting.sendEvent('SWO', 'Source', 'USB'); + } this.initializeSWO(e.session, e.body.args); } diff --git a/src/frontend/swo/sources/usb.ts b/src/frontend/swo/sources/usb.ts new file mode 100644 index 00000000..e94ba867 --- /dev/null +++ b/src/frontend/swo/sources/usb.ts @@ -0,0 +1,158 @@ +import { EventEmitter } from 'stream'; +import { SWORTTSource } from './common'; +import { promisify } from 'util'; +import * as vscode from 'vscode'; +import * as usb from 'usb'; + +/* + * NOTE: using legacy node-usb interface, because the modern + * WebUSB-compatible version doesn't contain a way to interrupt pending + * transfer, leading to problems with getting rid of the connection + */ + +export class UsbSWOSource extends EventEmitter implements SWORTTSource { + private dev?: usb.Device; + private iface?: usb.Interface; + private ep?: usb.InEndpoint; + + constructor( + private readonly device: string, + private readonly port: string + ) { + super(); + + this.start(); + } + + private async findDevice(): Promise< + | { + dev: usb.Device; + config: usb.ConfigDescriptor; + iface: usb.InterfaceDescriptor; + endpoint: usb.EndpointDescriptor; + productName: string + } + | undefined + > { + console.info('Looking for USB devices matching', this.device); + const devs = usb.getDeviceList(); + for (const dev of devs) { + dev.open(); + const { deviceDescriptor: dd } = dev; + const getStringDescriptor: (index: number) => Promise = + promisify(dev.getStringDescriptor).bind(dev); + const productName = await getStringDescriptor(dd.iProduct); + if (productName.match(this.device)) { + console.info( + 'Found device', + productName, + 'VID', + dd.idVendor.toString(16), + 'PID', + dd.idProduct.toString(16), + 'Serial', + await getStringDescriptor(dd.iSerialNumber) + ); + + for (const cfg of dev.allConfigDescriptors) { + for (const iface of cfg.interfaces) { + for (const alt of iface) { + const interfaceName = await getStringDescriptor(alt.iInterface); + if (interfaceName?.match(this.port)) { + for (const ep of alt.endpoints) { + if ((ep.bmAttributes & 3) === usb.usb.LIBUSB_TRANSFER_TYPE_BULK && + ep.bEndpointAddress & usb.usb.LIBUSB_ENDPOINT_IN) { + console.info( + 'Matched config', + cfg.bConfigurationValue, + 'interface', + alt.bInterfaceNumber, + 'alternate', + alt.bAlternateSetting, + 'endpoint', + ep.bEndpointAddress + ); + return { + dev, + config: cfg, + iface: alt, + endpoint: ep, + productName + }; + } + } + } + } + } + } + + console.warn('Couldn\'t match interface named', this.port); + } + dev.close(); + } + console.warn('Matching device not found'); + return undefined; + } + + public async start() { + const { dev, config, iface, endpoint, productName } = (await this.findDevice()) ?? {}; + if (!dev) { + vscode.window.showErrorMessage( + `Couldn't find a device matching '${this.device}' with interface '${this.port}` + ); + return; + } + + console.debug('Connecting to', productName); + await dev.open(); + this.dev = dev; + console.debug('Selecting configuration', config.bConfigurationValue); + await promisify(dev.setConfiguration).bind(dev)(config.bConfigurationValue); + console.debug('Claiming interface', iface.bInterfaceNumber); + this.iface = dev.interface(iface.bInterfaceNumber); + this.iface.claim(); + if (iface.bAlternateSetting) { + console.debug('Selecting alternate', iface.bAlternateSetting); + await dev.interface(iface.iInterface).setAltSettingAsync(iface.bAlternateSetting); + } + console.debug('Reading from endpoint', endpoint.bEndpointAddress); + + this.ep = this.iface.endpoint(endpoint.bEndpointAddress) as usb.InEndpoint; + this.ep.on('data', (buffer: Buffer) => { + console.debug(buffer.length, 'bytes received'); + this.emit('data', buffer); + }); + this.ep.on('error', (error) => { + console.error('Unexpected polling error', error); + }); + this.ep.startPoll(); + + this.emit('connected'); + } + + public get connected() { + return !!this.ep; + } + + public async dispose() { + if (this.ep) { + console.debug('Stopping polling...'); + await promisify(this.ep.stopPoll).bind(this.ep)(); + this.ep = undefined; + console.debug('Polling stopped'); + } + if (this.iface) { + console.debug('Releasing interface...'); + await this.iface.releaseAsync(); + this.iface = undefined; + console.debug('Interface released'); + } + if (this.dev) { + console.debug('Closing device...'); + this.dev.close(); + this.dev = undefined; + console.debug('Device closed'); + } + this.emit('disconnected'); + } +} diff --git a/webpack.config.js b/webpack.config.js index 3cba17b5..e62f4867 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,7 +25,8 @@ const extensionConfig = { devtool: 'source-map', externals: { vscode: 'vscode', - serialport: 'serialport' + serialport: 'serialport', + usb: 'usb' }, resolve: { extensions: ['.ts', '.js'] @@ -57,7 +58,8 @@ const adapterConfig = { devtool: 'source-map', externals: { vscode: 'vscode', - serialport: 'serialport' + serialport: 'serialport', + usb: 'usb', }, resolve: { extensions: ['.ts', '.js']