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

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

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

一、 访问硬件寄存器

嵌入式系统中,硬件寄存器通常被映射到特定的内存地址。通过指针可以直接访问这些寄存器。如下所示为某 32 芯片定义的寄存器:
  1. /* GPIOx(x=A,B,C,D,E,F,G) definitions */
  2. #define GPIOA (GPIO_BASE + 0x00000000U)
  3. #define GPIOB (GPIO_BASE + 0x00000400U)
  4. #define GPIOC (GPIO_BASE + 0x00000800U)
  5. #define GPIOD (GPIO_BASE + 0x00000C00U)
  6. #define GPIOE (GPIO_BASE + 0x00001000U)
  7. #define GPIOF (GPIO_BASE + 0x00001400U)
  8. #define GPIOG (GPIO_BASE + 0x00001800U)
  9.  
  10. /* AFIO definitions */
  11. #define AFIO AFIO_BASE
  12.  
  13. /* registers definitions */
  14. /* GPIO registers definitions */
  15. #define GPIO_CTL0(gpiox) REG32((gpiox) + 0x00U) /*!< GPIO port control register 0 */
  16. #define GPIO_CTL1(gpiox) REG32((gpiox) + 0x04U) /*!< GPIO port control register 1 */
  17. #define GPIO_ISTAT(gpiox) REG32((gpiox) + 0x08U) /*!< GPIO port input status register */
  18. #define GPIO_OCTL(gpiox) REG32((gpiox) + 0x0CU) /*!< GPIO port output control register */
  19. #define GPIO_BOP(gpiox) REG32((gpiox) + 0x10U) /*!< GPIO port bit operation register */
  20. #define GPIO_BC(gpiox) REG32((gpiox) + 0x14U) /*!< GPIO bit clear register */
  21. #define GPIO_LOCK(gpiox) REG32((gpiox) + 0x18U) /*!< GPIO port configuration lock register */
  22. #define GPIOx_SPD(gpiox) REG32((gpiox) + 0x3CU) /*!< GPIO port bit speed register */
如需要访问每个 GPIO 对应的 GPIO_CTL0 寄存器,只需要调用宏 GPIO_CTL0 并填入 GPIOx 定义的地址即可:
  • GPIO_CTL0(GPIOA) = 0xE800CF57;
如此便封装了硬件寄存器的地址,对于用户而言,读写见名知意的寄存器名,比直接与硬件寄存器地址打交道更为直观和方便,那么上述对硬件地址的封装究竟是如何实现的呢?这需要找到宏 REG32 的定义:
  1. #define REG32(addr) (*(volatile uint32_t *)(uint32_t)(addr))
  2. #define REG16(addr) (*(volatile uint16_t *)(uint32_t)(addr))
  3. #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语言中要定义一个类则是通过定义结构体来模拟实现的,结构体中的变量则是模拟的类中的成员变量,结构体中的函数指针则是模拟类中的成员函数,如下所示:
  1. typedef struct
  2. {
  3. int point_x; //圆心坐标x
  4. int point_y; //圆心坐标y
  5. double radius; // 圆的半径
  6. double (*func)(sCircle_t*); // 函数指针,用于对圆进行相关的计算操作
  7. }sCircle_t
上述使用结构体模拟了类定义了一个圆类,类中定义了成员变量和成员函数,则使用此结构体可以实例化一个对象。
  • sCircle_t sCircle;
如此便模拟了 C++ 中的创建对象的操作,但是还是和 C++ 中实例化对象有很大的区别,结构体中没有构造函数,因此在定义了变量之后,需要手动初始化,为结构体中的变量和函数指针赋初值。
  1. // 计算圆的面积
  2. double circle_area(sCircle_t* c)
  3. {
  4. return 3.14159 * c->radius * c->radius;
  5. }
  6. // 创建对象时初始化
  7. sCircle_t sCircle =
  8. {
  9. .point_x = 0,
  10. .point_y = 0,
  11. .radius = 5.5,
  12. .func = circle_area,
  13. };
  14. //或者在系统跑起来的时候调用一次初始化
  15. void Circle_init(sCircle_t* c)
  16. {
  17. c->point_x = 0,
  18. c->point_y = 0,
  19. c->radius = 5.5,
  20. c->func = circle_area,
  21. }
