diff --git a/configs/core-peripherals/README.md b/configs/core-peripherals/README.md new file mode 100644 index 00000000..b65be4a9 --- /dev/null +++ b/configs/core-peripherals/README.md @@ -0,0 +1,80 @@ +# Core Peripherals Index (`core-peripherals-index.yml`) + +This file lists SCVD files and optional conditions that decide whether each file is loaded for +a CPU connection. The input for the condition is based on the selected core and its available +features as described under `system-resources`>`processors` in the `*.cbuild-run.yml` file. + +## File format + +```yaml +core-peripherals: + - file: Nested_Vectored_Interrupt_Controller_M33_with_TZ.scvd + cpu-type: Cortex-M33 + cpu-features: + trustzone: present + info: Example for a CPU specific NVIC description. + + - file: My_M55_M85_CorePeripheral.scvd + cpu-type: + - Cortex-M55 + - Cortex-M85 + cpu-features: + mve: fp + dsp: present + fpu: dp + info: Example for a core peripheral description valid for multiple CPU types with available CPU features. + + - file: Memory_Protection_Unit.scvd + cpu-type: "*" + cpu-features: + mpu: present + fpu: "*" + info: Example for wildcard usage +``` + +- `file` (required): Path to an SCVD file, relative to this folder (`configs/core-peripherals`). +- `cpu-type` (optional): String or list of strings matching processor `core` from `system-resources`>`processors` in `*.cbuild-run.yml`. +- `cpu-features` (optional): Key/value map matched against processor properties. +- `info` (optional): Free-text description. + +## Filtering behavior + +### Processor selection + +- If the core connection's `pname` matches a child node of `system-resources`>`processors`, then that processor is used. +- If `pname` is missing, then a single-core system is assumed. Hence, the first processor in the list is used. +- If no processors are available, the collector treats the processor as unknown. In this case only entries without `cpu-type`/`cpu-features` +constraints, those using the "*" wildcard, and those using value `none` will match; entries requiring a specific `cpu-type` or specific feature values are excluded. + +### `cpu-type` matching + +- If omitted or set to the `"*"` wildcard, then entry is valid for all CPU types. +- Otherwise, SCVD files are loaded if the connection's core matches one of the given `core` values. +Matching is case-insensitive. + +### `cpu-features` matching + +- If omitted, no feature constraints are applied. +- All listed feature conditions must match (`AND` logic). +- Feature must exist in the selected processor object and values must match (case-insensitive). +- Feature value `"*"` matches any value for that key that indicates presence of the feature. + +## Feature keys and allowed values + +Use feature keys that can appear on a processor entry in `system-resources`>`processors` of `*.cbuild-run.yml`. + +- `fpu`: `sp`, `dp`, `none` +- `mpu`: `present`, `none` +- `dsp`: `present`, `none` +- `trustzone`: `present`, `none` +- `mve`: `int`, `fp`, `none` +- `pacbti`: `present`, `none` +- `endian`: `little`, `big`, `configurable` + +Additional notes for filter behavior: + +- `cpu-features` keys are matched exactly by key name (for example `mpu`, not `MPU`). +- Feature value comparison is case-insensitive after converting values to strings. +- If a key is listed in `cpu-features` but missing on the selected processor, the entry only matches if it has the value `none`. +This is to reflect that a missing processor feature usually means it is not implemented. +- Use `"*"` as a feature value to accept any value for that key that indicates presence of the feature. diff --git a/configs/core-peripherals/core-peripherals-index.schema.json b/configs/core-peripherals/core-peripherals-index.schema.json new file mode 100644 index 00000000..ea18f842 --- /dev/null +++ b/configs/core-peripherals/core-peripherals-index.schema.json @@ -0,0 +1,75 @@ +{ + "$comment": "Index of core peripheral SCVD files and conditions when to use them.", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/Open-CMSIS-Pack/vscode-cmsis-debugger/configs/core-peripherals/core-peripherals-index.schema.json", + "title": "Core Peripherals index", + "description": "Index listing core peripheral SCVD description files and CPU type/feature conditions under which each file is active.", + "version": "0.1.0", + "type": "object", + "properties": { + "core-peripherals": { + "$ref": "#/definitions/CorePeripherals" + } + }, + "required": ["core-peripherals"], + "additionalProperties": false, + "definitions": { + "CorePeripherals": { + "type": "array", + "description": "Array of core peripheral entries (SCVD files with optional activation conditions).", + "items": { + "$ref": "#/definitions/CorePeripheralEntry" + }, + "minItems": 0, + "uniqueItems": true + }, + "CorePeripheralEntry": { + "type": "object", + "description": "Single SCVD description entry with optional CPU type and CPU feature conditions.", + "properties": { + "file": { + "$ref": "#/definitions/ScvdFileType" + }, + "cpu-type": { + "$ref": "#/definitions/CpuType" + }, + "cpu-features": { + "$ref": "#/definitions/CpuFeatures" + }, + "info": { + "type": "string", + "description": "Optional human-readable description." + } + }, + "required": ["file"], + "additionalProperties": false + }, + "ScvdFileType": { + "type": "string", + "description": "Path or identifier of the SCVD description file." + }, + "CpuType": { + "description": "Specifies one or more CPU types. '*' is allowed as wildcard and is the default if omitted.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "CpuFeatures": { + "type": "object", + "description": "Map of CPU features and their value. Each feature key may only appear once. '*' is allowed as wildcard and is the default if omitted.", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/configs/core-peripherals/core-peripherals-index.yml b/configs/core-peripherals/core-peripherals-index.yml new file mode 100644 index 00000000..ead1c461 --- /dev/null +++ b/configs/core-peripherals/core-peripherals-index.yml @@ -0,0 +1,11 @@ +core-peripherals: + - file: Memory_Protection_Unit.scvd + cpu-type: "*" + cpu-features: + mpu: present + - file: Nested_Vectored_Interrupt_Controller.scvd + cpu-type: "*" + - file: System_Config_and_Control.scvd + cpu-type: "*" + - file: System_Tick_Timer.scvd + cpu-type: "*" diff --git a/src/cbuild-run/cbuild-run-types.ts b/src/cbuild-run/cbuild-run-types.ts index 5fa7405d..82db6fd7 100644 --- a/src/cbuild-run/cbuild-run-types.ts +++ b/src/cbuild-run/cbuild-run-types.ts @@ -36,8 +36,26 @@ export interface MemoryType { 'from-pack'?: string; }; +// Preliminary type, to be implemented in CMSIS Toolbox first. +export interface ProcessorType { + core: string; + revision: string; + 'max-clock': number; + pname?: string; + punits?: number; + endian?: 'little' | 'big' | 'configurable'; + fpu?: 'sp' | 'dp' | 'none'; + mpu?: 'present' | 'none'; + dsp?: 'present' | 'none'; + trustzone?: 'present' | 'none'; + mve?: 'int' | 'fp' | 'none'; + cdecp?: number; + pacbti?: 'present' | 'none'; +} + export interface SystemResourcesType { memory?: MemoryType[]; + processors?: ProcessorType[]; }; export type SystemDescriptionTypeType = 'svd'|'scvd'; @@ -139,7 +157,7 @@ export interface PunitType { address: number; }; -export interface ProcessorType { +export interface TopologyProcessorType { pname?: string; punits?: PunitType[]; apid?: number; @@ -148,7 +166,7 @@ export interface ProcessorType { export interface DebugTopologyType { debugports?: DebugPortType[]; - processors?: ProcessorType[]; + processors?: TopologyProcessorType[]; swj?: boolean; dormant?: boolean; sdf?: string; diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index 00593d60..b72bed5c 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -86,7 +86,10 @@ export const debugSessionFactory = ( hasCbuildRun = true ): Session => { // Ensure same object returned for multiple calls to getCbuildRun. - const cbuildRunMock = hasCbuildRun ? { getScvdFilePaths: () => paths } : undefined; + const cbuildRunMock = hasCbuildRun ? { + getContents: jest.fn(), + getScvdFilePaths: () => paths + } : undefined; return { session: { id }, getCbuildRun: async () => cbuildRunMock, diff --git a/src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap b/src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap new file mode 100644 index 00000000..65ef0b01 --- /dev/null +++ b/src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`CorePeripheralsIndexReader can read index file 1`] = ` +{ + "core-peripherals": [ + { + "cpu-features": { + "mpu": "present", + }, + "cpu-type": "*", + "file": "Memory_Protection_Unit.scvd", + }, + { + "cpu-type": "*", + "file": "Nested_Vectored_Interrupt_Controller.scvd", + }, + { + "cpu-type": "*", + "file": "System_Config_and_Control.scvd", + }, + { + "cpu-type": "*", + "file": "System_Tick_Timer.scvd", + }, + ], +} +`; diff --git a/src/views/core-peripherals/core-peripherals-index-reader.test.ts b/src/views/core-peripherals/core-peripherals-index-reader.test.ts new file mode 100644 index 00000000..6d336d73 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-index-reader.test.ts @@ -0,0 +1,73 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; + +// Tests are executed with different working directory, so different input path needed. +const TEST_INDEX_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); +const TEST_INDEX_PATH = path.resolve(TEST_INDEX_BASE_PATH, 'core-peripherals-index.yml'); +const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/empty-index/core-peripherals-index.yml'); + +describe('CorePeripheralsIndexReader', () => { + + it('finds all core peripherals entries of index file in folder', async () => { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const filesInDir = await fs.promises.readdir(TEST_INDEX_BASE_PATH, { + encoding: 'buffer', + withFileTypes: true + }); + const scvdFilesInDir = filesInDir.filter(entry => entry.isFile() && entry.name.toString().toLowerCase().endsWith('.scvd')); + const scvdFilePathsInDir = scvdFilesInDir.map(file => path.resolve(TEST_INDEX_BASE_PATH, file.name.toString())); + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); + const indexEntries = indexReader.getCorePeripherals().map(entry => path.resolve(TEST_INDEX_BASE_PATH, entry.file)); + expect(indexEntries.length).toBe(scvdFilePathsInDir.length); + scvdFilePathsInDir.forEach(filePath => { + expect(indexEntries.includes(filePath)).toBe(true); + }); + }); + + it('can read index file', async () => { + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); + expect(indexReader.hasContents()).toBe(true); + const contents = indexReader.getContents(); + expect(contents).toMatchSnapshot(); + }); + + it('returns empty array if no core peripherals parsed', async () => { + const indexReader = new CorePeripheralsIndexReader(); + // Get peripherals without parsing file first, should return empty array. + expect(indexReader.hasContents()).toBe(false); + expect(indexReader.getContents()).toBeUndefined(); + expect(indexReader.getCorePeripherals()).toEqual([]); + }); + + it('throws for an empty index file', async () => { + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(EMPTY_INDEX_PATH)).rejects.toThrow('Invalid \'core-peripherals-index\' file'); + }); + + it('parses only once', async () => { + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); + // Clear spy calls and parse an empty file. It should not throw because it should not attempt to parse the file again. + await expect(indexReader.parse(EMPTY_INDEX_PATH)).resolves.not.toThrow(); + }); + +}); diff --git a/src/views/core-peripherals/core-peripherals-index-reader.ts b/src/views/core-peripherals/core-peripherals-index-reader.ts new file mode 100644 index 00000000..6a72915a --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-index-reader.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import * as path from 'path'; +import * as yaml from 'yaml'; +import { FileReader, VscodeFileReader } from '../../desktop/file-reader'; +import { CorePeripheralEntryType, CorePeripheralsType } from './core-peripherals-index-types'; + +export class CorePeripheralsIndexReader { + private isParsed = false; + private contents: CorePeripheralsType | undefined; + private filePath: string | undefined; + private directory: string | undefined; + + constructor(private reader: FileReader = new VscodeFileReader()) {} + + public hasContents(): boolean { + return !!this.contents; + } + + public getContents(): CorePeripheralsType | undefined { + return this.contents; + } + + public async parse(filePath: string): Promise { + if (this.isParsed) { + return; + } + this.filePath = filePath; + this.directory = path.dirname(this.filePath); + const fileContents = await this.reader.readFileToString(this.filePath); + this.contents = yaml.parse(fileContents) as CorePeripheralsType; + if (!this.contents) { + throw new Error(`Invalid 'core-peripherals-index' file: ${this.filePath}`); + } + this.isParsed = true; + } + + public getCorePeripherals(): CorePeripheralEntryType[] { + const corePeripherals = this.contents?.['core-peripherals']; + if (!corePeripherals) { + return []; + } + const resolvedPeripherals = corePeripherals.map( + entry => ({ + ...entry, + file: this.directory ? path.resolve(this.directory, entry.file) : entry.file + }) + ); + return resolvedPeripherals ?? []; + } +} diff --git a/src/views/core-peripherals/core-peripherals-index-types.ts b/src/views/core-peripherals/core-peripherals-index-types.ts new file mode 100644 index 00000000..71c317f2 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-index-types.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface CpuFeaturesType { + [feature: string]: string; +} + +export interface CorePeripheralEntryType { + file: string; + 'cpu-type'?: string | string[]; + 'cpu-features'?: CpuFeaturesType; + info?: string; +}; + +export interface CorePeripheralsType { + 'core-peripherals': CorePeripheralEntryType[]; +}; diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts index 3d31b7f0..6f18d20f 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -19,27 +19,133 @@ import { CorePeripheralsScvdCollector } from './core-peripherals-scvd-collector' import { gdbTargetDebugSessionFactory } from '../../debug-session/__test__/debug-session.factory'; const TEST_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); -const EXPECTED_CORE_PERIPHERAL_FILES = [ - 'Memory_Protection_Unit.scvd', +const EXPECTED_DEFAULT_CORE_PERIPHERAL_FILES = [ 'Nested_Vectored_Interrupt_Controller.scvd', 'System_Config_and_Control.scvd', 'System_Tick_Timer.scvd', ]; -const EXPECTED_CORE_PERIPHERAL_FILE_PATHS = EXPECTED_CORE_PERIPHERAL_FILES.map( +const EXPECTED_DEFAULT_CORE_PERIPHERAL_FILE_PATHS = EXPECTED_DEFAULT_CORE_PERIPHERAL_FILES.map( file => path.resolve(TEST_BASE_PATH, file) ); +const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/empty-index'); +const NO_PERIPHERALS_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/no-peripherals'); +const COMPLEX_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/complex-index'); +const TEST_PROCESSOR_M33_TZ_DP = { + core: 'Cortex-M33', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core0', + fpu: 'dp', + mpu: 'present', + trustzone: 'present', +}; + +const TEST_PROCESSOR_M33_NO_TZ_DP = { + core: 'Cortex-M33', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core1', + fpu: 'dp', + mpu: 'present', + trustzone: 'none', +}; + +const TEST_PROCESSOR_M55_SP = { + core: 'Cortex-M55', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core2', + fpu: 'sp', + mpu: 'present', + trustzone: 'present', +}; + +const TEST_PROCESSOR_M85 = { + core: 'Cortex-M85', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core3', + fpu: 'sp', + mpu: 'present', + trustzone: 'none', +}; + +const TEST_PROCESSOR_M0P = { + core: 'Cortex-M0+', + revision: 'r0p0', + 'max-clock': 100, +}; + +const TEST_PROCESSORS = [ + TEST_PROCESSOR_M33_TZ_DP, + TEST_PROCESSOR_M33_NO_TZ_DP, + TEST_PROCESSOR_M55_SP, + TEST_PROCESSOR_M85 +]; describe('CorePeripheralsScvdCollector', () => { - it('finds all expected SCVD files', async () => { + it('returns all SCVD files that have no conditions', async () => { const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(TEST_BASE_PATH); - const debugSession = gdbTargetDebugSessionFactory('TestSession', [], 'unknown'); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id'); const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); - expect(scvdFilePaths.length).toBe(EXPECTED_CORE_PERIPHERAL_FILE_PATHS.length); - EXPECTED_CORE_PERIPHERAL_FILE_PATHS.forEach(filePath => { + expect(scvdFilePaths.length).toBe(EXPECTED_DEFAULT_CORE_PERIPHERAL_FILE_PATHS.length); + EXPECTED_DEFAULT_CORE_PERIPHERAL_FILE_PATHS.forEach(filePath => { expect(scvdFilePaths.includes(filePath)).toBe(true); }); }); + it('returns no SCVD files for empty index file', async () => { + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(EMPTY_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id'); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual([]); + }); + + it('returns no SCVD files for index file without file entries', async () => { + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(NO_PERIPHERALS_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id'); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual([]); + }); + + it.each([ + { pname: 'core0', expected: [ 'TZ_MPU_Cortex-M33.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'core1', expected: [ 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'core2', expected: [ 'M55_M85_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_SP.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'core3', expected: [ 'M55_M85_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_SP.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + // No matching processor, load defaults without restrictions. + { pname: 'no-match', expected: [ 'System_Tick_Timer.scvd'] }, + // No pname info, should fallback to first entry which is M33 with TZ and DP + { pname: undefined, expected: [ 'TZ_MPU_Cortex-M33.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + ])('filters SCVD files as expected for complex index file and multi-core setup (pname: $pname)', async ({ pname, expected }) => { + const resolvedExpected = expected.map(file => path.resolve(COMPLEX_INDEX_PATH, file)); + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(COMPLEX_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id', [], 'unknown', pname); + const cbuildRunReader = await debugSession.getCbuildRun(); + (cbuildRunReader?.getContents as jest.Mock).mockReturnValue({ 'system-resources': { processors: TEST_PROCESSORS } }); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual(resolvedExpected); + }); + + it.each([ + // No matching processor, load defaults without restrictions. + { pname: 'no-match', expected: [ 'System_Tick_Timer.scvd' ] }, + { pname: undefined, expected: [ 'M0_M23_Nested_Vectored_Interrupt_Controller.scvd', 'System_Tick_Timer.scvd' ] }, + ])('filters SCVD files as expected for complex index file and single-core setup (pname: $pname)', async ({ pname, expected }) => { + const resolvedExpected = expected.map(file => path.resolve(COMPLEX_INDEX_PATH, file)); + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(COMPLEX_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id', [], 'unknown', pname); + const cbuildRunReader = await debugSession.getCbuildRun(); + (cbuildRunReader?.getContents as jest.Mock).mockReturnValue({ 'system-resources': { processors: [ TEST_PROCESSOR_M0P ] } }); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual(resolvedExpected); + }); + }); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index 1ae10464..12ebb63c 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -14,36 +14,123 @@ * limitations under the License. */ -import * as fs from 'fs'; import * as path from 'path'; import { GDBTargetDebugSession } from '../../debug-session'; import { ScvdCollector } from '../component-viewer/component-viewer-base'; import { componentViewerLogger } from '../../logger'; +import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; +import { CorePeripheralEntryType } from './core-peripherals-index-types'; +import { ProcessorType } from '../../cbuild-run'; // Relative to dist folder at runtime -const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); +const CORE_PERIPHERAL_SCVD_BASE = path.resolve(__dirname, '..', 'configs', 'core-peripherals'); +const FEATURE_NOT_PRESENT_VALUE = 'none'; export class CorePeripheralsScvdCollector implements ScvdCollector { - public constructor(private readonly basePath: string = CORE_PERIPHERAL_SCVD_BASE) {} + private indexFilePath: string; + private indexReader: CorePeripheralsIndexReader; - public async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { - const resolvedBasePath = path.resolve(this.basePath); - const filePaths = []; + public constructor(private readonly basePath: string = CORE_PERIPHERAL_SCVD_BASE) { + this.indexFilePath = path.resolve(this.basePath, 'core-peripherals-index.yml'); + this.indexReader = new CorePeripheralsIndexReader(); + } + + private async getActiveProcessor(session: GDBTargetDebugSession, processors: ProcessorType[]): Promise { + const pname = await session.getPname(); + if (!pname) { + // No pname info available, return first processor in list as best effort + return processors[0]; + } + const result = processors.find(processor => processor.pname === pname); + return result; // If pname requested but not found in cbuild-run processors, then we fail. + } + + private filterCpuType(entry: CorePeripheralEntryType, processorType: string): boolean { + const cpuType = entry['cpu-type']; + if (!cpuType) { + // All CPU types supported + return true; + } + const processorTypeLowerCase = processorType.toLowerCase(); + if (typeof cpuType === 'string') { + // Single entry as string + return cpuType === '*' || cpuType.toLowerCase() === processorTypeLowerCase; + } + // Array with multiple entries + return cpuType.includes('*') || cpuType.some(type => type.toLowerCase() === processorTypeLowerCase); + } + + private filterCpuFeatures(entry: CorePeripheralEntryType, processor?: ProcessorType): boolean { + const cpuFeatures = entry['cpu-features']; + if (!cpuFeatures) { + // No specific CPU features required + return true; + } + const entryFeatures = Object.entries(cpuFeatures); + // If no processor, then use empty object as reference. This let's only pass entries without + // required features, or features with wildcard value. + const processorFeatures = Object.entries(processor ?? {}); + return entryFeatures.every(([entryFeatureKey, entryFeatureValue]) => { + const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); + const [, processorFeatureValue] = processorFeature ?? []; + const featureUndefined = processorFeatureValue === undefined || processorFeatureValue === null; + const featureNotPresent = featureUndefined || processorFeatureValue === FEATURE_NOT_PRESENT_VALUE; + if (entryFeatureValue === '*') { + // Wildcard value + if (featureNotPresent) { + // Required feature not found in processor info + return false; + } + // Wildcard value matches any value as long as the feature is present in processor info. + return true; + } + if (entryFeatureValue === FEATURE_NOT_PRESENT_VALUE) { + // Explicit "not present" value + if (featureNotPresent) { + // Required feature not found in processor info + return true; + } + // Processor has feature. + return false; + } + // Explicit value to match + if (featureUndefined) { + // No valid value for processor feature, treat as not supported + return false; + } + return processorFeatureValue.toString().toLowerCase() === entryFeatureValue.toLowerCase(); + }); + } + + private filterCorePeripheralEntry(entry: CorePeripheralEntryType, processor?: ProcessorType): boolean { + // Test if CPU type is included + if (!this.filterCpuType(entry, processor?.core ?? '*')) { + return false; + } + if (!this.filterCpuFeatures(entry, processor)) { + return false; + } + return true; + } + + public async getScvdFilePaths(session: GDBTargetDebugSession): Promise { try { - // eslint-disable-next-line security/detect-non-literal-fs-filename - const readFilePaths = await fs.promises.readdir(resolvedBasePath, { - encoding: 'buffer', - withFileTypes: true - }); - filePaths.push(...readFilePaths); - } catch (err) { - // Log error and return empty list if directory cannot be read, e.g. because it does not exist - componentViewerLogger.error(`Core Peripherals: Error reading SCVD files from ${resolvedBasePath}:`, err); + await this.indexReader.parse(this.indexFilePath); + } catch (error) { + componentViewerLogger.error(`Core Peripherals: Failed to parse index file ${this.indexFilePath}: ${error}`); + return []; + } + const corePeripherals = this.indexReader.getCorePeripherals(); + if (corePeripherals.length === 0) { + componentViewerLogger.warn(`Core Peripherals: No core peripherals found in index file ${this.indexFilePath}`); return []; } - const scvdFilePaths = filePaths - .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) - .map((file) => path.join(resolvedBasePath, file.name.toString())); - return scvdFilePaths; + const cbuildRunReader = await session.getCbuildRun(); + const contents = cbuildRunReader?.getContents(); + const systemResources = contents?.['system-resources']; + const processors = systemResources?.processors; + const activeProcessor = processors ? await this.getActiveProcessor(session, processors) : undefined; + const filteredCorePeripherals = corePeripherals.filter(entry => this.filterCorePeripheralEntry(entry, activeProcessor)); + return filteredCorePeripherals.map(entry => entry.file); } } diff --git a/test-data/core-peripherals/complex-index/core-peripherals-index.yml b/test-data/core-peripherals/complex-index/core-peripherals-index.yml new file mode 100644 index 00000000..5a264cc2 --- /dev/null +++ b/test-data/core-peripherals/complex-index/core-peripherals-index.yml @@ -0,0 +1,24 @@ +core-peripherals: + - file: TZ_MPU_Cortex-M33.scvd + cpu-type: Cortex-M33 + cpu-features: + mpu: present + trustzone: present + - file: M0_M23_Nested_Vectored_Interrupt_Controller.scvd + cpu-type: + - Cortex-M0+ + - Cortex-M23 + - file: M55_M85_Nested_Vectored_Interrupt_Controller.scvd + cpu-type: + - Cortex-M55 + - Cortex-M85 + - file: FPU_SP.scvd + cpu-type: "*" + cpu-features: + fpu: sp + - file: FPU_All.scvd + cpu-type: "*" + cpu-features: + fpu: "*" + - file: System_Tick_Timer.scvd + cpu-type: "*" diff --git a/test-data/core-peripherals/empty-index/core-peripherals-index.yml b/test-data/core-peripherals/empty-index/core-peripherals-index.yml new file mode 100644 index 00000000..e69de29b diff --git a/test-data/core-peripherals/no-peripherals/core-peripherals-index.yml b/test-data/core-peripherals/no-peripherals/core-peripherals-index.yml new file mode 100644 index 00000000..e6eeb321 --- /dev/null +++ b/test-data/core-peripherals/no-peripherals/core-peripherals-index.yml @@ -0,0 +1 @@ +core-peripherals: