Tutorial
PL/i Programming Language for i.CanDoIt
This set of pages will provide brief tutorials about programming in PL/i. This language is more simple than C, but more structured than Basic. The objective in creating this language was to compile compact code that would run efficiently and safely as a virtual machine, like a very lean Java.
Index of PL/i Tutorials
PL/i Programming Language (Summary from Device)

The following summary provides an overview of PL/i structure, syntax and grammar. PL/i is a descendant of PL/1, and has been chosen for i.CanDoIt because it is structured yet relatively simple. It compiles to interpreted byte-code to provide a safe execution environment. Bounds checking is always done on array subscripts, stack overflows are trapped gracefully, and pointers to raw memory are not allowed.

PL/i is case sensitive. Therefore, as an example, the "if..then" statement must be "if..then", not "IF..THEN" or "If..Then". This also applies to variable names. Therefore, "MyVar" and "myvar" are two different variables.

The primary program structures supported by PL/i include:

if .. then .. else
for loop
while .. do
select .. when .. otherwise
assignments
procedure calls

The basic structure for a program is as follows:

program <name>
declare
<variable>: <type>;
begin
<program>
end

Hyperlink index:

data types & records
variables & arrays
constants
procedure declarations
math functions
logic operators
assignments

if..then..else
for loop
while..do
select..when..otherwise

register (I/O) access
time functions
flash & eeprom access
miscellaneous functions
comment lines
error codes

local network communications

Additional detail and greater elaboration on structure is illustrated in the following annotated example. This program is relatively useless in terms of accomplishing anything meaningful, but does illustrate several key features of PL/i. The "get" functions get (read) a local register and "set" functions set (write) a local register. These differ from program variables in that local registers may be linked to I/O or network objects and are also the subject of action rules, data logging, scheduling, etc. Links between PL/i programs and the time/date scheduler, for example, are done via local registers.

program test

declare
   type myRec =
      record
         ival1: int;
         ival2: int;
         bval1: bit;
         bval2: bit;
         fval1: float;
         fval2: float;
      end;
   type myRecordSet = array [1..20] of myRec;

   myVar: myRecordSet;
   var1: int;
   var2: int;
   var3: float;
   var4: boolean;
   var5: int;
   i: int;
   stuff: array [1..10] of int;

   #RoomTemp101 = 223;
   #RoomTemp22  = 224;
   #RoomTemp120 = 225;

   procedure update_me (var n: myRec)

   declare
      j: int;

      procedure update_you (var k: int)
      declare
      begin
         seti (102, k);
      end;

   begin
      n.ival1 = geti (104);
      n.fval1 = getf (1015);
      update_you (n.ival1);
   end;

