3
\$\begingroup\$

I am new to AVR programming, and I am trying to learn the basic I/O with C++.

I am trying to find the simplest, most efficient way to read analog signals using the same function for different devices, just like the analogRead() function in the Wiring language (Arduino code). But I have not found anything very simplistic that I could use for AVR programming.

Any suggestions?

\$\endgroup\$
3
\$\begingroup\$

The nice thing about AVR is code ports very easily from processor to processor. I have a set of functions on file for setting up the peripherals.

Here's my ADC initialization function:

void init_adc(void)
{
    sei();  
    ADMUX = 0b11100000;
    ADCSRA = 0b10001100;
    ADCSRA = ADCSRA | (1<< ADSC);
}

The sei() function serves as a global interrupt enable, and only needs to be called once if multiple interrupts are being used.

The ADMUX register has the ADC set up with the internal 2.56V reference, the conversion result left adjusted so the most significant bits are in ADCH, and converting ADC0, single ended.

The ADSRA register has the ADC enabled, in interrupt mode, and sets the prescaler to \$\frac{f_{osc}}{16}\$. This register also houses the conversion complete interrupt flag.

ADCSRA = ADCSRA | (1<< ADSC);

This line starts the conversion. It's important to note that the ADC is not free running. A new conversion must be started after each conversion.

My bare bones interrupt service routine code:

ISR(ADC_vect)
{
    ? = ADCH;
    /*possible incrementing ADMUX for additional channels*/
    ADCSRA = ADCSRA | (1<< ADSC);
}

The most significant part of the result is in ADCH. ADCL contains the least significant bits. Read from ADCH as necessary. If you have multiple channels you're converting single ended, this is a good place to handle the multiplexing. For example:

if (n == 3)
{
    ADMUX = 0b00100000;
    n = 0;
}
else
{
    ADMUX++;
    n++;
}

I strongly recommend reading the datasheet to understand the operation of the ADC before you start writing code. Atmel's datasheets are pretty good, and offer good explanations and some sample code.

\$\endgroup\$
3
\$\begingroup\$

I don't use Arduino, so I don't know what is happening in those routines, but there are few common ways of taking an ADC measurement with AVR. One of the great things about AVR chips in the ATtiny and ATmega lines is that a lot of peripheral registers have the same (or very similar) names in different chips. I'm going to be using the ATtinyx4 (datasheet) in this example.

Also of note is that these chips are more commonly programmed in C (or assembly), not C++, although Arduino does use a version of C++.

The ADC first has to be set up. This typically involves setting the reference voltage, initializing the ADC multiplexer, setting the ADC clock prescaler, setting the mode, and enabling the ADC interrupt.

For example (in C):

  // _BV(BIT) is defined as (1<<(BIT)) 
  ADMUX = _BV(REFS1);   // Use internal 1.1V reference voltage, multiplexer = 0
  ADCSRA = 
      _BV(ADPS1) |      // Prescaler = 4: F_ADC = F_cpu / prescaler
      _BV(ADEN);        // Enable the ADC
  ADCSRB = _BV(ADLAR);  // Left Adjust Result for 8 bit resolution

To take a measurement, you would do this:

  ADCSRA |= ADSC;               // Start an ADC conversion
  while(ADCSRA & _BV(ADSC));    // Wait until conversion is complete
  ADC_Value = ADCH;             // Read the ADC high register

Notice that I only read the high register because I am only using 8 bits resolution. I could have easily read the entire ADC value instead.

If instead you wanted the code to automatically handle the ADC result, you could enable the ADC conversion complete interrupt.

  ADCSRA |= _BV(ADIE);     // Enable ADC Interrupt

And handle the ADC value in the ADC interrupt service routine. This might be more time efficient, but it will use more code space.

There is also a "free running mode" which means the ADC will continuously take a reading while it is enabled for the selected channel. All of this information is in the datasheet of AVR chips with ADC functionality under the "Analog to Digital Converter" chapter.

If you wanted to create your own function to do this, if might look like this:

uint8_t ADC_read(uint8_t channel)
{
  ADMUX &= (_BV(REFS1) | _BV(REFS0));    // Clear the ADC Multiplexer
  ADMUX |= channel;                      // Set the ADC multiplexer
  ADCSRA |= ADSC;               // Start an ADC conversion
  while(ADCSRA & _BV(ADSC));    // Wait until conversion is complete
  return (ADCH);                // Return the ADC high register value
}

Other things to consider: the first reading after you enable the ADC is typically junk, so if you aren't going to leave it enabled (uses more power this way) you will need to take two consecutive measurements, discarding the first one. Of course, the pins being used should be set as inputs with the digital input buffers disabled (DIDRx).


