AVR Embedded Tutorial - SPI/de

From Lazarus wiki
Jump to: navigation, search

Zur Übersichtseite AVR Embedded Tutorial.

Nutzung der Hardware-SPI-Schnittstelle bei einem ATmega328 / Arduino

Voraussetzung ist ein eingerichteter Crosscompiler für AVR Controller wie hier beschrieben:

Zu den Grundlagen von SPI, Master-Slave, Verdrahtung siehe Wikipedia:

Das ist eine ganz einfache SPI-Unit, bei der die Einstellungen für

  • Datenformat
  • Mode
  • ChipSelect
  • Speed

von Hand vorgenommen werden müssen. Zur Funktion der einzelnen Register und Flags siehe Datenblatt zum ATmega328.

SPI-Master

Zuerst kommt der

Header der Unit

welcher die Pin- und Portkonfiguration enthält:

unit gh_spi;
 
{$mode objfpc}{$H+}
{$goto on}
 
interface
 
uses
  ; 
 
procedure spi_init();
procedure spi_transfer_sync(var dout : array of uint8; var din : array of uint8; len : uint8);
procedure spi_transmit_sync(var dout : array of uint8; len : uint8);
function spi_fast_shift(data : uint8) : uint8;
 
const
  Psck      = 1 shl 5;  // PB5
  Pmiso     = 1 shl 4;  // PB4
  Pmosi     = 1 shl 3;  // PB3
  Pss       = 1 shl 2;  // PB2
  Pcs       = 1 shl 1;  // PB1
 
var
  DDRspi   : byte absolute DDRB;  // SPI an Port B
  PORTspi  : byte absolute PORTB;
  PINspi   : byte absolute PINB;
 
implementation

Wenn man mehrere SPI-Devices ansprechen will, kann man sich dafür mehrere CS (ChipSelect) Pins definieren. Das SS Pin muß auf Ausgang stehen. Steht es auf Eingang, kann darüber das SPI-Interface in den Slave-Mode geschaltet werden - und nix geht mehr. Und wenn es einmal auf Ausgang steht, kann man es auch gleich noch für CE (ChipEnable) oder CS (ChipSelect) verwenden.

Initialisierung

// SPI initialisieren
 
procedure spi_init();
begin
	DDRspi := DDRspi and not(Pmiso);  // MISO Eingang
	DDRspi := DDRspi or (Pmosi or Psck or Pss);  // MOSI, SCK, SS Ausgang
	// SS muß Ausgang sein, sonst Umschalten auf Slave von außen möglich
 
	SPCR := ((1 shl SPE) or (0 shl SPIE) or (0 shl DORD) or (1 shl MSTR) or (0 shl CPOL) or (0 shl CPHA) or (%01 shl SPR));
	// SPI enable, MSB first, Master select, SCK 1MHz = XTAL / 16 x 2
	SPSR := (1 shl SPI2X);	// SCK x 2 auf 1 MHZ
end;

Eingestellt werden hier

  • SPE = 1: SPI enable
  • SPIE = 0: kein Interrupt
  • DORD = 0: MSB first, 1: LSB first
  • MSTR = 1: als Master, kann aber durch Pin SS von Außen überschrieben werden, 0: als Slave
  • CPOL = 0: SCK Polarität normal, 1: SCK invertiert
  • CPHA = 0: SCK Phase normal, 1: SCK verschoben
  • SPR = 01: fcpu / 16
  • SPI2X = 1: fcpu * 2, ergibt 1MHz Clock bei 8Mhz Controllertakt, siehe Datenblatt für andere Clockraten.

Die Register entsprechend der Unit ATmega328P aus den Embedded AVR Sources. Für andere Controller unbedingt die Register anhand des Datenblattes überprüfen.

Daten übertragen

Um Daten von einem Device zu lesen, müssen gleichzeitig auch Daten geschrieben werden. Das können aber auch Dummydaten sein. Die Anzahl geschriebener und gelesener Bytes ist gleich.

// SPI Daten senden, empfangen
 
procedure spi_transfer_sync(var dout : array of uint8; var din : array of uint8; len : uint8);
var
  cnt : uint8;
begin
	for cnt := 0 to len - 1 do begin
		SPDR := dout[cnt];  // zu sendendes Byte ausgeben
		while (SPSR and (1 shl SPIF)) = 0 do;  // warten bis fertig
		din[cnt] := SPDR;  // empfangenes Byte einlesen
	end;
end;

Das Schöne bei Pascal ist, dass man mit richtigen Arrays arbeiten kann und nicht wie bei C mit Pointern rummachen muß. Wichtig ist hier, dass die Arrays per var übergeben werden. Dann arbeitet Pascal mit dem ursprünglichen Array, andernfalls wird eine Kopie erstellt, was unnötige Zeit und unnötigen Speicherbedarf erfordert.

Die Routine wartet, bis alle Bytes aus dout rausgeschrieben und gleichzeitig die empfangenen Daten in din eingelesen sind, was bei 1Mhz etwa 10µsec pro Byte dauert. Da das real nur wenige Systemtakte pro Byte sind lohnt sich hier ein Interrupt nicht. Die Routine kann durch andere Interrupts (Timer, Uart) unterbrochen werden, was das Ausgeben des nächsten Bytes verzögert, aber unproblematisch ist.

