Project M5 Car

M5 AtomMatrix Car
This project creates a 3-wheel car using M5Stack drivers and some 3D-printed parts.
Make sure you have already set up the M5 AtomMatrix before proceeding.

Videos

Autonomous Driving

Remote Controlled

Operation Modes


• Autonomous Driving
• Remote Control via Infrared Remote (e.g., a standard TV remote)
• Remote Control via TipControl App (requires an internet connection)

Schematics

Setup Drivers
  • i2cport (renamed i2cext)
    • scl: 32
    • sda: 26
  • ircontrol
    • inPin: 21
    • outPin: 25
  • m5sonic (renamed m5us)
    • triggerPin: 22
    • echoPins:
      • center: 19
      • left: 23
      • right: 33
  • m5hBridge (renamed motorL)
    • i2cAddress: 33
    • i2cport: i2cext
  • m5hBridge (renamed motorR)
    • i2cAddress: 32
    • i2cport: i2cext
  • neopixel
    • ledCount: 25
    • pin: 27
Remote Control Setup (ircontrol driver)
  1. Call learnRemote() to detect and store the IR remote’s protocol.
  2. Use saveButton() for each remote button you need:
    • down
    • left
    • mid (middle/select)
    • right
    • start
    • up

Events and Macros


Events

  • ircontrol
    • down
      • Fired when down is pressed on the remote
      • Slows the car down or drives it backward
    • left
      • Fired when left is pressed on the remote
      • Turns the car left briefly
    • mid
      • Fired when mid is pressed on the remote
      • Stops the car
    • right
      • Fired when right is pressed on the remote
      • Turns the car right briefly
    • start
      • Fired when start is pressed on the remote
      • Prepares the car for remote control
    • up
      • Fired when up is pressed on the remote
      • Increases the car’s speed
  • m5us
    • Cha_center
      • Enabled by the start macro
      • Fired when the center distance sensor changes by 1
      • Triggers the nav macro
  • sys
    • testUS
      • Manual system-level ultrasonic test trigger


Macros

Sys Driver
  • boot
    • Calls the tc macro
    • clear
      • Clears the neopixel matrix
  • tc
    • Displays “TC” on the neopixel matrix
    • multiReport
      • Reports driver data to a specific visualizer or log system

Control (Remote or Manual)
  • startRC
    • Sets constants needed for remote control operation
  • mobileDir
    • Controls turning behavior when driving manually
  • moveFwd
    • Controls forward motion and speed
  • moveSpot
    • Handles pivoting the car in place
  • stop
    • Disables all driving activity

Autonomous Navigation
  • start
    • Initialises constants
    • Enables the Cha_center event, which triggers nav on change
  • startLoop
    • Calls the navLoop macro to maintain continuous autonomous driving
  • moveDir
    • Controls turning while in autonomous mode
  • nav
    • Calls navLeft, navRight, and navCenter to determine next action
  • navLoop
    • Central loop that drives autonomous behavior
  • navCenter
    • Makes direction/speed decisions based on centre sensor
  • navLeft
    • Makes decisions based on left sensor (e.g., forward, backward, turn right)
  • navRight
    • Makes decisions based on right sensor (e.g., forward, backward, turn left)
  • navSpot
    • Spins in place to search for open paths
Neopixel Display
  • down
    • Displays a “reverse” arrow on the matrix
  • left
    • Displays a left arrow
  • right
    • Displays a right arrow
  • up
    • Displays a forward arrow (changes colour as speed increases)
  • setBar
    • displays a representation of the current speed on the matrix.

Ultrasonic Sensor Test

  • testUs
    • Triggers an ultrasonic sensor test (manually)

Macro Code

Sys

boot

{
  me.tc();
}
clear
{
  drv.neopixel.clear(1);
}
tc
Details

{
  drv.neopixel.clear(1); //clears the matrix
  drv.neopixel.setLeds(0,0,255,1,0,1,2,6,11,16); //write sthe “T” in blue
  drv.neopixel.setLeds(128,128,128,1,9,8,12,17,23,24); //writes the C in RGB(128,128,128)
}
multiReport

{
  drv.sys.reportDriversTo("demo","reini","designer","button","m5us","neopixel","rtc");
}

Control

StartRC

{
//sets the constants to help control the speed of the motors when running remotely
  me.var.speedB = -55;
  me.var.speed0 = 0;
  me.var.speed1 = 40;
  me.var.speed2 = 60;
  me.var.speed3 = 85;
  me.var.speed4 = 100;
}
mobileDir

