Building a "cheap" ROS + Arduino Turtlebot. Need help with code/hardware

Hi there,

i'm a mechanical engineer, so you'll have to excuse my lacking knowledge in electronics and code.

I've worked with the Turtlebot3 platform recently to try and get a grip with ROS, and have started building my own version of a Turtlebot'ish robot. Generally speaking, here's what i've got:

  • Raspberry Pi 4 running Ubuntu MATE 20.04, with ROS Noetic installed, taking care of high level stuff
  • Arduino MEGA2560 R3, taking care of low level stuff
  • 2 x DC geared motors with encoders
  • 1 x H-bridge

The following diagram shows the wiring (although the driver in use wasn't availabe, hence the L298N in the diagram. Note, that the only difference between the 2 is that the one in use has a separate GND pin for each motor):

i'm able to upload a simple motor driver sketch from James Brutons github: (link, and control the robot succesfully with the keyboard through a teleopnode, by also running a /rosserial node to establish communication between the MEGA and Pi.

Trying to implement odometry as the first step in being able to run the full ROS Navigation stack, i've come across this project which is similar to mine, but obviously with some proper software written for it. The author uses an almost identical setup to mine, but with the difference of an Arduino DUE instead of my MEGA. Trying to run his firmware, with a couple of slight modifications to try and make the code compatible with the MEGA, i run into issues with VERY jittery DC motors. The code implements the use of a PID control on the PWM for the wheels, which to my knowledge tries to make sure that the actual speed of the wheels matches the requested speed from /cmd_vel through ROS.

I've been messing around with the tuning of the PID, since that's what i initially though might be the issue, but i'm only able to get non-jittery motors when i set the P quite low, and the I and D values to 0.

Here is a recorded video of the issue:
Jittery wheels video

I would very much appreciate any inputs to this project, as it is my first serious one, and i would appreciate the help with the issue. Thank you.

I'll post the entire code here:

foxbot_core_config.h

  /*
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 *  Description: Core configuration file for foxbot robot.
 *  Author: Matthieu Magnon
 *
 *  Serial communication is enabled with the following command:
 *  rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=115200
 */

#ifndef FOXBOT_CORE_CONFIG_H_
#define FOXBOT_CORE_CONFIG_H_

// enable this line for Arduino Due
// #define USE_USBCON

/* Include librairies */

#include "ros.h"
#include <ros/time.h>
#include <tf/tf.h>
#include <geometry_msgs/Twist.h>
#include <nav_msgs/Odometry.h>

#include <Timer.h>
#include <MedianFilter.h>

/* Global parameters */
#define FREQUENCY_RATE 						  30 			// [ms] default 50ms
#define FREQUENCY_ODOMETRY 				  150 		// [ms] default 250ms
#define FREQUENCY_ROSPINONCE 				150 		// [ms]
#define FREQUENCY_CONTROLLER 				30 			// [ms] default 50ms

/* Rate computing parameters */
#define RATE_DIRECTION_MEDIAN_FILTER_SIZE 	3
#define RATE_CONV 							    0.012770702 	// [inc] -> [rad] TOR FORMER: 0.0073882. calculated by: 2*pi / 492 (encoder ticks per wheel rotation)

#define RATE_AVERAGE_FILTER_SIZE 			4

/* Rate controller parameters */
#define PWM_MAX				 				      255            // 12 bit        DEFAULT: 4095
#define RATE_CONTROLLER_KP 					1000.0 		    // default 150    DEFAULT: 130.0
#define RATE_CONTROLLER_KD 					0.0 //4500000000000         DEFAULT: 5000000000000.0
#define RATE_CONTROLLER_KI 					0.0 	    //0.00001                 DEFAULT: 0.00005
#define RATE_INTEGRAL_FREEZE				250      // DEFAULT: 250
#define RATE_CONTROLLER_MIN_PWM 		-255    // DEFAULT: -500
#define RATE_CONTROLLER_MAX_PWM 		255     // DEFAULT: 500

/* Mechanical parameters */
#define WHEEL_RADIUS 						    0.03365 		// [m]  TOR CHANGED FROM 0.0326 
// distance between the two wheels
#define BASE_LENGTH 						    0.280 		// [m]  0.288   TOR CHANGED FROM 0.272

/* Define frequency loops */
Timer _frequency_rate(FREQUENCY_RATE);
Timer _frequency_odometry(FREQUENCY_ODOMETRY);
Timer _frequency_rospinonce(FREQUENCY_ROSPINONCE);
Timer _frequency_controller(FREQUENCY_CONTROLLER);

/* Define median filter for direction */
MedianFilter motor_right_direction_median_filter(RATE_DIRECTION_MEDIAN_FILTER_SIZE);
MedianFilter motor_left_direction_median_filter(RATE_DIRECTION_MEDIAN_FILTER_SIZE);

/* Define pins */
// motor A (right)
const byte motorRightEncoderPinA = 20;
const byte motorRightEncoderPinB = 21;
const byte enMotorRight = 5;
const byte in1MotorRight = 4;   //26 C1 M1
const byte in2MotorRight = 3;   //28 C2 M2

// motor B (left)
const byte motorLeftEncoderPinA = 18;
const byte motorLeftEncoderPinB = 19;
const byte enMotorLeft = 8;
const byte in1MotorLeft = 7;    //30
const byte in2MotorLeft = 6;    //32

/* Define motors variables */
// right
volatile int motor_right_inc;
int motor_right_direction;
int motor_right_filtered_direction;
float motor_right_filtered_inc_per_second;
float motor_right_rate_est;
float motor_right_rate_ref;
int motor_right_check_dir;
int motor_right_pwm_rate;
unsigned long motor_right_prev_time;
int pwmMotorRight = 0;
// left
volatile int motor_left_inc;
int motor_left_direction;
int motor_left_filtered_direction;
float motor_left_filtered_inc_per_second;
float motor_left_rate_est;
float motor_left_rate_ref;
int motor_left_check_dir;
int motor_left_pwm_rate;
unsigned long motor_left_prev_time;
int pwmMotorLeft = 0;

/* Define controllers variables */
// right
unsigned long controler_motor_right_prev_time;
float controler_motor_right_prev_epsilon = 0.0;
float controler_motor_right_int = 0.0;
// left
unsigned long controler_motor_left_prev_time;
float controler_motor_left_prev_epsilon = 0.0;
float controler_motor_left_int = 0.0;

/* Mixer variable */
float linear_velocity_ref;
float angular_velocity_ref;
float linear_velocity_est;
float angular_velocity_est;

float yaw_est;
unsigned long odom_prev_time;

/* ROS Nodehanlde */
ros::NodeHandle  nh;

/* Prototype function */
void rateControler(const float rate_ref, const float rate_est, int & pwm_rate,
                   unsigned long & prev_time, float & previous_epsilon,
                   float & integral_epsilon);
float runningAverage(float prev_avg, const float val, const int n);
void setMotorRateAndDirection(int pwm_ref, const float rate_ref,
                              const byte enMotor, const byte in1Motor, const byte in2Motor);

/* Velocity command subscriber */
// callback function prototype
void commandVelocityCallback(const geometry_msgs::Twist& cmd_vel_msg);
// message
ros::Subscriber<geometry_msgs::Twist> cmd_vel_sub("/cmd_vel", commandVelocityCallback);

/* Odometry publisher */
nav_msgs::Odometry odom;
ros::Publisher odom_publisher("odom", &odom);

/* DEBUG */
#include <geometry_msgs/Point.h>
geometry_msgs::Point debug_left;
ros::Publisher debug_publisher1("debug_left", &debug_left);

#include <geometry_msgs/Point.h>
geometry_msgs::Point debug_right;
ros::Publisher debug_publisher2("debug_right", &debug_right);

template <typename type>
type sign(type value) {
    return type((value>0)-(value<0));
}


#endif // FOXBOT_CORE_CONFIG_H_

foxbot_core.ino

/*
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 *  Description: Core script for foxbot robot.
 *  Author: Matthieu Magnon
 *
 *  Serial communication is enabled with the following command:
 *  rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=115200
 */

#include "foxbot_core_config.h"

// Real Time debug GPIO with oscilloscop
#define RT_PIN0 9 // changed from 8 due to use by encoder
#define RT_PIN1 10 // // changed from 9 due to use by encoder

void setup()
{
	// serial
	nh.getHardware()->setBaud(115200);
  //	analogWriteResolution(12);  TOR: COMMENTED SINCE IM USING ARDUINO MEGA 2560

	// ROS node initialization
	nh.initNode();

	// Subscriber
	nh.subscribe(cmd_vel_sub);

	// Builtin LED
	pinMode(LED_BUILTIN, OUTPUT);

	// blink LED
	digitalWrite(LED_BUILTIN, LOW);

	// define pin mode motor Right
	pinMode(motorRightEncoderPinA, INPUT);
	attachInterrupt(digitalPinToInterrupt(motorRightEncoderPinA), motorRightIsrCounterDirection, RISING);
	pinMode(motorRightEncoderPinB, INPUT);
	pinMode(enMotorRight, OUTPUT);
	pinMode(in1MotorRight, OUTPUT);
	pinMode(in2MotorRight, OUTPUT);

	// define pin mode motor Left
	pinMode(motorLeftEncoderPinA, INPUT);
	attachInterrupt(digitalPinToInterrupt(motorLeftEncoderPinA), motorLeftIsrCounterDirection, RISING);
	pinMode(motorLeftEncoderPinB, INPUT);
	pinMode(enMotorLeft, OUTPUT);
	pinMode(in1MotorLeft, OUTPUT);
	pinMode(in2MotorLeft, OUTPUT);

	// define tasks frequency
	_frequency_rate.start((unsigned long) millis());
	_frequency_odometry.start((unsigned long) millis());
	_frequency_controller.start((unsigned long) millis());

	nh.advertise(odom_publisher);

	// DEBUG
	nh.advertise(debug_publisher1);
	nh.advertise(debug_publisher2);

	// RT
	pinMode(RT_PIN0, OUTPUT);
	pinMode(RT_PIN1, OUTPUT);

}

void loop() {

	// rate computation
	if(_frequency_rate.delay(millis())) {
		digitalWrite(RT_PIN0, HIGH);

		float dt;

		// MOTOR RIGHT
		// direction
		motor_right_direction_median_filter.in(motor_right_direction);
		motor_right_filtered_direction = motor_right_direction_median_filter.out();

		// filter increment per second
		dt = (millis() - motor_right_prev_time);
		motor_right_prev_time = millis();
		motor_right_filtered_inc_per_second = runningAverage(motor_right_filtered_inc_per_second,
				(float)motor_right_inc / dt * 1000.0f, RATE_AVERAGE_FILTER_SIZE);

		// estimated rate
		motor_right_rate_est = (float)motor_right_filtered_direction
							 * motor_right_filtered_inc_per_second * RATE_CONV;
		motor_right_inc = 0;

		if (abs(motor_right_rate_est) < 0.1f)
			motor_right_rate_est = 0.0f;

		motor_right_check_dir = 1;
		motor_right_direction = 0;

		// MOTOR LEFT
		// direction
		motor_left_direction_median_filter.in(motor_left_direction);
		motor_left_filtered_direction = motor_left_direction_median_filter.out();

		// filter increment per second
		dt = (millis() - motor_left_prev_time);
		motor_left_prev_time = millis();
		motor_left_filtered_inc_per_second = runningAverage(motor_left_filtered_inc_per_second,
				(float)motor_left_inc / dt * 1000.0f, RATE_AVERAGE_FILTER_SIZE);


		// estimated rate
		motor_left_rate_est = (float)motor_left_filtered_direction
							* motor_left_filtered_inc_per_second * RATE_CONV;
		motor_left_inc = 0;

		if (abs(motor_left_rate_est) < 0.1f)
			motor_left_rate_est = 0.0f;
		motor_left_check_dir = 1;
		motor_left_direction = 0;

		// MIXER
		motor_right_rate_ref = (linear_velocity_ref + BASE_LENGTH / 2.f * angular_velocity_ref)
							 / (WHEEL_RADIUS);
		motor_left_rate_ref  = (linear_velocity_ref - BASE_LENGTH / 2.f * angular_velocity_ref)
							 / (WHEEL_RADIUS);

		digitalWrite(RT_PIN0, LOW);
	}

	// rate controler
	if(_frequency_controller.delay(millis())) {
		digitalWrite(RT_PIN1, HIGH);

		// MOTOR RIGHT
		rateControler(motor_right_rate_ref, motor_right_rate_est, motor_right_pwm_rate,
					  controler_motor_right_prev_time, controler_motor_right_prev_epsilon,
					  controler_motor_right_int);
		pwmMotorRight = pwmMotorRight + motor_right_pwm_rate;
		pwmMotorRight = constrain(pwmMotorRight, 0, PWM_MAX);
		setMotorRateAndDirection(pwmMotorRight, motor_right_rate_ref, enMotorRight,
								 in1MotorRight, in2MotorRight);

		// MOTOR LEFT
		rateControler(motor_left_rate_ref, motor_left_rate_est, motor_left_pwm_rate,
					  controler_motor_left_prev_time, controler_motor_left_prev_epsilon,
					  controler_motor_left_int);
		pwmMotorLeft = pwmMotorLeft + motor_left_pwm_rate;
		pwmMotorLeft = constrain(pwmMotorLeft, 0, PWM_MAX);
		setMotorRateAndDirection(pwmMotorLeft, motor_left_rate_ref, enMotorLeft,
								 in1MotorLeft, in2MotorLeft);

		// DEBUG
		debug_left.x = motor_left_rate_ref;
		debug_left.y = motor_left_rate_est;

		debug_publisher1.publish(&debug_left);

		// DEBUG
		debug_right.x = motor_right_rate_ref;
		debug_right.y = motor_right_rate_est;
		debug_right.z = pwmMotorRight;
		debug_publisher2.publish(&debug_right);
		digitalWrite(RT_PIN1, LOW);
	}

	// update odometry
	if(_frequency_odometry.delay(millis())) {
		float dt, dx, dy;
		float qw, qx, qy, qz;

		dt = (float)(millis() - odom_prev_time) * 0.001f;
		odom_prev_time = millis();

		// compute linear and angular estimated velocity
		linear_velocity_est = WHEEL_RADIUS * (motor_right_rate_est + motor_left_rate_est) / 2.0f;
		angular_velocity_est = (WHEEL_RADIUS / BASE_LENGTH)
							 * (motor_right_rate_est - motor_left_rate_est);

		// compute translation and rotation
		yaw_est += angular_velocity_est * dt;
		dx = cos(yaw_est) * linear_velocity_est * dt;
		dy = sin(yaw_est) * linear_velocity_est * dt;

		// DEBUG
		debug_left.y = yaw_est * 57.2958;
		debug_left.z = angular_velocity_est * dt;
		debug_publisher1.publish(&debug_left);

		// compute quaternion
		qw = cos(abs(yaw_est) / 2.0f);
		qx = 0.0f;
		qy = 0.0f;
		qz = sign(yaw_est) * sin(abs(yaw_est) / 2.0f);

		// feed odom message
		odom.header.stamp = nh.now();
		odom.header.frame_id = "odom";
		odom.child_frame_id = "base_link";
		odom.pose.pose.position.x += dx;
		odom.pose.pose.position.y += dy;
		odom.pose.pose.position.z = 0.0;
		odom.pose.pose.orientation.w = qw;
		odom.pose.pose.orientation.x = qx;
		odom.pose.pose.orientation.y = qy;
		odom.pose.pose.orientation.z = qz;
		// Velocity expressed in base_link frame
		odom.twist.twist.linear.x = linear_velocity_est;
		odom.twist.twist.linear.y = 0.0f;
		odom.twist.twist.angular.z = angular_velocity_est;

    	odom_publisher.publish(&odom);
	}

	// update subscribers values
	if(_frequency_rospinonce.delay(millis())) {
		nh.spinOnce();
	}
}

void commandVelocityCallback(const geometry_msgs::Twist& cmd_vel_msg) {
	linear_velocity_ref  = cmd_vel_msg.linear.x;
	angular_velocity_ref = cmd_vel_msg.angular.z;
}

void motorRightIsrCounterDirection() {
	motor_right_inc ++;
	if ( motor_right_check_dir == 1) {
		if (digitalRead(motorRightEncoderPinB) && digitalRead(motorRightEncoderPinA)){
			motor_right_direction = 1;;
		} else {
			motor_right_direction = -1;
		}
		motor_right_check_dir = 1;
	}
}

void motorLeftIsrCounterDirection() {
	motor_left_inc ++;
	if ( motor_left_check_dir == 1) {
		if ( digitalRead(motorLeftEncoderPinB) && digitalRead(motorLeftEncoderPinA)){
			motor_left_direction = 1;
		} else {
			motor_left_direction = -1;
		}
		motor_left_check_dir = 0;
	}
}

void rateControler(const float rate_ref, const float rate_est, int & pwm_rate,
				   unsigned long & prev_time, float & previous_epsilon, float & i_epsilon) {

	float epsilon = abs(rate_ref) - abs(rate_est);
	float d_epsilon = (epsilon - previous_epsilon) / (prev_time - millis());

	// reset and clamp integral (todo : add anti windup)
	if (rate_ref == 0.0) {
		i_epsilon = 0.0;
	} else {
		i_epsilon += epsilon * (prev_time - millis()) * RATE_CONTROLLER_KI;
	}
	i_epsilon = constrain(i_epsilon, -RATE_INTEGRAL_FREEZE, RATE_INTEGRAL_FREEZE);

	prev_time = millis();
	previous_epsilon = epsilon;

	debug_left.z = i_epsilon * RATE_CONTROLLER_KI;

	pwm_rate = epsilon * RATE_CONTROLLER_KP
			 + d_epsilon * RATE_CONTROLLER_KD
			 + i_epsilon * RATE_CONTROLLER_KI;

	// saturate output
	pwm_rate = constrain(pwm_rate, RATE_CONTROLLER_MIN_PWM, RATE_CONTROLLER_MAX_PWM);
}

float runningAverage(float prev_avg, const float val, const int n) {

	return (prev_avg * (n - 1) + val) / n;
}

void setMotorRateAndDirection(int pwm_ref, const float rate_ref, const byte enMotor, const byte in1Motor, const byte in2Motor) {

		// avoid noisy pwm range
		if (abs(rate_ref) < 0.1)
			pwm_ref = 0;

		// write direction
		if (rate_ref > 0) {
			digitalWrite(in1Motor, LOW);
			digitalWrite(in2Motor, HIGH);
		}
		else if (rate_ref < 0) {
			digitalWrite(in1Motor, HIGH);
			digitalWrite(in2Motor, LOW);
		}

		// write pwm
		analogWrite(enMotor, pwm_ref);
}

Motors: https://www.banggood.com/Machifit-25GA370-DC-6V-Micro-Gear-Reduction-Motor-with-Encoder-Speed-Dial-Reducer-p-1491039.html?rmmds=myorder

Driver: Dc 5-12v 30a pwm dual channel motor control module h bridge motor drive controller board dhb-1a reversible control and pwm speed control for ordinary dc motor/smart car motor Sale - Banggood.com-arrival notice-arrival notice

github of original foxbot code: foxbot_core/foxbot_core.ino at master · MattMgn/foxbot_core · GitHub

Hello @traemand , welcome.

In my opinion if you know little about electronics and code you are in well over your head. Start simple and learn and build up to this project.

While I make no claim to knowing all there is to know about micro-controllers some of the things you mention are completely unknown to me, what is a teleopnode or a /rosserial node?

To me your project is the equivalent of me trying to tune a formula 1 racing engine.

Hopefully someone else can be more helpful.

Thanks for replying. I appreciate your concern, but i may have also exaggerated my lack of knowledge. While I do agree, I’m in over my head, I find that this is a passion project and I’m learning things as it’s needed in this project.

I should’ve probably left out the ROS related part, since i think it’s very likely it all comes down to the arduino code. One of the reason i brought it up is due to my concern that possibly the MEGA is having a hard time handling all the ROS stuff, and maybe thats affecting the frequency of the controller?

I had to google what ROS was (= Robot Operating System), which seems to run on Linux platforms. Looking at the fritzing sketch, I assume that the hard work is done on the Pi and commands/data exchanged with the Mega over the USB serial link.

It can be a bit of a black art trying to figure out if the processor you have (in this case, the Mega) is capable of doing everything you need in the time you need it to. Have you tried contacting the author you mentioned to see if they have been down the Mega route before using the Due?

A brief look over the code and you have a lot of float variables. The Mega doesn't have any hardware floating point support so it takes more time to compute in software. I don't think that the Due has hardware floating point support either, but the Due has the advantage of running a lot faster than the Mega.

Thanks for taking the time Mark. I have contacted him, but haven’t heard anything. He briefly mentions the interchangeability with other boards somewhere in his Instructables description, but I don't get the idea that it's tried and tested. I'm considering just getting a DUE to see if it works "out of the box", but i also dont like the idea of just throwing hardware at the problem.. unless of course it's actually needed.
You say it's a black art - any way of actually debugging this kind of stuff? How do i find out if i'm pushing the MEGA too hard to keep up?

In your loop(), there's a some digitalWrite() to RT_PIN0 & RT_PIN1. These seem to be set high for the duration of some activity and then set low again. I'm not sure what:

_frequency_rate.delay(millis())

does, but I assume it's performing some sort of check that a specific time interval has elapsed.

If that interval was, say, 10ms, then you should see a regular pulse starting every 10ms of some width. If you are running out of processing time, then I would expect to see that 10ms gap become longer as you added more processing.

Yes, that’s right, I wasn’t sure about those pins so I put some arbitrary unused pins. This sounds like I should get the oscilloscope out and check these pins?

Yes, a 'scope is a very useful tool to have. Anything that has been set to run at specific intervals will show signs of being delayed if you are running out of compute time.

Alright, thanks, I’ll try and get my hands on one and see if I can see anything suspicious

This may be of no use to you as you are using ROS, but...

I recently did a 'from scratch' robot build (ie, no ROS) of a similar robot and have been documenting it in my blog. Some of the things you mention were problems I saw and had to overcome. The series is still in progress but you may want to look at the latest post ([Basic SmartCar Bot – Control – Arduino++](Basic SmartCar Bot – Control – Arduino++ as it deals with the 'jittery wheels'. In my case the PWM frequency was way too high for the DC motor controller.

Is there a way we can follow this project, maybe on GitHub? This looks really cool... I'm curious to see how this will work in the end but don't know enough to help you, sorry.

Hey, I’ll check that out and let you know if any of it solves my problem! Thanks a lot for that heads up :v:

Hey, sure thing! I'm considering where to share this project, as it's still very much a "work in progress" project, and i'll need to learn a lot more as i go. I would love to join a sort of "robot builders club", or similar, where people help each other out and showcase projects. Any recommendations on the best platform for that ? I was thinking of making a hackaday project at some point in the future when the project is actually working, and document the heck out of it for other people to reproduce, but until then you can follow the project on my Instagram page (which is probably not the most relevant platform, anyway): https://www.instagram.com/torpanvilbot/

1 Like

Yeah, hackaday would be great! Until then, I'll follow your progress on insta. Good luck!

1 Like

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.