第二讲:嵌入式C语言基础回顾——指针与数组的深入理解、结构体与联合体、位运算与宏定义
好,咱们直接进入正题。这一讲的内容,说白了就是嵌入式C语言的「内功心法」。指针、结构体、位运算这些,你在任何一本C语言教材上都能看到,但到了MTK平台上,它们会以完全不同的面貌出现。我当年刚接手MTK项目时,就因为这些基础不扎实,踩了不少坑。
2.1 指针与数组:别被表象骗了
先问个问题:数组名到底是不是指针?很多人面试时被问到这个,支支吾吾。其实,数组名是地址常量,不是指针变量。它指向数组首元素,但你不能给它赋值。
核心区别:
int arr[10];—— arr是地址常量,&arr[0]等价于arrint *p = arr;—— p是指针变量,可以指向别处sizeof(arr)返回40(整个数组大小),sizeof(p)返回4或8(指针本身大小)
我在项目中遇到过一个问题:用sizeof(arr)计算数组长度,然后传给函数,结果函数里再用sizeof就变成了指针大小。嗯,这就是典型的「数组退化为指针」陷阱。
// 错误示范
void process_array(int arr[]) {
int len = sizeof(arr) / sizeof(arr[0]); // 这里arr已经退化为指针!
// len永远等于1(32位系统上)
}
// 正确做法
void process_array(int arr[], int len) {
for(int i = 0; i < len; i++) {
// 处理arr[i]
}
}
我的习惯:在MTK平台上,凡是涉及数组参数,我一定同时传递长度。别偷懒,否则调试时你会哭的。
2.2 指针数组 vs 数组指针
这两个概念,我当年也绕了好久。你想想看:
- 指针数组:
int *p[10];—— 一个数组,里面存了10个int指针 - 数组指针:
int (*p)[10];—— 一个指针,指向一个包含10个int的数组
怎么记?看优先级。中括号[]优先级高于星号*,所以int *p[10]先被解释为数组,里面元素是int *。而int (*p)[10]加了括号,*p先结合,所以p是指针。
在MTK的LCD驱动开发中,我经常用指针数组来管理多个显示缓冲区:
// 管理3个显示缓冲区
uint8_t *frame_buffers[3] = {
(uint8_t *)0x80000000,
(uint8_t *)0x80010000,
(uint8_t *)0x80020000
};
// 切换缓冲区
void switch_buffer(int index) {
LCD_SetAddress(frame_buffers[index]);
}
2.3 结构体与联合体:内存布局的艺术
结构体和联合体,在嵌入式里太重要了。说白了,结构体就是你把一堆变量打包,联合体就是让不同变量共用一块内存。
结构体对齐问题:这是新手最容易忽略的。我曾经因为结构体对齐,导致两个设备通信数据对不上,查了整整一天。
// 默认对齐(4字节对齐)
struct {
uint8_t a; // 偏移0
uint32_t b; // 偏移4(不是1!)
uint8_t c; // 偏移8
// 总大小12字节(末尾填充到4的倍数)
} __attribute__((packed)); // 加上这个,取消对齐
// 紧凑模式(1字节对齐)
struct __attribute__((packed)) {
uint8_t a; // 偏移0
uint32_t b; // 偏移1
uint8_t c; // 偏移5
// 总大小6字节
};
注意:在MTK平台上,如果结构体用于硬件寄存器映射,一定要用__attribute__((packed))。否则你的寄存器地址会错位,写进去的数据全乱套。
联合体的妙用:我经常用联合体来解析协议数据包。比如一个数据帧,可能是命令帧、数据帧或状态帧,但头部都一样:
typedef union {
uint8_t raw[64]; // 原始数据
struct {
uint8_t header;
uint8_t cmd;
uint8_t params[62];
} command_frame;
struct {
uint8_t header;
uint8_t data_len;
uint8_t data[62];
} data_frame;
struct {
uint8_t header;
uint8_t status;
uint8_t reserved[62];
} status_frame;
} Packet;
// 使用方式
Packet pkt;
pkt.raw[0] = 0xAA; // 填充头部
// 然后根据header判断类型,用对应的成员访问
2.4 位运算:嵌入式工程师的瑞士军刀
位运算在嵌入式里无处不在。设置寄存器、解析状态位、压缩数据……说白了,不会位运算,你连GPIO都控制不了。
常用操作口诀:
- 置位:
REG |= (1 << n); - 清零:
REG &= ~(1 << n); - 翻转:
REG ^= (1 << n); - 读取:
bit = (REG >> n) & 1;
我在MTK平台上写GPIO驱动时,经常要操作多个位:
// 设置GPIO模式为复用功能
#define GPIO_MODE_REG (*(volatile uint32_t *)0x10001000)
#define GPIO_MODE_SHIFT 4
#define GPIO_MODE_MASK 0x3
// 设置第2个GPIO为复用模式(模式值=2)
GPIO_MODE_REG &= ~(GPIO_MODE_MASK << (2 * GPIO_MODE_SHIFT));
GPIO_MODE_REG |= (2 << (2 * GPIO_MODE_SHIFT));
避坑指南:我曾经在操作32位寄存器时,直接用1 << 31,结果发现符号位问题导致结果不对。后来我统一用1UL << 31,加上UL后缀,确保是无符号操作。
2.5 宏定义:不只是文本替换
宏定义,很多人觉得简单,不就是#define PI 3.14嘛。但在嵌入式里,宏的用法远不止这些。
带参数的宏:要小心副作用!
// 错误示范
#define SQUARE(x) x * x
// SQUARE(a + b) 展开为 a + b * a + b,完全不对!
// 正确做法
#define SQUARE(x) ((x) * (x))
// 更安全的做法(多语句宏)
#define SET_BITS(reg, mask, val) do { \
(reg) &= ~(mask); \
(reg) |= ((val) & (mask)); \
} while(0)
条件编译:在MTK平台上,我们经常用宏来控制不同硬件版本:
#define HW_VERSION_1 1
#define HW_VERSION_2 2
// 在代码中
#if (HW_VERSION == HW_VERSION_1)
#define LED_PIN GPIO_PIN_10
#elif (HW_VERSION == HW_VERSION_2)
#define LED_PIN GPIO_PIN_12
#else
#error "Unsupported hardware version!"
#endif
我的经验:宏定义里尽量用do { ... } while(0)包裹多语句。这样不管外面怎么用,加不加分号,都不会出问题。这个习惯帮我避免了好几次编译错误。
2.6 综合实战:寄存器操作封装
最后,咱们把今天学的知识串起来。在MTK平台上,我经常这样封装寄存器操作:
// 寄存器基地址
#define UART_BASE 0x11002000
// 寄存器偏移
#define UART_THR 0x00 // 发送保持寄存器
#define UART_RBR 0x00 // 接收缓冲寄存器
#define UART_LSR 0x14 // 线状态寄存器
// 寄存器访问宏
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define UART_REG(offset) REG32(UART_BASE + offset)
// 位定义
#define LSR_DR (1 << 0) // 数据就绪位
#define LSR_THRE (1 << 5) // 发送保持寄存器空
// 结构体封装(推荐方式)
typedef struct {
volatile uint32_t THR; // 0x00
volatile uint32_t IER; // 0x04
volatile uint32_t FCR; // 0x08
volatile uint32_t LCR; // 0x0C
volatile uint32_t MCR; // 0x10
volatile uint32_t LSR; // 0x14
} UART_TypeDef;
#define UART0 ((UART_TypeDef *)UART_BASE)
// 使用
void uart_send_byte(uint8_t data) {
// 等待发送缓冲区空
while(!(UART0->LSR & LSR_THRE));
UART0->THR = data;
}
嗯,这一讲的内容就到这里。指针、结构体、位运算、宏定义,这些基础东西看似简单,但真正用好它们,需要大量的实践。我建议你拿到MTK开发板后,第一件事就是把这些知识点都跑一遍,看看实际效果。下一讲,咱们聊聊MTK平台的内存管理,那才是真正考验功底的地方。