Kampis Elektroecke

1-Wire Implementierung für den AVR

Über das von Dallas Semiconductor (mittlerweile Maxim Integrated) entwickelte 1-Wire Protokoll können zahlreiche Sensoren oder Peripheriebausteine über zwei oder drei Leitungen mit einem Master verbunden werden. Die Besonderheit an dieser Schnittstelle ist, dass die Datenleitung (DQ) gleichzeitig für die Spannungsversorgung genutzt werden kann (parasitäre Stromversorgung), wodurch der Bus mit nur zwei Leitungen arbeiten kann:

  • Die Datenleitung DQ
  • Eine Masseverbindung

Wenn die Datenleitung über einen Pull-up-Widerstand mit der Spannungsversorgung verbunden wird, sind, je nach verwendeter Leitung, Buslängen von 100 – 300 m möglich.

Schauen wir uns mal an, wie ein 1-Wire Bus auf einem AVR Mikrocontroller (hier ein XMega256A3BU) implementiert werden kann und wie dann mit Hilfe dieser Implementierung ein DS18B20 1-Wire Temperatursensor ausgelesen wird.

Grundlagen des 1-Wire Busses:

Der 1-Wire Bus verwendet keine separate Taktleitung für die Datensignale. Daher muss (ähnlich wie beim USART) ein vorgegebenes Timing eingehalten werden, damit eine funktionierende Kommunikation stattfinden kann. Beim 1-Wire gibt es zu jeder Zeit nur einen Master und die Datenleitung wird mit einem schwachen Pull-up Widerstand (meistens wird eine Spannung von 3 V oder 5 V verwendet) versehen.

Die Datenleitung ist bei einem Slave Device als bidirektionaler Open-Drain I/O Port ausgelegt, wodurch sowohl Master als auch die Slaves Daten senden können. Durch die Verwendung eines Open-Drain Treibers für die Datenleitung entsteht eine Wired-AND Verbindung, wodurch der Master immer nur mit einem einzelnen Slave kommunizieren kann. Der Master muss zudem in der Lage sein eine Verzögerung von 1 µs, bzw. von 0,25 µs für den sogenannten Overdrive Modus, einem schnellen Modus für eine höhere Datenübertragung von bis zu 142 KBit/s zu erzeugen.

Beim 1-Wire besteht eine Kommunikation aus insgesamt vier grundlegende Operationen:

  • Eine logische 1 senden
  • Eine logische 0 senden
  • Ein Bit einlesen
  • Reset auslösen

Bei jeder Datenübertragung durch den Master (egal ob lesend oder schreibend) wird zuerst ein Reset-Puls ausgelöst. Jeder angeschlossene Slave gibt am Ende des Resets einen sogenannten Presence Puls aus, indem es die Datenleitung auf Low zieht. Dieser Presence Pulse signalisiert dem Master, dass ein Gerät an den Bus angeschlossen ist. Erfolgt kein Puls, so ist kein Gerät angeschlossen. Nach einem Reset wird ein allgemeines ROM-Kommando gesendet um entweder die Busteilnehmer zu identifizieren oder einen Busteilnehmer auszuwählen. Sobald der Teilnehmer ausgewählt worden ist, wird ein gerätespezifisches Kommando abgesetzt.

Jede der grundlegenden Operationen ist aus einem festen Ablauf aus High/Low-Pegeln aufgebaut und muss zudem bestimmten Zeitvorgaben entsprechen.

Daraus ergibt sich der folgende Ablauf:

Operation Beschreibung Umsetzung
Logische 1 senden Eine 1 an die 1-Wire Slaves senden Bus auf Low ziehen, Delay A, Bus freigeben, Delay B
Logische 0 senden Eine 0 an die 1-Wire Slaves senden Bus auf Low ziehen, Delay C, Bus freigeben, Delay D
Bit einlesen Ein Bit von den 1-Wire Slaves einlesen Bus auf Low ziehen, Delay A, Bus freigeben, Delay E, Bus einlesen, Delay F
Reset Die 1-Wire Slaves reseten Delay G, Bus auf Low ziehen, Delay H, Bus freigeben, Delay I, Bus einlesen, Delay J

Und für die Verzögerungen gelten die folgenden Werte:

Parameter Verzögerungszeit [µs]
Standard Overdrive
A 6 1,0
B 64 7,5
C 60 7,5
D 10 2,5
E 9 1,0
F 55 7
G 0 2,5
H 480 70
I 70 8,5
J 410 40

Okay, die Grundlagen des 1-Wire Busses sind nun klar. Schauen wir uns mal die Implementierung an…

Implementierung des 1-Wire Busses:

