自动驾驶数据闭环与工程化

这篇文章的重点不在于具体的软件架构,而在于软件架构的工程化落地。因为再好的架构,也需要变成真正可以稳定运行的产品才能体现其价值。而自动驾驶相关的软件系统关联了非常多的技术领域,每个技术领域有专门的知识体系,需要专门的人才,需要解决特定的问题。而完整的自动驾驶系统是需要所有相关的技术一起协同工作的,任何一个领域在技术或工程上的短板,都将影响整个系统的可用性、可靠性、安全性。

同时在研发组织上,也需要让不同领域的技术团队能够独立的开发、独立测试验证,最后还能集成在一起。那么如何拆得开、拆得好,还能整合起来,就是工程落地过程中至关重要的顶层设计问题。

第1章叙述闭环的抽象概念以及研发闭环的界定。阐述如何依据最小变量原理来拆解研发闭环,使得复杂系统的研发及集成过程可控。

第2章叙述产品概念上的闭环思路。自动驾驶作为跨多个技术领域的复杂产品,很难一步到位解决所有问题。本章从产品角度出发,提出纵横交叉的产品演进路线,使得自动驾驶系统工程化道路上的每一步的难度降低,也让整个系统能在早期就开始逐步集成,提高整体工程化成功的可能性。

第3章是本文的重点。目的是阐述数据驱动闭环的基本原理以及方法论。

这一章中,我们先给出数据概念模型及其分析,重点阐明语义层级的概念。然后我假设一个AEB 的技术方案,作为我们讨论的功能范围的锚定点。基于概念模型,分析这个技术方案涉及算法的语义层级分解。

我们再给出智能驾驶系统集成的一般原理,分别阐述“算法串接、执行平台、数据规模”三个集成纬度的各自概念和相互组合关系。并将这个一般原理应用到 AEB 方案的系统集成上,给出一个参考的 AEB 功能的系统集成路径。这个集成路径上涉及了大量的工具链,我们重点分析了其中的仿真测试工具链,并对不同的仿真和测试的手段做了对比,分析其各自的用途和侧重点。至此,用于数据驱动闭环的技术基本设施叙述完毕。在此基础上,我们论述了如何基于这些技术设施,利用大量数据从算法开发和测试验证两个方面驱动AEB产品完成工程化落地。虽然 AEB 功能实际对量产开发并不需要把所有上述过方法论严格实施。但是这个方法论是迈向更高阶功能的基础。我们以 Level 2.5 和 Level 4 为例阐明该方法论的进一步应用。

最后,我们分析了什么是自动驾驶的核心技术:“算法能力”和“工程化能力”缺一不可。目前自动驾驶技术在实际应用中在这两方面都遇到了很大程度的困难,于是采取了很多手段规避这些困难,从而衍生出各种弱化版本的自动驾驶场景。本章对这些场景在算法能力和工程化难度两个纬度做了分析和比较。一方面可以了解产业图景背后的技术原因,一方面也可以直观的理解建立工程化能力的难度及重要性。

1. 闭环概念及研发闭环

我们每天都在跟闭环打交道,你的手指点击手机屏幕,手机系统将你选择的内容显示出来给你,这是一个交互闭环,在你使用手机的过程中,这个闭环会持续进行。算法分析你在某些短视频的停留时间,推断你的兴趣,就推送你可能喜欢的短视频,形成一个闭环。这个闭环持续运转,最终算法就会对你的兴趣点抓的死死的。

其实我们在产品设计、营销、研发等所有环节都会经常提到闭环。

产品设计研发、发布给用户、收集反馈进一步改进设计,形成闭环。

举行营销活动、发布广告、收集广告效果数据并分析,根据数据改进营销方式和广告设计、优选广告投放渠道,也形成了闭环。

深度学习算法训练过程中每一次梯度下降的迭代、损失函数计算偏离、反向传播后计算下一次梯度下降也是一个闭环。

敏捷开发中每一次两周的迭代,是一次 PDCA (plan-do-check-adjust) 的闭环,每一次 PI (Program Increment) 包含了多次迭代,PI本身是一个更大范围的 PDCA闭环。

闭环概念广泛存在,我们来梳理其一般化的概念,来更好的理解和应用。

1.1. 闭环的抽象概念

学术上,严谨的“闭环”概念是在控制论中出现的。较为抽象的定义是:当我们要准确控制一个系统的行为时,我们根据系统的输出来校正对它的输入,以达到较为准确的控制精度。因为系统的输出会导入到输入端的计算,形成一个不断往复的循环,称之为闭环(Closed Loop)。

图 1 控制系统

通俗的例子,我们要把鼠标移到屏幕中的某一个目标位置,我们手在移动鼠标的过程中,眼睛观察当前位置与目标位置,大脑计算两者之间的偏差,控制手中鼠标指针移动的方向和速度。移动过程的前一段会快一些,到目标位置附近再做精细调节。眼睛观察到到的新的鼠标偏离会导入到大脑中用来计算手部移动的距离。不断循环这个闭环过程,只到鼠标到达目标位置。看起来很简单,但却是一个非常通用的过程。自动驾驶的车辆控制算法、远程导弹的制导都是类似的过程。

我们从这些例子中抽象出组成闭环的几个关键概念:闭环标的、偏离定义、反馈回路。

1.1.1. 闭环标的

每一个“闭环”都有其“标的”。“标的”可以理解为这个闭环说需要达成的目标。也就是这个闭环的作用所在。比如“控制车辆按照规划的轨迹行使”,“正确识别图像中的人”。

对与一个测试闭环,具体要测试的目标物就是这个闭环的标的。

对于一个研发过程的闭环,研发的目标就是闭环的标的。

1.1.2. 偏离定义

