High Speed Operations
Can a high speed control system be built with Windows 95 or NT? The answer is yes - provided you can live with certain limitations. Shown below is a screen shot of a Windows 95 application that is gathering an analog voltage buffer of 512 points at a rate of about 240 buffers per second and doing a complex analysis of the data:
The total buffers read, the number per second and the number missed are shown in counters on the upper right of the form. About 1% of the buffers are missed during operation. This is not a large problem in this system because we read far more data for a sheet of veneer than we need. Another limitation of this application you will notice is the Test-Run switch. It is shown in the Run position. In this mode the program disables all menus and only updates the screen between sheets of veneer. The downside to all this is the operator can't change system operation while it is running. This was not a problem because the system only provides assistance to a human grader and can be taken offline for short periods of time without affecting production. We will make some suggestions later on how to solve this problem.
This system operates three cards from Keithley-Metrabyte to obtain the data and operate a sprayer to mark veneer sheets with colored ink. The basic idea in this system is to send a sound pulse through veneer and detect the time it takes to run across a sheet. This time is related to the stress grade of the veneer. A CTM-05 timing card is used to generate timing for a sound pulse generator and also to trigger input of the sound transducer. A second timing channel on the timing card is used to input an encoder to track veneer sheet position and speed. A DAS-1802 analog to digital converter is used to input buffers of 512 data points with a timing of 3 microseconds between points. This is done in DMA mode to avoid using too much CPU time. The time it takes to input a complete buffer is 1.536 milliseconds. A PIO-24 card is used to read and write various signals and also drives the paint sprayer to mark the sheets for grade.
The basic operation of the I/O in this system can be summarized as follows:
- Initialize the CTM card to provide timing for the system.
- Initialize the DAS to input a data buffer.
- Wait for data to be input, track sheets and time the sprayer.
- Initialize the DAS to input the next buffer.
- Analize the data.
- Output the results to the sprayer when sheet is complete.
- Loop back to step 3.
Here is a screen shot of the actual data that this system deals with. The idea is to find the first positive peak of the sound wave:
There are basically two ways to perform these operations: (1) do them in a program loop assigned to the Delphi program's idle loop or (2) create a separate thread to process the data.
If you do the first you must limit the program functions to avoid processing an excessive amount of Windows messages during operation. As a result, the program will have limited interaction with the operator when running. I chose this method due to problems with trying to implement the second method.
The second choice allows interactivity but requires more work to implement. The first problem you run into is: many interface card drivers return a process complete event into the general Windows message queue. This was true of the drivers provided by Keithley-Metrabyte and is also true for many of their competitors. This mode of signaling that the I/O is complete is totally worthless for high speed operation. First, you may be processing a previous Windows message and will need to wait until this is done before you will see the event. Second, it is difficult to pass the event from the main program thread to the separate I/O thread without increasing delays. The third problem has to do with how the Windows message queue is implemented. If you read about the details of the message queue you find that certain messages have priority and it isn't a true first-in first-out queue. These are the main reasons I was forced to use the first method.
There are other ways a driver can signal that it is finished with an operation. Some card drivers allow you to signal a semaphore or generate an event which competely bypasses the Windows message queue. Perhaps the driver authors will catch up with Win32 some day, but many of the existing drivers are based on the message queue mode because they were used with Windows 3.1 which didn't have semaphores.
It is also quite possible to poll for completion of the input buffer in the I/O thread. This presents another set of problems. If the priority of the polling thread is too low, other system activity will preempt it. If the priority of the polling loop is too high, all other system activity will halt. The only solution to this problem is to perform a Sleep operation somewhere in the I/O thread before you expect the I/O operation to complete. Here is where a wait on semaphore would come in handy if the drivers offered it. The problem with doing a Sleep operation is that it has timing increments of 10 milliseconds on the Intel platform. In this application we need to Sleep in increments more on the order of 2 milliseconds to accurately poll the I/O process. There are also problems associated with timer latiency as discussed in the previous Tip #2. You may delay for 2 milliseconds in a high priority thread, but due to system activity it may delay much longer.
Considering all the problems with trying to use a separate I/O thread, I decided to take the easy way out and use the idle loop for this project. This is the procedure I created for the Application.OnIdle event:
Procedure TDAS_Form.IdleProcess(Sender: TObject; var Done: Boolean); var ST:word; LE:SmallInt; c:LongInt; t:extended; begin Done:=false; if (LastError=0) and (DAS_ST=DAS_Cont) then begin LE:=K_DMAStatus(hFrame,ST,c); {check input status} if LE<>0 then {input error} begin LastError:=LE; {report error} DAS_ReportError; LastError:=K_DMAStart(hFrame); {try recovery, start another input} end else if ((ST and 1)=0) then {input complete} begin inc(NRun); {operate run counter} if NRun<0 then begin NRun:=1; PSecClearDelay(RunTime); end; if Assigned(PreDataProcess) then {copy data to processing buffer} PreDataProcess; LastError:=K_DMAStart(hFrame); {start a new input} if Assigned(PostDataProcess) then {do processing} PostDataProcess; if Visible then {do display if visible} if (NRun mod 100)=0 then begin SD5.Caption:=Format('NRun %d',[NRun]); t:=PSecCheckDelay(RunTime); if t>0.0 then SD6.Caption:=Format('%3.2f Hz' ,[NRun/t]); end; end; end; if LastError<>0 then {shut down if repeated error} begin DAS_ReportError; DAS_SetState(DAS_Stop); end; end;
One of the important items in this idle process is to set the Done variable to false. The Borland documentation doesn't go into details on what this does. You need to inspect the source for the ProcessMessages procedure to find out the details. Every Delphi program basically runs a loop to process Windows messages. As long as messages keep coming it will process them and not call your OnIdle process. When it finds the message queue empty, it will make a call to your OnIdle process. If the Done variable is set to true when you return, Delphi will call WaitMessage which waits for a new message to be placed into the Windows message queue. Because you want to keep polling in the OnIdle process, this must not occur or you will get a delay between Windows messages.
Using the OnIdle procedure to poll I/O and process the data is subject to the following problems:
First, if there is much Windows message activity the OnIdle procedure will not get called. This is usually not a big problem with Delphi unless you have someone moving the mouse around while the program is running. Windows 95 and NT use separate message queues for each program and message activity in other programs will not affect your program provided it has a higher priority.
The second problem has to do with priority. In order to avoid preemption of your OnIdle procedure it will need to run at a higher priority than the other programs in the system. We suggest that you use the following priority calls when your program is started. You must be very careful to avoid program loops because you can possibly hang Windows with these levels if your program misbehaves:
SetPriorityClass(GetCurrentProcess,HIGH_PRIORITY_CLASS); SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_HIGHEST);
The third problem to deal with is the screen saver. Many of the screen savers will hog system resources and take time away from your application. One solution to this is to increase the priority of your program even higher to the real-time class. Try this and when the screen saver kicks in it will usually hang your system! The only safe way to operate with a screen saver is to pick the blank screen saver. This screen saver only clears the screen to black and performs no operations that can disrupt your program. Using the real-time priority class is still dangerous unless you disable the screen saver.
The one drawback to this method is the inability of the operator to interact with the system during operation. In some ways this is good. As we noted before, Windows 95 and NT are not true real-time systems and will experience some delays in scheduling when background processes are going on. By keeping operator interaction down to a minimum, we avoid these problems.
A Sleep Solution
It sure would be nice if we could speed up the clock tick rate in Windows to allow finer timing increments. This could possibly allow a separate polling I/O thread to be used and allow more operator interactivity with the system. Well, there is a way to do this. You won't find this solution in any of the basic Win32 documentation provided by Microsoft. It is a dirty little secret that they try to hide from programmers. The place to look is in the Win32 SDK Multimedia reference. Go to the section on Miscellaneous Multimedia Services and look up the sub-section on Timer Resolution. There you will find documentation on timeBeginPeriod and timeEndPeriod. These handy little functions allow you to speed up the system clock period and get better resolution on Sleep and other timing calls. They are available for Windows 95 and NT but only work on the Intel platforms.
There were several reasons I avoided using this solution on my system. First, the system didn't need to be interactive when it ran. Second, by avoiding interactivity I get better performance because I don't need to worry about latency in the Win32 scheduler. And finally, this was a customer funded project and we had used up a considerable amount of time and money just getting the idle mode system working.