跳转至

快速的C语言复习

完整的仓库地址在Tutorial_AwesomeModernCPP中,您也可以光顾一下,喜欢的话给一个Star激励一下作者

尽管,笔者要说明的是C++在今天已经不可以使用简单的C超集来描述C++了,但是因为设计之初,C++就是要求尽力兼容C语言的。所以这里,我们默认大家的C语言水平都是可以合格的写出一个可以运行的基于某款或者是若干嵌入式联合系统的业务体系代码的。因此,这里只是快速的,出于完整的补充下C语言中常识的部分。

1. 基本数据类型与类型修饰符

值得一说的是,C语言本身就是一个强类型的程序设计语言。澄清一个变量是什么,是从C诞生以来就必须要做的标准动作。

我知道有人会说auto的事情,auto的确非常适合节约时间书写复杂的类型,但是笔者的态度是不要滥用。

C语言的类型系统是整个语言的基础,在嵌入式开发中,准确理解数据类型的大小和表示范围尤为重要,因为硬件资源往往是受限的。在我们编写C++的时候也是要注意这个事情。

1.1 整型家族

C语言提供了丰富的整型类型,每种类型都有其特定的用途和表示范围。需要注意的是,除了char类型在某些平台上固定为8位外,其他整型的实际大小是由具体实现决定的。

char c = 'A';              // 至少8位,通常用于字符
short s = 100;             // 至少16位
int i = 1000;              // 至少16位,通常为32位
long l = 100000L;          // 至少32位
long long ll = 100000LL;   // 至少64位(C99标准引入)

在嵌入式系统中,我们经常需要精确控制数据类型的大小。C99标准引入的stdint.h头文件提供了固定宽度的整型,这在编写可移植的嵌入式代码时极为重要,特别是一些基础库,你的东西可能会在32位到64位都会被使用(笔者注意到现在已经开始慢慢出现用于嵌入式平台的64位芯片了,所以还真要关心下)

#include <stdint.h>

int8_t   i8 = -128;        // 精确8位有符号整数
uint8_t  u8 = 255;         // 精确8位无符号整数
int16_t  i16 = -32768;     // 精确16位有符号整数
uint16_t u16 = 65535;      // 精确16位无符号整数
int32_t  i32 = -2147483648;// 精确32位有符号整数
uint32_t u32 = 4294967295U;// 精确32位无符号整数

那问题来了,什么时候用多大的呢?嗯,这个事情可以不必要如此的死板,不过有一个事情必须注意——你的数据范围得够用。那问题来了:N 位的数据到底能存多大?对于 无符号整数,N 位一共可以表示 2ⁿ 个数,取值范围是 0 ~ 2ⁿ − 1。那如果是 有符号整数 呢?最高位要拿来当符号位了,采用补码表示的话,范围就是 −2ⁿ⁻¹ ~ 2ⁿ⁻¹ − 1。大家都是嵌入式程序员,这点二进制应该都能算得过来。

1.2 浮点类型

浮点类型用于表示实数,但在嵌入式系统中使用浮点运算需要格外谨慎,因为许多微控制器不支持硬件浮点运算,软件模拟会带来显著的性能开销。

float f = 3.14f;           // 单精度,通常32位,精度约7位十进制数
double d = 3.14159265359;  // 双精度,通常64位,精度约15位十进制数
long double ld = 3.14L;    // 扩展精度,至少与double相同

在一些资源极端受限的嵌入式系统中,如果必须使用浮点运算,优先选择float而非double,因为它占用更少的内存和运算资源,double有时候太吃操作了。

1.3 类型修饰符

类型修饰符可以改变基本类型的属性,在嵌入式编程中有着特殊的重要性。

signed 和 unsigned

unsigned修饰符将整型变量的表示范围扩展到仅非负数,这在处理硬件寄存器值和位掩码时非常有用:

unsigned int counter = 0;       // 范围:0 到 4294967295(32位系统)
signed int temperature = -40;   // 范围:-2147483648 到 2147483647

const 修饰符

const关键字声明变量为只读,这在嵌入式开发中有多重作用。首先,它可以帮助编译器进行优化,将常量数据放置在ROM或Flash中而非RAM中,节省宝贵的RAM资源。其次,它提供了编译时的安全检查,防止意外修改不应改变的数据,这有时候很重要,实际上就是强调当前逻辑下这个是不变量(当然C++还提供更为牛逼的constexpr,这个等到C++的时候我们继续聊)

