Making Music With Multiple Stepper Motors
by Starman3787 in Circuits > Electronics
821 Views, 2 Favorites, 0 Comments
Making Music With Multiple Stepper Motors

This is a guide on how to make music using multiple stepper motors.
I am using an STM32F767ZI on a Nucleo 144 board, and using the arduino library.
As well as this, I am also using A4988 stepper motor drivers to simplify control of the stepper motors.
Your hardware and (maybe) software will likely be different, though do not worry; all the same concepts can be applied, and only a little tweaking should be needed.
This guide assumes you have already got the hardware all set up. If not, take a look at this guide for setting up with an A4988 driver. There are guides for other standard stepper motor drivers on that website too.
Making Sound

A crucial step towards making music using stepper motors is having an understanding of how the sound is actually made.
Let's start with how sound works.
A sound wave is made up of compressions and rarefactions. A higher frequency wave is one that has smaller rarefactions or a shorter wavelength, and will have a higher pitch. The opposite is also true, a lower frequency wave is one that has longer rarefactions or a longer wavelength, so will have a lower pitch.
A sound wave will always travel at the same speed at the same place, so, by changing the time between each compression, it will change the frequency of the wave. A longer time between compressions will result in a longer rarefaction or a larger wavelength, so will therefore have a lower frequency and will sound lower in pitch.
The diagram allows you to visualise this quite nicely. You can also take a look at Pasco, which is where the diagram is from, in order to learn more about sound.
How does a stepper motor fit into this?
It is the repetitive oscillating of the motor. We need to make the motor oscillate at a certain frequency in order to achieve a particular pitch.
As the period between compressions of the wave is the only thing we can really change, it would be a good idea to begin by calculating it for each note.
Firstly, in order to calculate the period, we will need to find the frequency of each note we want to use. This can vary from instrument to instrument due to differences in key, but we will be using the key of C. The same as a piano.
This is a table of the frequency of every note.
To calculate the period of a note in seconds, you can use the formula:
Wave period = 1 / Frequency
For reasons that will become clear later on, I then converted each period to microseconds, which can be done by multiplying the period in seconds by 10^6.
In order to do all this quickly, I simply set up an Excel spreadsheet to calculate the values that I input, using the formula:
=FLOOR.MATH((1/[REPLACE WITH CELL])*(10^6))
As well as this, I also found out that notes lower than F6 did not really work on the stepper motor I was using, so I did not bother to continue inputting the frequencies of notes any lower than this. This lower bound will likely vary depending on the motor you are using, but it is a good thing to bear in mind.
Credit to Pasco for the sound wave diagram.
Playing Notes on Multiple Stepper Motors
Playing a single note on one stepper motor with a single processor is fairly simple. There are many other guides on how you can do this.
The complexity comes in when you are trying to multitask.
Unlike when you are controlling a single stepper motor, a simple delay will not work. The reason for this is that it is a blocking function. Delay functions like this simply iterate over a single loop a certain number of times. And, as you probably know, a single processor can only process one thing at a time. That means the processor will continuously iterate over this empty loop until the delay is over. That is obviously not an issue when you only have one motor to worry about, but it becomes a problem when there are multiple motors all operating simultaneously with different timing delays between them.
In order to tackle this problem, we should break it down a little. Each motor must be alternated between HIGH and LOW (in order to create compressions) every specified period of time, which varies from note to note. In addition to this, each note must be able to be played independently from one another, for any duration.
In short, we will achieve this by polling. We will periodically check the time, and compare it to when the next motor change needs to happen.
Here is the code:
#define stepPin0 (7) #define stepPin1 (6) #define stepPin2 (5) #define stepPin3 (4) #define stepPin4 (3) unsigned long F[] = {0, 0, 0, 0, 0, 0, 715, 357, 178}; unsigned long Fs[] = {0, 0, 0, 0, 0, 0, 675, 337, 168}; unsigned long G[] = {0, 0, 0, 0, 0, 0, 637, 318, 159}; unsigned long Gs[] = {0, 0, 0, 0, 0, 0, 602, 301, 150}; unsigned long A[] = {0, 0, 0, 0, 0, 0, 568, 284, 142}; unsigned long Bb[] = {0, 0, 0, 0, 0, 0, 536, 268, 134}; unsigned long B[] = {0, 0, 0, 0, 0, 0, 506, 253, 126}; unsigned long C[] = {0, 0, 0, 0, 0, 0, 0, 477, 238}; unsigned long Cs[] = {0, 0, 0, 0, 0, 0, 0, 451, 225}; unsigned long D[] = {0, 0, 0, 0, 0, 0, 0, 425, 212}; unsigned long Eb[] = {0, 0, 0, 0, 0, 0, 0, 401, 200}; unsigned long E[] = {0, 0, 0, 0, 0, 0, 0, 379, 189}; // each "track" indicates a different motor // the format {NOTE, RELATIVE DURATION} should be used when inserting notes // the duration is calculated by multiplying the relative duration by the tick length // to make things simple, you should generally use a relative duration of 1 for the shortest note interval // for example, a 1 could be a quaver, so 2 would be a crotchet etc. // the notes are the same as their musical names. i.e. G7 is G[7] // to add a rest/pause, use 0 instead of a note. for example: {0, RELATIVE DURATION} int trackOne[][2] = { {A[7], 4} }; int trackTwo[][2] = { {B[7], 4} }; int trackThree[][2] = { {C[7], 4} }; int trackFour[][2] = { {D[7], 4} }; int trackFive[][2] = { {E[7], 4} }; int tickLengthMs = 63; // this is the duration of each tick in milliseconds. lower == faster tempo unsigned long calculatePeriodEnd(unsigned long number) { return (number + micros()); } unsigned long calculateNoteEnd(unsigned long duration) { return ((duration * tickLengthMs) + millis()); } unsigned long nextStateChangeDue[] = {calculatePeriodEnd(trackOne[0][0]), calculatePeriodEnd(trackTwo[0][0]), calculatePeriodEnd(trackThree[0][0]), calculatePeriodEnd(trackFour[0][0]), calculatePeriodEnd(trackFive[0][0])}; unsigned long nextNoteChangeDue[] = {calculateNoteEnd(trackOne[0][1]), calculateNoteEnd(trackTwo[0][1]), calculateNoteEnd(trackThree[0][1]), calculateNoteEnd(trackFour[0][1]), calculateNoteEnd(trackFive[0][1])}; unsigned int currentPositions[] = {0, 0, 0, 0, 0}; bool currentState[] = {LOW, LOW, LOW, LOW, LOW}; int pauseNextTicks[] = {0, 0, 0, 0, 0}; // the number of oscillations/ticks to skip to create a tiny gap between notes int skipTicks = 750; void setup() { pinMode(stepPin0, OUTPUT); pinMode(stepPin1, OUTPUT); pinMode(stepPin2, OUTPUT); pinMode(stepPin3, OUTPUT); pinMode(stepPin4, OUTPUT); } // this loop repeats far quicker than the shortest period between notes void loop() { // track notes and their duration // if a note is over, the current note should be changed and the expiry/end time for this new note should be set unsigned long currentMilliS = millis(); if (currentMilliS >= nextNoteChangeDue[0]) { currentPositions[0]++; nextNoteChangeDue[0] = calculateNoteEnd(trackOne[currentPositions[0]][1]); pauseNextTicks[0] = 1; } if (currentMilliS >= nextNoteChangeDue[1]) { currentPositions[1]++; nextNoteChangeDue[1] = calculateNoteEnd(trackTwo[currentPositions[1]][1]); } if (currentMilliS >= nextNoteChangeDue[2]) { currentPositions[2]++; nextNoteChangeDue[2] = calculateNoteEnd(trackThree[currentPositions[2]][1]); } if (currentMilliS >= nextNoteChangeDue[3]) { currentPositions[3]++; nextNoteChangeDue[3] = calculateNoteEnd(trackFour[currentPositions[3]][1]); } if (currentMilliS >= nextNoteChangeDue[4]) { currentPositions[4]++; nextNoteChangeDue[4] = calculateNoteEnd(trackFive[currentPositions[4]][1]); } // track oscillations and their durations // similar to notes, the current oscillation period should be updated each time the current one has come to an end // we also add an extra delay between individual notes // this will simply skip a predefined number of ticks - more ticks == a more exaggerated gap // it "bites" into notes though, so no additional delay is added. the current note will be a fraction of a second shorter to create a brief silence between notes unsigned long currentMicroS = micros(); if (currentMicroS >= nextStateChangeDue[0]) { if (pauseNextTicks[0] > 0) { if (pauseNextTicks[0] == skipTicks) pauseNextTicks[0] = 0; else pauseNextTicks[0]++; } else { currentState[0] = !currentState[0]; digitalWrite(stepPin0, currentState[0]); nextStateChangeDue[0] = calculatePeriodEnd(trackOne[currentPositions[0]][0]); } } if (currentMicroS >= nextStateChangeDue[1]) { if (pauseNextTicks[1] > 0) { if (pauseNextTicks[1] == skipTicks) pauseNextTicks[1] = 0; else pauseNextTicks[1]++; } else { currentState[1] = !currentState[1]; digitalWrite(stepPin1, currentState[1]); nextStateChangeDue[1] = calculatePeriodEnd(trackTwo[currentPositions[1]][0]); } } if (currentMicroS >= nextStateChangeDue[2]) { if (pauseNextTicks[2] > 0) { if (pauseNextTicks[2] == skipTicks) pauseNextTicks[2] = 0; else pauseNextTicks[2]++; } else { currentState[2] = !currentState[2]; digitalWrite(stepPin2, currentState[2]); nextStateChangeDue[2] = calculatePeriodEnd(trackThree[currentPositions[2]][0]); } } if (currentMicroS >= nextStateChangeDue[3]) { if (pauseNextTicks[3] > 0) { if (pauseNextTicks[3] == skipTicks) pauseNextTicks[3] = 0; else pauseNextTicks[3]++; } else { currentState[3] = !currentState[3]; digitalWrite(stepPin3, currentState[3]); nextStateChangeDue[3] = calculatePeriodEnd(trackFour[currentPositions[3]][0]); } } if (currentMicroS >= nextStateChangeDue[4]) { if (pauseNextTicks[4] > 0) { if (pauseNextTicks[4] == skipTicks) pauseNextTicks[4] = 0; else pauseNextTicks[4]++; } else { currentState[4] = !currentState[4]; digitalWrite(stepPin4, currentState[4]); nextStateChangeDue[4] = calculatePeriodEnd(trackFive[currentPositions[4]][4]); } } }
If you are not using the arduino library, you can still use this code but with a little modification. I would suggest you create your own equivalent functions for stuff provided by the arduino library, such as the digitalWrite(), millis(), etc. The specifics of how these will work for you can vary from hardware to hardware. For ARM-based hardware, you should be able to use the SysTick for timing, for example.
In order to play music, you will need to add the arrays (in the format shown) into each track array. Each track will play on a specific motor.
Also ensure you set the pins correctly.
You can add or remove motors by adding to or removing from the code.
Making It Sound Better
If you have already tried playing some music, you will likely notice how quiet it sounds.
You can amplify the sound by putting the motors on a surface that can be vibrated easily, such as on top of a cardboard box.
In addition to this, I also found that pinching the motor and resisting it spinning a bit would also increase the volume by a fair amount.
Improvements
Well, that should cover everything that I wanted to share with this. Hopefully this has helped you.
If you have any suggestions for improvements that could be made, I would love to hear them.