feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer- wins, so a real multi-driver bus works: many chips on one data line, only the enabled one drives, the rest release to Hi-Z. - busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest driver wins; equal strength + opposite = X (contention); no driver = Z; pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong, VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real tri-state), VX_INPUT_PULLUP/DOWN -> pull. - busNets.ts: per-net driver registry; resolves and pushes the resolved level into PinManager; warns once on contention. - syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys. - ChipRuntime.ts: pin register/write/set_mode route bus-net pins through busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep the legacy path; dispose releases the chip's bus drivers. SPICE source emission is skipped for bus pins (digital fast path beside SPICE). Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention), and the two-real-chip integration now exchanges 0xA5 through the registry. Full suite 2074 pass with the flag off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
474d132368
commit
23516247a6
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Phase 1 — 4-valued logic + drive-strength resolution (project/multichip-bus/).
|
||||
* Covers the exit criteria from 03-phases.md: a shared net with a pull-up and
|
||||
* two tri-state drivers resolves to 0/1/Z/X per the rules; contention -> X.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
resolveNet,
|
||||
modeToDrive,
|
||||
resolvedToBool,
|
||||
Strength,
|
||||
HIGHZ_DRIVE,
|
||||
type Drive,
|
||||
} from '../simulation/customChips/busLogic';
|
||||
|
||||
const strong = (v: 0 | 1): Drive => ({ value: v, strength: Strength.STRONG });
|
||||
const pull = (v: 0 | 1): Drive => ({ value: v, strength: Strength.PULL });
|
||||
const supply = (v: 0 | 1): Drive => ({ value: v, strength: Strength.SUPPLY });
|
||||
|
||||
describe('busLogic — resolveNet', () => {
|
||||
it('no drivers -> Z (floating)', () => {
|
||||
expect(resolveNet([]).v).toBe('Z');
|
||||
expect(resolveNet([HIGHZ_DRIVE, HIGHZ_DRIVE]).v).toBe('Z');
|
||||
});
|
||||
|
||||
it('a single strong driver wins', () => {
|
||||
expect(resolveNet([strong(1)]).v).toBe('1');
|
||||
expect(resolveNet([strong(0)]).v).toBe('0');
|
||||
});
|
||||
|
||||
it('any real driver overrides Z', () => {
|
||||
expect(resolveNet([strong(1), HIGHZ_DRIVE, HIGHZ_DRIVE]).v).toBe('1');
|
||||
});
|
||||
|
||||
it('strong beats pull', () => {
|
||||
expect(resolveNet([strong(0), pull(1)]).v).toBe('0');
|
||||
expect(resolveNet([pull(0), strong(1)]).v).toBe('1');
|
||||
});
|
||||
|
||||
it('a lone pull resistor wins over all-Z (floating bus reads the pull)', () => {
|
||||
expect(resolveNet([pull(1), HIGHZ_DRIVE, HIGHZ_DRIVE]).v).toBe('1');
|
||||
expect(resolveNet([pull(0)]).v).toBe('0');
|
||||
});
|
||||
|
||||
it('supply beats strong', () => {
|
||||
expect(resolveNet([supply(1), strong(0)]).v).toBe('1');
|
||||
});
|
||||
|
||||
it('two strong drivers that agree -> that value (no contention)', () => {
|
||||
expect(resolveNet([strong(1), strong(1)]).v).toBe('1');
|
||||
});
|
||||
|
||||
it('two strong drivers that disagree -> X (contention)', () => {
|
||||
const r = resolveNet([strong(0), strong(1)]);
|
||||
expect(r.v).toBe('X');
|
||||
expect(r.strength).toBe(Strength.STRONG);
|
||||
});
|
||||
|
||||
it('reports the winning strength', () => {
|
||||
expect(resolveNet([pull(1)]).strength).toBe(Strength.PULL);
|
||||
expect(resolveNet([strong(1)]).strength).toBe(Strength.STRONG);
|
||||
expect(resolveNet([]).strength).toBe(Strength.HIGHZ);
|
||||
});
|
||||
|
||||
// The Phase 1 exit scenario: a data bus with a pull-up + two tri-state drivers.
|
||||
it('tri-state bus: only the enabled driver drives; releasing falls to the pull-up', () => {
|
||||
const pullup = pull(1);
|
||||
// Driver A enabled to 0, driver B released -> bus reads 0.
|
||||
expect(resolveNet([pullup, strong(0), HIGHZ_DRIVE]).v).toBe('0');
|
||||
// Both drivers released -> bus floats up to the pull-up -> 1.
|
||||
expect(resolveNet([pullup, HIGHZ_DRIVE, HIGHZ_DRIVE]).v).toBe('1');
|
||||
// Both drivers enabled to OPPOSITE values -> contention -> X.
|
||||
expect(resolveNet([pullup, strong(0), strong(1)]).v).toBe('X');
|
||||
});
|
||||
});
|
||||
|
||||
describe('busLogic — modeToDrive', () => {
|
||||
it('outputs drive strong at their value', () => {
|
||||
expect(modeToDrive(1 /* OUTPUT */, 1)).toEqual({ value: 1, strength: Strength.STRONG });
|
||||
expect(modeToDrive(16 /* OUTPUT_LOW */, 0)).toEqual({ value: 0, strength: Strength.STRONG });
|
||||
expect(modeToDrive(17 /* OUTPUT_HIGH */, 1)).toEqual({ value: 1, strength: Strength.STRONG });
|
||||
});
|
||||
|
||||
it('plain input / analog release the bus (Hi-Z)', () => {
|
||||
expect(modeToDrive(0 /* INPUT */, 1).strength).toBe(Strength.HIGHZ);
|
||||
expect(modeToDrive(4 /* ANALOG */, 0).strength).toBe(Strength.HIGHZ);
|
||||
});
|
||||
|
||||
it('pull-up / pull-down contribute a pull-strength level', () => {
|
||||
expect(modeToDrive(2 /* INPUT_PULLUP */, 0)).toEqual({ value: 1, strength: Strength.PULL });
|
||||
expect(modeToDrive(3 /* INPUT_PULLDOWN */, 1)).toEqual({ value: 0, strength: Strength.PULL });
|
||||
});
|
||||
});
|
||||
|
||||
describe('busLogic — resolvedToBool', () => {
|
||||
it('only a driven 1 is high; 0/Z/X read low', () => {
|
||||
expect(resolvedToBool(resolveNet([strong(1)]))).toBe(true);
|
||||
expect(resolvedToBool(resolveNet([strong(0)]))).toBe(false);
|
||||
expect(resolvedToBool(resolveNet([]))).toBe(false); // Z
|
||||
expect(resolvedToBool(resolveNet([strong(0), strong(1)]))).toBe(false); // X
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Phase 1 — multi-driver bus net registry (project/multichip-bus/). Drives the
|
||||
* busNets registry against a real PinManager: tri-state release, pull-up
|
||||
* fallback, contention warning, and per-chip driver teardown.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { PinManager } from '../simulation/PinManager';
|
||||
import {
|
||||
setBusDrive,
|
||||
clearBusDriversForChip,
|
||||
resetBusNets,
|
||||
} from '../simulation/customChips/busNets';
|
||||
import { Strength, HIGHZ_DRIVE, type Drive } from '../simulation/customChips/busLogic';
|
||||
|
||||
const NET = 100500; // a stand-in syntheticNetPin key
|
||||
const strong = (v: 0 | 1): Drive => ({ value: v, strength: Strength.STRONG });
|
||||
const pull = (v: 0 | 1): Drive => ({ value: v, strength: Strength.PULL });
|
||||
|
||||
describe('busNets — multi-driver resolution into PinManager', () => {
|
||||
let pm: PinManager;
|
||||
beforeEach(() => {
|
||||
resetBusNets();
|
||||
pm = new PinManager();
|
||||
});
|
||||
afterEach(() => {
|
||||
resetBusNets();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('a single strong driver sets the net level', () => {
|
||||
setBusDrive(pm, NET, 'cpu::D0', strong(1));
|
||||
expect(pm.getPinState(NET)).toBe(true);
|
||||
setBusDrive(pm, NET, 'cpu::D0', strong(0));
|
||||
expect(pm.getPinState(NET)).toBe(false);
|
||||
});
|
||||
|
||||
it('tri-state hand-off: the releasing driver yields to the active one', () => {
|
||||
setBusDrive(pm, NET, 'rom::D0', strong(1)); // ROM drives 1
|
||||
setBusDrive(pm, NET, 'ram::D0', HIGHZ_DRIVE); // RAM released
|
||||
expect(pm.getPinState(NET)).toBe(true);
|
||||
|
||||
// ROM releases, RAM drives 0 -> bus follows RAM.
|
||||
setBusDrive(pm, NET, 'rom::D0', HIGHZ_DRIVE);
|
||||
setBusDrive(pm, NET, 'ram::D0', strong(0));
|
||||
expect(pm.getPinState(NET)).toBe(false);
|
||||
});
|
||||
|
||||
it('a pull-up holds the bus high only while every driver is released', () => {
|
||||
setBusDrive(pm, NET, 'pullup::D0', pull(1));
|
||||
setBusDrive(pm, NET, 'drv::D0', HIGHZ_DRIVE);
|
||||
expect(pm.getPinState(NET)).toBe(true); // floats up to the pull-up
|
||||
|
||||
setBusDrive(pm, NET, 'drv::D0', strong(0)); // a strong driver beats the pull
|
||||
expect(pm.getPinState(NET)).toBe(false);
|
||||
|
||||
setBusDrive(pm, NET, 'drv::D0', HIGHZ_DRIVE); // released again -> pull-up wins
|
||||
expect(pm.getPinState(NET)).toBe(true);
|
||||
});
|
||||
|
||||
it('contention (two strong, opposite) warns once and resolves low (X)', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
setBusDrive(pm, NET, 'a::D0', strong(0));
|
||||
setBusDrive(pm, NET, 'b::D0', strong(1)); // now in contention
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
expect(pm.getPinState(NET)).toBe(false); // X projects to low
|
||||
|
||||
// Re-resolving while still in contention does not warn again.
|
||||
setBusDrive(pm, NET, 'a::D0', strong(0));
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clearing the contention and re-entering it warns afresh.
|
||||
setBusDrive(pm, NET, 'b::D0', HIGHZ_DRIVE);
|
||||
setBusDrive(pm, NET, 'b::D0', strong(1));
|
||||
expect(warn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('clearBusDriversForChip releases that chip across every net', () => {
|
||||
setBusDrive(pm, NET, 'rom::D0', strong(1));
|
||||
setBusDrive(pm, NET, 'ram::D0', HIGHZ_DRIVE);
|
||||
expect(pm.getPinState(NET)).toBe(true);
|
||||
|
||||
clearBusDriversForChip(pm, 'rom'); // ROM removed -> only RAM's Hi-Z left
|
||||
expect(pm.getPinState(NET)).toBe(false); // floats (Z) -> low
|
||||
});
|
||||
|
||||
it('a reader notified via onPinChange sees the resolved level synchronously', () => {
|
||||
let seen: boolean | null = null;
|
||||
pm.onPinChange(NET, (_p, s) => {
|
||||
seen = s;
|
||||
});
|
||||
setBusDrive(pm, NET, 'drv::D0', strong(1));
|
||||
expect(seen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -82,10 +82,22 @@ describe.skipIf(!haveFixtures)('chipbus Phase 0 — two REAL chips exchange a by
|
|||
...range(8).map((i) => [`OUT${i}`, pinKey('reader', `OUT${i}`)] as [string, number]),
|
||||
]);
|
||||
|
||||
const driver = await ChipInstance.create({ wasm: driverWasm, pinManager: pm, wires: driverWires });
|
||||
// componentId must be distinct so each chip's bus drivers are keyed apart
|
||||
// (the real app always passes it; busNets keys drivers by `${id}::${pin}`).
|
||||
const driver = await ChipInstance.create({
|
||||
wasm: driverWasm,
|
||||
componentId: 'driver',
|
||||
pinManager: pm,
|
||||
wires: driverWires,
|
||||
});
|
||||
driver.start(); // chip_setup drives 0xA5 onto the shared D0..D7 net keys.
|
||||
|
||||
const reader = await ChipInstance.create({ wasm: readerWasm, pinManager: pm, wires: readerWires });
|
||||
const reader = await ChipInstance.create({
|
||||
wasm: readerWasm,
|
||||
componentId: 'reader',
|
||||
pinManager: pm,
|
||||
wires: readerWires,
|
||||
});
|
||||
reader.start();
|
||||
|
||||
// Fire the reader's 1ms polling timer (advance sim time well past it).
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import type { I2CBusManager } from '../I2CBusManager';
|
|||
import { SPIBus, SPIDevice } from './SPIBus';
|
||||
import { WasiShim, type SimNanosFn, type WriteStdoutFn } from './WasiShim';
|
||||
import { setChipPinDrive } from './chipPinDrives';
|
||||
import { isSyntheticChipPin } from './syntheticPins';
|
||||
import { isSyntheticChipPin, isSyntheticNetPin } from './syntheticPins';
|
||||
import { requestElectricalResolve } from '../spice/electricalResolveHook';
|
||||
import { chipBusEnabled } from './chipNets';
|
||||
import { setBusDrive, clearBusDriversForChip } from './busNets';
|
||||
import { modeToDrive } from './busLogic';
|
||||
|
||||
function readCString(memory: WebAssembly.Memory, ptr: number): string {
|
||||
const u8 = new Uint8Array(memory.buffer);
|
||||
|
|
@ -94,6 +97,9 @@ interface PinEntry {
|
|||
name: string;
|
||||
mode: number;
|
||||
arduinoPin: number | null;
|
||||
/** Last level written/initialized — used to compute the bus drive on a mode
|
||||
* flip (e.g. OUTPUT -> INPUT releases the bus without forgetting the level). */
|
||||
value: 0 | 1;
|
||||
}
|
||||
|
||||
interface AttrEntry {
|
||||
|
|
@ -292,6 +298,9 @@ export class ChipInstance {
|
|||
for (const d of this.spiDevices) this.spiBus.removeDevice(d.device);
|
||||
}
|
||||
this.spiDevices = [];
|
||||
// Stop driving any bus nets this chip contributed to, then re-resolve them
|
||||
// so a removed chip releases the bus (its drivers no longer count).
|
||||
if (this.componentId) clearBusDriversForChip(this.pinManager, this.componentId);
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
|
|
@ -364,7 +373,28 @@ export class ChipInstance {
|
|||
* on the net) are emitted as chip sources; a chip pin wired to a real board
|
||||
* pin is already driven by that board's voltage source.
|
||||
*/
|
||||
/** True if this pin sits on a multi-chip BUS net (Phase 1): its key is a
|
||||
* syntheticNetPin and the chipbus flag is on. Such pins resolve through the
|
||||
* driver-strength registry (busNets) instead of last-writer-wins PinManager. */
|
||||
private _isBusPin(p: PinEntry): boolean {
|
||||
return p.arduinoPin != null && chipBusEnabled() && isSyntheticNetPin(p.arduinoPin);
|
||||
}
|
||||
|
||||
/** Register this pin's current (mode, value) as a bus driver and re-resolve. */
|
||||
private _busDrive(p: PinEntry): void {
|
||||
if (p.arduinoPin == null) return;
|
||||
setBusDrive(
|
||||
this.pinManager,
|
||||
p.arduinoPin,
|
||||
`${this.componentId}::${p.name}`,
|
||||
modeToDrive(p.mode, p.value),
|
||||
);
|
||||
}
|
||||
|
||||
private _syncSpiceDrive(p: PinEntry): void {
|
||||
// A bus net is served by the digital driver-strength path; emitting a SPICE
|
||||
// chip source per chip on the same net would create false analog contention.
|
||||
if (this._isBusPin(p)) return;
|
||||
if (!this.componentId || !p.name) return;
|
||||
if (p.arduinoPin == null || !isSyntheticChipPin(p.arduinoPin)) return;
|
||||
const isOutput =
|
||||
|
|
@ -383,9 +413,12 @@ export class ChipInstance {
|
|||
const name = readCString(this.memory!, namePtr);
|
||||
const handle = this.pins.length;
|
||||
const arduinoPin = this.wires.has(name) ? this.wires.get(name)! : null;
|
||||
const p: PinEntry = { name, mode, arduinoPin };
|
||||
const value: 0 | 1 = mode === ChipInstance.MODE_OUTPUT_HIGH ? 1 : 0;
|
||||
const p: PinEntry = { name, mode, arduinoPin, value };
|
||||
this.pins.push(p);
|
||||
if (arduinoPin != null) {
|
||||
if (this._isBusPin(p)) {
|
||||
this._busDrive(p);
|
||||
} else if (arduinoPin != null) {
|
||||
if (mode === ChipInstance.MODE_OUTPUT_LOW) this.pinManager.triggerPinChange(arduinoPin, false);
|
||||
if (mode === ChipInstance.MODE_OUTPUT_HIGH) this.pinManager.triggerPinChange(arduinoPin, true);
|
||||
}
|
||||
|
|
@ -402,7 +435,12 @@ export class ChipInstance {
|
|||
private _pin_write(handle: number, value: number): void {
|
||||
const p = this.pins[handle];
|
||||
if (!p || p.arduinoPin == null) return;
|
||||
this.pinManager.triggerPinChange(p.arduinoPin, value !== 0);
|
||||
p.value = value !== 0 ? 1 : 0;
|
||||
if (this._isBusPin(p)) {
|
||||
this._busDrive(p);
|
||||
} else {
|
||||
this.pinManager.triggerPinChange(p.arduinoPin, value !== 0);
|
||||
}
|
||||
this._syncSpiceDrive(p);
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +460,12 @@ export class ChipInstance {
|
|||
const p = this.pins[handle];
|
||||
if (!p) return;
|
||||
p.mode = mode;
|
||||
if (p.arduinoPin != null) {
|
||||
// OUTPUT_LOW/HIGH carry an initial level; plain OUTPUT keeps the last value.
|
||||
if (mode === ChipInstance.MODE_OUTPUT_LOW) p.value = 0;
|
||||
if (mode === ChipInstance.MODE_OUTPUT_HIGH) p.value = 1;
|
||||
if (this._isBusPin(p)) {
|
||||
this._busDrive(p);
|
||||
} else if (p.arduinoPin != null) {
|
||||
if (mode === ChipInstance.MODE_OUTPUT_LOW) this.pinManager.triggerPinChange(p.arduinoPin, false);
|
||||
if (mode === ChipInstance.MODE_OUTPUT_HIGH) this.pinManager.triggerPinChange(p.arduinoPin, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 4-valued logic + drive-strength resolution for chip-to-chip bus nets.
|
||||
* Phase 1 of the multi-chip digital bus track (project/multichip-bus/).
|
||||
*
|
||||
* A real wire is one of four states (the IEEE 4-state model): 0, 1, Z (high
|
||||
* impedance / floating / tri-stated), X (unknown — typically two drivers
|
||||
* fighting). Phase 0 gave every endpoint of a chip-to-chip net ONE shared key;
|
||||
* this module turns the "release the bus by switching to VX_INPUT" idiom that
|
||||
* rom-32k/ram-64k/8255 already use into REAL tri-state, so a multi-driver bus
|
||||
* (many chips, one data line, only the enabled one drives) resolves correctly.
|
||||
*
|
||||
* Resolution is by (value, strength), not value alone (01-how-proteus-works.md
|
||||
* section 3): strongest driver wins; equal strength + opposite values = X
|
||||
* (contention); no real driver = Z (floating); a pull resistor is a `pull`
|
||||
* driver that loses to `strong` but beats all-Z.
|
||||
*
|
||||
* Single-bit by design: a net is one wire. 3vl (packed 0/1/X vectors) is for
|
||||
* multi-bit bus values and waveform display in a later phase; bit-level driver
|
||||
* resolution needs Z + strength, which 3vl does not model, so it is a small
|
||||
* self-contained fold here.
|
||||
*/
|
||||
|
||||
export type V4 = '0' | '1' | 'Z' | 'X';
|
||||
|
||||
/** Verilog-style drive strengths — only the few that matter for digital buses. */
|
||||
export enum Strength {
|
||||
HIGHZ = 0, // not driving (a tri-stated / input pin contributes this)
|
||||
PULL = 1, // pull-up / pull-down resistor, or INPUT_PULLUP/PULLDOWN
|
||||
STRONG = 2, // a normal gate / chip output (the default for VX_OUTPUT)
|
||||
SUPPLY = 3, // a hard power / ground rail
|
||||
}
|
||||
|
||||
/** One driver's contribution to a net. `HIGHZ` strength means "not driving". */
|
||||
export interface Drive {
|
||||
value: 0 | 1;
|
||||
strength: Strength;
|
||||
}
|
||||
|
||||
export const HIGHZ_DRIVE: Drive = Object.freeze({ value: 0, strength: Strength.HIGHZ });
|
||||
|
||||
export interface Resolved {
|
||||
v: V4;
|
||||
/** Strength of the winning driver(s); HIGHZ when the net is floating (Z). */
|
||||
strength: Strength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a net from its drivers' contributions:
|
||||
* - keep the drivers of maximum real strength;
|
||||
* - all agree -> that value at that strength;
|
||||
* - disagree -> X (contention) at that strength;
|
||||
* - no real driver -> Z at HIGHZ.
|
||||
*/
|
||||
export function resolveNet(drives: Iterable<Drive>): Resolved {
|
||||
let maxS: Strength = Strength.HIGHZ;
|
||||
let val: 0 | 1 | null = null;
|
||||
let contention = false;
|
||||
|
||||
for (const d of drives) {
|
||||
if (d.strength <= Strength.HIGHZ) continue; // tri-stated: contributes nothing
|
||||
if (d.strength > maxS) {
|
||||
maxS = d.strength;
|
||||
val = d.value;
|
||||
contention = false;
|
||||
} else if (d.strength === maxS) {
|
||||
if (val === null) val = d.value;
|
||||
else if (val !== d.value) contention = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (val === null) return { v: 'Z', strength: Strength.HIGHZ };
|
||||
if (contention) return { v: 'X', strength: maxS };
|
||||
return { v: val === 1 ? '1' : '0', strength: maxS };
|
||||
}
|
||||
|
||||
// Chip pin modes (mirror ChipInstance MODE_* / velxio-chip.h vx_pin_mode).
|
||||
const VX_INPUT = 0;
|
||||
const VX_OUTPUT = 1;
|
||||
const VX_INPUT_PULLUP = 2;
|
||||
const VX_INPUT_PULLDOWN = 3;
|
||||
const VX_ANALOG = 4;
|
||||
const VX_OUTPUT_LOW = 16;
|
||||
const VX_OUTPUT_HIGH = 17;
|
||||
|
||||
/**
|
||||
* Map a chip pin's mode + its last-written level to a bus Drive.
|
||||
* OUTPUT / OUTPUT_LOW / OUTPUT_HIGH -> strong drive of `value`
|
||||
* INPUT_PULLUP -> pull 1 ; INPUT_PULLDOWN -> pull 0
|
||||
* INPUT / ANALOG -> Hi-Z (release the bus)
|
||||
* This makes "switch to VX_INPUT" mean "stop driving" — real tri-state for free.
|
||||
*/
|
||||
export function modeToDrive(mode: number, value: 0 | 1): Drive {
|
||||
switch (mode) {
|
||||
case VX_OUTPUT:
|
||||
case VX_OUTPUT_LOW:
|
||||
case VX_OUTPUT_HIGH:
|
||||
return { value, strength: Strength.STRONG };
|
||||
case VX_INPUT_PULLUP:
|
||||
return { value: 1, strength: Strength.PULL };
|
||||
case VX_INPUT_PULLDOWN:
|
||||
return { value: 0, strength: Strength.PULL };
|
||||
case VX_INPUT:
|
||||
case VX_ANALOG:
|
||||
default:
|
||||
return HIGHZ_DRIVE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a resolved 4-value onto the boolean PinManager carries. A driven 1 is
|
||||
* high; everything else (0, floating Z with no pull, contention X) reads low.
|
||||
* Z and X are surfaced separately (see resolveNet) for warnings / diagnostics;
|
||||
* callers that care about float-vs-low inspect the V4, not this boolean.
|
||||
*/
|
||||
export function resolvedToBool(r: Resolved): boolean {
|
||||
return r.v === '1';
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Multi-driver bus net registry — Phase 1 of the multi-chip digital bus track
|
||||
* (project/multichip-bus/). Sits between the chip runtime and the PinManager
|
||||
* for chip-to-chip BUS net keys (syntheticNetPin) only.
|
||||
*
|
||||
* Phase 0 gave every endpoint of a chip-to-chip net one shared PinManager key,
|
||||
* but PinManager is last-writer-wins — wrong for a bus where several chips can
|
||||
* drive one data line and the rest release it (Hi-Z). This registry tracks each
|
||||
* chip pin's (value, strength) contribution per net, resolves the net with the
|
||||
* 4-valued rules (busLogic.resolveNet), and pushes the RESOLVED level into the
|
||||
* PinManager so readers see the bus, not the last writer. Contention (two strong
|
||||
* drivers disagree -> X) is surfaced as a console warning, like Proteus.
|
||||
*
|
||||
* Only BUS net keys flow through here; board pins and single-chip-to-component
|
||||
* synthetic pins keep the legacy direct PinManager path untouched.
|
||||
*/
|
||||
import { resolveNet, resolvedToBool, type Drive } from './busLogic';
|
||||
|
||||
interface PinManagerLike {
|
||||
triggerPinChange(pin: number, state: boolean, source?: 'mcu' | 'external'): void;
|
||||
}
|
||||
|
||||
// netKey -> (driverId -> Drive). driverId = `${componentId}::${pinName}`.
|
||||
const nets = new Map<number, Map<string, Drive>>();
|
||||
// Nets currently flagged as in contention — so we warn once per onset, not per
|
||||
// re-resolve, and clear when the contention is gone.
|
||||
const inContention = new Set<number>();
|
||||
|
||||
function recompute(pm: PinManagerLike, netKey: number): void {
|
||||
const drivers = nets.get(netKey);
|
||||
const resolved = resolveNet(drivers ? drivers.values() : []);
|
||||
|
||||
if (resolved.v === 'X') {
|
||||
if (!inContention.has(netKey)) {
|
||||
inContention.add(netKey);
|
||||
console.warn(
|
||||
`[chipbus] bus contention on net ${netKey}: two strong drivers disagree (resolved X)`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
inContention.delete(netKey);
|
||||
}
|
||||
|
||||
// PinManager is boolean; push the projected level (only a driven 1 is high).
|
||||
pm.triggerPinChange(netKey, resolvedToBool(resolved));
|
||||
}
|
||||
|
||||
/** Set (or replace) one chip pin's contribution to a bus net and re-resolve. */
|
||||
export function setBusDrive(
|
||||
pm: PinManagerLike,
|
||||
netKey: number,
|
||||
driverId: string,
|
||||
drive: Drive,
|
||||
): void {
|
||||
let m = nets.get(netKey);
|
||||
if (!m) {
|
||||
m = new Map();
|
||||
nets.set(netKey, m);
|
||||
}
|
||||
m.set(driverId, drive);
|
||||
recompute(pm, netKey);
|
||||
}
|
||||
|
||||
/** Remove every driver a chip contributes (on dispose) and re-resolve its nets. */
|
||||
export function clearBusDriversForChip(pm: PinManagerLike, componentId: string): void {
|
||||
const prefix = `${componentId}::`;
|
||||
for (const [netKey, m] of nets) {
|
||||
let changed = false;
|
||||
for (const id of [...m.keys()]) {
|
||||
if (id.startsWith(prefix)) {
|
||||
m.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) recompute(pm, netKey);
|
||||
}
|
||||
}
|
||||
|
||||
/** Test seam: wipe all bus-net driver state. */
|
||||
export function resetBusNets(): void {
|
||||
nets.clear();
|
||||
inContention.clear();
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue