I've actually got a GREAT example of what we're talking about going on right this moment.
I'm working on code for a self balancing robot right now. It's my first time building a self-balancer. I just started last night with the code for it so I'll show you the first few steps. Keep in mind that I've already spent a lot of time just running example codes with various parts to familiarize myself with their function. But last night was the first time I assembled it and started writing the actual code for the robot.
(Thanks to @PickyBiker for the 3-D print design and build!)
(For those keeping score, GPT_Stepper refers to the fact that the steppers are direct drive from the GPT timers on the UNO-R4-WiFi. It does NOT mean Chat-GPT wrote any of the code. Y'all know me better than that.)
This is the first code I started with:
See the Code
#include "GPT_Stepper.h"
const uint8_t rightStepPin = 2;
const uint8_t rightDirPin = 5;
const uint8_t leftStepPin = 4;
const uint8_t leftDirPin = 7;
GPT_Stepper leftStepper(leftStepPin, leftDirPin, 17000.0);
GPT_Stepper rightStepper(rightStepPin, rightDirPin, 17000.0);
float speed = 10000.0;
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n\nStarting BalanceBot_Test.ino\n\n");
leftStepper.init();
rightStepper.init();
}
void loop() {
static unsigned long pm = millis();
unsigned long cm = millis();
if(cm - pm >= 5000){
pm=cm;
leftStepper.setSpeed(speed);
rightStepper.setSpeed(speed);
speed = -speed;
}
}
All it does is run the motors in opposite directions every 5 seconds. That's it. Basic basic basic just see if the motors will run.
The next piece of code had no motors. It's just a trimmed down example from the library for the accelerometer I'm using. All it did was read a pitch value from an accelerometer. That's it. Just print the pitch to the screen. Basic basic small steps.
See the Code
#include "ICM_20948.h"
#define WIRE_PORT Wire1
#define AD0_VAL 1
ICM_20948_I2C icm;
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n\nICM_Test\n\n");
WIRE_PORT.begin();
WIRE_PORT.setClock(400000);
setupIMU();
}
void loop() {
static uint32_t lastMicros = micros();
if (newData()) {
Serial.println(readPitch());
uint32_t time = micros();
Serial.println(time - lastMicros);
lastMicros = time;
}
}
void setupIMU() {
bool initialized = false;
while (!initialized) {
// Initialize the ICM-20948
// If the DMP is enabled, .begin performs a minimal startup. We need to configure the sample mode etc. manually.
icm.begin(WIRE_PORT, AD0_VAL);
Serial.print(F("Initialization of the sensor returned: "));
Serial.println(icm.statusString());
if (icm.status != ICM_20948_Stat_Ok) {
Serial.println(F("Trying again..."));
delay(500);
} else {
initialized = true;
}
}
Serial.println("\n\nConnected to IMU\n\n");
bool success = true;
success &= (icm.initializeDMP() == ICM_20948_Stat_Ok);
success &= (icm.enableDMPSensor(INV_ICM20948_SENSOR_GAME_ROTATION_VECTOR) == ICM_20948_Stat_Ok);
success &= (icm.setDMPODRrate(DMP_ODR_Reg_Quat6, 0) == ICM_20948_Stat_Ok);
// Enable the FIFO
success &= (icm.enableFIFO() == ICM_20948_Stat_Ok);
// Enable the DMP
success &= (icm.enableDMP() == ICM_20948_Stat_Ok);
// Reset DMP
success &= (icm.resetDMP() == ICM_20948_Stat_Ok);
// Reset FIFO
success &= (icm.resetFIFO() == ICM_20948_Stat_Ok);
// Check success
if (success) {
Serial.println("DMP enabled!");
} else {
Serial.println("Enable DMP failed!");
Serial.println("Please check that you have uncommented line 29 (#define ICM_20948_USE_DMP) in ICM_20948_C.h...");
while (1)
; // Do nothing more
}
}
icm_20948_DMP_data_t data;
bool newData() {
icm.readDMPdataFromFIFO(&data);
if ((icm.status == ICM_20948_Stat_Ok) || (icm.status == ICM_20948_Stat_FIFOMoreDataAvail)) // Was valid data available?
{
return true;
}
return false;
}
double readPitch() {
double pitch = 0;
if ((data.header & DMP_header_bitmap_Quat6) > 0) // We have asked for GRV data so we should receive Quat6
{
// Q0 value is computed from this equation: Q0^2 + Q1^2 + Q2^2 + Q3^2 = 1.
// In case of drift, the sum will not add to 1, therefore, quaternion data need to be corrected with right bias values.
// The quaternion data is scaled by 2^30.
//Serial.printf("Quat6 data is: Q1:%ld Q2:%ld Q3:%ld\r\n", data.Quat6.Data.Q1, data.Quat6.Data.Q2, data.Quat6.Data.Q3);
// Scale to +/- 1
double q1 = ((double)data.Quat6.Data.Q1) / 1073741824.0; // Convert to double. Divide by 2^30
double q2 = ((double)data.Quat6.Data.Q2) / 1073741824.0; // Convert to double. Divide by 2^30
double q3 = ((double)data.Quat6.Data.Q3) / 1073741824.0; // Convert to double. Divide by 2^30
// Convert the quaternions to Euler angles (roll, pitch, yaw)
// https://en.wikipedia.org/w/index.php?title=Conversion_between_quaternions_and_Euler_angles§ion=8#Source_code_2
double q0 = sqrt(1.0 - ((q1 * q1) + (q2 * q2) + (q3 * q3)));
double q2sqr = q2 * q2;
// // roll (x-axis rotation)
// double t0 = +2.0 * (q0 * q1 + q2 * q3);
// double t1 = +1.0 - 2.0 * (q1 * q1 + q2sqr);
// double roll = atan2(t0, t1) * 180.0 / PI;
// pitch (y-axis rotation)
double t2 = +2.0 * (q0 * q2 - q3 * q1);
t2 = t2 > 1.0 ? 1.0 : t2;
t2 = t2 < -1.0 ? -1.0 : t2;
pitch = asin(t2) * 180.0 / PI;
// // yaw (z-axis rotation)
// double t3 = +2.0 * (q0 * q3 + q1 * q2);
// double t4 = +1.0 - 2.0 * (q2sqr + q3 * q3);
// double yaw = atan2(t3, t4) * 180.0 / PI;
}
return pitch;
}
Next was to combine those two. Still no PID, but can I read the accelerometer and run the motors at the same time. No wheels on the bot. Just seeing if the motors turn when I tilt it and making sure they go the right direction. Again, basic basic small steps. One at a time.
See the Code
#include "GPT_Stepper.h"
#include "ICM_20948.h"
#define WIRE_PORT Wire1
#define AD0_VAL 1
const uint8_t rightStepPin = 2;
const uint8_t rightDirPin = 5;
const uint8_t leftStepPin = 4;
const uint8_t leftDirPin = 7;
GPT_Stepper leftStepper(leftStepPin, leftDirPin, 17000.0, true);
GPT_Stepper rightStepper(rightStepPin, rightDirPin, 17000.0, false);
float speed = 10000.0;
ICM_20948_I2C icm;
icm_20948_DMP_data_t data;
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n\nStarting BalanceBot_Test.ino\n\n");
leftStepper.init();
rightStepper.init();
WIRE_PORT.begin();
WIRE_PORT.setClock(400000);
setupIMU();
}
void loop() {
controlLoop();
}
float pitchToSpeed(float pitch) {
float pmin = -45.0;
float pmax = 45.0;
float smin = -10000;
float smax = 10000;
return (pitch - pmin) * (smax - smin) / (pmax - pmin) + smin;
}
void controlLoop() {
if (newData()) {
float speed = pitchToSpeed(readPitch());
leftStepper.setSpeed(speed);
rightStepper.setSpeed(speed);
}
}
void setupIMU() {
bool initialized = false;
while (!initialized) {
// Initialize the ICM-20948
// If the DMP is enabled, .begin performs a minimal startup. We need to configure the sample mode etc. manually.
icm.begin(WIRE_PORT, AD0_VAL);
Serial.print(F("Initialization of the sensor returned: "));
Serial.println(icm.statusString());
if (icm.status != ICM_20948_Stat_Ok) {
Serial.println(F("Trying again..."));
delay(500);
} else {
initialized = true;
}
}
Serial.println("\n\nConnected to IMU\n\n");
bool success = true;
success &= (icm.initializeDMP() == ICM_20948_Stat_Ok);
success &= (icm.enableDMPSensor(INV_ICM20948_SENSOR_GAME_ROTATION_VECTOR) == ICM_20948_Stat_Ok);
success &= (icm.setDMPODRrate(DMP_ODR_Reg_Quat6, 0) == ICM_20948_Stat_Ok);
// Enable the FIFO
success &= (icm.enableFIFO() == ICM_20948_Stat_Ok);
// Enable the DMP
success &= (icm.enableDMP() == ICM_20948_Stat_Ok);
// Reset DMP
success &= (icm.resetDMP() == ICM_20948_Stat_Ok);
// Reset FIFO
success &= (icm.resetFIFO() == ICM_20948_Stat_Ok);
// Check success
if (success) {
Serial.println("DMP enabled!");
} else {
Serial.println("Enable DMP failed!");
Serial.println("Please check that you have uncommented line 29 (#define ICM_20948_USE_DMP) in ICM_20948_C.h...");
while (1)
; // Do nothing more
}
}
bool newData() {
icm.readDMPdataFromFIFO(&data);
if ((icm.status == ICM_20948_Stat_Ok) || (icm.status == ICM_20948_Stat_FIFOMoreDataAvail)) // Was valid data available?
{
return true;
}
return false;
}
double readPitch() {
double pitch = 0;
if ((data.header & DMP_header_bitmap_Quat6) > 0) // We have asked for GRV data so we should receive Quat6
{
// Q0 value is computed from this equation: Q0^2 + Q1^2 + Q2^2 + Q3^2 = 1.
// In case of drift, the sum will not add to 1, therefore, quaternion data need to be corrected with right bias values.
// The quaternion data is scaled by 2^30.
//Serial.printf("Quat6 data is: Q1:%ld Q2:%ld Q3:%ld\r\n", data.Quat6.Data.Q1, data.Quat6.Data.Q2, data.Quat6.Data.Q3);
// Scale to +/- 1
double q1 = ((double)data.Quat6.Data.Q1) / 1073741824.0; // Convert to double. Divide by 2^30
double q2 = ((double)data.Quat6.Data.Q2) / 1073741824.0; // Convert to double. Divide by 2^30
double q3 = ((double)data.Quat6.Data.Q3) / 1073741824.0; // Convert to double. Divide by 2^30
// Convert the quaternions to Euler angles (roll, pitch, yaw)
// https://en.wikipedia.org/w/index.php?title=Conversion_between_quaternions_and_Euler_angles§ion=8#Source_code_2
double q0 = sqrt(1.0 - ((q1 * q1) + (q2 * q2) + (q3 * q3)));
double q2sqr = q2 * q2;
// // roll (x-axis rotation)
// double t0 = +2.0 * (q0 * q1 + q2 * q3);
// double t1 = +1.0 - 2.0 * (q1 * q1 + q2sqr);
// double roll = atan2(t0, t1) * 180.0 / PI;
// pitch (y-axis rotation)
double t2 = +2.0 * (q0 * q2 - q3 * q1);
t2 = t2 > 1.0 ? 1.0 : t2;
t2 = t2 < -1.0 ? -1.0 : t2;
pitch = asin(t2) * 180.0 / PI;
// // yaw (z-axis rotation)
// double t3 = +2.0 * (q0 * q3 + q1 * q2);
// double t4 = +1.0 - 2.0 * (q2sqr + q3 * q3);
// double yaw = atan2(t3, t4) * 180.0 / PI;
}
return pitch;
}
I don't ever make the next move until the last one is working. By the time my robot stands up there will probably be dozens of these little simple one-step tests. Right now I am building a simple PID loop. I know it won't be able to stand, but I want to make sure I get it at least moving in the correct direction before I let it get too complicated. After it stands I'll work on making it roll forward and backwards. And then turning. One simple step at a time.
If there are any new parts or new bits of code that I haven't used before, then they get their own test sketch all by themselves to learn how they work before they get added to the larger code.