Language Description

Types and Literals

Antares DSL is a dynamically typed programming language. Expressions do have a type, but type checking when evaluating assignments or calling functions is only done at runtime.

Antares DSL supports the following basic types:

  • Number

    • Long (used in digital circuits)

    • Float (used in analog circuits)

  • String

  • Digital Signal

Number values are primarily used as counting variables in loops and in arithmetic expressions. Strings are currently only used as names of pins in circuits. Most of the time, you will use digital signal values.

Antares DSL uses special prefixes to specify hexadecimal or binary values of digital signals.

  • 0x (Hexadecimal literal)

  • 0x? (Undefined / floating hexadecimal value)

  • 0b (Binary literal)

// Long number literal
42

// Float number literal
3.14

// String literal
"Hello"

// Hexadecimal literal
0xFA

// Hexadecimal literal with 8 undefined (floating) bits
0x?8

// Binary literal
0b1001

// Binary literal with undefined (floating) bit
0b00Z01

When hexadecimal values are combined with or assigned to other hexadecimal values with a different bit width, the value with the smaller bit width is left padded with zeros.

Note that there is no "Boolean" type. Booleans are represented as Numbers, where 0 (zero) is interpreted as "false", and every value other than 0 is interpreted as "true".

Statements and Expressions

An Antares DSL script is a sequence of statements. All of these statements are dealing somehow with expressions that get evaluated to values. Statements return those values as their result. These results can be used by other statements that surround those statements, or are simply returned as the final result of a script.

This is important to remember when writing conditions scripts from which Antares expects a number to be interpreted as boolean value. In such scripts, the value returned by the last statement of the script will be used as return value of the script.

// This would just return 0
0

// This would return the result of evaluating the expression
A * (B + C)

// This would return the result of the `then` block in the `if` statement
if (1) {
    A + B
} else {
    C
}

Operations

The kind of operations you can use in expressions depends on the type of terms the expression contains.

When expressions contain digital signals that are fully or partially undefined, the user preference "Undefined Gate Input Behaviour" is applied to derive a defined value to be used in the expression. The default value of this preference is 0.

Arithmetic Operations

Antares DSL supports the following arithmetic operations.

  • + (Integer addition)

  • - (Integer subtraction)

  • * (Integer multiplication)

  • / (Integer division)

  • ^ (Integer power)

  • % (Modulo operation)

  • << (Bitwise shift left)

  • >> (Bitwise shift right)

A + 25 - B
A * (B + C)
A / B
A % 2

// Raises 2 to the power 3
2^3

// Shift every bit of 128 by 2 bits to the right
128 >> 2

Logic Operations

Antares DSL supports the following logic operations. These logic operations are performed on every bit of the two terms (or the one term in case of the unary "not" operation).

  • and (Bitwise AND)

  • or (Bitwise OR)

  • not (Bitwise NOT)

A and B
A and (B or C)
not A

Comparison Operations

Antares DSL support the following comparison operations. The result of a comparison operation is a value of type "Number", where 0 represents "false" and 1 represents "true".

  • == (Equal)

  • != (Non equal)

  • > (Greater than)

  • >= (Greater than or equal)

  • < (Smaller than)

  • <= (Smaller than or equal)

A == 42
A != B
A > B
A >= 17
B <= A

Bit Access Operations

Bit access operations are available both for numbers and digital signals and allow to query and to set individual bits of such values.

The bit access operator @ uses the bit position to reference an individual bit within a value, where the first (least significant bit) is referenced by 0 (zero).

// Querying bit 2 of value 4 (binary 100) returns 1
4@2

// Use expressions for the bit position
A@(I + 1)

// Setting bit 0 in value 4 (binary 100) returns 5 (binary 101)
4@0 = 1

Mixing Types in Operations

Since Antares supports arithmetic operations on both numbers and signals signals, you can mix values of different types in an operation.

F0 + 2 + F1

In this example, F0 and F1 are the names of input pins and are therefore mapped to variables holding a digital signal.

The first thing to be aware of is that operations on digital signals try to produce results with the same bit width. If you add two 1-bit digital signals, the result is still a 1-bit signal, although this might lead to an overflow and therefore to a result different from what you’ve might expected. The reason for this behaviour is that its results are closer to the signals produced by your circuit when simulated in "deep simulation" mode.

If you mix types in an operation, the term of the left side determines the type of the result. Therefore, the result of

2 + F1

is a number and not a digital signal, because 2 is a number. Adding digital signals to numbers doesn’t limit the result to a particular size other than the maximum length of numbers.

