Software Engineering for Embedded Systems
5.3.6 Detailed Design
After we have decided upon an architecture for the system, we can work on designing the subsystems within it. Again, graphical representations can be quite useful; especially if the corresponding requirements are graphical, as in this and similar cases they can be reused.
For example, state-based system behavior is best presented graphically. Figure 5.6 shows a finite state machine design for a UART Receive data ISR (the two circles labeled “UART Rx ISR” in Figure 5.5) which recognizes certain valid NMEA-0183 messages and enqueues them for later decoding. Corrupted or unneeded messages are discarded, saving buffer space in RAM and processor time. These messages have a specific format which includes the talker type (GPS, depth sounder, etc.), the message type, multiple data fields (often with each fol- lowed by engineering units), and a checksum for error detection.
We use an FSM-plus-ISR combination for two reasons. First, we want to minimize the amount of time spent in the ISR. Structuring the problem as an FSM allows us to write code which doesn’t waste any time waiting for things to happen. The ISR is only executed when the UART has received a byte of data. Second, parsing data such as text is a good match for FSMs as it allows us to define rules to step through the different fields in a mes- sage and validate them.
Figure 5.7 shows a flowchart design for the task “Process NMEA Data” which decodes these enqueued messages and acts on the data. This code is much less time-critical, so it is implemented in a task which the scheduler runs after being notified that the RxQ holds at least one valid message. The flowchart emphasizes the consecutive and conditional pro- cessing nature of the activity: decode the message, check to see if it is valid or not, update variables, and so on.
Talker1 Sentence Type Sentence Body Checksum 1 Checksum 2 $
Append char to buf.
Any char. except *, \r or \n Append char to buf. Inc. counter
buf == $SDDBT, $VWVHW, or $YXXDR
Enqueue all chars. from buf
Any char. except * Enqueue char * Enqueue char Any char. Save as checksum 1 Any char. Save as checksum2, Increment # of msgs available *, \r or \n, or counter == 6 \r or \n Start
Figure 5.6 Finite state machine design for recognizing NMEA-0183 depth below transducer, heading and speed through water, and battery voltage messages.
Start Decode NMEA Sentence Invalid? Update Skip? Write to Data Flash Update End RxQ Data In Data Out Data Out CurPos MemUsed Yes Yes No No
There are various other representations available beyond these two workhorses, so we direct the reader other texts (see Chapter 12 and Chapter 13 of Koopman).
5.3.7 Implementation
Now that we have a detailed design it should be straightforward to proceed to the actual im- plementation of the code. C is the dominant programming language for embedded systems, followed by C⫹⫹ and assembly language. C is good enough for the job, even though there are some languages which are much safer. The risk with C is that it allows the creation of all sorts of potentially unsafe code. Ganssle identifies “The Use of C” (or “The Misuse of C”) as reason eight for projects getting into trouble. There are a few points to keep in mind as you develop the code:
䡲 Three fundamental principles should guide your decisions when implementing code: simplicity, generality, and clarity (Kernighan and Pike).
▫ Simplicity is keeping the programs and functions short and manageable.
▫ Generality is designing functions that can work, in a broad sense, for a variety of situations and that require minimum alterations to suit a task.
▫ Clarity is the concept of keeping the program easy to understand, while re- maining technically precise.
䡲 Code should conform to your company’s coding standards to ensure that it is easy to read and understand. See Koopman’s Chapter 17 for more information on why coding style matters. There are many coding standards examples available (Ganssle, J.). The rules we have seen broken most often, and which have the biggest payoff, are these:
▫ Limit function length to what fits onto one screen or page.
▫ Use meaningful and consistent function and variable naming conventions.
▫ Avoid using global variables.
▫ If you find yourself writing the same code with minor variations, it may be worth parameterizing the code so only one version is needed.
䡲 Data sharing in systems with preemption (including ISRs) must be done very care- fully to avoid data race vulnerabilities. See Chapter 12 of this text and Koopman’s Chapters 19 and 20 for more details.
䡲 Static analysisshould be used to ensure that your code is not asking for trouble. Most (if not all) compiler warnings should be turned on to identify potential bugs lurking in your source code. Tools such as LINT are also helpful and worth considering.
䡲 Magic numbers are hard-coded numeric literals used as constants, array sizes,
character positions, conversion factors, and other numeric values that appear di- rectly in programs. They complicate code maintenances because if a magic num- ber is used in multiple places, and if a change is needed, then each location must
be revised and it is easy to forget about one. Similarly, some magic numbers may depend on others (e.g., a time delay may depend on the MCU’s clock rate). It is much better to use a const variable or a preprocessor #define to give the value a meaningful name which can be used where needed.
䡲 It is important to track available design margin as you build the system up. How much RAM, ROM, and nonvolatile memory are used? How busy is the CPU on average? How close is the system to missing deadlines? The Software Gas Law states that software will expand to fill all available resources. Tracking the resource use helps give an early warning. For more details, see Koopman’s discussion on the cost of nearly full resources.
䡲 Software configuration management should be used as the code is developed
and maintained to support evolving code and different configurations as well.