Trying to set up ADC in free-running mode on Arduino Nano Every/ATmega4809 however it is reading incorrect values

I wanted to use the ArduinoFFT library for a sound reactive project but the Nano Every which I'm using has the atmega4809 instead of 328 so I need to initialize the ADC differently.

I've been referencing the Atmega4809 datasheet (http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega4808-4809-Data-Sheet-DS40002173A.pdf#page=395) and borrowing the example code from this app note (http://ww1.microchip.com/downloads/en/AppNotes/TB3209-Getting-Started-with-ADC-90003209A.pdf) (see example 9-2 near the bottom).

Just trying to get it working for now, using a potentiometer as input and sending the value to serial monitor. The problem is, the values I'm reading in are not correct. They're sort of all over the place, as in wildly bouncing between ~100-1023. There's no problem with the wiring because it works as intended when I use analogRead() instead of initializing the ADC through its registers.

Here's what I got. Any ideas? Thanks in advance!


#include <avr/io.h>
#include <stdbool.h>

uint16_t adcVal;

void ADC0_init(void);
uint16_t ADC0_read(void);
void ADC0_start(void);
bool ADC0_conversionDone(void);

void ADC0_init(void){
  
  //disable digital input buffer
  PORTD.PIN4CTRL &= ~PORT_ISC_gm;
  PORTD.PIN4CTRL |= PORT_ISC_INPUT_DISABLE_gc;

  //disable pull-up resistor
  PORTD.PIN4CTRL &= ~PORT_PULLUPEN_bm;

  //           divide clock by 4 | set reference voltage to VDD
  ADC0.CTRLC = ADC_PRESC_DIV4_gc | ADC_REFSEL_VDDREF_gc;

  //            Enable ADC    | 10 bit mode
  ADC0.CTRLA =  ADC_ENABLE_bm | ADC_RESSEL_10BIT_gc;

  //            Select ADC channel - using Arduino Nano Every pin A6 which is PD4 which is channel AIN4
  ADC0.MUXPOS = ADC_MUXPOS_AIN4_gc;

  //            Enable freerun mode
  ADC0.CTRLA |= ADC_FREERUN_bm;
}

 uint16_t ADC0_read(void){
  //            clear interrupt flag by writing 1
    ADC0.INTFLAGS = ADC_RESRDY_bm;

  //            return value stored in result register  
    return ADC0.RES;
  }

void ADC0_start(void){
  //            start conversion
  ADC0.COMMAND = ADC_STCONV_bm;
}

bool ADC0_conversionDone(void){
  //         returns 1 if result ready interrupt flag is set    
  return (ADC0.INTFLAGS & ADC_RESRDY_bm);
}


void setup() {

  Serial.begin(9600);
  ADC0_init();
  ADC0_start();
}

void loop() {
  
  if(ADC0_conversionDone()){
    adcVal = ADC0_read();
  }
  Serial.println(adcVal);
}

This is from something I had used a while back (untested here). Its single conversion mode, but maybe something useful here to try ...

void setup() {
  Serial.begin(9600);
  //Init
  PORTD_PIN4CTRL = 0x04;              // disable digital input buffer on A6 (AIN4)
  ADC0_CTRLA = ADC_RESSEL_10BIT_gc;   // 10-bit resolution
  ADC0_CTRLC = 0x54;                  // reduced capacitance, Vdd ref, prescaler of 32
  ADC0_MUXPOS = 0x04;                 // select A6 (AIN4)
  ADC0_CTRLA |= ADC_ENABLE_bm;        // turn ADC on
}

void loop() {
  //Read
  ADC0_COMMAND |= ADC_STCONV_bm;         // start a conversion
  while (ADC0_COMMAND & ADC_STCONV_bm);  // wait while busy
  uint16_t adcVal = ADC0_RESL;           // get ADC value
  Serial.println(adcVal);
}
  1. Isn't clock/4 (4MHz) too fast? Should be less than 1.5MHz.
  2. This isn't the order of initialization recommended in the datasheet. In particular, it suggests that ENABLE happen last. Since the ADC will presumably already have been initialized by the Arduino code, you may have to clear the ENABLE bit before changing the config. (I didn't actually see anything that said that the config can only be changed while the ADC is disabled, but that sort of thing is pretty common.)

Maybe make a copy of ADC0.RES before resetting the interrupt flag then return the copy.

Hey, so this totally works and im ecstatic. I only had to change ADC0_RESL to ADC0_RES since RESL only accesses the lower 8 bits of resolution. Or at least that's what it seems like from looking at the datasheet and the fact that adcVal overflows back to 0 once it goes past 255. But its working with the full 10 bit resolution now. Now I gotta see if I can implement the free running mode on top of this.

