Abstract: This article first analyzes the traps and defects of the C language, summarizing the common mistakes; it discusses the shortcomings of compiler semantic checks and provides preventive measures. Using the Keil MDK compiler as an example, it introduces the features of this compiler, its handling of undefined behavior, and some advanced applications. Based on this, the concept of defensive programming is introduced, proposing various measures to prevent issues during the programming process. It emphasizes the important role of testing in writing high-quality embedded programs and common testing methods. Finally, this article attempts to view programming from a higher perspective, discussing some universal programming philosophies.
Programming Philosophy
1 Programming Style
The book “Structure and Interpretation of Computer Programs” states at the beginning: Programs are written for people to read, and only incidentally for machines to execute.
1.1 Clean Style
The choice of coding style has always been a controversial topic, such as the placement of indentation and braces. The coding style can affect the readability of the program; when faced with a source code that has misplaced braces and inconsistent alignment, it is hard to be interested in reading it. We always look at others’ programs, and if our coding styles are similar, it feels more comfortable to read the source code. However, the issue of coding style is subjective, and it is impossible to reach a consensus on coding styles. Therefore, as long as your coding style is clean and the structure is clear, that is sufficient. Beyond that, there are no other requirements for coding style.
Charles Simonyi, the programmer who proposed the Hungarian naming convention and former chief architect at Microsoft, said: I think a code listing brings joy similar to a tidy home. You can tell at a glance whether a home is chaotic or neat. This may not mean much. Just because a house is tidy does not mean it is clean; it may still harbor dirt! However, first impressions are important; they at least reflect certain aspects of the program. I bet I can tell from three meters away whether a program is poorly written. I may not be able to guarantee it is good, but if it looks terrible from three meters away, I can guarantee that the program was written carelessly. If it was written carelessly, then logically it may not be elegant.
1.2 Clear Naming
Variables, functions, macros, etc., all need naming, and clear naming is one of the hallmarks of excellent code. One key point of naming is that the name should clearly describe the object so that even a novice programmer can easily understand your code logic. We need to consider who our code is primarily written for: ourselves, the compiler, or others? I believe the main audience for code is others, followed by ourselves. Without clear naming, it is difficult for others to maintain your program because remembering more than a dozen poorly named variables is very challenging; moreover, after some time, when you look back at your code, you may not remember what those poorly named variables mean.
Giving an object a clear name is not a simple task. First, recognizing the importance of naming requires a process, which may be related to the widespread use of Tan’s C programming textbooks in universities: books filled with variable names like a, b, c, x, y, z fail to convey excellent programming philosophy at a critical beginner stage. Secondly, appropriately naming an object is also challenging; it needs to be accurate, unambiguous, and concise, requiring a certain level of proficiency in English. Meeting all these requirements can become quite difficult. Additionally, naming must consider overall consistency; there should be a unified style within the same project, and adhering to this style is not easy.
Regarding how to name, Charles Simonyi said: When faced with a structure that has certain attributes, do not casually pick a name and let everyone ponder the relationship between the name and the attributes; you should use the attributes themselves as the name of the structure.
1.3 Appropriate Comments
Comments have always been a point of contention; I oppose both the lack of comments and excessive comments. Code without comments is clearly poor, but too many comments can hinder the readability of the program. Due to potential ambiguities in comments, they may misrepresent the true intent of the program. Furthermore, excessive comments can waste programmers’ time. If your coding style is clean and your naming is clear, then your code’s readability should not be too poor, and the purpose of comments is to facilitate understanding of the program.
It is recommended to use good coding styles and clear naming to reduce the need for comments. Comments should focus on modules, functions, variables, data structures, algorithms, and key code, emphasizing the quality of comments rather than quantity. If you need a large block of comments to explain what the program does, you should consider whether it is because the variable names are not clear enough or the code logic is too chaotic. At that point, you should consider how to simplify the program rather than adding comments.
2 Data Structures
Data structures are the foundation of program design. Before designing a program, one should first consider the necessary data structures.
Charles Simonyi, former chief architect at Microsoft: The first step in programming is to imagine. It is to have a very clear grasp of the ins and outs in your mind. At this initial stage, I use paper and pencil. I just doodle and do not write code. I might draw some boxes or arrows, but basically, it is just doodling because the real ideas are in my mind. I like to imagine the structures that need maintenance, which represent the real world I want to code. Once this structure is considered quite rigorously and clearly, I start writing code. I sit in front of the terminal, or in the old days, I would take a piece of white paper and start writing code. It is quite easy. I just need to translate the ideas in my head into code; I know what the result should look like. Most of the code will flow naturally, but the data structures I maintain are key. I will think about the data structures first and keep them in mind throughout the coding process.
Butler Lampson, who developed Ethernet and the SDS 940 operating system: The most important quality of a programmer is the ability to organize the solution to a problem into an easily manipulable structure.
Gary A., who developed the CP/M operating system: If I cannot confirm that the data structure is correct, I will never start coding. I will first draw the data structure and spend a long time thinking about it. Once the data structure is determined, I start writing small pieces of code and continuously improve and monitor it. Testing during the coding process ensures that the modifications are localized, and if there are any issues, they can be discovered immediately.
Bill Gates, the founder of Microsoft: The most important part of writing a program is designing the data structure. The next important part is breaking down various code blocks.
Dan Bricklin, who wrote the world’s first spreadsheet software: In my view, the most important part of writing a program is designing the data structure; in addition, you must know what the human-computer interface will look like.
Let’s illustrate with an example. When introducing defensive programming, it was mentioned that the LCD display used by the company has general anti-interference capability. To improve the stability of the LCD, it is necessary to periodically read the critical register values inside the LCD and compare them with the initial values stored in Flash. There are more than a dozen LCD registers to be read, and the values read from each register can vary from 1 to 8 bytes. If the data structure is not considered, the resulting program will be very lengthy.
void lcd_redu(void) { read the first register value; if (first register value == Flash stored value) { read the second register value; if (second register value == Flash stored value) { ... read the tenth register value; if (tenth register value == Flash stored value) { return; } else { reinitialize LCD; } } else { reinitialize LCD; } } else { reinitialize LCD; } }
Analyzing this process, we find that many similar elements can be extracted, such as the command number for reading the LCD register, which will go through the process of reading the register, checking if the value is the same, and handling exceptions. Therefore, we can extract some common elements, organize them into a data structure, and use a unified method to process this data, separating the data from the processing process.
We can first extract the common elements and organize them into a data structure:
typedef struct { uint8_t lcd_command; // LCD register uint8_t lcd_get_value[8]; // Values written to the register during initialization uint8_t lcd_value_num; // Number of initial values written to the register } lcd_redu_list_struct;
Here, lcd_command represents the command number for the LCD register; lcd_get_value is an array representing the values to be initialized for the register. This is because an LCD register may need to initialize multiple bytes, which is determined by hardware characteristics; lcd_value_num indicates how many bytes of initial values a register has, as the number of initial values varies for each register, and we need this information when processing data with the same method.
In this example, the data we are going to process is all fixed in advance, so after defining the data structure, we can organize this data into a table:
/* LCD partial register setting value list */ lcd_redu_list_struct const lcd_redu_list_str[]= { {SSD1963_Get_Address_Mode,{0x20} ,1}, /* 1 */ {SSD1963_Get_Pll_Mn ,{0x3b,0x02,0x04} ,3}, /* 2 */ {SSD1963_Get_Pll_Status ,{0x04} ,1}, /* 3 */ {SSD1963_Get_Lcd_Mode ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00} ,7}, /* 4 */ {SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /* 5 */ {SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00} ,7}, /* 6 */ {SSD1963_Get_Power_Mode ,{0x1c} ,1}, /* 7 */ {SSD1963_Get_Display_Mode,{0x03} ,1}, /* 8 */ {SSD1963_Get_Gpio_Conf ,{0x0F,0x01} ,2}, /* 9 */ {SSD1963_Get_Lshift_Freq ,{0x00,0xb8} ,2}, /* 10 */ };
At this point, we can use a single processing procedure to complete the reading, checking, and exception handling of dozens of LCD registers:
/** * LCD display redundancy * Call this program once every period */ void lcd_redu(void) { uint8_t tmp[8]; uint32_t i, j; uint32_t lcd_init_flag; lcd_init_flag = 0; for (i = 0; i < sizeof(lcd_redu_list_str) / sizeof(lcd_redu_list_str[0]); i++) { LCD_SendCommand(lcd_redu_list_str[i].lcd_command); uyDelay(10); for (j = 0; j < lcd_redu_list_str[i].lcd_value_num; j++) { tmp[j] = LCD_ReadData(); if (tmp[j] != lcd_redu_list_str[i].lcd_get_value[j]) { lcd_init_flag = 0x55; // Some debug statements, print out the specific error information goto handle_lcd_init; } } } handle_lcd_init: if (lcd_init_flag == 0x55) { // Reinitialize LCD // Some necessary recovery measures } }
Through a reasonable data structure, we can separate the data from the processing process, and the LCD redundancy judgment process can be implemented with very concise code. More importantly, separating data from the processing process is more conducive to code maintenance. For example, through experiments, we find that we need to add a value of an LCD register for judgment; at this point, we only need to add the new register information in any position of the LCD register setting value list according to the data structure format without adding any processing code! This is just one of the advantages of data structures; using data structures can also simplify programming, making complex processes simpler, which can only be deeply understood after actual programming.