每一个闭环都有一个从输入到输出的正向路径,输出结果与期望达到的结果直接有一个偏离,这个偏离需要能够被明确的定义。闭环多次迭代的目的就是要缩小这个偏离。

图 2 闭环的属性

1.1.3. 反馈回路

反馈回路是根据正向路径的输出来修正输入的机制。

1.1.4. 概念的抽象

熟悉控制理论和深度学习算法的人对这些概念会非常熟悉,比如在深度学习算法里关于这这些概念会被称作“损失函数”、“反向传播”之类的术语。本文的目的不是讲述具体的算法原理,而是自动驾驶系统研发的工程化。这个工程化的过程是由一系列闭环组成的。

这些闭环有的是某一个具体技术研发的过程,有的是测试的方式,有的是具体某个算法执行时的原理。但只要是某个特定过程形成了一个闭环,就都可以归纳出上述抽象概念,也能演绎出抽象概念在具体上下文语境中的具体体现。

1.2. 研发闭环

如前文所述,“闭环”是一个很通用的概念,可以出现在很多领域,尤其在自动驾驶系统的研发和运行过程中处处可见。为了更准确的描述本文的话题,我们先澄清容易误解的概念。这里先区分“研发闭环” (Develop Closed Loop) 与“运行时闭环” (Runtime Closed Loop)。

1.2.1. 研发闭环与运行时闭环

自动驾驶功能中比较基础的功能是“车道居中”,即在有车道线的情况下,保持车辆沿车道中心线行使。这个功能的关键技术是两项:车道线识别的视觉感知算法和横向车辆控制算法。

图 3 车道保持功能示意

“运行时闭环”就是:车辆行使过程中,摄像头采集前方道路图像,识别出车道线并转换成合适的坐标系,估算当前车辆在车道中的位置,控制算法接收车道线信息、车辆位置信息、车辆自身的速度、方向等信息,控制方向盘的角度或扭矩。整个闭环周而复始的运转,保持车辆居中。

这个运行时闭环的标的是“保持车辆居中”,闭环的偏离定义为车辆方向与车道中线方向的偏离,反馈回路是根据偏离计算出的横向控制指令再控制车辆状态发生变化。

而“研发闭环”是开发这个“运行时闭环”的研发、测试和集成过程。我们可以为上述运行时闭环设计两个独立的研发闭环,一个用于开发和测试感知算法,从视觉图像中计算车道线,并根据通过其它手段获(如仿真系统)得的车道线真值验证计算是否正确,这个研发闭环反复迭代,持续改进算法;另一个研发闭环之间获取车道线的真值,只是开发和测试横向控制算法。这两个研发闭环可以独立进行,最后集成在一起。

  • 原型系统 vs 量产开发

如果只是要做一个原型系统,其实并不复杂。可以使用一个200万像素的前视摄像头(内置ISP)并做好相机内外参的标定,一台工控机(x86带 GPU),一台带线控的车辆。车道线识别的卷积神经网络有很多论文和开源算法可供参考,单纯的横向控制做到 Demo 级别也相对比较容易。

但是如果是需要一个能安装到上万台车辆上的量产系统,事情就不那么简单了,可以想到的难点至少有:

  • 摄像头的ISP性能在不同环境下都能输出较好的画质

  • 车道线识别算法的准确度,尤其是在车道线不清晰、雨雪天气下

  • 车道线识别算法对车道弯曲程度的识别是否准确

  • 横向控制算法在不同车速情况下的稳定性

  • 横向控制算法在不同弯道情况下的稳定性

  • 从获取图像到控制指令被执行的延迟是否足够小,并且足够稳定

当整套系统移植到车载嵌入式平台后还有其它的工程难题:

  • 要选择合适的能够执行深度学习算法的嵌入式处理器

  • 深度学习的算法模型转换到目标嵌入式处理器,会不会缺失算子,会不会有精度损失,会不会执行延迟过长

  • 深度学习算法要对特定的嵌入式平台做优化,是否需要减支,能否定点化,如何充分利用内存带宽,如何提高 Cache 命中率等等

  • 支持视觉算法运行的软件架构如何设计,不同模块之间如何通讯

  • 视觉算法运行的OS往往是个软实时系统,车辆控制算法往往运行在OS硬实时系统上,视觉算法极端情况下丢帧,控制算法如何容错

  • ECU 跟整车系统的电源管理、网络管理系统、诊断系统的对接

  • ECU 的更新升级,包括调试模式、工厂模式、OTA模式等等

这还没有算上硬件设计和测试的复杂性,硬件上还要考虑散热、抗震、电磁兼容性、耐久性等一系列问题。所以实现一套能量产的系统远比开发一个原型系统要复杂得多。开发原型系统只需要几个主要的工程师把算法跑通,就可以控制车辆在道路上进行演示。但这距离工程化量产还差得很远。

原型系统可以暂时忽略上述各种难点问题,在一个非常理想的工况上直接展示核心功能。原型系统的研发可以只使用一个大的涵盖所有环节的研发闭环,内置 ISP的摄像头,工控机上运行视觉算法可控制算法,最理想的道路环境,直接在车上进行调试。

  • 研发闭环的拆解

但是对于工程化的量产开发,不可能把上述所有难点都在一个研发闭环内解决。而且不同的难点之间跨越的技术领域差距非常大。都是深度学习算法相关,但是用英伟达的GPU做算法训练跟在特定嵌入式平台做算法移植和优化,几乎是两个完全不同的专业。

所以在研发时需要把上述各自技术难点拆分在不同的研发闭环中进行。让每一项关键技术点能分别在一个研发闭环中进行独立的开发和优化。每个研发闭环的标的关注于尽可能少的技术点。