const int MAX_BUFFER_SIZE = 256;           // 常量整数
const uint8_t lookup_table[] = {0, 1, 4, 9, 16, 25};  // 常量数组,可存放在Flash中

在函数参数中使用const可以明确表明函数不会修改传入的数据,这在设计API时是良好的实践:

void process_data(const uint8_t* data, size_t length) {
    // 函数承诺不修改data指向的内容
}

volatile 修饰符

volatile 的字面含义是“易变的”,它是嵌入式 C 编程中极其重要、但也最容易被误解的一个关键字。它的核心作用,并不是“禁止编译器优化”,而是明确地告诉编译器:这个变量的值,可能会在当前程序控制流之外发生变化。在嵌入式系统中,这种“控制流之外”的变化通常来自硬件外设、中断服务程序(ISR)、DMA,或其他并发执行上下文。

正因为如此,编译器在面对被 volatile 修饰的对象时,不能假设该变量在两次访问之间保持不变。对于 volatile 变量的每一次读和写,在抽象机模型中都属于可观察行为,必须真实地发生在内存中,而不能被缓存到寄存器、合并或直接消除。这并不意味着编译器“完全不能优化”,而是它不能对 volatile 对象做出“值稳定”的假设,其它与之无关的代码仍然可以被正常优化。

在嵌入式编程中,volatile 最常见的使用场景,是在中断与主循环之间传递状态信息。例如,一个在中断回调中被置位、在主循环中被轮询的事件标志位,就必须声明为 volatile。否则,在较高优化级别下,编译器可能会认为该变量在主循环中从未被修改,从而将读取操作提前、缓存,甚至直接优化掉,导致程序行为与预期严重不符。

再从另一个角度看,如果一个普通变量在同一执行路径中被连续写入不同的值,但中间没有任何可观察行为依赖它,那么在没有 volatile 的前提下,编译器完全有理由认为这些写操作是“多余的”,并将其消除。而一旦该变量被声明为 volatile,这些写操作就都变成了不可被消除的内存访问,必须严格按顺序发生。

需要特别强调的是,volatile 只解决编译器层面的可见性问题,它并不保证原子性,也不提供任何线程同步或内存顺序语义。对 volatile 变量的复合操作(例如自增)在中断或多线程环境中依然可能产生竞争条件。如果程序需要的是原子性或同步保证,就必须借助关中断、锁、原子指令或专门的并发原语来实现。这就是为啥任何操作系统都要封装和提供锁的原语

volatile uint32_t* const GPIO_IDR = (volatile uint32_t*)0x40020010;  // GPIO输入数据寄存器
volatile uint8_t uart_rx_flag = 0;  // 在中断中被修改的标志

void UART_IRQHandler(void) {
    uart_rx_flag = 1;  // 中断中修改
}

int main(void) {
    while (uart_rx_flag == 0) {
        // 如果没有volatile,编译器可能优化掉这个循环
    }
}

另外,在访问硬件寄存器时,通常需要同时使用volatileconst,这个事情我相信读过SDK的朋友都知道的。

#define RCC_BASE    0x40023800
#define RCC_AHB1ENR (*(volatile uint32_t*)(RCC_BASE + 0x30))  // 可读可写的寄存器

2. 运算符与表达式

2.1 算术运算符

C语言提供了标准的算术运算符,但在嵌入式系统中使用时需要注意溢出和类型提升的问题:

int a = 10, b = 3;
int sum = a + b;        // 加法:13
int diff = a - b;       // 减法:7
int product = a * b;    // 乘法:30
int quotient = a / b;   // 整数除法:3(截断)
int remainder = a % b;  // 取模:1

在嵌入式开发中,除法和取模运算通常开销较大,特别是在没有硬件除法器的MCU上。在性能关键的代码中,应尽量避免除法运算,或者用位移操作替代2的幂次的除法:

uint32_t value = 1024;
uint32_t div_by_2 = value >> 1;   // 相当于 value / 2,但更快
uint32_t div_by_8 = value >> 3;   // 相当于 value / 8

2.2 位运算符

