Firmware Design
We used a headless Raspberry Pi connected over serial to an Arduino Uno to control all of our electronics, three stepper motors and a DC motor (plus a 16x2 LCD screen connected directly to the Pi). See more about how this works under Electrical Design.
Arduino
We used the L298N library to control the DC motor and the AccelStepper library to control the stepper motors with non-blocking functions (which is essential considering our design requires running three steppers simultaneously). The Arduino sketch is explained here step-by-step and the entire script is available at the bottom of the page.
The Arduino receives all instructions via a serial connection. To eliminate buffer characters, each
command is contained between a pair of parenthesis. One of the steps of the loop()
function
is to check if there's any data that needs to be read from serial. If there is then it executes the
SerialRead()
function which parses which motor to move, encoded in the first two
characters, and how many steps/ the speed at which to move it. The commands follow the pattern
sa
, sb
, sc
, and dr
for steppers A-B, as determined
by the input pins at the top of the sketch, and the DC motor. Stepper motors then take any long number
as the number of steps such as(sa500)
. The DC motor takes a speed int from 0-255 such as
dr200
. The final function, ds
, stops the DC motor and requires no argument.
This is equivalent to dr0
. While the DC motor is capable of running in both directions,
it's hard-coded as forward here for convenience.
if (Serial.read() == '(') {
command = Serial.readStringUntil(')');
Serial.println(command);
// parse steps (always starts at index 2)
long steps = command.substring(2, command.length()).toInt();
// A stepper
if (command.startsWith("sa")) {
commandMove(stepperA, steps, 0);
}
If the event of a stepper motor command, commandMove()
is called which checks if the stepper
is currently moving (as determined by speed = 0, which only happens when a target destination is reached
as will be explained shortly) and, if it's not, runs stepper.move(steps)
which sets the
step number from the command as the motor's target destination and
stepper.setSpeed(1100)
which has the effect of setting the max consistent speed and
overriding the acceleration functions.
void commandMove(AccelStepper &stepper, long steps, int index) {
// determine movement based on command and current motor state
if (stepper.speed() == 0) {
// move stepper `steps` steps
stepper.move(steps); // set relative target position
stepper.setSpeed(1100); // must set speed after moveTo to get rid of accl
} else { // if stepper is currently moving
next_target[index] = steps;
}
}
The AccelStepper library allows for three steppers to run simultaneously by moving each motor one step
per Arduino loop. More about how this works can be found here. Within the
loop()
function, stepperEachLoop()
is called for each stepper motor. This first
calls runSpeedToPosition()
which advances the motor one step towards it's target. It then
checks if the current position of the stepper is equal to the target position and, if it is, sets the
motor speed to 0.
void stepperEachLoop(AccelStepper &stepper, int index) {
// things that have to happen or be checked for each stepper in each loop
// if steps remaining until target step 1 step
stepper.runSpeedToPosition();
if (stepper.targetPosition() == stepper.currentPosition()) {
stepper.setSpeed(0);
}
...
This is why commandMove()
checks if stepper.speed() == 0
before setting a new
target, to check if the motor is currently working towards a target destination.
At the beginning of the sketch, an array called next_target
is filled with three 0s. Index
0-2 correspond to steppers A-B and represents the next target for a motor if a command is sent before it
reaches its first target destination, creating a buffer of one command. When the stepper speed is not 0
in commandMove()
, it sets the next target position for the motor.
returning to stepperEachLoop()
, if the speed is 0 and the next target is a value other than
0, it sets the new target position, motor speed, and resets the next target to 0. Essentially, it waits
until the motor has reached the previous target before passing through the next position changing
command.
...
// check if stepper has stopped and needs to set a new target
if (stepper.speed() == 0 & next_target[index] != 0) {
stepper.move(next_target[index]); // set target as next target
stepper.setSpeed(1100);
next_target[index] = 0; // reset next target
}
}
Raspberry Pi
Since the Raspberry Pi was headless we needed a convenient way to get the IP so we could connect
remotely. We used the Raspberry Pi (with I2C enabled) to control the 16x2 LCD with the socket, board,
and Sparkfun serLCD
circuit-python libraries. A script called on_startup.py
(source
on GitHub) ran immediately after the Pi booted by making it executable and adding the path to rc.local.
This script simply got the current IP and wrote it to the
screen.
def get_ip_address():
ip_address = ''
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip_address = s.getsockname()[0]
s.close()
return ip_address
serlcd.write('Ready to Disco?\n')
serlcd.write(get_ip_address())
A small addition was added in the main.py
software as well which simply write
Disco Diva
to the screen when the program starts. This functionality could easily be
expanded to display the name of the current song if we had
more time.
The Entire Arduino Sketch
Can also be found on our project GitHub.
/* Arduino-side firmware for GPIO motor control
using DRV8825 stepper drivers and a L298N H-bridge for the DC motor.
pins listed below for sanity:
Stepper Motor A:
DIR (blue) -> pin 8
STP (green) -> pin 9
Stepper Motor B:
DIR (blue) -> pin 10
STP (green) -> pin 11
Stepper Motor C:
DIR (blue) -> pin 12
STP (green) -> pin 13
DC Motor
IN 1 (mark) -> pin 4
IN 2 (blue) -> pin 2
ENB (black) -> pin 3 (pwm)
*/
#include <Arduino.h>
#include <L298N.h>
#include <AccelStepper.h>
// *** DC motor setup ***
const unsigned int IN1 = 4;
const unsigned int IN2 = 2;
const unsigned int EN = 3;
L298N DCmotor(EN, IN1, IN2);
// *** stepper motor setup ***
AccelStepper stepperA(AccelStepper::DRIVER, 9, 8); // stp = 9, dir = 8
AccelStepper stepperB(AccelStepper::DRIVER, 11, 10); // stp = 11, dir = 10
AccelStepper stepperC(AccelStepper::DRIVER, 13, 12); // stp = 13, dir = 12
// buffer for target position-change before initial target reached
long next_target[3] = { 0, 0, 0 };
// *** START CODE ***
void setup() {
// initialize serial communication
Serial.begin(115200);
// maximum speed in steps per second
stepperA.setMaxSpeed(1100);
stepperB.setMaxSpeed(1100);
stepperC.setMaxSpeed(1100);
}
void loop() {
// steppers need to take a step each loop and potentially update state
stepperEachLoop(stepperA, 0);
stepperEachLoop(stepperB, 1);
stepperEachLoop(stepperC, 2);
// check and process serial command if needed
if (Serial.available() != 0) {
// doesn't need to be a separate function but nice for organization
SerialRead();
}
}
void stepperEachLoop(AccelStepper &stepper, int index) {
// things that have to happen or be checked for each stepper in each loop
// if steps remaining until target step 1 step
stepper.runSpeedToPosition();
/* set speed to 0 when target position reached (accelstepper doesn't do this
automatically,setting the speed to 0 doesn't physically do anything but it
forces the program to agree that the motor is stopped). Using setSpeed(0)
instead of stop() because not using acceleration
*/
if (stepper.targetPosition() == stepper.currentPosition()) {
stepper.setSpeed(0);
}
// check if stepper has stopped and needs to set a new target
if (stepper.speed() == 0 & next_target[index] != 0) {
stepper.move(next_target[index]); // set target as next target
stepper.setSpeed(1100);
next_target[index] = 0; // reset next target
}
}
void SerialRead() {
String command;
// commands start with '(' and end with ')' to avoid garbage characters
if (Serial.read() == '(') {
command = Serial.readStringUntil(')');
Serial.println(command);
// parse steps (always starts at index 2)
long steps = command.substring(2, command.length()).toInt();
// A stepper
if (command.startsWith("sa")) {
commandMove(stepperA, steps, 0);
}
// B stepper
else if (command.startsWith("sb")) {
commandMove(stepperB, steps, 1);
}
// C stepper
else if (command.startsWith("sc")) {
commandMove(stepperC, steps, 2);
}
// DC motor run with `drN` where N is speed between 0-255
else if (command.startsWith("dr")) {
unsigned short speed = command.substring(2, command.length()).toInt();
DCmotor.setSpeed(speed);
DCmotor.forward();
}
// stop DC motor with `ds`
else if (command == "ds") {
DCmotor.setSpeed(0);
DCmotor.forward();
}
}
}
void commandMove(AccelStepper &stepper, long steps, int index) {
// determine movement based on command and current motor state
if (stepper.speed() == 0) {
// move stepper `steps` steps
stepper.move(steps); // set relative target position
stepper.setSpeed(1100); // must set speed after moveTo to get rid of accl
} else { // if stepper is currently moving
next_target[index] = steps;
}
}