EE109 – Spring 2023 Introduction to Embedded Systems

EE109 – Spring 2023: Introduction to Embedded Systems

Project – Spring 2023

Introduction

This semester's class project is to build a device that measures the range to an object by using an ultrasonic rangefinder and displays the range on an LCD and on a circular dial. It also relays that range to a remote device.

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. 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 3 checkpoints (first due 4/12 or 4/14, second due 4/19 or 4/21, and the third due 4/26 or 4/28) 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.

For a copy of the project grading sheet, click here

Rangefinder Overview

In its simplest form the rangefinder measures the distance to an object in front of it. The distance in centimeters is shown on the LCD display and also on an indicator dial similar where a pointer rotates to point to the range. A block diagram of our rangefinder is shown below and it will have the following features.

Operation of the Rangefinder

The following is a description of the operation of the rangefinder.

To see a short video demonstrating the operation of the rangefinder, click here.

Getting Started

We suggest starting with a copy of your code from Lab 8 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 three new elements:

All three 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)

"Acquire" button to initiate a range measurement
PORTB, bit 5 (PB5)

LED control signal
PORTC, bit 0 (PC0)

"Adjust" button to toggle local/remote range threshold
PORTC, bit 1 (PC1)

Rangefinder trigger
PORTC, bit 2 (PC2)

Rangefinder output
PORTC, bit 3 (PC3)

Tri-state buffer enable signal
PORTC, bit 4 (PC4)

LED control signal
PORTC, bit 5 (PC5)

Buzzer
PORTD, bit 0 (PD0)

Serial received data
PORTD, bit 1 (PD1)

Serial transmitted data
PORTD, bit 2 (PD2)

Rotary encoder
PORTD, bit 3 (PD3)

Rotary encoder

Hardware Construction Tips

The range sensor, buttons, 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 10 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 "Acquire" Button

The "Acquire" button is use to initiate a range measurement. Each time it is pressed the ultrasonic range sensor takes a new range measurement and the results are displayed on the LCD. For the "Acquire" button, use one of the buttons from your lab kit and interface it to Port B, bit 4 (D12). Make sure to turn on the pull-up resistor for that port bit, and also include code for debouncing the button.

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. At the start of the program the timer can be configured but left in the stopped state (all prescaler bit cleared to zeros). When the ISR is invoked by the start of the pulse, the timer's count can be set to zero with the statement

    TCNT1 = 0;

and then the timer is started by loading the correct prescalar bits.

When the ISR is invoked again by the end of the pulse, the timer can be stopped by setting the prescalar bits to all zeros 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.

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: 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 rangefinder 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 rangefinder 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.

The project steps above are Checkpoint 1. For full credit, demonstrate these to a CP by 4/12 or 4/14.


Checkpoint 2 - Servo Motor Indicator Dial

Besides displaying the range on the LCD, the range is also shown on a round dial by using a servo motor to rotate an indicator to point to the range. 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 2 of Lab 8.

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 range values to servo positions. The number that goes in the OCR2A register has a linear relationship with the range value. Linear equations are based on two points, and you have the two points:

Using these two points, determine a linear equation for mapping the range 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. Make sure the calculations do not result in overflows of the fixed point variables.

Every time the range 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 range from 0 to 400 cm. 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 fixed range of 200 cm, 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 which should be at the 200 cm range. After doing this it should correctly show the ranges from 0 to 400 cm.

Adjusting the Range Threshold Settings

The rotary encoder is used to set the range thresholds from 1 cm to 400 cm as an integer value. Separate thresholds are set for the locally measured range and the remote range that is sent to the local unit. These thresholds will be used to light up LEDs or sound an alarm. The "Adjust" button selects which threshold setting the encoder is adjusting, either the local threshold or the remote one.

The design of this project ran out of available I/O ports to accommodate the Adjust button so we are using the "Right" button of the LCD as the "Adjust" button. Normally you have to use the A-D converter to convert the voltage on PC0 to determine which LCD button was pressed. When the Right button is pressed, a voltage of 0V is applied to the PC0 input and we can read this as a digital value the same way the regular buttons (like the "Acquire" button) are read through the PINC register. In your code make sure DDRC bit 0 is a 0 so it's an input. When you want to check the "Adjust" button, simply read bit 0 of PINC as you would for any other button and check it's value. Don't forget to add code for debouncing on PC0.

When the "Adjust" button is pressed to change the mode, the LCD should indicate this on the display to show which threshold the encoder is going to change. This can be done by displaying "Local" or "Remote" on the LCD.

As the user rotates the encoder knob the range threshold should be shown on the LCD display. The software must ensure that the setting is never adjusted to be outside the limits listed above. Whenever a new threshold value is set, it is stored in the non-volatile EEPROM (see below).

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 two range threshold values. In this way when the device is turned on it will have the same settings 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_word

This function reads two bytes from the EEPROM starting at the address specified and returns the 16-bit value. It takes one argument, the the EEPROM address (0-1023) to read from. For example to read a word from EEPROM address 100:

