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).

In­halts­ver­zeich­nis

Angepasstes SPI Interface (Klasse UrsAD7606SpiDevice)

Klasse UrsAD7606Spi

Voraussetzung

Verwendung

Methodenübersicht

Polling-Beispiel (ESP32-AD7606-SPI)

Übersicht

Klasse SerialControl

Klasse AD7606Control

Hauptprogramm

Interrupt-Beispiel (ESP32-AD7606-SPI-Interrupt)

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.

Angepasstes SPI Interface (Klasse UrsAD7606SpiDevice)

Die Klasse SPIClass aus der Arduino-Bibliothek ist ohne Modifikationen nicht geeignet. Die Klasse UrsAD7606SpiDevice ist ein Wrapper um SPIClass, der für diesen Zweck passendere Methoden bereit stellt. Nicht vorgesehen ist der Betrieb mehrerer Devices am gleichen SPI-Bus. Hierzu wären weitere Anpassungen notwendig.

Die Methoden begin() und end() fassen alle Aufgaben zusammen, die zur Initialisierung und zum Zurücksetzen der Schnittstelle notwendig sind. Um später die Übertragung zu starten, ist nur noch eine Anweisung notwendig. Hierdurch wird ein hoher Durchsatz erreicht, man verliert aber gleichzeitig an Flexibilität. Im Einzelnen:

Die Methode read16Bytes() liest genau 16 Bytes in eine Puffer-Variable ein. Dabei wird berücksichtigt, dass die Byte-Reihenfolge für eine 16-Bit-Übertragung nicht korrekt ist. Die Bytes werden passend umgestellt.

Die Methoden enableReadDoneInterrupt, disableReadDoneInterrupt, startTransfer und copy16Bytes sind für eine interrupt-getriebene Steuerung versehen. Sie werden im zweiten Beispiel (ESP32-AD7606-SPI-Interrupt) benutzt.

Methode Funktion Anmerkung
void begin(
AD7606SpiFrequency frequency,
AD7606SpiBus busSPI)
Initialisiert die Schnittstelle.
Der Pin-Modus und der Ruhezustand der SPI-Pins wird eingestellt.
Die Voreinstellung für frequency ist 26MHz und für busSPI ist HSPI. MOSI wird zwar nicht benutzt aber von der Schnittstelle allokiert.
void end() Setzt die SPI-Schnittstelle zurück. Alle betroffenen Pins werden in den INPUT-Modus versetzt. Auch MOSI.
void read16Bytes(int16_t* out) Leist 8 Zahlen int16_t-Format ein.  
 void enableReadDoneInterrupt(
void (*userHandler)(void))
Aktiviert den TransferDone-Interrupt für die SPI-Schnittstelle. Übergeben werden muss ein Pointer auf die Handler-Funktion. Die Handler-Funktion sollte dem IRAM-Attribut versehen werden, da sie innerhalb einer ISR ausgeführt wird.
void disableReadDoneInterrupt() Deaktiviert den Interrupt wieder.   
void startTransfer()  Startet die Datenübertragung der SPI-Schnittstelle.  Die Methode ist dem IRAM-Attribut versehen, kann also ohne Probleme in einer ISR aufgerufen werden. 
void copy16Bytes(uint8_t* out)  Kopiert die von der SPI eingelesenen Daten aus dem SPI-Puffer-Register in das angegebene Datenfeld. .  Die Methode ist dem IRAM-Attribut versehen, kann also ohne Probleme in einer ISR aufgerufen werden.  

Klasse UrsAD7606Spi

Die Klasse dient zur Kontrolle eines AD7606 durch eine ESP32. Die Messdaten werden seriell über die SPI-Schnittstelle eingelesen.

Voraussetzung

Das AD7606-Board ist auf serielle Übertagung eingestellt (Lötbrücke auf Stellung SPI).

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 ESP32 AD7606 ESP32
GND GND +5V 5V
OS1 GND (LOW) OS0 GND (LOW)
RAGE GND (LOW) OS2 GND (LOW)
CVB GPIO5 CVA GPIO5
RD GPIO14 RST GPIO25
BUSY GPIO23 CS GPIO15
FRST - VIO 3.3V
DB1 - DB0 -
... - ... -
DB7/DoutA GPIO12 DB6 -
...   ...  
DB15/BYTE SEL GND (LOW) 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, //nanoVolt
   Volt10 = 305175 //nanoVolt
};

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, AD7606Range range> class UrsAD7606Spi

