杰理 SDK 文档中心
首页
首页
  • 项目概览

    • AD16N 系列芯片与 SDK 能力总览
  • 快速开始

    • 环境搭建与编译工具链
    • 编译构建指南
    • 烧录与固件升级
  • SDK 工程架构

    • SDK 目录结构与模块分层
    • 构建系统与批处理工具
    • BSP 板级支持包
  • mbox_flash 小音箱应用

    • 应用初始化与启动流程
    • 应用配置系统
    • 按键、UI 与用户交互
  • 音频子系统

    • 音频解码框架与调度
    • 音频格式解码器实现
    • MIDI 合成与播放
    • 音频编码与录音
    • EQ/DRC 与音效处理
    • DAC/ADC 音频接口与采样
  • 存储与文件系统

    • 媒体 IO 抽象层 MIO
    • 存储设备驱动
    • 文件系统支持
  • 平台系统库

    • 系统基础服务
    • CPU 平台与运行库
    • 固件升级与更新机制
    • 蓝牙与扩展连接接口
  • 电源与低功耗管理

    • 电源管理与低功耗设计
    • 锂电池充电管理
  • 硬件与文档参考

    • SDK 文档中心与版本发布记录
    • 芯片数据手册与硬件设计参考

媒体 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 输出设备(如氛围灯、振动马达),保证输出节奏与音频严格同步。

其设计思路是:

  1. 文件即波形:MIO 数据文件头部(struct mio_info)声明了 PWM 通道数、IO 通道数和采样速率(level),文件体按固定帧长存放各通道的占空比/电平数据。
  2. DAC 节拍驱动:MIO 不自行定时,而是由 DAC 填充回调(fill_audac.c)每消费一包音频数据调用 d_mio_kick(),把"已播放的采样数"换算为"应输出的帧数",从而与音频零漂移同步。
  3. 钩子解耦: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

字段语义:

字段类型含义
logou32魔数 MIO_LOGO (0X55AA1212),用于识别 MIO 文件
versionu32协议版本 MIO_VER_V1_1 (0X00000200)
data_lenu32数据体长度(当前实现未使用)
levelu8速率等级,决定 dac_step = 32 * level,且必须非 0
remainu8保留字段
rateu16名义采样率(当前实现未直接使用)
pwm_totalu8PWM 通道数,上限 MIO_MAX_CHL_PWM (1)
io_totalu8IO 通道数,上限 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 生效分支):

  1. 采样域归一化:dac_cnt 是 DAC 实际采样率下的累计消费数,先按 32000 参考采样率折算(tmp_cnt = dac_cnt * 320 / (dac_sr_read()/100)),再减去已折算输出的 dac_used_cnt,得到"待输出帧数等价采样量"。这样即使 DAC 采样率动态变化(如 44.1k/48k),MIO 帧率仍与音频时间轴一致。
  2. 步进判断:剩余量 ≥ dac_step 才允许输出一帧;否则清除 B_MIO_KICK 并返回 false,等待下一次 kick。
  3. 原子更新:dac_used_cnt += dac_step 放在关中断临界区内,防止与 mio_kick(中断上下文)竞争。
  4. 输出:PWM 通道取 r_buf[0..pwm_total) 每通道 1 字节占空比;IO 通道把帧内剩余字节打包成 16 位值(r_buf[i] | (r_buf[i+1] << 8))连同 io_mask 一次输出。
  5. 错误处理: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宏1PWM 通道数上限(注释保留 4 路的演进空间)
MIO_MAX_CHL_IO宏15IO 通道数上限
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_15PWM 输出引脚
PWM_FRE宏3000 (Hz)TIMER0 PWM 频率
MIO_API_IO_PORT宏JL_PORTAIO 输出端口基址
MIO_API_IO_OFFSET宏1IO 位域起始偏移
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_MEN0x8201内存不足(通道对象分配失败)
E_MIO_READ0x8202文件读取错误(头部读取异常时返回 false)
E_MIO_LOGO0x8203mio_check:头部 logo 非 0X55AA1212
E_MIO_VER0x8204mio_check:version 非 MIO_VER_V1_1
E_MIO_CHL0x8205mio_check:pwm_total > 1 或 io_total > 15 或两者之和为 0
E_MIO_END0x8206数据播完(预留,供上层判断结束)
E_MIO_LOCATE0x8207定位失败(预留)
E_MIO_LEVEL0x8208mio_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 处于典型的多上下文环境,代码对此做了分级保护:

  1. 生产者(中断上下文):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() 临界区。
  2. 消费者(解码线程):mio_run → mio_run_one 在解码线程上下文中执行帧读取与 GPIO/PWM 输出。
  3. 通道表保护:regist/unregist_mio_channel 对 g_mio_obj[i] 的写操作关中断,防止打开/关闭与 mio_run 轮询交错。
  4. 硬件寄存器保护: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 池,总量可忽略。

扩展点

  1. 平台移植:实现 read/pwm_init/pwm_run/io_init/io_run 5 个钩子并通过 mio_a_hook_init(或自定义 hook 函数)注入,即可在任意引脚/定时器上输出。当前参考实现见 mio_api.c。
  2. 弱符号覆盖:mio_module_init/mio_start/mio_kick/mio_run/mio_open/mio_close 均为 weak 声明,应用层可整体替换默认实现。
  3. 通道数扩展:调整 g_mio_obj[2] 的数组长度与 MAX_MIO_CHANNEL 即可支持更多并发通道(受 MIO_MAX_CHL 上限约束)。
  4. PWM 通道扩容:MIO_MAX_CHL_PWM 注释中保留了 4 路的历史值,扩展多路 PWM 需同步扩展 MIO_MAX_RBUF 与 r_size 计算。
  5. 新解码器接入: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] 变化,并核对与音频节拍的对齐关系。

Related Links

  • mio_api.h(协议与 API 声明)
  • mio_phy.c(核心物理层实现)
  • mio_api.c(平台适配钩子实现)
  • decoder_api.c(MIO 打开调用方)
  • fill_audac.c(DAC 节拍上报调用方)
  • errno-base.h(MIO 错误码)
  • 解码器整体流程与 VFS 文件系统,参见本目录下相邻页面
Next
存储设备驱动