Skip to content

Commit a6ff082

Browse files
committed
feat(timer): add alarms! #19
Also: 1. Implement peripheral Atomic Register Access for all peripheral 2. Add setInterrupt() method to RP2040 to let peripherals fire interrupts 3. Create MockClock for testing 4. Renamed LoggingPeripheral to BasePeripheral close #19
1 parent 813cdca commit a6ff082

File tree

11 files changed

+338
-41
lines changed

11 files changed

+338
-41
lines changed

src/clock/clock.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface IClockTimer {
2+
pause(currentMicros: number): void;
3+
resume(currentMicros: number): void;
4+
}
5+
6+
export interface IClock {
7+
readonly micros: number;
8+
9+
pause(): void;
10+
11+
resume(): void;
12+
13+
createTimer(deltaMicros: number, callback: () => void): IClockTimer;
14+
15+
deleteTimer(timer: IClockTimer): void;
16+
}

src/clock/mock-clock.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { IClock, IClockTimer } from './clock';
2+
3+
export class MockClockTimer implements IClockTimer {
4+
constructor(readonly micros: number, readonly callback: () => void) {}
5+
6+
pause() {
7+
/* intentionally empty */
8+
}
9+
10+
resume() {
11+
/* intentionally empty */
12+
}
13+
}
14+
15+
export class MockClock implements IClock {
16+
micros: number = 0;
17+
18+
private readonly timers: MockClockTimer[] = [];
19+
20+
pause() {
21+
/* intentionally empty */
22+
}
23+
24+
resume() {
25+
/* intentionally empty */
26+
}
27+
28+
advance(deltaMicros: number) {
29+
const { timers } = this;
30+
const targetTime = this.micros + deltaMicros;
31+
while (timers[0] && timers[0].micros <= targetTime) {
32+
const timer = timers.shift();
33+
timer?.callback();
34+
}
35+
this.micros += deltaMicros;
36+
}
37+
38+
createTimer(deltaMicros: number, callback: () => void) {
39+
const timer = new MockClockTimer(this.micros + deltaMicros, callback);
40+
this.timers.push(timer);
41+
this.timers.sort((a, b) => a.micros - b.micros);
42+
return timer;
43+
}
44+
45+
deleteTimer(timer: IClockTimer) {
46+
const timerIndex = this.timers.indexOf(timer as MockClockTimer);
47+
if (timerIndex >= 0) {
48+
this.timers.splice(timerIndex, 1);
49+
}
50+
}
51+
}

src/clock.ts renamed to src/clock/realtime-clock.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { getCurrentMicroseconds } from './utils/time';
1+
import { getCurrentMicroseconds } from '../utils/time';
2+
import { IClock, IClockTimer } from './clock';
23

