Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to simulate signal for PIO? #106

Open
mingpepe opened this issue Nov 23, 2022 · 8 comments
Open

How to simulate signal for PIO? #106

mingpepe opened this issue Nov 23, 2022 · 8 comments

Comments

@mingpepe
Copy link
Contributor

mingpepe commented Nov 23, 2022

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
Copy link
Contributor

urish commented Dec 6, 2022

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

@mingpepe
Copy link
Contributor Author

mingpepe commented Dec 9, 2022

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
Copy link
Contributor

urish commented Dec 9, 2022

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:

  • The clock counts nanoseconds.
  • The clock maintains a list of active timers (implementing createTimer() of the IClock interface).
  • After executing each CPU instruction, the clock increments the current time (based on the MCU clock frequency), and checks whether any timers should fire.

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
Copy link
Contributor Author

mingpepe commented Dec 10, 2022

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
Copy link
Contributor Author

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.

mcu.core.PC = 0x10000000;
mcu.execute();

rp2040js/src/rp2040.ts

Lines 350 to 360 in fa0dc6c

execute() {
this.clock.resume();
this.executeTimer = null;
this.stopped = false;
for (let i = 0; i < 100000 && !this.stopped && !this.core.waiting; i++) {
this.core.executeInstruction();
}
if (!this.stopped) {
this.executeTimer = setTimeout(() => this.execute(), 0);
}
}

@urish
Copy link
Contributor

urish commented Jan 1, 2023

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
Copy link
Contributor

c1570 commented Jan 18, 2023

#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
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants