01
Program structure optimization
1. Program writing structure
While formatting doesn't affect code quality, it's still advisable to follow certain writing rules when actually writing programs. A clear and concise program is easier to maintain later. When writing programs, especially with statements like While, for, do…while, if…else, switch…case, or nested combinations of these statements, indentation should be used.
2. Identifier
In addition to following the naming rules for identifiers, user identifiers used in a program should generally not use algebraic symbols (such as a, b, x1, y1) as variable names. Instead, select English words (or abbreviations) or Chinese pinyin with relevant meanings as identifiers to increase the readability of the program, such as: count, number1, red, work, etc.
3. Program Structure
C is a high-level programming language that provides a very complete set of standardized flow control structures. Therefore, when designing microcontroller application system programs using C, it is essential to adopt structured programming methods as much as possible. This will ensure a clear program structure, making debugging and maintenance easier.
For a large application, the entire program is usually divided into several modules based on functionality, with different modules performing different functions. Each module can be written separately, and even written by different programmers. Generally, the function performed by a single module is relatively simple, and the design and debugging are relatively easier. In the C language, a function can be considered a module.
Modularization, in essence, involves more than just dividing the entire program into functional modules. It also requires maintaining the relative independence of variables between modules, minimizing the use of global variables. Frequently used functional modules can be encapsulated into an application library for direct access when needed. However, if modules are divided too finely, the program's execution efficiency will decrease (entering and exiting a function consumes time for register protection and restoration).
4. Define constants
In the process of programmatic design, if frequently used constants are directly written into the program, then every time the value of a constant changes, it is necessary to find all the constants in the program and modify them one by one. This will inevitably reduce the maintainability of the program. Therefore, constants should be defined using preprocessor directives whenever possible, which can also avoid input errors.
5. Reduce conditional statements
Use conditional compilation (ifdef) instead of if statements where possible, which helps reduce the length of the generated code.
6. Expression
Where the order of operations in an expression is unclear or easily confused, parentheses should be used to explicitly specify their order. An expression should generally not be too complex; if it is too complex, it will become difficult to understand over time, hindering future maintenance.
7. Functions
Before using functions in a program, their types should be declared. The declared function type must be consistent with the originally defined function type. Functions without parameters or return types should be declared with "void". To shorten code length, some common code segments can be defined as functions. To shorten program execution time, after debugging, some functions can be replaced with macros. Note that macros should be defined only after debugging is complete, as most compilers only report errors after macro expansion, increasing the difficulty of debugging.
8. Minimize the use of global variables and maximize the use of local variables.
Because global variables are stored in data memory, defining a global variable reduces the available data memory space for the MCU. If too many global variables are defined, the compiler may not have enough memory to allocate. Local variables, on the other hand, are mostly located in the MCU's internal registers. In most MCUs, register operations are faster than data memory operations, and there are more and more flexible instructions, which is conducive to generating higher quality code. Moreover, the registers and data memory occupied by local variables can be reused in different modules.
9. Set appropriate compiler options
Many compilers offer several different optimization options. Before using them, you should understand the meaning of each option and then choose the most suitable one. Often, if the highest level of optimization is selected, the compiler will pursue code optimization almost pathologically, which may affect the correctness of the program and cause runtime errors. Therefore, you should be familiar with the compiler you are using and know which parameters are affected by optimization and which are not.
02
Code optimization
1. Choose appropriate algorithms and data structures
Familiarity with algorithm languages is essential. Replacing slower sequential search methods with faster binary search or random search methods, and replacing insertion sort or bubble sort with quicksort, merge sort, or root sort, can significantly improve program execution efficiency.
Choosing a suitable data structure is also important. For example, when dealing with a large number of insertion and deletion instructions in a randomly stored set of data, using a linked list is much faster. Arrays and pointers are closely related; generally, pointers are more flexible and concise, while arrays are more intuitive and easier to understand. For most compilers, using pointers generates shorter code and is more efficient than using arrays.
However, in Keil, the opposite is true; using arrays generates shorter code than using pointers.
2. Use the smallest possible data type
If a variable can be defined using a character type (char), don't use an integer type (int); if a variable can be defined using an integer type, don't use a long integer type (long int); and avoid using floating-point type variables if possible. Of course, after defining a variable, don't assign a value outside its scope. If you assign a value outside the scope of a variable, the C compiler won't report an error, but the program will produce incorrect results, and such errors are difficult to detect.
3. Use increment and decrement commands
High-quality program code can usually be generated by using increment and decrement instructions and compound assignment expressions (such as a-=1 and a+=1). Compilers can usually generate instructions like inc and dec, while many C compilers will generate instructions of 2 to 3 bytes when using instructions like a=a+1 or a=a-1.
4. Reduce computational intensity
You can replace the original complex expression with an expression that requires less computation but has the same function.
Optimizing code is a crucial step in microcontroller development. It can help improve program efficiency, reduce resource consumption, and enhance overall performance. Here are some methods for optimizing code:
1. Code Conciseness: Avoid redundant code, remove unnecessary comments and unused variables, which can reduce the size of the program and reduce memory usage.
2. Algorithm optimization: Select efficient algorithms and data structures to reduce unnecessary computation and memory operations. Especially when dealing with complex logic, algorithm optimization can significantly improve performance.
3. Loop optimization: Avoid using complex calculations and function calls in loops, and try to use simple operations to reduce the execution time of each iteration.
4. Interrupt Service Routine Optimization: Interrupt service routines should be as short as possible, avoiding time-consuming operations within the interrupt, thus reducing interrupt response time.
5. Bitwise operations: When performing bitwise operations, using bitwise operators (such as bitwise AND, bitwise OR, bitwise XOR, etc.) can be faster than using arithmetic operations.
6. Use macros: For constant values and repetitive code blocks, using macros can reduce the amount of code and improve code readability.
7. Memory Management: Allocate memory properly and avoid memory leaks and unnecessary memory allocation to improve memory usage efficiency.
8. Conditional compilation: Use conditional compilation directives to exclude debugging code or select different code execution paths under different configurations.
9. Code refactoring: Regularly refactor the code to improve its maintainability and scalability, while also eliminating code smells.
10. Performance Analysis: Use performance analysis tools to identify bottlenecks in the program and optimize them accordingly.
We can simplify and improve the efficiency of the code in the following ways.
1. Use a modular structure: decompose functional blocks
Modular programming makes code clearer and improves its reusability.
For example, for an electronic clock program, the code can be divided into a display module, a key handling module, a timing module, a timer interrupt service routine, etc.
Each module handles relatively independent tasks, which reduces code duplication and facilitates maintenance and debugging.
Timing module: Utilizes the microcontroller's timer to precisely control the second pulse of the clock.
Display module: Different time displays are implemented using loops or lookup tables, which reduces unnecessary branch jump instructions.
Key module: Key debouncing and key event handling can be implemented with an interrupt and simple logic, instead of writing separate logic for each key.
2. Instruction optimization and loop structures
In assembly language, loops and conditional jumps often consume more instruction cycles, so optimizing loop structures and reducing branch jumps is crucial.
For example:
Lookup table method: A lookup table can be used to replace complex calculations or conditional judgments. For example, when processing the carry-over from seconds to minutes, a pre-set lookup table can reduce calculation operations.
Delay program optimization: If you need to implement timing using delays, consider using timer interrupts instead of simple loop delays. Loop delays not only increase code size but also reduce runtime efficiency.
3. Utilize macro instructions and subroutines
Macros and subroutines can save lines of code and improve reusability.
In assembly language, you can define frequently used instruction sets as macros or subroutines, such as the display refresh code for a digital tube. You can call them directly each time you need them, avoiding repetitive writing.
Macros: When writing repetitive code blocks, encapsulate them as macros and simplify them by passing parameters, such as displaying numbers (scan number, value). This makes the code more concise.
Subroutine calls: Logic like refreshing a clock display can be written as subroutine calls. The program automatically returns after each call, avoiding repetitive code.
4. Make good use of the microcontroller's hardware resources
Microcontrollers come with some hardware resources that can help simplify code and improve efficiency.
A hardware timer can be used to count the second pulses, thus eliminating a lot of delay loops.
For example, key debouncing and timing logic can be accomplished through interrupts, where each event is handled in the interrupt service routine, instead of requiring tedious processing in the main program.
For example, some microcontroller registers can perform simple calculations directly without additional instructions, thus improving execution efficiency.
5. Streamlined instruction set and memory management
Minimize unnecessary instructions, make full use of registers, and avoid frequent data movement instructions.
For example:
Optimization of constant operations: Operations on fixed values can be performed directly through registers. For example, a fixed increment operation can be performed using an increment instruction instead of a multi-step addition.
Data and code segment optimization: Reduce unnecessary data movement and place frequently used data in memory areas easily accessible to the CPU.
6. Debugging and Performance Testing
Because the core principle of assembly programming is "efficiency first," debugging and optimization are especially important after the program is written.
You can use the following methods to test and optimize performance:
Step-by-step testing: Debug each module separately to ensure their functionality and performance, and then combine the modules together.
Time consumption analysis: Analyze where the program consumes the most instructions and whether further optimization is possible. Using hardware debugging tools to analyze clock frequency and response time is also very effective.
Having many lines of code does not necessarily mean low efficiency.
The key is to avoid redundancy when writing code and to make flexible use of modular design and microcontroller resources.
The final program must operate efficiently without losing its clear structure.