10.2 堆栈指令先入先出FFL/FFU和先入后出LFL/LFU的编程
被称为堆栈指令执行的寄存器批量数据操作是对寄存器区域数据存放和数据进出的管理,这是对寄存器数据块的操作,由一个负责数据装载的指令FFL和一个负责数据卸载的指令FFU相互配合完成。这两条指令必须成对使用,因装载和卸载的操作关联,它们必须共同使用一个数组操作的控制结构数据。所谓操作关联,指的是操作对象是同一个数组的数据;确定数组位置的指针会跟随装载或卸载的操作动作而浮动。
根据堆栈数组先入先出的原则,装载指令FFL在梯级条件跳变时,控制结构数据的POS加1,装载数据到指针POS所指向的数组元素;卸载指令FFU在梯级条件跳变时,卸载最前面的数据,同时全体数据前移,控制结构数据的POS减1。这个控制结构数据的指针变化非常像可逆的增减计数器的动作,你应该还没有忘记,前面我们提过,数组操作所需的控制结构数据是从计数器演变过来的,或者说是一个特殊的专用计数器。
控制结构数据状态位也在堆栈指令中具有特殊的含义,完成位置位,其含义是数组堆栈满,不能继续对数组装载,此时的装载操作执行无效;栈空位置位,数组不能继续卸载,返回0给目标操作数。如果对数组堆栈操作有限定要求,不妨把这两个位用作限制条件,或通过这些位状态来满足某种特定的需求。
梯级逻辑编写如图10-9所示,本条装载指令FFL的执行是当梯级条件FIFO_Load跳变,控制结构数据的指针Control_LU.POS加1,将源操作数Source_FIFO的数据装载至指针所指的堆栈数组元素Array_FIFO[6],完成数据装载操作。

图10-9 堆栈装载的梯级逻辑
梯级逻辑编写如图10-10所示,本条卸载指令FFU的执行是当梯级条件FIFO_Unload跳变,执行卸载数据操作,将堆栈数组最前面的数据弹出至目标操作数Dest_FIFO,并且数组全体数据前移一个数据单元,同时Control_FIFO.POS减1,将0充填原来指针所指的数据单元,即刚移走的数据单元Array_FIFO[6]。

图10-10 堆栈卸载的梯级逻辑
堆栈数据装载和卸载操作的示意图如图10-11所示。定义10个双整字的数组作为数据堆栈区域,装载指令FFL操作将源操作数送至指针所指的数组数据单元;卸载指令FFU操作将数据送至目标操作数,然后全体数据前移,置0充填原指针所指的数据单元,即堆栈数据的最后一个数据单元。
通过装载和卸载两条堆栈指令的配合使用,可以完成先入先出的数据堆栈存取,因而常常用于更新式的数据采集。其做法是,推掉一个最旧的数据,采集一个新数据补充,总是保存指定长度的最后的一组数据。
实际运用中常常会遇到这样的需求,针对一个对象按照一定的时间间隔采集数据,把这个数据存放在指定的数据缓冲区,并保持最后的新鲜数据,每当采集到一个新的数据,就推掉最前面的旧数据。这是典型的数据堆栈的管理,用先入先出的堆栈指令FFL和FFU来完成实在是太合适不过了。
假定有一个这样的需求实例,每隔2s从模拟量输入通道0采集一次数据,将数据存放在指定的堆栈数组,然后将这个堆栈数组的数据送到人机界面列表显示,当这个堆栈数组最初是空的时候,一直执行装载数据的动作,不卸载数据,直到堆栈数组装满,然后总是推掉最前面的数据,将刚采集的数据充填在最后一个数据单元,一直保持最后的20个数据。编写的梯级逻辑如图10-12所示。

图10-11 堆栈数据装载和卸载操作的示意图