In general, your init doesn't seem that different from mine. So I'm not really sure what was going wrong with mine.

I have some general coding questions if you don't mind:

  • Why do you use ADC0_COMMAND instead of ADC0.COMMAND or ADC0_CTRLA instead of ADC0.CTRLA etc? Like what is the difference between using a period or underscore with struct variables?

  • in this while loop, what is the purpose of bitwise AND'ing ADC0_COMMAND with ADC_STCONV_bm? According to the datasheet, ADC0_COMMAND is 1 while a conversion is in progress and automatically clears to 0 when the conversion is done, so shouldn't that in and of itself satisfy the logic for the while loop?

Really appreciate you taking the time to help!!

Thank you for the comment.

Is this a requirement somewhere I didn't see? I used 4 as the prescaler because thats what was used in the example code in the app note.

hmm, I noticed this too and actually agree with you. But again I just followed the order that was in the app note example code.

So based on your comments I changed the prescaler to 32 and moved the enable to the end of the initialization. And I'm actually reading the right values! However, it only reads it correctly on startup, it never updates adcVal with new values. So I'm not sure what the problem is there. Either way, progress is being made!

So you mean something like this?

uint16_t ADC0_read(void){

    resultCopy = ADC0.RES;

  //            clear interrupt flag by writing 1
    ADC0.INTFLAGS = ADC_RESRDY_bm;

  //            return value stored in result register  
    return resultCopy;
  }

Yes. uint16_t resultCopy = ADC0.RES ;

Oh, I was just using (playing with) the low level register defines. The resource (header file) for that is iom4809.h. This file is difficult to track down, it might be available on github somewhere, but it also might be on your PC after installing Atmel Studio or MPLAB or maybe Visual Studio. On my PC, the folder is C:\Program Files (x86)\Atmel\Studio\7.0\packs\atmel\ATmega_DFP\1.6.364\xc8\avr\include\avr
iom4809.h (218.2 KB)

The underscore versions are not "structure variables." The low-level defines create bother the structure based definitions (with the "."), and old-style single-register defintions (the ones with underscores.) It's nice that the naming is pretty consistent!
(I believe that the structure-based accesses should be slightly more efficient than individual accesses on most modern CPU/Compiler combimations. The compiler will generally put the base address of the peripheral in an index register, and thereafter reference the sub-parts as index+offset load/stores, which are smaller than a load/store to an arbitrary address. Especially if you access more than one of the sub-parts "nearby.")

iom4809.h. This file is difficult to track down

On a typical windows Arduino install, it should be in:
...\AppData\Local\Arduino15\packages\DxCore\tools\avr-gcc\7.3.0-atmel3.6.1-azduino4b\avr\include\avr\iom4809.h

I'm going to be honest, this answer doesn't make a lot of sense to me. Unfortunately I lack the knowledge of C/C++ to really understand so a lot of the terms you are using fly over my head. Maybe I could pick your brain a little more?

I have some working code here, got the ADC working properly in free running mode. So for all intents and purposes, I've achieved what I came here to do. You can see that I'm using the underscores. It also functions identically if I change the underscores to periods (e.g., if I change ADC0_COMMAND to ADC.COMMAND). So what exactly are they doing differently from each other behind the scenes?

void setup() {
  Serial.begin(19200);
  //Init
  PORTD_PIN5CTRL = 0x04;              // disable digital input buffer on analog pin A7 (PD5/AIN5)
  ADC0_CTRLA = ADC_RESSEL_10BIT_gc;   // 10-bit resolution
  ADC0_CTRLC = 0x53;                  // reduced capacitance, Vdd ref, prescaler of 16 (0x40 | 0x10 | 0x03)
  ADC0_MUXPOS = 0x05;                 // select A7 (AIN5)
  ADC0_CTRLA |= ADC_FREERUN_bm;       // start freerun mode
  ADC0_CTRLA |= ADC_ENABLE_bm;        // turn ADC on
  ADC0_COMMAND |= ADC_STCONV_bm;      // start a conversion
}
void loop() {
  
  if(ADC0_INTFLAGS & ADC_RESRDY_bm){
      uint16_t adcVal = ADC0_RES;           // get ADC value
      Serial.println(adcVal);
  }   
}

Perusing through the source files in platformIO, it shows that, for example, ADC0_COMMAND is in a #define for _SFR_MEM8(0x0608). And then ADC0.COMMAND is the variable COMMAND within the ADC_t struct (could be wrong, i really don't fully understand structs yet). So how are they accomplishing the same thing?

thank you for your time!

Huh. It turns out that in this case they produce exactly the same code. That wasn't what I expected.

