Detailed Explanation of Hardware Communication from SCM to C/C++ Pointer

 A theoretical overview of single-chip microcomputer

1.1 Single Chip Microcomputer (Single Chip Microcomputer) is referred to as single chip microcomputer, which integrates the main functional components (CPU, RAM, ROM, I/O port, timer/counter, serial port, etc.) A complete microcomputer. Commonly used single-chip microcomputer: 51 single-chip microcomputer, STM32 single-chip microcomputer

1.2 Composition of the minimum system board: reset circuit diagram

Reset: Refers to returning the system to the initial state and restarting the execution of the program. The reset level of different MCUs may be different. For example, the 51 microcontroller is a high-level reset, and the STM32 is a low-level reset. In order to prevent accidental touches during the normal execution of the system, the reset needs a certain period of time (such as 0.1S) before being judged to be reset. ) The figure shows a high-level reset, and the resistor can be regarded as a pull-down resistor

Software reset: 32 microcontrollers can use watchdog or specific functions to reset software.

Power-on reset: Since the charge in the capacitor is zero when the power is turned on, there is a process of charging the capacitor from 0V to 5V, and the RST of the resistor drops from 5V to 0V at this time. Since the RST terminal is higher than 1.5V, it is regarded as a high level, so in fact, the system reset will be triggered within 0.1S when the capacitor is charged from 0 to 3.5.

Button reset: When the button does not move normally, the capacitor is fully charged, and the resistor has no current so there is no voltage drop. As shown in the figure, the RST pin is at a low level at this time. When the button is pressed, the capacitor discharges instantaneously. When the button is released, it is actually equivalent to a power-on process. The reset will be triggered as above (actually, the manual button can be reset normally without adding a capacitor, but there is instability)

 1.3 Clock crystal oscillator


Principle of crystal oscillator: The main component of crystal oscillator is quartz crystal, and the structure is coated with conductive silver layer on both sides of quartz sheet. Using the piezoelectric effect of quartz (electricity produces deformation, and the deformation will generate electricity), the quartz crystal will be deformed by alternating current, and the deformation will generate voltage. When the frequency of the external circuit is the same as the natural frequency of the crystal, the amplitude of the crystal is the largest. Stable oscillation (sine wave), use this sine wave to provide a stable clock frequency for the microcontroller. 

1.4 Common development boards and microcontrollers

Raspberry Pi 4B:

Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz
Raspberry Pi (Chinese name is "Raspberry Pi", abbreviated as RPi, (or RasPi / RPI) is designed for learning computer programming education) , a microcomputer the size of a credit card, whose system is based on Linux.
The Raspberry Pi can be understood as a miniature computer, which is almost the same as the computer we usually use.

Arduino:

AVR single-chip microcomputer and ARM chip
Arduino are a convenient, flexible and easy-to-use open source electronic prototype platform.
It is built on the open source code simple I/O interface version, and has a Processing/Wiring development environment similar to Java and C languages. It mainly includes two parts: the hardware part is the Arduino circuit board that can be used for circuit connection; the other is the Arduino IDE, the program development environment in your computer. Just write the program code in the IDE, after uploading the program to the Arduino board, the program will tell the Arduino board what to do.
To put it simply, arduino is a secondary packaging of the microcontroller.

Stm32:

The full name of the ARM Cortex-M core microcontroller
stm32 is the 32-bit series microcontroller chip of STMicroelectronics. The STM32 series is designed for ARM Cortex®-M0, M0+, M3, M4 and M7 cores designed for embedded applications requiring high performance, low cost, and low power consumption.
Mainstream products (STM32F0, STM32F1, STM32F3), ultra-low power products (STM32L0, STM32L1, STM32L4, STM32L4+), high-performance products (STM32F2, STM32F4, STM32F7, STM32H7)

51 microcontroller:

51 single-chip microcomputer is a general term for single-chip microcomputers compatible with the Intel 8051 instruction system.
51 single-chip microcomputers are widely used in household appliances, automobiles, industrial measurement and control, and communication equipment. Because the instruction system and internal structure of 51 single-chip microcomputer are relatively simple, many colleges and universities in China use it to teach the introduction of single-chip microcomputer.
The 51 single-chip microcomputer is an 8-bit 8051 microcontroller that was upgraded and upgraded from the 8031 ​​microcontroller chip launched by Intel in 1981, uses the CISC instruction set, and has a Von Neumann architecture. Later, Intel authorized the core of the 8051 microcontroller to other chip manufacturers, making chips similar to the 8051 appear widely on the market. This chip using the 8051 core is called 51 for short.
From the above examples, we can intuitively see the difference between the processors used by these development boards and single-chip microcomputers, but the Raspberry Pi, Arduino, stm32 and our daily mobile phones all use arm-based processors.

1.5 Learning Materials

1: Guo Tianxiang at station B, Mr. Tang talking about e-sports

2: CSDN: Xiaoyu teaches you modulus electricity, beryllium acetate, Uxin Electronics

3: Hardware: Hard Creation Society

4: Data sheet: Peninsula Chip

1.6 References

https://blog.csdn.net/weixin_44161383/article/details/103593337

https://blog.csdn.net/florence_jz/article/details/129729236

https://blog.csdn.net/qq_57707070/article/details/128664070

Two computer languages

2.1 We know that the simplest computer must also consist of two parts, CPU (instruction processor) and RAM (instruction memory), such as a common mobile phone, the most important performance ratio of a computer is the processing speed of the CPU and the capacity of the RAM.

2.2 Data is stored in the form of binary in the computer. No matter what content we transmit, it will eventually be compiled into binary to be recognized by the machine, so there is a process from the user interface to the machine language:

2.3 Since binary is stored in memory, how does the computer find this binary data, which involves the concept of memory address. Every time we define a variable, a space will be allocated in memory, and this space will occupy different binary digits according to the size of the data. In order to calculate the data size more conveniently, we usually use bytes to represent the data size, 1byte=8-bit binary.

2.4 So how does memory allocate byte space specifically? Memory is actually divided into ordered byte arrays according to capacity, and each byte has a unique memory address. As shown in the figure below, the memory with 4G capacity will be divided into 2^32 bytes, and then define the byte numbers in order

 2.5 Usually, the computers or mobile phones we use often hear 32-bit and 64-bit, so what are these 32-bit and 64-bit related? This is actually related to the cpu instruction set, which represents the maximum number of bits that the cpu can process data at a time , which also means that the cpu can address the memory. The 32-bit operating system cannot address more than 4GB of memory, and the 64-bit operating system can address more than 4GB of memory. Therefore, in the case of the same operating frequency, the processing speed of the 64-bit processor will be faster than that of the 32-bit processor.

Three pointers in C/C++

3.1 What are pointers? In the above content, we talked about the concept of memory address, and the CPU processing instructions is an addressing process. Then how to get this address number in C/C++? This involves pointers. We can define a pointer variable to point to this memory address, and then we can get the address number and address data.

3.2 There is no difference between pointer variables and ordinary variables in terms of definition, they are all used for assignment, and both need type and variable name, but ordinary variables point to data, and pointer variables point to the address of data. Define the integer variable a and the pointer variable p as follows

int a;//定义普通整型变量
int *p;//定义整型指针变量

3.3 Every time we define a variable, an address will be allocated in the memory. For example, if we define two integer variables, we can print the addresses of these two variables:

#include <iostream>
using namespace std;
int main() {
    int a=10;
    int b=10;
    printf("&a:0x%x\n",&a);
    printf("&b:0x%x\n",&b);
    return 0;
}

The addresses of the following variables a and b are different

 3.4 The use of pointers, since it is a pointer variable, point the pointer to the existing memory address. As follows: p pointer points to the address of a

#include <iostream>
using namespace std;
int main() {
    int var_runoob=10;//定义普通整型变量
    int *p;//定义整型指针变量
    p = &var_runoob;//在指针变量中存储var_runoob的地址
    printf("&a:0x%x\n",&var_runoob);
    printf("&p:0x%x\n",p);
    return 0;
}

It can be seen that the addresses of a and p are the same

 Pointer assignment process diagram: p itself is an address variable, so there is no need to use the & symbol to get the address

 3.5 Arithmetic with pointers

