Spring 2026 Project
Speedometer
Introduction
This semester’s class project is to build a device that measures the speed that an object is moving. It uses an ultrasonic rangefinder to make two distance measurements to the object and tracks the time between the measurements. From these it determines the objects speed which is displayed on an LCD.
Note: This is a non-trivial project that requires you to not only use the concepts and skills you’ve learned from past labs but to extend that knowledge and use the higher-level understanding of how embedded systems and microcontrollers work to include use of new components and modules.
All students are expected to do the work on the project individually. Sharing code with another student is an Academic Integrity volation. No use of AI tools is allowed.
Getting a full score is not a guarantee. It will require time and intelligent work (not just guess, but verifying on paper and using some of the approaches outlined below). But it is definitely doable. We have broken the project into 2 checkpoints (first due 4/22 or 4/24, and the second due 4/29 or 5/1) to help limit the scope of your work to more manageable deliverables. Also, note that while we will still provide help in terms of concept and approach, we will leave it to you to do most of the debugging. When you do need help debugging you should have already used the tools at your disposal: the DMM or scope to verify basic signals are as you expect, LCD print statements to see where your code is executing, whether you are getting into an ISR at all, or what the value of various readings/variables is. Test your code little by little (this is the reason we do checkpoints) and do not implement EVERYTHING and only then try to compile. In addition, when something doesn’t work, back out any changes you’ve made (comment certain portions, etc.) to get to a point where you can indicate what does work and then slowly reintroduce the new changes to see where an issue might lie. If you have not done your own debugging work and just ask for help, we will direct you to go back and perform some of these tasks.
To start, please read the overall project a few times before you even consider thinking about the many details. Get the big picture, hear the terms we use, look for the requirements and/or limitations we specify. Only then consider starting on your project.
Project Grading
The following policies are in effect for the grading of the project.
-
Grading of the project will only be done during your registered lab session during weeks 1, 2 or 3 of the project. You can get help during the open lab hours but no grading will be done at these times.
-
You must submit the current code on Vocareum before demonstrating the project and be able to concisely explain how your code works and how you developed it. Suspected use of AI will be flagged and no credit given.
-
Only full demos of all the items to be graded can be done (no partial checkpoints).
-
Checkpoint 1 is due after 2 weeks. It can be demoed in week 3 for a 50% maximum credit.
-
Checkpoint 2 is due after 3 weeks
-
In weeks 2 and 3, we will allow two demos per person and call names in random order.
-
No extensions for the checkpoint due dates will be granted. We can not extend the project due dates past the end of the semester.
For a copy of the project grading sheet, click here
Speedometer Overview
In its simplest form the speedometer makes two measurments of the distance to an object in front of it, and keeps track of the time between the two measurements. Based on the difference in the ranges, and the time between them, it determines the speed that the object is moving.
The distance of the two ranges in centimeters is shown on the LCD display along with the time between them in tenths of a second. The calculated speed of the object is shown on the LCD in cm/sec. A block diagram of our speedometer is shown below and it will have the following features.
-
An ultrasonic rangefinder for measuring distances up to 400cm.
-
Buttons on the LCD to initiate the first and second range measurement.
-
An LCD display for showing the measured ranges, the time span and the object’s speed. The display is also used for setting a speed threshold.
-
A control knob for setting a speed threshold.
-
A multicolor LED for showing how the measured speed compares to the speed threshold.
-
A indicator dial showing how much time is left to make the second range measurement.
-
A serial link to another speedometer. The local speed it transmitted to the other device, and the other device’s speed measurement is received from it.
-
A buzzer for playing an alarm tones depending on how the remote speed compares to the threshold on the local device.

Operation of the Speedometer
The following is a description of the operation of the speedometer.
-
When the “Start” button is pressed a signal is sent to the ultrasonic sensor to initiate a range measurement by sending out an ultrasonic pulse.
-
As the sensor makes a range measurement, it sends to the Arduino a 0→1→0 pulse that varies in width proportional to the range to the object that reflected the ultrasonic pulse. The Arduino monitors the received pulse watching for the initial rising edge and the falling edge at the end of the pulse to determine the length of the pulse. Once this is known it performs the calculations to find the corresponding distance.
-
When the first range measuring event has completed, the unit displays the distance in centimeters with one decimal place (e.g. “27.6”) on the LCD display in the upper left.
-
Once the first range measurement has been made, a timer is started that tracks the time between the first range measurement and second one. The elapsed time in seconds with one decimal place (e.g. 7.5) is continuously displayed on the LCD in the upper right. In addition, a dial indicator driven by the servo motor rotates to show how much time is left to make the second measurement. The second range measurement must be made within 10 seconds of the first one.
-
When the “Stop” button is pressed, the second range measurement is made in the same manner as the first one described above. The timer that is tracking the elapsed time is stopped at this point. The second range measurement can be displayed in the middle of the top line.
-
If both range measurements were successful, the Arduino uses the two ranges and the elapsed time to calculate the speed the object was moving. The speed is displayed on the LCD in the lower left in cm/sec with one decimal place (e.g. 12.5cm/s). If the object was moving away from the speedometer (second range greater than the first) the speed is shown as a positive number. If it was moving towards the speedometer the speed is shown as a negative number.
-
If the user turns the rotary encoder knob, the speed threshold number is changed up or down and displayed on the LCD in the middle of the lower row. This number is an integer value and can change from 1 to 99 cm/sec.
-
When the speed threshold is changed, the new value is stored in the Arduino’s EEPROM non-volatile memory so the value is retained even if the power is turned off. Whenever the speedometer is turned on or restarted, the threshold value is read from the EEPROM memory making it unnecessary for the user to set the speed threshold each time the device is turned on.
-
The multicolor LED is used to show how the speed measurement compares to speed threshold that was set by the rotary encoder. If the speed measurement made the speedometer is greater than the speed threshold, the red LED comes on. If the speed is less than or equal to the threshold, the green LED comes on. If no speed measurement has yet been made, or the last one failed the blue LED is lit. Note that the action of the LEDs are based on the magnitude of the measured speed, not the direction the object is moving. For example if the threshold is set to 20, and the measured speed is -25cm/s (moving towards the speedometer), this would cause the red LED to come on since 25 is greater than 20.
-
Each time a successful speed measurement has been made, the speed value is transimitted over a serial link to remote device. Speed measurements from a remote device are received by the local device and the remote speed value is displayed in the lower right of the LCD.
-
The buzzer is used to indicate how a received speed from a remote device compares to the speed threshold set on the local device. If the remote speed is faster, the buzzer plays a three-note sequence of tones increasing in pitch. If the remote speed is lower, it plays a three-note sequence of tones decreasing in pitch. The same policy applies with the buzzer as described above with the LED, the action is based on the magnitude of the speed, not the direction.
To see a short video demonstrating the operation of the speedometer, click here.
Getting Started
We suggest starting with a copy of your code from Lab 7 since this project uses
the rotary encoder and PWM signals. Download from the class web site the file
project.zip that contains the Project_Answers.txt file with information about
what needs to be demonstrated to show the project is working.
Most of the components used in this project have been used in previous labs, and your C code from the other labs can be reused if that helps. The project involves the use of two new elements:
- The ultrasonic range sensor
- The non-volatile memory (EEPROM) on the Arduino
Both of these are described below.
Which Port Bits to Use
Since the operation of the project may have to be confirmed by running the code on one of the instructor’s project boards, it is very important (and required) that all students use the port connections shown below to connect their rangefinder, buttons, rotary encoder, LED, etc. If other port bits are used the project will not operate when the program is run on another board.
| Port Bit | Function |
| PORTB, bit 3 (PB3) | PWM signal to servo motor |
| PORTB, bit 4 (PB4) | 74LS125 Tri-State buffer enable |
| PORTB, bit 5 (PB5) | Buzzer |
| PORTC, bit 1 (PC1) | RGB LED red signal |
| PORTC, bit 2 (PC2) | RGB LED green signal |
| PORTC, bit 3 (PC3) | RGB LED blue signal |
| PORTC, bit 4 (PC4) | Rotary encoder |
| PORTC, bit 5 (PC5) | Rotary encoder |
| PORTD, bit 0 (PD0) | Serial receive signal |
| PORTD, bit 1 (PD1) | Serial transmit signal |
| PORTD, bit 2 (PD2) | Rangefinder output (rangefinder to Arduino) |
| PORTD, bit 3 (PD3) | Rangefinder trigger (Arduino to rangefinder) |
Hardware Construction Tips
The range sensor, LED, rotary encoder, buzzer, ICs, and the various resistors should all be mounted on your breadboard. It’s strongly recommended that you try to wire them in a clean and orderly fashion. Don’t use long wires that loop all over the place to make connections. You will have about 13 wires going from the Arduino to the breadboard so don’t make matters worse by having a rat’s nest of other wires running around on the breadboard. Feel free to cut off the leads of the LEDs and resistors so they fit down close to the board when installed.
Make use of the bus strips along each side of the breadboard for your ground and +5V connections. Use the red for power, blue for ground. There should only be one wire for ground and one for +5V coming from your Arduino to the breadboard. All the components that need ground and/or +5V connections on the breadboard should make connections to the bus strips, not wired back to the Arduino.
Checkpoint 1
The parts of the project described below are part of Checkpoint 1 and are due 4/22 or 4/24.
The “Start” and “Stop” Buttons
The “Start” and “Stop” buttons are use to initiate the two range measurements. The buttons on the LCD will be use for these functions. The LEFT button on the LCD will be used for “Start”, and the RIGHT button will be used for “Stop”.
The ADC function will have to be used to sample the ADC value on channel 0 to see if a button on the LCD has been pressed. When one of these buttons is pressed the ultrasonic range sensor takes a new range measurement and the results are displayed on the LCD.
Ultrasonic Range Sensor
The range sensor is a single module that can be mounted on your breadboard. It requires +5V power and ground, and two digital signals. One signal is an output from the Arduino and is used to initiate a range measurement. The other signal is an input to the Arduino and is the pulse generated by the sensor to indicate the range.
A copy of the manufacturer’s datasheet for the sensor can be viewed with this link.

The four pins at the bottom of the sensor should be installed in four separate blocks of holes on the breadboard. The purpose of each pin is shown on the front of the sensor: VCC, Trig, Echo, GND. The trigger (Arduino → sensor) and echo (sensor → Arduino) signals are digital signals so those can be interfaced to the digital I/O port bits of the Arduino. Read the datasheet for information on the format of these signals.
Determining the Sensor Pulse Width
Note: Because the Arduino does not have
hardware support for floating point operations, you MAY NOT use floating point
types NOR constants (e.g. 0.1) for any calculations you perform. Doing
so will result in a deduction in visual grading.
The pulse that comes back from the sensor can be connected to a I/O port bit that has the Pin Change Interrupt enabled. The ISR will be invoked once on the rising edge of the pulse (start of the measurement) and again on the falling edge (end of the measurement).
The ISR can control one of the timers to produce a count of how many counter clock cycles it took to get from the rising edge to the falling edge. For determining the width of the measurement pulse from the sensor, it is recommended that you use the 16-bit TIMER1 to count at a known frequency.
When the ISR is invoked by the start of the pulse, TIMER1 can be enabled to measure the pulse width. When the ISR is invoked again by the end of the pulse, the timer can be stopped and the value in the count registered can be examined.
pulse_count = TCNT1;
Since the rate at which the timer was counting is known, the count value can be used to determine how long the pulse was in the high state. Then, using information given in the manufacturer’s datasheet you can perform calculations to convert the time to distance. It’s recommended that your code convert the time to distance in millimeters (mm). This will give better precision when displaying the distance in cm/sec with one decimal place.
In order get the most accurate distance values you want to have the counter operating at as fast a frequency as possible. From the manufacturer’s datasheet you can determine what could be the longest time possible between the start and end of the pulse. You should select a counter prescalar that allows the count to go as high as possible before that time is reached but still can be represented by an unsigned 16-bit number.
Note: When making a range measurement you are NOT ** using this timer as
you did in the stopwatch lab where you generated an interrupt at a fixed
interval and then update a software variable count. Instead, you will start the
timer (hardware counter) and let it count freely until you get back the echo
and then stop the timer and read it’s current count from the TCNT1
register. **The timer intro video in the Stopwatch lab discusses the two ways
to use the timer: to generate a periodic interrupt (as we did in the stopwatch
lab) or to count the time between events (THIS project). Please ensure you
understand the difference and implement the timing correctly. You will lose
points if you try to use the timer in the same style as the stopwatch lab.
Time-out Function on Pulse Width Measurement
We want our speedometer to be robust and not subject to problems that may be caused by electrical noise spikes on the signal lines. For example, a noise spike on the sensor output may be incorrectly taken to be the start of a measurement pulse, but the Arduino may then sit there forever waiting for the falling edge of the pulse. To prevent this from happening we want to install a “watch dog timer” function that will prevent the speedometer from getting locked up.
The manufacturer’s datasheet tells what the maximum range is for the sensor. From that we can calculate what the highest count value is that we would ever see from our TIMER1 if it’s operating properly. If the TIMER1 count ever goes above this value, we can assume that something has gone wrong and we want to abandon this measurement. You should set up your TIMER1 so that it generates an interrupt if this maximum count value is reached. The ISR can then stop the timer and reset whatever is needed to discard this measurement and wait for a new measurement to be initiated.
Measuring the Elapsed Time
Once the first range measurement has been made the speedometer has to keep track of how much time elapses between that measurement and the following one. To do this the project will use code to implement a stopwatch very much like the stopwatch that was done in Lab 5 using TIMER1 to count tenths of seconds. It’s important to note that in this project TIMER1 is being used for more than one task:
- Determining the width of the echo pulse from the rangefinder for the first range measurement.
- Timing how long between the first range measurement and the second one.
- Determining the width of the echo pulse from the rangefinder for the second range measurement.
The way TIMER1 is used differs between the tasks. In the first and third tasks, TIMER1 is counting from the start of the range measurement (echo signal goes 0→1) until it completes (echo signal goes 1→0) and then the count value is used to determine the distance. In the second task, the stopwatch, TIMER1 is generating interrupts every 0.1 second and these are counted to determine how much time has gone by between the two range measurements.
Once the program has completed the first range measurement, it should reconfigure TIMER1 to be a stopwatch just like in Lab 5 generating interrupts every 0.1 seconds and increment a count of how many 0.1 sec time periods have gone by.
The stopwatch action of TIMER1 should continue until the “STOP” button is pressed to initiate the second range measurment. At that time the TIMER1 can be stopped and elapsed time needs to be saved since it will be used later. The program now does the same steps as before to measure the width of the echo pulse by using TMER1. The necessary steps for configuring it can be done in the pin change interrrupt ISR when the start of the rangefinder’s echo signal is detected.
We want to gracefully handle the case where the “START” button makes the first measurement but the “STOP” button is never pressed leaving the stopwatch running. If the time on the stopwatch reaches 10.0 seconds, the program should terminate the timing and return to the state where it is waiting for the “START” button to be pressed to make a new speed measurment. It is not necessary to put any error message on the screen but when you get to the point later in the project where you are installing the RGB LED, this is one of the situations where the blue segment of the RGB LED should be illuminated to show the measurement failed.
Calculating the Object’s Speed
After the second of the two range measurement, your program should have a distance value in millimeters for the two ranges. From the stopwatch that was running between the two range measurements, the program should have the elapsed time in tenths of seconds. The speed in cm/sec can easily be calcuated as the difference between the two range measurement divided by the elapsed time.
One problem that must be addressed is to prevent a loss of precision since the Arduino does the calculation using fixed point integer arithmetic and all fractional values are lost. For example, suppose the first range measurement was 14.2cm (142mm) and the second was 19.7cm (197mm) with an elapsed time of 1.5 seconds (15 tenths of seconds). If we use a calculator to find the speed from these numbers we get
speed = (19.7 - 14.2) / 1.5 = 3.666 cm/sec
but if we do the calculation using the millimeter and tenths of seconds values in fixed point arithmetic we get
speed = (197 - 142) / 15 = 3 cm/sec
To prevent the loss of the fractional part of the result, we need to instead calculate the speed in mm/sec, and then display the integer and fractional part of the result separately
speed(mm/sec) = ( range2(mm) - range1(mm) ) / elapsed time (seconds)
= ( range2(mm) - range1(mm) ) / (elapsed time (tenths of seconds) / 10)
= (( range2(mm) - range1(mm) ) * 10) / elapsed time (tenths of seconds)
Using the above example,
speed = ((197 - 142) * 10) / 15 = 36 mm/sec
which we can separate with division and modulus operators into “3” and “6” and then print out as “3.6” on the LCD.
The integer portion of this result should be retained since it will be used in the program to see how it compares to the speed threshold set by the rotary encoder for determining which RGB LED segments to light up.
Adjusting the Speed Threshold Settings
The rotary encoder is used to set the speed threshold from 1 to 99 cm/sec as an integer value. The threshold will be used to determine which color of the RGB LED to illuminate based on the measured speed and also whether or not to sound the buzzer alarm.
As the user rotates the encoder knob the speed threshold is shown on the LCD display as it is changed. The display of the threshold as it changes should not affect the other items shown on the LCD. In other words, do not switch to a different screen just to shows the threshold value as it changes.
Your program must ensure that the threshold is limited to never going above the maximum value of 99 cm/sec or lower than the minimum value of 1 cm/sec. Whenever a new threshold value is set, it is stored in the non-volatile EEPROM (see below).
Speed Comparison LEDs.
The project incorporates a multicolor (red, green, blue) LED that is used to show the comparison between the most recent speed measurement and the speed threshold setting done with the rotary encoder. If the speed measured is greater than the speed threshold, the red LED is lit. If the speed is less than or equal to the threshold the green LED is lit. The speeds should be compared to an accuracy of 1 cm/sec.
If no speed measurement has yet been made, or if the measurement of the speed failed for some reason, such as being beyond the 400cm limit, the red and green LEDs should go off and the blue LED should come on. The blue LED should stay on until there is a valid speed measurement.
This is the same common anode RGB LED that was used in Lab 8. The three LEDs inside it have separate cathode (lower voltage) leads, but the their anode leads, the one that goes to higher voltage, are connected together so it is a referred to as a “common anode” device. On the package, the common anode lead can be identified since it is longer than the other three. When wiring up an RGB LED, all three elements need to have separate current limiting resistors as shown in the diagram below. We recommend using the same resistor values that were used in Lab 8: 240Ω for red and blue, and 1.5kΩ for green.

