媒体 IO 抽象层 MIO
MIO(Media IO)是杰理 AD16N 系列 MCU SDK 中用于驱动外部灯光/马达等 PWM/IO 外设的媒体 IO 抽象层:它将"数据文件 + DAC 播放节奏"转换为"PWM 占空比 + 多路 GPIO 电平输出",通过解耦的平台钩子(hook)实现与具体芯片引脚的适配。
Purpose and Scope
本页完整阐述 MIO 子系统的设计与实现,覆盖:
- MIO 文件协议(
mio_info头部)与运行时对象(sound_mio_obj) - 分层架构:核心物理层(
mio_phy.c)与应用适配层(mio_api.c) - 通道注册/注销、状态机与 DAC 节流驱动的完整控制流
- 配置选项、API 参考、错误码与并发/中断注意事项
本页不涉及以下内容(属于兄弟目录的独立主题):
- 具体 WAV/MP3 解码流程,参见解码器相关页面(MIO 仅在 decoder_api.c 中被打开并挂接)
- 文件系统(VFS)的挂载与寻址,参见存储/文件系统相关页面
- DAC 音频通路本身,MIO 只是 DAC 数据消费节奏(
d_mio_kick)的下游消费者
概述
MIO 解决的核心问题是:如何在音乐播放的同时,用音轨数据同步驱动多个 PWM/IO 输出设备(如氛围灯、振动马达),保证输出节奏与音频严格同步。
其设计思路是:
- 文件即波形:MIO 数据文件头部(
struct mio_info)声明了 PWM 通道数、IO 通道数和采样速率(level),文件体按固定帧长存放各通道的占空比/电平数据。 - DAC 节拍驱动:MIO 不自行定时,而是由 DAC 填充回调(
fill_audac.c)每消费一包音频数据调用d_mio_kick(),把"已播放的采样数"换算为"应输出的帧数",从而与音频零漂移同步。 - 钩子解耦:
sound_mio_obj内保存 5 个函数指针(read/pwm_init/pwm_run/io_init/io_run),平台层通过mio_a_hook_init()注入基于vfs、TIMER0、GPIO 的具体实现;核心层只依赖指针,不感知硬件。
编译期通过 HAS_MIO_EN 宏整体开关;未使能时所有 d_mio_* 宏展开为空操作,实现零开销裁剪。
架构
flowchart TD
subgraph sg_App["应用/解码层"]
Decoder["WAV 解码器<br/>decoder_api.c"]
FillAudac["DAC 填充回调<br/>fill_audac.c"]
end
subgraph sg_MIO["MIO 抽象层"]
Phy["mio_phy.c<br/>核心状态机 / 通道管理"]
Api["mio_api.c<br/>平台适配钩子"]
Obj["sound_mio_obj<br/>通道对象"]
Info["mio_info<br/>文件头协议"]
end
subgraph sg_HW["平台资源"]
VFS["VFS 文件系统"]
Timer0["TIMER0 PWM<br/>IO_PORTA_15"]
GPIO["JL_PORTA GPIO<br/>IO 输出"]
end
Decoder -->|"d_mio_open / 传 pfile"| Phy
Decoder -->|"mio_a_hook_init 注入钩子"| Api
Api -->|"安装 read/pwm/io 指针"| Obj
FillAudac -->|"d_mio_kick(dac_packet_num)"| Phy
Phy -->|"读写"| Obj
Obj --> Info
Obj -->|"read 钩子"| Api
Api -->|"vfs_read"| VFS
Obj -->|"pwm_run / io_run 钩子"| Api
Api -->|"TIMER0 PWM 占空比"| Timer0
Api -->|"GPIO 电平"| GPIO
架构说明:
mio_phy.c(核心层):持有全局通道表g_mio_obj[2],负责通道注册/注销、状态位管理、mio_open/close的校验流程,以及mio_run()的批量驱动。该层由DECODER_WAV_EN条件编译保护。mio_api.c(适配层):提供mio_a_*系列平台实现并通过mio_a_hook_init()注入对象;由HAS_MIO_EN条件编译保护。当前实现:PWM 走IO_PORTA_15的 TIMER0 输出(3 kHz),IO 走JL_PORTA从BIT(1)起的若干位。sound_mio_obj(运行时对象):核心层与适配层之间的唯一契约,包含状态、缓冲、节流计数器和 5 个函数指针。- 调用方:
decoder_api.c在解码 WAV 时打开 MIO 文件;fill_audac.c在 DAC 数据被消费时上报节拍。
数据结构与文件协议
MIO 文件头 struct mio_info
MIO 数据文件以固定头部开头,声明通道布局与速率,定义于 mio_api.h:
//-- MIO struct
struct mio_info {
u32 logo;
u32 version;
u32 data_len;
u8 level;
u8 remain;
u16 rate;
u8 pwm_total;
u8 io_total;
u8 remain1[2];
};
Source: mio_api.h
字段语义:
| 字段 | 类型 | 含义 |
|---|---|---|
logo | u32 | 魔数 MIO_LOGO (0X55AA1212),用于识别 MIO 文件 |
version | u32 | 协议版本 MIO_VER_V1_1 (0X00000200) |
data_len | u32 | 数据体长度(当前实现未使用) |
level | u8 | 速率等级,决定 dac_step = 32 * level,且必须非 0 |
remain | u8 | 保留字段 |
rate | u16 | 名义采样率(当前实现未直接使用) |
pwm_total | u8 | PWM 通道数,上限 MIO_MAX_CHL_PWM (1) |
io_total | u8 | IO 通道数,上限 MIO_MAX_CHL_IO (15) |
设计意图:头部将"通道配置"与"数据体"分离,解码器无需预先知道外设布局——读头部即可得到每帧数据长度 r_size = pwm_total + (io_total + 7) / 8 字节(PWM 每通道 1 字节占空比,IO 通道按位打包,8 通道一组占 1 字节)。
运行时对象 sound_mio_obj
typedef struct _sound_mio_obj {
u8 status;
u16 io_mask;
u8 r_buf[MIO_MAX_RBUF];
u16 r_size;
u16 dac_step;
u32 dac_cnt;
u32 dac_used_cnt;
struct mio_info info;
void *pfile;
u32(*read)(void *, u8 *, u32);
void (*pwm_init)(u32);
void (*pwm_run)(u32, u32);
void (*io_init)(u32);
void (*io_run)(u32, u32);
} sound_mio_obj;
Source: mio_api.h
status:状态位集合,见下文状态机。io_mask:IO 通道位掩码,由mio_port_init()根据io_total计算(mask <<= 1; mask++循环)。r_buf[MIO_MAX_RBUF]:单帧数据缓冲。MIO_MAX_RBUF = MIO_MAX_CHL_PWM + (MIO_MAX_CHL_IO+7)/8 = 1 + 2 = 3字节。dac_step:每帧对应的 DAC 采样数(32 * level)。dac_cnt/dac_used_cnt:DAC 已消费采样累计值与已折算输出的累计值,两者之差驱动帧输出节奏。pfile:VFS 文件句柄。- 5 个函数指针:
read(读数据)、pwm_init/pwm_run(PWM 初始化/占空比)、io_init/io_run(IO 初始化/电平输出)——这就是"抽象层"的抽象点。
常量与编译开关
//-- number of MIO channel
#define MIO_MAX_CHL 16
#define MIO_MAX_CHL_PWM 1 //4
#define MIO_MAX_CHL_IO (MIO_MAX_CHL-MIO_MAX_CHL_PWM)
#define MIO_MAX_RBUF (MIO_MAX_CHL_PWM+(MIO_MAX_CHL_IO+7)/8)
//-- MIO logo
#define MIO_LOGO 0X55AA1212
#define MIO_VER_V1_1 0X00000200
#define B_MIO_EN BIT(0)
#define B_MIO_START BIT(1)
#define B_MIO_KICK BIT(2)
#define B_MIO_ERR BIT(7)
Source: mio_api.h
HAS_MIO_EN 为全局编译开关(定义于 app 配置中):为真时 d_mio_* 宏直接映射到 mio_* 函数;为假时全部展开为 ... 空参数列表(无操作),实现裁剪。
分层实现详解
核心物理层 mio_phy.c
该文件由 #if defined(DECODER_WAV_EN) && (DECODER_WAV_EN) 保护,仅当 WAV 解码使能时编译。核心数据结构是全局通道表:
sound_mio_obj *g_mio_obj[2];
Source: mio_phy.c
最多支持 2 个并发 MIO 通道(MAX_MIO_CHANNEL),指针数组便于按索引轮询。
通道注册与注销
static sound_mio_obj *regist_mio_channel(sound_mio_obj *obj)
{
u32 i;
void *pres = NULL;
for (i = 0; i < MAX_MIO_CHANNEL; i++) {
if (NULL == g_mio_obj[i]) {
break;
}
}
if (i >= MAX_MIO_CHANNEL) {
return NULL;
}
if (NULL == obj) {
obj = my_malloc(sizeof(sound_mio_obj), MM_MIO);
local_irq_disable();
g_mio_obj[i] = obj;
memset(obj, 0, sizeof(sound_mio_obj));
local_irq_enable();
return g_mio_obj[i];
} else {
return obj;
}
}
Source: mio_phy.c
- 线性扫描空闲槽位,满则返回
NULL(由mio_open转为失败)。 - 对象内存来自
my_malloc(..., MM_MIO)内存池(MM_MIO枚举定义于 my_malloc.h),便于内存统计与回收。 - 写全局表时用
local_irq_disable()/local_irq_enable()保护,避免与中断上下文中的mio_run()竞争。 - 若调用方已传入外部对象(非空
obj),则直接复用而不分配——这是为上层复用对象预留的入口。
注销(unregist_mio_channel)对称地清空槽位并 my_free 归还内存。
打开与校验 mio_open
bool mio_open(void **pp_obj, void *pfile, void *func)
{
if ((NULL == pfile) || (NULL == func)) {
return false;
}
void(*hook)(sound_mio_obj *);
hook = func;
*pp_obj = regist_mio_channel(*pp_obj);
if (NULL != *pp_obj) {
sound_mio_obj *obj;
obj = *pp_obj;
hook(obj);
obj->pfile = pfile;
vfs_seek(obj->pfile, 0, SEEK_SET);
if (sizeof(struct mio_info) != vfs_read(obj->pfile, &obj->info, sizeof(struct mio_info))) {
/* return E_MIO_READ; */
return false;
}
mio_port_init(obj);
u32 err = mio_check(obj);
if (err != 0) {
mio_close((void **)&obj);
log_info("mio_check err : 0x%x\n", err);
return false;
}
obj->status = B_MIO_EN;
return true;
}
return false;
}
Source: mio_phy.c
打开流程依次为:注册通道 → 注入钩子(hook(obj))→ 定位文件头并读取 mio_info → 初始化端口(mio_port_init)→ 校验(mio_check)→ 置 B_MIO_EN。任一步失败即回滚关闭。mio_check 逐项校验:
u32 mio_check(sound_mio_obj *obj)
{
if (MIO_LOGO != obj->info.logo) {
return E_MIO_LOGO;
}
if (MIO_VER_V1_1 != obj->info.version) {
return E_MIO_VER;
}
if ((obj->info.pwm_total > MIO_MAX_CHL_PWM) || (obj->info.io_total > MIO_MAX_CHL_IO) || (obj->info.pwm_total + obj->info.io_total) == 0) {
return E_MIO_CHL;
}
obj->r_size = obj->info.pwm_total + (obj->info.io_total + 7) / 8;
if (0 == obj->info.level) {
log_info("mio level is 0 err!\n");
return E_MIO_LEVEL;
}
obj->dac_step = 32 * obj->info.level;
log_info("mio_check ok\n");
return 0;
}
Source: mio_phy.c
校验完成后同步推导出两个关键运行参数:每帧字节数 r_size 与每帧 DAC 采样步长 dac_step = 32 * level(level 相当于速率倍率:level 越大,每帧播放越快)。
状态机
MIO 通道状态由 obj->status 的 4 个位组合表达:
stateDiagram-v2
[*] --> Idle: 未注册/已关闭
Idle --> Enabled: mio_open 校验通过 (B_MIO_EN)
Enabled --> Working: mio_start (B_MIO_EN | B_MIO_START)
Working --> Waiting: 无 DAC 余量,清除 B_MIO_KICK
Waiting --> Working: 下一次 d_mio_kick (B_MIO_KICK)
Working --> Error: 文件读取失败 (B_MIO_ERR)
Error --> Idle: mio_close 重置 status=0
Enabled --> Idle: mio_close
Waiting --> Idle: mio_close
| 状态位 | 含义 |
|---|---|
B_MIO_EN (BIT0) | 通道已成功打开并通过校验 |
B_MIO_START (BIT1) | 已调用 mio_start,允许输出 |
B_MIO_KICK (BIT2) | 本帧有待输出的 DAC 节拍(由 mio_kick 置位,输出后被清除) |
B_MIO_ERR (BIT7) | 读取错误,通道进入不可用状态 |
关键判定宏:
MIO_WORKING = B_MIO_EN | B_MIO_START:mio_kick要求通道处于该状态(且无B_MIO_ERR)才累计节拍。MIO_ACTIVE = B_MIO_EN | B_MIO_KICK | B_MIO_START:mio_run_one要求三个位同时成立才开始驱动输出——即"已打开 + 已启动 + 有待消费的节拍"。
核心流程:DAC 节拍驱动的端到端执行
sequenceDiagram
participant Dec as WAV 解码器<br/>decoder_api.c
participant Phy as MIO 物理层<br/>mio_phy.c
participant Hook as 平台钩子<br/>mio_api.c
participant VFS as VFS 文件
participant DAC as DAC 填充<br/>fill_audac.c
participant HW as GPIO / TIMER0
Dec->>Phy: d_mio_open(&mio, pfile, mio_a_hook_init)
Phy->>Hook: mio_a_hook_init(obj)
Hook-->>Phy: 注入 read/pwm_init/pwm_run/io_init/io_run
Phy->>VFS: vfs_seek(0) + vfs_read(mio_info)
Phy->>Phy: mio_check(logo/version/chl/level)<br/>推导 r_size 与 dac_step
Phy->>Phy: mio_port_init() → pwm_init / io_init
Phy-->>Dec: true, status = B_MIO_EN
Dec->>Phy: d_mio_start(obj) → B_MIO_START
loop 播放循环(每包 DAC 数据)
DAC->>Phy: d_mio_kick(obj, dac_packet_num)
Phy->>Phy: dac_cnt += packet; status |= B_MIO_KICK; kick_decoder()
Phy->>Phy: mio_run() → mio_run_one()
Phy->>Phy: 按 32000/实际采样率折算待输出帧数
Phy->>VFS: obj->read(pfile, r_buf, r_size)
Phy->>HW: pwm_run(buf[0..pwm_total))<br/>io_run(io_mask, buf 打包值)
end
节拍折算算法 mio_run_one
mio_run_one 是 MIO 的"心跳"核心(mio_phy.c):
#define MIO_ACTIVE (B_MIO_EN | B_MIO_KICK | B_MIO_START)
static bool mio_run_one(void *mio_obj)
{
if (NULL == mio_obj) {
return false;
}
sound_mio_obj *obj = mio_obj;
if (MIO_ACTIVE != (obj->status & (MIO_ACTIVE | B_MIO_ERR))) {
return false;
}
#if 1
u64 tmp_cnt = obj->dac_cnt;
tmp_cnt = tmp_cnt * (32000 / 100) / (dac_sr_read() / 100);
u32 tmp_dac_cnt = tmp_cnt;
tmp_dac_cnt -= obj->dac_used_cnt;
if (tmp_dac_cnt < obj->dac_step) {
obj->status &= ~B_MIO_KICK;
return false;
}
local_irq_disable();
obj->dac_used_cnt += obj->dac_step;
local_irq_enable();
#else
if (obj->dac_cnt < obj->dac_step) {
obj->status &= ~B_MIO_KICK;
return false;
}
local_irq_disable();
obj->dac_cnt -= obj->dac_step;
local_irq_enable();
#endif
u32 size = obj->read(obj->pfile, obj->r_buf, obj->r_size);
if (size != obj->r_size) {
obj->status |= B_MIO_ERR;
return false;
}
u32 i = 0;
if (0 != obj->info.pwm_total) {
for (i = 0; i < obj->info.pwm_total; i++) {
obj->pwm_run(i, obj->r_buf[i]);
}
}
if (0 != obj->info.io_total) {
obj->io_run(obj->io_mask, obj->r_buf[i] | (obj->r_buf[i + 1] << 8));
}
return true;
}
Source: mio_phy.c
算法要点(#if 1 生效分支):
- 采样域归一化:
dac_cnt是 DAC 实际采样率下的累计消费数,先按32000参考采样率折算(tmp_cnt = dac_cnt * 320 / (dac_sr_read()/100)),再减去已折算输出的dac_used_cnt,得到"待输出帧数等价采样量"。这样即使 DAC 采样率动态变化(如 44.1k/48k),MIO 帧率仍与音频时间轴一致。 - 步进判断:剩余量 ≥
dac_step才允许输出一帧;否则清除B_MIO_KICK并返回false,等待下一次 kick。 - 原子更新:
dac_used_cnt += dac_step放在关中断临界区内,防止与mio_kick(中断上下文)竞争。 - 输出:PWM 通道取
r_buf[0..pwm_total)每通道 1 字节占空比;IO 通道把帧内剩余字节打包成 16 位值(r_buf[i] | (r_buf[i+1] << 8))连同io_mask一次输出。 - 错误处理:
read返回值不等于r_size(文件耗尽/读失败)置B_MIO_ERR并停止。
mio_run 则遍历全局表,对每个通道用 while 循环"榨干"所有可输出帧,避免一包 DAC 数据只能驱动一帧的滞后:
void mio_run(void)
{
u32 i;
for (i = 0; i < MAX_MIO_CHANNEL; i++) {
while (true == mio_run_one(g_mio_obj[i]));
}
}
Source: mio_phy.c
DAC 侧接入点
MIO 的两个外部接入点均通过 HAS_MIO_EN 宏隔离:
d_mio_kick(dac_mge.sound_later[i]->mio, dac_packet_num/*DAC_PACKET_SIZE*/);
Source: fill_audac.c
if (0 == mio_res) {
d_mio_open(&first_sound->mio, mio_pfile, (void *)mio_a_hook_init);
}
Source: decoder_api.c
d_mio_kick 把"本包 DAC 采样数"上报给 MIO 通道对象,并触发 kick_decoder() 唤醒解码线程;解码线程随后调用 mio_run() 消费节拍。这构成了 DAC 中断 → kick → 解码线程 run 的经典生产者-消费者模型,MIO 输出天然与音频播放进度对齐。
平台适配层 mio_api.c
适配层把抽象指针绑定到具体硬件。mio_a_hook_init 一次性完成全部注入:
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;
}
Source: mio_api.c
mio_a_read直接转发vfs_read,MIO 不关心文件来自 flash、SD 卡还是网络流。- PWM 输出使用 TIMER0:
mio_a_pwm_init将IO_PORTA_15配置为 TIMER0 PWM 输出(时钟 std24m、PWM_FRE=3000Hz);mio_a_pwm_run在关中断保护下更新占空比寄存器:
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
}
Source: mio_api.c
- IO 输出使用
JL_PORTA从BIT(1)起的位域:mio_a_io_init配置下拉输出(PU0清位、PD0置位、DIR输出、OUT清零),mio_a_io_run按io_ver写OUT寄存器。
设计意图:适配层内每个硬件操作都用独立的 #if MIO_EN(本仓库中为 0)包裹,便于在无硬件环境下编译验证;新增平台只需替换此文件或提供新的 hook 函数,核心层零改动。
使用示例
以下代码均从仓库实际源码中提取。
示例 1:打开 MIO 通道(解码器侧调用)
解码器在识别到 MIO 文件后,将 VFS 文件句柄与平台钩子一起交给 mio_open:
if (0 == mio_res) {
d_mio_open(&first_sound->mio, mio_pfile, (void *)mio_a_hook_init);
}
Source: decoder_api.c
示例 2:DAC 消费节拍上报(中断/填充侧)
fill_audac 每填充一包 DAC 数据即上报采样数,MIO 据此折算应输出的帧数:
d_mio_kick(dac_mge.sound_later[i]->mio, dac_packet_num/*DAC_PACKET_SIZE*/);
Source: fill_audac.c
示例 3:钩子注入(平台侧)
新平台只需实现 5 个函数并注入对象,即可接入 MIO:
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;
}
Source: mio_api.c
示例 4:按位打包的 IO 输出
IO 通道数可多达 15 路,帧内按位打包存储,输出时合并为 16 位值一次写入 GPIO 寄存器:
if (0 != obj->info.io_total) {
obj->io_run(obj->io_mask, obj->r_buf[i] | (obj->r_buf[i + 1] << 8));
}
Source: mio_phy.c
配置选项
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
HAS_MIO_EN | 编译宏 (0/1) | 由 app 配置决定 | 总开关:使能后 d_mio_* 映射到真实实现,否则为空操作 |
DECODER_WAV_EN | 编译宏 (0/1) | 由 app 配置决定 | mio_phy.c 的编译前提(MIO 绑定 WAV 解码器) |
MIO_MAX_CHL | 宏 | 16 | 系统 MIO 通道总数上限 |
MIO_MAX_CHL_PWM | 宏 | 1 | PWM 通道数上限(注释保留 4 路的演进空间) |
MIO_MAX_CHL_IO | 宏 | 15 | IO 通道数上限 |
MIO_MAX_RBUF | 宏 | 3 | 单帧数据缓冲字节数(1 + (15+7)/8) |
MIO_LOGO | 宏 | 0X55AA1212 | 文件头魔数 |
MIO_VER_V1_1 | 宏 | 0X00000200 | 当前协议版本 |
MIO_EN | 宏 (mio_api.c) | 0 | 平台层 PWM/IO 实际输出使能(硬件调试用) |
MIO_API_PWM_PORT | 宏 | IO_PORTA_15 | PWM 输出引脚 |
PWM_FRE | 宏 | 3000 (Hz) | TIMER0 PWM 频率 |
MIO_API_IO_PORT | 宏 | JL_PORTA | IO 输出端口基址 |
MIO_API_IO_OFFSET | 宏 | 1 | IO 位域起始偏移 |
MAX_MIO_CHANNEL | 宏 | 2 | 全局通道槽位数(g_mio_obj[2]) |
MM_MIO | 内存池枚举 | — | 通道对象分配的内存池标识(my_malloc.h) |
API 参考
void mio_module_init(void)
清空全局通道表 g_mio_obj(memset 为 0),在系统启动阶段调用。
- 参数:无
- 返回:无
- Source: mio_phy.c
void mio_start(void *mio_obj)
使通道进入可输出状态。
- 参数:
mio_obj— 通道对象指针;为NULL时直接返回。 - 行为:若已置
B_MIO_EN,则附加B_MIO_START。 - Source: mio_phy.c
void mio_kick(void *mio_obj, u32 dac_packt)
上报 DAC 已消费的采样数(节拍)。
- 参数:
mio_obj— 通道对象;dac_packt— 本包 DAC 采样数。 - 前置条件:通道状态等于
MIO_WORKING(B_MIO_EN | B_MIO_START)且无B_MIO_ERR,否则忽略。 - 行为:
dac_cnt += dac_packt,置B_MIO_KICK,调用kick_decoder()唤醒解码线程。 - 注意:通常在中断/填充上下文调用,本身不直接操作共享计数(累计操作不在此关中断,但消费侧
dac_used_cnt的更新在临界区)。 - Source: mio_phy.c
void mio_run(void)
遍历全部通道并连续驱动输出(while 榨干模式)。
- 参数:无
- 返回:无
- Source: mio_phy.c
bool mio_open(void **pp_obj, void *pfile, void *func)
打开 MIO 数据文件并初始化通道。
- 参数:
pp_obj— 通道对象指针的指针(可传入既有对象以复用)。pfile— VFS 文件句柄,不能为NULL。func— 钩子注入函数(如mio_a_hook_init),不能为NULL。
- 返回:
true成功(status = B_MIO_EN);false失败(参数非法、通道满、头部读取失败或mio_check校验失败)。 - 校验错误:
E_MIO_LOGO/E_MIO_VER/E_MIO_CHL/E_MIO_LEVEL(详见错误码表)。 - Source: mio_phy.c
void mio_close(void **pp_obj)
关闭通道:清零状态、复位端口、关闭 VFS 文件、注销并释放通道对象。
- 参数:
pp_obj— 通道对象指针的指针;关闭后置NULL。 - Source: mio_phy.c
void mio_a_hook_init(sound_mio_obj *obj)
注入平台适配钩子(read/pwm_init/pwm_run/io_init/io_run)。
- 参数:
obj— 目标通道对象。 - Source: mio_api.c
所有
mio_*函数均以__attribute__((weak))声明(mio_api.h),允许上层覆盖默认实现。
失败模式与边界情况
MIO 专用错误码
错误码定义于 errno-base.h,基址 0x8200:
| 错误码 | 值 | 触发场景 |
|---|---|---|
E_MIO_NO_MEN | 0x8201 | 内存不足(通道对象分配失败) |
E_MIO_READ | 0x8202 | 文件读取错误(头部读取异常时返回 false) |
E_MIO_LOGO | 0x8203 | mio_check:头部 logo 非 0X55AA1212 |
E_MIO_VER | 0x8204 | mio_check:version 非 MIO_VER_V1_1 |
E_MIO_CHL | 0x8205 | mio_check:pwm_total > 1 或 io_total > 15 或两者之和为 0 |
E_MIO_END | 0x8206 | 数据播完(预留,供上层判断结束) |
E_MIO_LOCATE | 0x8207 | 定位失败(预留) |
E_MIO_LEVEL | 0x8208 | mio_check:level == 0(速率等级无效) |
关键边界与失败路径
- 文件提前结束:
mio_run_one中read返回值不等于r_size时置B_MIO_ERR并返回false。此后MIO_ACTIVE判定被B_MIO_ERR屏蔽,通道停止输出——因此 MIO 数据文件长度必须是r_size的整数倍。 - 通道表满:
regist_mio_channel返回NULL,mio_open返回false。系统最多 2 个并发通道(g_mio_obj[2])。 - 参数校验:
mio_open对pfile/func判空;mio_start/mio_kick/mio_run_one对对象判空——失败静默返回,不产生崩溃。 - 未启动即 kick:
mio_kick要求B_MIO_START已置位,否则丢弃节拍,避免"先输出后打开"的乱序。 - level 异常:
level == 0使dac_step = 0,会导致tmp_dac_cnt < 0判定恒假、帧率无限——因此在mio_check阶段直接拒绝。
并发与中断安全
MIO 处于典型的多上下文环境,代码对此做了分级保护:
- 生产者(中断上下文):
fill_audac在 DAC 中断/填充路径调用mio_kick,仅做dac_cnt += dac_packt累加并唤醒解码线程。累加操作与消费侧之间通过dac_used_cnt的"读写分离"避免竞争:mio_run_one先无锁读取dac_cnt快照做折算,仅在提交dac_used_cnt += dac_step时进入local_irq_disable()/enable()临界区。 - 消费者(解码线程):
mio_run → mio_run_one在解码线程上下文中执行帧读取与 GPIO/PWM 输出。 - 通道表保护:
regist/unregist_mio_channel对g_mio_obj[i]的写操作关中断,防止打开/关闭与mio_run轮询交错。 - 硬件寄存器保护:
mio_a_pwm_run更新 TIMER0 PWM 寄存器同样关中断,保证占空比写原子性。
注意:
dac_cnt的累加未在临界区内,但由于消费侧只在"折算后的剩余量 ≥ dac_step"时提交dac_used_cnt,且dac_used_cnt单调追赶,最坏情况只是多输出一帧的误差,不会产生负值或数据竞争导致的崩溃。
性能与运行注意事项
- L2 缓存驻留:
mio_kick用AT(.audio_d.text.cache.L2)标注(mio_phy.c),将其代码段放入音频专用 L2 缓存,降低中断路径抖动——这是音频实时链路的常见优化。 - 批量榨干模式:
mio_run的while循环在单次唤醒中连续输出多帧,摊薄线程切换与文件寻址开销。 - 中断开销:每包 DAC 数据一次
mio_kick,仅做整数累加与标志置位,无系统调用;真正的 I/O 在解码线程完成。 - 64 位中间运算:采样率折算使用
u64 tmp_cnt中间量避免溢出(dac_cnt为 32 位,乘以 320 后需要 64 位承载)。 - 内存占用:每通道对象约
sizeof(sound_mio_obj)字节(含 3 字节r_buf与头部),来自MM_MIO池,总量可忽略。
扩展点
- 平台移植:实现
read/pwm_init/pwm_run/io_init/io_run5 个钩子并通过mio_a_hook_init(或自定义 hook 函数)注入,即可在任意引脚/定时器上输出。当前参考实现见 mio_api.c。 - 弱符号覆盖:
mio_module_init/mio_start/mio_kick/mio_run/mio_open/mio_close均为 weak 声明,应用层可整体替换默认实现。 - 通道数扩展:调整
g_mio_obj[2]的数组长度与MAX_MIO_CHANNEL即可支持更多并发通道(受MIO_MAX_CHL上限约束)。 - PWM 通道扩容:
MIO_MAX_CHL_PWM注释中保留了4路的历史值,扩展多路 PWM 需同步扩展MIO_MAX_RBUF与r_size计算。 - 新解码器接入:
decoder_api.c中d_mio_open的调用模式可复制到其他解码器(前提是文件头满足mio_info协议)。
测试与验证
仓库未提供独立的 MIO 单元测试文件;验证依赖集成路径:
- 编译期由
HAS_MIO_EN与DECODER_WAV_EN双重条件编译控制,未使能时验证宏展开为空操作(零副作用)。 - 运行期
mio_check打印"mio_check ok"或"mio_check err : 0x%x"、"mio level is 0 err!"等日志(log_info),是定位 MIO 文件格式问题的主要手段。 - 硬件验证建议:构造
pwm_total=1, io_total=0, level=N的最小 MIO 文件,观察IO_PORTA_15占空比随r_buf[0]变化,并核对与音频节拍的对齐关系。