Real-Time Data Acquisition Using DMA

Build your own real-time data acquisition system with the hardware and software tools Tom presents here.


January 01, 1990
URL:http://www.drdobbs.com/tools/real-time-data-acquisition-using-dma/184408282

Figure 2

Figure 2

Figure 3

Figure 4

JAN90: REAL-TIME DATA ACQUISITION USING DMA

REAL-TIME DATA ACQUISITION USING DMA

Here are all the tools -- hardware and software -- you need for your own data acquisition system

This article contains the following executables: NOLAN.ZIP

Tom Nolan

Tom is an associate scientist for Applied Research Corporation, specializing in real-time data acquisition and control and is currently working under contract with the Laboratory for High Energy Astrophysics at NASA's Goddard Space Flight Center. He can be reached at NASA/Goddard Space Flight Center, Code 664, Greenbelt, MD 20771.


In this article I describe hardware and software that I designed for an IBM PC-compatible computer to acquire real-time data from an external source in a way that still permits the PC to perform analysis and display of the data as it is acquired. A 286-based PC-compatible is an attractive workstation for this kind of development because it is relatively inexpensive, portable, easy to program, and add-on interface hardware can easily be designed for it.

The first problem in a data acquisition system is how to actually acquire the data. Direct memory access (DMA) provides a simple solution from a hardware design standpoint. With DMA, the data from some external device is stuffed directly into the PC memory a byte or a word at a time, without processor intervention. DMA can also be made to work in the other direction: From the PC memory to an external device or even from one area of memory to another. While the transfer of data is taking place, the CPU is free to perform any other tasks it wants to. Its speed will be somewhat degraded because the DMA operation is stealing cycles, holding off the processor for just long enough to transfer the next byte or word. This feature of leaving the processor free allows us to think of this operation as a primitive kind of multitasking: The DMA transfer takes place in the foreground, in real time, while the analysis and display of data takes place in the background, using whatever processor time is left over.

PC DMA Architecture

In an IBM PC or compatible, most of the hardware and control signals necessary to carry out a DMA transfer are present on the system board and I/O bus. To take advantage of these "built-in" facilities, you must build a small amount of handshaking circuitry on an interface card that plugs into the PC backplane, then route the data from its external source to this interface card. In keeping with its "open system"philosophy, IBM makes public all the information you need to understand and work with the signals present on its I/O bus (see the References).

On a PC system board, there is a DMA controller, which is an Intel 8237A integrated circuit. A single 8237 controls four separate DMA "channels,"numbered 0 through 3. Each can be individually programmed, and all four can be active simultaneously. On a PC, channel 0 is used for refreshing the dynamic memory, and its control signals are not even present on the system bus. Channel 2 is used by the floppy disk, and channel 3 is used by the hard disk, leaving channel 1 available as a spare. Channel 1 can be somewhat over-subscribed, because many add-on peripherals (such as network adapters and tape backup systems) use it.

On an AT, two 8237 chips are present, relieving some of the congestion. The control signals for the additional channels are present only on the extended AT I/O bus (the short connector in the backplane in front of the longer, standard PC connector). Most commercial add-on boards that use DMA are equipped with switches allowing the selection of a non-interfering channel. However, cards that have only the standard PC connector cannot take advantage of the AT's extra channels. On an AT, channel 2 is used for the floppy, and channel 4 is used for cascading the two controllers. Memory refresh and hard disk transfers are performed without DMA. That leaves channels 0, 1, 3, and 5 - 7 available for add-on use.

The 8237 DMA controller is a complex chip, occupying a block of 16 addresses in the PC's I/O space. The base address of the primary controller is 0, so I/O ports O to F (hex) are used to address it. The secondary (AT only) controller's base address is CO but it deals in words instead of bytes, and so is addressed on the even-numbered ports in the range CO-DE. Figure 1 shows the I/O port assignments of the 8237 chips.