Für die Implementierung verwende ich mein XMEGA-A3BU Xplained Entwicklungsboard und als Slaves einen DS18B20 1-Wire Temperatursensor. Die Implementierung soll allgemein umgesetzt werden, sodass der fertige 1-Wire Treiber z. B. auch auf einem ATmega32 oder einer anderen Plattform, wie dem Raspberry Pi, verwendet werden kann.

Im ersten Schritt muss der I/O für die Datenleitung DQ, in diesem Beispiel der Pin 0 des Portes E, initialisiert werden. Der I/O muss als Ausgang geschaltet und gesetzt werden um einen High-Pegel auf den Bus zu legen.

Für den benötigten Pull-up Widerstand nutze ich den internen Pull-up Widerstand des Mikrocontrollers.


Hinweis:

Je nach verwendetem Mikrocontroller werden die Pull-up Widerstände anders eingeschaltet. Diese Zeile muss dem entsprechend an den verwendeten Mikrocontroller angepasst werden.


Nach der Initialisierung gebe ich den Busteilnehmern etwas Zeit um die interne Initialisierung abzuarbeiten. Anschließend führe ich einen kompletten Reset aller Busteilnehmer durch:

Der Aufbau der Reset-Funktion ergibt sich aus den Timings der Spezifikation und dem Ablauf eines 1-Wire Resets:

Um Timingprobleme durch Interrupts zu vermeiden wird direkt nach dem Funktionsaufruf das SREG gespeichert und die globalen Interrupts deaktiviert. Nach der Verzögerungszeit I wird DQ als Eingang geschaltet und der Busstatus eingelesen. Wenn der Bus keinen Low-Pegel führt, ist der Reset fehlgeschlagen und es wird eine entsprechende Fehlermeldung zurückgegeben.

Analog zu der Reset-Funktion werden die Funktionen zum Senden eines Bits/Byte

bzw. zum Lesen eines Bits/Bytes programmiert:

Damit ist der Bus fertig initialisiert und die Funktionen zum Schreiben, bzw. Lesen des Busses sind erstellt. Der Master kann nun damit beginnen die Adressen der angeschlossenen Busteilnehmer in Erfahrung zu bringen, sodass er einzelne Busteilnehmer ansprechen und mit ihnen kommunizieren kann.

Eine Übertragung mittels 1-Wire folgt strikt dem selben Ablauf und eine falsche Reihenfolge führt automatisch zu einem Übertragungsfehler:

  1. Initialisierung der Teilnehmer durch einen Reset
  2. Übermittelung des ROM-Kommandos (1-Wire spezifisch)
  3. Übermittelung des Funktionskommandos (Zielgerät spezifisch)

Über die Adressen können dann die einzelnen Busteilnehmer angesprochen werden. Jeder 1-Wire Slave besitzt ein 64 Bit großes ROM über das sich die einzelnen Slaves identifizieren und welches vom Master genutzt wird um einzelne Geräte anzusprechen. Das ROM beinhaltet

  • eine 8 Bit CRC (Bits [63:56])
  • eine 48 Bit lange Seriennummer (Bits [55:8])
  • einen 8 Bit langen ID-Code für die Bausteinfamilie (Bits [7:0])

Es werden zwei Verfahren bereitgestellt, mit denen der Master an die Informationen in den ROMs gelangen kann:

  • Search ROM Kommando (Code 0xF0): Ein Suchalgorithmus, der die einzelnen Teilnehmer des Busses identifiziert und die Teilnehmeradressen ermittelt.
  • Read ROM Kommando (Code 0x33): Erfragt die Identifikation eines einzelnen Busteilnehmers. Deutlich einfacher als Search ROM, funktioniert aber nur wenn maximal ein Teilnehmer am Bus angeschlossen ist.

Der dritte Punkt in der Übertragung unterscheidet sich je nach Familie des angesprochenem Zielgerätes und wird für die Ermittelung der ROM-Codes nicht benötigt.

Wenn also der ROM-Code eines einzelnen Busteilnehmers erfragt werden soll, so muss der Master einen Reset durchführen, den Read ROM-Befehl senden und anschließend die Informationen aus dem ROM einlesen:

Wobei OneWire_ROM_t eine entsprechende Struktur ist um die Informationen aus dem ROM zu speichern:

Der Master ist nun bereits in der Lage ein einzelnes 1-Wire Gerät am Bus zu identifizieren. Hier mit einem DS18B20 Temperatursensor als Beispiel:

Falls sich mehrere (auch unterschiedliche) Teilnehmer am Bus befinden muss der Search ROM-Befehl genutzt werden um die einzelnen Teilnehmer zu identifizieren. Die Funktionsweise des Suchalgorithmus erklärt Maxim Integrated in der Application Note 187. Für die Implementierung des Suchalgorithmus in den 1-Wire Treiber nutze ich die angepasste Referenzimplementierung von Maxim Integrated:


