[C Language] Debugging questionable function pointers and writing callback functions (Callback function parameters in the structure and false callback function __weak declaration)

[C Language] Debugging questionable function pointers and writing callback functions (Callback function parameters in the structure and false callback function __weak declaration)

Article directory

  • Debugging questionable function pointers
    • function pointer
    • function call
  • Writing callback function
    • callback function inside the structure
    • fake callback function
  • Appendix: Compressed strings, big and small endian format conversion
    • Compressed string
      • floating point number
      • Compressed Packed-ASCII strings
    • Big-endian conversion
      • What are big endian and little endian
      • Endianness in data transfer
      • Summarize
      • Big-endian conversion function

Debugging questionable function pointers

Function pointer

First of all, a function pointer is a pointer to a function address.
The address can be represented by the (void *) type

For example, function:

void a(int i)
{<!-- -->
    printf("i: %d\\
",i);
}

Its address is (void *) & amp;a
Then we can define a typedef to point to the pointer

void a(int i)
{<!-- -->
    printf("i: %d\\
",i);
}
typedef void (*abc)(int i);
fxn= &a;

Similarly, if the return value of a is of int type, it can also be achieved through strong conversion.

int a(int i)
{<!-- -->
    printf("i: %d\\
",i);
}
typedef void (*abc)(int i);
fxn=(void *) & amp;a;

But it is not recommended to do this because if you do this, fxn will have no return value.
It is better to change typedef to typedef int (*abc)(int i);
In this way, you can also get the return value when calling the fxn function.

Function call

Now we change the declaration of function a to void
And when assigning a value to fxn, there is no need to force transfer with (void *)
Back to the original state
Consider the following code:

#include 
typedef void (*abc)(int i);

void a(int i)
{<!-- -->
    printf("i: %d\\
",i);
}

int main(void)
{
    abc fxn;

    fxn=a;
    printf("Get the content and call %d\\
",fxn);
    fxn(1);
    ((abc) fxn) (2);
    ((void (*)(int)) fxn) (3);

    (*fxn)(4);
    (* (abc) fxn) (5);
    (* (void (*)(int)) fxn) (6);

    fxn= &a;
    printf("Get address and call %d\\
",fxn);
    fxn(1);
    ((abc) fxn) (2);
    ((void (*)(int)) fxn) (3);

    (*fxn)(4);
    (* (abc) fxn) (5);
    (* (void (*)(int)) fxn) (6);
}
 

operation result:

Get content call 4199760
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6
Get address call 4199760
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6

The upper part is the content of directly calling the fxn function
The lower part is the address of calling the fxn function

Among them, when i is 1 2 3, it is called directly; when it is 4 5 6, it is called after fetching the content.

First, by printing the fxn variable twice, we found that both are 4199760
That is to say
Function content = function address

fxn= & amp;fxn

Then it is also true to get the content above the address in reverse.

*fxn=*( & amp;fxn)

Some people may say that fxn is the address during typedef.
This is indeed the case, because function pointers can only be defined this way
But the difference between function pointers and ordinary variable pointers is that

Assigning a variable pointer to a variable cannot be compiled.
Assigning a variable pointer to a variable address results in the variable address.
When a function pointer is assigned to a function, the function address is obtained
Assigning a function pointer to a function address results in the function address

To put it bluntly, it is a compilation problem

Strictly speaking, the first line of the following code is irregular, but after compiler optimization, both are the same.

fxn=a;
fxn= &a;

However, if you replace it with variables and variable pointers, an error will be reported.

int b=1;
int * c=b;

This is the difference between function pointer and variable pointer

So go back to our test code just now
The following six calling methods are exactly the same:

 fxn(1);
    ((abc) fxn) (2);
    ((void (*)(int)) fxn) (3);

    (*fxn)(4);
    (* (abc) fxn) (5);
    (* (void (*)(int)) fxn) (6);

And the same goes for function assignment:

fxn=a;
fxn= &a;

so:

However, in order to ensure code readability as much as possible, it is recommended to use the following format:

fxn=a;
fxn(1);

Callback function writing

The callback function here is the function pointer just mentioned
The callback function can be passed to other functions in the form of parameters.
thus being called within other functions

Callback function within the structure

This method is often seen in ecological SDKs such as TI ADI.
Among them, TI likes to have a structure inside a structure the most. Even if there is only one variable, a structure must be inside it.
For example, TI millimeter wave radar SDK:
definition:


Assignment:

transfer:

You can see that whether it is definition, assignment or call, it is the simplest method.

We can define a structure and put the function pointer variable we just defined into it:

typedef void (*abc)(int i);

typedef struct
{<!-- -->
    abc fxn;
}text;

When calling, use the simplest method to assign a value and call:

text stu;
stu.fxn=a;
stu.fxn(1);

Complete code:

#include 
typedef void (*abc)(int i);

typedef struct
{<!-- -->
    abc fxn;
}text;

void a(int i)
{<!-- -->
    printf("i: %d\\
",i);
}

int main(void)
{
   text stu;
   stu.fxn=a;
   stu.fxn(1);
}
 

Hypocritical callback function

In the STM32 ecosystem, for example, there are many callback functions in the HAL library.
Its definitions all use the __weak statement
That is __attribute__((weak))
It is a modified variable in the GNU compiler
Used to tell the compiler that this function can be overridden and modified.
definition:

transfer:

As for the assignment, the user has to write it himself.
for example:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{<!-- -->
if(huart== & RF_UART_Handle)
  {<!-- -->
//HAL_UART_Transmit( & amp;RF_UART_Handle, & amp;RxBuffer,1,0xFFFF);
HAL_UART_Receive_IT( & amp;RF_UART_Handle, & amp;RxBuffer,1);
  }
}

Although this kind of callback function is called Callback, it actually has nothing to do with the function pointer and is not a real callback function.

Appendix: Compressed strings, big and small endian format conversion

Compressed string

First, the HART data format is as follows:


The focus is on floating point numbers and string types
I won’t talk about Latin-1. It’s basically not used.

Floating point numbers

In floating point numbers, such as 0x40 80 00 00 means 4.0f

In the HART protocol, floating point numbers are sent in big-endian format, that is, the high bit is sent first and the low bit is sent last.

The array sent is: 40,80,00,00

But in the C language, floating point numbers are stored in little-endian format, that is, 40 is in the high bit and 00 is in the low bit.
Floating point: 4.0f
Address 0x1000 corresponds to 00
Address 0x1001 corresponds to 00
Address 0x1002 corresponds to 80
Address 0x1003 corresponds to 40

If you use the memcpy function directly, you need to perform big-endian conversion, otherwise it will be stored as:
Address 0x1000 corresponds to 40
Address 0x1001 corresponds to 80
Address 0x1002 corresponds to 00
Address 0x1003 corresponds to 00

Big-endian conversion:

void swap32(void * p)
{<!-- -->
   uint32_t *ptr=p;
   uint32_t x = *ptr;
   x = (x << 16) | (x >> 16);
   x = ((x & amp; 0x00FF00FF) << 8) | ((x >> 8) & amp; 0x00FF00FF);

   *ptr=x;
}

Compressed Packed-ASCII string

Essentially, the highest 2 bits of the original ASCII are removed and then spliced together, such as spaces (0x20)
After splicing four spaces, it becomes
1000 0010 0000 1000 0010 0000
Hexadecimal: 82 08 20
After checking the table, the ones before 0x20 cannot be recognized.
That is, only the ASCII table 0x20-0x5F can be recognized

Write the compression/decompression function later:

//The strings and numbers passed in must be declared in advance, and the string size is at least str_len and the array size is at least str_len%4*3 str_len must be a multiple of 4
uint8_t Trans_ASCII_to_Pack(uint8_t * str, uint8_t * buf, const uint8_t str_len)
{<!-- -->
   if(str_len%4)
   {<!-- -->
      return 0;
   }
\t 
   uint8_t i=0;
   memset(buf,0,str_len/4*3);
   for(i=0;i<str_len;i + + )
   {<!-- -->
      if(str[i]==0x00)
      {<!-- -->
         str[i]=0x20;
      }
   }

   for(i=0;i<str_len/4;i + + )
   {<!-- -->
      buf[3*i]=(str[4*i]<<2)|((str[4*i + 1]>>4) & amp;0x03);
      buf[3*i + 1]=(str[4*i + 1]<<4)|((str[4*i + 2]>>2) & amp;0x0F);
      buf[3*i + 2]=(str[4*i + 2]<<6)|(str[4*i + 3] & amp;0x3F);
   }

   return 1;
}

//The strings and numbers passed in must be declared in advance, and the string size is at least str_len and the array size is at least str_len%4*3 str_len must be a multiple of 4
uint8_t Trans_Pack_to_ASCII(uint8_t * str, uint8_t * buf, const uint8_t str_len)
{<!-- -->
   if(str_len%4)
   {<!-- -->
      return 0;
   }

   uint8_t i=0;

   memset(str,0,str_len);

   for(i=0;i<str_len/4;i + + )
   {<!-- -->
      str[4*i]=(buf[3*i]>>2) & amp;0x3F;
      str[4*i + 1]=((buf[3*i]<<4) & amp;0x30)|(buf[3*i + 1]>>4);
      str[4*i + 2]=((buf[3*i + 1]<<2) & amp;0x3C)|(buf[3*i + 2]>>6);
      str[4*i + 3]=buf[3*i + 2] & amp;0x3F;
   }

   return 1;
}


Big-endian conversion

When parsing data such as serial ports, it is inevitable to encounter problems with big and small endian formats.

What are big endian and little endian

The so-called big-endian mode means that the high-order bytes are arranged at the low address end of the memory, and the low-order bytes are arranged at the high address end of the memory.

The so-called little endian mode means that the low-order bytes are arranged at the low address end of the memory, and the high-order bytes are arranged at the high address end of the memory.

To put it simply: big endian – high end, little endian – low end

For example, the number 0x12 34 56 78 is represented in memory as:

1) Big endian mode:

Low address ——————> High address

0x12 | 0x34 | 0x56 | 0x78

2) Little endian mode:

Low address ——————> High address

0x78 | 0x56 | 0x34 | 0x12

It can be seen that the big-endian mode is similar to the storage mode of strings.

Big and small endianness in data transmission

For example, the address bits and start and end bits are generally in big-endian format.
like:
Starting bit: 0x520A
Then the buf sent should be {0x52,0x0A}

The data bits are generally in little-endian format (single byte has no big or small endian distinction)
like:
A 16-bit data is sent out as {0x52,0x0A}
The corresponding uint16_t type number is: 0x0A52

For floating point number 4.0f converted to 32 bits it should be:
40 80 00 00

In terms of big-endian storage, the buf sent out is sent in sequence 40 80 00 00

For little-endian storage, 00 00 80 40 is sent.

Since memcpy and other functions copy by byte address, the copy format is little-endian format, so when the data is stored in little-endian, there is no need to perform big-endian conversion.
like:

uint32_t dat=0;
uint8_t buf[]={<!-- -->0x00,0x00,0x80,0x40};
   memcpy( & amp;dat,buf,4);
   float f=0.0f;
   f=*((float*) & amp;dat); //Forced address transfer
   printf("%f",f);

Or better solution:

 uint8_t buf[]={<!-- -->0x00,0x00,0x80,0x40};
   float f=0.0f;
   memcpy( & amp;f,buf,4);

For data stored in big endian (such as HART protocol data, all in big endian format), the copied format is still in little endian format. Therefore, when the data is stored in little endian, big and small endian conversion is required.
like:

uint32_t dat=0;
uint8_t buf[]={<!-- -->0x40,0x80,0x00,0x00};
   memcpy( & amp;dat,buf,4);
   float f=0.0f;
   swap32( & amp;dat); // Big-endian conversion
   f=*((float*) & amp;dat); //Forced address transfer
   printf("%f",f);

or:

uint8_t buf[]={<!-- -->0x40,0x80,0x00,0x00};
   memcpy( & amp;dat,buf,4);
   float f=0.0f;
   swap32( & amp;f); // Big-endian conversion
   printf("%f",f);

Or better solution:

uint32_t dat=0;
uint8_t buf[]={<!-- -->0x40,0x80,0x00,0x00};
   float f=0.0f;
   dat=(buf[0]<<24)|(buf[0]<<16)|(buf[0]<<8)|(buf[0]<<0)
   f=*((float*) & amp;dat);

Summary

Solid. If the data is in little-endian format, you can directly use the memcpy function to convert it. Otherwise, the address is forced to be transferred by shifting.

For multi-bit data, such as transmitting two floating point numbers at the same time, you can define the structure and then copy it with memcpy (the data is in little-endian format)

For little-endian data, you can write it directly with memcpy. If it is a floating point number, there is no need to perform a forced transfer.

For big-endian data, if you don’t mind the trouble or want to make the code more concise (but the execution efficiency will be reduced), you can also use memcpy to write the structure first and then call the big-endian conversion function. However, it should be noted here that the structure must be all unsigned. Integer floating point types can only be forced to be converted again after big and small endian conversion and writing. If floating point types are used in the structure, they need to be forced to be converted twice.

Therefore, for big-endian data, it is recommended to assign values by shifting, then perform forced conversion of individual numbers, and then write them into the general structure.

Multiple structures with different variable sizes require byte alignment issues
You can use #pragma pack(1) to align it to 1
But it will affect efficiency

Big-endian conversion function

It is implemented directly by operating on the address. The variable passed in is a 32-bit variable.
The intermediate variable ptr is the address of the incoming variable

void swap16(void * p)
{
   uint16_t *ptr=p;
   uint16_t x = *ptr;
   x = (x << 8) | (x >> 8);

   *ptr=x;
}

void swap32(void * p)
{<!-- -->
   uint32_t *ptr=p;
   uint32_t x = *ptr;
   x = (x << 16) | (x >> 16);
   x = ((x & amp; 0x00FF00FF) << 8) | ((x >> 8) & amp; 0x00FF00FF);

   *ptr=x;
}

void swap64(void * p)
{
   uint64_t *ptr=p;
   uint64_t x = *ptr;
   x = (x << 32) | (x >> 32);
   x = ((x & amp; 0x0000FFFF0000FFFF) << 16) | ((x >> 16) & amp; 0x0000FFFF0000FFFF);
   x = ((x & amp; 0x00FF00FF00FF00FF) << 8) | ((x >> 8) & amp; 0x00FF00FF00FF00FF);

   *ptr=x;
}