HAL库的一些知识

断言

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

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

示例程序

包含assert.h

#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编译器下输出:

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

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

情况一:检查函数参数

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

情况二:检查函数返回值

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

注意事项

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

  1. 断言只在调试模式下有效,因为在==发布模式==下,断言会被编译器忽略。

  2. 断言应该用于检查程序运行时的条件是否满足,而不是用于处理已知错误或异常情况。

  3. 断言的条件应该是纯粹的布尔表达式,不能包含副作用或需要计算的表达式。

  4. 断言的错误信息应该尽可能地清晰和明确,以便于快速定位和解决问题

HAL库的实例

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

NVIC使能函数如下,使用了assert_param函数,该函数为HAL库自编写:

其中IS_NVIC_DEVICE_IRQ,定义如下

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

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

要实现真正的断言需要宏定义USE_FULL_ASSERT(可以在stm32xx_hal_conf.h文件中取消注释,如下图,也可以在IDE如MDK中宏定义)

接着编写assert_failed()函数,可在在main.c中定义

实现自己的断言形式

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

需要注意的是,在某些特殊情况下,__FILE____LINE__的值可能会出现意外的变化。例如,在代码中使用了#pragma或者__LINE__宏时,可能会导致__FILE____LINE__的值出现异常。因此,在使用__FILE____LINE__时,应该注意这些情况的可能影响,并尽量避免使用会导致异常的代码结构。

在gcc编译器下输出:

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

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

结构体

定义方法

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

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

HAL库中的实例

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

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

结构体上可以互相嵌套的

FreeRTOS的实例

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

在task.h中

观察上面,tskTCBtskTaskControlBlock在C语言语法上的使用有所不同

  • tskTCB是被typedef struct重定义出来的,可以直接作为一种结构体变量类型使用,这边有使用typedef进行了重命名

  • tskTaskControlBlock是结构体类型,其实就是结构体标签,但在使用时需要加前缀struct,说明是一个结构体

  • 之所以一个在.h中typedef,一个在.c中typedef是因为tskTCB与TCB_t只在task.c中使用,而tskTaskControlBlock与TaskHandle_t在其他文件也会使用,因此需要在.h中进行重定义

  • 之所以先使用了struct tskTaskControlBlock而不是直接使用tskTaskControlBlock来声明结构体,是因为在.h文件中只需要知道该结构体的存在即可,而不需要知道其具体实现细节,这样可以提高代码的封装性和可维护性。

下面给出一个简单的实例,说明tskTCBtskTaskControlBlock在C语言语法上的使用的不同

结构体指针

结构体指针的基本用法

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

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

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

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

结构体指针作为函数参数

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

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

结构体指针数组

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

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

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

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

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

数组指针

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

数组指针的基本用法

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

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

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

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

数组指针作为函数参数

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

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

数组指针的动态内存分配

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

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

数组指针的二维数组

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

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

指针数组

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

在上面的示例程序中,我们首先定义了三个整型变量 abc,然后定义了一个指针数组 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

数组指针和指针数组的区别 - 知乎 (zhihu.com)

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

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

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

输出:

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

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

输出:

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

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

输出:

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

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

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

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

输出:

数组作函数参数

指针的大小由什么决定

指针大小是由当前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)

字符串指针

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

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

FreeRTOS中的实例

"start_task"就直接被赋值给字符串指针。

指针常量与常量指针

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

指针常量-const pointer(int *const p)

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

常量指针-pointer to const(const int *p, int const *p)

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

简单的区分方法

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

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

指向常量的指针常量

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

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

综合示例

FreeRTOS的一些实例

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

  • 第二个参数const char * const pcName是一个指向常量字符数组的指针,表示任务的名称。它是一个指向常量字符的指针常量,具体来说,它包含两个关键字 const,第一个 const 表示 pcName 指向的字符内容是常量,不可修改,第二个 const 表示 pcName 本身是一个常量指针,即 pcName 的值(指向地址的指针)也是不可修改的。因此,该指针不能再指向其他地址,也不能修改所指向的字符内容。

  • 第四个参数void * const pvParameters是一个指向void类型的指针,具体来说,它包含了一个 const 关键字,表示指针 pvParameter 本身是一个指针常量,即它的值(指向地址的指针)是不可修改的。同时,它是一个 void 指针,表示可以指向任意类型的数据。

  • 最后一个参数TaskHandle_t * const pxCreatedTask是一个指向TaskHandle_t类型变量的指针,这是一个==指针常量==,在这个声明中,const关键字用于指定指针本身是一个常量,即不能再指向其他位置,而*表示指针所指向的变量是可修改的。。因此,这个声明定义了一个可以修改指向内容,但不能修改指针指向的位置的常量指针。

函数指针的定义和声明

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

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

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

函数指针的赋值和使用

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

例如,下面是一个函数指针的赋值示例:

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

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

函数指针的地址强制赋值

函数指针作为参数

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

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

函数指针作为返回值

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

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

函数指针作为回调函数

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

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

函数指针和typedef

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

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

FreeRTOS的函数指针实例

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

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

void * 类型的参数

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

函数指针的地址赋值

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

指向任何类型的函数指针

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

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

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

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

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

含参宏定义

语法

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

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

示例程序

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

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

宏定义函数

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

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

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

宏定义函数可以嵌套使用:

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

FreeRTOS的宏定义函数实例

串口库中的HAL_UART_GET_FLAG(HANDLE, FLAG)函数,其实就是一个宏定义函数

类型强制转换

位带操作

(20条消息) 快速理解STM32位带操作原理和用途_strongerHuang的博客-CSDN博客

按位与或,异,按位取反,逻辑取反

按位与运算

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

按位或运算

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

按位异或运算

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

按位取反运算

按位取反运算符(~)的作用是将操作数的二进制位逐位进行取反操作,即0变为1,1变为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

Last updated