Some "saucy operations" of C language and their deep understanding

Edited from: https://mp.weixin.qq.com/s/lmhp4hHdxHfseRRHukTf4w

In this series of articles, Mr. Yu Zhennan will tell you some advanced knowledge of embedded C language, commonly known as "Sao operation", which will help you to take your level to a higher level!
C language is a very flexible and powerful programming language. For the same algorithm and function, we can write it in a satisfactory way, or we can write it in an obscure way. Moreover, many people who claim to be programming masters just like to write the program into a bible, thinking that it can realize the correct function even if it is incomprehensible to others, which is a manifestation of superb technology. I can't comment on the advisability of such an approach, because everyone has their own style and personality. Let him program against his will, and programming may become dull and uninteresting. Many times it is not that we want to write programs that are difficult to understand, but that we want to understand other people's programs. In this article, Zhennan lists some programming techniques that I have seen and used, and makes an in-depth analysis.

1. The essence of a string is a pointer

String is the most basic concept in C language, and it is also the most commonly used. In embedded development, we often want to display some strings on the serial port assistant or debugging terminal through the serial port as a message prompt, so that we can understand the running status of the program; or convert the value of some constants into strings to Displayed on LCD and other display devices. So, what exactly is a string in C language? In fact, the string itself is a pointer, and its value (that is, the address pointed to by the pointer) is the address of the first character of the string. In order to explain this problem, I often give an example: how to convert a value into the corresponding hexadecimal string. For example, convert 100 to "0X64". We could write a function like this:

void Value2String(unsigned char value,char *str)
{

 char *Hex_Char_Table="0123456789ABCDEF";

 str[0] = '0';
 str[1] = 'X';
 str[4] = 0;

 str[2]=Hex_Char_Table[value>>4];

 str[3]=Hex_Char_Table[value&0X0F];
}

String constants are essentially byte sequences in memory, as shown in the following figure:

pictureAbove, Zhennan said that "the string itself is a pointer", so the moment to witness the true meaning of this sentence has come, we will simplify the above program:

void Value2String(unsigned char value,char *str)
{

 str[0]='0';str[1]='X';str[4]=0; 

 str[2]="0123456789ABCDEF"[value>>4];

 str[3]="0123456789ABCDEF"[value&0X0F];
}

The pointer variable Hex_Char_Table is actually redundant, "the string itself is a pointer", so you can directly use [] with the subscript to extract the characters in it. Any variable or constant that is essentially a pointer type (that is, expresses the address meaning) can directly use [] or * to access the data elements in the data sequence it points to.

Second, the escape character \

To express a byte data sequence (several bytes stored continuously in memory) in C language, we can use byte arrays, such as unsigned char array[10]={0,1,2,3,4,5, 6,7,8,9}. In fact, a string is essentially a sequence of bytes, but usually the values ​​of the bytes it stores are the code values ​​of printable characters in ASCII, such as 'A', ' ', '|' and so on. Can other values ​​appear in the string? In this way, we can express a sequence of bytes in the form of a string. Many times, it may be more convenient than byte array. The escape character in the string is used to do this. Please see the following procedure:

const unsigned char array[10]={0,1,2,3,4,5,6,7,8,9};

char *array="\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09";

In these two ways of writing, the memory byte sequence pointed to by array is basically the same (the latter has a 0 at the end). Of course, if we pass array to strlen to calculate the length, the returned value is 0. Because its first byte has a value of 0. But we can still use array[n] to access the data in the sequence.

char *str="ABCDEFG";

char *str="\x41\x42\x43\x44\x45\x46\x47";

The two writing methods in the above program are completely equivalent.

The purpose of escape characters in strings is to express other numeric or special characters in sequences where only ASCII-printable characters should be seen. For example, the frequently used carriage return and line feed "\r\n", its essence is "\x0d\x0a"; usually what we call the string end character \0 is actually the octal escape expression of 0.

Third, the connection of string constants

While studying the source code of some open source software, I saw a rather unusual usage of string constants, which I will introduce to you here. Sometimes, in order to make the content level of string constants clearer, you can break up a long string into several short strings, which are connected end to end in sequence, which is equivalent to long strings in meaning. For example, "0123456789ABCDEF" can be decomposed into "0123456789" "ABCDEF", that is, multiple string constants can be directly connected to grow a string. This way of writing may be more used when printf prints debugging information.

printf("A:%d B:%d C:%d D:%d E:%d F:%d\r\n",1,2,3,4,5,6);

printf("A:%d " \
    "B:%d " \
    "C:%d " \
    "D:%d " \
    "E:%d " \
    "F:%d\r\n",1,2,3,4,5,6);

