首页/目录/全部文章

全部文章

八个专题的源码、算法与协议笔记都在这里。

笔记列表

slam_toolbox 运行模式

04 运行模式

模式子类只改“回调里何时处理、走哪条 Process*”。公共层不变。

4.1 异步建图 AsynchronousSlamToolbox

文件:src/slam_toolbox_async.cpp

laserCallback:取 odom → getLasershouldProcessScan立刻 addScan

  • 匹配/回环慢时,回调堵住 executor,后续扫描由 MessageFilterscan_queue_size(应设 1)丢掉。
  • 不会“越积越多”,所以在线建图用这个。
  • 反序列化拒绝 LOCALIZE_AT_POSE

Launch:launch/online_async_launch.pyasync_slam_toolbox_node + mapper_params_online_async.yaml

4.2 同步建图 SynchronousSlamToolbox

文件:src/slam_toolbox_sync.cpp

laserCallback:同样取 odom / laser / shouldProcess,然后 q_.push(PosedScan)

on_activate 额外起 run()

1
2
3
4
100 Hz 循环
若未暂停 PROCESSING:
出队 → getLaser → addScanImpl
队列 >10:打警告,建议停车等处理完
  • 录包离线、或要求尽量不丢帧时用。
  • 在线跑同步且 CPU 不够,队列会无限涨。
  • 服务 slam_toolbox/clear_queue 清空队列。
  • reset 会先清队列再调基类 reset。

addScan 基类会再锁一次 smapper_mutex_;同步路径直接调 addScanImpl,锁在 run() 里已经拿着。

4.3 定位 LocalizationSlamToolbox

文件:src/slam_toolbox_localization.cpp

on_configure

  • processor_type_ = PROCESS_LOCALIZATION
  • 关交互模式、拆掉 MapSaver(定位不存图)

on_activate 额外:

  • /initialpose(与 AMCL / RViz “2D Pose Estimate” 同接口)
  • 服务 slam_toolbox/clear_localization_buffer

loadPoseGraphByParams:若给了 map_file_name,按 LOCALIZE_AT_POSE 反序列化。map_start_at_dock 在定位里明确不支持(会 warn)。

覆盖的 addScan

  1. 若当前是 LOCALIZATION 且已有 process_near_pose_,先改成 PROCESS_NEAR_REGION(响应 /initialpose
  2. PROCESS_NEAR_REGION:把扫描位姿设成给定 pose,ProcessAgainstNodesNearBy(..., addScanToLocalizationBuffer=true),然后回到 LOCALIZATION,并更新 reprocessing_transform_
  3. PROCESS_LOCALIZATIONProcessLocalization,不更新 reprocessing
  4. 成功则只更新 TF 和 pose dataset_->Add

serialize_map 在定位模式直接报错。反序列化必须是 LOCALIZE_AT_POSE

官方 README 也写了:定位对里程计质量要求高,新手仍可用 AMCL。

4.4 实验性 Lifelong

文件:src/experimental/slam_toolbox_lifelong.cpp

启动会 warn:实验性。思路是继续往图里加节点的同时,按 扫描重叠 IoU 给旧节点打分,低于 lifelong_node_removal_score 的删掉,让计算量有上界。

关键参数:lifelong_minimum_scorelifelong_iou_matchlifelong_node_removal_scorelifelong_overlap_score_scalelifelong_search_use_tree

产品上更稳妥的做法:用普通建图把图画完,再切定位,不要在边缘设备上开 lifelong。

4.5 建图/定位切换

MapAndLocalizationSlamToolbox 继承 Localization,允许运行时在 mapping / localization 之间切。参数 localization_on_configure 决定 configure 时落在哪边。给 Nav2 里“先建图后定位”的同一进程用。

4.6 去中心化多机

decentralized_multirobot_slam_toolbox_node:每台车独立跑一份 toolbox,交换 LocalizedLaserScan,在共享全局系里对齐位姿图。细节见源码 docs/decentralized_multi_robot_slam.md。第一阶段不必碰。

4.7 怎么选

场景 节点
室内车第一次建图 async + mapping YAML
用 bag 离线出高精度图 sync + offline YAML
已有 .posegraph,日常跑 localization + map_file_name
要给 AMCL 一张 pgm 建图时调 save_map,或 map_saver_cli
继续在旧图上补扫 async/sync + deserialize_map(START_AT_GIVEN_POSE)

slam_toolbox Karto 内核

05 Karto 内核

源码:lib/karto_sdk/include/karto_sdk/Mapper.hlib/karto_sdk/src/Mapper.cpp
这是 slam_toolbox 的算法本体。ROS 层只负责把 LocalizedRangeScan 喂进来,再把 CorrectedPose 变成 TF。

5.1 主要对象

类型 角色
LocalizedRangeScan 一帧扫描 + 里程计位姿 + 修正位姿
LaserRangeFinder 角分辨率、量程、相对 base 的 OffsetPose、是否 360°
Mapper 处理入口、参数、持有 Graph / SensorManager / Matcher
MapperGraph 顶点=扫描,边=相对位姿约束
ScanMatcher 相关匹配:粗搜 + 细搜
MapperSensorManager LastScan、RunningScans 滑动窗口
ScanSolver 抽象优化器,实现是 Ceres

SMapperslam_mapper.cpp)只是包一层:configure() 把 YAML 写进 Mapper 的 setter,getOccupancyGrid()OccupancyGrid::CreateFromScans

5.2 Mapper::Process(建图主路径,约 L2731)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
校验 LaserRangeFinder::Validate
未初始化 → Initialize(rangeThreshold)
取该传感器 LastScan
若有上一帧:
T = (odom_last → corrected_last)
本帧 corrected = T * odom_curr // 用上一帧的修正量预测
HasMovedEnough?不够则 false
若 use_scan_matching 且有上一帧:
SequentialScanMatcher::MatchScan(本帧, RunningScans, bestPose, cov)
SetSensorPose(bestPose) // 同时更新 corrected
AddScan(赋 id)
AddVertex + AddEdges(cov) // 与近邻连边
AddRunningScan
若 do_loop_closing:
对每个传感器 TryCloseLoop
SetLastScan
return true

