OpenHarmony 小鸿 AI 开发实战 10:CI1302 语音上行的 UART 帧、Opus 与 VAD

CI1302 不是把一段 WAV 文件一次性交给 WS63。它通过 UART2 连续发送带帧头、命令字、payload 长度和帧尾的协议帧:唤醒事件触发会话,0x0105 携带一小帧 Opus,0x0106 表示本段 VAD 结束。WS63 在中断回调里只收字节,在任务中解析,再把 Opus 交给 Agent 通过 WebSocket 上行。

本文依据当前 OpenHarmony mini、LiteOS-M 上的 CI1302 UART ring、帧解析、Opus bridge 和 Agent 排序逻辑整理。源码分支与第 08、09 篇相同,本轮重新执行了后端 protocol_smoke.py,确认模拟的一帧上行能够进入 ASR→LLM→TTS 流程。但这不是新的实机麦克风录音,也没有覆盖串口噪声、真实 VAD 阈值和弱网堆积。

++UART 中断只搬运数据,不在回调里解析协议++

ci1302_audio_listen_task.cUART_INT_MODE 下注册 RX callback。回调检查长度后把收到的字节压入 ring,并立即返回;AudioListenTask 循环调用 parse_rx_ring_buff()。这样复杂状态机、日志和 Agent 事件不会运行在 UART 回调上下文。

复制代码
void uart_read_rx_int_handler(
    const void *buffer, uint16_t length, bool error)
{
    unused(error);
    if (buffer == NULL || length == 0) {
        return;
    }
    if ((uint32_t)length > CI1302_UART_RING_SIZE) {
        return;
    }
    ci1302_uart_ring_push(
        (const uint8_t *)buffer,
        (uint32_t)length);
}

当前 UART2 参数在板级代码中为 921600、8N1。高波特率减少音频帧排队时间,但并不保证业务不丢包;中断禁用过久、ring 满、Agent staging 未清空或 WebSocket 背压都可能成为后续瓶颈。

++4 KiB ring 隔离中断节奏与任务节奏++

环形缓冲大小是 4 * 1024。push 在临界区更新写指针;当下一位置追上读指针时,当前实现通过推进读指针丢弃最旧字节,为新数据腾空间。pop 每次取一个 byte,解析器可以跨多次 UART callback 延续状态。

复制代码
#define CI1302_UART_RING_SIZE (4 * 1024)

void ci1302_uart_ring_push(
    const uint8_t *data, uint32_t len);
bool ci1302_uart_ring_pop_byte(uint8_t *out);
uint32_t ci1302_uart_ring_used(void);

丢旧字节能避免生产者永久阻塞,但会破坏一帧的头、长度或尾。解析器因此必须能重新寻找帧头,而不能假设 ring 中第一个 byte 永远是完整帧起点。

++帧解析器按字节状态推进并重新同步帧头++

当前帧头固定为 A5 A5 5A 5A。解析器逐字节匹配头部,再读取命令字、payload length、payload 和 tail。0x0105 的最大 payload 被限制为 80 bytes;超长会打印 overflow 并丢弃,避免长度字段把静态数组写穿。

复制代码
uint8_t frame_head[4] = {
    0xA5, 0xA5, 0x5A, 0x5A
};

#define CI1302_MAX_FRAME_PAYLOAD  (80)
static uint8_t s_0105_payload[
    CI1302_MAX_FRAME_PAYLOAD];

帧尾不是只检查一个固定值;源码保存多组控制帧 tail,并对音频帧进行对应处理。文章不把完整私有协议表复制出来,只保留理解上行链所需的命令和边界。

++0x0102、0x0105 与 0x0106 承担不同职责++

0x0102 进入唤醒词事件路径;0x0105 是一帧 Opus payload;0x0106 结束当前 VAD segment。另一些命令用于播放拉取和状态反馈,例如 0x020A 请求 PCM、0x020D 表示播放侧状态。这些下行命令属于第 15 篇,本文只解释为何 parser 必须区分控制帧和音频帧。

复制代码
switch (cmd_type) {
case 0x0102:
    agent_send_event(AGENT_EVT_WAKE_WORD_DETECTED);
    break;
case 0x0106: {
    uint32_t total = s_segment_audio_data_len;
    s_segment_audio_data_len = 0;
    g_ci1302_last_vad_segment_bytes = total;
    agent_send_event(AGENT_EVT_CI1302_VAD_SEGMENT_END);
    break;
}
case 0x020A:
    msg_send = eAud_SendAudioData;
    ci1302_post_audio_event_with_retry(
        msg_send, "020A");
    break;
}

++0x0105 当前就是 Opus,不在 WS63 重新编码++

当 parser 完成 0x0105,先检查当前 Agent 状态是否允许上行,再累计本段字节数,最后调用 ci1302_uplink_send_opus_frame()。bridge 函数不解码、不重采样,只把 payload 交给 agent_feed_audio_data()

复制代码
if (s_cmd_type == 0x0105) {
    if (agent_should_uplink_ci1302_opus() &&
        (s_pl_len > 0U)) {
        s_segment_audio_data_len +=
            (uint32_t)s_pl_len;
        (void)ci1302_uplink_send_opus_frame(
            s_0105_payload,
            (size_t)s_pl_len);
    }
}

当前 mongoose_protocol.c 的实际宏也声明 "opus"。该文件顶部仍有一段历史注释写"上行为 PCM",但它与当前 parser、bridge 和服务端 libopus 解码路径冲突。事实检查应以可执行分支为准,并把旧注释视为需要修正的维护债务。

++Agent 只保留 80-byte staging,不能无限排队++

