A sufficiently high level of technology is indistinguishable from magic.
—Arthur C. Clarke
In this chapter, I'll attempt to bring all of the elements we've discussed so far together into a complete embedded application. I don't have much new material to add to the discussion at this point, so the body of the chapter is mainly a description of the code presented herein. My goal is to describe the structure of this application and its source code in such a way that there is no magic remaining for you. You should leave this chapter with a complete understanding of the example program and the ability to develop useful embedded applications of your own.
The application we're going to discuss is not much more complicated than the "Hello, World!" example found in most other programming books. It is a testament to the complexity of embedded software development that this example comes near the end of this book, rather than at its beginning. We've had to gradually build our way up to the computing platform that most books, and even high-level language compilers, take for granted.
Once you're able to write the "Hello, World!" program, your embedded platform starts to look a lot like any other programming environment. The hardest parts of the embedded software development process—familiarizing yourself with the hardware, establishing a software development process for it, and interfacing to the individual hardware devices—are behind you. You are finally able to focus your efforts on the algorithms and user interfaces that are specific to the product you're developing. In many cases, these higher-level aspects of the program can be developed on another computer platform, in parallel with the lower-level embedded software development we've been discussing, and merely ported to the embedded system once both are complete.
Figure 9-1 contains a high-level representation of the "Hello, World!" application. This application includes three device drivers, the ADEOS operating system, and two ADEOS tasks. The first task toggles the Arcom board's red LED at a rate of 10 Hz. The second prints the string "Hello, World!" at 10 second intervals to a host computer or dumb terminal connected to one of the board's serial ports.
In addition to the two tasks, there are three device drivers shown in the figure. These control the Arcom board's LEDs, timers, and serial ports, respectively. Although it is customary to draw device drivers below the operating system, I chose to place these three on the same level as the operating system to emphasize that they actually depend more on ADEOS than it does on them. In fact, the embedded operating system doesn't even know (or care) that these drivers are present in the system. This is a common feature of the device drivers and other hardware-specific software in an embedded system.
The implementation of main is shown below. This code simply creates the two tasks and starts the operating system's scheduler. At such a high level the code should speak for itself. In fact, we've already discussed a similar code listing in the previous chapter.
#include "adeos.h" void flashRed(void); void helloWorld(void); /* * Create the two tasks. */ Task taskA(flashRed, 150, 512); Task taskB(helloWorld, 200, 512); /********************************************************************* * * Function: main() * * Description: This function is responsible for starting the ADEOS * scheduler only. * * Notes: * * Returns: This function will never return! * *********************************************************************/ void main(void) { os.start(); // This point will never be reached. } /* main() */
As I said earlier, one of two things this application does is blink the red LED. This is done by the code shown below. Here the function flashRed is executed as a task. However, ignoring that and the new function name, this is almost exactly the same Blinking LED function we studied in Chapter 7. The only differences at this level are the new frequency (10 Hz) and LED color (red).
#include "led.h" #include "timer.h" /********************************************************************** * * Function: flashRed() * * Description: Blink the red LED ten times a second. * * Notes: This outer loop is hardware-independent. However, it * calls the hardware-dependent function toggleLed(). * * Returns: This routine contains an infinite loop. * **********************************************************************/ void flashRed(void) { Timer timer; timer.start(50, Periodic); // Start a periodic 50ms timer. while (1) { toggleLed(LED_RED); // Toggle the red LED. timer.waitfor(); // Wait for the timer to expire. } } /* flashRed() */
The most significant changes to the Blinking LED program are not visible in this code. These are changes made to the toggleLed function and the Timer class to make them compatible with a multitasking environment. The toggleLed function is what I am now calling the LED driver. Once you start thinking about it this way, it might make sense to consider rewriting the driver as a C++ class and add new methods to set and clear an LED explicitly. However, it is sufficient to leave our implementation as it was in Chapter 7 and simply use a mutex to protect the P2LTCH register from simultaneous access by more than one task.[1]
Here is the modified code:
#include "i8018xEB.h" #include "adeos.h" static Mutex gLedMutex; /********************************************************************** * * Function: toggleLed() * * Description: Toggle the state of one or both LEDs. * * Notes: This version is ready for multitasking. * * Returns: None defined. * **********************************************************************/ void toggleLed(unsigned char ledMask) { gLedMutex.take(); // Read P2LTCH, modify its value, and write the result. // gProcessor.pPCB->ioPort[1].latch ^= ledMask; gLedMutex.release(); } /* toggleLed() */
A similar change must be made to the timer driver from Chapter 7 before it can be used in a multitasking environment. However, in this case there is no race condition.[2] Rather, we need to use a mutex to eliminate the polling in the waitfor method. By associating a mutex with each software timer, we can put any task that is waiting for a timer to sleep and, thereby, free up the processor for the execution of lower-priority ready tasks. When the awaited timer expires, the sleeping task will be reawakened by the operating system.
Toward this end, a pointer to a mutex object, called pMutex, will be added to the class definition:
class Timer { public: Timer(); ~Timer(); int start(unsigned int nMilliseconds, TimerType = OneShot); int waitfor(); void cancel(); TimerState state; TimerType type; unsigned int length; Mutex * pMutex; unsigned int count; Timer * pNext; private: static void interrupt Interrupt(); };
This pointer is initialized each time a software timer is created by the constructor. And, thereafter, whenever a timer object is started, its mutex is taken as follows:
/********************************************************************** * * Method: start() * * Description: Start a software timer, based on the tick from the * underlying hardware timer. * * Notes: This version is ready for multitasking. * * Returns: 0 on success, -1 if the timer is already in use. * **********************************************************************/ int Timer::start(unsigned int nMilliseconds, TimerType timerType) { if (state != Idle) { return (-1); } // // Take the mutex. It will be released when the timer expires. // pMutex->take(); // // Initialize the software timer. // state = Active; type = timerType; length = nMilliseconds / MS_PER_TICK; // // Add this timer to the active timer list. // timerList.insert(this); return (0); } /* start() */
By taking the mutex when the timer is started, we guarantee that no task (not even the one that started this timer) will be able to take it again until the same mutex is released. And that won't happen until either the timer expires naturally (via the interrupt service routine) or the timer is canceled manually (via the cancel method). So the polling loop inside waitfor can be replaced with pMutex->take(), as follows:
/********************************************************************** * * Method: waitfor() * * Description: Wait for the software timer to finish. * * Notes: This version is ready for multitasking. * * Returns: 0 on success, -1 if the timer is not running. * **********************************************************************/ int Timer::waitfor() { if (state != Active) { return (-1); } // // Wait for the timer to expire. // pMutex->take(); // // Restart or idle the timer, depending on its type. // if (type == Periodic) { state = Active; timerList.insert(this); } else { pMutex->release(); state = Idle; } return (0); } /* waitfor() */
When the timer does eventually expire, the interrupt service routine will release the mutex and the calling task will awake inside waitfor. In the process of waking, the mutex will already be taken for the next run of the timer. The mutex need only be released if the timer is of type OneShot and, because of that, not automatically restarted.
The other part of our example application is a task that prints the text string "Hello, World!" to one of the serial ports at a regular interval. Again, the timer driver is used to create the periodicity. However, this task also depends on a serial port driver that we haven't seen before. The guts of the serial driver will be described in the final two sections of this chapter, but the task that uses it is shown here. The only thing you need to know about serial ports to understand this task is that a SerialPort is a C++ class and that the puts method is used to print a string of characters from that port.
#include "timer.h" #include "serial.h" /********************************************************************** * * Function: helloWorld() * * Description: Send a text message to the serial port periodically. * * Notes: This outer loop is hardware-independent. * * Returns: This routine contains an infinite loop. * **********************************************************************/ void helloWorld(void) { Timer timer; SerialPort serial(PORTA, 19200L); timer.start(10000, Periodic); // Start a periodic 10 s timer. while (1) { serial.puts("Hello, World!"); // Output a simple text message. timer.waitfor(); // Wait for the timer to expire. } } /* helloWorld() */
Though the periodicity has a different length, the general structure of this task is the same as that of the flashRed function. So, the only thing left for us to discuss is the makeup of the serial port driver. We'll start with a description of a generalized serial ports interface and then finish with the specifics of the serial controller found on the Arcom board.
At the application level, a serial port is simply a bidirectional data channel. This channel is usually terminated on each end with a hardware device called a serial communications controller (SCC). Each serial port within the SCC—there are usually at least two serial ports per controller—is connected to the embedded processor on one side and to a cable (or the connector for one) on the other side. At the other end of that cable there is usually a host computer (or some other embedded system) that has an internal serial communications controller of its own.
Of course, the actual purpose of the serial port is application-dependent. But the general idea is this: to communicate streams of data between two intelligent systems or between one such device (the target) and a human operator. Typically, the smallest unit of data that can be sent or received over a serial port is an 8-bit character. So streams of binary data need to be reorganized into bytes before transmission. This restriction is similar to that of C's stdio library, so it makes sense to borrow some programming conventions from that interface.
In order to support serial communications and emulate a stdio-style interface, I've defined the SerialPort class as it is shown below. This class abstracts the application's use of the serial port as bidirectional data channel and makes the interface as similar as possible to what we've all seen before. In addition to the constructor and destructor, the class includes four methods—putchar,[3] puts, getchar, and gets —for sending characters and strings of characters and receiving the same. These routines are defined exactly as they would be in any ANSI C-compliant version of the header file stdio.h.
Here's the actual class definition:
#include "circbuf.h" #define PORTA 0 #define PORTB 1 class SerialPort { public: SerialPort(int port, unsigned long baudRate = 19200L, unsigned int txQueueSize = 64, // Transmit Buffer Size unsigned int rxQueueSize = 64); // Receive Buffer Size ~SerialPort(); int putchar(int c); int puts(const char *s); int getchar(); char * gets(char *s); private: int channel; CircBuf * pTxQueue; // Transmit Buffer CircBuf * pRxQueue; // Receive Buffer };
Note the private data members channel, pTxQueue, and pRxQueue. These are initialized within the constructor and used to interface to the hardware-specific part of the serial driver described in the next section. I'll have more to say about this interface shortly, but for now just be aware that the SerialPort class does not contain any code that is specific to a particular Serial Controller. All of that is hidden inside the SCC class that it references.
Let's take a look at the SerialPort constructor. This routine is responsible for initializing the three private data members and configuring the requested data channel within the SCC hardware:
#include "scc.h" static SCC scc; /********************************************************************** * * Method: SerialPort() * * Description: Default constructor for the serial port class. * * Notes: * * Returns: None defined. * **********************************************************************/ SerialPort::SerialPort(int port, unsigned long baudRate, unsigned int txQueueSize, unsigned int rxQueueSize) { // // Initialize the logical device. // switch (port) { case PORTA: channel = 0; break; case PORTB: channel = 1; break; default: channel = -1; break; } // // Create input and output FIFOs. // pTxQueue = new CircBuf(txQueueSize); pRxQueue = new CircBuf(rxQueueSize); // // Initialize the hardware device. // scc.reset(channel); scc.init(channel, baudRate, pTxQueue, pRxQueue); } /* SerialPort() */
Once a SerialPort object has been created, the aforementioned methods for sending and receiving data can be used. For example, in the helloWorld function shown earlier, puts("Hello, World!") is the statement that sends the text string to serial port A (a.k.a. SCC channel 0). The data is sent over the serial channel at a rate of 19,200 bits per second, as selected by the baudRate parameter to the SerialPort constructor.
The send and receive methods rely on the circular buffers pointed to by pTxQueue and pRxQueue, respectively. pTxQueue is a transmit buffer that provides overflow memory in case the rate at which characters are sent by the application exceeds the baud rate of the channel. This usually happens in short spurts, so it is expected that the transmit buffer won't usually fill up all the way. Similarly, the receive buffer, pRxQueue, provides overflow memory for bytes that have been received at the serial port but not yet read by the application. By default, the above constructor creates each of these as 64-byte buffers. However, these sizes can be set to smaller or larger values, depending on the needs of your application, simply by overriding the default arguments to the constructor.
The implementations of the send methods putchar and puts are shown below. In putchar we start by checking if the transmit buffer is already full. If so, we return an error indication to the caller, so he will know that the character was not sent. Otherwise, we add the new character to the transmit buffer, ensure that the SCC transmit engine is running, and return success. The puts method makes a series of calls to putchar, one for each character in the string and then adds a newline character at the end.
/********************************************************************** * * Method: putchar() * * Description: Write one character to the serial port. * * Notes: * * Returns: The transmitted character is returned on success. * -1 is returned in the case of an error. * **********************************************************************/ int SerialPort::putchar(int c) { if (pTxQueue->isFull()) { return (-1); } // // Add the character to the transmit FIFO. // pTxQueue->add((char) c); // // Start the transmit engine (if it's stalled). // scc.txStart(channel); return (c); } /* putchar() */ /********************************************************************** * * Method: puts() * * Description: Copies the null-terminated string s to the serial * port and appends a newline character. * * Notes: In rare cases, this function may return success though * the newline was not actually sent. * * Returns: The number of characters transmitted successfully. * Otherwise, -1 is returned to indicate error. * **********************************************************************/ int SerialPort::puts(const char * s) { const char * p; // // Send each character of the string. // for (p = s; *p != '\0'; p++) { if (putchar(*p) < 0) break; } // // Add a newline character. // putchar('\n'); return ((p - s) + 1); } /* puts() */
The receive method getchar is similar to putchar. It starts by checking if the receive buffer is empty. If so, an error code is returned. Otherwise, one byte of data is removed from the receive buffer and returned to the caller. The gets method calls getchar repeatedly until either a newline character is found or there is no more data available at the serial port. It then returns whatever string was found up to that point. The code for both of these methods follows:
/********************************************************************** * * Method: getchar() * * Description: Read one character from the serial port. * * Notes: * * Returns: The next character found on this input stream. * -1 is returned in the case of an error. * **********************************************************************/ int SerialPort::getchar(void) { int c; if (pRxQueue->isEmpty()) { return (-1); // There is no input data available. } int rxStalled = pRxQueue->isFull(); // // Read the next byte out of the receive FIFO. // c = pRxQueue->remove(); // // If the receive engine is stalled, restart it. // if (rxStalled) { scc.rxStart(channel); } return (c); } /* getchar() */ /********************************************************************** * * Method: gets() * * Description: Collects a string of characters terminated by a new- * line character from the serial port and places it in s. * The newline character is replaced by a null character. * * Notes: The caller is responsible for allocating adequate space * for the string. * * Warnings: This function does not block waiting for a newline. * If a complete string is not found, it will return * whatever is available in the receive queue. * * Returns: A pointer to the string. * Otherwise, NULL is returned to indicate an error. * **********************************************************************/ char * SerialPort::gets(char * s) { char * p; int c; // // Read characters until a newline is found or no more data. // for (p = s; (c = getchar()) != '\n' && c >= 0; p++) { *p = c; } // // Terminate the string. // *p = '\0'; return (s); } /* gets() */
The two serial ports on the Arcom board are part of the same Zilog 85230 Serial Communications Controller. This particular chip is, unfortunately, rather complicated to configure and use. So, rather than fill up the SerialPort class shown earlier with device-specific code, I decided to divide the serial driver into two parts. The upper layer is the class we have just discussed. This upper layer will work with any two-channel SCC that provides byte-oriented transmit and receive interfaces and configurable baud rates. All that is necessary is to implement a device-specific SCC class (the lower layer described next) that has the same reset, init, txStart, and rxStart interfaces as those called from the SerialPort class.
In fact, one of the reasons the Zilog 85230 SCC device is so difficult to configure and use is that it has many more options than are really necessary for this simple application. The chip is capable of sending not only bytes but also characters that have any number of bits up to 8. And in addition to being able to select the baud rate, it is also possible to configure many other features of one or both channels and to support a variety of other communication protocols.
Here's how the SCC class is actually defined:
#include "circbuf.h" class SCC { public: SCC(); void reset(int channel); void init(int channel, unsigned long baudRate, CircBuf * pTxQueue, CircBuf * pRxQueue); void txStart(int channel); void rxStart(int channel); private: static void interrupt Interrupt(void); };
Notice that this class also depends upon the CircBuf class. The pTxQueue and pRxQueue arguments to the init method are used to establish the input and output buffers for that channel. This makes it possible to link a logical SerialPort object with one of the physical channels within the SCC device. The reason for defining the init method separately from the constructor is that most SCC chips control two or more serial channels. The constructor resets them both the first time it is called. Then, init is called to set the baud rate and other parameters for a particular channel.
Everything else about the SCC class is an internal feature that is specific to the Zilog 85230 device. For that reason, I have decided not to list or explain this rather long and complex module within the book. Suffice it to say that the code consists of macros for reading and writing the registers of the device, an interrupt service routine to handle receive and transmit interrupts, and methods for restarting the receive and transmit processes if they have previously stalled while waiting for more data. Interested readers will find the actual code in the file scc.cpp.
[1] There is a race condition within the earlier toggleLed functions. To see it, look back at the code and imagine that two tasks are sharing the LEDs and that the first task has just called that function to toggle the red LED. Inside toggleLed, the state of both LEDs is read and stored in a processor register when, all of the sudden, the first task is preempted by the second. Now the second task causes the state of both LEDs to be read once more and stored in another processor register, modified to change the state of the green LED, and the result written out to the P2LTCH register. When the interrupted task is restarted, it already has a copy of the LED state, but this copy is no longer accurate! After making its change to the red LED and writing the new state out to the P2LTCH register, the second task's change will be undone. Adding a mutex eliminates this potential.
[2] Recall that the timer hardware is initialized only once—during the first constructor invocation—and thereafter, the timer-specific registers are only read and written by one function: the interrupt service routine.
[3] You might be wondering why this method accepts an integer argument rather than a character. After all, we're sending 8-bit characters over the serial port, right? Well, don't ask me. I'm just trying to be consistent with the ANSI C library standard and wondering the very same thing myself.