Go Down

Topic: [Projekt] ADC in OOP (Read 1 time) previous topic - next topic

combie

Aug 22, 2017, 06:40 pm Last Edit: Sep 15, 2017, 11:47 pm by combie
Hi!

In letzter Zeit häufen sich hier die Fragen zu dem AnalogDigitalConverter.
Auch reichen für viele Zwecke die Arduino Funktionen nicht aus.

Darum habe ich mal einen Teil meines ADC Geschreibsels zu einer Klasse zusammen gefasst.
Der Code ist für den ATMega328P geschrieben.
Sollte unverändert auch auf dem ATMega168P laufen.
Also für alle ProMini, Nano und UNO Boards.


Oft findet sich in meinem Code ein unübersichtliches per Hand Register setzen. Wenn man nicht gerade das Datenblatt vor Augen hat, ist das Gedöns kaum lesbar.

Dabei bietet C++ doch so schöne und viele Möglichkeiten das zu abstrahieren.


Was kann es besser, als das Original?
1. Den Vorteiler für den ADC Takt manipulieren
2. Reduzierung auf 8 Bit Wandlung
3. Den internen Temperatur Sensor auslesen
4. Die Band Gab Referenz lesen
5. Etwas schneller, unnötiges Register setzen wird vermieden

Was fehlt noch?
1. Wandeln im Hintergrund (free running mode)
2. Triggern durch Timer
3. Eine Abstraktion des gleitenden Mittelwertes.


Im Anhang finden sich zwei Dateien, eine Adc.h und eine Adc.cpp, in welcher sich die ganze Magie verbirgt.
Man nehme die beiden Dateien, und werfe sie in den gleichen Order, wo auch die *.ino Datei liegt.
Zu späterer Zeit wird dann auch daraus eine Lib zusammengefügt werden.


Bereit gestellt werden zwei Klassen
1. Adc, welche die Hauptarbeit erledigt
2. AdcDebug, welche zusätzlich die aktuelle Registerbelegung zeigt

Jetzt erstmal ein paar Beispiele, wie man den internen Temperatursensor ausließt. Es wird der Integerwert ausgegeben, keine berechnete Temperatur in Celsius. Das soll in einem weiteren Beitrag folgen.

Diese Beispiele machen alle das gleiche, zeigen nur die unterschiedlichen Möglichkeiten, wie man die Adc Klasse nutzen kann.

Da möge sich jeder die Variante aussuchen, die ihm mehr liegt.

Zu Anfang die flache Variante:
Code: [Select]

#include "Adc.h"


Adc adc;

void setup()
{
   Serial.begin(9600);
   adc.enable();
   adc.setClockDivisor(Adc::DIV_128);
   adc.setReference(Adc::REF_11);
   adc.setResolution(Adc::RES_10BIT);
   adc.setSource(Adc::MUX_THERMO);
}

void loop()
{
  int t = adc.getValue();
  Serial.print("Value: ");
  Serial.println(t);
  Serial.println("--------------");
  delay(1000);
}



Methodenverkettung und kürzerer Zugriff:
Code: [Select]

#include "Adc.h"


Adc adc;

void setup()
{
   Serial.begin(9600);
   adc.enable().setClockDivisor(Adc::DIV_128).setReference(Adc::REF_11).setResolution(Adc::RES_10BIT).setSource(Adc::MUX_THERMO);
}

void loop()
{
  int t = adc();
  Serial.print("Value: ");
  Serial.println(t);
  Serial.println("--------------");
  delay(1000);
}


So weit zum grundsätzlichen....
----------------------------

In Setup wird die Grundeinstellung gemacht.
Details kann man beim konkreten Zugriff noch festlegen.

Hier jetzt die Aufgabe, Pin A0 unter Zuhilfenahme der 1,1V Referenz lesen und A1 mit der Vcc Referenz
Code: [Select]
#include "Adc.h"


Adc adc;


void setup()
{
   Serial.begin(9600);
   adc  .enable()
        .setClockDivisor(Adc::DIV_128)
        .setResolution(Adc::RES_10BIT);
}

void loop()
{
  Serial.print("A0 Value: ");
  Serial.println(adc.setReference(Adc::REF_11)(Adc::MUX_ADC0));
  Serial.print("A1 Value: ");
  Serial.println(adc.setReference(Adc::REF_VCC)(Adc::MUX_ADC1));
  Serial.println("--------------");
  delay(1000);
}


Es ist völlig gleichwertig, ob man dieses schreibt:
Code: [Select]

 Serial.println(adc.setReference(Adc::REF_11)(Adc::MUX_ADC0));


