从零到一:在ROS里把自己写的程序编译并跑起来
前两天有个刚入坑ROS的朋友问我,说自己在Ubuntu上把ROS装好了,也在网上抄了一个发布者订阅者的小例子,代码看懂了,但一说到“编译”“运行”就懵了。他原话是:“我就想让自己写的那几行代码跑起来,怎么比写代码还难。”
这个问题我太有感触了。很多人第一次接触ROS,卡住的往往不是C++语法,也不是消息通信机制,而是根本不知道ROS那套“编译”“运行”的流程到底是怎么转起来的。网上教程一搜一大把,但多数上来就让你敲catkin_make,敲完了也不知道发生了什么,报错了更是一头雾水。
这篇文章我就从怎么编译、怎么运行、报错怎么排查这三个角度,把“在ROS里跑自己程序”这件事彻底讲明白。主要针对用catkin工具链的ROS 1(比如Noetic、Melodic),但理解清楚之后,ROS 2的colcon也就一通百通了。适合刚装好ROS、准备开始写自己第一个节点的朋友,也适合那种“跟着教程跑通了但不敢自己写”的同学。
1. 先把编译想明白:程序不是写完就能跑的
1.1 “编译”到底在干什么
很多非科班出身的朋友,第一次看到“编译”“链接”“可执行文件”这些词会觉得挺劝退的。其实没那么玄乎,你写出来的C++代码是给人看的,但计算机的CPU只认机器码,也就是一串又一串的二进制指令。编译器的作用,就是把你看得懂的std_msgs::String、ros::init这些东西,翻译成CPU能看懂、能执行的机器指令。
这个翻译过程大概分两步。第一步叫编译,把每个.cpp源文件单独翻译成一个中间产物——目标文件(.o文件)。第二步叫链接,把一堆目标文件和ROS自带的那些库文件“拼”到一起,最后生成一个完整的可执行文件。可以这么理解:编译就像你写好了每章节的稿件,链接就是把这些稿件按目录装订成一本书。书没装订好,单章写得再工整也没法交付。
ROS里常用的catkin_make,本质就是把当前工作空间下所有功能包组织起来,统一完成“编译+装订”这两件事。所以当你说“编译”的时候,实际上是在让整个工作空间根据每个包的配置,把所有代码变成可执行文件。
1.2 catkin的层层嵌套:工作空间、功能包、节点
ROS 1里项目组织的核心层级是:工作空间(workspace)下面放功能包(package),功能包里面放节点(node)源码。你在终端里经常看到的catkin_ws就是工作空间,src目录下放的是一个个功能包,每个包里有src文件夹、CMakeLists.txt、package.xml这几个基本文件。
顶层还有个你一定会碰到的devel目录,这是编译产物存放的地方。源文件编译出来的可执行文件和Python脚本,都会按功能包分类放到devel/lib下面。ROS运行节点的时候,实际上就是在运行devel/lib里那些现成的文件。
这样设计的好处是:源码和产物分离,工作空间可以随时删掉build和devel重新编译,源码不会丢失。我刚入门那会儿不懂这个,有一次误删了整个build目录,以为辛辛苦苦写的代码都没了,后来才发现源码在src里好好的,重新编译一遍就行。
2. 编译前的环境准备与工程搭建
2.1 ROS环境怎么来的:安装方式与版本选择
不管你是用官方源手动装,还是用“鱼香ROS一键安装”这类脚本快速部署,只要最终装的版本和你Ubuntu系统匹配(比如Ubuntu 20.04对应ROS Noetic,18.04对应Melodic),后面编译运行的流程基本一致。很多新手在环境这里栽跟头,原因多半是系统版本和ROS版本不匹配,装完了终端里连roscore都起不来。
我的建议是:刚入门别自己折腾源码编译安装ROS,直接用官方二进制包方式安装,省时省力。一键安装脚本的好处是把换源、依赖配置这些琐碎步骤都自动化了,实测对于初期学习来说很友好。但无论用哪种方式装完,都要确认一下环境变量是否写进了~/.bashrc。因为ROS工具链依赖大量环境变量,比如ROS_DISTRO、CMAKE_PREFIX_PATH,如果这些变量没设好,后续用catkin_make或rosrun时会报出各种莫名其妙找不到命令的问题。
检查方法很简单:新开一个终端,输入echo $ROS_DISTRO,能输出版本号(比如noetic)就说明环境正常。如果这个命令是空白的,说明source /opt/ros/<版本>/setup.bash没生效,需要手动加到~/.bashrc里。
2.2 创建工作空间和功能包
确认环境没问题后,开始搭建自己的工程。先创建工作空间:
mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_init_workspacecatkin_init_workspace会在src目录下生成一个CMakeLists.txt符号链接,这条命令的作用是让src目录变成标准的catkin工作空间。然后回到工作空间根目录,空编译一次:
cd ~/catkin_ws catkin_make这一步会生成build和devel两个目录。build里是编译过程的中间文件,devel里是最终产物。空编译成功,说明整个工具链在你这台机器上是通的,这时候再开始写自己的代码,排错起来思路清晰。
然后创建功能包。假设包名叫my_first_pkg,依赖项用roscpp、std_msgs:
cd ~/catkin_ws/src catkin_create_pkg my_first_pkg roscpp std_msgscatkin_create_pkg会自动生成这个包的基础骨架,包括CMakeLists.txt、package.xml、src目录等。后面的roscpp和std_msgs是声明这个包依赖哪些ROS库。roscpp就是ROS的C++客户端库,我们写C++节点必须依赖它;std_msgs包含标准消息类型,比如String、Int32这些基础消息。
2.3 写一个最简单的发布者程序
功能包建好后,在src/my_first_pkg/src/目录下新建一个talker.cpp,先写一个最简单的发布者节点。这段代码做的事情是:初始化一个ROS节点,名为talker_node,然后在/my_topic话题上不断发布字符串消息:
#include "ros/ros.h" #include "std_msgs/String.h" #include <sstream> int main(int argc, char **argv) { ros::init(argc, argv, "talker_node"); ros::NodeHandle n; ros::Publisher pub = n.advertise<std_msgs::String>("/my_topic", 1000); ros::Rate loop_rate(2); while (ros::ok()) { std_msgs::String msg; std::stringstream ss; ss << "hello from my_first_pkg " << count; msg.data = ss.str(); ROS_INFO("Publishing: %s", msg.data.c_str()); pub.publish(msg); ros::spinOnce(); loop_rate.sleep(); ++count; } return 0; }这段代码不复杂,但它是理解ROS发布者模型的最佳入门样例。ros::init初始化节点,n.advertise告诉ROS主节点我要往/my_topic发消息,loop_rate(2)控制发布频率为每秒2次,while (ros::ok())循环里构造消息、发布消息。
3. 核心实操:用catkin_make编译并运行自己的程序
3.1 改CMakeLists.txt,让编译系统认识你的代码
代码写完不是直接就能编译的。catkin用的是CMake这套构建系统,你得在CMakeLists.txt里明确告诉它:这个包有哪些源文件要编译、编译出来的可执行文件叫什么名字、要链接哪些库。
用编辑器打开my_first_pkg/CMakeLists.txt,找到这几段并修改。第一处是add_executable,也就是声明“要生成一个可执行文件”:
add_executable(talker src/talker.cpp)这里talker是你要生成的可执行文件名,src/talker.cpp是源码路径。这个名字是可以自定义的,但建议和你程序功能保持一致,方便后面运行的时候好记。
第二处是target_link_libraries,意思是“把编译出来的可执行文件和ROS的库链接起来”:
target_link_libraries(talker ${catkin_LIBRARIES})${catkin_LIBRARIES}是CMake里的一个变量,指代当前包依赖的所有库。你可能会问,为什么依赖了roscpp和std_msgs,这里不需要逐个写出来?因为catkin已经把依赖关系打包成了一个变量,直接用就行。这也是ROS封装CMake的一种便利之处。
如果你要写Python节点,不需要走add_executable这套流程,Python本来就是脚本语言,不需要编译成机器码。但需要让系统把.py文件安装到devel/lib对应目录下,做法是在CMakeLists.txt里加:
catkin_install_python(PROGRAMS scripts/talker.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})这个后面讲Python节点时再细说。
3.2 开始编译:catkin_make怎么用
配置好CMakeLists.txt后,回到工作空间根目录:
cd ~/catkin_ws catkin_make终端里会滚动一堆编译日志。第一次编译会多一些,因为要把ROS依赖的配置都生成出来;以后再编译,只会编译你改动过的部分。
编译成功时,日志末尾会出现类似这样的内容:
[100%] Built target talker看到这句,说明可执行文件已经生成了。如果编译过程中有错,终端会变成红色报错,并提示具体是哪个文件、哪一行有问题。判断编译是否成功的标志,是最后一行有没有出现Built target。
编译完之后,去devel/lib/my_first_pkg目录下看一眼:
ls ~/catkin_ws/devel/lib/my_first_pkg能看到一个名为talker的文件,这就是“可执行文件”。它本质上就是编译+链接之后的机器码程序,ROS运行节点时,实际执行的就是这个东西。
这里需要注意一个很多新手都会踩的坑:每次修改了.cpp源码,都要重新执行catkin_make,否则运行的老旧版本不会变。我见过不少朋友改了代码、保存文件、直接rosrun,结果发现程序行为和原来一模一样,其实就是忘了重新编译。
另一个常见困惑是:为什么修改CMakeLists.txt后推荐重新编译?因为CMakeLists.txt是构建配置,改了配置之后,CMake需要重新生成Makefile,catkin_make会自动检测到这种变化并重新生成,所以统一习惯就是:任何改动,回工作空间根目录敲一次catkin_make准没错。
3.3 source环境变量:让系统找到你的程序
编译成功后,新开一个终端,输入rosrun my_first_pkg talker,有很大概率会报错:
[rosrun] Couldn't find executable named talker below /home/xxx/catkin_ws/src/my_first_pkg这个报错让无数新手抓狂。自己的程序明明编译好了,文件我也看到了,为什么ROS就是找不到?
原因在于:ROS通过环境变量ROS_PACKAGE_PATH来定位功能包,通过CMAKE_PREFIX_PATH等变量来确定工作空间位置。你新开的终端没有加载你当前工作空间的配置,它只知道系统ROS自带的功能包路径,并不知道~/catkin_ws下有个my_first_pkg。
解决办法就是source一下环境:
source ~/catkin_ws/devel/setup.bash这一行的作用,是把当前工作空间的路径信息注入到当前终端的环境变量里。执行之后,再运行rosrun my_first_pkg talker就没问题了。
但每次开新终端都要手动source一遍太麻烦了,更推荐的做法是把这个命令追加到~/.bashrc里,这样每个新终端都会自动加载:
echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc source ~/.bashrc这里有个细节值得说一下:setup.bash到底从哪来?是编译时生成的。catkin_make除了生成可执行文件,还会在devel目录下生成一系列setup.*文件(bash、zsh、sh等),它们的作用就是通知系统“这个工作空间里有这些功能包,可以用rosrun找到它们”。所以如果你删了devel目录又没重新编译,source ~/catkin_ws/devel/setup.bash就会失败。
3.4 运行之前,先启动ROS主节点
要真正让节点跑起来,除了解释节点程序本身,还需要一个“调度中心”——ROS Master。它负责管理节点之间的通信,帮发布者和订阅者互相找到对方。在ROS 1里,启动这个调度中心的命令是:
roscore需要在另一个终端运行,并且要保持这个终端一直开着。roscore本质上是启动了ROS的master节点,同时包含了参数服务器等功能。
运行顺序是:先开一个终端跑roscore,再开另一个终端source环境并运行talker节点:
source ~/catkin_ws/devel/setup.bash rosrun my_first_pkg talker然后在第三个终端里用rostopic echo /my_topic查看话题内容,就能看到talker节点发布的字符串消息了。
很多刚接触ROS的朋友会搞混rosrun和直接运行可执行文件的区别。比如有人直接执行./devel/lib/my_first_pkg/talker,也能看到输出,但一来这样不方便,二来rosrun会帮你从功能包名去索引可执行文件,并且还会加载ROS相关的参数、日志、命名空间等上下文,所以约定俗成用rosrun。
3.5 编译运行Python节点:不需要“编译”的运行
说完C++,再说说Python节点。不少初学者会觉得Python写起来快、调试方便,ROS也支持Python节点。关键区别在于:Python脚本本身是解释执行的语言,不需要像C++那样经过编译和链接生成可执行文件,但它同样需要被ROS“找到”。
假设你写了一个scripts/talker.py,放在功能包的scripts目录下:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import rospy from std_msgs.msg import String def talker(): pub = rospy.Publisher('/my_topic', String, queue_size=10) rospy.init_node('talker_node', anonymous=True) rate = rospy.Rate(2) count = 0 while not rospy.is_shutdown(): msg = "hello from python %d" % count rospy.loginfo(msg) pub.publish(msg) count += 1 rate.sleep() if __name__ == '__main__': try: talker() except rospy.ROSInterruptException: pass注意第一行#!/usr/bin/env python3,这叫shebang,作用是告诉系统用哪个解释器来运行这个脚本。然后要给脚本加上可执行权限:
chmod +x scripts/talker.py接着在CMakeLists.txt里添加我刚才提过的catkin_install_python:
catkin_install_python(PROGRAMS scripts/talker.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})然后回工作空间根目录catkin_make。这里编译不是把Python变成机器码,而是把脚本“安装”到devel/lib/my_first_pkg/下。之后就可以这样运行:
rosrun my_first_pkg talker.py注意运行时后面要带.py后缀,因为可执行文件名就是脚本文件名。很多新手在这里容易和C++节点混淆,C++节点rosrun时不用加后缀,Python节点要按实际文件名来。
4. 高频报错与排查技巧实录
4.1 “Couldn't find executable”不一定是没编译
前面提到过的报错[rosrun] Couldn't find executable named talker below ...,很多人第一反应是重新编译,但其实这个报错至少有三种可能。
第一种可能,压根没编译成功,devel/lib/my_first_pkg下没有这个文件。这种情况去查看编译日志,解决编译报错。
第二种可能,编译成功了,但当前终端没source环境。刚才说过,新终端不会自动认识你的工作空间,执行source ~/catkin_ws/devel/setup.bash即可。
第三种可能,add_executable里设置的可执行文件名和你rosrun时输入的名字不一致。比如你写的是add_executable(talker_node src/talker.cpp),运行的时候用rosrun my_first_pkg talker,那必然找不到。检查方法很简单:去devel/lib/my_first_pkg目录下ls一下,看实际文件名是什么,用实际文件名运行。
4.2 CMakeLists.txt写错导致的编译失败
CMakeLists.txt是新手最容易写崩的地方。常见的一类是语法错误,比如括号不匹配、大小写不一致。CMake是区分大小写的,add_executable写了Add_executable就会报错。
另一类常见问题是忘了取消注释。用catkin_create_pkg生成的CMakeLists.txt里,绝大多数指令是被注释掉的示例代码。很多人找不到add_executable,其实它就在文件后面,只是没解锁。把前面的#删掉再修改内容就行。但要注意,不要同时启用示例部分的add_executable和你的自定义部分,否则有些情况下会重复定义同一目标。
编译报错时,终端输出的第一行红色内容往往提示了具体的文件路径和行号。比如:
/home/xxx/catkin_ws/src/my_first_pkg/CMakeLists.txt:58: error: ...这个意思是第58行有问题,直接翻到那一行检查。
4.3 运行Python节点时报“No module named ...”
Python节点运行后,如果提示ModuleNotFoundError: No module named 'rospy',最大的可能是你用错了Python解释器。ROS Noetic默认绑定Python 3,Melodic默认是Python 2。如果你的系统默认python命令指向的是Python 2,而写的rospy是Noetic下的Python 3版本,就会导入失败。
排查方式:
which python python --version如果Python版本和ROS不匹配,可以在脚本里明确指定解释器,或者用python3命令运行。还有一种做法是安装python3-catkin-pkg等依赖包,确保ROS工具的Python模块都装全了。
4.4 运行节点时连接roscore失败
启动节点时,如果终端报:
ERROR: cannot connect to master ...这说明节点找不到ROS Master。原因一般有两个:第一,roscore没启动;第二,ROS_MASTER_URI环境变量设置不正确。
roscore没启动的话,开一个新的终端执行roscore就行。ROS_MASTER_URI默认是http://localhost:11311,如果你在~/.bashrc里设置过其他地址,比如指向了某台远程主机,而当前roscore就在本机跑,连接就会失败。排查时执行:
echo $ROS_MASTER_URI如果输出不是http://localhost:11311,结合自己场景判断是否需要改回来。
4.5 问题排查速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
catkin_make报错 | 源码语法错误 / CMakeLists.txt配置错误 | 看红色报错,定位到文件行号 |
| 编译成功但rosrun找不到 | 没source工作空间 | 执行source ~/catkin_ws/devel/setup.bash |
| rosrun报找不到可执行文件 | 可执行文件名写错 | 到devel/lib/my_first_pkg下用ls确认名称 |
| 运行节点连不上master | roscore没启动 | 另开终端执行roscore |
| Python节点No module named rospy | Python版本不匹配 / 缺依赖 | 确认系统Python版本,安装python3依赖 |
| 修改代码后运行还是老行为 | 忘记重新编译 | 回到工作空间根目录执行catkin_make |
| source setup.bash报不存在 | devel目录被删除 | 重新执行catkin_make生成 |
5. 一些实操中的个人习惯与心得
聊到最后,分享几个我用了很久的习惯,虽然不是必须的,但确实能让编译运行流程更省心。
第一,代码修改后不要急着运行,先编译一遍。哪怕你觉得“就改了一个变量名,应该没问题”,也建议编译一下。编译是验证代码语法正确性的第一道关卡,早发现早处理。
第二,把常用的功能包依赖在创建时写全。catkin_create_pkg的依赖写全了,CMakeLists.txt里大部分配置就已经就位,后面加代码会快很多。我一开始建包时只写了roscpp,后来加入tf、visualization_msgs等功能时,还得回头去改CMakeLists.txt和package.xml的依赖,比较繁琐。
第三,养成看完整报错日志的习惯。ROS的报错信息有时候很长,但关键信息往往在最前面的几行,比如哪一行代码出错、哪个库缺失。不要一看到红色报错就慌,冷静下来从第一行开始读,大部分问题都能自己解决。
第四,也是最重要的一点:刚开始学习时,尽量从最小可运行示例开始改动。比如talker这个程序跑通了,你想加一个订阅者,那就先从抄一个简单的listener开始,确认能通信了,再逐步加入自己的业务逻辑。不要在没跑通基础通信的情况下,就试图把一堆复杂逻辑塞进一个节点里。那样出了问题,你根本不知道是通信的锅还是逻辑的锅。
ROS的编译运行体系,说到底就是一套“你告诉CMake怎么把源码变成程序,再把程序交给rosrun去执行”的流程。理解了这个本质,后面不管是用catkin_make、catkin build还是ROS 2里的colcon,都是一个套路:构建、安装、source、运行。希望这篇文章能帮你跨过“编译运行”这道坎,去享受写自己ROS程序的乐趣。