首页 > 用户笔记 > 轻染的笔记 阅读:38

C语言指针在嵌入式系统中的常用操作(6个)

在嵌入式系统开发过程中,C语言指针常用的操作有以下 6 种。

一、 访问硬件寄存器

嵌入式系统中,硬件寄存器通常被映射到特定的内存地址。通过指针可以直接访问这些寄存器。如下所示为某 32 芯片定义的寄存器:
/* GPIOx(x=A,B,C,D,E,F,G) definitions */
#define GPIOA                      (GPIO_BASE + 0x00000000U)
#define GPIOB                      (GPIO_BASE + 0x00000400U)
#define GPIOC                      (GPIO_BASE + 0x00000800U)
#define GPIOD                      (GPIO_BASE + 0x00000C00U)
#define GPIOE                      (GPIO_BASE + 0x00001000U)
#define GPIOF                      (GPIO_BASE + 0x00001400U)
#define GPIOG                      (GPIO_BASE + 0x00001800U)

/* AFIO definitions */
#define AFIO                       AFIO_BASE

/* registers definitions */
/* GPIO registers definitions */
#define GPIO_CTL0(gpiox)           REG32((gpiox) + 0x00U)    /*!< GPIO port control register 0 */
#define GPIO_CTL1(gpiox)           REG32((gpiox) + 0x04U)    /*!< GPIO port control register 1 */
#define GPIO_ISTAT(gpiox)          REG32((gpiox) + 0x08U)    /*!< GPIO port input status register */
#define GPIO_OCTL(gpiox)           REG32((gpiox) + 0x0CU)    /*!< GPIO port output control register */
#define GPIO_BOP(gpiox)            REG32((gpiox) + 0x10U)    /*!< GPIO port bit operation register */
#define GPIO_BC(gpiox)             REG32((gpiox) + 0x14U)    /*!< GPIO bit clear register */
#define GPIO_LOCK(gpiox)           REG32((gpiox) + 0x18U)    /*!< GPIO port configuration lock register */
#define GPIOx_SPD(gpiox)           REG32((gpiox) + 0x3CU)    /*!< GPIO port bit speed register */
如需要访问每个 GPIO 对应的 GPIO_CTL0 寄存器,只需要调用宏 GPIO_CTL0 并填入 GPIOx 定义的地址即可:
GPIO_CTL0(GPIOA) = 0xE800CF57;
如此便封装了硬件寄存器的地址,对于用户而言,读写见名知意的寄存器名,比直接与硬件寄存器地址打交道更为直观和方便,那么上述对硬件地址的封装究竟是如何实现的呢?这需要找到宏 REG32 的定义:
#define REG32(addr)                  (*(volatile uint32_t *)(uint32_t)(addr))
#define REG16(addr)                  (*(volatile uint16_t *)(uint32_t)(addr))
#define REG8(addr)                   (*(volatile uint8_t *)(uint32_t)(addr))
可以看到这个宏首先是将 addr 强转为了 32bit 的无符号数,再转化为 32bit 地址,最后访问地址内容。

转化过程:数字 -> 32bit数字 -> 32bit地址 -> 访问地址内容
转化操作:addr -> 32bit无符号数:(uint32_t)(addr) -> 32bit地址:(uint32_t*)(uint32_t)(addr) -> 访问地址内容:*((uint32_t*)(uint32_t)(addr))

在理解这行代码时 volatile 关键字可以忽略,但在实际使用中 volatile 关键字的作用不可忽略,其作用如下:

二、模拟面向对象操作

