If you donβt have a reverse backup radar system in your car, or if you park forward in your garage, you probably know this feeling of βam I too close, am I gonna hit the wall??β. There are a lot of ways to resolve this issue, but if you like tinkering with electronics and writing code, thereβs only one way: create a home made electronic system.
I used a PIC16F microcontroller and a HC-SR04 ultrasonic sensor, but you can of course easily replicate this proximity sensor with any microcontroller you prefer.
Weβre first going to learn how to measure a distance with the ultrasonic sensor, and use a 2*16 LCD display to test the measuring. After we have this part down, weβll implement an intuitive way to display the proximity to the wall, using 4 LEDs instead of the LCD display.
Step 1: List of Components
I wonβt go in too much detail on the basic functioning of a PIC microcontroller, but you can find more information on those tutorials on how to set up the tools for programming PIC microcontrollers, and how to write your first PIC program.
Here is a list of the components weβll need:
- A PIC16F88, or any microcontroller of your choice.
- A programmer for your microcontroller, if needed. For the PIC, I use a PicKit 3.
- A breadboard to build the circuit.
- A power supply of your choice. You can use a pre-made one from Amazon, or for more fun, you can build your own!
- A 16*2 characters LCD display, preferably based on a HD44780 controller. I use this one from Adafruit, which has the advantage of being sold with a pin header soldered. Also prepare a resistor for the brightness, and a potentiometer for the contrast.
- A HC-SR04 ultrasonic sensor. Here is a $10 5-pack from Amazon.
- 4 LEDs: 2 green, 1 orange and 1 red.
- 4 resistors (330 ohms is perfect).
Step 2: Build the Circuit
Letβs build the circuit on the breadboard. Start by connecting the power supply to the power rails on the breadboard. Donβt power it until the full circuit is finished.
The ultrasonic sensor has 4 pins: Vcc, Trig, Echo and Gnd. To use it weβll generate a high pulse on the trigger pin, and then the echo pin will generate a high signal which duration will tell us how long it took for the sound to go from the module to the obstacle and back. Connect Vcc to the +5V rail and Gnd to the ground. Connect Trig to the pin RA6 of the PIC (pin 15) and Echo to RA7 (pin 16). If youβre using a different microcontroller, Trig should be connected to any digital output, and Echo to any digital input.
For the LCD display, weβll make the following connections, that you can also see on the schematic attached:
- Enable β RA0 (or any digital output)
- RS β RA1 (or any digital output)
- R/W β Ground (we only need to send data to the LCD display, and donβt need the read mode corresponding to R/W=1)
- Vss β Ground
- Vdd β +5V
- Vo β Connect to a potentiometer as shown on the schematic
- If your LCD display has a LED backlight, connect LED+ to +5V with a series resistor, and LED- to the ground.
- Data pins D4 to D7 β pins RB4 to RB7 (or any digital outputs). Weβre going to use the 4-pin mode to control the LCD display, so we wonβt use data pins D0 to D3. This allows us to use pins RB0 to RB3 of the PIC to control the LED proximity meter.
- Finally, connect the 2 green LEDs to RB0 and RB1, the orange one to RB2, and the red one to RB3. Donβt forget to put a 330 resistor in series with each LED.
Step 3: Logic for the Ultrasonic Sensor
Some mathβ¦
We need to do a bit of math to find out how to get the distance, when the sensor gives us a duration. As a reminder, the HC-SR04 will output a high signal on its Echo pin, and the duration of this signal will be the time it took for the signal to travel from the Trig pin, to the object and back. So weβll need to divide this time by 2. Weβll see how to calculate a time with the microcontroller right down below. Letβs assume we have this time in seconds.
The speed of sound is v=340.3 m/s, which is v=34030 cm/s. Speed is calculated as v=d/t, with d being the distance, and t the time.
So the distance will be equal to d=v*t, which is d=(34030*t)/2. This gives us d=17015*t.
Logic for the HC-SR04 sensor
The first thing we need to do is get the duration during which the Echo pin produces a high signal. We are going to use the Timer1 on the PIC. Letβs configure it, using the T1CON register. First, set the clock source to be the internal clock. This means that the frequency of the clock used by the timer will be Fosc/4, in our case 2MHz, as our clock is set to 8MHz. We can disable the timer1 oscillator, as we are using the internal clock as a source. Turn the timer OFF initially, and set the prescaler to 2:1. This means that the frequency of the timer will be half of the clock source, which is 1MHz. That is, every 1us, the timer value will increase.
This gives us time_s = timer_time/1000000. And so d=17015*timer_time/1000000, which is d=0.017015*timer_time, or d=timer_time/58.77.
The code used to configure the timer is the following, in C language for XC8 compiler.
T1CONbits.TMR1CS = 0; // Clock source is internal clock (Fosc/4)<br>T1CONbits.TMR1ON = 0; // Timer off T1CONbits.T1CKPS = 0b01;// Prescaler 2:1 T1CONbits.T1OSCEN = 0; // Disable timer1 oscillator
Letβs put the code specific to the HC-SR04 into separate dist_measuer.c and .h files. Here is the content of the header file:
/*** CHANGE PINS BELOW ***/<br>#define HCSR04_TRIG PORTAbits.RA6 #define HCSR04_TRIG_TRIS TRISAbits.TRISA6 #define HCSR04_ECHO PORTAbits.RA7 #define HCSR04_ECHO_TRIS TRISAbits.TRISA7 /*** STOP CHANGES *** /** * Initialize timer1 and pins of HC-SR04 */ void HCInit(); /** * Calculate the time taken for signal to get to obstacle and come back * @return time, equal 2 times the distance to object */ uint16_t HCCalculateTime(); /** * Calculate the distance to object * @return distance in cm */ uint16_t HCCalculateDistance();
Here is the function HCCalculateTime(), which sends a signal on the trigger pin, and calculates the time during which echo stays high:
uint16_t HCCalculateTime() {<br> uint16_t time = 0; // Reset timer TMR1L = 0; TMR1H = 0; // Trigger high/low HCSR04_TRIG = 1; __delay_us(20); HCSR04_TRIG = 0; // Wait for ECHO to be high while (!HCSR04_ECHO); T1CONbits.TMR1ON = 1; // Wait for ECHO to be low again while (HCSR04_ECHO); T1CONbits.TMR1ON = 0; // Read timer value time = (TMR1H << 8) | TMR1L; return time; }
Finally, here is the function HCCalculateDistance(). It simply calls HCCalculateTime() and divides the time by 58.77:
uint16_t HCCalculateDistance() {<br> uint16_t time = HCCalculateTime(); uint16_t distance = time / 58.77; return distance; }
Youβll notice that I used a type uint16_t for the time and the distance. This is because the timer saved its value in 2 8-bit registers, giving us a size of 16 bits. As for the distance, we could use a type uint8_t, but it would be limited to 255cm, which is 2.55m.
Step 4: LCD Library and Main Loop
I wonβt detail here how to control a LCD display with a microcontroller, but you can find more details in this tutorial, and you can also find a PIC library for controlling HD44780-based LCD displays on this Github repository. Simply add the files LCD.c and LCD.h to your project, and change the name of the pins in LCD.h if youβre using different pins
As weβve said before, weβre using the 4-bit mode to control the display. This means that anytime we want to send data on the data line, weβll need to send the 4 MSB first, and then the 4 LSB.
If you want to test the system now, you only need to call the HCCalculateDistance, and then display the distance on the LCD display:
while(1) { // Get distance distance = HCCalculateDistance(); // Print it on LCD display display_distance(distance); __delay_ms(1000); }
The result is shown on the attached video. Now that we have a functional distance measuring logic, letβs see how to implement the proximity meter with the 4 LEDs!
Step 5: Getting Rid of the LCD Display
If youβre using a LCD display like I am, now is the time you can get rid of it. There are two main reasons why we want to remove the LCD display from the final version of this project. The first one is that printing text on the LCD display is using a lot of memory on the PIC for no reason. Our code related to the LCD, on a PIC16F88, takes around 20% of the program memory. To compare, weβll see how much memory our program uses at the end of this tutorial. With the LCD related code, it takes 17% of the data memory and 39% of the program memory.
The second reason is simply that it is simply not needed in the final product. Being able to print text on a LCD display was helpful when writing the logic for distance measuring, but it will not be useful in the end.
On a side note, there were other options that we could have used to debug our program during development. I chose this method because itβs fun to display information visually on a display, but we could also have used the UART interface of the PIC to output text on a serial console on our computer.
This is an important thing to keep in mind when working with embedded systems. The resources of the systems we work with are often very limited, and while itβs convenient to have a way to display data, itβs rarely needed in the final product.
To get rid of the LCD display, simply remove it from the breadboard and remove all the connection that were related to it. You should be left with the attached circuit.
Step 6: Implement the Proximity Meter
Determine the levels for the meter
The idea behind the proximity meter is simple. We have 4 LEDs that weβll turn ON or OFF depending on the distance from the car to the wall. The colors of those LEDs will make it easy to quickly see how far we are from hitting the wall.
Knowing this, we can first determine the threshold distances we want to assign to each LED. For this, go in your garage with a tape measurer, and write down the limit distances for each of the following states. The distance I got for my garage are shown in the attached picture.
Letβs add those values to our program. Weβll define them as #define, which will make it easier to change them later if needed; weβll just need to change those #define instead of changing the value everywhere itβs used in the code.
// Proximity levels (higher level = object is closer)
#define METER_LEVEL_1 75 #define METER_LEVEL_2 65 #define METER_LEVEL_3 55 #define METER_LEVEL_4 40
The logic to control the proximity meter
Weβll write a function update_proximity_meter() to take care of the meter. Weβll give it the distance in cm as a parameter, and it will turn each LED on or off depending on the distance.
void update_proximity_meter(uint16_t distance) {
PORTBbits.RB3 = 0; PORTBbits.RB2 = 0; PORTBbits.RB1 = 0; PORTBbits.RB0 = 0; if (distance <= METER_LEVEL_4) { PORTBbits.RB3 = 1; } if (distance <= METER_LEVEL_3) { PORTBbits.RB2 = 1; } if (distance <= METER_LEVEL_2) { PORTBbits.RB1 = 1; } if (distance <= METER_LEVEL_1) { PORTBbits.RB0 = 1; } }
Step 7: Logic for the Main Loop
We need to read the distance at regular intervals, and update the meter accordingly. But this is not very efficient; even when youβre not needing the system to tell you how close your car is, it will constantly be updating the meter.
There are many ways to make this more efficient. One of the best would be to use a PIR sensor to detect motion. When motion is detected, it would trigger an interrupt on the PIC and start the whole measuring / updating the meter process.
To keep it simple, and only use the components weβre already using, weβll simply put the system to sleep after each reading. But we need to put the system in idle mode when the car is not in the garage, and when the car is parked but not moving anymore. In order to do this, weβll use two different durations for the loop delay, and weβll add a counter of identical distances:
// Delay between 2 distances measures (in ms)
#define DELAY_IDLE 5000 // When no object is close #define DELAY_ACTIVE 750 // When an object is close // If distance is the same for this many times, go to idle mode
#define THRESHOLD_COUNT_IDENTICAL 20
Here is the logic weβll use:
- If the distance is greater than the largest threshold, use DELAY_IDLE
- If the distance has been the same for more than THRESHOLD_COUNT_IDENTICAL, use DELAY_IDLE
- Otherwise, we need to update the proximity meter more frequently, and weβll use DELAY_ACTIVE
In the first two cases, we want the LEDs to be OFF. Weβll just call update_proximity_meter() with a distance greater than the largest threshold.
Here is the code for the main loop:
void main(void) {
init(); uint8_t count_identical = 0; uint16_t distance = 0; uint16_t prev_distance = 0; while(1) { // Get distance distance = HCCalculateDistance(); // Update counter of identical if (distance != prev_distance) { count_identical = 0; } else if (count_identical <= THRESHOLD_COUNT_IDENTICAL) { count_identical++; } prev_distance = distance; // Update the meter if (distance >= METER_LEVEL_1 || count_identical >= THRESHOLD_COUNT_IDENTICAL) { update_proximity_meter(METER_LEVEL_1 + 1); // All LEDs OFF __delay_ms(DELAY_IDLE); } else { update_proximity_meter(distance); __delay_ms(DELAY_ACTIVE); } } return; }
As you can see, the code for the main function is pretty small. This is because we encapsulated as many code as we could. If we still had the LCD display in the project, we would just need to call the function display_distance() that we wrote in the first part of this tutorial, and this would just add one line to the main function. Itβs very important to keep your code as modular as possible; it greatly improves the readability and maintainability of your program. In this example, anyone can quickly understand what the program does, even if the inner working of each function is not known.
Step 8: Filter the Measures
If youβve experimented a bit with the distance measurer, and itβs easier if you still have the LCD display attached, you probably noticed that the distance value sometimes jumps for one single read, even though the object has barely moved. Thereβs an easy way to fix this issue and make the system more reliable. Weβre going to filter the values, by simply averaging the distances over a given number of measures.
Letβs modify our dist_measure.c and .h files. The function HCInit() will now take an argument, which will be the number of measures. Weβll limit it to 5 samples per measure, so getting a distance doesnβt take too much time. Weβll use a static variable n_samples, that will be accessible by all functions in this file, but not by any other files.
Below is the modified code for the HCInit() function:
#define _XTAL_FREQ 8000000 // System clock frequency static uint8_t n_samples; /** * Initialize timer1 and pins of HC-SR04 * @param number of samples for the filter */ void HCInit(uint8_t _n_samples) { [... PIC pins init code ...] // Save n_samples if (_n_samples > 5) { n_samples = 5; } else { n_samples = _n_samples; } }
Now we just need to loop n_sample times in HCCalculateDistance(), and calculate the average of the n_samples values:
/**
* Calculate the distance to object * @return distance in cm */ uint16_t HCCalculateDistance() { uint16_t time = 0; uint16_t distance = 0; uint8_t i = 0; for (i=0 ; i<n_samples ; i++) {
time = HCCalculateTime(); distance += time / 58.77; __delay_us(100); } distance /= n_samples; return distance; }
And thatβs it, now the distances returned by HCCalculateDistance() will be smoother, and this should get rid of the wrong values.
Step 9: Final Result
Full code
The full code for this project can be found on my Github page.
Memory consumption
As promised, letβs compare the memory consumption of this program, with what it was when we had the LCD display. It is much smaller than before, and this is only due to us removing the code related to the LCD display. The size of the program memory is half of what it used to be. And we also added some code for the averaging and for the delay logic!
Result
The result when moving the system closer/further away from a wall is shown on the attached video.
Improvements
There are many ways we could improve this device. Iβve listed some below, but feel free to experiment and implement your own ideas!
- Use a PIR motion sensor to trigger the system ON/OFF. It would be a much better idea than the logic we currently have with our delays, and would reduce the power consumption of the system.
- Add another level, when the car is much too close and about to hit the wall. You could blink the LEDs or emit a sound with a buzzer.
- Power the device with a battery, and get rid of the power cable.
Thanks for reading, and I hope this was helpful! Donβt forget to checkΒ The Embedded LabΒ for more tutorials!