# HAL库的一些知识

### 断言

C语言断言是一种用于==检查程序运行时条件是否满足的技术==。它可以帮助我们在开发过程中发现和调试错误，提高程序的健壮性和可靠性。下面我们通过示例程序和各种使用情况来学习C语言断言。

当程序不满足条件时会终止

#### 示例程序

包含`assert.h`

```c
#include <stdio.h>
#include <assert.h>
int main() {
    int a = 10;
    int b = 20;
    assert(a == b);
    printf("a=%d, b=%d\n", a, b);
    return 0;
}
```

在gcc编译器下输出：

```c
Error(s):
a.out: 1031855375/source.c:6: main: Assertion `a == b' failed.
```

在上面的示例程序中，我们使用了断言`assert`来检查变量`a`和`b`是否相等。由于`a`和`b`不相等，程序会在运行时触发断言失败，==终止程序的运行==，并输出错误信息。这样做可以帮助我们在开发过程中尽早发现错误，避免出现更严重的问题。

#### 情况一：检查函数参数

```c
#include <stdio.h>
#include <assert.h>
void print_number(int n) {
    assert(n >= 0 && n <= 100);
    printf("The number is: %d\n", n);
}
int main() {
    print_number(50);
    print_number(-10);
    return 0;
}
```

在上面的示例程序中，我们使用了断言`assert`来检查函数`print_number`的参数`n`是否在有效范围内。由于第二次调用`print_number`时，参数`n`不在有效范围内，程序会在运行时触发断言失败，终止程序的运行，并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法参数的情况。

#### 情况二：检查函数返回值

```c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int divide(int a, int b) {
    assert(b != 0);
    return a / b;
}
int main() {
    int a = 10;
    int b = 0;
    int c = divide(a, b);
    printf("c=%d\n", c);
    return 0;
}
```

在上面的示例程序中，我们使用了断言`assert`来检查函数`divide`的参数`b`是否为0。由于第二次调用`divide`时，参数`b`为0，程序会在运行时触发断言失败，终止程序的运行，并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法返回值的情况。

#### 注意事项

在使用C语言断言时，需要注意以下几点：

1. 断言只在调试模式下有效，因为在==发布模式==下，断言会被编译器忽略。
2. 断言应该用于检查程序运行时的条件是否满足，而不是用于处理已知错误或异常情况。
3. 断言的条件应该是纯粹的布尔表达式，不能包含副作用或需要计算的表达式。
4. 断言的错误信息应该尽可能地清晰和明确，以便于快速定位和解决问题

#### HAL库的实例

HAl库中NVIC使能中断时，使用了断言检查输入的中断参数的合理性，

NVIC使能函数如下，使用了`assert_param`函数，该函数为HAL库自编写：

```c
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn)
{
  /* Check the parameters */
  assert_param(IS_NVIC_DEVICE_IRQ(IRQn));

  /* Enable interrupt */
  NVIC_EnableIRQ(IRQn);
}
```

其中IS\_NVIC\_DEVICE\_IRQ，定义如下

```c
#define IS_NVIC_DEVICE_IRQ(IRQ)                ((IRQ) >= (IRQn_Type)0x00U)
```

这是一个宏定义，用于判断输入的中断号是否是合法的。 宏定义的名称为`IS_NVIC_DEVICE_IRQ`，其接受一个参数`IRQ`，代表输入的中断号。 该宏定义使用了一个条件表达式`(IRQ) >= (IRQn_Type)0x00U`，其中`(IRQ)`代表输入的中断号，`(IRQn_Type)0x00U`代表中断号的最小值，即中断号为0的枚举类型。 因此，当输入的中断号大于等于0时，即该中断号是合法的，宏定义返回真值；否则返回假值，表示该中断号不合法。

而`assert_param`函数定义如下，一般情况没有宏定义`USE_FULL_ASSERT`，也就是说会执行执行((void)0)，==即什么都不做。==

```c
#ifdef  USE_FULL_ASSERT
/**
  * @brief  The assert_param macro is used for function's parameters check.
  * @param  expr If expr is false, it calls assert_failed function
  *         which reports the name of the source file and the source
  *         line number of the call that failed.
  *         If expr is true, it returns no value.
  * @retval None
  */
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
```

要实现真正的断言需要宏定义`USE_FULL_ASSERT`（可以在stm32xx\_hal\_conf.h文件中取消注释，如下图，也可以在IDE如MDK中宏定义）

![](https://s2.loli.net/2023/04/03/CdLylNnmU27Y41V.png)

接着编写assert\_failed()函数，可在在main.c中定义

```c
#ifdef  USE_FULL_ASSERT

/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t* file, uint32_t line)
{
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */

  /* Infinite loop */
  while (1)
  {
  }
}
#endif
```

#### 实现自己的断言形式

`__FILE__`和`__LINE__`是C语言中的两个预定义宏，它们可以在任何时候使用，但是它们的值会根据它们所在的代码位置而变化。

需要注意的是，在某些特殊情况下，`__FILE__`和`__LINE__`的值可能会出现意外的变化。例如，在代码中使用了[#pragma](https://blog.csdn.net/liuchunjie11/article/details/80502529)或者[\_\_LINE\_\_宏](https://blog.csdn.net/nyist_zxp/article/details/107890791)时，可能会导致`__FILE__`和`__LINE__`的值出现异常。因此，在使用`__FILE__`和`__LINE__`时，应该注意这些情况的可能影响，并尽量避免使用会导致异常的代码结构。

```c
#include <stdio.h>
#define vAssertCalled(char, int) printf("Error: %s, %d\r\n", char, int)
#define configASSERT( x ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ )
void print_number(int n) {
    configASSERT(n>0);
    printf("The number is: %d\n", n);
}
int main() {
    print_number(50);
    print_number(-10);
    return 0;
}
```

在gcc编译器下输出：

```c
The number is: 50
Error: /home/ren/Desktop/rextester_linux_2.0/usercode/2081197636/source.c, 5
The number is: -10
```

可见这种断言可以输出错误的文件与行数，==同时程序还会正常执行==

**`在对断言的使用中，一定要遵循这样一条规定：对来自系统内部的可靠的数据使用断言，对于外部不可靠数据不能够使用断言，而应该使用错误处理代码。换句话说，断言是用来处理不应该发生的非法情况，而对于可能会发生且必须处理的情况应该使用错误处理代码，而不是断言。`**

### 结构体

定义方法

```c
struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;
```

在一般情况下，**tag、member-list、variable-list** 这 3 部分至少要出现 2 个。以下为实例：

```c
//此声明声明了拥有3个成员的结构体，分别为整型的a，字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct 
{
    int a;
    char b;
    double c;
} s1;
 
