3、嵌入式C语言基础:指针与数组、结构体与联合体、位操作、volatile关键字、内存管理

各位同学,欢迎来到第三章。说实话,C语言基础这部分,很多工程师觉得「我都会啊」,但真到了写伺服驱动代码时,各种坑就冒出来了。我见过太多人因为指针用错导致电机飞车,因为volatile漏写导致电流采样值死活不对。今天咱们就把这些硬骨头啃下来。

本章核心:伺服驱动器的底层代码,90%的bug都出在这五个知识点上。不是语法不会,是嵌入式场景下的特殊用法没吃透。

3.1 指针与数组:伺服驱动里的「地址游戏」

指针这东西,说白了就是地址。但在嵌入式里,它不只是地址,更是操作硬件的钥匙。

指针和数组的关系

数组名就是首地址,这个大家都知道。但我在项目中遇到过一个问题:用sizeof求数组长度,结果传参后就不对了。为什么?因为数组作为函数参数时,会退化成指针。

// 错误示范
void process_data(uint16_t arr[]) {
    size_t len = sizeof(arr) / sizeof(arr[0]);  // 这里len永远是1或2!
}

// 正确做法
void process_data(uint16_t *arr, size_t len) {
    for(size_t i = 0; i < len; i++) {
        // 处理数据
    }
}

函数指针:伺服里的回调利器

伺服驱动里,经常要用到函数指针。比如电流环的PI参数切换,不同工况用不同算法。我习惯把函数指针放在结构体里,这样切换就像换表格一样简单。

typedef struct {
    float kp;
    float ki;
    float (*calc_func)(float err, float *integral);
} pi_controller_t;

// 两种算法
float pi_standard(float err, float *integral) { ... }
float pi_anti_windup(float err, float *integral) { ... }

// 使用时直接赋值
pi_controller_t current_pi = {1.5, 0.01, pi_standard};

我的习惯:指针变量命名时加个p_前缀,比如p_buffer。这样一眼就能看出是指针,避免误用。

3.2 结构体与联合体:打包数据,节省内存

结构体在伺服里太常用了。电机参数、编码器数据、PID系数,全得用结构体包起来。

结构体对齐:一个隐形杀手

你想想看,如果结构体成员顺序没排好,编译器会自动填充字节,导致结构体变大。我曾经在DSP上调试一个通信协议,结构体大小总是对不上,查了两天才发现是字节对齐的问题。

// 浪费内存的写法
typedef struct {
    uint8_t  status;    // 1字节
    uint32_t position;  // 4字节
    uint16_t speed;     // 2字节
} motor_data_t;  // 实际占用12字节!因为对齐填充

// 优化写法:按大小降序排列
typedef struct {
    uint32_t position;  // 4字节
    uint16_t speed;     // 2字节
    uint8_t  status;    // 1字节
} motor_data_t;  // 实际占用8字节,省了33%

联合体:寄存器操作的瑞士军刀

联合体让同一块内存有多种解读方式。比如读取32位编码器数据,可以同时按字节和整数值访问。

typedef union {
    uint32_t value;
    uint8_t  bytes[4];
    struct {
        uint16_t low_word;
        uint16_t high_word;
    } words;
} encoder_data_t;

// 使用场景:通过SPI逐字节读取编码器
encoder_data_t enc;
enc.bytes[0] = spi_read();
enc.bytes[1] = spi_read();
enc.bytes[2] = spi_read();
enc.bytes[3] = spi_read();
// 现在 enc.value 就是完整的32位位置值

注意:联合体的字节序(大小端)跟芯片有关。ARM Cortex-M一般是小端,但有些DSP是大端。跨平台时一定要确认。

3.3 位操作:寄存器级的「手术刀」

伺服驱动里,位操作是基本功。配置定时器、设置GPIO、读写状态寄存器,全得靠它。

常用位操作技巧

操作 表达式 说明
置位某位 REG |= (1 << n) 将第n位置1
清零某位 REG &= ~(1 << n) 将第n位清0
翻转某位 REG ^= (1 << n) 将第n位取反
读取某位 (REG >> n) & 1 获取第n位的值
连续多位操作 REG = (REG & ~MASK) | (val << shift) 先清零再赋值

实战:配置PWM输出引脚

