Software Design

1.Introduction

As the size and complexity of the software increase, software development changes from simple "coding" to 
"software engineering"...so engineering skills require to design complex systems which include modular design,
layered architecture, abstraction, and verification.

Good engineers employ well-defined processes when developing complex systems. When we work within a structured 
framework, it is easier to prove our system works (verification) and to modify our system in the future 
(maintenance).

The only hope for success in a large software system will be to break it into simple modules. The software will 
be easy to test, and easy to change. Golden Rule of sw development: Write software for others as you wish they 
would write for you.

The easiest way to debug is to write a software without any bugs. Test it now : when we find a bug, fix it 
immediately. We should completely test each module individually, before combining them into a larger system. 
We should not add new features before we are convinced the existing features are bug-free. It is better to 
have some parts of the system that run 100% reliability than to have the entire system with bugs.
Pay attention to warnings...

2.Quality programming 

There are two categories of performance criteria with which we evaluate the "goodness" of our software. 
Quantitative criteria include dynamic efficiency (speed of execution), static efficiency (ROM and RAM program 
size), and accuracy of the results. Qualitative criteria center on ease of understanding. If you software is 
easy to understand then it will be :
  . Easy to debug, including both finding and fixing mistakes.
  . Easy to verify, meaning we can prove it is correct.
  . Easy to maintain, meaning we can add new features.

3. Software style guide

Organization of a code file

The Code file should be organized in the following order :

a) Comments
   - Name of the file.
   - The Overall purpose of the software module.
   - The names of the programmers.
   - The creation and last update dates.
   - The Hardware/Software configuration required to use the module.
   - Copyright information.

b) including .h files

   - putting them all together at the top will help us to draw a call graph, which will show us how our modules
     are connected. IN PARTICULAR, IF WE CONSIDER EACH CODE FILE TO BE A SEPARATE MODULE, then the list of
     #includes statements specifies which other modules can be called from this module.
   - We should avoid having one header file include other header files.

c) extern references

     (The keyword extern tells the compiler that this is not a definition for this variable or function as it’s 
     already defined in some other file. This way compiler will let you use the same variable in multiple files).

   - After including the header files, we can declare any external variables or functions.
     External references will be resolved by the linker, when various modules are linked together to create a
     single executable application. Placing them together at the top of the file will help us see how this 
     software system fits together (i.e., is linked to) other systems. 

d) #define statements

   #define macros can define operations or constants. Since the definitions are placed in the code file
   (e.g.,file.c), they will be private. This mean they are available within this file only. If we wish to 
   create public macros, then we place them in the header file for this module.
e) struct union enum statments
    
   Again, if these definitions are located in the code file(e.g.,file.c), they will be private.

f) Global variables and constants
  
   After the structure definitions, we should include the global variables and constants.

   We can specify where the data is allocated. If it is a variable that needs to exist permanently, 
   we will place it in RAM as a global variable. If it is a constant that needs to exist permanently, 
   we will place it in ROM using const. If the data is needed temporarily, we can define it as local. 
   The compiler will allocate locals in registers or on the stack in whichever way is most efficient.

   You can (and should) see more details on page https://bluetechs.wordpress.com/zothers/x/data/ 
  
g) Prototype of private functions

   Just like global variables, we can restrict access to private functions by defining them as static.
   Prototypes for the public functions will be included in the corresponding header file.
   Although not necessary we will include the parameter names with the prototypes. Descriptive parameter 
   names will help document the usage of the function.

   static void plot(int16_t,int16_t) ;
   static void plot(int16_t time, int16_t pressure) ;   // more explicit

h) Implementations of the functions

   Private functions should be defined as static. 
   The functions should be sequenced in a logical manner...
   
i) Incliding .c files
   
   If the compiler does not support projects, then we would end the file with #include statements that add
   the necessary code files. (most compilers support projects).

