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不相等,程序会在运行时触发断言失败,==终止程序的运行==,并输出错误信息。这样做可以帮助我们在开发过程中尽早发现错误,避免出现更严重的问题。

情况一:检查函数参数

#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不在有效范围内,程序会在运行时触发断言失败,终止程序的运行,并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法参数的情况。

情况二:检查函数返回值

#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库自编写:

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,定义如下

#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),==即什么都不做。==

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

接着编写assert_failed()函数,可在在main.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或者__LINE__宏时,可能会导致__FILE____LINE__的值出现异常。因此,在使用__FILE____LINE__时,应该注意这些情况的可能影响,并尽量避免使用会导致异常的代码结构。

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

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

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

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

结构体

定义方法

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

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

//此声明声明了拥有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==

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文件中就可以直接使用

GPIO_InitTypeDef gpio_init_struct;

结构体上可以互相嵌套的

FreeRTOS的实例

在task.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中

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

观察上面,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语言语法上的使用的不同

#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;
}

结构体指针

结构体指针的基本用法

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

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

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

struct Person *p;

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

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

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

结构体指针作为函数参数

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

#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 函数来输出它的成员变量。

结构体指针数组

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

#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;
}

在上面的示例程序中,我们定义了三个结构体变量 p1p2p3,然后定义了一个指针数组 arr,其中每个元素都是一个指向 Person 结构体类型的指针。我们将三个结构体变量的地址分别存储到了数组的不同元素中,并使用循环遍历数组,输出结构体的成员变量。 总的来说,结构体指针是 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 函数释放动态分配的内存空间。

数组指针

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

数组指针的基本用法

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

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

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

int *p;
p = arr;

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

*p = 4;

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

p++;

数组指针作为函数参数

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

#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 函数来输出它的元素。

数组指针的动态内存分配

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

#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 函数释放动态分配的内存空间。

数组指针的二维数组

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

#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 语言时,掌握数组指针的使用方法是非常重要的

指针数组

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

#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;
}

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

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

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

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

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

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

#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;
}

输出:

arr = 0x7fff5fbff620
&arr[0] = 0x7fff5fbff620

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

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

#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;
}

输出:

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

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

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

#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;
}

输出:

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

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

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

#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;

#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;
}

输出:

ptr[2] = 3

数组作函数参数

//----数组作函数参数--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)

字符串指针

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

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

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

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

FreeRTOS中的实例

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关键字放在指针符号 * 后面。

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

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

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

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 的值(指向地址的指针)也是不可修改的。因此,该指针不能再指向其他地址,也不能修改所指向的字符内容。

综合示例

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的一些实例

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

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关键字用于指定指针本身是一个常量,即不能再指向其他位置,而*表示指针所指向的变量是可修改的。。因此,这个声明定义了一个可以修改指向内容,但不能修改指针指向的位置的常量指针。

函数指针的定义和声明

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

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

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

int (*pFunc)(int, int);

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

函数指针的赋值和使用

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

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

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

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

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

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

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

函数指针的地址强制赋值

#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接受一个函数指针作为参数,然后调用该函数指针指向的函数:

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;
}

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

函数指针作为返回值

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

#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;
}

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

函数指针作为回调函数

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

#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;
}

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

函数指针和typedef

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

#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() 的参数如下

    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定义的函数指针类型,表示任务函数的类型。

typedef void (* TaskFunction_t)( void * );

void * 类型的参数

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

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。这个函数指针可以用来调用该地址处的函数,即启动应用程序。

#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来实现指向任何类型的函数指针。需要注意的是,使用指向任何类型的函数指针时需要特别小心,确保指向的函数和实际需要调用的函数类型一致,否则可能会导致未定义的行为或者程序崩溃。

#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来声明函数指针变量,并直接使用函数指针变量来调用函数。 需要注意的是,在使用指向任何类型的函数指针时,必须确保指向的函数和实际需要调用的函数类型一致,否则可能会导致未定义的行为或者程序崩溃。

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

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

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;
}

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

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

含参宏定义

语法

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

#define macro_name(parameter_list) replacement_text

其中,macro_name是宏的名称,parameter_list是宏的参数列表,可以包含零个或多个参数,用逗号隔开。replacement_text是宏的替换文本,它可以包含参数和其他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 语言中一种宏替换的形式,它通过在代码中定义一个宏来代替函数的调用。宏定义函数不会产生函数调用的开销,可以在一定程度上提高程序的执行效率。 宏定义函数的语法格式如下:

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

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

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

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

#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;
}

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

FreeRTOS的宏定义函数实例

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

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

类型强制转换

位带操作

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

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

按位与运算

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

#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。示例程序如下:

#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。示例程序如下:

#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。示例程序如下:

#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;
}

逻辑取反

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

#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

Last updated