Oder es lieber so hat:
Code: [Select]

adc.setReference(Adc::REF_11);
adc.setSource(Adc::MUX_ADC0);
Serial.println(adc());


Auch die Arrayschreibweise ist genehm:
Code: [Select]

adc.setReference(Adc::REF_11);
Serial.println(adc[Adc::MUX_ADC0]);


Über Kommentare und Verbesserungsvorschläge würde ich mich sehr freuen.





// edit: 08.09.2017
Hier jetzt die neueste Version, als Arduino Lib
ATMega328P wird unterstützt
ATMega168P wird unterstützt
ATMega168 wird unterstützt
ATMega88 wird unterstützt
Fehlerbereinigung


// edit: 15.09.2017
Default Prescaler Berechnung hinzugefügt
ATMega32U4 wird unterstützt
ATMega16U4 wird unterstützt
ATMega2560 wird unterstützt
ATMega1280 wird unterstützt
ATTiny 45/85 werden unterstützt
Weitere Beispiele:
Betriebsspannung messen mit Kalibrierwert im EEPROM
Interner Temperatursensor mit Kalibrierung

Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Serenifly

Nettes Projekt :) Es ist wirklich schade das manche Teile des Prozessors von der Arduino Software so vernachlässigt werden

Sollte auch kein Problem sein dass auf dem Atmega2560 auszudehnen. Wenigstens für single ended Messungen (ohne den Differenzverstärker). Da ist lediglich der Multiplexer komplizierter und man hat eine zusätzliche Referenzsspannung.
Es ist nur doof dass das MUX5 Bit im ADCSRB Register steht


Der Datentyp der enum Elemente ist in diesem Fall int. Mit C++11 strongly typed enums kannst das auf byte festlegen

combie

Quote
Der Datentyp der enum Elemente ist in diesem Fall int. Mit C++11 strongly typed enums kannst das auf byte festlegen
Danke!

In den Beispielen macht es keinen Unterschied.
Aber wenn man später mal Arrays mit Konfigurationen verwalten muss, ist jedes eingesparte Byte Gold wert.
Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

combie

Quote
Sollte auch kein Problem sein dass auf dem Atmega2560 auszudehnen.
Wie würdest du das tun, ohne dass das ein #ifdef #ifndef Grab wird?

Quote
Es ist nur doof dass das MUX5 Bit im ADCSRB Register steht
Och, das ist nur ein bisschen schieben und Masken Gehampel.
Vielleicht nicht schön, aber unumgänglich.


Auch die Tinys stehen da noch an...
Gerade der 85 steht in meinem egoistischen Interesse.

Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Serenifly

Wie würdest du das tun, ohne dass das ein #ifdef #ifndef Grab wird?
Wenn du nicht allzuviele Prozessoren abdecken willst, ist es nicht zu extrem. Klar geht etwas Übersicht verloren, aber letztzlich machen es alle Libraries so.

Theoretisch könnte man eine gemeinsame Oberklasse mit abstrakten Methoden haben die man in Prozessor-spezifischen Unterklassen implementiert. Aber dann ist der Anwender dafür veranwortlich die passende Klasse für seinen Prozessor zu instantieeren. Das ist auch nicht unbedingt schön


combie


Na?
Wie ist es....

Besteht ein Interesse an einer Weiterentwicklung?


Denn, wenn nicht, dann erspare ich mir das erstellen der Doku und das bürsten der TestCodes.


Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Tommy56

Interesse besteht auf alle Fälle.

Gruß Tommy
"Wer den schnellen Erfolg sucht, sollte nicht programmieren, sondern Holz hacken." (Quelle unbekannt)

combie

#7
Sep 01, 2017, 03:19 pm Last Edit: Sep 04, 2017, 09:14 am by combie
Ja, dann möchte ich dir mal zeigen, wie weit ich bin....

Und, ja, es ist ein Präprozessor Grab geworden!  :o
So ganz zufrieden bin ich damit nicht.

Eine kleine Sammlung AVRs wird mittlerweile unterstützt.
Bis auf den Leonardo+Micro, habe ich mittlerweile alle AVR Arduinos erwischt(glaube ich).
Auch die Tiny85 sind dabei.


Was immer getan werden sollte, ist den ADC Takt einstellen.
Das Datenblatt, sagt in welchem Bereich gültige Werte zu erwarten sind.
Bei 16MHz ist der 128er Teiler angemessen
Demzufolge ist für 8Mhz der 64er sinnvoll.
Wenn man nur auf 8 Bit wert legt, kann man noch kleiner werden..