Für dout, din darf auch das gleiche Array angegeben werden, dann werden die ursprünglichen Daten durch die empfangenen Daten ersetzt.

Achtung! Hier und für die folgenden Routinen gilt: Wird die Routine aufgerufen, ohne dass SPE im SPCR gesetzt wurde (siehe Init), bleibt der Controller in der Warteschleife hängen, da das Datenbyte nicht gesendet und folglich das SPIF-Flag nicht gesetzt wird.

Daten nur Schreiben

Wenn nur Daten geschrieben werden soll, aber keine Antwort vom Device erwartet wird, kann man die Routine vereinfachen.

// SPI Daten senden ohne Empfang
 
procedure spi_transmit_sync(var dout : array of uint8; len : uint8);
var
  cnt : uint8;
begin
	for cnt := 0 to len - 1 do begin
		SPDR := dout[cnt];  // zu sendendes Byte ausgeben
		while (SPSR and (1 shl SPIF)) = 0 do;  // warten bis fertig
	end;
end;

Hier gilt das Gleiche wie für die Transfer-Routine.

Einzelnes Byte senden und empfangen

Mitunter muß nur ein einzelnes Byte übertragen werden, zum Beispiel zum Abfragen eines Status.

// SPI einzelnes Byte senden, empfangen
 
function spi_fast_shift(data : uint8) : uint8;
begin
	SPDR := data;		// zu sendendes Byte ausgeben
	while (SPSR and (1 shl SPIF)) = 0 do;  // warten bis fertig
	spi_fast_shift := SPDR;  // empfangenes Byte einlesen
end;

Raus, warten, rein. Eigentlich ganz einfach.

Und fertig ist die Unit.

end.

Anwendung als Master

Die beispielhafte Anwendung der Unit als Master für ein NRF-Modul.

Am Programmanfang muss natürlich spi_init aufgerufen werden. Außerdem sind die Register des NRF-Moduls als Konstanten deklariert und ein Sende- und ein Empfangsbuffer als array of uint8 eingerichtet.

Um das CS müssen wir uns selbst kümmern. Beim NRF-Modul ist das CS low-aktiv, der Ruhepegel ist also high und wird zur Aktivierung auf low gezogen.

Konfig schreiben

procedure nrf_set_config(reg, data : uint8);
begin
	PORTspi := PORTspi and not(Pspi_cs);  // CS setzen, ist beim Modul low-aktiv
	spi_fast_shift(cW_REGISTER or (cREGISTER_MASK and reg));  // Controlbyte senden
	spi_fast_shift(data);  // Datenbyte senden
	PORTspi := PORTspi or Pspi_cs;  // CS löschen, ist beim Modul low-aktiv
end;

Es wird ein Byte zur Adressierung des Registers und ein Byte mit dem Wert gesendet, auf den das Register gesetzt werden soll.

Konfig auslesen

function nrf_get_config(reg : uint8) : uint8;
begin
	PORTspi := PORTspi and not(Pspi_cs);  // CS setzen, ist beim Modul low-aktiv
	spi_fast_shift(cR_REGISTER or (cREGISTER_MASK and reg));  // Controlbyte senden
	nrf_get_config := spi_fast_shift(0);  // Dummy senden und Datenbyte lesen
	PORTspi := PORTspi or Pspi_cs;  // CS löschen, ist beim Modul low-aktiv
end;

Es wird ein Byte zur Adressierung des Registers gesendet, dann wird ein Dummybyte gesendet. Der empfangene Registerinhalt wird übergeben.

Register schreiben

procedure nrf_write_register(reg : uint8; len : uint8);
begin
	PORTspi := PORTspi and not(Pspi_cs);  // CS setzen, ist beim Modul low-aktiv
	spi_fast_shift(cW_REGISTER or (cREGISTER_MASK and reg));  // Controlbyte senden
	spi_transmit_sync(nrftxbuf, len);  // Daten aus Tx-Buffer senden
	PORTspi := PORTspi or Pspi_cs;  // CS löschen, ist beim Modul low-aktiv
end;

Es wird ein Controlbyte gesendet, gefolgt von den zu schreibenden Daten. Während des gesamten Vorgangs bleibt CS low.

Register auslesen

procedure nrf_read_register(reg : uint8; len : uint8);
begin
	PORTspi := PORTspi and not(Pspi_cs);  // CS setzen, ist beim Modul low-aktiv
	spi_fast_shift(cR_REGISTER or (cREGISTER_MASK and reg));  // Controlbyte senden
	spi_transfer_sync(nrftxbuf, nrfrxbuf, len);  // Daten in Rx-Buffer lesen, Tx-Buffer ist Dummy
	PORTspi := PORTspi or Pspi_cs;  // CS löschen, ist beim Modul low-aktiv
end;

Es wird ein Controlbyte gesendet, gefolgt von Dummydaten. Die empfangenen Daten stehen im Rx-Buffer.

Eine umfangreiche Anleitung für die nRF24L01+ Funkmodule folgt demnächst.

Siehe auch