4. 硬件平台与实时性
内存和缓存,说白了就是嵌入式系统的「血液」和「高速公路」。很多实时性卡顿,根源都在这里。我做了十几年嵌入式,见过太多项目因为内存碎片导致系统崩溃,或者因为Cache没处理好,数据死活不对。
这一章,咱们就聊聊怎么把这几个硬骨头啃下来。
4.1 内存池管理:告别碎片化
标准库的 malloc/free 用起来方便,但在实时系统里,它是个定时炸弹。为什么?因为频繁分配释放会产生大量内存碎片,最终导致分配失败。
我个人习惯,在实时任务里,一律使用内存池。
4.1.1 固定大小内存池
这是最常用的方式。预先划分一块连续内存,切成固定大小的块。分配和释放都是 O(1) 复杂度,没有碎片。
/* 固定大小内存池示例 */
#define POOL_SIZE 10
#define BLOCK_SIZE 64
typedef struct {
uint8_t buffer[POOL_SIZE][BLOCK_SIZE];
uint32_t free_mask; /* 位图标记空闲块 */
} mem_pool_t;
void* pool_alloc(mem_pool_t *pool) {
/* 找到第一个空闲位 */
uint32_t idx = __CLZ(__RBIT(pool->free_mask));
if (idx >= POOL_SIZE) return NULL;
pool->free_mask &= ~(1UL << idx);
return &pool->buffer[idx][0];
}
void pool_free(mem_pool_t *pool, void *ptr) {
uint32_t idx = ((uint32_t)ptr - (uint32_t)pool->buffer) / BLOCK_SIZE;
pool->free_mask |= (1UL << idx);
}
.noinit 段,这样系统复位后数据还在,调试时特别有用。
4.1.2 变长内存池
如果数据包大小不固定,可以用伙伴算法(Buddy System)。它把内存按 2 的幂次分割,分配和释放效率也很高。
| 特性 | 固定大小池 | 伙伴算法池 |
|---|---|---|
| 分配速度 | O(1) | O(log N) |
| 内存利用率 | 低(有内碎片) | 中等 |
| 适用场景 | 固定长度消息、任务栈 | 变长网络包、文件系统缓存 |
4.2 DMA传输优化:让CPU喘口气
DMA(直接存储器访问)是实时系统的「隐形助手」。它能在不占用 CPU 的情况下,完成数据搬运。
4.2.1 双缓冲(Ping-Pong)模式
这是我最喜欢的 DMA 用法。两个缓冲区轮流工作:一个给 DMA 写,一个给 CPU 读。没有锁,没有等待。
/* 双缓冲DMA示例 */
#define BUF_SIZE 1024
static uint8_t ping_buf[BUF_SIZE] __attribute__((aligned(32)));
static uint8_t pong_buf[BUF_SIZE] __attribute__((aligned(32)));
static volatile uint32_t active_buf = 0;
void dma_callback(void) {
/* DMA完成中断:切换缓冲区 */
active_buf ^= 1;
/* 重新配置DMA到另一个缓冲区 */
DMA->SAR = (uint32_t)(active_buf ? pong_buf : ping_buf);
DMA->CR |= DMA_CR_EN;
}
void process_data(void) {
uint8_t *data = active_buf ? ping_buf : pong_buf;
/* CPU处理 data,DMA同时在写另一个缓冲区 */
/* ... */
}
4.2.2 链表DMA(Scatter-Gather)
当数据不连续时,链表 DMA 可以一次传输多个不连续的内存块。我曾在视频采集项目中用这个,把帧率从 15fps 提到了 30fps。
关键指标: DMA 传输延迟 = 源地址建立时间 + 目的地址建立时间 + 传输时间。对于高速外设(如以太网),建议使用 4 字节对齐的缓冲区,否则会触发总线错误。
4.3 Cache一致性维护:别让数据「打架」
Cache 是双刃剑。用好了,性能翻倍;用不好,数据错乱。特别是在多核系统或 DMA 场景下,Cache 一致性问题能把人逼疯。
4.3.1 问题根源
CPU 读写 Cache,DMA 直接读写内存。两边数据不一致了,程序就跑飞了。
/* 典型问题场景 */
uint8_t buffer[1024];
/* CPU写buffer */
buffer[0] = 0xAA; /* 数据在Cache里,还没写回内存 */
/* DMA从内存读buffer */
DMA->SAR = (uint32_t)buffer;
DMA->CR |= DMA_CR_EN; /* DMA读到的是旧数据! */
4.3.2 解决方案
| 方法 | 操作 | 开销 |
|---|---|---|
| Clean(清空) | 将Cache写回内存 | 中 |
| Invalidate(失效) | 丢弃Cache内容,下次从内存读 | 低 |
| Clean+Invalidate | 先写回再失效 | 高 |
4.3.3 非Cache区域
对于 DMA 缓冲区,我建议直接配置为非 Cache 区域。ARM Cortex-M7 的 MPU 可以做到:
/* 配置MPU:DMA缓冲区为非Cache */
MPU->RNR = 0;
MPU->RBAR = (uint32_t)buffer;
MPU->RASR = (0x03 << 0) | /* 全权限 */
(0x01 << 16) | /* 1区 */
(0x01 << 18) | /* 可执行 */
(0x00 << 19) | /* 非共享 */
(0x00 << 24) | /* 非Cache */
(0x1F << 1); /* 大小 */
4.4 TCM紧耦合内存使用:零等待的「黄金地段」
TCM(Tightly Coupled Memory)是挂在 CPU 内部总线上的内存,访问速度跟 CPU 一样快,没有 Cache Miss 的惩罚。
4.4.1 放什么到TCM
- 中断服务程序(ISR):要求最低延迟
- 实时控制循环:如电机 FOC、PID 计算
- 关键数据:如任务栈、调度器变量
- Bootloader:启动阶段 Cache 还没初始化
/* 将关键函数放到ITCM */
__attribute__((section(".itcm")))
void critical_isr(void) {
/* 零等待执行 */
/* ... */
}
/* 将关键数据放到DTCM */
__attribute__((section(".dtcm")))
volatile uint32_t system_tick;
4.4.2 TCM vs Cache
| 特性 | TCM | Cache |
|---|---|---|
| 访问延迟 | 1个时钟周期 | 1-10个周期(命中/未命中) |
| 确定性 | 完全确定 | 不确定(依赖命中率) |
| 大小 | 通常 64KB-512KB | 通常 16KB-2MB |
| 功耗 | 低 | 较高 |
核心原则: TCM 放「确定性」代码和数据,Cache 放「大块但非实时」的内容。别把整个程序塞进 TCM,它没那么大。
4.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你想想看,从内存分配开始,到数据搬运,再到缓存一致性,最后到紧耦合内存——这是一条完整的优化链路。
嗯,到这里,内存与缓存优化的核心内容就讲完了。记住:没有银弹,每种技术都有它的适用场景。关键是要理解你的系统瓶颈在哪里,然后对症下药。