Embedded C language (below)

Conditional compilation

You can use preprocessing instructions to create conditional compilation, that is, you can use these instructions to tell the compiler to execute or ignore code blocks based on compile-time conditions.

  1. #ifdef, #else and #endif instructions
    Let's use an example to see these instructions:
#ifdef HI /* 如果用#define 定义了符号 HI,则执行下面的语句 */
#include <stdio.h>
#define STR "Hello world"
#else /* 如果没有用#define 定义符号 HI,则执行下面的语句 */
#include "mychar.h"
#define STR "Hello China"
#endif

The #ifdef instruction indicates that if the preprocessor has defined the following identifier, all instructions before the #else or #endif instruction are executed and all C code is compiled. If the #elif instruction is not defined and there is an #elif instruction, then #else and # are executed. The code between endif instructions.

#ifdef, #else and C are very similar to if else. The main difference between the two is that the preprocessor does not recognize the curly braces {} used to mark blocks, so it uses #else (if needed) and #endif (must be present) ) To mark instruction blocks.

  1. #ifndef instruction
    #ifndef instruction is similar in usage to #ifdef instruction, and can also be used together with #else and #endif, but its logic is opposite to #ifdef instruction.
  2. #if and #elif
    #if instruction is very similar to if in C language. #if is followed by an integer constant expression. If the expression is non-zero, the expression is true. You can use C's relational and logical operators in the instruction:
#if MAX==1
printf("1");
#endif
可以按照 if else 的形式使用#if #elif:
#if MAX==1
printf("1");
#elif MAX==2
printf("2");
#endif

Another purpose of conditional compilation is to make the program easier to transplant. Changing several key definitions at the beginning of the file can set different values ​​and include different files according to different systems.

Pointer usage

What is a pointer? Fundamentally, a pointer is a variable whose value is a memory address. Just as the value of a char type variable is a character, the value of an int type variable is an integer, and the value of a pointer variable is an address.

Because the hardware instructions of computers or embedded devices rely heavily on addresses, pointers express the instructions that programmers want to express to a certain extent in a way closer to the machine. Therefore, programs that use pointers are more efficient. In particular, pointers can effectively handle arrays, and array notation is actually using pointers in disguise, for example: the array name is the address of the first element of the array.

To create a pointer variable, you must first declare the type of the pointer variable. If you want to declare ptr as a pointer that stores the address of an int type variable, you must use the indirect operator * to declare.

Assuming that ptr is known to point to bah, it is expressed as follows:

ptr = &bah;

Then use the indirect operator * to find the value stored in bah: value = *ptr; this operator is sometimes called the dereference operator. The effect of the statement ptr=&bah;value=*ptr; put together is equivalent to: value=bah;

So how to declare a pointer variable? is that so:

pointer ptr; // 不能这样声明一个指针变量

Why can't you declare a pointer variable like this? Because the type of the variable pointed to by the pointer must be specified when declaring a pointer variable, the storage space occupied by different variable types is different, and some pointer operations need to know the size of the operation object. In addition, the program must know the type of data stored in the specified address. E.g:

int *pi; // pi 是指向 int 类型变量的指针
char *str; // str 是指向 char 类型变量的指针
float *pf, *pg; // pf, pg 都是只想 float 类型变量的指针

The type specifier indicates the type of the object pointed to by the pointer, and the dereference symbol * indicates that the declared variable is a pointer. The declaration of int *pi means that pi is a pointer and *pi is of type int, as shown in Figure 5.3.4.
Insert picture description here
This is just a simple use of pointers. The world of actual pointers is ever-changing and colorful. Even for many years of C language development veterans, sometimes they will make mistakes when facing the use of pointers. Successors should be more cautious in searching for pointers. The pointers are common Applications and precautions are introduced.

  1. Pointers and arrays
    As mentioned earlier, you can use the address operator & to get the address of the variable, and in the array you can also use the address operator to get the address of any member of the array member, for example:
int week[7] = {
    
    1, 2, 3, 4, 5, 6, 7};
int *pw;
pw = &week[2];
printf("week is: %d", *pw);

The output result is: week is 3. For the interpretation of this code, refer to Figure 5.3.3 above.

  1. Pointers and functions
    The simplest way to use pointers in a function is as a formal parameter of a function, such as:
int sum(int *pdata)
{
    
    
int i = 0;
int temp = 0;
for(i=0;i<10;i++) {
    
    
temp = temp + (*pdata);
pdata++; }
return temp;
}

