diff --git a/README.md b/README.md index c27155c..2bb2610 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ Here is an example of a uC/OS-III RTOS view Here is an example of an RTX5 RTOS view ![RTX5](./images/RTX5.png) +Here is an example of an eCos view +![eCos](./images/eCos.png) + NOTE: The tab name is in the screenshots is called `XRTOS` so it does not conflict with Cortex-Debug. Once the migration is complete, it will be called `RTOS` and Cortex-Debug itself will not have this functionality # Contributors and maintainers @@ -41,3 +44,4 @@ NOTE: The tab name is in the screenshots is called `XRTOS` so it does not confli | uC/OS-III | @github0null | | ThreadX | @raphaelmeyer | | RTX5 | @thorstendb-ARM | +| eCos | @RallySmith | diff --git a/images/eCos.png b/images/eCos.png new file mode 100644 index 0000000..efec400 Binary files /dev/null and b/images/eCos.png differ diff --git a/package.json b/package.json index d0dbdc3..8edd297 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "embedded", "rtos", "cortex", - "rtx5" + "rtx5", + "ecos" ], "activationEvents": [ "onDebugResolve:cortex-debug", diff --git a/src/rtos/rtos-ecos.ts b/src/rtos/rtos-ecos.ts new file mode 100644 index 0000000..70d8a90 --- /dev/null +++ b/src/rtos/rtos-ecos.ts @@ -0,0 +1,443 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import * as vscode from 'vscode'; +import * as RTOSCommon from './rtos-common'; + +enum DisplayFields { + ID, + Address, + Name, + Status, + Priority, + StackPercent, + //StackSize, // displayed in StackPercent + StackBase, + StackLimit, + StackPtr, + StackTop + // CONSIDER: Cyg_Thread: sleep_reason and wake_reason + // CONSIDER: Cyg_Thread: timer + // CONSIDER: Cyg_Thread: suspend_count, wakeup_count and wait_info + // CONSIDER: Cyg_SchedThread: queue and mutex_count + // CONSIDER: Cyg_SchedThread: original and inherited priorities + // CONSIDER: Cyg_SchedThread_Implementation: timeslice_count +} + +const numType = RTOSCommon.ColTypeEnum.colTypeNumeric; + +const eCosItems: { [key: string]: RTOSCommon.DisplayColumnItem } = {}; + +/* eCos thread IDs are indexed from 1, so 0 can be used as an invalid thread ID marker */ +eCosItems[DisplayFields[DisplayFields.ID]] = { + width: 1, + headerRow1: 'Thread', + headerRow2: 'ID', + colType: numType +}; + +eCosItems[DisplayFields[DisplayFields.Address]] = { + width: 3, + headerRow1: '', + headerRow2: 'Address', + colGapBefore: 1 +}; + +eCosItems[DisplayFields[DisplayFields.Name]] = { + width: 4, + headerRow1: '', + headerRow2: 'Name' +}; + +eCosItems[DisplayFields[DisplayFields.Status]] = { + width: 3, + headerRow1: '', + headerRow2: 'Status', +}; + +eCosItems[DisplayFields[DisplayFields.Priority]] = { + width: 1.5, + headerRow1: '', + headerRow2: 'Priority', + colType: numType +}; + +eCosItems[DisplayFields[DisplayFields.StackPercent]] = { + width: 4, + headerRow1: 'Stack Usage', + headerRow2: '% (Used B / Size B)', + colType: RTOSCommon.ColTypeEnum.colTypePercentage, +}; + +eCosItems[DisplayFields[DisplayFields.StackBase]] = { + width: 2, + headerRow1: 'Stack', + headerRow2: 'Base', + colType: numType +}; + +eCosItems[DisplayFields[DisplayFields.StackLimit]] = { + width: 2, + headerRow1: 'Stack', + headerRow2: 'Limit', + colType: numType +}; + +eCosItems[DisplayFields[DisplayFields.StackPtr]] = { + width: 2, + headerRow1: 'Stack', + headerRow2: 'Pointer', + colType: numType +}; + +eCosItems[DisplayFields[DisplayFields.StackTop]] = { + width: 2, + headerRow1: 'Stack', + headerRow2: 'Top', + colType: numType +}; + +const DisplayFieldNames: string[] = Object.keys(eCosItems); + +// eCos thread status is a logical-OR bitmask of states: +enum ThreadStatus { + RUNNING = 0, + SLEEPING = (1 << 0), + COUNTSLEEP = (1 << 1), + SUSPENDED = (1 << 2), + CREATING = (1 << 3), + EXITED = (1 << 4), + SLEEPSET = (SLEEPING | COUNTSLEEP), +} + +export class RTOSeCos extends RTOSCommon.RTOSBase { + private pxThreadList: RTOSCommon.RTOSVarHelperMaybe; + private xCurrentThread: RTOSCommon.RTOSVarHelperMaybe; + + private stackIncrements = -1; // negative numbers => stack expands from higher address to lower addresses + + private stale = true; + private timeInfo = ''; + private helpHtml: string | undefined; + + private foundThreads: RTOSCommon.RTOSThreadInfo[] = []; + private finalThreads: RTOSCommon.RTOSThreadInfo[] = []; + + // NOTE: under eCos the idle thread appears as a normal + // schedulable thread and so we do not need special handling; other + // than if we want to highlight it (e.g. special case handling of + // "cyg_idle_thread_loops[cpunum]" reporting) + constructor(public session: vscode.DebugSession) { + super(session, 'eCos'); + if (session.configuration.rtosViewConfig) { + if (session.configuration.rtosViewConfig.stackGrowth) { + this.stackIncrements = parseInt(session.configuration.rtosViewConfig.stackGrowth); + console.log('eCos: stackIncrements:', this.stackIncrements); + } + } + } + + public async tryDetect(useFrameId: number): Promise { + this.progStatus = 'stopped'; + try { + if (this.status === 'none') { //colType: numType + // Use "Cyg_Thread::thread_list" as basic eCos detection mechanism: + this.pxThreadList = await this.getVarIfEmpty(this.pxThreadList, useFrameId, 'Cyg_Thread::thread_list'); + // Vector holding per-CPU currently active thread pointer: "Cyg_Scheduler_Base::current_thread[CYGNUM_KERNEL_CPU_COUNT]" + this.xCurrentThread = await this.getVarIfEmpty(this.xCurrentThread, useFrameId, 'Cyg_Scheduler_Base::current_thread[0]'); + // ASCERTAIN: Best method to get information about the depth of the above vector (#CPUs in SMP configuration). + // Possibly use non-C symbol as per the thread stacked context shape symbols. + this.status = 'initialized'; + } + return this; + } catch (e) { + if (e instanceof RTOSCommon.ShouldRetry) { + console.error(e.message); + } else { + this.status = 'failed'; + this.failedWhy = e; + } + return this; + } + } + + protected createHmlHelp(th: RTOSCommon.RTOSThreadInfo, thInfo: RTOSCommon.RTOSStrToValueMap) { + function strong(text: string) { + return `${text}`; + } + if (this.helpHtml === undefined) { + this.helpHtml = ''; + try { + let ret: string = ''; + if (!thInfo['name']?.val) { + ret += `Thread name missing: Enable ${strong('CYGVAR_KERNEL_THREADS_NAME')} if desired.
`; + } + // CONSIDER: Checking for further kernel features that may be useful for run-time diagnostics: + // e.g. CYGFUN_KERNEL_THREADS_STACK_LIMIT, CYGFUN_KERNEL_THREADS_STACK_MEASUREMENT, etc. + if (ret) { + ret += 'Note: Make sure to consider the performance/resources impact of any eCos configuration changes.
'; + this.helpHtml = '\n' + + '

\n${ret}\n

\n'; + } + } catch (e) { + console.log(e); + } + } + } + + private getThreadState(threadStatus: number, threadCurrent: boolean) { + let stateText = ''; + + if (threadStatus & ThreadStatus.SUSPENDED) { + stateText = 'suspended+'; + } + + switch (threadStatus & ~ThreadStatus.SUSPENDED) { + case ThreadStatus.RUNNING: + if (threadCurrent) { + stateText = 'running'; + } else if (threadStatus & ThreadStatus.SUSPENDED) { + stateText = 'suspended'; + } else { + stateText = 'ready'; + } + break; + case ThreadStatus.SLEEPING: + stateText = 'sleeping'; + break; + case ThreadStatus.SLEEPSET: + case ThreadStatus.COUNTSLEEP: + stateText = 'counted-sleep'; + break; + case ThreadStatus.CREATING: + stateText = 'creating'; + break; + case ThreadStatus.EXITED: + stateText = 'exited'; + break; + default: + stateText = 'unknown'; + break; + } + + return stateText; + } + + protected async getStackInfo(thInfo: RTOSCommon.RTOSStrToValueMap | null, thHardware: RTOSCommon.RTOSStrToValueMap | null) { + const stackInfo: RTOSCommon.RTOSStackInfo = { + stackStart: 0, + }; + stackInfo.stackTop = 0; + + if ((thInfo === null) || (thHardware === null)) { + return stackInfo; + } + + const stackBase = thHardware['stack_base'].val; + const stackLimit = thHardware['stack_limit'].val; + const stackSize = thHardware['stack_size'].val; + const stackPtr = thHardware['stack_ptr'].val; + + // If CYGFUN_KERNEL_THREADS_STACK_MEASUREMENT (with + // CYGNUM_KERNEL_THREADS_STACK_DATA_INIT as the init value) we could + // calculate the stackInfo.stackPeak value by checking the target + // memory. See rtos-embos.ts for an example implementation. + + if (stackSize && stackBase && stackLimit && stackPtr) { + const base = parseInt(stackBase); + const limit = parseInt(stackLimit); + const size = parseInt(stackSize); + const ptr = parseInt(stackPtr); + stackInfo.stackTop = Math.abs(base + size); + stackInfo.stackSize = size; + if (this.stackIncrements < 0) { + // Descending: + stackInfo.stackStart = limit; + stackInfo.stackEnd = stackInfo.stackTop; + stackInfo.stackFree = Math.abs(ptr - limit); + stackInfo.stackUsed = Math.abs(stackInfo.stackEnd - ptr); + } else { + // Ascending: + stackInfo.stackStart = stackInfo.stackTop; + stackInfo.stackEnd = limit; + stackInfo.stackFree = Math.abs(stackInfo.stackEnd- ptr); + stackInfo.stackUsed = Math.abs(ptr - stackInfo.stackStart); + } + } else { + // Force value since stackStart needs to be set: + stackInfo.stackStart = stackInfo.stackTop; + } + + return stackInfo; + } + + private getThreadInfo(thListHead: RTOSCommon.RTOSVarHelperMaybe, frameId: number): Promise { + return new Promise((resolve, reject) => { + if (!thListHead || !thListHead.varReference) { + resolve(); + return; + } + + if (this.progStatus !== 'stopped') { + reject(new Error('Busy')); + return; + } + + // thListHead is head object of circular linked list: + const thFirstAddress = parseInt(thListHead?.value || ''); + let thActiveAddress = 0; + + // Only CPU[0] currently: + this.xCurrentThread?.getValue(frameId).then( + async() => { + try { + const thActive = await this.xCurrentThread; + thActiveAddress = parseInt(thActive?.value || ''); + } catch (e) { + console.log('RTOSeCos.getThreadInfo() xCurrentThread error', e); + } + }, + (e) => { + reject(e); + } + ); + + this.pxThreadList?.getVarChildrenObj(frameId).then( + async (thHead: RTOSCommon.RTOSStrToValueMap) => { + try { + let thCurrent = thHead; + let thAddress = thFirstAddress; + + do { + if (Object.hasOwn(thCurrent, 'list_next')) { + const thHardware = await this.getVarChildrenObj(thCurrent['Cyg_HardwareThread']?.ref, ''); + const thSched = await this.getVarChildrenObj(thCurrent['Cyg_SchedThread']?.ref, '') || {}; + const thSchedImpl = await this.getVarChildrenObj(thSched['Cyg_SchedThread_Implementation']?.ref, '') || {}; + + let thName = '[EMPTY]'; + if (thCurrent['name']) { + const matchName = thCurrent['name'].val.match(/"([^*]*)"$/); + thName = matchName ? matchName[1] : thName; + } + + const threadRunning = (thAddress === thActiveAddress); + const stackInfo = await this.getStackInfo(thCurrent, thHardware); + + const display: { [key: string]: RTOSCommon.DisplayRowItem } = {}; + const mySetter = (x: DisplayFields, text: string, value?: any) => { + display[DisplayFieldNames[x]] = { text, value }; + }; + + mySetter(DisplayFields.ID, thCurrent['unique_id'].val); + mySetter(DisplayFields.Address, RTOSCommon.hexFormat(thAddress)); + mySetter(DisplayFields.Priority, Math.abs(parseInt(thSchedImpl['priority'].val)).toString()); // decimal + mySetter(DisplayFields.Status, this.getThreadState(parseInt(thCurrent['state'].val), threadRunning)); + mySetter(DisplayFields.Name, thName); + if ((stackInfo.stackUsed !== undefined) && (stackInfo.stackSize !== undefined)) { + const stackPercentVal = Math.round((stackInfo.stackUsed / stackInfo.stackSize) * 100); + const stackPercentText = `${stackPercentVal} % (${stackInfo.stackUsed} / ${stackInfo.stackSize})`; + mySetter(DisplayFields.StackPercent, stackPercentText, stackPercentVal); + } else { + mySetter(DisplayFields.StackPercent, '[unknown]'); + } + if (thHardware) { + mySetter(DisplayFields.StackBase, thHardware['stack_base'].val); // could use value de-referenced in getStackInfo() + mySetter(DisplayFields.StackLimit, RTOSCommon.hexFormat(stackInfo.stackStart)); + mySetter(DisplayFields.StackPtr, thHardware['stack_ptr'].val); + if (stackInfo.stackTop !== undefined) { + mySetter(DisplayFields.StackTop, RTOSCommon.hexFormat(stackInfo.stackTop)); + } + } + + const thread: RTOSCommon.RTOSThreadInfo = { + display: display, + stackInfo: stackInfo, + running: threadRunning, + }; + this.foundThreads.push(thread); + this.createHmlHelp(thread, thCurrent); + + thAddress = parseInt(thCurrent.list_next?.val); + if (0 !== thAddress) { + const thNextThread = await this.getVarChildrenObj(thCurrent.list_next?.ref, 'list_next'); + thCurrent = thNextThread || {}; + } + } else { + console.log('invalid eCos thread reference : no list_next'); + } + } while ((thAddress !== 0) && (thFirstAddress !== thAddress)); + + resolve(); + } catch (e) { + console.log('RTOSeCos.getThreadInfo() error', e); + } + }, + (e) => { + reject(e); + } + ); + }); + } + + public refresh(frameId: number): Promise { + return new Promise((resolve) => { + if (this.progStatus !== 'stopped') { + resolve(); + return; + } + + const timer = new RTOSCommon.HrTimer(); + + this.stale = true; + this.timeInfo = new Date().toISOString(); + this.foundThreads = []; + + this.pxThreadList?.getValue(frameId).then( + async () => { + try { + await this.getThreadInfo(this.pxThreadList, frameId); + this.foundThreads.sort((a, b) => parseInt(a.display['ID'].text) - parseInt(b.display['ID'].text)); + + this.finalThreads = [...this.foundThreads]; + this.stale = false; + this.timeInfo += ' in ' + timer.deltaMs() + 'ms'; + resolve(); + } catch (e) { + resolve(); + console.error('RTOSeCos.refresh() failed: ', e); + } + }, + (reason) => { + resolve(); + console.error('RTOSeCos.refresh() failed reason: ', reason); + } + ); + }); + } + + public lastValidHtmlContent: RTOSCommon.HtmlInfo = { html: '', css: '' }; + + public getHTML(): RTOSCommon.HtmlInfo { + const htmlContent: RTOSCommon.HtmlInfo = { html: '', css: '' }; + let msg = ''; + if (this.status === 'none') { + htmlContent.html = '

eCos not yet fully initialized. Will update the next time program pauses.

\n'; + return htmlContent; + } else if (this.stale) { + const lastHtmlInfo = this.lastValidHtmlContent; + msg = ' Following info from last query may be stale.'; + htmlContent.html = `

