EE 109 - Spring 2025 Introduction to Embedded Systems

Spring 2025 Project

Temperature Monitor

Table of Contents

Introduction
Overview
Operation of Temperature Monitor
Getting Started
Which Port Bits to Use
DS18B20 Temperature Sensor
Temperature Calculations
High/Low Temperature Setting Button
Adjusting Temperature Settings
Non-volatile EEPROM Memory
Red and Blue (out of range) LEDs
Green (in range) LED
Servo Motor Indicating Dial
SEND Button to Transmit Data
Serial Interface
Tri-State Buffer
Transmitting Data
Receiving Data
Selection of Local or Remote Settings
Software Issues
Building Your Design

Introduction

This semester’s class project is to build a device that monitors the temperature in a room and indicate whether the temperarure is within an acceptable range or too hot or too cold. It uses a digital temperature sensor to measure the abient temperature and then compares it to two settings that can be adjusted by the user.

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/16 or 4/18, the second due 4/23 or 4/25 and the third due 4/30 or 5/2) 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

Temperature Monitor Overview

In its simplest form the temperature monitor measures the temperature in a room and shows the temperature on an LCD display. Based on two settings that can be adjusted by the user, it decides whether the room temperature is within an acceptable range, or has the room become too hot or too cold. The temperature is shown on an LCD display and the LEDs are used to indicate whether the temperature is too hot, too cold, or acceptable. A block diagram of our temperature monitor is shown below and it will have the following features.

Image

Operation of the Temperature Monitor

The following is a description of the operation of the temperature monitor.

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

Getting Started

Create a project folder inside your ee109 folder like you have done for all the previous lab assignments this semester. We suggest starting with a copy of your code from Labs 6 and 7 since this project uses the rotary encoder and PWM signals. It’s strongly recommended that you remove from the file any code or variables that from Labs 6 or 7 will not be used in this project. The project code is complex enough that having extra unused variables or function only serves to make it harder to debug.

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:

Download from the class web site the file project.zip that contains the files ds18b20.c and ds18b20.h that are needed to work with the DS18B20 temperature sensor. It also contains the Project_Answers.txt file with information about what needs to be demonstrated to show the project is working.

Additional details about the temperature sensor, the EEPROM memory and the serial link are provided later on this web page.

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 temperature sensor, 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 BitFunction
PORTB, bit 3 (PB3)PWM signal to servo motor
PORTB, bit 4 (PB4)Enable signal to 74HCT125 buffer
PORTB, bit 5 (PB5)HI/LO button
PORTC, bit 1 (PC1)One-wire bus to DS18B20 sensor
PORTC, bit 2 (PC2)RGB LED blue signal
PORTC, bit 3 (PC3)RGB LED green signal
PORTC, bit 4 (PC4)RGB LED red signal
PORTC, bit 5 (PC5)LOCAL/REMOTE button
PORTD, bit 0 (PD0)Serial receive
PORTD, bit 1 (PD1)Serial transmit
PORTD, bit 2 (PD2)Rotary encoder
PORTD, bit 3 (PD3)Rotary encoder

The project will use the RIGHT button on the LCD as the SEND button. More on this below.

Hardware Construction Tips

The temperature sensor, buttons, LED, rotary encoder, 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 sections below describe the parts of the project that are part of Checkpoint 1 and are due 4/16 or 4/18. Students are strongly advised to get these parts of the project working before attempting to complete any of the rest of the project.

DS18B20 Temperature Sensor

A new component to this project is the DS18B20 temperature sensor. This is an integrated circuit that connects to the microcontroller using a single wire called a “1-Wire” interface by the manufacturer. This single wire is used to both send commands from the Arduino to the sensor, and to read back data from the sensor to the Arduino. Your code will preform the sending and receiving of data on this wire.

A picture, pin diagram and schematic diagram of the DS18B20 is shown below. The IC needs to have ground and power connections provided on pins 1 and 3 respectively.

WARNING! Make sure you install the power and ground connections to the DS18B20 correctly and don’t reverse them. If the power and ground are reversed the DS18B20 will quickly get very hot and eventually burn out.