在上述的结构体中定义了函数指针double (func)(sCircle_t); ,但是并没有指明这个函数指针指向的函数的功能作用是什么,那么就意味着,只要在使用之前改变了函数指针指向的函数,那这个函数指针就可以实现不同的函数功能:
  1. // 计算圆的面积
  2. double circle_area(sCircle_t* c)
  3. {
  4. return 3.14159 * c->radius * c->radius;
  5. }
  6.  
  7. // 计算圆的面积
  8. double circle_circum(sCircle_t* c)
  9. {
  10. return 2 * 3.14159 * c->radius;
  11. }
  12.  
  13. //或者在系统跑起来的时候调用一次初始化
  14. int main(void)
  15. {
  16. sCircle_t sCircle;
  17. sCircle.radius = 5;
  18.  
  19. //函数指针指向的计算圆面积的函数 则实现圆面积计算
  20. sCircle.func = circle_area;
  21. printf("圆面积:%lf\n",sCircle.func(&sCircle));
  22.  
  23. //函数指针指向的计算圆周长的函数 则实现圆周长计算
  24. sCircle.func = circle_circum;
  25. printf("圆周长:%lf\n",sCircle.func(&sCircle));
  26. }
这是不是就有点类似于 C++ 中多态的表现形式。

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

在通讯数据发送组包与接收解包的过程中,由于通讯 UART、SPI、I2C、CAN 等驱动底层收发数据一般是 8bit 或者 16bit 宽度(以 8bit 宽度为例),也就意味着不管你发送的是什么数据(结构体,uint32_t 类型的数组或者变量),最终到通讯驱动层都是 8bit 宽度的数组。一般的组包或者解包操作都是通过 memcpy 函数来转化的:
  1. //定义一个需要发送的数据结构
  2. typedef struct
  3. {
  4. uint32_t a;
  5. uint16_t b;
  6. uint8_t c[2];
  7. }sObj_t;
  8. //定义一个需要发送的数据对象
  9. sObj_t sSendObj;
  10. //定义一个通讯发送数据的8bit宽度数组
  11. uint8_t ui8buf[100] = {0};
  12. //常规转化操作
  13. memcpy(ui8buf,sSendObj,sizeof(sSendObj));
  14. 进阶指针转化:
  15.  
  16. //组包时直接对地址进行赋值
  17. *((uint32_t*)&ui8buf[0]) = sSendObj.a;
  18. *((uint16_t*)&ui8buf[4]) = sSendObj.b;
  19.  
  20. //解包时直接解析地址对应的位宽数据
  21. sSendObj.a = *((uint32_t*)&ui8buf[0]);
  22. sSendObj.b = *((uint16_t*)&ui8buf[4]);
上述使用指针来转化反而还不如使用 memcpy 函数来的简单实在,说实话在嵌入式系统中我也不建议新手这样操作,这种指针操作会存在很多未知的风险,大家就当涨涨见识,请勿用于实际工程代码中。

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

四、注册中断、回调函数

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

五、 管理申请的空间

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

六、数据结构操作等

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

以下是一个简单的单链表示例代码:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. // 定义链表节点结构体
  5. typedef struct Node {
  6. int data;
  7. struct Node* next;
  8. } Node;
  9.  
  10. // 创建新节点
  11. Node* createNode(int data) {
  12. Node* newNode = (Node*)malloc(sizeof(Node));
  13. newNode->data = data;
  14. newNode->next = NULL;
  15. return newNode;
  16. }
  17.  
  18. // 在链表头部插入节点
  19. void insertAtHead(Node** head, int data) {
  20. Node* newNode = createNode(data);
  21. newNode->next = *head;
  22. *head = newNode;
  23. }
  24.  
  25. // 打印链表
  26. void printList(Node* head) {
  27. Node* current = head;
  28. while (current != NULL) {
  29. printf("%d ", current->data);
  30. current = current->next;
  31. }
  32. printf("\n");
  33. }
  34.  
  35. int main() {
  36. Node* head = NULL;
  37. insertAtHead(&head, 3);
  38. insertAtHead(&head, 2);
  39. insertAtHead(&head, 1);
  40. printList(head);
  41. return 0;
  42. }

相关文章