Therefore, when your script needs to avoid data truncation by overflow, the above example should be formulated as

2 * F1 + F0

Since expressions are evaluated from left to right, the first term 2 * F1 evaluates to a number, and then adding that number to F0 still evaluates to a number.

There are situations when you can’t live with results of digital signal expressions being truncated to the bit width of the left-most term. An example of such a situation is an execution script of a multiplier circuit with 8 bit inputs A and B and a 16 bit output O. If you would simply write the execution script as O = A * B, the result would never exceed 255, because the result of the multiplication is truncated to 8 bit before it is assigned to the output.

To overcome this limitation, you can explicitly convert digital signals to numbers using e.g. the following expression:

// Either so
O = (1 * A) * (1 * B)
// or so
O = (0 + A) * (0 + B)

The seemingly redundant multiplication by 1 converts the signal values to numbers which will then be used for expression evaluation without any upper bound other than the maximum value for long numbers.

Variables and Assignments

As in any other programming language, you can use named variables to store values. Variables must be explicitly declared using var.

Variable are case-sensitive; their names must start with a character.

Use the assignment operator "=" to assign a value to a variable.

var A = 42
var B1 = A * (C + 1)

Variables are accessible from the scope they are declared in, or in any nested scope. Variables can be redeclared within a deeper scope.

var A = 42
var B = 17
if (B == 17) {
    // This creates a another instance of A within this scope.
    // The outer A is not changed.
    var A = 0

    // This accesses B from the outer scope
    B = 1
}

Depending on the context in which your script is executed, Antares makes sure that some variables are already predefined. For example, if you write the execution logic script of a RS Latch, the pin names "S", "R", "Q" and "!Q" are automatically predefined, which allows you to easily access the signals at these pins just by using them as variables.

If you want to use scripting in your circuits, make sure you don’t name your pins like any of the reserved keywords of Antares DSL (like var, if, then and so forth.)
if (S and R) {
    Q = 0
    '!Q' = 0
    return
}
If your pin name doesn’t start with a character or contains white space characters such as blanks, you have to quote the pin name using single quotes. This is for example always the case with negated pin names like "!Q".

Arrays

While variable values are usually "scalar", they can also represent an array of values. This can be useful when writing an execution script for a circuit like a "Register File" in a CPU that has to store multiple register values.

// Write array field
R[0] = 0xE3
R[1] = 0x73

// Read array field
A = R[0]
Although arrays in Antares DSL are conceptually "associative arrays" (i.e. more like maps than arrays) and would support access to values using arbitrary keys, currently only keys of type "Number" are supported.

Conditions

Conditions are used to execute code blocks depending on a certain condition.

The if statement is used if there are only two options.

if (A == 0) {
    B = 0xF
} else {
    B = 0x0
}

The when statement is used if there are more than two options.

when (A) {
    1 : B = 11
    2 : B = 22
    3 : {
        B = 33
        C = 0
    }
    else : B = 99
}

Loops

Use the for statement to iterate over a block of code while incrementing (or decrementing) a dedicated loop variable.

for (i in 1 to 3) {
    A[i] = 2 * i
}

Functions

Antares DSL supports calling functions that consume argument values and return a result value.

var A = 0
hello(A, 42)

var B = hello(A, 0) + hello(A, 1)

Some functions are globally defined and are available to all scripts written in Antares DSL. See Global DSL Functions for a description of these functions.

Other functions depend on the context the script is running in. For example, when writing action script for Antares "Usecases", Antares provides the function setInputAt(time, inputName, signal) that allows to set the input of a circuit at a particular simulation time.

Check out the corresponding user manual sections to learn what external functions are available in particular contexts.

The current version of Antares DSL does not yet allow you to define your own functions.

Return Statement

Use the return statement to exit the entire script.

if (A) {
    return 1
}

The expression of the return statement is optional. If you don’t specify an expression, 0 is returned.

Asserted Pins

When writing execution scripts for subcircuits, it is often necessary to calculate output signals depending on whether input pins have been asserted or not. An edge-triggered D flipflop for example changes its state only when the clock input "C" has been asserted, i.e. went from 0 to 1 in the current simulation step.

Use the unary ^ operator to find out if an input port has been asserted.

if (^C) {
    Q = D
    '!Q' = not D
}
The assertion operator respect the "Logic" property of input pins. If an input pin has negative logic, the assertion operator returns 1 if the input pin changed from 1 to 0.

Initialization Code Blocks

When writing execution scripts for subcircuits, you sometimes want to have code that is only executed when the simulation is started, for example to initialize output pin values. You can do so using the init code block.