The following is the sequence of operations involved in setting up and carrying out a DMA transfer. A channel must be selected for use and the hardware and software must both agree on it. The software (writing to the DMA controller) programs the transfer mode (read or write), the starting address of the area in memory to which or from which the data will be transferred, the number of bytes (words, in the case of the secondary controller) to be transferred, and then clears the channel mask bit, enabling the hardware to begin transferring data. The hardware requests a DMA cycle by raising the DMA request line (DRQ) for the particular channel to a high level. The DMA controller responds by lowering the DMA acknowledge line (DACK), which is normally high, for that channel. This signals that the next bus cycle can be used by the hardware to transfer a byte or word of data. If the controller is programmed for a write transfer, when DACK goes low the hardware may place the next value on the data lines. In a read transfer, the I/O bus data lines contain the next value in memory. The controller takes care of everything else: Incrementing the memory location, driving the address lines, and handling the CPU control lines. From a hardware design standpoint, this is a simple interface.

Things become somewhat more complicated if the program desires notification when the DMA transfer is complete. The DMA controller raises a total count signal (TC) along with the last DACK when the programmed count expires. The TC line is available on the I/O bus, and can be wired to one of the interrupt request (IRQ) lines to cause a CPU interrupt when the transfer is complete. The interrupt service routine in software can then program the DMA controller to set up the next transfer, closing the loop and creating a foreground task that will run indefinitely.

Building a DMA Interface Circuit

Figure 2 depicts a circuit that performs all the DMA handshaking described earlier. Together with the software supplied in the accompanying listings, it forms the nucleus of a real-time data acquisition system. It was designed to accept data from a D/PAD (a commercial telemetry receiver), but it will work with any data source that can provide a byte at a time and a clock signal when each byte is ready.

At the heart of the circuit is the FIFO (U1 in Figure 2), a chip that implements in hardware the first-in, first-out data structure familiar to most programmers. There are eight parallel data lines coming in to the FIFO, and eight going out. The incoming bytes are written into the FIFO memory by strobing the write clock, and the bytes are read out in the same order as they were written by strobing the read clock. Both of the strobes are active-low pulses. The FIFO acts as an elastic buffer between the data source and the computer: If the computer is busy for some period of time (servicing another interrupt for example), incoming data just gradually fill up the FIFO until the computer gets around to taking them out. If the FIFO ever overflows, data are lost. The depth of the FIFO and the incoming data rate determine the maximum tolerable software delay. The FIFO I used has a capacity of 1024 bytes.

The FIFO provides an "empty" flag, a signal that is low when the FIFO is empty and high when it contains data. This signal is used directly to drive the channel 1 DMA request line, DRQ1. This means that as long as there is something in the FIFO, there will be a request to transfer data. In single-transfer mode, the DMA controller guarantees that even when DRQ is asserted continuously, the CPU will get every other bus cycle. In other words, we can't shut out the CPU by holding up the request line like this. Assuming that the DMA controller has been correctly programmed by the software, there will be DMA acknowledge signals returned on DACK1. Each DACK, an active-low signal, strobes the read clock on the FIFO, which causes the next byte to appear on the FIFO output lines. The DACK also gates the next chip in line, which is a "tri-state" buffer (U2) whose output is connected to the PC data bus. When the buffer is not enabled (DACK signal high), its output lines are inactive and do not interfere with the PC bus operation. When DACK is low, the buffer drives its 8 bits onto the PC bus, where they are picked up and deposited in memory as directed by the DMA controller.

The number of bytes that are transferred in this manner is dependent on the count programmed into the DMA controller by the software. On the last transfer the DMA controller asserts the TC signal, driving it high. There is only one TC for all the DMA channels, so in order to distinguish it from other DMA operations that may be going on, it is ANDed with the negated DACK signal. The conjunction of these two sets a flip-flop (U3B), whose output goes directly to IRQ3 on the PC bus, causing an interrupt in the CPU. This is the same interrupt used by COM2 if one is present, so another interrupt must be used in case of conflict. To make sure the interrupt is recognized, the interrupt line must be held high until the CPU transfers control to the interrupt service routine. Unfortunately, there is no way to determine automatically that the ISR is executing, so the software must explicitly notify the hardware when it is safe to lower the IRQ. This is done by creating an I/O port.

When the software reads or writes from an I/O port, the CPU places the I/O port address on the PC address bus, together with an "address enable"(AEN) strobe and either an IOR or an IOW signal, depending on whether the I/O port was read or written. Normally, any device recognizing its own I/O address uses the next bus cycle to transfer a byte of data, either to or from the CPU, thus satisfying the program's read or write. However, it is not actually necessary for any data to be transferred. The circuit takes advantage of this by using just the fact that an I/O read has occurred on its own address to clear the interrupt request.