{
  // Read the current forward speed level (can be -1 to 4)
  spd1 = me.var.fwd;
  spd2 = me.var.fwd;
  
  // Check which speed we are currently running at
  switch (spd1) {
      case(-1) { // Reverse mode
          // Reverse one motor to back up while pivoting
          spd1 = -55;
          spd2 = 0;
      };
      case(0) { // In-place spin
          // Spin in place: one motor forward, one backward
          spd1 = 50;
          spd2 = -50;
      };
      case(1) { // Slow forward turn
          spd1 = 60;
          spd2 = 0;
      };
      case(2) { // Moderate speed forward turn
          spd1 = 85;
          spd2 = 0;
      };
      case(3) { // Fast forward turn
          spd1 = 100;
          spd2 = 0;
      };
      case(4) { // Maximum speed with slight curve
          spd1 = 100;
          spd2 = -10; // Small reverse on second motor to help with aggressive pivot
      };
  };
  
  // Decide which way to turn the car
  switch (direction) {
      case("left") {
          // Flash left indicator
          drv.neopixel.left(255, 255, 255);
  
          // Right motor moves forward (or reverse), left motor slower or off
          drv.motorR.setSpeed(spd1);
          drv.motorL.setSpeed(spd2);
  
          // Clear left indicator
          drv.neopixel.left(0, 0, 0);
  
          // Let the turn complete
          delay(duration);
  
          // Resume normal forward motion
          lib.log("fwd");
          me.moveFwd(me.var.fwd);
      };
      case("right") {
          // Flash right indicator
          drv.neopixel.right(255, 255, 255);
  
          // Left motor moves, right motor is slower/off
          drv.motorL.setSpeed(spd1);
          drv.motorR.setSpeed(spd2);
  
          // Clear right indicator
          drv.neopixel.right(0, 0, 0);
  
          // Let the turn complete
          delay(duration);
  
          // Resume forward drive
          me.moveFwd(me.var.fwd);
      };
  };
}
moveFwd

{
  // Clear any existing NeoPixel LED states
  drv.neopixel.clear(true);
  // If speed is -1, set motors to reverse speed and show red down arrow
  if (speed == -1) {
      drv.motorL.setSpeed(me.var.speedB);  // speedB is  backward speed setting
      drv.motorR.setSpeed(me.var.speedB);
      drv.neopixel.down(255, 0, 0);         // Red color indicates reverse
      me.var.fwd = -1;                      // Record that car is moving backward
  }
  // If speed is 0, stop both motors and update forward state accordingly
  else if (speed == 0) {
      drv.motorL.stop();
      drv.motorR.stop();
      me.var.fwd = 0;                       // Car is stopped
  }
  // If speed is 1, set both motors to speed1 and show white up arrow (slow forward)
  else if (speed == 1) {
      drv.motorL.setSpeed(me.var.speed1);
      drv.motorR.setSpeed(me.var.speed1);
      drv.neopixel.up(255, 255, 255);       // White indicates slow forward speed
      me.var.fwd = 1;                       // Record forward speed level
  }
  // If speed is 2, set motors to speed2 and show green up arrow (medium slow)
  else if (speed == 2) {
      drv.motorL.setSpeed(me.var.speed2);
      drv.motorR.setSpeed(me.var.speed2);
      drv.neopixel.up(0, 255, 0);           // Green indicates moderate forward speed
      me.var.fwd = 2;
  }
  // If speed is 3, set motors to speed3 and show yellow up arrow (medium fast)
  else if (speed == 3) {
      drv.motorL.setSpeed(me.var.speed3);
      drv.motorR.setSpeed(me.var.speed3);
      drv.neopixel.up(255, 255, 0);         // Yellow indicates higher forward speed
      me.var.fwd = 3;
  }
  // If speed is 4, set motors to speed4 and show red up arrow (fastest forward)
  else if (speed == 4) {
      drv.motorL.setSpeed(me.var.speed4);
      drv.motorR.setSpeed(me.var.speed4);
      drv.neopixel.up(255, 0, 0);           // Red indicates max forward speed
      me.var.fwd = 4;
  };
}
moveSpot

