做运动控制的人,应该都遇到过这个需求:想知道电机轴究竟转了多少圈,还想知道当前圈内的精确位置。增量式编码器就是干这个的,但编码器本身输出的只是一串脉冲,真正要得到“转动圈数”和“单圈脉冲数”,还得在PLC里做一层换算。这篇内容围绕博途(TIA Portal)SCL编程,把编码器脉冲计数转成圈数和单圈位置的核心算法讲透,包括原理、SCL实现、溢出处理、正反转负数取模,以及现场调试最容易踩的坑。
如果你正在用S7-1200/1500做运动控制、卷径计算、角度定位或者位置反馈,尤其需要把高速计数器的数值变成“第几圈、圈内第几个脉冲”,这篇文章可以直接当参考代码用。
1. 编码器与计数原理:先搞清楚脉冲和圈数的关系
1.1 增量式编码器是怎么数圈的
增量式编码器最常见的输出是A、B、Z三路信号。A相和B相互差90度相位,通过判断哪个相超前,PLC或高速计数器就能知道旋转方向;Z相每转一圈输出一个脉冲,通常用来给单圈位置做零点参考。
编码器有一个重要参数叫分辨率,也叫线数,比如常见的1000线、1024线、2500线、4096线。这里的“线数”指的是编码器每转一圈A相或B相输出的脉冲个数。注意,如果使用A/B两相正交计数并且采用4倍频,那么高速计数器实际每圈得到的脉冲数是线数的4倍。比如2500线编码器接成4倍频,每圈就是10000个脉冲。这是后面算法里每圈脉冲数(PPR)一定要填对的地方,很多人刚开始就把这个数搞错了。
那为什么需要“圈数 + 单圈脉冲”而不直接用计数器的累计值?因为一个高速计数器值本身只是一个“总脉冲数”。比如当前计数值是12345,PPR是1024,那么计算器会告诉你转过了12圈,还多出57个脉冲。如果不拆开,你想显示“当前第几圈、圈内位置”就很麻烦,而且用总脉冲数除以PPR得到的浮点数可能包含小数,做精确定位时容易引入误差。
1.2 从物理脉冲到PLC计数:高速计数器的作用
PLC普通数字量输入模块扫描周期太慢,编码器转速一高,脉冲就丢了。所以必须用CPU自带的高速计数器(HSC)或者独立的计数模块。高速计数器依靠硬件逻辑计数,不受PLC扫描周期影响,编码器转得再快,它也能把脉冲数准确记下来。
在博途里,S7-1200/1500可以通过设备组态启用高速计数器,设置计数模式、方向、硬件中断等。组态完成后,计数器当前值会映射到某个输入地址,比如%ID1000。SCL程序只需要周期性地读取这个地址里的DInt值,然后交给算法处理。
这里有一个容易忽略的点:高速计数器计数范围通常是32位有符号数,最大值大概21亿多,最小值负21亿多。如果编码器长期高速旋转,总脉冲数最终会超出这个范围,计数器会溢出回绕。所以SCL算法里不能只简单地做“当前值除以PPR”,必须有防溢出处理。后面我会专门讲这个。
2. 算法设计:圈数、方向、单圈脉冲的换算逻辑
2.1 核心算法思路:累计脉冲再拆圈和余数
算法最朴素的做法是:把每次扫描周期读到的当前计数值和上次值相减,得到这个周期内的脉冲增量Delta;然后把Delta累加到一个大的累计变量里;最后用累计脉冲数除以每圈脉冲数,商是圈数,余数是单圈脉冲。
用公式表达:
- 累计脉冲 = 累计脉冲 + (当前计数值 - 上次计数值)
- 整圈数 = 累计脉冲 / PPR 的整数部分
- 单圈脉冲 = 累计脉冲 mod PPR
为什么不直接用绝对计数值除以PPR?因为高速计数器会溢出,溢出后当前值会发生很大跳变,直接除以PPR就全乱了。而用增量累加的方式,即使32位计数器来回溢出,只要每次周期内增量判断正确,累计值仍然可以准确反映真实总脉冲数。后续如果需要支持掉电保持,也只需要保持累计值这一个变量。
还有一点:如果PLC是首次上电,或者程序从停止转为运行,这时候不能上来就算增量,因为上一次计数值可能是以前的数据,算出来的Delta没有任何意义。所以算法里需要一个初始化标志,第一次执行时把当前计数值存入旧值,增量记为0,不做累加。
2.2 方向判断和负数处理:防止圈数乱跳
编码器正转时,计数器值增加,Delta大于0;反转时,计数器值减少,Delta小于0。这个方向信息很重要,因为圈数是有正负的,代表正转或反转的圈数。
难点在于负数取余。例如累计脉冲是-1,PPR是1024,按照数学上的取模,-1 mod 1024应该等于1023,也就是从零点反向走了1个脉冲后,位置应当落在第1023个脉冲处。整圈数是多少呢?应该是-1圈加1023个脉冲,也就是说“反向走了1个脉冲”可以理解成先反向转1圈,再正向走1023个脉冲。所以圈数是-1,单圈脉冲是1023。
如果直接用高级语言的取余函数,很多情况下负数的余数会是负数,比如-1 mod 1024得到-1,这样就出现了“圈数为0、单圈脉冲为-1”的不合理状态。所以取余后必须做归一化修正:判断单圈脉冲是否小于0,如果是,则圈数减1,单圈脉冲加PPR;判断单圈脉冲是否大于等于PPR,如果溢出,则圈数加1,单圈脉冲减PPR。
我见过不少现场程序,只考虑了正转,一反转就出现圈数和位置对不上。其实只要把余数归一化这一小步做了,正反转都能稳定。
2.3 32位溢出处理:计数器跳变不要慌
高速计数器32位有符号数一旦达到临界值,再走一个脉冲就会从最大值翻到最小值,或者从最小值翻到最大值。如果程序只简单做“当前计数值 - 上次计数值”,这个Delta会变成巨大的数,累计值就错了。
处理思路是:因为两次扫描周期内,编码器转过的脉冲数通常远远小于32位计数范围的一半,所以一旦发现Delta超过某一阈值,就说明发生了溢出回绕,而不是真的转了这么多脉冲。具体做法:
- 如果Delta > 2147483647,说明实际是正转越过最大值回绕到最小值,应把Delta减去4294967296。
- 如果Delta < -2147483648,说明实际是反转越过最小值回绕到最大值,应把Delta加上4294967296。
比如从2147483647变为-2147483648,表面上的Delta等于-4294967295,修正后为1。这个修正很关键,否则累计值会瞬间错乱。
在实际工业现场,一个PLC扫描周期(一般几毫秒到几十毫秒)内,编码器脉冲变化很难超过10亿个,所以这个判断是可靠的。如果你用循环中断OB且高速转,也可以留出更大的阈值余量,但哪怕按半值判断,已经足够覆盖绝大多数场景。
3. 博途SCL代码实现与逐行解读
3.1 变量定义与初始化
我建议把算法封装成一个独立的函数块,比如叫FB_EncoderCalc。这样不管是哪个轴、哪台设备,只要填上每次读到的计数当前值,就能得到圈数和单圈脉冲,便于复用。
输入变量:
bEnable:使能信号uiPPR:每圈实际脉冲数,已经包含4倍频diCurCnt:高速计数器当前值
输出变量:
diTurns:整圈数diPosInRev:单圈脉冲位置,范围0到uiPPR-1rAngleDeg:换算出的角度,便于监控
静态变量:
xInit:初始化标志rOldCnt:上次读取的计数值,用LREAL存储rTotalPulses:累计脉冲,用LREAL存储,避免32位溢出rDelta:单周期脉冲增量rTurnsFloat:累计脉冲换算成圈数的浮点结果diTempTurns:临时整圈数rTempPos:临时单圈位置
用LREAL而不是DInt累加,主要有两个原因:一是LREAL是64位浮点,范围极大,不会轻易溢出;二是做除法、取余数时不需要频繁处理整数转换边界。当然,如果项目要求高精度且需要长时间累计,也可以改用LInt(64位整数),但考虑到S7-1200对LInt支持有限,LREAL在实际工程中足够可靠。
3.2 核心SCL代码
下面这段代码可以在S7-1500或者S7-1200的SCL中直接使用。我故意把每行都做了注释,方便你移植。
FUNCTION_BLOCK FB_EncoderCalc VAR_INPUT bEnable : BOOL; // 使能 uiPPR : DINT; // 每圈实际脉冲数,已含倍频 diCurCnt : DINT; // 当前高速计数器计数值 END_VAR VAR_OUTPUT diTurns : DINT; // 整圈数 diPosInRev : DINT; // 单圈内脉冲位置 0 ~ uiPPR-1 rAngleDeg : LREAL; // 换算后的角度,仅用于监控 END_VAR VAR xInit : BOOL; rOldCnt : LREAL; rTotalPulses: LREAL; rDelta : LREAL; rTurnsFloat : LREAL; diTempTurns : DINT; rTempPos : LREAL; END_VAR BEGIN // 使能关闭时不做累加,但保持输出不变 IF NOT #bEnable THEN RETURN; END_IF; // 首次调用初始化 IF NOT #xInit THEN #rOldCnt := DINT_TO_LREAL(#diCurCnt); #rTotalPulses := 0.0; #xInit := TRUE; RETURN; END_IF; // 本周期增量 = 当前值 - 上次值 #rDelta := DINT_TO_LREAL(#diCurCnt) - #rOldCnt; // 处理32位计数器溢出回绕 IF #rDelta > 2147483647.0 THEN #rDelta := #rDelta - 4294967296.0; END_IF; IF #rDelta < -2147483648.0 THEN #rDelta := #rDelta + 4294967296.0; END_IF; // 更新旧值 #rOldCnt := DINT_TO_LREAL(#diCurCnt); // 累计总脉冲数 #rTotalPulses := #rTotalPulses + #rDelta; // 换算圈数和单圈位置 #rTurnsFloat := #rTotalPulses / DINT_TO_LREAL(#uiPPR); // TRUNC是向零取整,负数时圈数可能偏大,后面修正 #diTempTurns := TRUNC(#rTurnsFloat); // 计算余数(单圈脉冲) #rTempPos := #rTotalPulses - DINT_TO_LREAL(#diTempTurns) * DINT_TO_LREAL(#uiPPR); #diPosInRev := LREAL_TO_DINT(#rTempPos); // 负数余数归一化 IF #diPosInRev < 0 THEN #diTempTurns := #diTempTurns - 1; #diPosInRev := #diPosInRev + #uiPPR; END_IF; // 浮点误差可能让结果超过PPR,做一个保护性修正 IF #diPosInRev >= #uiPPR THEN #diTempTurns := #diTempTurns + 1; #diPosInRev := #diPosInRev - #uiPPR; END_IF; // 输出 #diTurns := #diTempTurns; #rAngleDeg := DINT_TO_LREAL(#diTurns) * 360.0 + DINT_TO_LREAL(#diPosInRev) * 360.0 / DINT_TO_LREAL(#uiPPR); END_FUNCTION_BLOCK代码逻辑并不复杂:先算增量,再修正溢出,然后累加,最后拆出圈数和单圈位置。值得留意的是TRUNC向零取整对负数不友好,所以后面必须有“单圈脉冲小于0就借位”的处理。这段代码我在实际项目里用过多次,正反转、快速启停都稳定。
3.3 高速计数器的读取与组态
函数块写好了,怎么把高速计数器值喂进来?这就涉及博途的HSC组态。
在设备组态中启用HSC后,你可以看到它分配的输入地址,比如%ID1000。在OB1或者在定时中断OB里,直接把这个地址赋值给函数块的输入变量:
"FB_EncoderCalc".bEnable := TRUE; "FB_EncoderCalc".uiPPR := 10000; // 2500线编码器4倍频 "FB_EncoderCalc".diCurCnt := %ID1000; // HSC当前计数值 "FB_EncoderCalc"();%ID1000这种绝对地址写法在SCL里可以直接使用,如果需要跨OB访问,也可以把HSC计数值映射到DB变量,再从DB读取。
建议在循环中断OB中调用这个FB,比如OB200,循环时间10ms或20ms。这样算法执行频率固定,监控和计算都更有规律。如果放在OB1里,循环时间会随程序大小波动,虽然一般不影响结果,但调试时看到的角度或者位置曲线可能带有细微抖动。
4. 实际调试中的常见问题与排查技巧
4.1 正反转时计数值跳变怎么办
有时候监控发现,电机刚开始反转,diCurCnt却先增加再减少,或者出现一瞬间很大的正增量。最常见原因是高速计数器的计数方向组态和编码器接线方向不一致。A/B相接反了,正转变反转,反转变正转。
排查时先不要看算法,直接监控高速计数器的原始当前值。手动缓慢旋转电机轴,看计数是增大还是减小。如果方向和期望相反,最简单的方法是在HSC组态里修改计数方向,或者交换A/B相接线。改完以后,再监控diTurns是否随正反转正确增减。
还有一种情况:程序读取HSC地址的时机和计数器翻转正好碰上,导致这次读到的值和上一次值之间存在一个“半周期”的相位差。这种跳动通常很小,在位置控制场景里可以通过延长采样周期或增加滤波来缓解。但要注意,滤波加多了会影响动态响应,要在跟随性和稳定性之间取平衡。
4.2 脉冲丢失和干扰导致的计算误差
如果你发现实际转了10圈,但diTurns只有9.8圈,那多半是物理层有脉冲丢失。注意,高速计数器硬件计数不会因为扫描周期慢而丢脉冲,它丢脉冲通常是以下几个原因:
- 编码器线缆屏蔽层没有可靠接地。
- HTL信号和PLC输入接口不匹配,比如用了PNP输出却接成了NPN。
- 布线时和其他强电电缆捆在一起,干扰打到了计数输入端。
- 编码器供电电压不足或者纹波太大。
排查干扰时,我习惯在HSC组态里打开输入滤波或硬件中断的滤波选项,适当增加滤波时间可以滤掉一部分脉冲干扰,但滤波设太大也会滤掉真实脉冲,尤其是高速旋转时。
如果现场已经出现累计误差,更要重视Z相信号。每个Z脉冲代表一圈,可以用Z信号作为硬件复位,把单圈位置清零,同时校验累计圈数是否偏移。这样即使中间偶尔丢一两个脉冲,下一圈零点也能把单圈位置重新校准回来。
4.3 零点丢失时圈数无法校正怎么办
纯增量式编码器有一个先天不足:掉电后位置信息丢失。上电后你只知道当前从某个位置开始,不知道设备在机械零点附近还是第几圈。如果设备运行前没有回零,圈数和单圈位置都是相对值。
解决办法通常有三种:
- 用Z信号配合回零程序,每次开机先找零位。
- 用带多圈功能或者绝对值编码器,直接输出绝对圈数和位置。
- 把累计脉冲存在掉电保持的DB变量里,下次上电继续累加。但需要确保机械位置没有在掉电期间被人为移动,否则一切都是空的。
如果现场没有Z信号,只能在机械行程两端装接近开关做硬限位回零,或者依靠一个固定参考点来校正。总之,算法可以算出相对圈数,但绝对零点必须靠外部信号建立。
5. 扩展:把圈数变成角度和位移,以及工程化建议
5.1 角度与直线位移换算公式
算出diTurns和diPosInRev之后,用途就多了。最直接的是角度:
- 总角度 = (整圈数 * PPR + 单圈脉冲) / PPR * 360
- 单圈角度 = 单圈脉冲 / PPR * 360
如果编码器装在电机尾轴,而电机经过减速机后输出,还要把减速比考虑进去。比如减速比是20:1,电机转20圈输出轴转1圈,那么输出轴角度 = 电机角度 / 20。
如果是装在丝杠或者皮带轮上测直线位移,就用导程换算:
- 位移 = (整圈数 * PPR + 单圈脉冲) / PPR * 导程
举个例子,丝杠导程10mm,每圈脉冲10000,累计脉冲是12345,那么位移 = 12345 / 10000 * 10 = 12.345mm。这里diTurns=1,diPosInRev=2345,计算结果是完全一致的。
5.2 掉电保持与多圈累加的设计
如果你希望设备断电再上电后,圈数还能接着上次继续算,关键不在算法,而在变量是否掉电保持。
在博途里,函数块的实例DB变量区可以设置“保持”属性。把rTotalPulses、rOldCnt、xInit都设为保持,这样断电后CPU会保存这些值。再上电时,程序会接着原来的累计值继续累加,diTurns也不会归零。
但这里有一个隐患:如果断电期间编码器因为外力转动了,但PLC没有记录,累计值就和机械实际位置不一致了。所以工程上通常会要求上电后做一次“位置校验”或者“回零”。如果用了绝对值编码器,读取到的是绝对位置,就不存在这个问题。
另外,如果HSC硬件本身有掉电保持功能,可以让HSC在模块维持供电时继续计数。但很多普通HSC并不保证掉电后保持,所以最稳妥的方案还是让PLC软件在断电前把关键值写入保持性DB。
5.3 最后一点调试建议
我个人的经验是,这种算法写起来不复杂,最容易出错的往往是输入参数和监控方式。尤其uiPPR一定要确认是“编码器线数”还是“实际每圈脉冲数”。2500线编码器如果用了4倍频,PPR是10000,不是2500。这个数错了,后面的圈数、角度、位移全都不对。
调试时建议先把高速计数器原始值、算法累加值、圈数、单圈脉冲放到监控表里,手动慢速旋转一圈,确认单圈脉冲是否从0走满一圈又回到0;再快速旋转几圈,确认圈数是否累加正确;最后做正反转测试,看看负数取余和零点回归是否正常。这套流程走完了,再接到运动控制或HMI上,基本就不会出大问题。