位运算符是嵌入式编程的核心工具,它们直接操作数据的二进制位,常用于硬件寄存器配置、标志位管理和高效的数学运算。

uint8_t a = 0b10110011;  // 二进制字面量(C23标准,部分编译器支持)
uint8_t b = 0b11001010;

// 按位与:两位都为1时结果为1
uint8_t and_result = a & b;  // 0b10000010

// 按位或:任一位为1时结果为1
uint8_t or_result = a | b;   // 0b11111011

// 按位异或:两位不同时结果为1
uint8_t xor_result = a ^ b;  // 0b01111001

// 按位取反:0变1,1变0
uint8_t not_result = ~a;     // 0b01001100

// 左移:向左移动位,右侧补0
uint8_t left_shift = a << 2; // 0b11001100

// 右移:向右移动位
uint8_t right_shift = a >> 2;// 0b00101100(逻辑右移,无符号数)

位运算在嵌入式开发中的典型应用包括:

寄存器位操作

// 设置某一位
#define SET_BIT(reg, bit)    ((reg) |= (1 << (bit)))

// 清除某一位
#define CLEAR_BIT(reg, bit)  ((reg) &= ~(1 << (bit)))

// 切换某一位
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1 << (bit)))

// 读取某一位
#define READ_BIT(reg, bit)   (((reg) >> (bit)) & 1)

// 示例:配置GPIO
SET_BIT(GPIOA->MODER, 10);    // 设置PA5的模式位
CLEAR_BIT(GPIOA->ODR, 5);     // 清除PA5的输出

位域掩码

#define STATUS_READY    0x01  // 0b00000001
#define STATUS_BUSY     0x02  // 0b00000010
#define STATUS_ERROR    0x04  // 0b00000100
#define STATUS_TIMEOUT  0x08  // 0b00001000

uint8_t status = 0;
status |= STATUS_READY;              // 设置就绪标志
if (status & STATUS_ERROR) {         // 检查错误标志
    // 处理错误
}
status &= ~STATUS_BUSY;              // 清除忙碌标志

2.3 关系与逻辑运算符

关系运算符用于比较,返回整数结果(0表示假,非0表示真):

int a = 5, b = 10;
int equal = (a == b);        // 等于:0
int not_equal = (a != b);    // 不等于:1
int less = (a < b);          // 小于:1
int greater = (a > b);       // 大于:0
int less_equal = (a <= b);   // 小于等于:1
int greater_equal = (a >= b);// 大于等于:0

逻辑运算符具有短路特性,这在嵌入式编程中可以用于条件优化:

// 逻辑与:左侧为假时不评估右侧
if (ptr != NULL && *ptr == 0) {  // 安全检查,防止空指针解引用
    // 处理
}

// 逻辑或:左侧为真时不评估右侧
if (error_flag || check_critical_condition()) {
    // 当error_flag为真时,不会调用函数
}

// 逻辑非
if (!is_ready) {
    // 等待就绪
}

2.4 其他重要运算符

三元条件运算符是C语言中唯一的三元运算符,可以简化简单的if-else语句:

int max = (a > b) ? a : b;  // 等价于 if (a > b) max = a; else max = b;

// 在嵌入式中的应用
uint8_t clamp(uint8_t value, uint8_t min, uint8_t max) {
    return (value < min) ? min : ((value > max) ? max : value);
}

sizeof运算符返回类型或对象的字节大小,在编译时求值,常用于数组大小计算:

uint32_t array[10];
size_t array_size = sizeof(array);           // 40字节(假设uint32_t为4字节)
size_t element_count = sizeof(array) / sizeof(array[0]);  // 10个元素

// 在嵌入式中用于缓冲区管理
uint8_t buffer[256];
void clear_buffer(void) {
    memset(buffer, 0, sizeof(buffer));
}

逗号运算符从左到右计算表达式,返回最右边表达式的值:

int x = (a = 5, b = a + 10, b * 2);  // x = 30

// 在for循环中常见
for (int i = 0, j = 10; i < j; i++, j--) {
    // 同时更新两个变量
}

3. 控制流语句

3.1 条件语句

if-else语句是最基本的条件分支:

if (temperature > TEMP_HIGH_THRESHOLD) {
    activate_cooling();
} else if (temperature < TEMP_LOW_THRESHOLD) {
    activate_heating();
} else {
    maintain_temperature();
}

