We will illustrate the process of dividing a software task into modules with an abstract but realistic example. The overall goal of the example shown in Figure 7.2 is to sample data using an ADC, perform calculations on the data, and output results. The organic light emitting diode (OLED) could be used to display data to the external world. Notice the typical format of an embedded system in that it has some tasks performed once at the beginning, and it has a long sequence of tasks performed over and over. The
structure of this example applies to many embedded systems such as a diagnostic medical instrument, an intruder alarm system, a heating/AC controller, a voice recognition module, automotive emissions controller, or military surveillance system. The left side of Figure 7.2 shows the complex software system defined as a linear sequence of ten steps, where each step represents many lines of assembly code. The linear approach to this program follows closely to linear sequence of the processor as it executes instructions. This linear code, however close to the actual processor, is difficult to understand, hard to debug, and impossible to reuse for other projects. Therefore, we will attempt a modular approach considering the issues of functional abstraction, complexity abstraction, and portability in this example.
The modular approach to this problem divides the software into three modules containing seven subroutines. In this example, assume the sequence Step4-Step5-Step6 causes data to be sorted. Notice that this sorting task is executed twice.
Figure 7.2. A complex software system is broken into three modules containing seven subroutines.
Functional abstraction encourages us to create a Sort subroutine allowing us to write the software once, but execute it from different locations. Complexity abstraction encourages us to organize the ten-step software into a main program with multiple modules, where each module has multiple subroutines.
For example, assume the assembly instructions in Step1 cause the ADC to be initialized. Even though this code is executed only once, complexity abstraction encourages us to create an ADC_Init subroutine so the system is easier to understand and easier to debug. In a similar way assume Step2 initializes the OLED port, Step3 samples the ADC, the sequence Step7-Step8 performs an average, and Step10 outputs to the OLED. Therefore, each well-defined task is defined as a separate subroutine. The subroutines are then grouped into modules. For example, the ADC module is a collection of subroutines that operate the ADC. The complex behavior of the ADC is now abstracted into two easy to understand tasks: turn it on, and use it. In a similar way, the OLED module includes all functions that access the OLED. Again, at the abstract level of the main program, understanding how to use the OLED is a matter knowing we first turn it on then we transmit data. The math module is a collection of subroutines to perform necessary calculations on the data. In this example, we assume sort and average will be private subroutines, meaning they can be called only by software within the math module and not by software outside the module. Making private subroutines is an example of “information hiding”, separating what the module does from how the module works. When we port a system, it means we take a working system and redesign it with some minor but critical change. The OLED device is used in this system to output results. We might be asked to port this system onto a device that uses an LCD in place of the OLED for its output. In this case, all we need to do is design, implement and test an LCD module with
two subroutines LCD_Init and LCD_Out that function in a similar manner as the existing OLED routines. The modular approach performs the exact same ten steps in the exact same order. However, the modular approach is easier to debug, because first we debug each subroutine, then we debug each module, and finally we debug the entire system. The modular approach clearly supports code reuse. For example, if another system needs an ADC, we can simply use the ADC module software without having to debug it again.
Observation: When writing modular code, notice its two-dimensional aspect. Down the y-axis still
represents time as the program is executed, but along the x-axis we now visualize a functional block diagram of the system showing its data flow: input, calculate, output.
7.5. Making Decisions
The previous section presented fundamental concepts and general approaches to solving problems on the computer. In the subsequent sections, detailed implementations will be presented.
7.5.1. Conditional if-then Statements
Decision making is an important aspect of software programming. Two values are compared and certain blocks of program are executed or skipped depending on the results of the comparison. In assembly language it is important to know the precision (e.g., 8-bit, 16-bit, 32-bit) and the format of the two values (e.g., unsigned, signed). It takes three steps to perform a comparison. You begin by reading the first value into a register. If the second value is not a constant, it must be read into a register, too. The second step is to compare the first value with the second value. You can use either a subtract instruction with the S (SUBS) or a compare instruction (CMP CMN). The CMP CMN SUBS instructions set the condition code bits. The last step is a conditional branch.
Observation: Think of the three steps 1) bring first value into a register, 2) compare to second value, 3) conditional branch, bxx (where xx is eq ne lo ls hi hs gt ge lt or le). The branch will occur if (first is xx second).
In Programs 71 and 7.2, we assume G is a 32-bit unsigned variable. Program 7.1 contains two separate if-then structures involving testing for equal or not equal. It will call GEqual7 if G equals 7, and GNotEqual7 if G does not equal 7. When testing for equal or not equal it doesn’t matter whether the numbers are signed or unsigned. However, it does matter if they are 8-bit or 16-bit. To convert these examples to 16 bits, use the LDRH R0,[R2] instruction instead of the LDR R0,[R2] instruction. To convert these examples to 8 bits, use the LDRB R0,[R2] instruction instead of the LDR R0,[R2]
Program 7.1. Conditional structures that test for equality (this works with signed and unsigned numbers).
When testing for greater than or less than, it does matter whether the numbers are signed or unsigned.
Program 7.2 contains four separate unsigned if-then structures. In each case, the first step is to bring the first value in R0; the second step is to compare the first value with a second value; and the third step is to execute an unsigned branch Bxx. The branch will occur if the first unsigned value is xx the second unsigned value. GLess7 if G is less than 7, and GLessEq7 if G is less than or equal to 7. When comparing unsigned values, the instructions BHI BLO BHS and BLS should follow the subtraction or comparison instruction.
A conditional if-then is implemented by bringing the first number in a register, subtracting the second number, then using the branch instruction with complementary logic to skip over the body of the if-then.
To convert these examples to 16 bits, use the LDRH R0,[R2] instruction instead of the LDR R0,[R2] instruction. To convert these examples to 8 bits, use the LDRB R0,[R2] instruction instead of the LDR R0,[R2] instruction.
Example 7.1. Assuming G1 is 8-bit unsigned, write software that sets G1=50 if G1 is greater than 50. In other words, we will force G1 into the range 0 to 50
Solution: First, we draw a flowchart describing the desired algorithm, see Figure 7.3. Next, we restate the conditional as “skip over if G1 is less than or equal to 50”. To implement the assembly code we bring G1 into Register R0 using LDRB to load an unsigned byte, subtract 50, then branch to next if G1 is less than or equal to 50, as presented in Program 7.3. We will use an unsigned conditional branch because the data format is unsigned.
Figure 7.3. Flowchart of an if-then structure.
Program 7.3. An unsigned if-then structure. LDRB used because 8-bit, BLS used because it is unsigned.
Checkpoint 7.5: Assume you have an 8-bit unsigned global variable N. Write C code that executes the function isTen, if N is equal to 10.
Checkpoint 7.6: Assume H1 and H2 are two 16-bit unsigned variables. Write C code that executes the