MatchScan 是前端:在局部 running scans 上做相关网格搜索,得到相对里程计更好的 R,t 和协方差。
AddEdges 把相邻约束交给 ScanSolver::AddConstraint(先积累,不算)。
TryCloseLoop 成功才会 CorrectPoses()ScanSolver::Compute()

5.3 回环:MapperGraph::TryCloseLoop(约 L1515)

1
2
3
4
5
6
7
8
9
10
11
12
FindPossibleLoopClosure          // 距离 loop_search_maximum_distance 内的候选链
// 链长 ≥ loop_match_minimum_chain_size
while 还有候选:
粗匹配 LoopScanMatcher::MatchScan(..., coarse=false 细搜关)
粗响应 > loop_match_minimum_response_coarse
且 cov xx/yy < loop_match_maximum_variance_coarse
细匹配 SequentialScanMatcher
细响应 ≥ loop_match_minimum_response_fine
SetSensorPose(bestPose)
LinkChainToScan // 加回环边
CorrectPoses() // 调 solver Compute,写回各节点 pose
FireEndLoopClosure // ROS 层 LoopClosureListener 发 event

误回环会把整图拧断。阈值宁高勿低。关掉 do_loop_closing 可先验证里程计+前端。

ROS 层 LoopClosureListener 挂在 FireEndLoopClosure:发 /slam_toolbox/loop_closure_event,并 requestPoseGraphPublish()。整图只在回环后发,平时用 new_node_event 做增量。

5.4 定位:ProcessLocalization(约 L2883)

前半与 Process 几乎一样(预测、HasMovedEnough、MatchScan、加顶点加边、可回环),最后:

1
AddScanToLocalizationBuffer(scan, vertex)

缓冲区是队列,长度 = scan_buffer_size。超出则:

  • RemoveNodeFromGraph(拆邻边,并从 Ceres RemoveNode / RemoveConstraint
  • delete 旧扫描

所以定位时图上只有“旧地图节点 + 最近 N 个窗口节点”。窗口过期不影响序列化进来的底图。

ClearLocalizationBuffer/initialpose 时调用,窗口清空,LastScan / RunningScans 也清。

5.5 附近重定位:ProcessAgainstNodesNearBy

FindNearByScan 按当前(指定的)位姿找最近顶点,把 RunningScans 重置成该邻域,再 MatchScan。用于:

  • 续建:START_AT_GIVEN_POSE
  • 定位:/initialpose 后的第一帧

5.6 匹配在搜什么

Karto 的 scan matching 是 相关网格搜索,不是 ICP 迭代最近点:

  • 把参考扫描 smearing 成查找表
  • correlation_search_space_dimension × 角度范围内离散搜
  • 粗角度步长 coarse_angle_resolution,细角度 fine_search_angle_offset
  • 响应低于阈值或 use_response_expansion 时扩大搜索窗
  • distance_variance_penalty / angle_variance_penalty:偏离里程计越多惩罚越大

走廊等退化场景响应会变平,匹配可能沿走廊滑。这是前端固有问题,不是 YAML 能彻底消灭的。

5.7 和 ROS 门槛的关系

shouldProcessScan 先滤一遍,HasMovedEnough 再用 minimum_travel_distance / minimum_travel_heading 滤一遍。
默认 shouldProcessScan0.8 * dist²,所以多数时候 ROS 层先挡住。把 check_min_dist_and_heading_precisely 打开后,原地旋转也能过 ROS 层,再交给 Karto 的航向门槛。

slam_toolbox Ceres 求解器

06 Ceres 求解器

源码:solvers/ceres_solver.hppsolvers/ceres_solver.cpp
通过 pluginlib 注册为 karto::ScanSolversolver_plugins.xml)。

G2O / SPA / GTSAM 的代码还在树里,默认不编译。作者对比后认为 Ceres + SPARSE_NORMAL_CHOLESKY + SCHUR_JACOBI 足够。

6.1 插件如何挂上

SlamToolbox::setSolver()

1
2
3
4
solver_plugin 默认 "solver_plugins::CeresSolver"
solver_loader_.createSharedInstance(...)
solver_->Configure(shared_from_this()) // 读 YAML
smapper_->getMapper()->SetScanSolver(solver_.get())

反序列化时 loadSerializedPoseGraphReset → 遍历顶点 AddNode → 遍历边 AddConstraintSetScanSolverCompute 一次,把图落到优化器里。

6.2 问题定义

每个图节点是 2D 位姿 (x, y, θ),存在 nodes_[unique_id] = Eigen::Vector3d
θAngleManifold 参数化,避免角度跳变。

每条边是 LinkInfo:相对位姿 GetPoseDifference() + 协方差。
信息矩阵 = cov.Inverse(),再 llt().matrixU() 得到 sqrt_information,残差是标准 PoseGraph2dErrorTermceres_utils.h)。

第一个节点的 (x,y,θ) 在第一次 Compute 时被 SetParameterBlockConstant,钉住规范,消除刚体自由度。

6.3 何时 Optimize

  • AddNode / AddConstraint:只往 ceres::Problem 里加块,不算
  • TryCloseLoopCorrectPosesScanSolver::Computeceres::Solve
  • 成功则 corrections_ 填所有节点新位姿,Karto 写回 LocalizedRangeScan::SetCorrectedPose

所以日常前端匹配不跑 Ceres,只有回环或加载旧图才跑。这是能在弱 CPU 上建大图的原因之一。

6.4 mode: localization 的含义

YAML 的 mode 主要给 Ceres 看,不是选节点类型。

mode == "localization" 时:Problem::Options.enable_fast_removal = true,方便窗口过期时 RemoveNode / RemoveConstraint。内存大约翻倍。
建图 YAML 应写 mapping,定位 YAML 写 localization。和用哪个可执行文件是两件事,但应对齐。

