Raspberry Pi - SPI/de

From Lazarus wiki
Jump to: navigation, search

Nutzung der SPI-Schnittstelle am Raspberry Pi mit Freepascal

Gleich vorweg: Es ist eine gute Idee, für die Hardwarekomponenten wie SPI, I2C und GPIOs des Raspberry Pi die pascalio Lib (https://github.com/SAmeis/pascalio) zu verwenden.

Allerdings ist die pascalio auch sehr umfangreich und wer verstehen möchte, was da passiert, ist vielleicht mit dieser Anleitung ganz gut bedient.

SPI unter Linux

Auch für die SPI-Schnittstelle gilt das Linux-Prinzip "Alles ist eine Datei". Den Vorteil, dass man mit einfachen Dateizugriffen auf die Schnittstelle zugreifen kann, erkauft man sich allerdings mit geringerer Geschwindigkeit - meist unkritisch - und damit, dass auch andere Programme auf die gleiche Schnittstelle zugreifen können.

Auf dem Raspberry Pi (wir reden hier von Version 3 mit Raspbian Stretch, Stand Okt 2017) gibt es eine SPI-Schnittstelle, die 2 Devices ansprechen kann: /dev/spidev0.0 und /dev/spidev0.1 (Also genaugenommen gibt es mehrere, aber nur eine ist nach außen geführt.)

Zu den Grundlagen von SPI, Master-Slave, Verdrahtung siehe Wikipedia: https://de.wikipedia.org/wiki/Serial_Peripheral_Interface

Header

unit test_spi;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  Classes, SysUtils, BaseUnix;
 
type
  Tspi = class
 
  procedure Init();
  procedure Close();
 
  function DataIn(var din; cnt : byte) : integer;
  function DataOut(const dout; cnt : byte) : integer;
 
  procedure TransferSync(const dout; var din; len : byte);
  procedure TransmitSync(const dout; len : byte);
  function FastShift(dout : byte) : byte;
 
  private
    { private declarations }
  public
    { public declarations }
  end;
 
const
  // SPI Mode Flags
 
  cSPI_CPHA = $01;
  cSPI_CPOL = $02;  
 
  cSPI_MODE_0 = (0 or 0);
  cSPI_MODE_1 = (0 or SPI_CPHA);
  cSPI_MODE_2 = (SPI_CPOL or 0);
  cSPI_MODE_3 = (SPI_CPOL or SPI_CPHA);
 
  cSPI_CS_HIGH   = $04;
  cSPI_LSB_FIRST = $08;
  cSPI_3WIRE     = $10;
  cSPI_LOOP      = $20;
  cSPI_NO_CS     = $40;
  cSPI_READY     = $80;
 
  cSPI_CTRL = $6B;  // ist das "Magic Byte"
 
  // Controlregister
  // Read/Write + Size + MagicByte + Register
 
  cSPI_RD_MODE          : uint32 = $80016B01;
  cSPI_WR_MODE          : uint32 = $40016B01;
  cSPI_RD_LSB_FIRST     : uint32 = $80016B02;
  cSPI_WR_LSB_FIRST     : uint32 = $40016B02;
  cSPI_RD_BITS_PER_WORD : uint32 = $80016B03;
  cSPI_WR_BITS_PER_WORD : uint32 = $40016B03;
  cSPI_RD_MAX_SPEED_HZ  : uint32 = $80046B04;
  cSPI_WR_MAX_SPEED_HZ  : uint32 = $40046B04;
 
  cSPI_DEVICE = '/dev/spidev0.0';  // Device 0, ChipSelect 0
//  cSPI_DEVICE = '/dev/spidev0.1';  // Device 0, ChipSelect 1
  cSPI_SPEED  = 1000000;  // Datenrate in Hz
  cSPI_BITS   = 8;  // Datenbits
  cSPI_LSBF   = 0;  // LSB first = -1
  cSPI_MODE   = cSPI_MODE_0;
 
type
  spi_ioc_transfer = record  // Controlregister für FileIO benötigt
    tx_buf : uint64;  //pointer;
    rx_buf : uint64;  //pointer;  // immer 64-bit
    len : uint32;    // Anzahl Zeichen
    speed : uint32;  // Datenrate in Hz
    delay : uint16;  // Verzögerung CS in usec
    bpw : uint8;  // Bits per Word
    csc : uint8;  // CS change
    txn : uint8;
    rxn : uint8;
    pad : uint16;
  end;  // ingesamt 32 Byte
 
var
  spi : Tspi;
  spihnd : longint;  // der filehandle für die Schnittstelle
 
implementation

Initialisierung

 // SPI initialisieren
 
procedure Tspi.Init();
var
  val8 : byte;
  val32 : longword;
begin
  try
    spihnd := FpOpen(cSPI_DEVICE, O_RdWr);  // Schnittstelle für Read/Write öffnen
 
    if spihnd <> -1 then begin
      val8 := cSPI_MODE;
      FpIOCtl(spihnd, cSPI_WR_MODE, @val8);  // Mode setzen
      val8 := cSPI_BITS;
      FpIOCtl(spihnd, cSPI_WR_BITS_PER_WORD, @val8);  // Datenbits per Byte setzen
      val8 := cSPI_LSBF;  //-1
      FpIOCtl(spihnd, cSPI_WR_LSB_FIRST, @val8);  // MSB oder LSB first setzen
      val32 := cSPI_SPEED;
      FpIOCtl(spihnd, cSPI_WR_MAX_SPEED_HZ, @val32);  // Speed setzen
    end;
  finally
 
  end;
end;

Die Schnittstelle wird beim Programmstart geöffnet und die Parameter eingestellt. Die Parameter können auch später noch geändert werden. Zum Beispiel kann die Clockrate erst langsam gesetzt und dann erhöht werden, was für einige SD-Karten nötig ist.

Achtung! Die Clockrate wird aus dem momentanen Systemtakt abgeleitet. Ändert sich der Takt, ändert sich die Taktrate. So geht eine Clockrate von 1Mhz bei 600MHz Systemtakt bei höherer Prozessorlast und 1200MHz Systemtakt auf 2MHz hoch.

Und: Die Clockrate wird durch einen Teiler 2^n aus dem Systemtakt abgeleitet und stimmt nie genau. Wenn man 1MHz angibt, liegt die resultierende Rate irgendwo bei 800kHz.

Beenden

// SPI beenden
 
procedure Tspi.Close();
begin
  if spihnd <> -1 then begin
    FpClose(spihnd);
  end;
end;

Ja, am Programmende machen wir die Schnittstelle brav wieder zu.

Daten auslesen

// SPI Buffer Read
 
function Tspi.DataIn(var din; cnt : byte) : integer;
begin
  if spihnd <> -1 then begin
    DataIn := FpRead(spihnd, din, cnt);
  end;
end;

Da "Alles eine Datei" ist, können wir ein einfaches Read auf die SPI-Schnittstelle anwenden. Das gibt den Takt für die angegebene Anzahl Bytes aus und liest die Antwort des Devices in ein Array din.

Daten senden

// SPI Buffer Write
 
function Tspi.DataOut(const dout; cnt : byte) : integer;
begin
  if spihnd <> -1 then begin
    DataOut := FpWrite(spihnd, dout, cnt);
  end;
end;

Genauso können wir Daten mit Write senden. Dabei wird die angegebene Anzahl Bytes aus dem Array dout ausgegeben.

SPI Transfer

Allerdings verlangen die meisten SPI-Devices eine etwas komplexere Interaktion als einfaches Senden und Empfangen. Oft werden gleichzeitig Daten gesendet und empfangen, und das überfordert die Möglichkeiten eines Read und Write. Hier brauchen wir eine Transfer-Funktion.

// SPI Daten senden, empfangen
 
procedure Tspi.TransferSync(const dout; var din; len : byte);
var
  outbuf : array of byte;
  inbuf : array of byte;
  transfer : spi_ioc_transfer;
begin
  if len > 0 then begin
    SetLength(outbuf, len);
    FillByte(outbuf[0], len, 0);
    Move(dout, outbuf[0], len);
 
    SetLength(inbuf, len);
    FillByte(inbuf[0], len, 0);
 
    FillByte(transfer, SizeOf(transfer), 0);
    transfer.tx_buf := uint64(@outbuf[0]);
    transfer.rx_buf := uint64(@inbuf[0]);
    transfer.len := len;
    transfer.delay := 0;
    transfer.speed := cSPI_SPEED;
    transfer.bpw := cSPI_BITS;
    transfer.csc := 0;
 
    try
      FpIOCtl(spihnd, $40206B00, @transfer);
    finally
      Move(inbuf[0], din, len);
    end;
  end;
end;

Das ist jetzt schwierig. Um gleichzeitig Senden und Empfangen zu können, brauchen wir zwei Buffer outbuf und inbuf, in die wir die Daten spiegeln. Deren Startadressen übergeben wir - unbedingt als 64-bit-Werte, auch bei einem 32bit-OS - in das Transfer-Register. Außerdem bekommt das Register noch die zu übertragende Anzahl Bytes und ein paar Einstellungen übergeben.

Witzigerweise kann man hier jedesmal die Einstellungen ändern, also zum Beispiel die Clockrate anpassen.

Anschließen übergeben wir das Transfer-Register mit den Einstellungen dem Dateihandler, der brav die Arrays abklappert und die Daten rausschickt und einliest. Das mystische $40206B00 ist dabei wieder ein Write ($40), gefolgt von der Anzahl der Bytes im Transfer-Register ($20 = 32 Bytes), dem Magic Byte ($6B, siehe oben) und dem Controlregister ($00) der Schnittstelle. Einfach so lassen und nicht dran rumfummeln.

Disclaimer: Das funktioniert hier und jetzt auf dem Raspberry Pi 3 unter Raspbian. Das kann auf anderen Linux-OS und Hardware-Plattformen anders aussehen, vor allem wenn die Byteorder der Adressen eine andere ist.

Und wenn ich nur Daten empfangen will? Dann übergebe ich einfach ein leeres Array. Ich kann auch für dout und din das gleiche Array angeben, dann werden die alten Arraydaten mit den neuen Daten überschrieben.

SPI Transmit

// SPI Daten senden ohne Empfang          
 
procedure Tspi.TransmitSync(const dout; len : byte);
var
  outbuf : array of byte;
  transfer : spi_ioc_transfer;
begin
  if len > 0 then begin
    SetLength(outbuf, len);
    FillByte(outbuf[0], len, 0);
    Move(dout, outbuf[0], len);
 
    FillByte(transfer, SizeOf(transfer), 0);
    transfer.tx_buf := uint64(@outbuf[0]);
    transfer.rx_buf := uint64(@outbuf[0]);  // gleicher Buffer
    transfer.len := len;
    transfer.delay := 0;
    transfer.speed := cSPI_SPEED;
    transfer.bpw := cSPI_BITS;
    transfer.csc := 0;
 
    try
      FpIOCtl(spihnd, $40206B00, @transfer);
    finally
 
    end;
  end;
end;

Sollen Daten nur geschrieben werden, kann man obiges DataOut nehmen, oder diese abgespeckte Variante der Tranfer-Routine.

SPI FastShift

// SPI einzelnes Byte senden, empfangen
 
function Tspi.FastShift(dout : byte) : byte;
var
  din : byte;
  transfer : spi_ioc_transfer;
begin
  din := 0;
  FillByte(transfer, SizeOf(transfer), 0);
  transfer.tx_buf := uint64(@dout);
  transfer.rx_buf := uint64(@din);
  transfer.len := 1;
  transfer.delay := 0;
  transfer.speed := cSPI_SPEED;
  transfer.bpw := cSPI_BITS;
  transfer.csc := 0;
 
  try
    FpIOCtl(spihnd, $40206B00, @transfer);
  finally
    FastShift := din;
  end;
end;

Wird nur ein einzelnes Byte gesendet und empfangen, zum Beispiel für eine Statusabfrage, bietet sich diese Version an. Hier steht das Ergebnis nicht in einem Buffer, sondern wird als Funktionswert zurückgegeben.

Fertsch

end.