Writing New Devices - host

Name

Writing New Devices -- extending the synthetic target, host-side

Description

On the host-side adding a new device means writing a Tcl/Tk script that will handle instantiation and subsequent requests from the target-side. These scripts all run in the same full interpreter, extended with various commands provided by the main I/O auxiliary code, and running in an overall GUI framework. Some knowledge of programming with Tcl/Tk is required to implement host-side device support.

Some devices can be implemented entirely using a Tcl/Tk script. For example, if the final system will have some buttons then those can be emulated in the synthetic target using a few Tk widgets. A simple emulation could just have the right number of buttons in a row. A more advanced emulation could organize the buttons with the right layout, perhaps even matching the colour scheme, the shapes, and the relative sizes. With other devices it may be necessary for the Tcl script to interact with an external program, because the required functionality cannot easily be accessed from a Tcl script. For example interacting with a raw ethernet device involves some ioctl calls, which is easier to do in a C program. Therefore the ethernet.tcl script which implements the host-side ethernet support spawns a separate program rawether, written in C, that performs the low-level I/O. Raw ethernet access usually also requires root privileges, and running a small program rawether with such privileges is somewhat less of a security risk than the whole eCos application, the I/O auxiliary, and various dynamically loaded Tcl scripts.

Because all scripts run in a single interpreter, some care has to be taken to avoid accidental sharing of global variables. The best way to avoid problems is to have each script create its own Tcl namespace, so for example the ethernet.tcl script creates a namespace ethernet:: and all variables and procedures reside in this namespace. Similarly the I/O auxiliary itself makes use of a synth:: namespace.

Building and Installation

When an eCos device driver or application code instantiates a device, the I/O auxiliary will attempt to load a matching Tcl script. The third argument to synth_auxiliary_instantiate specifies the type of device, for example ethernet, and the I/O auxiliary will append a .tcl suffix and look for a script ethernet.tcl.

If the device being instantiated is application-specific rather than part of an eCos package, the I/O auxiliary will look first in the current directory, then in ~/.ecos/synth. If it is part of an eCos package then the auxiliary will expect to find the Tcl script and any support files below libexec/ecos in the install tree - note that the same install tree must be used for the I/O auxiliary itself and for any device driver support. The directory hierarchy below libexec/ecos matches the structure of the eCos repository, allowing multiple versions of a package to be installed to allow for incompatible protocol changes.

The preferred way to build host-side software is to use autoconf and automake. Usually this involves little more than copying the acinclude.m4, configure.in and Makefile.am files from an existing package, for example the synthetic target ethernet driver, and then making minor edits. In acinclude.m4 it may be necessary to adjust the path to the root of the repository. configure.in may require a similar change, and the AC_INIT macro invocation will have to be changed to match one of the files in the new package. A critical macro in this file is ECOS_PACKAGE_DIRS which will set up the correct install directory. Makefile.am may require some more changes, for example to specify the data files that should be installed (including the Tcl script). These files should then be processed using aclocal, autoconf and automake in that order. Actually building the software then just involves configure, make and make install, as per the instructions in the toplevel README.host file.

To assist developers, if the environment variable ECOSYNTH_DEVEL is set then a slightly different algorithm is used for locating device Tcl scripts. Instead of looking only in the install tree the I/O auxiliary will also look in the source tree, and if the script there is more recent than the installed version it will be used in preference. This allows developers to modify the master copy without having to run make install all the time.

If a script needs to know where it has been installed it can examine the Tcl variable synth::device_install_dir . This variable gets updated whenever a script is loaded, so if the value may be needed later it should be saved away in a device-specific variable.

Instantiation

The I/O auxiliary will source the device-specific Tcl script when the eCos application first attempts to instantiate a device of that type. The script should return a procedure that will be invoked to instantiate a device.

namespace eval ethernet {
    …
    proc instantiate { id instance data } {
        …
        return ethernet::handle_request
    }
}
return ethernet::instantiate

