How to build a robot to win the Science Olympiad Robot Tour event

When I first got this event, I was excited because it involved something I enjoy: programming. I was determined to win. The rules for the event are simple: create a robot that can autonomously navigate a track to different points without bumping into the walls.

Before this, I had not done anything with electronics, and I had never taken a physics class, so I didn't really know where to start. I tried looking up some example robots from past years, but I didn't have much luck because this was the first year of the event. I searched online for a kit to start with, and ended up getting the OSOYOO V2.1 Robot Kit. This kit came with an Arduino Uno, and lots of sensors that I thought I may use. After putting the car together, it looked like this:

Picture of the robot car after I first put it together

There is two main things I need the robot to be able to do in order to get around the track. First, the robot needs to be able to go a precise amount of distance, and second the robot needs to be able to turn to a precise direction. This is because the robot needs to know exactly where it is on the track at all times, so that it does not hit the walls or go outside the track. In order to keep track of the distance the robot has traveled, I got a HC-020K Disc Encoder which I put on one of the front wheels. When the wheel turns, the encoder turns with it. The disc has slots in it that let a laser pass through, and you can use the amount of times the laser passed through the disc to determine the distance that the car has traveled.

Picture showing the wheel encoder disk used to measure distance

To measure the direction the robot is facing, I got an MPU-6050 sensor, which is an accelerometer and gysoscope. The gyroscope measures angular velocity, which can be integrated to get the angle. I used the library MPU6050_light to do this. Since it is integrated, the values become more and more inaccurate over time, but the robot is not running long enough for this to be an issue.

Here is some basic code to test various movements of the car:

1
#define speedPinR 9
2
    #define RightMotorDirPin1  12   
3
    #define RightMotorDirPin2  11
4
    #define speedPinL 6
5
    #define LeftMotorDirPin1  7
6
    #define LeftMotorDirPin2  8
7
    
8
    void initPins()
9
    {
10
        pinMode(RightMotorDirPin1, OUTPUT); 
11
        pinMode(RightMotorDirPin2, OUTPUT); 
12
        pinMode(speedPinL, OUTPUT);  
13
    
14
        pinMode(LeftMotorDirPin1, OUTPUT); 
15
        pinMode(LeftMotorDirPin2, OUTPUT); 
16
        pinMode(speedPinR, OUTPUT); 
17
        stop(); 
18
    } 
19
    
20
    void forwards()
21
    {
22
        digitalWrite(RightMotorDirPin1, HIGH);
23
        digitalWrite(RightMotorDirPin2,LOW);
24
        digitalWrite(LeftMotorDirPin1,HIGH);
25
        digitalWrite(LeftMotorDirPin2,LOW);
26
        analogWrite(speedPinL, 120);
27
        analogWrite(speedPinR, 100);
28
    }
29
    
30
    void reverse()
31
    {
32
        digitalWrite(RightMotorDirPin1, LOW);
33
        digitalWrite(RightMotorDirPin2,HIGH);
34
        digitalWrite(LeftMotorDirPin1,LOW);
35
        digitalWrite(LeftMotorDirPin2,HIGH);
36
        analogWrite(speedPinL, 120);
37
        analogWrite(speedPinR, 100);
38
    }
39
    
40
    void stop()
41
    {
42
        digitalWrite(RightMotorDirPin1, LOW);
43
        digitalWrite(RightMotorDirPin2,LOW);
44
        digitalWrite(LeftMotorDirPin1,LOW);
45
        digitalWrite(LeftMotorDirPin2,LOW);
46
    }
47
    
48
    void turnLeft(byte speed = 140)
49
    {
50
        digitalWrite(RightMotorDirPin1, HIGH);
51
        digitalWrite(RightMotorDirPin2,LOW);
52
        digitalWrite(LeftMotorDirPin1,LOW);
53
        digitalWrite(LeftMotorDirPin2,HIGH);
54
        analogWrite(speedPinL,speed);
55
        analogWrite(speedPinR,speed);
56
    }
57
    void turnRight(byte speed = 140)
58
    {
59
        digitalWrite(RightMotorDirPin1, LOW);
60
        digitalWrite(RightMotorDirPin2,HIGH);
61
        digitalWrite(LeftMotorDirPin1,HIGH);
62
        digitalWrite(LeftMotorDirPin2,LOW);
63
        analogWrite(speedPinL,speed);
64
        analogWrite(speedPinR,speed);
65
    }
