Raspberry Pi Ultrasonic digital clock

Imagine you waking up sometime in middle of night and want to know what time it is. Well you have your favorite toy – the mobile handset – but that is not supposed to be healthy alongside your sleeping brain! The florescent hands of a wall clock fade out after an hour or so when lights are switched off. On the other hand, to have a permanently ON digital clock is a bit disturbing.  I built this project to work as an on-demand clock and the demand is when you hold your hand or any object in front of it for a second or two. Isn’t that cool?! Read on to find out more…

Here is how the completed project looks.

finished-clock-rough

Working principle

It is quite simple and straightforward to understand how it works. The clock display (4 x 7-Segment displays) is normally in switched off condition. Ultrasonic distance sensor HC-SR04 is used to measure distance of the nearest object from it. If the distance gets to less than 10cm, then it reads the time from a Real time clock chip DS1307. This time is then displayed on the display for approximately 30 seconds after which, the display is switched off again. RTC DS1307 is backed up with a battery for timekeeping thus making sure the time is not lost in case of power failure.

Electronics

Here is the circuit diagram made using a nice tool Fritzing.

Schematic diagram

Schematic diagram

As you can see from the diagram, I have used following components.

  1. Raspberry Pi (Old 1B model)
  2. Ultrasonic distance sensor (HC-SR04)
  3. I2C I/O expander (MCP23008)
  4. 7-segment display (Common cathode) x 4
  5. Real Time Clock IC (DS1307)
  6. 3V Coin cell (CR2032)

Of course there are alternatives to these components but I had these components lying them in the junk so thought to put them to use.

Ultrasonic sensor

The HC-SR04 is an ultrasonic ranging sensor. Its working is based on the principle of how bats ‘see’ with ultrasonic sound using a technique called echolocation. It contains an ultrasonic transmitter, a receiver and some control circuitry. There are 4 pins to connect – VCC, Trigger, Echo and Ground. The operating voltage is 5V DC and it needs around 15mA of current which can be easily supplied through the Pi. The Trigger pin, when supplied with a 10uS pulse sends out a burst of 40KHz ultrasonic signals from the transmitter. When this signal gets echoed back from an object, the ECHO pin sends out a pulse. The time between ‘triggering’ the transmitter and ‘receiving’ back the signal represents distance to the object.

Although the operating voltage is 5V, the trigger pin can work well at 3.3V so the Pi GPIO can interface with the trigger pin. However, input on the GPIO is tolerant only up to 3.3V while the ECHO pin outputs a pulse of 5V. Hence a voltage divider is required to reduce this 5V to 3.3V. A simple resistor voltage divider shown below can do the trick.

r1-r2_schem

Here is how the resistor values are calculated. We can neglect the current consumed by the GPIO pin of pi when used as input. We can also assume value for one of the resistors and calculate the value of other resistor. Assuming all the current flows through the resistors and assuming R1 = 1K, R2 can be calculated with this formula.

5V/(R1+R2) = 3.3V/R2

5V/3.3V = (R1+R2)/R2

1.5 = (R1+R2)/R2

1.5R2 = R1 + R2

0.5R2 = R1

With R1 = 1000Ω, which is an arbitrary choice,

R2 = 1000 / 0.5

R2 = 2000Ω

Thus with R1 as 1K and R2 as 2K, the input to GPIO of Pi will get converted to 3.3V from 5V.

I2C I/O Expander (MCP23008)

When we use the 7-segment display, we need 8 I/O pins per display digit. Since the Pi is limited in number of pins, we need an I/O expander. MCP23008 comes in handy which provides additional 8 outputs via I2C bus. Also since there are 3 address pins for this IC, practically 8 such ICs can be connected to the same I2C bus thus giving additional 64 I/O lines. There is also another IC MCP23016 which gives 16 I/O lines. But I had a few of the MCP23008 lying around so I have used them. Meanwhile, If you need a heads up about I2C, please google for it.