// 假设GPIO控制寄存器地址为0x40020000
#define GPIO_CTRL   (*(volatile uint32_t *)0x40020000)
#define PIN_PWM1    (1 << 5)   // 第5位控制PWM1输出

// 使能PWM1输出
GPIO_CTRL |= PIN_PWM1;

// 禁用PWM1输出
GPIO_CTRL &= ~PIN_PWM1;

我的建议:位操作一定要加括号!1 << n的优先级容易搞错。另外,多用宏定义给位掩码起名字,代码可读性会好很多。

3.4 volatile关键字:告诉编译器「别优化我」

volatile可能是嵌入式里最容易被忽略的关键字。它的作用就一句话:告诉编译器,这个变量的值随时可能被意外改变,每次使用都必须从内存重新读取。

什么时候必须用volatile?

  • 硬件寄存器:比如定时器计数值、ADC结果寄存器
  • 中断服务程序修改的变量:主循环和中断共享的变量
  • 多线程/多核共享变量:虽然裸机编程不常见,但RTOS里很关键

一个血的教训

我曾经调试一个伺服驱动器,电流采样值总是跳变。查了三天,最后发现是ADC中断里更新了一个全局变量,但主循环里没加volatile。编译器优化后,主循环一直读取寄存器里的旧值,根本没看到中断的更新。

// 错误写法
uint16_t adc_value;  // 没加volatile!

void ADC_IRQHandler(void) {
    adc_value = ADC->DR;  // 中断里更新
}

void main_loop(void) {
    while(1) {
        if(adc_value > 3000) {  // 编译器可能优化成只读一次!
            // 过流保护
        }
    }
}

// 正确写法
volatile uint16_t adc_value;  // 加上volatile

注意:volatile不能保证原子性。如果变量是32位的,而MCU是16位内核,读写操作可能被中断打断。这时需要关中断或使用原子操作。

3.5 内存管理:嵌入式里的「精打细算」

伺服驱动器的RAM通常只有几十KB到几百KB。内存管理做不好,轻则跑飞,重则电机失控。

静态分配 vs 动态分配

我个人强烈建议:嵌入式底层代码尽量用静态分配。malloc/free在MCU上风险太高——内存碎片、分配失败、不确定的执行时间,这些都是伺服驱动不能接受的。

// 推荐:静态分配
#define MAX_MOTORS 4
motor_control_t motors[MAX_MOTORS];

// 不推荐:动态分配
motor_control_t *p_motor = (motor_control_t *)malloc(sizeof(motor_control_t));
if(p_motor == NULL) {
    // 处理分配失败... 但伺服里怎么处理?停机?

栈空间管理

中断服务程序里别用太多局部变量。我记得有一次,一个工程师在定时器中断里定义了一个512字节的数组,结果栈溢出,程序随机崩溃。查了两周才找到原因。

我的经验:中断函数里尽量用全局变量或静态变量。如果非要用局部变量,控制在几十字节以内。另外,链接脚本里给中断栈多分配一些空间,至少256字节。

const和static的妙用

  • const:把常量放到Flash里,节省RAM。比如PID系数表、正弦表
  • static:限制变量作用域,避免命名冲突。模块内部的全局变量都加static
// 正弦表放在Flash里
const float sin_table[256] = {0.0000, 0.0245, ...};

// 模块内部变量,外部不可见
static uint32_t last_tick = 0;

void timer_callback(void) {
    // 只能在本文件内访问last_tick
}

本章小结:指针是地址,结构体是打包,联合体是复用,位操作是精细控制,volatile是防优化,内存管理是保稳定。这五个点,每一个都在伺服驱动里天天用。别觉得基础,基础不牢,地动山摇。

嵌入式C语言基础核心知识体系 C语言基础 指针与数组 结构体与联合体 位操作 volatile 内存管理 函数指针 数组退化 地址运算 字节对齐 联合体复用 寄存器映射 置位/清零 掩码操作 寄存器配置 硬件寄存器 中断共享变量 防优化 静态分配 栈空间管理 const/static 伺服驱动器典型应用场景 电流环PI参数配置 | 编码器数据读取 | PWM寄存器配置 中断服务程序 | 内存池管理 | 故障保护逻辑

专注资料整理