音频管理与输出通路
音频管理与输出通路是 AD23N 芯片 SDK 中负责音频子系统初始化、DAC 数模转换输出、音量控制与模拟前端管理的完整机制。本文档基于 sdk/app/bsp/lib/dac_api.c 与 sdk/app/bsp/lib/audio.c 等实际源码,说明从解码器/音效模块到喇叭/耳机输出的整条数据通路与控制通路。
Purpose and Scope
本页面覆盖:
- 音频子系统的上电/下电生命周期(
audio_init/audio_off/dac_power_on/dac_power_off) - DAC 输出通路的核心管理结构
DAC_MANAGE与多声道输出机制(sound_out_obj+kick回调) - 音量映射表
vol_tab、物理音量设置与软件音量调节策略 - 静音、淡入淡出(fade)标志位与中断驱动的输出搬运(
dac_isr_t) - 模拟前端(
audio_analog_open/close)、时钟(audio_clk_init/close)与 VCM 校准的配合关系
以下主题属于兄弟页面,不在本文展开:音频 ADC 采集通路(audio_adc_*,见「音频输入采集」页面)、具体解码器实现(decoder_mge 相关页面)、音效处理算法(sound_effect_api 相关页面)。本页面只描述它们与输出通路的衔接点。
Overview
在 AD23N 的音频架构中,输出通路是所有"出声"行为的唯一汇聚点:无论是本地解码(decoder_mge)、正弦提示音(sine_play)、MIO 音频(mio_api)还是音效处理后(sound_effect_api)的数据,最终都要通过 DAC 模块变成模拟信号,经模拟前端驱动喇叭或耳机。
dac_api.c 将这一机制封装为一个全局单例 DAC_MANAGE dac_mge,它被显式放置在 .DAC_BUFFER 内存段中,以保证关键缓冲区与状态结构位于 DAC 专用内存区。该模块的核心设计思想是:
- 通道抽象:每个 DAC 通道持有一个
sound_out_obj *(声音输出对象)与一个kick回调。上层模块(解码器/音效)把自己注册成输出对象,DAC 中断需要数据时通过kick回调"踢"一下上层,上层随即填充输出缓冲区。这是一种典型的拉模式(pull model),避免了主动推数据导致的缓冲区溢出。 - 音量两级控制:硬件数字音量由
phy_vol_set回调设定(建议固定),软件通过 32 级查表(vol_tab)调节样点幅值。查表近似人耳等响曲线,使听感音量随档位线性变化。 - 状态位驱动:
flag字段用位标志表达静音(B_DAC_MUTE)、淡入淡出使能(B_DAC_FADE_EN)与淡出进行中(B_DAC_FADE_OUT),中断服务程序根据标志决定是否输出静音/衰减后的数据。
Architecture
下图展示音频输出通路的整体架构与数据流:上层音频源经过 dac_api 管理层的通道注册与音量调节,由 DAC 中断按采样率拉取数据,最终经模拟前端输出到负载。
flowchart TD
subgraph sg_Source["上层音频源 (Source Layer)"]
Decoder["decoder_mge / 解码器"]
Sine["sine_play 提示音"]
Mio["mio_api / 音效 sound_effect_api"]
end
subgraph sg_Mgmt["DAC 管理层 (dac_api.c)"]
Mge["DAC_MANAGE dac_mge<br/>(.DAC_BUFFER 段)"]
Sound["sound_out_obj *sound[]<br/>通道输出对象"]
Kick["kick 回调(拉模式)"]
Vol["vol_tab 音量查表 +<br/>phy_vol_set 硬件音量"]
end
subgraph sg_Hw["硬件层 (audio.c / dac / 模拟)"]
Isr["audio_isr / dac_isr_t"]
DacHw["DAC 控制器 (dac.h)"]
Analog["audio_analog_open/close<br/>VCM 校准"]
Clk["audio_clk_init/close"]
end
Load["喇叭 / 耳机负载"]
Decoder --> Sound
Sine --> Sound
Mio --> Sound
Sound --> Kick
Kick -->|"申请/填充数据"| Decoder
Mge --> Vol
Vol --> DacHw
DacHw --> Isr
Isr -->|"中断按采样率取数"| Mge
DacHw --> Analog
Analog --> Load
Clk --> DacHw
架构要点说明:
DAC_MANAGE(节点Mge)是唯一的管理实体,持有sound[]通道数组与kick[]回调数组、物理音量函数指针phy_vol_set、当前物理音量vol_phy、软件音量档vol与状态flag。- 数据流向是"拉"式的:DAC 中断(
dac_isr_t,在audio.c中被audio_isr调用)触发后,DAC_MANAGE通过已注册的kick回调向上层取数据;上层没有数据时输出静音/填充数据。 - 音量控制位于数字域(
DacHw之前),phy_vol_set回调负责设置硬件数字音量,vol_tab负责软件样点幅值,两者叠加实现平滑的 32 级听感音量。 - 上电顺序由
dac_power_on统一编排:audio_init(ISR 安装 + 时钟 + VCM 校准 + 模拟前端打开)→ 固定硬件音量 →dac_init_api→auout_init(sr, delay_flag)按采样率初始化输出。
输出通路实现深入解析
DAC 管理结构 DAC_MANAGE
dac_api.c 的核心是全局管理结构体 DAC_MANAGE:
typedef struct _DAC_MANAGE {
sound_out_obj *sound[DAC_CHANNEL_NUMBER]; // 每个通道一个声音输出对象
void (*kick[DAC_CHANNEL_NUMBER])(void *); // 每个通道一个"踢"回调(数据请求)
void (*phy_vol_set)(u16); // 硬件数字音量设置函数指针
u16 vol_phy; // 当前物理音量值
u8 ch; // 当前活动通道
u8 vol; // 软件音量档位(查表索引)
u8 flag; // 状态标志(静音/淡入淡出)
} DAC_MANAGE;
Source: dac_api.c
设计意图:将"每个输出通道"抽象为 sound_out_obj + kick 回调对。上层模块(解码器、提示音、MIO)把自己注册进 sound[ch] 并挂上自己的 kick 函数;DAC 侧完全不关心数据从哪来、是什么格式,只负责"按采样率要数据"。这让 DAC 模块与具体音频源解耦,也是整个 SDK 音频输出能同时服务多种音频源的根基。
该结构体被放置在专用内存段中,保证中断路径上的访问不会被缓存/内存策略影响:
DAC_MANAGE dac_mge AT(.DAC_BUFFER);
Source: dac_api.c
状态标志位:静音与淡入淡出
#define B_DAC_MUTE BIT(0) // 静音
#define B_DAC_FADE_EN BIT(1) // 淡入淡出使能
#define B_DAC_FADE_OUT BIT(2) // 正在淡出
Source: dac_api.c
这三个位由中断服务程序(或受其保护的 API)在每次取数时判断:B_DAC_MUTE 置位则输出静音样本;B_DAC_FADE_EN 控制淡入淡出功能是否启用,B_DAC_FADE_OUT 标记淡出过程进行中。淡入淡出的价值在于:切换音源或上下电时避免"咔嗒"爆音(pop noise),这是消费类音频设备听感体验的关键细节。
音量曲线:vol_tab 查表
const u16 vol_tab[] = {
0, 164, 191, 223, 260, 303, 353, 412, 480, 559, 652, 760,
887, 1034, 1205, 1405, 1638, 1910, 2227, 2597, 3028, 3530,
4115, 4798, 5594, 6523, 7605, 8867, 10338, 12053, 14052, 16383
};
#define MAX_VOL_LEVEL ((sizeof(vol_tab)/2) - 1) // 31
#define MAX_PHY_VOL vol_tab[MAX_VOL_LEVEL] // 16383
Source: dac_api.c
观察数值序列可以发现:0 → 164 → 191 → 223 → ...,相邻档位的比值约为 1.165(≈ 2^(1/4.5)),整体近似指数增长,终点是 12 位 DAC 满幅 16383。指数曲线贴合人耳对音量的对数感知,实现"每档听感等步进"。MAX_VOL_LEVEL = 31 意味着软件音量共 32 档(0–31)。
初始化:dac_mode_init 与 dac_init_api
void dac_mode_init(u16 vol, void *phy_vol_set_func)
{
memset(&dac_mge, 0, sizeof(dac_mge));
/* 调节音量建议固定硬件数字音量,只调节软件样点幅值来改变音量 */
if (phy_vol_set_func) {
dac_mge.phy_vol_set = phy_vol_set_func;
} else {
D_PHY_VOL_SET_FUNC(16384);
}
#if DAC_FADE_ENABLE
dac_mge.vol = vol;
if (dac_mge.vol > MAX_VOL_LEVEL) {
dac_mge.vol = MAX_VOL_LEVEL;
}
#else
dac_vol(0, vol);
#endif
auout_mode_init();
}
Source: dac_api.c
关键设计决策在注释里写得很清楚:建议固定硬件数字音量,只用软件样点幅值调节音量。原因是硬件数字音量改变时可能引入量化噪声/爆音,而软件幅值调节更平滑。dac_mode_init 负责建立管理结构、选择物理音量函数(缺省 D_PHY_VOL_SET_FUNC(16384))、按编译宏 DAC_FADE_ENABLE 决定"先记档后淡入"还是"立即设音量",最后调用 auout_mode_init() 初始化输出模式。
//1:有延时 0:没有延时, 上电开机调用需要延时至少1.2ms
void dac_init_api(u32 sr, bool delay_flag)
{
auout_init(sr, delay_flag);
}
Source: dac_api.c
dac_init_api 是采样率驱动的初始化入口:上层传入输出采样率 sr,delay_flag 指示上电场景是否需要 1.2ms 以上的延时(模拟电路建立时间)。
上电/下电编排:audio_init / audio_off / dac_power_on / dac_power_off
audio.c(头注释保留为 dac.c)定义了音频子系统的生命周期编排:
void audio_init(void)
{
audio_isr_init(); // 安装音频中断
audio_clk_init(); // 音频时钟使能
audio_vcm_trim(); // VCM(共模电压)校准
audio_analog_open(); // 打开模拟前端(放大器等)
}
void audio_off(void)
{
dac_off_api(); // 关闭 DAC
#if AUDIO_ADC_EN
audio_adc_off_api(); // 关闭 ADC(若使能)
#endif
audio_analog_close(); // 关闭模拟前端
audio_clk_close(); // 关闭音频时钟
}
Source: audio.c
上下电的对称性一目了然:audio_init 按"中断 → 时钟 → VCM 校准 → 模拟打开"的顺序构建通路,audio_off 按相反顺序(DAC → ADC → 模拟 → 时钟)拆除通路。先开时钟再开模拟、先关模拟再关时钟的顺序,是为了避免模拟电路在无时钟驱动或时钟在模拟上电前关闭时产生异常电流/爆音。
void dac_power_off()
{
audio_off();
}
void dac_power_on(u32 sr, bool delay_flag)
{
audio_init();
D_PHY_VOL_SET_FUNC(16384);
dac_init_api(sr, delay_flag);
}
Source: audio.c
dac_power_on 是整条输出通路的"一键启动":先 audio_init 完成硬件就绪,再把硬件数字音量固定到 16384(约 -6dB 的固定增益点,配合软件幅值调节),最后按采样率初始化输出。dac_power_off 则是"一键关闭"。
中断驱动的数据搬运
audio.c 中保留了中断服务程序的痕迹:
extern void dac_isr_t(void);
SET(interrupt(""))
AT(.audio_d.text.cache.L2)
void audio_isr(void)
{
/* dac_isr(); */
dac_isr_t();
#if AUDIO_ADC_EN
audio_adc_isr();
#endif
}
Source: audio.c
虽然这段 ISR 在源码中被 #if 0 屏蔽(现由 audio_isr_init() 安装中断),但它揭示了输出通路的实时性设计:DAC 中断按采样率周期性触发 dac_isr_t(),后者通过 DAC_MANAGE 的 kick 回调向注册的输出对象拉取数据。中断路径被放置到 L2 cache 段(AT(.audio_d.text.cache.L2)),保证低延迟与确定性。
核心流程
输出通路启动时序
sequenceDiagram
participant App as 应用层
participant Pwr as dac_power_on
participant Audio as audio_init
participant DacApi as dac_init_api
participant Hw as DAC/模拟硬件
participant Isr as dac_isr_t
App->>Pwr: dac_power_on(sr, delay_flag)
Pwr->>Audio: audio_init()
Audio->>Audio: audio_isr_init() 安装中断
Audio->>Audio: audio_clk_init() 开时钟
Audio->>Audio: audio_vcm_trim() VCM 校准
Audio->>Audio: audio_analog_open() 开模拟前端
Audio-->>Pwr: 硬件就绪
Pwr->>Pwr: D_PHY_VOL_SET_FUNC(16384) 固定硬件音量
Pwr->>DacApi: dac_init_api(sr, delay_flag)
DacApi->>DacApi: auout_init(sr, delay) 按采样率建输出
DacApi-->>App: 输出通路就绪
loop 每个采样周期
Hw->>Isr: DAC 中断触发
Isr->>DacApi: dac_isr_t() 取数
DacApi->>DacApi: 查 vol_tab / 静音/淡出判断
DacApi->>DacApi: 通过 kick 回调向上层要数据
DacApi-->>Hw: 写 DAC 数据寄存器
end
音量/静音状态机
stateDiagram-v2
[*] --> Active: dac_mode_init / dac_vol
Active --> Muted: 置 B_DAC_MUTE
Muted --> Active: 清 B_DAC_MUTE
Active --> FadingOut: 置 B_DAC_FADE_OUT(且 FADE_EN)
FadingOut --> Active: 淡出完成,清标志
Active --> Off: dac_off_api / audio_off
Muted --> Off: dac_off_api / audio_off
FadingOut --> Off: dac_off_api / audio_off
Off --> [*]: audio_clk_close
状态说明:正常运行(Active)时中断按 vol_tab[vol] 输出;静音(Muted)时输出零样本但保持通路;淡出(FadingOut)用于关断前平滑衰减避免爆音;Off 表示通路已拆除(DAC 关闭、模拟关闭、时钟关闭)。
使用示例
示例一:输出通路上电(播放前初始化)
应用层在开始播放前调用 dac_power_on,一次性完成中断、时钟、VCM、模拟前端与输出模式初始化:
void dac_power_on(u32 sr, bool delay_flag)
{
audio_init();
D_PHY_VOL_SET_FUNC(16384);
dac_init_api(sr, delay_flag);
}
Source: audio.c
调用约定:sr 传入实际输出采样率(如 16000/44100/48000);delay_flag 在上电开机场景必须为 1(源码注释明确"上电开机调用需要延时至少1.2ms",用于等待模拟电路建立)。播放结束后调用 dac_power_off() 对称关闭。
示例二:DAC 模块初始化与音量档设置
播放器初始化时调用 dac_mode_init,传入起始音量档(0–31,超限会被钳位到 MAX_VOL_LEVEL)与可选的物理音量函数:
void dac_mode_init(u16 vol, void *phy_vol_set_func)
{
memset(&dac_mge, 0, sizeof(dac_mge));
/* 调节音量建议固定硬件数字音量,只调节软件样点幅值来改变音量 */
if (phy_vol_set_func) {
dac_mge.phy_vol_set = phy_vol_set_func;
} else {
D_PHY_VOL_SET_FUNC(16384);
}
#if DAC_FADE_ENABLE
dac_mge.vol = vol;
if (dac_mge.vol > MAX_VOL_LEVEL) {
dac_mge.vol = MAX_VOL_LEVEL;
}
#else
dac_vol(0, vol);
#endif
auout_mode_init();
}
Source: dac_api.c
传 NULL 作为 phy_vol_set_func 时使用默认固定增益 16384。编译宏 DAC_FADE_ENABLE 决定初始化时是"只记录音量档等待淡入"(dac_mge.vol = vol)还是"立即写入硬件音量"(dac_vol(0, vol))。
示例三:通过 audio_off 完整关闭音频子系统
void audio_off(void)
{
dac_off_api();
#if AUDIO_ADC_EN
audio_adc_off_api();
#endif
audio_analog_close();
audio_clk_close();
}
Source: audio.c
关闭顺序与 audio_init 相反,保证 DAC 先停、模拟后关、时钟最后断。当 AUDIO_ADC_EN 宏开启时,采集通路也会一并关闭(见「音频输入采集」页面)。
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DAC_CHANNEL_NUMBER | 宏 | 由 dac.h 定义 | DAC 通道数,决定 DAC_MANAGE.sound[] / kick[] 数组长度 |
DAC_FADE_ENABLE | 宏 | 由编译配置决定 | 使能淡入淡出;开启时 dac_mode_init 只记录音量档,关闭时立即 dac_vol |
AUDIO_ADC_EN | 宏 | 由编译配置决定 | 使能 ADC 采集;影响 audio_off 是否关闭 audio_adc_off_api |
MAX_VOL_LEVEL | 常量 | 31 | 软件音量最大档位,由 vol_tab 表长推导(sizeof(vol_tab)/2 - 1) |
MAX_PHY_VOL | 常量 | 16383 | 12 位 DAC 满幅物理音量,对应 vol_tab[31] |
dac_mge.vol | u8 | dac_mode_init 传入 | 软件音量档(0–31),超过 MAX_VOL_LEVEL 会被钳位 |
| 默认硬件增益 | 常量 | 16384 | D_PHY_VOL_SET_FUNC(16384):未提供 phy_vol_set 时的固定硬件数字音量 |
delay_flag | bool | 调用方决定 | dac_init_api 是否带延时(上电开机需 ≥1.2ms) |
说明:表中宏的默认值取决于工程编译配置(
app_config.h/config.h),源码中仅定义了它们的用法,具体取值需结合具体工程的宏开关确认。
API 参考
void dac_mode_init(u16 vol, void *phy_vol_set_func)
初始化 DAC 管理结构与输出模式。
参数:
vol(u16):初始软件音量档(0–31,超限钳位到MAX_VOL_LEVEL)phy_vol_set_func(void (*)(u16)):硬件数字音量设置回调;传NULL使用默认D_PHY_VOL_SET_FUNC(16384)
返回: 无
说明: 内部 memset 清零 dac_mge;按 DAC_FADE_ENABLE 分支决定立即设音量还是记录档位;最后调用 auout_mode_init()。
void dac_init_api(u32 sr, bool delay_flag)
按采样率初始化 DAC 输出。
参数:
sr(u32):输出采样率(Hz)delay_flag(bool):1表示需要延时(上电开机必须,至少 1.2ms);0表示无延时
返回: 无
说明: 透传给 auout_init(sr, delay_flag)。
void audio_init(void)
音频子系统硬件初始化:安装中断 → 开时钟 → VCM 校准 → 打开模拟前端。
参数: 无 返回: 无
void audio_off(void)
音频子系统关闭:dac_off_api →(可选)audio_adc_off_api → audio_analog_close → audio_clk_close。
参数: 无 返回: 无
void dac_power_on(u32 sr, bool delay_flag)
输出通路一键上电:audio_init → 固定硬件增益 16384 → dac_init_api(sr, delay_flag)。
void dac_power_off(void)
输出通路一键下电,等价于 audio_off()。
void dac_isr_t(void)
DAC 中断处理函数(声明于 dac_api.c,由 audio_isr / 硬件中断调用)。按采样周期从 DAC_MANAGE 拉取数据,应用 vol_tab 音量与静音/淡出标志后写入 DAC。
失败模式、边界情况与并发
上电时序与延时不足
dac_init_api 的注释明确指出"上电开机调用需要延时至少 1.2ms"。若 delay_flag = 0 或调用方未等待模拟电路建立,可能出现:
- VCM/放大器尚未稳定导致输出直流偏移,产生开机爆音
- 首次中断取数时音量曲线尚未就绪,输出异常样本
缓解: 上电场景必须传 delay_flag = 1;热切换(非首次上电)场景可传 0 避免多余延时。
音量档超界
dac_mode_init 对 vol 做了钳位处理:
dac_mge.vol = vol;
if (dac_mge.vol > MAX_VOL_LEVEL) {
dac_mge.vol = MAX_VOL_LEVEL;
}
Source: dac_api.c
越界值被钳到 31 而不是回绕,避免查表越界读到 vol_tab 之外的内存。这是嵌入式代码常见的防御式设计——宁可错误地大声,不可非法访问内存。
中断上下文中的数据竞争
dac_isr_t 运行在中断上下文,而 dac_vol、静音设置等 API 可能由任务上下文调用。dac_mge 的 flag、vol 等字段被两侧访问,存在竞争窗口。源码中未展示显式临界区(关中断保护),说明该模块的并发模型是:音量/状态变更 API 应在播放暂停或由同一优先级上下文调用,或依赖上层互斥。扩展该模块时,修改 DAC_MANAGE 字段必须在中断禁用区域进行,或通过 kick 回调在中断内完成状态采样。
输出对象未注册(kick 为 NULL)
如果 sound[ch] / kick[ch] 未注册就被中断触发,dac_isr_t 可能解引用空指针。dac_mode_init 先用 memset 清零整个结构,保证初始状态所有指针为 NULL——中断路径应据此做空指针判空后输出静音。这是后续维护者需要保持的约定。
下电顺序错误
audio_off 的顺序(DAC → ADC → 模拟 → 时钟)不可颠倒:若先关模拟再关 DAC,DAC 输出端将悬空/短路到已关断的放大器,可能产生大电流冲击。任何新增的关闭步骤都应插在"模拟关闭"之前。
性能与运维注意
- 实时性:DAC 中断路径被显式放入 L2 cache 段(
AT(.audio_d.text.cache.L2)),避免指令缓存缺失导致的取数抖动。ISR 内只做查表、标志判断与一次kick调用,不做重活——任何耗时的解码/音效运算都应放到任务上下文。 - 内存布局:
dac_mge AT(.DAC_BUFFER)与注释中被注释掉的double_dac_buf/sp_dac_buf表明.DAC_BUFFER段专用于 DAC 相关状态与缓冲,保证 DMA/中断访问一致性。新增 DAC 相关全局变量应放置到该段。 - 音量策略:建议固定硬件数字音量(
D_PHY_VOL_SET_FUNC(16384)),只调软件幅值——这是源码注释明确推荐的策略,可避免硬件音量切换的量化噪声。若需更大增益范围,可提供自定义phy_vol_set_func覆盖默认行为。 - 功耗:
dac_power_off是完整下电路径(含audio_clk_close),待机时应调用它而非只静音,以关闭模拟与时钟节省功耗。
扩展点
- 自定义物理音量函数:
dac_mode_init的phy_vol_set_func参数允许接入平台自定义的硬件音量控制(如外部功放增益引脚、寄存器配置),不传则用默认固定增益。 - 新增音频源接入:任何新音源(如自定义解码器)只需实现
sound_out_obj并注册到某个通道的sound[ch]+kick[ch],即可无缝汇入输出通路;接入时须遵循"中断内通过kick拉数据"的约定。 - 淡入淡出策略:
DAC_FADE_ENABLE编译宏开关控制淡入淡出行为;需要不同衰减曲线时,可扩展flag的B_DAC_FADE_*位与中断内的幅值计算。 - 采样率适配:
dac_init_api(sr, delay_flag)将采样率参数透传给auout_init,多采样率切换只需重新调用该函数。
相关链接
- dac_api.c(DAC 管理层源码) — 本页面主要分析对象:
DAC_MANAGE、vol_tab、dac_mode_init、dac_init_api - audio.c(音频子系统生命周期源码) —
audio_init/audio_off/dac_power_on/dac_power_off/audio_isr - audio_adc_cpu.c — ADC 采集通路(输入方向),见「音频输入采集」页面
- audio_power_trim.c — 音频电源/校准相关底层
- 关联头文件:
dac_api.h、dac.h、audio_analog.h(模拟前端)、decoder_mge.h(解码管理)、sound_effect_api.h(音效)、sine_play.h(提示音)、mio_api.h(MIO 音频)——均位于sdk/app/bsp/lib/或对应 include 目录