SPI-Schnittstelle auf Xilinx FPGA, Taktdomänen und Zeitbeschränkungen

Ich verbinde ein Raspberry Pi-Board mit einem Entwicklungsboard mit einem Spartan 6. Ich möchte dies mit SPI tun. Aufgrund des Designs des Entwicklungsboards muss ich SPI CLK und DATA mit Standard-IO-Pins verbinden.

Ich bin mir der Notwendigkeit bewusst, Taktdomänen mit Doppelpufferung zu kreuzen, um Metastabilität zu vermeiden. Das RPi und der SPI CLK befinden sich offensichtlich in einer separaten Domäne zum internen FPGA-Fabric. Ich sehe kein allzu großes Problem: Nur ein 8-Bit-Register und das Signal, das besagt, wann ein Byte bereit ist, müssen mit der internen Fabric-Uhr synchronisiert werden. Ich versuche nicht, hohe Datenraten zu bekommen. Ein Byte wird nur etwa alle 25 us geschrieben (das liegt daran, dass das RPi ein GPIO langsam liest, aber kein Problem für dieses Projekt). Ich denke darüber nach, den SPI mit 15 MHz zu takten, und könnte dies bei Bedarf sogar reduzieren.

Das ist mein Verilog. Es simuliert und testet gut.

module my_spi_in (
  // RPI clock domain
  input i_RPI_spi_data,
  input i_RPI_spi_clk,
  input i_RPI_reset,
  // internal 64MHz domain
  input i_sys_clk,
  output [7:0] o_data,
  output o_fifo_write
);

  // registers in RPI clock domain
  reg [7:0] r_RPI_shift_in = 8'b0;
  reg [2:0] r_RPI_ctr = 3'b0;
  reg r_RPI_word_done = 1'b0;

  // synchronisation registers
  reg [7:0]r_data_sync_1 = 8'b0;
  reg [7:0]r_data_sync_2 = 8'b0;
  reg [2:0] r_word_done_sync = 3'b0;

  // RPI clock domain : input shift register logic
  always @ (posedge i_RPI_spi_clk, posedge i_RPI_reset) begin
    if (i_RPI_reset == 1'b1) begin
      r_RPI_shift_in <= 8'b0;
      r_RPI_ctr <= 3'b0;
    end else begin
      r_RPI_ctr <= r_RPI_ctr + 1'b1;
      r_RPI_shift_in <= {i_RPI_spi_data, r_RPI_shift_in[7:1]};
    end
  end

  // RPI clock domain : word done
  always @ (negedge i_RPI_spi_clk) begin
    if (~i_RPI_reset && r_RPI_ctr == 3'b000) r_RPI_word_done <= 1'b1;
    else r_RPI_word_done <= 1'b0;
  end

  // sync registers
  always @ (posedge i_sys_clk) begin
    r_data_sync_1 <= r_RPI_shift_in;
    r_data_sync_2 <= r_data_sync_1;
    r_word_done_sync[0] <= r_RPI_word_done;
    r_word_done_sync[1] <= r_word_done_sync[0];
    r_word_done_sync[2] <= r_word_done_sync[1];
  end

  assign o_data = r_data_sync_2;
  assign o_fifo_write = r_word_done_sync[1] && ~r_word_done_sync[2];
endmodule

In meiner .ucf-Datei habe ich nur Folgendes, um ISE mitzuteilen, dass dies keine "echte" Uhr ist (sie wird ohne diese nicht erstellt):

NET "i_RPI_spi_clk" CLOCK_DEDICATED_ROUTE = FALSE;
NET "i_RPI_reset" CLOCK_DEDICATED_ROUTE = FALSE;

Meine Frage: Ist das die beste Vorgehensweise? Muss ich noch etwas tun? (Idealerweise wäre es gut, auch einige Zeitbeschränkungen für die SPI-Uhr und -Daten festzulegen, um die Tools auf die Geschwindigkeit der SPI-Schnittstelle aufmerksam zu machen.)

Vielen Dank im Voraus für Ihren Rat.

BEARBEITEN: Ich sollte klarstellen, dass das RPi nur ein einzelnes Byte überträgt, bevor ein GPIO-Pin überprüft wird. Dies erweist sich als langsam (dauert etwa 25 us), sodass auf dem SPI-Bus niemals zwei Bytes hintereinander liegen. Es gibt SPI-Aktivität für etwa 0,5 us (ein Byte bei 15 MHz), dann passiert für etwa 24 us nichts, bis das RPi den GPIO gelesen hat. Dies ist offensichtlich viel langsamer als SPI es kann – die RPi-Lesezeit verlangsamt die Übertragung ziemlich – aber das ist für dieses System durchaus akzeptabel.

Antworten (2)

Der übliche Ansatz besteht darin, MOSI, CS und SCLK zur internen FPGA-Fabric-Clock-Domäne (die mit einer weitaus höheren Rate als der SPI-Bus läuft) zu kreuzen und dort die gesamte eigentliche Arbeit zu erledigen.

Das Überqueren einer Taktdomäne mit einem parallelen Registerausgang hat zumindest die inhärente Möglichkeit eines ungültigen Zustands, wo dies bei einem seriellen Bus wirklich nicht der Fall ist. Dies liegt daran, dass Ihr Metastabilitätsfilter möglicherweise verschiedene Pegel auf verschiedenen Bits registriert, wenn mehrere Bits den Status innerhalb des Setup- oder Haltefensters ändern. Wenn Sie den seriellen Stream in die Core-Clock-Domäne bringen, können Sie auch Dinge wie die Implementierung von Glitch-Filtern einfach tun, was sich lohnen kann.

Auch das ist ein guter Rat. Meine Antwort befasst sich mit einer direkteren Lösung für den vorhandenen Code.
Danke dafür. Mein interner Takt beträgt nur 64 MHz, und ich möchte ihn nicht besonders erhöhen. Ich würde vermuten, ich möchte, dass es mindestens 8 oder 10 mal höher ist als die SPI-Rate? Tatsächlich wird der Shifter-Ausgang einige ungültige Werte haben, wenn er im falschen Moment gelesen wird - aber es gibt dort ein Signal, um dies zu verhindern. Ich könnte auch ein weiteres Register hinzufügen, um dies zu verhindern. Aber "es ist möglich, dass Ihr Metastabilitätsfilter verschiedene Ebenen auf verschiedenen Bits registriert, wenn mehrere Bits den Status innerhalb des Setup- oder Hold-Fensters ändern" - das verstehe ich nicht ganz. Könnten Sie das bitte erklären? vielen Dank.
Ich könnte die SPI-Geschwindigkeit auf etwa 4 MHz senken und CLK/MOSI mit einem Fabric-Takt von 64 MHz abtasten. Die Datenrate wird kaum beeinträchtigt, und die Steigerung der Einfachheit ist durchaus attraktiv. Danke schön.
Okay, ich verstehe die Idee. Tatsächlich kann ich so das Takten mit dem SPI CLk vollständig vermeiden - indem ich zwei der Signale in den Registern zur Flankenerkennung verwende. Danke! großartige Idee.
@dmb Denken Sie immer daran, dass Sie nur ein einzelnes Bit über eine Taktgrenze hinweg zuverlässig synchronisieren können. Dies ist derselbe Grund, warum Gray-Code für Zeiger auf asynchrone FIFOs verwendet wird. In Ihrem Fall synchronisieren Sie ein / das Steuerbit mit der empfangenden Taktdomäne und verwenden es dann als Freigabe zum Erfassen der anderen Signale in der neuen Domäne.
Zur Verdeutlichung (und sicher sein, dass ich es verstehe): Wenn wir einen parallelen Bus durch Sync-Register laufen lassen, können wir nicht garantieren, dass sie alle im selben Zyklus des Synchronisierungstakts synchronisieren, oder? Wenn sie jedoch durch ein anderes (synchronisiertes) Signal qualifiziert werden, das (sagen wir) 3 Taktzyklen (des Synchronisierungstakts) nach ihrer Änderung auftritt, sollten sie bis dahin alle synchronisiert sein. (Vielleicht ist das keine gute Praxis, aber es ist gut zu verstehen, was passiert und warum.) Vielen Dank an alle. Hervorragende Hilfe und Beratung.

Nein, Sie haben eine völlig falsche Vorstellung, wenn es darum geht, einen Multi-Bit-Bus über eine Taktdomänengrenze zu übertragen.

Here, the problem is not metastability, but rather sampling the bits on the bus at a time when they are known not to be changing, so that you always get a self-consistent value.

Therefore, it is correct to synchronize and delay the r_RPI_word_done signal before doing edge-detection on it, but it is NOT correct to put the data itself through multiple registers.

Your internal clock is several times faster than the SPI clock (right?), so by the time the o_fifo_write pulse occurs, you KNOW that the data bits are stable and can be sampled safely. You do not need the r_data_sync_1 and r_data_sync_2 registers, and you should directly

assign o_data = r_RPI_shift_in;

Tatsächlich ist das Verzögern der Daten sehr kontraproduktiv, da es praktisch garantiert , dass Sie die Daten zu einem Zeitpunkt abtasten, an dem sie sich ändern, was dazu führt, dass einige Bits aus einem Wort und einige Bits aus dem nächsten Wort erfasst werden.

OK danke. Da jedes Byte durch eine lange inaktive Zeit getrennt ist (das Übertragen eines Bytes dauert etwa 0,5 us, vor dem nächsten liegen etwa 24 us), haben die Register für die Daten, wie Sie angeben, tatsächlich keinen Sinn.