每个研发闭环独立演进、测试。最后集成在一起,构成完整的系统。一旦在整个系统运行中出现的问题,要能回溯到该问题所在的独立闭环中去复现、解决、再测试。然后再进行集成后的验证。这就是工程化

工程化是对复杂系统按照合理逻辑进行分解,分解到合适规模的团队能够进行规范化分析、设计和研发,最后还能集成为完整系统的过程。最后体现出来,其重要特征是过程可重复、结果可预期、问题可追溯,流程自动化

这边文章重点在是研发过程的工程化,所以是以研发闭环为基点进行讨论,每个研发闭环的标的往往是整体自动驾驶运行时闭环的某一个环节。还有可能某一个研发闭环,在运行时可能是开环 (Open Loop) 。

1.2.2. 最少变量原理

你可能看到过这么一个故事:在一个大工厂,有一台大机器,突然有一天出了问题,停工不能生产了,请了好多修理工都未能修好,老板最后找到了最有名的机械师,他这边敲敲,那边敲敲,最后确定了一个位置,选准了一个角度,敲了一锤子,机器正常运转起来。然后开价“要一万元”。理由是:“抡一锤子只值一元,但在那里敲,什么角度,用多大力就值9999元,这就是技术的价值。”

且不论故事的真假,这个故事说明了一个现象,人们遇到问题的时候,希望能有一个技术牛人一锤子下去就解决问题,哪怕要为此付出一定代价。

我也遇到过很多次,自动驾驶的研发团队的二十多个人围着一辆车,不同技术部门、不同工种的人轮番上去,你试试、我试试,就是解决不了问题。团队的Leader 肯定也跟前文故事中的老板一样,希望能有这么一个牛人,上去就能指出来问题在哪里,应该如何整改。

但是故事之所以是故事,正因为它只是一个美好的愿望。

相比与这个故事,我更青睐另一个事实。A、B两地的电话线路断了,维修人员先到A、B的中点C,检测AC和BC是否能通电话。如果AC通畅而BC不通,则继续到BC的中点完成类似的检测。我们清楚的知道,无论AB距离多长,这个方法都能让我们以对数函数的形式快速收敛到故障点附近。

第二个故事完美的符合了工程化的特征:过程可重复、结果可预期、问题可追溯。我们知道一定能在某个有限时间内发现问题并解决问题。很多时候发现问题比解决问题难得多。而第一个故事更多是看运气了,能否有这么一个牛人、这个牛人能花多长时间解决问题,这次解决了,下一次能否解决,完全都是不可预期的。

事实上,当一个闭环系统中的可变因素超过达到两个及以上的时候,你就很难确定是哪个可变因素导致的问题。

当我们分析一个可变量时,我们应该尽可能隔离其它可变量的影响。所以拆解研发闭环时,我们希望每一个小闭环针对的只是整体问题域的一个单一可变量。就像故事二中,我们测试AC段时,不会受到BC段的影响。这往往就要求我们能够对其他可变量进行准确的控制甚至隔离,体现在具体应用中就是提供其它可变量的数据模拟 (Mock)。自动驾驶研发中往往把这个称为各种形式的在环仿真。

1.2.3. 研发闭环之间的组成关系

一般我们对自动驾驶数据闭环的理解就是:驾驶专业采集车或者通过量产车辆采集数据,用来改进各种算法、发现并解决驾驶场景中出现的各种问题,改进自动驾驶系统,形成自动驾驶能力的闭环迭代。

这个理解本身并没有问题,但是这么复杂的系统,如果没有一个合适的层级分解,直接就整个复杂系统的进行讨论,那就是在讨论一个玄学问题,而不是一个工程问题。

一个大的研发闭环是由一系列相互关联的小闭环组成特定。有的是层级关系,大闭环内有小闭环,不同小闭环之间还有相互衔接的形式。我们来看具体的例子。

图 4 车辆控制算法开发闭环演进

上图演示了车辆控制算法开发时4个可能的闭环 (A, B, C, D)。

一般车辆控制算法做原型开发时会直接使用“闭环B”,在这个闭环中,控制算法是使用Model Based Development 模式开发,常用的是使用MATLAB+Simulink构建算法模型,模型生成的C代码会编译运行在一个快速原型设备(如:dSpace AutoBox),快速原型设备与车辆总线相连,通过车辆线控系统控制车辆运行。

旁注:关于Model Based Development。 这是一种低代码的开发方式。MATLAB 是数学分析软件,基本上对数学计算有密集需求的专业(如应用数学、自动控制等),从大学开始就是使用MATLAB内置的简便的脚本语言进行数学计算和显示。Simulink 作为 MATLAB的插件存在,提供可视化的编程环境。内置了各种现成的函数实现,使用者只需要将这些模块可视化的装配起来就可以实现自己的算法思想。基本上控制算法工程师从学校学习到实际工作都是这样进行的。这样他们可以专注于将控制理论应用到实际场景,而不必花太多时间在 C 语言编程上。

大多数精通C语言的车载嵌入式工程师,解决的是怎么跟操作系统、跟车辆控制器硬件打交道,对控制理论并不熟悉。毕竟每个领域都不简单,不是每个人都有足够的精力掌握跨领域的知识。

使用 MATLAB + Simulink ,就可以把控制算法工程师的建立的可视化算法(控制模型)自动转换成高质量的 C代码。这样就弥补了两个领域的鸿沟,让两个领域的专家可以专注自己擅长的工作。

Simulink 的可视化开发,本质上跟儿童编程语言 Scratch 的工作模式没啥区别,只不过提供了更专业的函数库和更丰富灵活的表达方式。如果有好事者,完全也可以为 Scratch 编写一个C代码生成器。