The addition and subtraction of pointers is different from our normal addition and subtraction, because pointers are address variables, and increment and decrement are also address operations

For example, p++, p--, p + i and other operations are performed on the pointer variable p, and the result is also a pointer, but the memory address pointed to by the pointer advances or retreats i operands compared to the memory address pointed to by p

Example: define a p address variable and then increment it

#include <iostream>
using namespace std;
int main() {
    int *p;
    printf("&p2:0x%x\n", p);
    p++;
    printf("&p2:0x%x\n", p);
    p++;
    printf("&p2:0x%x\n", p);
    p++;
    printf("&p2:0x%x\n", p);
    return 0;
}

 Printing can see that the address has increased by 4 bytes each time, because an int type occupies 4 bytes

3.6 Illegal pointer, null pointer, does not point to anything. Define a null pointer as follows:

#include <iostream>
using namespace std;
int main() {
    int *p=NULL;
    printf("&p:0x%x\n",p);
    return 0;
}

The address of the console output pointer p is 0, 0 means that it does not point to any memory

On most operating systems, programs are not allowed to access memory at address 0 because that memory is reserved for the operating system. However, memory address 0 has a special significance, it indicates that the pointer does not point to an accessible memory location

3.7 Wild pointer, the location pointed by the pointer is unknown.

reason:

  • pointer is not initialized
  • pointer out of bounds access
  • The space pointed to by the pointer is freed

avoid:

  • Be careful not to access the array out of bounds
  • Timely assign the pointer to a null pointer
  • Avoid returning the address of a local variable
  • Check validity before using a pointer

Example:

#include <iostream>
using namespace std;

//指针函数,返回变量a的地址
int* test( ) {
    int a = 5;
    return &a;
}

int main() {
    //定义指针变量P,指向a的地址
    int* p = test();
    printf("&p:0x%x\n",p);
    return 0;
}

After printing, the address of the pointer p is also 0, which does not point to any memory

The address of the variable a is only valid in the test() function. When the address of a is passed to the pointer p, because the test function exits, the space address of the variable a is released, causing p to become a wild pointer.

 3.8 Second-level pointers, that is, pointers to pointers. It can also be said that the type of pointer is a pointer.

#include <iostream>
using namespace std;
int main() {
    int a = 10;
    int* p= &a;//p是int类型的一级指针
    int** pp= &p;//pp是存放的一级指针的地址

    printf("&p:0x%x\n",p);
    printf("&p:0x%x\n",&p);
    printf("&p:0x%x\n",pp);
    //int*** ppp =&pp;//这里的ppp就是三级指针
    return 0;
}

Printing shows that the addresses of the second-level pointer pp and the first-level pointer *p are the same, indicating that pp stores the address of the first-level pointer p

 3.9 Array pointer is a pointer that points to an array, and the address of the pointer is the address of the first element of the array

#include <iostream>
using namespace std;
int main() {
    int a[3]= {1,2,3};// 声明一个int类型的数组,这个数组有3个元素
    int *p = a;// 声明一个int类型的指针变量.指向a数组

    printf("&a:0x%x\n\n",&a);

    printf("&a[0]:0x%x\n",&a[0]);
    printf("&a[1]:0x%x\n",&a[1]);
    printf("&a[2]:0x%x\n\n",&a[2]);

    printf("&a[2]:0x%x\n",p);

    return 0;
}


 Printing shows that the address of the array variable a is the address of the 0th element of the array, and the pointer p is also the address of the first element of the array

3.10 An array of pointers is an array, and each element in the array is a pointer. The address of the array is the address of the first element pointer

#include <iostream>
using namespace std;
int main() {
    int* p[3];//声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向int类型的指针

    printf("&p[0]:0x%x\n",&p[0]);
    printf("&p[1]:0x%x\n",&p[1]);
    printf("&p[2]:0x%x\n",&p[2]);

    printf("&p:0x%x\n",p);
    return 0;
}

You can see that the address of the array is the same as the pointer address of the first element.

The use of four binary in computer

4.1 Signed and unsigned numbers are used to represent positive and negative numbers. The highest bit (the first bit) is the sign bit, the sign bit of a positive number is "0", and the sign bit of a negative number is "1". For example, 1 and -1 are expressed as follows