//此声明声明了拥有3个成员的结构体，分别为整型的a，字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
    int a;
    char b;
    double c;
};
//用SIMPLE标签的结构体，另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
 
//也可以用typedef创建新类型
typedef struct
{
    int a;
    char b;
    double c; 
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
```

最常用的是用typedef创建新类型，==一般在头文件中定义==，在C文件中可以直接调用，

#### HAL库中的实例

如HAL库创建GPIO的属性的结构体，在stm32f1xx\_hal\_gpio.h定义，==也就说一般是在头文件中进行结构体typedef==

```c
typedef struct
{
  uint32_t Pin;       /*!< Specifies the GPIO pins to be configured.
                           This parameter can be any value of @ref GPIO_pins_define */

  uint32_t Mode;      /*!< Specifies the operating mode for the selected pins.
                           This parameter can be a value of @ref GPIO_mode_define */

  uint32_t Pull;      /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
                           This parameter can be a value of @ref GPIO_pull_define */

  uint32_t Speed;     /*!< Specifies the speed for the selected pins.
                           This parameter can be a value of @ref GPIO_speed_define */
} GPIO_InitTypeDef;
```

在C文件中就可以直接使用

```C
GPIO_InitTypeDef gpio_init_struct;
```

**结构体上可以互相嵌套的**

#### FreeRTOS的实例

在task.c中有如下程序结构体定义

```c
typedef struct tskTaskControlBlock       /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
		/*中间程序较多省略*/
   
} tskTCB;

/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
 * below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;
```

在task.h中

```c
struct tskTaskControlBlock; /* The old naming convention is used to prevent breaking kernel aware debuggers. */
typedef struct tskTaskControlBlock * TaskHandle_t;
```

观察上面，`tskTCB`与`tskTaskControlBlock`在C语言语法上的使用有所不同

* tskTCB是被typedef struct重定义出来的，可以直接作为一种结构体变量类型使用，这边有使用typedef进行了重命名
* tskTaskControlBlock是结构体类型，其实就是结构体标签，但在使用时需要加前缀struct，说明是一个结构体
* 之所以一个在.h中typedef，一个在.c中typedef是因为tskTCB与TCB\_t只在task.c中使用，而tskTaskControlBlock与TaskHandle\_t在其他文件也会使用，因此需要在.h中进行重定义
* 之所以先使用了`struct tskTaskControlBlock`而不是直接使用`tskTaskControlBlock`来声明结构体，是因为在.h文件中只需要知道该结构体的存在即可，而不需要知道其具体实现细节，这样可以提高代码的封装性和可维护性。

下面给出一个简单的实例，说明`tskTCB`与`tskTaskControlBlock`在C语言语法上的使用的不同

```c
#include <stdio.h>
#include <stdlib.h>
typedef struct tskTaskControlBlock {
    int priority;
    int stackSize;
    void *stackPointer;
    // 其他成员变量
} tskTCB;
int main() {
    // 使用tskTaskControlBlock方式定义和初始化结构体变量
   struct tskTaskControlBlock myTask1 = {
        .priority = 1,
        .stackSize = 1024,
        .stackPointer = malloc(1024)
        // 其他成员变量的初始化
    };
    printf("myTask1 priority: %d\n", myTask1.priority);
    // 使用tskTCB方式定义和初始化结构体变量
    tskTCB myTask2 = {
        .priority = 2,
        .stackSize = 2048,
        .stackPointer = malloc(2048)
        // 其他成员变量的初始化
    };
    printf("myTask2 stackSize: %d\n", myTask2.stackSize);
    // 使用tskTaskControlBlock方式访问结构体成员
    myTask1.priority = 3;
    printf("myTask1 priority after update: %d\n", myTask1.priority);
    // 使用tskTCB方式访问结构体成员
    myTask2.stackSize = 4096;
    printf("myTask2 stackSize after update: %d\n", myTask2.stackSize);
    return 0;
}
```

### 结构体指针

#### 结构体指针的基本用法

结构体指针的定义方式与普通指针相同，只是指向的类型是结构体类型。例如，下面是一个结构体类型的定义：

```c
struct Person {
    char name[20];
    int age;
    char sex;
};
```

我们可以定义一个指向该结构体类型的指针变量 `p`，如下所示：

```c
struct Person *p;
```

接下来，我们可以通过该指针变量来访问结构体成员变量。例如，我们可以通过指针 `p` 访问结构体成员变量 `name`，如下所示：

```c
strcpy(p->name, "Tom");
```

上面的代码将字符串 `"Tom"` 复制到了指针 `p` 所指向的结构体变量的 `name` 成员变量中。

#### 结构体指针作为函数参数

结构体指针常常被用作函数参数，以便在函数中对结构体进行操作。下面是一个示例程序：

```c
#include <stdio.h>
#include <string.h>
struct Person {
    char name[20];
    int age;
    char sex;
};
void printPerson(struct Person *p) {
    printf("name:%s, age:%d, sex:%c\n", p->name, p->age, p->sex);
}
int main() {
    struct Person p1;
    strcpy(p1.name, "Tom");
    p1.age = 18;
    p1.sex = 'M';
    printPerson(&p1);
    return 0;
}
```

在上面的示例程序中，我们定义了一个名为 `printPerson` 的函数，它接受一个指向结构体类型 `Person` 的指针作为参数，并输出结构体的成员变量。在 `main` 函数中，我们定义了一个结构体变量 `p1`，给它的成员变量赋值，并将其地址传递给 `printPerson` 函数来输出它的成员变量。

#### 结构体指针数组

结构体指针还可以用于定义结构体指针数组。下面是一个示例程序：

```c
#include <stdio.h>
#include <string.h>
struct Person {
    char name[20];
    int age;
    char sex;
};
int main() {
    struct Person p1 = {"Tom", 18, 'M'};
    struct Person p2 = {"Lucy", 20, 'F'};
    struct Person p3 = {"David", 22, 'M'};
    struct Person *arr[3] = {&p1, &p2, &p3};
    for (int i = 0; i < 3; i++) {
        printf("name:%s, age:%d, sex:%c\n", arr[i]->name, arr[i]->age, arr[i]->sex);
    }
    return 0;
}
```

在上面的示例程序中，我们定义了三个结构体变量 `p1`、`p2` 和 `p3`，然后定义了一个指针数组 `arr`，其中每个元素都是一个指向 `Person` 结构体类型的指针。我们将三个结构体变量的地址分别存储到了数组的不同元素中，并使用循环遍历数组，输出结构体的成员变量。 总的来说，结构体指针是 C 语言中非常重要的数据类型之一，它可以用于访问结构体的成员变量、作为函数参数、进行动态内存分配以及定义结构体指针数组等。在学习 C 语言时，掌握结构体指针的使用方法是非常重要的。

#### 结构体指针的动态内存分配

结构体指针还可以用于动态内存分配，以便在程序运行时动态地创建结构体对象。下面是一个示例程序：

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Person {
    char name[20];
    int age;
    char sex;
};
int main() {
    struct Person *p = (struct Person*)malloc(sizeof(struct Person));
    if (p == NULL) {
        printf("memory allocation failed.\n");
        return -1;
    }
    strcpy(p->name, "Tom");
    p->age = 18;
    p->sex = 'M';
    printf("name:%s, age:%d, sex:%c\n", p->name, p->age, p->sex);
    free(p);
    return 0;
}
```

