一个聪明的自制模数实现
Posted
技术标签:
【中文标题】一个聪明的自制模数实现【英文标题】:A clever homebrew modulus implementation 【发布时间】:2013-01-28 06:35:36 【问题描述】:我正在使用一些旧版软件(RSLogix 500,别问)对 PLC 进行编程,它本身不支持模数运算,但我需要一个。我无权访问:模数、整数除法、局部变量、截断操作(尽管我可以通过舍入来破解它)。此外,我可以使用的所有变量都按数据类型排列在表格中。最后,它应该适用于浮点小数,例如12345.678 MOD 10000 = 2345.678
。
如果我们做我们的等式:
dividend / divisor = integer quotient, remainder
有两个明显的实现。
实施1:
执行浮点除法:dividend / divisor = decimal quotient
。然后将截断操作组合在一起,以便找到integer quotient
。将它乘以divisor
并找出dividend
和那个之间的差异,得到remainder
。
我不喜欢这样,因为它涉及到一堆不同类型的变量。我不能将变量“传递”给子程序,所以我只需要分配位于多个不同变量表中的一些全局变量,这很难理解。不幸的是,“难以理解”很重要,因为它需要足够简单,让维护人员能够搞砸。
实施 2:
创建一个循环,使得 dividend > divisor
divisor = dividend - divisor
。这是非常干净的,但它违反了 PLC 编程的一条重要规则,即永远不要使用循环,因为如果有人无意中修改了索引计数器,你可能会陷入无限循环,机器会发疯或出现不可恢复的故障。 Plus 回路很难进行维护故障排除。另外,我什至没有循环指令,我必须使用标签和跳转。哎哟。
所以我想知道是否有人有任何聪明的数学技巧或比这些更聪明的模数实现。我可以访问 + - * /、指数、sqrt、三角函数、日志、abs 值和 AND/OR/NOT/XOR。
【问题讨论】:
你真的不能使用循环吗?这似乎很奇怪,因为几乎所有有趣的程序都必须能够循环。 能否保证输入在程序循环中的变化有多大? @templatetypedef PLC 在整个程序中使用一个大的隐式循环,并且只在结束/开始时更新 I/O 缓冲区 @templatetypedef PLC 程序很少关注数据处理,而非常关注实时操作。风格更像是“当传感器 A 被触发时,将伺服 B 转至接收位置,到达该位置后,触发电磁阀 C 500 毫秒,然后释放并触发电磁阀 D。”所以基本上一个循环劫持了程序控制,你有被卡住的风险,所以整个序列不起作用。 @JanDvorak 不能保证,实际上模函数是更大计算的一部分。基本上,我正在捕获编码器位置并将其与其他一些参数一起存储到移位寄存器中,并且我的编码器在 10,000 个脉冲处溢出,因此,如果您检测到上升沿和下降沿,就在折点处您会必须解决这种情况。 【参考方案1】:您要处理多少位?你可以这样做:
if dividend > 32 * divisor dividend -= 32 * divisor
if dividend > 16 * divisor dividend -= 16 * divisor
if dividend > 8 * divisor dividend -= 8 * divisor
if dividend > 4 * divisor dividend -= 4 * divisor
if dividend > 2 * divisor dividend -= 2 * divisor
if dividend > 1 * divisor dividend -= 1 * divisor
quotient = dividend
展开的次数与dividend
中的位数一样多。一定要小心那些溢出的乘法。这就像您的 #2 一样,只是它需要 log(n) 而不是 n 次迭代,因此完全展开是可行的。
【讨论】:
有趣。所以本质上这是在模拟循环的行为,但实际上没有使用循环?我不确定我是否理解它是如何工作的,但假设它确实如此,它非常聪明! 另外,我认为这仅适用于整数,因为它基于不遵循浮点数据模式的 2 的幂? 同样的逻辑也适用于浮点数。您需要的迭代次数是 log_2(max(type)/min(type)),浮点数明显更大(32 位浮点数约为 256,64 位浮点数约为 2048)。然而,精度早在这么多迭代之前就成为一个问题。【参考方案2】:如果您不介意过于复杂并浪费计算机时间,您可以使用周期性触发函数计算模数:
atan(tan(( 12345.678 -5000)*pi/10000))*10000/pi+5000 = 2345.678
说真的,减去 10000 一次或两次(您的“实施 2”)会更好。一般浮点模数的常用算法需要许多位级操作,这对您来说可能是不可行的。参见例如http://www.netlib.org/fdlibm/e_fmod.c(算法很简单,但由于特殊情况,代码很复杂,因为它是为 IEEE 754 双精度数字编写的,假设没有 64 位整数类型)
【讨论】:
信不信由你,这对我来说实际上是一个理想的解决方案。计算时间不是问题。计算仅每秒或每两次发生一次,因此效率不是问题。它适合单个计算指令,我可以简单地评论“这是一种计算模数的晦涩方法”。它对梯级排序也不敏感。我非常喜欢它。【参考方案3】:这一切似乎都过于复杂了。您有一个在 10000 处滚动的编码器索引,并且对象沿着您在任何给定点跟踪其位置的线滚动。如果您需要沿线转发项目停止点或动作点,只需添加所需的多少英寸,如果您的目标结果大于 10000,则立即减去 10000。
另外,或者另外,每次 PLC 扫描您总是会获得一个新的编码器值。如果当前值和上一个值之间的差异为负值,您可以激活工作触点以标记回绕事件并对该扫描中的任何计算进行适当的更正。 (**或增加一个二级计数器,如下所示)
在不了解实际问题的情况下,很难提出更具体的解决方案,但肯定有更好的解决方案。我认为这里根本不需要 MOD。此外,地板上的人会感谢你没有用模糊的巫师东西填满机器。
我引用:
最后,它必须适用于浮点小数,例如 12345.678 MOD 10000 = 2345.678
有一个出色的功能可以做到这一点 - 它是一个减法。为什么它需要比这更复杂?如果您的输送线实际上长于 833 英尺,则滚动第二个计数器,该计数器在主索引翻转时递增,直到您有足够的距离来覆盖所需的地面。
例如,如果您需要 100000 英寸的传送带内存,则可以有一个在 10 处翻转的辅助计数器。如上所述,可以很容易地检测到主编码器翻转,并且每次都增加辅助计数器。那么,您的工作编码器位置是计数器值加上当前编码器值的 10000 倍。仅在扩展单元中工作,并使辅助计数器以您需要的任何值翻转,以免丢失任何部件。问题再次归结为一个简单的减法(如上)。
例如,我将此技术用于行星齿轮旋转零件支架。我有一个编码器,每次初级旋转一次翻转,而行星齿轮卫星部件(它们本身围绕定子齿轮旋转)需要 43 次初级旋转才能返回到相同的起始方向。使用在主编码器翻转点处递增(或递减,取决于方向)的简单计数器,它可以让您完全绝对地测量零件所在的位置。在这种情况下,辅助计数器在 43 处翻转。
这对于直线输送机的工作方式相同,唯一的区别是直线输送机可以无限行驶。那么问题只需要受限于线路上最坏情况部分所采取的最长线性路径。
请注意,我从未使用过 RSLogix,这里是一般的想法(我在这里使用了通用符号,我的语法可能有点错误,但你应该明白)
通过以上内容,您最终会得到一个值 ENC_EXT
,它实质上已将您的编码器从 10k 英寸的编码器转换为 100k 英寸的编码器。我不知道您的输送机是否可以反向运行,如果可以,您还需要处理向下计数。如果您的程序的整个其余部分仅适用于 ENC_EXT
值,那么您甚至不必担心您的编码器仅达到 10k 的事实。现在它达到 100k(或任何你想要的),并且可以使用减法而不是模数来处理环绕。
后记:
PLC 首先是状态机。 PLC 程序的最佳解决方案通常是与此想法一致的解决方案。如果你的硬件不足以完全代表机器的状态,那么 PLC 程序应该尽最大努力用它所拥有的信息来填补缺失的状态信息的空白。上述解决方案做到了这一点——它采用了不足 10000 英寸的状态信息并将其扩展以适应流程的要求。
这种方法的好处是您现在保留了绝对状态信息,不仅针对输送机,还针对生产线上的任何部件。您可以向前和向后跟踪它们以进行故障排除和调试,并且您有一个更简单、更清晰的坐标系可用于未来的扩展。通过模数计算,您将丢弃状态信息并尝试以功能方式解决个别问题 - 这通常不是使用 PLC 的最佳方式。你不得不忘记你从其他编程语言中学到的东西,并以不同的方式工作。 PLC 是一种不同的野兽,它们在这样处理时效果最好。
【讨论】:
原因是因为它降低了实现的能力。它只能在单个减法给出结果的情况下,否则它需要一个循环,即实现二。您的解决方案本质上是将 while 更改为适用于该特定范围的 if。我正在寻找一个可重用的函数,因为我已经遇到了足够多的这个问题,并且在某些情况下我需要展开一个多次缠绕的位置。 @BenMordecai 这就是我对二级计数器所说的。您只需要保留一个以相同方式翻转的整数计数器。我添加了更多细节。 我的编码器翻转由高速计数器子系统实时处理。我的 MOD 函数仅用于需要单个计算的事件处理。令人沮丧的是,似乎没有一种简单的计算方法可以解决,但条件方法确实适用于我将遇到的大部分用例。 我认为通过使用计数器,您实际上不是在使用程序扫描 as 循环,而计数器的作用是让您摆脱“展开计算”进步的状态?并不是说它有什么问题,我只是想了解您是否是这个意思。 @BenMordecai 程序扫描是一个循环,但我没有在任何意义上使用它作为一个循环,不。在任何给定点,您的 PLC 都知道编码器的状态 - 它知道位置是介于 0-10k 之间的某个值。在一个且只有一个时间点,当编码器翻转时,新值会小于前一个值,同时沿一个方向行进。我的意思是,通过使用这个单一事件来增加一个计数器,您可以知道传送带的位置在比 0-10k 更宽的范围内。【参考方案4】:你可以使用一个子程序来做你正在谈论的事情。你可以把棘手的代码藏起来,这样维护技术人员就永远不会遇到它。对于您和您的维护人员来说,这几乎肯定是最容易理解的。
我使用 RSLogix500 已经有一段时间了,所以我可能会弄错几个术语,但你会明白的。
为您的浮点数和整数分别定义一个数据文件,并按照 MOD_F 和 MOD_N 的行给它们一些符号。如果你把这些设置得足够吓人,维护技术人员就不要管它们了,你所需要的只是在数学过程中传递参数和工作空间。
如果你真的担心它们会弄乱数据表,有办法保护它们,但我忘记了它们在 SLC/500 上是什么。
接下来,如果可能的话,定义一个子程序,在数字上远离现在使用的子程序。将其命名为 MODULUS。同样,维护人员几乎总是远离 SBR,如果它们听起来像编程名称。
在 JSR 指令之前的梯级中,将要处理的变量加载到 MOD_N 和 MOD_F 数据文件中。注释这些梯级,说明它们为 MODULUS SBR 加载数据。让任何有编程背景的人都能清楚地了解 cmets。
仅在需要时有条件地调用您的 JSR。维护技术人员不会打扰非执行逻辑的故障排除,因此如果您的 JSR 未处于活动状态,他们很少会查看它。
现在您拥有自己的带围墙的小花园,您可以在其中编写循环而无需维护。只使用那些数据文件,不要假设任何东西的状态,但那些文件是你所期望的。换句话说,您不能信任间接寻址。只要您在 MODULUS JSR 中定义索引,索引寻址就可以。不要相信任何传入的索引。使用 MOD_N 文件中的一个单词、一个跳转和一个标签来编写 FOR 循环非常容易。您的整个实施#2 应该少于十个梯级左右。我会考虑使用表达式指令或其他东西......让您只需输入表达式的指令。该指令可能需要 504 或 505。适用于组合的浮点/整数数学。请检查结果以确保四舍五入不会杀死您。
完成后,验证您的代码,如果可能的话,最好是完美的。如果此代码曾经导致数学溢出并使处理器出现故障,您将永远不会听到它的结束。如果你有一个模拟器,在模拟器上运行它,使用奇怪的值(以防它们以某种方式弄乱函数输入的加载),并确保 PLC 没有故障。
如果你做到了这一切,没人会意识到你在 PLC 中使用了常规的编程技术,你会没事的。只要它有效。
【讨论】:
是的,这是我在这台机器上一代的方法,除了我使用实现 1 而不是 2。【参考方案5】:这是一个基于@Keith Randall 答案的循环,但它也保留了减法除法的结果。为了清楚起见,我保留了 printf。
#include <stdio.h>
#include <limits.h>
#define NBIT (CHAR_BIT * sizeof (unsigned int))
unsigned modulo(unsigned dividend, unsigned divisor)
unsigned quotient, bit;
printf("%u / %u:", dividend, divisor);
for (bit = NBIT, quotient=0; bit-- && dividend >= divisor; )
if (dividend < (1ul << bit) * divisor) continue;
dividend -= (1ul << bit) * divisor;
quotient += (1ul << bit);
printf("%u, %u\n", quotient, dividend);
return dividend; // the remainder *is* the modulo
int main(void)
modulo( 13,5);
modulo( 33,11);
return 0;
【讨论】:
以上是关于一个聪明的自制模数实现的主要内容,如果未能解决你的问题,请参考以下文章