{
  // Store the current speed of the left and right motors before changing them
  speedL = drv.motorL.var.speed;
  speedR = drv.motorR.var.speed;
  switch (direction) {
      case("left") {
          // Light up left NeoPixel with yellow (255,255,0) to indicate turning left
          drv.neopixel.left(255, 255, 0);
  
          // Set left motor to reverse at speed1, right motor forward at speed1
          // This makes the car pivot left by spinning wheels in opposite directions
          drv.motorL.setSpeed(me.var.speed1 * -1);
          drv.motorR.setSpeed(me.var.speed1);
  
          // Wait for the specified duration while turning
          delay(duration);
  
          // Turn off the left NeoPixel after the turn
          drv.neopixel.left(0, 0, 0);
      };
      case("right") {
          // Light up right NeoPixel with yellow to indicate turning right
          drv.neopixel.right(255, 255, 0);
  
          // Set left motor forward at speed1, right motor in reverse at speed1
          // This pivots the car right by spinning wheels in opposite directions
          drv.motorL.setSpeed(me.var.speed1);
          drv.motorR.setSpeed(me.var.speed1 * -1);
  
          // Wait for the specified duration while turning
          delay(duration);
  
          // Turn off the right NeoPixel after the turn
          drv.neopixel.right(0, 0, 0);
      };
  };
  
  // Restore the motors to their previous speeds after the turn is complete
  drv.motorL.setSpeed(speedL);
  drv.motorR.setSpeed(speedR);
}
stop

{
  me.var.loopRunning = false; // stops the automopus driving loop if started
  drv.m5us.eventEnabled("Cha_center",false); //disables the event Cha_center to stop autonomous driving
  //stops the motors
  drv.motorL.stop();
  drv.motorR.stop();
  me.tc();
}

Autonomous Navigation

start

{
  drv.neopixel.clear(1);
  //set the speeds for the car
  me.var.speedB = -35;
  me.var.speed0 = 0;
  me.var.speed1 = 40;
  me.var.speed2 = 60;
  me.var.speed3 = 80;
  me.var.speed4 = 100;
  //enable event Cha_center to fire and call nav macro to run the car
  drv.m5us.eventEnabled("Cha_center",true);
  me.var.loopRunning = false; //not using the macro loop to drive autonomously
}
startLoop

{
  drv.neopixel.clear(1);
  me.var.speedB = -50;
  me.var.speed0 = 0;
  me.var.speed1 = 50;
  me.var.speed2 = 60;
  me.var.speed3 = 85;
  me.var.speed4 = 100;
  //disable the event cha_center as we are using the loop contained in the macro to run the car
  drv.m5us.eventEnabled("Cha_center",false);
  me.var.loopRunning = true; //true as we are using the loop in navLoop()
  me.navLoop(); //run navLoop macro
}
moveDir

{
  // Save the current speeds of the left and right motors
  speedL = drv.motorL.var.speed;
  speedR = drv.motorR.var.speed;
  
  // Determine action based on the direction variable
  switch (direction) {
      case("left") {
          // Light up the left neopixel to indicate turning left
          drv.neopixel.left(255, 255, 255);
  
          // Set left motor speed relative to the right motor speed multiplied by factor
          drv.motorL.setSpeed(speedR * factor);
  
          // Pause for the specified duration to perform the turn
          delay(duration);
  
          // Turn off the left neopixel after turning
          drv.neopixel.left(0, 0, 0);
      };
      case("right") {
          // Light up the right neopixel to indicate turning right
          drv.neopixel.right(255, 255, 255);
  
          // Set right motor speed relative to the left motor speed multiplied by factor
          drv.motorR.setSpeed(speedL * factor);
  
          // Pause for the specified duration to perform the turn
          delay(duration);
  
          // Turn off the right neopixel after turning
          drv.neopixel.right(0, 0, 0);
      };
      case("back") {
          // Turn off the up neopixel and light the down neopixel red to indicate moving backward
          drv.neopixel.up(0, 0, 0);
          drv.neopixel.down(255, 0, 0);
  
          // Command the car to move backward by setting forward speed to -1
          me.moveFwd(-1);
  
          // Pause for the specified duration while moving backward
          delay(duration);
  
          // Turn off the down neopixel after moving backward
          drv.neopixel.down(0, 0, 0);
      };
  }
  
  // Restore the original speeds to both motors
  drv.motorL.setSpeed(speedL);
  drv.motorR.setSpeed(speedR);
}
nav

{
  drv.m5us.update(); // gets the latest values from distance sensors
  //calls the other nav macro
  me.navCenter();
  me.navLeft();
  me.navRight();
}
navLoop