The speedometer uses three digital I/O pins to control the RGB LED, one for each color. Remember that to turn on an LED color the signal to it must be a logical 0 while a logical 1 will turn it off.
The project steps above are Checkpoint 1. For full credit, demonstrate these to a CP by 4/22 or 4/24.
To get checked off you must first upload and submit the code you have written to this point on Vocareum. The CP/TA who does your Checkpoint will view the code submission on Vocareum and you will be asked to explain how your code works in addition to demonstrating that it works on your project board.
You can request to have the checkpoint graded a maximum of two times. The grading will be of all items in the checkpoint. No partial checkpoints will be done.
Checkpoint 2
The parts of the project described below are part of Checkpoint 2 and are due 4/29 or 5/1.
Non-volatile EEPROM Memory
The ATmega328P microcontroller contains 1024 bytes of memory that retains its data when the power is removed from the chip. It’s similar to the FLASH memory where your program is stored but intended to be written to and read from your program rather than just during the “make flash” operation. In this project we will be using the EEPROM to store the speed threshold value. In this way when the device is turned on it will have the same setting as it had previously.
The avr-gcc software includes several routines that you can use for accessing the EEPROM. To use these functions, your program must have this “include” statement at the beginning of the file that uses the routines.
#include <avr/eeprom.h>
The two routines that you should use for your program are described below.
-
eeprom_read_byte- This function reads one byte from the EEPROM at the address specified and returns the 8-bit value. It takes one argument, the the EEPROM address (0-1023) to read from. For example to read a byte from EEPROM address 100:int8_t x; x = eeprom_read_byte((void *) 100); -
eeprom_update_byte- This function writes one byte to the EEPROM at the address specified. It takes two arguments, the address to write to and the 8-bit value of the byte to be stored there. For example to write the byte 0xf4 to address 200 in the EEPROM:eeprom_update_byte((void *) 200, 0xf4);
Your code should use the above routines to store the speed threshold in the EEPROM whenever it has been changed. You can assume the threshold will always be in the range of 1 to 99 cm/sec. The number can be stored in an 8-bit “int8_t” variable and only requires writing a single “int8_t” variable to the EEPROM. You can choose any addresses in the EEPROM address range (0 to 1022) to store the value. When your program starts up it should read the value from the EEPROM, but it must then test the value to see if it is a valid threshold value. If the EEPROM has never been programmed, it contains all 0xFF values. If you read the EEPROM data and the value is not in the range 1 to 99, then your program should ignore this number and revert to using a default threshold value that is defined in your source code.
Servo Motor Indicator Dial
Besides displaying on the LCD the elapsed time since the first range measurement, the time is also indicated on a round dial by using a servo motor to rotate an indicator to point to the number of second that have gone by since the the first measurement was made.
The servo is controlled by Timer/Counter2 since this is the only timer with an output signal that is not blocked by the LCD panel. Servos normally operate with PWM signals with a period of 20ms, but with Timer2 the longest period that can be achieved is 16.4ms and the servos will work properly with this period PWM signal.
The width of the PWM pulse must vary between 0.75ms and 2.25ms. Assuming Timer2 is using the largest prescalar so as to have the maximum period, use that to determine the values that must go in the OCR2A register for the minimum pulse width of 0.75ms, and the value for the maximum pulse width of 2.25ms. Note: The calculation of these numbers was done in review question 1 of Lab 7.
Once you have determined the two OCR2A values for the minimum and maximum pulse width, you can use those to develop an equation to map the time value to a servo position. The number that goes in the OCR2A register has a linear relationship with the time value. Linear equations are based on two points, and you have the two points:
-
To indicate an elapsed time of 0 seconds, the motor should rotate fully to the left which will happen if the PWM signal is at it’s maximum width.
-
To indicate an elapasd time 10 seconds, the motor should rotate fully to the right which will happen if the PWM signal is at it’s minimum width.
Note that time is being measured by TIMER1 by counting tenths of seconds since the first measurement was made. Therefore the two points for the linear equation are (0, max OCR2A) and (100, min OCR2A).
Using these two points, determine a linear equation for mapping the elapsed time 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. Make sure the calculations do not result in overflows of the fixed point variables.
Every time the elapsed time to be displayed changes, a new value for OCR2A should be calculated using the equation you developed above and stored in that register.
Reminder: Any use of Floating Point variables OR constants will result in large deductions in the visual grading
Installing the Dial
At the the instructor’s podium you can find sheets of small paper indicator dials that show the elapsed time from 0 to 10 seconds. These can be cut out and taped to the bottom of the servo. To align the servo motor’s pointer properly we suggest the following procedure. Temporarily change your program to show a fixed time of 5 seconds, the midpoint of the rotation, and have the servo point there. Pull the indicator arm off the servo and reinstall it so it is pointing parallel to the two long sides of the servo and pointing at the 5 second speed. After doing this it should correctly show the elapased times from 0 to 10 seconds.
Transferring Data Between Devices
Each time a successful speed measurement has been made the speed value in mm/sec is sent out the serial link to a remote device. The local device also receives speed values from a remote device and displays the speed on cm/sec on the LCD.
Serial Interface
The serial interface between speedometer units will use an RS-232 link to send the speed values between the units. The serial input and output of the Arduino uses voltages in the range of 0 to +5 volts. These are usually called “TTL compatible” signal levels since this range was standardized in the transistor-transistor logic (TTL) family of integrated circuits. Normally for RS-232 communications these voltages must be converted to positive and negative RS-232 voltages levels in order for it to be compatible with another RS-232 device. However for this project we will skip using the voltage converters and simply connect the TTL level signals between the project boards.
Locating the RX and TX signals
The Ardunio’s transmitted (TX) and received (RX) data appears at two places on the LCD shield as shown in the figure below. The black connector along the top of the LCD shield has two holes marked “TX” and “RX” that can be used by inserting wires into these holes as has been done in previous labs. Alternatively, you can attach the wire jumpers to the D1 pin for the TX signal, or the D0 pin for RX signal.

