MSP430 Example: Morse Code Blinker with Delay Loop

As the first step down the path of building a QRP beacon controller, we will examine an MSP430 Morse code blinker application. This application blinks a static string on a development board LED, using a delay loop for timing. As previously discussed in the slow timer example, delay loops are a rather horrible way to do timing if you have any concern for power consumption; therefore, a future example will show how to convert the delay loop to a hardware timer with deep sleep between state changes.

Note: The ASCII-to-Morse conversion used for this application is the ASCII-to-Morse encoding specified here. Please refer to the encoding description to understand how the decode works.

This example should compile and run on any MSP430 development board which has an LED on P1.0. This includes both the ez430-f2012/f2013 and the Launchpad.

Example code

Example source code to go with this tutorial can be found in the Mercurial repository msp430-examples/morse_spin on this server. The salient C file, morse_spin.c is available for individual download while reading this article. Details not relevant to the encoding and delay operationst are left out of this article, so the source (or other examples on this site, as they are filled in) should be consulted to clear these up.

The delay loop

As previously mentioned, timing for this example is provided by a simple delay loop. A delay loop is a bit of code which causes a processor to do "nothing" for a specified period of time, usually by performing some innocuous operation such as adding zero to a register. In the case of the MSP430, several possible "no operation" (NOP) codes are specified in the processor documentation, but the standard NOP code is the single-cycle instruction:

MOV #0, R3

At first glance, this seems a little strange — wouldn't storing 0 to R3 have detrimental effects on our program? However, further examination of the documentation reveals that R3 is "Constant Generator Register CG2", which is always read as a constant specified by the addressing mode of the read. Thus stores to R3 have no effect, and we achieve a single cycle NOP.

Now that we have a delaying instruction, we can write a delay loop function, which delays for a specified period of time:

void delay(unsigned int d) {
    for (; d > 0; d--) {
       nop();
       nop();
    }
}

To know precisely how long this function delays, we need to know a few things:

  1. Whether or not the function is inlined at the call point in which we are interested.
  2. How long a function call takes, or if the function is inlined, how long any register saving and setup it performs will take.
  3. What instructions are used to implement the for loop.
  4. How long the body of the for loop takes to execute.

We already know the answer to the last question, from the processor documentation; two NOP instructions will take two clock cycles. The answers to the other question require knowledge about how the compiler implemented the delay function, and precise timing will generally involve either writing the delay loop in assembler or opening up the executable to examine the compiler output. These considerations are among the many reasons that hardware timer-based delays are generally preferable to delay loops.

Regardless of the precise compiler implementation, it is clear that this delay function will take O(d) cycles to complete, with the cost of function/inline setup disappearing for nontrivial values of d. We can assume that the for loop implementation takes at least one clock cycle per iteration, and no more than a small number of cycles; 4*d clock cycles per iteration is a likely ballpark for this loop.

Morse code timings

If you are unfamiliar with International Morse Code, the rules are relatively simple. Note, however, that there have been many "Morse" codes over the years, and they differ in details. The Morse code we're using here is the International Morse used by the US and international Amateur Radio community.

In Morse code, each character is made up of a series of "dots" and "dashes", which we tend to call "dits" and "dahs", respectively. Dits represent a short tone, and dahs a longer tone, with short pauses between dits and dahs within a letter and longer pauses between letters. In International Morse code, all timings are based on integral multiple of the length of a dit.

Suppose that a dit is 1 time unit in length. The intra-character spacing (that is, the time between dits and dahs within a letter) is then also 1 time unit in length. A dah becomes 3 time units, and the inter-character spacing (the time between letters) is also 3 time units in length. The space between words is seven time units long.

Our encoding (specified here) is not capable of expressing the seven-dit space between words without extra work, so our space between words fudges things a bit and uses six time units instead. (This is a natural fallout of expressing a space character as a character containing no symbols; the three unit delay before and after the "empty" character run together into a six unit delay.)

For the sake of this example, we will define the following delay constants:

#define DELAY_DIT 0x5555
#define DELAY_DAH 0xFFFF

Determining the speed of the Morse code generated in WPM requires a bit more (trivial) computation, which we leave as an exercise for the reader. Note that the above delay constants represent the slowest code which can be flashed on the 16-bit MSP430 without increasing the number of NOP instructions in our delay loop.

