耳机开机后,应用层需要先准备好运行环境:读取配置数据、创建消息处理机制、初始化各业务模块,之后才能处理按键、连接、音乐播放等事件。

一、应用层从哪里开始

正常启动时,main() 完成平台侧准备,然后调用 app_init():

正文配图

app_init() 是应用层初始化的主要入口。看这个函数,可以了解 SDK 开机时启用了哪些模块,以及模块之间的初始化顺序和条件。

二、app_init() 初始化系统和模块

app_init() 下,关键的初始化函数有:

  1. nv_record_init():准备 NV 数据、工厂参数
  2. app_os_init():创建 app 邮箱和线程
  3. app_battery_open():检查电池、充电状态,确定启动分支
  4. app_key_open():按键检测及开机按键处理
  5. bt_app_init():蓝牙模块
  6. app_audio_open():音频模块

这里的初始化,是为了让模块具备处理业务的条件。例如,音频模块初始化完成,并不代表已经开始播放音乐,后面还需要播放事件触发。

三、建立应用线程和邮箱

在 app_init() 中,会调用 app_os_init():

正文配图

这个函数主要完成两件事:先创建 app 邮箱,再创建 app 线程。

正文配图

邮箱用于保存待处理的应用消息,app_thread 负责取出消息,并根据模块编号交给对应的处理函数。所以,这里的消息链路大致为:

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
    subgraph R0["① 消息投递"]
        direction LR
        R0N0["模块产生消息"]
        R0N1["放入 app 邮箱"]
        R0N0 --> R0N1
    end
    class R0N0,R0N1 blue;
    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
    subgraph R1["② 应用线程处理"]
        direction LR
        R1N0["app_thread 取出消息"]
        R1N1["调用对应模块的处理函数"]
        R1N0 --> R1N1
    end
    class R1N0,R1N1 green;
    style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
    R0 -..->|"通过邮箱交接"| R1
    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;

这是 app 线程的处理方式。蓝牙和音频等子系统也有各自的线程或回调,整个 SDK 由这些任务协作运行。

四、初始化完成后的工作

以上工作处理完成之后,系统则从准备模块进入处理事件的运行阶段。

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
    subgraph KEY["按键业务"]
        direction LR
        A["按键事件"] --> B["判断按键功能"] --> C["执行播放、音量等操作"]
    end
    subgraph BT["蓝牙业务"]
        direction LR
        D["蓝牙事件"] --> E["更新连接状态"] --> F["执行连接后的业务"]
    end
    subgraph BAT["电池业务"]
        direction LR
        G["电池事件"] --> H["更新电量状态"] --> I["上报电量或处理低电量"]
    end
    KEY ~~~ BT ~~~ BAT
    class A,B,C blue;
    class D,E,F green;
    class G,H,I amber;
    style KEY fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
    style BT fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
    style BAT fill:#fffbf2,stroke:#e5d0ab,color:#72501e
    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;

此时各线程由 RTOS 调度运行。main 线程则在 app_init() 初始化完成之后,进入 while 循环,通过 evt = osSignalWait(0x0, osWaitForever); 阻塞等待信号。如图所示:

正文配图

信号由其他模块主动发送。业务模块需要关机或复位时,通常调用 app_shutdown() 或 app_reset(),最终通过 osSignalSet() 通知 main 线程。这里关机信号为 0x04,复位信号为 0x08。

其他模块负责提出关机、复位请求,main 线程负责统一收尾并执行最终操作。这里的信号属于 RTOS 的线程通知,与 app_thread 使用的应用邮箱是两套机制。

五、邮箱和线程通知的区别

线程信号和邮箱都用于线程间通信。

  • 线程信号:用标志位表示某个事件发生了,例如关机、复位。同一标志位未清除前,重复发送会合并,不能记录发生次数。
  • 邮箱:保存一条条消息,可以携带事件类型和参数。成功投递的消息会排队,等待线程逐条处理。

因此,main 用信号接收关机、复位请求;app_thread 用邮箱接收并分发各模块的业务消息。