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×4=10000。如果采用1倍频,单圈脉冲数就是2500。很多新手在这里翻车:编码器手册上写的是“2500 P/R”,他直接把这个值当成单圈脉冲数写进程序,结果实际转一圈PLC计了10000个数,位置显示差了4倍。
注意:编码器铭牌上的“线数”或“脉冲数/转”通常指每转输出的方波周期数,不是PLC高速计数器实际计到的数值。必须结合PLC侧的倍频设置来换算。
方向识别方面,如果PLC的高速计数器硬件已经自动处理了方向(比如S7-1200的HSC在AB相计数模式下会自动增减),那SCL程序里只需要读取计数值即可。但如果你用的是普通数字量输入点做软件计数,那就需要在SCL里判断A/B相的上升沿顺序来确定方向。后者对扫描周期要求极高,一般只用于低速场合。
2.2 单圈脉冲数的确定方法与校验
单圈脉冲数(Pulses Per Revolution,PPR)是整个算法的基础常数。它的确定有三种途径:
- 理论计算法:编码器线数 × 倍频数。比如500线编码器配4倍频,PPR=2000。
- 实测法:手动转动编码器轴整一圈,读取高速计数器计数值的变化量。这是最可靠的方法,能发现理论计算中忽略的细节。
- 手册查表法:部分绝对值编码器会直接给出“每圈分辨率”参数,比如17位单圈绝对值编码器,PPR=2^17=131072。
实测法的操作步骤很简单:在博途里建一个监控表,把高速计数器的当前值读出来,手动把编码器轴旋转整整一圈(可以用联轴器上的标记对齐),记录前后差值。如果差值等于理论值,说明配置正确;如果差值是理论值的整数倍或分数,说明倍频设置或线数理解有误。
我个人的经验是,对于皮带输送、辊道这类应用,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对应-2048,4095对应-1)。处理方法是:
IF MultiTurn > 2047 THEN MultiTurn := MultiTurn - 4096; END_IF;不做这一步,反转时圈数会从0直接跳到4095,HMI上显示的位置会瞬间“飞”到几万圈之外,操作工看到能吓一跳。
3. SCL算法整体架构设计
3.1 功能块划分与数据流设计
整个算法我习惯封装成一个FB(功能块),命名为FB_EncoderCalc,对外接口包括:
| 接口变量 | 数据类型 | 说明 |
|---|---|---|
| RawCount | DINT | 高速计数器原始值或绝对值编码器解析后的总脉冲 |
| PPR | DINT | 单圈脉冲数 |
| Reset | BOOL | 位置清零信号 |
| CurrentTurn | DINT | 输出:当前圈数 |
| CurrentPulse | DINT | 输出:圈内脉冲位置 |
| TotalPulse | DINT | 输出:总脉冲数(可正可负) |
| Direction | BOOL | 输出:当前旋转方向 |
数据流是这样的:原始计数值进入FB后,先做溢出处理和方向判断,然后除以PPR得到圈数和余数。余数就是圈内脉冲位置,商就是圈数。但这里有个细节——当计数值为负时,整数除法的余数在SCL里是带符号的,需要额外处理才能得到正确的圈内位置。
3.2 溢出与边界处理策略
高速计数器的计数值通常是DINT(32位有符号整数),范围是-2147483648到2147483647。对于高PPR编码器,比如PPR=131072,32位计数器只能记录约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 = -1,PPR = 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转/分,PPR=1000,脉冲频率=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. 实际项目中的经验体会
我在多个产线项目里用过这套算法,从简单的输送带定位到复杂的多轴同步,整体表现稳定。最让我印象深刻的是一个卷绕设备,编码器装在卷轴上,PPR=3600,卷轴转速最高300转/分。最初用软件计数,高速时丢脉冲严重,后来改用硬件高速计数器+这套SCL算法,位置精度控制在±1个脉冲以内,完全满足工艺要求。
另一个体会是:参数可视化太重要了。我在HMI上专门做了一个调试页面,显示原始计数值、总脉冲、圈数、圈内脉冲、速度、方向这些量。调试时一眼就能看出问题出在哪一层。比如原始计数值正常但总脉冲不对,那就是溢出处理有问题;总脉冲正常但圈数不对,那就是PPR或除法逻辑有问题。这个页面花了我半小时做,但省下的调试时间至少几十小时。
最后分享一个小技巧:如果编码器线数不是2的整数次幂(比如360线、720线),PPR算出来可能不是整数。这时候不要用浮点数,而是把PPR放大10倍或100倍,用整数运算,最后显示时再缩小。比如PPR=3600,放大10倍变成36000,总脉冲也放大10倍,圈数计算不受影响,圈内脉冲显示时除以10即可。这样既保证了精度,又避免了浮点运算的误差。