DAC/ADC 音频接口与采样
本文档介绍 fw-AD16N_GP-MCU_SDK(Jieli 杰理 GP-MCU)音频子系统中 DAC/ADC 音频接口与采样机制,涵盖 DAC 音量管理、AUDAC 中断填充与多路混音、单声道/立体声转换、数字音量、ADC 采样扫描、应用层集成等核心实现。
Purpose and Scope
本页是音频子系统(5-audio-subsystem)中关于 DAC 输出链路与 ADC 采样接口 的参考页,面向需要理解或修改音频输出/采样路径的工程师。本页覆盖:
- DAC 公共接口层:
audio_dac_api.h/audio_dac_api.c提供的初始化、音量、静音、通道注册等 API,以及 32 级物理音量表vol_tab。 - DAC 管理结构体:
audac_basic.h中的DAC_MANAGE与全局实例dac_mge,说明多路数据流句柄、kick 回调、声道标记与音量字段的组织方式。 - AUDAC 中断填充机制:
fill_audac.c中的fill_dac_fill_phy(多路混音填充)、fill_dac_only_one(单路直通填充)与fifo_dac_fill,这是 DAC 持续输出 PCM 数据的核心热路径。 - ADC 采样集成:应用层对 ADC 的初始化(
adc_init)与周期扫描(adc_scan)、config_audio_adc_enable开关、ADC 中断优先级配置。 - 应用层集成点:
app.c中的开机音量恢复、app_config.c中的音频使能配置。
不纳入本页范围(由同目录下其他页面或上层页面覆盖):解码器与 sound_out_obj 声音源的完整生命周期、USB 音频(UAC)接口、EQ/音效处理、模拟前端(audio_analog)的详细电路配置。涉及这些主题时会给出交叉指引,但不展开。
Overview
在 GP-MCU 音频子系统中,DAC(数字-模拟转换) 负责把解码后的 PCM 样点转换为模拟音频信号输出,ADC(模拟-数字转换) 负责把麦克风等模拟输入采样为数字信号。两者共同构成芯片与外部模拟世界的音频接口。
DAC 输出链路的核心设计是一个 中断驱动的"拉"式填充模型:DAC 硬件按采样率周期性消耗 PCM 数据,当数据不足时触发音频中断(ISR),ISR 调用 fifo_dac_fill → fill_dac_fill_phy 从各活跃声道(channel)的环形缓冲区 p_obuf 中"拉取"样点,多路样点叠加混音、经过数字音量缩放与饱和裁剪后写入 DAC FIFO。这一模型把混音、音量、声道格式转换都收敛到同一个 ISR 热路径中,保证输出连续不中断。
ADC 侧则由应用层在定时器 tick 中周期性调用 adc_scan() 完成通道扫描,采样结果用于按键检测、电量检测等应用功能。
关键概念:
- AUDAC:Audio DAC 的统称,指 DAC 输出子系统的整体(含填充、混音、音量管理)。
- DAC_TRACK_NUMBER / B_STEREO:决定单声道/立体声数据在 FIFO 中的组织方式;
sound_out_obj.info中的B_STEREO位标记数据流是否为立体声。 - 物理音量(phy vol):DAC 硬件寄存器实际使用的音量,由
vol_tab32 级指数表映射而来,默认最大值为 16384。 - kick 回调:当 ISR 消费完一段缓冲数据后,通过
sound_kick/ 注册的kick函数通知上游声音源补充数据,形成"消费-补充"闭环。
Architecture
下图展示 DAC 输出链路与 ADC 采样在系统中所处的层次与依赖关系:
flowchart TD
subgraph sg_App["应用层 (app)"]
App["app.c<br/>开机音量恢复 / 定时扫描"]
AppCfg["app_config.c<br/>config_audio_adc_enable"]
end
subgraph sg_DAC_API["DAC 接口层 (audio_dac_api)"]
DAC_API["audio_dac_api.c<br/>dac_vol / dac_init / 音量表"]
DAC_MGE["dac_mge<br/>(DAC_MANAGE 全局实例)"]
DAC_HD["audio_dac_api.h<br/>公共 API 声明"]
end
subgraph sg_FILL["AUDAC 填充层 (ISR 热路径)"]
FIFO["fifo_dac_fill"]
FILL_PHY["fill_dac_fill_phy<br/>多通道混音填充"]
FILL_ONE["fill_dac_only_one<br/>单通道直通填充"]
end
subgraph sg_SOUND["声音流与缓冲"]
SOUND["sound_out_obj<br/>p_obuf 环形缓冲"]
KICK["sound_kick / kick 回调"]
end
subgraph sg_HW["芯片底层 (uc03)"]
DAC_CPU["audio_dac_cpu.c<br/>物理寄存器控制"]
DAC_HW["DAC 硬件 FIFO"]
MIO["MIO 数据搬运"]
end
App -->|"dac_vol / dac_mode_init / dac_init"| DAC_API
DAC_API --> DAC_MGE
DAC_MGE --> DAC_HD
DAC_HD --> FILL_PHY
FIFO --> FILL_PHY
FIFO --> FILL_ONE
FILL_PHY -->|"cbuf_read_alloc 读取 PCM"| SOUND
FILL_PHY -->|"消费后触发补数"| KICK
FILL_PHY -->|"d_mio_kick 搬运到硬件"| MIO
DAC_API -->|"audio_dac_ops 操作集"| DAC_CPU
DAC_CPU --> DAC_HW
App -->|"adc_init / adc_scan"| AppCfg
层次说明:
- 应用层(
app.c)是 DAC/ADC 的入口使用者:上电时通过dac_vol(0, vol)恢复上次音量(见 app.c),初始化时调用adc_init(),并在系统 tick 中每 2 个 tick 执行一次adc_scan()(见 app.c)。 - DAC 接口层 封装了音量表、模式初始化与硬件操作集
audio_dac_ops,对外提供dac_vol、dac_mute、dac_init等稳定 API,屏蔽芯片差异。 - AUDAC 填充层 是 ISR 热路径:
fill_dac_fill_phy逐样点累加多路声道数据并做格式转换;fill_dac_only_one用于单路场景的零拷贝直通。 - 声音流与缓冲 是填充层的数据来源:每个声道对应一个
sound_out_obj,其p_obuf环形缓冲存放解码后的 PCM 样点。 - 芯片底层(uc03)提供物理 DAC 寄存器控制(
audio_dac_cpu.c)与 MIO 数据搬运,填充层通过d_mio_kick把样点送入硬件 FIFO。
DAC 管理核心:DAC_MANAGE 与 dac_mge
所有 DAC 运行状态收敛在单个全局结构体 dac_mge 中,其类型定义于 audac_basic.h:
typedef struct _DAC_MANAGE {
/*多路数据流句柄*/
sound_out_obj *sound_pre[AUDAC_CHANNEL_TOTAL];
sound_out_obj *sound_later[AUDAC_CHANNEL_TOTAL];
/*多路数据流句柄所对应的触发函数*/
void (*kick[AUDAC_CHANNEL_TOTAL])(void *);
u8 ch; /*通路标记*/
u16 vol_l_phy; /*左声道物理音量*/
u16 vol_r_phy; /*右声道物理音量*/
u8 vol_l; /*左声道音量*/
u8 vol_r; /*右声道音量*/
u8 flag; /*静音等控制标记*/
/* u8 track; */
} DAC_MANAGE;
字段设计意图:
sound_pre/sound_later:同一数据流的"前"(pre)与"后"(later)两个阶段的句柄。填充 ISR 只从sound_later[i]->p_obuf读取数据(见fill_dac_fill_phy),sound_pre用于链路前级管理。AUDAC_CHANNEL_TOTAL决定最多可同时混音的路数。kick[i]:每个声道注册的"数据不足时"触发函数,配合sound_kick()在 ISR 中回调,通知上游解码器/声音源继续向缓冲填数。ch:位图形式的通路标记,BIT(i)置位表示第 i 路声道当前参与输出。填充循环用dac_mge.ch & BIT(i)判断是否处理该路。vol_l_phy/vol_r_phy:物理音量,直接参与数字音量缩放计算(见下文混音算法)。vol_l/vol_r:逻辑音量等级(0~31),用于 UI 层。flag:控制标记位,同文件定义了四个标志:
#define B_DAC_MUTE BIT(0)
#define B_DAC_FADE_EN BIT(1)
#define B_DAC_FADE_OUT BIT(2)
#define B_DAC_DIG_VOL BIT(3)
B_DAC_MUTE 表示静音,B_DAC_DIG_VOL 表示启用数字音量(此时混音器在 ISR 内按物理音量缩放样点),B_DAC_FADE_EN/B_DAC_FADE_OUT 配合 audio_dac_fade.c 实现淡入淡出(fade)控制。
音量系统:32 级指数音量表
DAC 音量采用查表映射方式:逻辑音量等级(0~31)通过 vol_tab[] 映射为物理音量值。该表定义于 audio_dac_api.c:
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 , 16384
};
#define MAX_VOL_LEVEL ((sizeof(vol_tab)/2) - 1)
#define MAX_PHY_VOL vol_tab[MAX_VOL_LEVEL]
__attribute__((always_inline))
u32 get_dac_max_phy_vol(void)
{
return MAX_PHY_VOL;
}
设计要点:
- 表值为指数增长(低等级步进小、高等级步进大),符合人耳对音量感知近似对数的特性,使各级音量听感均匀。
MAX_VOL_LEVEL = 31,MAX_PHY_VOL = 16384。get_dac_max_phy_vol()被声明为always_inline并在填充 ISR 热路径中使用,避免函数调用开销。- 应用层音量恢复代码中
vol <= 31的判界与MAX_VOL_LEVEL一致(见 app.c)。
初始化函数 dac_mode_init(u16 vol) 负责清空管理结构体并按是否启用淡入淡出设置初始音量:
void dac_mode_init(u16 vol)
{
memset(&dac_mge, 0, sizeof(dac_mge));
#if DAC_FADE_ENABLE
if (vol > MAX_VOL_LEVEL) {
vol = MAX_VOL_LEVEL;
}
dac_mge.vol_l = vol;
dac_mge.vol_r = vol;
#else
stereo_dac_vol(0, vol, vol);
#endif
}
DAC_FADE_ENABLE 编译开关决定了音量初始化路径:启用 fade 时只设置逻辑音量等级(由 fade 模块逐步逼近),未启用时直接通过 stereo_dac_vol 写入物理音量。
AUDAC 中断填充:多路混音热路径
DAC 持续输出依赖 ISR 周期性调用填充函数。fifo_dac_fill 是统一入口,当前实现直接转发到 fill_dac_fill_phy:
AT(.audio_isr_text)
u32 fifo_dac_fill(u8 *buf, u32 len, AUDIO_TYPE type)
{
return fill_dac_fill_phy(buf, len);
}
来源:fill_audac.c
fill_dac_fill_phy 的完整流程(fill_audac.c)可分为四个阶段:
阶段一:缓冲区清零与按键提示音注入
u32 i, sp_cnt, active_flag;
s32 t_sp;
s16 r_sp;
s16 *sp_buf;
s16 *rptr[AUDAC_CHANNEL_TOTAL];
u32 olen[AUDAC_CHANNEL_TOTAL];
u32 p_cnt[AUDAC_CHANNEL_TOTAL];
u32 sp_number = len / 2;
s16 all_cnt = 0;
memset(p_cnt, 0, sizeof(p_cnt));
memset(rptr, 0, sizeof(rptr));
memset(olen, 0, sizeof(olen));
memset(buf, 0, len);
d_key_voice_read(buf, len);
来源:fill_audac.c
先把输出缓冲整体清零(无数据声道输出静音),然后调用 d_key_voice_read 把按键提示音(key voice)样点直接合入缓冲区——提示音不经过 sound_out_obj 通道,属于旁路注入。
阶段二:按活跃声道预读环形缓冲
active_flag = 0;
for (i = 0; i < AUDAC_CHANNEL_TOTAL; i++) {
if (0 == (dac_mge.ch & BIT(i))) {
continue;
}
if (false == dac_cbuff_active(dac_mge.sound_later[i])) {
continue;
}
rptr[i] = cbuf_read_alloc(dac_mge.sound_later[i]->p_obuf, &olen[i]);
if (0 == olen[i]) {
log_char('z');
rptr[i] = 0;
}
fifo_dac_fill_kick(i, sp_number / DAC_TRACK_NUMBER);//mio依赖单声道数据量
}
来源:fill_audac.c
仅处理 ch 位图标记且 dac_cbuff_active 判定为活跃的声道;cbuf_read_alloc 返回可读区域指针(不立即消费),olen 为该区域字节数。fifo_dac_fill_kick 内部调用 d_mio_kick 把数据搬运到 MIO 硬件,其数据量按 sp_number / DAC_TRACK_NUMBER 计算——注释明确说明"mio 依赖单声道数据量",即 MIO 按单声道样点数计数。
阶段三:逐样点混音与声道格式转换
核心循环逐样点遍历所有活跃声道,把各声道样点累加到 t_sp:
for (sp_cnt = 0; sp_cnt < sp_number; sp_cnt++) {
t_sp = 0;
for (i = 0; i < AUDAC_CHANNEL_TOTAL; i++) {
if (0 == rptr[i]) {
continue;
}
if (0 == olen[i]) {
rptr[i] = cbuf_read_alloc(dac_mge.sound_later[i]->p_obuf, &olen[i]);
if (0 == olen[i]) {
log_char('x');
}
continue;
}
t_sp += *rptr[i];
if ((0 == (B_STEREO & dac_mge.sound_later[i]->info)) && \
(2 == DAC_TRACK_NUMBER)) {
if (1 == (sp_cnt & 0x01)) {
p_cnt[i]++;
rptr[i]++;
}
} else if ((B_STEREO & dac_mge.sound_later[i]->info) && \
(1 == DAC_TRACK_NUMBER)) {
p_cnt[i]++;
rptr[i]++;
t_sp += *rptr[i];
p_cnt[i]++;
rptr[i]++;
t_sp = t_sp / 2;
} else {
p_cnt[i]++;
rptr[i]++;
}
if ((p_cnt[i] * 2) >= olen[i]) {
cbuf_read_updata(dac_mge.sound_later[i]->p_obuf, p_cnt[i] * 2);
olen[i] = 0;
p_cnt[i] = 0;
rptr[i] = cbuf_read_alloc(dac_mge.sound_later[i]->p_obuf, &olen[i]);
if (0 == olen[i]) {
log_char('y');
}
}
}
...
}
来源:fill_audac.c
这段代码实现了两种声道格式适配:
- 单声道源 → 立体声 FIFO(
B_STEREO未置位且DAC_TRACK_NUMBER == 2):每两个输出样点只消费一个输入样点(奇数样点处跳过输入指针),把单声道数据"复制"到左右两个输出槽位。 - 立体声源 → 单声道 FIFO(
B_STEREO置位且DAC_TRACK_NUMBER == 1):一次消费左右两个样点,相加后取半(t_sp = t_sp / 2)得到平均样点。 - 格式一致:直接逐样点消费。
当已消费的样点对 p_cnt[i] * 2 达到本次可读区域长度 olen[i] 时,调用 cbuf_read_updata 真正提交消费,并重新 cbuf_read_alloc 获取下一段可读区域——这是环形缓冲"读分配/提交"两阶段语义的典型用法。
阶段四:数字音量、饱和裁剪与收尾
if (dac_mge.flag & B_DAC_DIG_VOL) {
u16 max_phy_vol = get_dac_max_phy_vol();
if (max_phy_vol != dac_mge.vol_l_phy) {
/* 使用数字音量默认以左声道音量调整样点 */
t_sp = (t_sp * dac_mge.vol_l_phy) / (max_phy_vol + 1);
}
}
t_sp += sp_buf[sp_cnt];
#ifdef __PI32V2__
L_sat(t_sp, t_sp);
#else
if (t_sp > 32767) {
t_sp = 32767;
} else if (t_sp < -32768) {
t_sp = -32768;
}
#endif
sp_buf[sp_cnt] = t_sp;
all_cnt++;
来源:fill_audac.c
- 启用
B_DAC_DIG_VOL且左声道物理音量未达最大时,按t_sp * vol_l_phy / (max_phy_vol + 1)缩放样点——注释说明数字音量默认以左声道音量调整所有样点(左右声道共用左值)。 - 混音累加后可能溢出 16 位,故必须饱和裁剪:
__PI32V2__架构使用L_sat指令,其余架构用 if-else 夹到 [-32768, 32767]。 - 最后把按键提示音样点
sp_buf[sp_cnt]叠加进混音结果,写回输出缓冲。
收尾阶段对所有处理过的声道提交剩余消费并触发补数:
for (i = 0; i < AUDAC_CHANNEL_TOTAL; i++) {
if (0 == (dac_mge.ch & BIT(i))) {
continue;
}
if (0 != rptr[i]) {
cbuf_read_updata(dac_mge.sound_later[i]->p_obuf, p_cnt[i] * 2);
}
sound_kick(dac_mge.sound_later[i], dac_mge.kick[i]);
}
#if TCFG_AUDIO_AUTO_MUTE_ENABLE
audio_energy_detect_run((s16 *)buf, len);
#endif
return all_cnt * 2;
来源:fill_audac.c
sound_kick 在缓冲被消费后回调该声道注册的 kick[i],驱动上游补充数据,保证环形缓冲不会因 ISR 持续消费而枯竭。若编译开关 TCFG_AUDIO_AUTO_MUTE_ENABLE 开启,还会调用 audio_energy_detect_run 对输出样点做能量检测,用于自动静音(无声音时静音输出以省电)。返回值 all_cnt * 2 为实际填充的字节数。
单通道直通路径:fill_dac_only_one
当系统只配置一路输出时,可使用 fill_dac_only_one 走零拷贝直通路径,避免逐样点混音的 CPU 开销:
AT(.audio_isr_text)
u32 fill_dac_only_one(u8 *buf, u32 len)
{
u32 rlen = 0;
memset(buf, 0, len);
u32 i;
for (i = 0; i < 1; i++) {
if (0 == (dac_mge.ch & BIT(i))) {
continue;
}
if (false == dac_cbuff_active(dac_mge.sound_later[i])) {
continue;
}
rlen = cbuf_read(dac_mge.sound_later[i]->p_obuf, buf, len);
fifo_dac_fill_kick(i, len / 2 / DAC_TRACK_NUMBER);//mio依赖单声道数据量
sound_kick(dac_mge.sound_later[i], dac_mge.kick[i]);
}
#if TCFG_AUDIO_AUTO_MUTE_ENABLE
audio_energy_detect_run((s16 *)buf, len);
#endif
return rlen;
}
来源:fill_audac.c
与混音路径的区别:直接 cbuf_read 一次拷贝整段数据到输出缓冲,不做逐样点累加、不做数字音量缩放(音量由硬件/物理音量控制),仅保留 MIO kick 与能量检测。这是对"单路、无混音需求"场景的优化分支。
ADC 采样:初始化与周期扫描
ADC 侧在 SDK 中的集成点集中在应用层 app.c 与配置层 app_config.c。初始化流程在 app_init 类函数中按序执行:
/* key */
adc_init();
key_init();
来源:app.c
adc_init() 在按键初始化之前调用,说明 ADC 通道先就绪、按键检测再基于 ADC 采样结果工作(典型场景:ADC 按键)。随后音频相关初始化链继续执行 auadc_analog_variate_init() 与 audio_init():
app_power_init();
auadc_analog_variate_init();
audio_init();
来源:app.c
auadc_analog_variate_init() 负责 AUADC(模拟 ADC 输入)模拟变量的初始化,是麦克风/线路输入采样通路的前置步骤。
周期扫描由系统主循环 tick 驱动,每 2 个 tick 执行一次 adc_scan():
if ((0 == (tick_cnt % 2))) {
adc_scan();
}
来源:app.c
adc_scan() 是 ADC 通道扫描的轮询入口,供按键、电量检测等功能轮询各通道的采样值。这种"定时轮询"设计把 ADC 采样与实时性要求不高的应用任务解耦,避免为每个通道单独开中断。
配置开关与中断优先级定义于 app_config.c:
const int IRQ_ADC_IP = 3;
...
const u8 config_audio_adc_enable = 1;
来源:app_config.c
config_audio_adc_enable = 1:ADC 音频采样功能的总开关,置 1 使能 ADC 音频通路。IRQ_ADC_IP = 3:ADC 中断优先级设为 3(数字越大优先级越高),与IRQ_ENCODER_IP、IRQ_RF_QUEUE_IP等一起构成音频相关中断的优先级矩阵。
Core Flow:DAC 输出填充时序
下图展示一次 DAC 中断填充从触发到硬件输出的完整时序:
sequenceDiagram
participant HW as DAC 硬件
participant ISR as 音频中断 (ISR)
participant FIFO as fifo_dac_fill
participant FILL as fill_dac_fill_phy
participant CBUF as sound_later[i]->p_obuf
participant MIO as MIO 硬件
participant SRC as 解码器/声音源
participant EN as 能量检测模块
HW->>ISR: DAC FIFO 数据不足触发中断
ISR->>FIFO: 调用 fifo_dac_fill(buf, len)
FIFO->>FILL: 转发 fill_dac_fill_phy(buf, len)
FILL->>FILL: memset 清零 buf<br/>d_key_voice_read 注入提示音
loop 每个活跃声道 (dac_mge.ch & BIT(i))
FILL->>CBUF: cbuf_read_alloc 获取可读区域
FILL->>MIO: fifo_dac_fill_kick (d_mio_kick)
FILL->>FILL: 累加样点 + 单/双声道转换
FILL->>CBUF: cbuf_read_updata 提交消费
FILL->>SRC: sound_kick 通知补充数据
end
FILL->>FILL: 数字音量缩放 + 饱和裁剪<br/>叠加提示音样点
FILL->>EN: audio_energy_detect_run (自动静音检测)
FILL-->>FIFO: 返回实际填充字节数
ISR->>HW: 样点写入 DAC FIFO 输出
时序关键点:
- 拉式模型:DAC 硬件按采样率消耗数据,数据不足时由 ISR 主动"拉取",而非解码器"推送"——这保证了输出时钟由硬件主导,解码器只需在
sound_kick回调中补数。 - 环形缓冲两阶段操作:
cbuf_read_alloc先只读预览,cbuf_read_updata才提交消费,保证 ISR 与写方(解码器)并发访问安全。 - kick 触发发生在 ISR 末尾,解码器在中断上下文之外补数,避免在 ISR 内做重活。
- MIO kick 与数据搬运并行:
fifo_dac_fill_kick把样点交给 MIO 搬运到硬件,与后续样点处理流水化。
应用层集成示例
示例一:开机音量恢复
系统上电后从存储中读取上次音量并写回 DAC,音量上限与 MAX_VOL_LEVEL(31)对齐:
if ((vol <= 31) && (res == sizeof(vol))) {
dac_vol(0, vol);
log_info("powerup set vol : %d\n", vol);
}
来源:app.c
dac_vol(char set, u8 vol) 的第一个参数 set 用于指示设置方式(0 表示直接设置),第二个参数为逻辑音量等级。读取失败或越界时静默跳过,不阻塞启动流程。
示例二:DAC 多路通道注册与公共 API
应用通过 regist_dac_channel 把解码链路(前级句柄、后级句柄、kick 函数)注册到 dac_mge 的空闲槽位,接口声明见 audio_dac_api.h:
bool dac_mute(bool mute);
void dac_mode_init(u16 vol);
void dac_init(u32 sr, u32 delay_flag);
void dac_init_api(u32 sr);
void dac_sr_api(u32 sr);
void dac_off_api(void);
u8 dac_vol(char set, u8 vol);
u8 stereo_dac_vol(char set, u8 vol_l, u8 vol_r);
void dac_usb_vol(u8 vol_r, u8 vol_l);
bool regist_dac_channel(void *psound_pre, void *psound_later, void *kick);
bool unregist_dac_channel(void *dec_hld);
bool dac_cbuff_active(void *sound_hld);
u32 get_dac_max_phy_vol(void);
void uac_audio_dac_percent(u8 ch);
void audio_dac_sync_once(void);
头文件还定义了数据包粒度常量:DAC_PACKET_SIZE (32*8),解码缓冲 DAC_DECODER_BUF_SIZE 在裁剪 RAM 版(CUT_RAM_BUF_ENABLE)下为 4 包、常规版为 5 包(见 audio_dac_api.h),用于约束解码器每次向 DAC 缓冲提交的数据量。
示例三:ADC 使能配置
const u8 config_audio_adc_enable = 1;
来源:app_config.c
置 1 使能 ADC 音频采集;若产品不需要录音/语音输入,可置 0 以省去 ADC 通路初始化。
Configuration Options
DAC/ADC 音频接口相关的编译期与运行期配置如下(均来自已核对的源码):
| 配置项 | 类型 | 默认值/示例 | 说明 |
|---|---|---|---|
DAC_PACKET_SIZE | 宏 | (32*8) | DAC 数据包粒度(字节),定义于 audio_dac_api.h |
DAC_DECODER_BUF_SIZE | 宏 | DAC_PACKET_SIZE * 4(CUT_RAM_BUF_ENABLE 时)/DAC_PACKET_SIZE * 5(常规) | 解码器提交到 DAC 的缓冲上限,见 audio_dac_api.h |
CUT_RAM_BUF_ENABLE | 编译开关 | 未定义 | 裁剪 RAM 缓冲模式,减小 DAC 解码缓冲占用 |
DAC_FADE_ENABLE | 编译开关 | 未定义 | 启用淡入淡出音量控制,改变 dac_mode_init 的初始化路径(audio_dac_api.c) |
TCFG_AUDIO_AUTO_MUTE_ENABLE | 编译开关 | 未定义 | 启用自动静音:填充时对输出样点做能量检测(fill_audac.c) |
AUDAC_CHANNEL_TOTAL | 宏 | 芯片相关 | DAC 可同时混音的最大通道数,决定 sound_pre/sound_later/kick 数组长度 |
DAC_TRACK_NUMBER | 宏 | 芯片相关(uc03 为 2) | DAC 输出声道数(1=单声道,2=立体声),参与单/双声道转换判定 |
HAS_MIO_EN | 编译开关 | 未定义 | 启用 MIO 数据搬运,填充时调用 d_mio_kick |
config_audio_adc_enable | 常量 | 1 | ADC 音频使能开关(app_config.c) |
IRQ_ADC_IP | 常量 | 3 | ADC 中断优先级(app_config.c) |
MAX_VOL_LEVEL | 宏 | 31 | 逻辑音量最大等级,由 vol_tab 推导 |
MAX_PHY_VOL | 宏 | 16384 | 最大物理音量值(vol_tab[31]) |
注:
DAC_FADE_ENABLE、TCFG_AUDIO_AUTO_MUTE_ENABLE、HAS_MIO_EN、CUT_RAM_BUF_ENABLE、AUDAC_CHANNEL_TOTAL、DAC_TRACK_NUMBER等宏的具体取值定义于各芯片/工程配置头文件(如audio_dac.h等),源码中以其为分支条件使用。
API Reference
以下 API 均声明于 audio_dac_api.h,实现在 audio_dac_api.c(DAC 管理/音量)与 audio_dac_cpu.c(物理层)中。
void audac_clr_buf(void)
清空 DAC 相关环形缓冲,用于格式/采样率切换或暂停时丢弃残留数据。
bool dac_mute(bool mute)
设置 DAC 静音状态。
- 参数:
mute—true静音,false取消静音。 - 返回:设置后的静音状态。
- 对应
dac_mge.flag中的B_DAC_MUTE位。
void dac_mode_init(u16 vol)
初始化 DAC 管理模式并设置初始音量。
- 参数:
vol— 初始逻辑音量(0~31)。 - 行为:清零
dac_mge;DAC_FADE_ENABLE下钳位音量等级并写入vol_l/vol_r,否则调用stereo_dac_vol(见 audio_dac_api.c)。
void dac_init(u32 sr, u32 delay_flag)
DAC 硬件初始化并设置采样率。
- 参数:
sr— 采样率(如 44100/48000);delay_flag— 模拟上电延时标记,用于上电时序控制。
void dac_init_api(u32 sr)
DAC 初始化的高级封装(dac_init 的 API 版本)。
void dac_sr_api(u32 sr)
动态切换 DAC 采样率,用于解码采样率变化场景(如 44.1k↔48k 切换)。
void dac_off_api(void)
关闭 DAC 输出并释放相关资源,对应低功耗流程。
u8 dac_vol(char set, u8 vol)
设置 DAC 音量(左右声道一致)。
- 参数:
set— 设置方式(0=直接设置);vol— 逻辑音量等级(0~31)。 - 返回:生效后的音量等级。
- 应用示例:开机恢复音量
dac_vol(0, vol)(见 app.c)。
u8 stereo_dac_vol(char set, u8 vol_l, u8 vol_r)
独立设置左右声道音量。
- 参数:
set— 设置方式;vol_l/vol_r— 左右声道音量等级。 - 返回:生效后的音量等级。
void dac_usb_vol(u8 vol_r, u8 vol_l)
USB 音频(UAC)场景下的音量设置入口(注意参数顺序为右、左)。
bool regist_dac_channel(void *psound_pre, void *psound_later, void *kick)
注册一路数据流到 DAC 混音器。
- 参数:
psound_pre— 前级sound_out_obj句柄;psound_later— 后级sound_out_obj句柄(其p_obuf为 ISR 读取源);kick— 数据不足时的触发函数指针。 - 返回:
true注册成功,false失败(无空闲槽位)。 - 对应
dac_mge.sound_pre[i]、sound_later[i]、kick[i]与ch位图。
bool unregist_dac_channel(void *dec_hld)
注销一路数据流。
- 参数:
dec_hld— 解码器/声音源句柄。 - 返回:
true注销成功。
bool dac_cbuff_active(void *sound_hld)
判断指定 sound_out_obj 的 DAC 环形缓冲是否活跃(有数据可读/运行中),填充 ISR 用它跳过空声道。
u32 get_dac_max_phy_vol(void)
获取最大物理音量(MAX_PHY_VOL = 16384)。
- 声明为
always_inline,在填充 ISR 的数字音量缩放中直接调用(见 audio_dac_api.c)。
void uac_audio_dac_percent(u8 ch)
UAC 场景下按百分比刷新 DAC 音量(与 USB 音量联动)。
void audio_dac_sync_once(void)
DAC 同步一次,用于声道/格式切换后重新对齐数据流。
填充层内部接口(ISR 使用)
AT(.audio_isr_text)
void fifo_dac_fill_kick(u32 i, u32 dac_packet_num);
AT(.audio_isr_text)
u32 fill_dac_fill_phy(u8 *buf, u32 len);
AT(.audio_isr_text)
u32 fill_dac_only_one(u8 *buf, u32 len);
AT(.audio_isr_text)
u32 fifo_dac_fill(u8 *buf, u32 len, AUDIO_TYPE type);
来源:fill_audac.c
fifo_dac_fill_kick(i, num):第i路声道的 MIO 搬运触发,HAS_MIO_EN下调用d_mio_kick。fill_dac_fill_phy(buf, len):多路混音填充主实现,返回实际填充字节数。fill_dac_only_one(buf, len):单路直通填充,返回拷贝字节数。fifo_dac_fill(buf, len, type):统一入口,转发到fill_dac_fill_phy。- 以上函数均标注
AT(.audio_isr_text),即放置于音频中断专用代码段,保证 ISR 路径的代码常驻与缓存友好。
Failure Modes、边界情况与并发
缓冲枯竭(underrun)与补数机制
填充 ISR 从 p_obuf 读取数据时,若 cbuf_read_alloc 返回 olen == 0(无数据可读),代码打调试字符 'z'/'x'/'y'(见 fill_audac.c)并跳过该声道;由于缓冲区已 memset 清零,缺失声道输出静音而非噪声。同时 sound_kick 会通知上游补数。潜在风险:若解码器持续跟不上(如高码率 + 低优先级任务),会出现周期性静音/杂音(log 中出现大量 'z'),需调大 DAC_DECODER_BUF_SIZE 或提升解码任务优先级。
混音溢出与饱和裁剪
多路样点累加 + 提示音叠加 + 数字音量缩放均可能超出 16 位范围。代码在写回前强制裁剪到 [-32768, 32767](__PI32V2__ 用 L_sat 指令),否则会产生 wrap-around 爆音。这是混音路径的必做步骤,任何新增混音源都必须保证进入该裁剪点。
声道格式不匹配(mono/stereo)
fill_dac_fill_phy 按 sound_later[i]->info 的 B_STEREO 位与全局 DAC_TRACK_NUMBER 做格式适配。边界情况:单声道源输出到立体声 FIFO 时,奇数样点槽位复用同一输入样点,输入指针只前进一半——若上游按错误的声道数填数,会导致左右声道相位错位或数据偏移。声道格式信息必须由 sound_out_obj 创建方与 DAC 配置保持一致。
数字音量与物理音量的一致性
数字音量缩放以左声道物理音量 vol_l_phy 为准(见 fill_audac.c)。边界情况:仅调用 stereo_dac_vol 设置右声道时,数字音量不随之变化,左右声道听感可能不一致;dac_usb_vol(UAC 音量)的左右参数顺序与 stereo_dac_vol 相反,调用时需注意。
并发访问模型
- ISR 与解码器写方:
p_obuf环形缓冲采用"读分配/提交"两阶段(cbuf_read_alloc/cbuf_read_updata),ISR 只读不写缓冲头,解码器只写不读,天然支持单生产者单消费者无锁并发。 - 音量/静音设置与 ISR:
dac_vol、dac_mute修改dac_mge字段,而 ISR 同时读取这些字段(vol_l_phy、flag)。由于是单字节/双字节变量且无原子性要求,风险较低,但音量高频变更时建议在非中断临界区外执行,避免撕裂读。 dac_mge.ch位图:通道注册/注销(regist_dac_channel/unregist_dac_channel)与 ISR 的ch & BIT(i)判定并发,注册/注销应在音频暂停或加临界区保护时进行。
ADC 轮询抖动
adc_scan() 在应用 tick 中每 2 tick 执行一次(app.c),采样时刻受主循环负载影响存在抖动。对采样率/时序敏感的模拟输入应用需自行评估;IRQ_ADC_IP = 3 的中断优先级配置仅覆盖中断驱动场景。
Performance 与运维注意事项
- ISR 热路径优化:
fill_dac_fill_phy整段位于AT(.audio_isr_text)代码段,且get_dac_max_phy_vol声明为always_inline,目的是最小化中断延迟与代码段换页开销。修改此路径时应避免引入耗时的循环、浮点或系统调用。 - 多通道混音的 CPU 代价:混音路径按
sp_number(样点数)× 活跃声道数双重循环逐样点处理。AUDAC_CHANNEL_TOTAL越大、采样率越高,ISR 占用 CPU 越多;纯单路场景应使用fill_dac_only_one直通路径。 - MIO 搬运流水化:
fifo_dac_fill_kick在读取每段缓冲后立即触发 MIO 搬运,使 DMA 传输与后续样点处理重叠,避免硬件 FIFO 饥饿。 - 缓冲大小与 RAM 权衡:
DAC_DECODER_BUF_SIZE在CUT_RAM_BUF_ENABLE下从 5 包减到 4 包(audio_dac_api.h),以更小的 RAM 占用换取更频繁的补数回调,超低端产品可开启,但需验证解码器实时性。 - 自动静音节能:
TCFG_AUDIO_AUTO_MUTE_ENABLE开启时,每次填充都运行audio_energy_detect_run能量检测,无信号时静音输出以降低功耗;代价是额外的逐包能量统计开销。
Extension Points
- 新增混音通道:实现一个带
p_obuf的sound_out_obj与kick回调,通过regist_dac_channel注册到空闲槽位即可自动参与混音(受AUDAC_CHANNEL_TOTAL限制)。 - 自定义填充策略:
fifo_dac_fill是统一入口,可替换其内部实现(如改为fill_dac_only_one直通或加入自定义后处理)而不影响上层调用方。 - 淡入淡出:启用
DAC_FADE_ENABLE并配合audio_dac_fade.c(audio_dac_fade.h声明接口)实现音量渐变,适合开机/切歌防爆音场景。 - 数字音量自定义缩放:
B_DAC_DIG_VOL分支中的缩放公式可替换为任意映射(如不同声道独立增益),只需保持进入饱和裁剪点。 - 模拟输入通路:ADC 采样(
adc_init/adc_scan)与应用功能(按键、电量)解耦,可扩展新的轮询消费方复用采样结果。
Tests 与验证建议
SDK 仓库中 DAC/ADC 相关测试以板级验证为主(未发现独立单元测试文件)。建议的验证清单:
- 音量阶梯测试:遍历
vol_tab0~31 级,确认各级听感递增、无跳变爆音。 - 混音压力测试:同时注册多路通道(含单声道/立体声混合),确认无破音、无声道串扰。
- 采样率切换测试:
dac_sr_api在 44.1k/48k 间切换,确认无 pop 音、无数据错位。 - 低缓冲压力测试:开启
CUT_RAM_BUF_ENABLE后以最高码率播放,观察 log 中'z'/'x'/'y'调试字符是否频繁出现(频繁出现表明缓冲偏小)。 - 自动静音测试:
TCFG_AUDIO_AUTO_MUTE_ENABLE下播放静音 PCM,确认输出被静音且恢复有声后正常输出。 - 开机音量恢复测试:写入音量等级 0 与 31 边界值并重启,确认
app.c中的vol <= 31判界生效。
Related Links
- 音频子系统总览 — 音频子系统的整体结构与本页所属分类
- DAC 物理层实现:
sdk/apps/app/bsp/cpu/uc03/audio_dac_cpu.c与sdk/apps/include_lib/cpu/uc03/audio_dac.h - DAC 淡入淡出:
sdk/apps/app/bsp/common/audio/audio_dac_fade.c与sdk/apps/include_lib/audio/audio_dac_fade.h - 音频能量检测(自动静音):
sdk/apps/app/bsp/common/audio/audio_energy_detect.h/audio_energy_detect.c - 声音源管理(
sound_out_obj):sdk/apps/include_lib/audio/sound_mge.h与sound_kick.h - 按键提示音注入:
d_key_voice_read(按键提示音模块) - ADC 应用侧接口:
adc_api.h(adc_init/adc_scan)与audio_adc.h