The Arduino communicates with the DS18B20 by connecting the “DQ” pin (pin 2) to one of the Arduino’s I/O port bits. Note from the schematic that the DQ line must have an external 4.7kΩ pull-up resistor on it. This is not an option, without it the 1-Wire bus will not operate. You can not use the port bit’s internal pull-up instead of the external resistor.

From the class web site you can download the files ds18b20.c and ds18b20.h that contains software to communicate with the DS18B20 temperature sensor over the 1-Wire bus. The routines in ds18b20.c assume the DS18B20’s “DQ” pin is connected to Port C, bit 1. Make sure you are using that I/O pin for the connection to the sensor

Testing the DS18B20

Once you have wired the DS18B20 to +5V, ground and PC1 for the data connection, you can test the sensor and the connections to it using a test program available on the class web site. Download the temp_test.zip file to a another folder inside project area and break it apart. It will contain a Makefile and a test.hex file. Edit the PROGRAMMER line in the Makefile if necessary, and then run the command make test to download the sensor test program to your Arduino.

When the test program runs, it will go into a loop querying the DS18B20 for the temperature data. If it can not read data from the sensor, it will write “No DS18B20 found” on the LCD screen. This error is most likely due to a wiring mistake, or neglecting to install the pull-up resistor.

If it can query the sensor, it will write the two bytes of data it received on the LCD in hexadecimal, and the results of converting that to Fahrenheit degrees. You should be able to see the temperature change if you warm or cool the sensor.

Important: Make sure the test program is displaying valid temperature results before you proceed with writing your own code to read the sensor and convert the data to degrees Fahrenheit. If the test program can’t read the sensor that has to be fixed before you can proceed.

DS18B20 Application Programming Interface

We’ve provided some of the routines that do the work necessary for using the DS18B20. The ds18b20.c file contains the following three top-level routines that make up the API (Application Programming Interface) to the sensor.

The ds18b20.h file contains declarations for these three three functions and can be included in your program file to declare the functions.

char ds_init(void);
void ds_convert(void);
char ds_temp(unsigned char *);

Below is a sample of some code that uses these routines to read the temperature data.

#include "ds18b20.h"

main()
{
    unsigned char t[2];

    if (ds_init() == 0) {    // Initialize the DS18B20
        // Sensor not responding
    }

    ds_convert();    // Start first temperature conversion

    while (1) {
        if (ds_temp(t)) {    // True if conversion complete
            /*
              Process the values returned in t[0]
              and t[1] to find the temperature.
            */

            ds_convert();   // Start next conversion
        }
    }   
}

The temperature data is returned in two elements of the char array that is passed to the ds_temp function. Element 0 of the array contains the eight least significant bits. Bits 3, 2, 1 and 0 are the fractional part of the temperature value. Element 1 of the array contains the four most significant bits of the temperature value. The result is essentially a 12-bit 2’s complement number stored in two 8-bit bytes with the sign (‘S’ in the diagram below) extended to fill it out to a 16-bit 2’s complement number. The two bytes together represent the temperature in degrees Celsius times 16. For example if the temperature is 27.5 degrees Celsius, 16 times this is 44010, or 0x01B8 in hex. The two values in the array elements would then be 0x01 in element 1 and 0xB8 in element 0.

DS18B20 Low-Level Functions

The three API high-level routines described above make use of several low-level routines in ds18b20.c that are not intended to be seen or used by a user of the API. These routines do the actual work of communicating with the DS18B20 to acquire the temperature data.

The code for most of the above routines is provided in the ds18b20.c file but you will have to write the code for the ds_write0bit, ds_write1bit and ds_readbit routines.

The Arduino sends bits to, and receives bits from, the temperature sensor (the DS18B20) by signaling over the 1-Wire bus. The 1-Wire bus uses a single wire that is used to send data from the Arduino to the DS18B20, and at other times from the DS18B20 to the Arduino.