When the format string of printf is very long, we break it up reasonably and divide it into multiple lines, so that the program will appear more tidy.

4. Splitting skills of long strings

Many times we need to split long strings. The question is how to achieve it? Many people may think of using that separator character, such as spaces and commas. Then go to a number of delimiting characters in front of the parameters to be extracted, and then form a new short string of characters at the corresponding positions. As shown in the figure below: pictureThis method is certainly feasible, but it is a bit clumsy. In fact, for such a long string with obvious separators, we can adopt the idea of ​​"breaking up" or "exploding". The specific process is as follows: replace all separators in the long string with '\0' , which is the string terminator. At this point, the long string is decomposed into several short strings stored sequentially in memory. If you want to take out the nth short string, you can use this function:

char * substr(char *str,n)
{
 unsigned char len=strlen(str);

 for(;len>0;len--) {if(str[len-1]==' ') str[len-1]=0;}

 for(;n>0;n--)
 {
  str+=(strlen(str)+1);
 } 

 return str;
}

Many times we need to access multiple short strings in a long string at one time. Zhennan often does this: through a loop, replace all separators in the long string with '\0', here During the process, the position of the first character of each short string is recorded into an array, the code is as follows:

unsigned char substr(unsigned char \*pos,char \*str)
{

 unsigned char len=strlen(str);
 unsigned char n=0,i=0;

 for(;i<len;i++) 
 {
 if(str[i]==' ') 
 {
 str[i]=0;pos[n++]=(i+1);
 }
 }

 return n;
}

For example: we want to extract "abc", "50" and "off" in "abc 1000 50 off 2500", which can be achieved by using the above function.

unsigned char pos[10];
char str[30]; 

strcpy(str,"abc 1000 50 off 2500");

substr(pos,str);
str+pos[0]; //"abc"
str+pos[2]; //"50"
str+pos[3]; //"off"

5. Take out the digits of the value

In actual projects, we often need to extract some digits of a value, such as using a digital tube to display a value or converting a value into a string, which will involve this operation. So how to achieve this operation? Although this question seems simple, there are not a few people who ask this question. Consider the function below:

void getdigi(unsigned char *digi,unsigned int num)
{

 digi[0]=(num/10000)%10;
 digi[1]=(num/1000)%10;
 digi[2]=(num/100)%10;
 digi[3]=(num/10)%10;
 digi[4]=num%10;
}

Its main operations are division and remainder. This function just takes out the digits of each digit of an integer number, what about floating point?

In fact, it is the same reason, please see the following function (we use 4 digits for both the integer and the decimal part by default).

void getdigi(unsigned char *digi1,unsigned char *digi2,unsigned float num)
{
 unsigned int temp1=num;
unsigned int temp2=((num-temp1)\*10000);

 digi1[0]=(temp1/1000)%10;
 digi1[1]=(temp1/100)%10;
 digi1[2]=(temp1/10)%10;
 digi1[3]=(temp1)%10;

 digi2[0]=(temp2/1000)%10;
 digi2[1]=(temp2/100)%10;
 digi2[2]=(temp2/10)%10;
 digi2[3]=(temp2)%10;
}

Some people say that I prefer to use the sprintf function to directly format and print the value into a string, and you will get it naturally.

char digi[10];

sprintf(digi,"%d",num); //**整型 

char digi[10];

sprintf(digi,"%f",num); //**浮点

no problem. But using the sprintf function on an embedded platform usually costs a lot.

As an embedded engineer, you must cherish words like gold, especially when hardware resources are relatively tight. sprintf is very powerful, we are just a simple operation of extracting numerical numbers or converting numerical values ​​into corresponding strings, using it is a bit wasteful. At this time, I usually choose to write a small function or macro to implement it myself.

6. The essence and usage skills of printf

printf is an entry-level standard library function that we are very familiar with. Whenever we say the computer golden sentence "Hello World!", we actually mention it inadvertently: printf("hello world!"); Outputting any variables, constants, and strings in a specific format, base, or form provides us with great convenience, and even becomes an important Debug method for many people when debugging programs. But in embedded, we need to analyze its essence. The bottom layer of the printf function is based on a function of fputc, which is used to realize the specific output method of a single character, such as displaying the character on the display, or storing it in an array (similar to sprintf), or sending it out through the serial port , not even a serial port, but an interface such as Ethernet, CAN, I2C, etc. The following is the implementation of the fputc function in an STM32 project:

int fputc(int ch, FILE \*f)
{
while((USART1->SR&0X40)==0);
{
USART1->DR = (u8) ch;
}

return ch;
}

In fputc, ch is sent through USART1. In this way, when we call printf, the corresponding information will be printed from USART1.

"I know what you said above, what's new!" Indeed, it is common for us to print information through the serial port. So, have you seen the fputc below?

int fputc(int ch, FILE *f)
{
 LCD_DispChar(x,y,ch);
 x++;
 if(x>=X_MAX)
 {
     x=0;y++;

      if(y>=Y_MAX)*
      {
       y=0;
      }
 }
     return ch;
}

This fputc displays the characters on the LCD (while maintaining the display position information of the characters), so that when we call printf, the information will be directly displayed on the LCD.

To put it bluntly, fputc is a directional output of data. In this way, we can make printf more flexible to meet more diverse application requirements. In Zhennan’s projects, there was such a situation: MCU has multiple serial ports, serial port 1 is used to print debugging information, serial port 2 communicates with ESP8266 WIFI module, and serial port 3 communicates with SIM800 GPRS module. All three serial ports need formatted output, but there is only one printf, what should I do? Our solution is to modify fputc so that printf can be time-multiplexed by 3 serial ports. The specific implementation is as follows:

unsigned char us=0; 

int fputc(int ch,FILE *f)
{
 switch(us)
 {
  case 0:
  while((USART1->SR&0X40)==0);
  USART1->DR=(u8)ch;  
  break;

  case 1:
  while((USART2->SR&0X40)==0);
  USART2->DR=(u8)ch;  
  break;

  case 2:
while((USART3->SR&0X40)==0);
USART3->DR=(u8)ch;  
break;
 }

 return ch;
}

When calling, assign different values ​​to us according to the needs, and printf will be used by whoever uses it.

#define U_TO_DEBUG     us=0;
#define U_TO_ESP8266    us=1;
#define U_TO_SIM800     us=2;

U_TO_DEBUG
printf("hello world!");

U_TO_ESP8266
printf("AT\r\n");


U_TO_SIM800
printf("AT\r\n");

7. About the transmission of floating point numbers

Many people can't use and deal with floating point very well. The main root cause is that they don't understand its expression and storage methods very well. The most typical example is that people often ask me: "How to use a serial port to send a floating point number?" We know that there are many data types in C language, among which unsigned char, unsigned short, unsigned int, and unsigned long are called integers, As the name suggests they can represent integers. The range of values ​​that can be expressed is related to the number of bytes occupied by the data type. The expression method of the value is simple, as shown in the figure below: pictureone byte can express 0~255, two bytes (unsigned short) can naturally express 0~65535, and so on. When we need to send an integer value, we can do this:

unsigned short a=0X1234; 

UART_Send_Byte(((unsigned char *)&a)[0]);

UART_Send_Byte(((unsigned char *)&a)[1]);

That is to say, it is sufficient to send several bytes constituting the integer in order. Of course, the receiver must know how to restore the data, that is to say, it must know what type of bytes it has received together, which is guaranteed by the specific communication protocol.

unsigned char buf[2];
usnigned short a; 

UART_Receive_Byte(buf+0);
UART_Receive_Byte(buf+1); 

a=(*(usnigned short *)buf);

OK, it's easier to understand about integers. But switching to float, many people are a little confused. Because the numerical expression of float is somewhat complicated. Some people use the following method to send floating point:

float a=3.14;

char str[10]={0}; 

ftoa(str,a); //**浮点转为字符串* *即3.14**转为"3.14"

UART_Send_Str(str); //**通过串口将字符串发出

Obviously, this method is very "amateur".

Some people also asked me: "The numbers before and after the floating-point small number can be sent, but how to send the decimal point?" This nakedly reflects his misunderstanding of the floating-point type.

Don't be confused by the appearance of the float value, it is actually just 4 bytes, as shown in the following figure:

picture

So, the correct way to send floating point numbers is this:

float a=3.14;

UART_Send_Byte(((unsigned char *)&a)[0]);

UART_Send_Byte(((unsigned char *)&a)[1]);

UART_Send_Byte(((unsigned char *)&a)[2]);

UART_Send_Byte(((unsigned char *)&a)[3]);

The receiver restores the data to float:

unsigned char buf[4];
float a; 

UART_Receive_Byte(buf+0);

UART_Receive_Byte(buf+1);

UART_Receive_Byte(buf+2);

UART_Receive_Byte(buf+3); 

a=*((float *)buf);

