Open mingpepe opened 1 year ago
Sorry for not replying earlier - still need help with this?
It's still a problem. The solution I thought about is let CPU, DMA, PIO, ... not executed in single thread. But that's a big change.
The way Wokwi solves this is by using a different implementation of the clock that does not follow real time (it does best-effort to sync with real time, though).
Once you use a virtual clock, you get can all the peripherals accurately synchronized with this clock. In a nutshell:
createTimer()
of the IClock
interface).This enables accurate synchronization of PIO, DMA, etc., when they are all running on the same thread.
What approach have you thought using for synchronizing them over multiple threads?
I have not traced detail about clock in this emulator yet.
What approach have you thought using for synchronizing them over multiple threads?
Indeed, it is a problem, just thought this idea but not dive deeply yet. Maybe each thread always waiting for the clock signal to work, but checkout what each component can do within 1 clock may be a hard task.
If I have time later, I will check how other emulator handle this, like QEMU.
After executing each CPU instruction, the clock increments the current time (based on the MCU clock frequency), and checks whether any timers should fire.
Where does the code check the timers? As timers are implemented by setTimeout
and will be checked when runtime is not busy. I mean the end of execute
. Just want to check if I miss something.
https://github.com/wokwi/rp2040js/blob/fa0dc6cc323bb1f8447ec9e8a575f011b6114a0d/demo/emulator-run.ts#L21-L22
https://github.com/wokwi/rp2040js/blob/fa0dc6cc323bb1f8447ec9e8a575f011b6114a0d/src/rp2040.ts#L350-L360
Nope, you don't miss anything - rp2040js currently implements timers in a way that does not support accurate timers, unfortunately.
This is Wokwi's internal Clock implementation:
export type ClockEventCallback = () => void;
/**
* Linked list of pending clock events. We use a linked in instead of an array for performance,
* similar to how AVR8js implements clock events:
* https://github.com/wokwi/avr8js/commit/968a6ee0c90498077888c7f09c58983598937570
*/
interface IClockEventEntry {
cycles: number;
callback: ClockEventCallback;
next: IClockEventEntry | null;
}
/* Dummy implementation, to be compatible with rp2040js interface */
class Timer {
constructor(readonly callback: ClockEventCallback) {}
pause() {}
resume() {}
}
export class RPClock {
private nextClockEvent: IClockEventEntry | null = null;
private readonly clockEventPool: IClockEventEntry[] = []; // helps avoid garbage collection
skippedCycles = 0;
cpu: { cycles: number } = { cycles: 0 };
constructor(readonly frequency = 125e6) {}
get micros() {
return this.nanos / 1000;
}
get nanos() {
return (this.cpu.cycles / this.frequency) * 1e9;
}
addEvent(nanos: number, callback: ClockEventCallback) {
const cycles = Math.round((nanos / 1e9) * this.frequency);
return this.addEventCycles(cycles, callback);
}
clearEvent(callback: ClockEventCallback) {
let { nextClockEvent: clockEvent } = this;
if (!clockEvent) {
return false;
}
const { clockEventPool } = this;
let lastItem: IClockEventEntry | null = null;
while (clockEvent) {
if (clockEvent.callback === callback) {
if (lastItem) {
lastItem.next = clockEvent.next;
} else {
this.nextClockEvent = clockEvent.next;
}
if (clockEventPool.length < 10) {
clockEventPool.push(clockEvent);
}
return true;
}
lastItem = clockEvent;
clockEvent = clockEvent.next;
}
return false;
}
addEventCycles(cycles: number, callback: ClockEventCallback) {
const { clockEventPool } = this;
cycles = this.cpu.cycles + Math.max(1, cycles);
const maybeEntry = clockEventPool.pop();
const entry: IClockEventEntry = maybeEntry ?? { cycles, callback, next: null };
entry.cycles = cycles;
entry.callback = callback;
let { nextClockEvent: clockEvent } = this;
let lastItem: IClockEventEntry | null = null;
while (clockEvent && clockEvent.cycles < cycles) {
lastItem = clockEvent;
clockEvent = clockEvent.next;
}
if (lastItem) {
lastItem.next = entry;
entry.next = clockEvent;
} else {
this.nextClockEvent = entry;
entry.next = clockEvent;
}
return callback;
}
/** @deprecated */
pause() {
/* Not really used; Kept for compatibility with rp2040js clock */
}
/** @deprecated */
resume() {
/* Not really used; Kept for compatibility with rp2040js clock */
}
tick() {
const { nextClockEvent } = this;
if (nextClockEvent && nextClockEvent.cycles <= this.cpu.cycles) {
this.nextClockEvent = nextClockEvent.next;
if (this.clockEventPool.length < 10) {
this.clockEventPool.push(nextClockEvent);
}
nextClockEvent.callback();
}
}
createTimer(deltaMicros: number, callback: () => void) {
const timer = new Timer(callback);
this.addEvent(deltaMicros * 1000, callback);
return timer;
}
deleteTimer(timer: any) {
if (timer instanceof Timer) {
this.clearEvent(timer.callback);
}
}
createEvent(callback: () => void) {
const clock = this;
return {
schedule(nanos: number) {
clock.clearEvent(callback);
clock.addEvent(nanos, callback);
},
unschedule() {
clock.clearEvent(callback);
},
};
}
skipToNextEvent() {
const { nextClockEvent } = this;
if (nextClockEvent) {
this.cpu.cycles = nextClockEvent.cycles;
this.nextClockEvent = nextClockEvent.next;
if (this.clockEventPool.length < 10) {
this.clockEventPool.push(nextClockEvent);
}
nextClockEvent.callback();
}
}
get nextClockEventCycles() {
return this.nextClockEvent?.cycles ?? 0;
}
}
And as you observed, you also need to update execute
- it should call the tick()
method of the clock after every call to executeInstruction
, to make sure the clock events are called at the right time.
I hope to introduce this change to rp2040js at some point, but right now, for most use cases, the setTimeout()
event system also does the trick.
https://github.com/wokwi/rp2040js/pull/117 makes PIO run in sync with the CPU. There, PIO is tied to CPU ticks instead of some other separate clock. Not sure it's actually correct but certainly it's looking better now (previously, ClockDiv was ignored completely, etc.).
@c1570, sadly this does not solve my problem, the main issue is the timing to simulate signal for PIO's IN
instruction.
Finally, I add a callback before IN
instruction to change the status of gpios.
The roughly flows for my PIO application are described as below
Since CPU, DMA, PIO and timers all execute in single thread, it's hard to simulate the signal with expected timing. When PIO is running and waiting for data from CPU, but meanwhile CPU is not running (the only thread is occupied by PIO). Similar situation for PIO waiting for DMA execution.
Any idea to this problem?