在嵌入式系统中,对于多个互斥条件,使用else-if链可以避免不必要的条件检查,提高执行效率。

switch语句适用于多路分支,编译器通常会将其优化为跳转表,在某些情况下比多个if-else更高效:

switch (command) {
    case CMD_START:
        start_operation();
        break;

    case CMD_STOP:
        stop_operation();
        break;

    case CMD_PAUSE:
        pause_operation();
        break;

    case CMD_RESUME:
        resume_operation();
        break;

    default:
        handle_unknown_command();
        break;
}

在嵌入式开发中,switch语句常用于状态机实现:

typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_PAUSED,
    STATE_ERROR
} SystemState;

SystemState current_state = STATE_IDLE;

void state_machine_update(void) {
    switch (current_state) {
        case STATE_IDLE:
            if (start_button_pressed()) {
                current_state = STATE_RUNNING;
                initialize_operation();
            }
            break;

        case STATE_RUNNING:
            perform_operation();
            if (error_detected()) {
                current_state = STATE_ERROR;
            } else if (pause_button_pressed()) {
                current_state = STATE_PAUSED;
            }
            break;

        case STATE_PAUSED:
            if (resume_button_pressed()) {
                current_state = STATE_RUNNING;
            }
            break;

        case STATE_ERROR:
            handle_error();
            if (reset_button_pressed()) {
                current_state = STATE_IDLE;
            }
            break;
    }
}

3.2 循环语句

for循环通常用于已知迭代次数的情况:

// 传统for循环
for (int i = 0; i < 10; i++) {
    array[i] = i * i;
}

// 在嵌入式中常见的循环模式
for (size_t i = 0; i < ARRAY_SIZE; i++) {
    process_element(array[i]);
}

// 无限循环(在嵌入式主循环中常见)
for (;;) {
    // 永远执行
    process_tasks();
}

while循环在条件未知或依赖于循环体内计算时使用:

while (uart_data_available()) {
    uint8_t data = uart_read();
    process_data(data);
}

// 嵌入式中的典型等待循环
while (!is_ready()) {
    // 等待就绪
}

do-while循环至少执行一次循环体,适用于某些初始化场景:

uint8_t retry_count = 0;
do {
    result = attempt_communication();
    retry_count++;
} while (result != SUCCESS && retry_count < MAX_RETRIES);

在嵌入式系统中,无限循环是主程序的标准结构:

int main(void) {
    system_init();
    peripherals_init();

    while (1) {  // 或 for(;;)
        // 主循环
        read_sensors();
        process_data();
        update_outputs();
        handle_communication();
    }
}

3.3 跳转语句

break语句用于提前退出循环或switch语句:

for (int i = 0; i < MAX_ITEMS; i++) {
    if (items[i] == target) {
        found_index = i;
        break;  // 找到目标,退出循环
    }
}

continue语句跳过当前迭代的剩余部分,继续下一次迭代:

for (int i = 0; i < data_count; i++) {
    if (data[i] == INVALID_VALUE) {
        continue;  // 跳过无效数据
    }
    process_valid_data(data[i]);
}

goto语句虽然常被批评,但在嵌入式C中,它在错误处理和资源清理场景中有合理的使用场景:

int initialize_system(void) {
    if (!init_hardware()) {
        goto error_hardware;
    }

    if (!init_peripherals()) {
        goto error_peripherals;
    }

    if (!init_communication()) {
        goto error_communication;
    }

    return SUCCESS;

error_communication:
    cleanup_peripherals();
error_peripherals:
    cleanup_hardware();
error_hardware:
    return ERROR;
}

4. 函数

函数,笔者记得另一种叫法是子程序,一个函数就是完成一段逻辑,给人看的代码。从这个角度上,C语言模块化编程的基础就是函数。

我真见过一些朋友认为函数跳转是浪费时间的,所以不该写函数——对,但是后面不对,因为显然不知道现代编译器会优化不必要的函数跳转而直接内联(即直接向调用点安插片段,节约压栈弹栈,以及触发刷新流水线所消耗的时间),此外,你真的需要到这个地步以至于需要在乎函数跳转的时间嘛?

4.1 函数定义与声明

// 函数声明(原型)
int calculate_checksum(const uint8_t* data, size_t length);

