User Tools

Site Tools


Queueing event interrupts

by Richard Russell, June 2006; amended June 2009 and December 2011

Consider using the EVENTLIB library instead of the code listed in this article.

The ON CLOSE, ON MOUSE, ON MOVE, ON SYS and ON TIME statements interrupt your program when one of the specified events occurs. Perhaps the most obvious way to use these statements is to cause a procedure (an interrupt service routine) to be called when the interrupt happens, as follows:

        ON SYS PROCsys(@wparam%,@lparam%) : RETURN

It is important that any event parameters in which you are interested (@msg%, @wparam% and/or @lparam%) are read in the statement immediately following the ON statement otherwise they might have changed as a result of a subsequent interrupt. For example the following code will not work reliably:

        ON SYS wp%=@wparam%:lp%=@lparam%:PROCsys(wp%,lp%):RETURN : REM Don't do this!

Although wp% will contain the @wparam% parameter relevant to the ON SYS call, lp% may not contain the relevant value of @lparam%, because another interrupt may have occurred in between.

So with care it is possible to ensure that every event is processed by your program, with no chance of any being missed, but not necessarily in the order in which they occurred! To see why, consider the following code:

        ON SYS PROCsys(@wparam%) : RETURN
        DEF PROCsys(W%)
        PRINT W%

Suppose two ON SYS interrupts happen in quick succession, with @wparam% values of 1 and 2 in that order. The first will result in PROCsys being called, but before the PRINT statement gets a chance to be executed the second interrupt will occur. So the actual sequence of events will be:

  1. PROCsys(1)
  2. PROCsys(2)
  3. PRINT 2
  5. PRINT 1

resulting in the following output:


So the events were processed in the opposite order to that in which they occurred!

Often this won't matter, and indeed in many circumstances it will be irrelevant because there is no likelihood of two interrupts happening sufficiently close together. For example this will be the situation if you are using ON SYS only to respond to menu selections, since you should only get one interrupt for each selection. In such a case you can safely use an alternative approach where the interrupt simply sets a global variable that you can poll elsewhere:

        ON SYS Click% = @wparam% : RETURN
        Click% = -1
          temp% = INKEY(1)
          IF temp%=-1 SWAP temp%,Click%
          CASE temp% OF
            WHEN ....
            WHEN ....
            REM. etc.

This routine conveniently uses INKEY as both a delay (to avoid using too much CPU time) and to monitor for keypresses, which is handy for implementing keyboard shortcuts for menu items.

However life isn't always this easy! Occasionally it may be necessary to ensure that every event is registered, even if two or more occur in very quick succession, and to ensure that the events are processed in the order in which they happen. An example might be handling events from a dialogue box. One way of dealing with this is to implement a First In First Out queue of events which can be written as the events occur (however fast) and read as they are processed, even if relatively slowly.

At first thought this doesn't sound too difficult - creating a FIFO queue in software is straightforward - but there is a major difficulty: we must transfer the event into the queue in just one statement. If we don't it won't work: either data could be lost or the events could be processed in the wrong order! Achieving this sounds like a tall order, but it can be done.

Method 1: Using an array

For a queue with six entries the code for writing into the queue would be as follows:

        DIM Q%(6)
        ON SYS Q%()=Q%(0)+1,@wparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5) : RETURN

This may need a little explanation. To do it all in a single statement we use the ability to load an entire array from a comma-separated list of values. The clever bit is that some of the values we load into the array depend on elements of that same array, as follows:

  • Q%(0) = Q%(0)+1
  • Q%(1) = @wparam%
  • Q%(2) = Q%(1)
  • Q%(3) = Q%(2)
  • Q%(4) = Q%(3)
  • Q%(5) = Q%(4)
  • Q%(6) = Q%(5)

Hopefully you can see what is happening here. Elements Q(1) to Q(6) act as a shift register: each time an event occurs the previously-stored data is shifted one place along the queue (the data in element Q%(6) is discarded) and the new data is stored in Q%(1). Element Q%(0) is loaded with the value Q%(0)+1, in other words it is incremented. This zeroth element of the array acts a pointer to the oldest event stored in the queue.

So by using this cunning method we manage to store each event into the queue, and increment a pointer, in just one statement! How, then, do we read the data out? This is the code:

        WHILE Q%(0)
          event% = Q%(0)<=DIM(Q%(),1) AND Q%(Q%(0) AND Q%(0)<=DIM(Q%(),1))
          Q%(0) -= 1
          REM Do something with event%

Firstly we examine Q%(0), which is the pointer. If this is zero the queue is empty and we need take no further action. If it is non-zero there is at least one event in the queue. The next line reads the oldest event in the queue, by using Q%(0) as the subscript; the comparisons ensure that a 'Bad subscript' error doesn't occur if the queue overflows.

Note that as Q%(0) is accessed and the queue's contents retrieved in the same statement, there is no possibility of reading the wrong event. Even if another interrupt has occurred since Q%(0) was tested in the previous line it makes no difference: the oldest event will always be read.

Finally the next line simply decrements the pointer, so it points to the next event to be read (if any), and leaves the other elements in the array unchanged. Again, it doesn't matter if another interrupt occurs between the previous statement and this one.

