Programming with NanoJ
1.NanoJ program
A NanoJ program makes a protected runtime environment available within the firmware. Here, the user can create his own processes. These can then trigger functions in the controller by, for example, reading or writing entries in the object dictionary. Through the use of protective mechanisms, a NanoJ program is prevented from crashing the firmware. In the worst case, the execution is interrupted with an error code stored in the object dictionary. If the NanoJ program was loaded on the controller, it is automatically executed after the controller is switched on or restarted.
1.1Available computing time
A NanoJ program receives computing time cyclically in a 1 ms clock (see following figure). Because computing time is lost through interrupts and system functions of the firmware, only approx. 30% – 50% of computing time is available to the user program (depending on operating mode and application). In this time, the user program must run through the cycle and either complete the cycle or yield the computing time by calling the yield() function. In the former case, the user program is restarted with the start of the next 1 ms cycle; the latter results in the program being continued on the next 1 ms cycle with the command that follows the yield() function.
If the NanoJ program needs more time than was allotted, it is ended and an error code set in the object dictionary.
Tip
When developing user programs, the runtime behavior must be carefully examined, especially for more time-intensive tasks. For example, it is therefore recommended that tables be used instead of calculating a sine value using a sin function.
Note
If the NanoJ program does not yield the computing time after too long a time, it is ended by the operating system. In this case, the number 4 is entered in the statusword for object 2301h; in the error register for object 2302h, the number 5 (timeout) is noted, see 2301h NanoJ Status and 2302h NanoJ Error Code.
1.2 Sandbox
Using processor-specific features, a so-called sandbox is generated. When used in the sandbox, a user program can only access specially assigned memory areas and system resources. For example, an attempt to directly write to a processor IO register is acknowledged with an MPU Fault and the user program terminated with the corresponding error code in the object dictionary.
1.3 NanoJ program – communication possibilities
A NanoJ program has a number of possibilities for communicating with the controller:
• Read and write OD values using PDO mapping
• Directly read and write OD values using system calls
• Call other system calls (e.g., write debug output)
The OD values of the user program are made available in the form of variables via PDO mapping. Before a user program receives the 1 ms time slot, the firmware transfers the values from the object dictionary to the variables of the user program. As soon as the user program receives computing time, it can manipulate these variables as regular C variables. At the end of the time slot, the new values are then automatically copied by the firmware back to the respective OD entries. To optimize the performance, three types of mapping are defined: input, output, and input/output (In, Out, InOut).
• Input mappings can only be read; they are not transferred back to the object dictionary.
• Output mappings can only be written.
• Input/output mappings, on the other hand, can both be read and written.
The set mappings can be read and checked via the GUI for objects 2310h, 2320h, and 2330h. Up to 16 entries are allowed for each mapping. Whether a variable is stored in the input, output or data range is controlled in NanoJEasy via the specification of the linker section
1.4 Executing a NanoJ program
When executing a cycle, the NanoJ program essentially consists of the following three steps with respect to the PDO mapping:
1. Read values from the object dictionary and copy them to the input and output areas
2. Execute a user program
3. Copy values from the output and input areas back to the object dictionary
The configuration of the copy processes is based on the CANopen standard. In addition, values of the object dictionary can be accessed via system calls. This is generally slower; mappings are therefore to be preferred. The number of mappings is limited (16 entries each in In/Out/ InOut).
Tip
Nanotec recommends: Map OD entries that are used and changed frequently and use system calls to access OD entries that are used less frequently.
A list of available system calls can be found in chapter System calls in a NanoJ program.
Tip
Nanotec recommends accessing a given OD value either by mapping or using a system call with od_write(). If both are used simultaneously, the system call has no effect.
1.5 NanoJ program – OD entries
The NanoJ program is controlled and configured in object range 2300h to 2330h (see 2300h NanoJ Control).
Example: To start the TEST1.USR user program, the following sequence can, for example, be used: • Check entry 2302h for error code. • If no error: Start the NanoJ program by writing object 2300h, bit 0 = "1".
Note
It can take up to 200 ms for the NanoJ program to start.
Check entry 2302h for error code and object 2301h, bit 0 = "1".
Note
Due to limitations in the USB implementation, file "VMMCODE.USR" is, following a restart of the controller, set to a size of 16 kB and the creation date set to 13.03.2012.
To stop a running program: write entry 2300h with bit 0 value = "0".
1.6 Structure of a NanoJ program
A user program consists of at least two instructions:
• the preprocessor instruction #include "wrapper.h"
• the void user(){} function
The code to be executed can be stored in the void user() function
Note
The file names of the user programs must not be longer than eight characters plus three characters in the suffix; file name main.cpp is permissible, file name aLongFileName.cpp is not permissible
Note
In NanoJ programs, global variables may only be initialized within functions. It then follows:
• No new operator
• No constructors
• No initialization of global variables outside of functions
Examples:
The global variable is to be initialized within the void user() function:
unsigned int i; void user(){
i = 1;
i += 1; }
The following assignment is not correct:
unsigned int i = 1; void user() {
i += 1; }
1.7 NanoJ program example
The example shows the programming of a square wave signal in object 2500h:01h.
// file main.cpp
map S32 outputReg1 as inout 0x2500:1
#include "wrapper.h"
// user program
void user()
{
U16 counter = 0;
while( 1 )
{
++counter;
if( counter < 100 )
InOut.outputReg1 = 0;
else if( counter < 200 )
InOut.outputReg1 = 1;
else counter = 0; // yield() 5 times (delay 5ms)
for(U08 i = 0; i < 5; ++i )
yield();
}
}// eof
2.Mapping in the NanoJ program
With this method, a variable in the NanoJ program is linked directly with an entry in the object dictionary. The creation of the mapping must be located at the start of the file here, even before the #include "wrapper.h" instruction. A comment is permitted above the mapping.
Tip
Nanotec recommends:
• Use mapping if you need to access an object in the object dictionary frequently, e.g., controlword 6040h or statusword 6041h.
• The od_write() and od_read() functions are better suited for accessing objects a single time, see Accessing the object dictionary.
2.1 Declaration of the mapping
The declaration of the mapping is structured as follows:
map <TYPE> <NAME> as <input|output|inout> <INDEX>:<SUBINDEX>
Where:
• <TYPE>
The data type of the variable; U32, U16, U08, S32, S16 or S08.
• <NAME>
The name of the variable as it is used in the user program.
• <input|output|inout>
The read and write permission of a variable: a variable can be declared as an input, output or inout. This defines whether a variable is readable (input), writable (output) or both (inout) and the structure by means of which it must be addressed in the program.
• <INDEX>:<SUBINDEX>
Index and subindex of the object to be mapped in the object dictionary.
Each declared variable is addressed in the user program via one of the three structures: In, Out or InOut depending on the defined write and read direction.
2.2 Example of mapping
Example of a mapping and the corresponding variable accesses:
map U16 controlWord as output 0x6040:00
map U08 statusWord as input 0x6041:00
map U08 modeOfOperation as inout 0x6060:00
#include "wrapper.h"
void user()
{
[...]
Out.controlWord = 1;
U08 tmpVar = In.statusword; InOut.modeOfOperation = tmpVar;
[...]
}
2.3 Possible error at od_write()
A possible source of errors is a write access with the od_write() function (see System calls in a NanoJ program) of an object in the object dictionary that was simultaneously created as mapping. The
code listed in the following is incorrect:
The line with the od_write(0x6040, 0x00, 5 ); command has no effect. As described in the
introduction, all mappings are copied to the object dictionary at the end of each millisecond.
This results in the following sequence:
1. The od_write function writes the value 5 in object 6040h:00h.
2. At the end of the 1 ms cycle, the mapping is written that also specifies object 6040h:00h, however,
with the value 1.
3. From the perspective of the user, the od_write command thus serves no purpose.
3 System calls in a NanoJ program
With system calls, it is possible to call up functions integrated in the firmware directly from a user
program. Because direct code execution is only possible in the protected area of the sandbox, this
is implemented via so-called Cortex-Supervisor-Calls (Svc Calls). An interrupt is triggered when
the function is called. The firmware thus has the possibility of temporarily allowing code execution
outside of the sandbox. Developers of user programs do not need to worry about this mechanism – for
them, the system calls can be called up like normal C functions. Only the wrapper.h file needs to be
integrated as usual.
3.1 Accessing the object dictionary
void od_write (U32 index, U32 subindex, U32 value)
This function writes the transferred value to the specified location in the object dictionary.
index Index of the object to be written in the object dictionary
subindex Subindex of the object to be written in the object dictionary
value Value to be written
Note
It is highly recommended that the processor time be passed on with yield() after calling a od_write(). The value is immediately written to the OD. For the firmware to be able to trigger actions that are dependent on this, however, it must receive computing time. This, in turn, means that the user program must either be ended or interrupted with yield().
U32 od_read (U32 index, U32 subindex)
This function reads the value at the specified location in the object dictionary and returns it.
Active waiting for a value in the object dictionary should always be associated with a yield().
Example
while (od_read(2400,2) != 0) // wait until 2400:2 is set
{ yield(); }
3.2 Process control
void yield()
This function returns the processor time to the operating system. In the next time slot, the program continues at the location after the call.
void sleep (U32 ms)
This function returns the processor time to the operating system for the specified number of
milliseconds. The user program is then continued at the location after the call.