In fact, we should discover the essence of the data type: no matter what the data type is, its basic composition is nothing more than a few bytes stored in memory. It's just that we artificially endow these bytes with a specific encoding method or numerical expression. After seeing through these, we realize the nature of data, and we can even manipulate data directly.

8. Direct manipulation of data

Manipulate data directly? Let's give an example: take the opposite of an integer. The general implementation method is as follows:

int a=10;
int b=-a; //-1*a;

Such an operation may involve a multiplication operation, which takes more time. When we understand the essence of integer numbers, we can do this:

int a=10;

int b=(~a)+1;

This may not be enough to explain the problem, then let's look at another example: take the opposite of a floating point number. It seems the only way to do it is:

float a=3.14;

float b=a*-1.0;

Actually, we can do it like this:

float a=3.14;
float b;

((unsigned char *)&a)[3]^=0X80;
b=a;

That's right, we can directly modify the sign bit of the high byte of the floating point in memory. This is much more efficient than multiplying by -1.0.

Of course, these operations require you to have a perfect grasp of pointers in C language.

Nine, rounding and comparison of floating point

Let's talk about the first question first: how to realize the rounding of floating point? Many people have encountered this problem, but it is actually very simple, just add the floating point + 0.5 and round it up. OK, the second question: the comparison of floating point. There is still a need to elaborate on this issue. First of all, we need to know that the judgment in C language, that is, ==, is a strong matching behavior. That is, both sides of the comparison must have every bit exactly the same before they are considered equal. This is fine for integers. But the float type is not applicable, because two seemingly equal floating-point numbers, in fact, their memory representation cannot guarantee that every bit is exactly the same. At this time, we make an agreement: as long as the difference m between two floating points is small enough, they are considered equal, and m generally takes 10e-6. That is, two floating-point numbers are considered equal as long as they have the same 6 decimal places. It is precisely because of this agreement that many C compilers set the precision of float to 7 digits after the decimal point, such as ARMCC (MDK compiler).

float a,b;

if(a==b) 
...  //**错误

if(fabs(a-b) <= 0.000001) 
...//**正确

Ten, superb for loop

We are very familiar with the for loop, and usually we use it quite well, as in the following example:

int i;

for(i=0;i<100;i++)
{...}

However, if we have a deeper understanding of the nature of the for loop, we can use it to perfection.

The thing in the parentheses behind the for is called the "loop control body", which is divided into three parts, as shown in the figure below: A, B, and C. In fact, the three parts are very random and can be any expression. So, we can write an infinite loop like this:

for(1;1;1) //1**本身就是一个表达式:常量表达式
{
  ...
}

Of course, we often simplify it to:

for(;;)
{
  ...
}

Since A in the loop control body is just an initialization operation before the loop starts, it should be fine for me to write like this:

int i=0;

 for(printf("Number:\r\n");i<10;i++)
 {
  printf("  %d\r\n",i);
 }

B is the condition of loop execution, and C is the operation after loop execution, then we can write a standard if statement in the form of for to achieve the same function:

if(strstr("hello world!","abc"))
{
 printf("Find Sub-string");
}
char *p;

for(p=strstr("hello world!","abc");p;p=NULL) 
{
 printf("Find Sub-string");
}

The above example may be a bit tasteless, "Why should I use for when an if can handle things?", that's right. We are here mainly to explain the flexible usage of the for loop. A deep understanding of its essence will help us get twice the result with half the effort in actual development and understand other people's code.

Let me list a few more examples of the flexible application of the for loop for your aftertaste. example 1:

 char *p; 

 for(p="abcdefghijklmnopqrstuvwxyz"; printf(p); p++) 
 printf("\r\n");

Tip: We are too familiar with printf, but how many people know that printf has a return value? What should the output look like?

Example 2:

char *p;
unsigned char n;

for(p="ablmnl45ln",n=0;((\*p=='l')?(n++):0),\*p;p++);

Hint: Remember the ternary and comma expressions in C? What should n be equal to?

Example 3:

unsigned char *index="C[XMZA[C[NK[RDEX@";

char *alphabet="EHUIRZWXABYPOMQCTGSJDFKLNV ";

int i=0; 

for(;(('@'!=index[i])?1:(printf("!!Onz\r\n"),0));i++) 
{
printf("%c",alphabet[index[i]-'A']);
}

Reminder: Tianshu mode is turned on. If you don't understand, you may miss something!

END

*The copyright belongs to the original author, if there is any infringement, please contact to delete. *
RTOS implements dual-core MCU message communication
After I joined the Linux driver engineer, I only knew the truth...

Guess you like

Origin blog.csdn.net/qq_41854911/article/details/131525129