The IC can work up to 5V of supply but I have powered it with 3.3V. This is again because the I2C bus of Pi can work at 3.3V only. If it is needed to make this IC work at 5V, a level shifter is advised which will translate the 3.3V I2C bus from Pi to 5V I2C bus for the IC and vice versa. However, intensity of the LEDs connected to outputs of the IC was quite good even at 3.3V hence I decided to keep it to 3.3V only.

Connections to MCP23008 from Pi are straightforward, Pin 1 of the IC is SCL (Serial Clock) which connects to Pin 3 of the Pi. Pin 2 of the IC is SDA (Serial Data) which connects to pin 5 of the Pi. No pull-ups are required because the Pi already has 1.8K pull up on both these lines. Reset pin of the IC is permanently connected to VCC as I am not going to use the resetting feature of this IC. Note that Reset is an active low signal.

Address pins of the IC needs to be wired differently for each IC. The rightmost IC shown in schematic is wired for address 0 which means all address pins A2, A1 and A0 are connected to ground. The next one is wired for address 1 (001) which means A0 is at 1 and A1, A2 are connected to ground and so on with the 4th IC wired for 3 (011) with A2 connected to ground and A0 and A1 connected to VCC.

The GPIO of IC are directly connected to various segments of the 7-segment LED display through current limiting 470Ω resistors. The resistor value is decided based on required current for the LED. A few trials showed that current of 4mA is sufficient to give required intensity. The forward voltage drop across the LED segment is approximately 1.7V which I found be measuring the voltage. Using this the resistor value can be found with the equation:

R = 3.3V - V(LED) / 4mA

R = 3.3 - 1.7 / 4mA

R = 400 Ω

I decided to keep it slightly lesser than 4mA and chose 470Ω resistor value.

7-Segment display connections

You may note that the connections of MCP23008 GPIO to the 7-segment display are quite wired. One set of GPIO connects to two different displays. The only reason for this is to keep the PCB layout simple. I did this project on a general purpose PCB by wiring and soldering all the pins together hence it was more important for me to keep the layout simple and avoid any wires on the board.  Having a look at the Fritzing diagram above you can see that almost all signals get routed straightforward except a couple of them where I made some errors. But with the general purpose PCB, I could use both the top layer and bottom layer for the connections so I could avoid external wires completely on this board. To make up for this, I had to make a kind of lookup table in the code but that was a small price worth paying for.

I have used common cathode displays here so the common pin goes to ground and the segments are driven by MCP23008.

RTC DS1307

RTC connections are the simplest among all others. It just connects to the same I2C bus on pins 5 and 6 of the IC. Pins 1 and 2 need a watch crystal of 32.768KHz for precise timekeeping. No external capacitor is needed for the crystal. Care needs to be taken to mount the crystal as close as possible to the IC pin. A coin cell backup on the VBAT pin helps keep the time updated in case of power loss.

This IC is also powered from 3.3V supply rail since it interfaces with the I2C bus.

I have also added 0.1uF capacitors for every IC for noise suppression if any across the power rails. These capacitors need to be as close as possible to the VCC pin.

Here are the images of assembled boards.

Let us have a look on the software now.

Software

Get your Raspberry pi up and running if not done already. If you need any help, please google for it as it is out of scope of this post to explain basics of Raspberry Pi.

Before proceeding to write the code, a couple of packages are required. First one is i2c-tools. This package may not be required for the final code but it is quite useful utility to see whether the I2C devices we connected are responding properly or not. The next package is python smbus. This is required if the code is to be written in python which I have used. This package helps call the I2C read/write functions from python code. So install these two packages.

sudo apt-get install i2c-tools
sudo apt-get install python-smbus

Check if you have got a file /etc/modprobe.d/raspi-blacklist.conf. If the file is present, check for an entry blacklist i2c-bcm2708 in that file. Comment out that entry with a leading # if not there already. Do not worry if the file is not present or if the entry is not there. This is a provision to block some I2C controllers from using the kernel I2C drivers.

