在 ISP(Image Signal Processor)系统中,AP 与 ISP 之间的内存交互本质上是一个**"AP 申请可 DMA 访问的共享内存 → 内核建立映射 → 硬件寻址读写 → 同步与回收"**的过程。下面按数据流分层详细拆解。
一、ISP 内存需求的特殊性
与普通应用内存不同,ISP 使用的内存有严格约束:
| 特性 | 要求 | 原因 |
|---|---|---|
| 物理连续性 | 最好物理连续(或 IOMMU 映射) | ISP DMA 通常按物理地址突发传输 |
| Cache 一致性 | 必须显式同步 | AP(带 Cache)与 ISP(无 Cache)同读写 |
| 大页/对齐 | 通常 4KB/64KB/1MB 对齐 | DMA 效率、页表映射要求 |
| 安全/隔离 | 可能需要 TrustZone 隔离 | 保护 RAW 图像等敏感数据 |
二、整体架构与角色
┌─────────────────────────────────────────┐
│ 用户空间 (Camera HAL / APP) │
│ 调用 ioctl/mmap │
├─────────────────────────────────────────┤
│ 内核空间 (V4L2 / ISP Driver) │
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ CMA/ION │ │DMA-BUF │ │IOMMU │ │
│ │ 分配器 │ │ 框架 │ │/SMMU │ │
│ └────┬────┘ └────┬────┘ └───┬────┘ │
├────────┼────────────┼───────────┼──────┤
│ APB/AHB/AXI 总线 │ │ │
├────────┼────────────┼───────────┼──────┤
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ ISP 硬件 │ │
│ │ (DMA 控制器 → 处理流水线) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
三、内存申请与下发的完整流程
阶段 1:用户态发起 Buffer 请求
Camera HAL(如 Android CamX/HAL3)通过 V4L2(Video4Linux2) 或厂商私有接口请求图像 Buffer:
c
// 典型调用链
ioctl(fd, VIDIOC_REQBUFS, &req); // 申请 N 个 Buffer
ioctl(fd, VIDIOC_QUERYBUF, &buf); // 查询 Buffer 信息
mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset); // 映射到用户态
关键参数 :req.memory = V4L2_MEMORY_DMABUF 或 V4L2_MEMORY_MMAP。
阶段 2:内核态分配内存(多种机制)
ISP 驱动在内核态根据平台选择不同的分配器:
方式 A:CMA(Contiguous Memory Allocator)------ 最常用
c
// ISP 驱动中
struct page *page = dma_alloc_from_contiguous(dev, count, order);
phys_addr_t phys = page_to_phys(page); // 物理地址(ISP DMA 直接用这个)
- 内核启动时预留一片物理连续内存 (
cma=64M等参数)。 - 适合 ISP 输入/输出需要大段物理连续地址的场景。
方式 B:ION / DMA-BUF ------ 现代主流(Android/Linux)
c
// 分配共享 Buffer
int fd = ion_alloc(len, heap_id_mask, flags); // 旧 ION
// 或
struct dma_buf *dmabuf = dma_buf_export(...); // 标准 DMA-BUF
- ION (Android)或 DMA-BUF (Linux 主线)分配文件描述符化的内存。
- 支持物理不连续内存(通过 IOMMU/SMMU 映射为 ISP 可见的 IOVA)。
- 零拷贝共享:AP、ISP、GPU、VPU 都可以通过 fd 共享同一块内存。
方式 C:SMMU/IOMMU 映射(离散内存聚合)
如果系统没有足够大的连续物理内存:
物理页: [Page0 @ 0x1000] [Page1 @ 0x5000] [Page2 @ 0x3000] // 物理不连续
↓ SMMU 页表映射
IOVA: [0x80000000] [0x80001000] [0x80002000] // ISP 看到的"连续"地址
c
// 驱动代码示例
iova = iommu_map(domain, iova, phys, size, IOMMU_READ|IOMMU_WRITE);
// 将 IOVA 写入 ISP 寄存器,ISP 以为自己访问的是连续地址
阶段 3:地址下发给 ISP 硬件
这是核心步骤 。驱动将内存地址配置到 ISP 的寄存器中:
c
// ISP 驱动:配置 DMA 读写地址
void isp_config_buffer(struct isp_device *isp, struct isp_buffer *buf)
{
phys_addr_t dma_addr = buf->dma_addr; // 物理地址或 IOVA
// 写入 ISP 寄存器(通过 AXI/APB 总线)
writel(dma_addr, isp->base + ISP_REG_DMA_SRC_ADDR); // 输入 RAW 地址
writel(dma_addr + offset_y, isp->base + ISP_REG_DMA_DST_ADDR_Y); // 输出 Y 地址
writel(dma_addr + offset_uv, isp->base + ISP_REG_DMA_DST_ADDR_UV); // 输出 UV 地址
// 配置长度、stride、格式等
writel(buf->size, isp->base + ISP_REG_DMA_SIZE);
}
关键概念:
- ISP 是总线主设备(Bus Master),内置 DMA 控制器。
- 它直接通过 AXI/AHB 总线发起读写,不需要 CPU 干预。
- CPU 只需告诉 ISP"数据在物理地址
0x8A000000处,去取吧"。
阶段 4:Cache 同步(数据一致性)
AP(CPU)写数据到内存后,如果 ISP 直接读取,可能读到 Cache 里的旧数据(因为内存里的数据还没回写)。必须在下发前做Cache 同步:
c
// 方式 1:写回并无效化 Cache(最常用)
dma_sync_sg_for_device(dev, sglist, nents, DMA_TO_DEVICE); // CPU → 内存 → ISP
dma_sync_sg_for_cpu(dev, sglist, nents, DMA_FROM_DEVICE); // ISP → 内存 → CPU
// 方式 2:分配时标记为 Non-Cacheable(简单但性能差)
dma_alloc_attrs(dev, size, &dma_handle, GFP_KERNEL, DMA_ATTR_NON_CONSISTENT);
// 方式 3:硬件自动一致性(ARM CCI/DSU 等,高端平台支持)
典型时序:
- AP 填充图像数据到 Buffer(走 Cache)。
dma_sync_sg_for_device():把 Cache 脏数据刷回内存。- 下发地址给 ISP,启动 ISP。
- ISP DMA 读内存 → 处理 → 写回内存。
- ISP 中断通知 AP 完成。
dma_sync_sg_for_cpu():无效化 CPU Cache,确保 AP 读到最新数据。
阶段 5:中断回收与 Buffer 轮转
ISP 处理完后通过中断通知 AP:
ISP 完成帧处理 ──→ 触发 IRQ ──→ ISP 驱动 ISR ──→ V4L2 唤醒用户态
│
▼
将 Buffer 标记为 DONE
用户态 dequeue 取走图像
空 Buffer 重新 enqueue 给 ISP
这是典型的 "生产者-消费者"环形队列(Ring Buffer) 模型:
- AP 预先分配 N 个 Buffer(如 4~8 个)组成队列。
- ISP 处理完一个,AP 填下一个,实现流水线并行。
四、一个完整的代码级流程示例
以 Linux V4L2 + CMA + 物理连续内存为例:
c
// ========== 1. 驱动初始化:预留/申请 CMA ==========
static int isp_probe(struct platform_device *pdev)
{
struct isp_dev *isp = devm_kzalloc(&pdev->dev, sizeof(*isp), GFP_KERNEL);
// 从 CMA 分配 3 个 Buffer(用于 Triple Buffering)
for (i = 0; i < 3; i++) {
isp->buf[i].vaddr = dma_alloc_coherent(&pdev->dev, SIZE,
&isp->buf[i].dma_addr, GFP_KERNEL);
// dma_addr: ISP 用的物理地址
// vaddr: CPU 用的虚拟地址
}
}
// ========== 2. 用户态请求 Buffer ==========
static int isp_reqbufs(struct file *file, void *priv, struct v4l2_requestbuffers *p)
{
// 将内核分配的 dma_addr 通过 mmap 暴露给用户态
// 用户态拿到虚拟地址,可直接读写
}
// ========== 3. 下发配置并启动 ISP ==========
static void isp_start_streaming(struct isp_dev *isp)
{
struct isp_buffer *buf = list_first_entry(&isp->ready_list, ...);
// Cache 同步:CPU 写的数据刷到内存
dma_sync_single_for_device(isp->dev, buf->dma_addr, buf->size, DMA_TO_DEVICE);
// 配置 ISP 硬件寄存器
writel(buf->dma_addr, isp->base + ISP_DMA_ADDR_REG);
writel(0x1, isp->base + ISP_START_REG); // GO! ISP 开始 DMA
}
// ========== 4. 中断处理 ==========
static irqreturn_t isp_irq_handler(int irq, void *dev_id)
{
struct isp_dev *isp = dev_id;
// 同步 Cache:让 CPU 能看到 ISP 写的新数据
dma_sync_single_for_cpu(isp->dev, buf->dma_addr, buf->size, DMA_FROM_DEVICE);
// 唤醒等待的进程
vb2_buffer_done(&buf->vb, VB2_BUF_STATE_DONE);
// 切换下一个 Buffer
isp_switch_buffer(isp);
return IRQ_HANDLED;
}
五、关键问题与优化
| 问题 | 解决方案 |
|---|---|
| 物理内存不够连续 | 使用 IOMMU/SMMU 做 IOVA 映射,或增大 CMA 预留 |
| Cache 不同步花屏 | 严格使用 dma_sync_* 接口,或分配 Non-Cacheable 内存 |
| Buffer 切换延迟 | Triple/Quad Buffering,ISP 处理时 AP 准备下一帧 |
| 安全域隔离 | TrustZone + Secure Memory,ISP 安全通路访问安全内存 |
| 跨 IP 共享 | 使用 DMA-BUF fd,ISP、GPU、VPU 零拷贝共享 |
六、总结
AP 向 ISP 下发内存的完整链路可以概括为:
- 申请 :AP(用户态)通过 V4L2 → 内核驱动调用
dma_alloc_coherent/ion_alloc/dma_buf分配物理连续或 IOMMU 映射的内存。 - 映射 :内核返回物理地址(或 IOVA)给驱动,同时通过
mmap映射虚拟地址到用户态。 - 同步 :AP 填完数据后,调用
dma_sync_*_for_device刷 Cache。 - 下发:驱动将物理地址/IOVA 写入 ISP 的 DMA 寄存器。
- 硬件读写:ISP 通过 AXI 总线直接 DMA 读写内存,无需 CPU 参与。
- 中断回传:ISP 完成触发中断,驱动同步 Cache 并通知 AP 取数据。
理解这个流程的关键在于:ISP 是独立的总线主设备,CPU 只负责"指路和同步",实际搬运由 DMA 完成。