Also z.B.:

constexpr uint8_t rstPin = 25; // (RST) Pin für den Reset
constexpr uint8_t cvaPin = 22; // (CVA) Pin startet die Messung (mit CVB verbunden)
constexpr uint8_t busyPin = 23; // (BUSY) Pin für BUSY-Signal
// SS, Clock & MISO werden über die SPI-Schnittstelle definiert
// HSPI: SS=15 (CS), SLCK=14 (RD), MOSI=unbenutzt, MISO=12 (DoutA)
constexpr AD7606SpiBus spiBus = AD7606SpiBus::busHSPI;
constexpr AD7606SpiFrequency spiFrequency = AD7606SpiFrequency::MHz26;

UrsAD7606Spi<rstPin, cvaPin, busyPin, AD7606Range::Volt5> ursAD7606Spi;
...
ursAD7606Spi.begin(spiFrequency, spiBus);

Methoden

Methode Funktion Anmerkung
void begin(
  AD7606SpiFrequency frequency = AD7606SpiFrequency::MHz26,
  AD7606SpiBus AD7606SpiBus = AD7606SpiBus::busHSPI,
  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: 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 (MISO) und CS (SS) ist HIGH, für RST und CVA ist er LOWMOSI wird nicht benutzt aber von der SPI-Schnittstelle allokiert.

Die erste Konvertierung nach einem Reset hat gelegentlich unsinnige Werte ergeben. Mit dummyRead kann eingestellt werden, dass eine Blindmessung ausgeführt wird.
void end()  Gibt die Pins und Schniitstelleewieder frei.wieder frei. Stellt für alle betroffenen Pins (auch MOSI!) den INPUT-Modus ein.
void reset() Sendet ein Reset-Signal an den AD7606.  
void start() Startet eine Messung. Der CVA-Pin erhält einen HIGH-Puls.
Die Methode ist dem IRAM-Attribut versehen, kann also ohne Probleme in einer ISR aufgerufen werden.
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.
 void enableReadDoneInterrupt(
void (*userHandler)(void))
Aktiviert den TransferDone-Interrupt für die SPI-Schnittstelle. Übergeben werden muss ein Pointer auf die Handler-Funktion. Die Handler-Funktion sollte dem IRAM-Attribut versehen werden, da sie innerhalb einer ISR ausgeführt wird.
void disableReadDoneInterrupt() Deaktiviert den Interrupt wieder.  
void startSpiTransfer() Startet die Datenübertragung der SPI-Schnittstelle. Die Methode ist dem IRAM-Attribut versehen, kann also ohne Probleme in einer ISR aufgerufen werden.
void copyData(AD7606DataSet* dest) Kopiert die von der SPI eingelesenen Daten aus dem SPI-Puffer-Register in das angegebene Datenfeld. Die Methode ist dem IRAM-Attribut versehen, kann also ohne Probleme in einer ISR aufgerufen werden.

Polling-Beispiel (ESP32-AD7606-SPI)

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 UrsAD7606Spi (s.o.) genutzt.

In dem Hauptprogramm ESP32-AD7606-SPI.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'
//    doReset:      Callback für Kommand0 'r'
//    doSample:     Callback für Kommando 's'
SerialControl(Callback doSample, UintCallback timerControl, UintCallback countControl, , Callback doReset)

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.

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 Starten der einzelnen Messungen wird Timer1 (HardwareTimer(1)) benutzt. Zur Synchronisation mit der Auslese-Routine wird das Flag timerTick gesetzt.

// Messung per Timer-Interrupt starten.
void IRAM_ATTR timerInterruptHandler() {
   // Nächste Messung starten
   ursAD7606Spi.start();
   timerAlarmWrite(sampleTimer, interruptAt * 2, true);

   // Der AD7606 schaltet BUSY schon HIGH, bevor start() zurück kommt (langsames GPIO!)
   timerTick = true;

   if (++currentSampleCount == sampleCount) { // Alle Messungen wurden gestartet.
      timerStop(sampleTimer);
      timerAlarmDisable(sampleTimer);
      return;
   }
}

Das Auslesen der Daten geschieht per Polling:

void IRAM_ATTR AD7606Control::startSampling() {
   if (!beSilent) {
      outDev->println("Start sampling");
      Serial.flush(); //Übertragung abschließen.
   }

   currentDestCount = 0; // Ziel für das erste Datenpaket hat den Index 0.
   samplingDone = false; // Messreihe läuft.

   // Timer initialisieren
   sampleTimer = timerBegin(0, 40, true);
   timerAttachInterrupt(sampleTimer, timerInterruptHandler, false); // true für Parameter 'edge' gibt eine Fehlermeldung

   // Timer zurück setzen, Alarm freigeben
   timerAlarmWrite(sampleTimer, (interruptAt - 2) * 2, true); // Der erste Interrupt trifft immer zu spät ein.
   timerRestart(sampleTimer);
   timerAlarmEnable(sampleTimer);
   ursAD7606Spi.start();   // Erste Messung starten.
   currentSampleCount = 1; // Eine Messung gestartet.
   while (ursAD7606Spi.isBusy()); // Warten, bis der Messvorgang beendet ist.
   ursAD7606Spi.readData(&samples[currentDestCount++]); // ersten Wert auslesen.

   // Weitere Werte auslesen.
   while (currentDestCount < sampleCount) {
      while (!timerTick); // Warten, bis die nächste Messung über den Timer gestartet wurde.
      while (ursAD7606Spi.isBusy());
      timerTick = false;
      ursAD7606Spi.readData(&samples[currentDestCount++]); // Wert auslesen.
   }
   if (!beSilent)
      samplingDone = true;
}

Hauptprogramm

Wegen der Auslagerung der einzelnen Aufgaben in separate Klassen gestaltet sich das Hauptprogramm recht einfach:

SerialControl serialControl(ad7606.startSampling, ad7606.setTime, ad7606.setCount, ad7606.reset);

AD7606DataSet data;

void setup() {
   Serial.begin(115200);
   Serial.println("\n---------------------------------");
   Serial.print(APP_NAME); Serial.print(" Version "); Serial.println(VERSIONSTRING);
   Serial.print('\a');

   while (Serial.available()) {
      Serial.read();
   }

   ad7606.begin(&serialControl); // Schnittstelle initialisieren

   serialControl.println("loop running");
}

void loop() {
   if (ad7606.hasFinished()) { // Messreihe komplett
      ad7606.printReport();
   }

   serialControl.handle(); // Input über Serial verarbeiten
}

Interrupt-Beispiel (ESP32-AD7606-SPI-Interrupt)

Im Unterscheid zum vorhergehenden Beispiel werden Interrupts genutzt, um die verschiedenen Phasen des Programms zu kontrollieren:

Ein Timer (HardwareTimer(1)) startet regelmäßig die Messung über UrsAD7606Spi::start() (s.o.). Für die fallende Flanke des BUSY-Signals wird ein Pin-Change-Interrupt eingerichtet, der den Start des SPI-Datentransfers auslöst.

void IRAM_ATTR busyChangeInterruptHandler() {
   ursAD7606Spi.startSpiTransfer();
}

// ...

attachInterrupt(busyPin, busyChangeInterruptHandler, FALLING);

Das Ende des SPI-Datentransfers, d.h. wenn 16 Bytes eingelesen wurden, wird über einen Interrupt erkannt, den das SPI-Interface liefert.

void IRAM_ATTR spiReadDoneInterruptHandler() {
   if (currentDestCount == sampleCount - 1) { // Alle Messungen wurden eingelesen.
      ursAD7606Spi.copyData(&samples[currentDestCount++]); // letzten Wert auslesen

      if (!beSilent)
         samplingDone = true; // Messreihe komplett.
      return;
   }

   // Weitere Messungen stehen aus.
   ursAD7606Spi.copyData(&samples[currentDestCount++]); // Wert auslesen
}

// ...

 ursAD7606Spi.enableReadDoneInterrupt(spiReadDoneInterruptHandler);

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