Run the following commands to add the I2C modules to running kernel.

modprobe i2c-dev
modprobe i2c-bcm2708

Reboot the Pi and then run the following command to check if it is able to detect I2C devices connected.

sudo i2cdetect -y 1

This i2cdetect tool scans the I2C bus for any devices present and then outputs the addresses of those devices. In my case I found total 5 devices – 0x20 to 0x23 for the MCP23008 and 0x68 for DS1307. The -y flag in the command acts as a yes response to the question asked by the command. Without that flag the tool will prompt you to accept if you want to scan all the addresses in the I2C address range. The number 1 is the I2C bus number. On older Raspberry Pi devices, the I2C bus was numbered 0. So if you have an older Pi (Pre-Oct 2012) then use 0 instead of 1.

Here is the output from my pi board. As you can see I have an older version board.

i2c-detect

i2cdetect showing all connected devices

If you are not sure about the version of the Pi you have or want to make sure about it, you can run the following command.

sudo i2cdetect -l

This will list the I2C adapters you have as either i2c-0 or i2c-1.

Once all the devices were detected, I moved on to creating the code. I created a simple python script to do all the tasks in this project. It is uploaded on github and reproduced below. It isn’t modular yet but I plan to do that in future.

</pre>
<pre># Python code to control the ultrasonic digitcal clock based on Raspberry Pi
# TODO:
# 1. Make it modular
# 2. Add error handling (like, handle signals for kill, interrupt etc.)
# 3. Spawn / fork a process for usonic sensor and keep only I2C in this module (or the other way round)

import smbus            # Required to communicate to I2C devices
import time             # Required to measure time lapse between trigger and echo of HC-SR04 sensor
import RPi.GPIO as GPIO # Required to control the trigger and echo pins of HC-SR04 sensor
import syslog           # Required to print any messages to system log instead of console when we run this code in background

GPIO.setmode(GPIO.BCM)  # We are using BCM mode i.e. refer the Pi pins as GPIO pin numbers instead of physical pin numbers
GPIO.setwarnings(False) # Just so it doesn't clutter the console / system log
TRIG = 18               # HC-SR04 trigger pin connected to GPIO 18 which is board pin 12
ECHO = 17               # HC-SR04 echo pin connected to GPIO 17 which is board pin 11

# I2C addresses of the 4 I/O expanders used. 0x20 represnts the rightmost display showing minutes (unit digit)
mcp_address1 = 0x20
mcp_address2 = 0x21
mcp_address3 = 0x22
mcp_address4 = 0x23

# I2C address of DS1307
address_rtc = 0x68

# This look-up table is used to take care of the wierd connections of MCP23008 ICs to the displays
# A dictionary is used with key-value pairs. 'dig' key is only for reference and not used in code
# IC1-DISP1 key corresponds to the data to be sent to MCP23008 (0x20) to display part of the 'dig' digit on it.
# Similarly, IC2-DISP1 value should be sent to MCP23008 (0x21) at the same time to display the remaining part of 'dig' digit on it