Signaling on a 1-Wire bus is based on the idea that both devices connected to the bus allow the bus to “float”, also called “releasing the bus”, by not putting any signal on the bus. A pull-up resistor connected to the bus then pulls the bus to the high state and it sits in the high state when neither device is trying to send any information. When one device needs to send data to the other it sends 0’s and 1’s by putting a zero volts signal on the bus for different lengths of time and letting it float the rest of the time. The receiving device notes how long the bus was in the 0 volts state and from that determines whether a 0 or 1 was being sent to it.

The diagram below shows the timing for sending a 0 bit and a 1 bit from the Arduino to the DS18B20. For each bit being sent, the Arduino must make the bus low for the indicated amount of time, and then release it (so it goes back to the high state) for the indicated amount of time. After that has been done, it can then repeat the process to send the next bit.

Reading bits is done in a similar fashion. The Arduino signals the DS18B20 to send bits by pulling the bus down to 0v for short time and then releasing it to go high. It then monitors the bus to see if the other device pulls the bus low (sending a 0) or leaves it high (sending a 1). This can be done by simply reading the state of the bit in the PIN register after the specified amount of time has gone by.

The diagram below shows the timing for reading a 0 and 1 bit sent from the DS18B20 to the Arduino.

The operations of pulling the 1-Wire bus down to zero volts, or letting it float so the pull-up resistor makes it a high voltage, can be done by just changing the value of the DDR bit for that I/O port bit.

Therefore all you need to do to change the state of the bus is change the DDR bit.

The DS18B20 datasheet describes the timing for writing ones and zeros, and for reading a bit from the DS18B20 in more detail on pages 15-17. You should refer to these pages when completing the code for the these routines.

Temperature Calculations

The DS18B20 converts the temperature into a two’s complement 12-bit value (8-bits for the integer portion and a 4-bit fractional portion) representing the temperature in degrees Celsius, and stores these in two bytes that can be transferred from the DS18B20 by your program. The DS18B20 datasheet on pages 5 and 6 describes the format of how the temperature data is stored, and includes several examples of how values are represented.

Your program should convert these two bytes to an Fahrenheit value. The Fahrenheit value to be displayed should include one digit to the right of the decimal point (e.g. 74.6). The bits for the fractional part of a degree Celsius that are included in the data returned from the DS18B20 must be properly factored into the conversion to the Fahrenheit temperature. In other words, don’t throw away any of the precision of the sampled value. Use all of it to give the most accurate Fahrenheit value possible for displaying (to one decimal place).

Hint: The ds_temp routine returns the temperature in two bytes. Combine these two 8-bit values into a single 16-bit signed variable before doing any operations to convert it to Fahrenheit.

Converting from Celsius temperature to Fahrenheit is usually done with the formula

however in this project all temperature calculations must be done in integer (fixed point) arithmetic. Do NOT use any floating point numbers (float or double) either as constants (e.g. 0.1) or variables.

Watch this video for more tips on doing floating point operations using integers. The slides are available here.

When using integer arithmetic, and variables with a limited numeric range, it’s easy to go wrong. For example if we do the following

unsigned char c, f;
f = (9 / 5) * c + 32;

the integer division of “9 / 5” will be 1 and give result of “F = C + 32”. On the other hand, if we do it as

unsigned char c, f;
f = (9 * c) /  5 + 32;

The product “9 * c” will probably give a result outside the range of the unsigned char variable and result in an overflow. To get the correct answer, you must do the conversion using variables of the right size and do the calculations in the proper order.

Reminder: Any use of Floating Point variables OR constants will result in large deductions in the visual grading.

Testing Your Temperature Conversions

If you want to test your code that converts from the 16-bit temperature value returned by the sensor to a Fahrenheit value, try using some of the temperature values listed on page 6 of the DS18B20 datasheet (linked on Labs web page).

Pick one of the 16-bit values from the table, for example 0191 hexadecimal, and set the variable in your code that holds the sensor data to 0x0191 rather than having it hold the value returned by the sensor. Then run your program and see what Fahrenheit value your code calculates. According to the table this sensor value represents 25.0625 degrees Celcius, and using a calculator this is 77.1 degrees Fahrenheit. If your result is way off then something is wrong with the calculation. Try this test for few other values from the table and confirm your code is coming up with the right Fahrenheit numbers.