旁注:关于快速原型设备。使用 MATLAB + Simulink 编写的可视化模型最终是要转换为 C 代码再编译执行的。因为自动控制系统往往都是运行在嵌入式实时计算平台,所以这些 C 代码需要运行在一个 RTOS 系统上,通过各种现场总线与其它设备进行交互,这是嵌入式开发的领域,这就又要求学自动控制的学生也要精通嵌入式开发,精通两个领域难度就大了,毕竟时间只有这么多。

快速原型设备就是想解决这个问题。这个设备内置了自定义的标准嵌入式硬件,以及配套的实时操作系统。同时提供将 C 代码编译并运行在这个设备上的工具链。设备定型后,其输入输出能力也就确定了。既然控制算法工程师是使用 Simulink 进行工作,那这个设备的开发商也就很贴心的将对设备输入输出进行控制的能力,设计成在Simulink 上可以进行拖拽布局的模块。控制算法工程师就在Simulink 上使用这些模块与自己的算法部分进行对接。这样只需要全程在 Simulink 上工作就能够完成实际的控制动作了。而从模型转换为 C代码,编译、部署到这个标准嵌入式硬件的过程都由设备的开放商提供的工具自动完成。模型开发者不需要关心,只需要专注自己控制领域的技术就行。

量产的嵌入式设备是一般为特定项目专门设计,功能够用、性能满足的情况下,成本越低越好,更不可能去开发配套的 Simulink 模块。而快速原型设备可以堆砌很多接口,提供高性能的硬件,方便的开发工具链,目的是适用于尽可能广泛的项目,当然价格也贵。不过只需要小批量用于开发,成本就不是重要因素,方便才是最重要的。

有了这样的设备就解决了开发前期的问题,节省时间,让控制算法工程师可以不依赖其他工程师的工作而快速投入前期的原型开发,所以叫快速原型设备。

但是“闭环B”是需要实际的车辆的,当车辆未就绪或者资源不够时,我们可以先采用“闭环A”,这个闭环中我们使用软件仿真工具代替车辆,仿真软件可以配置模拟车辆(ego car)的动力学参数,给出ego car 周围环境的真值出道路环境的真值。这样控制算法开发人员可以脱离车辆,先在桌面进行开发,而且感知结果使用的是真值,相当于减少了可变量。

“闭环C”与“闭环A”的差别在于去掉了快速原型设备,使用一块MCU开发板来运行控制算法模型生成的 C 代码。这可以用来验证控制算法模型转换成 C 代码后是否正确。这个MCU应该就是我们量产时打算使用的芯片,但是量产硬件的开发还需要时间,我们可以使用厂家提供的开发板来快速搭建试验环境。可能导致不正确的原因至少有:

  • Simulink模型转换成 C 代码时有错误,可能需要对模型进行调整

  • MCU性能比快速原型设备差,出现之前(闭环A)没有发现的问题

  • 模型生成的 C 语言代码在往 RTOS 集成时出现的 Bug。这是将来在量产开发中会出现的问题,可以提前在这个阶段发现。

“闭环D”与 “闭环C”相比,用实车替换了仿真,相当于增加了真实的车辆动力学因素进闭环。

下表这四个闭环各自的标的,同时列出每个闭环的可变量与不可变量(真值)。

表格 1 控制算法各闭环的比较

闭环ID

闭环标的

可变量

不变量(真值)

A

控制算法模型

算法模型

C语言代码及运行环境

车辆动力学参数

道路环境感知结果

B

控制算法模型在实车的效果

算法模型

车辆动力学参数

C语言代码及运行环境

道路环境感知结果

C

控制算法模型转换成C代码后的正确新

算法模型

C语言代码及运行环境

道路环境感知结果

车辆动力学参数

D

控制算法在嵌入式平台实车运行的效果

算法模型

C语言代码及运行环境

车辆动力学参数

道路环境感知结果

我们可以看到, A 到 B 、A 到 C 两个过程,是分别增加一个不同的可变量(我们假设Smart Senor 给出的感知结果是可信赖的)。这两个可变量又一起出现在 D 中。

然而 闭环 D 还远不是终点,这4个关联到闭环主要是测试车辆控制算法。Smart Sensor 还需要替换成自研的感知算法,MCU开发板还要替换成量产的硬件。所以这四个关联闭环还只是更大范围闭环的一部份,需要被集成在更高一个层级的闭环中。

通过这个例子,我们可以看到一个研发闭环的几个特征。

  • 研发闭环的独立性

每个研发闭环应该是可以独立运转的。能否独立运转也是我们在设计一个研发闭环时必须考虑的边界条件。在独立运转的前提下来我们尽量给闭环赋予最少的可变量,这样就可以专注于测试这个可变量本身的正确性。

然而为了形成闭环,某些数据或过程又是必需的。比如上面例子中的闭环A,我们需要要有一个模拟车以及车周围的环境信息。解决方法就是提供模拟数据。简单的模拟数据可以自己编造,这也是软件测试过程中的常用方法。自动驾驶系统的模拟数据比较复杂,往往需要通过仿真工具来实现。

所以,仿真工具的目的是为了给独立运转的闭环提供模拟数据(Mock Data)。不同目的的闭环,对仿真工具提供的模拟数据的要求是不一样的。比如上面的闭环A,只关心车辆动力学模型是否准确,以及车周围环境的真值数据,置于渲染的是否美观逼真,对这个闭环意义不大。

  • 研发闭环的衔接与演进

我们在一个研发闭环中放入最少的可变量,是为了在测试这个可变量的时候隔离其它可变量的影响。但是我们的最终目标是要将所有的可变量集成到一个最大的闭环并能正常工作。