The id argument is a unique identifier for this device instance. It will also be supplied on subsequent calls to the request handler, and will match the return value of synth_auxiliary_instantiate on the target side. A common use for this value is as an array index to support multiple instances of this types of device. The instance and data arguments match the corresponding arguments to synth_auxiliary_instantiate on the target side, so a typical value for instance would be eth0, and data is used to pass arbitrary initialization parameters from target to host.

The actual work done by the instantiation procedure is obviously device-specific. It may involve allocating an interrupt vector, adding a device-specific subwindow to the display, opening a real Linux device, establishing a socket connection to some server, spawning a separate process to handle the actual I/O, or a combination of some or all of the above.

If the device is successfully instantiated then the return value should be a handler for subsequent I/O requests. Otherwise the return value should be an empty string, and on the target-side the synth_auxiliary_instantiate call will return -1. The script is responsible for providing diagnostics explaining why the device could not be instantiated.

Handling Requests

When the target-side calls synth_auxiliary_xchgmsg, the I/O auxiliary will end up calling the request handler for the appropriate device instance returned during instantiation:

namespace eval ethernet {
    …
    proc handle_request { id request arg1 arg2 txdata txlen max_rxlen } {
        …
        if { <some condition> } {
            synth::send_reply <error code> 0 ""
            return
        }
        …
        synth::send_reply <reply code> $packet_len $packet
    }
    …
}

The id argument is the same device id that was passed to the instantiate function, and is typically used as an array index to access per-device data. The request, arg1, arg2, and max_rxlen are the same values that were passed to synth_auxiliary_xchgmsg on the target-side, although since this is a Tcl script obviously the numbers have been converted to strings. The txdata buffer is raw data as transmitted by the target, or an empty string if the I/O operation does not involve any additional data. The Tcl procedures binary scan, string index and string range may be found especially useful when manipulating this buffer. txlen is provided for convenience, although string length $txdata would give the same information.

The code for actually processing the request is of course device specific. If the target does not expect a reply then the request handler should just return when finished. If a reply is expected then there should be a call to synth::send_reply. The first argument is the reply code, and will be turned into a 32-bit integer on the target side. The second argument specifies the length of the reply data, and the third argument is the reply data itself. For some devices the Tcl procedure binary format may prove useful. If the reply involves just a code and no additional data, the second and third arguments should be 0 and an empty string respectively.

Attempts to send a reply when none is expected, fail to send a reply when one is expected, or send a reply that is larger than the target-side expects, will all be detected by the I/O auxiliary and result in run-time error messages.

It is not possible for the host-side code to send unsolicited messages to the target. If host-side code needs attention from the target, for example because some I/O operation has completed, then an interrupt should be raised.

Interrupts

The I/O auxiliary provides a number of procedures for interrupt handling.

synth::interrupt_allocate <name>
synth::interrupt_get_max
synth::interrupt_get_devicename <vector>
synth::interrupt_raise <vector>

synth::interrupt_allocate is normally called during device instantiation, and returns the next free interrupt vector. This can be passed on to the target-side device driver in response to a suitable request, and it can then install an interrupt handler on that vector. Interrupt vector 0 is used within the target-side code for the real-time clock, so the allocated vectors will start at 1. The argument identifies the device, for example eth0. This is not actually used internally, but can be accessed by user-initialization scripts that provide some sort of interrupt monitoring facility (typically via the interrupt hook). It is possible for a single device to allocate multiple interrupt vectors, but the synthetic target supports a maximum of 32 such vectors.

synth::interrupt_get_max returns the highest interrupt vector that has been allocated, or 0 if there have been no calls to synth::interrupt_allocate. synth::interrupt_get_devicename returns the string that was passed to synth::interrupt_allocate when the vector was allocated.

synth::interrupt_raise can be called any time after initialization. The argument should be the vector returned by synth::interrupt_allocate for this device. It will activate the normal eCos interrupt handling mechanism so, subject to interrupts being enabled and this particular interrupt not being masked out, the appropriate ISR will run.

