Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Embedded Systems

Real-Time Data Acquisition


JUN89: REAL-TIME DATA ACQUISITION

REAL-TIME DATA ACQUISITION

Collecting and storing large amounts of data, such as music, has special requirements

Mike Bunnell and Mitch Bunnell

Mike and Mitch are engineers at Lynx Real-Time Systems Inc. and can be reached at 550 Division St., Campbell, CA 95008.


The acquisition and storage of large amounts of data is vital in many industrial, research, and aerospace/defense applications in which computer systems typically must collect analog data at very high rates (1,000 to 1,000,000 samples/sec.) for relatively long periods (a few seconds to a few days). Consequently, computers used in these environments must be able to assemble huge volumes of data in real time and store them in an organized way so that the information can be analyzed later. Until recently, systems that could do this cost more than $100,000. With current hardware and software technologies, however, equivalent PC-based systems are one-tenth that price.

A computer system -- large or small -- designed for real-time data acquisition must meet four basic requirements: It needs to have an interface that converts continuous real-world data into discrete, digitized samples at a rapid sampling rate; there must be sufficient throughput to mass storage to save the samples at the sampling rate; a data acquisition program must read the samples from the interface and write them to mass storage; and the software must have sufficient real-time response so data samples are never lost. In this article, we'll describe the elements of a common PC-based data acquisition system and show how it can be used in a typical application.

Principles of Real-Time Data Acquisition

Although most real-world data is analog in nature, digital computers, by definition, can only process discrete digital data. To solve this dilemma, an analog-to-digital (A/D) interface is needed. Because analog data is continuous, the A/D interface must take a snapshot of the analog data at one point in time. This process, called sampling, is usually done at a particular sampling rate depending on how fast the analog data is expected to change. The interface must digitize each sample by converting it to a numerical value. The resolution, or number of bits, used to represent this value is dependent on the analog interface. Unless the sampling rate is slow (less than 100 samples/sec.) the interface must provide some way of buffering the digitized data until the CPU accesses it. This can be done either by providing a first-in, first-out (FIFO) buffer as part of the analog interface or direct-memory-access (DMA) capability so the interface can save the data directly to computer memory without using the CPU.

Handling the analog interface is left to a device-dependent code -- the device driver. The device driver sets the operating parameters of the interface, maintains the data buffers, and services requests to read converted data or set the sampling rate. In the same way as the analog-to-digital converter is the interface between the computer and the real world, the device driver is the interface between a data acquisition program and the converter hardware.

In a data acquisition system, sampled data is saved to mass storage (typically a hard disk) for later processing. The reason for saving the data to disk and not just computer memory is that there is usually more data that must be saved than there is memory available. For convenience sake, it is preferable that data be saved in named files on the disk so the data can be conveniently accessed simply by reading it from the appropriate files.

Writing data to a file must be fast enough to keep up with the sampling rate. If, for example, the sampling rate is 100,000 samples/sec. and each sample is 2 bytes, then the computer must be capable of writing 200,000 bytes/sec. to a file on a sustained basis. Although disk-drive manufacturers may quote burst transfer rate and seek rates, these do not reveal the sustained data rate. The best way to determine this rate is to do real-life testing.

This testing process must include writing data to a file, a process that is typically slower than writing directly to the disk. On most operating systems (including Unix and DOS), file access is optimized assuming random intermittent access and blocks are dynamically added to the file as it grows. The data blocks that make up a file may be scattered all over the disk, making access to some files faster than to others. To provide faster reads and writes to files used for high-speed data acquisition, some operating systems offer contiguous files that have data blocks that are preallocated on the media to provide maximum speed for sequential access. The file cannot grow dynamically beyond a preset maximum size, but it can be written to as fast as writing directly to the disk.