For the RX and TX signals, there is no advantage or difference in using one connection point over the other. The D1 pin is connected to “TX”, and D0 pin is connected to “RX” on the LCD board so you can use either one.
Tri-State Buffer to Eliminate Contention on RX Line
When using the the serial signals to communicate with another device there is an issue with programming the Arduino that needs to be resolved. If the received data coming from another Arduino (or the TX data being looped back) is connected directly to the RX input (Arduino port D0) it creates a conflict with a signal used to program the Arduino’s microcontroller. Both the RX data and the programming hardware try to put an active logic level on the D0 input and this can prevent the programming from succeeding. When this happens you will get error messages like these.
avrdude: stk500_recv(): programmer is not responding
avrdude: stk500_getsync() attempt 1 of 10: not in sync: resp=0x00
avrdude: stk500_recv(): programmer is not responding
avrdude: stk500_getsync() attempt 2 of 10: not in sync: resp=0x00
The solution for this is to use a tri-state gate to isolate the incoming data from the RX input until after the programming is finished. The gate you will using is a 74HCT125 that contains four non-inverting tri-state buffers (see below).
Each of the four buffers have one input, one output and an enable input that controls the buffer’s tri-state output. In the diagram below on the left, the enable inputs are the connections going to the little circles on the side of the gate. The circle indicates that it is an “active low enable”, meaning that the gate is enabled when the enable input is a logical 0. In that state, it simply copies the input logic level to the output (0 → 0, 1 → 1). However when the gate is disabled (enable input = 1) its output is in the tri-state or hi-Z state regardless of the input. In that condition, the gate output has no effect on any other signal also attached to the output.
The serial communications circuit needs to include the 74HCT125 tri-state buffer in the path of the received data as shown below. The received data from the other Arduino should go to one of the buffer inputs, and the output of the buffer should be connected to the RX port of the Arduino. Remember to also connect pin 14 of the 74HCT125 to +5V, and pin 7 to ground.