C++ 或者 python 中面向对象是通过定义类(class)来实现的,然后创建对象来实现的,一个类中包含了最基本的成员变量和成员函数,那么C语言中要定义一个类则是通过定义结构体来模拟实现的,结构体中的变量则是模拟的类中的成员变量,结构体中的函数指针则是模拟类中的成员函数,如下所示:
typedef struct
{
    int point_x;        //圆心坐标x   
    int point_y;        //圆心坐标y
    double radius;      // 圆的半径
    double (*func)(sCircle_t*);  // 函数指针,用于对圆进行相关的计算操作
}sCircle_t
上述使用结构体模拟了类定义了一个圆类,类中定义了成员变量和成员函数,则使用此结构体可以实例化一个对象。
sCircle_t sCircle;
如此便模拟了 C++ 中的创建对象的操作,但是还是和 C++ 中实例化对象有很大的区别,结构体中没有构造函数,因此在定义了变量之后,需要手动初始化,为结构体中的变量和函数指针赋初值。
// 计算圆的面积
double circle_area(sCircle_t* c)
{
    return 3.14159 * c->radius * c->radius;
}
// 创建对象时初始化
sCircle_t sCircle =
{
    .point_x = 0,
    .point_y = 0,
    .radius = 5.5,
    .func = circle_area,
};
//或者在系统跑起来的时候调用一次初始化
void Circle_init(sCircle_t* c)
{
    c->point_x = 0,
    c->point_y = 0,
    c->radius = 5.5,
    c->func = circle_area,
}
在上述的结构体中定义了函数指针double (func)(sCircle_t); ,但是并没有指明这个函数指针指向的函数的功能作用是什么,那么就意味着,只要在使用之前改变了函数指针指向的函数,那这个函数指针就可以实现不同的函数功能:
// 计算圆的面积
double circle_area(sCircle_t* c)
{
    return 3.14159 * c->radius * c->radius;
}

// 计算圆的面积
double circle_circum(sCircle_t* c)
{
    return 2 * 3.14159 * c->radius;
}

//或者在系统跑起来的时候调用一次初始化
int main(void)
{
    sCircle_t sCircle;
    sCircle.radius = 5;

    //函数指针指向的计算圆面积的函数 则实现圆面积计算
    sCircle.func = circle_area;
    printf("圆面积:%lf\n",sCircle.func(&sCircle));

    //函数指针指向的计算圆周长的函数 则实现圆周长计算
    sCircle.func = circle_circum;
    printf("圆周长:%lf\n",sCircle.func(&sCircle));
}
这是不是就有点类似于 C++ 中多态的表现形式。

三、通讯过程快速组包与解包

在通讯数据发送组包与接收解包的过程中,由于通讯 UART、SPI、I2C、CAN 等驱动底层收发数据一般是 8bit 或者 16bit 宽度(以 8bit 宽度为例),也就意味着不管你发送的是什么数据(结构体,uint32_t 类型的数组或者变量),最终到通讯驱动层都是 8bit 宽度的数组。一般的组包或者解包操作都是通过 memcpy 函数来转化的:
//定义一个需要发送的数据结构体
typedef struct
{
    uint32_t a;
    uint16_t b;
    uint8_t c[2];
}sObj_t;
//定义一个需要发送的数据对象
sObj_t sSendObj;
//定义一个通讯发送数据的8bit宽度数组
uint8_t ui8buf[100] = {0};
//常规转化操作
memcpy(ui8buf,sSendObj,sizeof(sSendObj));
进阶指针转化:

//组包时直接对地址进行赋值
*((uint32_t*)&ui8buf[0]) = sSendObj.a;
*((uint16_t*)&ui8buf[4]) = sSendObj.b;

//解包时直接解析地址对应的位宽数据
sSendObj.a = *((uint32_t*)&ui8buf[0]);
sSendObj.b = *((uint16_t*)&ui8buf[4]);
上述使用指针来转化反而还不如使用 memcpy 函数来的简单实在,说实话在嵌入式系统中我也不建议新手这样操作,这种指针操作会存在很多未知的风险,大家就当涨涨见识,请勿用于实际工程代码中。

为什么这个指针操作存在很多未知风险?则对它分析便可知:

四、注册中断、回调函数

在很多库或者操作系统甚至上位机中,都会有事先准备好一个函数,待事件响应时由系统调用这个函数来进行事件响应的机制,这个函数就是回调函数。

五、 管理申请的空间

在一些资源相对丰富的嵌入式系统中,会涉及到动态内存的分配和释放,这部分空间则是通过指针来进行管理的。
#include <stdlib.h>
//申请空间
char* str = (char*)malloc(10 * sizeof(char));
//释放空间
free(str);

六、数据结构操作等

嵌入式系统中使用各种数据结构(如链表、栈、队列等)来组织和管理数据,指针是实现这些数据结构的基础。

以下是一个简单的单链表示例代码:
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构体
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 创建新节点
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 在链表头部插入节点
void insertAtHead(Node** head, int data) {
    Node* newNode = createNode(data);
    newNode->next = *head;
    *head = newNode;
}

// 打印链表
void printList(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

int main() {
    Node* head = NULL;
    insertAtHead(&head, 3);
    insertAtHead(&head, 2);
    insertAtHead(&head, 1);
    printList(head);
    return 0;
}

相关文章