Note: At this time it is not possible for a device to allocate a specific interrupt vector. The order in which interrupt vectors are assigned to devices effectively depends on the order in which the eCos devices get initialized, and that may change if the eCos application is rebuilt. A future extension may allow devices to allocate specific vectors, thus making things more deterministic. However that will introduce new problems, in particular the code will have to start worrying about requests for vectors that have already been allocated.

Flags and Command Line Arguments

The generic I/O auxiliary code will process the standard command line arguments, and will set various flag variables accordingly. Some of these should be checked by device-specific scripts.

synth::flag_gui

This is set when the I/O auxiliary is operating in graphical mode rather than text mode. Some functionality such as filters and the GUI layout are only available in graphical mode.

    if { $synth::flag_gui } {
        …
    }
synth::flag_verbose

The user has requested additional information during startup. Each device driver can decide how much additional information, if any, should be produced.

synth::flag_keep_going

The user has specified -k or --keep-going, so even if an error occurs the I/O auxiliary and the various device driver scripts should continue running if at all possible. Diagnostics should still be generated.

Some scripts may want to support additional command line arguments. This facility should be used with care since there is no way to prevent two different scripts from trying to use the same argument. The following Tcl procedures are available:

synth::argv_defined <name>
synth::argv_get_value <name>

synth::argv_defined returns a boolean to indicate whether or not a particular argument is present. If the argument is the name part of a name/value pair, an = character should be appended. Typical uses might be:

    if { [synth::argv_defined "-o13"] } {
        …
    }

    if { [synth::argv_defined "-mark="] } {
        …
    }

The first call checks for a flag -o13 or --o13 - the code treats options with single and double hyphens interchangeably. The second call checks for an argument of the form -mark=<value> or a pair of arguments -mark <value>. The value part of a name/value pair can be obtained using synth::argv_get_value;

    variable speed 1
    if { [synth::argv_defined "-mark="] } {
        set mark [synth::argv_get_value "-mark="]
        if { ![string is integer $mark] || ($mark < 1) || ($mark > 9) } {
            <issue diagnostic>
        } else {
            set speed $mark
        }
    }

synth::argv_get_value should only be used after a successful call to synth::argv_defined. At present there is no support for some advanced forms of command line argument processing. For example it is not possible to repeat a certain option such as -v or --verbose, with each occurrence increasing the level of verbosity.

If a script is going to have its own set of command-line arguments then it should give appropriate details if the user specifies --help. This involves a hook function:

namespace eval my_device {
    proc help_hook { } {
        puts " -o13          : activate the omega 13 device"
        puts " -mark <speed> : set speed. Valid values are 1 to 9."
    }

    synth::hook_add "help" my_device::help_hook
}

The Target Definition File

Most device scripts will want to check entries in the target definition file for run-time configuration information. The Tcl procedures for this are as follows:

synth::tdf_has_device <name>
synth::tdf_get_devices
synth::tdf_has_option <devname> <option>
synth::tdf_get_option <devname> <option>
synth::tdf_get_options <devname> <option>
synth::tdf_get_all_options <devname>

synth::tdf_has_device can be used to check whether or not the target definition file had an entry synth_device <name>. Usually the name will match the type of device, so the console.tcl script will look for a target definition file entry console. synth::tdf_get_devices returns a list of all device entries in the target definition file.

Once it is known that the target definition file has an entry for a certain device, it is possible to check for options within the entry. synth::tdf_has_option just checks for the presence, returning a boolean:

    if { [synth::tdf_has_option "console" "appearance"] } {
        …
    }

synth::tdf_get_option returns a list of all the arguments for a given option. For example, if the target definition file contains an entry:

synth_device console {
    appearance -foreground white -background black
    filter trace {^TRACE:.*} -foreground HotPink1 -hide 1
    filter xyzzy {.*xyzzy.*} -foreground PapayaWhip
}

A call synth::tdf_get_option console appearance will return the list {-foreground white -background black}. This list can be manipulated using standard Tcl routines such as llength and lindex. Some options can occur multiple times in one entry, for example filter in the console entry. synth::tdf_get_options returns a list of lists, with one entry for each option occurrence. synth::tdf_get_all_options returns a list of lists of all options. This time each entry will include the option name as well.