{
  //continues in this loop till the stop macro is called
  while (me.var.loopRunning) {
  drv.m5us.update();
  me.navCenter();
  me.navLeft();
  me.navRight();
  };
}
navCenter

{
  // Read the current distance value from the center ultrasonic sensor
  center = drv.m5us.var.center;
  
  // Initialize speed to 0 (stopped)
  speed = 0;
  
  // Adjust speed based on the measured distance from the center sensor
  if (center >= 300) {
      // If distance is large (>=300), go at max speed (4)
      speed = 4;
      me.moveFwd(speed);
  }
  else if (center >= 200) {
      // Moderate distance (>=200), go slightly slower (3)
      speed = 3;
      me.moveFwd(speed);
  }
  else if (center >= 100) {
      // Closer distance (>=100), slow down further (2)
      speed = 2;
      me.moveFwd(speed);
  }
  else if (center >= 30) {
      // Very close but safe distance (>=30), go at minimal speed (1)
      speed = 1;
      me.moveFwd(speed);
  }
  else if (center < 20) {
      // If very close (<20), back up by moving backward for 750ms
      speed = -1;
      me.moveDir("back", 1, 750);
  }
  else if (center < 30) {
      // If between 20 and 30, call navSpot() to find an open spot or reorient
      me.navSpot();
  };

}
navLeft

{
  // Read the distance value from the left ultrasonic sensor
  left = drv.m5us.var.left;
  
  // Initialize speed to 0 (stopped)
  speed = 0;
  
  // Adjust speed based on the distance measured by the left sensor
  if (left >= 300) {
      // If distance is large (>=300), set max speed (4)
      speed = 4;
  }
  else if (left >= 200) {
      // If distance is moderately large (>=200), set speed to 3
      speed = 3;
  }
  else if (left >= 100) {
      // If distance is medium (>=100), slow down to speed 2
      speed = 2;
  }
  else if (left >= 50) {
      // If distance is close (>=50), slow further to speed 1
      speed = 1;
  }
  else if (left < 25) {
      // If very close (<25), perform a corrective move:
      // turn right at 1.2x speed for 500 milliseconds
      me.moveDir("right", 1.2, 500);
      // Also set speed to -1 to indicate backing up or stopping
      speed = -1;
  };
  
  // Update the neopixel LED bar to visually indicate the current speed state
  drv.neopixel.setBar(0, speed);
}
navRight

{
  // Read the distance from the right ultrasonic sensor
  right = drv.m5us.var.right;
  // Initialize speed to 0 (stopped)
  speed = 0;
  
  // Set speed based on how far the obstacle is on the right side
  if (right >= 300) {
      // Clear path, set max speed (4)
      speed = 4;
  }
  else if (right >= 200) {
      // Medium-far obstacle, reduce speed to 3
      speed = 3;
  }
  else if (right >= 100) {
      // Medium distance, slow down to speed 2
      speed = 2;
  }
  else if (right >= 50) {
      // Close obstacle, slow further to speed 1
      speed = 1;
  }
  else if (right < 25) {
      // Obstacle very close (<25), turn left to avoid
      me.moveDir("left", 1.2, 500);
      // Indicate reverse or stop with speed -1
      speed = -1;
  };
  
  // Update neopixel bar at position 4 with current speed state for visual feedback
  drv.neopixel.setBar(4, speed);
}
navSpot

{
  // Read distance values from center, left, and right ultrasonic sensors
  center = drv.m5us.var.center;
  left = drv.m5us.var.left;
  right = drv.m5us.var.right;
  // Clear any existing up/down neopixel indicators
  drv.neopixel.up(0, 0, 0);
  drv.neopixel.down(0, 0, 0);
  
  // Compare sensor values to decide which side has the greatest distance (more open space)
  if ((left > center) & (left > right)) {
      // Left side is most open: light up left neopixel indicator
      drv.neopixel.left(255, 255, 0);
  
      // Move the car on the spot to the left for 250ms
      me.moveSpot("left", 250);
  
      // Turn off the left neopixel indicator
      drv.neopixel.left(0, 0, 0);
  }
  else if ((right > center) & (right > left)) {
      // Right side is most open: light up right neopixel indicator
      drv.neopixel.right(255, 255, 0);
  
      // Move the car on the spot to the right for 250ms
      me.moveSpot("right", 250);
  
      // Turn off the right neopixel indicator
      drv.neopixel.right(0, 0, 0);
  };
}

Neopixel Display

down

