Forum: Projekte & Code STM32F0x2 ADC ohne HAL Funktionen


von Malte _. (malte) Benutzerseite


Lesenswert?

Hallo,
hier eine Minimalimplementierung um auf einem STM32F042 einen AD Pin und 
die Die Temperatur auszulesen. Gegebenenfalls müssen die Pins, die 
eingelesen werden sollen, noch auf Analogen Eingang gestellt werden.
Gestestet mit PCLK=12MHz.
1
void initAdc(void) {
2
  __HAL_RCC_ADC1_CLK_ENABLE();
3
  ADC1->CFGR2 = ADC_CFGR2_CKMODE_1; //PCLK div by 4. Allowed range is 0.6MHz to 14MHz.
4
  ADC1->SMPR = ADC_SMPR_SMP_0 | ADC_SMPR_SMP_1 | ADC_SMPR_SMP_2; //slowest possible sampling time, 239.5 clock cycles
5
  ADC1_COMMON->CCR = ADC_CCR_TSEN; //temperature sensor enabled
6
  ADC1->CR |= ADC_CR_ADCAL; //start calibration
7
  while (ADC1->CR & ADC_CR_ADCAL); //wait for calibration to end
8
  HAL_Delay(1); //errata workaround
9
  ADC1->CR |= ADC_CR_ADEN; //can not be done within 4 adc clock cycles, due errata
10
  while ((ADC1->ISR & ADC_FLAG_RDY) == 0); //measured 8 loop cycles @12MHz until bit was set
11
}
12
13
uint16_t getAdc(uint32_t channel) {
14
  ADC1->ISR |= ADC_ISR_EOC; //clear end of conversion bit
15
  while (ADC1->CR & ADC_CR_ADSTART); //otherwise the channel can not be changed
16
  ADC1->CHSELR = (1 << channel);
17
  ADC1->CR |= ADC_CR_ADSTART;
18
  while ((ADC1->ISR & ADC_ISR_EOC) == 0);
19
  uint16_t val = ADC1->DR;
20
  return val;
21
}
22
23
void readTemperatureSensor(void) {
24
  int32_t tsCal1 = *((uint16_t*)0x1FFFF7B8); //30°C calibration value
25
  int32_t tsCal2 = *((uint16_t*)0x1FFFF7C2); //110°C calibration value
26
  int32_t temperature = getAdc(ADC_CHANNEL_TEMPSENSOR);
27
  int32_t temperatureCelsius = 80000 / (tsCal2 - tsCal1) * (temperature - tsCal1) + 30000;
28
  temperatureCelsius /= 1000;
29
  dbgPrintf("Temp:%i°C\n\r", temperatureCelsius);
30
}

Der Code mag auf den ersten Blick trivial erscheinen und damit kaum ein 
Posting wert. Nur habe ich am Ende drei Abende herumprobiert, bis die 
Ansteuerung funktionierte.
Die Gründe dafür waren:
1. Alle online gefundenen Beispiele verwenden einfach die HAL.

2. Der ADC verhält sich nicht so wie im Datenblatt beschrieben und wenn 
ein nicht funktionieren mehrere Ursachen hat, wird es plötzlich 
schwierig. Was nicht wie erwarten funktioniert hat:

2.1. Startet man die Kalibrierung und aktiviert danach den ADC, wird das 
Bit nicht gesetzt. Dies ist auch im Errata beschrieben.

2.2. Laut Datenblatt soll man beim Aktivieren des ADC warten, bis das 
Ready Bit gesetzt ist. Bei den ersten Versuchen funktionierte das aus 
unbekannten Gründen nicht.

2.3. Deaktivieren des ADC mit dem ADDIS Bit. Klappte ebenfalls nicht. 
Wohl auch ein Fehler bei den ersten Versuchen einer Implementierung.

2.4. Überall in der ADC Lib hat man defines für passende Bitfelder, nur 
für die Channels ist es die ID und nicht die Bitmaske.