6.5 推荐与可调项

参数 推荐 说明
ceres_linear_solver SPARSE_NORMAL_CHOLESKY 大图默认;可开 dynamic_sparsity
ceres_preconditioner SCHUR_JACOBI(配置里常见)或 JACOBI 作者 benchmark 偏好 SCHUR_JACOBI
ceres_trust_strategy LEVENBERG_MARQUARDT DOGLEG 也可,LM 资料多
ceres_loss_function None 里程计好时用平方损失;打滑用 HuberLoss

求解容差约 function_tolerance=1e-3(相对 5 cm 栅格是 50 倍)。num_threads=50 只是上限,实际按核数。

6.6 二次开发

换求解器:改 solver_plugin,不要改 Mapper.cpp
真要改残差:动 PoseGraph2dErrorTerm,属于 L5。
ISlam 管理层不要直接碰 Ceres。

slam_toolbox TF、雷达与序列化

07 TF、雷达模型与序列化

7.1 坐标系约定

默认(setParams):

1
2
3
4
map_frame    = map
odom_frame = odom
base_frame = base_footprint
scan_topic = /scan // 必须绝对路径

树:

1
map ──(toolbox 发)──► odom ──(底盘发)──► base_footprint ──(URDF)──► laser

GetPoseHelper 查的是 odom ← base_footprint
LaserAssistant::readLaserPose 查的是 base_footprint ← laser(扫描 stamp)。
getLaser 失败日志:Failed to compute laser pose —— 静态 TF / URDF 没把 laser 接到 base_frame

不要两个节点抢发 map→odom

7.2 雷达模型:LaserAssistant

文件:src/laser_utils.cpp

第一次见到某个 frame_id

  1. readLaserPose()tf->transform(laser恒等, base_frame_)
  2. isInverted:把 base 的 z 轴变到雷达系,变完后 z≤0 视为倒置,后续距离倒序
  3. makeLaser
    • SetOffsetPose(x, y, mountingYaw) —— 只用平面 x,y 和 yaw,忽略雷达相对底盘的 z/pitch/roll(2D 假设)
    • min_laser_range / 消息 range_min 取合理下界
    • max_laser_range 同时是匹配用的 RangeThreshold(超消息 range_max 会警告并截断)
    • 角范围接近 2π 标成 360° 雷达;若多一个点([0,360] 而不是 [0,360))会强制非 360,避免 2021-03 那类坐标系 bug

dataset_->Add(laser) 后,序列化会带上这份雷达模型。换雷达或改外参应重新建图,不要混用旧 .data

7.3 序列化文件

serialization.hpp

文件 内容
<stem>.posegraph Mapper::SaveToFile:图、节点位姿、约束
<stem>.data Dataset::SaveToFile:雷达模型 + 扫描读数

服务 serialize_mapfilename 不要带扩展名
save_map 是另一回事:包 map_saverpgm + yaml,给 AMCL / 显示用,不能拿来 deserialize_map

定位 / 续建必须用 .posegraph + .data

2021-03 ABI

扫描在优化器里的存储坐标系修过一次。360° 或斜装雷达的旧文件,在新代码里可能炸图或翻转。新项目只用新代码建的图。选项见官方 README。

7.4 反序列化后如何对齐

deserializePoseGraphCallback 读文件 → loadSerializedPoseGraph(节点/边灌进 Ceres 并 Compute)→ updateMap → 按 match_typeprocessor_type_

match_type 下一帧
START_AT_FIRST_NODE ProcessAtDock,假定还在起点附近
START_AT_GIVEN_POSE PROCESS_NEAR_REGION + initial_pose(map 系 base)
LOCALIZE_AT_POSE 定位模式 + 附近匹配

YAML 启动加载:map_file_name + (map_start_pose: [x,y,yaw]map_start_at_dock: true)。两者都给时用 pose。

7.5 续建时的 reprocessing_transform_

新旧会话的轮速 odom 原点不同。dock / near-region 对齐成功后,setTransformFromPoses(..., update_reprocessing_transform=true) 估一个齐,之后 getLocalizedRangeScan 先乘它再进 Karto。没对齐就续建,图会在旧图旁边另起一摊。

slam_toolbox 接口、参数与排障

08 接口、参数与排障

8.1 订阅

