简介:围绕Carsim与Simulink搭建AEB联调仿真平台的技术文档,面向汽车电子、ADAS算法验证及车辆动力学仿真方向的学习者和工程师,帮助建立从道路场景建模到控制逻辑验证的整体框架。文档重点展开3D道路、前方车辆速度与雷达感知构成的道路环境模型,并在Simulink中拆分感知器、决策器和执行器模块,说明碰撞检测、危险预测、制动命令等控制逻辑,同时交代碰撞时间、碰撞速度、制动距离等结果输出以及曲线可视化和统计分析方法。压缩包共1个docx文件,约15KB,体量轻便,适合作为AEB联合仿真的入门提纲与模块清单。已有606人学习,可据此快速搭建Carsim与Simulink联调的知识地图,并延伸工况设计与问题排查。
1. 一套 AEB 联调仿真,最难的不是刹车而是握手
第一次把 CarSim 和 Simulink 接起来跑 AEB 工况时,我盯着示波器看了很久:两车距离一路拉开,前车速度归零,本车越跑越远。问题出在 CarSim 的 Run Control 还停在 Internal 模式,两边各算各的,S-Function 只是个装饰品。基于 CarSim 和 Simulink 的 AEB 联调仿真要解决的正是这一类“介于两者之间”的事:CarSim 负责 3D 道路、前方车辆运动状态和雷达探测,Simulink 负责承载感知、决策、执行的控制链路,两侧按同一时间轴推进,最后把碰撞时间、制动距离、碰撞速度这些指标落成能分析的数据。它适合正在做 AEB 算法验证、又不想每次都动用实车或台架的人,也适合刚入门的同学拿它把“雷达—TTC—制动”这条链路走一遍完整的闭环。
2. 联调地基:CarSim 场景模型与 Simulink 接口怎么搭
联调出问题,九成出在地基上。CarSim 和 Simulink 之间的接口看起来只是拖一个 S-Function 模块,实际上背后有一组隐式约定:谁推进时间、谁负责积分、状态量按什么单位、按什么顺序传,任何一条没对齐,结果都不会报错,只会悄悄跑偏。所以这一章按“先建模型、再接通道、后配求解器”的顺序走,每一步都落到可检查的具体操作上。
2.1 CarSim 侧建模:3D 道路、前车运动与雷达参数
第一件事是切仿真模式,位置在 Run Control 面板的 Models 区,把 Simulink 勾上、CarSim 自己的 Internal Solver 取消。改完点一下 Send to Simulink,CarSim 会生成(或更新)一个 S-Function 模块文件,Simulink 模型里引用的就是它。这一步不做,后面怎么调都白搭——CarSim 会自己闷头算完,Simulink 拿到的永远是 t=0 那个初值。
车辆模型直接在 CarSim 的库里选,AEB 标定阶段常用的是 C 级轿车或 SUV,关键参数是质量、轴距、制动系统压力—力矩曲线,尤其是制动执行器的响应延迟,它会直接决定制动距离仿真的真不真。我一般会把制动系统里的压力建立时间从默认的 0.2 s 调到 0.3 s 左右,模拟真实液压系统的建立过程,这样仿真出来的制动距离和实车差得不会太离谱。
道路用 3D Road 模块搭。中心线可以按 X-Y 坐标表打点,也可以导入 IMA 文件或按曲率—里程描述。AEB 场景不建议一上来就铺两公里长直道,因为直道上雷达横向误差体现不出来;更合适的是一段 200~300 m、带轻微弯道的双向路,曲率半径控制在 500 m 以上,既保证雷达能稳定跟住前车,又能把横向定位的偏差暴露出来。路面附着系数按测试需求改,干燥沥青填 0.85,湿滑路面填 0.5 左右,这一项直接决定 AEB 能不能刹住。
前车放在 Traffic 模块里,位置模式选“相对于本车”或“绝对坐标”都行。AEB 工况里最有价值的不是匀速前车,是带减速曲线的:比如初速 50 km/h、在触发时刻以 -3 m/s² 减速。这种工况能同时考到 TTC 计算的灵敏度和决策阈值的合理性。前车的速度曲线用时间—速度表填,注意单位,CarSim 里是 km/h,Simulink 侧常用 m/s,混用是后面排查章节里最常见的坑之一。
雷达是场景模型的一部分,在 CarSim 的 Sensors 里配一个前视雷达:探测距离 100~150 m、水平视场角 ±10°、更新周期 20~50 ms。配好之后,CarSim 会在每个仿真步输出一份目标列表,包含相对距离、相对速度、横向偏移。也可以用简化做法——雷达只输出前车的绝对状态,相对量在 Simulink 里自己算,这样调试起来更可控。两种都行,前者更接近真实传感器链路,后者更适合先把控制逻辑跑通。
导出的通道我一般固定成这么一组,方便后面接:
| 方向 | 通道名 | 含义 | 单位 |
|---|---|---|---|
| 输出 | Vx | 本车纵向速度 | km/h |
| 输出 | Ax | 本车纵向加速度 | g |
| 输出 | Xcg | 本车质心纵向位置 | m |
| 输出 | Ycg | 本车质心横向位置 | m |
| 输出 | Tgt_Vx | 前车纵向速度 | km/h |
| 输出 | Tgt_Xcg | 前车质心纵向位置 | m |
| 输出 | Radar_Range | 雷达测得相对距离 | m |
| 输出 | Radar_RangeRate | 雷达测得相对速度 | m/s |
| 输入 | IMP_MYBK | 四轮制动压力 | MPa |
| 输入 | IMP_THROTTLE | 节气门开度 | 0~1 |
前四行是车辆动力学状态,中间三行是前车和雷达,最后两行是 Simulink 回灌给 CarSim 的执行器指令。导入导出表在 CarSim 的 I/O Channels 里配,配完记得点一次 Refresh,Simulink 侧模块的端口顺序会跟着变。
2.2 Simulink 侧骨架:S-Function、通道映射与求解器配置
Simulink 模型的骨架很固定,从左到右一条线:CarSim S-Function 模块输出车辆与前车状态 → 雷达预处理 → TTC 计算与决策 → 制动压力限幅 → 回灌给 S-Function 的输入端口。中间挂一个 Scope 和一组 To Workspace 做记录。整个模型里最容易被忽视的是 S-Function 模块上那个“Sample time”参数,它必须和 CarSim 里的仿真步长一致,我一般统一设成 0.001,也就是 1 ms。
求解器配置我习惯用脚本统一管,避免每次改参数在三个面板里来回翻:
% setup_aeb_sim.m % 统一配置 CarSim-Simulink 联合仿真的求解参数 % 每次换工况前跑一遍,避免手动改配置留下残余 mdl = 'aeb_joint_sim'; % Simulink 模型名,按实际改 cs = getActiveConfigSet(mdl); set_param(cs, 'SolverType', 'Fixed-step'); % CarSim 官方示例推荐定步长 set_param(cs, 'Solver', 'ode4'); % 四阶龙格库塔,稳定性够用 set_param(cs, 'FixedStep', '0.001'); % 1 ms,与 CarSim 步长对齐 set_param(cs, 'StopTime', '10'); % 10 s 足够跑完 60 km/h 接近工况 set_param(cs, 'SaveOutput', 'on'); set_param(cs, 'SaveFormat', 'StructureWithTime'); % 保留信号名,便于后处理 set_param(cs, 'LimitDataPoints', 'off'); % 不允许抽点,否则尖峰会被吃掉 save_system(mdl);这段脚本干了四件事:把求解器锁定为定步长 ode4、把步长压到 1 ms、把仿真时长设成 10 s、把输出格式设成带时间戳的结构体。定步长是必须的,因为 CarSim 的车辆模型内部就是按固定步长推进的,Simulink 用变步长会导致两侧时间轴对不齐,表现是积分一步跨过了一个制动事件,车辆直接“瞬移”。ode4 相比 ode3 在同样步长下精度更高,代价是每步多算几次导数,对 1 ms 步长来说开销可以接受。LimitDataPoints关掉很重要,默认开启时 Simulink 会对长仿真自动抽点,TTC 曲线上的毛刺会被抹平,看起来一切正常,实际已经丢了信息。
通道映射这一步要靠 CarSim 生成的 S-Function 端口顺序来对。导入表里配了几个输入,S-Function 模块上就有几个入口,顺序不能错。我一般会在 S-Function 后面紧跟一个 Bus Selector,把几十路信号按名字拆出来,而不是靠端口序号硬连线——端口序号一旦因为 CarSim 里加了一个通道就整体错位,这种错位不会报错,只会让刹车压力打到节气门上。
3. 控制链路:从雷达相对量到 TTC 分级制动
场景搭好只是把舞台摆上,真正决定 AEB 表现的是中间这条链路:雷达给出的原始量怎么变成可用的相对运动学量,相对量怎么变成 TTC,TTC 怎么变成分级制动压力。这三步在 Simulink 里都是纯逻辑,用 MATLAB Function 块写比拖几十个 Gain 和 Switch 清楚得多,也方便版本管理和复用。
3.1 相对运动学量:把绝对坐标换算成 TTC 需要的东西
CarSim 输出的是绝对量——本车质心位置、前车质心位置、各自速度。TTC 只认两个东西:纵向相对距离和接近速度。所以第一件事是做坐标换算,同时把单位统一到 m 和 m/s。这一步看着简单,但坐标轴取错、速度作差方向搞反,都会让 TTC 恒为无穷大,表现就是“仿真跑完了但一次刹车都没触发”。
function [d_rel, v_rel, ttc, in_lane] = rel_kin( ... ego_x, ego_y, ego_v, tgt_x, tgt_y, tgt_v, lane_half_width) % 输入: % ego_x, ego_y 本车质心纵向/横向位置(m) % ego_v 本车纵向速度(m/s) % tgt_x, tgt_y 前车质心纵向/横向位置(m) % tgt_v 前车纵向速度(m/s) % lane_half_width 车道半宽(m),用于横向过滤 % 输出: % d_rel 纵向相对距离(m),正值表示前车在前方 % v_rel 接近速度(m/s),正值表示正在接近 % ttc 碰撞时间(s),无法碰撞时为 inf % in_lane 前车是否在本车道内(逻辑量) dx = tgt_x - ego_x; % 纵向间距,CarSim 的 X 向即车头朝向 dy = tgt_y - ego_y; % 横向偏差 % 横向过滤:偏差超过车道半宽就认为不是同车道目标 in_lane = abs(dy) <= lane_half_width; d_rel = dx; v_rel = ego_v - tgt_v; % 本车更快时为正,代表距离在缩短 if in_lane && v_rel > 0.1 ttc = d_rel / v_rel; % 只有正在接近时 TTC 才有物理意义 else ttc = inf; % 前车更快或不在本车道,判定为不会碰撞 end end这段函数的逻辑分三层。第一层算间距,dx直接用前车减本车,因为 CarSim 的 X 轴指向前方,不需要额外旋转,如果道路有弯道,横向偏移dy会随曲率变化,所以横向过滤用lane_half_width而不是固定值,弯道上适当放宽到 2.0 m 能减少误判。第二层算接近速度,v_rel = ego_v - tgt_v的符号约定必须和 TTC 的计算保持一致,写成tgt_v - ego_v的话 TTC 会是负数,后面所有阈值判断全部失效。第三层是两个保护条件:in_lane防止把隔壁车道的车当成目标,v_rel > 0.1防止前车更快时除出一个负数或极大值。0.1这个死区按车速量级定,车速 60 km/h(约 16.7 m/s)时 0.1 m/s 的相对速度相当于 167 s 的 TTC,已经远大于任何制动阈值,判成inf不影响决策。
单位统一在这段函数外面做。CarSim 输出的Vx是 km/h,进函数前除以 3.6;制动压力回灌时反过来乘。我习惯在模型最外层做一次单位换算,函数内部一律用 SI 单位,这样函数拿去复用到别的项目里也不用改。
3.2 TTC 分级决策:阈值怎么设、制动压力怎么给
TTC 拿到之后就是决策。工程上常用三级:TTC 小于 2.4 s 给预警,小于 1.6 s 给部分制动,小于 0.6 s 给全力制动。这三个数不是拍脑袋定的,它们来自“系统延迟 + 驾驶员反应”的叠加:雷达 50 ms、总线 20 ms、液压建立 300 ms,光执行链路就接近 0.4 s,再加驾驶员反应时间,0.6 s 的全力制动阈值其实已经踩在物理极限上了。所以这套仿真的价值不在于把阈值调得多漂亮,而在于能把“阈值 - 制动距离 - 碰撞速度”这三者的关系量化出来。
function [p_cmd, level] = aeb_decision(ttc, v_ego, p_max, t_warn, t_part, t_full) % 输入: % ttc 碰撞时间(s),来自 rel_kin % v_ego 本车速度(m/s),用于低速抑制 % p_max 最大制动压力(MPa),按车型标定 % t_warn / t_part / t_full 三级 TTC 阈值(s) % 输出: % p_cmd 制动压力指令(MPa) % level 0=无动作 1=预警 2=部分制动 3=全力制动 level = 0; p_cmd = 0; % 低速抑制:低于 8 km/h 时 AEB 不介入,避免停车场误触发 if v_ego < 8 / 3.6 return; end if ttc <= t_full level = 3; p_cmd = p_max; % 全力制动,直接给满压 elseif ttc <= t_part level = 2; % 部分制动:在 40%~80% 之间随 TTC 线性插值,避免阶跃冲击 ratio = (t_part - ttc) / (t_part - t_full); p_cmd = p_max * (0.4 + 0.4 * ratio); elseif ttc <= t_warn level = 1; p_cmd = 0; % 预警级只报警不制动 end end三个阈值参数我一般设成t_warn = 2.4、t_part = 1.6、t_full = 0.6。部分制动那一段用线性插值而不是直接给固定值,是因为固定值会让制动压力在阈值附近跳变,仿真曲线上出现方波,看着很“干净”,实际标定时根本没法用。插值区间取 40%~80% 是把执行器的死区和饱和区都留出来:低于 40% 压力打不动卡钳,高于 80% 再往上加对减速度贡献已经很小。低速抑制那 8 km/h 的线也不是随口说的,AEB 标准工况里低速场景主要由其他功能覆盖,这里卡一个下限可以避免在起步工况下误触发,把仿真结果污染掉。
p_max按车型标定,轿车一般在 12~15 MPa,SUV 因为质量大,往往要 15 MPa 以上。这个参数建议做成模型工作区变量,方便后面批量扫参。
4. 数据落地:输出信号选择与后处理脚本
仿真跑完只是拿到一堆 MAT 文件,真正能支撑结论的是从里面提炼出的三个指标:碰撞时间、碰撞速度、制动距离。这三项如果靠手点 Scope 去读数,基本等于白做,所以记录和后处理要提前设计好。
4.1 记录哪些信号,怎么记
To Workspace 模块不能见信号就接,接多了文件几百 MB,打开都费劲。我一般固定记录这么一组:
| 信号 | 来源 | 用途 |
|---|---|---|
| ttc | 决策模块输出 | 判断触发时刻与阈值合理性 |
| p_cmd | 决策模块输出 | 看制动压力建立曲线 |
| level | 决策模块输出 | 分级触发时序 |
| v_ego | CarSim 输出 | 算减速度、碰撞速度 |
| d_rel | rel_kin 输出 | 算制动距离 |
| ax | CarSim 输出 | 校核减速度与压力是否匹配 |
To Workspace 的变量名和上面这张表一一对应,Save format 选Structure With Time,和前面求解器配置里的SaveFormat保持一致。采样点不做抽稀,1 ms 步长跑 10 s 也就一万个点,对 MATLAB 来说毫无压力。真正的负担来自那些接了几十路内部信号的模型,那些信号大多只在调试阶段有用,正式跑的时候应该断开。
4.2 后处理:把 MAT 文件变成一张指标表
后处理脚本的目标是输出一张“工况 - 初始车速 - 碰撞速度 - 制动距离 - 最小 TTC”的表,一次仿真出一行,几十次仿真拼起来就能看出参数调整的方向。
function res = post_aeb(mat_file, ego_v0) % 输入: % mat_file 仿真输出的 MAT 文件名 % ego_v0 该工况的初始车速(km/h),用于填表 % 输出: % res 一行结果:初始车速、碰撞速度、制动距离、最小 TTC、是否碰撞 S = load(mat_file); t = S.ax.time; ax = squeeze(S.ax.signals.values); v = squeeze(S.v_ego.signals.values); % km/h d = squeeze(S.d_rel.signals.values); % m ttc = squeeze(S.ttc.signals.values); % s % 制动触发时刻:压力从 0 抬起来的那一帧 p = squeeze(S.p_cmd.signals.values); idx_brk = find(p > 0.1, 1, 'first'); % 减速度积分得到速度损失,用于交叉校核 dv = cumtrapz(t, ax * 9.81) * 3.6; % ax 单位是 g,换成 km/h res.ego_v0 = ego_v0; res.collision = any(d <= 0); % 间距归零即判定碰撞 res.v_impact = v(end); % 仿真结束时的车速 res.d_brake = d(idx_brk) - d(end); % 制动距离 res.ttc_min = min(ttc(isfinite(ttc))); % 忽略 inf 后的最小 TTC res.dv_check = dv(end); % 与 v(1)-v(end) 交叉验证 end几个细节值得说。idx_brk用压力阈值找出制动起点,因为 TTC 曲线在触发前是缓慢下降的,靠导数找拐点不如直接看压力;cumtrapz那行是交叉校核,积分出来的速度损失和首尾速度差应该对得上,差得多说明采样或单位有问题,这个检查我每次跑完都做一遍,抓到过好几次 km/h 和 m/s 混用的错误;ttc_min前先过一遍isfinite,因为前车更快的那段时间 TTC 是inf,直接min会得到inf,看起来像是没触发。d_brake用触发时刻的间距减去结束时刻的间距,而不是用总位移,是为了排除触发前的接近段,让不同初始间距的工况能横向比较。
跑完一组工况,把所有res拼成 table 存成 CSV,后面画图、写报告、对比不同阈值都能直接用。
5. 避坑排查:联调时最容易翻车的五处
这些坑基本都踩过一遍,写下来备查。
5.1 接口与求解器层面的问题
现象:仿真刚跑 0.002 s 就报 S-Function 错误,或者画面完全不动。原因基本只有一个——CarSim 的 Run Control 还停在 Internal Solver,S-Function 只是把初值传过来,之后两边各自推进,一进循环时间轴就对不上。解决方式是回 CarSim 勾上 Simulink、取消 Internal,点 Send to Simulink 重新生成模块,然后回 Simulink 把旧模块删掉重新拖一个,旧模块缓存了老的端口定义,光改设置不换模块还是会报同样的错。
现象:仿真能跑,但曲线全是锯齿,积分一步就发散。原因是 Simulink 用了变步长求解器,或者定步长设成 0.01 以上。CarSim 的轮胎模型在滑移率突变时刚度很高,步长一大数值积分就炸。解决办法是把求解器锁成 ode4、步长压到 0.001,并且确认 S-Function 模块上的 Sample time 和求解器步长是同一个数——两个数不一致时 Simulink 会自己插值,插出来的值没有物理意义。
现象:制动压力怎么给,车速都不降。十有八九是通道映射错了。CarSim 导入表里配了几个输入,S-Function 端口顺序就变了,如果 Simulink 侧还按老顺序硬连线,压力会打到节气门上。排查方法是在 S-Function 后面挂一个 Display,看每一路输入的实际数值,正常应该是 0 到十几 MPa 的量级,如果某一路上跑的是 0~1 的小数,说明连错了。
5.2 模型与参数层面的问题
现象:TTC 一直是 inf,一次都没触发。除了前面说的作差方向反了,还有一个隐蔽原因是横向过滤太严。弯道上dy会随曲率增大,lane_half_width设成 1.5 m 时,半径 500 m 的弯道跑 150 m 横向偏移就超过阈值了,前车被判定成“不在本车道”。解决是把横向阈值按曲率动态放宽,或者干脆在弯道工况里先关掉横向过滤做对照,确认是过滤逻辑的问题再调。
现象:仿真里刹住了,实车标定时刹不住。这是最贵的一类坑。仿真里制动压力是理想阶跃,实车液压系统有 200~400 ms 的建立延迟,这段时间车还在以原速前进,60 km/h 下就是 5~7 m。解决办法是在决策输出和 CarSim 输入之间串一个一阶惯性环节,时间常数按实测的液压响应曲线拟合,一般 0.15~0.25 s。加上之后仿真出来的制动距离会比之前长,但这才是能拿去和实车对标的数。
现象:同一套参数,不同工况跑出来的最小 TTC 差了一大截。先别怀疑算法,看前车的减速曲线。前车在触发时刻突然减速时,v_rel会在一步之内跳变,TTC 跟着出现尖峰;如果前车是缓慢减速,TTC 曲线就平滑。两种都合理,但放在一张表里比较就没有意义。解决办法是把前车的减速方式也作为一种工况参数显式记录下来,同一种减速方式内部比较,跨方式只看趋势。
6. 二次开发:换车型、加传感器、批量扫参
这套模型跑通之后,改动量最大的三个方向是换车型、加传感器和批量扫参。
换车型不要直接改 CarSim 里的车辆参数,而是把车辆数据集复制一份、改个名,然后在 Simulink 里通过变量指向新的数据集。原因是 CarSim 的车辆参数分散在几十个面板里,直接在原数据集上改,改到一半忘了改过哪些,回头对不上。复制一份、命名带上日期和用途,是最省事的做法。制动系统的压力—力矩曲线要跟着一起换,这条曲线不对,前面标定好的p_max全部作废。
加传感器主要是往前视雷达之外再补一路。常见的是加一个摄像头模型,输出目标的横向位置和宽度,用来做车道内的目标筛选。加的时候注意新传感器的更新周期不要设得比求解器步长还小,否则 Simulink 会在一个步长内多次调用,仿真速度直接掉一个量级。我一般把摄像头周期设成 50 ms、毫米波设成 20 ms,两边做时间对齐后再融合。
批量扫参是最能体现这套仿真价值的地方,一条parfor就能把 TTC 阈值和初始车速的所有组合跑完:
% sweep_aeb.m —— 扫 TTC 全力制动阈值与初始车速的组合 v_list = [30 40 50 60 70]; % 初始车速 km/h tfull_list = [0.5 0.6 0.7 0.8]; % 全力制动 TTC 阈值 s results = []; for i = 1:numel(v_list) for j = 1:numel(tfull_list) % 把参数写进模型工作区,再触发一次仿真 assignin('base', 'V0_KMH', v_list(i)); assignin('base', 'T_FULL', tfull_list(j)); sim('aeb_joint_sim'); % 仿真结束后直接调后处理,结果拼进总表 r = post_aeb('aeb_joint_sim_out.mat', v_list(i)); r.t_full = tfull_list(j); results = [results; r]; end end writetable(struct2table(results), 'aeb_sweep_result.csv');这段脚本的关键是参数注入方式。assignin把变量写进 base 工作区,模型里所有引用V0_KMH、T_FULL的地方会自动取到新值,不需要每次改模型。仿真输出文件名固定,下一轮会覆盖,如果想保留每一轮的数据,文件名里拼上i和j就行,但要注意磁盘占用。真正想并行的时候把外层for换成parfor,前提是每一轮用的模型是独立副本,否则多个 worker 会抢同一个 S-Function 文件。
验证改造有没有成功,我固定看三件事:制动距离随初始车速的变化是不是单调递增、最小 TTC 是否落在设定阈值附近、积分出的速度损失和首尾速度差是否对得上。三条里有一条不成立,先回去查通道和单位,不要急着调算法。
这套模型我前后改过四五版,最深的教训是:每次改完 CarSim 的 I/O 通道,一定要回 Simulink 把 S-Function 模块删掉重拖,而不是只点一下刷新。端口顺序变了不会报错,只会让刹车压力悄悄打到别的地方去,跑出来的曲线看着还挺像那么回事。从那以后我每次改完通道都会先跑一个 3 秒的空载工况,看每一路输入的量级对不对,再开始正式仿真。希望帮到你。
本文还有配套的精品资源,点击获取