1. 项目背景与核心需求拆解在工业自动化现场编码器就像设备的“眼睛”它把机械轴的旋转角度、速度、位移这些物理量转换成PLC能读懂的脉冲信号。但问题来了——PLC读到的永远只是一串脉冲数而操作人员关心的是“转了多少圈”“走了多少毫米”“当前角度是多少”。这中间就隔着一层换算逻辑这层逻辑写得好不好直接决定了整个运动控制系统的精度和稳定性。我接触过不少项目发现很多人在博途里处理编码器数据时习惯性地把高速计数器读到的值直接除以一个固定常数然后送到HMI显示。这种做法在低速、短距离场景下勉强能用一旦遇到多圈绝对值编码器、变速运行、或者需要断电位置保持的场合就会暴露出各种问题圈数跳变、脉冲累积误差、正反转计数混乱等等。说到底是没有把“单圈脉冲数”和“转动圈数”这两个核心量拆开处理。这个项目的核心目标很明确在博途SCL环境下实现一套通用的编码器圈数与单圈脉冲数计算算法。它要能处理增量式编码器的A/B相正交信号也要能兼容绝对值编码器的多圈数据解析要能实时计算当前圈数和圈内脉冲位置还要能处理溢出、方向翻转、断电重启后的位置恢复。适合的读者是那些已经会用博途基本指令、但对SCL结构化编程还不太熟练的自动化工程师以及需要做运动控制但不想依赖工艺对象TO的开发者。为什么选择SCL而不是梯形图因为这类算法涉及大量条件判断、数据类型转换、边界处理用梯形图写出来会变成一张“蜘蛛网”后期维护极其痛苦。SCL的文本化表达更接近高级语言逻辑清晰调试方便而且博途对SCL的编译优化做得不错执行效率完全能满足毫秒级扫描周期的要求。2. 编码器信号基础与核心参数解析2.1 增量式编码器的脉冲与方向识别增量式编码器输出的是A相、B相两路方波信号两者相位差90度。正转时A相超前B相反转时B相超前A相。PLC的高速计数器模块通常支持四种计数模式单相计数、双相计数1倍频、双相计数2倍频、双相计数4倍频。这里有个关键选择——倍频数直接决定了单圈脉冲数的计算基准。举个例子一个标称2500线的增量式编码器如果采用4倍频计数PLC每收到一个完整的A/B周期会计4个数那么单圈脉冲数就是2500×410000。如果采用1倍频单圈脉冲数就是2500。很多新手在这里翻车编码器手册上写的是“2500 P/R”他直接把这个值当成单圈脉冲数写进程序结果实际转一圈PLC计了10000个数位置显示差了4倍。注意编码器铭牌上的“线数”或“脉冲数/转”通常指每转输出的方波周期数不是PLC高速计数器实际计到的数值。必须结合PLC侧的倍频设置来换算。方向识别方面如果PLC的高速计数器硬件已经自动处理了方向比如S7-1200的HSC在AB相计数模式下会自动增减那SCL程序里只需要读取计数值即可。但如果你用的是普通数字量输入点做软件计数那就需要在SCL里判断A/B相的上升沿顺序来确定方向。后者对扫描周期要求极高一般只用于低速场合。2.2 单圈脉冲数的确定方法与校验单圈脉冲数Pulses Per RevolutionPPR是整个算法的基础常数。它的确定有三种途径理论计算法编码器线数 × 倍频数。比如500线编码器配4倍频PPR2000。实测法手动转动编码器轴整一圈读取高速计数器计数值的变化量。这是最可靠的方法能发现理论计算中忽略的细节。手册查表法部分绝对值编码器会直接给出“每圈分辨率”参数比如17位单圈绝对值编码器PPR2^17131072。实测法的操作步骤很简单在博途里建一个监控表把高速计数器的当前值读出来手动把编码器轴旋转整整一圈可以用联轴器上的标记对齐记录前后差值。如果差值等于理论值说明配置正确如果差值是理论值的整数倍或分数说明倍频设置或线数理解有误。我个人的经验是对于皮带输送、辊道这类应用PPR的微小误差会随着运行距离累积跑个几百米后位置偏差可能达到几厘米。所以每次项目调试我都会花五分钟做一次实测校验把实测PPR写进DB块作为最终参数而不是直接用理论值。2.3 多圈绝对值编码器的数据格式多圈绝对值编码器输出的不是脉冲序列而是一帧包含单圈位置和多圈计数的数据。以常见的SSI或BiSS协议为例一帧数据通常是25位或29位其中低位是单圈位置比如13位对应8192个位置高位是圈数比如12位对应4096圈。在SCL里处理这类数据核心是位提取和拼接。假设收到一个32位双字RawData单圈占低13位圈数占高12位那么SingleTurn : RawData AND 16#1FFF; // 取低13位 MultiTurn : (RawData SHR 13) AND 16#0FFF; // 取高12位这里有个坑圈数是有符号的正转增加、反转减少但原始数据里圈数通常以无符号格式存储。如果设备需要双向旋转必须做符号扩展。比如12位圈数当值大于2047时实际代表的是负数2048对应-20484095对应-1。处理方法是IF MultiTurn 2047 THEN MultiTurn : MultiTurn - 4096; END_IF;不做这一步反转时圈数会从0直接跳到4095HMI上显示的位置会瞬间“飞”到几万圈之外操作工看到能吓一跳。3. SCL算法整体架构设计3.1 功能块划分与数据流设计整个算法我习惯封装成一个FB功能块命名为FB_EncoderCalc对外接口包括接口变量数据类型说明RawCountDINT高速计数器原始值或绝对值编码器解析后的总脉冲PPRDINT单圈脉冲数ResetBOOL位置清零信号CurrentTurnDINT输出当前圈数CurrentPulseDINT输出圈内脉冲位置TotalPulseDINT输出总脉冲数可正可负DirectionBOOL输出当前旋转方向数据流是这样的原始计数值进入FB后先做溢出处理和方向判断然后除以PPR得到圈数和余数。余数就是圈内脉冲位置商就是圈数。但这里有个细节——当计数值为负时整数除法的余数在SCL里是带符号的需要额外处理才能得到正确的圈内位置。3.2 溢出与边界处理策略高速计数器的计数值通常是DINT32位有符号整数范围是-2147483648到2147483647。对于高PPR编码器比如PPR13107232位计数器只能记录约16384圈就会溢出。如果设备需要长时间连续运行溢出处理必不可少。我的做法是在FB内部维护一个“溢出计数”变量。每次扫描周期比较当前原始值和上一周期原始值如果差值超过PPR的一半比如从正最大值跳到负最大值就认为发生了一次正向溢出溢出计数加1反之减1。最终总脉冲数 溢出计数 × 2^32 当前原始值。提示溢出判断的阈值设为PPR的一半是个经验值。设太小会误判正常高速旋转时单周期变化量可能超过阈值设太大则可能漏判。对于转速极高的场合建议直接用64位整数LINT做累加彻底避免溢出问题。3.3 断电位置保持的实现很多设备要求断电后重新上电位置不能丢。对于增量式编码器如果PLC断电期间编码器轴发生了转动上电后位置就丢了。解决办法有两种一是用绝对值编码器二是给编码器加UPS电源并在PLC里做断电中断保存。绝对值编码器方案最简单上电后直接读取当前圈数和单圈位置不需要归零。增量式方案则需要在OB100启动组织块里做位置恢复或者在OB82诊断中断里做保存。我通常会在FB里加一个Retain保持性DB每秒钟把当前总脉冲数写入一次上电时再读回来作为初始值。但要注意如果断电期间轴转动了这个值就是错的所以必须配合机械归零开关使用。4. 核心算法实现与代码详解4.1 圈数与圈内脉冲的分离计算这是整个算法的核心。假设TotalPulse是累计总脉冲数可正可负PPR是单圈脉冲数那么// 计算圈数和圈内脉冲 #TempTurn : #TotalPulse / #PPR; // 整数除法得到圈数 #TempPulse : #TotalPulse MOD #PPR; // 取模得到圈内脉冲 // 处理负数情况 IF #TempPulse 0 THEN #TempTurn : #TempTurn - 1; #TempPulse : #TempPulse #PPR; END_IF; #CurrentTurn : #TempTurn; #CurrentPulse : #TempPulse;为什么负数要特殊处理举个例子TotalPulse -1PPR 1000。在SCL里-1 / 1000 0整数除法向零取整-1 MOD 1000 -1。这意味着圈数是0圈内脉冲是-1这显然不对——实际应该是圈数-1圈内脉冲999。所以必须做修正。这个修正逻辑我见过很多人写错包括一些工作多年的工程师。他们要么忘了处理负数要么用浮点数做除法再取整结果在边界值上出现跳变。用整数除法和MOD配合修正是最稳妥的方案。4.2 方向判断与正反转计数如果PLC硬件已经处理了方向TotalPulse本身就是带符号的正转增加、反转减少方向判断只需要看当前值和上一周期值的差值IF #TotalPulse #LastTotalPulse THEN #Direction : TRUE; // 正转 ELSIF #TotalPulse #LastTotalPulse THEN #Direction : FALSE; // 反转 END_IF; #LastTotalPulse : #TotalPulse;但如果用的是软件计数普通输入点就需要在SCL里检测A/B相的边沿。这里分享一个稳定的实现方法用两个边沿检测指令分别捕捉A相和B相的上升沿然后判断哪个先到。#A_Rising : #A_Phase AND NOT #A_Last; #B_Rising : #B_Phase AND NOT #B_Last; IF #A_Rising AND NOT #B_Phase THEN #TotalPulse : #TotalPulse 1; // 正转 ELSIF #B_Rising AND NOT #A_Phase THEN #TotalPulse : #TotalPulse - 1; // 反转 END_IF; #A_Last : #A_Phase; #B_Last : #B_Phase;注意软件计数对扫描周期极其敏感。如果PLC扫描周期大于编码器脉冲间隔的一半就会丢脉冲。比如编码器转速1500转/分PPR1000脉冲频率25kHz周期40微秒。而S7-1200的典型扫描周期是1-10毫秒根本来不及响应。所以软件计数只适用于转速低于10转/分的场合高速场合必须用硬件高速计数器。4.3 多圈绝对值编码器的SCL解析对于SSI绝对值编码器博途通常通过工艺对象或专用通讯块读取原始数据。假设我们已经拿到了32位原始数据RawData单圈13位、圈数12位、其余为状态位解析代码如下// 提取单圈位置低13位 #SingleTurn : #RawData AND 16#1FFF; // 提取圈数高12位从第13位开始 #MultiTurn : (SHR(IN : #RawData, N : 13)) AND 16#0FFF; // 圈数符号扩展12位有符号 IF #MultiTurn 2047 THEN #MultiTurn : #MultiTurn - 4096; END_IF; // 计算总脉冲数以单圈脉冲为基准 #TotalPulse : #MultiTurn * 8192 #SingleTurn;这里8192是2的13次方对应单圈分辨率。如果编码器是17位单圈那就改成131072。总脉冲数算出来后再套用前面4.1节的圈数分离逻辑就能得到统一的圈数和圈内位置输出。4.4 位置清零与预设值功能实际项目中操作工经常需要把当前位置设为“0”或者某个预设值。这个功能在SCL里实现很简单但要注意清零的时机和方式。IF #Reset THEN #Offset : #TotalPulse; // 记录当前值为偏移量 #Reset : FALSE; // 复位触发信号 END_IF; #DisplayPulse : #TotalPulse - #Offset; // 显示值 实际值 - 偏移量用偏移量而不是直接清零TotalPulse的好处是清零操作不影响底层计数逻辑不会造成脉冲丢失或跳变。而且可以随时恢复原始值方便调试。预设值功能同理把Offset设成TotalPulse - PresetValue即可。我通常会在HMI上做一个“示教”按钮操作工把设备手动移到目标位置按下按钮当前位置就被设为预设值。这比让操作工输入数字靠谱得多。5. 实操调试与常见问题排查5.1 博途项目配置要点在博途里配置高速计数器时有几个参数必须和SCL算法匹配计数模式选择“AB相正交计数”还是“单相计数”决定了PPR的计算方式。倍频因子1倍频、2倍频、4倍频直接影响PPR。输入滤波编码器信号频率高时滤波时间要设小比如0.1微秒否则会丢脉冲。计数方向有些模块可以反转计数方向如果发现正转时计数值减小改这个参数比改程序快。我习惯在PLC变量表里给高速计数器的当前值定义一个符号名比如HSC1_Count然后在SCL里直接引用。这样比用绝对地址ID1000可读性好得多后期换模块也不用改程序。5.2 典型问题速查表现象可能原因排查方法圈数跳变忽大忽小溢出处理逻辑错误监控原始计数值看是否在边界附近跳变位置显示差整数倍PPR设置错误实测单圈脉冲数对比理论值反转时圈数从0跳到最大值圈数未做符号扩展检查绝对值编码器解析代码低速时正常高速时丢脉冲输入滤波时间过大减小滤波时间或提高计数器响应频率断电后位置丢失未做保持性存储检查DB块的Retain属性HMI显示负数圈内脉冲负数取模未修正检查4.1节的修正逻辑5.3 调试心得与避坑经验第一个坑PPR不要用常数要用变量。我早期做项目时把PPR写成常数直接嵌在代码里。后来客户换了不同线数的编码器程序要重新下载产线停了半小时。从那以后我把所有编码器参数都放在DB块里HMI上可以修改换编码器只需要改一个数字。第二个坑注意DINT除法的性能。SCL里的整数除法在S7-1200上大约需要几百微秒如果FB在每个扫描周期都调用而且PPR是变量累积起来可能影响扫描周期。优化方法是把圈数计算放在定时中断里比如10ms一次而不是主循环里每周期都算。第三个坑绝对值编码器的数据更新率。SSI编码器通常每帧数据需要几十微秒到几百微秒如果PLC读取太快可能读到重复数据或半帧数据。博途的SSI通讯块一般有“数据有效”标志位必须等这个位为TRUE再解析数据否则会出现位置随机跳变。第四个坑清零信号的防抖。HMI上的清零按钮如果没做防抖操作工按一下可能触发多次清零导致位置偏移量被反复覆盖。我通常会在SCL里加一个上升沿检测确保一次按下只执行一次清零。6. 算法扩展与性能优化6.1 支持多轴编码器的结构化封装一个项目里往往不止一个编码器。如果每个编码器都复制一份FB代码会非常臃肿。更好的做法是用多重背景DB把FB_EncoderCalc实例化多次。// 在OB1中调用 Encoder1_DB(RawCount : HSC1_Count, PPR : 2500, ...); Encoder2_DB(RawCount : HSC2_Count, PPR : 1000, ...);每个实例有独立的背景数据互不干扰。如果轴数很多比如10轴以上还可以用数组循环的方式批量处理但要注意SCL的循环在扫描周期内执行轴数太多时可能超时。6.2 速度计算与滤波有了圈数和时间算速度就是顺带的事。速度 (当前总脉冲 - 上一周期总脉冲) / 扫描周期 / PPR单位是转/秒。但原始速度值通常抖动很大需要滤波。我常用的一阶低通滤波公式#Speed_Filtered : #Speed_Filtered * 0.9 #Speed_Raw * 0.1;系数0.9和0.1可以根据实际需要调整。系数越大滤波效果越强但响应越慢。对于需要快速响应的场合比如伺服定位滤波系数不要超过0.7。6.3 与运动控制工艺对象的取舍博途有现成的运动控制工艺对象TO配置好编码器后自动就能得到位置和速度。那为什么还要自己写SCL算法原因有几个一是TO占用资源较多每个轴都要一个TO实例PLC轴数有限二是TO的灵活性不如自定义FB比如你想在位置基础上做自定义补偿、非线性校正TO很难实现三是有些老型号PLC不支持TO只能用SCL自己算。我的建议是如果只是简单的显示和逻辑判断用SCL自定义FB足够如果需要做插补、凸轮同步、电子齿轮这些高级运动控制还是老老实实用TO。6.4 精度分析与误差控制整个算法的精度取决于三个因素编码器分辨率、PPR的准确性、以及计算过程中的舍入误差。编码器分辨率是硬件决定的没法改。PPR的准确性可以通过实测校验来保证。舍入误差主要来自整数除法和MOD运算但因为我们用的是整数运算只要PPR是整数就不会有累积误差。唯一需要注意的是如果PPR不是整数比如某些特殊编码器就必须用浮点数运算这时候要小心浮点数的精度问题。REAL类型只有24位有效数字对于大脉冲数比如超过100万浮点数的分辨率可能不够会导致圈内脉冲位置跳动。解决办法是用LINT做整数运算或者用双精度浮点数LREAL。7. 完整FB代码框架与调用示例7.1 FB接口定义FUNCTION_BLOCK FB_EncoderCalc VAR_INPUT RawCount : DINT; // 原始计数值 PPR : DINT : 1000; // 单圈脉冲数 Reset : BOOL; // 清零信号 PresetValue : DINT : 0; // 预设值 PresetTrig : BOOL; // 预设触发 END_VAR VAR_OUTPUT CurrentTurn : DINT; // 当前圈数 CurrentPulse : DINT; // 圈内脉冲 TotalPulse : DINT; // 总脉冲含偏移 Direction : BOOL; // 方向 Speed_RPM : REAL; // 转速 END_VAR VAR LastRawCount : DINT; Offset : DINT; OverflowCount : DINT; LastTotalPulse : DINT; Speed_Filtered : REAL; END_VAR VAR_TEMP TempTurn : DINT; TempPulse : DINT; DeltaPulse : DINT; END_VAR7.2 核心逻辑实现// 溢出处理 IF (#RawCount - #LastRawCount) 1073741824 THEN #OverflowCount : #OverflowCount - 1; ELSIF (#LastRawCount - #RawCount) 1073741824 THEN #OverflowCount : #OverflowCount 1; END_IF; #LastRawCount : #RawCount; // 计算总脉冲 #TotalPulse : #OverflowCount * 4294967296 #RawCount - #Offset; // 清零处理 IF #Reset THEN #Offset : #OverflowCount * 4294967296 #RawCount; #TotalPulse : 0; END_IF; // 预设值处理 IF #PresetTrig THEN #Offset : #OverflowCount * 4294967296 #RawCount - #PresetValue; #TotalPulse : #PresetValue; END_IF; // 圈数分离 #TempTurn : #TotalPulse / #PPR; #TempPulse : #TotalPulse MOD #PPR; IF #TempPulse 0 THEN #TempTurn : #TempTurn - 1; #TempPulse : #TempPulse #PPR; END_IF; #CurrentTurn : #TempTurn; #CurrentPulse : #TempPulse; // 方向判断 IF #TotalPulse #LastTotalPulse THEN #Direction : TRUE; ELSIF #TotalPulse #LastTotalPulse THEN #Direction : FALSE; END_IF; // 速度计算假设FB在10ms中断中调用 #DeltaPulse : #TotalPulse - #LastTotalPulse; #Speed_RPM : (#DeltaPulse * 6000.0) / (#PPR * 10.0); #Speed_Filtered : #Speed_Filtered * 0.8 #Speed_RPM * 0.2; #LastTotalPulse : #TotalPulse;7.3 调用注意事项这个FB建议放在10ms的循环中断OB里调用而不是主程序OB1。因为主程序扫描周期不固定速度计算会不准。放在中断里周期固定速度计算精度有保证。如果PPR可能为0比如HMI上操作工误删了参数除法会报错。所以调用前要加保护IF #PPR 0 THEN // 调用FB END_IF;另外OverflowCount * 4294967296这个乘法在DINT范围内可能溢出。如果设备运行圈数极多建议把TotalPulse改成LINT类型或者用REAL类型但注意精度。对于绝大多数工业场景DINT的21亿脉冲范围足够跑几百万圈了。8. 实际项目中的经验体会我在多个产线项目里用过这套算法从简单的输送带定位到复杂的多轴同步整体表现稳定。最让我印象深刻的是一个卷绕设备编码器装在卷轴上PPR3600卷轴转速最高300转/分。最初用软件计数高速时丢脉冲严重后来改用硬件高速计数器这套SCL算法位置精度控制在±1个脉冲以内完全满足工艺要求。另一个体会是参数可视化太重要了。我在HMI上专门做了一个调试页面显示原始计数值、总脉冲、圈数、圈内脉冲、速度、方向这些量。调试时一眼就能看出问题出在哪一层。比如原始计数值正常但总脉冲不对那就是溢出处理有问题总脉冲正常但圈数不对那就是PPR或除法逻辑有问题。这个页面花了我半小时做但省下的调试时间至少几十小时。最后分享一个小技巧如果编码器线数不是2的整数次幂比如360线、720线PPR算出来可能不是整数。这时候不要用浮点数而是把PPR放大10倍或100倍用整数运算最后显示时再缩小。比如PPR3600放大10倍变成36000总脉冲也放大10倍圈数计算不受影响圈内脉冲显示时除以10即可。这样既保证了精度又避免了浮点运算的误差。