November 27, 2017 / by Sergey Kapustin
Ros Arduino Integration
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
- Use a unified system for development of ROS software and Arduino firmware
- 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.