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