Checking the handling of events on the parallel port by stimulus presentation softwares

Christophe Pallier1

This short paper describes an approach to checking how well stimulus presentation softwares handle events on the parallel port.

The PC parallel port remains a simple solution to interface a PC with external equipements such as, a manual response box, a fMRI scanner or an EEG acquisition system. For example, the documentation of the ``Expe'' stimulus presentation software describes a very simple response box that allows measurements of reaction times with millisecond precision. This precision can be achieved under DOS, by setting an interruption that pools the parallel port every millisecond.

With stimulus presentation software running under multitasking systems like Windows or Linux, the actual precision of the timing is somewhat uncertain. Even with programs using real-time priority, the process responsible for recording the parallel port events may be sleeping for a certain amount of time. Under such condition, can we be sure that very short triggering signals are not lost?

I describe here my solution to check softwares that use the parallel port. To use it, you will need:

In the testing mode, one PC runs the stimulation software while the other simulates response presses or triggering signals on the parallel port.

Our software consists of DOS programs which fit into a bootable floppy. To use it, the image file EXPE.BIN must be dumped onto the floppies as follows:

Do this for each of the two floppies, then boot the computers with them (not forgetting to set the BIOS boot option to 'floppy first' if necessary).

To start, when you see the DOS prompts, type 'lpt' on each computer. Now, press the keys 0, 1, 2 , 3, or 4 on one computer and note how this toggles the corresponding bits of the data lines of the parallel port on the local computer. If the second, remote computer also runs lpt, you should see that some bits from the status register are also toggled when you press 0, 1, 2, 3, 4 on the local PC. The wiring of a laplink cable produces the following correspondance:

0 <-> ERR
1 <-> SLC
2 <-> PPR
3 <-> ACK
4 <-> BSY

If information does not seem to flow between the computers, check that the parallel ports are well linked with the cable, and that the parallel port settings are set to bidirectional in the Bios. You can find documentation on the PC parallel ports on the Internet, particularly at Parallel port central by Jan Axelson.

Finally, quit the lpt program pressing the ESC key.

Now, to simulate response presses, you can run the small Expe scripts send.pro and receive.pro:

On the local PC, type:

expe62 send -d

On the remote PC, type:

expe62 receive -d

Press a key when asked, and watch. The local PC sends 10msec ``ticks'' on the paralel port every second; the remote computer logs every change occuring on its parallel port. If everything works fine, the remote computer reports changes that occur every seconds.

The information displayed on the screen is also saved in the files '__TEST__.RES' for further analyses.

Here is the source of send.pro:

%%% send.pro
writeln "This script sends a series of ticks on the parallel port"
defvar TICKDURATION SOA N start VAL
TICKDURATION := 10  % in milliseconds
SOA := 1000 % time between two ticks
N := 100 % total number of ticks to send
VAL := 1 % value written on the data port

writeln "Press any key to start"
readkey
start := clock
writeln "(Press 'Ctrl-Pause' to interrupt)"
save "start:" start
for i 1 N
 waittill start + i * SOA
 putlpt1  255
 save clock
 writeln  i " time:=" fmt "%8d" clock-start " " 
 wait TICKDURATION
 putlpt1  0
endfor
writeln "End of test"

And the source of receive.pro:

%%% receive.pro
writeln "detect changes on the parallel status port (Ctrl-Pause to quit)"
defvar a first
while true
begif a<>getlpt1stat
 save clock
 a:=getlpt1stat
 if first==0 first:=clock
 writeln "->" a "  at " fmt "%8d" clock-first
endif 
endwhile

The functions to read and write on the parallel port data and stasuts register are: putlpt1, getlpt1, putlpt1stat, getlpt1stat.

These scripts can be modified. For example, send.pro can be edited to adjust parameters such the SOA, tick duration,... If one wanted to add a random jitter in the button presses, it would suffice to add a line as `wait random 500' just before `putlpt1 255' in send.pro.