在上面的示例程序中，我们使用 `malloc` 函数动态地分配了一个大小为 `sizeof(struct Person)` 的内存空间，并将其地址强制转换成了指向结构体类型 `Person` 的指针。然后，我们可以通过该指针来访问结构体的成员变量，并在程序结束时使用 `free` 函数释放动态分配的内存空间。

### 数组指针

数组指针是==指向数组的指针变量==，它可以用来访问数组元素，也可以用来定义二维数组。本文将介绍数组指针的基本用法和常见的使用情况。

#### 数组指针的基本用法

数组指针的定义方式与普通指针相同，只是指向的类型是数组类型。例如，下面是一个数组类型的定义：

```c
int arr[3] = {1, 2, 3};
```

我们可以定义一个指向该数组类型的指针变量 `p`，如下所示：

```c
int *p;
p = arr;
```

上面的代码将数组 `arr` 的地址赋值给了指针变量 `p`。现在，我们可以通过指针 `p` 来访问数组元素。例如，我们可以通过指针 `p` 访问数组元素 `arr[0]`，如下所示：

```c
*p = 4;
```

上面的代码将数组元素 `arr[0]` 的值修改为了 `4`。 我们还可以使用指针运算符 `++` 和 `--` 来访问数组的下一个或上一个元素。例如，下面的代码将指针 `p` 指向数组的下一个元素：

```c
p++;
```

#### 数组指针作为函数参数

数组指针常常被用作函数参数，以便在函数中对数组进行操作。下面是一个示例程序：

```c
#include <stdio.h>
void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main() {
    int arr[3] = {1, 2, 3};
    printArray(arr, 3);
    return 0;
}
```

在上面的示例程序中，我们定义了一个名为 `printArray` 的函数，它接受一个指向整型数组的指针和数组的大小作为参数，并输出数组的元素。在 `main` 函数中，我们定义了一个整型数组 `arr`，给它的元素赋值，并将其地址和数组大小传递给 `printArray` 函数来输出它的元素。

#### 数组指针的动态内存分配

数组指针还可以用于动态内存分配，以便在程序运行时动态地创建数组对象。下面是一个示例程序：

```c
#include <stdio.h>
#include <stdlib.h>
int main() {
    int size = 3;
    int *p = (int*)malloc(size * sizeof(int));
    if (p == NULL) {
        printf("memory allocation failed.\n");
        return -1;
    }
    for (int i = 0; i < size; i++) {
        p[i] = i + 1;
    }
    for (int i = 0; i < size; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");
    free(p);
    return 0;
}
```

在上面的示例程序中，我们使用 `malloc` 函数动态地分配了一个大小为 `size * sizeof(int)` 的内存空间，并将其地址强制转换成了指向整型数组的指针。然后，我们可以通过该指针来访问数组的元素，并在程序结束时使用 `free` 函数释放动态分配的内存空间。

#### 数组指针的二维数组

数组指针还可以用于定义二维数组。下面是一个示例程序：

```c
#include <stdio.h>
int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int (*p)[3] = arr;
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
    return 0;
}
```

在上面的示例程序中，我们定义了一个二维整型数组 `arr`，然后定义了一个指向含有 `3` 个整型元素的一维数组的指针变量 `p`。我们将二维数组 `arr` 的地址赋值给了指针变量 `p`，现在我们可以通过指针 `p` 来访问二维数组的元素。 总的来说，数组指针是 C 语言中非常重要的数据类型之一，它可以用于访问数组元素、作为函数参数、进行动态内存分配以及定义二维数组等。在学习 C 语言时，掌握数组指针的使用方法是非常重要的

### 指针数组

首先它是一个数组，数组的元素都是指针，==与数组指针相区别==，

```c
#include <stdio.h>
int main() {
  // 定义三个整型变量
  int a = 1, b = 2, c = 3;
  // 定义一个指针数组，存储三个整型变量的地址
  int* ptr_arr[3] = {&a, &b, &c};
  // 访问指针数组中的元素
  for (int i = 0; i < 3; i++) {
    printf("%d ", *ptr_arr[i]);
  }
  return 0;
}
```

在上面的示例程序中，我们首先定义了三个整型变量 `a`、`b` 和 `c`，然后定义了一个指针数组 `ptr_arr`，并将这三个整型变量的地址存储到了这个数组中。最后，我们通过指针数组 `ptr_arr` 访问了这三个整型变量的值。

#### 指针数组与数组指针的区分

下面到底哪个是数组指针，哪个是指针数组呢： A) int \*p1\[10]; B) int (\*p2)\[10];