这个增加可变量的过程是逐步累积的,最好一次增加一个,每增加一个可变量,就形成一个新的闭环,正如上文例子中 “A演进到B”、“A演进到C”以及“B+C=D”的过程。这个逐步做加法的过程,相当闭环之间的衔接或者是原始闭环的演进。闭环演进的步子越小,就越容易发现问题出现在哪一步。

这个做加法的顺序反过来做减法,就恰恰是我们在出现故障时定位问题的方法。

我们能不能跳过ABC,直接到 D,当然也是可以做到的。但是问题在于这么多可变量在一起集成,没有任何的前置验证,最终闭环D能正确运转的可能性很低。体现出来就是项目延期,不知道什么时候能完成。而且出了故障,没有测试环境可以用于隔离某个可变量进行诊断。

ABC三个闭环的存在,实际上也为闭环D提供了故障检测的技术手段。用数学的说法,ABC三个闭环的成功概率是闭环D成功的先验概率,有了这个先验概率,根据概率论的贝叶斯定律,我们是可以算出闭环D的成功概率的。

没有ABC闭环,闭环D的成功完全是靠运气了。还是那句话,我们需要的是工程上的确定性,不到万不得已不需要牛人力挽狂澜。我们要的是让一群普通人一开始就走在堂堂皇皇的王者之道上,而不是寄希望于某个多智近妖的人在混乱中“受任于败军之际,奉命于危难之间”,工程化不指望奇迹!

  • 研发闭环的层级

研发闭环不仅有顺序的演进,还是有层级的。最大的一层闭环就是本节开头说的:驾驶专业采集车或者通过量产车辆采集数据,用来改进各种算法、发现并解决驾驶场景中出现的各种问题,改进自动驾驶系统,形成自动驾驶能力的闭环迭代。这个大闭环是由内层的多级子闭环组成的。

自动驾驶技术在工程上涉及很多完全不同的专业技术,感知算法与控制算法是完全不同的技术方向,感知算法中不同传感器的算法原理也完全不同,前融合与后融合的技术路线也是差异巨大。还有操作系统、中间件跟前面这些算法也没什么关系。

这些不同领域的技术完全可以在各自独立的闭环中进行开发和验证,最后一起装配成最大的闭环。根据各自技术领域的特点,各子闭环内部还能继续嵌套更小的闭环。单独一层的闭环甚至可以衍生出独立的产品,可以自己研发,也可以采购成熟的现有产品。

对闭环的层级分解实质上是划定出了组成完整自动驾驶系统各个子产品的可能边界。这个子产品边界的划分,可以让不同的团队并行的独立完成自己的开发活动,也可以通过采购成熟产品来加快整个系统的研发与建设。

1.3. 测试与仿真的闭环

每个或大或小的独立研发闭环,也要为这个闭环主要的研发内容提供工程上可行的测试方法与测试环境。自动驾驶系统的软硬件又往往是异构平台,功能集成难度大,测试方式也比一般软件测试复杂,对测试工具的依赖也更多,会用到各种在环仿真系统。

图 5 表示出各种级别的测试已经仿真的范围和演进关系。我们从测试与仿真的层级、工具、执行者几个角度进行讨论。

  • 单元测试与模块测试

单元测试主要是指函数级别的测试。理论上要求每一个独立函数、或者类型的成员函数都应该有对应的单元测试代码。单元测试的重要性,怎么说都不为过。单元测试中,我们要考核代码覆盖率和分支覆盖率,要求都达到100%。也就是每一行代码,每一个可能分支都被单元测试程序执行过。

单元测试还对软件架构的合理性有重要作用,这个很少被人提及。原因就是它可以考验软件架构在函数粒度上的可测性。

不是能够为每一个函数都写出有效的测试代码的,一个函数输入的参数组合可能有几万种,就不可能全被覆盖,一个函数的执行依赖函数外部的状态(比如全局变量),同样的输入,因为外部状态可能有不同的结果,单元测试也不好写(了解一下什么是纯函数)。一个函数干了太多事情,比如有两千行,单元测试基本就没法做了。

所以当能为所有函数写出单元测试代码时,就已经强迫目标代码在函数级别设计上做了优化:函数功能尽量单一,尽量写成纯函数,一个函数代码别太长等等。

可以看一个反面例子,请搜索:“丰田事件 1万多全局变量建了个超大bug基地”。

函数级别的单元测试关注点是检查函数在不同输入的情况下,其输出的正确性。而且这个函数最好内部不保存状态(纯函数)。这个测试范围再大一些,多个函数(或多个代码文件)一起协同工作,在多次关联调用之间需要保存状态,一般而言,我喜欢把这个范围称为功能模块。可以把模块理解为更大粒度的函数。模块有其对外公开的调用接口,已经可被观察到的对外相应,在内部有多个不公开的子模块或函数,有内部保存的状态。

从程序语言上看,一个模块相当于 Python 的模块,Rust 的模块,Java 的 package,C++ 的一个多个类的集和,C 语言关联度比较高的一个或多个代码文件。

对模块的测试关注点是不同输入情况下,模块的响应是否正确,模块的内部状态是否正确。

单元测试和模块测试一般都可以使用特定的单元测试框架来编写,例如 gtest 用来写 C++ 的单元测试,Rust 内置有单元测试机制。同时还有各种代码检查工具来验证代码是否符合编码规范,检查代码的圈复杂度,分析潜在的代码错误。

单元测试和模块测试的编写者就是开发该功能模块的程序员本身或其小组成员。一般来说程序员每次代码提交前,关联模块的单元测试和模块测试代码都应该能执行通过。每一处代码修改,与其关联到所有模块的单元测试代码都要被重新执行。

单元测试和模块测试的模拟数据一般就写在测试代码中,看测试框架的能力,也可以从外部加载测试数据。