The address comparator at U4 raises its output line whenever its hard-wired address appears on the PC bus in conjunction with AEN. For simplicity's sake, the address is only partially decoded, that is, whenever bits 9 - 2 of the address are "11101000" regardless of the rest, the decoder output goes high. Binary 11101000XX is hex 3A0 - 3A3, so any of these addresses will work. (So will 7A0 - 7A3, BA0 - BA3, and FA0 - FA3.) Normally, these addresses are unused in a PC, any other address range can be selected by wiring the appropriate combination of pins to +5V and ground. To finally turn off the flip-flop driving the IRQ, the decoder output is ANDed with IOR. The end result is that the program can read I/O port 3A0 (or any of its equivalents) to clear the interrupt request.

One final bit of logic is used to reset the FIFO to its empty state. This reset is done at the start of data acquisition to clear out any old data and to start filling the FIFO on a "frame" boundary, which marks the beginning of a new block of data. The same I/O port address decode is used in conjunction with IOW this time, to set the flip-flop at U3A, lowering the active-low "reset"signal on the FIFO. The software can thus cause the reset to happen by writing to I/O port 3A0. The flip-flop is cleared at the next "frame sync" pulse from the data source, bringing the FIFO out of reset and allowing it to begin to fill. The software must immediately program the DMA controller to begin a transfer -- before the FIFO overflows -- to avoid losing data.

Programming the DMA Interface

Now let's turn to the software that makes this system run. It is contained in two files, dma.c (Listing One, page 94) and test.c (Listing Two, page 96). The first file is a subroutine package containing all of the subroutines to set up and run the DMA hardware, and the second file is a sample main program to show how the subroutines are called.

The software operates on a "Ping-Pong" buffer principle. Two buffers are allocated at run time. While one buffer is being filled by DMA, the other buffer's contents are available to the program for whatever data processing is desired. When the transfer into the first buffer is complete, the buffers are swapped and a new transfer is started. The size of the buffers is chosen so that there is enough time for the program to do all the processing it needs to do in between buffer swaps. At a medium data rate of say, 64K bits per second, there are eight seconds of data in a 64K byte buffer.

Although a lot of processing can be done in eight seconds, it is not an infinite amount of time. If a program needs to do some heavy data crunching, its first action, when buffers are swapped, should be to copy some or all of the new buffers into a spare area in memory and work on it there. The data acquisition is entirely interrupt-driven, and once started, it runs by itself, alternately filling and swapping buffers like clockwork. It can be thought of as the higher priority "foreground" task, with the data processing filling up the time as the "background" task.

The main program calls alloc_dma_buf( ) to allocate the Ping-Pong buffers. The buffer size, in bytes, is specified in the global variable buf_size. Because of the way the DMA controller works, neither buffer is allowed to straddle a physical 64K byte "page"boundary in memory. Another way to say this is that each DMA buffer must lie entirely within the range X0000-XFFFF (hex) where X identifies one of the ten 64K pages in the 640K base memory of the PC. This requirement is difficult to meet and alloc_dma_buf( ) does it by using the DOS memory allocation functions to grab all available memory, then fitting the two buffers into the first pages large enough to contain them. Of course, the maximum-length DMA transfer this scheme can handle is 64K bytes.

The program then calls dma_setup( )to prepare the DMA controller and enable the interrupt service routine. The channel number to be used by the hardware (channel 1 in the example) is specified in the global variable dma_chan, and the interrupt number in dma_irq. dma_setup( ) locates a number of registers (port addresses) that will be used later on. A single DMA controller can only handle data in 64K chunks (bytes or words, depending on which controller). To gain access to the full 640K address space, the upper 4 bits of the transfer address will be placed in a page register. There is one page register for each DMA channel, allocated as shown in Figure 3. The correct page register address is selected by dma_setup( ), as well as the base, count, mask, and mode register addresses from Figure 1.