话题 类型 条件
scan_topic(默认 /scan LaserScan MessageFilter 等到 odom 可变换
TF odom → base_frame 每帧必须有
/initialpose PoseWithCovarianceStamped 仅定位节点

/scan 的 QoS 是 SensorDataQoS(Best Effort)。/mappose 是 Transient Local + Reliable。

8.2 发布

话题 类型 何时
/mapmap_name OccupancyGrid map_update_interval,且有订阅者
/map_metadata MapMetaData 同上
pose PoseWithCovarianceStamped 每成功处理一帧
TF map → odom TF transform_publish_period,且扫描 stamp>0
slam_toolbox/new_node_event NewNodeEvent 建图成功加节点
slam_toolbox/loop_closure_event LoopClosureEvent 回环完成
slam_toolbox/pose_graph PoseGraph 仅回环后(或请求后)

协方差可乘 position_covariance_scale_ / yaw_covariance_scale_,给下游 EKF 降权。

8.3 服务

服务 用途
slam_toolbox/serialize_map .posegraph + .data
slam_toolbox/deserialize_map 读图,设 match_type
slam_toolbox/save_map 出 pgm(MapSaver)
slam_toolbox/dynamic_map 拉当前栅格
slam_toolbox/pause_new_measurements 停吃新扫描
slam_toolbox/reset 清空 Mapper/Solver,可选接着暂停
slam_toolbox/clear_queue 仅同步模式
slam_toolbox/clear_localization_buffer 仅定位
slam_toolbox/toggle_interactive_mode RViz 拖节点
slam_toolbox/manual_loop_closure 提交拖过的节点
slam_toolbox/clear_changes 丢掉未提交的拖动

管理层(未来 ISlam)优先包:serialize / deserialize / save / pause / reset / pose。

8.4 参数分组(调 YAML,不改代码)

坐标系与 IOodom_frame map_frame base_frame scan_topic resolution min/max_laser_range

何时处理throttle_scans minimum_time_interval minimum_travel_distance minimum_travel_heading check_min_dist_and_heading_precisely

前端匹配use_scan_matching correlation_search_space_* fine/coarse_search_angle_* distance/angle_variance_penalty use_response_expansion

回环do_loop_closing loop_search_maximum_distance loop_match_minimum_chain_size loop_match_minimum_response_coarse/fine loop_search_space_*

定位窗口scan_buffer_size scan_buffer_maximum_scan_distance mode: localization

Ceres:见 06-Ceres求解器.md

栅格map_update_interval min_pass_through occupancy_threshold

完整默认值见 config/mapper_params_online_async.yamlSMapper::configure 是参数进入 Karto 的唯一入口。

8.5 日志 → 原因

日志 / 现象 先查
Failed to compute odom pose odom→base_footprint,或 stamp 对不上
Failed to compute laser pose base→laser 静态 TF
回调有、地图不长 shouldProcessScan:车没动够 / 时间间隔 / 前 5 帧
Queue size has grown to 同步模式CPU不够,停车或清队列,或改异步
雷达左右反 isInverted、URDF yaw、扫描角方向
地图抽 两个节点发 map→odom
回环后整图飞 误回环;或 360/斜装 + 旧序列化文件
定位漂、底图不变 正常(窗口在动)
Cannot call serialize map in localization mode 换建图节点再存
/map 不更新 没有订阅者时 updateMap 直接 return

8.6 建议的排查顺序

传感器频率 → 时间戳是否单调、是否同一时钟 → /scan.frame_idview_frames 是否只有一处 map→odom → 静止是否还在爬(里程计)→ 再动回环阈值。

slam_toolbox 读码与二次开发

09 读码顺序与二次开发

9.1 建议读码顺序

不要从 Mapper.cpp 第 1 行读起。按调用链:

  1. launch/online_async_launch.py
    Lifecycle 如何 CONFIGURE/ACTIVATE;默认 YAML。
  2. config/mapper_params_online_async.yaml
    SMapper::configuresetParams 对一下名字。
  3. include/slam_toolbox/slam_toolbox_common.hpp
    把成员表对照 02
  4. src/slam_toolbox_async.cpplaserCallback(约 30 行)
    整条产品链的入口。
  5. shouldProcessScan / getLocalizedRangeScan / addScanImpl / setTransformFromPoses
    src/slam_toolbox_common.cpp
  6. get_pose_helper.hpplaser_utils.cpptoLaserMetadata / isInverted
  7. lib/karto_sdk/src/Mapper.cppProcess(约 2731)和 TryCloseLoop(约 1515)。
  8. solvers/ceres_solver.cppAddNode / AddConstraint / Compute
  9. src/slam_toolbox_localization.cppaddScanlocalizePoseCallback
  10. 需要时再读 lifelong、多机、merge_maps_kinematic、RViz 插件。

9.2 文件速查

想知道 打开
生命周期 slam_toolbox_common.cpp on_*
一帧何时被丢 shouldProcessScan
TF 怎么算 setTransformFromPosespublishTransformLoop
匹配公式在哪 Mapper.cpp ScanMatcher::MatchScan(声明在 Mapper.h
回环阈值 TryCloseLoop + YAML loop_match_*
存图格式 serialization.hpp
参数进 Karto slam_mapper.cpp configure
定位窗口淘汰 AddScanToLocalizationBuffer

9.3 二次开发切面

需求 不要做
换 topic / 坐标系 / 分辨率 YAML 改源码
启停、存图、切图 调 service 继承节点复制一份
ISlam 适配器 映射 serialize/deserialize/pause/reset/pose Mapper.cpp
雷达外参、倒置 URDF + 确认 LaserAssistant 日志 改 Karto 匹配
定位初值 /initialpose 自写全局重定位(除非 AMCL 也不够)
换优化器 solver_plugin 重写 LM
匹配在走廊滑 先查退化、补轮速/IMU;最后才动 ScanMatcher 一上来改相关网格

第一阶段继续:系统包或本树原样跑,用 service 做管理层。

ISlam 建议对应:

1
2
3
4
5
start/stop     → lifecycle 或 launch
save_map → serialize_map(产品图)+ save_map(pgm 可选)
load_map → deserialize_map
getPose → /pose 或查 TF map←base
getState → 看 /map 是否在更、有无 Lost(需自己根据 pose 协方差定义)

9.4 和本仓库其它文档

  • 产品主线:../../docs/SLAM开发知识.md
  • 学习模块 E3:../../docs/学习知识模块.md
  • 较短速览:../../docs/slam_toolbox源码分析.md(指向本目录)

9.5 编译这份源码

步骤、Humble 补丁、overlay 和冒烟见 10-编译与测试.md

系统 apt 已是 2.6.10 时,编 2.10 必须 overlay,不要和系统 launch 混用。

slam_toolbox 编译与测试

10 编译与测试

对应源码:../slam_toolboxros2 分支,包版本 2.10.0)。
本机环境:Ubuntu 22.04 · ROS 2 Humble · CMake 3.22 · Ceres 2.0.0。

官方 ros2 分支默认对着 Jazzy / Ubuntu 24.04 / Ceres ≥ 2.1。在 Humble 上能编过,是本仓库加过兼容层,不是上游原样。系统 apt 仍是 2.6.10(launch 还不是 Lifecycle),编完必须 overlay,不要和 apt 包混用。

10.1 工作空间

colcon 在仓库根目录,不在 third_party/slam_toolbox 里直接 cmake:

1
2
3
4
5
6
slamDev/
├── src/slam_toolbox → ../third_party/slam_toolbox
├── build/ gitignore
├── install/ gitignore
├── log/ gitignore
└── third_party/slam_toolbox/ 源码(含本机 Humble 补丁)

建软链(已做过可跳过):

1
2
3
cd /home/cp/work2/slamDev
mkdir -p src
ln -sfn ../third_party/slam_toolbox src/slam_toolbox

build/ install/ log/ 已在仓库 .gitignore

10.2 依赖

ros-humble-desktop + 本机已装的求解库即可。缺包时:

1
2
3
4
5
6
7
8
9
source /opt/ros/humble/setup.bash
sudo apt update
sudo apt install -y \
libceres-dev libsuitesparse-dev liblapack-dev libomp-dev libtbb-dev \
libboost-serialization-dev libboost-filesystem-dev libboost-thread-dev \
qtbase5-dev libqt5-core5a \
ros-humble-nav2-map-server ros-humble-interactive-markers \
ros-humble-rviz-common ros-humble-rviz-default-plugins \
python3-colcon-common-extensions

或:

1
2
3
cd /home/cp/work2/slamDev
source /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y

核对:

本机当时
ROS Humble(/opt/ros/humble
CMake 3.22.1(不认识 CMP0167
Ceres 2.0.0(没有 autodiff_manifold.h
apt slam_toolbox 2.6.10

10.3 Humble 兼容(本树已改)

2.10 原样在 Humble 上会失败。本仓库改动如下,再编不用手改:

失败点 文件 处理
Policy CMP0167 is not known lib/karto_sdk/CMakeLists.txt if(POLICY CMP0167)SET
tf2_sensor_msgs::tf2_sensor_msgs 不存在 CMakeLists.txt Humble 头文件包,补 INTERFACE target
ceres/autodiff_manifold.h 找不到 solvers/ceres_utils.h Ceres < 2.1 走 LocalParameterization
message_filters::Subscriber 不接 LifecycleNode + SensorDataQoS slam_toolbox_common.* SLAM_TOOLBOX_HUMBLE_COMPATrmw_qos_profile_sensor_data

CMake 在 ROS_DISTRO=humble 时定义 SLAM_TOOLBOX_HUMBLE_COMPAT。换 Jazzy 编同一棵树时不要带这个宏,走上游 API。

10.4 编译

1
2
3
4
5
6
cd /home/cp/work2/slamDev
source /opt/ros/humble/setup.bash
colcon build --packages-select slam_toolbox \
--allow-overriding slam_toolbox \
--symlink-install \
--cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

--allow-overriding 必须加:underlay 里已有 apt 的同名包。
C++17,CMakeLists.txt 默认 Release。首次大约 5 分钟。

只改过某个 .cpp 时同样命令即可,--symlink-install 下 launch / yaml 改完不用重编。

成功时:

1
2
Finished <<< slam_toolbox
Summary: 1 package finished

产物:

1
2
3
4
5
6
7
8
install/slam_toolbox/lib/slam_toolbox/
async_slam_toolbox_node
sync_slam_toolbox_node
localization_slam_toolbox_node
lifelong_slam_toolbox_node
map_and_localization_slam_toolbox_node
decentralized_multirobot_slam_toolbox_node
merge_maps_kinematic

库:install/slam_toolbox/lib/libtoolbox_common.solibkartoSlamToolbox.solibceres_solver_plugin.solibSlamToolboxPlugin.so

跳转定义:把 build/slam_toolbox/compile_commands.json 链到源码根(已链过可跳过):

1
2
ln -sfn /home/cp/work2/slamDev/build/slam_toolbox/compile_commands.json \
/home/cp/work2/slamDev/third_party/slam_toolbox/compile_commands.json

IDE / clangd Reload Window。

10.5 环境(overlay)

每个新终端:

1
2
source /opt/ros/humble/setup.bash
source /home/cp/work2/slamDev/install/setup.bash

确认吃到的是 2.10,不是 apt 2.6:

1
2
3
4
5
ros2 pkg prefix slam_toolbox
# 应为 /home/cp/work2/slamDev/install/slam_toolbox
# 若是 /opt/ros/humble,说明没 source overlay

ros2 pkg executables slam_toolbox

10.6 仓库自带测试

CMakeLists.txt 里 gtest 整段注释掉了colcon test --packages-select slam_toolbox 现在几乎无用例。

1
2
3
4
#if(BUILD_TESTING)
# ament_add_gtest(lifelong_metrics_test test/lifelong_metrics_test.cpp)
# ...
#endif()

test/lifelong_metrics_test.cpp 测 lifelong 的包围盒 / IoU,不是建图正确性。要跑:先取消这段注释,再:

1
2
3
4
5
6
cd /home/cp/work2/slamDev
source /opt/ros/humble/setup.bash
colcon build --packages-select slam_toolbox --allow-overriding slam_toolbox \
--cmake-args -DBUILD_TESTING=ON
colcon test --packages-select slam_toolbox
colcon test-result --verbose

test/process_constraints.py 是画「节点约束数」的离线脚本,要先在 publishGraph 里加它注释里的打印,不是 CI。

10.7 冒烟:包与节点

不接雷达,只确认 overlay 和可执行文件:

1
2
3
4
5
6
7
source /opt/ros/humble/setup.bash
source /home/cp/work2/slamDev/install/setup.bash

test "$(ros2 pkg prefix slam_toolbox)" = "/home/cp/work2/slamDev/install/slam_toolbox" && echo "overlay OK"

ros2 run slam_toolbox async_slam_toolbox_node --ros-args --params-file \
$(ros2 pkg prefix slam_toolbox)/share/slam_toolbox/config/mapper_params_online_async.yaml

Lifecycle 节点起来后停在 unconfigured(没有 launch 的 configure/activate)。另开终端:

1
2
3
4
5
source /opt/ros/humble/setup.bash
source /home/cp/work2/slamDev/install/setup.bash
ros2 lifecycle list /slam_toolbox # 或节点实际名
ros2 lifecycle set /slam_toolbox configure
# 无雷达时 activate 可能卡住等 scan/TF,Ctrl+C 结束即可

看到 CeresSolver: Using ...SlamToolbox: ... 日志,说明插件和公共层加载成功。

10.8 冒烟:launch(需要 scan + TF)

2.10 的 launch 是 Lifecycleautostart:=true 时自动 configure → activate。

最低输入:

  • /scansensor_msgs/LaserScan
  • TF odom → base_frame(默认 base_footprint,按 YAML 改)
  • 雷达相对 base 的 tf_static

仿真 / bag 记得 use_sim_time:=true,并先播 /clock

1
2
3
4
5
6
7
8
9
source /opt/ros/humble/setup.bash
source /home/cp/work2/slamDev/install/setup.bash

# 另开:ros2 bag play <bag> --clock
# 或:机器人驱动 + 雷达

ros2 launch slam_toolbox online_async_launch.py \
use_sim_time:=true \
autostart:=true

其它入口:

launch 节点 场景
online_async_launch.py async_slam_toolbox_node 在线建图(先跑这个)
online_sync_launch.py sync_slam_toolbox_node bag 回放不丢帧
localization_launch.py localization_slam_toolbox_node 已有 .posegraph
offline_launch.py sync + 离线 YAML 离线建图
merge_maps_kinematic_launch.py merge_maps_kinematic 刚体拼图,不是匹配

通过标准(async 建图):

  1. ros2 topic hz /map 有输出(有订阅者时才更新,RViz 订 /map
  2. ros2 topic echo /tf --once 里有 map → odom
  3. ros2 topic echo /pose --once 有位姿
  4. 转一圈墙角大致对齐,没有整图拧成麻花(回环阈值另见 08

存图:

1
2
3
4
5
6
7
ros2 service call /slam_toolbox/serialize_map slam_toolbox/srv/SerializePoseGraph \
"{filename: '/tmp/test_map'}"
# 生成 /tmp/test_map.posegraph 和 /tmp/test_map.data

ros2 service call /slam_toolbox/save_map slam_toolbox/srv/SaveMap \
"{name: {data: '/tmp/test_map'}}"
# 栅格,给 Nav2;和上面的位姿图不是一回事

定位模式:YAML 里填 map_file_name,或启动后再 deserialize_map。RViz 2D Pose Estimate/initialpose

10.9 对照实验(建议新手做)

改一个量,看图怎么变;比空跑 gtest 有用:

动作 预期
use_scan_matching: false 图跟着里程计漂
do_loop_closing: false 绕回起点对不齐
async 换 sync + bag 不丢帧,CPU 可能更高
不播 odom→base MessageFilter 一直等,不加节点
不 overlay,直接用 apt launch 2.6 行为,和这份源码对不上

10.10 编译失败对照

现象 原因 处理
提示加 --allow-overriding slam_toolbox apt 已占包名 加上该参数
CMP0167 is not known CMake < 3.30 确认 karto 的 if(POLICY CMP0167) 还在
tf2_sensor_msgs::... was not found Humble 无该 target 确认 CMakeLists.txt 里 INTERFACE 补丁
autodiff_manifold.h Ceres 2.0 确认 ceres_utils.h 的版本分支
Subscriber / SensorDataQoS 编译错 没带 Humble 宏 echo $ROS_DISTRO 为 humble 后再 colcon
ros2 pkg prefix 仍是 /opt/ros/humble 没 source install 先 humble 再 overlay
和系统节点抢 /mapmap→odom 同时开了 apt 节点 只留 overlay 这一份
SSH 不能跳转到定义 compile_commands.json 见 10.4,Reload Window

清掉重编:

1
2
cd /home/cp/work2/slamDev
rm -rf build/slam_toolbox install/slam_toolbox log

不要 rm -rf build 整个目录,除非没有其它包。

10.11 和二次开发的关系

  • 改 YAML / launch:--symlink-install 后直接 relaunch。
  • src/solvers/karto_sdk:按 10.4 重编,再 overlay。
  • 上实车若只用 apt 2.6.10:不要用 2.10 的 Lifecycle launch 去喂系统包。
  • 读码顺序仍见 09。编译只保证「能跑」,不代替沿 Process() 走一帧。

SLUB 小对象分配机制与实现详解

SLUB 小对象分配机制与实现详解

源码路径rk3588/kernel-6.1/mm/slub.cslab_common.c
内核版本:Linux 6.1(RK3588,通常 CONFIG_SLUB=y
文档目录linuxDoc/mm/


目录


一、原理

1.1 为什么需要 slab

内核大量 固定大小小对象task_structinodesk_buff 头等),若每对象一次 __alloc_pages(0)

  • 浪费内存(页内碎片)
  • 增加 buddy 锁竞争

Slab 分配器页为 slab,在页内切分等长 object,按 cache 复用。

1.2 SLUB 设计思想

SLUB(Six Less Unfair Buddy)相对 SLAB/SLOB:

  • per-CPU slab 指针:热路径无中心锁
  • 合并 partial slab 管理:减少 per-node 链表遍历
  • freelist 在 object 内嵌指针:减少元数据

RK3588 defconfig 一般启用 CONFIG_SLUB,禁用 SLAB/SLOB。

1.3 三级存储层次

kmem_cache_alloc 热路径CPU slab + freelistpartial slab 链表 nodenew_slab → __alloc_pages

1.4 kmalloc

kmalloc(size, gfp) 根据 size 索引到预建 kmalloc-* cache(slab_common.ccreate_kmalloc_caches),本质仍是 kmem_cache_alloc


二、实现方式

2.1 核心对象

结构 说明
struct kmem_cache 对象大小、对齐、构造函数、flags
struct slab 一页或多页容器,freelistinusefrozen
struct kmem_cache_cpu per-CPU:slabfreelisttid
struct kmem_cache_node per-node partial/full 链表 + list_lock

2.2 分配入口

1
2
3
4
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
return __kmem_cache_alloc_lru(s, NULL, gfpflags);
}

slab_alloc()slab_alloc_node()___slab_alloc()

2.3 快路径(inlined)

slub.cslab_alloc_node 内联路径:

  1. cpu_slab = slub_get_cpu_ptr(s->cpu_slab)
  2. c->slab 存在且 c->freelist 非空:直接从 freelist 弹对象get_freepointer
  3. 使用 tid 检测 CPU 迁移,不一致则走慢路径

2.4 慢路径:___slab_alloc

  1. get_partial():从 node partial 链表取 slab
  2. 若无:new_slab()allocate_slab()alloc_pages(order 由 oo/min_partial 决定)
  3. 新 slab 标记 frozen,填入 CPU slab
  4. debug 模式走 alloc_single_from_new_slab 与 red zone

2.5 释放:kmem_cache_free

  • 快路径:对象放回 CPU freelist
  • __slab_free:慢路径,可能将 slab 合并到 partial、或 discard_slab 归还 buddy
  • deactivate_slab:flush 当前 CPU slab 到 node 链表

2.6 slab_common.c 引导

函数 时机
create_boot_cache 极早期创建 task_struct 等 cache
kmem_cache_init 建立基本 cache
kmem_cache_init_late 完整 kmalloc cache 表
create_kmalloc_caches 按 size 分档

2.7 调试与硬化

CONFIG 目录/行为
SLUB_DEBUG redzone、poison
KASAN kasan/ 插桩
KFENCE kfence/ 采样检测
FAILSLAB failslab.c 故障注入

三、关键数据结构与接口

API 说明
kmem_cache_create 创建 cache
kmem_cache_alloc 分配对象
kmem_cache_free 释放对象
kmem_cache_alloc_node 指定 NUMA node
kmem_cache_alloc_bulk 批量分配
kmalloc / kfree 通用大小分配

锁顺序slub.c 文件头):slab_mutexnode->list_lockcpu_slab->lock


四、与页分配器关系

  • SLUB 不替代 buddy;slab 页来自 alloc_pages(gfp, order)
  • 若 buddy 失败,SLUB 返回 NULL,调用方处理;可能间接触发 OOM(见 08 文档)。
  • __GFP_COMP:folio/slub 大对象可能使用复合页。

五、调试与观测

手段 说明
/proc/slabinfo 各 cache 对象数、大小
/sys/kernel/slab/ sysfs 详情
slab_nomerge 启动参数 禁止合并 cache

附录:源码索引

主题 文件 约略行号
kmem_cache_alloc slub.c 3420
___slab_alloc slub.c 3100–3260
__slab_free slub.c 3459
kmem_cache_init slub.c 4818
kmalloc caches slab_common.c 869

虚拟内存与缺页机制与实现详解

虚拟内存与缺页机制与实现详解

源码路径rk3588/kernel-6.1/mm/memory.cmmap.cmprotect.cmremap.cmlock.cmadvise.c
内核版本:Linux 6.1(RK3588 / ARM64)
文档目录linuxDoc/mm/


目录


一、原理

1.1 虚拟地址空间

每个用户进程有 struct mm_struct,包含:

  • 页表根(pgd
  • VMA 链表 / maple treevm_area_struct):每段连续虚拟区间的权限、文件映射、堆栈属性
  • mmap_lock:保护 VMA 与页表并发

CPU 通过 MMU 将虚拟地址译为物理地址;页表项无效或访问违例触发 缺页异常

1.2 按需分配(Demand Paging)

匿名映射、文件映射通常 先建立 VMA,不立即分配物理页;首次访问触发 fault:

  • 匿名:分配零页或 COW 副本
  • 文件:从 page cache 读入或新建 cache 页
  • swap:从 swap 读回

1.3 写时复制(COW)

fork() 后父子共享只读 PTE;任一方写入触发 写保护 faultdo_wp_page() 分配新物理页并更新 PTE。

1.4 大页与 THP

若 VMA 允许且策略开启,可在 PMD/PUD 层安装 透明大页create_huge_pmd 等),减少 TLB miss。


二、实现方式

2.1 mmap:do_mmap()

mmap.cdo_mmap()(~1253 行):

  1. get_unmapped_area() 找空洞
  2. vm_mmap_pgoff()do_mmap_pgoff
  3. 创建 vm_area_struct,设置 vm_opsvm_flagsVM_READ/WRITE/EXECVM_SHARED 等)
  4. 文件映射调用 file->f_op->mmapgeneric_file_mmap
  5. 插入 maple tree / 链表,必要时 userfaultfd 通知

munmap / expand_stack / brk 在同文件修改或删除 VMA。

2.2 缺页总入口

架构层(arch/arm64/mm/fault.c)解析异常后调用:

handle_mm_fault(vma, address, flags, regs)__handle_mm_fault()

1
2
3
4
5
6
7
8
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
// 初始化 vm_fault vmf
pgd → p4d → pud → pmd 逐级分配/检查
// THP: create_huge_pud / wp_huge_pud / huge_pmd_set_accessed
return handle_pte_fault(&vmf);
}

2.3 PTE 级:handle_pte_fault()

根据 PTE 状态分支(同文件):

情况 处理
PTE 不存在 do_anonymous_page / 文件 fault
PTE 仅读 + 写访问 do_wp_page(COW)
swap PTE do_swap_page
NUMA hint fault do_numa_page
设备/特殊 vm_ops->fault

返回 vm_fault_tVM_FAULT_OKVM_FAULT_RETRY(可能释放 mmap_lock)、VM_FAULT_OOMVM_FAULT_SIGSEGV 等。

2.4 匿名页:do_anonymous_page

  • alloc_zeroed_user_highpage_movable 或类似分配
  • alloc_set_pte 安装 PTE
  • memcg charge(若启用)

2.5 写保护:do_wp_page

  • 若仍共享:分配新页,copy_page / copy_present_page
  • 更新 PTE 为可写
  • userfaultfdKSMTHP 有交叉分支

2.6 相关系统调用实现

文件 功能
mprotect.c 修改 VMA 权限,刷新 PTE 权限位
mremap.c 移动/扩展映射,可能 move_page_tables
mlock.c mlock 使页常驻,munlock 恢复
madvise.c MADV_FREE/DONTNEED/HUGEPAGE
mincore.c 查询页是否在内存
msync.c 映射区与文件同步

2.7 mmu_gather

批量 unmap/munmap 时用 struct mmu_gathermmu_gather.c)累积 TLB flush,减少 IPI。