This example has a few points worth explaining. The first point is that the pointer pdata exists as a function parameter, pointing to an address that stores an int type variable; the second point is the pointer pdata++; after the statement is executed, pdata only wants to increment the address It is not 1, but the size of the int type. The initial value of adding pdata is 0, and the int type occupies 2 bytes, then pdata++; after the statement is executed, the value of pdata becomes 2, instead of 1, and * The value of pdata is that the value of address 2 is not the value of address 1. The third point of this function is that there is a danger, that is, the function implements the sum of 10 int type variables starting from the address that pdata originally points to, if we Use it like this:

int data[5] = {
    
    1, 2, 3, -1, -2};
int x = sum(data);

It can be seen that the array name of the array data, that is, the first address of the array, is input into the function sum as a parameter, and the size of the array is only 5 ints, but the function sum calculates the sum of 10 numbers, so address overflow will occur. If the result is not correct, the program may even run away. To avoid this problem, the usual solution is to add a quantity parameter:

int sum(int *pdata, int length)
{
    
    
int i = 0;
int temp = 0;
for(i=0;i<length;i++) {
    
    
temp = temp + (*pdata);
pdata++; }
return temp;
}x = sum(data, 5);

Or give the pointer range:

int sum(int *pStart, int *pEnd)
{
    
    
int i = 0;
int temp = 0;
int length = (pEnd - pStart)/2; // 假设一个 int 占 2 个字节
for(i=0;i<length;i++) {
    
    
temp = temp + (*pdata);
pdata++; }
return temp;
}x = sum(data, &data[4]);

The relationship between pointers and functions has another important application besides pointers as function parameters. There are function pointers, such as the example in the typedef usage chapter:

typedef void (*pFunction)(void);

In this example, first * indicates that pFunction is a pointer variable, secondly, void indicates that the pointer variable returns a value of type void, and the void in the brackets indicates that the formal parameter of this function pointer is of type void. How to call a function using a function pointer?

Look at the following example:

int max(int a, int b)
{
    
    
return ((a>b)?a:b);
}
int main(void) {
    
    
int (*pfun)(int, int);
int a=-1, b=2, c=0;
pfun = max;
c=pfun(a, b);
printf("max: %d", c);
return 0; }

The output result is: 2.

  1. Pointer and hardware address
    The link between pointer and hardware address is a glimpse in the examples in the volatile usage chapter, and there is no detailed introduction. The following is a detailed explanation. For example, the base address of the internal SRAM in STM32F103ZET6 is 0x20000000. If we want to write data to the first 256 bytes of this space, we can use a pointer to point to this base address, and then start writing:
volatile unsigned char *pData = (volatile unsigned char *)(0x20000000);
int main(void) {
    
    
int i = 0;
for(i=0; i<256; i++) {
    
    
pData[i] = i+10; }
return 0; }

In addition to the memory address, it can also point to the register address of the hardware peripheral. The operation method is similar to the above example.
Basic principles of pointer application:

  • First, the type of pointer must be specified;
  • If it is an ordinary pointer variable, non-function parameter or function pointer, the address must be assigned to the pointer variable to avoid becoming a "wild pointer";

Callback

In C language, the callback function is an advanced application of function pointer. The so-called callback function, a general and simple introduction is a function that is passed as a parameter. Literally, the meaning of a callback function is: a function that is called back, how to understand this sentence? From a logical analysis, to "go back", there must be a known destination, and then visit at a certain moment; then the callback function is a known function body A, and the address of this function body A is the function The name "A" (the function name is the function pointer of this function body, pointing to the address of this function) tells another function B, and when that function B reaches a certain step, it will execute function A.

There are many applications of callback functions, because the subsequent programs are written under the HAL library of STM32, so here we only look at the callback functions from the HAL library.

We only look at the HAL library function of GPIO, the file name is "stm32f1xx_hal_gpio.c". We use the method of inverse analysis to look at this callback function.

The first is the declaration of the GPIO callback function:

__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)

You can see that the function name is: HAL_GPIO_EXTI_Callback, and the formal parameter is GPIO_Pin, which represents the pin number (Px0~Px15, x=A,B,C,D,E,F,G). Starting from the name of this function, it can be roughly clear This is a callback function for the external interrupt (EXTI) of a pin. Then everyone sees that there is a "__weak" in front, which is a modifier of virtual function, telling the compiler that if the user redefines this callback function with void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) elsewhere, then the user-defined callback function will be called first, otherwise it will be called The callback function decorated by this virtual function.

Next, let's see where this callback function is called:

void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
    
    
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u) {
    
    
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin); } }

It can be seen that it is called in the external interrupt service function of GPIO, which is consistent with the above-mentioned that this is an external pin interrupt callback function.

That concludes the GPIO callback function. In fact, most of the callback functions of the other peripherals in the STM32 HAL library are basically the same. If the user needs to design, he will redefine the required callback function by himself, and then be called in the interrupt.