Figure 1: 8237 DMA controller I/O port assignments

  0  C0  Channel O base address (write 2 bytes, LSB first).
  1  C2  Channel 0 count (write 2 bytes LSB first).
  2  C4  Channel base address
  3  C6  Channel base
  4  C8  Channel 2 base address
  5  CA  Channel 2 count
  6  CC  Channel 3 base address
  7  CE  Channel 3 count
  8  DO  Command/status register (not used in this application)
  9  D2  Request register (not used)
  A  D4  Single mask register (write only)

         |7| |6| |5| |4| |3| |2| |1| |0|
                              |   |   |__
                              |   |______channel select (0.3)
                              |__________mask bit value (0.1)

  B  D6  Mode register (write only)

  |7| |6| |5| |4| |3| |2| |1| |0|
  |   |   |   |   |   |   |   |__
  |   |   |   |   |   |   |______channel select (0.3)
  |   |   |   |   |   |
  |   |   |   |   |   |__________
  |   |   |   |   |______________01=write. 10=read
  |   |   |   |__________________0=disable autoinitialization, 1=enable
  |   |   |______________________0=disable autoinitialization, 1=decrement
  |   |
  |   |__________________________
  |______________________________01=single transfer mode

  C  D8  Clear byte pointer flip=flop (write only)
  D  DA  Master clear (not used)
  E  DC  Clear mask register (not used)
  F  DE  Write all mask registers (not used)

Figure 3: DMA page register assignments

  Channel  Page Register
   Number  Address (hex)
  ----------------------

   0       87
   1       83
   2       81
   3       82
   4       8F
   5       8B
   6       89
   7       8A

The next step uses an undocumented feature of DOS. Because DOS is not reentrant, care must be taken in calling DOS functions from an interrupt service routine (it may have interrupted a DOS operation in progress). The undocumented DOS function 34 (hex) returns in the register pair ES:BX, the address of its critical section flag. When the byte at this address is zero, it is safe to call any DOS function. When the byte is non-zero, DOS is executing a critical section of code and may not be reentered. Function 34 works in DOS, Versions 2 and 3; I have not tested it in Version 4. In dma_setup( ), the address of this flag is found and saved for later use.

The final step in dma_setup( ) is to place the address of the interrupt service routine dma_isr( ), in the specified vector, and enable the interrupt by clearing the corresponding mask bit in the interrupt controller.

Starting the Data Acquisition Process

To start up the data acquisition, the program calls start_dma( ). Two arguments are passed: The buffer address and the number of bytes to transfer. start_dma( ) programs the DMA controller with the buffer address and count, using the register addresses computed above in dma_setup( ). The 8237 is programmed 1 byte at a time, the least significant byte first. To ensure the correct order, a register on the 8237 resets its internal byte flip-flop to its initial state, but it is a safe assumption that in a PC this flip-flop is already clear so the LSB can be written first.

First, the mask bit on the 8237 for the selected channel is set, disabling DMA on this channel while the 8237 is being programmed. Next, the buffer address is split into 3 bytes by one of two methods, as illustrated in Figure 4. The method used depends on whether the channel number is located on the byte-oriented or the word-oriented controller. The lower 8 bits and the middle 8 bits are written to the 8237's base address register in two successive operations. The high bits are written to the page register. Next, the byte count is divided by two for word-oriented channels, then decremented and output to the 8237's count register in two writes. The reason it is decremented is that the terminal count is reached when the count remaining goes from 0 to FFFF. Finally, the channel mask bit is cleared, allowing DMA to begin.

When the last byte or word is transferred, the interface card causes a CPU interrupt. Because the address of dma_isr( ) was placed in the corresponding interrupt vector, this routine is entered as soon as the interrupt occurs. By declaring the function with type interrupt, the usual C entry code is replaced by interrupt entry code: The registers are saved and the correct data segment is loaded. The function returns by restoring the registers and issuing an IRET instruction. The stack segment may not be the same as the data segment as would be usual in small model code, so pointers to local variables are not allowed in an interrupt context.

In dma_isr( ) the variable curr_buf is set to point to the buffer that was just filled. The main program can use this address to gain access to the data. The buffer index is toggled to swap buffers and start_dma( ) is called, passing the address of the next buffer. Thus, dma_isr( ) propagates its own execution because when the next buffer fills, the interrupt will occur and dma_isr( )will get called again. Before exiting, the interrupt service routine clears its own interrupt request and outputs the end-of-interrupt signal to the interrupt controller. I declared the function reset_irq( ) in the calling program rather than here because it is hardware dependent -- the functions in dma.c work for any hardware configuration.