min_data = (
    {'dig': 0, 'IC1-DISP1': 0xE0, 'IC2-DISP1': 0x38, 'IC1-DISP2': 0x0E, 'IC2-DISP2': 0x83},
    {'dig': 1, 'IC1-DISP1': 0x40, 'IC2-DISP1': 0x08, 'IC1-DISP2': 0x08, 'IC2-DISP2': 0x80},
    {'dig': 2, 'IC1-DISP1': 0xD0, 'IC2-DISP1': 0x30, 'IC1-DISP2': 0x0D, 'IC2-DISP2': 0x03},
    {'dig': 3, 'IC1-DISP1': 0xD0, 'IC2-DISP1': 0x18, 'IC1-DISP2': 0x0D, 'IC2-DISP2': 0x82},
    {'dig': 4, 'IC1-DISP1': 0x70, 'IC2-DISP1': 0x08, 'IC1-DISP2': 0x0B, 'IC2-DISP2': 0x80},
    {'dig': 5, 'IC1-DISP1': 0xB0, 'IC2-DISP1': 0x18, 'IC1-DISP2': 0x07, 'IC2-DISP2': 0x82},
    {'dig': 6, 'IC1-DISP1': 0xB0, 'IC2-DISP1': 0x38, 'IC1-DISP2': 0x07, 'IC2-DISP2': 0x83},
    {'dig': 7, 'IC1-DISP1': 0xC0, 'IC2-DISP1': 0x08, 'IC1-DISP2': 0x0C, 'IC2-DISP2': 0x80},
    {'dig': 8, 'IC1-DISP1': 0xF0, 'IC2-DISP1': 0x38, 'IC1-DISP2': 0x0F, 'IC2-DISP2': 0x83},
    {'dig': 9, 'IC1-DISP1': 0xF0, 'IC2-DISP1': 0x18, 'IC1-DISP2': 0x0F, 'IC2-DISP2': 0x82}
)

# Similar array as above for the hours digits.
# Here IC1 actually corresponds to MCP23008 (0x22) and IC2 is MCP23008 (0x23)
# Last two entries for digit 0 were 0xe0 and 0x0d. Made them 0x00 for leading zero blanking on the hours display
hrs_data = (
    {'dig': 0, 'IC1-DISP1': 0x0E, 'IC2-DISP1': 0xE0, 'IC1-DISP2': 0x00, 'IC2-DISP2': 0x00},
    {'dig': 1, 'IC1-DISP1': 0x02, 'IC2-DISP1': 0x80, 'IC1-DISP2': 0x20, 'IC2-DISP2': 0x08},
    {'dig': 2, 'IC1-DISP1': 0x0C, 'IC2-DISP1': 0xD0, 'IC1-DISP2': 0xC0, 'IC2-DISP2': 0x0E},
    {'dig': 3, 'IC1-DISP1': 0x06, 'IC2-DISP1': 0xD0, 'IC1-DISP2': 0x60, 'IC2-DISP2': 0x0E},
    {'dig': 4, 'IC1-DISP1': 0x02, 'IC2-DISP1': 0xB0, 'IC1-DISP2': 0x20, 'IC2-DISP2': 0x0B},
    {'dig': 5, 'IC1-DISP1': 0x06, 'IC2-DISP1': 0x70, 'IC1-DISP2': 0x60, 'IC2-DISP2': 0x07},
    {'dig': 6, 'IC1-DISP1': 0x0E, 'IC2-DISP1': 0x70, 'IC1-DISP2': 0xE0, 'IC2-DISP2': 0x07},
    {'dig': 7, 'IC1-DISP1': 0x02, 'IC2-DISP1': 0xC0, 'IC1-DISP2': 0x20, 'IC2-DISP2': 0x0C},
    {'dig': 8, 'IC1-DISP1': 0x0E, 'IC2-DISP1': 0xF0, 'IC1-DISP2': 0xE0, 'IC2-DISP2': 0x0F},
    {'dig': 9, 'IC1-DISP1': 0x06, 'IC2-DISP1': 0xF0, 'IC1-DISP2': 0x60, 'IC2-DISP2': 0x0F}
)

# Deine all the registers of MCP23008. Refer datasheet for details
IODIR = 0x00
IPOL = 0x01
GPINTEN = 0x02
DEFVAL = 0x03
INTCON = 0x04
IOCON = 0x05
GPPU = 0x06
INTF = 0x07
INTCAP = 0x08
GPIOMCP = 0x09
OLAT = 0x0A

# Define all the registers for DS1307. Refer datasheet for details
SECONDS = 0x00
MINUTES = 0x01
HOURS = 0x02
DAY = 0x03
DATE = 0x04
MONTH = 0x05
YEAR = 0x06

CONTROL = 0x0E
STATUS = 0x0F

