Raspberry Pi Robot Tank Control Loop

When we last visited it, my Raspberry Pi Tank was able to be driven around in direct response to buttons being pressed on a web page. However, it could only drive ahead at full speed and wasn’t able to make decisions by itself, such as performing an emergency stop if its operator had just driven it into a wall.

I’ve started to address this with a couple of significant changes to my Python control script

Caution: this blog post includes some computer programs of dubious quality, which may bore or offend your sensibilities.

This poorly shot video illustrates the commands left, right, ahead full, ahead medium, ahead slow and emergency stop. The two LEDs are for the left and right motors respectively, and you should be able to see them dim when the speed is diminished. The stunt hand from off-stage right is simply shorting a GPIO input to ground to simulate the front bumper switch.

Running as a traditional control loop

The code runs to a couple of hundred lines of Python, so I won’t bore you with it here, but my basic premise goes along the lines of:

  • Sense
  • Decide
  • Act
  • Wait a bit to let the rest of the Pi do something
  • Goto 10

I felt it would also be useful in the future if I started to build up a model of the robot’s state and sensor inputs, so I’ve started to create a couple of data structures to track those, and started to build a decision function that takes the current state and any sensed input (including from the user) and creates the next state.

Asynchronous, character-mode user input

So far, I’ve been reading input from Python’s standard input so that my control script will run interactively just as easily as when its input is piped in from the fifo used by the FastCGI process. However, using sys.stdin.read(1) or key = raw_input() with default settings means that the terminal is in line mode and will block my process until it receives a new line of input, terminated by a new line character. This isn’t much use when running as a continuous loop 10 times per second, so we need to temporarily modify the terminal settings, taking care to set them back when we exit, otherwise we’ll bork the shell that called the python script.

This commit highlights the two changes for async input and putting the terminal into character mode.

I’ve chosen to use the Linux specific select() method for asynchronous input, because it works and I don’t need this control script to run on other platforms. Briefly, this allows the python script to test whether there are any characters waiting to be read on standard in, before we attempt to read a character.

This alone doesn’t solve the problem of the terminal waiting for a new line character before it sends buffered input to Python’s standard in, so we need to set the terminal to unbuffered mode and set it to character at a time mode. We take the time to preserve the original settings so we can set it back again when our program exits, using try / finally so that we attempt to reset the terminal even if we exit uncleanly.

Pulse Width Modulation For Speed and an emergency stop

The biggest reason that I’ve changed to a polled loop is so that I can turn the motor output pins on and off many times per second, basically a crude version of Pulse Width Modulation. As I’ve mentioned before, Linux and Python on a Raspberry Pi is a poor choice for proper PWM, where you generally need to control outputs to at least 1ms accuracy. However, I’m only roughly reducing the power sent to my DC motors and don’t particularly care if it’s running at 33% or 25% capacity in any particular second, so (Jedi Mind Trick) this will do fine.

This commit highlights my initial hastily drawn up duty cycle for ‘ahead slow’, ‘ahead medium’ and ‘ahead full’. At the moment, the control loop is set to roughly 10Hz, so I’ve added a counter that resets when it hits 10. Depending upon the current value of the counter and the target speed, we can turn the outputs on and off at certain times through any particular second. This duty cycle is illustrated in the following code snippet:

The emergency stop is provided by a GPIO pin configured as an input with the Raspberry Pi’s internal pull-up resistor enabled, so that the switch need only be a simple momentary contact, short-circuit to earth. Polling for this input is done at the same time as the PWM, so the robot will execute the stop command regardless of any user commands.

Next steps

Running as a polled loop on a timer is fine if you’ve got enough computing resources to handle it, or you need to optimise the code for being reasonably obvious to read. However, event driven software is generally more efficient and flexible, at a cost of needing slightly more care when writing it. That said, as long as I keep each trip around the loop down to less than 100ms, I can fairly easily add more sensors to the mix without adding complexity.

For example, my previous user input based version of the control script could have been amended so that it used one thread that blocked on user input and another thread that took that input and switched the motors on and off. That second thread could then have used a hardware interrupt to watch for a falling edge on the bumper switch input to trigger a python function to stop the motors, saving us the overhead of sampling that input switch 10 times a second.

This Raspberry Pi is turning into quite an expensive cheap computer, I’ve ordered Quick2Wire’s i2c Analogue input board, which should allow my robot to watch how much current its motors are using and stop them when they approach stall. That should also allow me to connect up the Infrared and Ultrasonic range finders I’ve just ordered, which I’m hoping will allow the robot to become aware of its surroundings. I was expecting that counting the pulses from the robot’s quadrature encoders would be the simplest sensor input to set up, but I haven’t yet found a sensible dual-channel i2c counter chip.

I’ve also started doing some homework on using gyros and accelerometers, so far it’s not looking like they’ll be useful over more than a few centimeters without the odometry inputs because the noise simply becomes too significant when integrating twice from acceleration to position. However, I hope they’ll give me some angular velocity to track turning, as well as confirming when a state change has completed (such as from all ahead full to stop).