# 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中宏定义）

<img src="https://s2.loli.net/2023/04/03/CdLylNnmU27Y41V.png" alt="" height="60%" width="60%">

接着编写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

<img src="https://s2.loli.net/2023/03/14/xGFAl7NqwmkKnWU.png" alt="" height="60%" width="60%">


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://oceanaparts.gitbook.io/halnote/hal-ku-de-yi-xie-zhi-shi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