// 函数定义
int calculate_checksum(const uint8_t* data, size_t length) {
    int checksum = 0;
    for (size_t i = 0; i < length; i++) {
        checksum += data[i];
    }
    return checksum & 0xFF;
}

4.2 函数参数传递

C语言使用值传递,但可以通过指针实现引用传递的效果:

// 值传递:修改不影响原变量
void swap_wrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

// 指针传递:可以修改原变量
void swap_correct(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 使用
int x = 10, y = 20;
swap_correct(&x, &y);  // x和y被交换

在嵌入式开发中,传递大型结构体时应使用指针以避免昂贵的拷贝:

typedef struct {
    uint32_t timestamp;
    float temperature;
    float humidity;
    uint16_t pressure;
} SensorData;

// 低效:传递整个结构体
void process_data_inefficient(SensorData data) {
    // 处理数据
}

// 高效:传递指针
void process_data_efficient(const SensorData* data) {
    // 处理数据,使用data->temperature访问成员
}

4.3 内联函数

现代的inline不再是内联函数的意思了——这一点各位再写C++的时候必须注意,他指代的是允许重复定义。因为他在一定程度上消灭了一个独立的符号编码从而回避了冲突——C编译器在现在也会主动的优化了。所以,这个关键字如果您发现您的编译器真吃这个,那就写,否则不用写。

// C99标准的内联函数
static inline uint16_t swap_bytes(uint16_t value) {
    return (value >> 8) | (value << 8);
}

// 宏定义方式(传统方法,但类型不安全)
#define SWAP_BYTES(x) (((x) >> 8) | ((x) << 8))

4.4 函数指针与回调

函数指针是实现回调的一个基本构件,回调就是回头调用——就是这个意思。我们保存住函数的地址,然后再需要的时候,回头调用,相当于我们将处理流存储了!

// 定义函数指针类型
typedef void (*EventCallback)(void* context);

// 回调注册系统
typedef struct {
    EventCallback callback;
    void* context;
} EventHandler;

EventHandler button_handler;

void register_button_callback(EventCallback callback, void* context) {
    button_handler.callback = callback;
    button_handler.context = context;
}

// 在中断或主循环中调用
void handle_button_event(void) {
    if (button_handler.callback != NULL) {
        button_handler.callback(button_handler.context);
    }
}

函数指针还可用于实现简单的多态,笔者记得有一本不错的嵌入式C语言教程编写的基于C的多态的例子是不错的,可惜忘记书名了(汗

typedef int (*MathOperation)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

int perform_operation(MathOperation op, int x, int y) {
    return op(x, y);
}

// 使用
int result = perform_operation(add, 10, 5);  // 15

5. 指针

指针是C语言最强大也最容易出错的特性,在嵌入式编程中尤为重要。这里因为是快速的复习,只是带大家闪过以下C的指针。

5.1 指针基础

int value = 42;
int* ptr = &value;       // ptr存储value的地址
int deref = *ptr;        // 解引用,deref = 42
*ptr = 100;              // 通过指针修改value

// 空指针
int* null_ptr = NULL;    // 应始终初始化指针

// 指针算术
int array[5] = {1, 2, 3, 4, 5};
int* p = array;
p++;                     // 指向array[1]
int val = *(p + 2);      // 访问array[3],val = 4

5.2 指针与数组

数组名在大多数情况下会退化为指向首元素的指针,欸,这可是要注意的是——数组不是指针!!!

int numbers[10];
int* ptr = numbers;      // 等价于 &numbers[0]

// 数组访问的两种方式
numbers[3] = 42;         // 下标方式
*(ptr + 3) = 42;         // 指针方式,等价

// 指针遍历数组
for (int* p = numbers; p < numbers + 10; p++) {
    *p = 0;
}

5.3 多级指针

这个玩意让我想起来一个梗图了——一个人指着一个人指着一个人.jpg,对,就这个意思。一个指向了指向了指向了指向了一个变量的指针变量的指针变量的指针变量。嗯,头都绕晕了,笔者建议是非必须,别玩这出,你这是给你的同事埋大的。

int value = 42;
int* ptr = &value;
int** ptr_ptr = &ptr;    // 指向指针的指针

// 解引用
int val1 = *ptr;         // 42
int val2 = **ptr_ptr;    // 42

多级指针在动态分配二维数组时很有用,但在嵌入式系统中应谨慎使用动态内存分配。

5.4 指针与const

const和指针的组合有多种含义:

int value = 42;

// 指向常量的指针:不能通过ptr修改value
const int* ptr1 = &value;
// *ptr1 = 100;  // 错误
ptr1 = &other;   // 可以,指针本身可以改变

// 常量指针:指针本身不能改变
int* const ptr2 = &value;
*ptr2 = 100;     // 可以,可以修改指向的值
// ptr2 = &other;  // 错误,指针不能改变

// 指向常量的常量指针:都不能改变
const int* const ptr3 = &value;
// *ptr3 = 100;    // 错误
// ptr3 = &other;  // 错误

6. 数组与字符串

6.1 数组

数组是相同类型元素的连续集合:

// 一维数组
int numbers[10];                     // 声明
int primes[] = {2, 3, 5, 7, 11};    // 初始化,大小自动推导为5
int matrix[3][4];                    // 二维数组

// 数组初始化
int zeros[100] = {0};                // 全部初始化为0
int partial[10] = {1, 2};           // 前两个元素为1和2,其余为0

// 指定初始化器(C99)
int sparse[100] = {[5] = 10, [20] = 30};

在嵌入式系统中,数组常用于缓冲区和查找表:

// 串口接收缓冲区
uint8_t uart_rx_buffer[256];
volatile size_t rx_head = 0;
volatile size_t rx_tail = 0;

// 查找表(节省计算资源)
const uint8_t sin_table[360] = {
    // 预计算的正弦值(0-255范围)
    128, 130, 133, 135, // ...
};

6.2 字符串

C语言中的字符串是以空字符'\0'结尾的字符数组:

char str1[10] = "Hello";             // 字符串字面量初始化
char str2[] = "World";               // 大小自动推导为6(包括'\0')
char str3[10];                       // 未初始化

// 字符串操作(需要包含string.h)
#include <string.h>

strcpy(str3, str1);                  // 复制字符串
strcat(str3, str2);                  // 连接字符串
int len = strlen(str1);              // 获取长度
int cmp = strcmp(str1, str2);        // 比较字符串

在嵌入式系统中,应优先使用带长度限制的安全函数版本:

char buffer[32];
strncpy(buffer, source, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';   // 确保以空字符结尾

// 更安全的做法
snprintf(buffer, sizeof(buffer), "Value: %d", value);

字符串处理的注意事项:

  • 确保目标缓冲区足够大
  • 始终确保字符串以'\0'结尾
  • 在资源受限的系统中,考虑使用固定大小的缓冲区避免动态分配

7. 结构体、联合体与枚举

7.1 结构体

结构体允许将不同类型的数据组合成一个单元:

// 定义结构体
struct Point {
    int x;
    int y;
};

// 使用typedef简化
typedef struct {
    int x;
    int y;
} Point;

// 创建和初始化
Point p1 = {10, 20};                 // 顺序初始化
Point p2 = {.y = 30, .x = 40};      // 指定初始化器(C99)

// 访问成员
p1.x = 100;
int y_value = p1.y;

// 指针访问
Point* ptr = &p1;
ptr->x = 200;                        // 等价于 (*ptr).x = 200

在嵌入式开发中,结构体广泛用于表示配置、状态和数据包:

// 传感器数据结构
typedef struct {
    uint32_t timestamp;
    float temperature;
    float humidity;
    uint16_t light_level;
    uint8_t status;
} SensorReading;

// 通信协议数据包
typedef struct {
    uint8_t header;
    uint8_t command;
    uint16_t length;
    uint8_t data[256];
    uint16_t checksum;
} __attribute__((packed)) ProtocolPacket;  // 禁用对齐填充

7.2 位域

位域允许在结构体中以位为单位分配存储,这在处理硬件寄存器时极为有用:

// 寄存器位域定义
typedef struct {
    uint32_t EN      : 1;   // 使能位
    uint32_t MODE    : 2;   // 模式选择(2位)
    uint32_t RESERVED: 5;   // 保留位
    uint32_t PRIORITY: 3;   // 优先级(3位)
    uint32_t         : 21;  // 未命名位域,填充
} ControlRegister;

// 使用
volatile ControlRegister* ctrl_reg = (ControlRegister*)0x40000000;
ctrl_reg->EN = 1;
ctrl_reg->MODE = 2;
ctrl_reg->PRIORITY = 7;

注意:位域的实现依赖于编译器和平台,在需要精确控制时应谨慎使用。

7.3 联合体

联合体的所有成员共享同一块内存,用于节省空间或类型双关:

// 基本联合体
union Data {
    int i;
    float f;
    char bytes[4];
};

union Data d;
d.i = 0x12345678;
printf("%02X", d.bytes[0]);  // 访问字节表示

在嵌入式编程中,联合体常用于数据类型转换和协议处理:

// 多类型数据容器
typedef union {
    uint32_t word;
    uint16_t halfword[2];
    uint8_t byte[4];
} DataConverter;

DataConverter dc;
dc.word = 0x12345678;
// 现在可以按字节访问:dc.byte[0], dc.byte[1], ...

// 结构体与联合体结合
typedef struct {
    uint8_t type;
    union {
        int int_value;
        float float_value;
        char string_value[16];
    } data;
} Variant;

7.4 枚举

枚举定义命名的整数常量集合,提高代码可读性:

// 基本枚举
enum Color {
    RED,      // 0
    GREEN,    // 1
    BLUE      // 2
};

// 指定值
enum Status {
    STATUS_OK = 0,
    STATUS_ERROR = -1,
    STATUS_BUSY = 1,
    STATUS_TIMEOUT = 2
};

// 使用typedef
typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_PAUSED,
    STATE_ERROR
} SystemState;

枚举在嵌入式开发中常用于定义状态、命令码和配置选项:

// 命令定义
typedef enum {
    CMD_NOOP = 0x00,
    CMD_READ = 0x01,
    CMD_WRITE = 0x02,
    CMD_ERASE = 0x03,
    CMD_RESET = 0xFF
} Command;

// 错误码
typedef enum {
    ERR_NONE = 0,
    ERR_INVALID_PARAM = 1,
    ERR_TIMEOUT = 2,
    ERR_HARDWARE_FAULT = 3,
    ERR_OUT_OF_MEMORY = 4
} ErrorCode;

8. 预处理器

预处理器在编译之前处理源代码,它是C语言灵活性的重要来源,在嵌入式开发中尤为重要。

8.1 宏定义

// 对象宏
#define MAX_SIZE 100
#define PI 3.14159f
#define LED_PIN 13

// 函数宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))

// 多行宏
#define SWAP(a, b, type) do { \
    type temp = (a);          \
    (a) = (b);                \
    (b) = temp;               \
} while(0)

宏的注意事项:

  • 参数应该加括号以避免优先级问题
  • 多行宏使用do-while(0)包装
  • 宏不进行类型检查,使用时要小心

在嵌入式开发中的典型应用:

// 寄存器位操作宏
#define BIT(n) (1UL << (n))
#define SET_BIT(reg, bit) ((reg) |= BIT(bit))
#define CLEAR_BIT(reg, bit) ((reg) &= ~BIT(bit))
#define READ_BIT(reg, bit) (((reg) >> (bit)) & 1UL)
#define TOGGLE_BIT(reg, bit) ((reg) ^= BIT(bit))

// 数组大小
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

// 范围检查
#define IN_RANGE(x, min, max) (((x) >= (min)) && ((x) <= (max)))

// 字节对齐
#define ALIGN_UP(x, align) (((x) + (align) - 1) & ~((align) - 1))

8.2 条件编译

条件编译允许根据条件选择性地包含或排除代码,这个东西是跨平台实现的一个基本利器。

// 基本条件编译
#ifdef DEBUG
    #define DEBUG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
    #define DEBUG_PRINT(fmt, ...) ((void)0)
#endif

// 使用
DEBUG_PRINT("Value: %d\n", value);  // 仅在DEBUG定义时输出

// 平台相关代码
#if defined(STM32F4) || defined(STM32F7)
    #define MCU_FAMILY_STM32F4_F7
    #include "stm32f4xx.h"
#elif defined(STM32L4)
    #define MCU_FAMILY_STM32L4
    #include "stm32l4xx.h"
#else
    #error "Unsupported MCU family"
#endif

// 功能开关
#define FEATURE_USB 1
#define FEATURE_ETHERNET 0

#if FEATURE_USB
    void usb_init(void);
#endif

#if FEATURE_ETHERNET
    void ethernet_init(void);
#endif

8.3 文件包含

// 系统头文件
#include <stdio.h>
#include <stdint.h>

// 用户头文件
#include "config.h"
#include "hal.h"

// 防止重复包含(头文件保护)
#ifndef CONFIG_H
#define CONFIG_H

// 头文件内容

#endif // CONFIG_H

// 或使用#pragma once(非标准但广泛支持)
#pragma once

8.4 预定义宏

编译器提供了一些有用的预定义宏:

// 文件和行号
#define LOG_ERROR(msg) \
    fprintf(stderr, "Error in %s:%d - %s\n", __FILE__, __LINE__, msg)

// 函数名
void some_function(void) {
    DEBUG_PRINT("Entered %s\n", __func__);
}

// 日期和时间
printf("Compiled on %s at %s\n", __DATE__, __TIME__);

// 标准版本
#if __STDC_VERSION__ >= 199901L
    // C99或更高版本
#endif

9. 存储类别与作用域

9.1 存储类别

C语言提供了几种存储类别说明符:

auto:局部变量的默认存储类别,很少显式使用:

void function(void) {
    auto int x = 10;  // 等价于 int x = 10;
}

static:有两种主要用途

静态局部变量保持值在函数调用之间:

void counter(void) {
    static int count = 0;  // 仅初始化一次
    count++;
    printf("Called %d times\n", count);
}

静态全局变量和函数限制作用域在当前文件:

static int file_scope_var = 0;  // 只在本文件可见

static void helper_function(void) {
    // 只能在本文件内调用
}

extern:声明变量或函数在其他文件中定义:

// file1.c
int global_counter = 0;

// file2.c
extern int global_counter;  // 声明,不分配存储空间
void increment(void) {
    global_counter++;
}

register:建议编译器将变量存储在寄存器中(现代编译器通常忽略):

void fast_loop(void) {
    register int i;
    for (i = 0; i < 1000000; i++) {
        // 循环变量建议存储在寄存器
    }
}

9.2 作用域规则

C语言有四种作用域:文件作用域、函数作用域、块作用域和函数原型作用域。

在嵌入式开发中,合理使用作用域可以避免命名冲突和意外的副作用:

// 文件作用域(全局)
int global_var = 0;
static int file_static_var = 0;  // 仅本文件可见

void function(void) {
    // 函数作用域
    int local_var = 0;

    if (condition) {
        // 块作用域
        int block_var = 0;
        // local_var和block_var都可见
    }
    // block_var在这里不可见
}

10. 内存管理

10.1 动态内存分配

虽然在嵌入式系统中应尽量避免动态内存分配(因为内存碎片和不确定性),但了解这些函数仍然重要:

#include <stdlib.h>

// 分配内存
int* array = (int*)malloc(10 * sizeof(int));
if (array == NULL) {
    // 分配失败处理
}

// 分配并清零
int* zeros = (int*)calloc(10, sizeof(int));

// 重新分配
array = (int*)realloc(array, 20 * sizeof(int));

// 释放内存
free(array);
array = NULL;  // 良好的实践

10.2 内存布局

理解程序的内存布局对嵌入式开发至关重要,这里我们放到后面更加专门的部分介绍,这里就是过一下。

+------------------+  高地址
|      栈(Stack)   |  向下增长,存放局部变量和函数调用
+------------------+
|        ↓         |
|                  |
|     未分配       |
|                  |
|        ↑         |
+------------------+
|     堆(Heap)     |  向上增长,动态分配内存
+------------------+
|   BSS段          |  未初始化的全局变量和静态变量
+------------------+
|   数据段(Data)   |  初始化的全局变量和静态变量
+------------------+
|   代码段(Text)   |  程序代码(只读)
+------------------+  低地址

在嵌入式系统中,通常需要精确控制变量的存储位置:

// 放置在特定内存区域(编译器扩展)
__attribute__((section(".ccmram")))
static uint32_t fast_buffer[1024];

// 对齐要求
__attribute__((aligned(4)))
uint8_t dma_buffer[256];

// 禁止优化
__attribute__((used))
const uint32_t version = 0x01020304;