agent_task.cAGENT_UPLINK_AUDIO_DATA_MAX 同样为 80。agent_feed_audio_data() 把一帧复制到 staging,并投递 AGENT_EVT_SEND_AUDIO;Agent 任务取出后构造 protocol_audio_packet_t。如果当前帧尚未发送完成,新帧不能无界覆盖。

复制代码
#define AGENT_UPLINK_AUDIO_DATA_MAX  (80)

static uint8_t
    g_uplink_audio_data_staging[
        AGENT_UPLINK_AUDIO_DATA_MAX];

Mongoose 发送侧还有 4096-byte backlog 上限。超过限制时 mg_send_audio() 返回 false,让上层知道当前网络发送缓存已经过深。这个机制是背压保护,不是零丢包保证;真实弱网下仍要统计 drop、延迟和队列恢复。

++VAD stop 必须排在最后一帧 Opus 之后++

最容易出现的时序错误是:CI1302 连续给出最后一个 0x01050x0106,两个事件进入不同处理路径;若 Agent 先发送 JSON listen/stop,服务端可能立即开始 ASR,而最后一帧二进制音频还在 staging。

当前代码收到 0x0106 后只设置 s_pending_listen_stop_after_uplinktry_flush_pending_listen_stop() 会检查 staging 是否已经清空、状态是否允许,再调用 protocol_send_stop_listening()。源码注释明确把这个顺序作为修复目标。

复制代码
最后一帧 0x0105
  -> copy 到 staging
  -> AGENT_EVT_SEND_AUDIO
  -> WebSocket binary 发出
0x0106
  -> pending listen/stop
  -> staging drained
  -> JSON listen/stop

++短段、重复 VAD 与启动抖动都要过滤++

Agent 为 VAD stop 设定三层过滤:进入 LISTENING 后 500 ms settle window;段累计不足 400 bytes 时忽略;两次 stop 小于 800 ms 时去重。这样可以抑制 CI1302 重复上报 0x0106 或空段造成的多次 ASR。

复制代码
#define AGENT_VAD_STOP_DEDUP_WINDOW_MS  (800U)
#define AGENT_VAD_MIN_SEGMENT_BYTES      (400U)
#define AGENT_LISTENING_SETTLE_MS        (500U)

这些阈值是当前工程选择,不是 CI1302 或 Opus 的通用标准。更换 VAD、帧长或语音场景后,应通过真实语料统计重新调整,而不是把源码常量当成硬件规格。

++auto、manual 与 realtime 决定何时允许 stop 和上行++

agent_listen_mode_t 包含 AUTO、MANUAL 和 REALTIME。AUTO 与 REALTIME 接受 VAD segment stop;MANUAL 由按键或显式结束控制。REALTIME 在 SPEAKING 时仍允许 CI1302 Opus 上行,用于语音打断;其他模式播放 TTS 时不继续上传麦克风帧。

这说明"是否上行"不能只看 WebSocket 是否连接,还要同时看 Agent 状态、监听模式、pending stop 和 stop 已发送标志。把这些条件散落在 UART parser 中会很难维护,所以 parser 只调用 agent_should_uplink_ci1302_opus()

++验证应覆盖字节流、协议顺序与真实语音三层++

字节流层用拆分帧头、错误长度、错误帧尾和 ring overflow 测重新同步;协议层确认二进制帧先于 stop、短段被过滤、背压返回失败;真实语音层再检查录音、VAD、服务端 Opus 解码、ASR 文本和弱网体验。

本轮重新通过的 protocol_smoke.py 使用替身 ASR/LLM/TTS 验证一帧上行和协议编排,输出 asr_to_llm=okprotocol_headers=ok。它能证明 Python 逻辑和协议顺序,没有替代 CI1302 麦克风、UART 921600、真实 Opus 内容和无线链路的实机测试。因此本文结论是"当前源码已接通 CI1302 Opus 上行与 VAD stop 排序",不是"今天完成了新的端到端语音质量验收"。

相关推荐
nullregedit4 天前
OpenHarmony 小鸿 AI 开发实战 04:WS63 BurnTool 烧录、Loader、导出与回滚
openharmony·嵌入式开发·开源鸿蒙·ws63·burntool
nullregedit4 天前
OpenHarmony 小鸿 AI 开发实战 05:烧录成功仍黑屏的 GPIO14/PWR_ON 故障复盘
openharmony·gpio·故障排查·开源鸿蒙·ws63
nullregedit5 天前
OpenHarmony 小鸿 AI 开发实战 01:先认对工程,WS63 与 ESP32-P4 路径怎么选
openharmony·嵌入式开发·开源鸿蒙·esp32-p4·ws63
北京麟卓12 天前
卓奕引擎×OrangePi 5B开发板 | 面向开源鸿蒙的一站式安卓兼容验证方案
openharmony·安卓应用兼容
Industio_触觉智能14 天前
开源鸿蒙OpenHarmony再进阶,OneConnect规范即将升级为国家级行业标准
嵌入式硬件·物联网·硬件架构·智能硬件·openharmony·开源鸿蒙
北京麟卓15 天前
鸿蒙平板生态建设遇阻?卓奕引擎:安卓应用快速迁移+迁移成本直降80%以上
openharmony·安卓兼容
Industio_触觉智能1 个月前
瑞芯微RK3576落地智能农业新生态,鸿蒙农机平板应用场景拓展
嵌入式硬件·openharmony·开源鸿蒙·核心板·鸿蒙开发板·rk3576·农机平板
fakerth1 个月前
【OpenHarmony】communication_ipc模块
操作系统·openharmony
阿钱真强道2 个月前
29 鸿蒙LiteOS RK2206 Socket编程实战 UDP通信+LWIP原理全解析
udp·socket·鸿蒙·liteos·开源鸿蒙·瑞芯微·rk2206