Der Mega und die Tinys haben zusätzlich Differenzial Inputs.
Auch mit Verstärker, bis zu 200fach.
Beim Mega2560 sind die Werte immer Vorzeichenbehaftet.
Wertebereich von 511 bis -512.
Der Tiny kann auch Vorzeichenlos lesen und die Polarität tauschen.
Damit kommt man bei zweifachem lesen, auf einen Wertebereich von 1023 bis -1023



Die Methodenverkettung ist erhalten geblieben.
Den AdcDebug, habe ich erstmal entfernt.
Interruptbetrieb hinzugekommen.



-------
So, das wars erstmal!
Verbesserungsvorschläge und Fragen/Kritik sind willkommen.


Leider ist anzunehmen, dass da noch Bugs drin stecken...
Denn die Masse aller Möglichkeiten/Variationen habe ich noch nicht testen können.


--

Die aktuelle Version findet sich im Eingangsposting dieses Threads
Geformt als Lib mit 3 Beispielen:
1. Grundfunktion.ino -- Ein Minimalbeispiel
2. UnoInterrupt.ino  -- Fast 10000 Messungen im Hintergrund, pro Sekunde
3. MegaDifferenzial.ino -- Auslesen eines DifferenzialEingangs, mit Offsetabgleich



Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Doc_Arduino

Hallo,

ich hatte das auch mal für jemanden statisch zu Fuss programmiert.  ;)
In deinem Fall würde ich aber nicht den Prescaler änderbar machen sondern die Wunschabtestfrequenz. Die dann innerhalb eines möglichen Bereiches liegen muss. Der passende Prescaler muss dann berechnet werden.

Und um die #defines kommste nicht drumherum, wenn du das für mehrere µC haben möchtest.

Code: [Select]

/*
 Doc_Arduino - german Arduino Forum
 IDE 1.6.13
 Arduino Mega2560

 10.02.2017
 
 - ADC läuft permanent im "free running" Modus
 - genutzt wird A2 mit 8Bit Auflösung höherer Samplerate 1000kHz
 
*/

#include <util/atomic.h>    // für cli() und sei() mit SREG Sicherung

volatile byte msb_ADC;
byte value;

void setup() {
  Serial.begin(9600);
  setup_ADC();
}

void loop() {

  value = readout_ADC();
  Serial.println(value);

  delay(500);

}

void setup_ADC ()
{
  ADMUX  = (1<<REFS0);   // AVcc externe Referenz
  ADMUX |= (1<<ADLAR);   // left adjusting       
  // kein MUXx > A0 Eingang
  ADMUX |= (1<<MUX1);    // Input ADC2 select     
  ADCSRA  = (1<<ADPS2);  // Prescaler 16         
  ADCSRA |= (1<<ADATE);  // Auto Trigger enable   
  ADCSRA |= (1<<ADIE);   // Interrupt enable     
  ADCSRA |= (1<<ADEN);   // ADC general enabled   
  ADCSRA |= (1<<ADSC);   // start conversion     
  ADCSRB  = 0;           // free running
}

byte readout_ADC ()
{
  byte data;
  ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {   // cli & sei mit SREG Sicherung
    data = msb_ADC;
  }
  return data;
}


ISR(ADC_vect) // Interrupt Handler Analog-Digital-Converter
{
  msb_ADC = ADCH;
}
Tschau
Doc Arduino '\0'

Messschieber auslesen: http://forum.arduino.cc/index.php?topic=273445
EA-DOGM Display - Demos: http://forum.arduino.cc/index.php?topic=378279

combie

#9
Sep 01, 2017, 05:21 pm Last Edit: Sep 04, 2017, 12:00 am by combie
Quote
In deinem Fall würde ich aber nicht den Prescaler änderbar machen sondern die Wunschabtestfrequenz. Die dann innerhalb eines möglichen Bereiches liegen muss.
Wenn man eine "Wunschabtestfrequenz" angeben/durchsetzen möchte, wird man das über einen Timer erledigen. Nur so bekommt man eine zeitlich, gestochen scharfe, Abtastfolge hin.
Das ist ein anderes Buch!
Aber doch eine spannende Sache, welche wohl auch noch abgehandelt werden möchte.

Quote
Der passende Prescaler muss dann berechnet werden.
Durchaus!
Wie würdest du das tun?
Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Doc_Arduino

#10
Sep 01, 2017, 06:56 pm Last Edit: Sep 01, 2017, 07:00 pm by Doc_Arduino
Hallo,