66
    
67
    void setup() 
68
    {
69
        initPins();
70
    }
71
    
72
    void loop()
73
    {
74
        // test movement of robot
75
        reverse();
76
        delay(1000);
77
        stop();
78
        delay(500);
79
    }

To have the robot go a certain distance I used an Arduino interrupt to call a function every time the wheel moving triggers the Disc Encoder:

1
// global variable that counts the times the sensor was triggered
2
    int pulses = 0;
3
    bool turning = false;
4
    
5
    void pulseEvent() 
6
    {
7
        // pulses generated when turning the robot should not count
8
        if (!turning)
9
        { 
10
            pulses++;
11
        }
12
    }
13
    
14
    void moveDistanceCM(float distance)
15
    {  
16
      int pulsesRequired = abs(distance) * 3.65;
17
      while (pulses < pulsesRequired)
18
      {
19
        if (distance > 0)
20
        {
21
          forwards();
22
        }
23
        else 
24
        {
25
          reverse();
26
        }
27
      }
28
      pulses = 0;
29
      stop();
30
    }
31
    
32
    void setup()
33
    {
34
        ...
35
        pulses = 0;
36
        // pin 2 has the disc encoder
37
        attachInterrupt(digitalPinToInterrupt(2), pulseEvent, RISING);
38
    }

The 3.65 represents the amount of pulses per centimeter, I found this using trial and error. Next I created a function to set the angle of the robot to whatever angle from 0-360°.

1
#include "Wire.h"
2
    #include <MPU6050_light.h>
3
    
4
    MPU6050 mpu(Wire);
5
    
6
    // Convert the angle to be within 0-360 range
7
    float normalizeAngle(float angle)
8
    {
9
        float normalizedAngle;
10
        if (angle < 0)
11
        {
12
            normalizedAngle = 360 - fmod(abs(angle), 360);
13
        }
14
        else 
15
        {
16
            normalizedAngle = fmod(angle, 360);
17
        }
18
        return normalizedAngle;
19
    }
20
    
21
    void setAngle(float angle, byte speed = 140)
22
    {
23
        mpu.update();
24
        float currAngle = normalizeAngle(mpu.getAngleZ());
25
        turning = true;
26
        while (abs(normalizeAngle(mpu.getAngleZ()) - angle) > 1)
27
        {
28
            mpu.update();
29
            currAngle = normalizeAngle(mpu.getAngleZ());
30
            float clockwiseTurn = fmod(currAngle - angle + 360, 360);
31
            float counterclockwiseTurn = fmod(angle - currAngle + 360, 360);
32
            // make sure the robot turns the least amount
33
            if (clockwiseTurn < counterclockwiseTurn)
34
            {
35
                turnRight(speed);
36
            }
37
            else 
38
            {
39
                turnLeft(speed);
40
            } 
41
        }
42
        stop();
43
        turning = false;
44
    }
45
    
46
    void setup()
47
    {
48
        ...
49
        Wire.begin();
50
        mpu.begin();
51
        // important to have accurate measurements
52
        mpu.calcOffsets();
53
        mpu.update();
54
    }
55
    

It's important to call mpu.update() as often as possible so the error from integration is minimal. I went back and added that to the distance function and any other code that loops.

Since I now had working angle and distance code, I could have just created functions for going up, right, down, or left on the grid, but I wanted something better. Here is an example track:

Diagram of example robot tour track

In this event, the goal is to go from the start point to the end point, and you get bonus points if you go through the gate zones. On the day of the event you get 10 minutes to prepare before you run the robot, and I didn't think this would be enough time to plan out the full route. Instead, I wanted to make a program that could determine the best route through all 3 gate zones, and only require the track as input.

To do this I used multiple recursive loops, and unfortunately due to the low memory capacity of the Arduino Uno I was not able to find the best possible route, but it was close enough to work. Here is the code for that:

1
#include "OptimalPath.h"
2
    
3
    static const int TRACK_SIZE = 9;
4
    static const int GATE_ZONE_COUNT = 3;
5
    static const int MAX_PATH_LENGTH = 50;
6
    
7
    // this is equal to factorial of GATE_ZONE_COUNT
8
    static const int GATE_ZONE_COMBINATIONS = 6;
9
    Point permutations[GATE_ZONE_COMBINATIONS][GATE_ZONE_COUNT];
10
    int permutationIndex = 0;
11
    
