/*******************************************************************************
* Mecanum Robot Control Sketch for Arduino Uno R4 WiFi
*
* This sketch provides full control for a 4-wheel Mecanum robot. It uses an
* Adafruit Motor Shield v2 for motor control, an Adafruit BNO055 IMU for
* orientation sensing, and reads four quadrature encoders using interrupts.
*
* HARDWARE:
* - Arduino Uno R4 WiFi
* - Adafruit Motor/Stepper/Servo Shield for Arduino v2
* - 4x DC Motors with Quadrature Encoders
* - Adafruit BNO055 9-DOF Absolute Orientation IMU
*
* LIBRARIES REQUIRED:
* - Adafruit Motor Shield V2 Library:
* https://github.com/adafruit/Adafruit_Motor_Shield_V2_Library
* - Adafruit BNO055 Library:
* https://github.com/adafruit/Adafruit_BNO055
* - Adafruit Unified Sensor Library:
* https://github.com/adafruit/Adafruit_Sensor
*
* WIRING:
* 1. Motor Shield:
* - Sits directly on top of the Arduino Uno.
* - Connect motors to M1, M2, M3, M4 terminals.
* - M1: Front Left (fl)
* - M2: Front Right (fr)
* - M3: Rear Left (rl)
* - M4: Rear Right (rr)
* - Provide power to the shield's power terminal.
*
* 2. BNO055 IMU:
* - Connect to the I2C pins (SDA, SCL).
* - VIN to 5V, GND to GND.
*
* 3. Encoders:
* - Connect encoder power to 5V and GND.
* - Connect encoder A/B signal outputs to the specified digital pins.
* - Encoder Front Left (fl): Channel A -> D2, Channel B -> D5
* - Encoder Front Right (fr): Channel A -> D7, Channel B -> D8
* - Encoder Rear Left (rl): Channel A -> D10, Channel B -> D11
* - Encoder Rear Right (rr): Channel A -> D12, Channel B -> D13
*
******************************************************************************/
// Required libraries
#include <Wire.h>
#include <Adafruit_MotorShield.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
// --- PIN DEFINITIONS ---
// Encoders are mapped to specific interrupt-capable pins
#define ENCODER_FL_A 2
#define ENCODER_FL_B 5
#define ENCODER_FR_A 7
#define ENCODER_FR_B 8
#define ENCODER_RL_A 10
#define ENCODER_RL_B 11
#define ENCODER_RR_A 12
#define ENCODER_RR_B 13
// --- OBJECT INITIALIZATION ---
// Create the motor shield object with the default I2C address
Adafruit_MotorShield motor_shield = Adafruit_MotorShield();
// Assign motors to shield ports
Adafruit_DCMotor *motor_fl = motor_shield.getMotor(1); // Front Left
Adafruit_DCMotor *motor_fr = motor_shield.getMotor(2); // Front Right
Adafruit_DCMotor *motor_rl = motor_shield.getMotor(3); // Rear Left
Adafruit_DCMotor *motor_rr = motor_shield.getMotor(4); // Rear Right
// Create the BNO055 IMU object
// Use -1 for the ID to let the library auto-detect it
Adafruit_BNO055 bno = Adafruit_BNO055(-1, 0x28, &Wire);
// --- ENCODER STATE VARIABLES ---
// Volatile variables are used because they are modified inside ISRs
volatile long encoder_count_fl = 0;
volatile long encoder_count_fr = 0;
volatile long encoder_count_rl = 0;
volatile long encoder_count_rr = 0;
// These store the previous state of the A/B signals to decode direction
volatile uint8_t last_state_fl;
volatile uint8_t last_state_fr;
volatile uint8_t last_state_rl;
volatile uint8_t last_state_rr;
// Quadrature Encoder state transition table.
// This table provides a fast lookup to determine the direction of rotation.
// Index is calculated as: (last_state << 2) | current_state
// Values: 0 = no change, 1 = forward, -1 = backward
const int8_t qem_lookup_table[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
// --- INTERRUPT SERVICE ROUTINES (ISRs) ---
// These functions are executed automatically when an encoder pin changes state.
// They must be as fast as possible. No delays or serial prints inside.
// A separate ISR is required for each pin attached via attachInterrupt.
void isr_fl() {
uint8_t current_state = (digitalRead(ENCODER_FL_A) << 1) | digitalRead(ENCODER_FL_B);
uint8_t index = (last_state_fl << 2) | current_state;
encoder_count_fl += qem_lookup_table[index];
last_state_fl = current_state;
}
void isr_fr() {
uint8_t current_state = (digitalRead(ENCODER_FR_A) << 1) | digitalRead(ENCODER_FR_B);
uint8_t index = (last_state_fr << 2) | current_state;
encoder_count_fr += qem_lookup_table[index];
last_state_fr = current_state;
}
void isr_rl() {
uint8_t current_state = (digitalRead(ENCODER_RL_A) << 1) | digitalRead(ENCODER_RL_B);
uint8_t index = (last_state_rl << 2) | current_state;
encoder_count_rl += qem_lookup_table[index];
last_state_rl = current_state;
}
void isr_rr() {
uint8_t current_state = (digitalRead(ENCODER_RR_A) << 1) | digitalRead(ENCODER_RR_B);
uint8_t index = (last_state_rr << 2) | current_state;
encoder_count_rr += qem_lookup_table[index];
last_state_rr = current_state;
}
// --- SETUP FUNCTION ---
void setup() {
Serial.begin(115200);
Serial.println("Mecanum Robot Initializing...");
// Initialize Motor Shield
if (!motor_shield.begin()) {
Serial.println("Could not find Motor Shield. Check connections.");
while (1);
}
Serial.println("Motor Shield Initialized.");
// Initialize BNO055 IMU
if (!bno.begin()) {
Serial.println("Could not find BNO055. Check connections.");
while (1);
}
bno.setExtCrystalUse(true);
Serial.println("BNO055 Initialized.");
// Setup Encoder Pins
pinMode(ENCODER_FL_A, INPUT_PULLUP);
pinMode(ENCODER_FL_B, INPUT_PULLUP);
pinMode(ENCODER_FR_A, INPUT_PULLUP);
pinMode(ENCODER_FR_B, INPUT_PULLUP);
pinMode(ENCODER_RL_A, INPUT_PULLUP);
pinMode(ENCODER_RL_B, INPUT_PULLUP);
pinMode(ENCODER_RR_A, INPUT_PULLUP);
pinMode(ENCODER_RR_B, INPUT_PULLUP);
// Get the initial state of the encoders
last_state_fl = (digitalRead(ENCODER_FL_A) << 1) | digitalRead(ENCODER_FL_B);
last_state_fr = (digitalRead(ENCODER_FR_A) << 1) | digitalRead(ENCODER_FR_B);
last_state_rl = (digitalRead(ENCODER_RL_A) << 1) | digitalRead(ENCODER_RL_B);
last_state_rr = (digitalRead(ENCODER_RR_A) << 1) | digitalRead(ENCODER_RR_B);
// Attach interrupts for each encoder pin.
// The ISR will be called on any CHANGE in the pin's state.
attachInterrupt(digitalPinToInterrupt(ENCODER_FL_A), isr_fl, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_FL_B), isr_fl, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_FR_A), isr_fr, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_FR_B), isr_fr, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_RL_A), isr_rl, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_RL_B), isr_rl, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_RR_A), isr_rr, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_RR_B), isr_rr, CHANGE);
Serial.println("Encoders Initialized with Interrupts.");
Serial.println("Initialization Complete. Ready to move.");
delay(1000);
}
// --- MAIN LOOP ---
void loop() {
// This is a demonstration sequence of the robot's movements.
// You can replace this with your own logic.
Serial.println("Moving forward...");
move_forward(200);
delay(1000);
stop_motors();
delay(500);
Serial.println("Moving backward...");
move_backward(200);
delay(1000);
stop_motors();
delay(500);
Serial.println("Strafing right...");
strafe_right(200);
delay(1000);
stop_motors();
delay(500);
Serial.println("Strafing left...");
strafe_left(200);
delay(1000);
stop_motors();
delay(500);
Serial.println("Turning 90 degrees clockwise...");
turn_relative(90.0, 150);
delay(500);
Serial.println("Turning 180 degrees counter-clockwise...");
turn_relative(-180.0, 150);
delay(500);
Serial.println("Moving diagonal forward-right...");
move_diagonal_forward_right(200);
delay(1000);
stop_motors();
delay(500);
Serial.println("Moving diagonal backward-left...");
move_diagonal_backward_left(200);
delay(1000);
stop_motors();
delay(500);
// Print sensor data periodically
print_sensor_data();
Serial.println("Sequence complete. Restarting in 5 seconds.");
delay(5000);
}
// --- HELPER FUNCTIONS ---
// Sets the speed and direction for all four motors.
void set_motor_speeds(int speed_fl, int speed_fr, int speed_rl, int speed_rr) {
// Front Left Motor
motor_fl->setSpeed(abs(speed_fl));
if (speed_fl > 0) motor_fl->run(FORWARD);
else if (speed_fl < 0) motor_fl->run(BACKWARD);
else motor_fl->run(RELEASE);
// Front Right Motor
motor_fr->setSpeed(abs(speed_fr));
if (speed_fr > 0) motor_fr->run(FORWARD);
else if (speed_fr < 0) motor_fr->run(BACKWARD);
else motor_fr->run(RELEASE);
// Rear Left Motor
motor_rl->setSpeed(abs(speed_rl));
if (speed_rl > 0) motor_rl->run(FORWARD);
else if (speed_rl < 0) motor_rl->run(BACKWARD);
else motor_rl->run(RELEASE);
// Rear Right Motor
motor_rr->setSpeed(abs(speed_rr));
if (speed_rr > 0) motor_rr->run(FORWARD);
else if (speed_rr < 0) motor_rr->run(BACKWARD);
else motor_rr->run(RELEASE);
}
// --- MOVEMENT FUNCTIONS ---
void stop_motors() {
set_motor_speeds(0, 0, 0, 0);
}
void move_forward(uint8_t speed) {
set_motor_speeds(speed, speed, speed, speed);
}
void move_backward(uint8_t speed) {
set_motor_speeds(-speed, -speed, -speed, -speed);
}
void strafe_right(uint8_t speed) {
set_motor_speeds(speed, -speed, -speed, speed);
}
void strafe_left(uint8_t speed) {
set_motor_speeds(-speed, speed, speed, -speed);
}
void rotate_clockwise(uint8_t speed) {
set_motor_speeds(speed, -speed, speed, -speed);
}
void rotate_counter_clockwise(uint8_t speed) {
set_motor_speeds(-speed, speed, -speed, speed);
}
void move_diagonal_forward_right(uint8_t speed) {
set_motor_speeds(speed, 0, 0, speed);
}
void move_diagonal_forward_left(uint8_t speed) {
set_motor_speeds(0, speed, speed, 0);
}
void move_diagonal_backward_left(uint8_t speed) {
set_motor_speeds(-speed, 0, 0, -speed);
}
void move_diagonal_backward_right(uint8_t speed) {
set_motor_speeds(0, -speed, -speed, 0);
}
// --- IMU-BASED MOVEMENT ---
// Gets the current heading from the BNO055 (0-360 degrees)
float get_heading() {
sensors_event_t event;
bno.getEvent(&event);
// The x-value of the orientation vector corresponds to heading
return event.orientation.x;
}
// Turns the robot by a specific number of degrees relative to its current heading.
// Positive degrees for clockwise, negative for counter-clockwise.
void turn_relative(float angle_degrees, uint8_t speed) {
float start_heading = get_heading();
float target_heading = start_heading + angle_degrees;
// Normalize target heading to be within 0-360
if (target_heading >= 360.0) target_heading -= 360.0;
if (target_heading < 0.0) target_heading += 360.0;
Serial.print("Start Heading: "); Serial.println(start_heading);
Serial.print("Target Heading: "); Serial.println(target_heading);
// Start rotation
if (angle_degrees > 0) {
rotate_clockwise(speed);
} else {
rotate_counter_clockwise(speed);
}
// Loop until the target is reached
while (true) {
float current_heading = get_heading();
float angle_turned = current_heading - start_heading;
// Handle the 0/360 degree wrap-around for calculating turned angle
if (angle_turned > 180.0) {
angle_turned -= 360.0;
} else if (angle_turned < -180.0) {
angle_turned += 360.0;
}
// Check if we've turned far enough
if (abs(angle_turned) >= abs(angle_degrees)) {
break; // Exit loop
}
// Small delay to prevent busy-waiting
delay(10);
}
stop_motors();
Serial.print("Turn complete. Final heading: ");
Serial.println(get_heading());
}
// --- SENSOR DATA & DEBUGGING ---
void print_sensor_data() {
// Get a new sensor event
sensors_event_t orientation_data, ang_velocity_data, linear_accel_data;
bno.getEvent(&orientation_data, Adafruit_BNO055::VECTOR_EULER);
bno.getEvent(&ang_velocity_data, Adafruit_BNO055::VECTOR_GYROSCOPE);
bno.getEvent(&linear_accel_data, Adafruit_BNO055::VECTOR_LINEARACCEL);
Serial.println("--- IMU DATA ---");
Serial.print("Heading (X): "); Serial.print(orientation_data.orientation.x, 2); Serial.println(" deg");
Serial.print("Roll (Y): "); Serial.print(orientation_data.orientation.y, 2); Serial.println(" deg");
Serial.print("Pitch (Z): "); Serial.print(orientation_data.orientation.z, 2); Serial.println(" deg");
Serial.println("\n--- ENCODER COUNTS ---");
Serial.print("Front Left: "); Serial.println(encoder_count_fl);
Serial.print("Front Right: "); Serial.println(encoder_count_fr);
Serial.print("Rear Left: "); Serial.println(encoder_count_rl);
Serial.print("Rear Right: "); Serial.println(encoder_count_rr);
Serial.println("--------------------");
}