1. 手动示教的困局:为什么码垛场景下它越来越不够用
干过现场调试的人都有体会,码垛这活儿看着简单,实际上是最容易把人耗死的一类应用。一个标准托盘,少则三四层,多则七八层,每层几个到十几个点位,你要是老老实实一个一个点去示教,一个托盘下来就是几十个示教点。换一个垛型,全部重来。客户说“把箱子从600×400换成500×350”,你心里就一沉——又得重新示教一遍。
问题的根子在于:手动示教记录的是“位置”,而不是“规律”。码垛的本质是高度重复的几何排列,层与层之间有固定的偏移量,列与列之间有固定的间距,这些规律用示教点是表达不出来的。你示教了第一层的四个角点,第二层、第三层的位置其实完全可以通过数学计算推出来,但传统示教方式逼着你把每一个都点一遍。
我见过太多现场,一个码垛程序里塞了上百个示教点,程序冗长不说,维护起来简直是灾难。哪天机器人零点丢了,或者夹具换了个长度,所有点位全部作废。更别提有些工况要求混码——同一托盘上不同尺寸的箱子交替摆放,手动示教基本没法优雅地处理。
Offs函数就是来解决这个问题的。它的逻辑非常朴素:你告诉机器人一个基准点,再告诉它往X、Y、Z方向各偏移多少,它自己算出目标位置。一个基准点加上若干组偏移参数,就能覆盖整个码垛阵列。垛型变了?改偏移参数就行,不用重新示教。夹具换了?改基准点就行,偏移量岿然不动。
这篇文章面向的是已经能跑通基本运动指令、但对码垛编程还停留在“一个点一个点教”阶段的同行。我会把Offs在码垛场景下的完整用法拆开讲清楚,包括基准点怎么选、偏移量怎么算、RAPID代码怎么写、实际跑起来会遇到哪些坑。文章末尾会给出一个可以直接拿去改的完整码垛程序框架。
2. Offs函数的底层逻辑:它到底在坐标系里做了什么
2.1 从robtarget的数据结构说起
要理解Offs,得先搞清楚ABB机器人的robtarget里到底存了什么。一个robtarget包含四个部分:trans(平移)、rot(姿态)、robconf(轴配置)、extax(外部轴)。Offs函数只动trans这一块,rot、robconf、extax原封不动地复制过去。
trans本身是一个pos类型,就是三个数值:x、y、z,单位毫米。Offs(p1, 100, 0, 50)的意思就是:取p1的姿态和配置,把它的x坐标加100,z坐标加50,生成一个新的robtarget。就这么简单。
但这里有个关键点很多人没意识到:Offs的偏移是在当前工件坐标系(wobj)下进行的,不是在世界坐标系下。如果你在wobj0(世界坐标系)下用Offs,那偏移方向就是世界坐标系的XYZ方向。如果你在自定义的工件坐标系下用Offs,偏移方向就跟着工件坐标系走。这个特性在码垛场景下极其重要——当托盘摆放有角度时,你只需要把工件坐标系对准托盘边缘,之后所有的偏移量都可以按“沿着托盘长边”“沿着托盘短边”来思考,不用去算世界坐标系下的分量。
2.2 为什么Offs比RelTool更适合码垛
ABB还有另一个类似的函数叫RelTool,它是在工具坐标系下做偏移的。RelTool(p1, x, y, z)的偏移方向跟着当前TCP的姿态走。如果你的工具是竖直向下的,RelTool的z方向就是竖直方向,x和y方向跟着工具旋转。
码垛场景下,绝大多数时候工具姿态是固定的——吸盘或者夹爪始终朝下。这种情况下Offs和RelTool的效果可能差不多。但一旦你的工具需要倾斜,比如从侧面伸进去抓取,RelTool的偏移方向就会跟着倾斜,而Offs始终保持在工件坐标系下的方向。码垛的排列逻辑是“沿着托盘方向”,不是“沿着工具方向”,所以Offs是更自然的选择。
还有一个实际考量:Offs的计算开销比RelTool小。在高速码垛循环里,每个周期省几毫秒,一天下来就是可观的产能差异。当然这个差异在大多数场景下不明显,但能省则省。
2.3 偏移量的计算:从托盘尺寸到RAPID参数
假设你有一个标准托盘1200×1000mm,箱子尺寸是400×300mm,要求每层摆两行三列,箱子之间留20mm间隙。我们来算一下偏移量。
第一列第一个箱子的放置点,假设已经示教好了,记为pBase。那么同一行第二个箱子的位置就是pBase往列方向偏移400+20=420mm。第三个再偏移420mm。换行的时候,行方向偏移300+20=320mm,列方向归零。
用Offs表达就是:
! 第一行 Offs(pBase, 0, 0, 0) Offs(pBase, 420, 0, 0) Offs(pBase, 840, 0, 0) ! 第二行 Offs(pBase, 0, 320, 0) Offs(pBase, 420, 320, 0) Offs(pBase, 840, 320, 0)这里假设列方向是工件坐标系的X轴,行方向是Y轴。实际项目中,你需要根据工件坐标系的定义来对应。如果托盘是斜着放的,你只需要把工件坐标系的一个轴对准托盘的列方向,另一个轴对准行方向,上面的偏移量就不用改。
层与层之间的偏移更简单:每层的高度就是箱子高度加上层间纸板厚度。假设箱子高200mm,纸板厚5mm,那第二层就是z方向偏移205mm,第三层410mm,以此类推。
注意:层高偏移一定要把纸板、隔板、托盘本身的厚度都算进去。我见过一个项目,现场用的是塑料托盘,比木托盘厚了15mm,结果第一层就撞了。后来把托盘厚度加进基准点的z值里才解决。
3. 基准点的选择策略:选对了省一半事
3.1 为什么基准点不能随便选
基准点是整个偏移体系的锚点,它的选择直接决定了后续偏移量的复杂程度。选得好,偏移量全是整齐的整数;选得不好,每个偏移量都要带小数,调试时改一个数牵动全身。
最理想的基准点是托盘上第一个箱子的理论放置位置,而且这个位置最好在托盘的某个角上。这样所有其他箱子的偏移量都是正数,方向统一,不容易搞混。如果你把基准点选在托盘中心,那偏移量就有正有负,写代码时容易漏掉负号。
另一个考量是:基准点要方便示教。如果基准点在托盘最外侧的角上,机器人可能够不到——因为夹具本身有尺寸,TCP能到达的位置和箱子实际放置位置之间有一个偏移。这种情况下,你可以把基准点示教在机器人容易到达的位置,然后在Offs里把这个夹具偏移补进去。
3.2 基准点与工件坐标系的配合
我的习惯是:先建工件坐标系,再示教基准点,基准点在工件坐标系下示教。工件坐标系的原点设在托盘的某个角上,X轴沿着托盘长边,Y轴沿着短边,Z轴竖直向上。这样基准点的坐标就是(某个值, 某个值, 某个值),偏移量直接按箱子间距来写。
如果托盘摆放有旋转角度,工件坐标系可以跟着旋转。示教工件坐标系时,用三点法:原点、X轴上一点、Y轴上一点。这样不管托盘怎么摆,偏移量都不用改。
有个细节:工件坐标系的Z轴方向要和重力方向一致,否则层高偏移会出问题。三点法示教时,X轴和Y轴的点要尽量选得远一些,这样坐标系的方向精度更高。我一般会在托盘上贴标记,用尖头工具去点,重复精度能控制在1mm以内。
3.3 多托盘场景下的基准点管理
实际产线经常有多个托盘——一个在码,一个在等,码满了叉车换走。这时候基准点怎么管理?
方案一:每个托盘一个工件坐标系,每个工件坐标系下示教一个基准点。程序里用wobj参数切换。优点是逻辑清晰,缺点是示教工作量大。
方案二:只示教一个基准点,其他托盘通过工件坐标系的偏移来定位。比如托盘间距是1500mm,那第二个托盘的工件坐标系就是第一个的工件坐标系偏移(1500, 0, 0)。这样只需要示教一次基准点。
方案三:用托盘定位装置保证每个托盘的位置一致性,所有托盘共用一个工件坐标系和一个基准点。这是最省事的,但对机械定位精度要求高。
我倾向于方案二,兼顾灵活性和工作量。具体做法是在RAPID里定义多个wobj,每个wobj的uframe基于第一个wobj的uframe用Offs计算出来。这样换托盘时只需要改一个偏移量。
4. 完整RAPID代码拆解:从单层到多层码垛
4.1 程序框架与数据声明
先看数据声明部分。我们需要定义基准点、工件坐标系、以及码垛的参数。
MODULE PalletizingModule ! 工件坐标系:托盘坐标系 PERS wobjdata wobjPallet := [FALSE, TRUE, "", [[0,0,0],[1,0,0,0]], [[0,0,0],[1,0,0,0]]]; ! 基准点:第一个箱子的放置位置(在wobjPallet下示教) PERS robtarget pBase := [[0,0,0],[1,0,0,0],[0,0,0,0],[0,0,0,0]]; ! 码垛参数 CONST num nBoxL := 400; ! 箱子长度(列方向) CONST num nBoxW := 300; ! 箱子宽度(行方向) CONST num nBoxH := 200; ! 箱子高度 CONST num nGapL := 20; ! 列间隙 CONST num nGapW := 20; ! 行间隙 CONST num nGapH := 5; ! 层间纸板厚度 CONST num nCols := 3; ! 每层列数 CONST num nRows := 2; ! 每层行数 CONST num nLayers := 5; ! 层数 ! 计算出的偏移量 CONST num nOffL := nBoxL + nGapL; ! 列偏移 = 420 CONST num nOffW := nBoxW + nGapW; ! 行偏移 = 320 CONST num nOffH := nBoxH + nGapH; ! 层偏移 = 205 ENDMODULE这里我把所有尺寸参数都定义成常量,好处是改垛型时只改这几个数,下面的逻辑代码完全不用动。nOffL、nOffW、nOffH是计算出来的偏移量,用常量表达式写,RAPID在编译时就会算好,不占运行时间。
提示:PERS和CONST的区别要搞清楚。PERS是持久化变量,断电后值还在,适合存基准点这种需要示教的数据。CONST是编译期常量,运行期间不能改,适合存尺寸参数。如果你把基准点定义成CONST,程序就没法在线示教了。
4.2 单层码垛的循环逻辑
单层码垛的核心是两层循环:外层遍历行,内层遍历列。
PROC PalletizeOneLayer(num nLayerIndex) VAR num i; VAR num j; VAR robtarget pTarget; FOR i FROM 0 TO nRows - 1 DO FOR j FROM 0 TO nCols - 1 DO ! 计算目标位置 pTarget := Offs(pBase, j * nOffL, i * nOffW, nLayerIndex * nOffH); ! 移动到目标位置上方的安全高度 MoveJ Offs(pTarget, 0, 0, 100), v1000, z50, tool0 \WObj:=wobjPallet; ! 直线下降到放置位置 MoveL pTarget, v300, fine, tool0 \WObj:=wobjPallet; ! 这里插入夹具释放动作 ! ReleaseGripper; ! 直线上升回安全高度 MoveL Offs(pTarget, 0, 0, 100), v500, z50, tool0 \WObj:=wobjPallet; ENDFOR ENDFOR ENDPROC这段代码有几个值得说的点。
第一,pTarget的计算用Offs一次性完成三个方向的偏移,而不是嵌套调用Offs。嵌套调用也能实现,但可读性差,而且每次调用都有计算开销。一次性算完最干净。
第二,接近和离开用安全高度。直接从上方100mm处下降,放完再升回去。这个100mm是经验值,根据夹具高度和现场障碍物调整。如果托盘周围有护栏,安全高度要高于护栏。
第三,MoveJ和MoveL的配合。在安全高度以上的移动用MoveJ,速度快,路径不要求精确。从安全高度到放置点是MoveL,保证垂直下降,避免夹具碰到已放置的箱子。放置点用fine,确保精确到位。
第四,\WObj:=wobjPallet不能漏。如果漏了,Offs就在世界坐标系下计算,偏移方向全错。这个错误很隐蔽,因为程序能跑,只是位置不对。
4.3 多层码垛与层间过渡
多层码垛就是在单层的基础上加一层循环,同时处理层间的过渡动作。
PROC PalletizeFullPallet() VAR num k; FOR k FROM 0 TO nLayers - 1 DO PalletizeOneLayer k; ! 层间过渡:如果需要放纸板,在这里插入 IF k < nLayers - 1 THEN ! PlaceCardboard k; ! 等待纸板放置完成 WaitTime 2; ENDIF ENDFOR ENDPROC层间过渡看起来简单,实际上是最容易出问题的地方。纸板放置后可能有翘曲,导致下一层的实际高度比理论值高。如果层数多,累积误差会越来越大。我的做法是:每层的z偏移量不要用理论值累加,而是在每层开始前用传感器测一下实际高度,动态修正。
如果现场没有测高传感器,那至少要在程序里留一个手动修正的接口。比如加一个PERS num nLayerComp,每层的z偏移加上这个补偿值。调试时根据实际码垛效果调整。
另一个坑是:纸板放置后,机器人的下降位置要相应提高。因为纸板本身有厚度,如果你还用原来的z偏移,夹具会压到纸板上。正确的做法是把纸板厚度加进层偏移里,这就是为什么nOffH = nBoxH + nGapH,nGapH就是纸板厚度。
4.4 混码场景的扩展思路
混码——同一托盘上不同尺寸的箱子——是码垛里最复杂的场景。但用Offs的思路,混码也可以处理得很优雅。
核心思路是:把托盘划分成若干区域,每个区域有自己的基准点和偏移参数。比如托盘左半边放大箱,右半边放小箱。大箱区域的基准点是pBaseL,小箱区域的基准点是pBaseR。两个区域各自用自己的nOffL、nOffW。
更复杂的情况是同一层里不同箱子交替排列。这时候可以用一个数组来定义每个位置的箱子类型和偏移量。
CONST num nPattern{6} := [1, 2, 1, 2, 1, 2]; ! 1=大箱,2=小箱 PROC PalletizeMixedLayer(num nLayerIndex) VAR num i; VAR robtarget pTarget; VAR num nOffsetX; nOffsetX := 0; FOR i FROM 1 TO 6 DO IF nPattern{i} = 1 THEN pTarget := Offs(pBase, nOffsetX, 0, nLayerIndex * nOffH); nOffsetX := nOffsetX + nOffL_Large; ELSE pTarget := Offs(pBase, nOffsetX, 0, nLayerIndex * nOffH); nOffsetX := nOffsetX + nOffL_Small; ENDIF ! 放置动作... ENDFOR ENDPROC这个模式可以扩展到任意复杂的排列。关键是维护好nOffsetX这个累加变量,它记录了当前箱子在列方向上的累计偏移。
5. 现场调试中那些文档不会告诉你的坑
5.1 工件坐标系的方向搞反了
这是新手最常犯的错误。示教工件坐标系时,X轴和Y轴的方向定义反了,结果Offs的偏移方向跟预期差90度。程序跑起来,箱子全摆到托盘外面去了。
排查方法:在示教器上手动移动机器人到基准点,然后手动移动到一个偏移位置,看机器人实际走的方向和你在代码里写的偏移方向是否一致。如果不一致,检查工件坐标系的定义。
预防措施:在托盘上贴方向标记,X轴用红色箭头,Y轴用蓝色箭头。示教时对着标记点,不容易搞错。
5.2 层高累积误差导致撞机
前面提过,纸板翘曲、箱子高度不一致、托盘不平,这些因素都会导致层高累积误差。层数少的时候看不出来,层数一多就撞了。
我的做法是在程序里加一个层高补偿变量,每层码完后根据实际效果手动调整。如果发现某一层开始偏低,就把补偿值调大。这个变量是PERS类型,调试时在示教器上直接改,不用重新下载程序。
更彻底的方案是加一个测高传感器,每层开始前测一下当前垛顶的高度,自动计算补偿值。但这个方案成本高,小项目不一定划算。
5.3 Offs的偏移量超出机器人工作空间
Offs只是数学计算,它不会检查计算结果是否在机器人工作空间内。如果你算出来的目标点机器人够不到,程序运行时会报“位置不可达”错误,然后停机。
预防方法:在程序里加一个范围检查。用ReadMotor或CalcJointT检查目标点的关节角度是否在限位内。如果超出,提前报警,而不是等到运动指令执行时才报错。
FUNC bool CheckReachable(robtarget pTarget) VAR jointtarget jt; VAR bool bOK; bOK := TRUE; jt := CalcJointT(pTarget, tool0 \WObj:=wobjPallet); ! 检查各轴是否在限位内 IF jt.robax.rax_1 < -170 OR jt.robax.rax_1 > 170 THEN bOK := FALSE; ENDIF ! 其他轴类似检查... RETURN bOK; ENDFUNC这个检查会增加一点计算时间,但在调试阶段非常有用。正式生产时可以去掉,或者只在第一个循环里检查。
5.4 夹具偏移没有补偿
Offs计算的是TCP的目标位置,但箱子实际放置的位置和TCP之间有一个夹具偏移。比如吸盘吸住箱子后,箱子底面比TCP低50mm。如果你直接让TCP到目标位置,箱子就放高了50mm。
解决方法:在基准点示教时就考虑夹具偏移。示教时让TCP停在目标位置上方50mm处,这样基准点本身就包含了夹具偏移。或者,在Offs里把夹具偏移加进去:Offs(pBase, x, y, z - 50)。
我倾向于第一种方法,因为基准点示教更直观。示教时看箱子实际放到位了,就记下这个位置,不用去算TCP和箱子底面的关系。
5.5 速度参数设置不当导致箱子移位
码垛时,MoveL的下降速度不能太快。太快的话,箱子在惯性作用下会移位,尤其是轻箱子。我一般用v300到v500,具体看箱子重量和摩擦系数。
加速和减速参数也要注意。z50表示转弯区数据,意思是机器人到达目标点前50mm就开始转弯,不精确到位。放置点必须用fine,确保精确到位。但接近点可以用z50,提高效率。
还有一个细节:如果箱子之间间隙很小,转弯区要设小一些,否则机器人转弯时会碰到已放置的箱子。间隙20mm的话,z设10mm比较安全。
6. 从Offs到更高效的码垛方案
6.1 用数组预计算所有点位
如果码垛的点位是固定的,可以在程序初始化时把所有目标点算好存到数组里,运行时直接取用,不用每次循环都调Offs。这样能省一点计算时间,但更重要的是代码更清晰。
VAR robtarget pAllTargets{15}; ! 3列×2行×5层=30个点,这里简化 PROC InitTargets() VAR num i, j, k; VAR num nIndex; nIndex := 1; FOR k FROM 0 TO nLayers - 1 DO FOR i FROM 0 TO nRows - 1 DO FOR j FROM 0 TO nCols - 1 DO pAllTargets{nIndex} := Offs(pBase, j * nOffL, i * nOffW, k * nOffH); nIndex := nIndex + 1; ENDFOR ENDFOR ENDFOR ENDPROC这样主循环里只需要遍历数组,逻辑更简单。而且数组里的点位可以在示教器上查看和修改,方便调试。
6.2 与视觉系统的配合
如果来料位置不固定,需要视觉系统定位,Offs依然有用。视觉系统给出箱子在托盘坐标系下的位置,你用Offs把这个位置和基准点结合起来,算出最终放置点。
! 视觉系统给出的偏移量 VAR num nVisionX; VAR num nVisionY; VAR num nVisionAngle; ! 计算最终放置点 pTarget := Offs(pBase, nVisionX, nVisionY, nLayerIndex * nOffH); ! 姿态需要根据角度调整 pTarget.rot := OrientZYX(nVisionAngle, 0, 0);注意姿态的调整不能用Offs,Offs只改位置不改姿态。姿态要用OrientZYX或类似的函数重新计算。
6.3 节拍优化的几个方向
码垛的节拍直接影响产能。用Offs本身不会提升节拍,但合理的程序结构可以。
第一个方向是减少不必要的精确到位。只有放置点用fine,其他点都用z转弯区。转弯区越大,机器人越流畅,节拍越快。
第二个方向是优化路径。码完一列后,不要回到安全高度再移动到下一列,而是直接在安全高度上平移过去。这需要把安全高度的移动和水平移动合并。
第三个方向是预读取下一个点位。在机器人还在移动时,就计算好下一个目标点,减少等待时间。RAPID是顺序执行的,这个优化需要用到中断或后台任务,比较复杂,但效果明显。
7. 一个可以直接复用的码垛程序模板
把上面的内容整合起来,给出一个完整的、可以直接改参数使用的码垛程序。这个模板假设托盘是规则的,箱子尺寸一致,层数固定。
MODULE PalletizingTemplate ! ========== 数据声明 ========== PERS wobjdata wobjPallet := [FALSE, TRUE, "", [[0,0,0],[1,0,0,0]], [[0,0,0],[1,0,0,0]]]; PERS robtarget pBase := [[0,0,0],[1,0,0,0],[0,0,0,0],[0,0,0,0]]; PERS robtarget pSafe := [[0,0,0],[1,0,0,0],[0,0,0,0],[0,0,0,0]]; CONST num nBoxL := 400; CONST num nBoxW := 300; CONST num nBoxH := 200; CONST num nGapL := 20; CONST num nGapW := 20; CONST num nGapH := 5; CONST num nCols := 3; CONST num nRows := 2; CONST num nLayers := 5; CONST num nOffL := nBoxL + nGapL; CONST num nOffW := nBoxW + nGapW; CONST num nOffH := nBoxH + nGapH; PERS num nLayerComp := 0; ! 层高补偿 ! ========== 主程序 ========== PROC main() ! 初始化 ! 这里可以加回原点、开夹具等动作 ! 开始码垛 PalletizeFullPallet; ! 结束动作 ! 这里可以加回原点、关夹具等动作 ENDPROC ! ========== 码整垛 ========== PROC PalletizeFullPallet() VAR num k; FOR k FROM 0 TO nLayers - 1 DO PalletizeOneLayer k; IF k < nLayers - 1 THEN ! 放纸板动作 WaitTime 2; ENDIF ENDFOR ENDPROC ! ========== 码一层 ========== PROC PalletizeOneLayer(num nLayerIndex) VAR num i; VAR num j; VAR robtarget pTarget; VAR num nZOffset; ! 计算当前层的z偏移,包含补偿 nZOffset := nLayerIndex * nOffH + nLayerComp * nLayerIndex; FOR i FROM 0 TO nRows - 1 DO FOR j FROM 0 TO nCols - 1 DO pTarget := Offs(pBase, j * nOffL, i * nOffW, nZOffset); ! 移动到安全位置 MoveJ Offs(pTarget, 0, 0, 100), v1000, z50, tool0 \WObj:=wobjPallet; ! 下降到放置点 MoveL pTarget, v300, fine, tool0 \WObj:=wobjPallet; ! 夹具释放 ! ReleaseGripper; WaitTime 0.3; ! 上升回安全高度 MoveL Offs(pTarget, 0, 0, 100), v500, z50, tool0 \WObj:=wobjPallet; ENDFOR ENDFOR ENDPROC ENDMODULE这个模板的使用方法:先把wobjPallet用三点法示教好,再把pBase示教到第一个箱子的放置位置,然后根据实际箱子尺寸修改那几个CONST常量。跑起来之后,如果发现层高有偏差,调整nLayerComp。
注意:模板里的WaitTime 0.3是夹具释放后的等待时间,根据实际夹具调整。吸盘的话可以短一些,机械夹爪要长一些,确保完全松开再上升。
8. 写在最后:Offs不是万能药,但码垛场景下它最趁手
Offs函数本身很简单,简单到很多人觉得它没什么可讲的。但正是这种简单,让它在码垛这种高度规律性的场景下变得极其高效。一个基准点,几个偏移参数,就能描述整个垛型。改垛型不用重新示教,改参数就行。这是手动示教永远做不到的。
当然,Offs也有它的边界。如果来料位置完全不固定,需要视觉系统实时定位,Offs只能作为最后一步的偏移计算。如果垛型极其复杂,每个箱子位置都不一样,那可能需要更复杂的算法,甚至用外部计算机算好点位再传给机器人。但在80%的码垛场景下,Offs加上合理的程序结构,已经足够解决问题。
我在实际项目里用这套思路做过不少码垛应用,从简单的单列码垛到复杂的混码,Offs都是核心工具。踩过的坑主要集中在工件坐标系的方向、层高累积误差、夹具偏移这几个方面。这些问题在文档里往往一笔带过,但现场调试时能卡你半天。希望这篇文章把这些坑都摊开讲清楚了,让你少走点弯路。
如果你正在做一个码垛项目,不妨先把基准点和工件坐标系搞扎实,然后用Offs把第一层跑通。第一层没问题了,多层就是加个循环的事。混码的话,先把区域划分清楚,每个区域独立处理。一步一步来,码垛没那么可怕。