If the variable file_handle is non-zero, it is assumed that file_handle was assigned to a file via a C open( )call. write_buf( ) is then called to write the just filled buffer to the disk file that file_handle represents. This is where the DOS critical section flag comes in handy. If the interrupt service routine happened to interrupt DOS at a critical location, the disk write is skipped and the buffer is lost. This will never occur if the background program does not make use of DOS calls, but it provides a simple semaphore when the program must (such as during the opening and closing of files).

Real-Time Applications

I built the circuit using wire-wrap components on a PC prototyping board. It requires only six chips in all, but the FIFO, are standard LS-series parts. I tested it using a simple program on a 12-MHz Compaq 286 computer. The system was able to acquire a continuous stream of data from the D/PAD at rates up to 100K bytes per second, write all the data to disk, and output the results of a minimal error checking algorithm to the screen without dropping any data. Of course, at the highest rate, the available disk storage of 30 Mbytes or so is exhausted in about five minutes!

I designed a more complex software package to receive telemetry from a NASA experiment flown on a sounding rocket from Woomera, Australia in March 1988. With similar DMA interface hardware, the Compaq acquired the telemetry at 25K bytes per second, recorded the full flight's worth of data on disk, and displayed a continuously updating graphical image of the astronomical target. The investment in software and hardware development was very small for a mission of its type, and it has served as a template for future ground support systems design.

References

IBM Corporation, Technical Reference, Personal Computer AT, IBM part number 1502243, 1984.

Intel Corporation, Microsystem Components Handbook, volume I, Intel part number 230843, 1989.

Eggebrecht, Lewis C. Interfacing to the IBM Personal Computer, Howard W. Sams & Company, Indianapolis, Ind. 1983.

_REAL-TIME DATA ACQUISITION USING DMA_ by Tom Nolan

[LISTING ONE]



/*-------------------------------------------------------------------------*/
/* dma.c -- subroutines for dma data acquisition
 * The calling routine must declare the variables in the "extern" list
 * below, and the reset_irq() function. Communication from the main
 * program to the subroutines is mostly through these global variables.
 * The calling routine must give values to dma_chan, dma_irq and buf_size,
 * then call alloc_dma_buf(), dma_setup(), and start_dma(). As each
 * dma buffer fills up, the interrupt service routine calls start_dma() on
 * the next buffer. The calling routine can wait for buf_index to
 * change, then process data pointed to by curr_buf. Cleanup is done
 * by dma_finish(), which is called automatically when the program exits.
 * Compiler: Microsoft C Version 5.0
 *           Set /Gs switch to remove stack probes (a necessity for any
 *          function called at interrupt state!)
 * Tom Nolan - 8/7/89
 */

#include <dos.h>

/* DMA Register Definitions */

#define DMA0_BASE   0x00   /* address of dma controller (chan 0-3) */
#define DMA1_BASE   0xC0   /* address of dma controller (chan 4-7) */

/* Interrupt Controller Definitions */

#define INTA00      0x20   /* base address of int ctrlr */
#define INTA01      0x21   /* address of int ctrlr 2nd reg */
#define EOI         0x20    /* code for non-specific end-of-int */


/* External Variables */

extern char far *dma_buffers[];   /* array containing buffer addresses */
extern int      buf_index;   /* index of current buffer in array */
extern char far *curr_buf;   /* pointer to just-filled buffer */
extern unsigned buf_size;   /* size of buffers in bytes */
extern int      lost_buffers;   /* count of buffers unable to be written */
extern int      dma_irq;   /* h/w interrupt when dma complete (0-7) */
extern int      dma_chan;   /* channel number for dma operation */
extern int      file_handle;   /* handle of archive file (0=no file) */
extern void     reset_irq();   /* function to reset interrupt request */


/* local variables - placed in static storage to
 * avoid excessive stack usage in interrupt routines */

static union  REGS  r;      /* general registers */
static struct SREGS s;      /* segment registers */
static int sel;         /* dma channel select bits */
static int basereg;      /* dma controller base address register */
static int cntreg;      /* dma controller count register */
static int maskreg;      /* dma controller mask register */
static int modereg;      /* dma controller mode register */
static int pagereg;      /* dma page address register */
static int page_tbl[] =      /* table of page register addresses */
      { 0x87, 0x83, 0x81, 0x82,    /* for dma channels 0, 1, 2, 3 */
        0x8f, 0x8b, 0x89, 0x8a };  /*                  4, 5, 6, 7 */
