Until now we used pretty simple program examples where all code fit into a single file and didn’t look very complicated. We didn’t care much about programming styles and modularizing. But due time, programs are going to grow bigger and more complex. It may become tough to maintain your code and even harder for other developers to read. We should follow some simple rules that make microcontroller programming more fun than scratching along.
Split your programs
Say you are writing a pretty complex program which deals with LCD, USART, and even more peripherals. To make them all work, you need routines to init, read, write, change mode. Imagine how many functions your single file would have to hold, and I am not talking about the main routine. It would end up with more scrolling than coding. So, smart solution is to split your project into several files – libraries. Each library would have to take care of one type tasks like controlling USART, other LCD and so on. For instance, our program needs these functions:
int main(); void MainPortInit(); uint8_t MainReadPort(); void USARTInit(); void USARTSendByte(uint8_t Data); uint8_t USARTReceiveByte(); void LCDInit(); void LCDclr(); void LCDGotoXY(uint8_t, uint8_t); void LCDSendChar(uint8_t);
Putting all these functions in one file would work, but the code would become heavy, hard to read and reuse. So lets split all these routines by assigning some logic. Logic relies on related functions like USART functions would go to one group, LCD to another. Some general functions may stay in the main code. After grouping our example, we have three groups of functions that go to separate files:
int main(); void MainPortInit(void); uint8_t MainReadPort(void);
void USARTInit(void); void USARTSendByte(uint8_t Data); uint8_t USARTReceiveByte(void);
void LCDInit(void); void LCDclr(void); void LCDGotoXY(uint8_t, uint8_t); void LCDSendChar(uint8_t);
After this operation, we end up with three source files. But how to link them into a single program. This is done by using header files. So we need to create a pair file to each source file.
main.c – main.h(not required)
usart.c – usart.h;
lcd.c – lcd.h
Header files must be the same name as source files but with extension .h. So when one source file needs to use a routine from another source file, you must include a header file by defining header file using quotes:
#include"main.h" #include"usart.h" #include"lcd.h"
We already include some headers in our programs but in side brackets, meaning that these are system header files located in system directories.
#include <avr/io.h> #include <avr/interrupt.h>
The last thing to do is to make header files work. The header file is wrapped around preprocessor defines so-called header guard:
#ifndef LCD_H #define LCD_H //contents #endif
This prevents multiple includes of the same header. First, it checks if this file has been included, if not then current is added. The current situation may occur when several source files use the same include or include each other. For instance, usart.c would include lcd.h and lcd.c would include usart.h – this would lead to recursive inclusion during compilation. So don’t worry too much and use preprocessor defines. Header guard has to be named with all caps. Better use the same name as a file with an underscore instead dot like LCD_H. To make the header function we need to put something in it. First of all, the header must have all function prototypes from c file included. For instance, lcd.h may look like:
#ifndef LCD_H #define LCD_H //is 4 bit mode? #define 4BIT void LCDInit(void); void LCDclr(void); void LCDGotoXY(uint8_t, uint8_t); void LCDSendChar(uint8_t); #endif
Also, there can be various definitions, type definitions or macro functions.
Also, header files can declare global variables that might be used in multiple source files. To make it possible declare the variable inside c file:
uint8_t Mode = 1;
Then in header file lcd.h define this variable by adding “extern” keyword.
extern uint8_t Mode;
This will make this variable visible in other c files if the header is included. Just be sure not to define variables in header files – do this in c file.
The last thing to mention about modularizing code is “static” functions. Let’s say we created separate lcd.c and lcd.h files. We wrote a bunch of routines like LCDsendCommand(uint8_t), LCDInit(), LCDWrite(uint8_t), LCDGotoXY(uint8_t, uint8_t), and so on. Some of these functions definitely will be used in other files, but some not. For instance, LCDsendCommand(uint8_t) will be used only inside lcd.c to make other routines work. We don’t need this function to be accessible by other c files. To do so, we add keyword “static” to this function.
static void LCDsendCommand(uint8_t);
Add this keyword in both – c and h files. This is a useful practice that helps the compiler to optimize function as it doesn’t have to be called outside the own c file.
Benefits of modularizing
It may seem complicated at the beginning, but it is straightforward and makes programming smooth. It is good practice to write separate libraries for separate function groups like USART, LCD, ADC, TWI, because once written you can include these libraries multiple times in different projects– just be smart and write them to be more universal – don’t tie to specific hardware, but use preprocessor defines for device specific stuff. Later you will see that programming microcontrollers may be the same as using/adapting right code library with minimal coding efforts.
Again this is only scope of few things. We are not going back to them as these are basics of C programming and can be found on every corner on the internet where “C tutorial” is mentioned. Better let’s focus on microcontroller stuff again.