Motivation

Für ein Projekt sollen Messungen mit dem AD7606 (ADC mit acht Kanälen und max. 200 kSpS) mit möglichst hoher Taktfrequenz durchgeführt werden. Die folgende Klasse für den STM32F103 (Bluepill) schafft per SPI eine Sample-Rate von 125 kSpS. Es werden nur drei Anschlussleitungen benötigt.

Klasse UrsAD7606Spi

Voraussetzung

Verwendung

Methodenübersicht

Beispiel (STM32F1-AD7606-SPI)

Übersicht

Klasse SerialControl

Klasse AD7606Control

Prozess

Interrupts

Verwendung

Hauptprogramm

Download


Versionshistorie

Version Anpassungen
1.0 (2021-11-25) Basis-Version

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!

Klasse UrsAD7606Spi

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 8 μs (≈ max. 125 kSpS) durchzuführen.

Voraussetzung

Das AD7606-Board ist auf serielle Übertagung eingestellt (Lötbrücke auf Stellung SPI). Eingelesen werden die Daten über Port A (PA6, MISO1, DOUTA beim AD7606).

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 PA3 CVA PA3
RD PA5 (SCK1) RST PA2
BUSY PA1 CS PA4 (NSS1)
FRST - VIO 3.3V
DB1 - DB0 -
... - ... -
DB7 (DOUTA) PA6 (MISO1) DB6 -
DB9 - DB8 (DOUTB) -
...   ...  
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 (UrsSTM32F1-AD7606.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];
};

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

Die Enumeration AD7606DmaTci liefert Konstanten für den Parameter enableRxTCI. Die Verwendung des Datentyps bool und dann die Wertangabe true oder false ist nicht sprechend.

// Zur Festlegung, ob der DMA-Transfer-Complete-Interrupt ausgelöst werden soll.
enum class AD7606DmaTci : bool {
   disable = false,
   enable = true
};

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

Also z.B.:

constexpr uint8_t rstPin = PA2; // (RST) Pin für den Reset
constexpr uint8_t cvaPin = PA3; // (CVA) Pin startet die Messung (mit CVB verbunden)
constexpr uint8_t busyPin = PA1; // (BUSY) Pin für BUSY-Signal. 
constexpr uint8_t csPin = BOARD_SPI1_NSS_PIN; // = PA4 (CS) Pin für CS-Signal.

UrsAD7606Spi<rstPin, cvaPin, busyPin, csPin, AD7606Range::Volt5> ursAD7606Spi;

Methoden

