Programming the ATmega328P Registers
This is the second article that I have written regarding programming the ATmega328P registers. In the first article we looked at how we could blink an LED using the hardware registers and interrupts. Now we will examine the ADC and how we can use it to measure voltage on one of the Analogue Inputs (AI's) of the Arduino UNO.
The Arduino IDE libraries make it trivial to read the voltage on an AI:
int value = analogRead(A0);
Using default settings, a return value of 0 would represent 0V, and a return value of 1023 (the maximum) would represent approximately 5V. By understanding what is going on behind the scenes you can go beyond this basic implementation and do things like:
- Change the conversion speed via the pre-scaler;
- Use different reference voltages;
- Make ADC reading non-blocking;
- Use an interrupt which returns when the value is read;
- Read other things such as the internal chip temperature, GND and VCC;
ATmega328P Analogue to Digital Converter (ADC)
Figure 1. ATmega328P ADC Simplified Block Diagram.
The Arduino ATmega328P has a 10-bit ADC which means it can return 2^10 (i.e. 0 – 1023) values. This is where the 1024 comes from in the equation from the data sheet:
ADC = (Vin*1024)/Vref
If you want to convert the ADC value to a voltage use the following formula (as the hardware rounds down):
float voltage = ((float) rawADC + 0.5 ) / 1024.0 * Vref;
Select the ADC Voltage Range
In order to convert an analog voltage to a digital value on any ADC, the converter has to be provided with the range of voltages. The lower limit is always GND on the Arduino (i.e. 0V) but you can select one of three sources to serve as the high reference:
- AREF - This is a separate pin on the microcontroller that can be used to provide any high reference voltage you wish to use as long as it’s in the range of 1.0V to VCC. On the Uno it’s wired to one of the black headers.
- AVCC - This pin on the microcontroller provides power to the ADC circuitry on the chip. On the Uno it is connected to VCC. This is the simplest option to use provided AVCC is connected to VCC.
- 1.1V - The microcontroller has an internal 1.1V reference voltage that can be used
I would default to AVCC unless you had a reason to do otherwise. This will give you a nominal 0 - 5V range.
Note that all 8 AI’s on the UNO are connected to the same ADC (see Figure 1). So you can only sample one input at a time. That’s what table 24-4 (data sheet) and the ADMUX register is used for.
ADC Control Registers (ADCSRA and ADCSRB)
The ADC module of the ATmega328P has two control and status registers, ADCSRA and ADCSRB. For basic ADC operations only the bits in the ADMUX and ADCSRA register have to be modified.
Figure 2. ADMUX Register
Assuming we are using AVCC, from Table 24-3 we need REFS1 = 0 and REFS0 = 1. To get a 10 bit result, ADLAR = 1, and if we are using A0 as our input then from Table 24-4, MUX3-0 = 0000. Thus
ADMUX = 0b01000000
Figure 3. ADCSRA Register
The things we need to worry about for ADCSRA are:
Bit 7 - The ADEN bit enables the ADC module. Must be set to 1 to do any ADC operations.
Bit 6 - Setting the ADSC bit to a 1 initiates a single conversion. This bit will remain a 1 until the conversion is complete. If your program using the polling method to determine when the conversion is compete, it can test the state of this bit to determine when it can read the result of the conversion from the data registers. As long as this bit is a one, the data registers do not yet contain a valid result.
Bit 5 - ADATE: ADC Auto Trigger Enable. When this bit is written to one, Auto Triggering of the ADC is enabled. The ADC will start a conversion on a positive edge of the selected trigger signal. The trigger source is selected by setting the ADC Trigger Select bits, ADTS in ADCSRB. We don't want auto triggering so select 0 for this bit.
Bit 4 - ADIF: ADC Interrupt Flag. This bit is set when an ADC conversion completes and the Data Registers are updated. The ADC Conversion Complete Interrupt is executed if the ADIE bit and the I-bit in SREG are set. ADIF is cleared by hardware when executing the corresponding interrupt handling vector. Alternatively, ADIF is cleared by writing a logical one to the flag. Beware that if doing a Read-Modify-Write on ADCSRA, a pending interrupt can be disabled. This also
applies if the SBI and CBI instructions are used. Set ADIF to 0.
Bit 3 - Setting the ADIE bit to a 1 enables interrupts. An interrupt will be generated on the completion of a conversion. The interrupt vector name is “ADC_vect”.
Bits 2:0 - The ADPS2, ADPS1 and ADPS0 bits selects the pre-scaler divisor value. Those available are:
Figure 4. ADC Pre-Scaler Table
The ADC circuitry needs to have a clock signal provided to it in the range of 50kHz to 200kHz. The ATmega328P clock is too fast (16MHz on the Uno) so the chip includes an adjustable pre-scaler to divide the processor clock down to something usable. The processor clock speed is divided by the pre-scaler value to give an ADC clock speed. Using a lower pre-scaler will make the conversion faster but at the cost of accuracy.
The lowest usable value is a pre-scaler of 16 for the UNO. The analogRead library uses a pre-scaler of 128 in order to get maximum resolution. Unless you understand the consequences, stick with 128.
The value for ADCSRA is going to depend on what conversion approach you use (see below).
ADC Data Register (ADCH and ADCL - high and low bytes)
The results of the conversion are stored in the two bytes of the Data Register. The most significant bits of the result are stored in ADCH and the least significant bits are in ADCL. For 10-bit conversion, the ADLAR bit in the ADMUX register should be zero.
Figure 5. ADC Data Registers
When using the 10-bit results (ADLAR=0), the results can be read into your program with:
int value = ADC;
The ADC measures voltage by charging an internal 14 pF capacitor and then measures that voltage with successive approximations. The ADC takes 13 ADC clock cycles to perform a conversion, except the first time the ADC is enabled, at which point it takes 25 ADC cycles, due to the initialisation overhead. The ADC Sample and Hold takes approximately 12μs and the entire conversion process can take up to 260 μs (depending on the pre-scaler selected). So there are at least 3 ways you can approach this:
- Put a long enough delay in your while loop so you know the conversion is done. This is the least elegant method!
- Set the ADSC bit in ADCSRA to a one. This starts the first conversion. Then poll the ADSC bit in ADCSRA until it becomes zero and read the ADC value.
- Use the ADC interrupt and handle the reading in the associated ISR - ADC_vect()
Don’t forget that you need to set the ADSC bit in ADCSRA to one, every time you want to do a conversion.
Sample ADC Register Polling Code
To cement the information above, I will provide some sample code to read the ADC using registers. The first example uses the polling technique (approach 2 above).
A few notes about this code:
- We need to use the standard Arduino loop() to allow serial events to be handled (i.e. the printing out of the result to the serial monitor).
- C5:: The delay(500) is just there to enable us to see the printed result. It is not functionally required, it is the job of the polling (i.e. checking the ADSC bit) to ensure the result is ready.
- With nothing connected to A0, the pin is floating and I was reading values of around 650 (but this could be anything due to electrical noise from the environment, or capacitively coupling with a nearby pin.). Connect A0 to GND and you should see 0, while connecting it to 5V should show 1023.
Sample ADC Register Interrupt Code
For our final example we will measure the ADC using interrupts.
Regarding the interrupt version of the code:
- We have to enable interrupts by setting the ADIE bit of the ADCSRA register.
- The sei() command is optional, setting ADIE is sufficient to enable this interrupt.
- Note we try to do as little as possible in the ISR.
- It is a little bit more complicated than the polling version, but not much.
- As with the polling example if nothing is connected to A0, then the pin is floating and you will get a random result. Connect A0 to GND and you should see 0, while connecting it to 5V should show 1023.