ROS是机器人学习和无人车学习最好Linux平台软件,资源丰厚。无人车的规划、控制算法通常运行在Linux系统上,各个模块通常使用ROS进行连接。
自动驾驶系统包括障碍物检测、行为决策、路径规划等一系列复杂的工程模块,同时还要支持激光雷达、相机、GPS等一系列传感器的实时数据收集和实时处理。如何将这些功能模块相互独立又相互交互集成一起,构建成一个稳定的自动驾驶系统是一个巨大的挑战,也是自动驾驶计算框架所承载的基本功能。
上图是一个ROS的基本框架,自动驾驶底层通信框架选择ROS,主要有三个方面的原因:
基于以上三点,在初期选择ROS作为自动驾驶底层框架,用于快速验证顶层算法和技术方案。
转自Apollo开发者社区公众号
ROS在开发过程中,基于功能把整个自动驾驶系统分成多个模块,每个模块负责自己消息的接收、处理、发布。当模块需要联调时,通过框架可以把各个模块快速的集成到一起。
进行通信优化的几点原因:
针对这一问题,Apollo ROS做了一个基于共享内存的通信机制减少数据的复制次数,从而提升这种通信模式的效率。
上图左侧是ROS原生的通讯框架,一个数据从发送方到接收方经历四次数据复制。第一次是从节点到用户内存的数据复制,第二次是从发送方到内核的数据复制,第三次是经过TCP连接,从内核再向接收节点用户态空间的复制,第四次是接收节点拿到这个信息之后,通过反序列化把信息取出来组成一个结构变化的信息。
右侧是Apollo ROS优化后的框架,它基于共享内存改进,可以减少两次数据拷贝。第一次是发送节点把消息序列化成流式数据,第二次是接收节点直接从共享内存里面取相应的消息指针,把共享内存消息取出来进行反序列化成结构化信息进行使用。减少了从用户到内核态以及从内核态到用户的两次数据拷贝。
对于有多个订阅节点的情况,例如Camera下游会有很多订阅节点,如果是三个节点,会有三条通信链路,分别是四次的内存拷贝,也就是12次数据拷贝。而在基于共享内存的通信方式下,每一条链路内存拷贝的次数只需要两次,三条链路只需要六次。
随着消息逐渐增大,基于共享内存通信延时比基于原声ROS Socket的通信延时降低一半。以5M数据为例,传送一帧5M大小的数据,基于ROS Socket大概需要四毫秒左右的时间,基于共享内存通信只需要两毫秒左右。
整个自动驾驶系统的网络拓扑结构非常复杂,数据流向的拓扑结构也比较复杂。在一些极端的情况下,整机数据量会增加。在一些多车道,路面状况比较复杂,车辆较多的情况下,感知和Planning模块,或者和其它模块之间的数据流就会成倍增加,所以在测试一些极端情况下,系统吞吐量也是自动驾驶需要考虑的一个重要方面。
在吞吐量测试1:1情况下,整机性能可以达到5.5GB每秒的速度,如果是1:4,性能提升会更明显。
CPU资源占用率在共享内存通信情况下降低约30%, 主要是因为减少了多次内存复制。
ROS是以Rosmaster节点管理器建立起来的一个P2P拓扑网络,这种拓扑网络有很明显的优势,如下:
两个比较明显的缺点:
Master单点在多机的方案里,这个单机单点的不足就会更加凸显。比如现在很多自动驾驶厂商所采用的比较主流的Nvidia Drive PX2板卡,它就包括两个系统:主系统和冗余备份系统即容错系统。如果使用ROS通信在PX2上进行部署,Master只能起在一个节点上。如上图所示:左侧是它的主系统,右侧是它的冗余备份系统。当主系统里面Rosmaster宕机之后,备份系统里面的节点其实并不知道Rosmaster已经处于一个宕机的状态,那么备份系统就起不到其目的和意义了。因为此时整个系统处于一个功能不完整的状态,所以就失去了冗余备份的意义。
Apollo ROS进行了比较大的改造:先把这个中心化的网络拓扑给去掉,然后建立了一个点对点之间的一个复杂网络拓扑,主要是使用RTPS服务发现协议去完成P2P网络拓扑。
如上图所示:右侧是ROS Node的一个框架图。左下角是引入RTPS服务相关的一些功能。其它部分是ROS Node现有的一些功能。Ros Node是分层级式的结构,最上层是Handler,Handler提供节点和ROS整个通信的基本交互的句柄。下一层左侧和右侧定义了这个节点发送和订阅的Channel信息。再下一层是Middleware,Middleware是这个节点和其它节点进行通信的时候去完成链路的建立和数据的发送。
接下来的一层左下角RTPS是新引入的一个功能。改造之后的ROS Node架构,当一个节点被启动的时候,它会通过RTPS向所有的节点发送信息:现在有一个新的节点要加入到这个拓扑网络。当它离开的时候,也会发送消息告诉所有的节点:现在这个节点要退出。以前这些功能都是通过Rosmaster来完成的。
下面通过几张图来描述:节点建立连接和通讯的一个主要流程。
Sub节点启动,通过组播向网络注册。
订阅节点在启动的时候,它会向当前这个域里面所有的节点发送信息:现在有一个新的节点要启动。
通过节点发现,两两建立unicast。
所有的节点在接收到新加入这个节点发生拓扑信息变更之后,会和新加入这个节点分别建立两两连接关系。
向新加入的节点发送它们已经有拓扑信息。
所有已经存在的节点会向新加入的节点发送它们已经有拓扑信息,也就是在新节点加入之前每个节点其实是维护了它和其它所有节点的一个连接关系,这个连接关系发送给接收节点,供接收节点去更新自己的网络拓扑结构。
收发双方建立连接,开始通信。
当新加入节点接到所有节点发送出来的历史拓扑信息之后,它会根据它自己注册的实际消息内容去决定和哪些节点建立实际的通信连接。如上图所示:新加入节点只和右下角的一个节点之间有拓扑关系,它除了维护所有的节点给它发送出的整个网络拓扑信息之外,同时会和发送节点建立点对点的通信连接。
通过RTPS拓扑发现方式,Apollo ROS去除了对Rosmaster这一个单点的依赖,从而提升整个系统的鲁棒性。这个修改完全是对ROS底层的修改,用户基于原生ROS代码写的节点程序,到Apollo ROS是完全兼容的一个迁移即开发者不需要去改动任何的接口,就可以直接使用RTPS网络拓扑这种新的关系建立。
原生ROS基于Message的订阅发布消息模型。发送者和接收者在进行实际通讯之前需要进行消息格式定义,其包含字段:基础的数据类型或者复杂的数据类型。在它们进行消息通信的时候,才可以有选择性的去建立通信连接和数据实际发送。如果有一个节点订阅的消息类型不是Channel预先指定的消息类型,这种通信连接是建立不起来的。或是强制指定一个节点去订阅某一种类型的Channel信息,但是它的实际回调函数里却写的是另外一种消息类型,这种编译可能在实际运行的时候就会报相关的一些错误。
Message是两个节点进行消息通信的抽象描述文件。这个描述文件提前定义好两个节点之间进行消息通信的基本数据类型。ROS采用这种方式是因为能比较大概率地对两个节点之间进行解耦合,同时两个节点之间也是跨语言的,即不需要关注两个节点是用什么语言写的,都可以通过这种描述文件去进行实际的消息通信。通过Message通信的时候接、收节点在接受到信息之后,会进行MD5的校验、验证这个消息是否符合它的预先订阅,或者是在使用消息之后才会去进行消息的回调处理。
但是ROS基于Message这种通讯方式有很多的缺点。它最大限度解放两个点之点的一个耦合关系也带来了一些问题。比如Message接口升级,不同版本之间的兼容是需要做大量的适配工作。再如某个模块进行升级,之前所录制的一些实验数据,在进行回放的时候就会产生不匹配的现象。
历史数据在接口升级之后也面临着无法转化和兼容使用的问题。
Apollo ROS实践里面引入了一种新的消息描述的格式去实现很好的向后兼容即Protobuf。只需要在使用的过程中,定义好必须的字段或者是一些新增的字段,新增的字段我们可以使用Optional属性去描述。在进行模块升级或者是模块之间的消息接口升级的时候,下游模块其实不需要关注新增字段对它来说会造成什么样的影响。如果它要去使用这个字段的话才需要去进行一定程度的适配。如果它的程序不使用这个新增的字段,就不需要做任何的修改。
上图是原生ROS和Apollo ROS对数据兼容支持的对比。
为了做好数据兼容,在原生ROS里面,开发者使用了一个trick:将Proto文件序列化成一个字符串信息放到Message信息里面,完成消息的向后兼容。比起Apollo ROS这个方式有两个明显的缺点:
Apollo ROS 为了满足数据兼容,深度整合了Protobuf的功能。用户可以直接定义Proto的字段信息,同时信息传递的过程不需要再进行额外的Message的数据转化。另外,在使用调试工具的时候,通过Rostopic echo可以看出原始消息传递的实际展示。
机器人操作系统(ROS)是一个成熟而灵活的机器人编程框架。ROS提供了所需的工具,可以轻松访问传感器数据,处理数据,并为机器人的电机和其它执行器生成适当的响应。整个ROS系统被设计为在计算方面完全分布,因此不同的计算机可以参与控制过程,并作为单个实体(机器人)一起行动。
因为自动驾驶使用的ROS架构是一个松耦合关系,每个节点独立运行,节点有一套自己的XYZ坐标系,当把他们组装到一块时,每个节点的坐标系都是相对独立的,但整个自动驾驶系统需要把每一个节点所使用的信息和一些参数转化到同一个世界坐标系里。TF节点就提供了对应的坐标系转换功能,TF消息也是通过基于Message的订阅和发布消息来完成的。
例如,当下游的Planning节点想使用Obstacle信息时,需要将Obstacle信息转化到同一个世界坐标系,这时候它会发起一个TF去查询Obstacle处于哪一个世界坐标系里面的哪一个位置,从而感知整个车身周围的情况,基于此再做一个合理的规划和决策行为。
ROS也提供了一些基本功能查看TF的运行机制。例如Rosrun提供了TF监控节点,通过这个节点我们能看到整个复杂网络拓扑结构里面节点之间的关系;每个节点之间进行TF转换时所用到的TF树的结构。此外,还提供了tf_echo命令,可以打印从A节点到B节点,例如Perception节点到Planning节点中间所使用了TF转变树的结构。
TF也提供了一个插件供开发者使用,可以用一些主流开发工具进行ROS工程开发。
下图是结合之前将Publiser使用TF的一个例子,想使用TF,只需要改动两部分:第一是定义TF的对象;第二是直接进行TF数据查询,然后就可以得到一个世界坐标系。
RQT顾名思义,R实质是ROS的缩写,QT是可视化的图形工具,RQT是ROS给开发者提供的一套比较方便的图形化相关展示的一套工具。几个比较常用的RQT功能:
在进行实际模拟的时候,可以用一套语言来定义机器人模型,这套语言被定义为统一机器人描述格式语言URDF。它也是一套xml的语言描述,这个描述格式里面包含两个核心的概念:一个是节点Link,一个是节点之间的连接关系Joint。Joint会指定Parent节点和Child节点,这样就可以描述一个完整的拓扑结构,也就是对整个网络拓扑结构的xml语言化描述。在进行仿真的时候,通过加载对应的URDF文件,在仿真环境里面实时地展示所需要调试的信息。
Simulation Description Format(SDF)是另外一个调试工具。之前介绍的Rviz调试工具,更多的是看到消息收发之间的实体化展示,例如展示点云、图像和其它一些信息。如果进行仿真模拟,如机器人模拟的时候,就用另一套工具Gazebo。Gazebo是ROS的一个开发包,它里面所使用的描述语言就是Simulation Description Format。用Gazebo加载URDF时,Gazebo首先把URDF描述语言转换成SDF语言,然后再进行加载和展示。
ROS是一个强大而灵活的机器人编程框架,从软件构架的角度说,它是一种基于消息传递通信的分布式多进程框架。ROS本身是基于消息机制的,可以根据功能把软件拆分成为各个模块,每个模块只是负责读取和分发消息,模块间通过消息关联。
转载于Apollo开发者社区公众号
ROS是一个强大而灵活的机器人编程框架,从软件构架的角度说,它是一种基于消息传递通信的分布式多进程框架。ROS本身是基于消息机制的,可以根据功能把软件拆分成为各个模块,每个模块只是负责读取和分发消息,模块间通过消息关联。
创建一个ROS开发环境和写一个C++工程有点类似,通过catkin create可以创建一个简单的工程。其中的文件组织方式如上图所示,包括:
上面介绍的package组织方式只是官方推荐的一种组织方式,使用catkin build编译,当source完环境变量之后,通过Ros提供的命令比如ROS run或者ROS launch启动时,package name可以自动补全,package里面包含的节点或者launch文件也是可以自动寻找,所以官方推荐使用这种组织方式。
DEVEL和BUILD这两个目录是build时自动产生的两个临时目录。
此外,开发环境还有描述ROS Package相关工作区的两个文件:
从上到下依次指定了Cmake的版本、project的名字、ROS工程所依赖的c++的版本、另外是依赖的库文件,最后生成可执行文件以及这个文件所链接的依赖库。
工程建立好之后,catkin build可以直接对工程进行编译。直接用 catkin build去编译,会把整个工程目录里面的所有的package进行统一编译,如果是构建一个比较复杂的系统,可能一个文件夹包含了很多节点或者package包,编译时间会比较久,可以通过指定package名,编译某一个固定的package包,提升编译效率。
在Eclipse下如何编译ROS基本工程:
首先是设置工作ROS工作区,然后将ROS package导入到Eclipse设置的工作区。
通过Eclipse提供的build或者run等功能去调试ROS工程。同时Eclipse里面提供的快捷键在编译程序里面同样适用。
上图的hello world程序展现了ROS框架写Node所使用的核心要素:
Include 就是include ROS的一个基本环境;
Main函数里面有三行需要重点注意:
最底下的while循环以10赫兹的消息频率进行发送,同时进行计数。
Spinonce:有一帧消息就把这消息立马发送出去,同时进行下一轮的消息等待。
在示例程序里面有一个ROS_INFO,它就是ROS提供的日志系统;
ROS的日志系统是分级的,即在编写节点程序的时候对打印的信息进行分级,对不同的分级,ROS会提供不同的颜色和格式进行展示。分级的作用是为了帮助开发者快速地定位到关键信息,不会对整个节点的逻辑产生实质性的影响。
日志系统提供了两种格式:
Subscriber与Publisher有三点明显的区别:
1、回调函数:subscriber作为信息的接收方有一个回调函数,回调函数定义了它接收到的每一帧信息如何使用;上图listener回调函数比较简单,它接收到信息后只是进行了打印处理。Publisher没有回调函数,它不需要对消息进行处理。
2、声明的时候:subscriber把回调函数传入到对应的node初始化程序里面。publisher声明的时候只需要注册要往哪一个topic上去发信息,同时还设置队列长度。
3、Rosspin:在ROS构架里所有的回调函数都不是主动触发的。Rosspin是阻塞性的,声明Rosspin之后,就阻塞在此,程序不会退出,它会一直监听自己对应的队列里面是否有新消息的到达,若有新消息到达就会触发回调函数处理。
如果Subscriber的主程序里除了订阅消息之外还有其他的功能则可以采用rosspin once,对所有已达消息进行回调函数的处理。同时可以写一个while循环,rosspin once按照一定的频率去处理回调函数的消息。
ROS提供Rosspin这两种方式,就是为了满足这两种场景。第一种是阻塞,只有一个回调函数进行处理,第二种是订阅回调函数消息以外,他还进行了一些封装和处置。
看似很复杂的自动驾驶,节点整体写下来都是比较简单的,就是按照上图Subscriber与Publisher方式来写。但是在实际的自动驾驶系统里面,所有的模块都不是简单的一个角色,它可能既是消息的订阅者也是消息的发送者,是一个复杂交互的功能,甚至是用到很多数据融合或者是消息对齐。
service
节点可以启动service去注册一项服务,另外一个节点在使用这项服务的时候可以直接call service完成一些实时的数据通讯交互。Message是一个被动的消息行为,发送者发送消息的时候并不知道消息会被谁去消费,接收者在接收消息的时候也不知道目前有几个发送节点在发送,发送和接收之间是一个什么状态也是不知道的,他们是一个松耦合和透明的关系。Service弥补了这种通信方式的不足,它需要及时回应。Client向server去发送service请求的时候,需要实时等待一个response,根据响应做出下一步的行为指示。
parameter
Parameter通信方式借鉴了service的原理。它启动了Parameter service,Parameter service是一个全局的服务器,各个节点在进行参数设置和获取的时候可以通过Parameter service的方式轻易完成。因为Parameter不像基于message消息通讯方式那么频繁,一个参数在设置完成之后,在整个网络拓扑运行期间所有的节点只需要在一个地方取此参数就行或者某个节点根据自己的运行状态去改变这个参数。
Parameter对应有一套ROS所使用的基本命令行工具—rosparam。
rosparam其它工具相比,有两个不同的地方:get和set。get是get某一个全局参数的值;set是设置某一个全局参数的值。
上图展示的例子是通过node所提供的nodeHandler指针去调用它的getparam,得到某个参数的数值。当它进行一些运算之后,也可以通过setparam去设置参数的值。设置之后,整个系统参数服务器里面对应的这个参数就会被设置成对应的值,其他节点在得到这个值之后再作出相应的处理。
自动驾驶节点比较多,网络拓扑也比较复杂,每个节点在进行消息通讯的时候有很多channel同时运行,如果只是通过命令行工具去查看节点的状态和节点之间的拓扑,会很麻烦。ROS提供了一些比较好用的可视化工具立体化展示某一个拓扑结构里面的拓扑网络,RViz就是其中之一。
RViz在整个ROS生态里可以看成是一个节点,它定义了整个拓扑结构里面所有的消息,然后按照固定的格式进行图形化展示,同时提供很多debug相关的功能。因为RViz也是一个普通的节点,所以在启动的时候可以通过rosrun命令的方式去启动RViz相关的功能。
RViz也提供了很多插件可以放到诸如eclipse这样的功能插件里面,在进行eclipse开发时可以通过eclipse的plugin去调取RViz的相关功能,进行可视化调试。