"I was ridin' in a getaway car, I was cryin' in a getaway car" - Taylor Swift
The Getaway Car™ is an autonomous vehicle that is capable of lane-keeping - staying in between lanes in a course, including steering. When the Getaway Car™ encounters a (first) stop box, it stops, and after a few seconds keeps going. When it encounters a second stop box, it stops. The Getaway Car™ uses a BeagleBone AI 64 and a webcam to capture footage of the course and act accordingly.
Based on code and concepts from these Hackster projects:
- https://www.hackster.io/really-bad-idea/autonomous-path-following-car-6c4992
- https://www.hackster.io/wall-ee/lane-keeping-rc-car-using-beaglebone-black-0da1f3
- User raja_961, “Autonomous Lane-Keeping Car Using Raspberry Pi and OpenCV”. Instructables. URL: https://www.instructables.com/Autonomous-Lane-Keeping-Car-Using-Raspberry-Pi-and/
Check out a video demonstration here!
The car uses PWM control over a servo and ESC in order to both steer the car and drive forward or backwards. Initializing the output pins and setting their duty cycle as shown below sets the car to not move and steer straight, and be ready for additional duty cycle changes in order to drive.
# P9_14 - Speed/ESC
import os
#set dutycycle, period, and enable for ESC
with open('/dev/bone/pwm/1/a/period', 'w') as filetowrite:
filetowrite.write('20000000')
with open('/dev/bone/pwm/1/a/duty_cycle', 'w') as filetowrite:
filetowrite.write('1550000')
with open('/dev/bone/pwm/1/a/enable', 'w') as filetowrite:
filetowrite.write('1')
# P9_16 - Steering
#set dutycycle, period, and enable for steering servo
with open('/dev/bone/pwm/1/b/period', 'w') as filetowrite:
filetowrite.write('20000000')
with open('/dev/bone/pwm/1/b/duty_cycle', 'w') as filetowrite:
filetowrite.write('1500000')
with open('/dev/bone/pwm/1/b/enable', 'w') as filetowrite:
filetowrite.write('1')
In order to determine how fast the car is moving, an optical encoder is mounted so that it can see slits in a wheel that rotates upon the drive shaft of the car. This encoder is powered via the Beaglebone-AI-64 and the output pin is routed to one of the GPIO pins of the BBAI, along with a debouncing capacitor. A Kernel level module is used to handle interrupts generated by the encoder and calculate the duration between these interrupts, and the duration value is then printed to the kernel log. The ISR doing this is shown below, including comments describing functionality.
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/kernel.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/ktime.h>
#include <linux/unistd.h>
//Initialize IRQ number and encodergpiodesc
unsigned int irq_number;
struct gpio_desc *encodergpio;
ktime_t start, end, elapsed_time;
//Ktime values to track encoder duration
//IRQ handler which records time since last ISR and prints it to kernel log to be read by the driving code
static irq_handler_t gpio_irq_handler(unsigned int irq, void *dev_id, struct
pt_regs *regs) {
end = ktime_get();
elapsed_time = ktime_sub(end,start);
start = ktime_get();
printk("Encoder has ticked, time between: %llu \n",elapsed_time);
return (irq_handler_t) IRQ_HANDLED;
}
// probe function
static int led_probe(struct platform_device *pdev)
{
//Start first time interval upon init
start = ktime_get();
//Define encodergpio
encodergpio = devm_gpiod_get(&pdev->dev, "userbutton", GPIOD_IN);
//debounce encoder input
gpiod_set_debounce(encodergpio, 1000000);
//initialize IRQ
irq_number = gpiod_to_irq(encodergpio);
//Check requests
if(request_irq(irq_number, (irq_handler_t) gpio_irq_handler,
IRQF_TRIGGER_RISING, "my_gpio_irq", NULL) != 0){
printk("Error!\nCan not request interrupt nr.: %d\n", irq_number);
return -1;
}
//Initilization complete
printk("Module is inserted and Button is mapped to IRQ Nr.: %d\n", irq_number);
return 0;
}
// remove function
static int led_remove(struct platform_device *pdev)
{
//Turn off LED and free IRQ
free_irq(irq_number, NULL);
printk("Module removed and IRQ freed\n");
return 0;
}
//Match compatible
static struct of_device_id matchy_match[] = {
{.compatible = "hello"},
{},
};
// platform driver object
static struct platform_driver adam_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "The Rock: this name doesn't even matter",
.owner = THIS_MODULE,
.of_match_table = matchy_match,
},
};
module_platform_driver(adam_driver);
MODULE_DESCRIPTION("424's finest");
MODULE_AUTHOR("GOAT");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:adam_driver");
Once this module has been made and inserted into the kernel, a small bit of code can be used in a python file to read the time between encoder tics:
#testfile to pull the latest encoder timing from the kernel log (if the last log input is an encoder timing)
with open('/var/log/kern.log') as f:
lines = f.readlines()
lastline = lines[len(lines)-1]
array = lastline.split()
if(array[11]=="between:"):
print(array[12])
In order to test different turning values and speed, a simple python script can be used which changes the PWM duty cycle for steering and ESC based upon command line inputs:
#Test file to drive and turn the car using command line args.
#Will ask to type 1 or 2 to control speed or steering, then a value
#1 - 10 to control left-right or speed value
while(1):
print("Steer: 1 Speed: 2")
choice = int(input())
print("value, 1 to 10")
given = int(input())
speedvals = (["1550000","1620000","1630000","1640000","1650000","1660000","167000 0","1680000","1690000","1800000"])
steervals = ["1100000","1200000","1300000","1400000","1500000","1600000","1700000","1800000","1900000","2000000"]
if(choice == 1):
with open('/dev/bone/pwm/1/b/duty_cycle', 'w') as filetowrite:
filetowrite.write(steervals[given-1])
elif (choice == 2):
with open('/dev/bone/pwm/1/a/duty_cycle', 'w') as filetowrite:
filetowrite.write(speedvals[given-1])
A small bit of code like that shown below can be used to maintain a constant speed, with the AI64 varying the PWM value based upon the duration between encoder tics. It will constantly work to keep the duration value between encoder ticks around a certain value:
def go():
with open('/dev/bone/pwm/1/a/duty_cycle', 'w') as filetowrite:
filetowrite.write(str(go_forward))
# start engines
go_forward = 1600000
go()
#forever
while(1):
#Loop should repeatedly vary the pwm wave to keep the encoder timing at a proper amount
#read lastest encoder value
#can place this in the calculation loop of the main driving code to keep the speed around a certain amount
with open('/var/log/kern.log') as f:
lines = f.readlines()
lastline = lines[len(lines)-1]
array = lastline.split()
if(array[11]=="between:"):
encoderval = int(array[12])
print(encoderval)
#if slower than a certain timing
if (encoderval > 12500000):
#increase duty cycle
go_forward = str(int(go_forward) + 1000)
else:
#decrease duty cycle
go_forward = str(int(go_forward) - 1000)
go()
#enter encoderdriver mod to begin tracking encoder timings
os.system("sudo modprobe encoderdriver")
Handling stop signs
Stop signs on the ground were detected by converting the video feed from the camera into an HSV format and using OpenCV functions to determine if the majority of the frame was the color red above a particular threshold. The lane detection code is structured to continually check for the stop sign while it turns to stay within the boundaries, and if it does detect the sign the speed PWM is set to 0% so that the car will stop. The car waits two seconds, then resets the speed PWM to begin moving again. It checks for the second stop sign less frequently so that it doesn't accidently detect the first stop sign again, and once the second stop sign is detected in the video frame the car stops again and the lane detection code ends.
PWM and Error PlotsBelow are plots of the error response and PWM values that the car calculated when running the track.
The resolution used for the computer vision of the project was determined by testing the most resolution the BeagleBone could handle and still reasonably process each video frame in a timely manner. The actual resolution used (320 x 240) was much lower than the resolution that the camera could provide, but it was enough to allow OpenCV functions determine the slope of the blue lane markers in the region of interest of the frame. Using the lower resolution significally helped with performance and allowed the car to quickly adjust its trajectory along the track in a timely manner. The proportional gain and derivative gain were adjusted through several iterations of trial-and-error when testing the car on the track. Because of the range of values that could be used to change the steering PWM, both gains were small (<.1) to allow the car to make the larger turn of the track without overcompensating and losing sight of either lane marker.
Comments
Please log in or sign up to comment.