char far *dos_crit_addr;   /* address of DOS critical section flag */
static void          /* space for saved int vector contents */
   (interrupt far *dma_int_save)();

/* macros for extracting bytes from 20-bit addresses */

#define LSB(x)  *((unsigned char *) &x)
#define MSB(x)  *(((unsigned char *) &x) + 1)
#define PAGE(x) *(((unsigned char *) &x) + 2)

/* Function Prototypes */

void dma_setup(void);
void dma_finish(void);
int alloc_dma_buf(void);
void start_dma(char far *, unsigned);
void interrupt far dma_isr(void);
int write_buf();

/*-------------------------------------------------------------------------*/
int alloc_dma_buf()      /* allocate a pair of dma buffers */
{
   unsigned buf;      /* temp variables for various */
   unsigned max;      /*   paragraph addreses       */
   unsigned seg;
   unsigned size;      /* buffer size in paragraphs */

   /* This routine allocates a pair of buffers that can be
    * filled by dma. The buffers are guaranteed to be
    * aligned so that they do not cross physical page boundaries.
    * Before calling this routine, set the value of buf_size to
    * the required number of bytes in each buffer. The maximum
    * buffer size is 64K bytes, which can be allocated
    * by specifying a buf_size of zero. The byte count is converted
    * to paragraphs, which are the units the DOS memory allocation
    * functions work with. Buffer addresses returned in dma_buffers[0]
    * and dma_buffers[1]. Return value is zero if allocation succeeded,
    * non-zero (an MS-DOS error code) otherwise.
    */

   size = (buf_size == 0) ? /* convert bytes to paragraphs */
         0x1000 : buf_size >> 4; /* ..by dividing by 16 */
   _dos_allocmem(0xffff, &max);   /* get max paragraphs from dos */
   _dos_allocmem(max, &seg);   /* now grab it all */

   buf = seg;      /* initial attempt at buffer segment */
   if( ((buf + size - 1) & 0xf000)    /* if buffer crosses  */
           != (buf & 0xf000) ) /*  phys page bdry    */
      buf = (buf & 0xf000) + 0x1000; /*...adjust to next phys page */

   dma_buffers[0] = (char far *)   /* convert buffer segment  */
          ((long) buf << 16); /*... to far pointer for return */

   buf += size;         /* initial attempt at next buffer */
   if( ((buf + size - 1) & 0xf000)
         != (buf & 0xf000) )
               buf = (buf & 0xf000) + 0x1000; /* adjust if crosses page bdry */

   dma_buffers[1] = (char far *)   /* return it as a far pointer */
            ((long) buf << 16);

   size = buf + size - seg;   /* compute actual size needed */
   return             /* free unneeded memory and     */
               _dos_setblock(size, seg, &max); /* return error if not enough */
}

/*-------------------------------------------------------------------------*/
void dma_setup()      /* set up for dma operations */
{
   /* Before calling this routine set the following variables:
    *    dma_chan - channel number (hardware dependent)
    *   dma_irq  - interrupt request number 0-7 (hardware dependent)
    */

   int intmsk;

   sel  = dma_chan & 3;   /* isolate channel select bits */
   pagereg = page_tbl[dma_chan];   /* locate corresponding page reg */

   if(dma_chan < 4)   /* setup depends on chan number */
   {
      basereg = DMA0_BASE + sel * 2;   /* standard dma controller */
      cntreg  = basereg + 1;       /* note that this controller  */
      maskreg = DMA0_BASE + 10;    /*     is addressed on byte   */
      modereg = DMA0_BASE + 11;    /*         boundaries         */
   }
   else
   {
             basereg = DMA1_BASE + sel * 4; /* alternate dma ctrlr (AT only) */
             cntreg  = basereg + 2;       /* note that this controller     */
      maskreg = DMA1_BASE + 20;   /*     is addressed on word      */
      modereg = DMA1_BASE + 22;   /*         boundaries            */
   }

   r.h.ah = 0x34;          /* dos "get critical flag addr" function */
   intdosx(&r, &r, &s);
   dos_crit_addr = (char far *) /* save its address so it can be tested */
      (((long) s.es << 16) | r.x.bx);   /* ... as a far pointer */

   if(dma_irq < 0 || dma_irq > 7)   /* validate interrupt number */
      return;
   dma_int_save =            /* save current contents of dma int vec */
      _dos_getvect(dma_irq + 8);
   _dos_setvect(dma_irq+8, dma_isr); /* set up new int service routine */

   intmsk = inp(INTA01);         /* get current interrupt enable mask */
   intmsk &= ~(1 << dma_irq);    /* clear mask bit for dma interrupt */
   outp(INTA01, intmsk);         /* output new mask, enabling interrupt */
   atexit(dma_finish);         /* register exit function */
}

