5.21 Audio设备
5.21 Audio设备
📚 本节导读
学习时长: 约 40 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言基础、音频基础知识、5.1 设备驱动框架
🎯 学习目标
- 理解 OneOS Audio 设备驱动的三层架构(应用层 → 播放器框架 → I2S 硬件层)
- 掌握 Audio 设备的核心数据结构(
os_audio_device、os_audio_ops、os_audio_caps) - 掌握音频播放器的内存池缓冲队列机制与 TX 写完回调
- 理解 I2S 硬件抽象层的操作接口
- 掌握音频参数配置(采样率、声道数、采样位宽、音量)
一、概述
Audio 音频设备是 OneOS 提供的多媒体音频驱动框架,支持音频播放(Playback)和录音(Record)功能。驱动源码位于 drivers/audio/ 目录下,核心头文件为 audio.h,实现文件为 audio_player.c。I2S 硬件抽象层位于 drivers/audio/i2s.h。
Audio 设备框架的特点:
- 三层架构:应用层调用
os_device_write()写入 PCM 数据 → 播放器框架管理内存池缓冲队列 → I2S 硬件层驱动 DMA 传输 - 内存池缓冲队列:使用
os_mempool管理音频数据块,通过tx_count/tx_size实现背压流控 - TX 完成回调:通过
OS_DEVICE_CB_TYPE_TX回调自动归还已播放的缓冲区,形成生产者-消费者模型 - I2S 硬件抽象:
os_i2s_device将 I2S 外设抽象为独立设备,支持 TX/RX 使能、频率/声道/位宽配置 - 统一控制接口:通过
os_device_control()配置音频参数(采样率、声道、音量等)
1.1 使用场景
一句话核心:Audio 框架是"音频播放器/录音器的内存池缓冲框架"——应用把 PCM 数据 os_device_write 给播放器,播放器用内存池缓冲队列缓存、按块交给 HAL 层 DMA 播放,播放完回调归还。上层不用关心 DMA/中断细节。
与"应用直接调 I2S DMA"对比:
| 维度 | 用 Audio 播放器框架 | 应用直接操作 I2S |
|---|---|---|
| 缓冲管理 | 内存池队列自动管理 | 手动维护缓冲 |
| 背压 | tx_count/tx_size 自动流控 | 需自己判断 |
| 播放完成 | 回调自动归还 | 手动处理 |
| 与 HAL 解耦 | 应用只写 PCM | 绑死 I2S 寄存器 |
典型应用场景:
| 场景 | 谁在用 | 实际工作情况 |
|---|---|---|
| 语音播放 | 播放器 + 音频文件 | 解码出 PCM 后 os_device_write 给播放器,DMA 播放 |
| 录音 | 录音器 | DMA 收满调 os_audio_rx_done,应用读 PCM |
| 语音交互 | 播放 + 录音双通道 | 同一 os_audio_device 注册播放器和录音器 |
| 提示音 | 短音播放 | 直接写 PCM 播放,播完回调 |
选型边界表:
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 无音频需求 | 不用 Audio | 无 I2S/编解码器 |
| 极简单蜂鸣器 | GPIO/PWM | 不需要 PCM 播放器 |
| 高保真录音 | 专用录音框架 | Audio 框架偏播放为主 |
结合源码的使用注意点:
- 写前先 configure:
os_device_control(AUDIO_CTL_CONFIGURE)配置采样率/声道/位宽,与 I2S 硬件一致才正常播放; - 写不满是背压不是错误:
os_device_write返回< len说明缓冲满,等 TX 回调归还后重试; - 播放器/录音器都要
os_audio_tx_complete/os_audio_rx_done:HAL 层 DMA 中断里必须调,否则缓冲不会归还、计数不更新。
二、核心数据结构
2.1 Audio 设备结构体 struct os_audio_device
来源:drivers/audio/audio.h
struct os_audio_device
{
struct os_device parent;
const struct os_audio_ops *ops;
struct os_audio_replay *replay;
struct os_audio_record *record;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
parent | 首成员继承 os_device_t,设备类型注册为 OS_DEVICE_TYPE_SOUND,复用设备框架的 open/read/write/control |
ops | Audio 操作函数表(getcaps/configure/init/deinit/start/stop/transmit/receive) |
replay | 播放器控制块(内存池缓冲队列,_audio_player_init 分配) |
record | 录音控制块(录音时分配) |
关键理解 ——
parent首成员继承:os_audio_player_register里device = &(audio->parent)后os_device_register(device, name),因此os_audio_device就是普通设备,可用os_device_open("sound0")打开后os_device_write写 PCM 数据。
2.2 Audio 操作函数表 struct os_audio_ops
来源:drivers/audio/audio.h
struct os_audio_ops
{
os_err_t (*getcaps)(struct os_audio_device *audio, struct os_audio_caps *caps);
os_err_t (*configure)(struct os_audio_device *audio, struct os_audio_caps *caps);
os_err_t (*init)(struct os_audio_device *audio);
os_err_t (*deinit)(struct os_audio_device *audio);
os_err_t (*start)(struct os_audio_device *audio);
os_err_t (*stop)(struct os_audio_device *audio);
os_size_t (*transmit)(struct os_audio_device *audio, const void *writeBuf, os_size_t size);
os_size_t (*receive)(struct os_audio_device *audio, void *readBuf, os_size_t size);
};函数指针详解(源码无注释,下表补充说明,不属于源码):
| 函数指针 | 说明 |
|---|---|
getcaps | 获取音频设备能力(支持的采样率/声道/位宽等) |
configure | 配置音频参数(采样率、声道、位宽等) |
init | 初始化音频硬件(I2S/DMA 配置) |
deinit | 去初始化音频硬件 |
start | 启动音频播放/录音 |
stop | 停止音频播放/录音 |
transmit | 硬件发送一帧音频数据(HAL 层启动 DMA,DMA 完成后调 os_audio_tx_complete) |
receive | 硬件接收一帧音频数据(HAL 层实现) |
2.3 音频配置结构体 struct os_audio_configure
来源:drivers/audio/audio.h
struct os_audio_configure
{
uint32_t samplerate;
uint16_t channels;
uint16_t samplebits;
uint32_t recorde_fifo_size;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
samplerate | 采样率(Hz),如 44100 |
channels | 声道数:1=单声道,2=立体声 |
samplebits | 采样位宽(bit),如 16 |
recorde_fifo_size | 录音 FIFO 大小(默认 OS_AUDIO_RECORD_FIFO_SIZE=3528) |
注意:
recorde_fifo_size是源码的原始拼写(recorde不是record),使用时应保持一致,不要"改正"。
2.4 音频能力结构体 struct os_audio_caps
来源:drivers/audio/audio.h
struct os_audio_caps
{
int config_type;
union
{
int value;
struct os_audio_configure config;
} udata;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
config_type | 配置类型:AUDIO_PARAM_CMD(0) 或 AUDIO_VOLUME_CMD(1) |
udata.value | 当 config_type = AUDIO_VOLUME_CMD 时:音量值(0~AUDIO_VOLUME_MAX=100) |
udata.config | 当 config_type = AUDIO_PARAM_CMD 时:完整音频参数配置 |
配置类型宏(drivers/audio/audio.h,与项目逐字一致):
#define AUDIO_PARAM_CMD 0 /* set all params */
#define AUDIO_VOLUME_CMD 1 /* set volume */| 宏定义 | 值 | 说明 |
|---|---|---|
AUDIO_PARAM_CMD | 0 | 设置全部参数(采样率+声道+位宽) |
AUDIO_VOLUME_CMD | 1 | 仅设置音量(用 udata.value) |
2.5 音频播放器控制块 struct os_audio_replay
来源:drivers/audio/audio.h
struct os_audio_replay
{
os_mempool_dummy_t mp_dummy;
os_mempool_id mp;
uint8_t *write_data;
void *start_addr;
os_bool_t activated;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
mp_dummy | 内存池虚拟控制块(os_mempool_init 需要) |
mp | 内存池句柄(os_mempool_init 创建,用于分配/归还音频块) |
write_data | 当前待播放的数据指针(os_mempool_alloc 得到) |
start_addr | 内存池起始地址(os_calloc 分配,注销时释放) |
activated | 是否已激活播放(避免重复调用 ops->start) |
播放器使用 内存池(mempool) 作为音频数据缓冲队列:
- 内存池块大小由
OS_AUDIO_REPLAY_MP_BLOCK_SIZE定义(默认 1024) - 块数量由
OS_AUDIO_REPLAY_MP_BLOCK_COUNT定义(默认 8) - 内存池总大小 =
OS_MEMPOOL_SIZE(BLOCK_COUNT, BLOCK_SIZE) activated标志避免重复调用start
2.6 音频缓冲信息 struct os_audio_buf_info
来源:drivers/audio/audio.h
struct os_audio_buf_info
{
uint8_t *buffer;
uint16_t block_size;
uint16_t block_count;
uint32_t total_size;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
buffer | 缓冲区指针 |
block_size | 每个数据块大小 |
block_count | 数据块数量 |
total_size | 总缓冲区大小(= block_size * block_count) |
2.7 采样率宏定义(位掩码)
来源:drivers/audio/audio.h,与项目逐字一致:
#define AUDIO_SAMP_RATE_8K 0x0001
#define AUDIO_SAMP_RATE_11K 0x0002
#define AUDIO_SAMP_RATE_16K 0x0004
#define AUDIO_SAMP_RATE_22K 0x0008
#define AUDIO_SAMP_RATE_32K 0x0010
#define AUDIO_SAMP_RATE_44K 0x0020
#define AUDIO_SAMP_RATE_48K 0x0040
#define AUDIO_SAMP_RATE_96K 0x0080
#define AUDIO_SAMP_RATE_128K 0x0100
#define AUDIO_SAMP_RATE_160K 0x0200
#define AUDIO_SAMP_RATE_172K 0x0400
#define AUDIO_SAMP_RATE_192K 0x0800| 宏定义 | 值 | 说明 |
|---|---|---|
AUDIO_SAMP_RATE_8K | 0x0001(bit0) | 8 kHz |
AUDIO_SAMP_RATE_11K | 0x0002(bit1) | 11.025 kHz |
AUDIO_SAMP_RATE_16K | 0x0004(bit2) | 16 kHz |
AUDIO_SAMP_RATE_22K | 0x0008(bit3) | 22.05 kHz |
AUDIO_SAMP_RATE_32K | 0x0010(bit4) | 32 kHz |
AUDIO_SAMP_RATE_44K | 0x0020(bit5) | 44.1 kHz |
AUDIO_SAMP_RATE_48K | 0x0040(bit6) | 48 kHz |
AUDIO_SAMP_RATE_96K | 0x0080(bit7) | 96 kHz |
AUDIO_SAMP_RATE_128K | 0x0100(bit8) | 128 kHz |
AUDIO_SAMP_RATE_160K | 0x0200(bit9) | 160 kHz |
AUDIO_SAMP_RATE_172K | 0x0400(bit10) | 172 kHz |
AUDIO_SAMP_RATE_192K | 0x0800(bit11) | 192 kHz |
关键理解 —— 采样率宏是位掩码,用于声明"支持哪些采样率":这些宏的值是
1<<n(bit0~bit11),常用于getcaps回调里以位与方式判断硬件是否支持某采样率。例如if (caps->udata.config.samplerate & AUDIO_SAMP_RATE_44K)判断是否支持 44.1kHz。注意:这里存的是"采样率标志位",不是采样率的数值本身——配置时caps.udata.config.samplerate = 44100用的是实际数值,而getcaps里用位掩码,两者含义不同。
三、I2S 硬件抽象层
3.1 I2S 设备结构体 struct os_i2s_device
来源:drivers/audio/i2s.h
struct os_i2s_device
{
os_device_t parent;
const struct os_i2s_ops *ops;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
parent | 首成员继承 os_device_t(os_i2s_register 里 os_device_register(&i2s->parent, name)) |
ops | I2S 操作函数表(init/transimit/receive/enable_tx/enable_rx/set_frq/set_channel/set_bits) |
3.2 I2S 操作函数表 struct os_i2s_ops
来源:drivers/audio/i2s.h
struct os_i2s_ops
{
os_err_t (*init)(struct os_i2s_device *i2s);
os_err_t (*transimit)(struct os_i2s_device *i2s, uint8_t *pData, uint32_t Size);
os_err_t (*receive)(struct os_i2s_device *i2s, uint8_t *pData, uint32_t Size);
os_err_t (*enable_tx)(struct os_i2s_device *i2s, os_bool_t enable);
os_err_t (*enable_rx)(struct os_i2s_device *i2s, os_bool_t enable);
os_err_t (*set_frq)(struct os_i2s_device *i2s, uint32_t frequency);
os_err_t (*set_channel)(struct os_i2s_device *i2s, uint8_t channels);
os_err_t (*set_bits)(struct os_i2s_device *i2s, uint8_t bits);
};函数指针详解(源码无注释,下表补充说明,不属于源码):
| 函数指针 | 说明 |
|---|---|
init | 初始化 I2S 外设(时钟/引脚/DMA) |
transimit | 启动 DMA 发送(注意源码拼写是 transimit,不是 transmit) |
receive | 启动 DMA 接收 |
enable_tx | 使能/禁用 TX |
enable_rx | 使能/禁用 RX |
set_frq | 设置采样频率 |
set_channel | 设置声道数 |
set_bits | 设置采样位宽 |
注意:
transimit是源码的原始拼写(应为transmit),使用时应保持一致,不要"改正"。
I2S 缓冲信息结构 os_i2s_buf_info(drivers/audio/i2s.h,与项目逐字一致):
struct os_i2s_buf_info
{
uint8_t *buffer;
uint16_t block_size;
uint16_t block_count;
uint32_t total_size;
};3.3 I2S 控制命令与播放事件
I2S 控制命令(drivers/audio/i2s.h,与项目逐字一致):
#define OS_AUDIO_CMD_TX_ENABLE IOC_SOUND(0x00)
#define OS_AUDIO_CMD_RX_ENABLE IOC_SOUND(0x01)
#define OS_AUDIO_CMD_SET_FRQ IOC_SOUND(0x02)
#define OS_AUDIO_CMD_SET_CHANNEL IOC_SOUND(0x03)
#define OS_AUDIO_CMD_SET_BITS IOC_SOUND(0x04)
#define OS_AUDIO_CMD_TX_DISABLE IOC_SOUND(0x05)
#define OS_AUDIO_CMD_RX_DISABLE IOC_SOUND(0x06)| 命令 | 参数 | 说明 |
|---|---|---|
OS_AUDIO_CMD_TX_ENABLE | IOC_SOUND(0x00) | 使能 TX |
OS_AUDIO_CMD_RX_ENABLE | IOC_SOUND(0x01) | 使能 RX |
OS_AUDIO_CMD_SET_FRQ | IOC_SOUND(0x02) | 设置频率 |
OS_AUDIO_CMD_SET_CHANNEL | IOC_SOUND(0x03) | 设置声道 |
OS_AUDIO_CMD_SET_BITS | IOC_SOUND(0x04) | 设置位宽 |
OS_AUDIO_CMD_TX_DISABLE | IOC_SOUND(0x05) | 禁用 TX |
OS_AUDIO_CMD_RX_DISABLE | IOC_SOUND(0x06) | 禁用 RX |
播放事件枚举(drivers/audio/i2s.h,与项目逐字一致):
enum
{
I2S_REPLAY_EVT_NONE = 0x00,
I2S_REPLAY_EVT_START = 0x01,
I2S_REPLAY_EVT_STOP = 0x02,
};| 事件 | 值 | 含义 |
|---|---|---|
I2S_REPLAY_EVT_NONE | 0x00 | 无事件 |
I2S_REPLAY_EVT_START | 0x01 | 开始播放 |
I2S_REPLAY_EVT_STOP | 0x02 | 停止播放 |
四、关键 API
4.1 音频播放器注册
os_err_t os_audio_player_register(struct os_audio_device *audio, const char *name, void *data);- audio: Audio 设备控制块指针
- name: 设备名称(如
"sound0") - data: 用户私有数据
- 返回:
OS_SUCCESS注册成功
注册后设备类型为 OS_DEVICE_TYPE_SOUND,可通过 os_device_open("sound0") 打开使用。
4.2 音频录音器注册
os_err_t os_audio_recorder_register(struct os_audio_device *audio, const char *name, void *data);| 参数 | 说明 |
|---|---|
audio | Audio 设备控制块(含 ops) |
name | 设备名称(如 "sound0") |
data | 用户私有数据 |
| 返回 | OS_SUCCESS 注册成功 |
与播放器注册类似,但绑定的是录音操作(
os_audio_recorder_ops)。同一块硬件通常通过一个os_audio_device同时注册播放器和录音器(名字可不同)。
4.3 TX 发送完成通知
os_err_t os_audio_tx_complete(struct os_audio_device *audio);| 参数 | 说明 |
|---|---|
audio | 播放器所在 Audio 设备 |
由 HAL 层在 DMA 发送完成中断中调用,通知框架一个数据块已播放完毕(触发 _audio_player_callback 归还缓冲区、tx_count--,见 6.2)。
4.4 RX 接收完成通知
void os_audio_rx_done(struct os_audio_device *audio, uint8_t *pbuf, os_size_t len);| 参数 | 说明 |
|---|---|
audio | 录音器所在 Audio 设备 |
pbuf | 收到的音频数据 |
len | 数据长度 |
由 HAL 层在 DMA 接收完成中断中调用,通知框架已接收到音频数据。
4.5 I2S 设备注册
os_err_t os_i2s_register(struct os_i2s_device *i2s, const char *name, void *data);| 参数 | 说明 |
|---|---|
i2s | I2S 设备控制块(含 ops) |
name | 设备名称(如 "i2s0") |
data | 用户私有数据 |
| 返回 | OS_SUCCESS 注册成功 |
注册后可用
os_device_open("i2s0")打开,再通过os_device_control+OS_AUDIO_CMD_*命令配置频率/声道/位宽、使能 TX/RX(见 3.3)。
4.6 I2S 中断处理
void os_hw_i2s_isr(struct os_i2s_device *i2s, struct os_device_cb_info *info);| 参数 | 说明 |
|---|---|
i2s | I2S 设备 |
info | 设备回调信息(DMA 完成/错误等事件) |
I2S 中断服务例程,由 HAL 层在 I2S DMA 中断中调用,用于触发 TX/RX 完成回调。
五、使用示例
5.1 音频播放器注册示例
#include <audio.h>
#include <device.h>
/* 音频播放器 HAL 层实现 */
static os_err_t my_audio_init(struct os_audio_device *audio)
{
os_kprintf("audio hardware init\r\n");
return OS_SUCCESS;
}
static os_err_t my_audio_start(struct os_audio_device *audio)
{
os_kprintf("audio playback start\r\n");
return OS_SUCCESS;
}
static os_err_t my_audio_stop(struct os_audio_device *audio)
{
os_kprintf("audio playback stop\r\n");
return OS_SUCCESS;
}
static os_size_t my_audio_transmit(struct os_audio_device *audio, const void *buf, os_size_t size)
{
/* 启动 DMA 传输一帧音频数据 */
/* 在 DMA 完成中断中调用 os_audio_tx_complete(audio) */
return size;
}
static const struct os_audio_ops my_audio_ops =
{
.init = my_audio_init,
.start = my_audio_start,
.stop = my_audio_stop,
.transmit = my_audio_transmit,
};
/* 注册音频播放器 */
void audio_player_init(void)
{
static struct os_audio_device audio_dev;
os_err_t ret;
audio_dev.ops = &my_audio_ops;
ret = os_audio_player_register(&audio_dev, "sound0", OS_NULL);
if (ret != OS_SUCCESS)
{
os_kprintf("audio player register failed, ret=%d\r\n", ret);
return;
}
}5.2 播放 PCM 音频数据
#include <audio.h>
#include <device.h>
#include <os_task.h>
#define AUDIO_BUF_SIZE (4096)
void audio_playback_task(void *arg)
{
os_device_t *audio_dev;
uint8_t *pcm_data;
os_size_t len;
/* 1. 打开音频设备 */
audio_dev = os_device_open("sound0");
if (audio_dev == OS_NULL)
{
os_kprintf("open sound0 failed!\r\n");
return;
}
/* 2. 配置音频参数:44.1kHz, 立体声, 16bit */
struct os_audio_caps caps = {
.config_type = AUDIO_PARAM_CMD,
.udata.config.samplerate = 44100,
.udata.config.channels = 2,
.udata.config.samplebits = 16,
};
if (os_device_control(audio_dev, AUDIO_CTL_CONFIGURE, &caps) != OS_SUCCESS)
{
os_kprintf("audio configure failed\r\n");
os_device_close(audio_dev);
return;
}
/* 3. 循环播放 PCM 数据 */
while (1)
{
os_size_t written;
/* 从文件或网络获取 PCM 数据 */
pcm_data = get_pcm_data(&len);
if (pcm_data == OS_NULL)
break;
/* 写入音频数据(返回实际写入字节数,可能 < len 表示缓冲满,需重试) */
written = os_device_write(audio_dev, 0, pcm_data, len);
if (written < len)
{
/* 背压:缓冲区满,等待 TX 回调归还后继续写 */
os_kprintf("write partial %d/%d, retry\r\n", (int)written, (int)len);
}
}
}5.3 音量控制示例
void audio_set_volume(os_device_t *audio_dev, int volume)
{
struct os_audio_caps caps;
/* 限制音量范围 */
if (volume < AUDIO_VOLUME_MIN)
volume = AUDIO_VOLUME_MIN;
if (volume > AUDIO_VOLUME_MAX)
volume = AUDIO_VOLUME_MAX;
caps.config_type = AUDIO_VOLUME_CMD;
caps.udata.value = volume;
if (os_device_control(audio_dev, AUDIO_CTL_CONFIGURE, &caps) != OS_SUCCESS)
{
os_kprintf("audio set volume failed\r\n");
return;
}
os_kprintf("audio volume set to %d\r\n", volume);
}5.4 I2S 设备注册示例
#include <audio/i2s.h>
#include <device.h>
/* I2S HAL 层实现 */
static os_err_t my_i2s_init(struct os_i2s_device *i2s)
{
/* 初始化 I2S 外设、DMA、GPIO 等 */
return OS_SUCCESS;
}
static os_err_t my_i2s_transmit(struct os_i2s_device *i2s, uint8_t *pData, uint32_t Size)
{
/* 启动 DMA 发送 */
return OS_SUCCESS;
}
static const struct os_i2s_ops my_i2s_ops =
{
.init = my_i2s_init,
.transimit = my_i2s_transmit,
};
void i2s_device_init(void)
{
static struct os_i2s_device i2s_dev;
os_err_t ret;
i2s_dev.ops = &my_i2s_ops;
ret = os_i2s_register(&i2s_dev, "i2s0", OS_NULL);
if (ret != OS_SUCCESS)
{
os_kprintf("i2s register failed, ret=%d\r\n", ret);
return;
}
}六、内部实现机制
6.1 内存池缓冲队列(生产者-消费者模型)
音频播放器使用内存池实现生产者-消费者模型:
应用层(生产者) HAL 层(消费者)
┌──────────────┐ ┌──────────────┐
│ os_device_ │ 写入数据块 │ DMA 传输 │
│ write() │ ──────────────→ │ 完成中断 │
│ │ │ │
│ 从 mempool │ │ 归还到 │
│ 分配空闲块 │ │ mempool │
└──────────────┘ └──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ os_mempool 缓冲池 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ blk0 │ │ blk1 │ │ blk2 │ │ ... │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ 空闲块 ← 分配 → 已用块 ← 归还 → 空闲块 │
└─────────────────────────────────────────────┘背压控制:通过 tx_count 和 tx_size 实现流控:
tx_count:当前已占用缓冲区数量tx_size:总缓冲区数量(=OS_AUDIO_REPLAY_MP_BLOCK_COUNT)- 当
tx_count == tx_size时,_audio_player_write直接return count(返回已写入的字节数),不再继续分配——这就是背压:应用看到返回值< 请求长度就知道缓冲满了,需等_audio_player_callback归还后继续写。注意:不是阻塞等待,是"写不满就返回已写的量"。
_audio_player_write 背压核心源码(drivers/audio/audio_player.c,与项目逐字一致):
while (count < size)
{
os_spin_lock_irqsave(&gs_device_lock, &level);
if (dev->tx_count == dev->tx_size)
{
os_spin_unlock_irqrestore(&gs_device_lock, level);
return count;
}
dev->tx_count++;
os_spin_unlock_irqrestore(&gs_device_lock, level);
audio->replay->write_data = os_mempool_alloc(audio->replay->mp, OS_WAIT_FOREVER);
if (size - count > OS_AUDIO_REPLAY_MP_BLOCK_SIZE)
{
memcpy(audio->replay->write_data, buff + count, OS_AUDIO_REPLAY_MP_BLOCK_SIZE);
count += OS_AUDIO_REPLAY_MP_BLOCK_SIZE;
}
else
{
memcpy(audio->replay->write_data, buff + count, size - count);
count += size - count;
}
if (audio->replay->activated != OS_TRUE)
{
if (audio->ops->start)
audio->ops->start(audio);
audio->replay->activated = OS_TRUE;
}
if (audio->ops->transmit)
{
audio->ops->transmit(audio, audio->replay->write_data, OS_AUDIO_REPLAY_MP_BLOCK_SIZE);
}
}分步说明:分配块后把数据拷贝进块(按块大小切分),首次写入时 ops->start 启动播放,然后 ops->transmit 启动 DMA 发送这块。最后 tx_count 由 DMA 完成回调减回(见 6.2)。
背压数值演算(tx_size = 8,缓冲全满时):
| 场景 | tx_count | tx_size | _audio_player_write 行为 |
|---|---|---|---|
| 有空闲块 | < 8 | 8 | tx_count++,继续分配块、拷贝、transmit |
| 缓冲已满 | 8 | 8 | if (tx_count == tx_size) 成立 → 返回已写入 count |
关键理解:
tx_count在_audio_player_write分配块时++,在_audio_player_callback(DMA 播放完)时--。两者在gs_device_lock自旋锁保护下增减,形成生产者(应用写)—消费者(DMA 播完归还)的计数平衡,实现背压。
6.2 TX 完成回调流程
1. 应用层调用 os_device_write() 写入 PCM 数据
2. 框架从 mempool 分配空闲块,拷贝数据
3. 调用 ops->transmit() 启动 DMA 传输
4. DMA 传输完成 → 硬件中断 → HAL 层调用 os_audio_tx_complete()
5. 框架通过 TX 回调 _audio_player_callback() 归还缓冲区到 mempool
6. tx_count--,应用层下一轮 os_device_write() 可继续写入回调核心源码 _audio_player_callback(drivers/audio/audio_player.c,与项目逐字一致):
os_err_t _audio_player_callback(os_device_t *dev, struct os_device_cb_info *info)
{
OS_ASSERT(dev != OS_NULL);
OS_ASSERT(info != OS_NULL);
struct os_audio_device *audio_dev = (struct os_audio_device *)dev;
os_mempool_free(audio_dev->replay->mp, info->data);
dev->tx_count--;
return OS_SUCCESS;
}分步详解:把刚播放完的缓冲区(info->data 指向内存池块)os_mempool_free 归还内存池,并把 dev->tx_count--(腾出一个空闲块额度)。这就是消费者归还环节,与 _audio_player_write 的 tx_count++ 配对。
关键理解 —— 回调如何被触发:
_audio_player_init里用os_device_control(dev, OS_DEVICE_CTRL_SET_CB, &cb_info)注册了OS_DEVICE_CB_TYPE_TX回调_audio_player_callback。HAL 层 DMA 播放完一块后调os_audio_tx_complete(audio),框架内部触发该 TX 回调(info->data= 刚播完的块)。
6.3 音频播放器初始化流程
os_audio_player_register()
│
├── 设置 device->type = OS_DEVICE_TYPE_SOUND
├── 设置 device->ops = &audio_ops
├── os_device_register()
│
└── 用户首次 open() 时触发 _audio_player_init():
├── 分配 os_audio_replay 控制块
├── 创建 mempool(块大小 = OS_AUDIO_REPLAY_MP_BLOCK_SIZE)
├── 设置 tx_count = 0, tx_size = BLOCK_COUNT
├── 调用 ops->init() 初始化硬件
└── 注册 OS_DEVICE_CB_TYPE_TX 回调完整源码 _audio_player_init(drivers/audio/audio_player.c,与项目逐字一致):
static os_err_t _audio_player_init(struct os_device *dev)
{
os_err_t result = OS_SUCCESS;
os_size_t mp_size;
void *start_addr;
struct os_audio_device *audio;
OS_ASSERT(dev != OS_NULL);
audio = (struct os_audio_device *)dev;
/* initialize replay & record */
audio->replay = OS_NULL;
/* initialize replay */
struct os_audio_replay *replay = (struct os_audio_replay *)os_calloc(1, sizeof(struct os_audio_replay));
if (replay == OS_NULL)
return OS_NOMEM;
memset(replay, 0, sizeof(struct os_audio_replay));
/* init memory pool for replay */
mp_size = OS_MEMPOOL_SIZE(OS_AUDIO_REPLAY_MP_BLOCK_COUNT, OS_AUDIO_REPLAY_MP_BLOCK_SIZE);
start_addr = os_calloc(1, mp_size);
replay->mp = os_mempool_init(&replay->mp_dummy, "replay_pool", start_addr, mp_size, OS_AUDIO_REPLAY_MP_BLOCK_SIZE);
if (replay->mp == OS_NULL)
{
os_free(replay);
LOG_E(DBG_EXT_TAG, "create memory pool for repaly failed");
return OS_NOMEM;
}
dev->tx_count = 0;
dev->tx_size = OS_AUDIO_REPLAY_MP_BLOCK_COUNT;
replay->activated = OS_FALSE;
audio->replay = replay;
audio->replay->start_addr = start_addr;
if (audio->ops->init)
audio->ops->init(audio);
struct os_device_cb_info cb_info = {
.type = OS_DEVICE_CB_TYPE_TX,
.data = OS_NULL,
.cb = _audio_player_callback,
};
os_device_control(dev, OS_DEVICE_CTRL_SET_CB, &cb_info);
return result;
}分步详解:
第 1 步:分配播放器控制块(条件 分配失败则返回)
audio->replay = OS_NULL;
struct os_audio_replay *replay = (struct os_audio_replay *)os_calloc(1, sizeof(struct os_audio_replay));
if (replay == OS_NULL)
return OS_NOMEM;
memset(replay, 0, sizeof(struct os_audio_replay));详细解释:先把 audio->replay 置空,再 os_calloc 分配播放器控制块并清零。失败返回 OS_NOMEM。这是内存池队列的"载体"。
第 2 步:创建内存池(条件 创建失败则回滚)
mp_size = OS_MEMPOOL_SIZE(OS_AUDIO_REPLAY_MP_BLOCK_COUNT, OS_AUDIO_REPLAY_MP_BLOCK_SIZE);
start_addr = os_calloc(1, mp_size);
replay->mp = os_mempool_init(&replay->mp_dummy, "replay_pool", start_addr, mp_size, OS_AUDIO_REPLAY_MP_BLOCK_SIZE);
if (replay->mp == OS_NULL)
{
os_free(replay);
LOG_E(DBG_EXT_TAG, "create memory pool for repaly failed");
return OS_NOMEM;
}详细解释:关键:内存池总大小 mp_size = OS_MEMPOOL_SIZE(块数, 块大小),先 os_calloc 分配一整块内存给 start_addr,再 os_mempool_init 把它切成 BLOCK_COUNT 个 BLOCK_SIZE 块。失败则 os_free(replay) 回滚并返回 OS_NOMEM。
第 3 步:初始化计数与激活标志(无条件执行)
dev->tx_count = 0;
dev->tx_size = OS_AUDIO_REPLAY_MP_BLOCK_COUNT;
replay->activated = OS_FALSE;
audio->replay = replay;
audio->replay->start_addr = start_addr;详细解释:tx_count = 0(初始无占用)、tx_size = BLOCK_COUNT(背压上限);activated = OS_FALSE(尚未开始播放);audio->replay 绑定控制块、start_addr 记录内存池起始地址(供注销释放)。
第 4 步:初始化硬件 + 注册回调(无条件执行)
if (audio->ops->init)
audio->ops->init(audio);
struct os_device_cb_info cb_info = {
.type = OS_DEVICE_CB_TYPE_TX,
.data = OS_NULL,
.cb = _audio_player_callback,
};
os_device_control(dev, OS_DEVICE_CTRL_SET_CB, &cb_info);
return result;
}详细解释:调用 ops->init 初始化音频硬件(I2S/DMA)。然后注册 TX 回调 _audio_player_callback——DMA 播放完一块时框架经此回调归还缓冲区、tx_count--。
_audio_player_init() 条件走向总结:
_audio_player_init(dev) 条件走向总结:
├── 断言 dev 非空
├── 分配 os_audio_replay(os_calloc)
│ ├── 失败 → return OS_NOMEM
│ └── 成功 → memset 清零
├── 计算 mp_size = OS_MEMPOOL_SIZE(BLOCK_COUNT, BLOCK_SIZE)
├── os_calloc(mp_size) 分配内存池内存
├── os_mempool_init 创建内存池
│ ├── 失败 → os_free(replay) + return OS_NOMEM
│ └── 成功 ↓
├── dev->tx_count = 0;dev->tx_size = BLOCK_COUNT
├── replay->activated = OS_FALSE;audio->replay = replay
├── ops->init(audio) 初始化硬件
├── 注册 OS_DEVICE_CB_TYPE_TX 回调 _audio_player_callback
└── return OS_SUCCESS七、Audio 的适配(接入新音频编解码器)
7.1 先认识适配层:Audio 框架是怎么"长"到音频硬件上的
drivers/audio/ 下是跨平台统一的框架层(audio_player.c 播放器内存池队列、audio_recorder.c 录音器),不碰具体寄存器。真正操作音频硬件(I2S/编解码器/DMA)的是各厂商 HAL 驱动(如 ST drv_audio.h、博通 drv_audio_icodec.c)。换音频硬件,就是实现一套 os_audio_ops 并注册。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用层(os_device_write PCM / 录音) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Audio 框架层(drivers/audio/,通用不改) │
│ audio_player.c 内存池队列 + 背压 + TX 回调 │
│ audio_recorder.c 录音 │
└───────────────┬──────────────────────────┬────────────────┘
│ ①实现 os_audio_ops │ ②DMA 中断里
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 厂商音频驱动(drv_audio.c)★需实现★ │
│ init/deinit/start/stop/transmit/receive/configure/getcaps│
│ DMA 中断 → os_audio_tx_complete / os_audio_rx_done │
└───────────────┬──────────────────────────┬────────────────┘
│ HAL 库(I2S/DMA/CODEC) │ 中断服务
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(I2S 外设 + 音频编解码器 / DMA) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入新音频硬件只需做两件事:① 实现 struct os_audio_ops(init/start/stop/transmit 等);② 在 DMA 发送完成中断调 os_audio_tx_complete、接收完成调 os_audio_rx_done,然后 os_audio_player_register/os_audio_recorder_register 注册。框架层不用改。
7.2 以播放器为例,看一整套适配
① 实现 os_audio_ops(5.1 示例已展示,核心是 transmit + 中断回调):
static const struct os_audio_ops my_audio_ops =
{
.init = my_audio_init, /* 初始化 I2S/DMA/编解码器 */
.start = my_audio_start, /* 使能 TX、启动播放 */
.stop = my_audio_stop, /* 停止播放 */
.transmit = my_audio_transmit, /* 启动 DMA 发一帧(核心) */
.configure = my_audio_configure, /* 采样率/声道/位宽/音量 */
};② transmit 里启动 DMA,中断里调 os_audio_tx_complete:
static os_size_t my_audio_transmit(struct os_audio_device *audio, const void *buf, os_size_t size)
{
/* 启动 DMA 把 buf 的 size 字节送给 I2S/编解码器 */
HAL_I2S_Transmit_DMA(&hi2s, (uint8_t *)buf, size);
/* 在 HAL_I2S_TxCpltCallback(DMA 完成中断)里调: */
/* os_audio_tx_complete(audio); 归还这块缓冲、tx_count-- */
return size;
}要点:
transmit必须启动 DMA 传输后立即返回(返回 size),不能等播放完——真正的"播放完"由 DMA 完成中断通知,中断里调os_audio_tx_complete让框架归还缓冲。若transmit返回的 size 与实际不符,或漏调os_audio_tx_complete,缓冲不会归还、tx_count不减,播放会卡死。
③ 注册播放器/录音器(5.1 示例):os_audio_player_register(&audio_dev, "sound0", NULL)。
7.3 接口契约:接入音频硬件需要实现什么
| 层 | 接口/回调 | 职责 | 说明 |
|---|---|---|---|
| 操作回调 | ops.init | 初始化 I2S/DMA/编解码器 | 可空 |
| 操作回调 | ops.start | 启动播放/录音 | 可空 |
| 操作回调 | ops.stop | 停止 | 可空 |
| 操作回调 | ops.transmit | 启动 DMA 发一帧(核心,必实现) | 返回 size |
| 操作回调 | ops.receive | 启动 DMA 收一帧 | 录音必实现 |
| 操作回调 | ops.configure | 采样率/声道/位宽/音量 | 可空 |
| 中断上报 | os_audio_tx_complete | DMA 发完回调 | 必调 |
| 中断上报 | os_audio_rx_done | DMA 收完回调 | 录音必调 |
| 注册 | os_audio_player_register | 注册播放器 | —— |
| 注册 | os_audio_recorder_register | 注册录音器 | —— |
7.4 适配新音频硬件的标准流程
第 1 步:确认音频外设——I2S 外设、编解码器芯片、DMA 通道。
第 2 步:新建厂商驱动——实现 os_audio_ops(init/start/stop/transmit/configure,transmit 必实现)。
第 3 步:写 DMA 中断——发送完成调 os_audio_tx_complete,接收完成调 os_audio_rx_done。
第 4 步:注册——os_audio_player_register(播放)、os_audio_recorder_register(录音)。
第 5 步:测试——写一段 PCM 播放验证音质;os_device_control 切采样率/声道/音量验证 configure。
7.5 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
transmit 实现了且返回 size? | 未实现则播放无声;返回值错则背压错乱 |
DMA 完成调了 os_audio_tx_complete? | 漏调则缓冲不归还、tx_count 不减,播放卡死 |
configure 与 I2S 硬件匹配? | 采样率/位宽/声道不匹配则播放异常 |
init 里正确配置 DMA? | DMA 配置错则数据传不出去 |
| 播放器/录音器都注册了? | 需要哪个注册哪个,缺了对应功能不可用 |
📝 本节小结
核心要点回顾
本节围绕 Audio 设备展开,核心要点如下:
- 三层架构:应用层(PCM 数据写入)→ 播放器框架(内存池队列管理)→ I2S 硬件层(DMA 传输)。
- 核心数据结构:
os_audio_device继承自os_device_t,通过os_audio_ops操作函数表实现音频操作;replay/record分别管播放和录音。 - 内存池缓冲队列:使用
os_mempool实现生产者-消费者模型,通过tx_count/tx_size实现背压流控(tx_count == tx_size时write返回已写字节数,非阻塞)。 - TX 完成回调:DMA 播放完一块 →
os_audio_tx_complete→_audio_player_callback归还缓冲、tx_count--,形成生产者-消费者闭环。 - I2S 硬件抽象:
os_i2s_device独立管理 I2S 外设,支持 TX/RX 使能、频率/声道/位宽配置(OS_AUDIO_CMD_*命令 +I2S_REPLAY_EVT_*事件)。 - 采样率宏是位掩码:
AUDIO_SAMP_RATE_*(bit0~bit11)用于getcaps位与判断硬件支持能力,与configure里的实际数值(44100)含义不同。 - 硬件适配:接入新音频硬件 = 实现
os_audio_ops(transmit 必实现)+ DMA 中断调os_audio_tx_complete/os_audio_rx_done+ 注册,框架层不用改。
Audio 设备驱动框架非常适合需要音频播放或录音的嵌入式 IoT 设备,如智能音箱、语音助手、对讲设备等。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 采样率/位宽与 I2S 不匹配 | 应用配置的采样率、声道、位宽必须与 I2S 硬件配置一致,否则播放异常 |
| 忽略背压流控 | 写满 tx_size 后 write 返回已写字节数(非阻塞),应用需按返回值重试 |
漏调 os_audio_tx_complete | DMA 播放完必须调它归还缓冲,否则 tx_count 不减、播放卡死 |
| 误以为采样率宏是数值 | AUDIO_SAMP_RATE_* 是位掩码(getcaps 用),configure 里采样率是实际数值(44100) |
| 误改源码拼写 | transimit(I2S)、recorde_fifo_size(configure)是源码原始拼写,保持一致 |
| 在中断回调里搬数据 | 音频 DMA 中断只做计数/通知,数据搬运放到任务上下文 |
transmit 不返回 size | transmit 启动 DMA 后立即返回实际 size,否则背压判断错误 |