Temperature Comparisons

While the temperature should be displayed to a 1/10 of a degree precision, you can do your temperature comparisons for turning LEDs on and off using just the integer portion of the temperature. You don’t have to do any rounding to get the integer values, simply drop the fractional part of the temperature. Similarly, the temperature settings done with the rotary encoder (and stored in the EEPROM) are only done to integer values.

High/Low Temperature Setting Button

The temperature monitor has a button for selecting whether the rotary encoder adjusts the low temperature threshold or the high temperature threshold. Whenever the button is pressed, the devices toggles to the allow setting the other threshold.

The selected mode must be indicated in some way on the LCD screen so the user will know which setting will be adjusted if they rotate the encoder. The recommended way is to put a character of some type next to the low or high temperature setting that is shown on the LCD.

For the HI/LO button, use one of the buttons from your lab kit and interface it to Port B, bit 5 (PB5). Make sure to turn on the pull-up resistor for this port bit.

It is recommended that you use the Pin Change Interrupt for that port to watch for a button press. Since each time the button is depressed it make the device switch to the other mode, it is necessary to do switch debouncing on this input or it will go back and forth between the modes several times whenever the button is pressed. Note that a Pin Change Interrupt will be generated whenever the input changes, so the ISR will be invoked when the button is pressed and when it is released. The ISR must deal with this issue and not just report the button has been pressed each time the ISR is run.

Adjusting Temperature Settings

The rotary encoder is used to set the high and low temperature thresholds between 50 and 90 degrees as an integer value. Both temperature settings should be shown at all times on the LCD. As the user rotates the knob the selected temperature setting, high or low, changes. The software must ensure that the setting is never adjusted to be outside the 50 to 90 degree limits. In addition the program must ensure that the low setting is never adjusted to be above the high setting, or that the high setting is adjusted to be below the low setting. The two settings can be set to be the same number.

The rotary encoder code from prevous labs can be adapted to implement the settings adjustment. Be aware that the input bits use for the encoder in this project are not the same as those used in previous labs. When using the encoder code from other labs, make sure to modify it so it is reading the correct PIN input bits. Also make sure to change the pin change interrupts configuration so the correct PORT and bits within the PORT are being used, and the correct ISR is being invoked.

Whenever a new settings value is selected, it is stored in the non-volatile EEPROM (see below).

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

Checkpoint 2

The parts of the project described below are part of Checkpoint 2 and are due 4/23 or 4/25.

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 high and low temperature threshold values. 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.

Your code should use the above routines to store the temperature thresholds in the EEPROM whenever they have been changed. You can assume the threshold will always be in the range of 50 to 90 degrees. The number can be stored in an 8-bit “int8_t” variable and only requires writing a single “int8_t” variable to the EEPROM for each vallue. You can choose any addresses in the EEPROM address range (0 to 1023) to store the values. When your program starts up it should read the values from the EEPROM, but it must then test the values to see if they are valid threshold values. 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 50 to 90, then your program should ignore this number and revert to using a default threshold value that is defined in your source code.

Red and Blue (out of range) LEDs

The two temperature threshold values and the current temperature are continuously compared to determine if the ambient temperature is outside the acceptable range as set with the rotary encoder. This is indicated by red, green and blue elements of the RGB LED. When doing the temperature comparison, only the integer part of the measured temperature need be used, the fractional part can be ignored.

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

Image

The temperature monitor 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.

Green (in range) LED

The green element of the RGB LED is used to indicate that the temperature is within the acceptable range as determined by the high and low temperature thresholds. Unlike the red and blue LED segment which are either off or on at full brightness, the brightness of the green segment is modified based on how the current temperature compares to both the high and low threshold settings.

The brightness of the green segment can be controlled by a PWM signal generated by TIMER1. As the temperature increases the width of the PWM signal will decrease to make the LED stay on longer and appear brighter.

In Labs 7 and 8 where you worked with PWM signals generated by one of the TIMER modules, the transitions of the PWM signals were produced by the hardware and the output appeared on one of the Arduion’s Port bits. Unfortunately installation of the LCD on the Arduino blocks both of the TIMER1 output bits so we have to resort to generating the output signal differently.