“\[]”的优先级比“\*”要高。p1 先与“\[]”结合，构成一个数组的定义，数组名为p1，int *修饰的是数组的内容，即数组的每个元素。那现在我们清楚，这是一个数组，其包含10 个指向int 类型数据的指针，即指针数组。至于p2 就更好理解了，在这里“（）”的优先级比“\[]”高，“*”号和p2 构成一个指针的定义，指针变量名为p2，int 修饰的是数组的内容，即数组的每个元素。数组在这里并没有名字，是个匿名数组。那现在我们清楚p2 是一个指针，它指向一个包含10 个int 类型数据的数组，即数组指针。

我们可以借助下面的图加深理解：

![img](https://s2.loli.net/2023/03/23/2vuNxAOlCkS8bgt.jpg)

[数组指针和指针数组的区别 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/55724052)

### 指针与数组地址结合的一些知识

#### 数组名是数组第一个元素的地址

例如，定义一个整型数组 `int arr[5] = {1, 2, 3, 4, 5};`，数组名 `arr` 就是第一个元素的地址，即 `&arr[0]`。

```c
#include <stdio.h>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  printf("arr = %p\n", arr);
  printf("&arr[0] = %p\n", &arr[0]);
  return 0;
}
```

输出：

```c
arr = 0x7fff5fbff620
&arr[0] = 0x7fff5fbff620
```

#### 数组元素的地址可以通过下标访问

例如，定义一个整型数组 `int arr[5] = {1, 2, 3, 4, 5};`，数组第三个元素 `arr[2]` 的地址可以表示为 `&arr[2]` 或者 `(arr + 2)`。

```c
#include <stdio.h>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  printf("&arr[2] = %p\n", &arr[2]);
  printf("(arr + 2) = %p\n", (arr + 2));
  return 0;
}
```

输出：

```c
&arr[2] = 0x7fff5fbff628
(arr + 2) = 0x7fff5fbff628
```

#### 数组元素的地址可以用指针变量来存储和访问

例如，定义一个整型指针变量 `int* ptr = NULL;`，可以将数组第一个元素的地址赋值给指针变量 `ptr`，即 `ptr = &arr[0];` 或者 `ptr = arr;`，然后就可以通过指针变量 `ptr` 访问数组元素。

```c
#include <stdio.h>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int* ptr = NULL;
  ptr = &arr[0];
  printf("ptr[2] = %d\n", ptr[2]);
  printf("*(ptr + 2) = %d\n", *(ptr + 2));
  return 0;
}
```

输出：

```c
ptr[2] = 3
*(ptr + 2) = 3
```

#### 数组名可以用作指针来访问数组元素

例如，定义一个整型数组 `int arr[5] = {1, 2, 3, 4, 5};`，可以通过数组名 `arr` 直接访问数组元素，即 `*(arr + 2)` 或者 `arr[2]`。

```c
#include <stdio.h>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  printf("*(arr + 2) = %d\n", *(arr + 2));
  printf("arr[2] = %d\n", arr[2]);
  return 0;
}
```

#### 指针也可以用来定义动态数组

例如，定义一个指向整型数组的指针变量 `int* ptr = NULL;`，然后通过 `malloc` 函数动态分配一个 10 个整型元素的数组，即 `ptr = (int*)malloc(10 * sizeof(int));`。分配后，就可以通过指针变量 `ptr` 访问动态数组元素，例如，`ptr[2] = 3;`。

```c
#include <stdio.h>
#include <stdlib.h>
int main() {
  int* ptr = NULL;
  ptr = (int*)malloc(10 * sizeof(int));
  if (ptr == NULL) {
    printf("Memory allocation failed!\n");
    return -1;
  }
  ptr[2] = 3;
  printf("ptr[2] = %d\n", ptr[2]);
  free(ptr);
  return 0;
}
```

输出：

```c
ptr[2] = 3
```

#### 数组作函数参数

```c
//----数组作函数参数--1
#include <stdio.h>
int sumOfElements(int A[],int size)
{
    int i,sum=0;
    for(i=0;i<size;i++)
    {
        sum=sum+A[i];
    }
    return sum;
}
int main()
{
    int a[5]={2,4,6,8,10};
    int size=sizeof(a)/sizeof(a[0]);
    int total=sumOfElements(a,size);
    printf("sum of elements=%d\n",total);
    return 0;
 }
```

### 指针的大小由什么决定

**指针大小是由当前CPU运行模式的寻址位数决定！**

\==所以在x86的64位PC机中是8字节，在arm架构的stm32单片机是4字节。==

**字长**：在同一时间中处理二进制数的位数叫字长。通常称处理字长为8位数据的CPU叫8位CPU，32位CPU就是在同一时间内处理字长为32位的二进制数据。二进制的每一个0或1是组成二进制的最小单位,称为一个比特（bit）。

　 　一般说来，计算机在同一时间内处理的一组二进制数称为一个计算机的“字”，而这组二进制数的位数就是“字长”。字长与计算机的功能和用途有很大的关系， 是计算机的一个重要技术指标。字长直接反映了一台计算机的计算精度，为适应不同的要求及协调运算精度和硬件造价间的关系，大多数计算机均支持变字长运算， 即机内可实现半字长、全字长（或单字长）和双倍字长运算。在其他指标相同时，字长越大计算机的处理数据的速度就越快。早期的微机字长一般是8位和16 位，386以及更高的处理器大多是32位。目前市面上的计算机的处理器大部分已达到64位。

　 字长由**微处理器（CPU）对外数据通路的数据总线**条数决定。

**最小可寻址单位**：内存的最小可寻址单位通常都是字节。也就是说一个指针地址值可对应内存中一个字节的空间。

**寻址空间**：寻址空间一般指的是CPU对于内存寻址的能力。CPU最大能查找多大范围的地址叫做寻址能力 ，CPU的寻址能力以字节为单位 （字节是最小可寻址单位），如32位寻址的CPU可以寻址2的32次方大小的地址也就是4G，这也是为什么32位寻址的CPU最大能搭配4G内存的原因 ，再多的话CPU就找不到了。

　　这里CPU的寻址位数是由**地址总线的位数**决定，32位CPU的寻址位数不一定是32位，因为32位CPU中32的意义为字长。

**有关寻址范围计算解释**，对于32位寻址的CPU，其地址值为32位的二进制数，所以可以表示的最大地址为2的32次方(即4G，最大内存空间为4GB，这里G表示数量、GB表示容量)。同时我们不难看出，**一个指针的值就是一个32位的二进制数，32位对应4字节**\*\*(Byte)。\*\*所以，**指针的大小实际上是由CPU的寻址位数决定，而不是字长。**

**再来分析一下如下的情况：**

　　32位处理器上32位操作系统的32位编译器，指针大小4字节。 　　32位处理器上32位操作系统的16位编译器，指针大小2字节。 　　32位处理器上16位操作系统的16位编译器，指针大小2字节。 　　16位处理器上16位操作系统的16位编译器，指针大小2字节。

这从结果看起来指针的大小和编译器有关？？

　　实际不是这样的，有这样的结果是因为**以上几种情况，处理器当前运行模式的寻址位数是不一样的，如下：**

　　Intel 32位处理器32位运行模式，逻辑寻址位数32，指针也就是32位，即4个字节 　　Intel 32位处理器16位虚拟机运行模式，逻辑寻址位数16，指针也就是16位，即2个字节

**编译器的作用**是根据目标硬件（即CPU）的特性将源程序编译为可在该硬件上运行的目标文件。如果一个编译器支持某32位的CPU，那么它就可以将源程序编译为可以在该CPU上运行的目标文件。**该源程序中指针大小也会被编译器根据该CPU的寻址位数（如32位）编译选择为4字节。**

[指针长度由谁决定？到底是多长？ - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/25362014)

### 字符串指针

从 C 语言角度来说，字符串在内存中是以字符数组的形式存在的，它的每个字符占据一个字节的内存空间，最后一个字符必须是 '\0'（空字符）。 在 C 语言中，字符串（如 "start\_task"）是以字符数组的形式存储在内存中，并且在编译时就会被赋值。因此，在函数调用时，可以直接将字符串常量作为参数传递给形参，就像传递一个指向字符数组首元素的指针一样。 例如，下面的代码中，我们定义了一个字符数组 str，然后直接将其作为参数传递给了函数 print\_string，而不必取地址或使用指针变量：

```c
void print_string(char *str) {
    printf("%s", str);
}
int main() {
    char str[] = "Hello, world!";
    print_string(str); // 直接传递字符数组 str
    return 0;
}
```

需要注意的是，字符串常量是不可修改的，因此不能通过形参来修改其内容。如果需要修改字符串，可以使用字符数组和指向字符数组的指针。 另外，还有一种常见的字符串指针用法，就是使用动态内存分配函数（如 malloc）来创建一个字符数组，并将其赋值给指针变量。这种方式可以在程序运行时动态地分配内存，非常灵活。例如：

```c
char *str;
str = (char *)malloc(sizeof(char) * 100); // 分配 100 字节的内存空间
strcpy(str, "Hello, world!"); // 将字符串 "Hello, world!" 复制到动态分配的内存中
printf("%s", str); // 输出字符串
free(str); // 释放内存空间
```

#### FreeRTOS中的实例

```c
void freertos_demo(void)
{
    xTaskCreate((TaskFunction_t )start_task,
                            (char * ) "start_task",
                           (configSTACK_DEPTH_TYPE) STARK_TASK_STACK_SIZE,
                           (void *) NULL,
                           (UBaseType_t) START_TASK_PRIO,
                           (TaskHandle_t *) &StartTask_Handler);
    vTaskStartScheduler();
}
```

"start\_task"就直接被赋值给字符串指针。

### 指针常量与常量指针

指针常量和指向常量的指针都是C语言中的指针类型，它们的区别在于指针本身的常量性和指针所指向的变量的常量性。

#### 指针常量-const pointer（int \*const p）

指针常量是指一个==指针变量本身是一个常量==，即不能再指向其他位置，但是可以通过这个指针访问和修改所指向的变量。在声明时，需要将const关键字放在指针符号 \* 后面。

```c
int a = 10, b = 20;
int * const p = &a;
*p = 30; // p指向的地址是一定的，但其内容可以修改
```

#### 常量指针-pointer to const（const int \*p， int const \*p）

常量指针本质上是一个指针，常量表示指针指向的内容，说明该指针指向一个“常量”。\*\*在常量指针中，指针指向的内容是不可改变的，指针看起来好像指向了一个常量。\*\*用法如下：

```c
int a = 10, b = 20;
const int *p = &a;
p = &b; // 指针可以指向其他地址，但是内容不可以改变
```

#### 简单的区分方法

指针常量:\*号在左，const在右，我们从左往右读，“\*”号读作“指针”，“const”读作“常量”，所以总的读作：“指针常量”。

常量指针：常量指针中const 总是位于\*号左侧，所以我们按照上面的方法依次从左往右读，合并起来就是“常量指针”。

#### 指向常量的指针常量

最经典的就是FreeRTOS的xTaskCreate动态创建函数的参数二`const char * const pcName`

具体来说，它包含两个关键字 `const`，第一个 `const` 表示 `pcName` 指向的字符内容是常量，不可修改，第二个 `const` 表示 `pcName` 本身是一个常量指针，即 `pcName` 的值（指向地址的指针）也是不可修改的。因此，该指针不能再指向其他地址，也不能修改所指向的字符内容。

#### 综合示例

```c
int main() {
    int m = 10;
    const int n = 20; // 必须在定义的同时初始化
 
    const int *ptr1 = &m; // 指针指向的内容不可改变
    int * const ptr2 = &m; // 指针不可以指向其他的地方
 
    ptr1 = &n; // 正确
    ptr2 = &n; // 错误，ptr2不能指向其他地方
 
    *ptr1 = 3; // 错误，ptr1不能改变指针内容
    *ptr2 = 4; // 正确
 
    int *ptr3 = &n; // 错误，常量地址不能初始化普通指针吗，常量地址只能赋值给常量指针
    const int * ptr4 = &n; // 正确，常量地址初始化常量指针
 
    int * const ptr5; // 错误，指针常量定义时必须初始化
    ptr5 = &m; // 错误，指针常量不能在定义后赋值
 
    const int * const ptr6 = &m; // 指向“常量”的指针常量，具有常量指针和指针常量的特点，指针内容不能改变，也不能指向其他地方，定义同时要进行初始化
    *ptr6 = 5; // 错误，不能改变指针内容
    ptr6 = &n; // 错误，不能指向其他地方
 
    const int * ptr7; // 正确
    ptr7 = &m; // 正确
 
    int * const ptr8 = &n;
    *ptr8 = 8;
 
    return 0;
}
```

#### FreeRTOS的一些实例

动态创建函数的入口参数多次用到了这些知识点

```c
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName, 
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )
```

* 第二个参数`const char * const pcName`是一个指向常量字符数组的指针，表示任务的名称。它是一个指向常量字符的指针常量，具体来说，它包含两个关键字 `const`，第一个 `const` 表示 `pcName` 指向的字符内容是常量，不可修改，第二个 `const` 表示 `pcName` 本身是一个常量指针，即 `pcName` 的值（指向地址的指针）也是不可修改的。因此，该指针不能再指向其他地址，也不能修改所指向的字符内容。
* 第四个参数`void * const pvParameters`是一个指向void类型的指针，具体来说，它包含了一个 `const` 关键字，表示指针 `pvParameter` 本身是一个指针常量，即它的值（指向地址的指针）是不可修改的。同时，它是一个 `void` 指针，表示可以指向任意类型的数据。
* 最后一个参数`TaskHandle_t * const pxCreatedTask`是一个指向TaskHandle\_t类型变量的指针，这是一个==指针常量==，在这个声明中，const关键字用于指定指针本身是一个常量，即不能再指向其他位置，而\*表示指针所指向的变量是可修改的。。因此，这个声明定义了一个可以修改指向内容，但不能修改指针指向的位置的常量指针。

### 函数指针的定义和声明

函数指针的定义和声明方式如下：

```c
返回类型 (*函数指针变量名)(参数列表);
```

其中，括号中的`*`表示指针类型，`函数指针变量名`是指针变量的名称，`参数列表`是函数的参数列表。例如，下面是一个函数指针的声明示例：

```c
int (*pFunc)(int, int);
```

这个声明表示`pFunc`是一个指向返回类型为`int`，参数列表为`(int, int)`的函数指针。

#### 函数指针的赋值和使用

函数指针的赋值和使用方式如下：

```c
函数指针变量名 = 函数名;
```

例如，下面是一个函数指针的赋值示例：

```c
int add(int a, int b) {
    return a + b;
}
int (*pFunc)(int, int) = add;
```

这个示例中，我们定义了一个名为`add`的函数，然后将其地址赋值给了指针变量`pFunc`。现在，`pFunc`就可以像一个函数一样被调用了：

```c
int result = pFunc(3, 4); // result = 7
```

这个示例中，我们通过指针变量`pFunc`调用了函数`add`，并且将其返回值赋给了变量`result`。

#### 函数指针的地址强制赋值

```c
#include "uart.h"

void delay(int d)
{
	while(d--);
}

int main()
{
	
	void (*app)(void);
	
	uart_init();

	putstr("bootloader\r\n");
	
	/* start app */
	app = (void (*)(void))0x08040001;  
	
	app();
	
	return 0;
}
```

#### 函数指针作为参数

函数指针可以作为函数的参数传递。例如，下面是一个示例程序，其中函数`operate`接受一个函数指针作为参数，然后调用该函数指针指向的函数：

```c
int add(int a, int b) {
    return a + b;
}
int multiply(int a, int b) {
    return a * b;
}
int operate(int a, int b, int (*pFunc)(int, int)) {
    return pFunc(a, b);
}
int main() {
    int result1 = operate(3, 4, add); // result1 = 7
    int result2 = operate(3, 4, multiply); // result2 = 12
    return 0;
}
```

在这个示例程序中，我们定义了两个函数`add`和`multiply`，它们分别实现了加法和乘法运算。然后，我们定义了一个函数`operate`，接受三个参数：两个整数`a`和`b`，以及一个函数指针`pFunc`。该函数会调用`pFunc`指向的函数，并将`a`和`b`作为参数传递给它。最后，我们在`main`函数中调用了`operate`函数，并传递了不同的函数指针作为参数，分别获得了加法和乘法的结果。

#### 函数指针作为返回值

函数指针可以作为函数的返回值返回。例如，下面是一个示例程序，其中函数`getFunction`返回一个函数指针：

```c
#include <stdio.h>
int add(int a, int b) {
    return a + b;
}
int multiply(int a, int b) {
    return a * b;
}
int (*getFunction(char op))(int, int) {
    if (op == '+') {
        return add;
    } else if (op == '*') {
        return multiply;
    } else {
        return NULL;
    }
}
int main() {
    int (*pFunc)(int, int) = getFunction('+');
    int result = pFunc(3, 4); // result = 7
    printf("%d",result);
    return 0;
}
```

在这个示例程序中，我们定义了两个函数`add`和`multiply`，它们分别实现了加法和乘法运算。然后，我们定义了一个函数`getFunction`，接受一个字符参数`op`，并根据`op`的不同返回不同的函数指针。最后，我们在`main`函数中调用了`getFunction`函数，并将`'+'`作为参数传递给它，然后将返回的函数指针赋值给了指针变量`pFunc`。现在，我们可以通过`pFunc`调用函数`add`了。

#### 函数指针作为回调函数

函数指针经常被用作回调函数，回调函数是指在程序运行期间，由其他函数调用的函数。回调函数可以作为参数传递给调用它的函数，并在需要的时候被调用。例如，下面是一个示例程序，其中函数`filter`接受一个整型数组和一个函数指针作为参数，然后调用该函数指针指向的函数来过滤数组中的元素：

```c
#include <stdio.h>
int isEven(int n) {
    return n % 2 == 0;
}
int isOdd(int n) {
    return n % 2 == 1;
}
void filter(int* arr, int len, int (*pFunc)(int)) {
    for (int i = 0; i < len; i++) {
        if (pFunc(arr[i])) {
            printf("%d ", arr[i]);
        }
    }
}
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6};
    int len = sizeof(arr) / sizeof(arr[0]);
    printf("Even numbers: ");
    filter(arr, len, isEven);
    printf("\nOdd numbers: ");
    filter(arr, len, isOdd);
    return 0;
}
```

在这个示例程序中，我们定义了两个函数`isEven`和`isOdd`，它们分别判断一个整数是否为偶数和奇数。然后，我们定义了一个函数`filter`，接受一个整型数组`arr`、数组长度`len`和一个函数指针`pFunc`作为参数。该函数会遍历数组中的元素，并调用`pFunc`指向的函数来判断每个元素是否符合条件。最后，我们在`main`函数中调用了`filter`函数，并将`isEven`和`isOdd`函数指针作为参数传递给它，分别打印出了数组中的偶数和奇数。

#### 函数指针和typedef

为了方便使用，我们可以使用`typedef`来定义函数指针类型。例如，下面是一个示例程序，其中使用了`typedef`来定义了一个名为`Func`的函数指针类型：

```c
#include <stdio.h>
typedef int (*Func)(int, int);
int add(int a, int b) {
    return a + b;
}
int main() {
    Func pFunc = add;
    int result = pFunc(3, 4); // result = 7
    printf("%d\n", result);
    return 0;
}
```

在这个示例程序中，我们使用`typedef`定义了一个名为`Func`的函数指针类型，它指向返回类型为`int`，参数列表为`(int, int)`的函数。然后，我们定义了一个函数`add`，实现了加法运算。最后，我们在`main`函数中声明了一个`Func`类型的指针变量`pFunc`，并将`add`函数的地址赋值给了它。现在，我们可以通过`pFunc`调用函数`add`了。

#### FreeRTOS的函数指针实例

动态创建函数xTaskCreate() 的参数如下

```c
    BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName, 
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )
```

`TaskFunction_t`是一个typedef定义的函数指针类型，表示任务函数的类型。

```c
typedef void (* TaskFunction_t)( void * );
```

**void \* 类型的参数**

`void *` 类型的参数是一种指向`不确定类型`的指针类型，它可以指向任意类型的数据，因为它不知道指向的数据的具体类型==，所以在使用它指向的数据时，需要进行类型转换==。 如果要使用 `void *` 类型的参数，需要先将它转换成实际的类型，然后再进行操作。例如，如果我们有一个 `void *` 类型的参数 `p`，它指向一个整数变量，我们可以将它转换为 `int *` 类型的指针，然后再使用 `*p` 访问指向的整数变量的值，示例代码如下：

```c
void myFunction(void *p) {
    int *ptr = (int *)p; // 将 void * 类型的参数转换成 int * 类型的指针
    printf("%d\n", *ptr); // 访问指向的整数变量的值
}
int main() {
    int num = 10;
    myFunction(&num); // 传递一个指向整数变量的指针作为参数
    return 0;
}
```

#### 函数指针的地址赋值

`void (*app)(void)` 是一个函数指针变量的定义，它表示一个指向参数为 `void`、返回值为 `void` 的函数的指针。这里定义的变量名为 `app`，它是一个指向函数的指针变量。 `(void (*)(void))` 是一个强制类型转换，它将一个无类型指针 `0x08040001` 转换成一个指向参数为 `void`、返回值为 `void` 的函数指针类型，即 `void (*)(void)` 类型。 因此，`app = (void (*)(void))0x08040001;` 的作用是将地址 `0x08040001` 强制转换成一个 `void (*)(void)` 类型的函数指针，并将它赋值给 `app` 变量，从而得到了一个指向地址 `0x08040001` 处的函数的指针 `app`。这个函数指针可以用来调用该地址处的函数，即启动应用程序。

```c
#include "uart.h"

void delay(int d)
{
	while(d--);
}

int main()
{
	
	void (*app)(void);
	
	uart_init();

	putstr("bootloader\r\n");
	
	/* start app */
	app = (void (*)(void))0x08040001;  
	
	app();
	
	return 0;
}
```

#### 指向任何类型的函数指针

在C语言中，可以使用`void*`类型的指针或者使用`typedef`来实现指向任何类型的函数指针。需要注意的是，使用指向任何类型的函数指针时需要特别小心，确保指向的函数和实际需要调用的函数类型一致，否则可能会导致未定义的行为或者程序崩溃。

```c
#include <stdio.h>
// 使用void*类型的指针实现
void func1() {
    printf("This is func1\n");
}
void func2(int x, int y) {
    printf("x + y = %d\n", x + y);
}
int main() {
    void* p1 = NULL;
    p1 = &func1;
    ((void(*)())p1)();
    
    // 使用typedef实现
    typedef void(*func_ptr_t)();
    func_ptr_t p2 = NULL;
    p2 = &func1;
    p2();
    p2 = &func2;
    p2(3, 5);
    return 0;
}
```

在上面的示例中，我们分别使用了`void*`类型的指针和`typedef`实现了指向任何类型的函数指针。在使用`void*`类型的指针时，需要将其强制转换为指向具体类型的函数指针，并使用括号将函数指针转换为函数调用。在使用`typedef`实现时，使用了函数指针类型别名`func_ptr_t`来代表指向任何类型的函数指针，使用`func_ptr_t`来声明函数指针变量，并直接使用函数指针变量来调用函数。 需要注意的是，在使用指向任何类型的函数指针时，必须确保指向的函数和实际需要调用的函数类型一致，否则可能会导致未定义的行为或者程序崩溃。

### 函数指针与变量指针赋值时的差异

函数指针作为形参时传入函数时不需要加 `&`，是因为函数名本身就是一个指向函数的指针，它可以直接传递给形参。例如：

```c
void doSomething(int a, int b, int (*pFunc)(int, int)) {
    int result = pFunc(a, b);
    // do something with the result
}
int add(int a, int b) {
    return a + b;
}
int main() {
    doSomething(1, 2, add); // 函数名 add 可以直接传递给 pFunc 形参，无需加 &
    return 0;
}
```

而变量指针作为形参时传入参数时需要加 `&`，是因为函数中的形参和实参是两个不同的变量，它们的地址不同。如果不加 `&`，就会把实参的值传递给形参，而不是地址，这样就无法在函数中修改实参的值。例如：

```c
void modify(int *p) {
    *p = 100;
}
int main() {
    int a = 0;
    modify(&a); // 传递 a 的地址
    printf("%d", a); // 输出 100
    return 0;
}
```

### 含参宏定义

#### 语法

含参宏定义的语法形式为：

```C
#define macro_name(parameter_list) replacement_text
```

其中，`macro_name`是宏的名称，`parameter_list`是宏的参数列表，可以包含零个或多个参数，用逗号隔开。`replacement_text`是宏的替换文本，它可以包含参数和其他C语句，但不需要加上分号。

#### 示例程序

下面是一个简单的示例程序，演示了如何使用含参宏定义：

```c
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
    int a = 3;
    int b = SQUARE(a + 1);
    printf("%d\n", b); // 输出 16
    return 0;
}
```

在这个示例程序中，我们定义了一个名为`SQUARE`的含参宏，它接受一个参数`x`，并返回`x`的平方。在`main`函数中，我们定义了一个整型变量`a`，并赋值为3。然后，我们使用`SQUARE`宏计算了`(a+1)`的平方，并将结果赋值给了整型变量`b`。最后，我们使用`printf`函数输出了`b`的值，结果为16。

### 宏定义函数

宏定义函数是 C 语言中一种宏替换的形式，它通过在代码中定义一个宏来代替函数的调用。宏定义函数不会产生函数调用的开销，可以在一定程度上提高程序的执行效率。 宏定义函数的语法格式如下：

```c
#define 函数名(参数列表) 宏体
```

其中，`函数名` 是宏定义函数的名称，`参数列表` 是函数的参数列表，`宏体` 是宏定义函数的函数体。 下面是一个使用宏定义函数的示例程序：

```c
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
    int x = 10, y = 20;
    int max = MAX(x, y);
    printf("max = %d\n", max);
    return 0;
}
```

在上面的示例程序中，我们定义了一个名为 `MAX` 的宏定义函数，它接受两个参数 `a` 和 `b`，返回它们中的最大值。在 `main` 函数中，我们定义了两个整数变量 `x` 和 `y`，然后调用宏定义函数 `MAX` 来获取它们中的最大值，并将结果赋值给变量 `max`。最后，我们通过 `printf` 函数输出结果。 除了上面的示例程序中所示的基本使用方法外，宏定义函数还有以下常见的使用情况：

#### 宏定义函数可以嵌套使用：

```c
#define SQUARE(x) ((x) * (x))
#define CUBE(x) (SQUARE(x) * (x))
int main() {
    int x = 3;
    int square = SQUARE(x);
    int cube = CUBE(x);
    printf("square = %d, cube = %d\n", square, cube);
    return 0;
}
```

在上面的示例程序中，我们定义了两个宏定义函数 `SQUARE` 和 `CUBE`，分别用于计算一个数的平方和立方。在 `CUBE` 函数的宏体中，我们调用了 `SQUARE` 函数来计算一个数的平方。 总的来说，宏定义函数是 C 语言中一种非常有用的宏替换形式，它可以简化代码，提高程序的执行效率。但需要注意的是，宏定义函数在展开后会直接替换原代码，因此在使用时需要仔细考虑宏定义函数的参数和宏体，以避免出现意外的错误。

#### FreeRTOS的宏定义函数实例

串口库中的HAL\_UART\_GET\_FLAG(**HANDLE**, **FLAG**)函数，其实就是一个宏定义函数

```c
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))
```

### 类型强制转换

### 位带操作

[(20条消息) 快速理解STM32位带操作原理和用途\_strongerHuang的博客-CSDN博客](https://blog.csdn.net/ybhuangfugui/article/details/108067563)

### 按位与或，异，按位取反，逻辑取反

#### 按位与运算

按位与运算符（&）的作用是将两个操作数的二进制位逐位进行与操作，只有在两个二进制位都是1时，结果才为1，否则为0。示例程序如下：

```c
#include <stdio.h>
int main() {
    int a = 15; //二进制表示为1111
    int b = 7;  //二进制表示为0111
    int c = a & b; //二进制表示为0111，即7
    printf("%d", c);
    return 0;
}
```

#### 按位或运算

按位或运算符（|）的作用是将两个操作数的二进制位逐位进行或操作，只要两个二进制位中有一个为1，结果就为1，否则为0。示例程序如下：

```c
#include <stdio.h>
int main() {
    int a = 15; //二进制表示为1111
    int b = 7;  //二进制表示为0111
    int c = a | b; //二进制表示为1111，即15
    printf("%d", c);
    return 0;
}
```

#### 按位异或运算

按位异或运算符（^）的作用是将两个操作数的二进制位逐位进行异或操作，如果两个二进制位不同，则结果为1，否则为0。示例程序如下：

```c
#include <stdio.h>
int main() {
    int a = 15; //二进制表示为1111
    int b = 7;  //二进制表示为0111
    int c = a ^ b; //二进制表示为1000，即8
    printf("%d", c);
    return 0;
}
```

#### 按位取反运算

按位取反运算符（\~）的作用是将操作数的二进制位逐位进行取反操作，即0变为1，1变为0。示例程序如下：

```c
#include <stdio.h>
int main() {
    int a = 15; //二进制表示为1111
    int b = ~a; //二进制表示为0000 0000 0000 0000 0000 0000 0000 1111，即-16
    printf("%d", b);
    return 0;
}
```

#### 逻辑取反

逻辑非运算符（!）的作用是对一个表达式取反，即如果表达式的值为真，则返回假，否则返回真

```c
#include <stdio.h>
int main() {
    int a = 0;
    int b = !a; //a为假，取反后b为真
    printf("%d", b); //输出1
    return 0;
}
```

### 弱定义

### HAL库的头文件包含

stm32f1xx\_hal\_conf.h文件里包含着所有外设的.h文件，就是#include "stm32f1xx\_hal\_rcc.h"，#include "stm32f1xx\_hal\_gpio.h"这样的头文件，而该文件又被包含在stm32f1xx\_hal.h文件里，stm32f1xx\_hal.h又被包含在stm32f1xx.h，也就是如以下的关系，

最终只需要调用stm32f1xx.h

而在正点原子的Hal库例程中，将"stm32f1xx.h"包含在sys.h中，同时sys.h还实现了typedef uint32\_t u32这样的重定义，并实现了IO的位带操作，因此我们可以直接调用sys.h

![](https://s2.loli.net/2023/03/14/xGFAl7NqwmkKnWU.png)