12
    // track is filled with s for start point, t for target point, g for goal point, and x for a wall
13
    // const static char PROGMEM track[TRACK_SIZE][TRACK_SIZE] = {
14
    //         {'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x'},
15
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
16
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
17
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
18
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
19
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
20
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
21
    //         {'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x'},
22
    //         {'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x'}
23
    // };
24
    
25
    
26
    char getTrack(byte i, byte j)
27
    {
28
        return (char)pgm_read_byte(&track[i][j]);
29
    }
30
    
31
    Point getPoint(char point)
32
    {
33
        for (int i = 0; i < TRACK_SIZE; i++)
34
        {
35
            for (int j = 0; j < TRACK_SIZE; j++)
36
            {
37
                if (getTrack(i, j) == point)
38
                {
39
                    return Point(j, i);
40
                }
41
            }
42
        }
43
    }
44
    
45
    Vector<Point> getGateZones()
46
    {
47
        Point gateZonesStorage[GATE_ZONE_COUNT];
48
        Vector<Point> gateZones(gateZonesStorage);
49
        for (int i = 0; i < TRACK_SIZE; i++)
50
        {
51
            for (int j = 0; j < TRACK_SIZE; j++)
52
            {
53
                if (getTrack(i, j) == 'g')
54
                {
55
                    gateZones.push_back(Point(j, i));
56
                }
57
            }
58
        }
59
        return gateZones;
60
    }
61
    
62
    bool find(Vector<Point> vec, Point target)
63
    {
64
        for (Point p : vec)
65
        {
66
            if (p == target)
67
            {
68
                return true;
69
            }
70
        }
71
        return false;
72
    }
73
    
74
    void swap(Point& a, Point& b) {
75
        Point temp = a;
76
        a = b;
77
        b = temp;
78
    }
79
    
80
    void getPermutations(Vector<Point>& points, int index)
81
    {
82
        if (index == GATE_ZONE_COUNT - 1)
83
        {
84
            for (int i = 0; i < points.size(); i++)
85
            {
86
                permutations[permutationIndex][i] = points[i];
87
            }
88
            permutationIndex++;
89
        }
90
        for (int i = index; i < GATE_ZONE_COUNT; i++)
91
        {
92
            swap(points[index], points[i]);
93
            getPermutations(points, index + 1);
94
            swap(points[index], points[i]);
95
        }
96
    }
97
    
98
    Vector<Point> getBestPath(Point curr, Point end, Vector<Point> seen, Vector<Point> path)
99
    {
100
        if (getTrack(curr.y, curr.x) == 'x') 
101
        {
102
            return Vector<Point>{};
103
        }
104
        // if current is seen
105
        if (find(seen, curr)) 
106
        {
107
            return Vector<Point>{};
108
        }
109
        if (curr == end) 
110
        {
111
            path.push_back(curr);
112
            return path;
113
        }
114
        
115
        seen.push_back(curr);
116
        path.push_back(curr);
117
        
118
        for (int i = 0; i < 4; i++)
119
        {  
120
          if (getTrack(curr.y + directions[i].y, curr.x + directions[i].x) != 'x')
121
          {
122
            Vector<Point> next = getBestPath(Point(curr.x + (directions[i].x * 2), curr.y + (directions[i].y * 2)), end, seen, path);
123
            // if we have completed a path
124
            if (!next.empty() && (next.back() == end))
125
            {
126
              return next;
127
            }
128
          }
129
        }
130
    
131
        path.pop_back();
132
        return Vector<Point>{};
133
    }
134
    
135
    Vector<Point> getPath()
