Understanding the Relationship Between .h and .c Files in Embedded C Programming

Relationship Between .h Files and .c Files

When referring to the programs of experts, I found that the strict programs written by others all include a “KEY.H” file, which defines the functions used in the .C file, such as Keyhit() and Keyscan()..H files are header files, probably meaning ‘Head’, which is necessary for structured design in programming, allowing for modularization of large programs and enabling debugging of connections between modules.

Introduction to .H Files:

In embedded C programming for microcontrollers, projects are generally structured by functional modularization. A project is divided into multiple functions, with the related programs placed in a C program document, referred to as a module, corresponding to the module name as the file name. A module typically consists of two documents: one is the header file *.h, which describes the data structures and function prototypes in the module; the other is the C file *.c, which defines data instances or objects and implements the function algorithms.

Function of .H Files

As part of project design, in addition to providing a detailed description of the overall functionality of the project, it also defines each module in detail, which means providing the header files for all modules. Typically, H header files define the functionality of each function in the module, as well as the requirements for input and output parameters. The specific implementation of the module is designed, programmed, and debugged based on the H file. For confidentiality and security, once the module is implemented, it is provided to other project members in the form of a connectable OBJ file or a LIB file. Since the source program document does not need to be provided, it can be publicly distributed, ensuring the ownership of the developers; on the other hand, it prevents others from intentionally or unintentionally modifying it, causing inconsistencies and version chaos. Therefore, the H header file is the basis for the detailed design of the project and the division of team work, as well as a functional description for testing the module. To reference data or algorithms within the module, simply include the specified module H header file using include.

Basic Composition of .H Files

/* The following is the header document for keyboard driver */#ifndef _KEY_H_ // Prevent multiple inclusions, if _KEY_H_ is not defined, compile the next line#define _KEY_H_ // This symbol is unique, indicating that once included, the symbol _KEY_H_ is defined/////////////////////////////////////////////////////////////////char keyhit( void ); // Key hit detectionunsigned char Keyscan( void ); // Get key value/////////////////////////////////////////////////////////////////#endif

Try to Use Macro Definitions #define

When I started looking at other people’s programs, I found many #define statements at the beginning of the file after the includes. At that time, I thought, is it really that troublesome to replace so many identifiers? I completely did not understand the benefits of this writing style. It turns out that using an identifier to represent a constant is beneficial for future modifications and maintenance; when modifying, you only need to change it at the beginning of the program, and all places where it is used will be modified, saving time.

#define KEYNUM 65 // Number of keys, used for Keycode[KEYNUM]#define LINENUM 8 // Number of keyboard rows#define ROWNUM 8 // Number of keyboard columns

Points to Note:

  • Macro names are generally written in uppercase

  • Macro definitions are not C statements and do not end with a semicolon

Do Not Define Variable Types Randomly

Previously, when writing programs, whenever I needed a new variable, I would directly define it at the beginning of the program, whether inside or outside a function. Although this is not a principle error, it is not a recommended practice.Now, let’s discuss the concepts related to variable types in C language.From the scope of the variable, it can be divided into local variables and global variables:

  • Global Variables: These are variables defined outside of functions, and global variables occupy resources throughout the execution of the program. Too many global variables can reduce the generality of the program, as global variables are one of the reasons for coupling between modules.

  • Local Variables: These are variables defined inside functions and are only valid within those functions.

From the perspective of the duration of variable values, there are two types:

  • Static Storage Variables: These are allocated fixed storage space during program execution.

  • Dynamic Storage Variables: These are allocated storage space dynamically as needed during program execution.

Specifically, there are four storage types:

  • auto

  • static

  • register

  • extern

By default, it is assumed to be of type auto, which is dynamic storage. If not initialized, it will have an indeterminate value. If a local variable is defined as static, its value remains unchanged within the function, and its default initial value is 0.It is allocated in the static storage area at compile time and can be referenced by various functions within this file.If multiple files are involved, and a variable from another file is referenced, it must be declared with extern in this file.However, if a global variable is defined as static, it can only be used within that one file.The register keyword defines a register variable, requesting the compiler to keep this variable in the CPU’s register, thus speeding up program execution.

Usage of Special Keywords const and volatile

const

const is used to declare a read-only variable.

const unsigned char a=1; // Define a=1, the compiler does not allow modification of a's value

Function: Protect parameters that should not be modified.

volatile

A variable defined as volatile indicates that this variable may be changed unexpectedly, so the compiler will not assume its value. Specifically, the optimizer must carefully re-read the value of this variable each time it is used, rather than using a backup stored in a register.

static int i=0;int main(void){...while (1){if (i) dosomething();}}/* Interrupt service routine. */void ISR_2(void){i=1;}

The intention of the program is to call the dosomething function in main when the ISR_2 interrupt occurs. However, since the compiler determines that i has not been modified in the main function, it may only execute the read operation from i to a register once, and then each if check uses the “i copy” in that register, causing dosomething to never be called.If the variable is marked as volatile, the compiler guarantees that read and write operations on this variable will not be optimized (will definitely execute).

Generally speaking, volatile is used in the following situations:

  • Variables modified in interrupt service routines that need to be detected by other programs should be marked as volatile;

  • Flags shared between tasks in a multitasking environment should be marked as volatile;

  • Memory-mapped hardware registers should also be marked as volatile, as each read and write may have different meanings.

Leave a Comment