The I/O auxiliary will not issue warnings about entries in the target definition file for devices which were not loaded, unless the -v or --verbose command line argument was used. This makes it easier to use a single target definition file for different applications. However the auxiliary will issue warnings about options within an entry that were ignored, because often these indicate a typing mistake of some sort. Hence a script should always call synth::tdf_has_option, synth:;tdf_get_option or synth::tdf_get_options for all valid options, even if some of the options preclude the use of others.

Hooks

Some scripts may want to take action when particular events occur, for example when the eCos application has exited and there is no need for further I/O. This is supported using hooks:

namespace eval my_device {
    …
    proc handle_ecos_exit { arg_list } {
        …
    }
    synth::hook_add "ecos_exit" my_device::handle_ecos_exit
}

It is possible for device scripts to add their own hooks and call all functions registered for those hooks. A typical use for this is by user initialization scripts that want to monitor some types of I/O. The available Tcl procedures for manipulating hooks are:

synth::hook_define <name>
synth::hook_defined <name>
synth::hook_add <name> <function>
synth::hook_call <name> <args>

synth::hook_define creates a new hook with the specified name. This hook must not already exist. synth::hook_defined can be used to check for the existence of a hook. synth::hook_add allows other scripts to register a callback function for this hook, and synth::hook_call allows the owner script to invoke all such callback functions. A hook must already be defined before a callback can be attached. Therefore typically device scripts will only use standard hooks and their own hooks, not hooks created by some other device, because the order of device initialization is not sufficiently defined. User scripts run from mainrc.tcl can use any hooks that have been defined.

synth::hook_call takes an arbitrary list of arguments, for example:

    synth::hook_call "ethernet_rx" "eth0" $packet

The callback function will always be invoked with a single argument, a list of the arguments that were passed to synth::hook_call:

    proc rx_callback { arg_list } {
        set device [lindex $arg_list 0]
        set packet [lindex $arg_list 1]
    }

Although it might seem more appropriate to use Tcl's eval procedure and have the callback functions invoked with the right number of arguments rather than a single list, that would cause serious problems if any of the data contained special characters such as [ or $. The current implementation of hooks avoids such problems, at the cost of minor inconvenience when writing callbacks.

A number of hooks are defined as standard. Some devices will add additional hooks, and the device-specific documentation should be consulted for those. User scripts can add their own hooks if desired.

exit

This hook is called just before the I/O auxiliary exits. Hence it provides much the same functionality as atexit in C programs. The argument list passed to the callback function will be empty.

ecos_exit

This hook is called when the eCos application has exited. It is used mainly to shut down I/O operations: if the application is no longer running then there is no point in raising interrupts or storing incoming packets. The callback argument list will be empty.

ecos_initialized

The synthetic target HAL will send a request to the I/O auxiliary once the static constructors have been run. All devices should now have been instantiated. A script could now check how many instances there are of a given type of device, for example ethernet devices, and create a little monitor window showing traffic on all the devices. The ecos_initialized callbacks will be run just before the user's mainrc.tcl script. The callback argument list will be empty.

help

This hook is also invoked once static constructors have been run, but only if the user specified -h or --help. Any scripts that add their own command line arguments should add a callback to this hook which outputs details of the additional arguments. The callback argument list will be empty.

interrupt

Whenever a device calls synth::interrupt_raise the interrupt hook will be called with a single argument, the interrupt vector. The main use for this is to allow user scripts to monitor interrupt traffic.

Output and Filters

Scripts can use conventional facilities for sending text output to the user, for example calling puts or directly manipulating the central text widget .main.centre.text. However in nearly all cases it is better to use output facilities provided by the I/O auxiliary itself:

synth::report <msg>
synth::report_warning <msg>
synth::report_error <msg>
synth::internal_error <msg>
synth::output <msg> <filter>