In the TIMER1 mode we will be using the timer generates the PWM signal by counting up from zero to some maximum value as set by the value in the OCR1A register. When it reaches that maximum value it can generate an interrupt and invoke the “TIMER1_OVF_vect” ISR. As it is counting from zero to the maximum value, when the count matches the value in the OCR1B register, it can generate a different interrupt and invoke the “TIMER1_COMPB_vect” ISR. By using the combination of these two ISRs we can create a PWM signal output on any I/O pin we choose.

To configure TIMER1 in this manner do the following in the timer initialization code.

For the ISR routines we need one for each of the interrupts that have been enabled above. These ISR’s will change the state of the output bit to produce the PWM signal.

    ISR(TIMER1_OVF_vect)
    {
        // Turn the PWM bit on
    }

    ISR(TIMER1_COMPB_vect)
    {
        // Turn the PWM bit off
    }

Determining the Green PWM Signal Width

The width of the PWM pulse needs to be based on the temperature as compared to the two threshold settings and this pulse width detemines how bright the green LED segment will be. Since the RGB LED is a common anode LED, when the PWM signal to it is zero, the LED will be on. This means a short duty cycle PWM signal (smaller value of OCR1B) gives a bright LED and long duty cycle signal (larger value of OCR1B) makes it dim.

Experimentation has shown that due to the non-linearities of the LED’s output any pulse width of less than 50% duty cycle all make the LED look like it’s on at full brightness. So our code to control it must change the OCR1B value to being between 50% of the OCR1A value for brightest and being equal to OCR1A for being dimmest. We can use a linear equation to determine the OCR1B value as a function of the temperature: OCR1B = f(temp). Our linear equation can be derived from the two points we know about.

When the temperature is at the low threshold, make the PWM signal on all the time to dim the LED. When the temperature is at the high threshold, make the PWM signal on 50% of the time to make it fully bright.

Your code should implement this relationship between the measured temperature and the two threshold values. Each time the temperature changes, if it is between the low and high threshold (green LED on), the code should calculate the new value to go in the OCR1B register.

Since the low and high threshold can both be set to the same value there is the question of what the green LED should do in the situation, full bright or full dim? For this case, the code should set the PWM width to being halfway between that used for full bright and full dim. Note that if you don’t check for this case your linear equation calculation can possibly result in a divide by zero.

Note that the TIMER1 is running at all times, so when you want the green LED to be off due to the temperature being outside the acceptable range, the easiest way to do that is simply set the PWM signal to being on all the time (OCR1B = OCR1A) and this will make the green segment appear to be off.

Servo Motor Indicator Dial

The servo motor is used to indicate when the temperature has gone above or below the acceptable range. When this happens, the servo moves a pointer across a dial taking 10 seconds to go from the start position to the end of its rotation.

The servo is controlled by TIMER2 and the servo’s PWM signal appears on Port B, bit 3 (PB3). 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 Lab 7.

For this project we want the operation of the servo motor to be handled exclusively by interrupts. Once the program starts the operation of the servo indicator the motion of the servo is handled by an ISR, not by the main program. The only thing the main program can do is stop the servo motion.

In Lab 7 we did not use interrupts to generate the PWM signal but in this case interrupts will be used, not to generate the signal but to keep track of how many times the ISR has been invoked. TIMER2 can generate an interrupt every time the count value reaches the maximum value of 255, which we know from above is 16.4ms. From that you can calculate how many of these periods are needed to span the 10 seconds the servo will be in operation. The ISR must use these numbers to know when it must change the value in OCR2A that determines the PWM pulse width.

For example (using different numbers), suppose we wanted the servo to move for 7 seconds and the ISR were run every 10ms, then it would take 7/0.010 = 700 invocations of the ISR to span the 7 seconds. If the value in OCR2A had to be changed from 10 to 30 during this time, then the OCR1A value would have be increased by one every 700/(30-10) = 35 times the ISR is run.

