19 KiB
Avaota F1 开发日志 - Day 3:音频系统开发
版本:v1.0
日期:2024-11-24
主机环境:Ubuntu 24.04 LTS
目标平台:Avaota F1 (全志 V821 / 32-bit RISC-V)
第一部分:阶段 3 - 音频系统实现
1. 音频采集模块(✅ 完成)
1.1 代码实现
实现了完整的音频采集类 audio_capture.cpp:
核心功能:
- 基于 ALSA (Advanced Linux Sound Architecture)
- 支持 PDM 麦克风采集
- 音频格式:16kHz, 16-bit, Mono (单声道)
关键实现:
-
设备初始化 (
init()):snd_pcm_open(&m_pcm_handle, device, SND_PCM_STREAM_CAPTURE, 0); snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0); snd_pcm_hw_params_set_channels(handle, params, 1); // Mono -
PCM 数据读取 (
read()):snd_pcm_sframes_t ret = snd_pcm_readi(handle, buffer, frames); -
错误处理与自动恢复:
- Overrun (缓冲区溢出):自动调用
snd_pcm_prepare()恢复 - Suspend (设备挂起):调用
snd_pcm_resume()唤醒设备 - 通用错误:使用
snd_pcm_recover()尝试恢复
- Overrun (缓冲区溢出):自动调用
1.2 缓冲区优化
为了平衡延迟和稳定性,配置了合理的缓冲区参数:
- Period Size: 160 frames (10ms @ 16kHz)
- Buffer Size: 1600 frames (100ms @ 16kHz)
这样的配置提供了:
- 低延迟:10ms 周期,适合实时语音交互
- 稳定性:100ms 缓冲,防止频繁的 underrun/overrun
2. 音频播放模块(✅ 完成)
2.1 代码实现
创建了音频播放类 audio_player.h 和 audio_player.cpp:
核心功能:
- 支持 I2S 扬声器 (MAX98357A)
- 音频格式:16kHz, 16-bit, Mono
- 与音频采集模块使用相同的参数配置
关键实现:
-
播放设备初始化 (
init()):snd_pcm_open(&m_pcm_handle, device, SND_PCM_STREAM_PLAYBACK, 0); // 配置相同的音频参数 (16kHz, S16_LE, Mono) -
PCM 数据写入 (
write()):snd_pcm_sframes_t ret = snd_pcm_writei(handle, buffer, frames); -
Underrun 处理:
- 检测到
-EPIPE错误时自动恢复 - 重新准备设备并重试写入
- 避免播放卡顿和杂音
- 检测到
3. 测试程序(✅ 完成)
3.1 功能实现
创建了独立的音频测试程序 test_audio.cpp,包含三个测试模式:
测试 1:音频采集 (录音)
- 录音时长:5 秒
- 输出文件:
test_recording.pcm(RAW PCM 格式) - 用途:验证麦克风和 ALSA 采集功能
/tmp/test_audio capture
测试 2:音频播放
- 播放录制的 PCM 文件
- 用途:验证扬声器和 ALSA 播放功能
/tmp/test_audio playback
测试 3:回环测试
- 同时进行录音和播放
- 用途:验证全双工音频能力,检测延迟
- 现象:应听到自己的声音(有轻微延迟)
/tmp/test_audio loopback
3.2 使用说明
# 运行所有测试(交互式)
./test_audio
# 运行单个测试
./test_audio capture # 或 1
./test_audio playback # 或 2
./test_audio loopback # 或 3
4. 编译配置(✅ 完成)
4.1 编译脚本
创建了专用的编译脚本 build_phase3.sh:
关键配置:
# 32位 RISC-V 工具链
CXX="${TOOLCHAIN_DIR}/bin/riscv32-unknown-linux-g++"
# ALSA 库链接
LDFLAGS+=" -lasound"
# 编译模块
- utils/logger.cpp
- audio/audio_capture.cpp
- audio/audio_player.cpp
- test_audio.cpp
编译步骤:
cd ~/path/to/AvaotaF1/src
chmod +x build_phase3.sh
./build_phase3.sh
输出:
- 可执行文件:
test_audio - 架构:ELF 32-bit RISC-V
- 已 strip,体积优化
第二部分:技术要点总结
1. ALSA 库关键概念
1.1 PCM 设备
- Capture (采集):从麦克风读取数据
- Playback (播放):向扬声器写入数据
- 设备名:
hw:0,0(硬件卡 0, 设备 0) 或default(默认设备)
1.2 音频参数
| 参数 | 值 | 说明 |
|---|---|---|
| Format | SND_PCM_FORMAT_S16_LE |
16-bit signed little-endian |
| Sample Rate | 16000 Hz | 16 kHz (语音级别) |
| Channels | 1 | 单声道 (Mono) |
| Access | SND_PCM_ACCESS_RW_INTERLEAVED |
交织模式 |
1.3 错误码处理
| 错误码 | 含义 | 解决方案 |
|---|---|---|
-EPIPE |
Overrun (采集) / Underrun (播放) | snd_pcm_prepare() |
-ESTRPIPE |
设备挂起 | snd_pcm_resume() |
| 其他 | 通用错误 | snd_pcm_recover() |
2. 架构设计对比
ESP32S3 vs AvaotaF1
| 组件 | ESP32S3 (原) | AvaotaF1 (新) |
|---|---|---|
| 麦克风接口 | PDM (ESP_I2S 库) | PDM (ALSA) |
| 扬声器接口 | I2S STD (ESP_I2S 库) | I2S (ALSA + MAX98357A) |
| API 风格 | Arduino 同步阻塞 | Linux ALSA 标准 |
| 缓冲管理 | ESP_I2S 自动 | 手动配置 Period/Buffer |
| 错误恢复 | 硬件自动 | 软件手动处理 |
关键差异:
- ESP32S3 的 I2S 库高度封装,使用简单
- ALSA 需要手动配置参数,但更灵活强大
- AvaotaF1 需要显式处理 Overrun/Underrun
3. 与网络模块集成
音频系统已按照 main.cpp 的设计预留接口:
3.1 音频采集上行 (WebSocket)
// 主程序中的音频采集线程
void audio_capture_thread() {
AudioCapture mic("hw:0,0", 16000, 1);
mic.init();
WSClient ws_aud(SERVER_HOST, SERVER_PORT, "/ws_audio");
ws_aud.connect();
while (running) {
int16_t buffer[320]; // 20ms @ 16kHz
mic.read(buffer, 320);
ws_aud.send_binary((uint8_t*)buffer, 640); // 640 bytes
}
}
3.2 音频播放下行 (HTTP)
// 主程序中的音频播放线程
void audio_player_thread() {
AudioPlayer speaker("hw:0,0", 16000, 1);
speaker.init();
HTTPClient http;
http.connect("http://192.168.110.188:8081/stream.wav");
http.stream_download([&](const uint8_t* data, size_t size) {
speaker.write((const int16_t*)data, size / 2);
return true;
});
}
第三部分:待完成事项
当前阶段完成度:约 80%
✅ 已完成:
- 音频采集类实现 - 100%
- 音频播放类实现 - 100%
- 测试程序 - 100%
- 编译脚本 - 100%
⚠️ 待验证(需要开发板环境):
-
Device Tree 配置 - 0%
- 启用 PDM 麦克风驱动
- 启用 I2S 扬声器驱动
- 验证
/proc/asound/cards设备存在
-
ALSA 设备测试 - 0%
arecord -l列出录音设备aplay -l列出播放设备- 原始 ALSA 工具测试
-
音频模块集成测试 - 0%
- 运行
test_audio程序 - 验证录音质量
- 验证播放音质
- 运行
-
端到端验证 - 0%
- 与服务器联调
- WebSocket 音频上传测试
- HTTP TTS 播放测试
第四部分:部署与测试指南
1. 编译(PC 端)
cd ~/path/to/AvaotaF1/src
./build_phase3.sh
# 验证输出
file test_audio
# 预期:ELF 32-bit LSB executable, UCB RISC-V, statically linked
2. 部署(SD 卡方式)
# PC 端:复制到 SD 卡
cp test_audio /path/to/sd_card/
# 板端:挂载并运行
mount /dev/mmcblk0p1 /mnt/extsd
cp /mnt/extsd/test_audio /tmp/
chmod +x /tmp/test_audio
3. 前置检查(板端)
# 检查声卡设备
cat /proc/asound/cards
# 应该看到:0 [xxx]: ...
# 列出录音设备
arecord -l
# 应该看到:card 0: ..., device 0: ...
# 列出播放设备
aplay -l
# 应该看到:card 0: ..., device 0: ...
Warning
如果以上命令返回 "No soundcards found",说明 Device Tree 音频驱动未正确配置,需要先修改 DTS 并重新编译烧录内核。
4. 原始 ALSA 测试(板端)
# 测试录音(5 秒)
arecord -D hw:0,0 -f S16_LE -r 16000 -c 1 -d 5 test.wav
# 测试播放
aplay -D hw:0,0 test.wav
5. 音频模块测试(板端)
# 运行所有测试
/tmp/test_audio
# 或单独测试
/tmp/test_audio capture # 录音 5 秒
/tmp/test_audio playback # 播放录音
/tmp/test_audio loopback # 回环测试
第五部分:可能遇到的问题
问题 1:ALSA 设备不存在
现象:
[AudioCapture] Cannot open device 'hw:0,0': No such device
原因:
- Device Tree 未启用音频驱动
- 内核音频模块未编译
- 硬件连接问题
解决:
- 检查
/sys/class/sound/目录 - 修改 Device Tree 启用
dmic和i2s0节点 - 重新编译内核:
make kernel_menuconfig→ 启用 ALSA 驱动 - 烧录新固件并重启
问题 2:录音有声音但有杂音
可能原因:
- 采样率不匹配
- 缓冲区配置不当
- 硬件增益过高
解决:
# 使用 alsamixer 调整音量
alsamixer
# 按 F4 选择 Capture,调整麦克风增益
# 或使用 amixer
amixer set 'Capture' 50%
问题 3:播放音质差或卡顿
可能原因:
- Underrun (缓冲区欠载)
- CPU 占用过高
- 缓冲区过小
解决:
- 增大 Buffer Size:修改
audio_player.cpp中的buffer_size参数 - 调整线程优先级:
pthread_setschedprio(pthread_self(), 90); // 实时优先级
问题 4:回环测试有严重延迟
原因:
- 缓冲区过大
- Period 设置不当
解决:
- 减小 Period Size:从 160 改为 80 (5ms)
- 减小 Buffer Size:从 1600 改为 800 (50ms)
第六部分:下一步计划
阶段 4:摄像头系统(Day 4-5)
优先级:高(最大技术风险点)
任务清单:
- 确认 GC2083 摄像头配置
- 加载 ISP 参数文件 (
isp_ini_gc2083) - 验证
sample_virvi示例程序 - 实现 Camera 类 (调用 MPP 库)
- 实现 JPEG 硬件编码
- 集成到 WebSocket 视频流
为何现在进入摄像头阶段:
- 音频系统代码已完成,等待板端验证
- 摄像头涉及 MPP 库和 ISP 配置,技术难度最高
- 预留更多时间处理可能的画质问题(绿屏/过暗)
🏆 Day 3 成果
核心成就:
- ✅ 完成音频采集类实现 (ALSA + PDM)
- ✅ 完成音频播放类实现 (ALSA + I2S)
- ✅ 创建完整的测试程序 (3 种测试模式)
- ✅ 配置编译脚本和工具链
- ✅ 文档化所有技术要点和部署流程
技术栈确认:
- 音频框架: ALSA (Advanced Linux Sound Architecture)
- 采集设备: PDM 麦克风 (hw:0,0)
- 播放设备: I2S 扬声器 MAX98357A (hw:0,0)
- 音频格式: 16kHz, 16-bit, Mono, PCM
代码质量:
- 完整的错误处理机制
- 自动恢复功能 (Overrun/Underrun/Suspend)
- 详细的日志输出
- 优化的缓冲区配置
音频系统软件开发完成:
- ✅ 音频采集和播放类已实现
- ✅ 测试程序已创建
- ⚠️ 等待板端 Device Tree 配置才能验证
第七部分:I2S 音频硬件配置(✅ 完成)
1. 硬件方案确认
芯片平台:全志 V821 (sun300iw1p1)
音频输出方案:I2S0 + MAX98357A 数字功放
关键引脚:
- PD12: I2S0_BCLK (位时钟)
- PD13: I2S0_LRCK (左右声道时钟)
- PD15: I2S0_DOUT0 (音频数据输出)
2. Device Tree 配置
2.1 问题分析
初始错误:
sunxi:pin-42000000.pinctrl:[ERR]: unsupported function i2s0 on pin PD12
sunxi-snd-plat-i2s: probe of 42032000.i2s0_plat failed with error -22
根本原因:
- Pinctrl 驱动中 PD12/13/15 的 I2S 功能定义为
i2s0_bclk、i2s0_lrck、i2s0_dout0 - 不支持通用的
"i2s0"函数名 - Pinctrl 框架不支持为多个引脚指定不同的 function
2.2 最终解决方案
创建三个独立的 pinctrl 节点,每个引脚一个:
/* I2S0 individual pin definitions for MAX98357A */
i2s0_bclk_pin: i2s0_bclk@0 {
pins = "PD12";
function = "i2s0_bclk";
drive-strength = <20>;
bias-disable;
};
i2s0_lrck_pin: i2s0_lrck@0 {
pins = "PD13";
function = "i2s0_lrck";
drive-strength = <20>;
bias-disable;
};
i2s0_dout0_pin: i2s0_dout0@0 {
pins = "PD15";
function = "i2s0_dout0";
drive-strength = <20>;
bias-disable;
};
i2s0_pins_b: i2s0_pins@1 {
pins = "PD12", "PD13", "PD15";
function = "gpio_in";
};
2.3 I2S 平台设备配置
修改 &i2s0_plat 节点引用三个独立的引脚配置:
&i2s0_plat {
pinctrl-used;
pinctrl-names = "default","sleep";
pinctrl-0 = <&i2s0_bclk_pin &i2s0_lrck_pin &i2s0_dout0_pin>;
pinctrl-1 = <&i2s0_pins_b>;
status = "okay";
};
2.4 MAX98357A Codec 节点
在根节点添加 MAX98357A 驱动:
/ {
max98357a: max98357a {
#sound-dai-cells = <0>;
compatible = "maxim,max98357a";
status = "okay";
};
};
并在 &i2s0_mach 中引用:
&i2s0_mach {
status = "okay";
i2s0_codec: soundcard-mach,codec {
sound-dai = <&max98357a>;
};
};
3. Pinctrl 驱动调试
3.1 驱动源码分析
文件位置:bsp/drivers/pinctrl/pinctrl-sun300iw1.c
关键发现:
- PD12/13/15 在非 FPGA 配置块中有完整的 I2S 功能定义
- Function 0x6 对应 I2S 功能
- 必须使用准确的函数名:
i2s0_bclk、i2s0_lrck、i2s0_dout0
驱动修改(已在前期完成):
- 注释掉简化版本的 PD12/13 定义(没有 I2S 功能)
- 确保使用完整版本的引脚定义
4. 编译与烧录
4.1 SDK 配置
添加 alsa-utils 工具:
cd ~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
make menuconfig
# 选择: Sound ---> alsa-utils
4.2 编译步骤
source build/envsetup.sh
lunch avaota_f1-tina
ulimit -n 4096
make -j4
pack
4.3 烧录
使用 PhoenixSuit 烧录生成的固件:
out/v821_linux_avaota_f1_uart0_nor.img
5. 硬件验证(✅ 成功)
5.1 系统状态检查
root@(none):/# cat /proc/asound/cards
0 [audiocodec ]: audiocodec - audiocodec
1 [sndi2s0 ]: sndi2s0 - sndi2s0 # ✅ I2S 设备已识别
root@(none):/# cat /proc/asound/pcm
00-00: ... playback 1 : capture 1
01-00: sunxi-snd-plat-i2s-HiFi HiFi-0 : playback 1 # ✅ I2S 播放设备
root@(none):/# ls /dev/snd/
controlC0 controlC1 pcmC0D0c pcmC0D0p pcmC1D0p timer # ✅ 设备节点存在
5.2 Pinctrl 配置验证
root@(none):/# cat /sys/kernel/debug/pinctrl/42000000.pinctrl/pinconf-pins | grep -A 3 "pin 108\|pin 109\|pin 111"
pin 108 (PD12): output drive strength (20 mA) # ✅ 配置已生效
pin 109 (PD13): output drive strength (20 mA) # ✅
pin 111 (PD15): output drive strength (20 mA) # ✅
5.3 音频测试(🎉 成功)
硬件连接:
| MAX98357A | Avaota F1 |
|---|---|
| BCLK | PD12 |
| LRC | PD13 |
| DIN | PD15 |
| VIN | 3.3V |
| GND | GND |
| SD | 3.3V |
测试命令:
# 播放随机白噪音测试
dd if=/dev/urandom bs=192000 count=1 | aplay -D hw:1,0 -f S16_LE -r 48000 -c 2
测试结果:
Playing raw data 'stdin' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo
✅ 扬声器输出刺耳的白噪音 - 证明硬件通路完全正常!
6. 成功标志
✅ I2S 设备已识别:sndi2s0 (card 1)
✅ 设备节点已创建:/dev/snd/pcmC1D0p
✅ Pinctrl 配置生效:Drive strength 20mA
✅ I2S 驱动加载:sunxi-snd-plat-i2s mapping ok
✅ MAX98357A 工作:音频数据成功解码并放大
✅ 扬声器输出正常:能听到测试音(白噪音)
关键成就:
- 从零开始分析并修复 Device Tree 配置
- 调试 pinctrl 驱动,找到正确的函数定义
- 创建三个独立节点,绕过 pinctrl 框架限制
- 验证完整的音频输出硬件通路
7. 经验总结
7.1 Device Tree 配置原则
- 精确匹配函数名:必须使用驱动源码中定义的准确名称
- 独立节点策略:每个引脚不同功能时,必须创建独立节点
- Pinctrl 属性:
pinctrl-0可以引用多个节点(phandle 列表) - Drive Strength:I2S 信号需要较高的驱动强度(20mA)
7.2 调试方法
- 查看内核日志:
dmesg | grep -i "i2s\|pinctrl" - 检查 pinmux 状态:
/sys/kernel/debug/pinctrl/*/pinmux-pins - 检查 pinconf 状态:
/sys/kernel/debug/pinctrl/*/pinconf-pins - 验证设备节点:
cat /proc/asound/cards,ls /dev/snd/
7.3 常见陷阱
❌ 错误做法:
pins = "PD12", "PD13", "PD15";
function = "i2s0"; // ❌ 驱动不支持
❌ 错误做法:
pins = "PD12", "PD13", "PD15";
function = "i2s0_bclk", "i2s0_lrck", "i2s0_dout0"; // ❌ 框架不支持多值
✅ 正确做法:
/* 每个引脚独立定义 */
i2s0_bclk_pin: i2s0_bclk@0 {
pins = "PD12";
function = "i2s0_bclk"; // ✅ 一个引脚一个功能
};
🏆 Day 3 最终成果
软件开发
- ✅ AudioCapture 类实现(ALSA 录音)
- ✅ AudioPlayer 类实现(ALSA 播放)
- ✅ test_audio 测试程序(3 种测试模式)
- ✅ 编译脚本和工具链配置
硬件配置
- ✅ I2S0 引脚 Device Tree 配置(PD12/13/15)
- ✅ MAX98357A Codec 驱动集成
- ✅ Pinctrl 驱动调试和修复
- ✅ 音频输出硬件验证成功
下一步行动
- 在阶段四中实现音频采集(PDM 麦克风配置)
- 集成音频模块到主程序网络流
- 开始摄像头系统开发(阶段 4)