mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
verilog: record to VCD
This commit is contained in:
1 parent
d7e9249d69
commit
60e5577fc3
17 files changed
+613
-5
No files matched your search
@@ -13,6 +13,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
|
||||
- Emulators now run on a timeline: step back, seek, reverse run, `rbreak`, and deterministic replay with a replay-verification test that found determinism bugs in several platforms.
|
||||
- Step Over for C and assembly sources, a Breakpoints pane, and more breakpoint conditions.
|
||||
- `8bws run` script `paddle X Y [BUTTONS]` moves the paddle on platforms and Verilog designs that have one.
|
||||
- `8bws run` script `vcd FILE [MAXMB]` / `vcd off` records a Verilog design's signal changes to a VCD file as it runs, streamed so the recording's length isn't limited by memory (a `.gz` name compresses it; about 3 MB per frame of a design with video, 4x less gzipped), stopping at 1 GB unless told otherwise.
|
||||
- Debug adapter: Verilog designs get a Signals scope (nested by module), and the `8bws` run script a `signals` command.
|
||||
- Verilator warnings (width mismatches, etc.) no longer fail the build and show as warnings; a Verilator error no longer also reports "tool called exit(1)".
|
||||
- Call Stack view renamed Call Graph, with clocks and more detail per line.
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
under a stack frame named for the frame and clock cycle.
|
||||
- A Machine view in a new 8bitworkshop panel shows the platform's debug info,
|
||||
live while the program runs, for platforms that have it.
|
||||
- **Record Signals to VCD...** writes a Verilog design's signal changes to a
|
||||
`.vcd` file as it runs, without holding them in memory, up to
|
||||
1 GB. `8bws run` has a
|
||||
matching `vcd FILE` / `vcd off` script command.
|
||||
- Infer symbol sizes from build output, instead of one byte per variable.
|
||||
- Removed Hex Editor extension integration, we'll come up with a better one.
|
||||
- Removed the assetUrl setting.
|
||||
|
||||
@@ -98,6 +98,8 @@ also appear in the editor title bar, the Explorer, or **Project Options**.
|
||||
| **Follow Active Editor** | Run whichever program is in the editor |
|
||||
| **Reset / Pause / Resume / Stop / Mute / Unmute Emulator** | Control the emulator panel (shown while it is open) |
|
||||
| **Show Machine View** | Open the Machine view in the 8bitworkshop panel: the platform's debug info (registers, video chip state), updated while the program runs |
|
||||
| **Record Signals to VCD...** | Verilog: write the design's signal changes for a few frames to a `.vcd` file, for a waveform viewer. About 3 MB per frame for a design with video; name the file `.vcd.gz` to compress it about 4x. A recording stops at 1 GB |
|
||||
| **Stop Recording VCD** | End a recording before its frames are up |
|
||||
|
||||
## Books
|
||||
|
||||
|
||||
@@ -196,6 +196,16 @@
|
||||
"command": "8bitworkshop.showMachine",
|
||||
"title": "Show Machine View",
|
||||
"category": "8bitworkshop"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.recordVcd",
|
||||
"title": "Record Signals to VCD...",
|
||||
"category": "8bitworkshop"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.stopVcd",
|
||||
"title": "Stop Recording VCD",
|
||||
"category": "8bitworkshop"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
@@ -558,6 +568,14 @@
|
||||
{
|
||||
"command": "8bitworkshop.showMachine",
|
||||
"when": "8bitworkshop.hasDebugInfo"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.recordVcd",
|
||||
"when": "8bitworkshop.canRecordVcd && !8bitworkshop.recordingVcd"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.stopVcd",
|
||||
"when": "8bitworkshop.recordingVcd"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { StopEvent } from '../../src/common/debugcontroller';
|
||||
import type { DebugSection } from '../../src/common/emucore';
|
||||
import { BuildInfo, DebugService, TimelineInfo } from '../../src/tools/debugservice';
|
||||
import { RunScript } from '../../src/tools/runscript';
|
||||
import { openVcdFile, VcdFile } from '../../src/tools/vcdfile';
|
||||
import { buildDebugContext } from '../../src/common/debugcontroller';
|
||||
import { encode as encodePng } from 'fast-png';
|
||||
|
||||
@@ -62,6 +63,19 @@ export interface EmuStatus {
|
||||
timeline?: TimelineInfo;
|
||||
/** the platform has debug info text for the Machine view */
|
||||
debugInfo?: boolean;
|
||||
/** the program has signals that can be recorded as a VCD (startVcd) */
|
||||
vcd?: boolean;
|
||||
}
|
||||
|
||||
/** A VCD recording started or ended (the 'vcd' event). */
|
||||
export interface VcdEvent {
|
||||
recording: boolean;
|
||||
file: string;
|
||||
/** when it ended: the clocks it holds and the bytes written */
|
||||
clocks?: number;
|
||||
bytes?: number;
|
||||
/** it stopped because the file reached its size limit */
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
/** What a debug view gets (see DebugViews in views.ts). */
|
||||
@@ -94,6 +108,8 @@ const DEBUG_METHODS = new Set([
|
||||
'seekFrame', 'location', 'timeline', 'registers', 'readMemory', 'writeMemory', 'disassemble', 'evaluate',
|
||||
'debugTree', 'signalTree', 'symbols', 'callStack',
|
||||
]);
|
||||
// the ones that replay frames the recording already has
|
||||
const REWIND_METHODS = new Set(['stepBack', 'reverseContinue', 'seekFrame']);
|
||||
// the ones that start the machine running toward a goal
|
||||
const FORWARD_METHODS = new Set(['continue', 'step']);
|
||||
|
||||
@@ -121,6 +137,8 @@ let muted = false;
|
||||
/** the views the host has showing: only these get data */
|
||||
let views = new Set<string>();
|
||||
let lastViewPush = 0;
|
||||
/** the VCD file being written, and the frame to stop at (if it has a length) */
|
||||
let vcdFile: { file: VcdFile, path: string, end: number | null } | null = null;
|
||||
|
||||
/**
|
||||
* The headless stand-in for the platform's Web Audio sink: SampleAudio hands
|
||||
@@ -162,16 +180,41 @@ const rpc: Rpc = new Rpc(parentPort, {
|
||||
},
|
||||
async loadROM(rom: any, files?: { [path: string]: FileData }, opts: LoadOptions = {}) {
|
||||
if (!target) throw new Error('emulator not started');
|
||||
stopVcd();
|
||||
target.setFileData(files || {});
|
||||
await target.loadROM(rom);
|
||||
if (!opts.paused) resume();
|
||||
rpc.emit('audioReset', null);
|
||||
return status();
|
||||
},
|
||||
/**
|
||||
* Record the design's signals to `file` as a VCD, for `frames` frames (or
|
||||
* until stopVcd or the file reaches `maxBytes`, by default 1 GB). It slows the simulation and
|
||||
* writes ~3MB a frame for a VGA design (a quarter of that if `file` ends in
|
||||
* .gz), so the host asks for a length.
|
||||
*/
|
||||
startVcd(file: string, frames?: number, maxBytes?: number) {
|
||||
if (!target) throw new Error('emulator not started');
|
||||
if (!target.supportsVcd) throw new Error(`'${target.id}' has no signals to record`);
|
||||
stopVcd();
|
||||
const rec = { file: openVcdFile(file, maxBytes), path: file, end: frames ? target.frameCount + frames : null };
|
||||
try {
|
||||
target.startVcd(chunk => rec.file.write(chunk), () => rec.file.full);
|
||||
} catch (e) {
|
||||
rec.file.close();
|
||||
throw e;
|
||||
}
|
||||
vcdFile = rec;
|
||||
rpc.emit('vcd', { recording: true, file } as VcdEvent);
|
||||
},
|
||||
stopVcd() {
|
||||
return stopVcd();
|
||||
},
|
||||
/** Call a DebugService method; stops come back as 'stopped' events. */
|
||||
debug(method: string, ...args: any[]) {
|
||||
if (!service) throw new Error('emulator not started');
|
||||
if (!DEBUG_METHODS.has(method)) throw new Error(`no debug method '${method}'`);
|
||||
if (REWIND_METHODS.has(method)) stopVcd();
|
||||
const result = (service as any)[method](...args);
|
||||
if (FORWARD_METHODS.has(method)) startRunning();
|
||||
return result;
|
||||
@@ -191,6 +234,7 @@ const rpc: Rpc = new Rpc(parentPort, {
|
||||
/** Show a recorded frame, stopped there. */
|
||||
seekFrame(frame: number) {
|
||||
if (!service) return null;
|
||||
stopVcd();
|
||||
service.seekFrame(frame);
|
||||
return status();
|
||||
},
|
||||
@@ -259,7 +303,7 @@ function status(): EmuStatus | null {
|
||||
const audio = target.getAudioParams();
|
||||
return {
|
||||
state: running ? 'running' : 'paused', platform: target.id, frame: target.frameCount,
|
||||
controls, paddles: target.acceptsPaddles(), debugInfo: target.hasDebugInfo, audio: audio ? { sampleRate: audio.sampleRate } : undefined,
|
||||
controls, paddles: target.acceptsPaddles(), debugInfo: target.hasDebugInfo, vcd: target.supportsVcd, audio: audio ? { sampleRate: audio.sampleRate } : undefined,
|
||||
timeline: service?.timeline() ?? undefined,
|
||||
};
|
||||
}
|
||||
@@ -311,9 +355,23 @@ function stopTimer() {
|
||||
timer = null;
|
||||
}
|
||||
|
||||
/** Finish the VCD file, if one is being written. */
|
||||
function stopVcd(): VcdEvent | null {
|
||||
const f = vcdFile;
|
||||
if (!f) return null;
|
||||
vcdFile = null;
|
||||
// the writer's last chunk goes out before the file closes
|
||||
const clocks = target ? target.stopVcd() : 0;
|
||||
f.file.close();
|
||||
const ev: VcdEvent = { recording: false, file: f.path, clocks, bytes: f.file.bytes, full: f.file.full };
|
||||
rpc.emit('vcd', ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
running = false;
|
||||
stopTimer();
|
||||
stopVcd();
|
||||
rpc.emit('status', status());
|
||||
target = null;
|
||||
service = null;
|
||||
@@ -351,6 +409,7 @@ function tick() {
|
||||
return;
|
||||
}
|
||||
if (!running) return; // stopped during the frame
|
||||
if (vcdFile && ((vcdFile.end != null && target.frameCount >= vcdFile.end) || !target.vcdRunning)) stopVcd();
|
||||
if (now - nextTime > interval * MAX_CATCHUP_FRAMES) nextTime = now; // too far behind
|
||||
if (frames) sendFrame();
|
||||
timer = setTimeout(tick, Math.max(0, nextTime - performance.now()));
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { BuildOutcome } from './buildcore';
|
||||
import type { BuildArgs } from './buildworker';
|
||||
import type { AudioChunk, EmuStatus, ScriptResult } from './emuworker';
|
||||
import type { AudioChunk, EmuStatus, ScriptResult, VcdEvent } from './emuworker';
|
||||
import { WorkerHandle, WorkerDisposedError } from './engine';
|
||||
import { EmulatorPanel, VIEW_TYPE } from './emulatorpanel';
|
||||
import { DebugViews } from './views';
|
||||
@@ -121,6 +121,8 @@ export function activate(ctx: vscode.ExtensionContext) {
|
||||
command('downloadToolchains', () => prepareToolchains());
|
||||
command('mute', () => setMuted(true));
|
||||
command('unmute', () => setMuted(false));
|
||||
command('recordVcd', () => recordVcd());
|
||||
command('stopVcd', () => emu?.started && emu.call('stopVcd'));
|
||||
command('showMachine', () => vscode.commands.executeCommand('8bitworkshop.machine.focus'));
|
||||
ctx.subscriptions.push(...views.register());
|
||||
muted = ctx.globalState.get<boolean>('muted', false);
|
||||
@@ -313,10 +315,12 @@ async function getEmu(platform: string): Promise<WorkerHandle> {
|
||||
emu.on('audioReset', () => panel?.resetAudio());
|
||||
emu.on('stopped', e => debugging?.backend.handleStop(e));
|
||||
emu.on('view', ev => views.show(ev));
|
||||
emu.on('vcd', (ev: VcdEvent) => vcdEvent(ev));
|
||||
emu.on('status', (s: EmuStatus | null) => {
|
||||
emuStatus = s;
|
||||
panel?.showStatus(s);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.hasDebugInfo', !!s?.debugInfo);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.canRecordVcd', !!s?.vcd);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.emuRunning', s?.state === 'running');
|
||||
if (s?.state === 'halted') output.appendLine(`Emulator halted at frame ${s.frame}: ${s.message}`);
|
||||
});
|
||||
@@ -628,6 +632,7 @@ async function startEmulator(target: Target, build: BuildOutcome, opts: { paused
|
||||
// a plain Run replaces what the debugger was looking at
|
||||
if (debugging && !opts.paused) vscode.debug.stopDebugging(debugging.session);
|
||||
// each run gets a fresh worker: platforms keep global state (see emuworker)
|
||||
if (emu?.started) await emu.call('stopVcd').catch(() => { });
|
||||
emu?.dispose();
|
||||
emu = undefined;
|
||||
emuStatus = null;
|
||||
@@ -652,6 +657,8 @@ async function startEmulator(target: Target, build: BuildOutcome, opts: { paused
|
||||
emu = undefined;
|
||||
views.clear();
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.hasDebugInfo', false);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.canRecordVcd', false);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.recordingVcd', false);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.emuRunning', false);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.emuOpen', false);
|
||||
},
|
||||
@@ -668,6 +675,7 @@ async function startEmulator(target: Target, build: BuildOutcome, opts: { paused
|
||||
worker.call('setMuted', muted);
|
||||
worker.call('setViews', views.subscriptions());
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.hasDebugInfo', !!emuStatus?.debugInfo);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.canRecordVcd', !!emuStatus?.vcd);
|
||||
panel.showStatus(emuStatus);
|
||||
running = target;
|
||||
runningBuild = build;
|
||||
@@ -703,6 +711,46 @@ async function reloadEmulator(target: Target, reason: BuildReason, result: Build
|
||||
}
|
||||
}
|
||||
|
||||
/** Verilog designs: write the signals' changes for a few frames to a VCD file. */
|
||||
async function recordVcd() {
|
||||
if (!emu?.started || !emuStatus?.vcd) {
|
||||
vscode.window.showInformationMessage('Run a Verilog design first; only designs with signals can be recorded.');
|
||||
return;
|
||||
}
|
||||
var dir = running ? vscode.Uri.joinPath(running.main, '..') : vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||
var base = running ? path.basename(running.main.path).replace(/\.[^.]*$/, '') : 'trace';
|
||||
var file = await vscode.window.showSaveDialog({
|
||||
defaultUri: dir && vscode.Uri.joinPath(dir, base + '.vcd'),
|
||||
filters: { 'Value change dump': ['vcd', 'gz'] },
|
||||
title: 'Record signals to a VCD file',
|
||||
});
|
||||
if (!file) return;
|
||||
// a VGA design writes about 3 MB a frame, so it stops by itself
|
||||
var answer = await vscode.window.showInputBox({
|
||||
title: 'Frames to record',
|
||||
prompt: 'A design with video writes about 3 MB per frame (a quarter of that if the file name ends in .gz), and runs more slowly while recording.',
|
||||
value: '2',
|
||||
validateInput: v => /^[1-9]\d*$/.test(v.trim()) ? undefined : 'Enter a whole number of frames',
|
||||
});
|
||||
if (!answer) return;
|
||||
try {
|
||||
await emu.call('startVcd', file.fsPath, Number(answer.trim()));
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage(`Could not record: ${e && e.message || e}`);
|
||||
}
|
||||
}
|
||||
|
||||
function vcdEvent(ev: VcdEvent) {
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.recordingVcd', ev.recording);
|
||||
if (ev.recording) return;
|
||||
var limit = ev.full ? ', stopped at the 1 GB limit' : '';
|
||||
output.appendLine(`Wrote ${ev.clocks} clocks to ${ev.file} (${mb(ev.bytes || 0)}${limit})`);
|
||||
vscode.window.showInformationMessage(`Recorded ${ev.clocks} clocks (${mb(ev.bytes || 0)}) to ${path.basename(ev.file)}${limit}.`, 'Show in Folder')
|
||||
.then(choice => {
|
||||
if (choice) vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(ev.file));
|
||||
});
|
||||
}
|
||||
|
||||
/** Turn sound on or off everywhere (the webview gain and the worker's push). */
|
||||
function setMuted(m: boolean) {
|
||||
muted = m;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { Rpc } from '../src/rpc';
|
||||
import { findRootDir } from '../src/projectinfo';
|
||||
import { Builder, ProjectFileProvider } from '../src/buildcore';
|
||||
import type { AudioChunk, EmuStatus, FrameEvent, ScriptResult, ViewEvent } from '../src/emuworker';
|
||||
import type { AudioChunk, EmuStatus, FrameEvent, ScriptResult, VcdEvent, ViewEvent } from '../src/emuworker';
|
||||
import { WorkerDebugBackend } from '../src/debugbackend';
|
||||
import { EmuDebugSession } from '../../src/tools/dapsession';
|
||||
import { DapClient } from '../../test/unit/dapclient';
|
||||
@@ -27,6 +28,7 @@ describe('extension emuworker', function () {
|
||||
var statuses: EmuStatus[];
|
||||
var audios: AudioChunk[];
|
||||
var viewEvents: ViewEvent[];
|
||||
var vcdEvents: VcdEvent[];
|
||||
|
||||
beforeEach(function () {
|
||||
worker = new Worker(path.join(__dirname, '..', 'emuworker.js'), { workerData: { rootDir: ROOT } });
|
||||
@@ -35,6 +37,8 @@ describe('extension emuworker', function () {
|
||||
statuses = [];
|
||||
audios = [];
|
||||
viewEvents = [];
|
||||
vcdEvents = [];
|
||||
rpc.on('vcd', v => vcdEvents.push(v));
|
||||
rpc.on('view', v => viewEvents.push(v));
|
||||
rpc.on('frame', f => frames.push(f));
|
||||
rpc.on('status', s => statuses.push(s));
|
||||
@@ -246,6 +250,87 @@ describe('extension emuworker', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('verilog VCD recording', function () {
|
||||
const MAIN = [
|
||||
'module top(clk, reset, hsync, vsync, rgb);',
|
||||
' input clk, reset;',
|
||||
' output hsync, vsync;',
|
||||
' output [3:0] rgb;',
|
||||
' reg [7:0] n;',
|
||||
' always @(posedge clk) n <= reset ? 0 : n + 1;',
|
||||
' assign hsync = n[7];',
|
||||
' assign vsync = 0;',
|
||||
' assign rgb = n[3:0];',
|
||||
'endmodule',
|
||||
'',
|
||||
].join('\n');
|
||||
var built: Awaited<ReturnType<Builder['build']>>;
|
||||
var dir: string;
|
||||
|
||||
before(async function () {
|
||||
var read = async (rel: string) => null;
|
||||
built = await new Builder(ROOT).build({
|
||||
platform: 'verilog', mainPath: 'main.v', mainText: MAIN,
|
||||
files: new ProjectFileProvider(read, ROOT, 'verilog'),
|
||||
});
|
||||
assert.deepEqual(built.diagnostics, []);
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), '8bws-vcd-'));
|
||||
});
|
||||
|
||||
async function waitForVcd(recording: boolean) {
|
||||
for (var i = 0; i < 400 && !vcdEvents.some(e => e.recording === recording); i++) await new Promise(r => setTimeout(r, 20));
|
||||
var ev = vcdEvents.find(e => e.recording === recording);
|
||||
assert.ok(ev, 'no vcd event ' + JSON.stringify(vcdEvents));
|
||||
return ev!;
|
||||
}
|
||||
|
||||
it('records a number of frames to a file and stops by itself', async function () {
|
||||
var s = await rpc.call<EmuStatus>('start', 'verilog', built.output, built.files);
|
||||
assert.ok(s.vcd, 'verilog design has signals to record');
|
||||
var file = path.join(dir, 'a.vcd');
|
||||
await rpc.call('startVcd', file, 2);
|
||||
var done = await waitForVcd(false);
|
||||
assert.equal(done.file, file);
|
||||
assert.ok(done.clocks! > 0 && done.bytes! > 0);
|
||||
var text = fs.readFileSync(file, 'utf-8');
|
||||
assert.equal(text.length, done.bytes);
|
||||
assert.ok(text.startsWith('$version'));
|
||||
assert.ok(/\$var wire 8 \S+ n \[7:0\] \$end/.test(text), text.slice(0, 400));
|
||||
assert.ok(text.endsWith('#' + done.clocks + '\n'));
|
||||
// the counter changes every clock
|
||||
assert.ok(text.split('\n').filter(l => /^b\d+ /.test(l)).length > 1000);
|
||||
});
|
||||
|
||||
it('stops when asked, and a hidden-length recording keeps going until then', async function () {
|
||||
await rpc.call('start', 'verilog', built.output, built.files);
|
||||
var file = path.join(dir, 'b.vcd');
|
||||
await rpc.call('startVcd', file);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
assert.ok(!vcdEvents.some(e => !e.recording), 'no length, so it runs on');
|
||||
var ev = await rpc.call<VcdEvent>('stopVcd');
|
||||
assert.equal(ev.file, file);
|
||||
assert.ok(ev.clocks! > 0);
|
||||
assert.equal(await rpc.call('stopVcd'), null);
|
||||
});
|
||||
|
||||
it('ends itself at the size limit', async function () {
|
||||
await rpc.call('start', 'verilog', built.output, built.files);
|
||||
var file = path.join(dir, 'full.vcd');
|
||||
await rpc.call('startVcd', file, 0, 100000);
|
||||
var done = await waitForVcd(false);
|
||||
assert.ok(done.full, JSON.stringify(done));
|
||||
assert.ok(done.bytes! >= 100000 && done.bytes! < 300000, 'bytes ' + done.bytes);
|
||||
assert.equal(fs.statSync(file).size, done.bytes);
|
||||
assert.equal(await rpc.call('stopVcd'), null, 'already finished');
|
||||
});
|
||||
|
||||
it('refuses platforms with no signals', async function () {
|
||||
var s = await rpc.call<EmuStatus>('start', 'nes', rom('nes/shoot2.c.rom'));
|
||||
assert.ok(!s.vcd);
|
||||
await assert.rejects(rpc.call('startVcd', path.join(dir, 'c.vcd')), /no signals/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('debugging', function () {
|
||||
var built: Awaited<ReturnType<Builder['build']>>;
|
||||
var stops: any[];
|
||||
|
||||
@@ -158,6 +158,11 @@ export interface Platform {
|
||||
setPaddles?(x: number, y: number, buttons: boolean[]): void;
|
||||
/** HDL platforms: clock cycles since power-up */
|
||||
getClockCount?(): number | undefined;
|
||||
/**
|
||||
* HDL platforms: call `sink` with the design's state before every clock
|
||||
* (null stops). Costs the fast frame path, so only while recording.
|
||||
*/
|
||||
setTraceSink?(sink: ((state: {[name: string]: any}) => void) | null): void;
|
||||
|
||||
startProbing?(): ProbeRecorder;
|
||||
stopProbing?(): void;
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
} from "./baseplatform";
|
||||
import { ProbeAll, SampledAudioParams, TrapCondition } from "./devices";
|
||||
import { History } from "./history";
|
||||
import { VCDWriter } from "./vcd";
|
||||
import type { WaveformMeta } from "./waveform";
|
||||
import { createCore, isRewindable, PlatformFrameInput } from "./platformcore";
|
||||
import { compareTimestamps, Timestamp, timestamp } from "./timeline";
|
||||
import { FileData } from "./workertypes";
|
||||
@@ -177,6 +179,10 @@ export class EmuCore {
|
||||
private recording: History | null = null;
|
||||
// the machine's state was changed from outside the recording
|
||||
private recordingStale = true;
|
||||
/** the VCD recording in progress (see startVcd) */
|
||||
private vcd: VCDWriter | null = null;
|
||||
/** the clocks in a recording that stopped itself (full), until stopVcd collects them */
|
||||
private vcdFinished = 0;
|
||||
private input: PlatformFrameInput | null = null;
|
||||
private probe: ProbeAll | null = null;
|
||||
// frames run on a platform without a timeline
|
||||
@@ -284,6 +290,8 @@ export class EmuCore {
|
||||
* to see their errors.
|
||||
*/
|
||||
async loadROM(data: Uint8Array | object, title = 'ROM') {
|
||||
// a new design has other signals
|
||||
this.stopVcd();
|
||||
await this.platform.loadROM(title, data);
|
||||
this.recordingStale = true;
|
||||
}
|
||||
@@ -451,6 +459,56 @@ export class EmuCore {
|
||||
return typeof p.getSerialOutput === 'function' ? p.getSerialOutput() : null;
|
||||
}
|
||||
|
||||
/** True if the platform can record its signals as a VCD (HDL platforms, once loaded). */
|
||||
get supportsVcd(): boolean {
|
||||
const p: any = this.platform;
|
||||
return typeof p.setTraceSink === 'function' && typeof p.getSignalMetadata === 'function'
|
||||
&& (p.getSignalMetadata() || []).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record every clock's signal changes as a VCD, handing the text to `write`
|
||||
* in chunks as it goes (nothing is kept, so a recording can be as long as
|
||||
* the disk allows). Recording slows the simulation. Stops any recording
|
||||
* already going; returns false if the platform has no signals to record.
|
||||
* If `isFull` is given it is asked before every clock, and the recording
|
||||
* ends itself when it says so (see vcdRunning).
|
||||
*/
|
||||
startVcd(write: (chunk: string) => void, isFull?: () => boolean): boolean {
|
||||
this.stopVcd();
|
||||
if (!this.supportsVcd) return false;
|
||||
const p: any = this.platform;
|
||||
const signals = (p.getSignalMetadata() as WaveformMeta[]).filter(s => s.name).map(s => ({ name: s.name!, len: s.len }));
|
||||
const writer = new VCDWriter(signals, write);
|
||||
this.vcdFinished = 0;
|
||||
p.setTraceSink((state: any) => {
|
||||
// between clocks, so the writer isn't in the middle of a write
|
||||
if (isFull && isFull()) this.vcdFinished = this.finishVcd();
|
||||
else writer.sample(state);
|
||||
});
|
||||
this.vcd = writer;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True while a VCD recording is going (it ends itself if its `isFull` says so). */
|
||||
get vcdRunning(): boolean { return this.vcd != null; }
|
||||
|
||||
/** Finish the recording, if there is one. Returns the number of clocks it holds. */
|
||||
stopVcd(): number {
|
||||
if (this.vcd) return this.finishVcd();
|
||||
const clocks = this.vcdFinished;
|
||||
this.vcdFinished = 0;
|
||||
return clocks;
|
||||
}
|
||||
|
||||
private finishVcd(): number {
|
||||
const writer = this.vcd!;
|
||||
this.vcd = null;
|
||||
(this.platform as any).setTraceSink(null);
|
||||
writer.finish();
|
||||
return writer.samples;
|
||||
}
|
||||
|
||||
/** True if the platform has debug info text to show (see getDebugInfo). */
|
||||
get hasDebugInfo(): boolean {
|
||||
const p: any = this.platform;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Writes signal traces as a VCD (value change dump) file, as a stream: it
|
||||
// keeps only each signal's last value and writes a line when one changes, so
|
||||
// memory use doesn't depend on how long the recording is.
|
||||
|
||||
export interface VcdSignal {
|
||||
/** the key in the state object; a `$` separates module names from the signal's */
|
||||
name: string;
|
||||
/** width in bits */
|
||||
len: number;
|
||||
}
|
||||
|
||||
export interface VcdOptions {
|
||||
/** name of the top-level scope */
|
||||
top?: string;
|
||||
/** one sample is one tick of this; `1 ns` unless the caller knows better */
|
||||
timescale?: string;
|
||||
}
|
||||
|
||||
// flush to `write` once this many characters are waiting
|
||||
const CHUNK_CHARS = 1 << 16;
|
||||
|
||||
/** The nth identifier code: printable ASCII from `!` to `~`, base 94. */
|
||||
export function vcdId(n: number): string {
|
||||
let s = '';
|
||||
do {
|
||||
s = String.fromCharCode(33 + n % 94) + s;
|
||||
n = Math.floor(n / 94) - 1;
|
||||
} while (n >= 0);
|
||||
return s;
|
||||
}
|
||||
|
||||
/** A signal's value as VCD text: `1` for a bit, `b101` for a vector (no leading zeros). */
|
||||
function formatValue(v: number | bigint, len: number, id: string): string {
|
||||
if (len === 1) return (v ? '1' : '0') + id;
|
||||
return 'b' + v.toString(2) + ' ' + id;
|
||||
}
|
||||
|
||||
/** The value as an unsigned number (a bigint past 32 bits), cut to `len` bits. */
|
||||
function normalize(v: any, len: number): number | bigint {
|
||||
if (typeof v === 'bigint') return BigInt.asUintN(len, v);
|
||||
if (typeof v !== 'number') return 0;
|
||||
if (len <= 32) return len === 32 ? v >>> 0 : (v & ((1 << len) - 1)) >>> 0;
|
||||
return BigInt.asUintN(len, BigInt(Math.trunc(v)));
|
||||
}
|
||||
|
||||
export class VCDWriter {
|
||||
private ids: string[];
|
||||
private last: (number | bigint | undefined)[];
|
||||
private buf: string[] = [];
|
||||
private bufChars = 0;
|
||||
/** samples taken so far; the next one is at this time */
|
||||
samples = 0;
|
||||
|
||||
constructor(private signals: VcdSignal[], private write: (chunk: string) => void, opts: VcdOptions = {}) {
|
||||
this.ids = signals.map((_, i) => vcdId(i));
|
||||
this.last = signals.map(() => undefined);
|
||||
this.header(opts);
|
||||
}
|
||||
|
||||
/** Record the signals' values at the next time step; only changes are written. */
|
||||
sample(state: { [name: string]: any }) {
|
||||
const { signals, ids, last } = this;
|
||||
let stamped = false;
|
||||
for (let i = 0; i < signals.length; i++) {
|
||||
const s = signals[i];
|
||||
const v = normalize(state[s.name], s.len);
|
||||
if (v === last[i]) continue;
|
||||
if (!stamped) {
|
||||
// the first sample opens $dumpvars, which gives every signal a value
|
||||
this.put(this.samples === 0 ? '#0\n$dumpvars\n' : '#' + this.samples + '\n');
|
||||
stamped = true;
|
||||
}
|
||||
last[i] = v;
|
||||
this.put(formatValue(v, s.len, ids[i]) + '\n');
|
||||
}
|
||||
if (this.samples === 0) this.put('$end\n');
|
||||
this.samples++;
|
||||
}
|
||||
|
||||
/** Write what's left, with a final time stamp so viewers show the whole recording. */
|
||||
finish() {
|
||||
if (this.samples > 0) this.put('#' + this.samples + '\n');
|
||||
this.flush();
|
||||
}
|
||||
|
||||
private header(opts: VcdOptions) {
|
||||
const out = ['$version 8bitworkshop $end\n', `$timescale ${opts.timescale || '1 ns'} $end\n`];
|
||||
// nest the signals in scopes named by the `$`-separated parts of their names
|
||||
let open: string[] = [];
|
||||
out.push(`$scope module ${opts.top || 'top'} $end\n`);
|
||||
this.signals.forEach((s, i) => {
|
||||
const parts = s.name.split('$');
|
||||
const leaf = parts.pop() || s.name;
|
||||
// a design's own name (the first part) is the top scope already
|
||||
const path = parts.slice(1);
|
||||
let common = 0;
|
||||
while (common < open.length && common < path.length && open[common] === path[common]) common++;
|
||||
for (let j = open.length; j > common; j--) out.push('$upscope $end\n');
|
||||
for (let j = common; j < path.length; j++) out.push(`$scope module ${path[j]} $end\n`);
|
||||
open = path;
|
||||
const range = s.len > 1 ? ` [${s.len - 1}:0]` : '';
|
||||
out.push(`$var wire ${s.len} ${this.ids[i]} ${leaf}${range} $end\n`);
|
||||
});
|
||||
for (let j = open.length; j > 0; j--) out.push('$upscope $end\n');
|
||||
out.push('$upscope $end\n', '$enddefinitions $end\n');
|
||||
this.put(out.join(''));
|
||||
}
|
||||
|
||||
private put(s: string) {
|
||||
this.buf.push(s);
|
||||
this.bufChars += s.length;
|
||||
if (this.bufChars >= CHUNK_CHARS) this.flush();
|
||||
}
|
||||
|
||||
private flush() {
|
||||
if (this.buf.length) this.write(this.buf.join(''));
|
||||
this.buf = [];
|
||||
this.bufChars = 0;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
// Signal traces that a platform (verilog) exposes to a waveform view.
|
||||
|
||||
export interface WaveformMeta {
|
||||
/** the key in the design's state, when it differs from the label (see vcd.ts) */
|
||||
name? : string;
|
||||
label : string;
|
||||
len : number;
|
||||
input : boolean;
|
||||
|
||||
@@ -128,6 +128,7 @@ var VerilogPlatform = function(mainElement, options) {
|
||||
var trace_signals;
|
||||
var trace_buffer;
|
||||
var trace_index;
|
||||
var trace_sink : ((state: {[name: string]: any}) => void) | null = null;
|
||||
|
||||
// for virtual CRT
|
||||
var framex=0;
|
||||
@@ -348,7 +349,7 @@ var VerilogPlatform = function(mainElement, options) {
|
||||
ncycles |= 0;
|
||||
var inspect = inspect_obj != null && inspect_sym != null;
|
||||
// use fast trace buffer-based update?
|
||||
if (sync && !trace && !inspect && (top as HDLModuleTrace).trace != null && scanlineCycles > 0) {
|
||||
if (sync && !trace && !inspect && !trace_sink && (top as HDLModuleTrace).trace != null && scanlineCycles > 0) {
|
||||
this.updateVideoFrameFast((top as any) as HDLModuleTrace);
|
||||
this.updateRecorder();
|
||||
return;
|
||||
@@ -361,6 +362,7 @@ var VerilogPlatform = function(mainElement, options) {
|
||||
this.snapshotTrace();
|
||||
if (trace_index == trace0) trace = false; // kill trace when wraps around
|
||||
}
|
||||
if (trace_sink) trace_sink(top.state);
|
||||
vidtick();
|
||||
if (framex++ < videoWidth) {
|
||||
if (framey < videoHeight) {
|
||||
@@ -514,6 +516,10 @@ var VerilogPlatform = function(mainElement, options) {
|
||||
return (trace_index == 0);
|
||||
}
|
||||
|
||||
setTraceSink(sink: ((state: {[name: string]: any}) => void) | null) {
|
||||
trace_sink = sink;
|
||||
}
|
||||
|
||||
getSignalMetadata() : WaveformMeta[] {
|
||||
return trace_signals;
|
||||
}
|
||||
|
||||
+8
-1
@@ -18,6 +18,7 @@ import { fail, hasOutput, note, output, setJsonMode, setServerMode, warn } from
|
||||
import { EmuTarget, loadPlatform } from './emutarget';
|
||||
import { RUN_SCRIPT_HELP, RunScript, parseNum } from './runscript';
|
||||
import { verifyReplay } from './verifyreplay';
|
||||
import { openVcdFile } from './vcdfile';
|
||||
import { buildDebugContext } from '../common/debugcontroller';
|
||||
import { buildProducts } from '../common/workertypes';
|
||||
import type { BuildInfo } from './debugservice';
|
||||
@@ -317,7 +318,13 @@ async function doRun(args: Args, positional: string[]): Promise<void> {
|
||||
const symbolFile = str(args, 'symbols');
|
||||
if (symbolFile) script.addSymbols(parseSymbolFile(fs.readFileSync(symbolFile, 'utf8')));
|
||||
script.startTracing();
|
||||
script.run(buildScript(args));
|
||||
// `vcd FILE` writes as it goes, so a long recording doesn't sit in memory
|
||||
script.openFile = openVcdFile;
|
||||
try {
|
||||
script.run(buildScript(args));
|
||||
} finally {
|
||||
script.finish();
|
||||
}
|
||||
|
||||
const video = target.getVideo();
|
||||
output({
|
||||
|
||||
@@ -49,6 +49,7 @@ export const RUN_SCRIPT_HELP = [
|
||||
' info - platform/machine debug info',
|
||||
' paddle X Y [BUTTONS] - move the paddle to X,Y (0-255); BUTTONS is a list of 0/1, like 1 0 0',
|
||||
' signals [PATH] - HDL signals under PATH (modules separated by dots; default the top)',
|
||||
' vcd FILE [MAXMB] | vcd off - record every clock\'s signal changes to a VCD file; .gz compresses; stops at MAXMB (default 1024, 0 = no limit); 8bws only',
|
||||
' reset - reset the emulator',
|
||||
' echo TEXT - print message',
|
||||
'(ADDR: number or symbol name; KEY: char, ENTER/SPACE/arrows/F1.., $hex;',
|
||||
@@ -160,6 +161,12 @@ export class RunScript {
|
||||
symbols: SymbolMap = {};
|
||||
private addr2symbol: { [addr: number]: string } = {};
|
||||
private probe: ProbeRecorder | null = null;
|
||||
/**
|
||||
* Opens the file for `vcd FILE`. Only the CLI sets this: scripts that come
|
||||
* from elsewhere (the debug console, language model tools) can't write files.
|
||||
*/
|
||||
openFile?: (path: string, maxBytes?: number) => { write(chunk: string): void; close(): void; readonly full: boolean };
|
||||
private vcdFile: { path: string, close(): void, full: boolean } | null = null;
|
||||
|
||||
constructor(readonly target: EmuTarget, private out = write,
|
||||
readonly debug = new DebugController(target)) { }
|
||||
@@ -186,6 +193,11 @@ export class RunScript {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Finish what the script left open (a VCD recording). Call when it is done. */
|
||||
finish() {
|
||||
if (this.vcdFile) this.cmdVcd(['vcd', 'off']);
|
||||
}
|
||||
|
||||
run(script: string) {
|
||||
for (let line of script.split(/\r?\n|;/)) {
|
||||
line = line.trim();
|
||||
@@ -535,6 +547,28 @@ export class RunScript {
|
||||
for (const e of treeChildren(root, path)) this.out(`${e.name}${e.expandable ? '/' : ''} ${e.value}\n`);
|
||||
}
|
||||
|
||||
cmdVcd(tokens: string[]) {
|
||||
if (tokens[1] === 'off' || tokens[1] === 'stop') {
|
||||
const file = this.vcdFile;
|
||||
if (!file) throw new Error('no VCD recording is on');
|
||||
this.vcdFile = null;
|
||||
const clocks = this.target.stopVcd();
|
||||
file.close();
|
||||
this.out(`wrote ${clocks} clocks to ${file.path}${file.full ? ' (stopped at the size limit)' : ''}\n`);
|
||||
return;
|
||||
}
|
||||
if (!tokens[1]) throw new Error('usage: vcd FILE [MAXMB] | vcd off');
|
||||
if (!this.openFile) throw new Error("'vcd' is not available here");
|
||||
if (!this.target.supportsVcd) throw new Error(`'${this.target.id}' has no signals to record`);
|
||||
this.finish();
|
||||
// MAXMB: stop at about this size; 0 for no limit
|
||||
const maxMB = tokens[2] != null ? parseFloat(tokens[2]) : NaN;
|
||||
if (tokens[2] != null && !(maxMB >= 0)) throw new Error('usage: vcd FILE [MAXMB] | vcd off');
|
||||
const file = this.openFile(tokens[1], isNaN(maxMB) ? undefined : maxMB * 1048576);
|
||||
this.target.startVcd(chunk => file.write(chunk), () => file.full);
|
||||
this.vcdFile = { path: tokens[1], close: () => file.close(), get full() { return file.full; } };
|
||||
}
|
||||
|
||||
cmdReset() { this.target.reset(); this.log('reset'); }
|
||||
cmdEcho(tokens: string[], line: string) { this.out(line.substring(tokens[0].length).trim() + '\n'); }
|
||||
}
|
||||
@@ -566,6 +600,7 @@ const COMMANDS: { [name: string]: Command } = {
|
||||
'pc': RunScript.prototype.cmdPC,
|
||||
'info': RunScript.prototype.cmdInfo,
|
||||
'signals': RunScript.prototype.cmdSignals,
|
||||
'vcd': RunScript.prototype.cmdVcd,
|
||||
'paddle': RunScript.prototype.cmdPaddle,
|
||||
'reset': RunScript.prototype.cmdReset,
|
||||
'back': RunScript.prototype.cmdBack,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// The file a VCD recording streams to (see EmuCore.startVcd). Node only.
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
export interface VcdFile {
|
||||
write(chunk: string): void;
|
||||
close(): void;
|
||||
/** bytes written to the file so far (after compression) */
|
||||
readonly bytes: number;
|
||||
/** `maxBytes` has been reached: the recording should stop */
|
||||
readonly full: boolean;
|
||||
}
|
||||
|
||||
/** The most the CLI writes unless told otherwise, about 400 frames of a design with video. */
|
||||
export const DEFAULT_VCD_MAX_BYTES = 1 << 30;
|
||||
|
||||
/**
|
||||
* Open `file` for a recording. A name ending in `.gz` is gzipped as it goes:
|
||||
* each chunk is its own gzip member, which gunzip reads as one stream, so
|
||||
* nothing needs flushing at the end. VCD compresses about 4x.
|
||||
* `maxBytes` (0 for no limit) sets when `full` turns true; the file goes a
|
||||
* little past it, since the recording stops between clocks.
|
||||
*/
|
||||
export function openVcdFile(file: string, maxBytes = DEFAULT_VCD_MAX_BYTES): VcdFile {
|
||||
const fd = fs.openSync(file, 'w');
|
||||
const gzip = /\.gz$/i.test(file);
|
||||
const f = {
|
||||
bytes: 0,
|
||||
get full() { return maxBytes > 0 && f.bytes >= maxBytes; },
|
||||
write(chunk: string) {
|
||||
const data = gzip ? zlib.gzipSync(chunk, { level: 1 }) : Buffer.from(chunk);
|
||||
fs.writeSync(fd, data);
|
||||
f.bytes += data.length;
|
||||
},
|
||||
close() { fs.closeSync(fd); },
|
||||
};
|
||||
return f;
|
||||
}
|
||||
@@ -272,6 +272,41 @@ describe('8bws CLI', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('run --platform verilog: vcd', function () {
|
||||
var dir;
|
||||
before(function () { dir = fs.mkdtempSync(path.join(os.tmpdir(), '8bws-vcd-')); });
|
||||
it('should stream signal changes to a VCD file, and gzip a .gz name', function () {
|
||||
var plain = path.join(dir, 'a.vcd'), gz = path.join(dir, 'a.vcd.gz');
|
||||
var out = cli('run', 'presets/verilog/ball_paddle.v', '-e', `run 1; vcd ${plain}; run 1; vcd off; vcd ${gz}; run 1; vcd off`);
|
||||
assert.ok(/wrote \d+ clocks to .*a\.vcd\b/.test(out), out);
|
||||
var text = fs.readFileSync(plain, 'utf8');
|
||||
assert.ok(text.startsWith('$version'), text.slice(0, 80));
|
||||
assert.ok(/\$var wire 9 \S+ hpos \[8:0\] \$end/.test(text));
|
||||
assert.ok(/^#0\n\$dumpvars$/m.test(text));
|
||||
var unzipped = require('zlib').gunzipSync(fs.readFileSync(gz)).toString('utf8');
|
||||
assert.ok(unzipped.startsWith('$version'));
|
||||
assert.ok(fs.statSync(gz).size < unzipped.length / 2, 'gzip shrinks it');
|
||||
});
|
||||
it('should close the file even if the script forgets `vcd off`', function () {
|
||||
var f = path.join(dir, 'b.vcd');
|
||||
cli('run', 'presets/verilog/ball_paddle.v', '-e', `vcd ${f}; run 1`);
|
||||
assert.ok(/\n#\d+\n$/.test(fs.readFileSync(f, 'utf8')), 'ends with a final time stamp');
|
||||
});
|
||||
it('should stop at the size limit, between clocks', function () {
|
||||
var f = path.join(dir, 'd.vcd');
|
||||
var out = cli('run', 'presets/verilog/ball_paddle.v', '-e', `vcd ${f} 0.5; run 2; vcd off`);
|
||||
assert.ok(/stopped at the size limit/.test(out), out);
|
||||
var size = fs.statSync(f).size;
|
||||
// about 2.7MB a frame, so the half megabyte is passed by a chunk or two, not by the two frames
|
||||
assert.ok(size >= 0.5 * 1048576 && size < 0.7 * 1048576, 'size ' + size);
|
||||
assert.ok(/\n#\d+\n$/.test(fs.readFileSync(f, 'utf8')), 'still ends cleanly');
|
||||
});
|
||||
it('should say so when the platform has no signals', function () {
|
||||
var e = cliFails('run', '--platform', 'apple2', '-e', `vcd ${path.join(dir, 'c.vcd')}`, 'test/roms/apple2/cosmic.c.rom');
|
||||
assert.ok(/no signals to record/.test(e.stdout + e.stderr), e.stdout + e.stderr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run: emulator control', function () {
|
||||
// these all reach through the Platform to its Machine (common/devices.ts)
|
||||
it('should reject an unknown platform', function () {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import { VCDWriter, vcdId } from "../../src/common/vcd";
|
||||
|
||||
function record(signals: { name: string, len: number }[], states: any[]) {
|
||||
const chunks: string[] = [];
|
||||
const w = new VCDWriter(signals, c => chunks.push(c));
|
||||
for (const s of states) w.sample(s);
|
||||
w.finish();
|
||||
return { text: chunks.join(''), chunks, w };
|
||||
}
|
||||
|
||||
describe('VCD writer', function () {
|
||||
it('makes identifier codes from printable characters', function () {
|
||||
assert.equal(vcdId(0), '!');
|
||||
assert.equal(vcdId(93), '~');
|
||||
assert.equal(vcdId(94), '!!');
|
||||
const ids = new Set(Array.from({ length: 20000 }, (_, i) => vcdId(i)));
|
||||
assert.equal(ids.size, 20000);
|
||||
for (const id of ids) assert.ok(/^[!-~]+$/.test(id));
|
||||
});
|
||||
|
||||
it('declares signals in scopes named by their `$` paths', function () {
|
||||
const { text } = record([
|
||||
{ name: 'top$clk', len: 1 },
|
||||
{ name: 'top$cpu$pc', len: 16 },
|
||||
{ name: 'top$cpu$alu$out', len: 8 },
|
||||
{ name: 'top$ram$we', len: 1 },
|
||||
], [{}]);
|
||||
const defs = text.slice(0, text.indexOf('$enddefinitions'));
|
||||
assert.deepStrictEqual(defs.split('\n').filter(l => /^\$(scope|upscope|var)/.test(l)), [
|
||||
'$scope module top $end',
|
||||
'$var wire 1 ! clk $end',
|
||||
'$scope module cpu $end',
|
||||
'$var wire 16 " pc [15:0] $end',
|
||||
'$scope module alu $end',
|
||||
'$var wire 8 # out [7:0] $end',
|
||||
'$upscope $end',
|
||||
'$upscope $end',
|
||||
'$scope module ram $end',
|
||||
'$var wire 1 $ we $end',
|
||||
'$upscope $end',
|
||||
'$upscope $end',
|
||||
]);
|
||||
});
|
||||
|
||||
it('dumps every value first, then only changes', function () {
|
||||
const { text } = record([{ name: 'm$a', len: 1 }, { name: 'm$b', len: 4 }], [
|
||||
{ m$a: 0, m$b: 5 },
|
||||
{ m$a: 0, m$b: 5 }, // nothing changed: no time stamp
|
||||
{ m$a: 1, m$b: 5 },
|
||||
{ m$a: 1, m$b: 0 },
|
||||
]);
|
||||
const body = text.slice(text.indexOf('$enddefinitions'));
|
||||
assert.equal(body.split('\n').slice(1).join('\n'), [
|
||||
'#0', '$dumpvars', '0!', 'b101 "', '$end',
|
||||
'#2', '1!',
|
||||
'#3', 'b0 "',
|
||||
'#4', '',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('cuts values to the signal width and reads bigints', function () {
|
||||
const { text } = record([
|
||||
{ name: 'm$n', len: 4 }, { name: 'm$s', len: 8 }, { name: 'm$w', len: 40 }, { name: 'm$x', len: 32 },
|
||||
], [{ m$n: 0x1f, m$s: -1, m$w: BigInt("0x123456789a"), m$x: -1 }]);
|
||||
assert.ok(text.includes('b1111 !\n'));
|
||||
assert.ok(text.includes('b11111111 "\n'));
|
||||
assert.ok(text.includes('b' + BigInt("0x123456789a").toString(2) + ' #\n'));
|
||||
assert.ok(text.includes('b' + '1'.repeat(32) + ' $\n'));
|
||||
});
|
||||
|
||||
it('writes in chunks, so a long recording is never held whole', function () {
|
||||
const chunks: string[] = [];
|
||||
const w = new VCDWriter([{ name: 'm$c', len: 16 }], c => chunks.push(c));
|
||||
for (let i = 0; i < 100000; i++) w.sample({ m$c: i });
|
||||
w.finish();
|
||||
assert.ok(chunks.length > 10, `${chunks.length} chunks`);
|
||||
assert.equal(w.samples, 100000);
|
||||
const all = chunks.join('');
|
||||
assert.ok(all.endsWith('#100000\n'));
|
||||
assert.equal(all.match(/^#/gm)!.length, 100001);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user