The TIMER2 ISR must handle these operations. It must keep track of how many times the ISR has been run and update the OCR1A value at the specified intervals. When the operation starts, it should load the OCR1A register with the value to rotate the servo to the full counter-clockwise position. After that it should increment the OCR1A value at regular intervals so as to make the servo end up pointing to the full clockwise position at the end of the 10 second interval.

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 count down time from 0 to 10 seconds. These can be cut out and taped to the bottom of the servo.

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

Checkpoint 3

The parts of the project described below are part of Checkpoint 2 and are due 4/30 or 5/1.

Most of Checkpoint 3 is involved with implementing the serial link to exchange the temperature threshold settings with another similar device. 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.

SEND Button to Transmit Data

The user has the option to send the high and low thresholds from the local device to another monitoring device. Due to a shortage of available I/O ports, none of the normal port bits are available to implement this button. However we can use one of the buttons on the LCD to accomplish the same thing. Normally when using the LCD buttons we have to convert the voltage they create on PC0 to a digital value. However when the RIGHT button is pressed it produces a voltage of zero volts on PC0, and when it is not pressed the voltage goes to 5V, pretty much like a normal button. Because of this we can read PC0 as a digital input and not have to use the ADC to convert it. In addition since the LCD is producing a voltage for the signal, we do not need to turn on a pull-up resistor for the PC0 input.

Reading the RIGHT button as a digital input also allows us to use a Pin Change Interrupt ISR in the same manner as with the other buttons. The input will also require switch debouncing as with the other switch inputs.

When the SEND (RIGHT) button is pressed the Arduino must transmit the high and low threshold setting over the serial link. The protocol for this is described in the next section.

Serial Interface

The serial interface between temperature monitoring devices will use an RS-232 link to send the temperature threshold 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.

Image

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.

Image

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 temperature monitoring 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 temperature monitors 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 temperature thresholds between two devices will consist of a string of up to six bytes in this order:

For example, to send the threshold values mentioned above, the transmission should consist of <6379>.

Transmitting Data

When your software determines that the threshold values need to be sent, it should call a routine that sends the characters for the thresholds 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 temperature settings 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 threshold settings, 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 threshold values have been received and are available. When the main part of the program sees this variable has been set, it gets the values 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 temperature 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 temperature monitor should operate on the principle that if an error of any kind is detected in the data being received, it discards the current remote settings data that it was in the midst of receiving and goes back to waiting for the next settings 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 threshold 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 threshold settings data from from a string of ASCII characters to two fixed-point numbers (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

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);

Selection of Local or Remote Settings

The LOCAL/REMOTE button on PC5 selects which set of high/low temperature thresholds are used on the local device. Each time the button is pressed the setting toggles between using the local or remote thresholds. The button has no effect if no valid remote threshold settings have yet been received from a remote device. The LCD must indicate which pair of settings are in use, either the local or remote values.

The LOCAL/REMOTE button on PC5 should be implemented using a Pin Change Interrupt ISR in the same manner as the HI/LO and SEND buttons. Since the LOCAL/REMOTE button is on PC5 and the SEND button input is PC0, the Pin Change Interrupt ISR for Port C will have to examine the input bits to determine which button caused the ISR to be run.

