![]() |
| PL/i Programming Language
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.
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" are accessible as Modbus registers from a remote Master.
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:
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.
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:
Record declaration syntax is as follows:
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 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:
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:
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:
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. Simply begin an assignment statement with the variable you wish to assign. For example:
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.
The following is a summary of math operators and functions recognized in PL/i:
The following is a summary of logic operators recognized in PL/i for use on binary data:
Logic Operators for conditional expressions The following is a summary of logic operators recognized in PL/i for use in conditional expressions:
Be sure to use parenthesis to establish precedence as necessary. The IF THEN ELSE statement has the following general syntax:
The ELSE part of the command is optional, thus the IF statement can have the format
The <condition> is any valid comparison or expression. The <statement> is any single statement terminated by a semi-colon. A compount 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:
Contitional operators are:
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.
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. 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.
The "statement" may be multiple statements if they are encapsulated within a begin..end; block. The select statement is analogous to the C case statement, or the Basic ON GOTO statement.
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. 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.
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.
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. 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:
Consider the following examples:
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. 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. 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. 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:
<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:
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:
and now our example becomes:
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" represenets on tenth of a second, and the counter rolls over to zero when full. Syntax for reading the tick counter is:
The function delay will suspend program execution for some number of seconds, or generate a delay. Examples:
The first example will pause program execution for 5 seconds. The second example will pause for two tenths of a second. 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 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. 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:
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.
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 programmers is available, and returns the variable size in bytes.
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. 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: 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:
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 makeing 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. 401 = Array subscript out of bounds 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) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||