嵌入式C/C++编程要点:从跑起来到跑得稳

说实话,很多刚转嵌入式的人,觉得C/C++嘛,大学都学过。但真到了嵌入式环境里,你会发现完全不是那么回事。我见过太多人,在PC上写得好好的代码,一放到STM32或者ARM Cortex-M上,直接就崩了。

为什么会这样?说白了,嵌入式C/C++有几个核心差异点,咱们一个一个说。

1. 资源受限下的编程习惯

我个人习惯,在嵌入式项目里,第一件事就是搞清楚:ROM多大?RAM多大?Cache有几级? 这些数字决定了你代码的写法。

  • 避免动态内存分配:new/delete、malloc/free,能不用就不用。我在项目中遇到过,一个malloc返回NULL,整个系统直接死机。后来我改用静态分配+内存池,稳得很。
  • 慎用STL容器:vector、map这些,底层有动态扩容,实时性没法保证。我建议自己实现轻量级的数据结构。
  • 内联函数与宏的取舍:小函数用inline,但别滥用。我曾经把一个200行的函数写成inline,结果代码体积暴涨,Cache miss率飙升。

核心原则:嵌入式C/C++,写的是确定性代码。每一行指令执行时间、每一字节内存占用,都要心里有数。

2. 数据结构基础:栈、队列、链表

这些数据结构,在嵌入式里不是用来考试的,是用来解决实际问题的。我挑几个重点说。

栈(Stack)

嵌入式里最常见的栈,就是函数调用栈。但你自己写代码时,栈也很有用。比如在路径规划里,DFS(深度优先搜索)就要用栈来保存回溯点。

// 一个简单的静态栈实现
#define STACK_SIZE 256

typedef struct {
    int data[STACK_SIZE];
    int top;
} Stack;

void push(Stack *s, int val) {
    if (s->top < STACK_SIZE - 1) {
        s->data[++s->top] = val;
    } else {
        // 栈溢出处理——我一般直接断言
        // 因为嵌入式里栈溢出是严重错误
    }
}

int pop(Stack *s) {
    if (s->top >= 0) {
        return s->data[s->top--];
    }
    return -1; // 空栈
}

避坑指南:我曾经在A*算法里用递归实现回溯,结果递归深度一深,栈直接爆了。后来改成显式栈,问题解决。记住:嵌入式里,递归要慎用,尤其是深度不确定的时候。

队列(Queue)

队列在嵌入式里太常用了。传感器数据缓冲、串口接收、任务调度,哪哪都有它。我推荐用环形队列,因为它没有内存碎片,而且入队出队都是O(1)。

// 环形队列——嵌入式标配
#define QUEUE_SIZE 64

typedef struct {
    int buffer[QUEUE_SIZE];
    int head;
    int tail;
    int count;
} RingQueue;

int enqueue(RingQueue *q, int val) {
    if (q->count >= QUEUE_SIZE) {
        return -1; // 满了
    }
    q->buffer[q->tail] = val;
    q->tail = (q->tail + 1) % QUEUE_SIZE;
    q->count++;
    return 0;
}

int dequeue(RingQueue *q, int *val) {
    if (q->count == 0) {
        return -1; // 空了
    }
    *val = q->buffer[q->head];
    q->head = (q->head + 1) % QUEUE_SIZE;
    q->count--;
    return 0;
}

你想想看,如果不用环形队列,用链表实现队列,每次入队都要malloc,出队要free。在实时性要求高的系统里,这简直是灾难。

链表(Linked List)

链表在嵌入式里,我主要用在内存管理任务调度上。但要注意,链表的遍历是O(n),如果数据量大,实时性会受影响。

我个人习惯,如果数据量固定且不大,优先用数组。如果数据量动态变化,再用链表。而且我一般用静态链表——节点从预分配的内存池里取,避免动态分配。

注意:链表在嵌入式里最大的坑是——指针错误。野指针、空指针、悬空指针,任何一个都能让系统跑飞。我建议:每次操作链表后,都检查一下指针有效性。别嫌麻烦,血的教训。

3. 内存管理:嵌入式里的头等大事

嵌入式内存管理,说白了就三个字:别乱搞

内存区域 特点 使用建议
栈(Stack) 自动分配释放,速度快 局部变量、函数参数,注意别太大
堆(Heap) 动态分配,有碎片风险 能不用就不用,或者用内存池替代
全局/静态区 程序整个生命周期存在 适合常量、全局状态、缓冲池
代码区(Flash) 只读,掉电不丢失 存放程序指令、常量数据

我见过最典型的错误:在中断服务函数里调用malloc。中断里要求快进快出,malloc的执行时间不确定,而且可能触发内存管理的中断,导致嵌套中断,系统直接挂掉。

我的经验法则:中断里只用栈变量,不用堆。如果非要传递数据,用全局的环形队列。

4. 实时性要求:时间就是生命

嵌入式系统,尤其是机器人路径规划,对实时性要求极高。你想想看,一个路径规划算法,如果计算时间超过控制周期,那机器人要么撞墙,要么原地打转。

实时性编程的几个要点:

  • 关中断时间要短:关中断是为了保护临界区,但关太久会影响其他中断响应。我一般控制在几十微秒以内。
  • 避免忙等待:while(flag); 这种写法,CPU全浪费了。用中断或者信号量来同步。
  • 任务优先级要合理:路径规划这种计算密集型任务,优先级不能太高,否则会饿死其他任务。但也不能太低,否则规划结果出不来。
  • 使用RTOS:FreeRTOS、RT-Thread这些,能帮你管理任务调度。但记住,RTOS不是万能的,它只是工具,关键还是你的代码设计。

一个小技巧:在路径规划算法里,我经常把计算任务拆分成多个小步骤,每个步骤执行时间固定。这样即使被高优先级任务打断,下次还能接着算。这叫「时间片化」,在嵌入式里很实用。

5. 实战建议:从零开始搭建嵌入式路径规划框架

如果你刚开始做嵌入式路径规划,我建议按这个顺序来:

  1. 先搞定基础数据结构:实现一个静态栈、一个环形队列、一个静态链表。别用STL,自己手写。
  2. 设计内存池:预分配一大块内存,自己管理分配和释放。这样就没有碎片问题,而且分配时间是确定的。
  3. 实现一个简单的任务调度器:哪怕只是轮询,也要把各个模块的执行时间测出来。
  4. 把路径规划算法跑在裸机上:先别上RTOS,裸机跑一遍,你才能深刻理解实时性的含义。
  5. 再移植到RTOS:用信号量、消息队列这些机制,把各个模块解耦。

嗯,这一步一步走下来,你对嵌入式C/C++的理解,绝对会上一个台阶。我当年就是这么过来的,虽然踩了不少坑,但每个坑都让我学到了东西。

下一章,我们会正式开始讲路径规划算法的核心——图搜索与A*算法。到时候,这些数据结构就派上大用场了。