Skip to content

Commit 735fbda

Browse files
committed
refactor(core): add clock and controller
1 parent be9793d commit 735fbda

File tree

11 files changed

+201
-99
lines changed

11 files changed

+201
-99
lines changed

eslint.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export default tseslint.config(
138138
exceptions: {
139139
ObjectExpression: true,
140140
TSEnumMember: true,
141+
TSTypeAnnotation: true,
141142
VariableDeclarator: true,
142143
},
143144
}],

src/core/bus/bus.ts

+31-42
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,41 @@
1-
import { asapScheduler, BehaviorSubject, filter, observeOn, shareReplay, skipWhile } from 'rxjs'
2-
3-
export const enum Control {
4-
CLOCK_IDLE = 0b0000,
5-
MEMORY_READ = 0b1000,
6-
IO_READ = 0b1001,
7-
MEMORY_WRITE = 0b1010,
8-
IO_WRITE = 0b1011,
9-
INTERRUPT = 0b1100,
10-
}
11-
12-
export interface Signals {
13-
data: number
14-
address: number
15-
control: Control
1+
import { BehaviorSubject, filter, type Observable, share } from 'rxjs'
2+
3+
export type Signal = 0b0 | 0b1
4+
5+
export interface ControlLines {
6+
RD: Signal
7+
WR: Signal
8+
MREQ: Signal
9+
IORQ: Signal
10+
CLK: Signal
11+
WAIT: Signal
12+
IRQ: Signal
13+
HALT: Signal
1614
}
1715

18-
const initialSignals: Signals = {
19-
data: 0x00,
20-
address: 0x00,
21-
control: Control.CLOCK_IDLE,
16+
const initialControlLines: ControlLines = {
17+
RD: 0b0,
18+
WR: 0b0,
19+
MREQ: 0b0,
20+
IORQ: 0b0,
21+
CLK: 0b0,
22+
WAIT: 0b0,
23+
IRQ: 0b0,
24+
HALT: 0b0,
2225
}
2326

2427
export class Bus {
25-
private readonly source$ = new BehaviorSubject(initialSignals)
28+
readonly data$ = new BehaviorSubject(0x00)
29+
readonly address$ = new BehaviorSubject(0x00)
30+
readonly control$ = new BehaviorSubject(initialControlLines)
2631

27-
private readonly shared$ = this.source$.pipe(
28-
observeOn(asapScheduler),
29-
shareReplay(1),
32+
readonly controlOnClockRise$: Observable<ControlLines> = this.control$.pipe(
33+
filter((control, index) => (index && control.CLK)),
34+
share(),
3035
)
3136

32-
get signals$() {
33-
return this.shared$
34-
}
35-
36-
get idle$() {
37-
return this.shared$.pipe(
38-
filter((signals) => (signals.control === Control.CLOCK_IDLE)),
39-
)
40-
}
41-
42-
put(next: Partial<Signals>) {
43-
const nextSignals = {
44-
...this.source$.getValue(),
45-
...next,
46-
}
47-
this.source$.next(nextSignals)
48-
return this.shared$.pipe(
49-
skipWhile((signals) => (signals !== nextSignals)),
50-
)
37+
setControl(lines: Partial<ControlLines>): void {
38+
const control = this.control$.getValue()
39+
this.control$.next(Object.assign(control, lines))
5140
}
5241
}

src/core/clock/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Clock

src/core/clock/clock.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Bus } from '../bus/bus'
2+
3+
export class Clock {
4+
constructor(
5+
private readonly bus: Bus,
6+
) {}
7+
8+
tick = (): void => {
9+
this.bus.setControl({ CLK: 0b1 })
10+
this.bus.setControl({ CLK: 0b0 })
11+
}
12+
}

src/core/controller/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Controller
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { firstValueFrom } from 'rxjs'
2+
import { describe, expect, it } from 'vitest'
3+
4+
import { Bus } from '../bus/bus'
5+
import { Clock } from '../clock/clock'
6+
import { Cpu } from '../cpu/cpu'
7+
import { Memory } from '../memory/memory'
8+
import { Controller } from './controller'
9+
10+
describe('Controller', () => {
11+
it('should step', async () => {
12+
const bus = new Bus()
13+
const memory = new Memory(bus)
14+
memory.load(new Uint8Array([0x01, 0x02]), 0x00)
15+
const controller = new Controller(
16+
bus,
17+
new Cpu(bus),
18+
new Clock(bus),
19+
memory,
20+
)
21+
await firstValueFrom(controller.step())
22+
expect(memory.getData()[0x02]).toBe(0x03)
23+
})
24+
})

src/core/controller/controller.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Observable, type Subscription } from 'rxjs'
2+
3+
import type { Bus, ControlLines } from '../bus/bus'
4+
import type { Clock } from '../clock/clock'
5+
import type { Cpu } from '../cpu/cpu'
6+
import type { Memory } from '../memory/memory'
7+
8+
export class Controller {
9+
constructor(
10+
private readonly bus: Bus,
11+
private readonly cpu: Cpu,
12+
private readonly clock: Clock,
13+
private readonly memory: Memory,
14+
) {}
15+
16+
step = (): Observable<void> => {
17+
return new Observable<void>((subscriber) => {
18+
const step = this.cpu.step()
19+
let subscription: Subscription
20+
21+
const handleResult = (result: IteratorResult<Observable<ControlLines>, void>) => {
22+
if (result.done) {
23+
subscriber.next()
24+
subscriber.complete()
25+
return
26+
}
27+
queueMicrotask(this.clock.tick)
28+
subscription = result.value.subscribe((signals) => {
29+
handleResult(step.next(signals))
30+
})
31+
}
32+
33+
handleResult(step.next())
34+
return () => subscription.unsubscribe()
35+
})
36+
}
37+
}

src/core/cpu/cpu.ts

+33-30
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
1-
import { filter, firstValueFrom, map, skipUntil } from 'rxjs'
1+
import { type Observable, take } from 'rxjs'
22

3-
import { type Bus, Control, type Signals } from '../bus/bus'
3+
import type { Bus, ControlLines } from '../bus/bus'
4+
5+
type AsyncControlGenerator<T> = Generator<Observable<ControlLines>, T, ControlLines>
46

57
export class Cpu {
68
constructor(
79
private readonly bus: Bus,
8-
) {
9-
this.bus.signals$.pipe(
10-
filter((signals) => (signals.control === Control.INTERRUPT)),
11-
).subscribe(this.handleInterrupt)
12-
}
13-
14-
async step() {
15-
// TODO: implement step
16-
// const opcode = await this.readMemory(...)
17-
}
10+
) {}
1811

19-
private readMemory(address: number) {
20-
const data$ = this.bus.put({
21-
address,
22-
control: Control.MEMORY_READ,
23-
}).pipe(
24-
skipUntil(this.bus.idle$),
25-
map((signals) => signals.data),
26-
)
27-
return firstValueFrom(data$)
12+
*step(): AsyncControlGenerator<void> {
13+
const x = yield* this.readMemory(0x00)
14+
const y = yield* this.readMemory(0x01)
15+
const result = x + y
16+
yield* this.writeMemory(result, 0x02)
2817
}
2918

30-
private writeMemory(address: number, data: number) {
31-
const complete$ = this.bus.put({
32-
data,
33-
address,
34-
control: Control.MEMORY_WRITE,
19+
*readMemory(address: number): AsyncControlGenerator<number> {
20+
this.bus.address$.next(address)
21+
this.bus.setControl({
22+
RD: 0b1,
23+
MREQ: 0b1,
3524
})
36-
return firstValueFrom(complete$)
25+
yield this.bus.controlOnClockRise$.pipe(take(1))
26+
this.bus.setControl({
27+
RD: 0b0,
28+
MREQ: 0b0,
29+
})
30+
return this.bus.data$.getValue()
3731
}
3832

39-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
40-
private handleInterrupt = (_signals: Signals) => {
41-
// TODO: implement interrupt handling
33+
*writeMemory(data: number, address: number): AsyncControlGenerator<void> {
34+
this.bus.data$.next(data)
35+
this.bus.address$.next(address)
36+
this.bus.setControl({
37+
WR: 0b1,
38+
MREQ: 0b1,
39+
})
40+
yield this.bus.controlOnClockRise$.pipe(take(1))
41+
this.bus.setControl({
42+
WR: 0b0,
43+
MREQ: 0b0,
44+
})
4245
}
4346
}

src/core/memory/memory.ts

+51-26
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,85 @@
1-
import { filter, map, type Observable, share, tap } from 'rxjs'
1+
import { filter, map, type Observable, share } from 'rxjs'
22

3-
import { type Bus, Control, type Signals } from '../bus/bus'
3+
import type { Bus } from '../bus/bus'
4+
5+
export enum MemoryOperationType {
6+
READ = 'READ',
7+
WRITE = 'WRITE',
8+
}
9+
10+
export interface MemoryOperation {
11+
type: MemoryOperationType
12+
data: number
13+
address: number
14+
}
415

516
export class Memory {
617
// TODO: use shared constants
718
private readonly data = new Uint8Array(0x100)
819

9-
private readonly read$: Observable<Signals>
10-
private readonly write$: Observable<Signals>
20+
readonly read$: Observable<MemoryOperation>
21+
readonly write$: Observable<MemoryOperation>
1122

1223
constructor(
1324
private readonly bus: Bus,
1425
) {
15-
this.read$ = this.bus.signals$.pipe(
16-
filter((signals) => signals.control === Control.MEMORY_READ),
17-
tap(this.read),
26+
const control$ = this.bus.control$.pipe(
27+
filter((control) => control.MREQ),
1828
share(),
1929
)
2030

21-
this.write$ = this.bus.signals$.pipe(
22-
filter((signals) => signals.control === Control.MEMORY_WRITE),
23-
tap(this.write),
31+
this.read$ = control$.pipe(
32+
filter((control) => control.RD),
33+
map(this.read),
34+
share(),
35+
)
36+
37+
this.write$ = control$.pipe(
38+
filter((control) => control.WR),
39+
map(this.write),
2440
share(),
2541
)
2642

2743
this.read$.subscribe()
2844
this.write$.subscribe()
2945
}
3046

31-
private read = (signals: Signals) => {
32-
this.bus.put({
33-
data: this.data[signals.address],
34-
control: Control.CLOCK_IDLE,
35-
})
47+
private read = (): MemoryOperation => {
48+
const address = this.bus.address$.getValue()
49+
const data = this.data[address]
50+
this.bus.data$.next(data)
51+
return {
52+
type: MemoryOperationType.READ,
53+
data,
54+
address,
55+
}
3656
}
3757

38-
private write = (signals: Signals) => {
39-
this.data[signals.address] = signals.data
40-
this.bus.put({
41-
control: Control.CLOCK_IDLE,
42-
})
58+
private write = (): MemoryOperation => {
59+
const address = this.bus.address$.getValue()
60+
const data = this.bus.data$.getValue()
61+
this.data[address] = data
62+
return {
63+
type: MemoryOperationType.WRITE,
64+
data,
65+
address,
66+
}
4367
}
4468

45-
get data$() {
46-
return this.write$.pipe(map(() => this.getData()))
69+
getData = (): number[] => {
70+
return Array.from(this.data)
4771
}
4872

49-
getData() {
50-
return Array.from(this.data)
73+
subscribeData = (onDataChange: (() => void)): (() => void) => {
74+
const subscription = this.write$.subscribe(onDataChange)
75+
return () => subscription.unsubscribe()
5176
}
5277

53-
load(data: Uint8Array, offset: number) {
78+
load(data: Uint8Array, offset: number): void {
5479
this.data.set(data, offset)
5580
}
5681

57-
reset() {
82+
reset(): void {
5883
this.data.fill(0)
5984
}
6085
}

src/rxjs.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { MonoTypeOperatorFunction } from 'rxjs'
2+
3+
declare module 'rxjs' {
4+
export function filter<T>(predicate: (value: T, index: number) => unknown): MonoTypeOperatorFunction<T>
5+
export function skipWhile<T>(predicate: (value: T, index: number) => unknown): MonoTypeOperatorFunction<T>
6+
}

src/ts-reset.d.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
type WithRequired<T, K extends keyof T> = T & Omit<T, K> & Required<Pick<T, K>>
33

44
interface ObjectConstructor {
5+
hasOwn<T extends Record<K, any>, K extends PropertyKey>(o: T, v: K):
6+
v is keyof T
7+
58
hasOwn<T, K extends PropertyKey>(o: T, v: K):
69
o is K extends keyof T
710
? WithRequired<T, K>
8-
: Extract<T, Record<K, unknown>>
11+
: Extract<T, Record<K, any>>
912
}

0 commit comments

Comments
 (0)