im Grunde kann man die Prescaler per #defines festnageln, weil die nur vom CPU Takt abhängig sind. Aber man kann die natürlich auch berechnen und bleibt damit flexibel.
Das sieht möglicherweise noch etwas wild aus, weil ich den Code aus meinem Frequenzgenerator rausgezogen und abgewandelt habe. Bei dem gebe ich die änderbare Frequenz vor und berechne live den passenden Prescaler und Timer Top-Wert. Das Prinzip ist, es werden alle vorhandenen Prescaler der Reihe nach durchgetestet bis das Ergebnis im gewünschten Limit liegt.

Mit freq_max auf 180kHz kann man das Ergebnis noch etwas mehr in Richtung 100kHz schieben.


Code: [Select]
/*
 *  Doc_Arduino - german Arduino Forum
 *  IDE 1.8.3
 *  Arduino Mega2560
 *  01.09.2017
 *  
 *  ADC Prescaler Berechnung
 */

int new_Prescaler;
float real_adc_freq;              

void setup() {
  Serial.begin(9600);
    
}  // Ende Setup

 
void loop() {

  for(long i = 16000000; i>=1000000; i=i-1000000) {
    calc_adc_Prescaler(i);
    Serial.print(F("CPU Takt:  ")); Serial.print(i);              Serial.print('\t');  
    Serial.print(F("Prescaler: ")); Serial.print(new_Prescaler);  Serial.print('\t');  
    Serial.print(F("ADC Freq:  ")); Serial.print(real_adc_freq);  Serial.print('\t');
    Serial.println();

    delay(1000);
  }

  Serial.println();
  
}


void calc_adc_Prescaler(long CPU_Takt)
{  
  // berechnet den Prescaler
  const int used_Prescaler[] = {2,4,8,16,32,64,128};  // mögliche Prescaler
  long adc_freq = 0;
  const long freq_min  =  50000;  // minimum 50kHz
  const long freq_max  = 180000;  // maximum 200kHz
  
  // 50kHz ... 200kHz
  for ( byte i=0; i<7; i++)  {             // kleinsten passenden Prescaler raussuchen
    adc_freq = (long) ((CPU_Takt*1.0/used_Prescaler[i])+0.5);  // Berechnung und runden
    if (adc_freq <= freq_max && adc_freq >= freq_min)  {
      new_Prescaler = used_Prescaler[i];
      break;
    }  
  }
  /* Ergebnis Gültigkeitskontrolle, ansonsten Timer stoppen */
  if (adc_freq < freq_min || adc_freq > freq_max)  {  
    Serial.print(F("ungueltiger ADC Takt: ")); Serial.println(adc_freq);  // Debug
    //
  }
  
  /* durch Ganzzahlrundungen Rechnung rückwärts */
  real_adc_freq = (float) (CPU_Takt/new_Prescaler);  // Hz
  
  
}  // end Funktion


Tschau
Doc Arduino '\0'

Messschieber auslesen: http://forum.arduino.cc/index.php?topic=273445
EA-DOGM Display - Demos: http://forum.arduino.cc/index.php?topic=378279

combie

#11
Sep 03, 2017, 09:27 pm Last Edit: Sep 03, 2017, 09:28 pm by combie
Ich habe mir das mal durch den Kopf gehen lassen....

Fest einstellen, möchte ich vermeiden, denn das ist es ja, was mich an den Arduino Analog Funkionen, unter Anderem, stört. Die automatische Festlegung von allem.

Es ist ja eigentlich mein Ziel ALLE Features erreichbar zu halten.
Ok, man/ich könnte eine zusätzliche Methode Adc:clockAutoConfig() erfinden, welche die manuelle Einstellung ersetzen kann. Dann bleibt es dem Anwender überlassen, wie er es denn tun möchte, manuell, oder automatisch.


In dem Zuge, des darüber denkens habe ich mal ein Testprogramm gebastelt, um zu testen, welche Teiler überhaupt sinnvoll sind.

Verwendet wird ein UNO.


Die obere, orangene, Linie ist A0 an Vcc
Die untere, rote, Linie ist A1 an GND
Die mittlere, hell blaue, Linie ist A2 am Spannungsteiler(2 mal 1K) auf 1/2Vcc
Die untere, dunkel blaue, stufige Linie zeigt die einzelnen Teilerstufen.

Wie man sieht, sind die Teiler 2,4,8 völlig unbrauchbar, bei 16MHz

Bei Teiler 16 zappelt die Rote unten an GND etwas rum.

Teiler 32,64,128 zeigen wunderschöne Geraden.