#########################################

# Trigger is input to HC-SR04 so output from Pi
GPIO.setup(TRIG,GPIO.OUT)
# Echo is output from HC-SR04 so input to Pi
GPIO.setup(ECHO,GPIO.IN)

# Just an arbitrary delay for HC-SR04 to be stable, may not be required..
time.sleep(2)

bus = smbus.SMBus(0) # Change to 1 for newer Raspberry Pi (Post Oct-2012)

# Set IODIR as OUTPUT
bus.write_byte_data(mcp_address1, IODIR, 0b00000000)
bus.write_byte_data(mcp_address2, IODIR, 0b00000000)
bus.write_byte_data(mcp_address3, IODIR, 0b00000000)
bus.write_byte_data(mcp_address4, IODIR, 0b00000000)

# Reset all the other registers, though their default values are already 0x00.
# It also ensures that at power on, all LEDs are off unless explicitly controlled (Register DEFVAL)

for reg in [IPOL,GPINTEN,DEFVAL,INTCON,IOCON,GPPU,INTF,INTCAP,GPIOMCP,OLAT]:
    bus.write_byte_data(address, reg, 0b00000000)
    bus.write_byte_data(address2, reg, 0b00000000)
    bus.write_byte_data(address3, reg, 0b00000000)
    bus.write_byte_data(address4, reg, 0b00000000)
    bus.write_byte_data(address, GPIOMCP, 0x00)
    bus.write_byte_data(address2, GPIOMCP, 0x00)
    bus.write_byte_data(address3, GPIOMCP, 0x00)
    bus.write_byte_data(address4, GPIOMCP, 0x00)

# Serves as a kind of display test. It will show sequence 0000, 1111, 2222 and so on up to 9999 on the display with a 200msec delay
for index in range(0,10):
    bus.write_byte_data(address, GPIOMCP, min_data[index]['IC1-DISP1'] | min_data[index]['IC1-DISP2'])
    bus.write_byte_data(address2, GPIOMCP, min_data[index]['IC2-DISP1'] | min_data[index]['IC2-DISP2'])

    bus.write_byte_data(address3, GPIOMCP, hrs_data[index]['IC1-DISP1'] | hrs_data[index]['IC1-DISP2'])
    bus.write_byte_data(address4, GPIOMCP, hrs_data[index]['IC2-DISP1'] | hrs_data[index]['IC2-DISP2'])

    time.sleep(0.2)

# Turn of all displays after display test is done
bus.write_byte_data(address, GPIOMCP, 0x00)
bus.write_byte_data(address2, GPIOMCP, 0x00)
bus.write_byte_data(address3, GPIOMCP, 0x00)
bus.write_byte_data(address4, GPIOMCP, 0x00)

#print "display test done"

# Initialize variables used later
showtime = False
nowtime = futuretime = time.time()

# Continuous loop starts
# All the print statements are left for debugging later if required
while True:
    # Start with setting the trigger pin to LOW. Wait for a second to help sensor be stable.
    # This delay of 1 second may be reduced but may affect performance sometimes.
    GPIO.output(TRIG, False)
#    print "Waiting For Sensor To Settle"
    time.sleep(1)

    # Now send a pulse of 10usec (as per datasheet) on the trigger pin
    GPIO.output(TRIG, True)
    time.sleep(0.00001)
    GPIO.output(TRIG, False)

    # After the pulse is sent, wait for a rising signal on the ECHO pin.
    while GPIO.input(ECHO)==0:
        pulse_start = time.time()

    while GPIO.input(ECHO)==1:
        pulse_end = time.time()

    # The time duration from sending trigger to getting a rising pulse on echo pin corresponds to distance
    # Note that the obtained value is in seconds
    pulse_duration = pulse_end - pulse_start

    # Multiply the duration by a constant. This constant is based on speed of sound but also corresponds to datasheet.
    # Sparkfun datasheet says to divide the duration (in usec) by 58 which corresponds to multiplying the seconds value by 17241
    # Based on speed of sound as 343m/sec, the distance can be calculated as time required / 2 (send and receive time)
    # Thus (time/2 * 343 * 100) = Distance. Since the speed is in meters, we convert it to cm by multiplying by 100
    # This comes to Distance = 17150 * time duration which is used in the following formula
    # This has not been calibrated though but roughly verified to see that the distance is approximately correct.
    distance = pulse_duration * 17150

    # Round off
    distance = round(distance, 2)