3-
export class ClockTimer {
4+
export class ClockTimer implements IClockTimer {
45
private jsTimer: NodeJS.Timeout | null = null;
56
private timeLeft: number = this.micros;
67

@@ -28,7 +29,7 @@ export class ClockTimer {
2829
}
2930
}
3031

31-
export class Clock {
32+
export class RealtimeClock implements IClock {
3233
baseTime: number = 0;
3334
pauseTime: number = 0;
3435
paused = true;

src/peripherals/peripheral.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import { RP2040 } from '../rp2040';
22

3+
const ATOMIC_NORMAL = 0;
4+
const ATOMIC_XOR = 1;
5+
const ATOMIC_SET = 2;
6+
const ATOMIC_CLEAR = 3;
7+
8+
export function atomicUpdate(currentValue: number, atomicType: number, newValue: number) {
9+
switch (atomicType) {
10+
case ATOMIC_XOR:
11+
return currentValue ^ newValue;
12+
case ATOMIC_SET:
13+
return currentValue | newValue;
14+
case ATOMIC_CLEAR:
15+
return currentValue & ~newValue;
16+
default:
17+
console.warn('Atomic update called with invalid writeType', atomicType);
18+
return newValue;
19+
}
20+
}
21+
322
export interface Peripheral {
423
readUint32(offset: number): number;
524
writeUint32(offset: number, value: number): void;
25+
writeUint32Atomic(offset: number, value: number, atomicType: number): void;
626
}
727

8-
export class LoggingPeripheral implements Peripheral {
28+
export class BasePeripheral implements Peripheral {
29+
protected rawWriteValue = 0;
30+
931
constructor(protected rp2040: RP2040, readonly name: string) {}
1032

1133
readUint32(offset: number) {
@@ -17,13 +39,17 @@ export class LoggingPeripheral implements Peripheral {
1739
}
1840

1941
writeUint32(offset: number, value: number) {
20-
console.warn(
21-
`Unimplemented peripheral ${this.name} write to ${offset.toString(16)}: ${value}`
22-
);
23-
if (offset > 0x1000) {
24-
console.warn(`Unimplemented atomic-write to peripheral ${this.name}`);
25-
}
42+
console.warn(`Unimplemented peripheral ${this.name} write to ${offset.toString(16)}: ${value}`);
43+
}
44+
45+
writeUint32Atomic(offset: number, value: number, atomicType: number) {
46+
this.rawWriteValue = value;
47+
const newValue =
48+
atomicType != ATOMIC_NORMAL
49+
? atomicUpdate(this.readUint32(offset), atomicType, value)
50+
: value;
51+
this.writeUint32(offset, newValue);
2652
}
2753
}
2854

29-
export class UnimplementedPeripheral extends LoggingPeripheral {}
55+
export class UnimplementedPeripheral extends BasePeripheral {}

src/peripherals/rtc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { LoggingPeripheral, Peripheral } from './peripheral';
1+
import { BasePeripheral, Peripheral } from './peripheral';
22

33
const RTC_CTRL = 0x0c;
44
const RTC_ACTIVE_BITS = 0x2;
55

6-
export class RP2040RTC extends LoggingPeripheral implements Peripheral {
6+
export class RP2040RTC extends BasePeripheral implements Peripheral {
77
running = true;
88

99
readUint32(offset: number) {

src/peripherals/syscfg.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { LoggingPeripheral, Peripheral } from './peripheral';
1+
import { BasePeripheral, Peripheral } from './peripheral';
22

33
const PROC0_NMI_MASK = 0;
44
// eslint-disable-next-line @typescript-eslint/no-unused-vars
55
const PROC1_NMI_MASK = 4;
66

7-
export class RP2040SysCfg extends LoggingPeripheral implements Peripheral {
7+
export class RP2040SysCfg extends BasePeripheral implements Peripheral {
88
readUint32(offset: number) {
99
switch (offset) {
1010
case PROC0_NMI_MASK:

src/peripherals/timer.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { MockClock } from '../clock/mock-clock';
2+
import { RP2040 } from '../rp2040';
3+
4+
const ALARM1 = 0x40054014;
5+
const ALARM2 = 0x40054018;
6+
const ALARM3 = 0x4005401c;
7+
const ARMED = 0x40054020;
8+
const INTR = 0x40054034;
9+
const INTR_CLEAR = INTR | 0x3000;
10+
const INTE = 0x40054038;
11+
const INTS = 0x40054040;
12+
13+
describe('RPTimer', () => {
14+
describe('Alarms', () => {
15+
it('should set Alarm 1 to armed when writing to ALARM1 register', () => {
16+
const rp2040 = new RP2040();
17+
rp2040.writeUint32(ALARM1, 0x1000);
18+
expect(rp2040.readUint32(ARMED)).toEqual(0x2);
19+
});
20+
21+
it('should disarm Alarm 2 when writing 0x4 to the ARMED register', () => {
22+
const rp2040 = new RP2040();
23+
rp2040.writeUint32(ALARM2, 0x1000);
24+
expect(rp2040.readUint32(ARMED)).toEqual(0x4);
25+
rp2040.writeUint32(ARMED, 0xff);
26+
expect(rp2040.readUint32(ARMED)).toEqual(0);
27+
});
28+
29+
it('should generate an IRQ 3 interrupt when Alarm 3 fires', () => {
30+
const clock = new MockClock();
31+
const rp2040 = new RP2040(clock);
32+
// Arm the alarm
33+
rp2040.writeUint32(ALARM3, 1000);
34+
expect(rp2040.readUint32(ARMED)).toEqual(0x8);
35+
expect(rp2040.readUint32(INTR)).toEqual(0);
36+
// Advance time so that the alarm will fire
37+
clock.advance(2000);
38+
expect(rp2040.readUint32(ARMED)).toEqual(0);
39+
expect(rp2040.readUint32(INTR)).toEqual(0x8);
40+
expect(rp2040.readUint32(INTS)).toEqual(0);
41+
expect(rp2040.pendingInterrupts).toBe(0);
42+
// Enable the interrupts for all alarms
43+
rp2040.writeUint32(INTE, 0xff);
44+
expect(rp2040.readUint32(INTS)).toEqual(0x8);
45+
expect(rp2040.pendingInterrupts).toBe(0x8);
46+
expect(rp2040.interruptsUpdated).toEqual(true);
47+
// Clear the alarm's interrupt
48+
rp2040.writeUint32(INTR_CLEAR, 0x8);
49+
expect(rp2040.readUint32(INTS)).toEqual(0);
50+
expect(rp2040.pendingInterrupts).toBe(0);
51+
});
52+
});
53+
});

src/peripherals/timer.ts

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import { getCurrentMicroseconds } from '../utils/time';
2-
import { LoggingPeripheral, Peripheral } from './peripheral';
1+
import { IClock, IClockTimer } from '../clock/clock';
2+
import { RP2040 } from '../rp2040';
3+
import { BasePeripheral, Peripheral } from './peripheral';
34

45
const TIMEHR = 0x08;
56
const TIMELR = 0x0c;
67
const TIMERAWH = 0x24;
78
const TIMERAWL = 0x28;
9+
const ALARM0 = 0x10;
10+
const ALARM1 = 0x14;
11+
const ALARM2 = 0x18;
12+
const ALARM3 = 0x1c;
13+
const ARMED = 0x20;
14+
const PAUSE = 0x30;
15+
const INTR = 0x34;
16+
const INTE = 0x38;
17+
const INTF = 0x3c;
18+
const INTS = 0x40;
819

920
/* eslint-disable @typescript-eslint/no-unused-vars */
1021
const ALARM_0 = 1 << 0;
@@ -13,11 +24,39 @@ const ALARM_2 = 1 << 2;
1324
const ALARM_3 = 1 << 3;
1425
/* eslint-enable @typescript-eslint/no-unused-vars */
1526

16-
export class RPTimer extends LoggingPeripheral implements Peripheral {
17-
latchedTimeHigh = 0;
27+
class RPTimerAlarm {
28+
armed = false;
29+
targetMicros = 0;
30+
timer: IClockTimer | null = null;
31+
32+
constructor(readonly name: string, readonly bitValue: number) {}
33+
}
34+
35+
export class RPTimer extends BasePeripheral implements Peripheral {
36+
private readonly clock: IClock;
37+
private latchedTimeHigh = 0;
38+
private readonly alarms = [
39+
new RPTimerAlarm('Alarm 0', ALARM_0),
40+
new RPTimerAlarm('Alarm 1', ALARM_1),
41+
new RPTimerAlarm('Alarm 2', ALARM_2),
42+
new RPTimerAlarm('Alarm 3', ALARM_3),
43+
];
44+
private intRaw = 0;
45+
private intEnable = 0;
46+
private intForce = 0;
47+
private paused = false;
48+
49+
constructor(rp2040: RP2040, name: string) {
50+
super(rp2040, name);
51+
this.clock = rp2040.clock;
52+
}
53+
54+
get intStatus() {
55+
return (this.intRaw | this.intForce) & this.intEnable;
56+
}
1857

1958
readUint32(offset: number) {
20-
const time = getCurrentMicroseconds();
59+
const time = this.clock.micros;
2160

2261
switch (offset) {
2362
case TIMEHR:
@@ -32,14 +71,104 @@ export class RPTimer extends LoggingPeripheral implements Peripheral {
3271

3372
case TIMERAWL:
3473
return time >>> 0;
74+
75+
case ALARM0:
76+
return this.alarms[0].targetMicros;
77+
case ALARM1:
78+
return this.alarms[1].targetMicros;
79+
case ALARM2:
80+
return this.alarms[2].targetMicros;
81+
case ALARM3:
82+
return this.alarms[3].targetMicros;
83+
84+
case PAUSE:
85+
return this.paused ? 1 : 0;
86+
87+
case INTR:
88+
return this.intRaw;
89+
case INTE:
90+
return this.intEnable;
91+
case INTF:
92+
return this.intForce;
93+
case INTS:
94+
return this.intStatus;
95+
96+
case ARMED:
97+
return (
98+
(this.alarms[0].armed ? this.alarms[0].bitValue : 0) |
99+
(this.alarms[1].armed ? this.alarms[1].bitValue : 0) |
100+
(this.alarms[2].armed ? this.alarms[2].bitValue : 0) |
101+
(this.alarms[3].armed ? this.alarms[3].bitValue : 0)
102+
);
35103
}
36104
return super.readUint32(offset);
37105
}
38106

39107
writeUint32(offset: number, value: number) {
40108
switch (offset) {
109+
case ALARM0:
110+
case ALARM1:
111+
case ALARM2:
112+
case ALARM3: {
113+
const alarmIndex = (offset - ALARM0) / 4;
114+
const alarm = this.alarms[alarmIndex];
115+
const delta = (value - this.clock.micros) >>> 0;
116+
this.disarmAlarm(alarm);
117+
alarm.armed = true;
118+
alarm.targetMicros = value;
119+
alarm.timer = this.clock.createTimer(delta, () => this.fireAlarm(alarmIndex));
120+
break;
121+
}
122+
case ARMED:
123+
for (const alarm of this.alarms) {
124+
if (this.rawWriteValue & alarm.bitValue) {
125+
this.disarmAlarm(alarm);
126+
}
127+
}
128+
break;
129+
case PAUSE:
130+
this.paused = !!(value & 1);
131+
if (this.paused) {
132+
console.warn('Unimplemented Timer Pause');
133+
}
134+
// TODO actually pause the timer
135+
break;
136+
case INTR:
137+
this.intRaw &= ~this.rawWriteValue;
138+
this.checkInterrupts();
139+
break;
140+
case INTE:
141+
this.intEnable = value & 0xf;
142+
this.checkInterrupts();
143+
break;
144+
case INTF:
145+
this.intForce = value & 0xf;
146+
this.checkInterrupts();
147+
break;
41148
default:
42149
super.writeUint32(offset, value);
43150
}
44151
}
152+
153+
private fireAlarm(index: number) {
154+
const alarm = this.alarms[index];
155+
this.disarmAlarm(alarm);
156+
this.intRaw |= alarm.bitValue;
157+
this.checkInterrupts();
158+
}
159+
160+
private checkInterrupts() {
161+
const { intStatus } = this;
162+
for (let i = 0; i < this.alarms.length; i++) {
163+
this.rp2040.setInterrupt(i, !!(intStatus & (1 << i)));
164+
}
165+
}
166+
167+
private disarmAlarm(alarm: RPTimerAlarm) {
168+
if (alarm.timer) {
169+
this.clock.deleteTimer(alarm.timer);
170+
alarm.timer = null;
171+
}
172+
alarm.armed = false;
173+
}
45174
}

0 commit comments

Comments
 (0)