Damit ist 500kHz ADC Takt beim meinem konkreten UNO brauchbar.
Da das außerhalb der Empfehlung des Datenblattes liegt, muss das nicht bei jedem ATMega328P so sein.





Der Testcode:
Code: [Select]
#include <Adc.h>

/**
 *
 * Die ADC Vorteiler durchtesten
 *
 * A0 fest auf Vcc gelegt
 * A1 fest auf GND gelegt
 * A2 an einen Spannungsteiler 2 X 1K, also auf 1/2 Vcc gelegt
 *
 * Ausgabe:
 * Waehle den seriellen Plotter im Werkzeuge Menue der IDE
*/

Adc adc;

Adc::AnalogClockDivisor AdcTeiler[] =
{
  Adc::DIV_2,
  Adc::DIV_4,
  Adc::DIV_8,
  Adc::DIV_16,
  Adc::DIV_32,
  Adc::DIV_64,
  Adc::DIV_128,
};



void  setup()
{
   Serial.begin(9600);

   adc  .enable()
        .setResolution(Adc::RES_10BIT)
        .setReference(Adc::REF_VCC);

}

void loop()
{
  static int count = 0;
  static int index = 0;

  if(count > 20)
  {
    count = 0;
    index = ++index % sizeof(AdcTeiler); // wrapp around
  }
  adc.setClockDivisor(AdcTeiler[index]);
  Serial.print(index * 10); // Stufen zeigen
  Serial.print(" ");
  Serial.print(adc(Adc::MUX_ADC0));
  Serial.print(" ");
  Serial.print(adc(Adc::MUX_ADC1));
  Serial.print(" ");
  Serial.print(adc(Adc::MUX_ADC2));
  Serial.println();
  count++;
 }
Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Doc_Arduino

Hallo,

warum so kompliziert? Unabhängig vom µC Takt muss man in einem Fenster zwischen 50 und 200kHz landen. Mehr muss man nicht austesten und bleibt jederzeit Datenblattvorgaben konform.
Tschau
Doc Arduino '\0'

Messschieber auslesen: http://forum.arduino.cc/index.php?topic=273445
EA-DOGM Display - Demos: http://forum.arduino.cc/index.php?topic=378279

combie

#13
Sep 03, 2017, 10:19 pm Last Edit: Sep 15, 2017, 06:53 pm by combie
Quote
By default, the successive approximation circuitry requires an input clock frequency between 50kHz and 200kHz to get maximum resolution. If a lower resolution than 10 bits is needed, the input clock frequency to the ADC can be higher than 200kHz to get a higher sample rate.
Der Kernsatz, ist dieser:
Quote
the input clock frequency to the ADC can be higher than 200kHz to get a higher sample rate.
Damit sind auch Taktraten über 200kHz Datenblatt konform.

Man kann also an die 50kHz runter gehen, wenn man keine schnellen Wandlungen braucht.
Und man kann die 200kHz überschreiten, wenn man Genauigkeit gegen Geschwindigkeit eintauschen muss.

Es ist also keineswegs so, dass der ADC Takt kleiner als 200kHz sein MUSS!

Quote
Unabhängig vom µC Takt muss man in einem Fenster zwischen 50 und 200kHz landen. Mehr muss man nicht austesten und bleibt jederzeit Datenblattvorgaben konform.
Ich sehe es als Fehler an, ihn ein für alle mal, fest zu legen.
Wenn ich den Weg "festlegen" beschreiten wollte, hätte ich mir die ganze Klasse sparen können.


Wer seine Meinung nie zurückzieht, liebt sich selbst mehr als die Wahrheit.

Quelle: Joseph Joubert

Doc_Arduino

Hallo,

ne ne ne combie. 50 bis 200kHz gilt für 10Bit. Mit 8Bit kann man bis 1000kHz gehen.
Festnageln? Haben wir hier ein Missverständnis? Du möchtest eine Klasse schreiben die flexibel reagieren kann. Darum muss man auf den µC Takt eingehen und danach richtet sich der Presclaer um am Ende im kHz Fenster zu landen. Beliebige kHz geht nicht.
Ich meinte in der Antwort weiter oben folgendes. Du kannst je nach verwendeten/anliegenden µC Takt den Prescaler per #defines "festnageln". Man muss nur eine ellenlange Liste erstellen. Also unsinnig. Deswegen die einmalige Berechnung nach µC einschalten. Mehr wollte ich dir gar nicht sagen.

Tschau
Doc Arduino '\0'

Messschieber auslesen: http://forum.arduino.cc/index.php?topic=273445
EA-DOGM Display - Demos: http://forum.arduino.cc/index.php?topic=378279

Go Up