#    print "Distance:",distance,"cm"

    # If the obstacle is within 10 cm of the sensor, we start reading the RTC and displaying it on display
    # We also start a timer of 30 seconds to display the time and then the display is switched off.
    # Within these 30 seconds, if any object is again in vicinity (10cm), the timer is restarted.
    if distance < 10:
        #print "Object found in vicinity, show time"
        syslog.syslog("USONIC distance: " + str(distance))
        nowtime = time.time()
        futuretime = nowtime + 30
        showtime = True

    if showtime:
        if time.time() < futuretime:
            # We only need hours and minutes
            mints = bus.read_byte_data(address_rtc, MINUTES)
            hrs = bus.read_byte_data(address_rtc, HOURS)

            # Convert the BCD data masking off any unwanted bits from the data read
            min_data_2 = (mints & 0xF0) >> 4
            min_data_1 = (mints & 0x0F)

            # Hours data contains a few more bits, mask them off as we don't need them
            hrs_data_2 = (hrs & 0x10) >> 4
            hrs_data_1 = (hrs & 0x0F)

            # Corresponding to the data to be displayed, pull out the GPIO data to be sent.
            # It also needs to be ORed because we are going to control two different digits from the same IC
            # The data for these 2 digits is ORed for an IC and sent to it.
            bus.write_byte_data(mcp_address1, GPIOMCP, min_data[min_data_1]['IC1-DISP1'] | min_data[min_data_2]['IC1-DISP2'])
            bus.write_byte_data(mcp_address2, GPIOMCP, min_data[min_data_1]['IC2-DISP1'] | min_data[min_data_2]['IC2-DISP2'])

            bus.write_byte_data(mcp_address3, GPIOMCP, hrs_data[hrs_data_1]['IC1-DISP1'] | hrs_data[hrs_data_2]['IC1-DISP2'] | 0x01)
            bus.write_byte_data(mcp_address4, GPIOMCP, hrs_data[hrs_data_1]['IC2-DISP1'] | hrs_data[hrs_data_2]['IC2-DISP2'])

        else:
            #print "Time over"
            # Switch off the display
            showtime = False
            bus.write_byte_data(address, GPIOMCP, 0x00)
            bus.write_byte_data(address2, GPIOMCP, 0x00)
            bus.write_byte_data(address3, GPIOMCP, 0x00)
            bus.write_byte_data(address4, GPIOMCP, 0x00)

    # Following else section is not used.
    #else:
        #bus.write_byte_data(address, GPIOMCP, 0x00)
        #bus.write_byte_data(address2, GPIOMCP, 0x00)
        #bus.write_byte_data(address3, GPIOMCP, 0x00)
        #bus.write_byte_data(address4, GPIOMCP, 0x00)
        #showtime = False

# The program won't come here in normal flow
GPIO.cleanup()

The code is with comments so should be self explanatory. Basically I am setting up the I2C devices and HC-SR04 GPIO pins, then checking for the distance sensed by HC-SR04 to be within 10cm and if so, reading the RTC and displaying the time on display for around 30 seconds.

There is a small script to set the time as well, based on the Pi’s time. If your Pi is connected to Internet it will update its time using NTP servers usually. The script here will write the current time to RTC.

I wanted to keep this clock super simple so didn’t add any AM/PM indicator or flashing LEDs etc. However, I plan to add more features it later such as alarm etc.

Do let me know if you liked this project and if you need any help in replicating it.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s