0000 0001
1000 0001

4.2 Original code

It is a machine number, a binary number with a sign bit added, because the value has positive and negative points

For example, the original code of 1 and -1 is expressed as follows:

0000 0001
1000 0001

4.3 Inverse Code

The result of the multiplication and division operation of the original code with the signed bit is correct, but there is a problem in the addition and subtraction operation.

For example: in decimal: 1 + (-1) = 0, but in binary:

00000001 + 10000001 = 10000010

Convert the result to a decimal number which is -2. So the inverse code was invented on the basis of the original code to solve this problem.

For example, the inverse code of 1 and -1 is expressed as follows:

0000 0001
1111 1110

4.4 Complement

Although the appearance of the inverse code solves the problem of addition and subtraction of positive and negative numbers, it allows the number 0 to have two "forms": "0" and "-0", but this is illogical, there should only be
one 0 , so the complement

The complement of a positive number is itself

The complement code of a negative number is based on the original code, the sign bit remains unchanged, and the rest of the bits are reversed, and finally +1 (equal to +1 on the basis of the inverse code), such as the inverse code of 1 and -1 is expressed as follows:

0000 0001
1111 1111

Summarize:

1. The original code, inverse code, and complement code
of a positive number are the same; 2. The inverse code of a negative number = the sign bit of the original code remains unchanged, and the other bits are reversed (reversal means: replace 0 with 1, and replace 1 with 0 );
3. The complement of a negative number = its complement + 1;
4. The complement and complement
of 0 are both 0;
6. To convert binary to decimal, the original binary code must be used for conversion.

4.5 Examples

Use "signed numbers" to simulate how it works in a computer

Add positive numbers:

For example: 1+1, the operation in the computer is as follows:

The original code of 1 is:

00000000 00000000 00000000 00000001

Because "the original code, inverse code, and complement of positive numbers are the same", therefore, the complement of 1 = the original code of 1, so the complement of 1 + the complement of 1 is equal to:

00000000 00000000 00000000 00000001
+
00000000 00000000 00000000 00000001

=

00000000 00000000 00000000 00000010

00000000 00000000 00000000 00000010 (converted to decimal) = 2

Subtract positive numbers:

For example: 1 - 2, the calculation in the computer is as follows:

The subtraction operation in the computer is actually an addition operation, so, 1 - 2 = 1 + ( -2 )

Step 1: Find out the complement of 1 (because the original code, inverse code, and complement of positive numbers are the same, so we can directly obtain the complement code through the original code):

1's complement:

00000000 00000000 00000000 00000001

The second step: Find out the original code of -2:

The original code of -2:

10000000 00000000 00000000 00000010

Step 3: Find out the inverse of -2:

Inverse of -2:

11111111 11111111 11111111 11111101

Step 3: Find out the complement of -2:

-2's complement:

11111111 11111111 11111111 11111110

Step 4: Add 1's complement to -2's complement:

00000000 00000000 00000000 00000001
+
11111111 11111111 11111111 11111110

=

11111111 11111111 11111111 11111111

Step 5: Convert the complement code of the calculation result to the original code, and do the opposite (if you want to convert binary to decimal, you must get the original binary code)

Complement: 11111111 11111111 11111111 11111111

=

Inverse code: 11111111 11111111 11111111 11111110

=

Original code: 10000000 00000000 00000000 00000001

Step 6: Convert the original binary code of the calculation result to decimal

Binary original code: 10000000 00000000 00000000 00000001 = 1*2^0 = -1

4.6 Why the value range of byte in java is -128~127

The byte in java occupies one byte, that is, 8bit (bit), the highest bit of which is the sign bit, and the remaining 7 bits are used to represent the value.

If the sign bit is 0, it is expressed as a positive number, and the range is 00000000~01111111 (complement code form), that is, 0-127 in decimal.

If the sign bit is 1, it is expressed as a negative number, the range is 10000000~11111111 (complement code form), -128~-1, 11111111 converted to the original code is 10000001, which is -1.

In the complement code, in order to avoid the existence of "-0", it is stipulated that 10000000 is -128, so it explains why the value range of byte is -128~127.