init {
    Q = 0
    '!Q' = 1
}

Keeping values across script calls

During simulation of a circuit, your scripts are called very often. In the case of subcircuit execution scripts, they are called whenever an input pin has changed its value. When a script gets called, its memory state is reset by Antares, meaning that all variable values from former runs are cleared.

However, there are situation where you need to keep such values across multiple script call. An example of such situations is a script for a CPU’s register file that must contain register values across script calls. You can achieve this using store variable declarations.

init {
    store fcStore
    for (i in 0 to 15) {
        fcStore[i] = 0
    }
}

Think of store variable declarations as normal var declarations, but with a live time that spans multiple script calls.

store variables are typically declared within an init block.

After having declared a variable as a store variable, in can be accessed like any other normal variable.

store fcStore
if (ENC and ^CLK) {
    if (FC == 10 or FC <= 4) {
        fcStore[FC] = C
    }
}

Accessing Circuit Elements

So far, we have seen how a subcircuits scripts accesses the values of the subcircuit’s pins by simply accessing them as normal variables.

O = I1 and I2

But what about situations where your script runs in the context of an entire circuit and you need to access the pins of any of the circuit’s element? This demand arises especially when writing scripts for Antares "Usecases" that need to display explanations depending on the state of the entire circuit.

To access pin values of any of the circuit’s element, you can use the "Property access syntax" based on the # operator.

#1:A == 1 and #1:B == 0

The first number right after the # tag represents the model ID of the circuit components whose pins you want to access. The second value right after the : identifies an individual port of that circuit component. Both together are resolved to the corresponding pin’s current signal value.

How do you find out the model ID of a circuit component? Select the component in your circuit and consult the property "Model ID" in the properties window.

The above example identifies pins by their names. Remember to use quoted port names like "!Q" if the port name doesn’t represent a legal variable name.

Since names are not mandatory for ports (and a lot of built-in components like logic gates don’t have port names), is is also possible to reference ports by their ID.

#1:1 == 1 and #1:1 == 0

There are two ways how to find out the ID of a port in a circuit component.

  • Hover with the mouse in the circuit over the port. The displayed tooltip shows the port ID if the circuit is in edit mode (i.e. not being simulated).

  • For custom subcircuits, you can also go to the subcircuit’s symbol editor, select the port in the editor, and use the value displayed in the property "Port ID".

To avoid the need to go to the property window every time to need a port ID of a built-in component, you can also use the following rules of thumb to figure it out by yourself.

  • Port IDs start with 1

  • Inputs come before outputs

Example

The following example has been taken from the "Register File" circuit in the "Tanenbaum CPU" example project.

The register file contains some 16 bit registers and some constant values used for microprogram execution. The circuit has a 16 bit input "C" and two 16 bit output "A" and "B". The "FA" an "FB" 4 bit inputs determine which of the 16 values should be written to the "A" and "B" output. The 4 bit "FC" input determine which register should store the new value arriving at "C" when "EN" is high and "CLK" is asserted.

init {
    store fcStore
    for (i in 0 to 15) {
        fcStore[i] = 0
    }
}

if (ENC and ^CLK) {
    if (FC == 10 or FC <= 4) {
        fcStore[FC] = C
    }
}

when (FA) {
    0 : A = fcStore[0]
    1 : A = fcStore[1]
    2 : A = fcStore[2]
    3 : A = fcStore[3]
    4 : A = fcStore[4]
    5 : A = 0
    6 : A = 1
    7 : A = 0xFFFF
    8 : A = 0x0FFF
    9 : A = 0x00FF
    10 : A = fcStore[10]
    else : A = 0x?16
}

when (FB) {
    0 : B = fcStore[0]
    1 : B = fcStore[1]
    2 : B = fcStore[2]
    3 : B = fcStore[3]
    4 : B = fcStore[4]
    5 : B = 0
    6 : B = 1
    7 : B = 0xFFFF
    8 : B = 0x0FFF
    9 : B = 0x00FF
    10 : B = fcStore[10]
    else : B = 0x?16
}

The register values are stored by the script using the store array variable "fcStore". Note how these values are initialized in the init block.

The if statement on line 8 make sure that the stored values are only updated if both "ENC" and "CLK" are asserted.

The when statements on line 14 and 29 dispatch the register or constant addresses in "FA" and "FB" to the corresponding output values "A" and "B".

It would be nice if the when statement could be used as an expression of the form

A = when (FA) {
    0: fcStore[0]
    1: fcStore[1]
    // ...
    else : 0x?16
}

This has not yet been implemented.