To acquire data at high speed, there must be a program to read the data from the analog input device and write it to the file on mass storage. This program must use a FIFO buffering scheme to simultaneously read data from the analog device and write it to disk. Buffering is important because it allows large buffers to be transferred to disk without losing input samples during the disk access. Although this can be done with a single task and asynchronous I/O, it is much easier to use two tasks -- one to store sampled data into the buffer and one to write data from the buffer to disk. The ability of a program to write to a file on disk presupposes that the program is running under some operating system. A multitasking operating system is needed to support the data acquisition program so it can read and write simultaneously (the CPU can't continually read from the analog input if it is in a busy wait loop waiting for the disk).

Real-Time Response

For a real-time data acquisition system to work, much care must be taken to ensure that samples are not lost because the input buffer of the analog device driver has overflowed. Because the input buffer is filled at the sampling rate, the buffer must be emptied within a certain period of time.

In a multitasking system, the problem of emptying the input buffer in time becomes one of CPU throughput and task response. The computer must run fast enough to be able to copy the data from the input buffer before any of it is overwritten. This is the easy part; 32-bit microprocessors can typically copy 2 to 8 Mbytes/sec. and the setup time is minimal. The more difficult part is task response. The input task waits for the input buffer to fill to a certain point by giving up use of the CPU. This gives other tasks in the system the chance to run (such as the task that writes the sampled data to disk). When the buffer has filled to the certain point, the device driver schedules the input task to run. The time to schedule a task, the amount of time for which task switches are disabled, and the time it takes to do a task switch all contribute to the worst-case task response.

Many operating system manufacturers quote only the best and typical task responses because they have not taken care to limit the time for which task switches are disabled. This is not useful because data samples can still be lost if the input task cannot respond in time occasionally. For this reason, programmers must construct their code with the worst-case task response times in mind rather than best, typical, or average response times.

A real-time operating system guarantees fast worst-case task response, making the operating system an important part of a high-speed data acquisition system. These requirements and how they can be met are best explained with an example of a data acquisition system.

A Data Acquisition System

For the purposes of this article, we'll describe a typical data acquisition system that can collect 12-bit analog data on up to 16 channels and save it to disk at rates from as low as once every two seconds up to 250,000/sec. (aggregate). The hardware in our system consists of an 80386 AT-compatible computer, an analog I/O board, and a high-capacity hard disk. The software consists of a real-time operating system, an analog I/O device driver, and a couple of utility programs.

The PC chosen for our high-speed data acquisition system is an 80386 AT compatible manufactured by Mylex. The 20-MHz model comes with 4 Mbytes of main memory and is rated at approximately 4 MIPS. The 80386 AT compatible was chosen for several reasons. First, it has the ability to run the LynxOS, Unix-compatible, real-time operating system. Second, many I/O boards are available for the AT bus. Third, because they are produced in such large volumes, 80386 AT compatibles have an excellent price/performance ratio. Figure 1 shows the hardware configuration.

The analog I/O board used in this system is the DT2821 from Data Translation. It features 16 input channels and 2 output channels, both with 12-bit (or 16-bit) resolution. The DT2821 was chosen because of its DMA support for both input and output channels. Either a DMA interface or on-board FIFO is necessary to allow high-speed transfers without hogging the CPU. The CPU needs to be free to perform other tasks, not the least of which is saving the transferred data to disk. Both the high-speed input and output capabilities of the DT2821 are used in our example. The board has an input throughput of up to 250KHz and an output throughput of up to 130KHz per channel.

The last hardware component of the computer system is the mass storage device. LynxOS, the operating system we use, comes with an optional high-speed SCSI interface that breaks the normal DMA bottleneck on the AT by bursting 32 bytes at a time over the bus using 16-bit transfers. This SCSI interface has a maximum throughput of 3.5 Mbytes/sec. The average AT SCSI interface, which does 8-bit DMA transfers, has a maximum throughput of 250K/sec. Our sample system uses a CDC 270-Mbyte Wren IV drive. Its throughput in our system is over 1 Mbyte/sec. We could have chosen the standard AT interface and hard drive, which has a sustained throughput of 211K/sec., but we wanted higher capacity.

The most important piece of software in our system is the operating system. We use LynxOS, a real-time, multitasking, multiuser operating system especially designed for closed-loop control and data acquisition. LynxOS is a 4.2BSD Unix look-alike with features added from System V Unix.

Compatibility with Unix gives users a wide choice of programs with which to develop software and process collected data, but the major feature of this operating system is its real-time capability. It has guaranteed worst-case interrupt response and task response delays, which means that the worst-case response to external events can be calculated for every program running. LynxOS also has user-controlled priority scheduling, which means that data acquisition tasks can be set at high priority and will not be affected by tasks running at lower priority. In addition, multiple streams of high-speed data can be acquired concurrently. Program development can be done while acquiring data, and data can be processed and displayed while other data is being acquired.

Another important feature of LynxOS is the ability to create contiguous files within the normal file system. Contiguous files are files whose data blocks are sequential on the disk. Accesses to contiguous files do not go through the disk cache. Instead, the data is transferred directly from user task memory via DMA to and from the mass storage device. Not only is there practically no CPU overhead transferring the data, but also larger amounts of data can be transferred per request. Most SCSI disk drives can transfer data with 64K requests more than twice as fast as 8K requests because of controller overhead in SCSI command processing.

Contiguous files are just like regular files, with two restrictions: The maximum size must be specified when creating them, and access requests to them must be made in multiples of 512 bytes. The size of a contiguous file can vary just as can a normal file. The creation size is simply the maximum size the file can become.

The next piece of software is the device driver for the DT2821 I/O board. The device driver is the link between the operating system and the DT2821 device. The operating system and the device driver make the device look just like a file on disk. The device can be opened, read from, or written to just as a file can.

When the device is opened for reading, the device driver starts up a DMA channel to read from the DT2821 and write into a 4K circular buffer. Figure 2 shows the circular DMA buffer in the DT 2821 driver.

The PC/AT DMA controller is programmed for auto-initialize mode so that when it fills up the 4K buffer, it starts over again automatically. This mode is important because at 250,000 samples/sec. there would only be 4 microseconds to reload the DMA controller, which is not enough time.

When it receives a read request, the driver uses a timer to wait approximately the time it takes to acquire 2K into the circular buffer. Then the driver copies from the circular buffer to the buffer in the requesting task. Waiting and copying is repeated until the requested number of bytes is copied, at which time read( ) returns to the calling program. The driver handles writing to the D/A ports of the DT2821 in a similar fashion.

The rest of the software is made up of two utility programs provided with the operating system. The first of these is saio. Saio allows you to set the rate of acquisition and the gain on each channel and to group channels for simultaneous access. It does this by making requests through the ioctl system call to the device driver.

The second utility program is dbuff. Dbuff reads data from standard input and writes it to standard output continuously. Dbuff begins by forking itself into two tasks. The input (producer) task puts its data into one of two shared buffers; the output (consumer) task gets the data from each shared buffer when full. Figure 3 shows the double-buffering scheme used by dbuff. This double-buffering scheme allows large buffers to be written to the disk, a necessity when high throughput is required. Dbuff uses many of the operating system features, such as multitasking, shared memory, and semaphores. A complete listing of dbuff is shown in Listing One.

Real-Time Considerations

Before we start acquiring data, we must make sure that we have the real-time response necessary to make our system work. The task reading from the DT2821 device has to read the data out of the circular buffer before the DMA controller overwrites the data. The task reading is awakened by the timer to read out of the 4K circular buffer after the DMA has time to fill half the buffer. This gives the task half of the time it takes to fill the buffer to get the data. At our maximum DMA transfer speed of 500K/sec., 2,048/500,000 seconds (approximately 4 milliseconds), are available to transfer the data.

The operating system guarantees 500 microseconds (0.50 milliseconds) worst-case response for the highest priority task on a 4-MIP 80386 computer. Therefore, if the task reading from the DT2821 is the highest priority task, we are assured of success. If there are other tasks at higher priority, such as another data acquisition task, we would have to measure its longest continuous CPU usage and add that to the 500 microseconds and make sure that that value was less than 4 milliseconds.

We can estimate the longest continuous CPU usage for our data acquisition tasks. The task accessing the DT2821 incurs a system call overhead of 25 microseconds when reading or writing. The longest stretch of time the DT2821 device driver uses the CPU is the 0.3 to 0.6 milliseconds it takes to copy 2K. Thanks to contiguous files, the task writing to the disk uses even less CPU time, only about 70 microseconds to set up the DMA controller and SCSI controller to transfer the data to or from disk, including the system call overhead. The continuous CPU usage for both data acquisition tasks is 0.02 + 0.60 + 0.07 = 0.69 milliseconds.

Thus the worst-case response of a task running at lower priority than both tasks doing the data acquisition is the guaranteed worst-case response of 500 microseconds plus the CPU usage of the data acquisition tasks, or 0.69 + 0.50 = 1.19 milliseconds.

A Data Acquisition Session

As an application of a high-speed data acquisition system, we can record a song on the hard disk at compact disc speeds and then play it back, first on just one channel, then in stereo.

This particular application shows acquisition of analog data, which is the kind of data usually acquired. The rate of acquisition is on the same order as that of a typical application. The quantity of data acquired is also common to many high-speed data acquisition applications. Finally, recording and playing back music is a good test of the acquisition system because you can hear whether data is acquired properly when it is played back.

Suppose we record the music from a home stereo receiver. Standard voltage levels for these devices are in the range -1 to +1 volts. The DT2821 board's bipolar range is -10 to 10 volts. So, we need to set the gain as close to 10 as we can for the input signal and use a voltage divider circuit made from two resistors to get the output in the correct range. We tie input channel 0 on the DT2821 to TAPE OUT LEFT on our stereo, and we tie output channel 0 to our resistor circuit, then to AUX LEFT input on the back of the amplifier. For stereo, we can tie input channel 1 and output channel 1 to TAPE OUT RIGHT and AUX RIGHT. Figure 4 shows the stereo/computer combination.

Normally, you must use a Nyquist filter, which is a low-pass filter with a cutoff of half the sampling frequency, on the input to the A/D converter. Experimentation has shown, however, that the filtering through the stereo is sufficient. It is also a good idea to put an anti-aliasing filter on the output so that the output frequency can't be heard. An anti-aliasing filter is not really necessary in this case because it is impossible to hear the sampling frequency of 44KHz. The amplifier effects some filtration internally as well.

Now let's do some data acquisition. Compact disc speed is 44,000 samples/sec., which means we will have to save 88K/sec. to disk for a single channel and 176K/sec. to disk for stereo.

First, we create a 30-Mbyte contiguous file to hold the song. With a file of this size we can record about 6 minutes from a single channel or 3 minutes of stereo. The LynxOS command to create a contiguous file is

mkcontig music.data 30m

Now we set input and output transfer rates and the input gain of the DT2821. The device node for the DT2821 is called dtaio and is located in the directory/dev. We'll just collect one channel of data first:

  saio - rate .000027 - gain 10                         < /dev/dtaio

  saio - rate .000027 > /dev/dtaio

The driver will set the gain and rate values as close as it can to the requested values. The actual values can be inspected as follows:

  saio < /dev/dtaio

Next we can set the stereo to our favorite FM station and record a song off the air. We can use dbuff and redirect the input from /dev/dtaio and direct the output to our file music.data:

  dbuff 21 20 < /dev/dtaio > music.data

When the song ends, we can press Ctrl-C to stop collecting. Note that dbuff takes two numeric arguments. The first value is used for the priority of the producer task, and the second value is used for the priority of the consumer task. We have set the priority of the task reading from /dev/dtaio to 21 and that of the task writing to the file music.data to 20. It is not really necessary to set the task accessing /dev/dtaio to be of higher priority than the consumer task in this case because the consumer is writing to a contiguous file and will not use much CPU time.

Now let's play back the song. We set the stereo to AUX and type

    dbuff 20 21 < music.data                         > /dev/dtaio

To add echo or reverberation to the data, you can run the data through the reverb program in Listing Two using:

  reverb < music.data | dbuff 20 21                          > /dev/dtaio

Reverb reads from standard input, so it is necessary to use redirection to make it read from music.data.

To hear what a song sounds like backward, you can use the program in Listing Three to reverse the samples. The program, hypothetically called reverse, could be executed as follows:

  reverse music.data | dbuff 20 21                          > /dev/dtaio

The output from reverse is piped through dbuff, then sent to the DT2821. We have told dbuff to set the priority of the task writing to /dev/dtaio higher than that of the producer task to guarantee its response time. We can combine these "filter" programs easily:

  reverse music.data | reverb | dbuff                    20 21 > /dev/dtaio

To record and play in stereo, we use saio to group input channels 0 and 1 and output channels 0 and 1:

  saio - group 0 1 < /dev/dtaio

  saio - group 0 1 > /dev/dtaio

The commands to record and play a song are the same as before.

We are now collecting data at 88,000 samples/sec., or 176K/sec. After acquiring the data, it can be processed and analyzed on our system or sent to another computer. (LynxOS comes with a powerful software development environment and X Windows, which can be used to create programs to display, edit, and process the acquired data. Also, it supports Ethernet and TCP/IP to provide high-speed links to other computers.)

Summary

The computer system described in this article meets all requirements of a high-speed data acquisition system. The DT2821 analog I/O board serves to change the real-world data to discrete digital data. The SCSI disk system provides the high-speed mass storage capability. LynxOS, dbuff, and the device driver for the DT2821 are the software that performs the acquisition. The operating system guarantees the real-time response and provides the environment to analyze the data.

Availability

All source code for articles in this issue is available on a single disk. To order, send $14.95 (Calif. residents add sales tax) to Dr. Dobb's Journal, 501 Galveston Dr., Redwood City, CA 94063, or call 800-356-2002 (from inside Calif.) or 800-533-4372 (from outside Calif.). Please specify the issue number and format (MS-DOS, Macintosh, Kaypro).

Real-Time Data Acquisition by Mike and Mitch Bunnell

[LISTING ONE]

<a name="0111_000b">

     /*  dbuff.c     Double buffering program for continuous
     reading from input and continuous writing to output

     */

     #include <stdio.h>
     #include <smem.h>
     #include <sem.h>

     extern char *malloc();
     extern int errno;

     #define BSIZE 65536      /* size of each buffer */

     struct xbuff {
          char buffer[BSIZE];
          int count;
          int psem;
          int csem;
          int done;
          struct xbuff *other;
     };

     /*
          Write function that is used by the output task
     */

     outputr(p, prio)
     register struct xbuff *p;
     int prio;
     {
          int count;

          setpriority(0, getpid(), prio);
          while () {
               sem_wait(p->csem);           /* wait for buffer to fill */
               if (p->count <= 0) {
                   sem_signal(p->psem);    /* leave if finished or error */
                   break;
               }
               count = write(1, p->buffer, p->count);  /* write output */
               if (count <= 0) {
                    /* exit if error on write */
                    p->done = 1;
                    sem_signal(p->psem);
                    break;
               }

               /* tell producer buffer has been emptied */
               sem_signal(p->psem);
               p = p->other;
          }
     }

     /*

               Read function that is used by the input task
     */
     inputr(p, prio)
     register struct xbuff *p;
     int prio;
     {
         int count;

         setpriority(0, getpid(), prio);
         do {
              /* wait for consumer to empty buffer */
              sem_wait(p->psem);
              if (p->done) {
                   break;
              }
             /* read from input and fill buffer */
             count = read(0, p->buffer, BSIZE);
             p->count = count;

             /* tell consumer task buffer is filled  */
             sem_signal(p->csem);
             p = p->other;
        }  while (count > 0); /* exit when no more data */
     }

     main(argc, argv)
     int argc;
     char **argv;
     {
         register struct xbuff *buffa, *buffb;
         int inprio, outprio;

         /* default to current priority  */
         inprio = outprio = getpriority(0, 0);
         if (argc == 2) {
             /* Get input priority from command line if present */
             inprio = atoi(argv[1]);
         }
         if (argc == 3) {
              /* Get output priority from command line if present */
              inprio = atoi(argv[1]);
              outprio = atoi(argv[2]);
         }

         /* Allocate shared memory  */
         buffa = (struct xbuff *) smem_get(
                 "buffa",
                 (long)sizeof(struct xbuff),
                 SM_READ | SM_WRITE);
         buffb = (struct xbuff *) smem_get(
                 "buffb",
                 (long)sizeof(struct xbuff),
                 SM_READ | SM_WRITE);

         /* delete old semaphores if they exist */
         sem_delete("buffac");
         sem_delete("buffap");
         sem_delete("buffbc");
         sem_delete("buffbp");

         buffa->csem = sem_get("buffac", 0);  /* Create new semaphores to */
         buffa->psem = sem_get("buffap", 1);  /* control access to shared */
         buffb->csem = sem_get("buffbc", 0);  /* memory                   */
         buffb->psem = sem_get("buffbp", 1);
         buffa->done = buffb->done = 0;

         buffa->other = buffb;
         buffb->other = buffa;

     /*
              Create another task to write.
              This task will read.
     */

          if (fork() != 0)             /* Create another task to  */
               inputr(buffa, inprio);  /* write.  This task will  */
          else                         /* read                    */
               outputr(buffa, outprio);
     }




<a name="0111_000c"><a name="0111_000c">
<a name="0111_000d">
[LISTING TWO]
<a name="0111_000d">

     /* Reverb.c    IIR filter program to add reverberation */

     #include  <file.h>

     extern char *malloc();

     ewrite(s)
     char *s;
     {
          write(2, s, strlen(s));
     }

     /*
          Read the whole size read() under UNIX returns the amount it
          read.  Last buffer is (biased) zero-filled.
     */
     fullread(fd, buff, size)
     int fd;
     char *buff;
     int size;
     {
          int i, j;

          i = 0;
          do {
              j = read(fd, &buff[i], size - i);
              if (j <= 0) {
                  /* This must be the last buffer of the file */
                  while (i < size)
                      buff[i++] = 0x800;
                  return -1;
              }
              i += j;
         }  while (i < size);

         return size;
     }

     main(ac, av)
     int ac;
     char **av;
     {
          short *ibuff, *obuff;
          int delay;
          int i;
          int fd;
          int rundown;
          int rv;
          char *fn;
          register short *p, *q;

          if (ac > 2) {
              ewrite("usage: reverb [delay]\n    (delay expressed in samples)\n");
              exit(1);
          }
          if (ac == 2)
              delay = atoi(av[1]);
          else
              delay = 10240;

          /* make sure delay is multiple of 512 bytes */
          delay -= delay & 511;

          /* make delay >= 512 andd <= 128K           */
          if (delay < 512)
              delay = 512;
          if (delay > 128*1024)
              delay = 128*1024;

          fd = 0;

          ibuff = (short *) malloc(delay * sizeof(*ibuff));
          obuff = (short *) calloc(delay * sizeof(*obuff));

          do {
              /* Read a buffer, but don't check error status yet */
              rv = fullread(fd, ibuff, delay * sizeof(short));

              /*
                Add the fresh input samples to the old samples, after
                dividing the old samples by 2
              */
              for (p = ibuff, q = obuff, i = 0; i < delay; ++i, ++p, ++q)
                 *q = ((*q - 0x800) >> 1) + *p;

              /*
                 Write the output reverbed buffer
              */
              write(1, obuff, delay * sizeof(short));
          } while (rv != -1);

          /*
              Allow sound in output buffer to "die down"
          */
          for (rundown = 11; --rundown >= 0; ) {
              for (q = obuff, i = 0; i < delay; ++i)
                   *q = (*q - 0x800) >> 1;

              write(1, obuff, delay * sizeof(short));
          }
     }






<a name="0111_000e"><a name="0111_000e">
<a name="0111_000f">
[LISTING THREE]
<a name="0111_000f">

     /*  reverse.c   Write a file in reverse to standard output */

     #include  <file.h>
     #include  <types.h>
     #include  <time.h>
     #include  <stat.h>

     main(ac, av)

     int ac;
     char **av;
     {
          int fd;
          short buff[4096];
          int rc;
          int i, j, t;
          long pos;
          struct stat s;

          ++av;
          if ((fd = open(*av, O_RDONLY, 0)) == -1) {
              perror(*av);            /* exit if can't open file */
              exit(1);
          }

           fstat(fd, &s);             /* find the size of the file */
          pos = s.st_size &  1;

          while (pos > 0) {
              /* See how many bytes can be read now */
              if (pos < sizeof(buff))
                  rc = pos;
              else
                  rc = sizeof(buff);

              pos -= rc;
             /* Seek back a block and read */
             lseek(fd, pos, 0);
             read(fd, buff, rc);

             /* Reverse the samples in the block */
             for (i = 0, j = (rc / 2) - 1; i < j; ++i, --j) {
                 t = buff[i];
                 buff[i] = buff[j];
                 buff[j] = t;
             }

             /* Write the reversed block */
             write(1, buff, rc);
           }

          close(fd);
     }












Copyright © 1989, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.