{
//creates an arrow pointing down with colour r,g,b
	drv.neopixel.setLeds(r,g,b,1,17,11,13);
}
left

{
  drv.neopixel.setLeds(0,0,0,1,11);
  drv.neopixel.setLeds(r,g,b,1,13,7,17);
}
right
{
  drv.neopixel.setLeds(0,0,0,1,11);
  drv.neopixel.setLeds(r,g,b,1,13,7,17);
}
up
{
  drv.neopixel.setLeds(r,g,b,1,7,11,13);  
}
setBar

{
  // Use a switch-case to handle different visual states based on the 'value' input
  switch (value) {
    case(-1) {
    // Clear column and display a red warning bar at top
    drv.neopixel.setLeds(0,0,0,1,15 + column,10 + column,5 + column,0 + column);
    me.setLeds(255,0,0,1,20 + column); // red LED at the top for error/alert
    };
    case(0) {
    // Clear column and show a neutral white light at the top
    drv.neopixel.setLeds(0,0,0,1,15 + column,10 + column,5 + column,0 + column);
    me.setLeds(255,255,255,1,20 + column); // white LED at the top
    };
    case(1) {
    // Green LEDs fill lower part of the column (moderate level)
    drv.neopixel.setLeds(0,0,0,1,10 + column,5 + column,0 + column); // clear lower
    drv.neopixel.setLeds(0,255,0,1,15 + column,20 + column); // green mid/top
    };
    case(2) {
    // More green LEDs lit up as level increases
    drv.neopixel.setLeds(0,0,0,1,5 + column,0 + column); // clear lower
    drv.neopixel.setLeds(0,255,0,1,10 + column,15 + column,20 + column); // green mid-top
    };
    case(3) {
    // High level, starts mixing yellow as a caution
    drv.neopixel.setLeds(0,0,0,1,0 + column); // clear bottom
    drv.neopixel.setLeds(0,255,0,1,10 + column,15 + column,20 + column); // green mid-top
    drv.neopixel.setLeds(255,255,0,1,5 + column); // yellow warning
    };
    case(4) {
    // Maximum level: green at top, yellow in middle, red at bottom
    drv.neopixel.setLeds(0,255,0,1,10 + column,15 + column,20 + column); // green top
    drv.neopixel.setLeds(255,255,0,1,5 + column); // yellow
    drv.neopixel.setLeds(255,0,0,1,0 + column); // red bottom (overload/critical)
    };
  };
}

Ultrasonic Sensor Test

testUs

{
  // Get distance sensor readings from variables (center, left, right)
  center = me.var.center;
  left = me.var.left;
  right = me.var.right;
  
  // Clear the LED matrix before setting any bars
  column = 2; // Column 2 corresponds to center sensor
  drv.neopixel.clear(true);
  
  // Set vertical bar on column 2 (center) based on distance reading
  if (me.var.center >= 300) {
      drv.neopixel.setBar(column, 4); // Long distance = full bar
  }
  else if (me.var.center >= 200) {
      drv.neopixel.setBar(column, 3);
  }
  else if (me.var.center >= 100) {
      drv.neopixel.setBar(column, 2);
  }
  else if (me.var.center >= 50) {
      drv.neopixel.setBar(column, 1);
  }
  else if (me.var.center < 50) {
      drv.neopixel.setBar(column, 0); // Very close = no bar
  };
  
  // Repeat logic for left sensor on column 1
  column = 1;
  if (me.var.left >= 300) {
      drv.neopixel.setBar(column, 4);
  }
  else if (me.var.left >= 200) {
      drv.neopixel.setBar(column, 3);
  }
  else if (me.var.left >= 100) {
      drv.neopixel.setBar(column, 2);
  }
  else if (me.var.left >= 50) {
      drv.neopixel.setBar(column, 1);
  }
  else if (me.var.left < 50) {
      drv.neopixel.setBar(column, 0);
  };
  
  // Repeat logic for right sensor on column 3
  column = 3;
  if (me.var.right >= 300) {
      drv.neopixel.setBar(column, 4);
  }
  else if (me.var.right >= 200) {
      drv.neopixel.setBar(column, 3);
  }
  else if (me.var.right >= 100) {
      drv.neopixel.setBar(column, 2);
  }
  else if (me.var.right >= 50) {
      drv.neopixel.setBar(column, 1);
  }
  else if (me.var.right < 50) {
      drv.neopixel.setBar(column, 0);
  };

}