The tri-state buffer’s enable signal is connected to a digital I/O port bit. For this project, use I/O port bit PB4 (Port B, bit 4) to control the tri-state buffer. When the Arduino is being programmed all the I/O lines become inputs. With no signal coming from the Arduino, the pull-up resistor will cause the PB4 line go high and put a logic one on the tri-state buffer’s enable line. This disables the output (puts it in the hi-Z state) and the programming can take place. Once your program starts, all you have to do is make that I/O line an output and put a zero in the corresponding PORT bit. This will make the enable line a logical zero, that enables the buffer, and now the received data will pass through the buffer to the RX serial port.
Note that the tri-state buffer and pull-up resistor is necessary even if you have configured the Arduino and project board to use a loopback where the transmitted data from the TX port is connected to the RX port. The TX and RX being connected together will interfere with the programming if the tri-state buffer is not used. The TX data must be connected to the buffer input so the it can be isolated from the RX port until the programming is completed.
Format of the Serial Signals
For all the speedometer devices to be capable of communicating with others, use the following parameters for the USART0 module in the Arduino. Refer to the slides from the lecture on serial interfaces for information on how to configure the USART0 module for these settings.
- Baud rate = 9600
- Asynchronous operation
- Eight data bits
- No parity
- One stop bit
Serial Interface Protocol
The serial data link between two speedometers uses a simple protocol:
- Data sent using ASCII characters for easier debugging
- Allows the system to recover from errors such as a partially transmitted or garbled data packet
In many devices that use serial links these features are implemented using relatively complex data structures and interrupt service routines so the processor does not have to spend much time doing polling of the receiver and transmitter. We’ll do it in a simpler manner that should work fine for this application.
The protocol for communicating the speed values between two devices will consist of a string of up to six bytes in this order:
-
The start of the string is indicated by the ‘@’ character.
-
A optional negative sign ‘-‘
-
Up to four ASCII digits (‘0’ through ‘9’) representing the speed in mm/sec.
-
After all characters for the thresholds have been sent the end of the threshold data string is indicated by sending the ‘$’ character.
For example, to send the speed of 17.4cm/sec, the transmission
should consist of @174$.
Transmitting Data
When your software determines that the speed value need to be sent, it should call a routine that sends the characters for the speed to the remote unit. The serial link is much slower than the processor so the program has to poll the transmitter to see when it can put the next character to be sent in the transmitter’s data register for sending. The UCSR0A register contains a bit (UDRE0 - USART Data Register Empty) that tells when it’s ready to be given the next character to send. While this bit is a zero, the transmitter is busy sending a previous character and the program must wait for it to become a one. Once the UDRE0 bit becomes a one, the program can store the next character to be sent in the UDR0 register.
while ((UCSR0A & (1 << UDRE0)) == 0) { }
UDR0 = next_character;
While your program is waiting for all the characters to be transmitted it should still respond to interrupts from modules with interrupts enabled, but it does not have to reflect any changes on the display until after all the data has been sent and it’s back in the main program loop.
Receiving Data
Receiving the speed data from the remote unit is a bit more complicated since you have no idea when the remote unit will send the data. One simple way to implement this is to have your program check for a received character each time through the main loop. If one has been received, then call a function that waits for the rest of the characters and when complete displays the settings on the LCD. Unfortunately this method of receiving characters has one very bad design flaw in it. If for some reason the string is incomplete, maybe only the first half of the string was sent, the device will sit in the receiver subroutine forever waiting for the rest of the data and the ‘$’ that marks the end of the transmission.
A better solution, and one that should be implemented in your program, is to use interrupts for the received data. Receiver interrupts are enabled by setting the RXCIE0 bit to a one in the UCSR0B register. When a character is received the hardware executes the ISR with the vector name “USART_RX_vect”.
For reading the incoming speed value, each time a character is received, an interrupt is generated and the ISR determines what to do with the character. After all the characters have been received, the ISR sets a global variable to indicate that a complete remote speed value has been received and is available. When the main part of the program sees this variable has been set, it gets the value and displays it. By using the interrupts, the program is never stuck waiting for a character that might never come.
It is also important to consider all the possible errors that might occur in the transmission of the data, such as missing start (‘@’) character, missing or corrupted speed value characters, missing end (‘$’) character, etc. The software must make sure all of these situations are handled cleanly and don’t leave the device in an inoperable state.
To handle possible errors in the received data, the speedometer should operate on the principle that if an error of any kind is detected in the data being received, it discards the current remote data that it was in the midst of receiving and goes back to waiting for the next speed value to start coming in. It does not have to notify the main program that an error was encountered, or try to correct the error, or guess what the correct data was. Simply throw it away and go back to waiting for a new data packet.
To implement this, use the following variables to properly receive the data:
-
A char buffer of at least 6 elements in size for storing the data from the remote device as it comes in (a possible negative sign, 4 digits and a ‘\0’ byte at the end.)
-
A global variable to act as a data started flag that tells whether or not the start character (‘@’) has been received indicating data is to follow.
-
A variable that tells how many data characters have been received and been stored in the buffer so far. This also tells the ISR where in the buffer it should store the next character.
-
A global variable to act as a data valid flag to indicate that the ‘$’ has been received and the buffer contains a valid settings string. This variable should be zero while receiving the settings data, and set to one only after receiving the ‘$’ that marks the end of the sequence.
If the ISR receives a ‘@’, this indicates the start of a new speed data sequence even if the previous sequence was incomplete. Set the data start variable to a one, and clear buffer count to 0. Also set the the valid data flag to zero to indicate that you now have incomplete data in the buffer. This sets up the ISR to wait for the next transmission.
For the next characters that arrive, your code must check to see if they are valid.
-
If the ISR receives something other than the negative sign, or digits 0 through 9 or the end of transmission marker ‘$’ is received, reset the data started flag to zero to discard what has been received so far. This will set up the ISR to wait for the next transmission.
-
If a sequence has started and a correct character is received, store it in the next buffer position and increment the buffer count. Your code should also make sure there is room in the buffer for the data. If the data tries to overrun the length of the buffer this would imply two transmissions have somehow been corrupted into looking like one, and in this case you should set the data started flag back to zero to discard this transmission.
-
If the ISR receives a ‘$’, and the buffer count is greater than zero (meaning the sequence has started) set the valid data flag variable to a one to indicate complete data in the buffer. However if the end transmission character is received but there is no threshold data (nothing in the buffer between the ‘@’ and the ‘$’, the flag variable should not be set to a one.
The main program can check the data valid variable each time through the main loop. When it sees it has been set to a one, it can call a function to convert the speed data from from a string of ASCII characters to a fixed-point number (see below). It should probably also clear the data valid variable to a zero so it doesn’t re-read the same data the next time through the loop.
Using sscanf to Convert Numbers
In Lab 5 you learned how to use the “snprintf” function to convert a number into a string of ASCII characters (e.g. 123 → “123”). Now we need to go the other way, from a string of ASCII characaters into single fixed-point number (e.g. “456” → 456). For this we can use the “sscanf” function that is part of the the standard C library.
Important: As with using snprintf, in order to use sscanf you must have the following line at the top of the program with the other #include statements.
#include <stdio.h>
The sscanf function is called in the following manner
sscanf(buffer, format, arg1, arg2, arg3, ...);
where the arguments are
-
buffer- A char array containing the items to be converted to numerical values. -
format- The heart of the sscanf function is a character string containing formatting codes that tell the function exactly how you want it to convert the characters it finds in input string. More on this below. -
arguments- After the format argument comes zero or more pointers to where the converted values are to be stored. For every formatting code that appears in the format argument, there must be a corresponding argument containing the a pointer to where to store the converted value.
The format argument tells sscanf how to format the output string and has a vast
number of different formatting codes that can be used. The codes all start with
a percent sign and for now we will only be working with one of them: %d.
This is used to format decimal integer numbers. When this appears in the format
string, the characters in the input string will be interpreted as representing
a decimal integer number, and they will be converted to the corresponding
value. The result will be stored in the variable that the corresponding
argument points to.
The format string must have the same number of formatting codes as there are arguments that follow it in the function call. Each formatting code tells how to convert something in the input string into its corresponding argument. The first code tells how to convert something that is stored where “arg1” points, the second code is for “arg2”, etc.
Example: Assume you have a char array containing the characters representing three numbers. The code below would convert them into the three int variables.
char buf[] = "12 543 865";
int num1, num2, num3;
sscanf(buf, "%d %d %d", &num1, &num2, &num3);
The arguments are pointers to where the three values are to be stored by using the form “&num1” which makes the argument a pointer to num1 rather than the value of num1. After the function has executed, the variables “num1”, “num2” and “num3” will contain the values 12, 543 and 865.
Important: The “%d” formatting code tells sscanf that the corresponding argument is a pointer to an int variable. When it converts the characters to a fixed-point value it will store it in the correct number of bytes for that variable type. If you wish to store a value in a “short”, or a “char” variable, you must modify the format code. The formatting code “%hd” tells it to store a 2 byte short, and “%hhd” tells it to store a 1 byte char.
Here’s the above example but modified to use three different variable types.
char buf[] = "12 543 865"; char num1;
short num2;
int num3;
sscanf(buf, "%hhd %hd %d", &num1, &num2, &num3);
Serial Test Point
In order for the EE109 teaching staff to confirm that your serial link is operating properly, we require that all student project boards add a test point on the breadboard where a connection can be made to our test board that sends and receives data.
The connection to the test board consists of three wires that will be plugged into your board: transmit (your board to the instructors), receive (the instructors board to yours) and ground. Select a place on the breadboard, preferably near one end of the breadboard where the 74LS125 is installed, where there are three adjacent 5-hole nodes available for use. There should be no gaps between the three 5-hole nodes.
Make connections to the three nodes in this order from left to right as shown below:
- Ground
- Transmit data (connected to the TX or D1 pins on the LCD)
- Receive data (connected to the input of one of the Tri-State buffers)

Once this has been done, when the instructor wants to test the serial interface to your board, they will plug the three wires from their board into these three connection points so the boards are hooked together for testing.
Buzzer
When a speed value is received from the remote device, a sequence of tones are played using the buzzer. The tones played by the buzzer depend on whether the received speed was above or below the threshold value set by the rotary encoder on the local device. The absolute value of the received speed is used in the comparison, not whether it was positive or negative motion. If the remote speed is faster, the buzzer plays a three-note sequence of tones starting with a low frequency note and increasing in pitch. If the remote speed is lower, it plays a three-note sequence of tones starting with a higher frequency tone and then decreasing in pitch.
In Lab 6 you worked with producing tones of different frequencies from the buzzer. In the final part of that lab the tones were generated using one of the timer modules and an ISR routine to control the output signal. The same method should be used here to create the buzzer tone. In this case you will have to use TIMER0, an 8-bit timer, since the other two timers are being used for other tasks. When the program wants to sound the buzzer it can start the timer running. Each time an interrupt occurs the ISR changes the state of the output bit driving the buzzer. After the required number of transitions for the tone have occurred, the ISR can shut the timer off to end the output. Do not use the delay functions to drive the buzzer.
Each tone in the sequence should play for between 1/4 and 1/2 second. You can select any sequence of three frequencies for the tones but you are not allowed to use the same frequencies from lab 6. It must be possible to hear all three tones and tell whether the pitch is going up or down.
To receive full credit for the buzzer output generation, use TIMER0, one of the two 8-bit timers, to generate the buzzer signal. If you use the delay routines as in the first part of Lab 7, you will only receive partial credit for this task.
The project steps above are Checkpoint 2. For full credit, demonstrate these to a CP by 4/29 or 5/1.
To get checked off you must first upload and submit the code you have written to this point on Vocareum. The CP/TA who does your Checkpoint will view the code submission on Vocareum and you will be asked to explain how your code works in addition to demonstrating that it works on your project board.
You can request to have the checkpoint graded a maximum of two times. The grading will be of all items in the checkpoint. No partial checkpoints will be done.
Software Issues
Multiple Source Code Files
Your software should be designed in a way that makes testing the components of the project easy and efficient. In previous labs we worked on putting all the LCD routines in a separate file and this practice should be continued here. Consider having a separate file for the encoder routines and its ISR. Code for the timers can also be in a separate file. The code related to the serial interface can be in a separate file. It’s up to you to decide how to separate the code into files that make logical sense based on the function of the code sections.
All separate code files must be listed on the OBJECTS line of the Makefile
to make sure everything gets linked together properly. This is described in
more detail in the section below.
To get full credit for this project, your source code must be in at least four “.c” files. This can include your main file, lcd.c, and a minimum of two others.
Proper “Makefile” for the Project
In class we discussed how the “make” program uses the data in the
“Makefile” to compile the various modules that make up a program. The role
of the Makefile is to describe which files affect others when they have been
modified, known as “dependencies”. This project may require several source
code files, some with accompanying “.h” header files, so the generic
Makefile should be modified to describe the configuration. For example,
let’s say you have four C files for the project and four header files:
-
The main program is in
project.cand has some global variables and functions declared inproject.h -
The LCD routines are in
lcd.cwith global declarations inlcd.h -
The functions to handle the rotary encoder are in
encoder.cwith global declarations inencoder.h -
The functions to handle the timers are in
timers.cwith global declarations intimers.h.
Let’s also say that project.h is “included” in both the encoder.c and
timers.c files, and the header files for the LCD, encoder and timer
routines are included in the project.c file. In this situation, the
following lines should be added to the Makefile after the “all: main.hex”
and before the “.c.o” line as shown below.
all: main.hex
project.o: project.c project.h lcd.h encoder.h timers.h
encoder.o: encoder.c encoder.h project.h
timers.o: timers.c timers.h project.h
lcd.o: lcd.c lcd.h
.c.o
In this example, if you edit the encoder.h file, the Makefile shows that
encoder.o and project.o must be recompiled since they are dependent on the
contents of encoder.h. For a proper Makefile it’s important to describe all
the dependencies so various modules will be recompiled if necessary. Adding
all the dependencies to the Makefile will make sure that any time a file is
edited, all the affected files will be recompiled the next time you type
make.
Accessing Global Variables
In the project you may need to use global variables that are accessed in multiple source files. A global variable can be used in multiple files but it must be defined in one place. We define variables with lines like
char a, b;
int x, y;
Global variables must be defined outside of any function, such as at the start of a file before any of the functions (like “main()”). Variables can only be defined once since when a function is defined, the compiler allocates space for the variable and you can’t have a variable stored in multiple places.
If you want to use that global variable in another source code file, the compiler needs to be told about that variable when compiling the other file. You can’t put another definition of the variable in the file for the reason given above. Instead we use a variable declaration. The declaration of a global variable (one that’s defined in a different file) is done using the “extern” keyword. The “extern” declaration tells the compiler the name of the variable and what type it is but does not cause any space to be allocated when doing the compilation of the file.
For example, let’s say the global variable “result” is accessed in two files, project.c and stuff.c, and you decide to define it in the project.c file. The project.c file would contain the line defining the variable
int result;
and the stuff.c file would need to have the line
extern int result;
declaring it in order to use the variable. If the “extern” keyword was omitted, both files would compile correctly, but when they are linked together to create one executable, you would get an error about “result” being multiply defined.
If you have global variables that need to be accessed in multiple files it’s recommended that you put the declarations in a “.h” file that can be included in all the places where they may be needed. For the example above, create a “project.h” file that contains the line
extern int result;
and then in both project.c and stuff.c add the line
#include "project.h"
It doesn’t cause problems to have the declaration of a variable, like from an “.h” file, included in the file that also contains the definition of the same variable. The compiler knows how to handle this correctly.
Building Your Design
It’s important that you test the hardware and software components individually before expecting them to all work together. Here’s a one possible plan for putting it together and testing it.
The project should be built in two stages with it checked off at each of the two stages.
For Checkpoint 1:
-
Confirm that the LEFT and RIGHT buttons on the LCD can be recognized for invoking the “Start” and “Stop” actions.
-
Add the range sensor to the board and connect the
TrigandEcholines to the Arduino digital I/O port bits. Put two channels of the scope on theTrigandEcholines and observe what happens when the Start button is pressed. The sensor should produce a pulse in response to the trigger signal. -
Configure TIMER1 to use it to measure the width of the pulse. Pick a prescaler value that will allow the timer to get the most accurate timing of the pulse width.
-
Implement the Pin Change Interrupt ISR for the sensor output signal. For debugging, have the TIMER1 count value printed on the LCD and see if it changes as you make measurements at differing distances.
-
Convert the count value from TIMER1 to the distance in millimeters. This should be done without using any floating point arithmetic. Write the distance value to the LCD after each range measurement is completed.
-
Check that your measurement calculation correctly handles distances greater than the specified maximum range by indicating on the LCD that the distance is too far.
-
Using what you learned in the TIMERS lab, implement a stopwatch that counts the time in tenths of a second as soon as the first measurement is complete.
-
Write code to handle the “Stop” button initiating the second range measurment.
-
After the second range measurement has been made, use the two distances, and the time between them as measured by the stopwatch to calculate the speed and display it in cm/sec with one decimal place. As with the distance measurement, this calculation should not be done with floating point operations.
-
Install the rotary encoder on the board and add code to use the rotary encoder to set the speed threshold value between 1 and 99 cm/sec.
-
Install the RGB LED and the three current limiting resistors on the board. Add code to light up the segments based on the speed threshold and the speed that has been measured.
For Checkpoint 2:
-
Write code to store the speed threshold value in the EEPROM, and read the EEPROM value when the program starts. Make sure to add code that checks that the threshold you loaded from the EEPROM is a valid value.
-
Add the servo motor. Write code to convert the elapsed time from the first range measurement into the OCR0A value to control the width of the PWM pulse to the servo.
-
Install the 74HCT125 tri-state buffer and add code to enable the the buffer after the program starts.
-
Write test code that continually sends data out the serial interface and use the oscilloscope to examine the output signal from the D1 port of the Arduino (transmitted data). For 9600 baud, the width of each bit should be 1/9600 sec. or 104μsec.
-
Add code to send the measured speed value (in mm/sec) out the serial link whenever a valid measurement has been made. Use the scope to check that the transmitted packet matches the specified protocol with the start and end characters present.
-
Write code to receive the remote speed and display it on the LCD in cm/sec. This also should be done without using any floating point routines.
-
Do a “loopback” test of your serial interface. Connect the D1 (TX) pin to the input pin on the 74HCT125 so you are sending data to yourself. Make a speed measurement and and see if the received speed is the same as the local one displayed on the LCD.
-
Add the code to sound the buzzer with the three tone sequence as described above.
Results
Getting your project checked off can be done in multiple checkpoints to ensure you earn partial credit for parts that are working. At each checkpoint we will confirm operation of all the features from the previous checkpoints plus the new features added in the current checkpoint.
Checkpoint 1:
- Splash screen with your name shown at start.
- Pressing “Start” makes the first range measurement in 0.1 cm precision.
- Properly handes an out-of-range situation
- Stopwatch runs showing time between measurements to 0.1 sec precision
- After 10 seconds the stopwatch times out and terminates the speed measurement
- Pressing “Stop” makes the second range measurement.
- Program correctly calculates the speed in 0.1 cm/sec precision..
- Rotary encoder can adjust the speed threshold.
- Speed threshold is limited to between 1 and 99.
- The RGB LED is red if the speed is greater than the threshold.
- The RGB LED is green if the speed is equal to or less than the threshold.
- The RGB LED is blue if no speed has been measured or the measurement failed.
Checkpoint 2:
Demonstrate items from the previous checkpoint. Then demonstrate:
- Threshold setting stored in EEPROM and retrieved when Arduino restarted (power cycled).
- Servo motor pointer counts down from 10 to 0 after first measurement made.
- The local speed measurement is t transmitted correctly to the remote device.
- Speed measurements from the remote device are received correctly and displayed on the LCD
- Buzzer sounds the proper tone sequence for received speed above or below the local threshold setting.
Review Questions
Be sure to answer the two review questions in Project_Answers.txt and
reprinted below:
1.Cost Analysis: Assume we are building 1000 units of this system. Use the provided part numbers (see the webpage) and the digikey.com or jameco.com website to find the total cost per unit (again assume we build a quantity of 1000 units) for these speedometers. Itemize the part costs (list each part and its unit cost when bought in units of 1000) and then show the final cost per unit below. Note: You only need to price the components used in the project (not all the components in your lab kit. Also, you do not need to account for the cost of the circuit board or wires. In a real environment we would manufacture a PCB (Printed Circuit Board). As an optional task, you can search online for PCB manufacturers and what the costs and options are in creating a PCB.
-
Reliability, Health, Safety: Assume this system was to be used in a real industrial monitoring application.
-
What scenarios might you suggest testing (considering both HW and SW) before releasing the product for use?
-
How might you make your design more reliable? By reliability we don’t just mean keeping it running correctly (though you should consider that), but also how you can detect that a connected component has failed to gracefully handle such situations. You can consider both HW and SW points of failure, issues related to the operational environment, etc. and how to mitigate those cases.
-
Submission
Make sure to comment your code with enough information to convey your approach and intentions. Try to organize your code in a coherent fashion.
The Project_Answers.txt file and all source code (all .c and .h files and the
Makefile) must be uploaded to the Vocareum web site by the due date. Make sure
you have included all files since the graders may download your submission,
compile it on their computer and download it to their board for checking the
operation. 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 in your project box. All the contents of the project box and components will need to be returned at the end the semester.

