Figure 1 : System
This project was designed to show that you can't solve every problem in software, that you need to understand a system's limitations and that adding hardware can actually simplify software development. Aim: to implement a system that scans 180 degrees, measure the distance to the nearest obstacle and display these as a polar plot. One system will be implemented in software (as much as possible), another using both software and dedicated hardware. Both systems use a common sensor platform as shown in figure 2. Position control is implemented using a standard analogue model servo (Link), distance measurement via an off the shelf, HC-SR04 ultrasonic module (Link). The 3D printed brackets where obtained from: (Link) (Link), the bottom base bracket was quickly done in openScad (Link). The base was laser cut, layout drawn in LibreCad (Link). All 3D model and dxf files are available here (Local).
Figure 2 : Sensor platform
Figure 3 : 3D printed parts
Figure 4 : Plastic base
The software biased system requires minimal external hardware, as shown in figures 5 and 6. The resistors shown are purely for protection i.e. the Raspberry Pi is not 5V tolerant. Resistors R2 and R4 are not really needed, these are 41 ohms (were to hand), just there to limit the current if i do something silly. Resistors R1 and R3 (4K7) form a potential, shifting the ultrasonic module's output pulse from 5V down to 2.5V i.e. Pi safe.
Figure 5 : Software system interface
Figure 6 : Software interface circuit diagram
The servo and ultrasonic sensor are controlled using a simple python prgram. The ultrasonic sensor control was based on this example: (Link) (Local). The servo control was based on this example: (Link) (Local). The full code listing is shown below:
import RPi.GPIO as GPIO
import time
# Servo position control func
def update(angle):
duty = float(angle) / 10 + 2.5
pwm.ChangeDutyCycle(duty)
#IO pin numbers
TRIG = 17
ECHO = 27
SERVO = 18
#Setup IO ports
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(TRIG, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
GPIO.setup(SERVO, GPIO.OUT)
pwm = GPIO.PWM(SERVO, 100)
pwm.start(5)
GPIO.output(TRIG, False)
#Initialise variables
direction = True
angle = 0
pos = 0
pos_prev = 17
pulse_start = 0
pulse_end = 0
b = [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ]
c = [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ]
#Main loop
while True :
#Send trigger (start) pulse to US sensor
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
#Wait until RX signal 0 or max delay
timeout = 0
while (GPIO.input(ECHO)==0) and (timeout < 50):
timeout = timeout + 1
pulse_start = time.time()
#Wait for RX signal
while GPIO.input(ECHO)==1:
pulse_end = time.time()
#Calculate time of flight
pulse_duration = pulse_end - pulse_start
#convert time to distance
distance = pulse_duration * 17150
distance = round(distance, 2)
#Trap if no pulse recieved
if distance < 0:
distance = 0
distance = distance * 10
#Set max distance for display
if distance > 600:
distance = 600
print "Distance: ", distance,"mm", "Angle: ", pos
#Move sensor head
update(angle)
time.sleep(0.5)
#update log file
file = open('plot_1.dat', 'w')
outputString = "\n"
#this array contains a single value i.e. scan line on plot
c[pos] = 600
c[pos_prev] = 0
#update distance string with new value
for i in range(0,18):
if pos == i:
b[i] = distance
outputString = outputString + str(i*10) + "\t" + str(b[i]) + "\t" + str(c[i]) + "\n"
#write data to file
file.write(outputString)
file.close
#update scan direction and position
pos_prev = pos
if direction:
angle = angle + 10
pos = pos + 1
else:
angle = angle - 10
pos = pos - 1
if angle > 180:
direction = False
angle = 170
pos = 17
if angle < 0:
direction = True
angle = 10
pos = 1
This program produces a log file, three columns, [Angle : Range to Object : Scan Line], as shown below:
0 106.5 0 10 600 0 20 600 0 30 600 0 40 83.7 0 50 73.8 0 60 66.2 0 70 64.8 0 80 88.6 0 90 600 0 100 600 600 110 83.2 0 120 83.4 0 130 69.6 0 140 84.9 0 150 107.2 0 160 106.6 0 170 69.1 0
A separate shell script program then reads this file and plots this data to the screen using GnuPlot. This automatically rereads this file and updates the display every 0.5 seconds. A screen shot of the display is shown in figure 7. RED regions indicate open space, WHITE regions indicate the distance to the closest object, the current sensor position is indicated by the GREEN scan line. A video of the displayed plot is available here: (Video). Figure 8 shows the objects in front of the scanning sensor (circled in red) for this plot, to its right is the stacked tables, to its left open space and the other sensor platform.
File: go.sh
#!/bin/bash gnuplot plot.gnu
File: plot.gnu
unset border
set polar
set angles degrees
set style line 10 lt 1 lc 0 lw 0.3
set grid polar 60
set grid ls 10
set xrange [-700:700]
set yrange [-700:700]
set xtics axis
set ytics axis
set xtics scale 0
set xtics ("" 100, "" 200, "" 300, "" 400, "" 500, "" 600)
set ytics 0, -100, -600
set size square
unset key
set_label(x, text) = sprintf("set label '%s' at (750*cos(%f)), (750*sin(%f)) center", text, x, x) #this places a label on the outside
#here all labels are created
eval set_label(0, "0")
eval set_label(60, "60")
eval set_label(120, "120")
eval set_label(180, "180")
eval set_label(240, "240")
eval set_label(300, "300")
#set style line 11 lt 1 lc 3 lw 2 pt 2 ps 2 #set the line style for the plot
plot "plot.dat" using 1:2 with filledcurves, "plot.dat" using 1:3 with line
pause 0.5
reread<
Figure 7 : Screen plot
Figure 8 : Table
This program works, but the timing of the servo pulse does vary a little, resulting in servo jitter i.e. delays in receiving the ultrasonic signal, operating system calls, or just simply updating the mouse pointer position etc, means that the servo pulse is not the correct length or is not produced at the correct time, as shown in figure 9. A video of the software controlled servo is available here: (Video).
Figure 9 : Pulse jitter
To remove this jitter the servo can be controlled by dedicated hardware i.e. using a CPLD, as shown in figures 10 - 13. The Raspberry Pi tells the CPLD what position to turn the servo to, low level control is now done in hardware, therefore, any small changes in the software timings no longer affects the servo control pulse. This hardware was based on the following example: (Link) (Local). To further simplify the software, control of the ultrasonic sensor can also be passed to the CPLD. The Raspberry Pi now requests a distance measurement from the CPLD which returns a 7 bit value. Again all low level control / timing is performed in hardware on the CPLD, a simple state machine, coded in VHDL.
Figure 10 : Hardware system interface
Figure 11 : CPLD
Figure 12 : Clock (4MHz) and Buffer
Figure 13 : I2C interface
Figure 14 : I2C interface IC
Figure 15 : I2C interface internals
To allow the Raspberry Pi to communicate to the CPLD it needs a data link. The simplest way to do this is to used three PCF8574 I2C 8bit IO expanders (Local), as shown in figures 13 - 15. Two are set up as outputs (servo position and US sensor control) and one as an input (US sensor data), the Raspbery Pi can then read or write to these I2C mapped addresses. Connected to these parallel ports is the CPLD as shown in figure 16 - 17. This operates from a 4MHz clock, with an internal clock divider generating the timing signals needed for each hardware subsystem. Individual VHDL files and the complete CPLD ISE project is available here:
Figure 16 : Top Level Hardware Controller, VHDL (left), Symbol (right)
Figure 17 : Hardware Controller
Top level control is implemented in another python program, shown below:
import smbus
import time
POS_I2C_ADDR = 0x38
DIS_I2C_ADDR = 0x39
CTL_I2C_ADDR = 0x3A
bus = smbus.SMBus(1)
pos = 0
pos_prev = 31
direction = True
# Distance (37)
b = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
c = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
bus.write_byte(DIS_I2C_ADDR, 0xFF)
while True:
# Trigger TX
bus.write_byte(CTL_I2C_ADDR, 0x01)
bus.write_byte(CTL_I2C_ADDR, 0x00)
# Time to send start = 2 x 50us
# Start pulse 200us
# Max RX wait 10ms
# Software delay 0.1s = 100ms
time.sleep(0.1)
# Time to read 50us
data = bus.read_byte(DIS_I2C_ADDR)
# print "Distance: " + str(data)
# Speed of sound 340m/s, clock 64KHz = 15us
# 1 sec = 340,000mm
# 1 tick = 2.5mm
distance = float(data) * 2.5
# Max distance = 255 x 2.5 = 638mm
if distance > 600:
distance = 600
# print "Distance: " + str(data)
file = open('plot.dat', 'w')
outputString = "\n"
c[pos] = 600
c[pos_prev] = 0
for i in range(0,31):
if pos == i:
b[i] = distance
outputString = outputString + str(i*6) + "\t" + str(b[i]) + "\t" + str(c[i]) + "\n"
file.write(outputString)
file.close
pos_prev = pos
if direction:
pos = pos + 1
else:
pos = pos - 1
if pos == 31:
direction = False
pos = 29
if pos < 0:
direction = True
pos = 1
# Print Angle
print "Angle: " + str( pos ) + " Distance: " + str(data)
bus.write_byte(POS_I2C_ADDR, int(float(pos) * 3.5))
time.sleep(0.2)
As servo control is implemented in dedicated hardware, timing resolution can be more accurately controlled, allowing more readings to be taken during a scan. A video of the hardware controlled servo is available here: (Video).
Figure 18 : System
The GNUplot display was a bit of a bodge, a quick and dirty solution. Finally got around to getting a nicer GUI working using Tkinter. Do confess GUIs are not my thing, to simplify the job went for a simple gif background (shown in figure 19) and canvas + lines.
Figure 19 : GIF background
Test code is below, current reading is displayed in green, readings recorded during the current sweep are a light blue, readings recorded on the previous sweep a dark blue. A video of the software display random test data value is available here: (Video).
import Tkinter as tk
import math
import random
canvasWidth = 860
canvasHeight = 860
canvasCenterX = (canvasWidth/2)
canvasCenterY = (canvasHeight/2)
centerRadius = 10
angleOffset = 90
lineWidth = 3
lineLength = 300
lineNumber = 0
lineObjects = []
colourScale = [(0,255,0), (0, 208, 226), (0,190,207), (0,170,188), (0,150,162), (0,130,142), (0,110,121), (0,90,100), (0,75,75), (0,50,50)]
clockwise = True
anticlockwise = False
# Init measurement array (36)
measurement = []
for i in range(0, 185, 5):
ran_number = random.random()
distance = int(ran_number*300)
measurementData = (i, distance)
measurement.append( measurementData )
def updateDisplay():
global lineNumber, lineObjects, clockwise, anticlockwise, measurement
#print len(measurement), lineNumber
angle = measurement[lineNumber][0]
length = measurement[lineNumber][1]
dx = length * math.sin(math.radians(angleOffset - angle))
dy = length * math.cos(math.radians(angleOffset - angle))
colour = '#%02x%02x%02x' % colourScale[0]
lineObjects.append(canvas.create_line(canvasCenterX, canvasCenterY, dx+canvasCenterX, dy+canvasCenterY, width=lineWidth, fill=colour))
if lineNumber != 0:
if clockwise:
canvas.delete(lineObjects[lineNumber-1])
angle = measurement[lineNumber-1][0]
length = measurement[lineNumber-1][1]
dx = length * math.sin(math.radians(angleOffset - angle))
dy = length * math.cos(math.radians(angleOffset - angle))
colour = '#%02x%02x%02x' % colourScale[1]
lineObjects[lineNumber-1] = canvas.create_line(canvasCenterX, canvasCenterY, dx+canvasCenterX, dy+canvasCenterY, width=lineWidth, fill=colour)
else:
if lineNumber < (len(measurement)-1):
canvas.delete(lineObjects[lineNumber+1])
angle = measurement[lineNumber+1][0]
length = measurement[lineNumber+1][1]
dx = length * math.sin(math.radians(angleOffset - angle))
dy = length * math.cos(math.radians(angleOffset - angle))
colour = '#%02x%02x%02x' % colourScale[1]
lineObjects[lineNumber+1] = canvas.create_line(canvasCenterX, canvasCenterY, dx+canvasCenterX, dy+canvasCenterY, width=lineWidth, fill=colour)
if clockwise:
lineNumber +=1
else:
lineNumber -=1
if ((lineNumber == len(measurement)) and clockwise) or ((lineNumber == -1) and anticlockwise):
for i in range(0, len(lineObjects)):
canvas.delete(lineObjects[i])
lineObjects = []
for i in range(0, len(measurement)):
angle = measurement[i][0]
length = measurement[i][1]
dx = length * math.sin(math.radians(angleOffset - angle))
dy = length * math.cos(math.radians(angleOffset - angle))
colour = '#%02x%02x%02x' % colourScale[5]
lineObjects.append(canvas.create_line(canvasCenterX, canvasCenterY, dx+canvasCenterX, dy+canvasCenterY, width=lineWidth, fill=colour))
if clockwise:
clockwise = False
anticlockwise = True
lineNumber -=1
else:
clockwise = True
anticlockwise = False
lineNumber = 0
measurement =[]
for i in range(0, 185, 5):
ran_number = random.random()
distance = int(ran_number*300)
measurementData = (i, distance)
measurement.append( measurementData )
root.after(200, updateDisplay)
root = tk.Tk()
frame = tk.Frame(root)
frame.pack()
canvas = tk.Canvas(frame, width=canvasWidth, height=canvasHeight, bg='black')
canvas.pack(expand=tk.YES, fill=tk.BOTH)
photo=tk.PhotoImage(file='background2.gif')
canvas.create_image(35, 50, image=photo, anchor=tk.NW)
colour = '#%02x%02x%02x' % colourScale[8]
canvas.create_oval(canvasCenterX-centerRadius, canvasCenterY-centerRadius, canvasCenterX+centerRadius, canvasCenterY+centerRadius, width=0, fill=colour)
root.after(0, updateDisplay)
root.mainloop()

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Contact details: email - mike.freeman@york.ac.uk, telephone - 01904 32(5473)
Back