Warum nicht einfach die HAL Lib verwenden?
Weil der Code vom CubeMX nicht funktionierte. Warum nicht? Der Cube baut 
alle ausgewählten AD Eingänge in eine Sequenz die alle automatisch 
nacheinander Konvertiert werden. Und ohne DMA war Pollen per Software 
bei 12MHz schlicht zu langsam um die Daten rechtzeitig abzuholen. Erst 
Heruntertakten des ADC und Auswählen der langsamsten Samplingtime machte 
eine Verwendung möglich. Außerdem spart der Verzicht auf die HAL 
Funktionen (Ok, HAL_Delay ist jetzt noch drin) für den ADC hier mal eben 
1,4KiB Flash (-Og Optimierung).

: Bearbeitet durch User
von ... (Gast)


Lesenswert?

Mach es richtig und schmeiss das ganze HAL-Zeug raus.
Dann bleibt incl. Vectortabelle irgendetwas um die 700 Byte übrig.
1
"A0":                               0x40
2
  .intvec   ro code   0x2000'0000   0x40  vector_table_M.o [3]
3
                    - 0x2000'0040   0x40
4
5
"P1-P2", part 1 of 2:              0x260
6
  .text     ro code   0x2000'0040  0x120  MAIN.o [1]
7
  .text     ro code   0x2000'0160   0xe0  I32DivModFast.o [3]
8
  .text     ro code   0x2000'0240    0x2  IntDivZer.o [3]
9
  .text     ro code   0x2000'0242    0x2  vector_table_M.o [3]
10
  .text     ro code   0x2000'0244   0x1e  cmain.o [3]
11
  .text     ro code   0x2000'0262    0x4  low_level_init.o [2]
12
  .text     ro code   0x2000'0266    0x8  exit.o [2]
13
  .text     ro code   0x2000'0270    0xa  cexit.o [3]
14
  .text     ro code   0x2000'027c   0x14  exit.o [4]
15
  .text     ro code   0x2000'0290    0xc  cstartup_M.o [3]
16
  .text     ro code   0x2000'029c    0x4  SYSTICK.o [1]
17
                    - 0x2000'02a0  0x260

Beitrag #6583111 wurde von einem Moderator gelöscht.
von Thomas H. (rokath)


Lesenswert?

Hast Du mal gemessen, wie lange GetAdc() benötigt? Wäre ganz cool das zu 
wissen. Einfach den Systick vor und nach GetAdc() auslesen.

Beispiel: https://github.com/rokath/trice.