The array can, of course, be any length (within reason). If events occur more quickly than they can be processed the queue will eventually fill and the oldest event(s) will be discarded; in that case event% will be set to zero.

If you need to know the value of @msg% and @lparam% as well as @wparam% you can extend the technique as follows. To write into the queue:

        DIM Q%(18)
        ON SYS Q%()=Q%(0)+3,@msg%,@wparam%,@lparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5),...,Q%(15) : RETURN

To read from the queue:

        WHILE Q%(0)
          lpar% = Q%(0)<=DIM(Q%(),1) AND Q%(Q%(0)-0 AND Q%(0)<=DIM(Q%(),1))
          wpar% = Q%(0)<=DIM(Q%(),1) AND Q%(Q%(0)-1 AND Q%(0)<=DIM(Q%(),1))
          msg%  = Q%(0)<=DIM(Q%(),1) AND Q%(Q%(0)-2 AND Q%(0)<=DIM(Q%(),1))
          Q%(0) -= 3
          REM Do something with msg%, wpar% and lpar%

The maximum length of queue that can be fitted into one line is 11 events (33 values):

        DIM Q%(33)
        ON SYS Q%()=Q%(0)+3,@msg%,@wparam%,@lparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5),...,Q%(30) : RETURN

To create a longer queue split the line using line-continuation characters (BBC BASIC for Windows version 5.91a or later only):

        DIM Q%(99)
        ON SYS Q%() = Q%(0)+3,@msg%,@wparam%,@lparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5),Q%(6),Q%(7),Q%(8),Q%(9),Q%(10),Q%(11),Q%(12),Q%(13),Q%(14),Q%(15),Q%(16),Q%(17),Q%(18),Q%(19),Q%(20),Q%(21),Q%(22),Q%(23),Q%(24),Q%(25),Q%(26),Q%(27),Q%(28),Q%(29),\
        \ Q%(30),Q%(31),Q%(32),Q%(33),Q%(34),Q%(35),Q%(36),Q%(37),Q%(38),Q%(39),Q%(40),Q%(41),Q%(42),Q%(43),Q%(44),Q%(45),Q%(46),Q%(47),Q%(48),Q%(49),Q%(50),Q%(51),Q%(52),Q%(53),Q%(54),Q%(55),Q%(56),Q%(57),Q%(58),Q%(59),Q%(60),Q%(61),Q%(62),Q%(63),\
        \ Q%(64),Q%(65),Q%(66),Q%(67),Q%(68),Q%(69),Q%(70),Q%(71),Q%(72),Q%(73),Q%(74),Q%(75),Q%(76),Q%(77),Q%(78),Q%(79),Q%(80),Q%(81),Q%(82),Q%(83),Q%(84),Q%(85),Q%(86),Q%(87),Q%(88),Q%(89),Q%(90),Q%(91),Q%(92),Q%(93),Q%(94),Q%(95),Q%(96) : RETURN

Using this technique you can make the queue as long as you like, within reason. However, requiring a very long queue is suggestive that you may be able to find a better solution by restructuring your program. For example you may be able to increase the frequency with which you poll the queue, or use an interrupt approach rather than polling.

Method 2: Using a string

This method is easier to understand than the foregoing one, and the maximum practical queue length is greater (over 5000 events), but it is more expensive of CPU time and memory.

The code for writing into the queue is as follows:

        Queue$ = ""
        !^wParam$ = ^@wparam% : ?(^wParam$+4) = 4
        ON SYS Queue$ += wParam$ : RETURN

Here wParam$ is a global string variable containing four characters, corresponding to the 4-byte (32-bit) value of @wparam%.

This is the code for reading the data out:

        WHILE Queue$<>""
          event% = !!^Queue$
          Queue$ = MID$(Queue$,5)
          REM Do something with event%

If events occur more quickly than they can be processed the queue will eventually fill and a String too long error will result.

If you need to know the value of @msg% and @lparam% as well as @wparam% you can extend the technique as follows. To write into the queue:

        Queue$ = ""
        !^iMsg$ = ^@msg% : ?(^iMsg$+4) = 4
        !^wParam$ = ^@wparam% : ?(^wParam$+4) = 4
        !^lParam$ = ^@lparam% : ?(^lParam$+4) = 4
        ON SYS Queue$ += iMsg$ + wParam$ + lParam$ : RETURN

Here iMsg$, wParam$ and lParam$ are global string variables containing the values of @msg%, @wparam% and @lparam% respectively.

To read from the queue:

        WHILE Queue$<>""
          event$ = LEFT$(Queue$,12)
          Queue$ = MID$(Queue$,13)
          event% = !^event$
          msg% = event%!0
          wpar% = event%!4
          lpar% = event%!8
          REM Do something with msg%, wpar% and lpar%

Note that the use of the temporary string event$ is important because the memory address of the string Queue$ alters as it changes length, and so could change between reading the msg%, wpar% and lpar% values.

This website uses cookies for visitor traffic analysis. By using the website, you agree with storing the cookies on your computer.More information
queueing_20event_20interrupts.txt · Last modified: 2018/04/15 12:04 by richardrussell