Motivation

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 ESP32 und das Beispiel gleicht im Wesentlich dem für den STM32F1 (Bluepill).

Klasse UrsAD7606bp

Voraussetzung

Verwendung

Methodenübersicht

Beispiel (ESP32-AD7606-bytepar)

Übersicht

Klasse SerialControl

Klasse AD7606Control

Hauptprogramm

Download


Versionshistorie

Version Anpassungen
1.0 (2021-11-15) Basis-Version
1.1 (2022-05-11) - Korrekturen bei der Timer-Ansteuerung
- AD7606 Signal CVA/CVB umgelegt von GPIO22 auf GPIO 5 (GPIO22 ist SCL-Signal der I²C-Schnittstelle)

Zur Beschreibung des AD7606 und des Entwicklungsboards siehe URS AD7606.

Klasse UrsAD7606bp

Die Klasse dient zur Kontrolle eines AD7606 durch eine ESP32. Die Messdaten werden byte-seriell eingelesen. Der ESP32 ist schnell genug, die acht Kanäle einer vorhergehenden Messung auszulesen, während eine aktuelle Wandlung läuft (für Details siehe Steuerung des Messvorgangs).

Im Beispiel werden die Wandlungen durch einen Timer ausgelöst. Bei einer Taktrate von 200kHz (entsprechend einer Taktzeit von 5μs) sind die zeitlichen Abweichungen recht groß. Ich habe Hinweise darauf gefunden, dass dies wohl am zugrunde liegenden Betriebssystem FreeRTOS liegt.

Zeitstempel Sollwert Delta Pulsd. Delta Alle Angaben in μs.
0,01 0,01
6,74 5,01 -1,73 6,73 1,73
11,23 10,01 -1,22 4,49 -0,51
15,72 15,01 -0,71 4,49 -0,51
20,21 20,01 -0,20 4,49 -0,51
26,21 25,01 -1,20 6,00 1,00
30,69 30,01 -0,68 4,48 -0,52
35,18 35,01 -0,17 4,49 -0,51
41,18 40,01 -1,17 6,00 1,00
45,67 45,01 -0,66 4,49 -0,51

Ab einer Taktzeit von 6μs (entsprechend 167kSpS) funktioniert das Timing einwandfrei. Hier ein Beispiel für eine Taktzeit von 10μs:

Der erste Takt hat eine um etwa 2μs verlängerte Dauer. Dies kann man ausgleichen, in dem man den zum Timer gehörenden Counter entsprechend vorbelegt.

 

Voraussetzung

Das AD7606-Board ist auf serielle Übertagung eingestellt (Lötbrücke auf Stellung SPI). Damit nicht jedes Datenbit einzeln aus dem GPIO-Register entnommen und wieder zu einem Messwert zusammengeführt werden muss, müssen die Datenleitungen an acht aufeinanderfolgende GPIOs angeschlossen werden. Beim ESP32 WROOM-32D sind die meisten Pins heraus geführt. Aufeinanderfolgend gibt es nur die Pins GPIO12 (DB0) bis GPIO19 (DB7).

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 GPIO5 CVA GPIO5
RD GPIO26 RST GPIO25
BUSY GPIO23 CS GPIO21
FRST - VIO 3.3V
DB1 GPIO13 DB0 GPIO12
... - ... -
DB7 GPIO19 DB6 GPIO18
DB9 - DB8 -
...   ...  
DB15/BYTE SEL 3.3V (HIGH) DB14/HBEN GND (LOW)
Lötbrücke Stellung SPI

Verwendung

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.

Methodenübersicht

Hilfsklassen (UrsAD7606.h)

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];
};

Definition der Klasseninstanz

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;

Methoden

Methode Funktion Anmerkung
void begin(bool dummyRead = true) 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: Es wir 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() Sendet ein Reset-Signal an den AD7606.  
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.
readAndSample(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.

Beispiel (ESP32-AD7606-bytepar)

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 ESP32: siehe oben, Abschnitt Voraussetzung.

Das ist die Ausgabe des Programms. Die Kanäle 2 und 8 sind an 3,3V des ESP32-Boards angeschlossen, die anderen sind kurzgeschlossen.

Übersicht

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.

Klasse SerialControl

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 Inputstream und ruft bei erkannten Kommandos die zugehörigen Callback-Funktionen auf.

Klasse AD7606Control

Die Messreihenaufnahme wird durch einen Timer-Interrupt gesteuert. Die zugehörige ISR muss als statische Funktion angelegt werden. Um das Besipiel 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 IRAM_ATTR timerInterruptHandler() {
   if (currentSampleCount >= sampleCount - 1) { // Alle Messungen wurden gestartet
      sampleTimer->dev->config.alarm_en = 0; // = timerAlarmDisable(), kopiert wegen IRAM_ATTR

      while (ursAD7606bs.isBusy()); // Warten, bis letze Messung beendet
      ursAD7606bs.readData(&samples[currentSampleCount]); // letzten Wert auslesen
      samplingDone = true; // Messreihe komplett
      isRunning = false;
      return;
   }

   // Weitere Messungen notwendig
   ursAD7606bs.startAndRead(&samples[currentSampleCount++]);
}

Hauptprogramm

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();
}

Download

Das ZIP-Archiv für Bibliothek UrsESP32-AD7606 zum Download. Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).

Das Archiv enthält die Bibliotheksdateien