[Projekt] ADC in OOP

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:

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

#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

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

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

Oder es lieber so hat:

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

Auch die Arrayschreibweise ist genehm:

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

Adc.zip (11.2 KB)

Nettes Projekt :slight_smile: 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

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.

Sollte auch kein Problem sein dass auf dem Atmega2560 auszudehnen.

Wie würdest du das tun, ohne dass das ein #ifdef #ifndef Grab wird?

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.

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

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.

Interesse besteht auf alle Fälle.

Gruß Tommy

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

Hallo,

ich hatte das auch mal für jemanden statisch zu Fuss programmiert. :wink:
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.

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

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.

Der passende Prescaler muss dann berechnet werden.

Durchaus!
Wie würdest du das tun?

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.

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

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:

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

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.

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:

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!

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.

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.

Deswegen die einmalige Berechnung nach µC einschalten

Das kann man dem Kompiler überlassen.

Nur setzen muss man es noch:

#include <Adc.h>



Adc adc;


// Default ADC Clock Vorteiler generieren:
// (der wiring.c entnommen und leicht modifiziert)
#ifndef ADC_DEFAULT_CLOCK_PRESCALER
  #if F_CPU >= 16000000 // 16 MHz / 128 = 125 KHz
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_128   
  #elif F_CPU >= 8000000 // 8 MHz / 64 = 125 KHz
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_64   
  #elif F_CPU >= 4000000 // 4 MHz / 32 = 125 KHz
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_32 
  #elif F_CPU >= 2000000 // 2 MHz / 16 = 125 KHz
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_16 
  #elif F_CPU >= 1000000 // 1 MHz / 8 = 125 KHz
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_8   
  #elif F_CPU >= 500000 // 0.5 MHz / 4 = 125 KHz
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_4   
  #else // 128 kHz / 2 = 64 KHz -> This is the closest you can get, the prescaler is 2
    #define ADC_DEFAULT_CLOCK_PRESCALER  Adc::DIV_2   
  #endif
#endif





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

   adc  .enable()
        .setResolution(Adc::RES_10BIT)
        .setClockDivisor(ADC_DEFAULT_CLOCK_PRESCALER)
        .setReference(Adc::REF_VCC)
        .setSource(Adc::MUX_ADC0);

  Serial.print("ADC_DEFAULT_CLOCK_PRESCALER ");
  Serial.println(ADC_DEFAULT_CLOCK_PRESCALER);
}

void loop() 
{
  Serial.println(adc());
  delay(1000);
 }

Du möchtest eine Klasse schreiben die flexibel reagieren kann.

Nein, das möchte ich nicht!
Ich möchte die vollständige Macht über den ADC!
Es soll kein Eigenleben entwickeln, welches ich nicht zu 100% unter Kontrolle habe.

Gegen die Erstellung eines Defaultwertes habe ich nichts!
Aber gegen den automatischen Festlegungszwang, auf einen Wert, egal, ob zur Laufzeit, oder zur Kompilezeit, wehre ich mich, mit allen mir zur Verfügung stehenden, Mitteln.
Der Anwender, auch ich, soll das einstellen dürfen, was ihm sinnvoll erscheint.

Bedenke, selbst du hast eben eingesehen, dass:

Unabhängig vom µC Takt muss man in einem Fenster zwischen 50 und 200kHz landen.

Keine unumstößliche Wahrheit ist:

Mit 8Bit kann man bis 1000kHz gehen.

Da ich keiner Klasse, auch dieser nicht, das Hellsehen beibringen kann, muss auch der Divisor einstellbar bleiben.

//EDIT:
Update des Anhangs, im ersten Posing.
Automatische Berechnung des default prescalers und ein Beispiel wie man ihn setzt.

Hallo,

ich kann deiner Logik nicht mehr folgen. Auf der einen Seite willst du eine Klasse schreiben die maximalen Komfort bieten soll und auf der anderen soll der Anwender von Hand den Prescaler raussuchen?
Unter den einstellbaren Parametern sind bestimmt unter anderen ob 10Bit oder 8 Bit Auflösung genutzt werden sollen. An Hand daran kann man den passenden Prescaler berechnen lassen. Wo ist das Problem? Den Endanwender interessiert es nicht welcher Prescaler eingestellt ist. Dem interessiert nur das der ADC mit dem richtigen Takt taktet.

Auf der einen Seite willst du eine Klasse schreiben die maximalen Komfort bieten soll

Nein, das stimmt nicht!

Das habe ich nie gesagt!
Das werde ich auch nicht sagen.
Und das meine ich auch nicht.

Es ist mir ein Rätsel, warum du darauf beharrst.....
Eben sage ich schon:

Nein, das möchte ich nicht!
Ich möchte die vollständige Macht über den ADC!

In der neuen Version, im Eingangsposting, habe ich deine Anregung, so weit es mir möglich ist aufgenommen.
Es wird dort automatisch, vom Kompiler ein Defaultwert erstellt. Welcher bei ca 125kHz ADC Takt liegt.

Diesen kann man nutzen, wenn man möchte.
Soweit konnte ich dir gerne entgegen kommen.
Habe auch extra ein Beispiel hinzugefügt, welches die Verwendung zeigt.

Hallo,

das kleine Problem ist, wir sind unterschiedlicher Auffassung der Definition"flexibel". Du fragst auf feste µC Frequenzen ab. Wenn jemand mit 20 oder 12MHz oder sonstwas um die Ecke kommt, funktioniert es nicht. Aber gut, lasse dich deswegen nicht aus der Ruhe bringen, mach einfach weiter wie du es möchtest ...

Wenn jemand mit 20 oder 12MHz oder sonstwas um die Ecke kommt, funktioniert es nicht.

Wie kommst du darauf?

Natürlich funktioniert die ADC_DEFAULT_CLOCK_PRESCALER Berechnung, für alle F_CPU von 20MHz bis runter zu 100kHz, perfekt.

Es kommt immer ein ADC-Takt von 200kHz bis 50kHz dabei rum.
Egal, wie krumm F_CPU ist.
Das ist doch (fast) genau das, was du dir gewünscht hast, oder?

(Oder habe ich da was wesentliches übersehen?)