language-Modula2-0.1: examples/Modula-2_Libraries/PMOS/sources/general/analogue.mod
IMPLEMENTATION MODULE AnalogueIO;
(********************************************************)
(* *)
(* Analogue Input and Output. *)
(* *)
(* Programmer: P. Moylan *)
(* Last edited: 24 August 1993 *)
(* Status: *)
(* Polling method is working. *)
(* DMA method fails, reason not yet known. *)
(* *)
(********************************************************)
FROM Windows IMPORT (* for testing *)
(* type *) Window, Colour, FrameType, DividerType,
(* proc *) OpenWindow, WriteString, WriteLn;
FROM NumericIO IMPORT (* for testing *)
(* proc *) WriteHexByte, WriteCard, WriteLongCard;
FROM SYSTEM IMPORT
(* type *) ADDRESS,
(* proc *) ADR;
FROM LowLevel IMPORT
(* proc *) OutByte, InByte,
LowByte, HighByte, MakeWord,
LS, IANDB, AddOffset;
FROM DMA IMPORT
(* proc *) LoadDMAparameters;
FROM TaskControl IMPORT
(* proc *) WaitForInterrupt, CreateInterruptTask;
FROM Semaphores IMPORT
(* type *) Semaphore,
(* proc *) CreateSemaphore, DestroySemaphore, Wait, Signal;
(************************************************************************)
CONST testing = FALSE;
VAR debug: Window; (* for testing *)
CONST
(* IOBase is the starting port number for the I/O board. The *)
(* value is jumper selectable. The factory setting is 300H. *)
IOBase = 300H;
(* The interrupt number is jumper selectable, with IRQ2 as the *)
(* factory setting. The interrupt controller normally maps IRQ2 to *)
(* processor interrupt number 10. The PC/AT is different: IRQ2 is *)
(* reserved as the slave request from interrupt controller 2, so *)
(* the request line which would have been IRQ2 becomes IRQ9, which *)
(* is the second request line of interrupt controller 2 and which *)
(* the interrupt controller maps to processor interrupt number 113 *)
(* (=71H). (Confused yet? It get worse.) But then the BIOS maps *)
(* interrupt 113 back to processor interrupt number 10. Where does *)
(* this leave us? If we don't mind the slight overhead caused by *)
(* this software mapping, we can just use interrupt number 10 and *)
(* keep compatibility across computer models. That's what this *)
(* version of this module does. If, on the other hand, the *)
(* sampling rate is so high that that overhead is intolerable, then *)
(* we would have to deal with the slave interrupt controller. *)
InterruptNumber = 10;
(* The I/O board has 14 addressable ports, of which the last two *)
(* belong to the on-board counter/timer chip. *)
CounterDataPort = IOBase+12;
CounterControlPort = CounterDataPort+1;
(* Each of the five counters has a Counter Mode Register. The bits *)
(* in this register govern the following options: *)
(* bits 15-13 gating control (000=no gating) *)
(* bit 12 1 = count on falling clock edge *)
(* bits 11-8 counter source selection *)
(* bits 7-5 counter mode *)
(* bit 4 0 = binary count, 1 = BCD count *)
(* bit 3 0 = count down, 1 = count up *)
(* bits 2-0 output control *)
(* The following mnemonics define options which we want to use. *)
(* For brevity, the options we never use are omitted. *)
ActiveHighGate = 8000H;
F1source = 0B00H;
ModeQ = 0A0H;
ModeJ = 60H;
ModeD = 20H;
ToggledOutput = 2;
PulseOutput = 1;
(* Some useful commands to the counter/timer chip. *)
Select4 = 8;
Select5 = 16;
LoadDataPointerRegister = 0;
LoadAllCounters = 5FH;
LoadAndArm = 060H;
Disarm = 0C0H;
EnableDataPointerSequencing = 0E0H;
ClearToggle = 0E0H;
ResetCounters = 0FFH;
(* Definitions for the DMA controller. *)
DMAchannel = 1;
DMAReadCode = 1;
(************************************************************************)
TYPE
SamplingMethod = (DMA, Polling);
VAR
(* The following record keeps track of information needed during *)
(* periodic sampling, to save the caller the bother of repeating *)
(* the information at every sample time. *)
SamplingInfo: RECORD
method: SamplingMethod;
ChannelSelectByte: BYTE;
BufferAddress: ADDRESS;
ByteCount: CARDINAL;
END (*RECORD*);
(* The semaphore called tick is used only when analogue input is *)
(* being collected in the periodic sampling mode. *)
tick: Semaphore;
(************************************************************************)
(* DIGITAL I/O *)
(************************************************************************)
PROCEDURE DigitalOut (value: BYTE);
(* Sends the given value to the digital output port of the board. *)
BEGIN
OutByte (IOBase+11, value);
END DigitalOut;
(************************************************************************)
PROCEDURE DigitalInput (): BYTE;
(* Reads the digital input port of the board. *)
BEGIN
RETURN InByte (IOBase+10);
END DigitalInput;
(************************************************************************)
(* ANALOGUE OUTPUT (RTI-815 ONLY) *)
(************************************************************************)
PROCEDURE AnalogueOut (channel: OutputChannelNumber; value: WORD);
(* Analogue output. The channel number should be 0 or 1. Only the *)
(* least significant 12 bits of the value are used. The value can *)
(* be treated as either a signed or an unsigned 12-bit number, *)
(* depending on hardware jumper selections. *)
BEGIN
OutByte (IOBase+5+2*channel, LowByte(value));
OutByte (IOBase+5+2*channel+1, HighByte(value));
END AnalogueOut;
(************************************************************************)
(* ANALOGUE INPUT - SINGLE SAMPLE *)
(************************************************************************)
PROCEDURE AnalogueInput (channel: InputChannelNumber; gain: GainCode): WORD;
(* Analogue input. The value returned can be a signed or unsigned *)
(* number, depending on jumper selections on the board. *)
(* This procedure picks up a single sample when called. It does *)
(* not use interrupts or DMA. It is recommended for use only in *)
(* those cases (e.g. isolated sample, or aperiodic sampling) where *)
(* the caller takes responsibility for timing. More commonly, the *)
(* periodic sampling procedures, given later, will be more *)
(* appropriate. This procedure should NOT be called when periodic *)
(* sampling has been activated; the results would be unpredictable. *)
BEGIN
(* Put the A/D converter into non-interrupt, non-DMA mode, and *)
(* clear any flags which might be outstanding. *)
OutByte (IOBase, 0); (* status/control port *)
OutByte (IOBase+9, 0); (* "flags clear" port *)
(* Start the conversion. (Function LS is a left shift). *)
OutByte (IOBase+1, BYTE(channel + ORD(LS(gain, 5))));
OutByte (IOBase+2, 0); (* "start conversion" port *)
(* Wait for conversion complete. The conversion time is in the *)
(* range 8-35 microseconds, depending on things like hardware *)
(* options selected, so there's not enough time to make a task *)
(* switch worthwhile. This is why this procedure does not use *)
(* an interrupt to sense completion. *)
WHILE ORD(IANDB (InByte(IOBase),60H)) = 0 DO
(* busy wait, until either the DONE or *)
(* OVERRUN ERROR flag is set. *)
END (*WHILE *);
(* Return the value. Note that we don't bother telling the *)
(* caller about an overrun error, since that is normally caused *)
(* by trying to drive the A/D converter too fast, and overheads *)
(* like procedure entry time make that impossible with this *)
(* procedure. *)
RETURN MakeWord (InByte(IOBase+4),InByte(IOBase+3));
END AnalogueInput;
(************************************************************************)
(* ANALOGUE INPUT - PERIODIC SAMPLING *)
(************************************************************************)
PROCEDURE DumpCounter (w: Window; counter: SHORTCARD);
(* For testing: dumps the mode, load, and hold registers of the *)
(* given counter. *)
BEGIN
OutByte (CounterControlPort, EnableDataPointerSequencing);
OutByte (CounterControlPort, LoadDataPointerRegister + counter);
WriteHexByte (w, InByte (CounterDataPort));
WriteString (w, " ");
WriteHexByte (w, InByte (CounterDataPort));
WriteString (w, " ");
WriteHexByte (w, InByte (CounterDataPort));
WriteString (w, " ");
WriteHexByte (w, InByte (CounterDataPort));
WriteString (w, " ");
WriteHexByte (w, InByte (CounterDataPort));
WriteString (w, " ");
WriteHexByte (w, InByte (CounterDataPort));
WriteLn (w);
END DumpCounter;
(************************************************************************)
PROCEDURE LoadCounter (counter: SHORTCARD;
mode, loadvalue, holdvalue: CARDINAL);
(* Sets the mode, and the values for the Load and Hold Registers, *)
(* for the specified counter. This does not arm the counter - it *)
(* just sets initial register values. (Note that the Hold Register *)
(* contents are irrelevant for many modes; but we specify the value *)
(* anyway, in order to have a uniform calling sequence in all cases.*)
BEGIN
(* Internal registers of the counter/timer chip are addressed *)
(* by sending a command to load the internal data pointer *)
(* register, which then sequences through the internal register *)
(* file as we send successive bytes to the data port. *)
OutByte (CounterControlPort, EnableDataPointerSequencing);
OutByte (CounterControlPort, LoadDataPointerRegister + counter);
OutByte (CounterDataPort, LowByte(mode));
OutByte (CounterDataPort, HighByte(mode));
OutByte (CounterDataPort, LowByte(loadvalue));
OutByte (CounterDataPort, HighByte(loadvalue));
OutByte (CounterDataPort, LowByte(holdvalue));
OutByte (CounterDataPort, HighByte(holdvalue));
END LoadCounter;
(************************************************************************)
PROCEDURE PollingMethod (SamplingInterval: LONGCARD);
(* Sets up periodic sampling by using counter number 4 to trigger *)
(* an A/D conversion at the end of each sampling interval, and *)
(* cause an interrupt on the "conversion complete" condition. If *)
(* only a single channel is being sampled, then the interrupt task *)
(* can finish the job by reading the converted value. For multi- *)
(* channel sampling, the interrupt task can use a polling method to *)
(* read the remaining channels. This approach is suitable for the *)
(* case where the sampling interval is long, or where there is only *)
(* one channel being sampled. In the multi-channel case, we have *)
(* some processor overhead caused by busy waits in the interrupt *)
(* task, but this is not too serious because we only use this *)
(* method when we are sampling infrequently. *)
VAR clock: CARDINAL; timer4count: LONGCARD;
BEGIN
SamplingInfo.method := Polling;
(* The count for timer 4 is the sampling interval divided by *)
(* the clock frequency. We have a choice of five clock *)
(* frequencies, namely F1=1MHz, F2=F1/16, F3=F2/16, etc. The *)
(* choice of which of these to use is dictated by the fact that *)
(* the count must fit into 16 bits. The clock is specified in *)
(* bits 11-8 of the counter mode register as 1011 for F1, 1100 *)
(* for F2, 1101 for F3, etc. *)
clock := 11; timer4count := SamplingInterval;
IF testing THEN
WriteString (debug, "clock, count = ");
WriteCard (debug, clock); WriteLongCard (debug, timer4count);
WriteLn (debug);
END (*IF*);
WHILE timer4count > 65535 DO
INC (clock); timer4count := timer4count DIV 16;
IF testing THEN
WriteString (debug, "clock, count = ");
WriteCard (debug, clock); WriteLongCard (debug, timer4count);
WriteLn (debug);
END (*IF*);
END (*WHILE*);
(* Use mode D for timer 4. This is a straightforward "rate *)
(* generator" mode, with no gating. *)
LoadCounter (4, ORD(LS(clock, 8)) + ModeD + PulseOutput,
CARDINAL(timer4count), 0);
(* Initialise the A/D converter by clearing any flags which *)
(* might be outstanding, specifying the gain and starting *)
(* channel number, and enabling interrupts on end of conversion *)
(* or overrun. *)
OutByte (IOBase+1, SamplingInfo.ChannelSelectByte);
OutByte (IOBase+9, 0); (* "flags clear" port *)
OutByte (IOBase, 3); (* enable interrupts *)
(* Load and arm counter 4. This will start the periodic *)
(* sampling operation. *)
OutByte (CounterControlPort, LoadAndArm + Select4);
END PollingMethod;
(************************************************************************)
PROCEDURE DMAMethod (NumberOfChannels, SamplingInterval: CARDINAL;
AmplifierGain: GainCode);
(* Sets up periodic sampling by using counter 4 to trigger the A/D *)
(* converter as fast as possible, once for each channel to be *)
(* sampled, using DMA to put the results into main memory. Counter *)
(* number 5 is used as a gating source for counter number 4; the *)
(* width of the gating pulse controls the number of channels to be *)
(* sampled, and the time between gating pulses controls the *)
(* sampling interval. This method can be used only when the *)
(* sampling interval is less than 65536 microseconds, because *)
(* counter 5 is 16 bits wide and will be driven from a 1 MHz clock. *)
(* (We could, with some ingenuity, relax this limit by using a *)
(* lower clock frequency, but then we would lose fine control of *)
(* the width of the gating pulse, possibly to the point where we *)
(* could not correctly control the number of channels to sample. *)
(* One way to solve this problem would be to sample more channels *)
(* than are actually required, but this would give extra software *)
(* complexity, and it is not certain that this is justified). *)
(* Remark: there are hardwired connections from the output of *)
(* counter 5 to the gate of counter 4, from the output of counter 4 *)
(* to the A/D converter, and from the output of counter 4 to timer *)
(* source number 5. The first two connections are useful to us, *)
(* the last is not; but this is not a limitation since we can *)
(* program the counters to ignore the "source" inputs and instead *)
(* take their inputs from a 1MHz crystal oscillator called F1. *)
VAR timer4count, GateWidth: CARDINAL;
BEGIN
SamplingInfo.method := DMA;
(* First, calculate a count for timer 4 which will produce a *)
(* train of pulses at a suitable rate, taking into account the *)
(* time needed for an A/D conversion. *)
(* The A/D conversion time is 25 microseconds, but for high *)
(* gains in the input amplifier this must be increased to allow *)
(* for a settling time in the amplifier. *)
(* For future thought: the hardware seems to include an option *)
(* which would allow us to trim this time down, but it's not *)
(* quite clear just how fast we can go. *)
IF AmplifierGain = 3 THEN
timer4count := 80;
ELSIF AmplifierGain = 2 THEN
timer4count := 40;
ELSE
timer4count := 25;
END (*IF*);
GateWidth := NumberOfChannels*timer4count;
(* Use mode Q for timer 4. In this mode, the counter does not *)
(* count while the gate is low. The first clock input after *)
(* the gate goes high loads the counter from its load register, *)
(* and counting starts on the second clock input. When the *)
(* count hits zero, the A/D converter is triggered, and the *)
(* counter is reloaded from its load register so that counting *)
(* starts again. This happens repeatedly until the gate goes *)
(* low again. *)
LoadCounter (4, ActiveHighGate + F1source + ModeQ + PulseOutput,
timer4count, 0);
(* Use mode J for timer 5. This makes the counter a free- *)
(* running oscillator where the count is reloaded each time it *)
(* gets to zero, alternately reloading from the Load and Hold *)
(* registers. (The Load Register is the one which is used when *)
(* we initially load and arm the counter.) *)
LoadCounter (5, F1source + ModeJ + ToggledOutput,
SamplingInterval - GateWidth, GateWidth);
(* Set the initial output of counter 5 low. This operation is *)
(* necessary because counter 5 has a toggled output (unlike *)
(* counter 4, which only puts out a pulse each time its count *)
(* goes to zero), so we must explicitly specify its initial *)
(* condition. *)
OutByte (CounterControlPort, ClearToggle + 5);
(* Arm the A/D converter and the DMA controller. The A/D *)
(* conversions will not start, however, until triggered by the *)
(* first pulse from timer 4. *)
WITH SamplingInfo DO
LoadDMAparameters (DMAchannel, DMAReadCode,
BufferAddress, ByteCount);
OutByte (IOBase+1, ChannelSelectByte);
END (*WITH*);
(* Enable DMA mode, with interrupt on DMA completion. *)
OutByte (IOBase, 0CH);
(* Load and arm both counters. This will start the cycle. *)
OutByte (CounterControlPort, LoadAndArm + Select4 + Select5);
END DMAMethod;
(************************************************************************)
PROCEDURE StartPeriodicSampling (first, last: InputChannelNumber;
SamplingInterval: LONGCARD;
AmplifierGain: GainCode;
VAR (*OUT*) Buffer: ARRAY OF BYTE);
(* Initiates a mode of operation in which channels first..last, *)
(* inclusive, will be sampled every SamplingInterval microseconds, *)
(* with the results stored in array Buffer. At each sampling time, *)
(* the specified channels are read as nearly simultaneously as the *)
(* hardware will allow. Procedure WaitForNextSample, below, should *)
(* be called to check when the data are available in array Buffer. *)
(* If WaitForNextSample is not called often enough, there can be a *)
(* data overrun in which data are overwritten. We do not signal *)
(* this as an error since the only thing which can be done about it *)
(* is to use the new data and ignore whatever data have been lost. *)
BEGIN
WITH SamplingInfo DO
ChannelSelectByte := BYTE (first + ORD(LS(AmplifierGain, 5)));
IF last <> first THEN
(* Specify that the channel number will auto-increment *)
INC (ChannelSelectByte, 80H);
END (*IF*);
BufferAddress := ADR (Buffer);
ByteCount := 2*(last-first+1);
END (*WITH*);
CreateSemaphore (tick, 0);
(* Temporary patch: since the DMA method seems to have a bug in *)
(* it (reason so far unknown), use the polling method always. *)
PollingMethod (SamplingInterval);
(*
IF (first=last) OR (SamplingInterval>65535) THEN
PollingMethod (SamplingInterval)
ELSE
DMAMethod (last-first+1,SHORT(SamplingInterval),AmplifierGain);
END (*IF*);
*)
IF testing THEN
DumpCounter (debug, 4);
DumpCounter (debug, 5);
END (*IF*);
END StartPeriodicSampling;
(************************************************************************)
PROCEDURE WaitForNextSample;
(* Pauses until the buffer specified in the preceding procedure has *)
(* been filled with valid data. Notice that this is essentially a *)
(* synchronization procedure; the caller does not have to do any *)
(* timing operations since a return from this procedure implies *)
(* that the next sample time has arrived. *)
(* WARNINGS: *)
(* 1. This procedure should not be called unless periodic sampling *)
(* is currently in effect. Otherwise, it might never return. *)
(* 2. Periodic sampling implies that the data buffer is re-filled *)
(* regularly, regardless of whether the user code has finished *)
(* with the previous data. There are no interlocks, and no *)
(* protection against data being updated just as one is reading *)
(* it. (Any such protection could interfere with the precision *)
(* of the timing of sampling external data). The caller is *)
(* advised to move data out of the buffer promptly, especially *)
(* when the sampling rate is high. *)
BEGIN
(* Wait for the interrupt which indicates operation complete. *)
(* This is all we need to do; the interrupt task looks after *)
(* all the details of re-arming the hardware, etc. *)
Wait (tick);
END WaitForNextSample;
(************************************************************************)
PROCEDURE StopPeriodicSampling;
(* Turns off the periodic sampling mode of A/D conversion. *)
BEGIN
(* Disarm the A/D converter. *)
OutByte (IOBase, 0);
OutByte (IOBase+9, 0);
(* Stop the clocks (counters 4 and 5). *)
OutByte (CounterControlPort, Disarm + Select4 + Select5);
DestroySemaphore (tick);
END StopPeriodicSampling;
(************************************************************************)
(* THE INTERRUPT TASK *)
(************************************************************************)
PROCEDURE InterruptTask;
(* This task has a job to do only during periodic sampling, since *)
(* that is the only time we use interrupts. Its job depends on *)
(* which method of periodic sampling is in use (see above - we use *)
(* one of two different methods, depending on the sampling interval *)
(* and the number of channels being sampled). The cases are: *)
(* 1. DMA method: the interrupt occurs on DMA completion, so we *)
(* only have to set up the DMA chip to be ready for the next *)
(* batch of data, re-arm the A/D converter, and inform the user *)
(* that the current data have arrived. *)
(* 2. Polling method: the interrupt occurs on A/D completion for *)
(* the first channel to be sampled, and we still have to store *)
(* the datum in memory. If multiple channels are to be sampled *)
(* then the interrupt task must finish the job by sampling the *)
(* remaining channels. (This puts a busy wait inside an *)
(* interrupt task, which is normally not a good idea; it is *)
(* justified in this case because the amount of time to wait is *)
(* less than the time it would take to leave and re-enter the *)
(* interrupt task). Finally, the A/D converter must be *)
(* re-armed to be ready for the next batch of samples. *)
VAR resultptr: POINTER TO BYTE;
BytesRemaining: CARDINAL;
BEGIN
LOOP (*FOREVER*)
WaitForInterrupt;
OutByte (IOBase+9, 0); (* "flags clear" port *)
WITH SamplingInfo DO
IF method = DMA THEN
(* Re-arm the DMA controller for the next sample. *)
LoadDMAparameters (DMAchannel, DMAReadCode,
BufferAddress, ByteCount);
ELSE (* We are using the polling method *)
(* Temporarily disable A/D interrupts. *)
OutByte (IOBase, 0); (* status/control port *)
resultptr := BufferAddress;
BytesRemaining := ByteCount;
LOOP
(* Read in a result. *)
resultptr^ := InByte(IOBase+3); (* low order byte *)
resultptr := AddOffset (resultptr, 1);
resultptr^ := InByte(IOBase+4); (* high order byte *)
resultptr := AddOffset (resultptr, 1);
DEC (BytesRemaining, 2);
IF BytesRemaining = 0 THEN
EXIT (*LOOP*)
END (*IF*);
(* Perform the next conversion. *)
OutByte (IOBase+2, 0); (* "start conversion" port *)
WHILE ORD(IANDB (InByte(IOBase),60H)) = 0 DO
(* busy wait, until either the DONE or *)
(* OVERRUN ERROR flag is set. *)
END (*WHILE *);
END (*LOOP*);
(* Re-enable interrupts. *)
OutByte (IOBase, 3);
END (*IF*);
(* Rearm the D/A converter. *)
OutByte (IOBase+1, ChannelSelectByte);
END (*WITH*);
Signal (tick);
END (*LOOP*);
END InterruptTask;
(************************************************************************)
(* INITIALISATION *)
(************************************************************************)
PROCEDURE InitialSetup;
(* Resets the A/D board and the counter/timer chip and installs *)
(* the interrupt task. *)
BEGIN
OutByte (CounterControlPort, ResetCounters);
OutByte (CounterControlPort, LoadAllCounters);
OutByte (CounterControlPort, LoadDataPointerRegister + 4);
OutByte (IOBase, 0); (* disable interrupts *)
OutByte (IOBase+9, 0); (* clear flags *)
CreateInterruptTask(InterruptNumber,InterruptTask,"A/D int handler");
END InitialSetup;
(************************************************************************)
BEGIN
IF testing THEN
OpenWindow (debug,yellow,blue,5,11,0,79,doubleframe,nodivider);
END (*IF*);
InitialSetup;
END AnalogueIO.