单元测试和模块测试很重要,不幸的是,在研发中还是经常会出现没有单元测试的情况。原因很简单,项目太紧,没时间。没有单元测试不影响代码运行,只是某些情况没有被测试而已,这样的代码合并到整个系统中,就是潜在的风险。

  • 子系统

模块往上是子系统。一个子系统表示能够完成某个方面的功能体系,比如完成了“视频采集子系统”,“车辆的识别子系统”,“泊车路径规划子系统”,“用户交互子系统”等等。存在形式可能是Linux内核中的一个驱动,应用层上的一个独立的进程或一个 SOA 服务,MCU RTOS 上一个或多个关联到 TASK,CP AUTOSAR 上的一个 SWC 等等。

与模块测试一样的是,子系统一般会有内部状态,需要检查在响应外部输入过程中,内部状态的正确性。

一个子系统应该能够单独被测试,就是它能独立的在一个测试闭环中运行并被校验。然后再做加法,即把更多的系统一个一个的逐步加入进测试闭环中,最后形成整个系统。

子系统测试跟模块测试的差别在于模块测试的访问接口一般都是 API 级别,也就是模块的 API 函数会直接被测试框架进行调用。而子系统的接口往往是与其它子系统的之间的交互,可能是通过各种网络(Can,FlexRay,以太网等等),也可以是通过共享内存、信号量等进程间通讯机制。

这决定了子系统的测试不太能跟模块测试采用同样的工具。子系统的测试不光要提供模拟数据,还要有能把模拟数据送入子系统的方法,以及检测子系统响应结果的方法。

子系统的测试一般要采购一些专用工具,同时要开发一些工具。采购的工具往往有总线模拟类(如 CanOE)。还有整体的自动化测试框架,如 ECUTest 。也有开源的替代品,如Robot Framework。自己开发的工具需要能将采购的工具连接起来,或者为特定的设备或子系统接口开发驱动。

“发布订阅”设计模式对子系统测试有重要意义。因为“发布订阅”模式解耦了消息的生产者和消费者,所以我们测试一个子系统时,就可以比较方便的模拟出该子系统需要对数据发送给它,同时也可以通过接收该子系统发出的消息来校验其行为或内部状态是否正确。

无论是自动驾驶常用中间件ROS/ROS2、百度 Cyber RT、DDS,还有SOA的基础协议SOME/IP都是支持发布订阅模式的。

  • 同构与异构

自动驾驶系统是一个异构系统。硬件上有高实时性的MCU和高性能多功能的 SoC芯片。MCU上运行RTOS系统,SoC 上有 Linux 或 QNX 也可以运行RTOS系统。SoC上有通用CPU,有用于渲染的GPU,有用于数学计算的 DSP 和深度学习的NPU,其中每个部分的软件实现方式也是不一样的。

单元测试、模块测试甚至基于CPU的子系统都可以在x86的开发设备上进行测试。但是如果子系统涉及到嵌入式平台的专用设备,其真实效果,尤其是性能表现只能到目标嵌入式平台进行测试。一般我们称之为处理器在环 (Processor in Loop)。

所以整个测试闭环以及测试工具系统,需要考虑对嵌入式平台的支持。

图 5 测试与仿真的类型
  • X in Loop

除了异构子系统集成测试比较特殊之外,前面讲的测试概念其实跟一般软件测试没太大区别。自动驾驶系统测试中比较特殊是各种形式的在环测试。

1.2.3节中的闭环A是典型模型在环 (MiL, Model in Loop),闭环C是属于软件在环 (SiL, Software in Loop),将模型生成了软件代码进行执行。

重点是Simulink 模型直接运行在快速原型设备里,仿真软件直接给出传感器的真值数据。也就是说就是仿真软件渲染了场景动画,也是给人看的,实际的真值直接发送给模型作为输入,当然真值数据怎么从仿真软件到模型需要工程师写代码来集成。

前文异构子系统集成测试就是一种处理器在环测试 (PiL, Processor in Loop)。异构子系统的集成是可以将多个子系统逐步集成进来的。如果把视觉算法也运行在目标嵌入式平台处理器上,视觉数据从仿真软件中提取出来通过网络发送到嵌入式平台,那就是 PiL 和 MiL 的混合模式。这时候仿真软件渲染的图像是有用的,运行在嵌入式平台的视觉算法根据图像识别出目标发送给Simulink模型。比纯粹的 MiL ,这一部集成了更多内容进仿真闭环中。

标准的硬件在环 (HiL, Hardware in Loop) 是要用正式量产的ECU硬件,而且数据与 ECU 硬件交互的方式都应该与实际在车上是一样。虽然车辆周围的环境是通过仿真模拟出来的,但从ECU本身的视角,几乎是分不出自己是在仿真测试环境还是在真实车上。这样整个测试的环境就与真实车辆更接近了了。

在PiL测试中,如果需要使用原始的图像数据,一般图像数据是通过对视频文件的回放,并通过网络发送到目标嵌入式平台 (ECU),而 HiL 则是直接通过物理设备将仿真出来的图像转成原始图像格式注入到 ECU 的视频采集端口,只是通过仿真跳过了传感器模数转换和ISP 处理的过程。这样ECU的采集模块也备集成到测试闭环中了。当然,完成这个视频数据从仿真软件到ECU视频端口,需要专门的硬件设备,这就是HiL台架的作用了。

车辆在环仿真 (ViL, Vehicle in Loop) 实际上跟 HiL 很接近,但是全套HiL设备要能小型化后装到车上,控制算法的执行不是基于仿真软件的动力学模型,而是实际物理车辆的控制系统。仿真软件把模拟的场景注入到ECU,相当于物理汽车认为自己在某个道路场景中驾驶,ViL 跟HiL 的差别在于使用了真实的车辆控制系统。

表格 2 列出了各种测试和仿真的定义和说明。

