|
Introduction
It's gonna be very fun to use various kinds of hardware sensors like an accelerometer with your micro computer. With those sensors, your micro computer can communicate with its surrounding environments.
What those sensors will provide us is analogue value in most cases, but micro computer itself can only manipulate digital value. So you need A/D (Analogue to Digital) converter to make use of those sensor data.
Now we're going to learn how to use A/D converter of ATMega8535 and read the output value of VR (variable resistance).
About A/D conversion
A/D converter (ADC) is the circuit to turn analogue values into digital values which can be manipulated directly by micro computer. Of course there's the circuit to do things in the reverse way (D/A converter, which means Digital to Analogue Converter), but ATMega8535 has A/D converter only. The features of the A/D converter of ATMega8535 is as follows.
- 10 bit resolution
- 65 - 260us conversion time
- 8 multiplexed single ended input channels
- 2 differential input channels with optional gain of 10x and 200x
- 0 - Vcc ADC input voltage range
- Selectable 2.56V ADC reference voltage
- Free running or single conversion mode
- Interrupt on ADC conversion complete
- Sleep mode noise canceler
We'll use 8 pins (PA0 - PA7) which are grouped as PORTA as analogue inputs (pin numbers are 40 - 33, in case of PDIP package). These pins are often refered to as ADC0 - ADC7 in some datasheets, but avr-libc doesn't have any macros with those names.
I'm going to demonstrate how to read the value of VR (variable resistence) with this function of A/D conversion in a sample program.
VR (variable resistence) is marked by a red rectangle in the photo below.
Registers
Let's see the registers related to A/D conversion. Pin names can be used when you use avr-libc.
- ADC Multiplexer Selection Register (ADMUX)
Bit No. 7 6 5 4 3 2 1 0 Pin name REFS1 REFS0 ADLAR MUX4 MUX3 MUX2 MUX1 MUX0 Initial value 0 0 0 0 0 0 0 0 Pin name Description REFS1-0(Reference Selection Bits) Select the voltage reference for the A/D converter. REFS1 REFS0 Voltage Reference 0 0 External voltage reference through AREF 0 1 AVCC 1 0 Reserved 1 1 Internal 2.56V voltage reference ADLAR(ADC Left Adjust Result) Specify the format of the ADC conversion result in the ADC Data Register. MUX4-0(Analog Channel
and Gain Selection Bits)Select the combination of analog inputs to be connected to A/D converter. MUX4-0 Single Input Differential Input Positive Negative Gain 00000 PINA0 - - - 00001 PINA1 - - - 00010 PINA2 - - - 00011 PINA3 - - - 00100 PINA4 - - - 00101 PINA5 - - - 00110 PINA6 - - - 00111 PINA7 - - - 01000 - PINA0 PINA0 x10 01001 - PINA1 PINA0 x10 01010(N/A when using PDIP) - PINA0 PINA0 x200 01011(N/A when using PDIP) - PINA1 PINA0 x200 01100 - PINA2 PINA2 x10 01101 - PINA3 PINA2 x10 01110(N/A when using PDIP) - PINA2 PINA2 x200 01111(N/A when using PDIP) - PINA3 PINA2 x200 10000 - PINA0 PINA1 x1 10001 - PINA1 PINA1 x1 10010 - PINA2 PINA1 x1 10011 - PINA3 PINA1 x1 10100 - PINA4 PINA1 x1 10101 - PINA5 PINA1 x1 10110 - PINA6 PINA1 x1 10111 - PINA7 PINA1 x1 11000 - PINA0 PINA2 x1 11001 - PINA1 PINA2 x1 11010 - PINA2 PINA2 x1 11011 - PINA3 PINA2 x1 11100 - PINA4 PINA2 x1 11101 - PINA5 PINA2 x1 11110 1.23V - - - 11111 GND - - -
- ADC Control and Status Register A (ADCSRA)
Bit No. 7 6 5 4 3 2 1 0 Pin name ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0 Initial value 0 0 0 0 0 0 0 0 Pin name Description ADEN(ADC Enable) Writing 1 to this bit enables the ADC. The power of the ADC will be turned off when this bit is set to 0. ADSC(ADC Start Convertion) When ADATE is 0, setting this bit to 1 starts a conversion. When ADATE is 1, setting this bit to 1 starts the first conversion. ADATE(ADC Auto Trigger Enable) Setting this bit to 1 enables ADC auto triggering. ADIF(ADC Interrupt Flag) This bit is set when a conversion completed. When ADIE is set, ADIF will be cleared automatically by executing the interrupt handler. To manually clear ADIF, write 1 to this bit. ADIE(ADC Interrupt Enable) Writing 1 to this bit enables interrupt on completion of conversion. ADPS2-0(ADC Prescaler Select Bits) Select the division factor between the XTAL freq and the input clock to the ADC by the combination of these 3 bits. ADPS2 ADPS1 ADPS0 Division Factor 0 0 0 2 0 0 1 2 0 1 0 4 0 1 1 8 1 0 0 16 1 0 1 32 1 1 0 64 1 1 1 128
- ADC Data Register
This is the register that holds the result of an ADC conversion. ADLAR bit in ADMUX affects the form of the result held in this register. When ADCL is read, this register will not be updated until ADCH is read. So when you need 2 bytes data, read ADCL first or the result might be changed.
When you read 2 bytes at once by accessing this register as ADC, make sure interrupts do not occur.Only 10 bits of this register can be used, so the maximum value is 0x3ff = (reference voltage - 1 LSB).
--- ADCH (ADLAR=0) ---
Bit No. 7 6 5 4 3 2 1 0 Pin name Unused Unused Unused Unused Unused Unused - - Initial value 0 0 0 0 0 0 0 0 --- ADCL (ADLAR=0) ---
Bit No. 7 6 5 4 3 2 1 0 Pin name - - - - - - - - Initial value 0 0 0 0 0 0 0 0 When ADLAR=0, the result is right adjusted.
--- ADCH (ADLAR=1) ---
Bit No. 7 6 5 4 3 2 1 0 Pin name - - - - - - - - Initial value 0 0 0 0 0 0 0 0 --- ADCL (ADLAR=1) ---
Bit No. 7 6 5 4 3 2 1 0 Pin name - - Unused Unused Unused Unused Unused Unused Initial value 0 0 0 0 0 0 0 0 When ADLAR=1, the result is left adjusted.
- Special Function IO Register(SFIOR)ACME/PUD/PSR2/PSR10 is not related to A/D conversion.
Bit No. 7 6 5 4 3 2 1 0 Pin name ADTS2 ADTS1 ADTS0 Reserved ACME PUD PSR2 PSR10 Initial value 0 0 0 0 0 0 0 0 Pin name Description ADTS2-0(ADC Auto Trigger Source) When ADATE bit in ADCSRA is set to 1, ADTS2-0 selects the source which triggers a conversion. ADTS2-0 Trigger Source 000 Free Running Mode(ADSC keeps to be 1) 001 Analog Comparator 010 External Interrupt Request 0 011 Timer/Counter0 Compare Match 100 Timer/Counter0 Overflow 101 Timer/Counter1 Compare Match B 110 Timer/Counter1 Overflow 111 Timer/Counter1 Capture Event
Circuit diagram
Here is the circuit diagram.
50KΩ volume is connected to PA0 pin of PORTA, and PA0 pin is pulled up by 10KΩ. With this setting, we can make sure PA0 is 0V when the value of the variable registence is 0KΩ, and PA0 is around 4.17V (Vcc x 50K/(50K + 10K), VCC=5V) when the registence of the volume is its maximum value : 50KΩ.
AVCC is connected to VCC (5V). AREF is also connected to VCC, so 0x3ff (the maximum value of the result of A/D conversion) + 1LSB corresponds to VCC (5V). If you open the volume completely, we can expect the result of A/D conversion would be around 853.182 (0x3ff x (4.17V/5V)) at maximum.
Sample program
Here is a sample program reading the value of a variable resistance using A/D conversion. You can download the source code from here. You also can download it as a tar archive of all the sample programs at this page.
This source code contains the following.
- Makefile
- adc.h
- adc.c
- util.h
- util.c
- usart.h
- usart.c
- main-int.c
- main-noint.c
usart.h/usart.c are the same with those found in LECTURE 3 Serial communication using USART. Make file is a bit changed but is easy to understand. So here, I'll show you other 6 files below. All the source code is distributed under modified BSD license (new BSD license). See the License. Licenses for documentation and source code are described there.
If you've never done fuse bits configuration, do "make fuse" first. See the description of the sample program at LECTURE 2 Make LED blink for the "make fuse".
If you want to upload the program which uses interrupt, do "make uisp-int". If you want to use the program which does not use interrupt, do "make uisp-noint". What these 2 programs do is the same. Just the way of implementation is different.
This sample program reads the value of the variable resistance every 500ms by an A/D conversion, and then sends a string like "PINA0 input = ..." via serial communication. The result of the A/D conversion is shown in decimal. To receive the result via serial communication, connect the board with the serial port of your PC (9 pin D-SUB) using a straight cable and runs your terminal soft with the following parameters.
- Asynchronous operation
- Baud rate is 9600 bps
- no parity check
- 1 stop bit
- 8 bits data
If you change the value of the variable resistance, the numeric part of the result string will change.
- adc.h
1 #ifndef ___ADC_H_INCLUDED_ 2 #define ___ADC_H_INCLUDED_ 3 4 #include <avr/io.h> 5 #include <inttypes.h> 6 7 8 /* Maximum value of the result of A/D conversion. */ 9 #define ADC_MAX 0x3ff 10 11 /* Definition of macros to select the clock value for A/D conversion. */ 12 #define ADC_CK_2 (_BV(ADPS0)) 13 #define ADC_CK_4 (_BV(ADPS1)) 14 #define ADC_CK_8 (_BV(ADPS0) | _BV(ADPS1)) 15 #define ADC_CK_16 (_BV(ADPS2)) 16 #define ADC_CK_32 (_BV(ADPS0) | _BV(ADPS2)) 17 #define ADC_CK_64 (_BV(ADPS1) | _BV(ADPS2)) 18 #define ADC_CK_128 (_BV(ADPS0) | _BV(ADPS1) | _BV(ADPS2)) 19 20 /* Definition of macros to select if you use completion interrupt. */ 21 #define ADC_NO_INT 0 22 #define ADC_USE_INT 1 23 24 25 #define adc_set_ch(ch) { ADMUX = (ch) & (_BV(MUX2) | _BV(MUX1) | _BV(MUX0)); } 26 #define adc_wait() loop_until_bit_is_set(ADCSRA, ADIF) 27 #define adc_get() ADC 28 #define adc_start() { ADCSRA |= _BV(ADIF); ADCSRA |= _BV(ADSC); } 29 30 31 void adc_set_interrupt(void (*func)(void)); 32 void adc_init(uint8_t clock, int use_interrupt); 33 34 35 #endif 36 37 /* End of adc.h */
- line 11 - 18
Defining some macros to select the clock value for A/D conversion. Specify one of these macros to adc_init() described later.
- line 25 - 28
adc_set_ch() macro can be used to specify analog input as PINA0 - PINA7(or PORTA0/PA0 - PORTA7/PA7). This macro sets MUX2-0 bit of ADMUX register to the specified value, so this program only supports 8 channel single inputs.
adc_wait() macro blocks till the completion of A/D conversion. Use this after executing adc_start() to wait for the end of A/D conversion. When A/D conversion completes, ADIF bit of ADCSRA register will be set to 1, so this macro checks the ADIF bit using loop_until_bit_is_set(sft, bit) of avr-libc.
adc_get() macro returns the value of ADC register (A/D conversion data register). Make sure any interrupts will not occur when you use this macro. In this program, ADLAR bit of ADMUX register becomes always 0, so the result of A/D conversion is the lower 10 bits of ADC register.
You can start A/D conversion by calling adc_start() macro, which sets the ADCS bit of ADCSRA register to 1. Before setting ADCS bit, uses _BV(ADIF) to clear the ADIF bit (this bit will be cleared by setting it to 1).
- adc.c
1 #include <avr/interrupt.h> 2 #include <avr/io.h> 3 #include <inttypes.h> 4 5 #include "adc.h" 6 7 8 static void (*adc_interrupt_handler)(void); 9 10 11 /* 12 * A/D conversion complete interrupt handler called by avr-libc. 13 * This will be enabled when adc_init(???,ADC_USE_INT) is called. 14 */ 15 ISR(ADC_vect) 16 { 17 adc_interrupt_handler(); 18 } 19 20 21 /* 22 * The setter of user function 23 * which will be called from A/D conversion complete interrupt handler. 24 */ 25 void 26 adc_set_interrupt(void (*func)(void)) 27 { 28 adc_interrupt_handler = func; 29 } 30 31 32 /* 33 * Initializes A/D converter. 34 * Specify one of the clock definition macros defined in adc.h to clock argment. 35 * 36 * If your program uses A/D conversion complete interrupt, 37 * call sei() after executing this function (adc_init()). 38 */ 39 void 40 adc_init(uint8_t clock, int use_interrupt) 41 { 42 clock &= (_BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0)); 43 44 if (use_interrupt) { 45 /* Enables A/D conversion and complete interrupt */ 46 ADCSRA = (_BV(ADEN) | _BV(ADIE) | clock); 47 } 48 else { 49 /* Enables A/D conversion */ 50 ADCSRA = (_BV(ADEN) | clock); 51 } 52 } 53 54 /* End of adc.c */
- line 11 - 18
Describes the interrupt handler, which will be used if you enable A/D conversion complete interrupt. As already explained in LECTURE 3 Serial communication using USART, you must write the code of interrupt handler like ISR(interrupt_id_macro){ ... } when you use avr-libc. The macro that means A/D conversion completion is defined as ADC_vect in avr/interrupt.h.
- line 21 - 29 (adc_set_interrupt())
Registers the user function. Interrupt handler of A/D conversion completion will call this user function.
- line 32 - 52 (adc_init())
Initializes A/D converter. You must specify ADC_CK_X macro defined in adc.h to clock argument.
When you don't need A/D conversion complete interrupt, set 1 to ADEN bit of ADCSRA register to enable A/D conversion and set the clock value for A/D conversion at the same time. If you need A/D conversion complete interrupt, set 1 to ADIE bit of ADCSRA register additionally.
- util.h
1 #ifndef ___UTIL_H_INCLUDED_ 2 #define ___UTIL_H_INCLUDED_ 3 4 #include <avr/io.h> 5 6 7 #define pullup() {\ 8 PORTA = 0xff; \ 9 PORTB = 0xff; \ 10 PORTC = 0xff; \ 11 PORTD = 0xff; \ 12 } 13 14 void delay_250ms(void); 15 void delay_500ms(void); 16 int itoa(char *buf, int target, int base, int width, char pad); 17 18 19 #endif 20 21 /* End of util.h */
- util.c
1 #include <util/delay.h> 2 #include "util.h" 3 4 5 void 6 delay_250ms(void) 7 { 8 int i; 9 10 for (i = 0; i < 25; i ++) { 11 _delay_ms(10); 12 } 13 } 14 15 16 void 17 delay_500ms(void) 18 { 19 int i; 20 21 for (i = 0; i < 50; i ++) { 22 _delay_ms(10); 23 } 24 } 25 26 27 #define INT64_MAX_DIGIT 20 28 29 int 30 itoa(char *buf, int target, int base, int width, char pad) 31 { 32 char tmpbuf[INT64_MAX_DIGIT + 1]; 33 char *tmppos = tmpbuf; 34 char *pos = buf; 35 char *left; 36 char *right; 37 char swap; 38 char ch; 39 int length = 0; 40 int i; 41 42 do { 43 *(tmppos ++) = "0123456789abcdef"[target % base]; 44 length ++; 45 } 46 while (target /= base); 47 48 *tmppos = '\0'; 49 left = tmpbuf; 50 right = tmppos - 1; 51 52 while (left < right) { 53 swap = *left; 54 *(left ++) = *right; 55 *(right --) = swap; 56 } 57 58 for (i = 0; i < width - length; i ++) { 59 *(pos ++) = pad; 60 } 61 62 tmppos = tmpbuf; 63 64 while ((ch = *(tmppos ++))) { 65 *(pos ++) = ch; 66 } 67 68 *pos = '\0'; 69 return (width > length) ? width : length; 70 } 71 72 /* End of util.c */
- line 16 - 24(delay_500ms())
I newly defined delay_500ms() to wait 500ms using _delay_ms() provided by avr-libc.
- line 27 - 70(itoa())
itoa() can be used to convert an integer value to a string to be displayed. itoa() inserts the specified character while the length of the resulting string becomes the specified one. For example, itoa(buf, 10, 10, 5, '*') will return "***10".
int itoa(char *buf, int target, int base, int width, char pad); buf : The buffer to save the resulting string. target : The number you want to convert to a string. base : The base number. width : The length of the resulting string. pad : The character to insert to the left of the resulting string while the length reaches the width.
I wanted to use some functions defined in avr-libc, but if I link avr-libc into my program, it easily exceeds the size of the flash memory region (8KB in case of ATmega8535). So I implemented the necessary code by myself.
- main-int.c(uses A/D conversion complete interrupt)
1 #include <avr/interrupt.h> 2 #include <avr/io.h> 3 #include <avr/sleep.h> 4 #include <inttypes.h> 5 6 #include "adc.h" 7 #include "usart.h" 8 #include "util.h" 9 10 11 static void adc_interrupt(void); 12 13 14 int 15 main(void) 16 { 17 pullup(); 18 PORTA &= ~_BV(PINA0); /* Disables PINA0 pullup. */ 19 20 /* (8MHz/64) = 125KHz */ 21 adc_init(ADC_CK_64, ADC_USE_INT); 22 adc_set_interrupt(adc_interrupt); 23 adc_set_ch(PINA0); 24 usart_init(USART_NO_INT); 25 26 sei(); 27 28 while (1) { 29 sleep_mode(); 30 delay_500ms(); 31 } 32 33 return 0; /* Just avoid compiler warnings... */ 34 } 35 36 37 static void 38 adc_interrupt(void) 39 { 40 char abuf[5]; 41 char *pos = abuf; 42 char ch; 43 char buf[32] = "PINA0 input = \r\n"; 44 int index = 14; 45 46 uint16_t data = adc_get(); 47 48 itoa(abuf, data, 10, 4, '0'); 49 50 while ((ch = *(pos ++))) { 51 buf[index ++] = ch; 52 } 53 54 usart_tx_string(buf); 55 } 56 57 /* End of main-int.c */
main() which uses A/D conversion completion interrupt is defined in this file.
- line 14 - 34(main())
First of all, executes the following sequence.
- pullup() (pullup.h, enables internal pullup of all the input port.)
- Disables PINA0 pullup because we need it as an input.
- adc_init() (adc.h, A/D converter initialization.)
- adc_set_interrupt() (adc.h, registers a user function, which will be executed automatically when A/D conversion completes.)
- adc_set_ch() (adc.h, set PINA0 as an analog input.)
- usart_init() (usart.h, initializes USART.)
Then, call sei() to enable system interrupts.After this, adc_interrupt() will be called every time A/D conversion complete. And then repeats the following.
- Calls sleep_mode() and go into an idle mode.
- Waits 500ms by using delay_500ms().
In this file, there is no code to start A/D conversion explicitly. It uses ADC(A/D Converter) Noise Reduction feature, which enables conversion during sleep mode to reduce noise. ADC Noise Reduction can be used as following.
- Set the ADEN bit of the ADCSRA register to 1 (starts A/D conversion).
- Check if the ADSC bit of the ADCSRA register is 0.
- Check if the ADATE bit of the ADCSRA register is 0 (auto conversion is disabled).
- Set the ADIE bit of the ADCSRA register to 1 (enables A/D conversion complete interrupt).
- Enter Idle mode (A/D conversion starts right after CPU stopped).
- When A/D conversion completes, A/D conversion complete interrupt ocurres, and wakes up the CPU.
In main.c, main() starts A/D conversion by calling sleep_mode(), process an interrupt by adc_interrupt(), and starts A/D conversion again by sleep_mode() after waiting 500ms.
- line 37 - 55 (adc_interrupt())
This is the user function which will be executed every time A/D conversion complete.
First of all get the result of A/D conversion by adc_get(). Then use itoa() to get a 4-character string expression of the resulting value of A/D conversion. '0' will be inserted until the length of the string becomes 4 letter length. After that, puts the resulting string to the USART sending buffer by usart_tx_string().
- main-noint.c(without A/D conversion complete interrupt)
1 #include <avr/io.h> 2 #include <inttypes.h> 3 4 #include "adc.h" 5 #include "usart.h" 6 #include "util.h" 7 8 9 #define INDEX_TO_INSERT 14 10 11 12 int 13 main(void) 14 { 15 char abuf[5]; 16 char *pos; 17 char ch; 18 char buf[32] = "PINA0 input = \r\n"; 19 int index; 20 uint16_t data; 21 22 pullup(); 23 PORTA &= ~_BV(PINA0); /* Disables PINA0 pullup. */ 24 25 /* (8MHz/64) = 125KHz */ 26 adc_init(ADC_CK_64, ADC_NO_INT); 27 adc_set_ch(PINA0); 28 usart_init(USART_NO_INT); 29 30 while (1){ 31 adc_start(); 32 adc_wait(); 33 data = adc_get(); 34 35 itoa(abuf, data, 10, 4, '0'); 36 index = INDEX_TO_INSERT; 37 pos = abuf; 38 39 while ((ch = *(pos ++))) { 40 buf[index ++] = ch; 41 } 42 43 usart_tx_string(buf); 44 delay_500ms(); 45 } 46 47 return 0; /* Just avoid compiler warnings... */ 48 } 49 50 /* End of main-noint.c */
This file is the another version that doesn't use A/D conversion complete interrupt.
- line 31 - 32
First of all, executes the following sequence.
- pullup() (pullup.h, enables internal pullup of all the input port.)
- Disables PINA0 pullup because we need it as an input.
- adc_init() (adc.h, A/D converter initialization.)
- adc_set_ch() (adc.h, set PINA0 as an analog input.)
- usart_init() (usart.h, initializes USART.)
Then enter while loop. Within while loop, executes adc_start() and adc_wait().
adc_start(); adc_wait();
By this, start A/D conversion explicitly and waits it to finish. adc_wait() blocks during A/D conversion.
After A/D conversion finished, gets the resulting value by adc_get(). Then uses itoa() and puts the resulting string to the USART sending buffer by usart_tx_string(). After that, executes delay_500ms() to wait for 500ms.
You can use various external sensors by A/D conversion. Make fun of it!