預安裝
- Ubuntu 16.04
- ROS kinetic (base 即可)
從最簡單的例子開始
使用終端命令行進行程序編譯
首先創建一個文件夾 hello_world_tutorial,存放我們的程序
mkdir hello_world_tutorial
cd hello_world_tutorial
創建 C++ 源文件,名為 hello_world_node.cpp
:
// 為了與 ROS 交互,需要調用 ROS C++ APIs
#include <ros/ros.h>
// 標准的 C++ main 函數
int main(int argc, char** argv) { // 該命令告訴 ROS 初始化了一個 node,名為 hello_world_node
ros::init(argc, argv, "hello_world_node"); // 在一般的 ROS node 程序中,我們會用 ros::NodeHandle nh 來啟動 node 程序, // ros::NodeHandle nh 默認會調用 ros::start() 函數,程序關閉時也會自動調用 ros::shutdown() 函數。 // 我們也可以直接通過 ros::start() 和 ros::shutdown() 來手動控制 node 的開啟和關閉
ros::start(); // 顯示 hello, world! 信息
ROS_INFO_STREAM("Hello, world!"); // 用 ros::spin() 保持該程序運行,一直等待處理 subscribe 的數據 // 由於該程序並沒有 sub,所以就是簡單的保持程序不退出而已, 直到接受到終止信號 SIGINT (ctrl-c)
ros::spin(); // 關閉 node 程序
ros::shutdown(); // 結束主程序
return 0; }
下邊將 C++ 源文件編譯成可執行文件
g++ hello_world_node.cpp -o hello_world_node -I/opt/ros/kinetic/include -L/opt/ros/kinetic/lib -Wl,-rpath,/opt/ros/kinetic/lib -lroscpp -lrosconsole
各參數含義
-I<dir>
指定頭文件的搜索路徑-L<dir>
指定靜態庫的搜索路徑-Wl,-rpath,/opt/ros/kinetic/lib
指定共享庫的搜索路徑-lroscpp -lrosconsole
指定需要鏈接的具體的庫文件
編譯之后,生成 hello_world_node 可執行文件。由於程序中生成了 ROS node,而 ROS node 需要與 ROS master 進行通訊注冊,否則會報錯。因此為了正常運行程序,需要先開啟 ROS master
roscore
然后運行 hello_world_node
./hello_world_node
如果一切順利,應該顯示類似如下信息:
[ INFO] [1561908777.116073864]: Hello, world!
上述編譯方式擴展性很差,對於如此簡單的 hello_world 程序,需要設置的參數已經這么多了。而且在 terminal 中書寫比較麻煩,修改也不方便。
上面是使用g++編譯器直接在終端中對源文件進行編譯,通過上面的過程了解到,當編譯的源文件需要鏈接其他庫文件、頭文件時需要額外添加命令以及文件路徑,這樣當需要鏈接的文件比較多時或者編譯文件進行修改重新編譯時,非常不方便,如果能在腳本文件中將編譯命令以及鏈接文件的路徑全部鏈接到,然后在終端直接執行這個腳本文件豈不是更加方便。因此前人進行了改進
改進:使用 Makefile 文件進行編譯
在Makefile中寫編譯規則(頭文件路徑、源文件路徑等),終端中直接make命令執行Makefile文件實現程序編譯
Makefile 編譯方式是將上述編譯命令和參數設置放入一個文件中,然后基於該文件,完成編譯過程。Makefile 文件有自己的一套語法規則,可以實現批量、相對自動化的編譯。
與前述 hello world 程序對應的 Makefile 文件內容如下:
# 聲明要使用的編譯器 CC=g++ # 聲明一些變量,實際上就是對應上述搜索路徑設置 CFLAGS=-I/opt/ros/kinetic/include LDFLAGS=-L/opt/ros/kinetic/lib -Wl,-rpath,/opt/ros/kinetic/lib -lroscpp -lrosconsole # % 作為通配符,代表對一類滿足條件的文件進行操作 # 這是由源文件 *.cpp 編譯成目標文件 *.o 的操作 %.o: %.cpp $(CC) -c -o $@ $< $(CFLAGS) # 也可以不用通配符,具體寫出要編譯的文件 # 這是由目標文件 *.o 通過鏈接 (linking) 操作生成最終的可執行文件 hello_world_node: hello_world_node.o $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)
對於 Makefile 的介紹,可以參考這里。
Makefile 文件的基本格式是
target: pre-req command
即,希望生成 target 文件,依賴 pre-req 文件,通過 command 命令實現。
需要注意的是,Makefile 要求 command 那一行開頭用 TAB 鍵縮進,不能用空格,如果出現如下報錯:
makefile:...: *** missing separator. Stop
說明誤用了空格鍵。如果你跟我一樣用的是 vs code 編輯器,可以在右下角選擇 Indent Using Tabs
。
將上述 Makefile 文件放在與 hello_world_node.cpp 同一路徑下,然后編譯
make # 或者指明某個 target 編譯任務,如: make hello_world_node
Makefile 編譯方式相比於剛才的命令行編譯方式有如下優點:
- 在設置好 Makefile 的前提下,編譯命令更簡單,只需要
make
,不必每次都輸入一長串命令 - Makefile 中將編譯和鏈接分開進行,如果項目中包含多個 c++ 源文件,改動了其中的一個,只需要重新生成改動文件的目標文件 (*.o) 即可,其他源文件不需要重新編譯,然后基於更新之后的目標文件,生成新的可執行文件。也就是說,如果源文件沒有改變,就不會浪費時間更新目標文件。
在書寫上邊的 Makefile 文件時,我們依然要明確設定頭文件和 library 的搜索路徑。為了進一步簡化這個過程,我們可以在 Makefile 中使用 pkg-config 設置搜索路徑。
改進:在 Makefile 中使用 pkg-config 設置搜索路徑
通過pkg-config變量設置搜索路徑,改寫Makefile文件,簡化了一些書寫改變不大
實際上,library 對應的搜索路徑包含在與該 library 對應的.pc
文件中,例如
roscpp library 對應的 .pc
文件為 /opt/ros/kinetic/lib/pkgconfig/roscpp.pc
,里面內容如下
prefix=/opt/ros/kinetic Name: roscpp Description: Description of roscpp Version: 1.12.14 Cflags: -I/opt/ros/kinetic/include -I/usr/include Libs: -L/opt/ros/kinetic/lib -lroscpp -lpthread /usr/lib/x86_64-linux-gnu/libboost_chrono.so ... Requires: cpp_common message_runtime rosconsole roscpp_serialization ...
.pc
文件里面的 Cflags
和 Libs
條目就是調用 roscpp 時要設置的路徑信息。我們可以通過 pkg-config 這個工具查找 roscpp.pc
文件,然后提取其中的路徑信息,放入 Makefile 中,這樣就避免了手動輸入。例如
$ pkg-config --cflags roscpp -I/opt/ros/kinetic/include $ pkg-config --libs roscpp -L/opt/ros/kinetic/lib -lroscpp -lpthread /usr/lib/x86_64-linux-gnu/libboost_chrono.so ...
因此,我們可以改寫 Makefile 文件如下:
CC=g++ # 通過 pkg-config 設置相應的路徑信息 CFLAGS=$(shell pkg-config --cflags roscpp) LDFLAGS=$(shell pkg-config --libs roscpp) %.o: %.cpp $(CC) -c -o $@ $< $(CFLAGS) hello_world_node: hello_world_node.o $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)
然后依然用 make
命令編譯文件,與前邊編譯方式相同,最終也是生成 hello_world_node 可執行文件。
在使用 pkg-config 時需要確保它能夠找到相應的 library。pkg-config 有自己的搜索 library 的路徑,存放在環境變量 PKG_CONFIG_PATH
中,可以通過 echo 命令查看
echo $PKG_CONFIG_PATH
如果我們安裝完 ROS,並且運行了source /opt/ros/kinetic/setup.bash
, ROS 相關的 library 對應的 .pc
文件就被加入了 pkg-config 的搜索路徑。通過 pkg-config <library> 就可以搜到相應的信息。
盡管 pkg-config 簡化了 Makefile 中設置頭文件和 library 路徑的過程,但是 Makefile 文件中后續的編譯過程依然需要手動設置。另外這里手動書寫的編譯命令是與操作系統平台相關的,Linux 中的編譯命令不能在 Windows 中使用,這就導致 Makefile 不能跨平台使用。
改進: CMake 跨平台編譯方式
Makefile不同跨平台使用,所以CMake應勢而生,解決程序跨平台編譯問題。
CMake 的一個功能是自動生成 Makefile 文件。另外,CMake 可以在 Linux 、Windows 和 Mac OS 上使用。
要使用 CMake,首先要創建一個 CMakeLists.txt 文件,包含必要的編譯設置。
與上述 hello_world_node 例子對應的 CMakeLists.txt 內容如下:
# 聲明 CMake API 版本 cmake_minimum_required(VERSION 2.8) # 聲明項目名稱 project(hello_world_tutorial) # 搜索依賴 library (即 roscpp) 的信息 # 與 pkg-config 功能類似,但可以跨平台使用 # pkg-config 查找 .pc 配置文件,而 find_package 查找 .cmake 配置文件 find_package(roscpp REQUIRED) # 搜索 roscpp 中調用的頭文件 include_directories(${roscpp_INCLUDE_DIRS}) # 設置待生成的可執行文件名字 add_executable(hello_world_node hello_world_node.cpp) # 設置編譯過程中 linking library target_link_libraries(hello_world_node ${roscpp_LIBRARIES})
find_package(roscpp REQUIRED)
會自動定義幾個變量,包括 roscpp_INCLUDE_DIRS
,roscpp_LIBRARY_DIRS
,roscpp_LIBRARIES
。在 CMakeLists.txt 中可以直接使用這些變量。REQUIRED
參數的作用是在找不到相應 library 時停止並報錯,提示
-- Configuring incomplete, errors occurred! See also ".../CMakeFiles/CMakeOutput.log".
如果不加 REQUIRED
,則只會提示找不到 library,整個過程並不會停止,顯示信息如下:
-- Configuring done -- Generating done -- Build files have been written to: ...
盡管顯示各種 done
,由於沒有找到必要的 library ,后續的編譯肯定會不成功。
通過 CMakeLists.txt 進行編譯時會產生一些中間文件,如果都放在 .cpp 源文件目錄下,會顯得很雜亂。最好單獨建一個文件夾,存放這些編譯文件。例如在 .cpp 源文件和 CMakeLists.txt 同一路徑下新建 build 文件夾。新的路徑結構如下:
├── build
├── CMakeLists.txt
└── hello_world_node.cpp
CMakeLists.txt 中的 find_package 之所以能找到相應的 library,是因為已經設置了搜索路徑,存放在環境變量 CMAKE_PREFIX_PATH
中,通過 echo $CMAKE_PREFIX_PATH
可以顯示當前 find_package 使用的搜索路徑。在安裝完 ROS 之后,source 命令會自動將 ROS 相關的 library 加入上述搜索路徑中。
通過 cmake
和 CMakeLists.txt
自動生成編譯文件 Makefile:
cd build # 進入剛才創建的 build 文件夾
cmake .. # 運行 cmake,它會調用上一層路徑中的 CMakeLists.txt 文件
運行完上述命令以后,產生了一些新文件,路徑結構如下:
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ └── Makefile
├── CMakeLists.txt
└── hello_world_node.cpp
可以看到自動產生了 Makefile ,此時就可以用 make
命令編譯文件了。
這里借張圖展示一下 CMake 編譯方式跨平台的能力
( From: https://cgold.readthedocs.io/en/latest/overview/cmake-can.html)

到了這里,我們就已經解決了最初的項目需求:讓 C++ 程序將內部變量以 ros topic 的形式發布出來。基本步驟:
1 . 改寫 C++ 程序,加入 ROS 元素,如 ros 頭文件,msg 頭文件等,設置 ros::init,
ros::NodeHandle ,pub msg 等,這些 ROS 元素可以使 C++ 程序在 ROS master 中以 ROS node 的形式注冊。
2. 我們原來的 C++ 程序有自己的 CMakeLists.txt 文件,在其中添加依賴的 ROS library。
3. 用基本的 cmake 方式編譯即可。
改進:針對 ROS 系統的 Catkin 編譯方式
ROS 的 Catkin 編譯系統的一個特點是將程序做成 package (稱為 catkin package 或者 ROS package) 的形式,可以理解成模塊化。典型的 ROS workspace 中包含 src, build, devel 三個文件夾,在分享時只需要分享 src 中的某個 package 即可,所有的編譯信息都在此 package 中。一個 package 在編譯時可以指定依賴於另一個 package。
另外,由於 ROS 中程序以及 library 變動比較頻繁,不太適合在整個系統層面安裝編譯之后的文件,通過 source devel 文件中的 setup.bash 文件可以告知系統去哪里查找相應的文件,避免了系統級的安裝 。
要構造 ROS package,我們首先要修改 CMakeLists.txt 文件如下:
cmake_minimum_required(VERSION 2.8) project(hello_world_tutorial) # 要用到 catkin find_package(catkin REQUIRED) # 聲明該項目為一個 catkin package catkin_package() find_package(roscpp REQUIRED) include_directories(${roscpp_INCLUDE_DIRS}) add_executable(hello_world_node hello_world_node.cpp) target_link_libraries(hello_world_node ${roscpp_LIBRARIES})
另外,還需要添加一個 package.xml 文件,指明該 package 在編譯和運行時依賴於哪些其他 package,同時也包含該 package 的一些描述信息,如作者、版本等。內容如下:
<package>
<name>hello_world_tutorial</name>
<maintainer email="you@example.com">Your Name</maintainer>
<description> A ROS tutorial. </description>
<version>0.0.0</version>
<license>BSD</license>
<!-- Required by Catkin -->
<buildtool_depend>catkin</buildtool_depend>
<!-- Package Dependencies -->
<build_depend>roscpp</build_depend>
<run_depend>roscpp</run_depend>
</package>
現在路徑結構如下:
├── build
├── CMakeLists.txt
├── hello_world_node.cpp
└── package.xml
跟之前一樣,進入 build 文件夾中,用 cmake + make 方式編譯
cd build
cmake ..
make
編譯結束之后會發現,並沒有在 build 根目錄下生成可執行文件。與普通的 cmake 編譯不同,catkin 編譯會生成一個 devel
文件夾,這里包含了生成的可執行文件,以及作為 library 使用的配置文件 .pc
,.cmake
。
對於我們的 hello_world_node package 來說,上述文件路徑如下:
- 可執行文件:
devel/lib/hello_world_tutorial/hello_world_node
.pc
配置文件:devel/lib/pkgconfig/hello_world_tutorial.pc
.cmake
配置文件:devel/share/hello_world_tutorial/cmake/hello_world_tutorialConfig.cmake
當作為 library 使用時,只需要將路徑 .../devel/lib/pkgconfig
添加到 PKG_CONFIG_PATH
環境變量中,或者將 .../devel
添加到 CMAKE_PREFIX_PATH
變量中。實際上,我們不需要手動設置這些環境變量,只需要通過 source devel 文件夾下的 setup.bash 文件即可,source setup.bash 不僅添加了以上兩個環境變量,還有諸如 ROS_PACKAGE_PATH
,PYTHONPATH
等。
source 之后,由於該 package 加入了 ROS_PACKAGE_PATH
,此時可以通過 ROS 相關的命令對該 package 進行操作,如 rospack find ...
, rosrun <package> <exe>
, roscd <package>
等。
為了更有條例地存放不同類型的文件,可以建立三個文件夾 src
, build
, devel
,其中 src
存放源文件,源文件又以 package 為單位分別存放,build
存放編譯過程中的中間文件,devel
存放最終生成的可執行文件和配置文件。這就是所謂的 out-of-source 編譯方式。在分享、發布程序時,我們可以很清楚的知道哪些是必要的源文件,哪些是最終生成的可執行文件和 library,哪些是作為副產品存在的中間文件。
路徑結構如下:
├── build
├── devel
└── src
└── hello_world_tutorial
├── CMakeLists.txt
├── hello_world_node.cpp
└── package.xml
在做了以上路徑設置之后,在編譯時,我們就需要特別指定各類文件對應的路徑:
cd build cmake ../src/hello_world_tutorial -DCATKIN_DEVEL_PREFIX=../devel make
catkin 的特點還體現在編譯多個 package 中。
我們可以在 src 文件夾中再添加一個 catkin package,這里我們就直接從網上下載一個簡單的 package:
git clone https://github.com/ros/robot_state_publisher.git -b kinetic-devel
現在路徑結構如下:
├── build
├── devel
└── src
├── hello_world_tutorial
│ ├── CMakeLists.txt
│ ├── hello_world_node.cpp
│ └── package.xml
└── robot_state_publisher
├── CHANGELOG.rst
├── CMakeLists.txt
├── doc.dox
├── include
├── package.xml
├── src
└── test
cd src ln -s /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake CMakeLists.txt
實際上,ROS 為我們提供了專門的命令,實現上述操作:
cd src
catkin_init_workspace src
此時,路徑結構如下:
├── build ├── devel └── src ├── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake ├── hello_world_tutorial └── robot_state_publisher
這就是典型的 ROS workspace 的結構。
此時就可以使用 cmake
同時編譯 src 中的所有 package 了,命令如下:
cd build cmake ../src -DCATKIN_DEVEL_PREFIX=../devel make
將以上三個命令合並在一起就是 ROS 中的 catkin_make 命令。