Dieser Abschnitt soll den Anschluss eines Temperatursensors vom Typ DS16S20 an einen MC ATMega8 erläutern.
Hierbei handelt es sich um den Nachfolger des Sensors DS1620. Hauptfeatures des DS16S20 sind:
Da die Möglichkeit des Betreibens mit "parasite power" nicht verwendet wird, müssen also alle drei Pins des Sensors (TO-92 Gehäuse) angeschlossen werden. Da beim Anschluss an Betriebsspannung (VDD) und Masse (GND) nicht weiter zu beachten ist, gilt es noch zu erwähnen, dass zwischen dem Datenpin (DQ) und Mikrocontroller die eigentliche Datenverbindung herzustellen ist. Da der Sensor mit einem Open-Drain-Ausgang daher kommt, muss zusätzlich ein 4,7K Pullup-Widerstand (WICHTIG!) zwischen der Busleitung und VDD eingefügt werden.
Die durch den Sensor gemessene Temperatur wird
durch einen 9 Bit Wert, also 2 Byte, repräsentiert. Der Wert liegt dann in
Zweierkomplementdarstellung vor. Doch was ist das Zwei-Komplement?
Nimmt man als Beispiel die binäre Zahl 1010 so wäre ihr Eins-Komplement 0101,
d.h. die Einsen werden einfach durch Nullen und umgekehrt die Nullen durch
Einsen ersetzt. Das Zwei-Komplement erhält man, indem man zusätzlich noch 1 zum
Eins-Komplement hinzu addiert. In oben angeführten Beispiel würde sich
demzufolge 0101+0001=0110 ergeben. Gesamt sieht das ganze dann so aus:
binär | vorzeichenlos | 2-Komplement |
00000000 00000001 00000010 00000011 ... ... 01111110 01111111 |
0 1 2 3 ... ... 126 127 |
0 1 2 3 ... ... 126 127 |
10000000 10000001 10000010 ... ... 11111110 11111111 |
128 129 130 ... ... 254 255 |
-128 -127 -126 ... ... -2 -1 |
Auf den IC bezogen sieht die ganze Sache fast genauso aus, nur dass hier ein 9 Bit Wert vorliegt. Des weiteren geht die Stufung nicht in ganzen Schritten sondern jeweils in 0,5°C Abschnitten.
Temperatur | Daten des IC |
+85°C | 0000 0000 1010 1010 |
+25°C | 0000 0000 0011 0010 |
+0,5°C | 0000 0000 0000 0001 |
+0°C | 0000 0000 0000 0000 |
-0,5°C | 1111 1111 1111 1111 |
-25°C | 1111 1111 1100 1110 |
-55°C | 1111 1111 1001 0010 |
Wie man aus den empfangenen Daten nun eine Temperatur gewinnt, bleibt Sache des Anwenders. Ich möchte hier nur eine simple Möglichkeit aufzeigen. Zunächst muss dafür gesorgt werden, dass im Falle einer negativen Temperatur (8. Bit gesetzt) alle Bits des oberen Bytes auf '1' gesetzt werden. Da auch das 7. Bit des unteren Bytes immer '1' ist, so denn die Temperatur unter Null ist, kann man schreiben:
temperature |= 0xFF80;
oder auch
temperature |= 0b1111111110000000;
Nun muss dem MC lediglich noch mitgeteilt werden, dass es sich bei dem so modifizierten Wert um eine negative Zahl handelt. Das geschieht einfach dadurch, dass man ihn von vornherein als signed short definiert hat.
signed short temperature = 0;
Da der Sensor die Werte in Schritten von 0,5°C ausgibt, muss letztendlich der Inhalt von temperature noch durch zwei geteilt werden. Das geschieht am besten kurz vor der Ausgabe, so dass man keine zusätzliche Variable mehr benötigt.
unsigned char buffer [5];
dtostrf (((double) temperature)/2, 5, 1, buffer);
Die Funktion dtostrf (...) gibt einen double-Wert (Gleitkomma) mit einer bestimmten Formatierung (hier: max. Länge 5; eine Nachkommastelle) in ein Array aus, damit dieser dann per USART ausgegeben oder auch auf einem LCD dargestellt werden kann.
lcd_puts (buffer);
Um eine Temperaturmessung zu starten, muss der
Master (also der Mikrocontroller) ein entsprechendes Kommando an den Slave
(Sensor) übermitteln. Nach abgeschlossener Messung kann die gemessene Temperatur
dann ausgelesen werden. Nach dem Start einer Temperaturmessung kann der Master
entweder eine fixe Zeitspanne warten (ca. 750ms) und dann einfach die Temperatur
abfragen oder ständig Lesezyklen erzeugen, nachdem er das Startkommando erzeugt
hat. Bei dieser Möglichkeit sendet der Sensor solange Nullen, bis er die Messung
abgeschlossen hat. Dann werden Einsen gesendet. Es bleibt dem Anwender
überlassen, ob er nun einfach wartet oder ständiges Polling (Nachfragen)
betreibt. Da ein MC im Normalfall jedoch noch andere Aufgaben zu erledigen hat,
empfiehlt sich eher die erste Variante.
Das mit dem Sensor über das Auslesen spezieller Register auch Auflösung größer
als 9 Bit erreicht werden können, soll hiermit erwähnt sein. Darauf näher
eingehen werde ich nicht. Auch die Alarmfunktionen (Termostatfunktionen) sollen
hier zunächst unbeachtet bleiben.
Die in den ROM fest eingebrannte Adresse besteht aus 64 Bit. Die ersten 8 Bit
(0:7) beinhalten den so genannten "family code", welcher den Sensor als
Temperatursensor kennzeichnet. Der Wert beträgt 10h. Die nächsten 48 Bit
beinhalten die eigentliche Seriennummer. Die obersten 8 Bit bilden das CRC-Byte
(cyclic redundancy check). Auf dessen Berechnung und Erläuterung soll hier
zunächst verzichtet werden, es handelt sich dabei um eine Art Prüfsumme.
Kommunikation:
Alle Daten werden mit dem niederwertigsten Bit (lsb) voran übertragen.
Jegliche Kommunikation mit dem Sensor erfolgt mit Hilfe der folgenden drei Schritte:
zu 1.
Jegliche Kommunikation über den Bus beginnt mit einem Reset-Impuls des Masters gefolgt von einem Impuls des Slaves, welcher anzeigt, dass er am Bus angeschlossen und bereit ist.
zu 2.
Dieses Kommando arbeitet mit der 64 Bit ROM-Adresse der/des Slave(s). Die ROM-Befehle ermöglichen dem Master Anzahl und Art der angeschlossenen Slaves zu erkennen. Insgesamt gibt es 5 je 8 Bit lange ROM-Befehle. Es muss immer zuerst ein ROM-Befehl gesendet werden, bevor der eigentliche Funktions-Befehl gesendet werden darf.
SEARCH ROM [F0h] | Mit Hilfe dieses Kommando wird es dem Master ermöglicht, Anzahl und Art der angeschlossenen Slaves sowie deren Adressen zu bestimmen. Sind mehrere unbekannte Slaves angeschlossen, muss ein mehr oder weniger komplizierter Eliminierungsprozess gestartet werden, um die Adressen aller Slaves zu erhalten. Darauf soll hier nicht weiter eingegangen werden. |
READ ROM [33h] | Dieser Befehl wird anstatt des zuvor erklärten verwendet, wenn nur ein Slave am Bus angeschlossen ist. Verwendet man ihn bei mehrer Slaves, kommt es zu Datenkollisionen auf dem Bus, da alle versuchen zur selben Zeit zu antworten. |
MATCH ROM [55h] | Dieser Befehl gefolgt von einer 64 Bit Adresse eines/des an den Bus angeschlossenen Slaves spricht diese(n) direkt an. Alle eventuell noch zusätzlich vorhandenen Slaves ignorieren den Befehl, wenn die gesendete Adresse nicht mit ihrer eigenen übereinstimmt und warten auf den nächsten Reset-Impuls. |
SKIP ROM [CCh] | Mit diesem Befehl werden alle am Bus angeschlossenen Slaves angesprochen. Er kann beispielsweise verwendet werden, um eine Temperaturmessung an allen Sensoren gleichzeitig in Gang zu setzen. Es muss nachfolgend dann keine Adresse gesendet werden. Ist nur ein Slave am Bus, so kann man mit diesem Kommando einfach mit diesem kommunizieren, ohne jedes mal dessen Adresse zu senden. |
ALARM SEARCH [ECh] | Hiermit kann nach eventuell aufgetretenen Alarmen gefragt werden, so denn die Termostatfunktionen des Sensors genutzt wurden. |
zu 3.
Nachdem der Master nun den Slave seiner Wahl mittels eines ROM-Befehls ausgewählt hat, kann nun der eigentliche Funktionswunsch übermittelt werden.
Convert T [44h] | Starten einer Temperaturmessung | Sensor sendet ständig Status an den Master. |
Read Scratchpad [BEh] | Liest den kompletten Speicherinhalt aus, inklusive des CRC-Bytes. | Sensor sendet bis zu 9 Byte an den Master. |
Write Scratchpad [4Eh] | Schreibt TH und TL in den Speicher des Sensors (flüchtig). | Master sendet 2 Bytes an den Slave. |
Copy Scratchpad [48h] | Kopiert TH und TL aus dem Speicher (flüchtig) des Sensors in dessen EEPROM. | Keine Busaktivität. |
Recall E2 [B8h] | Kopiert TH und TL aus dem EEPROM des Sensors in dessen flüchtigen Speicher. | Sensor sendet ständig Status an den Master. |
Read Power Supply [B4h] | Teilt dem Master die Versorgungsart des Sensors mit (normal bzw. per parasite power). | Sensor sendet Versorgungsstatus an den Master. |
Den Aufbau des Speichers des Sensors kann auch die folgende Grafik, welche dem Datenblatt des Sensors entnommen wurde, verdeutlichen.
Auf dem 1-Wire-Bus des Sensors gibt es verschiedene Signalarten:
Jegliche Kommunikation auf dem Bus beginnt durch ein Reset-Signal des Masters gefolgt von einem Antwort-Signal des Slaves. Dieses signalisiert dem Master, dass mindestens ein Slave am Bus und arbeitsbereit ist.
Erzeugt wird das Reset-Signal, indem der Master den Bus für mindestens 480µs gegen Masse zieht. Danach schaltet er seinen Buspin als Eingang und der Bus wird über den Pullup-Widerstand wieder auf H-Pegel gehoben. Wenn der DS18S20 die steigende Flanke detektiert, sendet er innerhalb von 15µs-60µs ein Antwortsignal, indem er den Bus für 60µs-240µs gegen Masse zieht.
Um Daten über den Bus übertragen zu können, müssen Schreib- bzw. Lesezyklen erzeugt werden (Read/Write Time Slots). Während eines Zyklus kann immer nur ein Bit übertragen werden. Die Dauer eines Zyklus muss mindestens 60µs betragen und zwischen den Zyklen muss jeweils eine Pause von mindestens 1µs bestehen.
Ein Schreibzyklus wird gestartet, indem der Master den Bus gegen Masse zieht. Schaltet er seinen Bus-Pin nun innerhalb von 15µs als Eingang (Bus bekommt wieder H-Pegel), so wird das vom Slave als '1' gedeutet. Macht er das nicht und hält den Bus für 60µs bis 120µs auf L-Pegel so interpretiert das als '0'.
Lesezyklen müssen immer nach den oben erwähnten Befehlen erzeugt werden. Das Timing ähnelt dem der Schreibzyklen, die Dauer beträgt analog 60µs bis 120µs und es muss eine Pause von 1µs zwischen den Zyklen eingehalten werden. Um einen Lesezyklus zu starten, muss der Master analog einem Schreibzyklus den Bus gegen Massen ziehen. Nach mindestens 1µs muss er den Bus dann wieder verlassen (Pin als Eingang). In den 15µs nach der durch den Master erzeugten fallenden Flanke kann dieser nun den Bus abtasten. Ist der Bus auf H-Pegel, so hat der Slave eine '1' gesendet, ist der Bus auf L-Pegel, so wurde eine '0' gesendet.
Die bis jetzt gegebenen Informationen müssten ausreichen, die entsprechende Software für den DS16S20 zu implementieren.
Die Vorgehensweise für das simple Auslesen einer Temperatur soll die folgende Grafik verdeutlichen:
Da die grobe Programmstruktur somit steht, kann sich nun an die Programmierung gemacht werden.
Wie bereits aus den obigen Ausführungen hervor
gegangen ist, ist der eigentliche Kommunikationsvorgang relativ unspektakulär,
die einzige Hürde bietet das vorgegebene Timing. So muss also eine Möglichkeit
gefunden werden, dieses mikrosekundengenau möglich zu machen. Ich habe mich zur
Verwendung eines Timers entschlossen, da mir Inline-Assembler dann doch nicht so
liegt :)
Es muss jedoch erwähnt werden, dass man bei dieser Funktion dafür Abstriche bei
der Genauigkeit machen muss.
#define XTAL 8 //in MHz
void delay_us (unsigned short us) {
TCCR1B |= _BV(CS10);
TCNT1 = 0;
while (TCNT1 < XTAL*us);
}
Die obige Funktion liefert nun in etwa eine beliebige Wartezeit zwischen 0 und 65536us. Natürlich kann die Timerinitialisierung (TCCR1B = ...->Prescaler 1) auch ausgelagert werden, damit sie nicht ständig erneut ausgeführt wird. Hauptgedanke der Funktion ist, dass der MC je einen Zählschritt je Takt ausübt. Bei 8MHz sind es also 8 Schritte je µs. Da die Funktion eine Auflösung von einer Mikrosekunde haben soll, ist somit die letzte Zeile (while (...)) selbsterklärend.
Als nächstes muss nun der Reset-Impuls erzeugt und das Antwortsignal vom Sensor entgegen genommen werden.
unsigned char reset (void) {
DDRx |= _BV(Px);
//Ausgang
PORTx &= ~_BV(Px);
delay_us (480);
DDRx &= ~_BV(Px);
delay_us (80);
if (!(PINx & _BV(Px))) {
//Prüfe Slave-Antwort
delay_us (450);
return 1;
}
else
return 0;
}
Für DDRx, PORTx, PINx und Px müssen natürlich die entsprechenden Register/Pins eingesetzt werden.
Nun müssen noch Elementarfunktionen zum Lesen und Schreiben von Bits entworfen werden.
unsigned char read_bit (void) {
DDRx |= _BV(Px);
PORTx &= ~_BV(Px);
delay_us (1);
DDRx &= ~_BV(Px);
delay_us (12);
if (!(PINx & _BV(Px)))
//Abtastung innerhalb von 15µs
return 0;
else
return 1;
}
void write_bit (unsigned char bitval) {
DDRx |= _BV(Px);
PORTx &= ~_BV(Px);
if (bitval)
PORTx |= _BV(Px);
delay_us (110);
DDRx &= ~_BV(Px);
PORTx &= ~_BV(Px);
}
Da Bits normalerweise immer byteweise übertragen werden, gilt es nun Funktionen dafür zu schreiben.
unsigned char read_byte (void)
{
unsigned char byte = 0;
for (i=0; i<8; i++) {
if (read_bit ())
byte |= _BV(i);
delay_us (120);
}
return byte;
}
void write_byte (unsigned char byte) {
for (i=0; i<8; i++) {
if (byte & _BV(i))
write_bit (1);
else
write_bit (0);
}
delay_us (120);
}
Mit Hilfe der Funktionen read_byte(), write_byte(...) und reset() ist nun eine Kommunikation mit dem Sensor möglich. Ein Auslesen des Scratchpad erfolgt nun durch die simplen Zeilen:
unsigned char read_scratchpad
(void) {
if (reset ()) {
write_byte (0xCC);
write_byte (0x44);
wait_ready ();
if (reset ()) {
write_byte (0xCC);
write_byte (0xBE);
for (i=0; i<9; i++)
scratchpad [i] = read_byte ();
return 1;
}
}
return 0;
}
Das Ergebnis der Messung steht nun unter
anderem im Array Scratchpad []. Wie bereits oben erwähnt, muss dem Sensor etwas Zeit
gegeben werden, eine Messung durchzuführen. Man kann man wie bereits
oben erwähnt, in einer Schleife Lesezyklen erzeugen, welche erst abbricht,
wenn der Sensor beginnt Einsen zu senden. Dann könnte
fortgefahren werden, indem der Master den nächsten Reset auslöst. Allerdings
sollten man zwischen zwei Aufrufen immer etwas Zeit (bspw. 1ms) verstreichen
lassen.
Hier nun eine Variante, in der der Eingang gepollt wird, bis der Sensor Einsen sendet.
void wait_ready (void) {
while (!(read_bit ()));
}
Eine komplette Implementierung kann wie bei den anderen Projekten auch, der Rubrik Programme entnommen werden (getestet auf ATMega8 bei 8MHz).