第三章:音频驱动模型

好,咱们今天聊聊QNX音频驱动的标准模型。说实话,这个模型我研究了很久,从最初的懵懂到现在的得心应手,中间踩过不少坑。今天我把这些经验都分享给你。

3.1 驱动入口点:一切从这里开始

每个音频驱动都有一个入口点,就像一扇门。QNX里,这个入口点通常是io_open。我习惯在入口点里做三件事:初始化硬件、注册回调、分配DMA缓冲区。

// 典型的驱动入口点
int audio_open(dispatch_t *dpp, int flags, void *handle) {
    audio_dev_t *dev = (audio_dev_t *)handle;
    
    // 1. 初始化硬件
    if (hw_init(dev) != EOK) {
        return -1;
    }
    
    // 2. 注册中断
    InterruptAttach(dev->irq, audio_isr, dev, 0, 0);
    
    // 3. 分配DMA缓冲区
    dev->dma_buf = mmap_device_memory(...);
    
    return EOK;
}

嗯,这里要注意一点:入口点不能阻塞太久。我曾经在一个项目里,因为硬件初始化太慢,导致系统启动卡了整整3秒。后来我把初始化拆成了两步:快速注册和延迟初始化。

3.2 回调函数:驱动的心脏

回调函数是音频驱动的核心。说白了,就是系统在特定事件发生时,会调用你注册的函数。QNX音频框架里,最重要的回调有这么几个:

回调名称 触发时机 我踩过的坑
audio_start 开始播放/录制 忘记检查DMA状态
audio_stop 停止播放/录制 中断没清干净
audio_query 查询设备能力 返回了错误采样率
audio_set_format 设置音频格式 参数校验不严格

你想想看,这些回调函数就像乐队的指挥。每个回调都要在合适的时间做合适的事。我建议你在每个回调函数开头加个日志,方便调试。

3.3 DMA管理:数据搬运工

DMA是音频驱动的关键。没有DMA,CPU就得亲自搬数据,那效率就太低了。QNX里管理DMA,我总结了几条经验:

DMA缓冲区设计要点:

  • 缓冲区大小要合理。太小容易溢出,太大浪费内存。我一般用4KB-16KB
  • 环形缓冲区是标配。用两个指针:读指针和写指针
  • 记得做缓存一致性处理。DMA和CPU看到的可能是不同数据
// DMA缓冲区管理示例
typedef struct {
    uint32_t *buf;      // 缓冲区基地址
    uint32_t size;      // 缓冲区大小
    volatile uint32_t rd_ptr;  // 读指针
    volatile uint32_t wr_ptr;  // 写指针
} dma_ring_buf_t;

int dma_write(dma_ring_buf_t *ring, uint32_t *data, uint32_t len) {
    // 检查是否有足够空间
    uint32_t avail = (ring->rd_ptr - ring->wr_ptr - 1) % ring->size;
    if (avail < len) {
        return -ENOMEM;
    }
    
    // 写入数据
    for (int i = 0; i < len; i++) {
        ring->buf[ring->wr_ptr] = data[i];
        ring->wr_ptr = (ring->wr_ptr + 1) % ring->size;
    }
    
    return EOK;
}

我的小技巧:DMA中断触发频率不要太高。我一般设置成每半缓冲区触发一次中断。这样既不会漏数据,也不会频繁打断CPU。

3.4 中断处理:及时响应

中断处理是音频驱动的最后一道防线。音频数据是实时流,中断响应慢了,就会出现爆音或者断流。我记得有一次,客户反馈播放音乐时偶尔有"咔咔"声,查了半天发现是中断处理函数里做了太多计算。

中断处理的原则很简单:快进快出。在中断里只做最必要的事:

  1. 读取中断状态寄存器
  2. 清除中断标志
  3. 更新DMA指针
  4. 唤醒等待的线程(如果需要)
// 精简的中断处理函数
const struct sigevent *audio_isr(void *arg, int id) {
    audio_dev_t *dev = (audio_dev_t *)arg;
    
    // 1. 读取状态
    uint32_t status = in32(dev->reg_base + INT_STATUS);
    
    // 2. 清除中断
    out32(dev->reg_base + INT_CLEAR, status);
    
    // 3. 更新DMA指针
    if (status & DMA_DONE) {
        dev->dma_rd_ptr = (dev->dma_rd_ptr + DMA_HALF_SIZE) % DMA_TOTAL_SIZE;
    }
    
    // 4. 唤醒处理线程
    if (dev->waiting) {
        pthread_mutex_lock(&dev->mtx);
        pthread_cond_signal(&dev->cond);
        pthread_mutex_unlock(&dev->mtx);
    }
    
    return NULL;
}

警告:千万不要在中断里做内存分配、文件操作、或者复杂的数学运算。我曾经见过一个驱动,在中断里调用了malloc,结果系统直接崩溃。中断里只做最轻量级的工作。

3.5 实战经验总结

说了这么多,我总结几条实战经验:

  • 先跑通再优化:我刚开始做音频驱动时,总想着一次就写出完美代码。结果往往是bug一堆。现在我的做法是:先让声音能出来,哪怕有杂音,然后再逐步优化。
  • 日志要详细:每个关键函数入口和出口都加日志。调试时你就知道这有多重要了。
  • 测试要全面:不同采样率、不同声道数、不同缓冲区大小都要测。我曾经因为只测试了44.1kHz,结果客户用48kHz时出了问题。

嗯,音频驱动模型就讲到这里。下一章我们聊聊具体的硬件适配,到时候我会拿一个真实的Codec芯片做例子,手把手教你写驱动。