Lab 7
Analog-to-Digital Conversion and Pulse Width Modulation
Honor Statement
This is an individual assignment. The code you write, use for the demo and
submit should be wholly your own and not produced by working in teams or using,
in part or in whole, code written by someone else (found online, from a fellow
student, acquaintance, etc.). You will be asked to verify this when you submit
your code on Vocareum by signing your name in the honor.txt
file on Vocareum.
Penalties for violation include a 0 on the assignment and a potential further
deduction or referral to Student Judicial Affairs.
Introduction
In this lab exercise you will learn how to use the Arduino’s analog-to-digital (ADC) conversion capability to interface the microcontroller to a variable resistor, also known as a “potentiometer”, and also to the buttons on the LCD module. These input devices will be used to control the generation of a pulse width modulation (PWM) signal from the Arduino Uno. One of the 8-bit timers will be use to generate a PWM signal to control the position of a servo motor. By adjusting the position of the potentiometer you’ll be able to control the position of the servo. In addition, if the RIGHT button on the LCD is pressed the servo rotates to the full clockwise position. If the LEFT button is pressed the servo rotates to the full counter-clockwise position. Pressing the SELECT button returns control of the servo to the potentiometer.
To see a short video demonstrating the operation of lab, click here.
For a copy of the Lab 7 grading sheet, click here
Getting Started
For this lab you will need to get a bag of Lab 7 and 8 components from the instructors. This bag will include the potentiometer and servo motor used in this lab and some additional parts that will be used in Lab 8. The servo should come with a black plastic arm mounted on the servo motor’s shaft. If the arm is missing, ask one of the instructors to get one for you and then press it down onto the motor’s shaft. Having the arm on the servo shaft makes it much easier to see the rotation of the motor.
Set up the Lab 7 assignment the same as has been done previously by creating a lab7 folder in your ee109 folder.
- From the class web site, download the file lab7.zip. This file contains lab7.c, adc.c, adc.h and Lab7_Answers.txt. The adc.c file is a template that will be used in Task 1 to write the code to support the ADC operations.
- Extract the files into the lab7 folder.
- Add a copy of Makefile, lcd.c, lcd.h from a previous lab to the lab7 folder.
- Modify the Makefile to work for the Lab 7 files. It should have
lab7.o
,lcd.o
andadc.o
on the OBJECTS line.
If you look at the lab7.c file you will see that some of the code has been provided for you. At the start of the program add code to do the following:
- Initialize the LCD as has been done in previous labs.
- Put up a splash screen with your name.
Once this has been done you are ready to move on to Task 1 and start adding additional code to interface with some of the hardware.
Task 1: Write the ADC routines
The Arduino’s analog-to-digital converter (ADC) module is used in this lab to adjust the width of the PWM signal going to the LEDs. The ADC will be used to determine the position of the potentiomter and also to determine if one of the buttons on the LCD has been pressed.
Information on using the ADC module is available in a lecture slide set. A more in-depth review of this material is provided in a separate document “Using the Atmel ATmega328P Analog to Digital Conversion Module available on the class web site. Refer to any of these resources for information on configuring the various registers in the module and how to do the conversions.
The adc.c
file contains the template for two routines that you will have to
write.
-
adc_init()
- This routine initializes the ADC module. It only needs to be called once at the start of the program but must be called before making any calls to the routine that takes the samples. -
adc_sample()
- This routine has one argument, the channel number (0 to 5) to use for the ADC sample. It starts an analog-to-digital conversion on the specified channel, waits for it to complete and then returns the 8-bit result of the conversion to the calling program.
The adc_init
routine must set up the proper values in the ADMUX and ADCSRA
registers. Use the information in the ADC lecture slide
set to determine which bits in the
register must be set or cleared by the adc_init
function.
Diagrams of the registers are shown below and are also provided on the Lab 7 grading rubric for you to fill in the values.
Important: When writing code to set or clear the bits in these registers by
using the <<
shift operations, you can use the name of the bit as shown in
the diagram. For example, to set the ADEN bit to a one, the following can be
done.
ADCSRA |= (1 << ADEN)
We strongly encourage you to use the symbolic bit names rather than the 0-7 bit number when doing set/clear/test bit operations.
Your code in adc_init
should do the following:
-
Set/clear the REFS[1:0] bits in ADMUX to select the high voltage reference. Using the AVCC reference is appropriate for this lab.
-
Set or clear the ADLAR bit in ADMUX such that we will use 8-bit conversion results (not 10-bit).
-
Set/clear the ADPS[2:0] bits in ADCSRA to select an appropriate prescalar value.
-
Set the ADEN bit in ADCSRA to enable the ADC module.
The channel select bits in ADMUX should not be configured in the adc_init
function. They will be set or cleared each time adc_sample
is called
according to the channel argument passed to that function.
The other bits should be left as zeros. The ADSC bit (shown below in red) will
be set to a one in the adc_sample
function, but in the initialization of the
registers it should be left unchanged.
The adc_sample routine is called to acquire a sample of the analog signal
from the ADC. The channel to sample is specified as an argument from 0 to 5
when calling the adc_sample routine. The adc_sample routine is declared as a
“unsigned char” function and the return value contains the result of the
conversion and can be saved in an unsigned char variable. For example, to
acquire a sample from ADC channel 2 and put the value in the variable data
,
the code would be
data = adc_sample(2);
The adc_sample routine must do the following.
-
Set/clear the MUX[3:0] bits in ADMUX to select the input channel as specified by the argument to the function. The correct bits to go in the MUX[3:0] bits are simply the lower four bits of the argument to the function so just use bit copying to fill these in.
-
Set the ADSC bit in the ADCSRA register to a 1. This starts the conversion process.
-
Enter a loop that tests the ADSC bit each time through the loop and exits the loop when ADSC is 0. This indicates that the conversion is now complete.
-
Copy the 8-bit conversion result from the ADCH register and return it to the calling program.
Once you have finished editing the adc.c file to complete the adc_init
and
adc_sample
routines, make sure to add adc.o
to the OBJECTS line in the
Makefile so it will get compiled when you do a “make” operation. Also make
sure the adc.h
file in the same folder with the adc.c
.
The LCD Buttons
The buttons on the LCD use the ADC to determine which, if any, are being pressed. This is different than how buttons in previous labs were interfaced to the Arduino. Rather than having all the LCD buttons produce an individual digital signal, all five of them (UP, DOWN, RIGHT, LEFT and SELECT) are instead interfaced through a multistage voltage divider creating a single analog signal that can be measured by the ADC.
Depending on which button is pressed, or none, this circuit produces a different voltage between 0 and 5 volts, and this signal is attached to the ADC Channel 0 input to the Arduino. By using the ADC to convert this voltage to a number it’s easy to determine which if any of the five buttons is pressed.
In the following task, we will find out what 8-bit number is returned by the
adc_sample
function for each of the five buttons that can be pressed. These
numbers can then be used in your program to determine which button was pressed.
Task 2: Determine the LCD buttons values
When a button is pressed the analog voltage on ADC channel 0 changes to a unique value that identifies the button that was pressed. However in order for your program to determine which button was pressed you have to know what the ADC output is for each button. The easiest way to do that is to write code to loop continuously reading the ADC result and writing the value to the LCD.
Use the ADC routines you just wrote to give the program the ability to use the
ADC to read the button’s analog signal. In the top part of the program, add a
call to adc_init
to initialize the ADC module and make it ready to take
samples. In the lab7.c there should be a place indicated for this function
call to be added.
Define an “unsigned char” variable to hold the ADC conversion result, and also allocate a “char” buffer of at least 4 characters to use for displaying the value.
In lab7.c look for a while(1) loop that is present in the program just for doing Tasks 2 and 3. This loop is will be used to sample the ADC and print the conversion value on the LCD screen. Add the following code inside this loop.
-
Call
adc_sample
with an argument of zero to read the value of the ADC channel 0 and store the returned value in the variable you define above. -
Use the snprintf routine to format the ADC result into a string of numerical characters that can be displayed. (e.g. something like
snprintf(buf, 5, "%4d", adc_result);
-
Use your lcd_moveto function to move the cursor to the first character position on the first row.
-
Use your lcd_stringout function to print the string on the display
Once the program is running, try pressing each button and see what value is displayed. You can record the values shown for each of the buttons on the Lab 7 grading sheet.
You should note that the ADC conversion results may not always be exactly the same each time a button is pressed. Due to electrical noise and thermal effects it may return, for example, 124 one time, 125 another and 126 yet another time. Your code must take this into account when working with the ADC results.
Checkpoint: To receive the checkpoint credit, show one of the teaching staff the values on the LCD for each of the buttons pressed.
The Variable Resistor
For this lab assignment one of the inputs to the ADC of the Arduino will come from a variable resistor or potentiometer (or “pot” for short) hooked up to be a voltage divider. A potentiometer as shown below is a three-terminal device that has a fixed resistance between two of its wires and a third wire, sometimes called the “slider” that can be adjusted to give anywhere from zero to the full resistance between the slider wire and the other two. In short, the potentiometer operates as a voltage divider (two resistors in series) where the resistance in the numerator can be adjusted by dialing/spinning the “pot”. Turn it fully one direction and you will see 0V on pin 2. Turn it fully in the other direction and you will see VS (5V) on pin 2. At any position in between you should the voltage on pin 2 scale linearly.
In schematic diagram above of a potentiometer, the total resistance between the top and bottom terminals of the potentiometer is given by R. Internally R can be viewed as two resistors R1 and R-R1 that are in series so the resistance between the top and bottom terminals is fixed at R. Changing the position of the potentiometer’s control changes the resistance of R1. Since the value of R is fixed, as R1 increases, R-R1 must decrease by an equal amount, and similarly if R1 goes down R-R1 must go up.
When hooked up as a voltage divider as above, we can see that V is given by
As the position of the slider is adjusted the value of R1 can change from R (making V=VS) to zero (making V=0). By adjusting the slider we can get any voltage from 0 to V=VS to appear on the slider terminal that will be connected to the ADC input.
Get the potentiometer from the parts provided in your kit and examine it. The resistance between the two fixed terminals (R in the above) is marked on the top next to the round control that is rotated. It should say “102” which is the resistance in the same format as used for marking fixed resistors: 1, 0, and two more zeros = 1000Ω.
Installing the Potentiometer on the Breadboard
The first part of the circuit uses a potentiomter to adjust the duty cycle of a PWM signal generated by one of the Arduino TIMER modules. In this section we will install the potentiometer on the breadboard, connect it to the ADC input of the Arduino and confirm that the analog voltage from the potentiometer is being converted to a digital value. The schematic diagram for this part is shown below.
On the potentiometer, the two pins that are on a line parallel to the sides of the device correspond to the top and bottom pins as shown above. The slider pin is the one offset from the other two.
-
Insert the potentiometer into your breadboard so that all three pins are in different 5-hole connection strips as shown below.
-
Using one of the jumpers make a connection from the slider pin’s 5-hole strip to the Arduino A1 (ADC channel 1) input.
-
Using a wire connect one of the other two pins (either one) to the 5V bus on the breadboard.
-
Use a jumper to connect the 5V on the Arduino to the 5V bus on the breadboard.
-
Do the same for ground. Connect a wire from the remaining pin of the potentiometer to the ground bus, and use a jumper to connect ground from the Arduino to the ground bus.
Task 3: Confirm Operation of the Potentiometer
To confirm that you have the potentiometer connected properly, use the multimeter to check that the voltage on the slider pin can be changed by rotating the control.
-
Turn on the DMM and set it for measuring DC volts.
-
Hook the black lead of the DMM to the ground bus of the breadboard, and hook the red lead to the slider pin which is connected to A1.
-
Plug in your Arduino’s USB cable into your computer so there is power to the Ardunio and the Arduino is providing 5V power to the breadboard.
-
Set the DMM to measure DC volts and try rotating the pot with your screwdriver. The indicated voltage should change between +5V and ground as you rotate the pot.
If the voltage is +5V when rotated fully clockwise and 0 when fully counter clockwise, but you want it to be the other way (0 for CW, +5V for CCW), then all you have to do is swap the 5V and ground wires coming from the power and ground buses to the potentiometer and it will now go the other way.
Now that the potentiometer is producing the variable voltage, check to see that
the voltage can be converted by the ADC into a number between 0 and 255. Your
lab7.c
file should still have the code to read the ADC value on channel 0 for
the buttons. Change the channel number in the adc_sample
function call to
use channel 1 where the potentiometer is connected. Flash the new code to your
Arduino, and then try rotating the potentiometer back and forth.
The program should be displaying the numerical values of its conversions of the voltage on channel 1, and these should be close to 0 at one end of the rotation and close to 255 at the other. If you are not seeing this, you will need to fix the problem before using the potentiometer and ADC code in the next task.
NOTE: The while(1) loop that was used for Tasks 2 and 3 can now be deleted or commented out in your lab7.c file. The while(1) loop that is below that section of the file is the code that will do the rest of the lab assignment.
Using Timers to Produce Pulse Width Modulation Signals
In previous labs, the Timer/Counter modules were used to implements time delays so that certain tasks would happen at a known rate, such as the 0.1 second counting rate of a stopwatch. When using the Timer/Counter modules in this way, they only interact with your program, usually by interrupts. The Timer/Counters can also be configured to produce signals that appear on one or more of the I/O pins and can be used to control external devices.
Each of the three Timer/Counter modules on the Arduino can produce output signals on two separate signals lines. Connections can be made to these signals just like with any other output from the microcontroller. The diagram below shows which I/O pins are used by the Timer/Counter modules when they have been configured to produce an output signal.
When working with the Timer/Counter modules the output signals are always
referred to by the names shown above inside the boxes such as “OC0A
” or
“OC2B
”. It’s important to be aware that that these signals share the
connections to the microcontroller with the digital I/O lines we have been
working with as shown above. For example OC2A
is the same connection on the
Arduino as the Port B, bit 3. At any one time, only one module, either the
Timer/Counter or the digital I/O can be using the line. For example, if the
Timer 1 is outputting a signal on the OC1A
pin, then Port B, Bit 1 can not be
used as a digital I/O by putting bits in the PORTB register.
One common use of the Timer/Counter modules in the Atmel microcontroller used on the Arduino Uno is to generate Pulse Width Modulation (PWM) signals. Please watch the video and review the slides that are linked on the Assignments page to learn more about pulse width modulation.
To generate PWM signals the timer has to perform two tasks. First it has to generate pulses with a constant period. The period of the pulses is the time from the 0→1 transition of one pulse until the next 0→1 transition. To generate pulses at a fixed period the timer has to count the clock signal until the pulse period has been reached and then reset back to zero and start over.
The second task for the timer is to control the width of the pulse which is the amount of time during the pulse period that the output is in the high or logic 1 state. The pulse output goes to the one state at the beginning of the pulse period, then at some point during the pulse period the timer must terminate the pulse by setting the pulse output to zero for the rest of the pulse period.
When working with PWM signals, your program configures the timer to generate the signals with the desired pulse period at the start of the program and never changes this setting since the pulse period is fixed value. As the program operates, it modifies or “modulates” the pulse width as needed to generate the proper signal.
To control the servo motor we will be using Timer/Counter2, one of the 8-bit Timer/Counters. In the PWM mode we will be using, each period of the pulse is limited to the length of time it takes the timer to count from 0 to its maximum value of 255. The timer simply counts up to the maximum count value and then starts over. The relationship between count value, prescalar and time is given by
We will use the prescaler to divide the 16Mhz clock by 1024, so the period of our pulses will be
or
For this task we will be using Timer2 in what is called “Fast PWM” mode and
this is illustrated below. At the start of the pulse period (count = 0) the
OC2A
output signal, which appears on Arduino port D11 (Port B, pin 3), will
go to the high state to start the pulse. Register “OCR2A
” is used to store a
value that determines the pulse width by telling the timer when to terminate
the pulse. During each pulse period, the timer counts up incrementing the count
value, and when it reaches the value in OCR2A
the output signal goes back to
zero while the timer continues to count up to 255. The counter then resets
back to zero and the process is repeated for the next pulse.
Your program will be have the job of changing the value in OCR2A
in order to
change the width of the pulse.
Task 4: Determine the PWM values for controlling TIMER2
Servos are designed to operate using a PWM signal with 20ms period, and the pulse width should only vary between 0.75ms to 2.25ms. In the task above we found that the longest pulse period we could get from the 8-bit TIMER2 was 16.4ms. While this pulse period isn’t optimal, it will work fine for controlling these servo motors.
Before starting to write the code to control the position of the servo motor,
first figure out what range of values will be placed in the OCR2A
register to
generate the PWM signal as specified above. You can use the equation below
with the prescalar set to 1024 to find the count value to generate the 0.75ms
wide pulse, and then repeat the steps to find the count value to generate the
2.25ms wide pulse.
These are the counts for the minimum and maximum pulse widths you will be
using, and the potentiometer will be used to change the OCR2A
value between
these limits. Turning the potentiometer one way will cause the program to
increase the value in OCR2A
making the pulse wider, turning it the other will
decrease the value to make the pulse narrower.
Now that you have the range of OCR2A values that can be used, you can add code to read the potentiometer with the ADC and convert the result to the value that goes in OCR2A. The number that goes in the OCR2A register has a linear relationship with the potentiometer value. Linear equations are based on two points, and you have the two points:
-
If the potentiometer value is 0, the motor should rotate fully to the left which will happen if the PWM signal is at it’s maximum width.
-
If the potentiometer value is 255, the motor should rotate fully to the right which will happen if the PWM signal is at it’s minimum width.
Therefore the two points for the linear equation are (0, max OCR2A) and (255, min OCR2A).
Using these two points, determine a linear equation for mapping the ADC values to the OCR2A values. Once you have figured out the equation, write code to implement it, but remember to only use fixed point variables and operations. Do not use floating point operations or variables.
Task 5: Configure and test the PWM output
The PWM signal to control the servo will be generated using TIMER2 in “Fast PWM” mode. Add the following function to your program to initialize Timer/Counter2 to operate as described above for PWM generation. In the code we want to initialize the PWM signal to a pulse with that is halfway between the minimum and maximum width used by the servo motor. This will cause the servo to rotate to its midpoint when the program starts. Determine an intial value of OCR2A that is midway between the two values you determined above and use that value below in the code where is says “INITIAL_PWM” on the line that sets the OCR2A value.
void timer2_init(void)
{
TCCR2A |= (0b11 << WGM20); // Fast PWM mode, modulus = 256
TCCR2A |= (0b10 << COM2A0); // Turn D11 on at 0x00 and off at OCR2A
OCR2A = INITIAL_PWM; // Initial pulse width (calculate this)
TCCR2B |= (0b111 << CS20); // Prescaler = 1024 for 16ms period
}
IMPORTANT: Make sure to have a call to this function somewhere at the start of your program. Also add code to set the DDR bit for Port B, bit 3 to a one to make it the PWM pulse output.
In the main loop of your progam add code to do the following.
-
Use
adc_sample
to convert the potentiometer value on channel 1 to a value between 0 and 255. -
Use the linear equation you determined above to write code to convert the ADC value to the PWM value and store this number in OCR2A.
Once you have this code running, connect an oscilloscope to the PB3 (D11) output and observe the PWM signal. You should be able to see the pulse width change as you rotate the potentiometer. Check to make sure that
- The pulse period is 16.4ms.
- The minimum pulse width is 0.75ms
- The maximum pulse width is 2.25ms
- The pulse width changes smoothly and doesn’t jump around as the potentiomer is turned.
Task 6: Button inputs to control the motor
Your program to control the servo motor will need to run in three modes. The “VARIABLE” mode was described above where the value in OCR2A is controlled by the potentiometer setting. However if either the LEFT or RIGHT button is pressed, it switches to the “LEFT” or “RIGHT” mode. In these modes, if the LEFT button was pressed, the motor should immediately rotate to the full counter-clockwise position and remain there regardless of the input from the potentiometer. If the RIGHT button was pressed it should rotate to the full clockwise position. The program should stay in one of these three modes until the SELECT, LEFT or RIGHT is pressed to switch it to a different mode.
Add code to the main loop of the program as follows.
-
Check the status of the buttons to see if the state has to be changed. If the SELECT button was pressed set the state to “VARIABLE”. If the RIGHT button was pressed set the state to “RIGHT, and if the LEFT button was pressed set the state to “LEFT”.
-
Determine the value to be stored in the OCR2A register. If the state is “VARIABLE”, sample ADC channel 1, convert the ADC value to the OCR2A value as described above and store in OCR2A. If the state is “RIGHT” or “LEFT” load OCR2A with the appropriate value to move full clockwise or counter-clockwise.
-
Display on the LCD the value that your program has stored in the OCR2A register. In “VARIABLE” mode, this should change as you rotate the potentiometer. In “LEFT” or “RIGHT” mode it should either the minimum or maxium value that you calculated. Note: You should only update the value on the LCD if it has changed. Do not do unnecassary updates of the LCD each time around the loop.
Connecting the Servo Motor
Get the servo motor from the bag of Lab 7 components that you acquired at the start of this lab. The cable to the motor has a three conductor connector with three wires going into it, red for power, black for ground and yellow for the PWM signal.
When connecting the motor to your Arduino, first disconnect the power to your Arduino before connecting the motor. Using a piece of breadboard hookup wire, make a connection from the +5V on the Arduino to the red wire by inserting the end of the wire into the connector socket for the red wire as shown in below. Do the same to connect the Arduino ground to the black wire, and from the Port B, bit 3 (D11) output pin to the yellow wire.
Once you reconnect the power to your Arduino the motor should move to the initial position as set by your PWM signal.
Try rotating the potentiometer and see if the motor responds correctly. Check to see that you can move the motor through an arc of about 150 degrees.
Results
When your program is running you should be able to confirm the following
- PWM signal period is correct on scope
- PWM minimum and maximum width is correct on scope
- PWM signal changes between minimum and maximum width
- Motor turns smoothly in both directions
- Pressing the LEFT or RIGHT button moves the motor fully counter-clockwise or fully clockwise.
- Pressing the SELECT button returns the motor to being controlled by the potentiometer position.
Once you have the assignment working demonstrate it to one of the instructors. The answers to the review questions below should be edited into the Lab7_Answers.txt file. The Lab7_Answers.txt file and all source code (lab7.c, adc.c, acd.h, lcd.c, lcd.h, and Makefile) must be uploaded to the Vocareum web site by the due date. See the Assignments page of the class web site for a link for uploading.
Please make sure to save all the parts used in this lab, including the servo motor, in your project box. These will be needed for labs throughout the rest of the semester. We suggest also saving all the pieces of wire you cut and stripped since those will be useful in later labs.
Review Questions
Answer the following questions and submit them in a separate text file (“Lab7_Answers.txt”) along with your source code.
-
Review the conceptual operation of an ADC and use the given parameters to find the desired value.
- Your 8-bit ADC has Lo and Hi reference voltages of 1V and 4V, what digital number would result from sampling a 2.0V?
- Your 5-bit ADC has Lo and Hi reference voltages of 0V and 5V, what input voltage would yield a digital number of 12?
- Your 6-bit ADC has a Lo reference voltages of 2V. A voltage is sampled at 2.4V and the digital result is 26. What was the Hi reference voltage used by the ADC?
-
Tammy Trojan has determined that the servo motor can be controlled by TIMER2 even though the PWM period is 16.4ms rather than the preferred 20ms. Billy Bruin also wants to control a similar servo motor but feels it must use a PWM signal with a 20ms period so he will have to use the 16-bit TIMER1 to produce the signal. Note: When TIMER1 is used for PWM, the OCR1A register determines the pulse period, and OCR1B register determines the pulse width. What values will Billy have to use for the TIMER1 prescalar, the OCR1A value to give a 20ms pulse period, and the values for OCR1B to give the minimum pulse width of 0.75ms and the maxumum pulse width of 2.25ms?