wokwi / rp2040js

A Raspberry Pi Pico Emulator in JavaScript
MIT License
384 stars 40 forks source link

How to simulate signal for PIO? #106

Open mingpepe opened 1 year ago

mingpepe commented 1 year ago

The roughly flows for my PIO application are described as below

  1. Pull data from CPU
  2. Wait expected signal <- Which I need to simulate in Typescript
  3. Write data to ISR (input shift register) and trigger DMA if FIFO is full.

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?

urish commented 1 year ago

Sorry for not replying earlier - still need help with this?

mingpepe commented 1 year ago

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.

urish commented 1 year ago

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:

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?

mingpepe commented 1 year ago

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.

mingpepe commented 1 year ago

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

urish commented 1 year ago

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.

c1570 commented 1 year ago

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.).

mingpepe commented 1 year ago

@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.