j) Employ run-time testing.

   If our compiler support assert() functions, use them liberally. In particular, place them in the beginning
   of functions to test the validity of the input parameters. Place them after calculations to test the validity
   of the results. Place them inside loops to verify indices and pointers are valid. There is a second benefit
   to using assert(). The assert() statements provide inherent documentation of the assumptions.

Organization of a header file

DEFINITIONS MADE IN THE HEADER FILE WILL BE PUBLIC,i.e., accessible by all modules. In general we wish to minimize
the scope of our data, so it is better to make global variables private rather than placing them in the header 
file.

There are two types of header files. The first type of header file has no corresponding code file,i.e., there is
a file.h, but no file.c. In this type of header, we can list global constants and helper macros. Example of global
constants are I/O port addresses of microcontrollers (lm3s1968.h), data types (integer.h) and calibration 
coefficients. Debugging macros could be grouped together and placed in a debug.h file.
The second type of header file does have a corresponding code file. The two files,e.g., file.h and file.c, form a
software module. In this type of header, we define the prototypes for the public functions of the module. 

We should avoid having one header file include other header files. Nested includes obscure the manner in
which the modules are interconnected.

The Header file should be organized in the following order :

a) opening comments
b) #define statements
c) struct union enum statements
d) Global variables and constants
e) Prototype of public functions

Often we wish to place definitions in the header file that must be include only once. If multiple files include
the same header file, the compiler will include the definitions multiple times. Some definitions, such as function
prototypes, can be defined then redefined. However, a common approach to header files uses #ifndef conditional
compilation. If the object is not defined, then the compiler will include everything from the #ifndef until the 
matching #endif. Each header file must have a unique object. One way to guarantee uniqueness is to use the name of
the header file itself in the object name.


#ifndef __File_H__
#define __File_H_
struct Position {
  int bvalid;
  int16_t x;
  int16_t y;
}; 
typedef struct Position PositionType;
#endif

 
Code structure

Employ modular programming techniques.
Conmplex functions should be broken into simple components, so that the details of the lower-level operations
are hidden from the overall algorithms at the higher levels. 

Minimize scope.
In general we hide the implementation of our software from its usage. The scope of a variable should be consistent
with how the variable is used. Global variables should be used only when the lifetime of the data is permanent,
or when data needs to be passed from one thread to another. Otherwise we should use local variables.

In embedded systems, we must have the discipline to restrict I/O port access only in the module that is designed
to access it. We should consider each interrupt vector address separtely, grouping it with the corresponding
I/O module.

Use types using typedef will clarify the format of a variable.New data types and structures will begin
with an upper case letter. The typedef allows us to hide the representation of the object and use an abstract 
concept instead. Example :

typedef int16_t Temperature;
void main (void) { Temperature lowT, highT ;
}
This allow us to change the representation of temperature without having to find all the temperature
variables in our software. We will use types for those objects of fundamental importance to our software,
and for those objects for which a change in implementation is anticipated. As always, the goal is to clarify.
If it doesn´t make it easier to understand, easier to debug, or easier to change, don´t do it.

Prototype all functions
Public functions obviously require a prototype in the header file.
Include both the type and name of the input parameters.
Specify the function as void even if it has no parameters. These prototypes are easy to understand.
void start(int32_t period,void(*functionPt)(void));
int16_t divide(int16_t dividend,int16_t divisor);
Thes prototypes are harder to understand :
start(int32_t,(*)());
int16_t divide(int16_t,int16_t);

Declare data and parameters as const whenever possible.
Declaring an object as const has two advantages. The compiler can produce more efficient code when dealing
with parameters that don´t change. The second advantage is to catch software bugs,i.e., situations where
the program incorrectly attempts to modify data that it should not modify.

goto statements are not allowed.
Debugging is hard enough without adding the complexity generated whe using goto.

++ and -- should not appear in complex statements.
*(--pt) = buffer[n++];
should be written :
--pt ;
*(pt) = buffer[n];
n++;
if it make sense to group, then put them in the same line :
buffer[n]=0; n++;