三、关键数据结构与接口

结构 说明
struct mm_struct 进程地址空间
struct vm_area_struct 虚拟区段
struct vm_fault fault 上下文(vma、addr、pgoff、gfp)
handle_mm_fault 缺页主入口
do_mmap 建立映射

mmap_read_lock / mmap_write_lock;fault 中可能 VM_FAULT_RETRY 释放锁后重试。


四、RK3588 平台说明

  • ARM64 使用 4 级页表(PGD/P4D/PUD/PMD/PTE,P4D 可能折叠)。
  • 编解码进程大量 匿名映射 + mmap(dma-buf fd):文件类 fault 在 dmabuf/file 路径,匿名堆栈在 do_anonymous_page
  • MADV_DONTNEED 可释放冷数据缓解内存压力(与 07 回收协同)。

五、调试与观测

手段 说明
/proc/<pid>/maps VMA 列表
show_vma / procfs 内核打印 VMA
handle_mm_fault trace mm tracepoints
userfaultfd 用户态处理 fault

附录:源码索引

主题 文件 约略行号
__handle_mm_fault memory.c 5079
handle_mm_fault memory.c 5262
do_mmap mmap.c 1253
mmu_gather mmu_gather.c 全文

页缓存与写回机制与实现详解

页缓存与写回机制与实现详解

