Für ein Projekt sollen Messungen mit dem AD7606 (ADC mit acht Kanälen und max. 200kSpS) mit möglichst hoher Taktfrequenz durchgeführt werden. Die folgende Klasse für den STM32F103 (Bluepill) schafft in gut 5μs einen Messzyklus. Timergesteuert, wegen der Zykluszeitkonstanz, kann eine Zykluszeit von 6μs, knapp 167MHz, erreicht werden.
Version | Anpassungen |
---|---|
1.0 (2021-11-05) | Basis-Version |
1.1 (2021-11-09) | Datenübernahme war falsch. Übernahme muss nach steigender Flanke des Clock-Signals geschehen. |
2021-11-14 | Die Anschlusstabelle enthielt Fehler. Korrekt ist CVA ⇒ PB6, DB14 (HBEN) ⇒ LOW
(GND). Das Beispielprogramm hatte einen Fehler in der Ausgabe-Funktion (printReport). |
1.1 (2021-11-26) |
|
Hinweis: Zur Installation des Arduino-Bootloaders auf einem STM32 siehe Arduino-FAQ.
Zur Beschreibung des AD7606 und des Entwicklungsboards siehe URS AD7606.
Wichtig: Damit die Geschwindigkeit erreicht wird, muss mit der Kompiler-Optimierung Fastest (-O3) kompiliert werden!
Die Klasse dient zur Kontrolle eines AD7606. Die Messdaten werden byte-seriell eingelesen. Bei einem STM32F103 (Bluepill) ist es möglich, Messungen mit einer minimalen Taktdauer von knapp 6 μs (≈ max. 167kSpS) durchzuführen.
Das AD7606-Board ist auf serielle Übertagung eingestellt (Lötbrücke auf Stellung SPI). Eingelesen werden die Daten über Port A (PA0..PA7). Die Steuersignale sind sämtlich an Port B angeschlossen.
Die folgenden Signale des AD7606 werden von der Klasse kontrolliert: RST (Reset), CVA (CONVST A , Konvertierung Start A, gemeinsam mit CVB), BUSY (Messung läuft), CS (Chip select), RD (Read, Taktsignal).
Alle anderen Signale, insbesondere die zur Konfiguration (z.B. Range) sind fest verdrahtet. Im Beispiel (s.u.) ist die Verschaltung wie in der nachfolgenden Tabelle. Es ist kein Problem weitere Signalsteuerungen einzubauen, um z.B. den Messbereich flexibel anzusteuern.
AD7606 | Bluepill | AD7606 | Bluepill |
---|---|---|---|
GND | GND | +5V | 5V |
OS1 | GND (LOW) | OS0 | GND (LOW) |
RAGE | GND (LOW) | OS2 | GND (LOW) |
CVB | PB6 | CVA | PB6 |
RD | PB10 (SCK) | RST | PB11 |
BUSY | PB0 | CS | PB1 (SS) |
FRST | - | VIO | 3.3V |
DB1 | PA1 | DB0 | PA0 |
... | - | ... | - |
DB7 | PA7 | DB6 | PA6 |
DB9 | - | DB8 | - |
... | ... | ||
DB15/BYTE SEL | 3.3V (HIGH) | DB14/HBEN | GND (LOW) |
Lötbrücke | Stellung SPI |
Zur Performancesteigerung ist die Klasse ist als C++-Template-Klasse angelegt. Die Steuerleitungen werden über die Typ-Parameter deklariert. Somit liegen sie in der Klasse als Konstante vor und müssen nicht aus dem RAM geladen werden.
Außerdem sind sämtliche Methoden als static inline deklariert. Dies erspart den Overhead des Aufrufs einer Member-Funktion.
Sämtliche Steuerleitungen sind an den Port B angeschlossen und werden durch direkten Zugriff auf die GPIO-Register kontrolliert. Die im STM32duino verwendete Maple-Bibliothek (libmaple) stellt hierfür die Strukturen gpioa und gpiob zur Verfügung. Sollen die Ports geändert werden, muss dies entsprechend geändert werden.
Die Enumeration AD7606Range ist so angelegt, dass die Member den Umrechnungsfaktoren LBS -> Volt entsprechen:
// Werte für die Auflösung. 5 Volt oder 10 Volt
enum class AD7606Range {
Volt5 = 152580,
Volt10 = 305175
};
AD7606DataSet nimmt die Rohwerte einer Messung mit acht Kanälen auf.
// Zur Aufnahme einer Messung mit 8 Kanälen
struct AD7606DataSet {
int16_t channel [8];
};
Die Enumeration AD7606DummyRead liefert Konstanten für den Parameter dummyRead. Die Verwendung des Datentyps bool und dann die Wertangabe true oder false ist nicht sprechend.
// Zur Festlegung, ob zu Beginn eine Blindmessung durchgeführt werden soll.
enum class AD7606DummyRead : bool {
disable = false,
enable = true
};
Bei der Definition der Klasseninstanz müssen die Steuersignale und die eingestellt Auflösung als Typparameter übergeben werden:
template<uint8_t pinRST, uint8_t pinCVA, uint8_t pinBUSY, uint8_t pinRD, uint8_t pinCS, AD7606Range range> class UrsAD7606bs
Also z.B.:
constexpr uint8_t rstPin = PB11; // (RST) Pin für den Reset
constexpr uint8_t cvaPin = PB6; // (CVA) Pin startet die Messung (mit CVB verbunden)
constexpr uint8_t busyPin = PB0; // (BUSY) Pin für BUSY-Signal
constexpr uint8_t clockPin = PB10; // (RD) Pin für die Ausgabe des Taktsignals
constexpr uint8_t ssPin = PB1; // (CS) Pin für die Ausgabe des Chip-Select-Signals (Slave-Select)
UrsAD7606bs<rstPin, cvaPin, busyPin, clockPin, ssPin, AD7606Range::Volt5> ursAD7606bs;
Methode | Funktion | Anmerkung |
---|---|---|
void begin(AD7606DummyRead dummyRead = AD7606DummyRead::enable) | Initialisiert die Schnittstelle. Der Pin-Modus und der Ruhezustand der Pins wird eingestellt. Es wird ein Reset-Signal für den AD7606 ausgelöst. dummyRead: bei enable wird eine Blindmessung durchgeführt. |
Die Pins RST, CVA,
RD und CS werden
auf den OUTPUT-Modus eingestellt,
BUSY auf INPUT. Der Ruhezustand
für RD und CS ist
HIGH, für RST und
CVA ist er LOW. Die erste Konvertierung nach einem Reset hat gelegentlich unsinnige Werte ergeben. Mit dummyRead kann eingestellt werden, dass eine Blindmessung ausgeführt wird. |
void reset( AD7606DummyRead dummyRead= AD7606DummyRead::enable) |
Sendet ein Reset-Signal an den AD7606. dummyRead: bei enable wird eine Blindmessung durchgeführt. |
Mit dummyRead kann eingestellt werden, dass eine Blindmessung ausgeführt wird. |
void start() | Startet eine Messung. | Der CVA-Pin erhält einen HIGH-Puls. |
bool isBusy() | Gibt an, ob eine lfd. Messung noch nicht abgeschlossen ist. | true, solange die Messung andauert. |
void readData(AD7606DataSet* dest) | Liest die Daten des letzten Messvorgangs aus. Die Daten werden nach dest übertragen. | |
void sample() | Startet eine Messung und wartet, bis die Messung abgeschlossen ist. | Kehrt zurück, sobald isBusy false liefert. |
sampleAndRead(AD7606DataSet* dest) | Startet eine Messung, liest währenddessen die Werte der vorhergehenden Messung aus und
wartet, bis die Messung abgeschlossen ist. Die Daten werden nach dest übertragen. |
Diese Methode erlaubt es, Messungen mit der maximalen Frequenz durchzuführen. Die Dauer liegt bei etwa 4μs, ist nicht sehr verlässlich. |
void startAndRead(AD7606DataSet* dest) | Startet eine Messung, liest währenddessen die Werte der vorhergehenden Messung aus. Kehrt
zurück, sobald die Daten eingelesen wurden. Die Daten werden nach dest übertragen. |
Bei der Rückkehr ist die lfd. Messung noch nicht abgeschlossen. Deshalb beim Aufruf zunächst gewartet bis eine evtl. laufende Messung abgeschlossen ist. |
float value(int16_t rawValue) | Rechnet einen eingelesenen Rohwert in einen Spannungswert um. | Der Umrechnungsfaktor ist ein Typparameter der Klasse. |
Im Beispiel wird eine einfache Ansteuerung des AD7606 über die serielle Schnittstelle gezeigt. Nach einem Startkommando wird eine einstellbare Anzahl von Messungen in einer einstellbaren Zykluszeit durchgeführt und zwischengespeichert. Ist die eingestellte Anzahl an Messungen erreicht, wird der Messvorgang gestoppt und die ermittelten Werte ausgegeben.
Das Programm versteht drei Kommandos ("\n" = Zeilenvorschub, LF, ASCII 0x0A):
Das Einlesen der Kommandos geschieht mit der Methode Serial.readStringUntil(). Diese Methode hat ein relativ kurzes Timeout, so dass die einzelnen Zeichen des Kommandos schnell hintereinander versendet werden müssen. Ggf. kann das Timeout über Serial.setTimeout(milliSeconds) angepasst werden.
Zum Anschluss des AD7606 an den Bluepill: siehe oben, Abschnitt Voraussetzung.
Das ist die Ausgabe des Programms. Die Kanäle 2 und 8 sind an 3,3V des Bluepill-Boards angeschlossen, die anderen sind kurzgeschlossen.
Um das Beispiel übersichtlicher zu gestalten, wurden die Funktionalitäten in zwei Klassen ausgelagert:
Die Klasse SerialControl kapselt die serielle Schnittstelle Serial. Sie wertet den eigehenden Datenstrom bzgl. der Kommandos aus und ruft bei erkannten Kommandos eine entsprechende Callback-Funktion auf.
Die Klasse AD7606Control kontrolliert den Messvorgang. Sie stellt die Callback-Funktionen zur Kommandoausführung bereit. Zu Ansteuerung des AD7606 wird die Bibliotheksklasse UrsAD7606bp (s.o.) genutzt.
In dem Hauptprogramm STM32F1-AD7606-bytepar.ino werden dann nur diese beiden Klassen in setup initialisiert und in loop angesteuert.
Die Klasse wird mit den Zeigern auf die Callback-Funktionen initialisiert.
// Initialisiert die Klasseninstanz
// timerControl: Callback für Kommando 't'
// countControl: Callback für Kommando 'c'
// doSample: Callback für Kommando 's'
SerialControl(Callback doSample, UintCallback timerControl, UintCallback countControl)
Die Methode begin wird in setup aufgerufen und initialisiert die serielle Schnittstelle. handle, immer wieder in loop aufgerufen, analysiert den Input-Stream und ruft bei erkannten Kommandos die zugehörigen Callback-Funktionen auf.
Die Messreihenaufnahme wird durch einen Timer-Interrupt gesteuert. Die zugehörige ISR muss als statische Funktion angelegt werden. Um das Beispiel einfach zu halten, wurden auch die anderen Member der Klasse als statische Member angelegt.
Die Methode begin wird in setup aufgerufen und initialisiert die Schnittstelle. Die Methoden setTime(uint microSeconds), setCount(uint sampleCount) und startSampling() sind die Methoden zur Kommando-Ausführung (Callback in SerialControl). Über hasFinished() kann abgerufen werden, ob eine Messreihe beendet wurde, also gültige Daten vorliegen. Das zugehörige interne Flag wird beim Aufruf der Funktion zurück gesetzt, so dass folgende Aufrufe den Wert false ergeben. printReport() gibt die Messreihe aus.
Zum Timing wird Timer1 (HardwareTimer(1)) benutzt. Die komplette Messreihenaufnahme erfolgt innerhalb der Timer-ISR.
// Messreihe per Timer-Interrupt aufnehmen.
void timerInterruptHandler() {
if (currentSampleCount >= sampleCount - 1) { // Alle Messungen wurden gestartet
sampleTimer.c_dev()->regs.gen->CR1 &= ~TIMER_CR1_CEN; //Timer abschalten [sampleTimer.pause();]
while (ursAD7606bs.isBusy()); // Warten, bis letze Messung beendet
ursAD7606bs.readData(&samples[currentSampleCount]); // letzten Wert auslesen
samplingDone = true; // Messreihe komplett
return;
}
// Weitere Messungen notwendig
ursAD7606bs.startAndRead(&samples[currentSampleCount++]);
}
Wegen der Auslagerung der einzelnen Aufgaben in separate Klassen gestaltet sich das Hauptprogramm recht einfach:
AD7606Control ad7606; // Ermöglicht "ad7606." anstatt "AD7606Interface::"
// Serielle Schnittstelle definieren
// Alle Aktionen mit der seriellen Schnittstelle laufen über dieses Objekt
SerialControl serialControl(ad7606.startSampling, ad7606.setTime, ad7606.setCount);
void setup() {
serialControl.begin();
delay(500); // Der Treiber der USB-USART muss sich nach dem Programmieren wieder bei Windows anmelden
serialControl.println("\n---------------------------------");
serialControl.print(APP_NAME); serialControl.print(" Verion "); serialControl.println(VERSIONSTRING);
ad7606.begin(&serialControl); // Schnittstelle initialisieren
serialControl.println("loop running");
}
void loop() {
if (ad7606.hasFinished()) { // Messreihe komplett
ad7606.printReport();
}
serialControl.handle();
}
Das ZIP-Archiv für Bibliothek UrsSTMF1-AD7606 zum Download. Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).
Das Archiv enthält die Bibliotheksdateien