Signal语言简介
1. 引言
Signal是为实时应用而设计的一种同步(synchronous)语言,而Ada等则被称为(asynchronous)异步语言。在有代表性的三种同步语言中,Signal和Lustre是声明式的(declarative),Esterel是命令式的(imperative)。
2. 一个例子—WATCHDOG进程
2.1. 问题
一个过程发出一个ORDER,它将在DELAY这段时间内被执行,执行结束后产生一个DONE信号。进程WATCHDOG用于控制DELAY,它也收到ORDER和DONE的拷贝。如果一个ORDER不能完成,进程WATCHDOG必须发出一个ALARM。
再有,如果一个ORDER还未完成又出现一个新的ORDER,那么重新开始计时。如果一个DONE在DELAY以后出现,或者另一个ORDER,那么这个DONE将被忽略。
2.2. 输入和输出信号
假定用整数表示ORDER。这样WATCHDOG将收到一个整数序列,整数之间有若干时间间隔。
WATCHDOG还收到一个DONE信号序列。DONE信号只是一个脉冲,就象按键按下后产生的单个信息。在signal语言中它的类型是event,其编码是一个永远为真的布尔量。
为了计算时间,同步语言不采用语言定义的设施,如秒,其精度不够。时间源也是一个event类型的信号。两个时间event的时间间隔由环境确定。我们假定时间信号的名称是TICK。参数DELAY即是若干个TICK,因此不能给出时间的物理量纲。
当超过了ORDER与DONE之间的DELAY,WATCHDOG输出ALARM,它可以是一个event,或者更好的是从执行开始统计的TICK数,假定称之为HOUR,其类型是整数。
因此,输入和输出是某种类型的值序列,序列中的每个值在某个瞬间出现。这样的序列称为signal。signal取值的瞬间的集合称为clock。
2.3. WATCHDOG的运行例子
为说明WATCHDOG的运行情景,我们使用时间图。在一条水平线上表示每个信号,如果某个时刻出现值,则在该时刻用星号标记,在星号上方标注该值。
假定DELAY=5。
7 8 9
ORDER : -----*---------------*----------------------*-------
t t t
DONE : ------------*----------------------------*-----*----
t t t t t t t t t t t t t t t t t
TICK : --*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*-
12
ALARM : -----------------------------------*----------------
在ORDER8出现后的第5个TICK出现ALARM,此时是自开始以来的第12个TICK。
另一种更精确的表达方法是只显示出现信号的时刻,用符号∣表示没有信号。
ORDER : | ∣ | 7 | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | 8 | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | 9 | ∣ | ∣ |
DONE : | ∣ | ∣ | ∣ | ∣ | t | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | t | ∣ | t | ∣ |
TICK : | t | t | t | t | ∣ | t | t | t | ∣ | t | t | t | t | t | t | t | t | t | t |
ALARM : | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | 12 | ∣ | ∣ | ∣ | ∣ | ∣ |
上图精确地指出ORDER7在第2个TICK上到达,第1个DONE在第4和第5个TICK之间出现。
2.4. Signal语言的同步假设
一个Signal程序不研究两个信号之间的确切间隔。它只知道信号的相对次序,即是同时出现还是先后出现。
Signal语言的第一个假设是环境可以告知两个信号的发生次序,即是同时出现还是先后出现。在任意一个瞬间,可以没有信号出现,也可以有一个或多个信号出现。Signal程序只考虑至少有一个信号出现的瞬间,此时对信号进行某些计算,产生新值。
第二个假设是计算所花费的时间为0。因此新值是在使用已有数据进行计算的同一逻辑瞬间产生的。
例如,如果在一个ORDER的第5个TICK或之前没有DONE信号到达,那么在第5个TICK或之后产生ALARM信号。ALARM信号的产生取决于TICK和DONE信号,并且必须在它们之后。但我们假设信号产生的时间为0,因此可以在第5个TICK的同一瞬间产生ALARM。
2.5. 用Signal编写的WATCHDOG
以下有编号的是Signal程序。
1 : process WATCHDOG =
2 : % emits an ALARM if an ORDER is not DONE within some DELAY %
3 : { integer DELAY;}
4 : ( ? integer ORDER;
5 : event DONE, TICK;
6 : ! integer ALARM; )
以上是进程的接口,它包括了使用WATCHDOG的其它进程需要知道的信息。
DELAY是一个参数。它是一个编译前确定的常数。符号?后面是输入信号,以及它的类型。符号!后面是输出信号,以及它的类型。
7 : (|
这是进程体的开始。进程体由一组方程(equation)组成。方程定义或限制信号的值。方程之间用符号| 隔开:(|…|…|…|)。
8 : HOUR ^= TICK
9 : | HOUR := (HOUR$ init 0) + 1
信号可以是输入输出信号,或是本地信号(如HOUR),其声明放在程序文本的最后。HOUR是TICK信号的计数器,通过它可以知道ALARM的时间。
第8行规定HOUR与TICK在同一瞬间出现,即HOUR与TICK有相同的时钟(clock),它们是同步信号。^=是时钟等式运算符。当上下文本身不足以定义一个信号的时钟时,那么通过第8行定义HOUR的时钟。
第9行定义了HOUR信号的值。由于Signal是声明式语言,它不使用象“HOUR := HOUR+1”这样的语句来修改变量,而是使用以“HOUR$”表达的前一个瞬间的值加1来设置HOUR信号的当前值。“init 0”表示HOUR信号在第一个瞬间的值被置为0。
10 : | CNT ^= TICK ^+ ORDER ^+ DONE
11 : | ZCNT := CNT $ init (-1)
12 : | CNT := DELAY when ^ORDER
13 : default -1 when DONE
14 : default ZCNT - 1 when ZCNT >= 0
15 : default -1
CNT是完成一个ORDER所花时间的递减计数器(请注意COUNT是Signal的保留字,因此使用CNT)。在一个ORGER到达时把CNT的初值置为DELAY,然后在每个TICK时CNT递减,直至出现一个DONE,或减到0。
第10行定义CNT出现的瞬间,它的时钟是3个输入信号的并集(运算符^+)。
ZCNT的值是CNT前一瞬间的值,它的第1个值是-1,这是WATCHDOG进程不等待终止时的CNT的“休息”值。
第12行在ORDER到达时把值DELAY给CNT。运算符^是从ORDER抽取类型为event的时钟。如果在该瞬间没有ORDER,但有DONE信号,CNT立刻取“休息”值-1。我们不需要写^,因为DONE的类型是event。
如果既没有ORDER也没有DONE信号,那么只要CNT大于等于0,把CNT减1,否则CNT保持为-1。
when和default是同步运算符,前者的优先级比后者高。
16 : | ALARM := HOUR when CNT = 0
当递减计数器为0时ALARM信号取HOUR的值。注意当CNT=0时HOUR总是出现的,因为CNT的时钟包含TICK的时钟,它与HOUR的时钟相同。
17 : |)
第17行表示方程组结束。这些方程的次序是任意的,编译器将分析它们之间的依赖关系,并确定何时计算它们。
以下是本地信号的声明:
18 : where
19 : integer HOUR, ZCNT, CNT;
20 : end % WATCHDOG %;
2.6. WATCHDOG进程的使用
WATCHDOG进程是一个应用程序的一个片段。应用程序将产生WATCHDOG进程的输入,并利用WATCHDOG进程的输出ALARM。
可使用文本编辑器或文法引导的图形编辑器编写Signal程序。
可把WATCHDOG进程放在一个独立文件中进行编辑,文件名可以是watchdog.stg。可把DELAY及其值放在另一个名为watchdog.par的文件中,可单独编译该文件。
Signal语言的编译器分析程序的文法,验证时钟定义,并确定计算的顺序。编译器的一个重要阶段是时钟计算(clock calculus),它验证信号的时钟是否精确定义和内聚,是否有迂回性(circulayity)。这些分析可能暴露不是程序员设计的局限性。编译器可能确定某些时钟包含在其它时钟中,这样就不需要频繁计算值,从而提高目标码的效率。
编译器在确认Signal源程序正确后,首先生成的目标码是C程序,然后再通过C编译器生成C的目标码。这样可以自动访问输入/输出、数学函数等C程序库,也可与应用程序的其它部分连接。
要用独立的输入文件描述每个输入信号的值,文件名的命名规则是<信号名>.dat。
要用独立的输入文件定义每个信号的时钟,文件名的命名规则是RC_<信号名>.dat。在文件中,用1表示信号的出现,0表示未出现。
以下是上述运行场景的各个输入文件:
RORDER.dat : 7 8 9
RC_ORDER.dat : 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0
RC_DONE.dat : 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0
RC_TICK.dat : 1 1 1 1 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1
请注意,RORDER.dat中的数值个数与RC_ORDER.dat中的1的个数相同。
输出信号的值放在一个名为WALARM.dat的文件中,其中没有时钟。这是一个包含本地信号的时间图:
ORDER : | ∣ | 7 | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | 8 | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | 9 | ∣ | ∣ |
DONE : | ∣ | ∣ | ∣ | ∣ | t | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | t | ∣ | t | ∣ |
TICK : | t | t | t | t | ∣ | t | t | t | ∣ | t | t | t | t | t | t | t | t | t | t |
HOUR: | 1 | 2 | 3 | 4 | ∣ | 5 | 6 | 7 | ∣ | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
ZCNT: | -1 | -1 | 5 | 4 | 3 | -1 | -1 | -1 | -1 | 5 | 4 | 3 | 2 | 1 | 0 | -1 | -1 | 5 | -1 |
CNT: | -1 | 5 | 4 | 3 | -1 | -1 | -1 | -1 | 5 | 4 | 3 | 2 | 1 | 0 | -1 | -1 | 5 | -1 | -1 |
ALARM : | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | ∣ | 12 | ∣ | ∣ | ∣ | ∣ | ∣ |
3. 信号
3.1. Siginal语言中的信号
在Signal语言中,信号是一个具有相同类型的值的序列,这些值在某个瞬间出现。信号出现的瞬间的集合是信号的时钟。
这些离散信号不同于其它应用中的连续的模拟信号,但可以通过对连续信号的采样获得离散信号。
Signal语言不考虑两个值之间的物理时间。如果需要,那么把物理时间定义为一个输入信号。Signal程序的环境负责提供间隔相同(例如1秒)的脉冲序列。
Signal语言只考虑事件的相对次序,即一个信号值是在另一个信号值之前出现,还是之后出现,或同时出现。也是由Signal程序的环境负责确定两个信号值的次序。Signal语言可以测试给定值的次序,然后以确定的方式定义随后的动作。
一个信号可在某个瞬间出现,然后在另一个瞬间缺席。用符号∣表示信号的缺席。在某一个瞬间,如果一个信号缺席,那么必然有另一个信号出现。Signal语言不考虑所有信号都缺席的瞬间。
例如, 以下序列:
a1: | 1 | 2 | ∣ | ∣ | 7 | ∣ | 5 | ∣ | ∣ | ∣ | … |
a2: | ∣ | 10 | ∣ | ∣ | 123 | ∣ | 5 | -1 | ∣ | ∣ | … |
a3: | 0 | 6 | ∣ | ∣ | 13 | ∣ | 2 | ∣ | ∣ | ∣ | … |
a4: | ∣ | 11 | ∣ | ∣ | ∣ | ∣ | 1 | ∣ | ∣ | ∣ | … |
可以简化为:
a1: | 1 | 2 | 7 | 5 | … |
a2: | ∣ | 10 | 123 | 5 | … |
a3: | 0 | 6 | 13 | 2 | … |
a4: | ∣ | 11 | ∣ | 1 | … |
两个信号如果在同一瞬间一起出现或一起缺席,那么称这两个信号是同步的,它们有相同的时钟(用符号^=表示)。在上例中,a1和a3是同步的,即a1^=a3。
时钟有偏序关系。例如,a4的所有瞬间也是a3的瞬间,a4的频度小于a3,因此a4的时钟被a3的时钟包含,即a4^<a3。但是a1的时钟与a2的时钟不可比较。
3.2. 信号的名称
在signal程序中必须声明信号,声明包括一个标识符和信号值的类型。
信号的名称不是传统的变量,它表示一个值的序列,在某个瞬间,它只有一个值,或没有值。通过某种运算符可访问一个信号以前的值,可以是前一个值,也可以是前面第N个值,甚至可以是一个滑动窗口中的前面N个值的集合。
信号的当前值不能用S:=S+1来修改。
3.3. 信号的类型
信号值的类型可以是:预定义的简单类型(如整型)、字符串、枚举类型、复合类型(如数组),本文只介绍简单类型。
3.3.1. 数值类型
数值类型包括:integer、real、dreal(即C语言中的double)、complex。他们具有传统的运算符和通常的特性。操作数的类型必须相同,但允许X+real(N)。整数除法产生整数商。取模运算用modulo表示(%在signal程序中是注释符)。函数与C语言中的函数相同,必须在本地声明函数头。
3.3.2. 布尔类型
布尔类型的名称是boolean,它与整数不同。通过常数true或false、事件信号、值的比较运算(运算符是=、 /= 、<、 <= 、> 、>=)、布尔运算(运算符是not、and、or)等方式获得布尔类型的值。这些运算符具有通常的特性。
3.3.3. 事件类型
在纯Signal语言中只使用时钟。它只有一种值,即布尔常数true。当一个布尔表达式的值是true时接受一个事件。
时钟运算符^应用于一个信号时抽取了它的时钟,产生了一个事件信号:
ORDER : ---- 7 -------- 8 --------------- 9 ------
^ORDER : ---- t -------- t --------------- t ------
在WATCHDOG程序中,ORDER的整数值没有被使用,只用了它的时钟,因此把该时钟作为输入也足够了。
3.4. 信号的声明
信号可声明为输入信号、输出信号或本地信号。输出信号和本地信号可被初始化,特别是用它们表示其它信号的过去值时需要被初始化。
下例解释前面所述的文法:
process DECLARE =
{ integer NBL, NBC; } % 参数 = 常数 %
( ? real a; % 输入信号%
event EV, HH_2; integer B;
! boolean ok init false, FOUND; ) % 输出信号 %
(| XX := sqrt(fabs(-A))
| ...
| OK := inter <= NBC when FOUND
|)
where % 本地信号 %
real XX, YY init -1.5; % only YY initialized %
integer inter;
% 函数接口 %
function sqrt = (?dreal A !dreal B); % double in C %
function fabs = (?dreal A !dreal B);
end %DECLARE%;
3.5. 常数和参数
常数是象18、true 等这类常规的记号。还有一些常数可作为参数,如WATCHDOG中的DELAY,上例中的NBL、NBC。它们必须是编译前已知的,例如,对上例DECLARE进程可以如下方式调用:
... DECLARE {20,M+1} (.. effective input signals ..)
这里M也是参数。常数没有时钟,它们在需要的时候出现。
4. 信号定义和运算符
一个Signal程序体是一组定义输出和本地信号的方程。对于每个信号,必须定义它的时钟,以及它出现时的值。
信号的运算符用于创建新的信号。有些运算符只能勇于同步信号,运算结果与运算对象有相等的时钟,这些称之为monochronous运算符。还有一些运算符可以任何时钟作用于信号,其结果可能有其它时钟,这些称之为polychronous运算符(笔者注:未能找到与monochronous和polychronous对应的中文词)。
4.1. 信号的定义
4.1.1. 定义信号的方程
必须用如下格式的一个唯一的方程来定义一个输出信号或本地信号:
<信号名> := <信号表达式>
在上述方程中,<信号名>与<信号表达式>的时钟相等,<信号表达式>的值必须能被<信号名>接受,<信号表达式>的计算时间为0(同步假设),<信号名>取<信号表达式>的值。
当运算符是monochronous时,所涉及的所有信号的时钟相等。
4.1.2. 例子
在以下进程中:
process PLUS1 =
( ? integer IN;
! integer OUT; )
(| OUT := IN + 1 |)
IN和OUT的时钟相等。如果把这个进程插入到一个更大的程序中,前者的时钟等式将被加入到后者的其它方程中。
上述程序运行时,只要IN的整数值可用,立即把这个整数值加1,然后在同一逻辑瞬间把结果送给OUT。
在测试环境中,如果单独运行这个进程,则以常规速度读入含有输入值的文件,并以同样速度产生一个输出文件。此种情况不需要输入时钟文件。
在以下另一个进程中:
process ONES =
( ? % no input %
! integer S; )
(| S := 1 |)
常数表达式1本身没有时钟。该进程所处的环境必须设置S的时钟。每次询问S是否出现时,将把它的值置为1。如果单独运行该进程,S的时钟将是“主时钟”,即是宿主机的通常速度,运行结果将是无限长的一串1。
如果一个进程包含几个无关的信号:
process TWO =
( ? % no input %
! integer S1, S2;)
(| S1 := 1
| S2 := 2 |)
该进程所处的环境必须设置S1和S2的时钟。如果单独运行该进程,则必须提供S1和S2的时钟文件。
4.1.3. 时钟等式方程
在上一个例子进程TWO中,定义信号的方程还不足以定义它的时钟。可以用另一种形式的方程:
| S1 ^= S2
来显式地说明S1和S2的时钟相等。
如果我们希望信号S随另一个信号A产生,但S的值与A无关,我们必须用以下的等式:
process S_ON_A = % produces a 1 on each input A %
( ? event A;
! integer S;)
(| S := 1
| S ^= A |) % or A ^= S %
如果我们知道2个输入信号是同步的,或者我们希望使它们同步,可以如下写:
process SYNC_IN =
( ? integer A, B;
! ... )
(| A ^= B
| ...
这将同时读输入A和B的值。在一个更大的程序中,时钟方程的集合将保证A和B是同步的。如果单独运行该进程,将并行地读A和B的值文件。
时钟等式可以有多个项,其中一个可以是一个表达式(参见例子WATCHDOG)。
定义S的值的方程必须是唯一的,但可以用多种方式定义它的时钟。当然,编译器将检查其相容性。
4.2. MONOCHRONOUS运算符
4.2.1. 与类型相关的运算符
传统的运算符(加、比较、与、或。。。。。。)和库函数只能用于同步信号。这些运算符也是使信号的时钟相等的一种手段。运算结果与操作数的时钟相同。
process ADD =
( ? integer A, B;
! integer S; )
(| S := A + B |)
在上面进程中,运算符+在A和B之间引入了隐含的时钟等式。表达式A+B和信号S有相同的时钟,加法和赋值的时间假设为0。
单目运算符(-、非、时钟运算符^)也是与操作数同步的。
4.2.2. 延时运算符
延时运算符$用于访问信号的过去值。信号A的延时值与A的时钟相同。
A$是A的前一个值。
A$N是A的前N次出现时值。N是一个正的常数表达式,表达式中可含参数,但不能含信号。例如,PIXEL $ (NBL*NBC-1)。
A$N的前N个值是未定义的。可用两种方式对其初始化。第一是在使用的地方:
| S := A $ init 0
| Y := 5*( X $ 2 init [10,20]) + X$ init 0
第二是在声明具有延时值的信号时:
| ZA := A $ 1
| ZB := A $
| ZC := A $ 3
这里:
integer ZA, ZB init 0, ZC init [10,20,30];
A : --- 1 --- 2 ------- 3 --- 4 --- 5 ------- 6 ---
ZA : --- ? --- 1 ------- 2 --- 3 --- 4 ------- 5 ---
ZB : --- 0 --- 1 ------- 2 --- 3 --- 4 ------- 5 ---
ZC : --- 10 --- 20 ------- 30 --- 1 --- 2 ------- 3 ---
以下是使用延时信号的例子。
要编写一个计数器1,2,3,。。。。。。,可以这样写:
| CNT := (CNT$ init 0) + 1
也可以:
| ZCNT := CNT$
| CNT := ZCNT + 1 |)
这里
integer ZCNT init 0;
ZCNT与CNT的时钟相同。但上面的定义没有设置它们的公用时钟。如果单独运行,对于CNT将产生一个无限序列1、2、3、4。。。。。。。一般用一个单独的时钟等式把该计数器与其它信号连接,例如:
HOUR ^= TICK
| HOUR := (HOUR$ init 0) + 1
4.3. when运算符
when运算符从信号的布尔表达式抽取值为true的瞬间,其作用象采样。因此运算结果的时钟频度要低于表达式的时钟。
4.3.1. 一元when
一元when的使用格式是:
when <布尔表达式>
例子:
NULL := when CNT = 0
上式的结果是事件类型。当布尔表达式为true时它出现,如下图:
CNT : --- 1 --- 0 ------ 2 --- 0 --- 0 --- 3 ----
NULL : --------- t ------------ t --- t ----------
该运算符用于“标记”信号有特殊性质的瞬间,可在用于同步另一个信号的时钟等式中使用它。例如,对某个布尔信号统计值为false的个数:
| CNT ^= when not B
| CNT :=(CNT $ init 0) + 1
4.3.2. 二元when
二元when的使用格式是:
<信号表达式> when <布尔表达式>
例子:
ALARM := HOUR when CNT = 0
结果如下图:
HOUR : --- 2 ------------- 4 --- 6 --- 8 -----
CNT : -------- 1 -- 0 --- 0 --- 3 --- 0 -----
ALARM : ------------------- 4 --------- 8 -----
二元when的优先级比传统运算符的优先级低。A when B的时钟频度比A的时钟和B的时钟小,它是^A与when B的交集。
例子:
A when prop(A)
上式表示在A的特性(property)得到验证时抽取A的值。
例子:
A when ^C
上式表示在C也出现时抽取A的值。
注意不能用when C来使其它信号与信号C同步。例如,下式是非法的:
CNT := (CNT $ init 0) + 1 when ^C
when ^C的时钟频度要比CNT$+1的时钟频度低,因此不能把它的值给CNT。这是Signal程序的常见错误之一。但是,下式是合法的:
S := 1 when ^C
它等价于:
S := 1 | S ^= C
4.4. default运算符
default运算符的作用是合并,它的使用格式是:
<信号A> default <信号B>
合并时A的优先级高于B的优先级,即如果在某一瞬间A和B都出现则取A的值,否则取B的值。
例如:
S := A default B
A : ---- 1 ---------- 2 ---- 3 -------------- 4 --- 5 ---
B : ---------- 10 --- 20 --------- 30 -- 40 -------- 50 ---
S : ---- 1 --- 10 --- 2 ---- 3 -- 30 -- 40 -- 4 --- 5 ---
A和B的类型必须是相容的。S的时钟是A和B的时钟并集。default运算符的优先级低于其它运算符的优先级。
default运算符的左边信号不能是常数。如果右边信号是常数,那么default结果的时钟是不定的,必须从外部来确定。例如:
S := X default false
上式中S的时钟必须在其它地方确定,并且必须包含X的时钟,如:S ^= X ^+ Y。
以下表达式是允许的:
S := A when B default C
上式看上去象if then else语句,但必须考虑时钟,即S取C值的条件不仅是B为false,而且A或B缺席。
在下式中SIGN不能获得X的时钟:
| SIGN := 1 when X >= 0 default -1
因此还必须有:SIGN ^= X。
可用default合并多个信号。例如:
| S := val1 when cond1
default val2 when cond2
default val3 when cond3
5. 高级特色
5.1. cell运算符
5.1.1. 定义
某些Signal程序只有唯一的基本时钟,限定所有信号使用这个时钟,这样信号间只允许传统的操作。这样的程序优化程度低,但可解决烦人的时钟问题。
为了在另一个信号的瞬间重复一个信号,Signal语言设计了cell运算符,其使用格式是:
C := A cell B
这里B是布尔表达式。C包含A 的所有值,并且还在B为true的瞬间取A的前值,以及在没有读A之前取某个初值,如下图所示:
A : --------- 1 ------------------ 2 ------- 3 ------------
B : --- t --------- t -- f -- t ------- t -- f -- f -- t --
C (init 0) : --- 0 --- 1 --- 1 ------- 1 -- 2 -- 2 -- 3 ------- 3 --
C的时钟是A的时钟和when B的时钟的并集。
cell运算符的优先级与when相同。
对于delay运算符,可在使用cell时初始化,或在声明被赋值信号时初始化,如下例所示:
C1 := A cell B init 0
| C2 := A cell B
cell运算符不属于Signal语言的核心,因为可以用其它运算符来实现它:
C ^= A ^+ (when B)
C := A default (C $ init 0)
5.1.2. cell运算符的使用
我们用以下式子来使所有信号X、Y、……具有公共时钟H:
H ^= X ^+ Y ^+ ...
XX := X cell H
YY := Y cell H
....
为了在另一个信号X的瞬间重复信号A的值,把X的时钟用作为一个永远为true的布尔信号:
AX := A cell ^X
如果只在X的瞬间移动A而不保持A本身:
A_ON_X := (A cell ^X) when ^X
所以A_ON_X具有A的时钟。
以下是一个例子:
( ? real COEF, % one value %
X; % sequence of reals %
! real Y; ) % Y = COEF * X for all X %
(| Y := ((COEF cell ^X) when ^X ) * X |)
5.2. 模块化
一个进程可以被一个系统的其它进程作为子进程来调用。调用者进程设置常数参数(如果有),给定输入信号,使用输出信号。例如,可以如下方式调用WATCHDOG:
PB_TIME := WATCHDOG {5} (DEMAND, when OVER, ^SECONDS)
参数值可以是任何常数表达式,它可包括本地参数,但不能包括信号。有效的输入信号是信号的表达式。如果被调用进程只有一个输出,那么调用语句可作为函数调用而放入任何信号表达式。
5.2.1. 调用格式例子
下例说明了其它可能的调用格式:
process MODU =
( ? integer A;
! integer S;)
(| BOOL ^= A
| S := -1 when BOOL
default 2 * ONE (A) + 1
default INT
| (INT, BOOL) := NO_IN {false} ()
| NO_OUT (INT, BOOL)
|)
where
integer INT; boolean BOOL;
process ONE =
( ? integer I;
! integer RES; )
(| RES := I/2 when I modulo 2 = 1 |)
;
process NO_IN = {boolean B0;}
( ?
! integer I; boolean B; )
(| I := 0
| ZB := B$ | B := ZB |)
where
boolean ZB init B0;
end % NO_IN %;
process NO_OUT =
( ? integer I; boolean B;
! )
(| I ^= when not B |)
end % MODU %;
5.2.2. 取模计数器子进程
假设输入信号TICK每秒出现。进程要在TICK出现的瞬间产生天、时、分、秒数。
进程CNT_MOD有两个事件输入,一是增加计数器,二是获得计数器的值。当计数器变成RESET为0时进程发出一个事件。
process BIG_BEN =
( ? event TICK;
! integer DAY, HOUR, MINUTE, SECOND; )
(| (SECOND, NEW_MINUTE) := CNT_MOD {60} (TICK, TICK)
| (MINUTE, NEW_HOUR ) := CNT_MOD {60} (TICK, NEW_MINUTE)
| (HOUR , NEW_DAY ) := CNT_MOD {24} (TICK, NEW_HOUR)
| DAY ^= TICK
| DAY := DAY$ + 1 when NEW_DAY default DAY$ init 1
|)
where
event NEW_MINUTE, NEW_HOUR, NEW_DAY;
process CNT_MOD = % Counter modulo N %
{ integer N;}
( ? event EV_OUT, % when Counter must be output %
EV_INC; % when Counter must be increased %
! integer CNT;
event RESET; )
(| CNT ^= EV_OUT ^+ EV_INC
| CNT := (ZCNT+1) modulo N when EV_INC
default ZCNT
| ZCNT := CNT$ init 0
| RESET := when CNT = 0 when EV_INC
|)
where
integer ZCNT;
end % CNT_MOD %;
end % BIG_BEN %;
5.3. 外加采样(Oversampling)
我们已看到可以约束输入信号的时钟:
process ADD =
( ? integer A, B;
! integer S; )
(| S := A + B |)
这里要求A和B的时钟是相等的。
可以更复杂的方式关联输入信号的时钟:
( ? integer X, Y;
....
| X ^= when Y = 0
这里Y的时钟是“主时钟”。上式表示只有当Y的值是0时才读X的值。
某些输入的时钟还可与一个本地信号关联:
( ? integer X;
...
(| FLIP := not (FLIP$ init false)
| X ^= when FLIP
where
boolean FLIP; ...
因此输入时钟不一定是较快的。FLIP信号在两次读X之间的“中间”瞬间出现。如果两次读X之间有多个瞬间,那么FLIP信号有足够时间来出现。这些“中间”瞬间称为外加采样(Oversampling)。
这个较快的信号可与其它信号同步,并且可以更精确地设置其时钟。
当然,可以对时钟施加的约束是有限制的。例如,when A=0 ^= when B=0是不可接受的,因为不可能在编译时保证A和B永远同时为0。
以下约束也是不可接受的:
A default B ^= when condition
6. 应用
6.1. 事件的间隔
以下假设START与FINISH是两个相隔一定时间的事件,FINISH在START之后。
6.1.1. START与FINISH之间的间隔
START与FINISH之间的间隔用一个时钟事件H在这个间隔中法出的脉冲个数来度量。CNT是一个计数器,在每个事件上出现,但只在事件H上增加。它在START上复位,在FINISH上给出START与FINISH的间隔。
| CNT ^= START ^+ FINISH ^+ H
| CNT := 0 when START
default CNT$ + 1 when H
default CNT$
| DURATION := CNT when FINISH
START : -----*-------------*------------------------
FINISH : -------------*-------------------------*----
H : -*---*---*---*---*---*---*---*---*---*---*--
CNT : -?---0---1---2---3-0-1---2---3---4---5-5-6--
DURATION: ------------ 2 ----------------------- 5 ---
如果START和FINISH精确地在H上出现,那么时间度量是精确的,否则,DURATION是START与FINISH之间的H的个数,并包含边界,误差小于一个间隔。
6.1.2. 事件是否在START与FINISH之间出现
可以测试一个事件是否在START与FINISH之间出现。假设H是一个事件,IN是一个H是否出现的布尔信号,可以把H的所有是否出现的状态保存在MEM中:
| MEM ^= START ^+ FINISH ^+ H
| MEM := START default not FINISH default (MEM$ init false)
% true when START default false when FINISH default MEM$ %
| IN := MEM when H
当H与START或FINISH同时出现时是什么情况?根据以上条件,H与START同时出现时H在间隔中,与FINISH同时出现时在间隔外。
6.2. 自动机
有限状态自动机经常被用于实时系统的建模,一个自动机有若干状态Si,其中一个状态是初始状态S0。当一个事件ei出现时,自动机可能改变当前状态,或在当前状态上循环,并执行某些合适的动作。
在Signal中,用一个变量S来标记状态迁移的到达状态,它的延迟值ZS表示出发状态。S的时钟是所有输入信号的时钟的并集。其它信号改变了值或达到某个值将引起状态迁移。
在异步语言中,自动机的状态可能是不确定的,例如当两个事件同时发生时,将迁移到哪个状态是不定的。而Signal程序必须规定事件的次序。
下例是关于灯的开关按钮。假设按钮按一下灯亮,再按一下灯灭,或在一分钟后自动灭。可以用一个两状态的自动机来描述:灯OFF时S=1, 灯ON时S=2。
process LIGHT =
( ? event SWITCH, H; % H every second %
! event PUT_ON, PUT_OFF;
)
% State changes %
(| S ^= SWITCH ^+ H ^= CNT
| S := 3 - ZS when SWITCH
default 1 when ZS = 2 when ZCNT = 1
default ZS % loop on current state %
| ZS := S $ init 1
% Actions on transitions %
| CNT := (60 when ZS = 1 default ZCNT - 1) when S = 2
default ZCNT
| ZCNT := CNT $ init 0
| PUT_ON := when S = 2 when ZS = 1
| PUT_OFF := when S = 1 when ZS = 2
|)
where
integer S, ZS, CNT, ZCNT;
end % LIGHT %;
计数器CNT只是在S为2 时使被使用,但它的延迟值ZCNT有相同的时钟,并且在S为1 时也被使用。CNT的定义方程是递归的,因此必须在外部定义它的时钟。
参考资料
【1】 Bernard HOUSSAIS,The Synchronous Programming Language A Tutorial,24th September 2004
【2】 Bernard HOUSSAIS,Cours de Programmation en Langage Synchrone SIGNAL, 24 septembre 2004