Der Multiplex Sensor Bus (MSB)
cyblord
Dieser Artikel nimmt am Artikelwettbewerb 2012/2013 teil.
In diesem Artikel wird der "Multiplex Sensor Bus" beschrieben. Dieser dient zur Ankopplung von Telemetriesensoren an M-Link Modellbauempfänger der Firma Multiplex. Damit sollte der Leser in die Lage versetzt werden, selbst Sensoren für das M-Link System zu entwickeln. Dazu wird, neben der Theorie des Busses selbst, auf die Implementierung von Sensoren auf Atmel AVR Microcontroller eingegangen und die Entwicklung anhand eines barometrischen Höhensensors praktisch dargestellt.
Überblick Multiplex M-Link System
Die Firma Multiplex brachte mit dem 2,4 GHz Fernsteuersystem "M-Link" ein Rückkanal- und damit Telemetriefähiges RC-Modellbausystem auf den Markt. Dabei werden spezielle RC-Empfänger eingesetzt welche sowohl Daten vom Sender empfangen als auch Daten über einen Rückkanal zurück zum Sender schicken können. Je nach Sender und Zubehör, können diese Telemetriedaten auf einem Display betrachtet oder per Tonsignale sowie Sprache ausgegeben werden. Auf der Sensorseite kommen hier spezielle Empfänger zum Einsatz. Diese haben einen Sensoranschluss. An diesen Anschluss können kompatible Sensoren angeschlossen werden. Der Empfänger fragt alle angeschlossenen Sensoren ab und sendet deren Daten zum Sender. Der Anschluss erfolgt dabei über einen halbduplex Eindrahtbus, genannt "Multiplex Sensor Bus" kurz: MSB.
Aus Benutzersicht wird dabei ein Sensor konfiguriert, in dem man jedem Wert, den der Sensor darstellen kann, eine Adresse (von 0 bis 15) zuordnet. Der Wert dieses Sensors wird dann am Senderdisplay bei dieser Adresse angezeigt. Natürlich darf jede Adresse nur einmal vergeben werden, somit ist die Anzeige von 16 verschiedenen Sensorwerten in einem Modell möglich.
Neben den Adressen kann jedem Sensor auch Alarmschwellen einprogrammiert werden. Überschreitet ein Sensorwert diese Alarmschwellen so wird beim Benutzer ein Alarm ausgelöst und die Adresse mit dem zugehörigen Wert wird im Senderdisplay grafisch hervogehoben.
Damit alle Sensoren möglichst auch ohne vorherige Konfiguration einsatzbereit sind und keine Adresskonflikte auftreten, empfiehlt Multiplex die Verwendung von Standardadressen für bestimmte Werteklassen.
Physikalischer Wert | Adresse | Sensortyp |
---|---|---|
Empfängerspannung | 0 | Empfänger |
Link Quality Indicator (LQI) | 1 | Empfänger |
Spannung | 2 | Spannungssensor |
Strom | 3 | Stromsensor |
Temperatur | 4 | Temperatursensor |
Vario | 6 | Vario / barometrisch |
Höhe | 7 | Vario / barometrisch |
Geschwindigkeit 3D | 9 | GPS-Sensor |
Höhe | 10 | GPS-Sensor |
Entfernung 3D | 11 | GPS-Sensor |
Der Multiplex Sensor Bus
Der MSB ist elektrisch gesehen ein halbduplex Eindrahtbus mit 3,3 Volt Pegeln. Er ist als asynchroner Bus ausgelegt und ist ein logischer UART.
Elektrische Schnittstelle
Die gesamte Schnittstelle zu den Sensoren besteht aus 3 Leitungen: Masse, Versorgungsspannung und Signal. Alle Sensoren sind parallel daran angeschlossen.
Auf der Signalleitung werden alle Daten übertragen. Dabei stellt ein Pegel von 3,3 Volt logisch 1 dar, und 0V=Masse logisch 0. Der Ruhepegel ist 1. Alle Daten werden Byteweise übertragen. Dabei wird jedes Byte von einem Startbit (logisch 0 = 0V) eingleitet und von einem Stopbit (logisch 1 = 3,3V) abgeschlossen. Die Daten werden mit 38400 Baud gesendet. Die Schnittstelle entspricht also einer Standard-UART-Schnittstelle mit den Parametern 8N1.
Logische Schnittstelle
Die Busteilnehmer sind als Master/Slave organisiert. Der Modellbauempfänger ist immer in der Rolle des Masters, die Sensoren sind immer Slaves. Es gibt nur einen Master am Bus. Jeder Sensorwert (nicht jeder Sensor!) besitzt eine eindeutige 4 Bit Adresse (0-15). Es sind also maximal 16 Sensorwerte auf dem gesamten Bus möglich. Ein Sensor kann auch mehrere Sensorwerte repräsentieren. So kann es einen Spannungs- und Stromsensor geben, oder einen Sensor welcher Höhe und Vario ausgibt. Der Master fragt reihum alle 16 Adressen ab, in dem er alle ca. 6 ms eine Adresse (0-15) auf dem Bus sendet. Alle Sensoren empfangen dieses Byte. Der Sensor welcher auf diese Adresse konfiguriert ist, antwortet auf die Anfrage mit einer 3 Byte langen Antwort.
Idle line timeout
Alle Sensore am Bus müssen den gesamten Datenverkehr mithören um zu erkennen wann ihre eigene Adresse abgefragt wird. Ein Sensor darf allerdings nicht sofort danach senden, denn es könnte sich auch um eine Antwort eines anderen Sensors handeln. Nach einem einzelnen Byte muss ein Sensor mindestens 256µS warten. In dieser Zeit darf kein weiteres Byte empfangen werden, sonst wird keine gültige Anfrage des Masters erkannt. Diese Zeitspanne ist der "Idle line timeout". In der Realität warten die Sensoren ca. 1,6ms bevor sie eine Antwort senden.
Datenformat und Adressen
Die Antwort des Sensors besteht immer aus genau 3 Bytes welche sich wie folgt zusammensetzen:
Das 1. Byte setzt sich aus 2 Nibbles zusammen. Das höherwertige Nibble muss die Adresse sein, auf welche der Sensor gerade antwortet. Sie ist also ein Echo auf die Anfrage des Masters. Das niederwertige Nibble gibt die Werteklasse an. Eine Werteklasse teilt dem Master mit, welche physikalische Einheit der Wert repräsentiert und welches Einheitenzeichen dem Benutzer angezeigt werden soll. Die Werteklassen sind fest vergeben und es können nur die vordefinierten verwendet werden. Aus dem 4-Bit Wertebereich ergeben sich maximal 16 Werteklassen. Davon sind aktuell 14 vergeben. Eine Tabelle mit allen Werteklassen findet sich weiter unten.
Die andere beiden Bytes ergeben zusammen den Wert selber als 16 Bit Wort. Dabei wird zuerst das LowByte, dann das HiByte übertragen. Der Wert wird als vorzeichenbehaftet im Zweierkomplement dargestellt. Das niederwertigste Bit im LoByte (und damit das niederwertigste Bit im gesamten 16 Bit Wort) hat eine spezielle Funktion. Es ist das Alarm-Flag. Der eigentlich Wert ist also um ein Bit nach links verschoben.
Durch ein gesetztes Alarm-Flag, teilt der Sensor mit, dass der Sensorwert außerhalb der eingestellten Grenzen liegt und dem Benutzer wird daraufhin am Sender dieser Alarm angezeigt.
Wertebereich
Da der eigentliche Sensorwert auf eine Breite von 15 Bit festgelegt ist und der Wert als Zweierkomplement interpretiert wird, ergibt sich ein maximaler Wertebereich für alle Sensorwerte von -16384 bis +16383.
Adressbereiche
Der Master sendet als Anfrage ein Byte. Somit sind hier Adresse von 0-255 (0x00-0xFF) möglich. Da es aber nur maximal 16 Sensoradressen auf dem Bus gibt (0-15) können die restlichen Adressen für Sonderfunktionen benutzt werden. Einige Sensoren verwenden diese z.B. zur Parametrisierung außerhalb des Busses. Es gibt aber auch einige Adresse- und Bereiche welche von Multiplex bereits belegt oder gesperrt sind. Diese sollten nicht für andere Funktionen verwendet werden:
Adressbereich | Funktion | Beschreibung |
---|---|---|
0x00-0x0F | Sensoradressen | für die normale Abfrage der Sensoren (0-15) |
0x5A | Reset | jeder Sensor sollte alle gespeicherten bzw. kumulierten Werte (z.B. min/max/avg) rücksetzen wenn dieser Befehl empfangen wird. |
0x80-0x8F | Reserviert | Für zukünftige Erweiterungen reserviert. Ein Sensor sollte darauf nicht reagieren. |
Werteklassen
Klasse | Beschreibung | Auflösung | Wertebereich |
---|---|---|---|
0 | Sonderklasse für Hinweise | ||
1 | Spannung | 0,1 V | -600 bis +600 |
2 | Strom | 0,1 A | -1000 bis +1000 |
3 | Steigen/Sinken | 0,1 m/s | -500 bis +500 |
4 | Geschwindigkeit | 0,1 km/h | 0 bis +6000 |
5 | Drehzahl | 100 rpm bzw. 10 rpm | 0 bis +500 / -5000 |
6 | Temperatur | 0,1 °C | -250 bis +7000 |
7 | Richtung | 0,1 Grad | 0 bis 3600 |
8 | Höhe | 1m | -500 bis +2000 |
9 | Füllstand | 1% Tank | 0 bis +100 |
10 | Link Quality Indicator | 1% LQI | 0 bis +100 |
11 | Stromverbrauch | 1 mAh | -16000 bis +16000 |
12 | Flüssigkeiten | 1 mL | 0 bis +16000 |
13 | Distanz | 0,1 km | 0 bis +16000 |
Spezielle Werte
Wenn ein Sensor zwar aktiv ist und seine Adresse abgefragt wird, aber er keinen gültigen Wert liefern kann, so kann er den Wert 0x8000 senden. Dies bewirkt dass im Senderdisplay "-.-" + Einheit angezeigt wird. Somit wird dem Benutzer mitgeteilt, dass die Adresse zwar vergeben ist, aber noch kein gültiger Wert dafür vorliegt.
Reales Beispielsignal
Dies ist eine Aufzeichnung eines realen Signals auf dem Sensorbus:
Der Master fragt hier die Adresse 3 ab und sendet diese auf den Bus. 1,6 ms später antwortet der Sensor mit einer 3 Byte langen Antwort: 54, 212, 1.
Das 1. Byte mit dem Wert 54 stellt Adresse (Echo) und Werteklasse dar. Die oberen 4 Bit entsprechen dem Wert 3, was dem korrekten Echo der angeforderten Adresse entspricht. Die unteren 4 Bit haben den Wert 6. Die entspricht der Werteklasse "Temperatur".
Adresse = (x>>4) = 3
Klasse = (x & 0x0F) = 6 = Temperatur
Die restlichen 2 Byte formen den 16 Bit Wert und das Alarm-Flag, nach der Formel:
Wert = ((HiByte<<8)+LoByte)>>1 = ((1<<8)+212)>>1 = 234 = 23,4 °C.
Alarmflag = (LoByte & 1) = 0 = kein Alarm
Implementierung auf AVRs
Da die Telemetriesensoren für die Verwendung in Flugmodellen vorgesehen sind, ist die Größe der fertigen Schaltung kritisch. Aus diesem Grund wird die Implementierung für einen ATtiny85 aufgezeigt. Dieser ist für fertige Layouts auch im kompakten und trotzdem unkritisch zu lötenden SOIC8 Gehäuse erhältlich.
In dieser Implementierung wird der Controller mit einem 8 MHz Quarzoszillator betrieben.
Die Implementierung des Sensorbusprotokolls erfolgt in Form von verschiedenen Schichten. Die unterste Schicht abstrahiert über die Hardware. Dies geschieht vor allem über #defines welche über die Namen der Register abstrahieren und die grundlegenden Funktionen (setze Hi/Lo, Frage ab usw.) ausführen. Darüber ist die Schicht für das reine Senden und Empfangen von Bytes auf dem Bus. Darüber liegt die Schicht welche für das Einhalten von Timings und das Reagieren auf Interrupts zuständig ist. Dazu werden Interruptvektoren und ein Timer verwendet. Darüber liegt das Interface, welches Eingaben vom Hauptprogramm entgegenimmt und entsprechend umwandelt.
Elektrische Anbindung
Wenn der Controller mit 3,3 Volt betrieben wird, so ist keine spezielle Verschaltung zur Anbindung an den MSB notwendig. Ein beliebiger Pin des Controllers kann direkt mit dem Sensorbus verbunden werden. Es kann optional ein 100 Ohm Widerstand zur Sicherheit vor den Controller geschaltet werden. Es ist zu beachten dass die Spannungsversorgung für den Sensor ebenfalls über den Sensoranschluss erfolgt. Hier kann die Spannung im Bereich zwischen 3,5 V - 9 V liegen. Deshalb muss ein passender Spannungsregler auf 3,3 V vorgesehen werden.
Hardwareabstraktion
Für den Sensorbus wird nur ein Portpin benötigt. Dieser wird mittels #defines spezifiziert.
//Port für den Sensorbus definieren
#define MB_BUS PB0
#define MB_DDR DDR0
#define MB_PORT PORTB
#define MB_PIN PINB
Der Master verfügt über Pullup-Widerstände welche den Bus auf 3,3 V Pegel hochziehen. Somit muss der Controller den Bus lediglich auf GND ziehen oder loslassen. Dies erledigen zwei defines. Ein weiteres define übernimmt das Abfragen des Pegels. MB_Bitlen enthält die Zeitdauer für ein Bit in µS.
//Grundlegende Funktionen des Portpins
#define MB_BUS_LO() (sbi(MB_DDR,MB_BUS))
#define MB_BUS_REL() (cbi(MB_DDR,MB_BUS))
#define MB_IS_BUS()(bit_is_set(MB_PIN,MB_BUS))
#define MB_BITLEN 26
UART
Da der Tiny85 keinen HW-UART besitzt muss er in Software nachgebildet werden.
Das Senden eines oder mehrerer Bytes auf den Bus gestaltet sich einfach. Es müssen nur alle 8 Bit im korrekten zeitlichen Abstand auf den Bus ausgegeben werden. Eingleitet wird die Datenübertragung mit einem Startbit. Da der Ruhepegel des Busses logisch 1 beträgt, ist das Startbit immer logisch 0. Die Geschwindigkeit muss 38400 Baud betragen, somit ist der Abstand zwischen den Bits genau 26 µS. Dieser zeitliche Abstand wird mit der _delay_us Funktion realisiert.
void mb_send(uint8_t data) {
MB_BUS_LO();
_delay_us(MB_BITLEN);
for (int i=0;i<8;i++) {
if ((data & 1)>0) MB_BUS_REL();
else MB_BUS_LO();
_delay_us(MB_BITLEN);
data=data>>1;
}
MB_BUS_REL();
_delay_us(MB_BITLEN);
}
Der Empfang von Bytes verläuft analog dazu.
Es ist zu beachten dass diese Funktion nicht auf ein Startbit wartet, da der Aufruf erst dann erfolgt wenn per Interrupt bereits ein solches Startbit detektiert wurde.
uint8_t mb_receive() {
uint8_t data=0;
_delay_us(MB_BITLEN);
_delay_us(MB_BITLEN/2);
for (int i=0;i<8;i++) {
data>>=1;
if (MB_IS_BUS()) data |=128;
_delay_us(MB_BITLEN);
}
return data;
}
Die Funktion wartet zu Beginn 1 1/2 Bitlängen um sich auf die Mitte der Bits zu synchronisieren.
Bus-Timing und Interrupts
Um den Sensorbus korrekt zu bedienen und alle Timings einzuhalten, wird ein 8-Bit Timer des Controllers verwendet. Darüber hinaus wird der Pin-Change-Interrupt am entsprechenden Pin gesetzt. So kann jeder beliebige Pin benutzt werden.
sbi(PCMSK,MB_BUS); //Pin Change Interrupt
sbi(GIMSK,PCIE); //Pin Change Interrupt ein
sbi(TCCR0B,CS02); //clk/256
OCR0A=50; //CTC bei ca. 1,6 ms
sbi(TIMSK,OCIE0A); //Compare Match Interrupt ein
sbi(TIMSK,TOIE0); // Timer 0 Overflow Interrupt ein
Der Timer wird im CTC-Modus mit einem 256 Takt Vorteiler betrieben. Der Compare-Match-Wert wird auf 50 gesetzt. Dies entspricht ca. 1,6 ms. Darüber hinaus wird der Interrupt für den Compare-Match aktiviert.
Wird ein Pin-Change-Interrupt ausgelöst, so wird zunächst geprüft, ob der entsprechende Pin auf Lo-Pegel ist. Damit ist ein Startbit erkannt.
ISR(PCINT0_vect) {
if (!MB_IS_BUS()) mb_startBitDetected();
}
Ist dies der Fall dann wird sofort eine Routine aufgerufen welche den Timer0 zunächst anhält. Dann wird das ankommende Byte vom Bus gelesen. Um zu entscheiden ob es gültig ist, wird der Timerstand überprüft. Ist er größer 18 (entspricht dem bus-idle-timeout) dann ist das Byte zunächst gültig. Weiter passiert aber noch nichts. Lediglich das Interrupt-Flag wird gelöscht und der Timer von 0 an neu gestartet.
void mb_startBitDetected() {
mb_timer_freeze();
mb_lastbyte=mb_receive();
mb_lastvalid=(TCNT0>18);
if (mb_lastvalid) mb_afterRequestByte=1;
sbi(GIFR,INTF0);
mb_timer_restart();
}
Ein Compare-Match-Interrupt des Timers signalisiert dass nun auch nach dem Byte mindestens ein bus-idle-timeout verstrichen ist. Damit ist das Byte als gültige Anfrage des Masters erkannt. Behandelt wird diese Anfrage innerhalb der mb_request_detected Funktion.
ISR(TIM0_COMPA_vect) {
if (mb_lastvalid) {
mb_requestDetected(mb_lastbyte);
mb_afterRequestByte=0;
mb_lastvalid=0;
mb_timer_restart();
}
}
Adressverwaltung
Um schnell auf Anfragen reagieren zu können, liegen die Antworten auf alle, von diesem Sensor repräsentierten Adressen in einem Array aus Structs. Das Struct sieht wie folgt aus:
typedef struct MB_DATA {
uint8_t active;
uint8_t response[3];
} MB_DATA_T;
MB_DATA_T mb_data[16];
Es besteht aus der 3 Byte langen Antwort und einem Flag welches anzeigt ob die Adresse vom Sensor beantwortet werden soll.
Diese Structs werden einem 16 Elemete großen Array gehalten. Der Index gibt die Adresse an, für welche das Struct steht.
Jetzt kann auf eine Anfrage des Masters, direkt das entsprechende Struct aus dem Array entnommen und über den Bus als Antwort gesendet werden.
void mb_requestDetected(uint8_t adress) { //Anforderung Sensorwerte für Adresse
if (adress>MB_MAX_ADRESS) mb_commandReceived(adress);
else {
if (mb_data[adress].active==1) {
mb_send(mb_data[adress].response[0]);
mb_send(mb_data[adress].response[1]);
mb_send(mb_data[adress].response[2]);
sbi(GIFR,INTF0);
}
}
}
Hierbei ist zu sehen dass alle Anfragen welche nicht an gültige Adressen gerichet sind, an eine spezielle Funktion weitergegeben werden. Diese gibt das Byte per Callback-Funktion an das darüberliegende Anwendungsprogramm weiter. So kann dieses darauf reagieren und z.B. spezielle Funktionen ausführen.
Einzelne Werte eingeben oder ändern
Dem MSB-System werden alle Werte durch die Funktion mb_update eingegeben. Diese erwartet als Parameter die Sensoradresse, die Werteklasse, den Wert selbst und das Alarm-Flag.
void mb_update(uint8_t adr,uint8_t unit,int16_t value,uint8_t alert) {
uint8_t b1,b2,b3;
b1=adr;
b1<<=4;
b1+=unit;
if (value!=MB_NOVALUE) {
value<<=1;
if (alert==MB_ALERT) value++;
}
b2=value;
b3=(value>>8);
mb_data[adr].response[0]=b1;
mb_data[adr].response[1]=b2;
mb_data[adr].response[2]=b3;
mb_data[adr].active=1;
}
Die eingegeben Werte werden zerlegt und an der richtige Stelle im Array abgelegt.
Das Interface
#define MB_SPECIAL 0
#define MB_VOLTAGE 1
#define MB_CURRENT 2
#define MB_VARIO 3
#define MB_SPEED 4
#define MB_RPM 5
#define MB_TEMP 6
#define MB_DIR 7
#define MB_ALT 8
#define MB_TANK 9
#define MB_LQI 10
#define MB_CAP 11
#define MB_FLUID 12
#define MB_LDIST 13
#define MB_NOALERT 0
#define MB_ALERT 1
void mb_init();
void mb_update(uint8_t adr,uint8_t uni,int16_t value,uint8_t alert);
void mb_novalue(uint8_t adr,uint8_t unit);
void mb_setCommandCallback(void (*commandReceived)(uint8_t));
void mb_send(uint8_t data);
void mb_sendString(char* s);
uint8_t mb_receive();
uint8_t mb_receiveBlocked();
uint8_t mb_isStart();
Beispielnutzung des Codes
#include "mbus.h"
void commandReceived(uint8_t data) {
[...]
}
int main() {
mb_init();
mb_setCommandCallback(&commandReceived);
sei();
while(1) {
mb_update(4,MB_VOLTAGE,55,MB_NOALERT);
[...]
}
return 0;
}
In diesem Beispiel würde nun der Sensor auf dem Senderdisplay bei Adresse 4 eine Anzeige von 5,5 Volt hervorrufen.
Referenz
Funktion | Beschreibung | |
---|---|---|
void mb_init() | Initalisiert den Sensorbus. Sollte vor jeglicher Nutzung des Interfaces aufgrufen werden. | |
void mb_update(uint8_t adr,uint8_t unit,int16_t value,uint8_t alert) | Dient zum eingeben neuer Werte in das System. Es muss die Adresse, die Werteklasse, der Wert selbst und das Alarm-Flag übergeben werden. | |
void mb_novalue(uint8_t adr,uint8_t unit) | Bewirkt dass die Sensoradresse zwar als belegt kennzeichnet, dort aber kein Wert angezeigt wird. | |
void mb_setCommandCallback(void (*commandReceived)(uint8_t)) | Erwartet einen Pointer auf eine Funktion welche für das Handling von speziellen Befehlen zuständig ist. | |
void mb_send(uint8_t data) | Erlaubt es direkt ein Byte über den Bus zu senden. Dies ist nützlich falls die serielle Verbindung noch anderweitig genutzt werden soll (z.B. zur Parametriesierung) | |
uint8_t mb_receive() | Erlaubt es direkt ein Byte über den Bus zu empfangen. Dies ist nützlich falls die serielle Verbindung noch anderweitig genutzt werden soll (z.B. zur Parametriesierung). Es wird nicht auf das Startbit gewartet. | |
uint8_t mb_receiveBlocked() | Wie mb_receive() aber es wird gewartet bis ein Startbit empfangen wurde. | |
uint8_t mb_isStart() | Gibt 1 zurück falls auf dem Bus ein Startbit (logisch 0) anliegt. |
Beispielprojekt: Höhensensor
Um die Verwendung des Multiplex Sensorbusses praktisch zu demonstrieren soll hier auf die Entwicklung eines barometrischen Höhensensors für Modellflugzeuge eingegangen werden.
Als Hardware kommen ein ATTiny85, ein Bosch Drucksensor vom Typ BMP180, ein 3,3 Volt Spannungsregler (TS5204), ein 8 MHz Quarzoszillator sowie Kleinzeug zum Einsatz.
Da der Drucksensor per I2C angeschlossen wird, und der Sensorbus nur einen Pin benötigt ist nach Abzug von 2 Pins für den Oszillator noch der RESET Pin des Controllers frei. Dieser ist an den ADC angeschlossen und soll deshalb noch als zusätzliche Spannungsmessung dienen. Der BMP180 liefert außerdem noch die Umgebungstemperatur. So wird der Sensor ein kombinierter Höhen-, Temperatur- und Spannungssensor.
Die folgenden Werte soll der Sensor auf dem Sensorbus bereitstellen können:
- Aktuelle Flughöhe in Metern
- Maximale Flughöhe in Metern
- Temperatur
- ext. Spannung (z.B. Flugakku)
- Steigen/Sinken (Vario, in m/s)
- maximale Steigrate (m/s)
- maximale Sinkrate (m/s)
Es sollen Alarme für folgende Messwerte programmierbar sein:
- Flughöhe
- Temperatur
- Spannung
Schaltung
Der Schaltplan ist ohne Überraschungen. Der Spannungsregler wird von 2 2µ2 Kondensatoren gestützt, der Drucksensor wird per I2C direkt an den Controller angeschlossen. Externe PullUp Widerstände fehlen. Aus Platzgründen werden die internen des Controllers verwendet. Zur Spannungsmessung kommt ein Spannungsteiler 10k/1k3 zum Einsatz. So können Spannungen bis 26 Volt (6 Lipozellen) gemessen werden. Der verwendete Spannungsregler arbeitet bis 16V Eingangsspannung. Somit sind die vorgegebenen 9 Volt mit ordentlich Reserve eingehalten.
Der Drucksensor
Kommunikation
Der Drucksensor wird per I2C-Bus abgefragt. Dazu wird eine normale I2C-Library für den ATTiny verwendet. Für die Kommandos an den BMP180 wurde mit dem Datenblatt und Beispielcode gearbeitet. Da die Messwerte des Sensors mit den ab Werk eingespeicherten Kalibrierwerten verrechnet werden müssen, werden hier vom Hersteller C-Routinen bereit gestellt.
Daraus wurde von mir ein C-Modul geschrieben, welches das folgende Interface bietet:
typedef struct bmp085 {
bmp085_calibs_t calibs;
long param_b5;
uint8_t oversampling_setting;
uint8_t (*bus_write)( uint8_t,uint8_t,uint8_t *,uint8_t);
uint8_t (*bus_read)( uint8_t, uint8_t, uint8_t *, uint8_t );
void (*delay_msec)(uint8_t);
} bmp085_t;
uint8_t bmp085_get_cal_param(bmp085_t* bmp085);
int16_t bmp085_get_temperature(bmp085_t* p_bmp085, unsigned long ut);
uint32_t bmp085_get_pressure(bmp085_t* bmp085, unsigned long up);
uint16_t bmp085_get_ut (bmp085_t* p_bmp085);
uint32_t bmp085_get_up (bmp085_t* p_bmp085);
Das Struct bmp085 dient als Zwischenspeicher für einen veränderlichen Wert welcher aus der Umgegungstemperatur berechnet und später für die Druckberechnung gebraucht wird. Um dem Code Zugriff auf den I2C-Bus zu geben, werden zwei Funktionspointer erwartet, welche die Schnittstelle zum I2C-Bus darstellen. Darüber hinaus wird eine weitere Funktion benötigt welche eine bestimmte Zeitdauer in ms wartet. Damit kann der Code selbständig die Zeitdauer aus den Einstellungen des Sensors berechnen und einhalten. Es ist auch möglich diese Zeitdauer fest mit der maximalen Wartezeit einzuprogrammieren.
Das Interface zum BMP180 bietet zwei Funktionen um die rohen Messwerte für Temperatur und Druck zu erhalten (_get_ut, _get_up) und zwei Funktionen um aus diesen Messwerten und den Kalibrierdaten die realen Werte für Temperatur und Druck zu bekommen. Die Funktion _cal_param liest am Anfang des Programms nach dem Power-Up einmalig die fest eingespeicherten Kalibrierwerte des Sensors aus und speicher diese in einem speziellen Struct.
Berechnung der Höhe
Das Hauptprogramm nutzt das Interface zum BMP180 um die realen Werte für Temperatur und Druck zu bekommen.
int16_t getBMP085Temperature() {
uint16_t ut=bmp085_get_ut(&bmp085);
return bmp085_get_temperature(&bmp085,ut);
}
uint32_t getBMP085Pressure() {
uint32_t up=bmp085_get_up(&bmp085);
return bmp085_get_pressure(&bmp085,up);
}
Die beiden Funktionen führen zuerst eine Abfrage an den Sensor durch um die Rohdaten zu erhalten und berechnen anschließend daraus den pyhsikalisch korrketen Wert und geben diesen zurück. Die Temperatur wird als vorzeichenbehafteter Wert zurückgegeben welcher die Temperatur in 1/10 Grad repräsentiert. Der Druck wird vorzeichenlos zurückgegeben und enthält den aktuellen Luftdruck in Pascal.
Um nun aus einem Referenzluftdruck (p0, gemessen am Boden beim PowerUp) und dem aktuellen Luftdruck (p) die Höhe des Sensors über dem Startplatz zu bestimmen, wird folgende Formel genutzt. Diese ist abgeleitet aus der internationalen Höhenformel.
[math]\displaystyle{ Alt[m]=-7990\times \ln(\frac{p}{p_0}) }[/math]
1 hPa entspricht so einer Höhendifferenz von 8,4 m. 1 Pa also 8,4 cm.
Im Code wird dies durch eine Funktion berechnet:
int16_t height(long p0,long p) {
double x=(double)p/p0;
x=(double)-7990*log(x);
return lround(x*10);
}
Diese Funktion gibt die Höhe in 1/10 Meter zurück.
Spannungsmessung
Die Spannungsmessung erfolgt über einen Spannungsteiler mit einem 10k und einem 1,3k Widerstand. Somit lassen sich bis zu 26 Volt messen. Die Referenzspannung entspricht der Versorgungsspannung also 3,3 Volt.
Der ADC-Wert, welcher der AVR liefert, hängt also in folgender Weise von der Eingangsspannung ab:
[math]\displaystyle{ ADC = \frac{V_{in}\times1024}{3.3} }[/math]
Da die Spannung über einen Spannungsteiler heruntergebrochen wird gilt:
[math]\displaystyle{ V_{in}=\frac{V}{10000+1300}\times1300 }[/math]
Somit lässt sich die reale Spannung (in 1/10 V) aus dem gemessenen ADC-Wert wie folgt berechnen:
[math]\displaystyle{ ADC = \frac{(\frac{\frac{V}{10}}{10000+1300}\times1300)\times1024}{3.3} }[/math]
Nach V umgestellt und auf ganze Zahlen erweitert, ergibt sich:
[math]\displaystyle{ V = \frac{ADC\times3729}{13312} }[/math]
Im Programm wird dies durch folgende Funktion realisiert:
uint16_t getVoltage() {
sbi(ADCSRA,ADSC);
loop_until_bit_is_clear(ADCSRA,ADSC);
uint32_t x=ADC;
x*=3729;
x/=13312;
return (uint16_t)x;
}
Die Funktion führt die komplette AD-Wandlung durch und berechnet das Ergebnis in 1/10 Volt.
Konfiguration
Da die Anzahl der Adressen am Sensorbus auf 16 begrenzt ist, müssen alle Sensoren konfigurierbar sein. Außerdem müssen benutzerdefinierte Alarme möglich sein.
Alle Konfigurationswerte werden über ein Struct beschrieben:
typedef struct Alert {
int16_t min;
int16_t max;
} Alert_t;
typedef struct Config {
uint8_t adr_alt;
uint8_t adr_maxalt;
uint8_t adr_temp;
uint8_t adr_volt;
uint8_t adr_vario;
uint8_t adr_minvario;
uint8_t adr_maxvario;
Alert_t alert_alt;
Alert_t alert_temp;
Alert_t alert_volt;
uint8_t vario_th_up;
uint8_t vario_th_lo;
} Config_t;
Config_t c;
Config_t c_ee EEMEM;
Alarme werden wiederum in einem eigenen Struct beschrieben. Durch die Deklaration einer Variablen vom Typ Config_t mit dem Zusatz: EEMEM können diese Daten einfach im EEProm abglegt werden. Dazu reicht dann ein Einzeiler.
void saveConfig(Config_t* config) {
eeprom_write_block(config,&c_ee,sizeof(Config_t));
}
void loadConfig(Config_t* config) {
eeprom_read_block(config,&c_ee,sizeof(Config_t));
}
Auch das Senden und Empfangen der Konfiguration über die serielle Schnittstelle des Sensorbusses gestaltet sich in diesem Fall einfach. So können die Daten von einem PC-Programm gespeichert und gelesen werden. Hier zeigt sich der Vorteil des MSB-Interfaces welches das direkte Senden und Empfangen von Bytes über den Sensorbus ermöglicht.
void sendConfig(Config_t* config) {
uint8_t crc=0;
mb_send(VERSION);
crc=_crc_ibutton_update(crc,VERSION);
uint8_t* x=(uint8_t*)config;
for (uint8_t i=0;i<sizeof(Config_t);i++) {
mb_send(x[i]);
crc=_crc_ibutton_update(crc,x[i]);
}
mb_send(crc);
}
void receiveConfig(Config_t* config) {
uint8_t buffer[REC_LEN];
for (uint8_t i=0;i<REC_LEN;i++) {
buffer[i]=mb_receiveBlocked();
}
uint8_t version=buffer[0];
uint8_t rec_crc=buffer[REC_LEN-1];
uint8_t calced_crc=0;
for (uint8_t i=0;i<REC_LEN-1;i++) {
calced_crc=_crc_ibutton_update(calced_crc,buffer[i]);
}
if (version==VERSION && calced_crc==rec_crc) {
mb_send(ACK);
uint8_t *x=(uint8_t*)config;
for (uint8_t i=0;i<sizeof(Config_t);i++) x[i]=buffer[i+1];
saveConfig(config);
}
else {
mb_send(NAK);
}
}
}
Die Kommunikation wird über eine CRC-Checksumme gesichert.
Um dem Sensor Befehle zum Senden oder Empfangen der Konfiguration zu geben, wird die Funktion zur Behandlung spezieller Befehle genutzt.
void commandReceived(uint8_t cmd) {
if (cmd==CMD_SENDDATA) sendConfig(&c);
else if(cmd==CMD_RECDATA) receiveConfig(&c);
}
mb_setCommandCallback(&commandReceived);
Hauptschleife
Die Hauptschleife des Sensorprogrammes hat die Aufgabe alle Sensorwerte zu erfassen, die Höhe und den Variowert über Oversampling und Mittelwertbildung zu berechnen und diese Werte an den richtigen Adressen auf dem Sensorbus auszugeben.
Um die Ausgabe auf dem Sensorbus zu vereinheitlichen, gibt es eine zusätzliche Funktion mlink_update welche zusätzlich zu einem Wert noch ein Alarm-Struct akzeptiert und anhand dieser Daten den korrekten Aufruf der eigentlichen Funktion mb_update durchführt.
void mlink_update(uint8_t adr,int16_t value,uint8_t unit, const Alert_t* alert) {
uint8_t al=MB_NOALERT;
if (value<alert->min || value>alert->max) al=MB_ALERT;
mb_update(adr,unit,value,al);
}
Diese Funktion wird dann im Hauptprogramm verwendet um die Sensorwerte auszugeben.
while(1) {
temperature=getBMP085Temperature();
for (i=0;i<P_BUF_LEN;i++) {
pressure_buffer[i]=getBMP085Pressure();
}
subPressure=calcMedian(pressure_buffer,i+1);
pressure+=subPressure;
pressure/=2;
alt_10cm=height(pRef,pressure);
vario_counter++;
if (vario_counter==5) {
vario_counter=0;
vario=vario+(alt_10cm-last_alt_10cm);
vario/=2;
last_alt_10cm=alt_10cm;
}
if (vario<min_vario) min_vario=vario;
if (vario>max_vario) max_vario=vario;
alt_m=lround((double)alt_10cm/10);
if (alt_m>max_alt) max_alt=alt_m;
volt=getVoltage();
mlink_update(c.adr_alt,alt_m,MB_ALT,&c.alert_alt);
mlink_update(c.adr_maxalt,max_alt,MB_ALT,&ALERT_OFF);
mlink_update(c.adr_temp,temperature,MB_TEMP,&c.alert_temp);
mlink_update(c.adr_volt,volt,MB_VOLTAGE,&c.alert_volt);
mlink_update(c.adr_vario,vario,MB_VARIO,&ALERT_OFF);
mlink_update(c.adr_minvario,min_vario,MB_VARIO,&ALERT_OFF);
mlink_update(c.adr_maxvario,max_vario,MB_VARIO,&ALERT_OFF);
}
Programmieren des Controllers
Da das Layout keine ISP-Anschlussmöglichkeit bietet muss der Controller vor dem einlöten programmiert werden. Es muss nur der Flash-Speicher geschrieben und die Fusebits eingestellt werden. Der EEProm wird vom Programm selbständig mit den Default-Daten gefüllt.
Da der Controller mit einem externen Oszillator auf 8 MHz laufen sollte müssen die Fusebits dahingehend angepasst werden. Außerdem muss RESET deaktiviert werden um mit dem RESET-Pin die Spannungsmessung durchführen zu können. Es ist zu beachten dass die zwar ohne weiteres mit einem normalen ISP-Programmer möglich ist, aber der Controller danach nicht mehr programmiert werden kann. Nur ein HV oder debugWire Programmer können dies wieder rückgängig machen.
Funktion | Wert |
---|---|
BODLEVEL | 1.8 Volt |
CKDIV8 | Nein |
CKOUT | Nein |
DWEN | Nein |
EESAVE | Ja |
RSTDISBL | Ja |
SELFPRGEN | Nein |
SPIEN | Ja |
CKSEL | Ext. Crystal OSC 3.0-8.0 MHz |
SUT | 258 CK/14CK + 4.1 ms |
WDTON | Nein |
Gesamte Lo-Fuse | 0xCC |
Gesamte Hi-Fuse | 0x5E |
Mithilfe von avrdude kann die Programmierung und das setzen der Fusebits sehr einfach und schnell vorgenommen werden. Der folgende Befehl schreibt den Flash und setzt die Fuses auf die korrekten Werte. Danach ist der Controller sofort einsatzbereit und kann eingelötet werden:
avrdude -pt85 -cstk500 -PCOM3 -u -Uflash:w:MLINK_ALT.hex:a -Ulfuse:w:0xcc:m -Uhfuse:w:0x5e:m
Die Parameter -c und -P müssen je nach Programmer und Anschlussnummer angepasst werden.
Layout und Platine
Das Layout ist doppelseitig ausgeführt. Allerdings befinden sich nur die Lötpads auf der Bottom-Seite. Alles andere ist auf dem Top-Layer untergebracht. Dies vereinfacht die eigene Herstellung der Platine mit heimischen Mitteln.
An die 3 Lötpads SIGNAL, SUPPLY, und GND wird direkt ein normales Servokabel angelötet. So lässt sich der Sensor direkt mit der Sensorbuchse eines M-Link Empfängers verbinden. Soll der Sensor mit anderen Sensoren an einem Bus betrieben werden, so muss er entweder das letzte Glied in der Kette sein, oder es wird ein handelsüblichen Y-Kabel verwendet.
Die restlichen beiden Lötpads entprechen dem Messeingang und Masse. Sie sind im Abstand von 2,54mm so kann direkt eine 2 polige Stiftleiste angelötet werden. Daran kann dann komfortabel der muss messende Akku eingesteckt werden.
Durch die Gehäuseform des Drucksensors muss dieser mit Heißluft eingelötet werden. Dies kann aber ohne spezielle Lötpaste o.ä. passieren. Das folgende Video zeigt wie man solche Gehäusearten einfach einlöten kann:
http://www.youtube.com/watch?v=c_Qt5CtUlqY
Signalvergleich
Nun ist es interessant einmal die Signale eines originalen Sensors mit denen des selbstgebauten Sensors zu vergleichen. Zu diesem Zweck wurden 3 Sensorabfragen durchgeführt und die erzeugten Signale mit einem LA gemessen.
Downloads
- Datei:Msb altimeter sch.sch
- Datei:Msb altimeter board.brd
- Datei:Msb altimeter source.zip
- Datei:Msb altimeter hex.hex