Unable to collect full eCos information.${msg}

\n` + lastHtmlInfo.html; + htmlContent.css = lastHtmlInfo.css; + return htmlContent; + } else if (this.finalThreads.length === 0) { + htmlContent.html = `

No ${this.name} threads detected, perhaps eCos is not yet initialized or threads are yet to be created!

\n`; + return htmlContent; + } + + const ret = this.getHTMLThreads(DisplayFieldNames, eCosItems, this.finalThreads, this.timeInfo); + // CONSIDER: Display other useful *global* eCos information (mutexes, memory pools, etc.) // see rtos-threadx.ts for example + htmlContent.html = msg + ret.html + (this.helpHtml || ''); + htmlContent.css = ret.css; + + this.lastValidHtmlContent = htmlContent; + return this.lastValidHtmlContent; + } +} diff --git a/src/rtos/rtos.ts b/src/rtos/rtos.ts index 6692de1..82ff80c 100644 --- a/src/rtos/rtos.ts +++ b/src/rtos/rtos.ts @@ -11,6 +11,7 @@ import { RTOSChibiOS } from './rtos-chibios'; import { RTOSZEPHYR } from './rtos-zephyr'; import { RTOSThreadX } from './rtos-threadx'; import { RTOSRTX5 } from './rtos-rtx5'; +import { RTOSeCos } from './rtos-ecos'; import { IDebugTracker, @@ -55,6 +56,8 @@ const RTOS_TYPES = { ThreadX: RTOSThreadX, // eslint-disable-next-line @typescript-eslint/naming-convention RTX5: RTOSRTX5, + // eslint-disable-next-line @typescript-eslint/naming-convention + eCos: RTOSeCos, }; const defaultHtmlInfo: RTOSCommon.HtmlInfo = { html: '', css: '' };