Servo Movement
So we need to simulate a microcontroller program that interacts with something and communicates with something. Since my final project involves a sprinkler head moving in a 2D plane, I want to simulate a program that reads a shape from somewhere and controls 2 servos (one per axis) that draw the shape. Ideally I would be able to read the servos’ positions and plot the graph they actually make in some display.
So I looked at Wokwi for simulation and the Arduino UNO R3 as the board, initially. One problem that came up is that the servo that comes with Wokwi doesn’t have a pin to read its current position.
I looked at Tinkercad to see if its servo had that 4th pin, but it doesn’t, so I went back to Wokwi, especially since it had the SD card peripheral.
To start, I added 2 servos (servo X and servo Y) and made them wipe. A clip of the simulation follows:
Then I implemented reading from an SD card. The program reads a shape text file inspired by SVG paths. Each line contains a movement code letter followed by some number of values in \([0, 1]\), separated by spaces. The specification is as follows:
Code | # Values | Description |
---|---|---|
M |
2 | Move to the (x, y) pair specified as fast as possible. |
L |
2 | Draw a line to the (x, y) pair specified. |
C |
6 | Draw a Bezier cubic. The first and second (x, y) pairs are control points. The last one is the final position. |
For now, the file must end in a newline. An example shape file and its simulation follows:
So then I added a 32×32 LED display to show where the sprinkler head would be if the servos were actually controlling one instead of just spinning around. There’s probably a lot of cheating with electronics involved since Wokwi doesn’t understand things like current and power and stuff. I also added a couple of convenience code letters:
Code | # Values | Description |
---|---|---|
H |
1 | Draw a horizontal line to the x position specified. |
V |
1 | Draw a vertical line to the y position specified. |
Anyway, here’s the new shape file and simulation:
The code, which can be found here, is as follows:
#include <Servo.h>
#include <SD.h>
#include <MD_MAX72xx.h>
const int CS_PIN = 10;
Servo servo_x;
Servo servo_y;
int pos = 0;
unsigned long micro_time = 0;
unsigned long wipe_time = 0;
const unsigned long PERIOD = 800uL * 1000;
const int LED_SIDE_LENGTH = 32;
MD_MAX72XX mx = MD_MAX72XX(MD_MAX72XX::PAROLA_HW, 9, sq(LED_SIDE_LENGTH / 8));
struct ShapeTracer {
bool init(const char* filename, Servo* servo_x, Servo* servo_y) {
if (shape_exists)
shape.close();
shape = SD.open(filename);
shape_exists = true;
if (!shape) {
shape_exists = false;
Serial.println("error opening file!");
return false;
}
servos[0] = servo_x;
servos[1] = servo_y;
for (int i = 0; i < 2; ++i)
next_pos[i] = servos[i]->read() / 180.0f;
finished = true;
return true;
}
void readFloats(float* buffer, int num_floats) {
for (int i = 0; i < num_floats; ++i)
buffer[i] = shape.parseFloat();
}
static float cubicInterp(float x0, float x1, float x2, float x3, float t) {
return (x0*(1-t) + x1*3*t)*(1-t)*(1-t) + (x2*3*(1-t) + x3*t)*t*t;
}
static float lineLength(float* prev_pos, float* next_pos) {
return sqrt(sq(next_pos[0] - prev_pos[0]) + sq(next_pos[1] - prev_pos[1]));
}
static float cubicLength(float* prev_pos, float* inter_pos, float* next_pos) {
float prev[2] { prev_pos[0], prev_pos[1] };
float next[2];
float len = 0.0;
for (int i = 0; i < CURVE_SUBDIVISION; ++i) {
for (int j = 0; j < 2; ++j)
next[j] = cubicInterp(prev_pos[j], inter_pos[j], inter_pos[2 + j], next_pos[j],
float(i + 1) / CURVE_SUBDIVISION);
len += lineLength(prev, next);
for (int j = 0; j < 2; ++j)
prev[j] = next[j];
}
return len;
}
bool calcNextMotion() {
finished = false;
for (int i = 0; i < 2; ++i)
prev_pos[i] = next_pos[i];
char move;
if (!shape.readBytes(&move, 1)) {
finished = true;
return false;
}
float floats[6];
switch (move) {
case 'M':
movement = MV_MOVE;
readFloats(next_pos, 2);
break;
case 'H':
movement = MV_LINE;
readFloats(next_pos, 1);
line_length = lineLength(prev_pos, next_pos);
break;
case 'V':
movement = MV_LINE;
readFloats(next_pos + 1, 1);
line_length = lineLength(prev_pos, next_pos);
break;
case 'L':
movement = MV_LINE;
readFloats(next_pos, 2);
line_length = lineLength(prev_pos, next_pos);
break;
case 'C':
movement = MV_CUBIC;
readFloats(floats, 6);
for (int i = 0; i < 4; ++i)
inter_pos[i] = floats[i];
for (int i = 0; i < 2; ++i) {
next_pos[i] = floats[4 + i];
curve_pos_1[i] = prev_pos[i];
}
curve_segment = 0;
curve_time = 0;
//line_length = cubicLength(prev_pos, inter_pos, next_pos);
break;
}
shape.find('\n');
time = 0;
return true;
}
bool calcNextCurveSegment() {
if (curve_segment == CURVE_SUBDIVISION)
return false;
time -= curve_time;
++curve_segment;
for (int i = 0; i < 2; ++i) {
curve_pos_0[i] = curve_pos_1[i];
curve_pos_1[i] = cubicInterp(prev_pos[i], inter_pos[i], inter_pos[2 + i], next_pos[i],
float(curve_segment) / CURVE_SUBDIVISION);
line_length = lineLength(curve_pos_0, curve_pos_1);
curve_time = (unsigned long)(line_length / speed * 1000000L);
}
return true;
}
bool mvMove() {
for (int i = 0; i < 2; ++i)
servos[i]->write(int(round(next_pos[i] * 180)));
return time >= MOVE_TIME;
}
bool mvLine() {
float interp = speed * time / 1000000L / line_length;
for (int i = 0; i < 2; ++i) {
float pos = prev_pos[i] + (next_pos[i] - prev_pos[i]) * interp;
servos[i]->write(int(round(pos * 180)));
}
return interp >= 1;
}
bool mvCubic() {
while (time >= curve_time) {
if (!calcNextCurveSegment()) {
for (int i = 0; i < 2; ++i)
servos[i]->write(int(round(next_pos[i] * 180)));
return true;
}
}
float interp = speed * time / 1000000L / line_length;
for (int i = 0; i < 2; ++i) {
float pos = curve_pos_0[i] + (curve_pos_1[i] - curve_pos_0[i]) * interp;
servos[i]->write(int(round(pos * 180)));
}
return false;
}
// d_time is in microseconds
void update(unsigned long d_time) {
if (finished) {
if (!calcNextMotion())
return;
}
time += d_time;
switch (movement) {
case MV_MOVE: finished = mvMove(); break;
case MV_LINE: finished = mvLine(); break;
case MV_CUBIC: finished = mvCubic(); break;
}
}
enum MovementType {
MV_MOVE, // move to the next position and stop
MV_LINE, // draw a line to the next position
MV_CUBIC, // draw a Bézier cubic curve to the next position
};
static const unsigned long MOVE_TIME = 250uL * 1000;
static const float speed = 0.8; // units per second
static const int CURVE_SUBDIVISION = 64;
File shape;
bool shape_exists;
MovementType movement;
float prev_pos[2];
float inter_pos[4];
float next_pos[2];
float line_length;
float curve_pos_0[2]; // current line segment in curve
float curve_pos_1[2];
int curve_segment;
unsigned long curve_time; // time needed for the current curve segment
Servo* servos[2];
bool finished;
unsigned long time; // in microseconds
};
ShapeTracer tracer;
void setup() {
Serial.begin(115200);
servo_x.attach(3);
servo_y.attach(5);
if (!SD.begin(CS_PIN)) {
Serial.println("Card initialization failed!");
while (true);
}
tracer.init("shape.txt", &servo_x, &servo_y);
// For dot matrix
//SPI.beginTransaction(SPISettings(16000000, MSBFIRST, SPI_MODE0));
mx.begin();
mx.clear();
micro_time = micros();
}
void setLed(float x, float y) {
int pixel_x = min(floor((1 - x) * LED_SIDE_LENGTH), LED_SIDE_LENGTH - 1);
int pixel_y = min(floor((1 - y) * LED_SIDE_LENGTH), LED_SIDE_LENGTH - 1);
int row = pixel_y % 8;
int col = pixel_x % LED_SIDE_LENGTH + (pixel_y / 8 * LED_SIDE_LENGTH);
mx.setPoint(row, col, true);
}
void loop() {
long d_time = micros() - micro_time;
micro_time += d_time;
tracer.update(d_time);
setLed(servo_x.read() / 180.0, servo_y.read() / 180.0);
/*wipe_time += d_time;
wipe_time %= PERIOD;
int angle = wipe_time * 9 / (PERIOD / 20) * 2;
if (angle > 180)
angle = 360 - angle;
servo_x.write(angle);
servo_y.write(angle);*/
}