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来检查变量a和b是否相等。由于a和b不相等,程序会在运行时触发断言失败,==终止程序的运行==,并输出错误信息。这样做可以帮助我们在开发过程中尽早发现错误,避免出现更严重的问题。
情况一:检查函数参数
在上面的示例程序中,我们使用了断言assert来检查函数print_number的参数n是否在有效范围内。由于第二次调用print_number时,参数n不在有效范围内,程序会在运行时触发断言失败,终止程序的运行,并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法参数的情况。
情况二:检查函数返回值
在上面的示例程序中,我们使用了断言assert来检查函数divide的参数b是否为0。由于第二次调用divide时,参数b为0,程序会在运行时触发断言失败,终止程序的运行,并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法返回值的情况。
注意事项
在使用C语言断言时,需要注意以下几点:
断言只在调试模式下有效,因为在==发布模式==下,断言会被编译器忽略。
断言应该用于检查程序运行时的条件是否满足,而不是用于处理已知错误或异常情况。
断言的条件应该是纯粹的布尔表达式,不能包含副作用或需要计算的表达式。
断言的错误信息应该尽可能地清晰和明确,以便于快速定位和解决问题
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中
观察上面,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语言语法上的使用的不同
结构体指针
结构体指针的基本用法
结构体指针的定义方式与普通指针相同,只是指向的类型是结构体类型。例如,下面是一个结构体类型的定义:
我们可以定义一个指向该结构体类型的指针变量 p,如下所示:
接下来,我们可以通过该指针变量来访问结构体成员变量。例如,我们可以通过指针 p 访问结构体成员变量 name,如下所示:
上面的代码将字符串 "Tom" 复制到了指针 p 所指向的结构体变量的 name 成员变量中。
结构体指针作为函数参数
结构体指针常常被用作函数参数,以便在函数中对结构体进行操作。下面是一个示例程序:
在上面的示例程序中,我们定义了一个名为 printPerson 的函数,它接受一个指向结构体类型 Person 的指针作为参数,并输出结构体的成员变量。在 main 函数中,我们定义了一个结构体变量 p1,给它的成员变量赋值,并将其地址传递给 printPerson 函数来输出它的成员变量。
结构体指针数组
结构体指针还可以用于定义结构体指针数组。下面是一个示例程序:
在上面的示例程序中,我们定义了三个结构体变量 p1、p2 和 p3,然后定义了一个指针数组 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 语言时,掌握数组指针的使用方法是非常重要的
指针数组
首先它是一个数组,数组的元素都是指针,==与数组指针相区别==,
在上面的示例程序中,我们首先定义了三个整型变量 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 类型数据的数组,即数组指针。
我们可以借助下面的图加深理解:

指针与数组地址结合的一些知识
数组名是数组第一个元素的地址
例如,定义一个整型数组 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接受一个函数指针作为参数,然后调用该函数指针指向的函数:
在这个示例程序中,我们定义了两个函数add和multiply,它们分别实现了加法和乘法运算。然后,我们定义了一个函数operate,接受三个参数:两个整数a和b,以及一个函数指针pFunc。该函数会调用pFunc指向的函数,并将a和b作为参数传递给它。最后,我们在main函数中调用了operate函数,并传递了不同的函数指针作为参数,分别获得了加法和乘法的结果。
函数指针作为返回值
函数指针可以作为函数的返回值返回。例如,下面是一个示例程序,其中函数getFunction返回一个函数指针:
在这个示例程序中,我们定义了两个函数add和multiply,它们分别实现了加法和乘法运算。然后,我们定义了一个函数getFunction,接受一个字符参数op,并根据op的不同返回不同的函数指针。最后,我们在main函数中调用了getFunction函数,并将'+'作为参数传递给它,然后将返回的函数指针赋值给了指针变量pFunc。现在,我们可以通过pFunc调用函数add了。
函数指针作为回调函数
函数指针经常被用作回调函数,回调函数是指在程序运行期间,由其他函数调用的函数。回调函数可以作为参数传递给调用它的函数,并在需要的时候被调用。例如,下面是一个示例程序,其中函数filter接受一个整型数组和一个函数指针作为参数,然后调用该函数指针指向的函数来过滤数组中的元素:
在这个示例程序中,我们定义了两个函数isEven和isOdd,它们分别判断一个整数是否为偶数和奇数。然后,我们定义了一个函数filter,接受一个整型数组arr、数组长度len和一个函数指针pFunc作为参数。该函数会遍历数组中的元素,并调用pFunc指向的函数来判断每个元素是否符合条件。最后,我们在main函数中调用了filter函数,并将isEven和isOdd函数指针作为参数传递给它,分别打印出了数组中的偶数和奇数。
函数指针和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 的宏定义函数,它接受两个参数 a 和 b,返回它们中的最大值。在 main 函数中,我们定义了两个整数变量 x 和 y,然后调用宏定义函数 MAX 来获取它们中的最大值,并将结果赋值给变量 max。最后,我们通过 printf 函数输出结果。 除了上面的示例程序中所示的基本使用方法外,宏定义函数还有以下常见的使用情况:
宏定义函数可以嵌套使用:
在上面的示例程序中,我们定义了两个宏定义函数 SQUARE 和 CUBE,分别用于计算一个数的平方和立方。在 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