源码路径rk3588/kernel-6.1/mm/filemap.creadahead.cpage-writeback.cbacking-dev.ctruncate.c
内核版本:Linux 6.1(RK3588)
文档目录linuxDoc/mm/


目录


一、原理

1.1 Page Cache 角色

页缓存缓存 文件数据 的物理页,使:

  • 读:优先内存命中,减少块设备 I/O
  • 写:先改缓存页,异步 写回(writeback) 到存储
  • mmap:文件映射直接 fault 到 cache 中的 folio

同一文件 inode 对应 struct address_space,其 i_pages(xarray)索引 offset → folio。

1.2 Folio(Linux 6.1)

folio 表示一个或多个连续 page 的缓存单元(尤其 THP、大块 I/O),filemap.c 路径以 folio 为中心,legacy API 经 folio-compat.c 适配。

1.3 预读(Readahead)

顺序读检测 访问模式,提前将后续页读入 cache,隐藏磁盘延迟。

1.4 写回与脏页

修改 mapped 或 write() 路径使页 置脏pdflush/bdi writeback 线程在脏页比例或 sync 时写回设备。

backing_dev 限制每设备的 inflight 写与脏页占比,防止内存被脏页占满。


二、实现方式

2.1 文件映射缺页:filemap_fault()

filemap.c ~3191:

  1. 根据 vmf->pgoffmapping->i_pages 查找 folio
  2. 未命中:pagecache_get_read / filemap_read_folio 读盘
  3. 可能 lock_page,返回 VM_FAULT_RETRY
  4. 安装 PTE,folio_mark_accessed