/*-------------------------------------------------------------------------*/
static void dma_finish()      /* called via atexit() mechanism */
{
   int intmsk;

   intmsk = inp(INTA01);        /* get current interrupt enable mask */
   intmsk |= (1 << dma_irq);    /* set mask bit for dma interrupt */
   outp(INTA01, intmsk);        /* output new mask, disabling interrupt */

   _dos_setvect(dma_irq+8, dma_int_save);/* restore old vector contents */
}

/*-------------------------------------------------------------------------*/
void start_dma(buf, count)           /* start a dma operation */
char far *buf;                 /* address of buffer to be filled */
unsigned count;                 /* size of buffer in bytes */
{
   int page;
   unsigned long addr =       /* 20-bit address of dma buffer */
         FP_OFF(buf) +
         (long) FP_SEG(buf) << 4;

   /* This routine starts a dma operation. It needs to know:
    *     - the address where the dma buffer starts;
    *     - the number of bytes to transfer;
    * The dma buffer address is supplied in segmented, far-pointer
    * form (as returned by alloc_dma_buf()). In this routine it is
    * converted to a 20-bit address by combining the segment and
    * offset. The upper four bits are known as the page number, and
    * are handled separately from the lower 16 bits. The transfer
    * count is decremented by 1 because the dma controller reaches
    * terminal count when the count rolls over from 0000 to ffff.
    *
    * The dma transfer stops when the channel reaches terminal count.
    * The terminal count signal is turned around in the interface
    * hardware to produce an interrupt when dma is complete.
    *
    * Channels 4-7 are on a separate dma controller, available on
    * the PC-AT only. They perform 16-bit transfers instead of 8-bit
    * transfers, and they are addressed in words instead of bytes.
    * This routine handles the addressing requirements based
    * on the channel number.
    *
    * dma_setup() needs to be called before start_dma() in order to
    * assign values to maskreg, modereg, etc.
    */

   page = PAGE(addr);      /* extract upper bits of address */

   if(dma_chan >= 4)      /* for word-oriented channels... */
   {
      count >>= 1;      /* convert count to words */
      addr  >>= 1;      /* convert address to words */
      page  &=  0x7e;      /* address bit 16 is now in 'addr' */
   }

   count--;            /* compute count-1 (xfr stops at ffff) */
   outp(maskreg,     sel | 0x04);   /* set mask bit to disable dma */
   outp(modereg,     sel | 0x44);/* xfr mode (sngl, inc, noinit, write) */
   outp(basereg,     LSB(addr) );   /* output base address lsb */
   outp(basereg,     MSB(addr) );   /* output base address msb */
   outp(pagereg,     page      );/* output page number to page register */
   outp(cntreg,      LSB(count));   /* output count lsb */
   outp(cntreg,      MSB(count));   /* output count msb */
   outp(maskreg,     sel       );   /* clear mask bit, enabling dma */
}

/*-------------------------------------------------------------------------*/
static void interrupt far dma_isr()
{
   /* This routine is entered upon completion of a dma operation.
    * At this point the current dma buffer is full and we can
    * write it to disk. We set the "available data" pointer
    * to point to the just-filled buffer, and start the next dma
    * operation on the other buffer. At the conclusion of
    * operations, we output a non-specific end-of-interrupt
    * to the interrupt controller.
    *
    * The PC bus provides no mechanism for "unlatching" an
    * interrupt request once it has been serviced. In order to
    * enable the next interrupt, the hardware must be designed
    * so that the request can be reset, by a write to an i/o
    * port, for example. The external routine reset_irq()
    * must be coded to perform this function.
    *
    * Declaring this routine as type 'interrupt', ensures
    * that all registers are saved, the C data segment is set
    * correctly, and that the routine returns with an IRET
    * instruction. Further interrupts are disabled during the
    * execution of this routine.
    */

   curr_buf = dma_buffers[buf_index];   /* post just-filled buf address */
   buf_index ^= 1;              /* index next buffer */
   start_dma(dma_buffers[buf_index],    /* start dma on next buffer */
            buf_size);
   if( file_handle )            /* if disk is enabled.. */
      write_buf();           /* write buffer to disk */
   reset_irq();              /* do hardware-specific reset */
   outp(INTA00, EOI);           /* signal end of int */
}