136
    {
137
        Vector<Point> gateZones = getGateZones();
138
        Point startPoint = getPoint('s');
139
        Point targetPoint = getPoint('t');
140
    
141
        getPermutations(gateZones, 0);
142
    
143
        Point bestPathStorage[MAX_PATH_LENGTH];
144
        Vector<Point> bestPath(bestPathStorage);
145
    
146
        int shortestPathLength = 999;
147
        
148
        for (int i = 0; i < GATE_ZONE_COMBINATIONS; i++)
149
        {     
150
            Point pointsStorage[1 + GATE_ZONE_COUNT + 1];
151
            Vector<Point> points(pointsStorage);
152
            points.push_back(startPoint);
153
            for (Point p : permutations[i])
154
            {
155
                points.push_back(p); 
156
            }
157
            points.push_back(targetPoint);
158
            
159
            Point pathStorage[MAX_PATH_LENGTH];
160
            Vector<Point> path(pathStorage);
161
    
162
            for (int j = 0; j < points.size() - 1; j++)
163
            {
164
                Point pathsStorage[25]; 
165
                Vector<Point> paths(pathsStorage);
166
                Point seenStorage[100];
167
                Vector<Point> seen(seenStorage);
168
                Vector<Point> bestPathPart = getBestPath(points[j], points[j+1], seen, paths);
169
                for (Point p : bestPathPart)
170
                {
171
                    path.push_back(p);
172
                }
173
            }
174
            
175
            if (!path.empty() && (path.size() < shortestPathLength))
176
            {
177
                bestPath.clear();
178
                for (Point p : path)
179
                {
180
                bestPath.push_back(p);
181
                }
182
                shortestPathLength = path.size();
183
            }
184
        }
185
        return bestPath;
186
    }

The reason this is not the best possible path is because on line 118 you can see I check each direction of the path, and return the first one that completes the path. To make this better, it should store all 4 paths and only return the shortest one, but in my testing I always ran out of memory on the Arduino. The regional competition is on a 5x5 track, but the state and national competitions are on bigger tracks. This code should work on bigger tracks, but I imagine the Arduino Uno would run out of memory.

The getPath function returns a vector containing all the points. I used the following code to convert the points to movements for the robot:

1
void loop()
2
    {
3
        pulses = 0;
4
        Vector<Point> path = getPath();
5
        for (int i = 0; i < path.size() - 1; i++)
6
        {
7
            if (path[i + 1].y - path[i].y == 2)
8
            {
9
                // down
10
                setAngle(180);
11
                delayMPU(100);
12
                setAngle(180, 100);
13
                delayMPU(50);
14
                setAngle(180, 100);
15
                delayMPU(50);
16
                moveDistanceCM(50);
17
                delayMPU(100);
18
            }
19
            else if (path[i + 1].y - path[i].y == -2)
20
            {
21
                // up
22
                setAngle(0);
23
                delayMPU(100);
24
                setAngle(0, 100);
25
                delayMPU(50);
26
                setAngle(0, 100);
27
                delayMPU(50);
28
                moveDistanceCM(50);
29
                delayMPU(100);
30
            }
31
            else if (path[i + 1].x - path[i].x == 2)
32
            {
33
                // right
34
                setAngle(270);
35
                delayMPU(100);
36
                setAngle(270, 100);
37
                delayMPU(50);
38
                setAngle(270, 100);
39
                delayMPU(50);
40
                moveDistanceCM(50);
41
                delayMPU(100);
42
            }
43
            else if (path[i + 1].x - path[i].x == -2)
44
            {
45
                // left
46
                setAngle(90);
47
                delayMPU(100);
48
                setAngle(90, 100);
49
                delayMPU(50);
50
                setAngle(90, 100);
51
                delayMPU(50);
52
                moveDistanceCM(50);
53
                delayMPU(100);
54
        }
55
    
56
        // wait for button press
57
        while (digitalRead(buttonPin) != HIGH) {};
58
    }

The reason I call setAngle so much is because calling it just once results in an overshoot because after the motors stop, momentum carries the robot a little bit farther. I found the easiest way to fix this was just to call setAngle a few times with a delay to stop the momentum. It's probably not the best solution, but it works.

Another issue I had with this robot is the batteries. According to the rules you must have 6 1.2-1.5 volt batteries, but the robot kit I bought came with 2 4.5 volt batteries. I had to buy a new battery pack and solder the +/- ends to the 12V pins on the motor driver. I really thought the solder would break off, or just not work, but luckily it all held together.

Here is how the final robot looked, complete with a button to turn it on, and the required wooden dowel attached to the front:

Picture of the completed robot that I used at the competition

At the regionals competition the robot worked perfectly; it didn't hit any walls, and it went from start to finish through all 3 goals. For this event, there is a time goal, and you get the most points the closer you are to the time. I was too busy working on making sure the car turned to the right angle, and went the right distance, that I didn't add any code to slow the car to reach the time goal. I ended up placing 2nd. My team did not place high enough to go to states, so I don't think I will continue to work on the robot. If I did though, I would get brakes, which I noticed the team that won had, to remove the momentum of movement that I couldn't account for. I would also get a better Arduino so I wouldn't run out of memory on bigger tracks. If you would like to look at the full code for this project, you can view it on my GitHub.