Methode Funktion Anmerkung
void begin(
  AD7606DmaTci enableRxTCI = AD7606DmaTci::disable,
  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 end(bool freePins = true) Gibt das SPI- und DMA-Interface frei.
freePins: RST, CVA, RD (SLCK), DOUTA (MISO) und CS werden in den INPUT-Modus versetzt.
 
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() ok Startet eine Messung. Der CVA-Pin erhält einen HIGH-Puls.
bool isBusy() ok Gibt an, ob eine lfd. Messung noch nicht abgeschlossen ist. true, solange die Messung andauert.
void readData(AD7606DataSet* dest) ok Liest die Daten des letzten Messvorgangs aus. Die Daten werden nach dest übertragen.  
void sample() ok 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.
float value(int16_t rawValue) Rechnet einen eingelesenen Rohwert in einen Spannungswert um. Der Umrechnungsfaktor ist ein Typ-Parameter der Klasse.
void beginTransfer(AD7606DataSet* dest) Bereitet SPI und DMA für den nächsten Zyklus vor.
Legt fest, dass die Daten nach dest übertragen werden.
Konkrete Verwendung: siehe Beispiele unten.
void startReading() Startet die SPI-DMA-Datenübertagung. Konkrete Verwendung: siehe Beispiele unten.
void endTransfer() Übertragung abschließen, Register zurückstellen. Damit SPI und DMA funktionieren müssen beide nach der Übertragung zurück gesetzt werden und vor jeder neuen Übertagung wieder initialisiert werden.
Konkrete Verwendung: siehe Beispiele unten.

Beispiel (STM32F1-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 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.

Ü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'
// doReset: Callback für Kommando 'r'
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

Um die höchstmögliche Messrate zu erreichen, müssten die Daten der vorhergehenden Wandlung während der aktuell laufenden Wandlung eingelesen werden. Leider dauert das Einlesen wegen der nötigen Rüst- und Abrüst-Zeiten für SPI und DMA zu lange. Durch teilweise Überschneidung von Wandlung und Auslesen lässt sich dennoch eine hohe Sample-Rate erzielen.

Prozess

Die Daten der letzten Messung können so lange eingelesen werden, so lange BUSY für die aktuelle Messung auf HIGH (= Messung läuft) liegt. Sobald BUSY wieder auf LOW geht, werden die Datenregister im AD7606 mit den neuen Werten überschrieben. Konkret heißt das, man kann mit dem Auslesen der Messdaten starten sobald BUSY auf LOW geht (= neue Messdaten liegen vor), muss damit aber fertig sein, bevor BUSY das nächste Mal auf LOW geht.

Im Beispiel wird dieser Prozess über Interrupts kontrolliert:

Die folgende Grafik zeigt deutlich die Überlappung von Wandlungs- und Auslese-Phase. Die neue Messung wird bereits gestartet (gelbe Pfeile), während die Daten der vorhergehenden Messung noch ausgelesen werden.

Theoretisch wäre so eine Zykluszeit von 7μs möglich, jedoch lässt die Arduino-Umgebung das leider nicht zu. Diese fordert zwischendurch Rechenzeit an (z.B. zur Fortschreibung von millis). Bei kürzeren Taktzeiten als 8μs steht nicht mehr genügend freie Rechenzeit zur Verfügung und der Auslesevorgang wird gestört. Das folgende Bild zeigt die Folgen einer solchen Störung bei einer Taktzeit von 7μs.

Im markierten Bereich findet solch eine Störung statt. Das CS-Signal wird verspätet gesetzt und ist außerdem verlängert. Das führt dazu, dass das Auslesen einer Messung ausgelassen wird (gelb: der tatsächlich Signalverlauf, orange: erwarteter Verlauf).

Interrupts

Die Arduino-Implementierungen der Interrupts besitzen aus Kompatibilitätsgründen einen großen Overhead. Im Beispielprogramm werden diese Mechanismen übergangen, um eine performantere Ausführung zu erreichen. Z.B der Timer-Update-Interrupt ist in der Datei ...cores\maple\libmaple\timer.c definiert als

__weak void __irq_tim1_up(void) {...}

Die Deklaration als weak erlaubt es, diese Funktion an anderer Stelle mit gleichem Namen zu überschreiben ( = strong). Der Linker bindet dann die Überschreibung ein. Im Beispiel werden die entsprechenden ISRs überschrieben (Datei AD7606Control.cpp):

Zu beachten:

Zur Aktivierung der Interrupts werden die Arduino-Standard-Mechanismen benutzt. Das ist eine einfache und sichere Methode und vermeidet inkompatible Register-Zugriffe. Damit klar ist, dass dies ohne die Zuordnung der ISRs geschieht, wird auf eine Dummy-Funktion verwiesen (Datei AD7606Control.cpp). Außerdem werden die betroffenen Interrupts auf höchste Priorität gesetzt:

// Das freigeben der Interrupts über attach.. erfordert den Verweis auf einen Handler != NULL.
void DummyHandler() {}


void AD7606Control::begin(SerialControl* sc) {
   ...

   sampleTimer.attachInterrupt(0, DummyHandler); // Soll nur den Interrupt freigeben
   attachInterrupt(busyPin, DummyHandler, FALLING); // Soll nur den Interrupt freigeben
   dma_attach_interrupt(DMA1, DMA_CH2, DummyHandler); // Soll nur den Interrupt freigeben

   // Interrupts mit hoher Priorität versehen
   nvic_irq_set_priority(NVIC_TIMER1_UP_TIMER10, 0);
   nvic_irq_set_priority(NVIC_EXTI1, 0);
   nvic_irq_set_priority(NVIC_DMA_CH3, 0);
}

Verwendung

Die Messreihenaufnahme wird durch einen Timer-Interrupt gesteuert. Die zugehörige ISR und auch anderen ISRs 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. In der ISR wird die nächste Wandlung gestartet. Es wird mitgezählt, wie viele Messungen gestartet wurden. Wenn die gewünschte Anzahl erreicht ist, wird der Prozess abgebrochen, d.h. der Timer deaktiviert:.

// ISR für Timer1-Upload-Interrupt
void __irq_tim1_up(void) {
   sampleTimer.c_dev()->regs.adv->SR = ~TIMER_SR_UIF; // Timer-Interrupt-Flag löschen

   if (currentSampleCount++ == sampleCount) { // Alle Messungen wurden gestartet.
      sampleTimer.c_dev()->regs.adv->CR1 &= ~TIMER_CR1_CEN;  //Timer abschalten
      return;
   }

   ursAD7606Spi.start(); // neue Messung starten.
}

In der Pin-Change-ISR wird das Auslesen der Messdaten gestartet.

// ISR für PA1-Interrupt (BUSY). Gleichzeitig PB1, PC1, ... Diese sind aber nicht aktiviert.
void __irq_exti1(void) {
   EXTI_BASE->PR = (1U << EXTI1); // Interrupt-Flag löschen.
   ursAD7606Spi.startReading();   // Auslesen starten.
}

Wenn alle Daten eingelesen wurden, wird der DMA-Transfer-Complete-Interrupt ausgelöst. In der ISR wird die Transaktion beendet und, um Zeit zu sparen, gleich eine neue gestartet, wenn weitere Messungen ausstehen:

// ISR für DMA1-Channel2-Transfer-Complete-Interrupt. Gleichzeitig auch Transfer-Error-Interrupt, ... Diese sind aber nicht aktiviert.
 void __irq_dma1_channel2(void) {
    ursAD7606Spi.endTransfer();

    if (currentDestCount == sampleCount-1) { // Alle Messungen wurden eingelesen.
       samplingDone = true; // Messreihe komplett.
       return;
    }

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

Der Start einer neuen Messreihe muss alle Module starten und die erste Messung beim AD7606 auslösen:

void AD7606Control::startSampling() {
   outDev->println("Start sampling");
   Serial.flush(); // Seielle Übertragung abschließen.
   currentDestCount = 0; // Neue Messreihe.

   sampleTimer.c_dev()->regs.adv->CNT = 58; // Zeit für start.
   sampleTimer.c_dev()->regs.gen->CR1 |= TIMER_CR1_CEN;  //Timer anschalten.

   ursAD7606Spi.start();
   ursAD7606Spi.beginTransfer(&samples[currentDestCount]); // Ziel für erste Messung.
   currentSampleCount = 1; // Erste Messung gestartet.
}

Der Counter des Timers wird nicht mit 0 vorbelegt. Zwischen Start des Timers und Beginn der Messung vergeht einige Zeit. Der Wert von 58 muss ggf. individuell angepasst werden.

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