To do more complex things, one needs to know a little bit about the syntax of the expe language in which these scripts are written (see the documentation at www.lscp.net/expe). Several examples of Expe scripts (identificable by their extension `.pro'), are provided on the floppy, to help and get acquainted with the language. To launch, e.g. 'pictures.pro', enter 'expe62 pictures -d'.

Now to test a stimulus presentation software such as Presentation or Eprime, one can run it on the local computer, and program the remote computer to send or read ticks on the parallel port. A simple test of Presentation follows.

1  Example: testing the "Presentation" software

Presentation is a psychology experiment generator developed by Neurobehavioral Systems, running under the various versions of Windows.

Let us create one of the simplest experiment in Presentation, consisting in a series of one second long trials, in which the text "Press me!" is displayed for 500 msec, followed by a empty screen for another 500 msec. Here is the scenario file:

scenario_type = trials;
active_buttons = 1;
button_codes=1;

begin;

picture {} default;
picture {
   text { caption="Press!"; };
   x=0;  y=0;
} pic1;
          
trial {         
   trial_duration = 1000;
   picture pic1;
   duration = 500;
};   

# ... copy the code for the same trial several times

trial {         
   trial_duration = 1000;
   picture pic1;
   duration = 500;
};   

To set up Presentation to record input from the parallel port, click on the ``Input Device" tab, and:

When the experiment is launched, Presentation records the parallel port events, which may originate from a remote PC running ëxpe62 send -d" . At the end of the experiment, the ``scenario report'' table displays the times at which button presses were detected. If there is no latency in the system, the difference between successive detection times should be equal to the SOA value in 'send.pro', that is 1000 milliseconds by default.

On a Dell CPx with a Pentium 650, I did not notice any latency, that is the reaction times were recorded correctly.

This limited test does not allow to detect potential systematic errors in the detection of events. For this to be possible, the two computers should first synchronize, through a handshaking procedure. Also, even if everything works well with this simple experiment, the reader must be aware that with more complex experiments, the likelyhood of processing latencies increases, and therefore, ideally, each experiment should be checked with an external response `simulator' based on send.pro.

2  Example 2: testing lptdeamon under Linux

`lptdeamon' is a small program I wrote for Linux, meant to monitor the parallel port every millisecond and report any change (lptdeamon-0.1-tar.gz)

Though the program uses the``real-time'' SCHED_FIFO policy and the RTC timer. I noticed latencies up to 6 milliseconds on a Dell CPx 650, with a linux 2.2.13 kernel. Maybe more recent kernels, or kernels with the low latency patches, would fare better.

/* Report parallel port events, pooling the status lines every 1/1024th sec 
** Author: Christophe Pallier <www.pallier.org> 
** Date: 01/12/1999 
** Last Modified: 06/12/2002
** Notes:
**   - Must be compiled with  'gcc -O2' 
**   - Using real time priority, this program must be ran as root 
*/

#include <stdio.h>
#include <linux/mc146818rtc.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <asm/io.h>
#include <sys/io.h>
#include <sched.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <signal.h>

#define LPT1         0x378

void 
waitsynchro(void)
/* Optional code to wait for an event before starting
** launching the parallel port scan.
*/
{
  /* empty */
}


int signal_received = 0;
void 
poked(int sig)
{
        signal_received = 1;
}


int
check_root_privileges(void)
{
  return (geteuid()==0);
}

int
main (void)
{
  int fd, retval, irqcount, a, b;
  int dummy[10000] ;
  unsigned long data;
  struct sched_param sp;
  struct timeval tv, tv2, tv3;
  struct timezone tz;
  float diff;


  if (!check_root_privileges()) {
    fprintf(stderr,"This program should be run with root privileges\n");
    fprintf(stderr,"Root should run 'chown root lptdeamon; chmod u+s lptdeamon'\n");
    exit(1);
  }

        signal(SIGTERM, poked);
        signal(SIGINT, poked);  
#ifdef SIGHUP
        signal(SIGHUP, poked);
#endif
#ifdef SIGQUIT
        signal(SIGQUIT, poked);
#endif


/* switch to real time mode */
  dummy[0] = 1 ;
  dummy[9999] = 1 ;
  if (mlockall(MCL_CURRENT | MCL_FUTURE) < 0) {
                perror ("Warning: mlockall") ;
        };
  sp.sched_priority = 60;
  if (sched_setscheduler (0, SCHED_FIFO, &sp) < 0) {
    perror ("Warning: sched_setscheduler");
  }

/* open & read parallel port */
  if (ioperm (LPT1, 3, 1) < 0) {
    perror("Cannot access the parallel port\n");
  }
  a = inb (LPT1 + 1);

/* setting up the RTC to interrupt every 1024Hz */
  fd = open ("/dev/rtc", O_RDONLY);
  retval = ioctl (fd, RTC_IRQP_SET, 1024);
  retval = ioctl (fd, RTC_PIE_ON, 0);
  irqcount = 0;

  waitsynchro();
  gettimeofday (&tv, &tz);

  while (!signal_received)
    {
      gettimeofday (&tv2, &tz);

      /* check if a bit on the status port has changed */
      b = inb (LPT1 + 1);
      if (b != a)
        {
          printf ("%d at  %.2f msec\n", 
                   b,
                   (tv2.tv_sec - tv.tv_sec) * 1000.0 + \
                        (tv2.tv_usec - tv.tv_usec) / 1000.0
                  );
          fflush (stdout);
        }

      /* block until the next timer interrupt */
      retval = read (fd, &data, sizeof (unsigned long));
      a = b;
      irqcount++;

      gettimeofday (&tv3, &tz);
      diff=(tv3.tv_sec - tv2.tv_sec) * 1000.0 + (tv3.tv_usec - tv2.tv_usec) / 1000.0;
      if (diff>1.5) {
        printf("delay of %f msec\n",diff);
      }       
    }

/* Suppress RTC periodic interrupt */
  retval = ioctl (fd, RTC_PIE_OFF, 0);
  close (fd);

/* go out of real time */
  munlockall();
  sp.sched_priority = 0;
  if (sched_setscheduler (0, SCHED_OTHER, &sp) < 0) {
    perror ("Warning: sched_setscheduler");
  }

  return 0;
}

Acknowledgements:

Thanks to Xavier Mayoral from University of Barcelona, for the lpt program, and to the FreeDOS project, for making it possible to provide a DOS floppy without enfringing Microsoft licence.


Footnotes:

1http://www.pallier.org. If you find this document and its associated software useful, do not hesitate to drop me a line.


File translated from TEX by TTH, version 3.01.
On 6 Dec 2002, 16:25.