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

    • SDK 简介与核心特性
    • 芯片平台与硬件资料
    • SDK 版本与发布信息
  • 快速开始

    • 环境搭建与工具链
    • 编译工程
    • 烧录与量产工具
  • 工程结构与构建系统

    • 工程目录布局
    • 构建与链接配置
  • 应用层开发

    • mbox_flash 应用框架
    • 板级支持包 (BSP)
    • 公共应用模块
    • UI 显示子系统
  • 蓝牙子系统

    • BLE 控制器、链路层与 HCI 传输
    • GATT 服务框架
    • BLE 应用示例:遥控器 / Dongle / 对讲机
    • 经典蓝牙支持
  • 音频子系统

    • 音频编解码器
    • 音频设备接口 (DAC / ADC / APA)
    • 音效处理与 EQ
    • 播放、录音与 MIO 工作流
  • 设备与文件系统

    • 存储设备驱动 (NorFlash / SDMMC / USB)
    • 文件系统 (FAT / nor_fs / SYDF)
    • 设备管理框架 (dev_mg)
  • 系统服务与电源管理

    • 消息机制 (msg / hot_msg)
    • 配置与参数存储 (app_config / VM)
    • 电源管理 (SOFT OFF / POWER DOWN)
  • 固件升级

    • 升级框架总览 (code_v1 / code_v2)
    • 双 Bank 升级机制
    • 升级通道:UART / 测试盒 / BLE OTA / USB / SD
  • 补丁包与版本维护

    • 版本升级补丁链 (v1.1.0 → v1.4.0)
    • 问题修复补丁
    • 固件裁剪与资源优化
  • 开发工具与支持

    • 辅助工具与脚本
    • 文档、配置说明与常见问题

播放、录音与 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_CHL16MIO 最大通道总数
MIO_MAX_CHL_PWM1其中 PWM 通道数(注释注明原可为 4)
MIO_MAX_CHL_IO15剩余 IO 通道数
MIO_MAX_RBUF1 + (15+7)/8 = 3IO 状态环形缓冲所需字节数

文件头信息 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: 清状态, 释放资源

关键控制流解释:

  1. 打开:mio_open 分配/初始化 sound_mio_obj,先通过 mio_a_hook_init(或自定义钩子)绑定 5 个函数指针,再读取数据流头部的 mio_info 并用 logo/version 校验合法性,成功后置 B_MIO_EN。
  2. 启动:mio_start 置 B_MIO_START,并调用 pwm_init/io_init 配置物理通道(此时 3 kHz 载波开始输出)。
  3. 踢包:音频播放任务每消费一个 DAC 数据包,调用 mio_kick(obj, dac_packt),MIO 记录 dac_cnt 并置 B_MIO_KICK——这是"播放进度 → IO 输出"的同步点。
  4. 运行:mio_run 读取当前 io_mask,从 r_buf 取出下一组状态,调用 pwm_run/io_run 写入硬件,再清除 B_MIO_KICK。dac_step/dac_used_cnt 控制更新的节流粒度,避免每个包都刷新硬件。
  5. 关闭: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宏0mio_api.c 内部开关,控制 PWM/IO 引脚操作是否实际写入寄存器(mio_api.c)
MIO_MAX_CHL常量16MIO 最大通道数(mio_api.h)
MIO_MAX_CHL_PWM常量1PWM 通道数,其余为 IO 通道(mio_api.h)
MIO_LOGO常量0X55AA1212数据流头部校验标识(mio_api.h)
MIO_VER_V1_1常量0X00000200版本号(mio_api.h)
MIO_API_PWM_PORT宏IO_PORTA_15PWM 输出引脚(mio_api.c)
PWM_FRE宏3000PWM 载波频率(Hz),决定 TIMER0 周期(mio_api.c)
MIO_API_IO_PORT宏JL_PORTAIO 通道所在端口(mio_api.c)
MIO_API_IO_OFFSET宏1IO 掩码位偏移起点(mio_api.c)
dac_mode_init(n)函数16DAC 模式参数(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)。绑定的钩子:

钩子默认实现行为
readmio_a_read经 vfs_read 读取数据流(mio_api.c)
pwm_initmio_a_pwm_init配置 IO_PORTA_15 复用 TIMER0 PWM,3 kHz 载波(mio_api.c)
pwm_runmio_a_pwm_run写占空比 PRD*duty/255,中断保护(mio_api.c)
io_initmio_a_io_initJL_PORTA 按掩码配置下拉输出(mio_api.c)
io_runmio_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_buf 3 字节,资源占用极小。
  • 节流参数:调整 dac_step 可权衡 IO 刷新频率与 CPU 占用;对快速变化的音频建议较小步进,对稳态电平可放大步进降低 GPIO 写次数。
  • 运维提示:若产品启用 MIO,务必确认 HAS_MIO_EN 与 MIO_EN 两个开关的状态一致,且 MIO_API_IO_PORT/MIO_API_IO_OFFSET 与板级原理图一致,避免驱动到未接线的引脚。

扩展点

  1. 弱符号 API 重写:mio_module_init/mio_open/mio_start/mio_kick/mio_run/mio_close 均为弱符号(mio_api.h),应用可在自己的源文件中提供同名强符号实现,替换整个状态机(例如把 IO 更新策略改为查表或算法生成)。
  2. 钩子函数指针替换:mio_a_hook_init 绑定默认实现后,应用可覆盖 obj->pwm_run/obj->io_run 等指针,实现自定义输出(如改接其他定时器、扩展 IO 数量、加 PWM 死区等),无需改动通用状态机。
  3. 数据源替换:obj->read 默认指向 vfs_read(mio_api.c),替换为任意 (void*, u8*, u32) -> u32 函数即可让 MIO 消费内存缓冲、网络流等任意数据源。
  4. 通道布局裁剪:修改 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 通道电平随节奏翻转。

相关链接

  • MIO 头文件(对象模型与 API 声明)
  • MIO 平台实现(PWM/IO 钩子)
  • MIO 物理层状态机(mio_phy.c)
  • 播放初始化序列(audio_init / dac_mode_init / dac_init_api)
  • 音量恢复与 DAC 控制(dac_vol)
  • DAC 淡入淡出驱动
  • ADC 录音 API
  • 音频缓冲填充(fill_audac.c)
  • 音频任务踢包(sound_kick.c)
Prev
音效处理与 EQ