博文

从一次报错到深入理解:CODESYS电子凸轮挺杆 SMC_CamRegister 完整指南

工控笔记

错误629的根因分析、三种解决方案对比与虚轴场景最优实践


前言:从一个实际报错说起

在做 CODESYS SoftMotion 运动控制项目时,主轴电子凸轮配置了15组挺杆(Tappet)点,其中部分挺杆的触发位置相同。运行时系统报出错误:

SMC_ERROR.SMC_CI_TOO_MANY_TAPPETS_PER_CYCLE (#629) 描述:Too many tappets became active during one cycle(一个周期内激活的挺杆数量过多)

正是这个错误,让我开始深入研究 SMC_CamRegister 功能块,并最终找到了适合自己项目的解决方案。本文将完整记录这次排查过程,从错误根因到底层原理,从官方功能块对比到自封装实现,希望对遇到同样问题的朋友有所帮助。


一、电子凸轮与挺杆(Tappet)基础

1.1 什么是挺杆

挺杆(Tappet)本质是一个位置触发的数字开关点。在机械凸轮机构中,挺杆随凸轮轮廓的起伏而上下运动,从而控制开关的通断。在 CODESYS 电子凸轮中,这一概念被数字化——通过在凸轮表中定义挺杆点,当主轴运行到指定位置时,自动触发对应的开关动作。

挺杆的核心配置要素包括:

  • 跟踪ID(Track ID):挺杆点的标识号,每个ID对应一个独立的开关输出
  • 触发位置(X):主轴运行到该位置时触发动作
  • 触发方向:正向通过(位置增大)或反向通过(位置减小)
  • 挺杆动作:打开(ON)/ 关闭(OFF)/ 反转(Invert)

注意:挺杆的动作与凸轮曲线无关,只与主轴运行的位置有关。修改凸轮曲线不会改变挺杆的触发位置。

图1:电子凸轮与挺杆工作原理示意图 图1:电子凸轮与挺杆工作原理示意图

1.2 挺杆的典型应用场景

  • 胶枪控制:主轴到达特定位置时开启/关闭胶枪,支持死区补偿提前触发
  • 气缸动作:主轴在各工位触发气缸伸缩
  • 飞剪/追剪:在特定位置触发切刀动作
  • 指示灯/报警:主轴运行到某些位置点亮指示灯或触发报警
  • 多工位集中管理:通过数组一次获取所有开关点状态

二、SMC_CamRegister 功能块详解

SMC_CamRegister 是 CODESYS SoftMotion(SM3_Basic 库)中的一个功能块,用于电子凸轮挺杆控制。它可以一次性读取凸轮表中定义的所有挺杆点的状态,输出为一个 BOOL 数组。

2.1 功能块接口

作用域名称类型说明
InOutMasterAXIS_REF_SM3主轴参考
InOutCamTableMC_CAM_REF含挺杆描述的凸轮表
InOutbTappetARRAY[1..MAX] OF BOOL所有挺杆点的输出状态数组
InputEnableBOOLTRUE时启动功能块
InputMasterOffsetLREAL主轴位置偏移量(默认0)
InputMasterScalingLREAL主轴缩放因子(默认1)
InputTappetHysteresisLREAL挺杆迟滞区间,防止抖动误触发
InputDeadTimeCompensationLREAL死区时间补偿(秒),通过线性外推预估主轴位置
OutputBusyBOOL功能块执行中
OutputErrorBOOL发生错误
OutputErrorIDSMC_ERROR错误码
OutputEndOfProfileBOOL凸轮周期结束脉冲

2.2 与 SMC_GetTappetValue 的区别

对比项SMC_CamRegisterSMC_GetTappetValue
读取方式一次读取全部挺杆状态,输出为数组一次只能读取一个挺杆,需实例化多个功能块
ID关联数组索引直接对应挺杆ID通过iID参数指定
适用场景集中管理多个挺杆,状态读取高效少量挺杆,简单使用
推荐度兼容性保留,新项目建议用更高级功能块仅保留兼容,不推荐新项目

2.3 关键参数详解

TappetHysteresis(迟滞):当主轴位置在挺杆触发点附近抖动(如编码器信号抖动)时,若不加迟滞,挺杆会反复通断。设置正值迟滞后,主轴必须离开触发点超过该值才能再次触发。

DeadTimeCompensation(死区补偿):例如胶枪从接收到信号到实际出胶有50ms延迟,设置 DeadTimeCompensation := 0.05,功能块会提前触发挺杆,补偿该延迟。

MasterOffset / MasterScaling:前者对主轴位置整体加偏移,后者乘缩放系数,常用于主轴与凸轮表之间的单位换算或位置微调。


三、错误629:SMC_CI_TOO_MANY_TAPPETS_PER_CYCLE 深度分析

3.1 错误信息

项目内容
错误号629
枚举名SMC_CI_TOO_MANY_TAPPETS_PER_CYCLE
所属模块MC_CamIn(前缀SMC_CI_)
描述Too many tappets became active during one cycle(一个周期内激活的挺杆数量过多)

3.2 错误根因

当主轴运行到某个位置时,凸轮表中在该位置附近配置的多个挺杆点同时被触发,且触发数量超过了 MC_CamIn 单个周期能处理的上限,就会报这个错。

实际项目中的情况:

  • 配置了15组挺杆点
  • 其中部分挺杆的启动位置是一样的(位置重合)
  • MC_CamIn 内部的挺杆处理有单周期上限(实测约3组)
  • 当多个挺杆在同一周期同时触发时,超过3组就报错629

图2:错误629触发场景 图2:SMC_CI_TOO_MANY_TAPPETS_PER_CYCLE(错误629)触发场景——多组挺杆位置重合导致单周期激活数超限

3.3 SMC_CamRegister 为什么能解决这个问题

关键在于两者的处理架构不同:

方式说明问题
MC_CamIn 内置处理挺杆挺杆状态由MC_CamIn内部管理,有单周期激活数限制报629错误
SMC_CamRegister 独立读取不依赖MC_CamIn内部处理,独立读取CamTable中所有挺杆状态输出到bTappet[]数组可绕过MC_CamIn的周期限制

核心区别:MC_CamIn把凸轮跟随和挺杆处理打包在一起,挺杆激活有内置上限;SMC_CamRegister是独立的功能块,只负责读挺杆状态,与MC_CamIn各自的限制互不干扰。


四、解决方案一:分离凸轮跟随与挺杆读取

解决思路:将凸轮跟随和挺杆读取分离。MC_CamIn只负责电子凸轮跟随,SMC_CamRegister独立处理全部15组挺杆。

4.1 变量声明

PROGRAM Main
VAR
    // 轴
    MasterAxis          : AXIS_REF_SM3;
    SlaveAxis           : AXIS_REF_SM3;
    
    // 凸轮表
    CamTable            : MC_CAM_REF;
    
    // ===** MC_CamIn:只负责凸轮跟随 **=**
    CamIn               : MC_CamIn;
    xExecute            : BOOL;
    bInSync             : BOOL;
    bCamBusy            : BOOL;
    bCamError           : BOOL;
    eCamErrorID         : SMC_ERROR;
    
    // **=** SMC_CamRegister:独立处理15组挺杆 **===
    CamRegister         : SMC_CamRegister;
    xEnable             : BOOL := TRUE;
    bTappetStates       : ARRAY[1..15] OF BOOL;  // 15组挺杆状态
    
    // 15组挺杆对应的控制输出
    bTappet_01          : BOOL;
    bTappet_02          : BOOL;
    // ... 直到 bTappet_15
    
    // 错误
    bRegError           : BOOL;
    eRegErrorID         : SMC_ERROR;
END_VAR

4.2 主程序逻辑

// 第一步:MC_CamIn 只做电子凸轮跟随
CamIn(
    Master          := MasterAxis,
    Slave           := SlaveAxis,
    Execute         := xExecute,
    CamTableID      := CamTable,
    MasterAbsolute  := TRUE,
    SlaveAbsolute   := TRUE,
    MasterOffset    := 0,
    SlaveOffset     := 0,
    MasterScaling   := 1,
    SlaveScaling    := 1,
    StartMode       := MC_CAM_START_MODE.CURRENT,
    BufferMode      := MC_BUFFER_MODE.Aborting,
    InSync          => bInSync,
    Busy            => bCamBusy,
    Error           => bCamError,
    ErrorID         => eCamErrorID
);

// 第二步:SMC_CamRegister 独立读取15组挺杆
CamRegister(
    Master              := MasterAxis,
    CamTable            := CamTable,
    Enable              := xEnable,
    MasterOffset        := 0,
    MasterScaling       := 1,
    TappetHysteresis    := 0.2,    // 加迟滞,防抖动误触发
    DeadTimeCompensation := 0,
    Busy                => ,
    Error               => bRegError,
    ErrorID             => eRegErrorID,
    EndOfProfile        => ,
    bTappet             => bTappetStates    // 一次读出全部15组
);

// 第三步:映射到具体控制信号
bTappet_01 := bTappetStates[1];
bTappet_02 := bTappetStates[2];
// ... 
bTappet_15 := bTappetStates[15];

4.3 关键注意事项

  • 加TappetHysteresis(迟滞):部分挺杆位置重合时,主轴稍微抖动就可能同时触发多个,加迟滞可以减少同一周期内的激活数
  • 凸轮表挺杆ID必须对应:确保15组挺杆的Track ID分别为1~15,bTappetStates[1]到bTappetStates[15]才能正确对应
  • 逐步测试验证:先用3组挺杆确认配合正常,再逐步增加到15组

五、官方推荐:MC_DigitalCamSwitch 功能块对比

在CODESYS官方文档中,SMC_CamRegister 和 SMC_GetTappetValue 被标注为”仅保留兼容”,官方建议新项目使用更高级的功能块,其中 MC_DigitalCamSwitch 是推荐选择之一。

5.1 官方功能块对比表

功能MC_DigitalCamSwitchSMC_CamRegister
官方定位当前和未来项目推荐使用仅保留兼容,不推荐新项目
多开关点支持(最多32个Tracks)仅读取凸轮表中已定义的挺杆
开/关补偿支持(通过外推法)
预测(Forecast)无(仅外推)
高精度(亚周期)HighPrecision版本支持周期级精度
数据基准设定位置或实际位置设定位置或实际位置
单周期激活限制无明确限制无明确限制

5.2 MC_DigitalCamSwitch 的额外优势

(1)开/关补偿:每个Track可以独立设置开补偿和关补偿(时间单位),比如胶枪提前50ms触发:

TrackOptions.OnCompensation[1]  := T#50MS;   // 提前开
TrackOptions.OffCompensation[1] := T#50MS;   // 提前关

(2)区间开关:每个Track可以设置区间(起始位置~结束位置),而不只是单点触发:

// Track1:主轴位置 90~120 之间输出 ON
Switch01.FirstOnPosition := 90;
Switch01.LastOnPosition  := 120;

(3)正反向独立控制:AxisDirection: 0=双向, 1=正向, 2=反向

5.3 能力对比总表

图5:功能块能力对比表 图5:四种挺杆处理方案能力对比表


六、底层原理:能否通过PDO自行封装?

6.1 MC_DigitalCamSwitch 底层做了什么

从底层架构来看,MC_DigitalCamSwitch的工作分为三层:

第1层 — 配置下发(Enable上升沿时):所有开关配置(Switches、TrackOptions、Outputs)被一次性传送到处理单元。

第2层 — 实时位置比较(每个总线周期):读取轴实际位置和速度,遍历所有Track进行位置比较,如果启用补偿则通过速度外推预测到达时间,提前触发输出。

第3层 — 输出执行:标准方式由SoftMotion在PLC侧计算后通过总线写远程IO;高性能方式则将配置下发给驱动器,由驱动器内部在本地硬件上直接切换输出(微秒级响应)。

6.2 自封装 vs 官方功能块

功能自封装(PDO方式)MC_DigitalCamSwitch差距
位置比较完全能做到完全能做到无差距
多Track循环遍历即可最多32个无差距
迟滞(Hysteresis)加状态保持即可支持无差距
正反向控制判断方向符号支持无差距
开/关补偿能做,精度受限更准确(内核数据)有差距
高精度(亚周期)做不到HighPrecision版支持根本差距
驱动器硬件直接输出做不到部分实现支持根本差距

核心差距在于:MC_DigitalCamSwitch集成在SoftMotion内核中,能访问轨迹规划器的内部数据(加速度、加加速度),外推精度更高。而自封装只能依赖当前速度的线性假设。


七、关键转折:主轴是虚轴!

前面分析的PDO延迟、亚周期精度、驱动器硬件比较——如果主轴是虚轴,这些全都不适用了!

虚轴的实际位置由SoftMotion内核内部计算,直接存在AXIS_REF_SM3结构体的内存中。没有PDO,没有总线延迟,没有硬件参与。

之前担心的点虚轴下的真相
PDO读取延迟不存在 — 位置就在内存里,直接访问
亚周期精度不存在 — 虚轴位置也是每个PLC周期更新一次
驱动器硬件比较不存在 — 没有物理驱动器
外推精度受总线影响不存在 — 无总线

结论:在虚轴场景下,自封装和官方功能块的精度基础完全一样,因为处理都在同一个PLC周期内、访问同一份内存数据。

图4:虚轴数据流 图4:虚轴数据流——为什么自封装方案在虚轴场景下最优


八、最终方案:自封装挺杆控制功能块

既然主轴是虚轴,所有数据本来就在PLC的内存空间里,我们完全可以自封装一个简单的功能块,干净利落绕过629错误,且没有任何数量限制。

图3:三种方案对比 图3:三种挺杆处理方案架构对比

8.1 数据类型定义

// 挺杆动作类型
TYPE EnumTappetAction :
(
    eSwitchON,      // 到达位置时切换到 ON
    eSwitchOFF,     // 到达位置时切换到 OFF
    eToggle         // 到达位置时取反(Invert)
) := eSwitchON;
END_TYPE

// 触发方向
TYPE EnumTappetDirection :
(
    eBoth,          // 正反向都触发
    ePositive,      // 仅正向(主轴位置增大)
    eNegative       // 仅反向(主轴位置减小)
) := eBoth;
END_TYPE

// 每个挺杆点的配置
TYPE TappetConfigType :
STRUCT
    // 基本信息
    bEnable         : BOOL;         // TRUE: 启用该挺杆
    sDescription    : STRING(80);   // 描述,如"胶枪"、"气缸1"
    
    // 触发位置
    fOnPosition     : LREAL;        // 触发位置(主轴坐标)
    fOffPosition    : LREAL;        // 关闭位置(区间型时使用)
    
    // 动作方式
    eAction         : EnumTappetAction;       // ON / OFF / 取反
    eDirection      : EnumTappetDirection;    // 正/反/双向
    
    // 保护
    fHysteresis     : LREAL;        // 迟滞,防止抖动误触发
    
    // 单次触发模式(可选)
    bOneShot        : BOOL;         // 每个周期只触发一次
END_STRUCT
END_TYPE

8.2 各字段说明

字段作用为什么需要
fOnPosition触发位置核心字段——你之前说”几组启动位置一样”指的就是这个值
fOffPosition关闭位置区间开关时使用(如位置90~120输出ON)
eAction动作方式对应凸轮表挺杆的:打开=ON / 关闭=OFF / 反转=Invert
eDirection触发方向主轴会反转时,防止反向运动误触发
fHysteresis迟滞区间主轴在触发点附近抖动时防止反复通断
bOneShot单次触发到位置触发一次动作,而不是保持状态

8.3 功能块实现

FUNCTION_BLOCK MyTappetController
VAR_INPUT
    // 直接从虚轴结构体读位置(不需要PDO!)
    fMasterPosition     : LREAL;      // MasterAxis.NcToPlc.ActPos
    fMasterVelocity     : LREAL;      // MasterAxis.NcToPlc.ActVel
    bEnable             : BOOL;
END_VAR
VAR_IN_OUT
    TappetConfig        : ARRAY[1..15] OF TappetConfigType;
END_VAR
VAR_OUTPUT
    bTappetOut          : ARRAY[1..15] OF BOOL;
END_VAR
VAR
    bPrevState          : ARRAY[1..15] OF BOOL;
    fPrevPosition       : LREAL;
    bDirectionPositive  : BOOL;
END_VAR

// 每个周期执行
IF bEnable THEN
    // 判断运动方向
    bDirectionPositive := (fMasterPosition >= fPrevPosition);
    fPrevPosition := fMasterPosition;
    
    FOR i := 1 TO 15 DO
        IF TappetConfig[i].bEnable THEN
            // 方向检查
            IF (TappetConfig[i].eDirection = ePositive AND NOT bDirectionPositive)
               OR (TappetConfig[i].eDirection = eNegative AND bDirectionPositive) THEN
                CONTINUE;
            END_IF;
            
            // 位置比较 + 迟滞
            IF fMasterPosition >= TappetConfig[i].fOnPosition 
               AND fMasterPosition <= (TappetConfig[i].fOnPosition 
                                       + TappetConfig[i].fHysteresis) THEN
                CASE TappetConfig[i].eAction OF
                    eSwitchON:  bTappetOut[i] := TRUE;
                    eSwitchOFF: bTappetOut[i] := FALSE;
                    eToggle:    bTappetOut[i] := NOT bPrevState[i];
                END_CASE
            END_IF;
            
            // 区间型:超出区间时关闭
            IF TappetConfig[i].fOffPosition > TappetConfig[i].fOnPosition THEN
                IF fMasterPosition > TappetConfig[i].fOffPosition THEN
                    bTappetOut[i] := FALSE;
                END_IF;
            END_IF;
            
            bPrevState[i] := bTappetOut[i];
        END_IF;
    END_FOR;
END_IF

8.4 配置示例

// 配置15组挺杆
TappetConfig[1].bEnable      := TRUE;
TappetConfig[1].sDescription := '胶枪开';
TappetConfig[1].fOnPosition  := 90;          // 90位置开胶枪
TappetConfig[1].fOffPosition := 120;         // 120位置关胶枪
TappetConfig[1].eAction      := eSwitchON;
TappetConfig[1].fHysteresis  := 0.5;

TappetConfig[2].bEnable      := TRUE;
TappetConfig[2].sDescription := '气缸顶出';
TappetConfig[2].fOnPosition  := 90;          // 和胶枪同位置触发
TappetConfig[2].eAction      := eToggle;     // 每次到90就取反
TappetConfig[2].fHysteresis  := 0.2;

// ... 配置到 TappetConfig[15]

8.5 调用方式

// 主程序中调用
MyTappetController(
    fMasterPosition  := MasterAxis.NcToPlc.ActPos,   // 直接读虚轴内存
    fMasterVelocity  := MasterAxis.NcToPlc.ActVel,
    bEnable          := TRUE,
    TappetConfig     := TappetConfig,
    bTappetOut       => bTappetOut
);

// 使用输出
bGlueGun   := bTappetOut[1];
bCylinder  := bTappetOut[2];
// ...

九、方案选型总结

场景推荐方案理由
虚轴 + 多组挺杆 + 位置可重合自封装功能块虚轴数据在内存中,自封装无限制,精度与官方一致
实轴 + 精度要求不高(几ms够用)MC_DigitalCamSwitch官方推荐,功能丰富,支持开/关补偿
实轴 + 需亚周期精度MC_DigitalCamSwitch_HighPrecision依赖驱动器硬件位置比较,微秒级响应
已有项目兼容维护SMC_CamRegister兼容性保留,可正常使用但不推荐新项目

自封装方案的核心优势:

  • 彻底绕开 SMC_CI_TOO_MANY_TAPPETS_PER_CYCLE — 自己写的循环,多少组都不限制
  • 不依赖凸轮表 — 挺杆位置直接在代码里配,修改方便
  • 代码透明 — 出了问题自己就能排查
  • 性能足够 — 虚轴场景下与官方功能块精度无差别

结语

回顾整个排查过程:从一次报错(629)出发,了解了 SMC_CamRegister 功能块的接口和用法,分析了错误根因(MC_CamIn单周期挺杆激活数限制),对比了官方推荐的 MC_DigitalCamSwitch,深入解析了底层处理原理,最终在”主轴是虚轴”这一关键信息下,找到了最优解——自封装功能块。

这个过程的核心方法论是:遇到问题不要只停留在”怎么解决”,而要追问”为什么会出现这个问题""官方方案底层是怎么做的""我的场景有什么特殊性”。 只有理解了底层原理,才能做出最优的技术选型。

希望这篇文章对正在做 CODESYS 电子凸轮项目的朋友有所帮助。如果有疑问或更好的方案,欢迎交流讨论。


— END —

← 返回博文列表