ADC0_COMMAND is in a #define for _SFR_MEM8(0x0608). And then ADC0.COMMAND is the variable COMMAND within the ADC_t struct
So how are they accomplishing the same thing?

ADC0_COMMAND is defined as a a register at address 0x608. Simple.
The ADC_t struct defines that COMMMAND is the 8th byte of the ADC_t struct, and elsewhere ADC0 will be defined as an ADC_t structure that starts at 0x600. So they both end up referencing the register at 0x608.

The advantages of the struct method is that you could have a second ADC, ADC1, and you'd only have to define ADC1 as being a struct that starts at 0x700, instead of having to defined ALL of the ADC1_xxx addresses. And sometimes the compiler will load "the address of ADC0" into an "index register", and then notice that all the pieces are a "short distance away", and use a smaller (or fewer) instructions to access "8 past the start of the struct" than it would use to access multiple individual addresses.

On an AVR, this is the difference between
LDS Rn, 0x608 ; (a 32bit instruction)
and
LDD Rn, Z+8 ; (a 16bit instruction)
Which isn't much at all, especially since it would also have to set up Z.
Call it a bit more than half the size. And the same speed.

On an ARM, you ALWAYS have to set up the equivalent of Z, which is usually a 16bit instruction plus a 32bit constant (or two 16bit instructions), so it gets closer to 1/3 the size (and 3x faster.)

1 Like

Here's the actual code comparison for AVR:

;; Disassembly of actual code.  44 bytes, 22 cycles.

  //Init
  ADC0_CTRLA = ADC_RESSEL_10BIT_gc;   // 10-bit resolution
 842:   10 92 00 06     sts     0x0600, r1
  ADC0_CTRLC = 0x53;                  // prescaler/etc
 846:   83 e5           ldi     r24, 0x53
 848:   80 93 02 06     sts     0x0602, r24
  ADC0_MUXPOS = 0x05;                 // select A7 (AIN5)
 84c:   10 93 06 06     sts     0x0606, r17
  ADC0_CTRLA |= ADC_FREERUN_bm;       // start freerun mode
 850:   80 91 00 06     lds     r24, 0x0600
 854:   82 60           ori     r24, 0x02  
 856:   80 93 00 06     sts     0x0600, r24
  ADC0_CTRLA |= ADC_ENABLE_bm;        // turn ADC on
 85a:   80 91 00 06     lds     r24, 0x0600
 85e:   81 60           ori     r24, 0x01  
 860:   80 93 00 06     sts     0x0600, r24
  ADC0_COMMAND |= ADC_STCONV_bm;      // start a conversion
 864:   80 91 08 06     lds     r24, 0x0608
 868:   81 60           ori     r24, 0x01       ; 1
 86a:   80 93 08 06     sts     0x0608, r24

;; hand coded alternative.  30 bytes, 24 cycles
;;  (assumes Z is usable) (all instructions are 2 bytes)
    ldi   ZH, 6
    ldi   Zl, 0   ; 0x600 base address in Z
    st    Z, r1   ; CTRLA = 0
    ldi   r24, 0x53
    std   Z+2, r24  ; CTRLC = 0x53
    std   Z+6, r17  ; MUXPOS = 5 (already in r17?)
    ld    r24, Z
    ori   r24, 0x2
    std   Z, r24    ; CTRLA |= FREERUN
    ld    r24, Z
    ori   r24, 1
    std   Z, r24    ; CTRLA |= ENABLE
    ldd   r24, Z+8
    ori   r24, 1
    std   Z+8, r24  ; COMMAND |= START

Really great comment, thank you, this helps a lot. So if I'm understanding right, if I do:

ADC0_COMMAND |= ADC_STCONV_bm;

I am writing a 1 directly to the first bit of the memory address 0x608. Nothing more than that. It is completely independent from the ADC0 struct.

If I do:

ADC0.COMMAND |= ADC_STCONV_bm;

I am writing a 1 to the first bit of the 8bit variable COMMAND. COMMAND is the 8th byte within the ADC0 struct, and the ADC0 struct starts at 0x600 so this 1 gets written to the first bit of 0x608. Thus it produces the same result.

So I had another unrelated question; In the example code from microchips app note, they use a simple function which returns 1 when the ADC0 conversion is done and a result is ready to be read, like this:

bool ADC0_conversionDone(void){
  return (ADC0.INTFLAGS & ADC_RESRDY_bm);
}

What is the reason they do a bitwise AND with ADC_RESRDY_bm? ADC_RESRDY_bm is equal to 0x01. Is this used to make sure that if any other bit in the INTFLAGS register is set, it doesn't evaluate the logic expression as true?

Yes; exactly.
The value & BITMASK construct ("Pattern"?) is extremely common for checking whether any bits from the mask are set. The single-bit mask checks a single bit.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.