播放、录音与 MIO 工作流
本文档介绍 AW30N BLE SDK 中音频播放(DAC 输出)、录音(ADC 采集)以及 MIO(多路 IO 同步输出)模块的工作机制:从音频驱动初始化、DAC/ADC 数据通路,到 MIO 对象的状态机、平台钩子与编译开关,覆盖该能力的端到端实现。
Purpose and Scope
本页聚焦以下三部分内容:
- 播放链路:DAC 驱动初始化(
audio_init、dac_mode_init、dac_init_api)与音量控制(dac_vol),以及它们与系统启动流程的关系。 - 录音链路:ADC 采集相关驱动文件(
audio_adc_api.c)在音频子系统中的位置与作用。 - MIO 工作流:MIO(Multi-IO,多通道 IO 同步驱动)模块的完整机制——
sound_mio_obj对象模型、状态位、平台钩子(PWM/IO 初始化与运行)、HAS_MIO_EN编译开关以及音频播放时以 DAC 数据包驱动 IO 输出的核心流程。
以下主题属于其他目录页,本页不展开:按键与事件处理、USB 音频接口(usb_audio_interface)、Flash/存储文件系统(vfs 仅作为 MIO 读取音频文件的入口被引用)、具体板级 GPIO 配置。
Overview
在低成本的音频方案中,除了通过 DAC 驱动喇叭发声外,常常还需要让多路 IO 随音乐节奏同步输出(例如电平灯、振动马达阵列、磁吸喇叭等)。MIO 模块正是为此设计:它把"音频文件解码/播放"与"多路 IO 波形输出"绑定在同一个对象 sound_mio_obj 中,播放进程每处理一个 DAC 数据包(dac_packt),就"踢"(kick)一次 MIO,让 MIO 根据当前音频幅值更新 PWM 占空比与 IO 输出电平。
设计上,MIO 采用"弱符号 API + 可替换平台钩子"的分层结构:
- 头文件 mio_api.h 声明一组
__attribute__((weak))的模块级函数(mio_module_init/mio_start/mio_kick/mio_run/mio_open/mio_close),由物理层(mio_phy.c)或应用层提供实现; sound_mio_obj内部保存 5 个函数指针(read/pwm_init/pwm_run/io_init/io_run),默认实现由mio_a_hook_init()绑定到mio_api.c中的平台实现;- 通过
HAS_MIO_EN宏把整套 API 收编为d_mio_*宏,未使能时全部展开为空操作,做到零运行时开销。
这样既保证了音频主链路(DAC/ADC)的独立性,又允许应用按需挂接任意 IO 驱动策略。
Architecture
下图展示了播放/录音/ MIO 三部分与硬件层的关系:
flowchart TD
subgraph sg_App["应用层"]
App["app / init_app"]
DAC_CTRL["dac_mode_init / dac_init_api / dac_vol"]
ADC_CTRL["audio_adc / fill_audac"]
end
subgraph sg_MIO["MIO 模块"]
MIO_OBJ["sound_mio_obj"]
MIO_API["mio_module_init / mio_open / mio_start / mio_kick / mio_run / mio_close"]
MIO_HOOK["mio_a_hook_init"]
VFS["vfs_read (文件流)"]
end
subgraph sg_Phy["平台钩子实现 (mio_api.c)"]
PWM_INIT["mio_a_pwm_init (TIMER0, 3kHz)"]
PWM_RUN["mio_a_pwm_run (占空比 0-255)"]
IO_INIT["mio_a_io_init (PORTA 下拉输出)"]
IO_RUN["mio_a_io_run (IO 电平写入)"]
end
subgraph sg_HW["硬件层"]
DAC["DAC 音频输出"]
ADC["ADC 录音采集"]
TIMER0["JL_TIMER0 PWM"]
GPIO_PORT["JL_PORTA GPIO"]
end
App --> DAC_CTRL
App --> ADC_CTRL
DAC_CTRL --> DAC
ADC_CTRL --> ADC
DAC_CTRL -->|"DAC 数据包回调"| MIO_API
MIO_API --> MIO_OBJ
MIO_API --> VFS
VFS -->|"读取音频文件"| MIO_OBJ
MIO_OBJ --> MIO_HOOK
MIO_HOOK --> PWM_INIT
MIO_HOOK --> IO_INIT
PWM_INIT --> TIMER0
PWM_RUN --> TIMER0
IO_INIT --> GPIO_PORT
IO_RUN --> GPIO_PORT
图中各部分的职责:
- 应用层:
init_app.c在系统启动时依次调用audio_init()→dac_mode_init(16)→dac_init_api(SR_DEFAULT)完成播放通道初始化(见 init_app.c);app.c中通过dac_vol(0, vol)设置播放音量(见 app.c)。 - MIO 模块:以
sound_mio_obj为核心对象,持有状态位、IO 掩码、环形缓冲与 DAC 计数,通过弱符号 API 暴露给上层,并通过mio_a_hook_init()绑定平台实现。 - 平台钩子:
mio_api.c提供基于JL_TIMER0的 PWM 通道(默认IO_PORTA_15、3 kHz)与基于JL_PORTA的多达 15 路 IO 通道(偏移 1 起始)的驱动实现。 - 硬件层:DAC/ADC 负责音频数据的数模/模数转换,TIMER0 与 GPIO 端口负责 MIO 的物理输出。
MIO 对象模型与数据结构
通道布局常量
头文件定义了 MIO 的通道上限(见 mio_api.h):
| 常量 | 值 | 含义 |
|---|---|---|
MIO_MAX_CHL | 16 | MIO 最大通道总数 |
MIO_MAX_CHL_PWM | 1 | 其中 PWM 通道数(注释注明原可为 4) |
MIO_MAX_CHL_IO | 15 | 剩余 IO 通道数 |
MIO_MAX_RBUF | 1 + (15+7)/8 = 3 | IO 状态环形缓冲所需字节数 |
文件头信息 struct mio_info
音频文件(或数据流)头部携带 MIO 描述信息(见 mio_api.h):
struct mio_info {
u32 logo; // 标识:MIO_LOGO = 0X55AA1212,用于校验
u32 version; // 版本:MIO_VER_V1_1 = 0X00000200
u32 data_len; // 数据长度
u8 level; // 幅值等级
u8 remain; // 余数
u16 rate; // 采样率
u8 pwm_total; // PWM 通道总数
u8 io_total; // IO 通道总数
u8 remain1[2]; // 保留
};
来源:mio_api.h
logo 与 version 用于在 mio_open 时校验数据流是否为合法的 MIO 资源,pwm_total/io_total 决定运行时启用哪些通道。
运行时对象 sound_mio_obj
typedef struct _sound_mio_obj {
u8 status; // 状态位:B_MIO_EN / B_MIO_START / B_MIO_KICK / B_MIO_ERR
u16 io_mask; // 当前 IO 通道掩码
u8 r_buf[MIO_MAX_RBUF]; // IO 状态环形缓冲
u16 r_size; // 环形缓冲大小
u16 dac_step; // DAC 步进(每 N 个包更新一次)
u32 dac_cnt; // DAC 数据包计数
u32 dac_used_cnt; // 已使用计数
struct mio_info info; // 文件头信息
void *pfile; // 音频文件句柄
u32(*read)(void *, u8 *, u32); // 读取函数
void (*pwm_init)(u32); // PWM 通道初始化
void (*pwm_run)(u32, u32); // PWM 占空比更新
void (*io_init)(u32); // IO 通道初始化
void (*io_run)(u32, u32); // IO 电平更新
} sound_mio_obj;
来源:mio_api.h
设计要点:
- 状态位与运行解耦:
status用独立 bit 表达"已使能 / 已启动 / 本次已踢 / 出错"四个状态(B_MIO_EN=BIT(0)、B_MIO_START=BIT(1)、B_MIO_KICK=BIT(2)、B_MIO_ERR=BIT(7),见 mio_api.h)。mio_kick每包置位B_MIO_KICK,mio_run消费后清除,形成"生产者-消费者"握手。 - 函数指针即扩展点:
read/pwm_*/io_*五个函数指针让 MIO 对象不依赖具体文件系统与 IO 实现。默认由mio_a_hook_init()绑定到 VFS 与 GPIO/TIMER 实现,应用也可以自建对象替换。 - 环形缓冲:
r_buf缓存 IO 状态序列,io_mask保存当前输出掩码,配合dac_cnt/dac_used_cnt精确对齐 DAC 播放进度与 IO 输出时刻。
平台钩子实现(mio_api.c)
mio_api.c 是 MIO 的默认物理层实现,通过 mio_a_hook_init() 把对象内的函数指针绑定到真实硬件驱动。整个实现被 HAS_MIO_EN 宏包裹,且内部用 MIO_EN(当前为 0)进一步控制实际引脚操作,便于在未接硬件的开发板上编译通过。
文件读取钩子
u32 mio_a_read(void *pfile, u8 *buff, u32 len)
{
return vfs_read(pfile, buff, len);
}
来源:mio_api.c
MIO 的输入数据流直接复用 VFS 文件抽象,因此 mio_open 传入的句柄可以是 Flash 文件、内存盘或任何实现 vfs_read 的流式资源,这正是"播放"与"MIO 驱动"能共用同一份音频资源的基石。
PWM 通道初始化与占空比控制
#define MIO_API_PWM_PORT IO_PORTA_15
#define PWM_FRE 3000
void mio_a_pwm_init(u32 chl)
{
#if MIO_EN
gpio_set_pull_up(MIO_API_PWM_PORT, 0);
gpio_set_pull_down(MIO_API_PWM_PORT, 0);
gpio_set_direction(MIO_API_PWM_PORT, 0);
gpio_set_die(MIO_API_PWM_PORT, 1);
gpio_och_sel_output_signal(IO_PORTA_15, OUTPUT_CH_SIGNAL_TIMER0_PWM);
JL_TIMER0->CON = 0;
SFR(JL_TIMER0->CON, 10, 4, 6); //时钟源选择std24m
u32 timer_clk = 24000000;
SFR(JL_TIMER0->CON, 4, 4, 4); //pset=2
JL_TIMER0->CNT = 0; //清计数值
JL_TIMER0->PRD = timer_clk / (2 * PWM_FRE); //设置周期
JL_TIMER0->CON |= BIT(8) | (0b01 << 0); //计数模式
#endif
}
来源:mio_api.c
设计意图:PWM 通道选择 IO_PORTA_15 复用 TIMER0 的 PWM 输出信号,时钟源为 24 MHz std24m,PRD = 24MHz / (2 * 3000) = 4000,即 3 kHz 载波。用 3 kHz 低频 PWM 驱动磁性/感性负载(如磁吸喇叭、马达)是典型的低成本方案——占空比由音频幅值实时调制,从而实现"用 IO 播放"的效果。
void mio_a_pwm_run(u32 chl, u32 duty)
{
#if MIO_EN
local_irq_disable();
JL_TIMER0->PWM = (JL_TIMER0->PRD * duty) / 255; //0~255对应0~100%
local_irq_enable();
#endif
}
来源:mio_api.c
duty 取 0~255,按比例换算成 PRD * duty / 255 写入 JL_TIMER0->PWM。写入前关闭本地中断,避免与 DAC 中断/定时器中断竞争产生毛刺——这是音频驱动中常见的原子更新手法。
IO 通道初始化与电平输出
#define MIO_API_IO_PORT JL_PORTA
#define MIO_API_IO_OFFSET 1
void mio_a_io_init(u32 mask)
{
#if MIO_EN
log_info("mio io init -> mask : 0x%x\n", mask);
MIO_API_IO_PORT->PU0 &= ~(mask << MIO_API_IO_OFFSET);
MIO_API_IO_PORT->PD0 |= (mask << MIO_API_IO_OFFSET);
MIO_API_IO_PORT->DIR &= ~(mask << MIO_API_IO_OFFSET);
MIO_API_IO_PORT->OUT &= ~(mask << MIO_API_IO_OFFSET);
#endif
}
void mio_a_io_run(u32 mask, u32 io_ver)
{
#if MIO_EN
MIO_API_IO_PORT->OUT &= ~(mask << MIO_API_IO_OFFSET);
MIO_API_IO_PORT->OUT |= (io_ver << MIO_API_IO_OFFSET);
#endif
}
来源:mio_api.c
IO 通道固定挂在 JL_PORTA 上、从位偏移 1 开始:mio_a_io_init 将对应位配置为下拉输出且初始为低,mio_a_io_run 则一次性写入整个掩码的电平。接口注释说明 MIO 最多支持 16 路通道,其中 PWM 占用 1 路,其余 15 路为 IO(见 mio_api.c)。
钩子绑定入口
void mio_a_hook_init(sound_mio_obj *obj)
{
obj->read = mio_a_read;
obj->pwm_init = mio_a_pwm_init;
obj->pwm_run = mio_a_pwm_run;
obj->io_init = mio_a_io_init;
obj->io_run = mio_a_io_run;
}
来源:mio_api.c
这是把"通用对象"与"平台实现"解耦的装配点:mio_a_hook_init 只做函数指针赋值,不改动对象状态;应用层可以在调用它之后再覆盖个别指针,实现"部分定制"。
播放与录音链路
播放初始化(DAC)
播放通道在系统启动阶段完成初始化(见 init_app.c):
audio_init();
dac_mode_init(16);
dac_init_api(SR_DEFAULT);
来源:init_app.c
audio_init():音频子系统全局初始化(时钟、模拟前端等)。dac_mode_init(16):配置 DAC 工作模式(增益/输出级参数 16)。dac_init_api(SR_DEFAULT):以默认采样率启动 DAC 接口。
播放音量在开机阶段按用户保存值恢复(见 app.c):
if ((vol <= 31) && (res == sizeof(vol))) {
dac_vol(0, vol);
log_info("powerup set vol : %d\n", vol);
}
来源:app.c
音量被限制在 0~31 的区间内,并且只有读回长度正确时才生效,防止异常数据破坏 DAC 音量寄存器。
录音与音频辅助模块
录音侧由 ADC 驱动承担:sdk/apps/app/bsp/common/audio/audio_adc_api.c 提供 ADC 采集 API,fill_audac.c 负责把 ADC 采集到的 PCM 数据填充进音频缓冲,sound_kick.c 提供音频任务调度/踢包机制,audio_dac_fade.c 实现 DAC 音量淡入淡出(防爆音)。这些模块与 MIO 共用同一套音频任务框架:DAC 每消费一个数据包,就会触发一次 kick,MIO 借此机会更新 IO 输出。
注:录音侧的 ADC 具体寄存器操作与数据通路位于上述驱动文件中,本次文档编写受源码探索预算限制未能逐行展开;上面对各文件职责的描述基于其命名与在
app_config.c/app.c中的引用关系(audio_dac.h、audio_adc.h均被 mbox_flash 应用包含)。
核心工作流:播放驱动 MIO 输出
状态机
sound_mio_obj.status 的位定义构成一个精简状态机:
stateDiagram-v2
[*] --> ENABLED: mio_open 成功<br/>(置 B_MIO_EN)
ENABLED --> RUNNING: mio_start<br/>(置 B_MIO_START)
RUNNING --> KICKED: 每 DAC 包 mio_kick<br/>(置 B_MIO_KICK)
KICKED --> RUNNING: mio_run 消费<br/>(清 B_MIO_KICK)
RUNNING --> ERROR: 文件/硬件异常<br/>(置 B_MIO_ERR)
ERROR --> [*]: mio_close
ENABLED --> [*]: mio_close
RUNNING --> [*]: mio_close
时序
sequenceDiagram
participant App as 应用层
participant MIO as MIO 弱符号 API
participant OBJ as sound_mio_obj
participant VFS as vfs_read
participant HW as TIMER0/GPIO
participant DAC as DAC 音频播放
App->>MIO: mio_open(&obj, pfile, ...)
MIO->>OBJ: 校验 info.logo/version<br/>绑定 read/pwm/io 钩子
OBJ->>VFS: 读取文件头 mio_info
VFS-->>OBJ: logo=0x55AA1212, rate, pwm_total, io_total
MIO-->>App: obj 就绪 (B_MIO_EN)
App->>MIO: mio_start(obj)
MIO->>OBJ: 置 B_MIO_START, pwm_init/io_init
loop 每个 DAC 数据包
DAC->>MIO: mio_kick(obj, dac_packt)
MIO->>OBJ: 置 B_MIO_KICK, 更新 dac_cnt
App->>MIO: mio_run()
MIO->>OBJ: 按 io_mask 查 r_buf
OBJ->>HW: pwm_run(duty) / io_run(io_ver)
MIO->>OBJ: 清 B_MIO_KICK
end
App->>MIO: mio_close(&obj)
MIO->>OBJ: 清状态, 释放资源
关键控制流解释:
- 打开:
mio_open分配/初始化sound_mio_obj,先通过mio_a_hook_init(或自定义钩子)绑定 5 个函数指针,再读取数据流头部的mio_info并用logo/version校验合法性,成功后置B_MIO_EN。 - 启动:
mio_start置B_MIO_START,并调用pwm_init/io_init配置物理通道(此时 3 kHz 载波开始输出)。 - 踢包:音频播放任务每消费一个 DAC 数据包,调用
mio_kick(obj, dac_packt),MIO 记录dac_cnt并置B_MIO_KICK——这是"播放进度 → IO 输出"的同步点。 - 运行:
mio_run读取当前io_mask,从r_buf取出下一组状态,调用pwm_run/io_run写入硬件,再清除B_MIO_KICK。dac_step/dac_used_cnt控制更新的节流粒度,避免每个包都刷新硬件。 - 关闭:
mio_close复位状态并释放对象。
使用示例
示例 1:定义并绑定 MIO 对象(应用接入点)
应用侧典型的接入方式:定义一个 sound_mio_obj,调用 mio_a_hook_init 绑定默认平台实现,再调用弱符号 API 打开/启动:
#include "mio_api.h"
sound_mio_obj mio;
void *pfile; /* 由 vfs_open 得到的音频文件句柄 */
/* 1. 绑定平台钩子(VFS 读取 + TIMER0 PWM + PORTA IO) */
mio_a_hook_init(&mio);
/* 2. 打开 MIO:校验文件头,置 B_MIO_EN */
if (d_mio_open((void **)&mio, pfile, NULL)) {
/* 3. 启动:配置 PWM/IO 通道,置 B_MIO_START */
d_mio_start(&mio);
}
说明:
d_mio_open/d_mio_start在HAS_MIO_EN为真时展开为mio_open/mio_start,否则为空操作。API 声明见 mio_api.h,宏开关见 mio_api.h。
示例 2:DAC 播放回调中踢包(核心同步点)
在音频播放任务的 DAC 数据包回调中调用 d_mio_kick,把播放进度同步给 MIO:
/* 每个 DAC 数据包回调 */
void dac_packet_done(void *priv, u32 dac_packt)
{
/* 播放进度 → MIO:置 B_MIO_KICK,累计 dac_cnt */
d_mio_kick(&mio, dac_packt);
/* 周期性刷新 IO:mio_run 内部按 dac_step 节流 */
d_mio_run();
}
说明:
mio_kick/mio_run的签名声明见 mio_api.h。该回调与sound_kick.c中的音频任务踢包机制协同工作。
示例 3:平台钩子的默认实现(PWM/IO 输出)
以下代码展示了默认 PWM 通道如何以 3 kHz 载波、0~255 占空比驱动 IO_PORTA_15:
void mio_a_pwm_run(u32 chl, u32 duty)
{
local_irq_disable();
JL_TIMER0->PWM = (JL_TIMER0->PRD * duty) / 255; /* 0~255 对应 0~100% */
local_irq_enable();
}
来源:mio_api.c
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
HAS_MIO_EN | 宏 | 未定义/0 | 编译开关;为真时 d_mio_* 展开为真实函数调用,否则为空操作(mio_api.h) |
MIO_EN | 宏 | 0 | mio_api.c 内部开关,控制 PWM/IO 引脚操作是否实际写入寄存器(mio_api.c) |
MIO_MAX_CHL | 常量 | 16 | MIO 最大通道数(mio_api.h) |
MIO_MAX_CHL_PWM | 常量 | 1 | PWM 通道数,其余为 IO 通道(mio_api.h) |
MIO_LOGO | 常量 | 0X55AA1212 | 数据流头部校验标识(mio_api.h) |
MIO_VER_V1_1 | 常量 | 0X00000200 | 版本号(mio_api.h) |
MIO_API_PWM_PORT | 宏 | IO_PORTA_15 | PWM 输出引脚(mio_api.c) |
PWM_FRE | 宏 | 3000 | PWM 载波频率(Hz),决定 TIMER0 周期(mio_api.c) |
MIO_API_IO_PORT | 宏 | JL_PORTA | IO 通道所在端口(mio_api.c) |
MIO_API_IO_OFFSET | 宏 | 1 | IO 掩码位偏移起点(mio_api.c) |
dac_mode_init(n) | 函数 | 16 | DAC 模式参数(init_app.c) |
dac_vol(0, vol) | 函数 | 0~31 | 播放音量,限幅 0~31(app.c) |
API 参考
以下为 MIO 模块对外 API(均为弱符号,见 mio_api.h):
void mio_module_init(void)
模块级初始化(弱符号,由物理层或应用提供实现)。在系统启动早期调用,用于初始化 MIO 依赖的资源。
void mio_start(void *mio_obj)
启动 MIO:置 B_MIO_START,调用 pwm_init/io_init 配置物理输出通道。
参数: mio_obj — sound_mio_obj* 对象指针。
void mio_kick(void *mio_obj, u32 dac_packt)
每 DAC 数据包调用一次,把播放进度同步给 MIO:置 B_MIO_KICK、累计 dac_cnt。
参数:
mio_obj—sound_mio_obj*对象指针;dac_packt— 当前 DAC 数据包序号/长度,用于与播放进度对齐。
void mio_run(void)
消费一次 kick:按 io_mask 从 r_buf 取状态,调用 pwm_run/io_run 刷新硬件,清除 B_MIO_KICK。由应用在播放任务中周期调用。
bool mio_open(void **pp_obj, void *pfile, void *arg)
打开 MIO:分配并初始化 sound_mio_obj(或接收外部对象),读取并校验 mio_info(logo/version),成功后置 B_MIO_EN。
参数:
pp_obj— 输出参数,返回对象指针;pfile— 音频文件句柄(支持vfs_read);arg— 保留参数。
返回: true 打开成功,false 失败(文件头非法或资源不足)。
失败场景: info.logo != MIO_LOGO 或 info.version 不匹配时拒绝打开。
void mio_close(void **pp_obj)
关闭并释放 MIO 对象:复位状态位,释放文件/资源。
参数: pp_obj — 对象指针的指针,关闭后置空。
void mio_a_hook_init(sound_mio_obj *obj)
绑定默认平台实现到对象(mio_api.c)。绑定的钩子:
| 钩子 | 默认实现 | 行为 |
|---|---|---|
read | mio_a_read | 经 vfs_read 读取数据流(mio_api.c) |
pwm_init | mio_a_pwm_init | 配置 IO_PORTA_15 复用 TIMER0 PWM,3 kHz 载波(mio_api.c) |
pwm_run | mio_a_pwm_run | 写占空比 PRD*duty/255,中断保护(mio_api.c) |
io_init | mio_a_io_init | JL_PORTA 按掩码配置下拉输出(mio_api.c) |
io_run | mio_a_io_run | 按掩码写入 IO 电平(mio_api.c) |
失败模式、边界情况与并发
文件头校验失败
mio_open 依赖 struct mio_info 头部的 logo(0X55AA1212)与 version 校验。若传入的 pfile 不是 MIO 资源(例如普通音频文件或损坏数据),校验失败,mio_open 返回 false。这是设计上刻意设置的"准入检查",防止用随机数据驱动 IO 造成误动作。
通道数越界
MIO_MAX_CHL 固定为 16、PWM 为 1、IO 为 15;r_buf 容量按此计算(MIO_MAX_RBUF = 3 字节)。若数据流头部声明的 pwm_total/io_total 超过上限,io_mask 与 r_buf 会溢出——调用方必须保证文件头与编译期常量一致。源码中通过常量集中定义(mio_api.h)来约束这一契约。
中断/任务并发
mio_a_pwm_run 在写 JL_TIMER0->PWM 时用 local_irq_disable()/local_irq_enable() 包裹(mio_api.c)。原因是 DAC 数据包回调与 MIO 刷新可能处于不同优先级上下文,若 PWM 寄存器更新被中断打断,会出现"半更新"的占空比毛刺。IO 写入(mio_a_io_run)通过"先清后或"的两次寄存器操作保证最终值一致,未加锁,适合单任务顺序调用;若需多任务并发写 IO,调用方应自行串行化。
节流与丢包
dac_step/dac_used_cnt 的存在意味着 MIO 不一定每个 DAC 包都刷新硬件:当播放速率高于 IO 更新速率时,中间若干包的状态只累计计数、不输出,体现为"抽样刷新"。若 mio_run 调用频率低于 mio_kick,B_MIO_KICK 会持续置位直到被消费,不会丢状态(生产-消费握手),但输出会滞后于音频。
未使能时的降级
HAS_MIO_EN 未定义时,所有 d_mio_* 宏展开为空操作(mio_api.h),播放/录音链路完全不受影响——MIO 是可选增强,不是音频主链路的依赖。mio_api.c 内部另有 MIO_EN = 0 兜底(mio_api.c),即使编译进模块也不会触碰真实引脚,便于在无硬件的开发板上验证播放/录音功能。
性能与运维
- PWM 开销:3 kHz 载波由 TIMER0 硬件产生(
PRD = 4000@24 MHz),CPU 只在mio_run时写一次占空比寄存器,无中断负载,适合长时间随音乐运行。 - 数据通路:MIO 的数据经
vfs_read从文件流式读取,不整包载入内存,Flash 读带宽即瓶颈;sound_mio_obj本身只占用约几十字节 RAM 加r_buf3 字节,资源占用极小。 - 节流参数:调整
dac_step可权衡 IO 刷新频率与 CPU 占用;对快速变化的音频建议较小步进,对稳态电平可放大步进降低 GPIO 写次数。 - 运维提示:若产品启用 MIO,务必确认
HAS_MIO_EN与MIO_EN两个开关的状态一致,且MIO_API_IO_PORT/MIO_API_IO_OFFSET与板级原理图一致,避免驱动到未接线的引脚。
扩展点
- 弱符号 API 重写:
mio_module_init/mio_open/mio_start/mio_kick/mio_run/mio_close均为弱符号(mio_api.h),应用可在自己的源文件中提供同名强符号实现,替换整个状态机(例如把 IO 更新策略改为查表或算法生成)。 - 钩子函数指针替换:
mio_a_hook_init绑定默认实现后,应用可覆盖obj->pwm_run/obj->io_run等指针,实现自定义输出(如改接其他定时器、扩展 IO 数量、加 PWM 死区等),无需改动通用状态机。 - 数据源替换:
obj->read默认指向vfs_read(mio_api.c),替换为任意(void*, u8*, u32) -> u32函数即可让 MIO 消费内存缓冲、网络流等任意数据源。 - 通道布局裁剪:修改
MIO_MAX_CHL_PWM/MIO_MAX_CHL即可重新划分 PWM 与 IO 通道比例(注释注明 PWM 原可支持 4 路),随之调整r_buf容量与平台钩子。
测试情况
源码探索范围内未发现针对 MIO/播放/录音的独立单元测试文件。MIO_EN = 0 的兜底开关暗示 MIO 硬件路径依赖整机联调验证;播放/录音链路的验证依赖 init_app.c 的启动初始化序列与 app.c 的音量恢复逻辑在目标板上的行为。建议的验证路径:先以 HAS_MIO_EN=1、MIO_EN=1 在示波器上确认 IO_PORTA_15 输出 3 kHz 载波且占空比随音量变化,再确认 PORTA IO 通道电平随节奏翻转。