4.7 << and >> and >>> in Java

First of all, << and >> and >>> are bitwise operators in java, which operate on binary.

In addition to these, there are &, |, ^, ~, and several bit operators. No matter what base the initial value is based on, it will be converted into binary for bit operation.

<< in Java means shifting to the left, regardless of positive and negative numbers, and filling the lower bits with 0.

The following data types default to byte as 8 bits, regardless of positive or negative when shifting to the left, the lower bits are filled with 0

Positive numbers: r = 20 << 2

Two's complement of 20: 0001 0100

After moving two places to the left: 0101 0000

Result: r = 80

Negative numbers: r = -20 << 2

The original binary code of -20: 1001 0100

One's complement of -20: 1110 1011

Two's complement of -20: 1110 1100

Two's complement after shifting left two bits: 1011 0000

Inverse code: 1010 1111

Original code: 1101 0000

Result: r = -80

'>>' in Java means right shift, if the number is positive, the high bit will be filled with 0, if it is negative, the high bit will be filled with 1;

Note: The following data types default to byte as 8 bits

Positive numbers: r = 20 >> 2 

Two's complement of 20: 0001 0100

After moving two places to the right: 0000 0101

Result: r = 5

Negative numbers: r = -20 >> 2 

The original binary code of -20: 1001 0100

One's complement of -20: 1110 1011

Two's complement of -20: 1110 1100

Two's complement after shifting right two bits: 1111 1011

Inverse code: 1111 1010

Original code: 1000 0101

Result: r = -5

' >>> ' means unsigned right shift, also called logical right shift, that is, if the number is positive, the high bit is filled with 0, and if the number is negative, the high bit is also filled with 0 after the right shift 

Note: The following data types default to int 32 bits

Positive numbers: r = 20 >>> 2

The result is the same as r = 20 >> 2;

Negative numbers: r = -20 >>> 2

-20 original code: 10000000 00000000 00000000 00010100

Inverse code: 11111111 11111111 11111111 11101011

Complement: 11111111 11111111 11111111 11101100

Shift right: 00111111 11111111 11111111 11111011

Result: r = 1073741819

Conversion between five bases

5.1 Common bases

Decimal:

The decimal system is Decimal, abbreviated as D,
which is composed of nine numbers from 0-9.


binary:

Binary is Binary, abbreviated as B,
consisting of two numbers 0 and 1.


Octal:

Octal is Octal, which is abbreviated as O and
consists of 0-7 numbers. In order to distinguish it from other numbers, it starts with 0.


Hexadecimal:

Hexadecimal is Hexadecimal, and it is abbreviated as H,
which means that it starts with 0x
. After counting to F, add 1 more to carry.
Composed of 0-9 and AF, English letters A, B, C, D, E, F represent numbers 10-15 respectively.

1 2 3 4 5 6 7 8 9 A B C D E F
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

5.2 Convert decimal to binary

The method of dividing a decimal number by 2 and taking the remainder, that is, when a decimal number is divided by 2, the remainder is the number on the right position, and the obtained quotient value continues to be divided by 2, and the operation continues until the quotient is 0.

As shown in the figure below: converting 150 to binary is: 10010110

 5.3 Convert binary to decimal

Expand the binary number according to the weight and add it to get the decimal number. As shown in the figure below, 10010110 is converted to decimal: 150

 5.4 Basic binary and decimal correspondence

Focus on where binary 1 is located 1 2 3 4
binary 0001 0010 0100 1000
decimal 1 2 4 8
relationship to the power of two 20 21 22 23

Six java serial port communication binary demonstration actual combat

The following is a binary combination code example of an instruction. The bytes are combined according to the rules. A byte has 8 bits, and some need to occupy two bits. Each bit of 0 and 1 represents a different meaning.

Then according to the rule that the high bit comes first and the low bit comes after, it is combined into an 8-bit byte.

Then combine the bytes into a byte array, convert the byte array into a hexadecimal instruction and send it to the machine

