Skocz do zawartości

I2C slave - test na zestawie FPGA ElbertV.2 (Spartan3A)


FlyingDutch

Pomocna odpowiedź

Cześć,

ponieważ I2C jest jednym z najprostszych szeregowych protokołów komunikacyjnych (i jest bardzo często stosowany) postanowiłem sprawdzić jak wygląda przykładowa implementacja na zestawie FPGA. Zacząłem od prostszej rzeczy, czyli implementacji "I2C slave" (poniekąd też slave jest mi bardziej przydatny do moich eksperymentów). Ponieważ projekt jest stosunkowo prosty i nie zajmuje dużo zasobów postanowiłem go przetestować na zestawie FPGA ElbertV.2 (ten używany w kursie FPGA Forbot'a). Masterem I2C będzie zwykłe "Arduino UNO" - z poziomu Arduino bardzo prosto można oprogramować komunikację po I2C z użyciem biblioteki Wire. Punktem wyjścia do implementacji "I2C Slave" jest darmowy projekt zamieszczony na stronie opencores.org. Tutaj link do projektu:

https://opencores.org/projects/i2cslave

Projekt ma stosunkowo prostą budowę i jest właściwie tylko szkieletem do budowy na jego podstawie bardziej skomplikowanych układów (będzie to wyjaśnione dalej). Jak przeważnie się to zdarza trzeba było dodać nowy zegar (DCMs 1 ze Spartan3A) 48 MHz (pętla PLL), ponieważ oryginalny projekt pracował z takim zegarem.

Po wykonaniu potrzebnych przeróbek projekt zajmuje niecałe 30% zasobów układu Spartan3 z Elberta - patrz screen:

 

ISE01.thumb.png.408f9f01ca16b540ab9083415b43d6b6.png

Jak można zobaczyć na podglądzie RTL - top entity projektu ma bardzo prosty interfejs - patrz screen:

TopEntity.thumb.png.8a527a50a3e736530c3f72b2c6aa2773.png

Tutaj kod w Verilog'u głównego modułu projektu (już po dodaniu zegara PLL 48 MHz)

`include "i2cSlave_define.v"


module i2cSlaveTop (
  clk,
  rst,
  sda,
  scl,
  myReg0
);
input clk;
input rst;
inout sda;
input scl;
output [7:0] myReg0;
wire clk48Mhz, clkBufg;

// Instantiate the module PLL clock
PLL_clock PLL_48Mhz (
    .CLKIN_IN(clk), 
    .RST_IN(~rst), 
    .CLKFX_OUT(clk48Mhz), 
    .CLKIN_IBUFG_OUT(clkBufg)
    );

i2cSlave u_i2cSlave(
  .clk(clk48Mhz),
  .rst(~rst),
  .sda(sda),
  .scl(scl),
  .myReg0(myReg0),
  .myReg1(),
  .myReg2(),
  .myReg3(),
  .myReg4(8'h12),
  .myReg5(8'h34),
  .myReg6(8'h56),
  .myReg7(8'h78)

);


endmodule

Jak widać "i2cSlaveTop" ma trzy wejścia:

  • clk - główny zegar projektu (w Elbercie 12 Mhz)
  • rst - reset asynchroniczny - stanem High więc dla naszej płytki FPGA trzeba go było zanegować
  • scl - zegar magistrali I2c

, jedno wejście/wyjście (inout) sda - dane magistrali I2C

i jedno wyjście (wektor 8 bitów) myReg0 - na tym wyjściu równoległym będą się pojawiać dane wysłane przez mastera I2C (Arduino UNO), dlatego podłączymy je do diod LED w zestawie Elbert, aby móc te dane łatwo obserwować.

Tutaj treść pliku ucf (user constraints) dla płytki FPGA Elbert:

NET "clk"           LOC = P129  | IOSTANDARD = LVCMOS33 | PERIOD = 12MHz;
	 	 
NET "rst" PULLUP;
NET "rst" LOC = P76;

NET "sda"           LOC = P31   | IOSTANDARD = LVCMOS33 | SLEW = FAST | DRIVE = 12;
NET "scl"           LOC = P32   | IOSTANDARD = LVCMOS33 | SLEW = FAST | DRIVE = 12;

# myReg0 - 8 bit data
NET "myReg0[7]"             LOC = P46   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[6]"             LOC = P47   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[5]"             LOC = P48   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[4]"             LOC = P49   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[3]"             LOC = P50   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[2]"             LOC = P51   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[1]"             LOC = P54   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "myReg0[0]"             LOC = P55   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;

Jak widać zdefiniowany jest domyślny pin zegara (12 MHz), reset - dolny z pięciu switchy, diody LED (myReg0 - dane przesłane po I2C do płytki Elbert), scl i sda 2 piny magistarli I2C (pierwsze dwa piny złącza P1 na płytce Elbert - patrz rysunek:

ZlaczeP1_Elbert.thumb.png.7b96d0abec4463480b856dccbee41977.png

Jeśli chodzi o piny magistrali I2C (scl, sda) od strony Arduino UNO to zaznaczyłem je fioletową obwódką na rysunku pinout'u dla UNO:

ArduinoUNO_pinout.thumb.jpg.c7cfdfa72f45bb538781fc9c4dc9db6e.jpg

Oczywiście pomiędzy zestawem FPGA (poziom logiki 3,3V) a Arduini UNO (poziom logiki 5V) trzeba wstawić na tych dwóch liniach (scl, sda) konwertery poziomów logicznych.Ja zrobiłem to za pomocą takiego konwertera poziomów (bo tylko taki miałem pod ręką)

https://botland.com.pl/pl/konwertery-napiec/2523-konwerter-poziomow-logicznych-dwukierunkowy-4-kanalowy-pololu.html

Tutaj zdjęcie całego układu (konwerter poziomów na płytce stykowej):

IMG_0174.thumb.JPG.89ec392020de2c33cb1b68a334dc9f93.JPG

Skoro połączenia mamy omówione wróćmy może do kodu projektu (Verilog). Przejdżmy do pliku  "i2cSlave_define.v" plik ten jest dołączany (dyrektywa include) za pomocą linii:

`include "i2cSlave_define.v"

do wszystkich plików żródłowych projektu (Verilog) podobnie jak w języku C. W pliku tym mamy zdefiniowane bardzo ważne parametry modulów - patrz źródła:

// ----------------------- i2cSlave_define.v --------------------

// stream states
`define STREAM_IDLE 2'b00
`define STREAM_READ 2'b01
`define STREAM_WRITE_ADDR 2'b10
`define STREAM_WRITE_DATA 2'b11

// start stop detection states
`define NULL_DET 2'b00
`define START_DET 2'b01
`define STOP_DET 2'b10

// i2c ack and nak
`define I2C_NAK 1'b1
`define I2C_ACK 1'b0

// ----------------------------------------------------------------
// ------------- modify constants below this line -----------------
// ----------------------------------------------------------------

// i2c device address
`define I2C_ADDRESS 7'h3c

// System clock frequency in MHz
// If you are using a clock frequency below 24MHz, then the macro
// for SDA_DEL_LEN will result in compile errors for i2cSlave.v
// you will need to hand tweak the SDA_DEL_LEN constant definition
`define CLK_FREQ 48

// Debounce SCL and SDA over this many clock ticks
// The rise time of SCL and SDA can be up to 1000nS (in standard mode)
// so it is essential to debounce the inputs.
// The spec requires 0.05V of hysteresis, but in practise
// simply debouncing the inputs is sufficient
// I2C spec requires suppresion of spikes of 
// maximum duration 50nS, so this debounce time should be greater than 50nS
// Also increases data hold time and decreases data setup time
// during an I2C read operation
// 10 ticks = 208nS @ 48MHz
`define DEB_I2C_LEN (10*`CLK_FREQ)/48

// Delay SCL for use as internal sampling clock
// Using delayed version of SCL to ensure that 
// SDA is stable when it is sampled.
// Not entirely citical, as according to I2C spec
// SDA should have a minimum of 100nS of set up time
// with respect to SCL rising edge. But with the very slow edge 
// speeds used in I2C it is better to err on the side of caution.
// This delay also has the effect of adding extra hold time to the data
// with respect to SCL falling edge. I2C spec requires 0nS of data hold time.
// 10 ticks = 208nS @ 48MHz
`define SCL_DEL_LEN (10*`CLK_FREQ)/48

// Delay SDA for use in start/stop detection
// Use delayed SDA during start/stop detection to avoid
// incorrect detection at SCL falling edge.
// From I2C spec start/stop setup is 600nS with respect to SCL rising edge
// and start/stop hold is 600nS wrt SCL falling edge.
// So it is relatively easy to discriminate start/stop,
// but data setup time is a minimum of 100nS with respect to SCL rising edge
// and 0nS hold wrt to SCL falling edge.
// So the tricky part is providing robust start/stop detection
// in the presence of regular data transitions.
// This delay time should be less than 100nS
// 4 ticks = 83nS @ 48MHz
`define SDA_DEL_LEN (4*`CLK_FREQ)/48

Dla nas najważniejsze są dwa  parametry:

`define I2C_ADDRESS 7'h3c

Pierwszy to adres naszego slave'a w magistrali I2C (jest dostepnych 128 różnych adresów) - wynosi on 3C zapisany heksadecymalnie. Drugi to częstotliwość zegara - 48 MHz:

`define CLK_FREQ 48

Teraz drugi plik "registerInterface":

`include "i2cSlave_define.v"


module registerInterface (
  clk,
  addr,
  dataIn,
  writeEn,
  dataOut,
  myReg0,
  myReg1,
  myReg2,
  myReg3,
  myReg4,
  myReg5,
  myReg6,
  myReg7

);
input clk;
input [7:0] addr;
input [7:0] dataIn;
input writeEn;
output [7:0] dataOut;
output [7:0] myReg0;
output [7:0] myReg1;
output [7:0] myReg2;
output [7:0] myReg3;
input [7:0] myReg4;
input [7:0] myReg5;
input [7:0] myReg6;
input [7:0] myReg7;

reg [7:0] dataOut;
reg [7:0] myReg0;
reg [7:0] myReg1;
reg [7:0] myReg2;
reg [7:0] myReg3;

// --- I2C Read
always @(posedge clk) begin
  case (addr)
    8'h00: dataOut <= myReg0;  
    8'h01: dataOut <= myReg1;  
    8'h02: dataOut <= myReg2;  
    8'h03: dataOut <= myReg3;  
    8'h04: dataOut <= myReg4;  
    8'h05: dataOut <= myReg5;  
    8'h06: dataOut <= myReg6;  
    8'h07: dataOut <= myReg7;  
    default: dataOut <= 8'h00;
  endcase
end

// --- I2C Write
always @(posedge clk) begin
  if (writeEn == 1'b1) begin
    case (addr)
      8'h00: myReg0 <= dataIn;  
      8'h01: myReg1 <= dataIn;
      8'h02: myReg2 <= dataIn;
      8'h03: myReg3 <= dataIn;
    endcase
  end
end

endmodule


 

Plik ten jest o tyle ważny, że widać w nim iż w projekcie jest używanych 8 rejestrów 8-mio bitowych (cztery wejściwe i cztery wyjściowe). Ale tylko jeden rejestr jest w głównym module (implementacja jest trochę niepełna - o czym autor pisze, że jest to punkt wyjścia do własnych implementacji). Dla nas jest jedynie istotne, że dla adresu 8'h00 (czyli 0 dziesiętnie rejestr myReg0 jest obsługiwany poprawnie), będzie to istotne przy pisaniu mastera I2C na Arduino UNO.

Tutaj zamieszczam działający projekt " I2C slave" (dla Elbert'a) - Xilinx ISE 14.7:

I2CSlave01.zip

Jeśli ktoś nie chce uruchamiać syntezy może od razu załadować za pomocą programu "Elbert V2 Configurator"  plik "i2cslavetop.bin" do zestawu FPGA. Po wgraniu do zestawu FPGA Elbert pliku konfiguracji i wykonaniu połączeń z Arduino UNO możemy przejść do programu dla Arduino. Najpierw warto programem "I2C scanner" sprawdzić, czy Arduino z załadowanym programem skanera  wykryje nam slave'a i2C pod adresem 0x3C. Tutaj kod progrmu "I2C scanner" - program należy skompilować za pomocą "Arduino IDE" i wgrać do płytki Arduino UNO. Kod programu:

#include <Wire.h>
 
void setup()
{
  Wire.begin();
 
  Serial.begin(9600);
  while (!Serial);             // Leonardo: wait for serial monitor
  Serial.println("\nI2C Scanner");
}
 
void loop()
{
  byte error, address;
  int nDevices;
 
  Serial.println("Scanning...");
 
  nDevices = 0;
  for(address = 1; address < 127; address++ )
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
 
    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address<16)
        Serial.print("0");
      Serial.print(address,HEX);
      Serial.println("  !");
 
      nDevices++;
    }
    else if (error==4)
    {
      Serial.print("Unknown error at address 0x");
      if (address<16)
        Serial.print("0");
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");
 
  delay(5000);           // wait 5 seconds for next scan
}

Tak wygląda prawidłowe wykrycie slave'a (FPGA) pod adresem 0x3C na magistrali I2C przez Arduino:

I2Cskaner_.thumb.png.f597016ab35adbed94dcbed3baa251f0.png

Jeśli to zadziała, to możemy wgrać taki krótki program mastera I2C na Arduino UNO:

#include <Wire.h>

void setup() {
  Wire.begin(); // join i2c bus (address optional for master)
}

byte x = 0;

void loop() {
  Wire.beginTransmission(0x3C); // transmit to device #3C
  Wire.write(0);               //wysylamy adres rejestru 0 (ten odczytany w kodzie Verilog)
  Wire.write(x);              // wysylamy jeden bajt - wartosc x
  Wire.endTransmission();    // stop transmitting
  x++;
  delay(500);
}

Jeśli wszystko poszło OK, możemy obserwować na diodach zestawu Elbert zmienijącą się wartość x (inkrementowaną co jeden obieg pętli głównej programu) przesyłaną z mastera (Arduino) do slave'a (FPGA). Patrz filmik (sorry nie mogę przesłać na forbot pliku mp4 z filmem).

Pozdrawiam

 

Edytowano przez FlyingDutch
  • Lubię! 2
Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.