Info:

Einige 1-Wire Peripheriegeräte (wie z. B. der DS18B20) verfügen über die Möglichkeit durch bestimmte Ereignisse in einen Alarmzustand zu wechseln. Das 1-Wire Protokoll bietet die Möglichkeit alle Geräte, bei denen das Alarmflag gesetzt ist, zu suchen. Die Suche wird auf identische Art durchgeführt wie die ROM-Suche, nur das die Suche nach Alarmgeräten mit dem Befehlscode 0xEC anstatt mit 0xF0 gestartet wird.


Ein vollständiger Suchlauf besteht aus vier verschiedenen Funktionen:

Funktionsname Beschreibung
OneWire_StartSearch Initialisiert den Algorithmus, startet die Suche und sucht den ersten Teilnehmer
OneWire_IsLast Überprüft ob der letzte Teilnehmer gefunden wurde
OneWire_SearchNext Sucht den nächsten Busteilnehmer
OneWire_StopSearch Stoppt die Suche

Nach jedem empfangenen Byte wird eine CRC berechnet um Übertragungsfehler zu erkennen. Da die Berechnung der CRC 1-Wire spezifisch ist, nutzt jedes 1-Wire Gerät das selbe CRC-Polynom. Daher kann die Berechnung der CRC, genau wie in der Referenzimplementierung, über eine feste Lookup-Table erfolgen.

Ein kompletter Suchlauf kann z. B. folgendermaßen implementiert werden (auch hier wieder für einen DS18B20 Temperatursensor):

Bei der Suche werden nur Teilnehmer gespeichert, die den richtigen ID-Code (0x28) besitzen. Die Suche liefert die Anzahl der gefundenen DS18B20 Temperatursensoren zurück und speichert die ROM-Codes in einem Array.


Hinweis:

Da es sich bei einem Mikrocontroller (in der Regel) um ein System mit einer statischen Speicherzuweisung handelt, muss die Größe des ROM-Arrays im Vorfeld bekannt sein, sprich man muss wissen wie viele Teilnehmer am Bus angeschlossen sind oder wie viele Teilnehmer maximal gesucht werden sollen.


Der Code um alle angeschlossenen DS18B20 Sensoren zu ermitteln sieht somit folgendermaßen aus:

Am Ende des ROM-Suchlaufs kennt der Master die ROM-Codes aller Busteilnehmer und kann damit beginnen mit den einzelnen Teilnehmern zu kommunizieren.

In der Regel kommuniziert der Master immer nur mit einem Teilnehmer gleichzeitig. Es gibt einige Ausnahmen, mit denen der Master einen Befehl an alle Teilnehmer senden kann (z. B. um bei einem DS18B20 eine Temperaturmessung zu starten) aber keine Kommunikation vom Slave in Richtung Master kann nur einzeln erfolgen. Zu Beginn der Kommunikation muss der Master das Zielgerät auswählen. Hierfür werden die ermittelten ROM-Codes genutzt.

Dazu sendet der Master einen Match ROM-Befehl (0x55), gefolgt von dem ROM-Code und anschließend den gerätespezifischen Befehl. Die Auswahl des Busteilnehmers kann z. B. folgendermaßen umgesetzt werden:

Die Funktion erwartet als Übergabewert einen Zeiger auf den entsprechenden ROM-Code. Wenn die Adresse des Zeigers NULL ist, sprich keine ROM-Struktur übergeben wurde, wird ein Skip ROM-Befehl gesendet um alle Busteilnehmer zu adressieren. Falls eine Adresse übergeben wurde, so werden der Match ROM-Befehl und die acht Bytes des ROM-Codes übertragen.

Damit ist das Zielgerät ausgewählt und kann im Anschluss daran vom Master mit einem Funktionscode bedient werden. Dieser Funktionscode (und ggf. ein paar zusätzliche Datenbytes) werden vom Master nacheinander übertragen. Falls der Master das Zielgerät auslesen will, so sendet er erst den entsprechenden Funktionscode und ließt dann die benötigte Anzahl an Datenbytes ein. Nachfolgend ein kurzes Beispiel um das Scratchpad, also die Zwischenablage, eines DS18B20 zu beschreiben:

Damit ist der 1-Wire Treiber abgeschlossen und stellt grundlegende Funktionen bereit um mit 1-Wire Geräten zu kommunizieren. Den fertigen 1-Wire Treiber inkl. einer Beispielimplementierung für DS18B20 Temperatursensoren für einen XMega256A3BU findet ihr in meinem GitLab-Repository.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.