Tuning PID controller for sharp turns in line follower robot

I'm working on a TWDD line-following robot using qtr-8a reflectance sensor array. The sensors are connected to BeagleBone Black, and BBB is sending the speed serially to an arduino Due. My approach is using a PID controller for the sensor so the error equals to zero when the robot is centred on the line and a positive/negative error depending on the robot's position. Applying Trial and error method I finally reached a Kp value that tracks straight lines perfectly. However, I'm still unable to turn and stay on the line even on a similarly smooth turns. I guess this is related to the Kd value. I'm not using the integral part Ki since the error is keep increasing. I tried to set conditions when the robots is drifting away from the line but it's not working probably (even without the conditions it is somehow turning smoother but then losing the line) I'm using the following draft code:

from bbio import *
import time

integral = 0
last_prop = 0
Kp = 20
Ki= 0
Kd = 150
amax = list(0 for i in range(0,8))
amin = list(1024 for i in range(0,8))
timeout = time.time() + 10

# Read ADC data from MCP3008
# ch: 0-7, ADC channel
# cs: 0-1, SPI chip select
# See MCP3008 datasheet p.21
def adc_read(cs, ch):
spidata = SPI1.transfer(cs, [1,(8+ch)<<4, 0])
data = ((spidata[1] & 3) << 8) + spidata[2]

return data

def setup():
# SPI1 setup
Serial2.begin(9600)
Serial5.begin(9600)
pinMode(GPIO1_7, OUTPUT)
digitalWrite(GPIO1_7, LOW)
SPI1.begin()
SPI1.setMaxFrequency(0,50000)  # => ~47kHz, higher gives occasional false readings
#  SPI1.setMaxFrequency(1,50000)
calibrate()

# reading the IR sensor data
def read_sensors():
sensors = []
for i in range(8):
sensors.append(adc_read(0,i))

return sensors
# calculating the error from PID controller
def calc_pid(x,sp):
global  integral, last_prop , Kp, Ki ,Kd
set_point = sp
pos = sensor_average(x)/sensor_sum(x)
prop = pos - set_point
integral = integral + prop
deriv = prop - last_prop
last_prop = prop
error = (prop*Kp + integral*Ki + deriv*Kd)/100

return error

def get_position(s):
return sensor_average(s)/sensor_sum(s)

def sensor_average(x):
avg = 0
for i in range(8):
avg += x[i]*i*100

return avg

def sensor_sum(x):
sum = 0
for i in range(8):
sum += x[i]
return sum

def get_sensor(x):
j= read_sensors()[x]
return j

def calc_setpoint(x,y):
avg = 0
sum = 0
for i in range(8):
avg += (x[i]-y[i])*i*100
sum += x[i]-y[i]
return avg/sum
# calibrate the sensors reading to fit any given line
def calibrate():
global amin
global amax

while(time.time() < timeout):
for i in range(0,8):
amin[i] = min(amin[i],read_sensors()[i])
amax[i] = max(amax[i],read_sensors()[i])
digitalWrite(GPIO1_7, HIGH)
digitalWrite(GPIO1_7,LOW)

# calculating the correspondent speed
def calc_speed(error):
avg_speed = 150
min = 100
pos = get_position(read_sensors())
speed = []
if(error < -35):
right = avg_speed -(2*error)
left = avg_speed + (2*error)
elif(error > 35):
right =avg_speed - (2*error)
left = avg_speed + (2*error)
else:
right = avg_speed - (error)
left = avg_speed + (error)
speed.append(right)
speed.append(left)
return speed

def loop():

s = read_sensors()
setpoint = calc_setpoint(amax,amin)
position = get_position(s)
err = calc_pid(s,setpoint)

print err

#print "divided by 100:"
speeds = calc_speed(err)
print speeds
right_motor = speeds[0]
left_motor = speeds[1]

Serial2.write(right_motor)

Serial5.write(left_motor)
delay(10)

run(setup,loop)


PS: the sent speed over serial is limited to 255 and I'm multiplying it by a factor from the Arduino side.

1 Answer

While calculating speeds, you are multiplying by 2*error. If PID (or PD) calculates an error, I believe it should be fed directly to the motors because in each loop it does not account for an additional 2* multiplier in speed in the next loop. That would act like an unforeseen impulse that is unaccounted for by the PID. So for starters, I would suggest try removing the 2*error term, compare to a smaller error (or as required) and go without the last else-if condition like below:

if(error < -20):
right = avg_speed -(error)
left = avg_speed + (error)
elif(error > 20):
right =avg_speed - (error)
left = avg_speed + (error)


Secondly; If you are going with hit-&-trial calculation of the PID constants, I would suggest, start with just P and get a decent line following (with manageable oscillations at low speeds). Then add an integral term (PI controller) and test it out.The Differential term can be added once the other two are giving a suitable response.

• first thanks for your reply. The multiplied error is only an approach I was trying so the robot would respond quicker for big errors (away from the line). I was using only right = avg_speed -(error) left = avg_speed + (error) but it didn't work as wanted for slightly sharper turns. Regarding the PD controller, I'm able to follow straight lines perfectly with my P value. Adding the D part enhanced the performance a bit for turns. However, Adding the Integral part caused a big overshoots, even the error value kept increasing when I added the integral term. – DieDen Dec 28 '16 at 11:53
• Well, for quicker response, always increase the proportional constant.Because it is directly multiplied with error, so it effects it more. You can incorporate the multiplier by 2 by increasing the proportional constant by say 1.5 to start with. As for the integral part, since PID is best designed once a system model is developed first (normal control systems) so hit-and-trial will be difficult to get right immediately. PD is easier to correct as the effects are move visible, while integral increases/decreases over time hence difficult to keep track of without mathematical models. – Atif Anwer Dec 29 '16 at 2:05