begin
   var1 = geti (#RoomTemp101);
   var2 = geti (#RoomTemp22);
   for i = 1 to 10
      begin
         myVar[i].ival1 = i * 10;
         var3 = i;
         myVar[i].fval1 = var3 / 2.0;
      end;
   for i = 10 down to 1
      update_me (myVar[i]);
   select
      when (var1 = var2) and (var1 = var5) 
         begin
            seti (98, 3);
            seti (99, 3);
         end;
      when var1 = var3 seti (98, 0);
      when var2 = var3 seti (99, 0);
      otherwise seti (97, 2);
   end;
end
A program always begins with "program" and a name.
The first block to appear is the "declare" block. It may be empty, but the word "declare" must at least be there. Data types are declared first. All type declarations must be made before any variable declarations. Bits are packed into bytes when included in a record.
Variable names may reference a single variable, an array, a record, or an array of records. The type "int" is a 32-bit integer. Types "int16" and "int8" are 16-bit and 8-bit integers respectively.
Constants are declared using the # symbol and numeric value assignment. No variable space is allocated for constants, and constants may not be changed once assigned. Constant names are useful for parameterization of program code, resulting in easier maintenance and greater ease of understanding the code.
Procedures are declared next after variables. The structure of a procedure follows the same structure of the overall program. After the procedure name declaration, there is a "declare" section for local variables, followed by the begin..end section with the procedure's code.
A unique feature of some languages, including PL/i, is the ability to declare local procedures inside of procedures. A local procedure may only be called by the procedure in which it is declared.
After all type, variable, and procedure declarations, we get to the code of the main program. It is contained within the outermost begin..end keyword pair.
For loops may contain a single statement to be executed some number of times. To execute multiple statements, enclose them in a begin..end block.
For loops count up, or down when "down" is specified, by one. Increments other than one are not allowed.
The select..when..otherwise statement is similar to the C case statement. Each "when" selects a single statement to be executed. To select multiple statements, enclose them in a begin..end.

Data Types and Records

Named data types may be declared in the "declare" block of a program or procedure, and must appear before any actual variables are declared. The general syntax for a named type declaration is:

type <name> = <type>;
type <name> = array [<a>..<b>] of <type>;

The <name> may be any name starting with a letter, and containing only letters or digits or underscore after the first letter. Names are case sensitive. If an array is specified, the starting and ending subscripts <a> and <b> must be specified, and <b> must be greater than <a> (and both must be literal numbers).

The <type> may be any valid simple type, or may be a record declaration with a field list. Types recognized by PL/i are listed below. Be sure to observe the note about type casting under Assignment Statement further down.

int
integer stored as 32-bit signed integer
float
IEEE 754 floating point, 32-bit format
int16
integer stored as 16-bit signed integer
uint16
integer stored as 16-bit unsigned integer
int8
integer stored as 8-bit signed integer
uint8
integer stored as 8-bit unsigned integer
boolean
boolean having a value of 0 or 1, stored as 8-bit unsigned integer
bit
bit having a value of 0 or 1, same as boolean if variable, packed into 8-bit groups if record field
record
byte packed list of fields comprised of above types (see below)

Note that 8 bits are stored as 8 bytes when declared as independent variables, but 8 bits will be stored in a single byte when they are fields within a record. It should also be noted that integer values less than 32 bits are processed internally as interim 32-bit data values and truncated only when stored to declared variables. Therefore, the following combination is a legal bounds check:

program test
declare
myVar: uint16;
result: uint16;

begin
myVar = geti (100);
if int(myVar) * 10 > 65535 then result = 65535;
else result = int(myVar) * 10;
endif;
end

Record declaration syntax is as follows:

type <name> = record <field list> end;

Each element of the field list will follow the same syntax as a variable declaration as noted below. Refer to example above for an illustration of a record declaration.

Variables and Arrays

Variables may be declared in the "declare" block of a program or procedure, and must appear after named type declarations. The general syntax for a variable declaration is:

<name> : <type>;
<name> : array [<a>..<b>] of <type>;

The <name> may be any name starting with a letter, and containing only letters or digits or underscore after the first letter. Names are case sensitive. If an array is specified, the starting and ending subscripts <a> and <b> must be specified, and <b> must be greater than <a> (and both must be literal numbers).

The <type> may be any valid simple type from the table above, or any previously declared named type including record types. Refer to example above for an illustration of several variable declarations including simple, array, and named array of records.

Variables declared inside a procedure are referred to as "local" variables. These local variables exist only for the duration of the procedure, and are only accessible to that procedure. Local variables are created when the procedure is called, and destroyed upon exit from the procedure. Only global variables declared at the very beginning of the program will exist indefinitely.

Constants and Numeric Literals

Definitions of constants live only for the life of the program compile process, after which they become hard coded into the program wherever used. They are intended to enhance program readability and maintainability through parameterization of things like register numbers. The syntax for a constant definition is:

# <name> = <integer>;

The <integer> must be an integer numeric literal. The intended primary use of constants is "naming" of registers within the program. Floating point constants are not currently supported.

Numeric literals may be integer or floating point, and the data type does matter in many instances. If the numeric string contains a decimal point, it will be interpreted as floating point. If not, it will be assigned as integer. Examples of literals:

1is an integer
1.0is the exact same value, but applied as a floating point number

For convenience, the constants TRUE and FALSE are predefined as 1 and 0. Both of these are defined as all upper case, and return a type of boolean.

Assignment Statement

Simply begin an assignment statement with the variable you wish to assign. For example:

OurAverage = (YourVariable + MyVariable) / 2;
A = B + C;
phi = sin (theAngle);

An important note about type casting: Variables may be any of the types noted above (under data types). However, math functions only operate on integer or floating point. If you mix types such as uint8, int16, etc., in a mathematical expression, you will get an "Unexpected types in assignment" error message. Any time you wish to assign a variable to a different type of variable, or do math with anything other then integer or floating point, you must first convert the data to "int" or "float" using the int and float functions.

In the following example, the first four assignment statements will work, but the last one will cause an "Unexpected types" error. The target of the assignment may be any type, but the operands of the expression must be integer or floating point.

program test
declare
thisVar: uint8;
thatVar: int;
otherVar: float;

begin
thisVar = thatVar;
thisVar = otherVar;
thatVar = int(thisVar);
otherVar = float(thisVar);
thatVar = thisVar;
end

Math Functions & Operators

The following is a summary of math operators and functions recognized in PL/i:

+
Addition
-
Subtraction
*
Multiplication
/
Division
%
Modulus
abs(n)
Returns absolute value of 'n'
acos(n)
Calculates arc cosine of 'n'
asin(n)
Calculates arc sine of 'n'
atan(n)
Calculates arc tangent of 'n'
cos(n)
Calculates cosine of 'n'
ln(n)
Produces the natural logarithm of n
log(n)
Products the logarithm of n
pow(x,y)
Produces x raised to the power of y
rand()
Provides a random number (use srand(n) to seed random number generator)
sin(n)
Calculates sine of 'n'
sqrt(n)
Calculates square root of 'n'
tan(n)
Calculates tangent of 'n'

Logic Operators for data

The following is a summary of logic operators recognized in PL/i for use on binary data:

&
Logical AND, bitwise, e.g., c = a & b;
~
Logical NOT, bitwise (invert each bit), e.g., a = ~b;
|
Logical OR, bitwise , e.g., c = a | b;
^
Logical Exclusive OR, bitwise , e.g., c = a ^ b;
<<
Logical Shift Left, bitwise, e.g., a = a << 3; will shift the contents of "a" left 3 bits
>>
Logical Shift Right, bitwise, e.g., a = a >> 5; will shift the contents of "a" right 5 bits

Logic Operators for conditional expressions

The following is a summary of logic operators recognized in PL/i for use in conditional expressions:

and
Logical AND, e.g., if (a > b) and (c > d) then ...
not
Logical NOT, e.g., if not (a > b) then ... which is of course the same as if a <= b then ...
or
Logical OR, e.g., if (a > b) or (c > d) then ...

Be sure to use parenthesis to establish precedence as necessary.

IF THEN ELSE Statement

The IF THEN ELSE statement has the following general syntax:

if <condition> then <statement> else <statement> endif;

The ELSE part of the command is optional, thus the IF statement can have the format

if <condition> then <statement> endif;

The <condition> is any valid comparison or expression. The <statement> is any single statement terminated by a semi-colon. A compound statement (series of statements) may be encapsulated in a begin..end to cause them to statements to be treated as a single statement. The entire statement may be spread over several lines of source code, for example:

if myVar1 > myVar2 then
begin
myOtherVar = geti (100);
myLastVar = myVar1 * myOtherVar;
seti (100, myLastVar);
end;
else seti (100, 0);
endif;

Conditional operators are:

=
Equal
<>
Not equal
>
Greater than
>=
Greater than or equal
<
Less than
<=
Less than or equal

FOR (loop) Statement

The variable <var> is assigned the value of the starting expression, and after each execution of the statement, it is incremented by one until the value of <var> is greater then the value of the ending expression. If "down" is specified, then <var> is decremented by one until the value of <var> is less than the value of the ending expression.

for <var> = <expression> to <expression> <statement>;
for <var> = <expression> down to <expression> <statement>;

The <statement> may consist of multiple statements encapsulated within a begin..end; block. The <var> is any declared variable. The expressions are any valid expression producing a value that could otherwise be assigned to a variable.

WHILE DO Statement

The given statement will be executed repeatedly for as long as the expression produces a true or nonzero value. However, if the expression is false upon the first inspection, the statement will never be executed.

while <expression> do <statement>;

The "statement" may be multiple statements if they are encapsulated within a begin..end; block.

SELECT Statement

The select statement is analogous to the C case statement, or the Basic ON GOTO statement.

select
when <expression> <statement>;
when <expression> <statement>;
...
when <expression> <statement>;
otherwise <statement>;
end;

The statement is executed when its corresponding condition tests true or nonzero. Multiple expressions may test true in a given select statement. If none of the expressions tested true by the time "otherwise" is reached, the otherwise statement is executed. It is not illegal for additional "when" clauses and another "otherwise" to follow the otherwise. Each subsequent otherwise is a cumulative test of any conditions testing true so far. Each instance of "statement" may be multiple statements if encapsulated in a begin..end; block.

User Functions (Procedures)

The definition of a procedure follows the same general structure as that of the program overall. It begins with "procedure" followed by the procedure name, followed by parameter declarations. This is followed by the "declare" block identifying local types and variables, followed by the begin..end; block of code for the procedure.

procedure myProc (a: int; b: int; var c: int)
declare
begin
<statement>
end;

The parameters (variables) known locally to the procedure as "a" and "b" in the above example are passed by value, and cannot be returned to the calling program. Any parameter declared with "var" in front of it will be a call by reference, and that variable will be returned to the calling program. A procedure can have no parameters, or any number of call by value or call by reference parameters. The fact that multiple "var" or call by reference parameters are allowed means a procedure (or function) can have multiple return values. This is as close as PL/i gets to use of pointers.

To invoke the procedure, you would simply reference it by name in a statement. To call a procedure with no parameters, simply name the procedure. To call a procedure with parameters, name the procedure with its parameters in parenthesis.

program myDemo

declare
x: uint;
y: uint;
z: uint;

procedure myProc (a: int; b: int; var c: int)
declare
begin
c = a + b;
end;

procedure yourProc
declare
begin
z = 0;
end;

begin
x = geti (100);
y = geti (101);
if x > y then myProc (x, y, z);
else yourProc();
endif;
seti (100, z);
end

The result returned by "myProc" comes back in variable "z". The procedure "yourProc" does not have any parameters. Note that the parenthesis are omitted from the declaration but included as an empty set in the call to the procedure.

The above example is also worth studying for purposes of noting where semicolons should be used at the end of a line.

Register Access

You may read any of the local registers using the get functions (procedures), and write them using the set functions. There are two forms of each, one for integer register contents, and one for floating point register contents. Syntax is:

<var> = geti ( <int reg> );
<var> = getf ( <float reg> );

seti ( <int reg> , <var> );
setf ( <float reg> , <var> );

Consider the following examples:

myData = geti (22);
seti (24, myData);
yourData = getf (1015);
setf (1017, yourData);

The above example will read the integer contents of register #22 into the variable myData, then write the value of myData to register #24. The example will then read the floating point contents of register #1015 into the variable yourData, then write the value of yourData to register #1017. Variables may be used in place of the literal constants used to identify registers in the above example.

INDIRECT REGISTER ACCESS

The co-processor's direct access to registers is limited to the first 60 each of integer registers and floating point register pairs. However, indirect access to all registers known to the server is available. To perform indirect access, you write the desired register number to the request pointer register, and wait for the acknowledge register to match the request. These special pointer registers start at 4001.

Reading a server register from the co-processor using indirect access:

Register 4001: The register number you wish to read (1, 1001, etc) is written into 4001 by your program.
Register 4002: Cleared when you write 4001, will contain same register number as found in 4001 when the read cycle is complete.
Register 4003/4004: Register 4003 contains the integer data read, or the 4003/4004 pair contains the floating point data read.

Writing a server register from the co-processor using indirect access:

Register 4007/4008: Write integer data to 4007 or floating point data to the register pair starting at 4007.
Register 4005: The register number you wish to write (1, 1001, etc) is written into 4005 by your program.
Register 4006: Cleared when you write 4005, will contain same register number as found in 4005 when the write cycle is complete.

Be sure to write data to 4007 before writing the register number to 4005 since writing 4005 starts the write process. Note that the 400x registers are virtual registers useful primarily in the co-processor PL/i program; however, they are functional on the server to allow testing of co-processor programs on the server.

Time Functions

There are several soft timers available to PL/i. These may be triggered, tested for timeout, checked for present time value, and retriggered. The timers count tenths of seconds. Syntax for timer functions is:

<var> = timer ( <timer> );
<var> = timeout ( <timer> );
trigger ( <timer> , <value> );

<timer> must be an integer timer number. The value returned by timer is a tick count of tenths of seconds remaining before timeout. The value returned by timeout is a boolean which is TRUE if the timer has timed out since the last call to timeout with that timer number, or FALSE if not yet timed out or previously timed out. The timeout function may be used in an expression, for example:

if timeout(2) then seti (10, 0); endif;

The above example will "turn off" the output at register 10 when timer 2 times out. If we wanted to make this more readable, we would have first declared constants such as:

#PumpTimer = 2;
#PumpMotor = 10;

and now our example becomes:

if timeout(#PumpTimer) then
seti (#PumpMotor, 0);
endif;

The trigger function will start, or restart (retrigger), the timer specified, resetting its time value to <value> in tenths of seconds. If the timer had not previously timed out, the new value will take effect without the timeout function returning a TRUE in between. However, if triggered with a value of zero, this will force the timer to zero and cause the timeout function to return TRUE upon its next call.

The function ticks will return the value of a continuously incrementing time counter. Each "tick" represents on tenth of a second, and the counter rolls over to zero when full. Syntax for reading the tick counter is:

myVar = ticks();

The function delay will suspend program execution for some number of seconds, or generate a delay. Examples:

delay (50);
delay (2);

The first example will pause program execution for 5 seconds. The second example will pause for two tenths of a second.

Flash and EEPROM Access

The definition of Flash and EEPROM access vary depending on what the target processor is. When the target is the AddMe III I/O processor, you have access to both EEPROM for frequently changing non-volatile memory, and Flash for infrequently changing non-volatile data memory. When running the program in debug mode on the web server, but the final target is the AddMe III I/O coprocessor, both Flash and EEPROM are emulated. When the final target is the web server, EEPROM is not available, and Flash is implemented as a Flash file in the web server's file system (but you still use the same functions for addressing Flash records). Syntax for the calls is:

read_flash ( <sector> , <var> );
write_flash ( <sector> , <var> );
read_eeprom ( <offset> , <var> );
write_eeprom ( <offset> , <var> );

Read will transfer data from non-volatile memory to the variable named. Write will transfer data from the variable named to non-volatile memory. EEPROM offset is given in bytes starting from zero. Flash sectors are numbered from one. Each Flash sector is 64 bytes. Exactly the variable size will be written to EEPROM. The <var> size up to 64 bytes will be written to a single sector of Flash with each write call. Single byte writes to Flash should be avoided since each byte will consume 64 bytes of Flash. Records of up to 64 bytes should be used for efficient Flash memory usage. EEPROM can be written a single byte at a time with no penalty.

Memory capacity allocated to these functions: There are 64 sectors of 64 bytes (4096 bytes total) of Flash memory set aside for PL/i data storage. This data may be initialized by uploading a CSV file from the server's Initial Data web page. If no initial data is uploaded, Flash data is initially all hex FF's or -1. There are 512 bytes of EEPROM set aside for PL/i data storage. EEPROM is also initialized to all FF's or -1.

Miscellaneous Functions

You may deliberately cause the program to stop executing by using the die function. This would be used in the event of detecting a fatal error of some sort. The abort code given in the call will be reported to the system's runtime error reporting mechanism for diagnostics. The syntax for the call is:

die ( <code> );

There are two procedures available for debugging programs. These will only function when running a program via the web interface, and will be treated as "no operation" instructions elsewhere.

read ( <var> , <tag> );
write ( <expression> , <tag> );

The "read" procedure will take input from the input window attempting to parse a single numeric value. The value will be assigned to the variable named. The "write" procedure will print the value of the argument (variable, literal, or expression) to the output window in the web interface. In both cases, the "tag" is a numeric value you choose to help you simply identify where in the program the read/write came from.

You may require knowledge of variable sizes in order to calculate offsets into EEPROM or Flash when using the memory access functions. A "sizeof" function familiar to C programmars is available, and returns the variable size in bytes.

<var2> = sizeof ( <var1> );

Comment Lines

PL/i recognizes "C++ style" comments. Anything beyond a // (double slash) on a given line will be treated as a comment and disregarded by the compiler. Compilation will resume on the next source line. If // appears at the start of a line, the entire line is discarded.

Error Codes

Compiler errors: Syntax and other errors that occur when the program is compiled will be listed with a text description on the error page. You will not be able to run the program until all compiler errors have been fixed. While the program is running, additional error checking is done as described below.

Common compiler errors that may not be obvious:
Syntax error at line 1: "program name" line is missing (must be present, should have no semicolon)
Syntax error at the very end of the program: There should be no semicolon after the last end.
Syntax error elsewhere: Look for missing semicolons or typographical errors.
Syntax error at "end;" statement: Look for missing end of earlier construct, such as "if..then" missing "endif".
"Identifier (name) not declared": Either a variable declaration is missing, or a function name was miss-typed.

PL/i traps a number of errors while your program is running. Some of these result in program termination while others do not. Only the worst errors will cause the program to stop. Other errors will allow the program to continue in hopes that you can recover. Runtime error codes are accessible in a phantom local register #0. To get the most recent error, read register zero as shown here:

<var> = geti (0);

The error codes that may be returned are listed below. When an error occurs, the error code will be held indefinitely until the next error occurs or until geti(0) is called. Each time geti(0) is called, the error code is reset to zero. Therefore if you are making regular use of geti(0), you can reasonably assume that any non-zero error is recent. If you wish to trap an error, such as timeout of a LonWorks NV fetch, do a dummy call to geti(0) just before to reset the error code, and then call geti(0) again right after to see if an error occurred at that point.

The following runtime errors are non-fatal errors detected by the PL/i runtime kernel. (Additional error codes specific to local network communications may be found on the help page for local network communications.)

401 = Array subscript out of bounds
402 = Divide by zero (result is mathematically defined as infinity, but returned as zero here)
406 = File open error, "flash.mem" file used for PL/i flash access on server (only)

The following runtime errors are fatal errors detected by the PL/i runtime kernel. Program execution is halted at the point where the error occurred. You will not get these values from a call to geti(0) since the program has already stopped. You can only get these errors through external examination of the program status.

451 = Unrecognized program instruction (contact support@csimn.com)
452 = Stack overflow (most likely cause: Too many variables, ran out of memory)
453 = Stack underflow
454 = Program counter out of bounds
455 = Memory allocation problem with runtime
456 = auto-run program file problem