Bit operation

Bit operations refer to operations between binary bits. In the design of embedded systems, binary issues are often dealt with, such as setting a certain position in a register to 1 or value 0, shifting data to the left by 5 bits, etc. Commonly used bit operators are shown in Table 5.3.1.
Insert picture description here

  1. The bitwise AND operator (&)
    participates in the two operands, and each binary bit performs the AND operation. If both are 1, the result is 1, otherwise it is 0. For example, 1011&1001, the first digit is 1, and the result is 1, the second digit is 0, and the result is 0; the third digit is 1 and the other is 0, and the result is 0; the fourth digit is both 1, and the result is 1. The final result is 1001.

  2. The bitwise OR operator (|)
    participates in the two operands of the operation, and each binary bit performs an "or" operation. If both are 0, the result is 1, otherwise it is 1. For example, 1011 | 1001, the first digit is 1, and the result is 1, the second digit is 0, and the result is 0; the third digit is 1 and the other is 0, and the result is 1, and the fourth digit is 1. The result is 1. The final result is 1011.

  3. The bitwise negation operator (~) The
    bitwise negation operator is used for bitwise negation of a binary number.
    For example, ~1011, the first bit is 1, and the negation is 0; the second bit is 0, and the negation is 1; the third bit is 1, and the negation is 0, and the result is 1; the fourth bit is 1, and the negation Is 0. The final result is 0100.

  4. Left shift (<<) and right shift (>>) operators The
    left shift (<<) operator is used to shift a number to the left by several places, and the right shift (>>) operator is used to shift a number to the right by several places . For example, suppose val is an unsigned char type data, and the corresponding binary number is 10111001. If val=va<<3, it means that val is shifted by 3 bits to the left, and then assigned to val. During the process of shifting to the left, the high shift is discarded and the low bit is filled with 0. The final val result is 1100100; if val=val>>3, Indicates that val is shifted by 3 bits to the right, and then assigned to val. In the process of shifting to the right, the low bit is discarded after being shifted out, and the high bit is filled with 0. Finally, the result of val is 00010111.

  5. Clearing or setting 1
    In embedded, the bit budget symbol is often used to realize clearing or setting.
    For example, the ODR register of the MCU controls the output level of the pin, the register is 32 bits, and each bit controls the level of a pin. Assuming that the output level of pin 1 of GPIOB needs to be controlled, set the first bit of this register to 1, output high level, and set the first bit of this register to 0, output low level.

#define GPIOB_ODR (*(volatile unsigned int *)(0x40010C0C))
GPIOB_ODR &= ~(1<<0);
GPIOB_ODR |= (1<<0);

The first line : Use #define to define the memory address corresponding to GPIOB_ODR as 0x40010C0C. This address is the ODR register address of the MCU.

The third line : GPIOB_ODR &= ~(1<<0) is actually GPIOB_ODR = GPIOB_ODR & (1<<0), first perform the AND operation of GPIOB_ODR and (1<<0), and assign the result of the operation to GPIOB_ODR. The value of 1<<0 is 00000000 00000000 00000000 00000010,
and then inverted to 11111111 11111111 11111111 11111101, then the first bit of GPIO_ODR and 0 and the operation, the result must be 0, other bits and 1 operation, the result is determined by the original value of GPIO_ODR . This is achieved, only the first bit of GPIO_ODR is cleared to 0, and the other bits remain unchanged, and the effect
of individually controlling the corresponding pin level output is realized.

The fourth line : GPIOB_ODR |= (1<<0) is actually GPIOB_ODR = GPIOB_ODR | (1<<0), first perform the OR operation of GPIOB_ODR and (1<<0), and assign the result of the operation to GPIOB_ODR. The value of 1<<0 is 00000000 00000000 00000000 00000010, then the first bit of GPIO_ODR is ORed with 0, the result must be 1, and the other bits and 0 are calculated, and the result is determined by the original value of GPIO_ODR. This achieves the effect that only the first bit of GPIO_ODR is set to 1, and the other bits remain unchanged, and the output of the corresponding pin is independently controlled to be high.


Baiwen Technology Forum:
http://bbs.100ask.net/

Baiwen.com embedded video official website:
https://www.100ask.net/index

Baiwen.com Development Board:
Taobao: https://100ask.taobao.com/Tmall
: https://weidongshan.tmall.com/

Technology Exchange Group (
Hongmeng Development/Linux/Embedded/Driver/Data Download) QQ Group: 869222007

MCU-Embedded Linux Exchange Group:
QQ Group: 536785813

Guess you like

Origin blog.csdn.net/thisway_diy/article/details/115318224