When the button causes the selection of thresholds being used to change this can affect the state of the LEDs and the operation of the servo motor. As described above the state of the RGB LED segments is based on the how the current temperature relates to the threshold settings, and if the setting are changed from local to remote or vice-versa, the LEDs may have to change to reflect the new situation. Similarly a change of the thresholds may cause the servo to start its 10 second rotation, or it may cause it to stop its motion if it was in the middle of rotating.

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

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 for other parts of the project. Consider having a separate file for the encoder routines and its ISR. Code for the timers can also be in a separate file. Code to handle the serial link can be in a separate file. Code to handle the three buttons and the RGB LED 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.

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:

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, DS18B20, encoder, timer 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 ds18b20.h encoder.h timers.h serial.h
encoder.o:   encoder.c encoder.h project.h
timers.o:    timers.c timers.h project.h
lcd.o:       lcd.c lcd.h
serial.o:    serial.c serial.h
ds18b20.o:   ds18b20.c ds18b20.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 LCD shield and write code to put a splash screen on the LCD. This will confirm that you can write messages on both lines of the display. Use your file of LCD routines from the previous labs to implement this.

  2. Install the DS18B20 temperature sensor on the breadboard along with the 4.7kΩ pull-up resistor. Make the connection from the sensor to PC1. Download the temp_test.zip file from the class web site and run the test program to confirm that your Arduino can receive data from the sensor.

  3. Edit the ds18b20.c file to finish the three routines that were left for you to complete.

  4. Write code that uses the provided DS18B20 routines to initialize the sensor and then read the temperature from it. At first you can display the two bytes of temperature data as four hex digits (using the %x format string in an snprintf function call. Put this code in a loop so it continuously read the temperature and displays it on the LCD. Is the displayed value about what you would expect to see? Try heating up the DS18B20 by pressing your figure on it, or cool it by blowing on it, and confirm that the temperature changes.

  5. Once you know the sensor is returning valid data, implement the code to convert the Celsius temperature to Fahrenheit and display it on the LCD with one digit to the right of the decimal point.

  6. Install the HI/LO button. Write code to read the button presse. Use of the Pin Change Interrupts is recommended. Add code to indicate on the LCD which temperature setting, low or high, is being adjusted.

  7. Install the rotary encoder. Define the temperature setting variables and modify your encoder routines from Lab 7 to change this number up and down. Display the numbers on the LCD. Add code to make sure the numbers stay between 50 and 90 degrees.

For checkpoint 2

  1. Add code to save the two temperature settings in the EEPROM whenever they have been changed, and to read the values from the EEPROM when the program starts up. Check that values are within 50 to 90 degrees.

  2. Install the RGB LED and the three current limiting resistors. Add code to illuminate the red LED if the temperature is too hot, or the blue LED if it’s too cold based on the measured temperature and the temperature settings.

  3. Add code to control the green segment of the RGB LED if the temperature is between the low and high settings. This involves using TIMER1 to produce a PWM signal to control the brightness of the green LED.

  4. Add the servo motor. Write code to cause the servo rotate for 10 seconds if the temperature is outside the acceptable range. Make sure the servo stops if during its rotation the temperature returns to within the acceptable range.

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. Add code to implement the SEND button on PC0. Send the temperature setting out the serial link whenever this button is pressed. Use the scope to check that the transmitted packet matches the specified protocol with the start and end characters present.

  4. Write code to receive the remote threshold settings and display them 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. Try sending the settings and see if the received remote settings are the same as the local ones displayed on the LCD.

  6. Install the LOCAL/REMOTE button and add code to sense when it has been pressed. Using a Pin Change Interrupt is recommended. Add code to change the indication on the LCD when the button is pressed to show which pair of threshold values are selected for use.

  7. Test the action of the servo motor as you switch between local and remote settings. The servo should start or stop based on the settings if they change.

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.

Note: When started the project must display a splash screen showing the student’s name. If the instructor doing the grading does not see a splash screen, they will not assign any points for work completed.

Checkpoint 1:

  1. Temperature display changes with accuracy of 0.1 degree steps
  2. Button selects which setting to adjust, indicated on LCD
  3. Rotary encoder can adjust settings between 50 and 90 degrees

Checkpoint 2:

Demonstrate items from the previous checkpoint. Then demonstrate:

  1. Settings stored in EEPROM and retrieved when restarted
  2. Red and blue LEDs indicate too hot or too cold
  3. Green LED changes brightness between low and high threshold
  4. Servo rotates for 10 seconds when temperature goes out of range
  5. Servo rotation stops if temperature returns to range

Checkpoint 3:

Demonstrate items from the previous checkpoint. Then demonstrate:

  1. Local thresholds transmitted to remote device
  2. Remote thresholds received and displayed
  3. Button selects which threshold to use, indicated on LCD
  4. LEDs and servo respond correctly to selected thresholds

Note: Sending and receiving the threshold data must be confirmed by connecting the student’s project to one of the instructor’s boards.

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 temperature monitoring 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.