public static String hexStringFormatNormal1(CmdNormal cmdNormal) {
        if (cmdNormal == null) {
            return "";
        }
        /**
         * 功能码8位二进制组成功能byte
         * Bit[0]  :  0-不需要从机返回信息 /  1-需要从机返回信息
         * Bit[1]  :  0-发送 /  1-返回
         * Bit[3]  :  0-快捷指令 /  1-常规指令
         * Bit[4:3] :  0-蓝牙灯具 /  1-2.4G灯具 /  2-DMX灯具
         * Bit[6:5] :  未使用,保持0
         * Bit[7]  :  0-独立一帧 / 1-多帧数据
         */
        StringBuffer stringBufferFunction = new StringBuffer();
        CmdFunction cmdFunction = cmdNormal.getCmdFunction();
        stringBufferFunction.append(cmdFunction.getIsMultiFrame());//7位(0-独立一帧 / 1-多帧数据)
        stringBufferFunction.append("0");//6位保持0
        stringBufferFunction.append(cmdFunction.getIsSetting());//5 0-查询 / 1-设置
        //stringBufferFunction.append("0");//3位保持0
        stringBufferFunction.append(OrderUtils.numToBinary(cmdFunction.getIsDeviceType(),2));//3,4位(0-蓝牙灯具 /  1-2.4G灯具 /  2-DMX灯具)
        stringBufferFunction.append(cmdFunction.getIsFunctionNormal());//2位 (0-快捷指令 /  1-常规指令)
        stringBufferFunction.append(cmdFunction.getIsFunctionBack());//1位(0-发送 /  1-返回)
        stringBufferFunction.append(cmdFunction.getIsMachineBack());//0位(0-不需要从机返回信息 /  1-需要从机返回信息)


        //功能码二进制转10进制
        int functionTen = Integer.parseInt(stringBufferFunction.toString(), 2);

        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(bytesToHexString(cmdNormal.getRollCode()));//滚码
        stringBuffer.append(bytesToHexString(functionTen));//功能码

        //地址码
        int address=cmdNormal.getAddress();
        stringBuffer.append(bytesToHexString(address>> 8 & 0xff));
        stringBuffer.append(bytesToHexString(address& 0xff));

        //当前帧
        stringBuffer.append(bytesToHexString(cmdNormal.getCurrentFrame()));
        //总帧
        stringBuffer.append(bytesToHexString(cmdNormal.getTotalFrame()));
        //数据模式
        stringBuffer.append(bytesToHexString(cmdNormal.getModeType()));

        CmdNormal.AllDataMode allDataMode=cmdNormal.getAllDataMode();
        for(CmdCode cmdCode:allDataMode.getCmdCodeList()){
            if(cmdCode.getLenth()==1){
                stringBuffer.append(bytesToHexString(cmdCode.getValue()));
            }else if(cmdCode.getLenth()==2){
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 8 & 0xff));//高8位
                stringBuffer.append(bytesToHexString(cmdCode.getValue() & 0xff));//低8位
            }else if(cmdCode.getLenth()==4){
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 24 & 0xff));//高24位
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 16 & 0xff));//高16位
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 8 & 0xff));//高8位
                stringBuffer.append(bytesToHexString(cmdCode.getValue() & 0xff));//低8位
            }else if(cmdCode.getLenth()==6){
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 40 & 0xff));//高40位
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 32 & 0xff));//高32位
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 24 & 0xff));//高24位
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 16 & 0xff));//高16位
                stringBuffer.append(bytesToHexString(cmdCode.getValue()>> 8 & 0xff));//高8位
                stringBuffer.append(bytesToHexString(cmdCode.getValue() & 0xff));//低8位
            }
        }
        return stringBuffer.toString();
    }
/**
     * 将数组转为16进制
     *
     * @param bArray
     * @return
     */
    public static String bytesToHexString(byte[] bArray) {
        if (bArray == null) {
            return null;
        }
        if (bArray.length == 0) {
            return "";
        }
        StringBuffer sb = new StringBuffer(bArray.length);
        String sTemp;
        for (int i = 0; i < bArray.length; i++) {
            sTemp = Integer.toHexString(0xFF & bArray[i]);
            if (sTemp.length() < 2)
                sb.append(0);
            sb.append(sTemp.toUpperCase());
        }
        return sb.toString();
    }

Guess you like

Origin blog.csdn.net/qq_29848853/article/details/130408759