/*-------------------------------------------------------------------------*/
static int write_buf()          /* write buffer to disk file */
{
   if( *dos_crit_addr )       /* first check dos critical section flag */
   {
      lost_buffers++;       /* ..if set, skip writing this buffer */
      return 0;       /* ..not really an error in this case */
   }

   r.x.dx = FP_OFF(curr_buf);  /* ok to write now, set address in */
   s.ds   = FP_SEG(curr_buf);  /*   proper registers for dos call */
   r.x.bx = file_handle;       /* set file handle to write to */
   r.x.cx = buf_size;       /* set byte count for write */
                /* WARNING - can't write 64K! */
   r.h.ah = 0x40;          /* dos write-to-file-handle function */
   if(intdosx(&r, &r, &s) == buf_size  /* check return value and..  */
         && r.x.cflag == 0)  /* ..carry flag for success code */
      return 0;          /* return success */
   else
   {
      lost_buffers++;        /* didn't write this buffer */
      return 1;        /* return failure */
   }
}





[LISTING TWO]


/*-------------------------------------------------------------------------*/
/* test.c -- test dma data acquisition
 * Compiler: Microsoft C Version 5.0
 * Must compile with -Gs option because
 * reset_irq() is called from interrupt.
 * Tom Nolan - 8/7/89
 */

#include <bios.h>
int far *dma_buffers[2];      /* pointers to two buffers */
int far *curr_buf;         /* pointer to current buffer */
int buf_size;            /* buffer size */
int buf_index;            /* index of current buffer */
int dma_irq = 3;         /* hardware int request line */
int dma_chan = 1;         /* hardware dma channel number */
int file_handle = 0;         /* file handle */
int lost_buffers = 0;         /* write errors */

/* In this program, each dma buffer will be filled with
 * NUMFR "frames", each of size FRSIZE (in words). The second
 * word of each frame is a frame counter, which increments
 * modulo 256. The program checks the frame counters to make
 * sure they are sequential and no data was lost.
 */

#define FRSIZE    64            /* words per frame */
#define NUMFR   8            /* frames per dma buffer */

/*-------------------------------------------------------------------------*/
main()
{
   int temp;
   int i;
   unsigned char frame;
   int far *cp;

   reset_irq();             /* clear interrupt request */
   buf_size = FRSIZE * NUMFR * sizeof(int); /* figure out buffer size */
   alloc_dma_buf();          /* allocate buffers */
   printf("buf1 = %p buf2 = %p\n",       /* informational output */
      dma_buffers[0], dma_buffers[1]);
   dma_setup();                /* set up for dma operations */

   outp(0x3a0,0);                /* reset fifo on hw interface */
   start_dma(dma_buffers[0], buf_size);   /* start up the data acq */
   file_handle = creat("tmp.dat");          /* open a file for raw data */
   temp = buf_index;

   while( !_bios_keybrd(_KEYBRD_READY) )   /* quit on next keystroke */
   {
      if(temp != buf_index)      /* wait for dma complete */
      {
         printf("%d: ",temp);   /* print buffer index */

         for(i=0, cp = curr_buf+1; i<NUMFR; i++, cp += FRSIZE)
         {
                if(frame != *cp) printf("*"); /* frame counter bad */
               else printf(" ");        /* frame counter good */
               printf("%04x", *cp);    /* print frame counter */
               frame = *cp + 1;/* next expected counter value */
         }
      printf(" : %d\n",lost_buffers);   /* keep track of lost writes */
      temp = buf_index;         /* next expected buffer number */
         }
   }
   close(file_handle);         /* close data file */
   exit(0);            /* halt dma and exit */
}

/*-------------------------------------------------------------------------*/
reset_irq()         /* clear interrupt request in hardware */
{
   inp(0x3A0);
}









Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.