AVR Embedded Tutorial - SPI/de

From Lazarus wiki
Revision as of 22:15, 21 October 2017 by Timm Thaler (talk | contribs) (Eine SPI-Unit für Embedded AVR / Arduino Controller)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

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

Voraussetzung ist ein eingerichteter Crosscompiler für AVR Controller wie hier beschrieben: http://wiki.freepascal.org/Arduino_und_Lazarus

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

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

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.