First, I have to express my gratitude for the lab executives managing Hardware Lab 3 for their support of the MDP groups in our lab. They've tolerated our nonsense and went beyond to accommodate our requests.

The purpose of this post is to share the lessons that I, working as part of the hardware/Arduino subsystem, have picked up over the course of the project. Additionally, I've concluded that there are certain limitations regarding what is possible - if anyone has successfully side-stepped/found alternative solutions to these issues, I would love to hear about it :).


The Arduino IDE is only slightly better than a text editor (though 2.0 looks pretty good). I used PlatformIO with Visual Studio Code. There are a few immediate advantages over the Arduino IDE:

  1. Syntax highlighting
  2. Ability to package libraries directly with a project
  3. Integrated Git client

The static analysis tools are also pretty useful (eg to identify SRAM usage).

Moving straight

This article discusses two ways of converting encoder feedback into velocity, and their associated issues.

  1. Count the number of encoder edges occurring over a set interval in time, the more edges, the higher the speed. This is problematic because of the poor resolution (12cpr! Wow!) of the encoders provided - at low speeds, the number of edges could fall to 0 in certain intervals.
  2. Count the time between encoder edges, the lower the elapsed time, the higher the speed. This seems to work fine, other than having to handle the motor stopping (no more edges means no more speed updates).

The course materials provided to us suggests building two closed-loop controllers to ensure each axis moves at a set speed, with the idea that if both axis turn at the same speed, the robot would move straight. Note same speed - since we're working with real hardware with inherent imperfections, this is impossible.

Example Speed-Time graph

What's the impact? Consider the graph above. It provides an example where the right motor axis is moving at a constant speed, but the left axis dips momentarily. Distance travelled by each axis is the area under the curve (ie the integral), and it is immediately obvious that the right axis would have moved further than the left. This would result in the robot veering to the left. Practically, if you tried to tune your speed controller, you would have observed that your actual speed is very noisy and does not perfectly track the setpoint.

Since we say that the issue here is one axis travelling further than the other, the obvious solution here is to make use of the position feedback from the encoders - we move each axis until both of them have travelled the same distance. However, this still does not mean that the robot would have moved straight. Lets consider an exaggerated example - our robot moves its left axis 10mm, then once done, moves its right axis 100mm, then once done, moves its left axis 90mm. Both axis would have moved 100mm in total, but did it move straight? No - it would have veered left.

The solution my team adopted is to write closed-loop controllers around position instead, with the end goal of minimizing the difference in position.

Control loops for motion

Each axis (to be precise, in the end, both axis shared the same) has a primary PID controller to spin the axis to reach the target setpoint. A secondary PID controller works on the difference in position of both axis, and outputs a correction factor that is added to the output of the primary PID controller.

For instance, if the left encoder position is 1000 and the right encoder position is 990, we know that the left axis has turned more than the right, so slow down the left and speed up the right axis.

Wheel Slip

We found that controlling acceleration and deceleration was crucial to moving accurately - if not, the wheels would slip. We simply limited the maximum rate of change of the motor power (to limit acceleration), where deceleration is handled by the primary PID controller mentioned (the output of the controller decreases as we near the target).

void Axis::setPower(int16_t target_power, bool cap_accel) {
  _target_power = target_power;

  if (!cap_accel) {
    _power = _target_power;
  } else {
    int16_t delta = _target_power - _power;
    if (delta > kMax_axis_accel) {
      _power += kMax_axis_accel;
    } else if (delta < kMax_axis_decel) {
      _power += kMax_axis_decel;
    } else {
      _power = _target_power;

  if (_power > 0) {
    _setPower(_power, _invert ^ _reverse);
  } else {
    _setPower(-_power, !(_invert ^ _reverse));

However, practically, wheel slip is inevitable due to factors outside our control (eg uneven surfaces). The obvious solution here is to make use of an IMU (which I understand has been made available from AY21-22) for feedback: to move straight, instead of using control loops to minimise the error between axis speed or position, use a control loop to maintain a particular bearing instead.

Execution Speed

Since we're forced to use a 16MHz, 8-bit microcontroller in 2021, we have to be extremely aware of the execution time of our code, especially our mathematical operations.

Math operations (especially divisions and operations on floats) are expensive. Using the smallest possible variable type helps here (ie use uint8_t if you only need to store a number ranging from 0 to 10, don't just use int everywhere), but we've found that avoiding floats and divisions is more critical here.

For example, we can perform our calculations with fixed point arithmetic instead of using floats. Instead of writing output = 0.5 * error;, we could rewrite this as output = (127 * error) / 255, which is equivalent to output = (127 * error) >> 8, avoiding floats or divisions.

Infrared Sensors

The field of vision of the IR sensors are asymmetrical - as such, their positions and orientations are critical to take full advantage of their range, to minimise noise and to prevent unwanted reflections from the ground.

The IR sensors being noisy is a known issue - the provided material discusses using median filtering to reduce the impact of noise. Taking a look at the power supply and signal lines on a oscilloscope, we see huge spikes:

No capacitor, Yellow: Supply Voltage, Blue: Signal Out

This issue has also been discussed at length here and here. Exploring the impacts of adding bulk capacitance and filters on the signal:

100uF, Yellow: Raw Signal, Blue: Filtered Signal
680uF, Yellow: Raw Signal, Blue: Filtered Signal

With that, and per the manufacturer's recommendation to add bulk capacitance, we added large bulk capacitance to the power supply rails and a second-order low pass filter to the signal output.

Wire harness for filtering

Do note that the combined 30mF~ of bulk capacitance on the 5V line is huge: the inrush current surge can cause issues/damage if not taken care of properly. The S9V11F5 on the previous adapter board has a built-in soft start to limit inrush current that mitigates this issue.

Yellow: Supply Voltage, Blue: Filtered Signal

Revised Hardware

The new revised hardware with a 32-bit STM32 on-board opens up new possibilities. Couple of things off the top of my head:

  • Utilising the on-board IMU should simplify the challenge of moving in a straight line accurately
  • The STM32F103 can run the Arduino core (STM32duino)- this may rapidly speed up prototyping
  • Making use of FreeRTOS for tasks and synchronisation can simplify project structures significantly