Optimizing Microcontroller Programs for Small Code and Fast Speed

Optimizing a program usually refers to enhancing the program code or the execution speed of the program.Optimizing code and optimizing speed are essentially a contradictory unity.Generally, optimizing the size of the code will lead to an increase in execution time;if the execution speed of the program is optimized, it usually results in an increase in code size.It is difficult to achieve both goals, and one must grasp a balance point during the design phase.

1. Optimizing Program Structure

1. Program Writing Structure

Although the writing format does not affect the quality of the generated code, it is still advisable to follow certain writing rules when actually writing programs. A clearly written program is beneficial for future maintenance. When writing programs, especially for statements like While, for, do…while, if…else, switch…case, and their nested combinations, a “formatted” writing style should be adopted.

2. Identifiers

User-defined identifiers in the program should not only adhere to naming rules but also avoid using algebraic symbols (like a, b, x1, y1) as variable names. Instead, choose meaningful English words (or abbreviations) or Pinyin as identifiers to enhance the readability of the program, such as: count, number1, red, work, etc.

3. Program Structure

The C language is a high-level programming language that provides a comprehensive standardized control structure. Therefore, when designing microcontroller application system programs in C language, it is essential to adopt a structured programming approach as much as possible to ensure that the entire application system program structure is clear, making it easier to debug and maintain.

For a larger application program, the entire program is usually divided into several modules based on functionality, with different modules completing different functions. Each module can be written separately and can even be developed by different programmers. Generally, the functionality completed by a single module is relatively simple, making design and debugging easier. In C language, a function can be considered a module.

Modular programming not only means dividing the entire program into several functional modules but also emphasizes maintaining the relative independence of variables between modules, that is, keeping modules independent and minimizing the use of global variables. Common functional modules can be encapsulated into an application library for direct calling when needed. However, if the modules are divided too finely, it may lead to reduced execution efficiency (the time spent protecting and restoring registers when entering and exiting a function).

4. Defining Constants

During program design, for frequently used constants, if they are directly written into the program, once the value of a constant changes, it is necessary to find and modify all occurrences of that constant in the program one by one, which inevitably reduces the maintainability of the program. Therefore, it is advisable to define constants using preprocessor commands to avoid input errors.

5. Reducing Conditional Statements

Wherever conditional compilation (ifdef) can be used, it should be used instead of if statements, which helps reduce the length of the generated code.

6. Expressions

For expressions where the order of operations is not clear or easily confused, parentheses should be used to explicitly specify their priority. An expression should not be overly complicated; if it is too complex, it will be difficult to understand later, hindering future maintenance.

7. Functions

For functions in the program, the type of the function should be specified before use, ensuring it matches the originally defined function type. Functions with no parameters and no return type should include the “void” specification. If the goal is to shorten the code length, common program segments can be defined as functions. If the goal is to reduce execution time, after debugging the program, some functions can be replaced with macro definitions. Note that macros should only be defined after debugging is complete because most compiler systems only report errors after macro expansion, which increases debugging difficulty.

8. Minimize Global Variables, Use Local Variables More

Global variables occupy data storage space; defining a global variable reduces available data storage space for the MCU. If too many global variables are defined, it could lead to insufficient memory allocation by the compiler. In contrast, local variables are mostly located in the internal registers of the MCU. In most MCUs, using register operations is faster than using data storage, and the instructions are more flexible, helping to generate higher quality code. Furthermore, the registers and data storage occupied by local variables can be reused in different modules.

9. Set Appropriate Compiler Options

Many compilers offer different optimization options. Before use, one should understand the meaning of each optimization option and choose the most suitable one. Typically, if the highest level of optimization is chosen, the compiler may obsessively pursue code optimization, which could affect the correctness of the program and lead to runtime errors. Therefore, it is important to be familiar with the compiler being used and to know which parameters will be affected during optimization and which will not.

2. Code Optimization

1. Choose Appropriate Algorithms and Data Structures

One should be familiar with algorithm languages. Replace slower sequential search methods with faster binary search or unordered search methods, and replace insertion sort or bubble sort with quick sort, merge sort, or radix sort to significantly improve program execution efficiency.

Choosing an appropriate data structure is also crucial; for example, using a large number of insert and delete instructions in a randomly stored dataset is much faster than using a linked list. 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 with higher execution efficiency than using arrays.

However, in Keil, the opposite is true; using arrays generates shorter code than using pointers.

2. Use the Smallest Data Types Possible

If a character type (char) variable can be used, do not use an integer (int) variable; if an integer variable can be used, do not use a long integer (long int); and avoid using floating-point (float) variables unless necessary. Of course, after defining a variable, do not exceed its scope. If a value is assigned beyond the variable’s range, the C compiler will not report an error, but the program’s runtime result will be incorrect, and such errors are difficult to detect.