be a parenthesis zealot.
When mixing arithmetic,logical,and conditional operations, explicity specify the order of operations.
if( ((x+1) & 0x0F) == ( y | 0x04) );

use enum instead of #define or const
The use of enum allows for consistency checking during compilation, and provides for easy to read software.
A good compiler will create exactly the same code.
enum Mode_state { ERROR,
                  NOERROR};
enum Mode_state Mode;
if(Mode==ERROR) { ...

#define statements, if used properly, can clarify our software easy to change. It is proper to use size
in all places that refer to the size of the data array.
#define SIZE 10;
uint16_t Data[SIZE];


Naming convention

Good names reduce the need of documentation. 
Names should have a meaning. UART_OutString
Give hints about the type. dataPt and timePt are pointers, voltageBuf is a buffer, 
Flag , Mode, U16 , Index, Cnt , L ...

Use a prefix to identify public objects.
Functions that can be accessed outside the scope of a module will begin with a prefix specifying the module
to which it belongs. It is poor style to use public variables, but if they need to exist, they too would 
begin with the module prefix.
For example if we see UART_OutString("Hello"); we know this public function belong to the UART module
where the policies are defined in UART.h and the implementation in UART.c.
Notice the similarity between this syntax(e.g.,UART_Init()) and the corresponding syntax we would use if 
programming the module as a class in object-oriented language like C++ or Java (e.g.,UART.Init()).
Using this convention, we can easily distinguish public and private objects.

Use names without lower-case letters to refer to objects with fixed values : TRUE, FALSE , NULL, MAX_VOLAGE...

Permanently-allocated globals will begin with a capital letter, but include some lower-case letters.
Local variables will begin with a lower case letter, and may or may not include upper case letters.

The importance of the naming policy is to extend the clarity to the places where the object is used.

Use capitalization to delimit words: maxTemperature, lastCharTyped, ...

Examples 
Constants :               CR_SAFE_TO_RUN
Local variables :         maxTemperature
Private global variable : MaxTemperature
Public global variable  : DAC_MaxVoltage
Private function        : ClearTime
Public function         : Timer_ClearTime

Comments

As software developers, our goal is to produce code that not only solves our current problem but can
also serve as the basis ot our future solution. In order to reuse software we must leave our code in a 
condition such that future programmers (including ourselves) can easilyunderstand its purpose, constraints,
and implementation. Documentation is not something tacked onto software it is done, but rather it is a 
discipline built into it at each stage of the development.Writing comments as we develop the software
forces us to think about what the software is doingand more importantly why we are doing it. I feel a 
comment that tell us why we perform certain fuunctions is more informative than comments that tell us what
the functions are.
Good comments assist us now while we are debugging, and will assist us later when we are modifying the software,
adding new features, or using the code in a different context.

int16_t SetPoint;
// The desired temperature for the control system
// 16-bit signed temperature with resolution of 0.5C
// The range is -55C to +125C
// A value of 25 means 12.5C
// Avalue of -25 means -12.5C

when a constant is used, we could add comments to explain what the constant means

V = 999;   // 999mV is the maximum voltage
Err = 1;   // error code of 1 means out of range 



.h file

A header file is a file with extension .h which contains C function declarations and macro definitions 
and to be shared between several source files. There are two types of header files: the files that the 
programmer writes and the files that come with your compiler.

C Preprocessor is a text substitution tool and they instruct compiler to do required pre-processing 
before actual compilation.

if you have a header file header.h as follows:
char *test (void);

and a main program called program.c that uses the header file, like this:
int x; 
#include "header.h" 
int main (void) { 
puts (test ()); 
}

the compiler will see the same token stream as it would if program.c read
(because the preprocessor has done its job before the compilation)

int x;
char *test (void);
int main (void) {
puts (test ());
}
References :

Book :
Real-Time Interfacing to ARM Cortex-M Microcontrollers
Jonathan W. Valvano

Leave a comment