类型

描述

工具

执行者

单元测试

对代码中的单个函数或类进行测试,尽可能覆盖所有的执行分支,覆盖类型的各种状态。验证函数或类方法的各种输入输出的正确性

单元测试框架

程序员

CI Script

模块级测试

多个相关函数或类型集合在一起进行测试,测试的范围比单元测试要大,但是使用相同的测试方法。

程序员

CI Script

子系统

特定功能的子系统,主要测试其输入输出,以及内部状态的转换

单元测试框架

开发特定工具

程序员

测试工程师

CI Script

同构子系统

集成测试

多个同构的子系统之间的集成测试,验证子系统之间的数据交互是否正确

异构子系统

集成测试

多个异构的子系统之间的集成测试,验证子系统之间的数据交互是否正确

开发特定工具

程序员

测试工程师

CI Script

感知融合算法

有效性测试

使用预定义的标准数据集验证算法的功能、准确度和执行性能。不同的算法都有各自的数据集,数据集可以逐步扩充

开发特定工具

程序员

测试工程师CI Script

模型在环仿真

Mil

对于 Model Based  开发,可以用模型开发工具和仿真工具配合进行仿真测试。

使用成熟工具 并为持续集成开发工具

程序员

测试工程师CI Script

处理器在环

仿真/测试

Processor in Loop

使用回放的传感器数据(已经加工过的二手数据),执行全流程的系统动作,验证整个系统功能的正确性,但不包含传感器。

使用成熟仿真软件并开发特定工具进行集成

测试工程师CI Script

硬件在环仿真

Hardware in loop

使用完整的软硬件产品,包含真实的传感器,利用仿真环境生成真实的场景及传感器数据发送到目标系统,完成全功能的产品测试。

实车测试

在真实车辆上进行测试

车辆在环仿真

Vehicle in Loop

2. 产品级闭环

上一章讲的是研发闭环,偏重与研发与测试仿真的协同。这一章我们从产品的角度来讨论。

自动驾驶系统是复杂度非常高的系统,从产品角度看,它复杂到没有一个传统意义上的产品经理能把待开发的系统准确定义清楚。我遇到过有的公司想招自动驾驶产品经理,招了两年,产品部门走马灯似的换人,也不知道自己到底想要什么样的人。还有的JD上对产品经理的要求几乎是十项全能,像极了上海人民广场大妈手上的征婚条件,既要又要。

为什么自动驾驶领域的产品这么难定义?因为他不是一个单一产品,它是一个高度复合的产品,本身就是由一系列软硬件子产品组合而成,同时它又被装配到了“车辆”这个更复杂的产品中。然后这个车辆会在无限可能的交通场景中行驶。也就是说自动驾驶产品是多重复杂性的叠加,至少包括内部组成的复杂性,装配环境的复杂性,以及使用场景的复杂性。

在分析这种产品时,我们要把它分解成多个、多级子产品,对逐个子产品进行分析,然后分析各子产品如何构成完整的系统。每个子产品有其特定的关注点、技术领域,有其自己独立的演进方向,在整个系统中,某个子产品可能有多个可替代的选项。

图 6 软件架构鸟瞰图
图 7 四级产品架构

2.1. 各层产品的独特性

图 6 中横向“层级”这个维度,每一层的每一个域(性能域或实时域)都有其独特性,要解决的问题、涉及的技术领域、需要的专业技能、开发与测试的方式,差别非常大。

  • “产品”的概念

我们需要对“产品”的概念有一个定义。一般我们理解的“产品”是从终端用户角度去看到,比如一个咖啡机、一个手机、一台车,软件产品比如浏览器,音乐播放器、微信APP 等等。

我们把产品的概念抽象一下,如图 8所示,“产品”具有一系列“特性”,并提供“交互接口”给它的“用户”。

我们把“车”当成一个产品,它的特性就是“百公里加速时间、语音控制大屏、代客泊车功能、自动变道功能”等等,它提供了“方向盘、仪表盘、刹车、油门”等交互接口给任何具有驾驶能力的用户。

我们把“嵌入式Linux”作为一个产品,它的特性就是“启动时间、进程调度、内存管理和各种外设的支持能力、信息安全、通讯能力”等等,它提供符合POSIX 标准的API,还通过定制的内核驱动提供新设备的API访问接口,Linux 应用开发者可以基于这些API实现自己想要的应用功能。

这两个例子涉及的领域差距非常大,但它们都是产品。

图 8 产品概念示例

如图 9所示,产品的交互接口、特性、特定的技术领域都是产品的内涵,它决定了产品的能力以及对外的交互方式。但产品还有其外延概念。

图 9 产品概念的内涵与外延

产品的外延至少包括了其市场定位、适用场合等,同时一个产品还需要有一定的独立性。所谓独立性是指它的设计、研发和销售是可以独立与其它产品进行的。比如说,我们可以独立的销售一张桌子,但是我们没办法只卖一支桌子腿。

产品的内涵与外延也是密切相关的,比如产品的独立性就与其对外的交互接口密切相关。

  • 产品的独立性

自动驾驶系统内部的子产品构成这么复杂,引申出来其内部各子产品的独立性非常重要。各子产品相对独立就可以分到不同团队进行开发,某些子产品就可以直接从市场上采购成熟的产品,就可以使用不同供应商的产品进行替代。

汽车OEM的供应链管理最喜欢的事情就是同一个零部件有多家供应商。避免单一供应商无法供货时生产停滞,还可以在价格是有较好的谈判地位。