My above examples used the macro _BV() to set a particular bit in a byte, but this is just a different way of left shifting a 1 so many places. The _BV() macro is typically defined in one of the AVR header files along with the definitions for chip specific register names that are included at the top of a program, but I don't know how that is handled in Arduino.

If the _BV() macro does not work, you can define it yourself as:

#define _BV(BIT)    (1<<(BIT))

Since a byte is 8 bits, it can be thought of as this: [bit7, bit6, ... bit1, bit0]. Setting an individual bit high by left shifting a 1 so many bits over means you can make more readable (and changeable) code then just setting a byte to a specific value. For example, this code:

ADCSRA = 
      _BV(ADPS1) |      // Prescaler = 4: F_ADC = F_cpu / prescaler
      _BV(ADIE) |       // Enable ADC Interrupt
      _BV(ADEN);        // Enable the ADC

makes a lot more sense than this code:

ADCSRA = 0x8A;          // 0b10001010

in addition to being more portable and easier to edit.

Some people do not like using (or seeing) this macro, so they prefer to just use the left shift operand. Either way is fine and uses the same amount of code space/clock cycles to operate. Forgoing the _BV() macro, my examples would look like this:

// Setting up the ADC
ADMUX = (1<<REFS1);     // Use internal 1.1V reference voltage, multiplexer = 0
ADCSRA = 
    (1<<ADPS1) |        // Prescaler = 4: F_ADC = F_cpu / prescaler
    (1<<ADEN);          // Enable the ADC
ADCSRB = (1<<ADLAR);    // Left Adjust Result for 8 bit resolution

// Taking a measurement
ADCSRA |= ADSC;               // Start an ADC conversion
while(ADCSRA & (1<<ADSC));    // Wait until conversion is complete
ADC_Value = ADCH;             // Read the ADC high register

// Enabling the ISR
ADCSRA |= (1<<ADIE);          // Enable ADC Interrupt

//Function to read an ADC Channel (once the ADC is setup)
uint8_t ADC_read(uint8_t channel)
{
  ADMUX &= (1<<REFS1) | (1<<REFS0));     // Clear the ADC Multiplexer
  ADMUX |= channel;                      // Set the ADC multiplexer
  ADCSRA |= ADSC;               // Start an ADC conversion
  while(ADCSRA & (1<<ADSC));    // Wait until conversion is complete
  return (ADCH);                // Return the ADC high register value
}

Again, these examples were based on the registers for the ATtinyx4. A different AVR chip with an ADC might have slightly different register and bit value names or locations... But using the actual register and bit names with comments means it is easy to alter if necssary.

\$\endgroup\$
  • 1
    \$\begingroup\$ @coding_corgi No problem. I should also point out that any functions included in your main file should be declared as "static" unless you will be using them in other files as well. This will save quite a bit of code space. Ex: static uint8_t ADC_read(uint8_t channel); \$\endgroup\$ – Kurt E. Clothier May 19 '13 at 17:57
  • \$\begingroup\$ Ok, I will be putting those functions in a header file, in your example you say "// _BV(BIT) is defined as (1<<(BIT))" - does that mean I have to do this: #define _BV ...? \$\endgroup\$ – user151324 May 19 '13 at 18:55
  • \$\begingroup\$ @coding_corgi With avr-gcc, the _BV() macro is defined in one of the standard included header files, but then again, I don't know about Arduino, so you can always define it yourself like you mentioned. Although you don't have to use it, it is just left shifting a 1 so many bits. For example, if BAR is bit 2, then (1<<BAR) = 0b00000100 = 0x04. I just think it makes the code easier to read. BV stands for "bit value." You can also use it for multiple bits, just OR them together, ex: BYTE = _BV(bit2) | _BV(bit6); Put this at the top of your file: #define _BV(BIT) (1<<(BIT)) \$\endgroup\$ – Kurt E. Clothier May 19 '13 at 20:44
  • \$\begingroup\$ This is an easy way to use the actual register and bit names found in the datasheet instead of just initializing some registers as REG_BYTE = 0b00100101, you can actually specify the exact bits to set: REG_BYTE = _BV(bit0) | _BV(bit2) | _BV(bit5); The actual register and bit names should be defined in the default included AVR header files. \$\endgroup\$ – Kurt E. Clothier May 19 '13 at 20:49
  • \$\begingroup\$ Could you include that in your example instead of _BV() please? \$\endgroup\$ – user151324 May 19 '13 at 20:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.