Channels ▼
RSS

Embedded Systems

Real-Time Enough

Source Code Accompanies This Article. Download It Now.


Bit-Cell Management

At a slightly higher level, you need to perform bit-cell management. What I mean by this is that we need to know where one bit ends and another bit starts. Obviously, if the bits have different values, this is easy: When the value changes, one bit has ended and another has started. However, what if there are several bits all with the same value, one after another? This is the topic of "clock recovery."

Because the data stream is arriving at 1200 baud, you know that each bit cell is 1/1200 of a second long, or 833.333... microseconds. Next you calculate how many samples (at 8 kHz) 1/1200 of a second is. Well, this is the same number I came up with earlier when I calculated how many samples I needed to store one complete waveform at 1200 Hz. It's the value 6 and 2/3. This means that every 6 2/3 samples we have the end of one bit, and the beginning of another. However, since I am working with integral samples, it means that I have to compromise a little bit, and potentially introduce drift. (Drift is introduced not only by rounding errors, but also because your sound card will not be sampling at exactly 8 kHz, nor will the phone company be transmitting at exactly 1200 baud; there will be some variation.)

The saving grace here, though, is that you are dealing with a serial protocol that has a start bit and a stop bit, which are different from each other. This means that you are guaranteed to have at least two bit reversals during an 8-bit data byte. Recall that a bit reversal (a change from a mark to a space or vice-versa) tells you the definitive location of a bit-cell boundary. You therefore use those for synchronization. Listing Two is the code that does that part. The value celladj is a floating-point value that indicates the fraction of a bitcell that elapses with each sample. (Yes, this could have been done with integers, but I got lazy.)

// store previous bit in preparation
handle -> previous_bit = handle -> current_bit;

// compute current bit (as above)
handle -> current_bit = (factors [0] * factors [0] + factors [1] * 
                       factors [1] > factors [2] * factors [2] + 
                       factors [3] * factors [3]);
// if there's a bit reversal
if (handle -> prevbit != handle -> current_bit) {
    // adjust cell position to be in the middle
    handle -> cellpos = 0.5;
}

// walk the cell along
handle -> cellpos += handle -> celladj;

// gone past the end of the bitcell?
if (handle -> cellpos > 1.0) {

  // compensate
  handle -> cellpos -= 1.0;

  // tell the higher level application that we have a new bit value
  bit_outcall (handle -> current_bit);
}

Listing Two

So far, you've accumulated a sequence of bits, and called bit_outcall with each bit as it arrived from the software modem. You now need to construct a software UART; something that looks for a start bit, accumulates 8 bits (ignores the stop bit), and calls a function to indicate that a completed data byte has arrived; see Listing Three. This implements a trivial state machine.

// if waiting for a start bit (0)
if (!handle -> have_start) {
    if (bit) {
        // ignore it, it's not a start bit
        return;
    }
    // got a start bit, reset
    handle -> have_start = 1;
    handle -> data = 0;
    handle -> nbits = 0;
    return;
}

// here, we have a start bit

// accumulate data
handle -> data >>= 1;
handle -> data |= 0x80 * !!bit;

handle -> nbits++;

// if we have enough...
if (handle -> nbits == 8) {
    // ship it to the higher level
    byte_outcall (handle -> data);

    // and reset for next time
    handle -> have_start = 0;
}
Listing Three

The state machine begins the operation with have_start set to zero, indicating that you have not yet seen the start bit. A start bit is defined as a zero, so while you receive 1s, you simply ignore them. Once you do get a zero, you set have_start to a 1, clear the data byte that you'll accumulate, and reset the number of bits that you've received to zero. As new bits come in, you shift them into place (using my favorite bit of obfuscated C, the "Boolean typecast operator," or "!!"), and bump the count of received bits. When the count reaches eight, you've received all of the bits you need to declare a completed byte, and you ship the completed data off to a higher level function (byte_outcall) and reset the have_start state variable back to zero to indicate that you are once again looking for a start bit.

If you are into such things, you could check to see that there is indeed a stop bit present. If there isn't a stop bit, you should declare a frame error in your serial transmission. I didn't bother for this application, but there is some commented-out code in the library that serves as a starting point. At this point, you have bytes coming from a sound card—pure magic.

What does this have to do with real time? Well, there's a lot of computations happening:

8000 x (24 x (multiply + accumulate) +

4 x multiplies + 2 accumulates) =

432,000 math operations per second

(to say nothing of the buffer management and housekeeping functions required).

So while I'd like to believe that I could do this on a late 1960s vintage PDP-8 minicomputer, the truth is that it would probably take at least an 8086-class processor to come close to the CPU requirements.

This ties in to the "real time enough" aspect. Just as certain tasks were completely out of the picture in terms of being done in software, so has the landscape changed in terms of "real-time" requirements. In 1993, I implemented the higher level caller-ID software, but relied on hardware FSK modems because it was inconceivable (to me, at least) that I could do this in software. The 1993 implementation relied on a real-time OS (QNX 4) to make sure that the CPU got allocated to the serial port handler when data arrived, and then got allocated to the high-level caller-ID software to ensure timely processing.

Today, I run the full software caller-ID package as described in this article on a free OS (FreeBSD 5.3) and I didn't even bother using the real-time scheduling features of the kernel. It barely uses enough CPU to show up as more than 0.00 percent on the CPU monitor. In short, it's "fast enough."


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.
 

Video