简介:基于激光雷达的反光柱三边定位算法实现,以机器人操作系统功能包形式提供,适合移动机器人定位导航方向的技术开发者与学习者用于工程落地参考。压缩包内部按功能拆为两个子包:一个是反光柱定位主功能包,承担三边定位核心流程;另一个是反光柱管理节点功能包,负责柱位信息管理;两个包均预留接口适配层,便于改造后快速融入已有导航工程。代码已经过优化和重构,虽与对应系列博文源码存在差异,但基本定位思路一脉相承,可对照学习算法从原理到代码的转化过程。资源共18个文件,其中7个头文件与2个源程序文件构成主要逻辑,3个文本文件提供数据或说明,2个启动文件用于运行节点,另有2个配置文件和2个参数配置文件保存运行参数,整包仅24KB,结构简洁。目前已有419人学习下载,从中既能了解三边定位的数据读取、柱位计算、节点管理如何拆分,也能复用功能包的组织方式和接口适配方法,减少自行搭建成本。
1. 反射式列定位:让移动机器人准确停靠第几根柱子的低成本方案
仓库里那台沿货架通道跑的 AGV,最怕的不是走不直,而是不知道自己停在了第几列。反光贴纸贴在立柱上,反射式光电传感器从车侧扫过,一个脉冲就是一根柱子,这就是 Reflective-column-positioning 这个工程解决的核心问题。
它不依赖视觉,不需要二维码,只要一对反射传感器加一卷反光贴纸,就能在已有 PLC 和基础传感的改造项目里落地。成本低、响应快、维护简单,这是它在仓储、巡检这类场景里一直没被淘汰的原因。
这篇笔记从拆包讲起,经过数据流、核心算法、现场调参,最后落到多传感器冗余和自动校准。新手可以从日志回放跑通,熟手直接跳到避坑章对号入座。下面进入正题。
2. 拆开 Reflective-column-positioning.zip:目录结构、数据流与最小可跑通命令
拿到压缩包先别急着找主程序,先把这它当作一个带注释的现场调试箱来看。这个工程方向我在类似项目里拆过三次,每次都是先从目录树推断传感器型号和采样方式,再对着日志确认信号形态,最后才动算法。压缩包内部结构不算复杂,但模块边界很清晰,单片机侧负责实时性,PC 侧负责离线分析,两侧通过日志文件对接。
2.1 压缩包目录长什么样:每个模块的职责
先看整体目录结构:
reflective-column-positioning/ ├── config/ │ ├── sensor.yaml # 传感器型号、检测距离、贴纸反射率 │ ├── controller.yaml # 采样率、去抖参数、通信端口 │ └── map.yaml # 列间距、贴纸宽度、容差参数 ├── src/ │ ├── signal_filter.c # 输入信号去抖与脉冲整形(单片机侧) │ ├── column_counter.c # 列计数状态机 │ ├── odom_fusion.c # 编码器里程计与列计数的融合 │ └── debug_dump.c # 调试日志落盘 ├── tools/ │ ├── replay.py # 离线回放日志,验证参数调整 │ ├── visualize.py # 把日志画成波形图 │ └── calibrate.py # 自动校准列间距参数 ├── logs/ │ └── demo.log # 一趟完整跑行的原始日志 └── README.md # 构建与接线说明config 下三个配置文件分工不同。sensor.yaml 描述传感器本身的安装参数,包括检测距离、安装高度、贴纸反射率;controller.yaml 描述采样与通信参数,采样率、去抖次数、串口波特率都在这里;map.yaml 描述场地语义,列间距、贴纸宽度、容差系数。三类参数分开,是因为变化频率完全不同——传感器换型号一年可能一次,列间距因为货架调整可能一个月变一次,去抖参数则可能一次调试就要改好几轮。
src 下四个 C 文件构成实时处理链路。signal_filter.c 负责输入信号去抖,column_counter.c 负责列计数状态机,odom_fusion.c 负责把编码器里程计和列计数融合,debug_dump.c 负责把每一步处理结果按固定格式写入日志。tools 下三个 Python 工具负责离线工作:replay 回放验证、visualize 可视化波形、calibrate 自动标定列间距。logs 目录里附带的 demo.log 是一趟完整跑行记录,里面包含大约 40 根柱子的触发序列。
这里有个工程习惯值得专门提一下:实车上的 C 代码和 PC 侧的 Python 工具共享同一份 YAML 配置格式。这样回放时用的参数和实车完全一致,绝对不会出现「实车一套参数、分析另一套参数」的错位。如果看到某个工程回放结果和实车不一致,先查配置文件是不是被改成了两份。
各文件职责可以归纳成一张表,方便新人快速定位修改点:
| 文件 | 运行侧 | 职责 | 关键输入 | 关键输出 |
|---|---|---|---|---|
| signal_filter.c | 单片机 | 电平去抖、边沿识别 | 原始电平 | 稳定边沿事件 |
| column_counter.c | 单片机 | 列计数状态机 | 边沿事件 | 当前列号 |
| odom_fusion.c | 单片机 | 里程计融合与置信度 | 列号+里程计 | 置信度标记 |
| debug_dump.c | 单片机 | 格式化日志落盘 | 全链路状态 | demo.log |
| replay.py | PC | 离线回放 | 日志+配置 | 列号序列、置信度 |
| calibrate.py | PC | 列间距自动标定 | 历史日志 | 更新后的 map.yaml |
2.2 数据流四段式拆解:从电平到列号
整个系统的数据流可以拆成四段,每一段解决一类问题。第一段是传感器原始电平,由反射式光电传感器输出开关量。常用的是 NPN 常开型,检测到反光贴纸时输出低电平,没有检测到时输出高电平。这个电平不能直接用——贴纸边缘的漫反射过渡区会让信号在几百微秒内产生一串抖动,直接数上升沿会数出一堆假列号。
第二段是去抖整形。单片机定时器以 1kHz 周期采样这个电平,连续 8 次采样结果一致才认为电平真正变化了。这样滤掉的是传感器自身响应抖动和外部电磁干扰带来的毛刺,保留下的是真实贴纸产生的宽脉冲。贴纸越宽、车速越慢,脉冲就越宽,去抖参数越不容易误伤真信号。
第三段是列计数。一个完整的「进入贴纸 → 离开贴纸」过程对应一个脉冲,状态机在脉冲的上升沿把列号加一。这一段的难点不是加一本身,而是当贴纸缺失或传感器被遮挡时,状态机能不能根据里程计自动补一列,补的时候还要把置信度降低,方便运维排查。
第四段是里程计融合。编码器里程计给出车身走过的距离,除以配置里的列间距,得到「按里程应该到第几列」的估计值。传感器计数值和里程计估计值一对偏差,越小置信度越高。这个置信度不只是给人看的,后续逻辑还会用它决定是否把这一列的触发当作有效定位点。
四段链路各自的时钟精度要求不一样。传感器采样用定时器中断保证固定周期,里程计用编码器脉冲中断累加,时间戳统一换算成毫秒。采样周期抖动超过 ±0.5ms,边沿时间就不准,融合时的里程-列号对齐会出现细微偏差,跑久了容易触发误判。这也是日志里每一行都带精确到毫秒时间戳的原因。
2.3 最小跑通命令与日志回放
先把三个配置文件看一遍,里面重点参数如下:
# config/sensor.yaml sensor: type: diffuse_reflective # 漫反射式传感器 pnp_npn: npn # NPN 常开,触发为低电平 response_time_ms: 1.0 # 传感器本身响应时间 install_height_cm: 45 # 光束距地面高度 baseline_enable: true # 动态基线,抗环境反光 # config/controller.yaml controller: sample_rate_hz: 1000 # 定时器采样频率 debounce_samples: 8 # 去抖连续采样次数 comm_port: /dev/ttyUSB0 # 日志读取串口设备 baudrate: 115200 log_enable: true # config/map.yaml map: column_spacing_m: 2.4 # 相邻立柱设计间距 patch_width_cm: 12 # 反光贴纸宽度 drift_tolerance_per_m: 0.005 # 每米累计误差容差sample_rate_hz 和 debounce_samples 决定了一个边沿的确认耗时。1000Hz 采样率下,8 次采样就是 8ms。如果贴纸宽度 12cm、车速 1m/s,光束扫过贴纸的持续时间约 120ms,远大于 8ms,所以去抖不会吞掉真实信号。反过来,如果是变频器噪声产生的 3ms 窄脉冲,去抖只会采到 3 个点,达不到 8,就被滤掉了。这两个参数要配合着改,只调一个意义不大。
日志回放是最快的验证方式:
cd Reflective-column-positioning python tools/replay.py \ --log logs/demo.log \ --config config/controller.yaml \ --map-config config/map.yaml \ --plot--plot 会用字符画把每根柱子的脉冲波形和列号标尺打印到终端。跑完输出每根柱子的触发时间、列号、置信度。理想情况下,40 根柱子全部列号连续、置信度都在 0.9 以上。如果回放结果里出现列号跳变且置信度低于 0.7,不要先怀疑代码,先查 map.yaml 里 column_spacing_m 是不是和现场实际间距一致。我见过不少翻车案例,一上来就改去抖参数,最后发现是间距配错——间距差 20cm,后面全乱。
提示:replay 工具只做算法验证,它读的是离线日志,不改变实车行为。回放参数调到全绿之后,必须上实车再跑一圈确认。
3. 核心算法拆解:反射脉冲如何变成可靠的列号
这个工程最值得读的代码不是列计数本身,而是它怎么在信号有毛刺、车速有波动、贴纸有缺陷的条件下保持计数不丢不重。三个关键模块分别对应:信号去抖、状态机计数、里程计融合置信度。下面逐个拆,代码都可以直接抄到自己的工程里改。
3.1 去抖与脉冲整形:贴纸边缘处理的第一道防线
传感器原始输出直接接单片机 GPI0。外部干扰来自三个方面:一是贴纸边缘漫反射过渡区,二是变频器产生的电磁噪声,三是通道里车辆经过扬起的灰尘对光束的散射。这些干扰表现为几微秒到几毫秒的窄脉冲,直接数边沿会数出很多假列号。
去抖的核心实现,C 语言版本如下:
#define DEBOUNCE_SAMPLES 8 #define SAMPLE_INTERVAL_US 200 static uint8_t stable_level = 0; static uint8_t pending_level = 0; static uint8_t count = 0; static uint32_t sample_ts = 0; void debounce_update(uint8_t raw_level, uint32_t now_us) { if (sample_ts == 0) { sample_ts = now_us; pending_level = raw_level; return; } // 固定采样间隔,间隔不足直接丢弃该次采样 if (now_us - sample_ts < SAMPLE_INTERVAL_US) return; sample_ts = now_us; if (raw_level == pending_level) { if (++count >= DEBOUNCE_SAMPLES) { if (stable_level != pending_level) { stable_level = pending_level; on_edge(stable_level, now_us); } count = 0; } } else { pending_level = raw_level; count = 0; } }这段代码的核心是「连续 N 次一致才翻转」。DEBOUNCE_SAMPLES=8、间隔 200us,意味着一个边沿需要 1.6ms 才能被确认。注意这里 1.6ms 是确认边沿的时间,和采样率 1kHz 的 8ms 是两种不同实现——如果你按照 1kHz 采样,去抖参数参考 controller.yaml 里的值;如果按这段 5kHz 采样的代码,参数对应 DEBOUNCE_SAMPLES=8。两者都能用,但要保持一致,不要把两套参数混在一起改。
去抖参数怎么调,给一个经验区间:
| 场景 | 建议 debounce_samples | 说明 |
|---|---|---|
| 低速 AGV,车速 ≤0.5m/s | 16 | 贴纸脉冲宽度大,去抖可以更激进 |
| 一般仓储,车速 1.0m/s 左右 | 8 | 默认值,兼顾毛刺滤除和响应 |
| 高速巡检,车速 ≥2m/s | 4 | 贴纸扫过时间缩短,去抖太大会吞信号 |
| 强电磁干扰现场 | 16 且传感器输出加屏蔽 | 优先滤除噪声,代价是响应变慢 |
去抖太激进的表现是偶尔漏掉窄脉冲;去抖太弱的表现是计数时多时少,但单次跑行很难看出规律。这两种情况在回放日志里都能通过脉冲宽度分布判断——漏检时实际触发的脉冲宽度普遍偏窄,误检时会出现宽度远小于贴纸理论宽度的窄脉冲。visualize.py 能把脉冲宽度直方图画出来,这个图比看列号序列更直观。
3.2 列计数状态机与丢补逻辑
状态机本身很简单,四态:WAIT_RISING 等待上升沿、IN_PATCH 贴纸内、WAIT_RISING 离开贴纸、WAIT_PREDICTED 预测后等待对齐。前两态对应真实贴纸的进入和离开,第三态是里程计预测补列后的特殊状态。
状态转移表:
| 当前状态 | 事件 | 动作 | 下一个状态 |
|---|---|---|---|
| WAIT_RISING | 上升沿 | 列号加一,记录触发里程 | IN_PATCH |
| IN_PATCH | 下降沿 | 记录当前里程 | WAIT_RISING |
| WAIT_RISING | 里程超差 | 列号加一,标记低置信 | WAIT_PREDICTED |
| WAIT_PREDICTED | 上升沿 | 对齐列号,不加一 | IN_PATCH |
| WAIT_PREDICTED | 里程超差 | 仍然没触发,继续等待 | WAIT_PREDICTED |
核心逻辑用 Python 写出来更清晰:
class ColumnCounter: def __init__(self, config): self.state = 'WAIT_RISING' self.column = 0 self.last_trigger_odom = 0.0 self.spacing = config['map']['column_spacing_m'] self.tol = config['map']['drift_tolerance_per_m'] def on_edge(self, level, odom_m): if level == 1 and self.state == 'WAIT_RISING': # 真实上升沿,列号加一 self.column += 1 self.state = 'IN_PATCH' self.last_trigger_odom = odom_m elif level == 0 and self.state == 'IN_PATCH': self.state = 'WAIT_RISING' elif level == 1 and self.state == 'WAIT_PREDICTED': # 预测后的真实触发到达,只对齐列号,不加一 self.state = 'IN_PATCH' self.last_trigger_odom = odom_m def on_odom(self, odom_m): if self.state != 'WAIT_RISING': return expected_next = self.last_trigger_odom + self.spacing # 超过预计位置加容差还没触发,补一列 if odom_m - expected_next > self.tol * (odom_m / self.spacing + 1): self.column += 1 self.state = 'WAIT_PREDICTED' self.last_trigger_odom = expected_next丢补逻辑的关键在最后一行的容差设计。固定容差 0.3m 在现场不够用——贴纸手工粘贴有累积误差,跑到第 20 列时误差可能已经到 0.25m,固定容差很容易触发误补。代码里的tol * (odom_m / spacing + 1)把容差设计成随跑行距离线性增长,跑到 60m 时容差约 0.32m,跑到 100m 时容差约 0.51m。这样既允许贴纸安装误差累积,又不至于在正常行驶时乱补。
还有一个细节容易被忽略:补列只用 last_trigger_odom 作为基准,不用当前 odom 直接减。这样做的原因是补列后如果真实贴纸紧接着就来了,两列之间的里程能保持相对准确,不会因为一次补列把后续所有间距计算都带偏。补列后真实触发到达时,逻辑只对齐列号不加一。这个分支漏掉的话,漏一次之后每过一根真实贴纸都会多算一列,越错越远。
WAIT_PREDICTED 状态出现时,置信度要降级。对应到日志里就是这条记录:
[PREDICT] t=48.20s odom=48.35m col=21 conf=0.62conf=0.62 意味着这一列不是实测触发,是里程计推算出来的。运维看到 PREDICT 记录时要警惕,连续两条以上 PREDICT 说明传感器或贴纸出了问题,需要下车检查。
3.3 里程计融合与置信度计算
列计数给出的是传感器视角的列号,里程计给出的是几何视角的列号。融合的价值在于互相校验。传感器视角可能因为漏检丢列,里程计视角可能因为车轮打滑累积误差,两者融合后,单个来源的短时错误可以被另一个来源纠正。
置信度计算的 Python 实现:
def compute_confidence(sensor_col, odom_col): diff = abs(sensor_col - odom_col) if diff < 0.2: return 1.0 elif diff < 0.5: return max(0.0, 1.0 - (diff - 0.2) / 0.3) else: return 0.0参数含义:偏差小于 0.2 列视为完全可信;偏差在 0.2~0.5 列之间置信度线性下降;超过 0.5 列置信度归零。这里的「列」不是整数,是把里程转换成列号后的浮点数。比如里程显示当前在第 12.3 列,传感器计到第 12 列,偏差 0.3,置信度约 0.67。
置信度归零不等于停机。工程上更稳妥的做法是:置信度低于 0.3 时,只标记 WARN 并把当前列号锁定为传感器值,等下一次触发再重新对齐。直接停机会让 AGV 卡在通道中间,对效率的伤害远大于计数错一列。锁定策略在 odom_fusion.c 里通过 lock_until_next_edge 开关控制。
融合前还要对速度做一阶低通滤波,避免单次编码器脉冲抖动传到列号推算:
#define ALPHA 0.3f static float speed_filtered = 0.0f; float filter_speed(float speed_raw) { speed_filtered = ALPHA * speed_raw + (1.0f - ALPHA) * speed_filtered; return speed_filtered; }alpha 取 0.3 的意思是 30% 新速度、70% 历史速度。取 0.8 响应快但波动大,取 0.1 平滑但滞后明显。AGV 加减速不算剧烈时 0.3 比较合适。如果现场启停频繁,可以降到 0.2,代价是列号推算在加速段有几米的滞后,但这对列定位没有实质影响。
注意:置信度公式里的 0.2 和 0.5 是经验值。如果现场贴纸间距本身就不均匀,0.5 要放大到 0.7,否则系统会天天报 WARN,运维麻木之后真故障反而被忽略。
4. 现场调参与避坑:反光误检、日照干扰与贴纸维护的 5 条记录
这一章全是现场调试验证过的坑,每条按现象→原因→解决的方式写,顺序按出现频率排。
4.1 现象:过一扇窗户就多计一列
现象:AGV 经过通道东侧窗户时,列号从 3 直接跳到 5。回放日志显示传感器在窗户位置输出了两个间隔极窄的脉冲。
原因:窗玻璃在特定太阳角度下形成高反光,漫反射传感器把玻璃反射误判为反光贴纸。贴纸反射率通常在 0.8 以上,玻璃在低角度入射时反射率也能到 0.6,固定阈值无法区分。
解决:阈值不能固定,要做基线自适应。传感器在无贴纸区域持续采集反射强度,形成动态基线,只有当信号超过基线 1.5 倍时才判定为贴纸。config/sensor.yaml 里 baseline_enable 开关打开后,系统在每次边沿触发后自动重新学习基线。改完这个参数,再让 AGV 在窗户前反复跑三遍,确认不再误触发。
4.2 现象:贴纸边角翘起导致同一根柱子时好时坏
现象:同一根柱子,上一圈跑过去计到了,下一圈没计到,再下一圈又计到了。日志里贴纸脉冲宽度忽宽忽窄。
原因:工程级反光膜贴久了边角起翘,传感器光束打在翘起边缘时入射角改变,反射光偏出接收器。翘起状态不稳定,运输振动时翘起角度还会变化。
解决:换用工程级反光膜并且加宽贴纸,从 8cm 加到 12cm。宽贴纸容错空间大,光束即使偏出一部分,剩余面积仍能产生足够反射。现场维护上要养成每周巡检的习惯,看到翘起用热风枪压平,压不平就换新的。贴纸是耗材,单价很低,一次漏检导致的停线损失够买几百张。
4.3 现象:原地转向时左右轮编码器脉冲打架
现象:AGV 在通道尽头原地调头,左右轮编码器一正一负,融合模块误以为车身又前进了一段,列号凭空多了一列。
原因:车控系统通常基于左右轮平均值计算车体中心位移。原地转向时左右轮速度相反,平均值是零,但如果融合前用了轮速累积的绝对里程,就会把转向扫过的弧长也算进去。
解决:融合前先做车体运动分解。检测到左右轮位移方向相反且幅度接近时,切换到旋转模式,旋转模式下里程计冻结,列号预测暂停。odom_fusion.c 预留了 rotation_mode 接口,标准做法是计算左右轮位移的交叉相关性,超过阈值才切入旋转模式,避免急刹车时误切。这个开关一定要加,不加的话每次调头都可能多一列。
4.4 现象:贴纸间距不均导致越到后面越乱
现象:前 10 列计数正常,到第 15 列开始频繁出现「预计该到但没到」,再往后整个列号错乱。
原因:贴纸安装用卷尺手工量距,相邻误差 ±2cm 很正常,累积 15 列就是 30cm。固定 0.3m 的容差在前期够用,后期就不够了。
解决:容差改成随累计距离线性增长。map.yaml 里 drift_tolerance_per_m=0.005 的含义是每跑 1m 允许 5mm 的累计误差,跑到 60m 时容差 0.3m,跑到 100m 时容差 0.5m。这个参数要结合现场贴纸安装精度来定:手工安装取 0.005 比较稳妥,用模板定位安装可以收紧到 0.002。改完之后回放历史日志,看 PREDICT 记录是否明显减少。
4.5 现象:回放全绿,实车还是偶尔错
现象:离线回放日志所有列都通过,置信度全在 0.9 以上,但上实车跑还是偶尔漏检。
原因:日志没有记录 CPU 负载和中断响应时间。实车上任务多,去抖采样中断偶尔被更高优先级任务抢占,采样间隔抖动超过预设值,边沿确认时间被拉长,脉冲宽度在阈值附近时就会出现漏检。回放环境没有这个抖动,所以回放永远全绿。
解决:回放工具里加时间误差注入,对每个采样点注入 ±0.2ms 随机抖动,模拟实车中断竞争。用这个验证去抖参数最接近真实效果。更彻底的方案是把去抖采样放到独立定时器中断并设最高优先级,中断回调里只做置标志位,不做耗时操作。日志里如果看到采样间隔偶尔跳到 300us 以上,就该检查中断配置了。
提示:现场调试先记日志,再回放调参,最后实车验证。顺序反了,你会在设备故障和电气噪声之间被来回折磨。
5. 进阶玩法:双传感器冗余与自动校准,让列定位真正可信
到这里,基础列定位已经能跑通。但现场真正要的是连续运行一年不出错,这需要两条进阶路线:冗余和自校准。
5.1 双传感器冗余:左右各装一只,交叉验证
在车身两侧各装一个反射传感器,检测同一条反光贴纸。左右触发的先后顺序固定,时间差在预期窗口内。只有一只触发时标记为弱确认,等第二只也触发才正式确认列号。
def dual_confirm(left_ts, right_ts, window_ms=50): if left_ts is None or right_ts is None: return 'WEAK' if abs(left_ts - right_ts) <= window_ms: return 'CONFIRMED' return 'MISALIGNED'成本翻倍,换来的是单只传感器故障时系统降级运行而不是停机。某个项目里一只传感器被飞溅的渣土糊住,系统自动降级,靠另一只继续跑完当天班次,运维交接时才处理。没有冗余的话,那几个小时都没有定位。
window_ms 取值取决于车体和安装位置。两个传感器沿车身纵向安装距离 0.5m、车速 1m/s 时,触发时间差约 0.5s,窗口要开到 600ms 而不是 50ms。50ms 适用于两个传感器横向对称安装的情况。这个参数按安装结构算,别照抄。
5.2 自动校准:用每次跑行学习真实列间距
手工贴纸间距不准,除了放宽容差,更彻底的办法是让系统自己学习。每跑完一趟,把实际触发里程和列号存入环形缓冲,用最小二乘拟合出平均列间距,连续三次拟合差异小于 5mm 就用新值替换 map.yaml。
import numpy as np def fit_spacing(trigger_odom, columns): # 拟合 columns = odom / spacing + bias coef = np.polyfit(trigger_odom, columns, 1) spacing = 1.0 / coef[0] return float(spacing)校准工具放在 PC 侧离线执行,不在单片机里跑。原因有两点:单片机内存有限,跑不下十几趟的触发里程缓冲;校准应该在调试期完成,运行中途自动改参数风险太大。工具跑完会生成一份校准报告,列出旧间距、新间距和拟合残差,残差超过 1cm 就提示去检查是否有贴纸漏贴。
5.3 值得投入吗:给决策者的三个判断维度
第一,现场已经有 PLC 和传感器,只缺算法,套这个工程改造,半天能见效,收益明显。第二,如果还没有硬件,反射传感器加反光贴纸的成本远低于视觉方案,但前提是通道两侧有平整的立柱或墙面可以贴纸。第三,现场存在大面积高反光金属面或强日照直射时,优先考虑遮挡光束或改用对射式,否则算法再强也扛不住物理干扰。
我现在的习惯是每周五下午把过去一周跑行的日志全部回放一遍,列号偏差超过 1 列就当天排查。这个习惯坚持了大半年,经手项目列定位错位率从千分之几降到接近零。希望帮到你,让每台移动设备都停在该停的那一列。
本文还有配套的精品资源,点击获取