第三章:音频驱动模型
好,咱们今天聊聊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 中断处理:及时响应
中断处理是音频驱动的最后一道防线。音频数据是实时流,中断响应慢了,就会出现爆音或者断流。我记得有一次,客户反馈播放音乐时偶尔有"咔咔"声,查了半天发现是中断处理函数里做了太多计算。
中断处理的原则很简单:快进快出。在中断里只做最必要的事:
- 读取中断状态寄存器
- 清除中断标志
- 更新DMA指针
- 唤醒等待的线程(如果需要)
// 精简的中断处理函数
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芯片做例子,手把手教你写驱动。