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)
- Call learnRemote() to detect and store the IR remote’s protocol.
- 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
- down
- m5us
- Cha_center
- Enabled by the start macro
- Fired when the center distance sensor changes by 1
- Triggers the nav macro
- Cha_center
- sys
- testUS
- Manual system-level ultrasonic test trigger
- testUS
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);
};
}