Solar Panel Controller,Mppt Wind Charge Controller,30A Mppt Solar Controller,Mppt Charge Controller GuangZhou HanFong New Energy Technology Co. , Ltd. , https://www.gzinverter.com
Zhou Ligong teaches you to learn programming techniques: do a layered design of software modules, callback functions should be written like this
Chapter Two focuses on programming technology, and this article delves into section 2.1.3, which discusses callback functions.
The second chapter introduces programming techniques, and within this chapter, section 2.1.3 specifically examines callback functions.
Let’s begin with the concept of layered design. Layered design involves breaking down software into modules that have a hierarchical relationship. Given that each layer is relatively independent, defining the interfaces between layers allows each layer to be developed separately. For instance, consider the design of a secure electronic lock. This hardware includes components such as a keypad, display, buzzer, lock, and memory drive circuit. Consequently, the software can be divided into three parts: the hardware driver layer, the virtual layer, and the application layer. Each major module can further be subdivided into smaller modules. Let’s explore this concept using the example of keyboard scanning.
(1) Hardware Driver Layer:
The hardware driver layer is positioned at the lowest level of the module and interacts directly with the hardware. Its role is to determine which key has been pressed, implementing software closely tied to the hardware circuit. More complex functionalities will be handled at other layers. Although the hardware driver layer can directly connect to the application layer, due to various changes in the hardware circuit, if the application layer were to directly operate the hardware driver layer, it would inevitably become dependent on the hardware layer. Therefore, the best approach is to introduce a virtual layer to manage hardware changes. Clearly, as long as the keyboard scanning method remains consistent, the generated key values will stay the same, meaning the software of the virtual layer will never need to change.
(2) Virtual Layer:
The virtual layer is designed according to the requirements of the application layer. It primarily serves to shield the details and changes of the objects, enabling the application layer to be implemented uniformly. Even if the control methods change, there is no need to rewrite the application layer code.
(3) Application Layer:
The application layer sits at the top of the module and is directly used for functional implementation. For example, the application layer might have only one “human-computer interaction†module externally. Of course, several internal modules can be divided for use. The data transfer relationship between the three layers is quite clear, namely, application layer → virtual layer → hardware driver layer, as illustrated in Figure 2.2. The solid lines in the figure represent dependencies, indicating that the application layer depends on the virtual layer, and the virtual layer depends on the hardware driver layer. A layered architecture offers the following advantages:
- Reduces system complexity: Since each layer is relatively independent, layers interact through well-defined interfaces, allowing each layer to be implemented separately, thus reducing the coupling between modules.
- Isolates changes: Software changes typically occur at the top and bottom layers. The top layer is the graphical user interface, and changes in requirements often directly affect the user interface. Most new and older versions of software will vary significantly in the user interface. The bottom layer is hardware, which changes faster than software development. Layered design can separate these changes, ensuring that changes in one area do not significantly impact other parts.
- Facilitates automated testing: Since each layer has independent features, it is easier to write test cases.
- Enhances program portability: Different parts of the platform are placed in separate layers through layered design. For instance, the lower module acts as a wrapper layer encapsulating the interface provided by the operating system, while the upper layer implements a graphical user interface for different platforms. When porting to different platforms, only different parts need to be implemented, and the middle layer can be reused.
[Insert Image]
Figure 2.2 Three-layer Structure
The application layer is at the top of the module and is directly used for functional implementation. For example, the application layer might have only one “human-computer interaction†module externally. Of course, several internal modules can be divided for use. The data transfer relationship between the three layers is very clear, that is, application layer → virtual layer → hardware driver layer, as shown in Figure 2.2. The solid lines in the figure represent dependencies, indicating that the application layer depends on the virtual layer, and the virtual layer depends on the hardware driver layer. A layered architecture offers the following advantages:
- Reduces system complexity: Since each layer is relatively independent, layers interact through well-defined interfaces, allowing each layer to be implemented separately, thus reducing the coupling between modules.
- Isolates changes: Software changes typically occur at the top and bottom layers. The top layer is the graphical user interface. Changes in requirements often directly affect the user interface. Most new and older versions of software will vary significantly in the user interface. The bottom layer is hardware. The change of hardware is faster than software development. Layered design can separate these changes, ensuring that changes in one area do not significantly impact other parts.
- Facilitates automated testing: Since each layer has independent features, it is easier to write test cases.
- Enhances program portability: Different parts of the platform are placed in separate layers through layered design. For instance, the lower module is a wrapper layer encapsulating the interface provided by the operating system, while the upper layer is a graphical user interface implemented for different platforms. When porting to different platforms, only different parts need to be implemented, and the middle layer can be reused.
[Insert Image]
Figure 2.2 Three-layer Structure
Now, let's delve into isolation of changes.
(1) Hollywood Principle (Hollywood):
Modules like keyboard scanning share the commonality of the calling relationship between layers, which remains constant over time. Even if upper and lower layers form a dependency relationship, the direct calling method is the simplest. To reduce the coupling between layers, communication between layers must follow certain rules. That is, the upper layer can directly call the function provided by the lower layer, but the lower layer cannot directly call the function provided by the upper layer, nor can layers call each other cyclically. Circular dependencies between layers can severely hinder the reusability and scalability of software, making each layer in the system unable to form a reusable component independently. Although the upper layer can also call the function provided by the adjacent lower layer, it cannot call across layers. In other words, the lower module implements the interface declared in the upper module and is called by the higher-level module. This is the famous Hollywood Extension Principle: "Don't call me, let me call you." When the lower layer needs to pass data to the upper layer, the callback function pointer interface is used to isolate changes. By inverting the dependent interface ownership, a more flexible, more durable, and easier-to-modify structure is created.
In fact, the expression of the callback function provided by the upper module (i.e., the caller) is to call another function through the function pointer in the lower module, that is, the address of the callback function is used as an argument to initialize the formal parameters of the lower module, and the lower module calls this function at some point. This function is a callback function, as shown in Figure 2.3. There are two ways to call it:
- In the function of the upper module A calling the lower module B, the callback function C is directly called;
- Using the registration method, when an event occurs, the lower module calls the callback function.
[Insert Image]
Figure 2.3 Use of Callback Function
At initialization, the upper module A passes the address of the callback function C as an argument to the lower module B. During operation, this callback function is called when the underlying module needs to communicate with the upper module. The calling mode is A→B→C, where the upper module A calls the lower module B. During the execution of B, the callback function is called to return information to the upper module. For the upper module, C not only monitors the running status of B but also interferes with the operation of B, which is essentially the upper module calling the lower module. Since the callback function is added, dynamic binding can be implemented on the fly. Here is an example of sorting arbitrary types of data using a standard bubble sort function.
(2) Data Comparison Function:
Assuming the data to be sorted is of int type, it is possible to exchange data by comparing the sizes of adjacent data. When two pointers e1 and e2 pointing to an int variable are given, the comparison function returns a number. If *e1 is less than *e2, the returned number is negative; if *e1 is greater than *e2, the returned number is positive; if *e1 is equal to *e2, then the returned number is zero, as shown in Listing 2.4.
Listing 2.4 compare_int() Data Comparison Function
```c
int compare_int(const int *e1, const int *e2)
{
return *e1 - *e2; // Ascending comparison
}
```
Since pointers of any data type can assign values to void* pointers, this feature can be utilized by using the void* pointer as a parameter to the data comparison function. When the function's formal parameter is declared as void *, although the bubbleSort() bubble sort function does not know what type of data the caller will pass inside, the caller knows the type of the data and the method of operation on the data. Then, the caller writes a data comparison function.
Since the caller must decide which data comparison function to call according to the actual situation at runtime, the function prototype is as follows according to the requirements of the comparison operation:
```c
typedef int (*COMPARE)(const void *e1, const void *e2);
```
Here, e1 and e2 are pointers to two values that need to be compared. When the return value is < 0, it means e1 < e2; when the return value = 0, it means e1 = e2; when the return value is > 0, it means e1 > e2.
When declared with a typedef, COMPARE becomes a function pointer type. With a type, you can define a function pointer variable of that type. For example:
```c
COMPARE compare;
```
At this point, as long as the function name (for example, compare_int) is used as the formal parameter of the argument initialization function, the corresponding data comparison function can be called. For example:
```c
COMPARE compare = compare_int;
```
Although the compiler sees a compare, the caller can implement a number of different types of compares, which can change the behavior of the function according to the type in the interface function. The implementation of the general data comparison function is shown in Listing 2.5.
Listing 2.5 Implementation of the Compare Data Comparison Function
```c
int compare_int(const void *e1, const void *e2)
{
return (*((int *)e1) - *((int *)e2)); // Ascending comparison
}
int compare_int_invert(const void *e1, const void *e2)
{
return *(int *)e2 - *(int *)e1; // Descending comparison
}
int compare_vstrcmp(const void *e1, const void *e2)
{
return strcmp(*(char**)e1, *(char**)e2); // String comparison
}
```
Note that if e1 is a large positive number and e2 is a large negative number, or vice versa, the calculation may overflow. Since it is assumed here that they are all positive integers, the risk is avoided.
Since the argument to the function is declared as a void * type, the data comparison function is no longer dependent on the specific data type. You can separate the changes in the algorithm, whether it is ascending or descending or string comparison, depending entirely on the callback function. Note that the reason you can't directly use strcmp() as a string comparison is because bubbleSort() passes the address of an array element of type char ** &array[i], instead of array[i] of type char*.
(3) BubbleSort() Bubble Sorting Function:
The standard function bubbleSort() is a classic example of using function pointers in C. A function that sorts an array of any type, where the size of the individual elements and the functions of the elements to be compared are given. Its prototype was initially defined as follows:
bubbleSort (parameter list);
Since bubbleSort() is sorting the data in the array, bubbleSort() must have a parameter to hold the starting address of the array, and a parameter to hold the number of elements in the array. In order to generalize or store the void * type element in the array, you can use the array to store any type of data passed by the user, so use the void * type parameter to save the starting address of the array. Its function prototype is as follows:
bubbleSort(void *base, size_t nmemb);
Since the type of the array is unknown, the length of the elements in the array is also unknown, and a parameter is also needed to save. Its function prototype evolved into:
bubbleSort(void *base, size_t nmemb, size_t size);
Here, size_t is a predefined type in the C standard library, specifically for saving the size of variables. The parameters base and nmemb identify this array, which is used to store the starting address of the array and the number of elements in the array. The size stores the size of a single element when it is packed.
At this point, if you pass a pointer to compare() as a parameter to bubbleSort(), you can "callback" compare() to compare the values. Since sorting is an operation on data, bubbleSort() has no return value, its type is void, and the bubbleSort() function interface is shown in Listing 2.6.
Listing 2.6 BubbleSort() Bubble Sort Function Interface (bubbleSort.h)
```c
#pragma once;
void bubbleSort(void *base, size_t nmemb, size_t size, COMPARE compare);
```
Although most beginners also choose callback functions, they often use global variables to hold intermediate data. The solution proposed here is to pass a parameter called "callback function context" to the callback function whose variable name is base. In order to accept any data type, select void * to represent this context. "Context" means that if an int type value is passed in, the int type data comparison function is called back; if a string is passed in, the string comparison function is called back.
When bubbleSort() declares base as a void * type, it allows bubbleSort() to use the same code to support different types of data comparisons. The key is the type field, which allows the type of data at runtime. Call different functions. This behavior of associating a function body with a function call at runtime based on the type of data is called dynamic binding, so the binding of a function occurs at runtime rather than at compile time, which is said to be polymorphic. Obviously, polymorphism is a runtime binding mechanism whose purpose is to bind the function name to the implementation code of the function. The name of a function is closely related to its entry address. The entry address is the starting address of the function in memory, so polymorphism is the runtime binding mechanism that dynamically binds the function name to the function entry address. bubbleSort() See Appendix 2.7 and Listing 2.8 for the interface and implementation.
Listing 2.7 BubbleSort() Interface (bubbleSort.h)
```c
#pragma once
#include
typedef int(*COMPARE)(const void *e1, const void *e2);
void bubbleSort(void *base, size_t nmemb, size_t size, COMPARE compare);
```
Listing 2.8 Implementation of the BubbleSort() Interface (bubbleSort.c)
```c
#include "bubbleSort.h"
void byte_swap(void *pData1, void *pData2, size_t stSize)
{
unsigned char *pcData1 = pData1;
unsigned char *pcData2 = pData2;
unsigned char ucTemp;
while (stSize--){
ucTemp = *pcData1; *pcData1 = *pcData2; *pcData2 = ucTemp;
pcData1++; pcData2++;
}
}
void bubbleSort(void *base, size_t nmemb, size_t size, COMPARE compare)
{
int hasSwap=1;
for (size_t i = 1; hasSwap && i < nmemb; i++) {
hasSwap = 0;
for (size_t j = 0; j < nmemb - 1; j++) {
void *pThis = ((unsigned char *)base) + size*j;
void *pNext = ((unsigned char *)base) + size*(j+1);
if (compare(pThis, pNext) > 0) {
hasSwap = 1;
byte_swap(pThis, pNext, size);
}
}
}
}
```
Static Type and Dynamic Type:
The static and dynamic type refer to the time when the name is bound to the type. If all the variables and expression types are fixed at compile time, it is called static binding; if all variables and expressions are of type until known at runtime, it is called dynamic binding.
Suppose you want to implement a bubble sort function for any data type and simply test it. The requirement is that the same function can be arranged from large to small, from small to large, and supports multiple data types at the same time. Such as:
```c
int array[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};
```
Obviously, just calling the comparison function's entry address compare_int to compare, you can call bubbleSort():
```c
int array[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};
bubbleSort(array, numArray, sizeof(array[0]), compare_int);
```
When the number is small, the performance of all sorting algorithms is not much different, because the advanced algorithm only shows a significant improvement in performance when the number of elements is more than 1000. In fact, in more than 90% of cases, the number of elements we store is only tens to hundreds, so bubble sorting may be a better choice. The implementation and usage of the bubbleSort() sample program are detailed in Listing 2.9.
Listing 2.9 BubbleSort() Bubble Sorting Sample Program
```c
#include
#include
#include "bubbleSort.h"
int compare_int(const void *e1, const void *e2)
{
return *(int *)e1 - *(int *)e2;
}
int compare_int_r(const void *e1, const void *e2)
{
return *(int *)e2 - *(int *)e1;
}
int compare_str(const void *e1, const void *e2)
{
return strcmp(*(char **)e1, *(char **)e2);
}
void main()
{
int arrayInt[] = { 39, 33, 18, 64, 73, 30, 49, 51, 81 };
int numArray = sizeof(arrayInt) / sizeof(arrayInt[0]);
bubbleSort(arrayInt, numArray, sizeof(arrayInt[0]), compare_int);
for (int i = 0; i < numArray; i++) {
printf("%d ", arrayInt[i]);
}
printf("\n");
bubbleSort(arrayInt, numArray, sizeof(arrayInt[0]), compare_int_r);
for (int i = 0; i < numArray; i++) {
printf("%d ", arrayInt[i]);
}
printf("\n");
char *arrayStr[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
numArray = sizeof(arrayStr) / sizeof(arrayStr[0]);
bubbleSort(arrayStr, numArray, sizeof(arrayStr[0]), compare_str);
for (int i = 0; i < numArray; i++) {
printf("%s", arrayStr[i]);
}
}
```
It can be seen that both the caller main() and the compare_int() callback function belong to the upper module, and bubbleSort() belongs to the lower module. When the upper module calls the lower module bubbleSort(), the address compare_int of the callback function is passed as a parameter to bubbleSort(), and then compare_int() is called. Obviously, using the parameter-passing callback function, the lower module does not need to know which function of the upper module needs to be called, thereby reducing the connection between the upper and lower layers, so that the upper and lower layers can be modified independently without affecting the implementation of another layer of code. In this way, each time you call bubbleSort(), bubbleSort() does not have to be modified as long as you give a different function name as an argument.
The greatest advantage of using a callback function is that it facilitates the layered design of software modules and reduces the coupling between software modules. That is, the callback function can isolate the caller from the callee, and the caller does not need to care who the callee is. When a specific event or condition occurs, the caller will call the callback function with the function pointer to handle the event.