3. Use Increment and Decrement Instructions

Using increment, decrement instructions, and compound assignment expressions (like a-=1 and a+=1) usually generates high-quality program code. Compilers can generally generate inc and dec instructions, while using a=a+1 or a=a-1 type of instructions may generate 2-3 bytes of instructions in many C compilers.

4. Reduce Computational Intensity

Replace complex expressions with simpler expressions that perform the same function. For example:

(1) Remainder Operation

a=a%8; can be changed to: a=a&7;

Explanation: Bitwise operations can be completed in one instruction cycle, while most C compilers call a subroutine to complete the “%” operation, resulting in long code and slow execution speed. Typically, for finding the remainder of 2n, bitwise methods can be used instead.

(2) Square Operation

a=pow(a,2.0); can be changed to: a=a*a;

Explanation: In microcontrollers with built-in hardware multipliers (like the 51 series), multiplication is much faster than square operations because the floating-point square operation is implemented via subroutine calls. In AVR microcontrollers with built-in hardware multipliers, such as ATMega163, multiplication can be completed in just 2 clock cycles. Even in AVR microcontrollers without built-in hardware multipliers, the subroutine for multiplication is shorter and faster than that for square operations. If calculating the cube, such as a=pow(a,3.0); it can be changed to: a=a*a*a; the efficiency improvement is even more evident.

(3) Use Shifts for Multiplication and Division

a=a*4; b=b/4; can be changed to: a=a<<2; b=b>>2;

Explanation: Typically, if you need to multiply or divide by 2n, shifting can be used instead. In ICCAVR, multiplying by 2n generates left-shift code, while multiplying by other integers or dividing by any number calls multiplication and division subroutines. Using shifts results in more efficient code than calling multiplication and division subroutines. In fact, any multiplication or division by an integer can be replaced with shifts, such as: a=a*9 can be changed to: a=(a<<3)+a.

5. Loops

(1) Loop Statements

For tasks that do not require loop variables to participate in calculations, they can be placed outside the loop. These tasks include expressions, function calls, pointer operations, array accesses, etc. All unnecessary operations should be grouped together and placed in an init initialization program.

(2) Delay Functions

Commonly used delay functions typically adopt an increment format:

void delay (void) { unsigned int i; for (i=0; i<1000; i++); } can be changed to a decrement delay function: void delay (void) { unsigned int i; for (i=1000; i>0; i–); }

Both functions have similar delay effects, but almost all C compilers generate code for the latter that is 1-3 bytes shorter than the former because almost all MCUs have instructions for zero transfer, and using the latter method can generate such instructions. The same applies when using a while loop; using decrement instructions to control the loop will generate 1-3 fewer bytes of code than using increment instructions.

However, when there are read/write instructions for arrays through the loop variable “i,” using pre-decrement loops may lead to array out-of-bounds issues, which should be noted.

(3) While Loops and Do…While Loops

Using while loops can take one of the following forms:

unsigned int i; i=0; while (i<1000) { i++; // user program } or: unsigned int i; i=1000; do

{ i–; // user program

} while (i>0);

In these two loop forms, the code generated by the do…while loop is shorter than that of the while loop after compilation.

6. Lookup Tables

In programs, avoid performing very complex calculations, such as floating-point multiplication, division, and square roots, or complex mathematical model interpolation calculations. For these time-consuming and resource-consuming operations, it is advisable to use lookup tables and store the data tables in program memory. If it is challenging to generate the required table directly, compute it at startup and then generate the required table in data memory, allowing direct lookup during program execution, thus reducing the workload of repeated calculations during execution.

7. Others

Using inline assembly and storing strings and constants in program memory are also beneficial for optimization.

Source: https://www.cnblogs.com/tianqiang/p/9005538.html

Optimizing Microcontroller Programs for Small Code and Fast SpeedDisclaimer: This article is reproduced from the “Internet”, copyright belongs to the author. If there is infringement, please contact us for deletion!
「Please Share If Useful」

👇Click Follow, TechnologyDelivering Quality Content on Time!👇

Optimizing Microcontroller Programs for Small Code and Fast Speed
  • Those once popular microcontrollers~

  • Can you imagine a Bluetooth chip costing less than 2 yuan?

  • Why do domestic chips also use English for “datasheet”?

  • Fascinating operational amplifier circuits

  • Why 50 ohms???

  • The most detailed diode basics

  • What is a BSP engineer?

  • How to play with the ubiquitous ESP8266?

Leave a Comment