Encoding ASCII to Morse

Now that we have a mapping table which maps ASCII characters to their Morse equivalents and a timing for Morse characters, we can implement the Morse encoder. The encoder will loop through a string, encoding each character of the string in turn until the end of the string is reached.

for (i = 0; str[i]; i++) {
    int inchar = 0;
    unsigned int j;
    for (j = 0; j < 8; j++) {
        int bit = morse_ascii[(int)str[i]] & (0x80 >> j);
        if (inchar) {
            P1OUT = 1;
            delay(bit ? DELAY_DAH : DELAY_DIT);
            P1OUT = 0;
            if (j < 7) {
                delay(DELAY_DIT);
            }
        } else if (bit) {
            inchar = 1;
        }
    }
    delay(DELAY_DAH);
}

Let's step through this loop one line at a time. The enclosing for loop is straightforward; C strings are terminated by a byte containing the value 0, so we start at the beginning of the string and continue until we reach that byte.

The inchar variable will indicate whether we are "in a character" or stepping through its padding (see the Morse encoding for details). When it is false, we are processing padding; when true, we are processing character data.

The inner loop processes each bit of the current character in turn. Its first line extracts the bit of interest and stores it in the variable bit:

int bit = morse_ascii[(int)str[i]] & (0x80 >> j);

The only tricky part of this statement is the mask; because j iterates from 0 to 7 (and not 7 to 0), we shift the constant 0x80 (or the most-significant bit of an 8-bit character) to the right, rather than shifting a 1 bit to the left, which is perhaps more idiomatic. Decisions such as which way to count a for loop are normally style decisions, although they may have impact on comparison instructions in some cases; in this case, we chose to iterate in the logical direction of the stored data bits. Once the bit of interest is selected (0x80 >> j), the current character of the string (str[i]) is masked with this bit to yield a boolean value bit. Note that bit may be either a zero or any power of two from 20 to 27; because we use it only as a boolean, this doesn't matter.

Next, we switch on the inchar variable. If it is false (as it is at the beginning of every character), we follow the else clause, and do nothing if bit is zero, and set inchar = 1 if bit is nonzero. This follows from our encoding definition. Recall that a character is preceded by zero or more 0 bits, followed by a 1 bit, followed by the character data; this manipulation of inchar will cause the loop to skip the leading zero bits and the first 1 bit it encounters.

If inchar is true, the magic happens. In that case, we must generate an LED flash of either dit- or dah-length. To do this, we light the LED, delay for the appropriate length of time, and then extinguish the LED:

P1OUT = 1;
delay(bit ? DELAY_DAH : DELAY_DIT);
P1OUT = 0;

If you are unfamiliar with the construction used as the argument to the delay function, it is called the ternary operator, and it is a succinct way to represent a specific if-then-else construction; if the condition before the ? is true, then the value of the expression is the value between ? and :. If it is false, then the value of the expression is the value following the :. For more information on this and other C constructions, see the excellent book The C Programming Language. In this case, we delay for a dah length if the bit is set, and a dit length if the bit is clear.

Following this LED blink, the code inserts an intra-character delay (dit-length) for every dit and dah except the final dit or dah of the character. For the final dit or dah, no delay is inserted the inter-character dah-length delay at the bottom of the outer loop will be inserted before moving to the next character in the string.

Applicability and improvements

By merely varying the action taken at the beginning and end of each dit or dah, or even just attaching something other than an LED to P1.0, this encoder can perform virtually any action to the beat of Morse code. If P1.0 were the PTT line for a QRP beacon, for example, this application could key out a custom beacon message with no change!

As it stands, this code has a fair number of limitations. The biggest limitation is its power consumption. Since the processor must execute an instruction every single cycle to achieve the proper timings, no sleep states can be entered, the high-speed oscillator must be enabled throughout, and power consumption is very high. In addition, the granularity and control of timing are rather poor, unless the delay function is made somewhat more complicated. Wall-clock delays are dictated by the speed of the high-speed oscillator driving the processor clock, and the precise code emitted by the compiler.

Many, if not all, of these limitations can be removed by moving to an interrupt-driven delay architecture using hardware timers. A future article in this series will explain how to do just that.