November 27, 2017 / by Sergey Kapustin

Integration of ROS and Arduino

ROS has good messaging system that uses publisher/subscriber model. My project requires an Arduino to talk to a ROS network. Rosserial for Arduino exists to enable such communication. My previous attempt to use rosserial on Atmega168 was not successful due to 1 kilobyte SRAM limit on the Atmega. This time, I will use Atmega2560 with 8 kilobytes of SRAM.

The goal of this post is to go through a set-up procedure of a ROS package in order to build and install AVR firmware. I also describe a couple of problems and solutions that I came across during the procedure.

ROS packages are built with catkin build system which lets me

  1. Use a unified system for development of ROS software and Arduino firmware
  2. Avoid using a graphical user interface such as Arduino IDE, but do all of my development in vim editor

I plan to use ROS on a Raspberry Pi as a controller. Since Raspberry Pi is short on resources, I chose to develop software on a desktop computer. Once the code is more or less in condition for testing on real hardware, I’ll sync the required build artifacts to the Raspberry Pi.

On YouTube

Procedure

At this point, ROS kinetic is already installed in Ubuntu 16.04 virtual box that runs on my mac. I ssh-ed into the VM, and ready to go.

First, I need to create a ROS workspace, which I named clawbot_ws. The workspace needs to be re-created whenever I switch to a new dev box.

1
2
3
4
5
$ export CLAWBOT_WS=${HOME}/dev/projects/clawbot_ws
$ source /opt/ros/kinetic/setup.bash
$ mkdir -p ${CLAWBOT_WS}/src
$ cd ${CLAWBOT_WS}
$ catkin_make

Second, I initialize ROS environment. For more details, see ROS wiki.

1
$ source ${CLAWBOT_WS}/devel/setup.bash

The environment has to be initialized every time I open a new shell. Often, I forget to do that, and then I waste my brain cycles to determine what’s wrong with the build. Therefore, I wrote a shell function and put it into ~/.bashrc so that it gets executed automatically whenever I open a new shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function ros() {
    if [ -d "/opt/ros/kinetic" ]; then
        export ROS_HOME=/opt/ros/kinetic
        export ROS_MASTER_URI=http://${HOSTNAME}:11311/
        export ROS_LOG_DIR=/var/log/ros
        export CLAWBOT_WS=${HOME}/dev/projects/clawbot_ws

        if [ -f "${ROS_HOME}/setup.bash" ]; then
            source ${ROS_HOME}/setup.bash

            if [ -f ${CLAWBOT_WS}/devel/setup.bash ]; then
                source ${CLAWBOT_WS}/devel/setup.bash
                roscd clawbot
            fi
        fi
    fi
}

ros

There is more stuff you can put into the function like I did here. For example, I change log directory to something I prefer. The function would take me directly to the source code via the command roscd clawbot. Of course, the package has to exist first. So, now, I go ahead and create the package:

1
2
$ cd ${CLAWBOT_WS}/src
$ catkin_create_pkg clawbot std_msgs rosserial_arduino rosserial_client

The tutorial in ROS wiki instructs to replace the content of CMakeLists.txt with their example. Instead, I will keep that auto-generated code for later modifications. For now, I just add or update the required macros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
project(clawbot)

find_package(catkin REQUIRED COMPONENTS
  rosserial_arduino
  rosserial_client
  std_msgs
)

catkin_package()

rosserial_generate_ros_lib(
  PACKAGE rosserial_arduino
  SCRIPT make_libraries.py
)

rosserial_configure_client(
  DIRECTORY firmware
  TOOLCHAIN_FILE ${ROSSERIAL_ARDUINO_TOOLCHAIN}
)

rosserial_add_client_target(firmware m2560 ALL)
rosserial_add_client_target(firmware m2560-upload)

I use m2560 for the target name, because it’s easier see what firmware I’m building for.

Now, create directory firmware and write the following CMakeLists.txt file into it

1
2
3
$ roscd clawbot
$ mkdir firmware
$ vi firmware/CMakeLists.txt

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 2.8.3)

include_directories(${ROS_LIB_DIR})
add_definitions(-DUSB_CON)

generate_arduino_firmware(
    m2560
    SRCS main.cpp
    BOARD mega2560
    PORT /dev/ttyACM0
)

Also, add the following main.cpp file into the same directory:

1
$ vi firmware/main.cpp

main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * main.cpp
 */
#include <ros.h>
#include <std_msgs/String.h>

#if defined(ARDUINO)
#include <Arduino.h>
#endif

ros::NodeHandle nh;
std_msgs::String str_msg;
ros::Publisher chatter("chatter", &str_msg);

char hello[13] = "hello world!";

void setup() {
    nh.initNode();
    nh.advertise(chatter);
}

void loop() {
    str_msg.data = hello;
    chatter.publish(&str_msg);
    nh.spinOnce();
    delay(1000);
}

For C++ projects, I prefer to name the file with function main() as main.cpp. It makes it easier to mentally locate the starting point when looking at a new project.

Now, initiate the build from the workspace directory:

1
2
$ pushd ${CLAWBOT_WS}
$ catkin_make VERBOSE=1 -j4 clawbot_firmware_m2560

I got an error that ros-kinetic-rosserial-arduino and ros-kinetic-rosserial packages could not be found. Simple enough to fix:

1
2
$ sudo apt install ros-kinetic-rosserial-arduino
$ sudo apt install ros-kinetic-rosserial

Let’s try again:

1
$ catkin_make VERBOSE=1 -j4 clawbot_firmware_m2560

This time, I got a different error:

1
2
3
CMakeFiles/m2560.dir/main.cpp.obj: In function `_GLOBAL__sub_I_nh':
/home/sergey/dev/projects/clawbot_ws/build/clawbot/ros_lib/ros/publisher.h:50: undefined reference to `ros::normalizeSecNSec(unsigned long&, unsigned long&)'
collect2: error: ld returned 1 exit status

The problem here is that C++ linker cannot find a symbol referring to a function ros::normalizeSecNSec.

Ok, let’s think:

  • Did I implement all functions?
  • For sure, there were only a couple anyway
  • Did I link against all required libraries?
  • The tutorial didn’t specify anything extra I needed to do. So, I’m pretty sure yes
  • Who calls that function anyway?
  • The error message shows file publisher.h and line 50, but the file doesn’t contain any call to normalizedSecNSec. Moreover, there is this global symbol _GLOBAL__sub_I_nh
  • OK, I need to take a look at time.cpp
1
2
3
4
5
$ find /home/sergey/dev/projects/clawbot_ws/build/clawbot/ros_lib/ -name "*.h" -o -name "*.cpp" | xargs grep normalizeSecNSec
...
/home/sergey/dev/projects/clawbot_ws/build/clawbot/ros_lib/ros/time.h:  void normalizeSecNSec(uint32_t &sec, uint32_t &nsec);
/home/sergey/dev/projects/clawbot_ws/build/clawbot/ros_lib/time.cpp:  void normalizeSecNSec(uint32_t& sec, uint32_t& nsec){
...
  • Who is supposed to build that file? I know nothing about what it contains.

When I go back to the wiki and double-check the example, I see this:

1
2
3
...
SRCS chatter.cpp ${ROS_LIB_DIR}/time.cpp
...

The second file in SRCS is time.cpp! When writing CMakeLists.txt, I removed it because nothing in my source code was using it. After all, I didn’t have to build any other ROS files using my project’s CMakeLists.txt.

It seems like an oversight. Why the file couldn’t be built as part of a ROS library? Alternatively, all the code in time.cpp could be implemented in the header file time.h. Who knows?

In any case, once I include the file in my CMakeLists.txt, the build completes fine.

Let’s look at the firmware size and make sure we still have room for more code of ours:

1
2
3
4
5
6
7
8
9
AVR Memory Usage
----------------
Device: atmega2560

Program:   10302 bytes (3.9% Full)
(.text + .data + .bootloader)

Data:       2343 bytes (28.6% Full)
(.data + .bss + .noinit)

The field Data indicates the amount of used up RAM at compile time, and it’s around 29%. We still have ~70% for the runtime. Although, it would be good to check the amount of SRAM used by the above hello-world program during the runtime. There is still 96% of free space for the program code.

Now, I upload the firmware to the Arduino:

1
$ catkin_make VERBOSE=1 -j4 clawbot_firmware_m2560-upload

Before I run the test, I need to add my user account to dialout group so that I’m allowed to access the USB device. I also restart the VM (logging out and back in would also work):

1
2
$ sudo usermod -a -G dialout sergey
$ sudo init 6

Also, since my $ROS_LOG_DIR points to /var/log/ros directory, I create it and change permissions like so:

1
2
$ sudo mkdir /var/log/ros
$ sudo chmod a+w /var/log/ros

Now, open three console windows.

In console 1, I start the ROS core

1
$ roscore

In console 2, I start a rosserial node to listen for messages from the Arduino:

1
$ rosrun rosserial_python serial_node.py /dev/ttyACM0

In console 3, I print the received messages

1
$ rostopic echo chatter

My console 3 shows this:

1
2
3
4
5
6
data: hello world!
---              
data: hello world!
---              
data: hello world!
---

At this point, the results look good enough to me to move forward with my project.

You may also like

December 18, 2017

IR Sensor ADC trigger, Median Filter, Analysis

Have you thought of how to filter the data coming from an analog sensor? Why do you even want to filter that data? Should you use arithmetic mean or median? Why can’t you just grab a sample from Arduino::analogRead() and be done with it?

January 09, 2018

Custom Protocol for Serial Link Communication

After spending your time on learning advanced “hello world” tutorial of some third-party library, and trying to code something more useful, you hit your first problem. Scavenging the internet for help on seemingly simple issues burns more of your precious time with little return. You think - there has to be a better way to move your project along.

December 06, 2017

IR Sensor Noise, Capacitor and Power

After connecting an infrared sensor to my Arduino, I noticed that an ultrasonic sensor, which is connected to the same board, started reporting inconsistent measurements. The investigation took me a few hours, but now I know better to pay attention to the power requirements for electronic components.