Ein Hinweis:
Division ist rechenzeitintensiv - muss es aber nicht, wenn der Divisor 
zur Compilezeit bekannt ist. Falls Du also in einem Pre-Compile Step die 
prozessorspezifischen Werte tsCal1 und tsCal1 ermitteln kannst (die 
ändern sich ja nur von Exemplar zu Exemplar, kannst Du mit dem 
Taschenrechner  rechnen:

KonstA = 80 / (tsCal2 - tsCal1)

KonstB = ( 80 / (tsCal2 - tsCal1)) * ( - tsCal2 ) + 30

temperatureCelsius  = KonstA * temperature + KonstB

KonstA ist nun irgendeine Kommazahl, sagen wir 0,123. Die multiplizierst 
Du nun mit einer geeigeten Zweierpotenz, sagen wir 1024. Das ergibt 
125,952, also rund 126. Bei Maximaltemperatur gibt das auch noch keinen 
Zahlenüberlauf. Die Zweierpotenz so wählen, dass die Zahlen möglichst 
groß werden aber sicher kein Overflow passiert.

Nun im Code:
```
int32_t temperature = getAdc(ADC_CHANNEL_TEMPSENSOR);
int32_t temperatureCelsius  = (126 * temperature)>>10) + KonstB;
```
Die Rechnung gehört natürlich in einen ausführlichen Codekommentar.

Klar, bei einer Temperaturmessung alle Sekunde ist das nicht nötig aber 
manchmal kommt es auf Speed an.

von Malte _. (malte) Benutzerseite


Lesenswert?

Ok, ich habe mal
1
static void startTimer(void) {
2
  __HAL_RCC_TIM2_CLK_ENABLE();
3
  TIM2->CR1 &= ~TIM_CR1_CEN;
4
  TIM2->CR2 = 0;
5
  TIM2->PSC = 0;
6
  TIM2->CNT = 0;
7
  TIM2->CR1 = TIM_CR1_CEN;
8
}
9
10
static uint32_t getTimerVal(void) {
11
  return TIM2->CNT;
12
}
13
...
14
startTimer();
15
uint32_t voltageJack = getAdc(ADC_CHANNEL_8);
16
uint32_t ticks = getTimerVal();
17
dbgPrintf("Ticks for Adc required: %u\r\n", ticks);
um meinen Code gebaut.
Ergebnis sind 1091 Ticks (manchmal mehr wohl wegen anderen nicht 
deaktivierten Interrupts). Das ist jetzt wenig verwunderlich, da ich den 
ADC mit 1/4 der Timerfrequenz betreibe und die Sampletime auf 239.5 ADC 
Takte steht. Das ergibt einfach einen Mindestwert von 958 Ticks. Dazu 
kommt dann noch die Conversion time ~12 Takte * 4 = 48 -> 1006. Passt 
also ziemlich gut :)

Mit den Divisionen hast du prinzipiell recht. Nur kam es bei meiner 
Anwendung einfach nicht auf Geschwindigkeit an - sonst hätte ich auch 
die Samplezeiten und Takt des ADC optimiert. Allenfalls länger schlafen 
könnte der MCU mit einer höheren ADC Frequenz. In meiner Anwendung messe 
ich 3 Eingänge alle 100ms ;) Umgekehrt habe ich zwei MCUs 
(https://github.com/Solartraveler/audiomux) mit der selben Firmware und 
die haben unterschiedliche Kalibrierungswerte.

: Bearbeitet durch User
von Thomas H. (rokath)


Lesenswert?

Danke für die Zeitmessung! Was ich nicht verstehe ist:

Warum musst Du den ADC so langsam machen? Du schreibst, du kannst ohne 
DMA die Daten nicht rechtzeitig abholen, aber bleibt das Ergebnis nicht 
ewig im Datenregister bis Du den ADC wieder neu triggerst?

Wenigstens die zweite Division könntest Du durch ein >>10 ersetzen indem 
Du mit Vielfachen von 1024 statt 1000 rechnest.

: Bearbeitet durch User
von Malte _. (malte) Benutzerseite


Lesenswert?

Thomas H. schrieb:

> Warum musst Du den ADC so langsam machen? Du schreibst, du kannst ohne
> DMA die Daten nicht rechtzeitig abholen, aber bleibt das Ergebnis nicht
> ewig im Datenregister bis Du den ADC wieder neu triggerst?
Sofern man nur einen ADC Kanal nutzt, ja. Allerdings packt die HAL alle 
ausgewählten ADC Eingänge in eine Chain, die man nur zusammen triggern 
kann.
Man wählt also Eingang 1, 3, 5, aktiviert den Start und dann ist im 
Register erst das Ergebnis für 1, wird dann von 3 und dann von 5 
überschrieben. Und ohne DMA zum Abholen ist das IMHO etwas unbrauchbar. 
Spätestens wenn ein anderer Interrupt dazwischen kommt, der länger als 
eine Konvertierung dauert, geht es schief. Meine Funktion oben triggert 
hingegen immer nur einen Kanal, so dass das da kein Problem wäre die 
Timings zu beschleunigen.

von Thomas H. (rokath)


Lesenswert?

Ok, jetzt ist es klar. Danke für die Aufklärung.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.