图10-12 更新式数据采集的梯级逻辑
编写一个自复位的计时器Timer_FIFO,其完成位DN提供每2s一次的工作脉冲,这个工作脉冲为堆栈数组的装载指令提供所需的梯级条件跳变。
堆栈数组的指针最初为零,当计时器产生第一个脉冲,装载FFL指令的梯级条件跳变,Control_FIFO.POS加1,指针POS指向1,从数据源采集来的数据装入第一个数据单元。如此类推,每当梯级条件跳变一次,指针POS加1,采集的数据装入指针所指的数组数据单元,直到栈满,也即Control_FIFO.DN位置位。栈满位作为FFU的梯级条件,根据先入先出的原则,FFU卸载最前面的一个数据,并将所有的数据往前移动,将0置入腾出的最后一个数据单元,同时Control_FIFO.POS减1。这种控制结构数据标签的POS增减的修改情形就是前面提到的相似于可逆增减计数器的控制结构数据指针。
正如前面提及的,装载指令FFL和卸载指令FFU共有同一个控制结构数据Control_FIFO,成对操作的指令FFL和FFU不但操作对象相同,而且操作动作关联。它们对同一个堆栈数组操作,修改同一个控制结构数据标签的指针,这些恰恰是控制结构数据的参数所示,堆栈数组的长度和指针。
我们把这个程序下载到控制器去运行测试,观察它的堆栈数组,你无论如何也不会相信自己的眼睛,怎么看数据都是少一个。也许堆栈数组的卸载操作不该编写在堆栈数组的装入操作的梯级后面,应该让它先卸出,再装入。当把卸出指令的操作梯级改为放置在装载指令操作的梯级前面,重新测试一下,情况仍然没有改变。
其实这个最后的数据是存在的,不过存在的时间极短,如果将装载指令FFL编写在前面梯级,后面梯级的卸载指令FFU紧跟着就操作卸载并移动了;如果将装载指令FFL编写在后面梯级,将延迟一个扫描周期的时间,前面梯级的卸载指令FFU才操作卸载并移动。但对于我们观察数据来讲是一样的,这就是为什么刚才我们调换两条指令执行的顺序结果仍然没有改变的原因。(https://www.daowen.com)
我们总是无法看到瞬间存在的最后一个数据,所看到的是卸载指令FFU卸载操作之后的数组最后一个数据元素填补的0,这个数据每持续2s将闪变一次,但我们感觉不到,这样给了我们总是少一个数的错觉。主要原因在于我们用FFL装载后的完成位作为卸载FFU的梯级条件,虽然这样的梯级关系简单明了,执行目的一看就知,但卸载指令FFU补0的操作让我们不能看到最后一个数据。如果只是为了得到你想要的数据长度N,大可不必用复杂的梯级条件来解决这个问题,直接将数组操作长度设为N+1就可以,注意创建数组的长度也必须是N+1或更大,否则例程执行将报错而引起停机。
堆栈数组先入先出的操作,先这么做着吧,直到有一天你读了别人写的程序,也是N+1的数组操作长度,遂发出会心的笑,原来那位台兄也是屡试不爽,委曲求全那,好在这个数组长度也就它自己消化,无伤大雅,我们就接受了吧。这也算是编程中常见的一种迁就,但我们必须确定,这个长度设置不会影响到其他。
可以想象一下,卸载指令FFU执行的最前面的一个数据卸载之后,全体向前移动一个单元,这种操作不就是前面谈到的COP指令的技巧性运用么,料想当初没有堆栈指令FFL和FFU时,就是用COP指令来解决问题的,或许组成堆栈指令FFU的宏中应该是包括了这个COP动作吧。这里,我们再一次看到指令功能代替了技巧的运用。
我们再来看看,如果对堆栈数组的目标操作数和源操作数进行特殊的选定,就可以实现数组的数据循环了。为简单起见,假定有一个需求是希望得到一组5个双整字数据的循环移动。模仿刚才的做法,固然可以得到数据的循环,既然不存在数组堆栈空和堆栈满的不同动作,我们就避免使用控制结构数据的DN位,因为它总是带来一些困惑,令读例程的人们难以理解,这个需求是一开始就处在堆栈完成状态,编写的梯级逻辑如图10-13所示。

图10-13 循环数据设置和执行梯级逻辑
程序扫描的初始化先将数组的数据预装好(这部分梯级逻辑也可以编写在初始化例程中),这个数据的初始状态可能是预定的数据值,也可能是某状态量字演变过来的十进制数字表达,通过MOV指令的立即数来进行例程执行的数据设置,为系统的可靠性提供保证。同时,将堆栈指令FFL和FFU共用的指针Control_FIFO_Cyc.POS预设为5,这确保了一开始就能进入循环并保证了数据的首尾相接,数据循环的指针永远定格在长度5上。
以循环步进动作FIFO_Cyc的位状态作为堆栈指令FFU和FFL执行的梯级条件,当梯级条件跳变,首先执行的是卸载指令FFU,将Array_FIFO_Cyc[0]卸出,送到循环缓冲数据单元Array_FIFO_Cyc_Buffer,同时POS减1,全体数据前移,并补0给最后一个数据单元,即Array_FIFO_Cyc[4];紧跟着执行的装载指令FFL立刻将POS加1,此时指针指向5,循环缓冲数据Array_FIFO_Cyc_Buffer作为装载指令FFL的源数据被送到指针所指的数据单元,即刚被补0的Array_FIFO_Cyc[4]。
这个过程精密而准确,每一个步骤都清楚地印证了装载指令FFL和卸载指令FFU的执行动作。试着将装载指令FFL和卸载指令FFU交换一下位置,即执行顺序的改变,得到的结果便不尽人意了,因为这样执行的动作进程不是我们所要的循环。
这样的卸载指令FFU和装载指令FFL的交替操作,实现了将数组的最前面的数据单元传递到最后一个数据单元,如此循环,如果将数组Array_FIFO_Cyc在别的地方引用,这将是一个循环的数组,是控制过程的动作所需要的。
还有一套先入后出堆栈操作指令LFL和LFU,其管理原则是按指针装载数据和按指针卸载数据,装载指令LFL的操作跟FFL是完全一样的,不同的是卸载的时候,按照指针所指卸出,即后入先出。先入先出和先入后出两对指令混合着用也是可以的。
现在我们试试能否将一个数组的最后一个单元传递到另外一个数组的最前面的一个单元,即将原有的数据颠倒顺序。第9章中我们曾使用数组排序指令SRT对数组Array_SRT进行排序,这是一个升序的执行结果,如果我们想要一个降序的结果,能否将排序的结果颠倒顺序呢?编写的梯级逻辑如图10-14所示。
梯级条件Start_SRT转为真,为倒序梯级逻辑提供了条件,两条MOV指令为LFU指令和LFL指令的指针设置了初始化数据,卸载指令给予堆栈满的状态,装载指令给予堆栈空的状态,卸载指令LFU从最后面一个数据开始卸载,装载指令LFL从最前面一个数据开始装载。
这两条指令是分别独立操作的,既不是相同的操作对象,操作上也没有指针的关联,这里不是成对使用的例子,故各自使用自己独立的结构数据标签Control_SRT1和Control_SRT2。
保持型的堆栈指令需要使能位跳变才能执行,这里采用解锁指令复位使能位的方法,迅速简单地连续操作,所以LFU和LFL指令都是无梯级条件的。
装载指令LFL的完成位则作为继续循环或离开循环的判断条件。离开循环时复位启动倒序操作的Start_SRT标签和装载指令控制结构数据标签,以准备下一次的启动,复位控制结构数据标签也是为了消除复位条件,以便Start_SRT能再次启动。Start_SRT的置位状态只会存在一个扫描周期。
倒序前后的数据表排列如图10-15所示。
对照数据表可以看出数据堆栈的搬运结果,这和通常的MOV指令和COP指令执行结果不一样,搬走后的数据,留下的是0,源数据不复存在。仅仅是将数组倒序作为操作目的,应该在每次倒序之前将操作对象复制到源数据。

图10-14 颠倒数组顺序的梯级逻辑

图10-15 倒序前后的数据表排列