synth::report is intended for messages related to the operation of the I/O auxiliary itself, especially additional output resulting from -v or --verbose. If running in text mode the output will go to standard output. If running in graphical mode the output will go to the central text window. In both modes, use of -l or --logfile will modify the behaviour.

synth::report_warning, synth::report_error and synth::internal_error have the obvious meaning, including prepending strings such as Warning: and Error:. When the eCos application informs the I/O auxiliary that all static constructors have run, if at that point there have been any calls to synth::error then the I/O auxiliary will exit. This can be suppressed with command line arguments -k or --keep-going. synth::internal_error will output some information about the current state of the I/O auxiliary and then exit immediately. Of course it should never be necessary to call this function.

synth::output is the main routine for outputting text. The second argument identifies a filter. If running in text mode the filter is ignored, but if running in graphical mode the filter can be used to control the appearance of this output. A typical use would be:

    synth::output $line "console"

This outputs a single line of text using the console filter. If running in graphical mode the default appearance of this text can be modified with the appearance option in the synth_device console entry of the target definition file. The System filters menu option can be used to change the appearance at run-time.

Filters should be created before they are used. The procedures available for this are:

synth::filter_exists <name>
synth::filter_get_list
synth::filter_add <name> [options]
synth::filter_parse_options <options> <parsed_options> <message>
synth::filter_add_parsed <name> <parsed_options>

synth::filter_exists can be used to check whether or not a particular filter already exists: creating two filters with the same name is not allowed. synth::filter_get_list returns a list of the current known filters. synth::filter_add can be used to create a new filter. The first argument names the new filter, and the remaining arguments control the initial appearance. A typical use might be:

    synth::filter_add "my_device_tx" -foreground yellow -hide 1

It is assumed that the supplied arguments are valid, which typically means that they are hard-wired in the script. If instead the data comes out of a configuration file and hence may be invalid, the I/O auxiliary provides a parsing utility. Typical usage would be:

    array set parsed_options [list]
    set message ""
    if { ![synth::filter_parse_options $console_appearance parsed_options message] } {
        synth::report_error \
	        "Invalid entry in target definition file $synth::target_definition\
	         \n  synth_device \"console\", entry \"appearance\"\n$message"
    } else {
        synth::filter_add_parsed "console" parsed_options
    }

On success parsed_options will be updated with an internal representation of the desired appearance, which can then be used in a call to synth::filter_add_parsed. On failure message will be updated with details of the parsing error that occurred.

The Graphical Interface

When the I/O auxiliary is running in graphical mode, many scripts will want to update the user interface in some way. This may be as simple as adding another entry to the help menu for the device, or adding a new button to the toolbar. It may also involve adding new subwindows, or even creating entire new toplevel windows. These may be simple monitor windows, displaying additional information about what is going on in the system in a graphical format. Alternatively they may emulate actual I/O operations, for example button widgets could be used to emulate real physical buttons.

The I/O auxiliary does not provide many procedures related to the graphical interface. Instead it is expected that scripts will just update the widget hierarchy directly.

So adding a new item to the Help menu involves a .menubar.help add operation with suitable arguments. Adding a new button to the toolbar involves creating a child window in .toolbar and packing it appropriately. Scripts can create their own subwindows and then pack it into one of .main.nw, .main.n, .main.ne, .main.w, .main.e, .main.sw, .main.s or .main.se. Normally the user should be allowed to control this via the target definition file. The central window .main.centre should normally be left alone by other scripts since it gets used for text output.

The following graphics-related utilities may be found useful:

synth::load_image <image name> <filename>
synth::register_ballon_help <widget> <message>
synth::handle_help <URL>

synth::load_image can be used to add a new image to the current interpreter. If the specified file has a .xbm extension then the image will be a monochrome bitmap, otherwise it will be a colour image of some sort. A boolean will be returned to indicate success or failure, and suitable diagnostics will be generated if necessary.

synth::register_balloon_help provides balloon help for a specific widget, usually a button on the toolbar.

synth::handle_help is a utility routine that can be installed as the command for displaying online help, for example:

    .menubar.help add command -label "my device" -command \
        [list synth::handle_help "file://$path"]