在 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:
// 典型调用链ioctl(fd,VIDIOC_REQBUFS,&req);// 申请 N 个 Bufferioctl(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)—— 最常用
// ISP 驱动中structpage*page=dma_alloc_from_contiguous(dev,count,order);phys_addr_tphys=page_to_phys(page);// 物理地址(ISP DMA 直接用这个)- 内核启动时预留一片物理连续内存(
cma=64M等参数)。 - 适合 ISP 输入/输出需要大段物理连续地址的场景。
方式 B:ION / DMA-BUF —— 现代主流(Android/Linux)
// 分配共享 Bufferintfd=ion_alloc(len,heap_id_mask,flags);// 旧 ION// 或structdma_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 看到的"连续"地址// 驱动代码示例iova=iommu_map(domain,iova,phys,size,IOMMU_READ|IOMMU_WRITE);// 将 IOVA 写入 ISP 寄存器,ISP 以为自己访问的是连续地址阶段 3:地址下发给 ISP 硬件
这是核心步骤。驱动将内存地址配置到 ISP 的寄存器中:
// ISP 驱动:配置 DMA 读写地址voidisp_config_buffer(structisp_device*isp,structisp_buffer*buf){phys_addr_tdma_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 同步:
// 方式 1:写回并无效化 Cache(最常用)dma_sync_sg_for_device(dev,sglist,nents,DMA_TO_DEVICE);// CPU → 内存 → ISPdma_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 + 物理连续内存为例:
// ========== 1. 驱动初始化:预留/申请 CMA ==========staticintisp_probe(structplatform_device*pdev){structisp_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 ==========staticintisp_reqbufs(structfile*file,void*priv,structv4l2_requestbuffers*p){// 将内核分配的 dma_addr 通过 mmap 暴露给用户态// 用户态拿到虚拟地址,可直接读写}// ========== 3. 下发配置并启动 ISP ==========staticvoidisp_start_streaming(structisp_dev*isp){structisp_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. 中断处理 ==========staticirqreturn_tisp_irq_handler(intirq,void*dev_id){structisp_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);// 切换下一个 Bufferisp_switch_buffer(isp);returnIRQ_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 完成。