int16_t x;
x = eeprom_read_word((void *) 100);
eeprom_update_word

This function writes two bytes to the EEPROM starting at the address specified. It takes two arguments, the address to write to and the 16-bit value of the word to be stored there. For example to write the word 0x2f47 to address 200 in the EEPROM:

eeprom_update_word((void *) 200, 0x2f47);

Your code should use the above routines to store the range thresholds in the EEPROM whenever they have been changed. You can assume the threshold will always be in the range of 1 to 400 cm. These numbers can be stored in an 16-bit "int16_t" variable and only requires writing a single "int16_t" variable to the EEPROM for each value. You can choose any addresses in the EEPROM address range (0 to 1022) to store the values. 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 400, then your program should ignore this number and revert to using a default threshold value that is defined in your source code.

Warning! The EEPROM on the microcontroller can be written to about 100,000 times and after that it will probably stop working. This limit should be well beyond anything we need for this project but it's very important that you make sure you don't have the above EEPROM writing routines in some sort of loop that might go out of control and do 100,000 writes before you realize the program isn't working right.

Range Comparison LEDs.

The project incorporates a multicolor (red, green, blue) LED that is used to show the comparison between the most recent local range measurement and the local range threshold setting done with the rotary encoder. If the range measured is greater than or equal to the range threshold, the the green LED is lit. If the range is less than the threshold the red LED is lit. The ranges should be compared to an accuracy of 1 cm.

If no range measurement has yet been made, or if the measurement of the range 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 range measurement.

This is the same common anode RGB LED that was used in Lab 9. 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 9: 240Ω for red and blue, and 1.3kΩ for green.

The design of the rangefinder has two digital I/O pins available for controlling the RGB LED, Port B bit 5 and Port C bit 4 (PB5 & PC4). As part of the project, you will have to design a circuit that uses these two I/O lines to control the three LED segments. Remember that to turn on an LED the signal to it must be a logical 0 while a logical 1 will turn it off. The logic for controlling the LEDs can be done however you choose to do so by using the ICs that have been provided for previous labs. It can be done with individual gates like the 74HCT00 or in a couple of other ways using one of the other ICs that were used in Lab 9. It's up to you to decide how to implement the circuit and design it.

The project steps above are Checkpoint 2. For full credit, demonstrate these to a CP by 4/19 or 4/21.


Checkpoint 3 - Serial Interface

Here is a video lecture/tutorial from previous semesters' labs that describes the operation of RS-232 serial and to the slides that accompany it.

The serial interface between rangefinder devices will use an RS-232 link to send the range data 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 PC3 (Port C, bit 3) 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 PC3 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 are 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 signal must be connected to the buffer input so it can be isolated from the RX port until the programming is completed.

Format of the Serial Signals

For all the rangefinder 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.

Serial Interface Protocol

The serial data link between two rangefinders uses a simple protocol:

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 measured range between two rangefinders will consist of a string of up to six bytes in this order:

For example, to send a range of 35.7 cm, the transmission should consist of <357>.

Transmitting Data

When your software determines that the range measuring event has completed, it should call a routine that sends the characters for the range 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.

Confirming Transmitted Data on the Oscilloscope

The oscilloscopes in VHE 205 have special triggering modes that allow them to capture serial data transfers and display the data. If you think your project board is transmitting the range data, it's a good idea to confirm this with the scope. If it doesn't work correctly with the scope then something is wrong that will need to be fixed before trying to connect your board to another rangefinder.

Follow the instructions on the Serial Data web page to configure the scope to display serial data. Note: In some places the instructions give two options depending on whether you are looking at "TTL Serial" or "True RS-232" signals. The signals in this lab are all "TTL Serial" so make sure to use that option.

Once the scope is configured properly, try taking a range measurement and see if the serial data displayed on the scope matches the range measurement shown on your LCD.

Receiving Data

Receiving the range 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 range 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 range data, 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 range 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 date, such as missing start ('<') character, missing or corrupted range 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 rangefinder should operate on the principle that if an error of any kind is detected in the data being received, it discards the current remote range data that it was in the midst of receiving and goes back to waiting for the next range data 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:

If the ISR receives a '<', this indicates the start of a new range 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.

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 range data from from a string of ASCII characters to a single 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
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);

Buzzer

In Lab 7 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 a one half second tone have occurred, the ISR can shut the timer off to end the output. Do not use the delay functions to drive the buzzer.

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 still receive partial credit for this task.

The project steps above are Checkpoint 3. For full credit, demonstrate these to a CP by 4/26 or 4/28.

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 serial interface routines can also be in a separate file. Code to handle the rangefinder, the two buttons and the LEDs can either be in separate files or in the main program. All separate code files must be listed on the OBJECTS line of the Makefile to make sure everything gets linked together properly.

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. This project requires several source code files, some with accompanying ".h" header files, so the generic Makefile should be modified for this configuration. The role of the Makefile is to describe which files affect others when they have been modified, known as "dependencies". For example, let's say you have four C files for the project and four header files:

Let's also say that project.h is "included" in both the encoder.c and serial.c files, and the header files for the LCD, encoder and serial 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 serial.h
encoder.o:   encoder.c encoder.h project.h
serial.o:    serial.c serial.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 three stages with it checked off at each of the three stages.

For checkpoint 1:

  1. Install the "Acquire" button on the board and add code to detect the "Acquire" button pressed. When the Acquire button is pressed generate a pulse on the port bit that will be connected to the Trig signal to the sensor. Check with the scope that the pulse is generated and is of the correct length.
  2. Add the range sensor to the board and connect the Trig and Echo lines to the Arduino digital I/O port bits. Put two channels of the scope on the Trig and Echo lines and observe what happens when the Acquire button is pressed. The sensor should produce a pulse in response to the trigger signal. Try holding your hand in front of the sensor and see if you can make the echo pulse width change by moving your hand farther and closer to the sensor as you trigger it.
  3. Determine how to configure TIMER1 to use it to measure the width of the pulse. You need to pick a prescaler value that will make it run as fast as possible so as get the most accurate timing of the pulse width. However it must run at a speed where the count will still be less than the maximum 16-bit value (65,535) if the sensor returns the maximum width output pulse.
  4. Write code to implement the Pin Change Interrupt ISR for the sensor output signal. This ISR needs to do different things depending on whether it's detecting the start of the pulse (zero timer count, start timer) or the end of the pulse (stop timer, set flag that measurement complete). For debugging, have the count value printed on the LCD and see if it changes as you make measurements at differing distances.
  5. Convert the count value from the timer to the distance in millimeters that you will need later to display the distance with one decimal place, and then to distance in centimeters for comparing to the two thresholds. This should be done without using any floating point arithmetic. Write the distance value to the LCD after each range measurement is completed.
  6. Check that your measurement calculation correctly handles distance greater than the specified maximum range by indicating on the LCD that the distance is too far.

For checkpoint 2

  1. Add the servo motor. Write code to convert the local range to the value to generate the correct width PWM pulse.
  2. Add the "Adjust" button to the the board and add code to detect when it has been pressed. Each time it's pressed it toggles the adjustment mode between local and remote, and this should be indicated on the LCD.
  3. Install the rotary encoder on the board and add code to use the rotary encoder to set the range threshold values. It should adjust the threshold that has been selected by the "Adjust" button and show the value on the LCD. Check that this allows you to adjust both values between 1 and 400 cm.
  4. Write code to store the range threshold values in the EEPROM, and read the EEPROM values when the program starts. Confirm that this is working by adjusting the distance values and cycling the power on the project. It should start up and display the distances you had set before. Make sure to add code that checks that the distances you loaded from the EEPROM are valid values.
  5. Install the RGB LED, the current limiting resistors and the associated LED control circuitry on the board (for controlling the blue LED). Add code to light up the segments based on the local range threshold and the distance that has been measured.

For checkpoint 3

  1. Install the 74HCT125 tri-state buffer. Write code to enable the the buffer after the program starts. Check that you can program the Arduino with the serial connections in place. This means the tri-state buffer is doing what it is supposed to do.
  2. 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.
  3. Write code to send the distance value in millimeters out the serial interface in the format specified above and check with the scope that it's being transmitted after each measurement. Check that the transmitted packet matches the specified protocol with the start and end characters present.
  4. Write code to receive the distance in mm and display the distance in cm on the LCD. This also should be done without using any floating point routines.
  5. 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 measurement and see if the remote distance is the same as the one you displayed on the LCD as the local distance.
  6. Add the code to sound the buzzer. Use some of your code from the ADC lab to play a tone for a short time (1 second or less).
  7. Make sure your program is using the received remote distance in the code that determines whether or not to play the buzzer. With a loopback in place, check that the buzzer plays whenever the measured range is less than the remote range threshold.

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:

  1. Splash screen with your name shown at start.
  2. Acquire button initiates a range measurement.
  3. Rangefinder measures range to object and shows the distance on the LCD with 0.1 cm precision

Checkpoint 2:

Demonstrate items from the previous checkpoint. Then demonstrate:

  1. Button selects which distance threshold to adjust and indicate selection on on the LCD.
  2. Rotary encoder can adjust both thresholds.
  3. Range is limited to between 1 and 400.
  4. Threshold settings stored in EEPROM and retrieved when Arduino restarted (power cycled).
  5. The LED is red if the local range is less than the threshold.
  6. The LED is green if the local range is equal to or greater than the threshold.
  7. The LED is blue if no range has been measured or the range was beyond 400cm.

Checkpoint 3:

Demonstrate items from the previous checkpoint. Then further show:

  1. Range data is transmitted to another rangefinder
  2. Range data is received from another rangefinder
  3. Buzzer sounds if remote range is below the remote threshold.

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 range finding systems. 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.
  2. 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.