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;
}
}