__handle_mm_fault 中文件 VMA 的 vm_ops->fault 衔接。

2.2 缓冲读:filemap_read / generic_file_read_iter

  • 按 offset 查找或分配 cache folio
  • 未 uptodate 则提交 bio 读盘
  • 拷贝到用户 iov

2.3 预读:readahead.c

  • page_cache_sync_readahead / ondemand_readahead
  • 根据 file->f_rastruct file_ra_state)维护预读窗口
  • readahead_expand 扩大窗口;force_page_cache_ra 强制预读

2.4 脏页与写回:page-writeback.c

步骤 说明
置脏 folio_mark_dirty__mark_inode_dirty
聚合 inode 挂到 bdi->wb 链表
写回 wb_writeback / write_cache_pages
完成 folio_end_writeback,清除 dirty

balance_dirty_pages:进程写超过 dirty_ratio 时节流。

2.5 backing-dev.c

每个块设备/文件系统注册 struct backing_dev_info

  • max_ratiodirty_bandwidth
  • cgroup writeback 关联(CONFIG_CGROUP_WRITEBACK

2.6 truncate.c

文件缩小或删除时 truncate_inode_pages:从 xarray 摘除 folio,若映射存在则通过 rmap unmap(见 06 文档)。

2.7 workingset 交互

频繁回收的 cache 页 refault 由 workingset.c 统计,影响 vmscan 对文件页的回收积极性(见 07 文档)。


三、关键数据结构与接口

结构/API 说明
struct address_space i_pages、host inode、a_ops
filemap_fault mmap 读 fault
filemap_read 缓冲读
folio_mark_dirty 标脏
sync(2) / fsync 触发写回

a_ops 常用read_foliowritepagesdirty_foliomigrate_folio


四、RK3588 平台说明

  • 编解码 读配置文件/码流 走 page cache;大文件顺序读可调 fadvise(SEQ)fadvise.c)。
  • dmabuf / GEM 不一定经过传统 page cache,但 VFS 层媒体仍可能用 shmem(见 12 文档)。
  • eMMC/UFS 慢时 dirty 内存高 会触发回收与写回竞争,表现为编码卡顿与 NR_WRITEBACK 升高。

五、调试与观测

手段 说明
/proc/meminfo Cached/Dirty/Writeback 缓存与写回量
echo w > /proc/sys/vm/drop_caches 丢弃 cache(调试用)
fiemap / sync 写回行为

trace:filemap tracepoints。


附录:源码索引

主题 文件 约略行号
filemap_fault filemap.c 3191
readahead readahead.c 全文
writeback page-writeback.c 全文
backing dev backing-dev.c 全文