Tuesday, September 24, 2019

Programming the ATmega328P ADC Registers

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
Regardless of the source selected, the data sheet advises putting a capacitor between AREF and ground to smooth out the voltage on that pin. Table 24-3 (data sheet) is used to select which voltage reference you want to use. Note that if you use either of the internal voltages references (AVCC or 1.1V) then that voltage is connected to the AREF pin internally. In this situation, do not connect any other voltage sources to that pin or it will be shorted to the internal reference (and probably let out the magic smoke).


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:
  1. Put a long enough delay in your while loop so you know the conversion is done. This is the least elegant method!
  2. 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.
  3. Use the ADC interrupt and handle the reading in the associated ISR - ADC_vect()
I suggest using approaches 2 or 3.

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.

Saturday, September 21, 2019

Programming the ATmega328P Registers and Interrupts

Why Use Register Programming?


Figure 1. The registers of interest

Normally you wouldn't bother to use register programming for the Arduino family. The libraries provided with the Arduino IDE do all the heavy lifting and make it easy to program the microprocessor without knowing exactly how it works. This convenience and readability is not without a cost though and sometimes for reasons of speed, code size or power consumption you will need to get closer to the metal. An example of this is writing a flight controller for a drone. For a realtime application like this (depending on the Arduino model) you are probably going to need to directly access the I/O registers and interrupts.

I’m afraid this is a fairly tedious way to code! I will try to explain why we are selecting the values in the code, otherwise it looks like gibberish! The comment numbers (e.g. C1) are referenced in the explanation below.

Hello World AKA Blink


The hardware equivalent of Hello World is to blink a LED. To demonstrate what you can do with registers and interrupts we will start with that example. There are many different ways to write this code. The complete listing is shown below.


C1:: We are using Timer 0 which is an 8 bit timer with two independent Output Compare Units, and PWM support (see Figure 1). The PWM outputs are mapped to D5 and D6 but we don’t need these here. We want to detect when the counter reaches the value stored in the OCR0A Register. The Output Compare Registers (OCR0A and OCR0B) are compared with the Timer/Counter value and can be used to generate an Output Compare interrupt request. We can use this to toggle our LED.

Figure 2. TCCR0A & TCCR0B Registers 

The meaning of the TCCR0A & TCCR0B register bits are shown in Figure 2. You turn on the bits required in these registers to get a certain behaviour. To work out what does what, have a look at Figure 3.

Figure 3. Explanation of Register Bits

We want CTC mode 2. So the Waveform Generator Mode (WGM01) bit needs to be 1. That is:

TCCR0A = 0b00000010;

another way to write this is:

TCCR0A = (1 << WGM01);

C2:: The Timer/Counter can be clocked internally, via the pre-scaler, or by an external clock source on the T0 pin. We will use the pre-scaler set to 256. The clock source is selected by the Clock Select logic which is controlled by the Clock Select (CS) bits located in the Timer/Counter Control Register (TCCR0B). For TCCR0B to get a pre-scaler of 256, CS02 needs to be 1 (see Figure 3).

TCCR0B = 0b00000100;

or

TCCR0B = (1 << CS02);

C3:: Next we set the Output Compare Register. The number of ticks for a delay of 4ms is 250, so let's run with that.

OCR0A = 250;

C4:: When the timer/counter reaches the OCR0A number, an interrupt will be triggered by setting the OCIE1A flag in TIMSK1. We can do that by:

TIMSK0 = 0b00000010;

or

TIMSK1 = (1 << OCIE1A);

The specific interrupt vector that will be called for this CTC event is:

ISR(TIMER0_COMPA_vect)

As an aside, a list of the available AVR interrupt vectors can be found at: http://ee-classes.usc.edu/ee459/library/documents/avr_intr_vectors/

Figure 4. Port B Registers

C5:: I used digital output D13 since it is attached to the onboard LED and saves me wiring one up, but obviously you can use any output pin. Here we are using the Data Direction Register (DDR) to set D13 as an output (see Figure 4).

DDRB = 0b00100000;

C6:: We need to set the global interrupt flag to enable interrupts:

sei();

C7:: Finally, we need the Interrupt Service Routine which is called when an interrupt occurs. All we do here is toggle D13 which turns the LED on and off (very quickly)! With a LED toggling every 4ms, it just looks on all the time albeit a bit dimmer. We are effectively using PWM to dim the LED, but we want to be able to see the blinks so we use the extraTime variable to slow things down.