Dataflow regions
Equations that are worked out again in every tick, for the parts of a controller that are wiring rather than modes.
Some parts of a controller have no modes at all. A siren that sounds whenever the alarm is armed and a door is open; a reading converted from one unit to another; a value that follows another one tick late. You could write each of these as a state with a self-loop, but the state adds nothing: the output is a formula over the inputs, in every tick. A dataflow region says exactly that. Instead of states and transitions it holds equations, and every equation is worked out again in every tick.
Equations instead of states
Section titled “Equations instead of states”Write dataflow where you would write region, and put equations inside:
Diagram
Code
scchart Alarm { input bool armed, door, window output bool siren, ready
dataflow { siren = armed && (door || window) ready = !door && !window }}| Tick | input bool armed | input bool door | input bool window | output bool siren | output bool ready |
|---|
Guided tour, 5 ticks:
- armed = false, door = false, window = falseEquations run in every tick, the first one included. Everything is shut, so
readyis true; the alarm is not armed, sosirenis false. - door = trueThe door opens.
readydrops to false in this same tick.sirenstays false: the alarm is still not armed. - armed = trueArming with the door open:
armed && (door || window)is true, sosirenis true at once. There is no state to move to and no delay. - door = falseThe door shuts and both outputs follow immediately:
sirenfalse,readytrue. - window = trueA window opens while armed:
sirentrue,readyfalse, again in the tick the input changed.
Each line such as siren = armed && (door || window) is an equation: a variable on the left, any
expression on the right. In every tick in which the region is active, the first tick included, KIELER
evaluates every equation and assigns the result. Nothing triggers it and nothing delays it: siren is
true in exactly the ticks in which the right-hand side is true.
If you know during actions, an equation behaves like
immediate during do siren = armed && (door || window): it runs in every tick, the entry tick included.
The dataflow form is shorter, and it reads as a description of wiring rather than as an action.
A few details:
- The braces can be left out:
dataflow:followed by the equations, up to the next region or the end of the chart. (Ordinary regions have the same short form,region:followed by their states.) - A dataflow region may start with declarations of its own (
bool over, say, before the equations). They are locals of that region. - The left-hand side must be a variable. A signal there is rejected with
Non-variables cannot be used in an assignment.; use abooloutput instead. - KIELER draws a dataflow region as a circuit: inputs on the left, operators as boxes, outputs on the right, wires in between. The diagrams on this site use a simplified version of that drawing.
The order of the equations does not matter
Section titled “The order of the equations does not matter”Equations are not statements run from top to bottom. Here the first equation reads celsius, which the
second one computes:
Diagram
Code
scchart Celsius { input int fahrenheit output int celsius output bool freezing
dataflow { freezing = celsius <= 0 celsius = (fahrenheit - 32) * 5 / 9 }}| Tick | input int fahrenheit | output int celsius | output bool freezing |
|---|
Guided tour, 4 ticks:
- fahrenheit = 50
freezingis written first but readscelsius, so KIELER computescelsiusfirst: (50 − 32) * 5 / 9 = 10, andfreezingis false. The order of the lines does not matter. - fahrenheit = 32
celsius= 0, sofreezingis true, both in this tick. - fahrenheit = 14(14 − 32) * 5 / 9 = −90 / 9 = −10: still
freezing. - fahrenheit = 10068 * 5 / 9 = 340 / 9, and
intdivision truncates:celsius= 37 (37.8 in real numbers).
freezing is written first but still sees this tick’s celsius.KIELER turns every equation into a small region of its own, so the equations of a dataflow region run
concurrently, like the regions of chapter 8, and the same rule decides their
order: within a tick, a variable is written before anything reads it. So celsius is computed before
freezing in every tick, and a chain of equations settles within one tick however you arrange the
lines. (Tick 4 also shows that int arithmetic is C’s: 340 / 9 is 37, rounded towards zero.)
One equation per variable, and no instant feedback
Section titled “One equation per variable, and no instant feedback”Because equations are concurrent, the rules of chapter 12 apply to them. Two
equations that both set the same variable, say open = door and open = window, are two concurrent
writes that disagree, and KIELER rejects the chart. So does a set of equations that needs its own
result in the same tick:
Diagram
Code
scchart Feedback { input int I output int X, Y
dataflow { X = Y + I Y = X }}KIELER rejects this chart
- Line 7, column 9
XandYdepend on each other within one tick: each waits for the otherKIELER says:The SCG is NOT asc-schedulable!
Xis set on line 6 and read on line 7;Yis set on line 7 and read on line 6. Within a tick every write of a variable, and every emission of a signal, comes before it is read. Break the cycle withpre(X)(last tick's value), by making one side wait a tick, or by moving the logic into one region.About this message
X needs this tick’s Y, and Y needs this tick’s X: neither can go first.Break the loop with a delay, so that one side uses the value from the previous tick
(pre, more on it below). KIELER accepts this version:
Diagram
Code
scchart FeedbackFixed { input int I output int X, Y
dataflow { X = pre(Y) + I Y = X }}| Tick | input int I | output int X | output int Y |
|---|
Guided tour, 3 ticks:
- I = 1
pre(Y)is 0 in the first tick, soX= 1 andY= 1. - I = 1
pre(Y)is last tick’sY(1):X= 2,Y= 2. The loop is broken by the tick boundary, so it is fine. - I = 2
X= 2 + 2 = 4,Y= 4: the chart adds upItick after tick.
X = pre(Y) + I adds I to last tick’s Y: with I = 1, 1, 2 it gives X = 1, 2, 4.Looking back: ->, fby and pre
Section titled “Looking back: ->, fby and pre”Dataflow needs a way to talk about earlier ticks. Three operators provide it:
Diagram
Code
scchart Delays { input int level output int now, start, late, before
dataflow { now = level start = 100 -> level late = 100 fby level before = pre(level) }}| Tick | input int level | output int now | output int start | output int late | output int before |
|---|
Guided tour, 4 ticks:
- level = 5The first tick is special:
startandlatetake their first operand, 100.pre(level)has no previous tick to look at, sobeforeis 0, the default ofint. - level = 6From now on
->gives its second operand as it is now:start= 6.fbygives its second operand as it was one tick ago:late= 5.beforeis also last tick’slevel: 5. - level = 9
nowandstartfollowlevel(9);lateandbeforeare one tick behind (6). - level = 9
levelstays at 9, so after one more tick the delayed outputs catch up: everything is 9.
start and late begin at 100, before at 0. From tick 2, now and start show this tick’s level; late and before show last tick’s.The chart’s equations are now = level, start = 100 -> level, late = 100 fby level and
before = pre(level). Tick by tick:
| Tick | level |
now |
start |
late |
before |
|---|---|---|---|---|---|
| 1 | 5 | 5 | 100 | 100 | 0 |
| 2 | 6 | 6 | 6 | 5 | 5 |
| 3 | 9 | 9 | 9 | 6 | 6 |
| 4 | 9 | 9 | 9 | 9 | 9 |
a -> b(read it “a, then b”) isain the first tick andbin every later tick. It initialises.a fby b(“a, followed by b”) isain the first tick and, in every later tick, the valuebhad in the previous tick. It delays by one tick and says what to show before there is a previous tick.a -> pre(b)means the same thing, and KIELER rewrites it toa fby b.pre(x)is the valuexhad at the end of the previous tick, as in chapter 11. In the first tick it is 0 (orfalse), whatever initial valuexwas declared with.
“The first tick” means the first tick of the state that contains the dataflow region. If that state is
left and entered again, -> and fby start over and give their first operand once more.
With a delay in between, an equation may use its own earlier value. That is how counters and running totals are written:
// 0, 1, 2, 3, ...ticks = 0 fby (ticks + 1)// all coins so fartotal = pre(total) + coinKIELER also accepts an equation that reads its own variable with no delay at all, such as n = n + 1
(or n += 1). It treats it as an update of the old value, so n counts 1, 2, 3, … from tick 1. If
you know Lustre or SCADE, where this would be a cycle, prefer the fby and pre forms: they say what
you mean.
The precedence trap
Section titled “The precedence trap”These two equations look alike:
Diagram
Code
scchart Count { output int wrong, right
dataflow { wrong = 0 fby wrong + 1 right = 0 fby (right + 1) }}Work out both counters after tick 3, run the chart, then open the explanation.
Explanation
After tick 3, wrong is 3 and right is 2: wrong counts 1, 2, 3, … and right counts 0, 1, 2, …
KIELER reads 0 fby wrong + 1 as (0 fby wrong) + 1, so the + 1 is added outside the delay, already
in the first tick.
The difference is in how KIELER’s grammar reads the delay operators. The operands of ->, fby and
sfby, and the three parts of the conditional c ? a : b, must each be an atom: a literal, a name, a
call such as pre(x), or an expression in parentheses. So these operators bind more tightly than any
arithmetic or comparison around them:
| You write | KIELER reads |
|---|---|
0 fby wrong + 1 |
(0 fby wrong) + 1 |
0 -> I + 100 |
(0 -> I) + 100: 100 in the first tick, I + 100 later |
A > B ? A : B |
A > (B ? A : B), which is 0 (false) whenever B is not 0 |
Put parentheses around every operand that is more than a single name or number:
0 fby (right + 1), 0 -> (I + 100), (A > B) ? A : B.
A delay whose type KIELER cannot work out
Section titled “A delay whose type KIELER cannot work out”KIELER keeps the value of every -> and fby in a helper variable, and works out the helper’s type
from the names and literals in the whole expression. If they mix bool and int, it gives up, and the
generated C declares the helper with the type UNKNOWN:
Diagram
Code
scchart MixedTypes { input int level output bool high
dataflow { high = false -> (level > 3) }}KIELER rejects this chart
- Line 6, column 12KIELER cannot give the
->expression a type: its values mix booleans, numbers or stringsKIELER says:Cannot serialize simulation interface. Unsupported type: UNKNOWN
KIELER stores->in a helper variable whose type comes from all the values and variables in the expression. Make them all one type, for example writelevel > 3into aboolvariable first and use that variable here.About this message
false is a bool, level an int: KIELER cannot type the helper of ->, and the simulation fails.Compute the comparison into a bool variable of its own first (over = level > 3), then write
high = false -> over.
Equations in sequence: ; and seq
Section titled “Equations in sequence: ; and seq”Sometimes a value is easiest to compute in steps, each refining the last. End an equation with a
semicolon, or with the keyword seq, and it runs before the equation that follows it, like a statement:
Diagram
Code
scchart Clamp { input int raw output int level
dataflow { level = raw; level = (level > 100) ? 100 : level; level = (level < 0) ? 0 : level }}| Tick | input int raw | output int level |
|---|
Guided tour, 4 ticks:
- raw = 42The semicolons chain the three equations into one sequence, run top to bottom like statements: 42, still 42, still 42.
level= 42. - raw = 250250, then the second line caps it at 100, then the third leaves 100 alone:
level= 100. - raw = -7−7 passes the upper limit and the third line raises it to 0:
level= 0. - raw = 100100 is not greater than 100, so it stays:
level= 100.
In a sequence a variable may be written several times and read in between. Without the semicolons the
three lines would be three concurrent writes of level, and KIELER rejects that chart with the
scheduling errors from above.
Wiring up instances of other charts
Section titled “Wiring up instances of other charts”A dataflow region can also connect copies of other charts, like chips on a circuit board. Here two half adders make a full adder, which adds three bits:
Diagram
Code
scchart FullAdder { input bool a, b, carryIn output bool sum, carryOut ref HalfAdderCell h1, h2
dataflow { h1 = {a, b} h2 = {h1.s, carryIn} sum = h2.s carryOut = h1.c || h2.c }}
scchart HalfAdderCell { input bool x, y output bool s, c
dataflow { s = x != y c = x && y }}| Tick | input bool a | input bool b | input bool carryIn | output bool sum | output bool carryOut |
|---|
Guided tour, 5 ticks:
- a = false, b = false, carryIn = false0 + 0 + 0: both half adders output false, so
sumandcarryOutare false. - a = true1 + 0 + 0 = 1:
h1.sis true,h2adds the carry 0 to it, sosumis true. - b = true1 + 1 + 0 = binary 10:
h1.cis true, socarryOutis true andsumfalse. Both instances react in the tick the input changed. - carryIn = true1 + 1 + 1 = binary 11:
sumandcarryOutare both true. - a = false0 + 1 + 1 = binary 10:
h1.sis true andh2turns it, with the carry, intocarryOuttrue andsumfalse.
h1 adds a and b, h2 adds the carry-in to h1’s sum, and either carry makes carryOut.ref HalfAdderCell h1, h2declares two instances of the chartHalfAdderCell. Each runs its own copy of that chart, concurrently with everything else.h1.x = asets one input of an instance.h1 = {a, b}sets its inputs in the order they are declared (x, theny); an_in the list skips one. Inputs you never set stay at 0 orfalse.h1.sreads an output.
The chart to simulate must come first in the file, since KIELER simulates the first scchart, and the
instance’s chart must be in the same file or imported (see Reusing charts). If
an instance’s chart terminates, KIELER starts it again from its initial state one tick later.
Dataflow next to states
Section titled “Dataflow next to states”A state can contain dataflow regions and ordinary regions side by side. They are concurrent, so they communicate within the tick:
Diagram
Code
scchart FridgeMonitor { input int temperature output bool warm output bool alarm = false
dataflow { warm = temperature > 5 }
region { initial state Normal if warm do alarm = true go to Alarm
state Alarm if !warm do alarm = false go to Normal }}| Tick | input int temperature | output bool warm | output bool alarm | Active state of the chart |
|---|
Guided tour, 4 ticks:
- temperature = 33 degrees: the equation makes
warmfalse. The control-flow region entersNormal. - temperature = 7The equation and the region run concurrently, and the region reads
warmonly after the equation has written it. Sowarmbecomes true andNormal→Alarmfires in the same tick:alarm= true. - temperature = 8Still warm: the chart stays in
Alarm. - temperature = 4Back to 4 degrees:
warmis false andAlarm→Normalfires in this tick, clearingalarm.
warm and the region reacts to it in the same tick.This split is often the clearest design: equations for the values that are always a formula of the inputs, states for the behaviour that has modes.
“Every tick, for ever” has a consequence: a dataflow region never finishes, so a superstate that
contains one never takes its join to transition. The exception is dataflow once { ... }, whose
equations run only in the tick its state is entered, after which the region terminates at once.
Three KIELER bugs to steer around
Section titled “Three KIELER bugs to steer around”The delay operators have three bugs in KIELER. The first two give wrong results without any message, so it pays to know them.
sfby chains stop early. sfby (“sequential followed by”) with two operands behaves like ->. A
chain a sfby b sfby c should give a, then b, then c, but KIELER never gets to c. With the
equations two = 1 sfby level and chain = 1 sfby 2 sfby 3:
| Tick | level |
two |
chain |
|---|---|---|---|
| 1 | 10 | 1 | 1 |
| 2 | 20 | 20 | 2 |
| 3 | 30 | 30 | 2 |
| 4 | 40 | 40 | 2 |
Use -> instead of sfby, with two operands: in a chain a -> b -> c the middle operand is never used.
fby outside a dataflow region blocks termination. -> and fby also work in ordinary effects,
such as immediate do a = 0 fby level go to End. But KIELER implements fby with a hidden helper region
that never finishes, so a superstate that uses fby anywhere inside can never terminate. In this
experiment two superstates are identical except for the operator:
Diagram
Code
scchart FbyTermination { input int level output int a = 0, b = 0 output bool doneA = false, doneB = false
region WithFby { initial state CopyA { initial state Start immediate do a = 0 fby level go to End final state End } join to FinishedA
state FinishedA { entry do doneA = true } }
region WithArrow { initial state CopyB { initial state Start immediate do b = 0 -> level go to End final state End } join to FinishedB
state FinishedB { entry do doneB = true } }}| Tick | input int level | output int a | output int b | output bool doneA | output bool doneB | Active state of region WithFby | Active state of the region of CopyA | Active state of region WithArrow | Active state of the region of CopyB |
|---|
Guided tour, 2 ticks:
- level = 5
CopyAandCopyBboth reach their final stateEndin tick 1.CopyB, which uses->, terminates and itsjoin tofires:doneB= true.CopyA, which usesfby, does not:doneA= false. - level = 6Nothing changes: KIELER’s helper region for
fbynever finishes, soCopyA’sjoin tocan never fire.
Note: Because of this fby, CopyA never finishes: its join to never fires
- Note, line 9, column 26KIELER computes fby in a helper region of
CopyAthat has no final state, soCopyAcannot terminate. Usepre(...)or a variable updated on a transition instead, or leaveCopyAwith a normal transition.
CopyA and CopyB both reach their final state in tick 1. Only CopyB, which uses ->, terminates: doneB becomes true, doneA never does.This site’s simulator reproduces the bug, so that it behaves like KIELER. Keep fby inside dataflow
regions.
Mixed types in a delay fail, as shown above, with
Cannot serialize simulation interface. Unsupported type: UNKNOWNCompiler messages explains it.
Try it
Section titled “Try it”Did it change?
Make changed true in every tick in which level differs from its value in the previous tick, and
false in the first tick, when there is no previous tick to compare with. Mind both the precedence trap
and the type trap.
Show the starter chart
scchart Changed { input int level output bool changed
dataflow { changed = false }}Show a solution
scchart Changed { input int level output bool changed bool differs
dataflow { differs = level != pre(level) changed = false -> differs }}Running maximum
Make highest the largest level seen so far, this tick’s included. It must work for negative readings
too, so it cannot start from 0. One equation is enough.
Show the starter chart
scchart Highest { input int level output int highest
dataflow { highest = level }}Show a solution
scchart Highest { input int level output int highest
dataflow { highest = level -> ((level > pre(highest)) ? level : pre(highest)) }}Check your understanding
Section titled “Check your understanding”Show the answer
Answer A: 0
In the first tick pre(I) is the default of the type, 0, whatever initial value I was declared with.
From tick 2 it is the value I had at the end of the previous tick (here 5).
Show the answer
Answer D: X = Y + 1 and Y = X
X = Y + 1 and Y = X need each other’s value in the same tick: a causality cycle. With Y = pre(X)
the delay breaks the cycle. The semicolon after X = I makes a sequence, so X is written twice, in
order. n = n + 1 reads the value from before its own write and counts up.
Key ideas
- A
dataflowregion holds equations. Every equation is evaluated again in every tick the region is active, the first tick included. - Equations run concurrently: their order does not matter, but each variable gets one equation, and
equations may not need each other’s results in a loop within one tick (break it with
preorfby). a -> binitialises,a fby bdelays by one tick with a start value,pre(x)delays and starts at 0 orfalse. Parenthesise their operands: they bind more tightly than arithmetic.- An equation ending in
;orseqbecomes a step of a sequence. ref Chart hdeclares an instance; set its inputs withh.x = ...orh = {...}and readh.y.- Avoid
sfby, and keepfbyinside dataflow regions: KIELER gets both wrong.
Next, Time: clocks, timeouts and periods measured in real time.