The 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(), Keyscan(), etc.The .H file is a header file, probably meaning ‘Head’, which is necessary for the structured design of programs. It allows for the modularization of large programs and facilitates the connection and debugging of various 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, and the corresponding file name is the module name. A module typically consists of two documents: one is the header file *.h, which describes the data structures and function prototypes within the module; the other is the C file *.c, which defines data instances or objects and implements the function algorithms.
The Role of .H Files
As part of project design, in addition to providing a detailed description of the overall functionality of the project, each module is also defined in detail, which includes providing all module header files. Typically, the H header file defines the functionality of each function within 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 allows for public distribution, ensuring the ownership of developers; on the other hand, it prevents others from intentionally or unintentionally modifying it, causing inconsistencies and version chaos. Therefore, the H header file serves as the basis for detailed project design and team work division, 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.
The 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 press detectionunsigned char Keyscan(void); // Get key value/////////////////////////////////////////////////////////////////#endif
Try to Use Macro Definitions #define
When I first looked at others’ 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 Randomly Define Variable Types
In the past, when writing programs, whenever I needed a new variable, I would directly define it at the beginning of the program, whether inside or outside the function. Although this is not a principle error, it is an undesirable practice.Now, let’s discuss the concepts related to variable types in C language.From the perspective of variable scope, variables can be divided into local variables and global variables:
-
Global Variables:These are variables defined outside of functions. Global variables occupy resources throughout the execution of the program. Having too many global variables reduces the generality of the program, as global variables are one of the reasons for coupling between modules.
-
Local Variables:These are variables defined within a function and are only valid within that function.
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 during program execution as needed.
Specifically, there are four storage types:
-
auto
-
static
-
register
-
extern
If no specification is given, it defaults to auto type, which is dynamic storage. If not initialized, it will have an uncertain 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 during compilation and can be referenced by various functions within the same file. If multiple files are involved, and a variable from another file is referenced, it must be specified with extern in that file. However, if a global variable is defined as static, it can only be used within that single file.The register keyword defines a register variable, requesting the compiler to store this variable in the CPU’s register to speed 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
Purpose:To 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 only uses the “i copy” in that register, causing doSomething to never be called.If the variable is marked as volatile, the compiler guarantees that all 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.