上汽说要掌握自己的“灵魂”,本质上也是这个产品独立性的问题。因为自动驾驶系统虽然需要域控制器硬件各种传感器硬件,但本质上任然是一个软件密集型的产品。其内部各组成部分(软件子产品)的切分就不如原先的硬件零部件那么清晰。以科技公司为主的供应商就会更倾向于软硬一体的解决方案,或者至少是在软件上提供整体的解决方案。而汽车OEM则希望软件硬件能分开,同时在软件组成上最好也能分拆成独立的子产品,这样就能在供应链上能保持更好的控制权。

产品内涵中的“特定技术领域”也能反映产品独立性的特征。图6中各层产品的技术领域差别非常大,相关研发人员的专业技能也相差很大,开发与测试的方法也有很大的差异。比如为嵌入式系统定制开发Linux的工程师技能与视觉算法的工程师差异非常大;能熟练在Linux上开发应用的工程师很少能熟练的编写 Linux 驱动;同样是算法工程师,从算法设计、算法实现、参数调优、嵌入式移植和优化,也需要很多不同技能的工程师来实现。所以这些不同的产品时需要不同技能背景的团队来各自独立的设计和开发。

当我们找一个设计用户级产品的产品经理来定义一个操作系统级的产品时,他是无法胜任的。我们让一个对操作系统非常有经验的专家去定义自动驾驶的具体功能及场景,他也需要补充很多知识,意味着要花更多时间。

2.2. 纵横交叉的产品演进路线

子产品的独立性,让图6中的各层产品可以独立演进,可以交错组合成完整的客户交付产品。

  • 横向分层产品的演进路线

图 10 中显示了每一层可能的产品演进方向。比如在“计算平台”,研发初期可以使用 x86 工控机或 Xavier 套件这样立即可得的设备开发必须的软件功能,当确定目标 SoC 平台后,可以使用该平台的原厂开发板,等基于目标平台的量产专用计算平台硬件开发完成后,再将软件系统切换过去。

对应的操作系统层,前期开发直接用 Ubuntu, 为了让开发人员环境一致,可以再用个 Docker;到了目标平台,就要用 Yocto 定制化一个适合的 Linux 系统,这也是目前大多数平台采用的方式。随着自动驾驶SoC 芯片性能越来越强,核心数越来越多,相应的虚拟化技术和容器技术也会被使用,相应的研究和开发可以先开始做,然后用于将来的产品。

图 10 横向分层产品演进路线

中间件这一层,一般习惯于先基于 ROS或 ROS2开发原型系统,再转向可以用于量产的中间件,如 Adaptive AUTOSAR 或百度 CyberRT 等。

  • 纵向产品组合路径

图 10中的每一层从左到右是单层的某个产品的演进路线。但是任何单独一层的产品是无法构成完整的自动驾驶系统的。我们不可能等到某一层的产品成熟了再去做整体的集成。相反,我们希望全系统集成的时间越早越好。

图 11 演示了多种可能的全系统集成路线,从上自下的每一条贯穿路线,都是一个可能的产品集成组合。

路径 ① 和 ② 正好对应与1.2.3节的“闭环 A”和“闭环 B”。

路径 ③ 是一个研发前期的原型系统仿真,中间件、操作系统、计算平台都是使用现成的产品,重点放在相关算法的研发。自动驾驶创业企业早期用于融资的多半就是这样的系统。

路径 ④ ⑥ 是一个典型的硬件在环仿真系统,从硬件到操作系统、中间件都已经是使用产品化的解决方案。

路径 ⑤ ⑧ ⑨ 都是量产交付的产品,因为最终的目标产品不一样,所以各层使用的子产品也不同。路径 ⑦ 是车辆在环仿真系统。

根据需要,还可以设计出各种不同的集成路径,每一条路径实际是也行程了一个独立的产品研发闭环。我们一般从最简单的路径开始,逐渐增加可变量,达到量产交付的最终产品形态。

 图 11 纵向产品组合路径
  • 产品规划的闭环思路

横向的分层是由各层产品的相对独立性决定的,纵向组合是构成完整系统的必然要求。一般来说,企业在做产品规划时会更关注最终形态产品,而对中间各层的子产品的规划重视不够,毕竟最终产品是要做商业交付的。

这样就会在事到临头发现没有合适的中间子产品,或者子产品功能不满足交付要求,需要重新选型并适配,或者还需要较多的研发时间以致延误项目进度。所以企业的在产品规划时,要充分重视个层子产品的规划,要用横向和纵向两个视角进行综合考虑。

横向视角代表这个子产品自身的发展,要遵从由该产品技术特性决定的客观规律,不能主观上拔苗助长;要跟随技术的发展趋势保持先进性;要有合适的产品经理和架构师,建设合适的团队;要能建立能独立的开发测试闭环,使得该子产品的研发过程不依赖其它子产品,可以独立迭代。

纵向视角会影响子产品演进过程中各步骤的功能规划和优先级设定。每一条纵向路径虽然其目的不同(有的是测试闭环,有的是最终交付闭环),都是一个完整的全流程闭环。纵向路径对中间子产品的要求首先是能保证这个纵向路径闭环能够运转。因此子产品的横向发展步骤要与多个纵向路径的需求进行匹配协同。也就意味着我们在规划某个子产品的路线图时,要同步要把纵向集成的因素考虑进去。

如果某个子产品不成熟,或者还没能达到量产要求,为了纵向路径能走通,我们要寻求前期的替代品。虽然这个阶段的子产品不能用于量产,但是它很好的支持了其上层产品的开发。于此同时,抓紧进行该层子产品的开发来了来了,为下一步替代临时方案做准备。

横纵两个方向的拆解和组合,是为了能让每一层的子产品都能有独立进行设计和开发,这样也就让所有层的都能够并行进行开发,然后再通过纵向的路径逐步集成。所以设计良好的产品规划,包括子产品规划,能够让整个工程化落地的过程少走弯路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值