OpenHarmony 小鸿 AI 开发实战 10:CI1302 语音上行的 UART 帧、Opus 与 VAD
CI1302 不是把一段 WAV 文件一次性交给 WS63。它通过 UART2 连续发送带帧头、命令字、payload 长度和帧尾的协议帧唤醒事件触发会话0x0105携带一小帧 Opus0x0106表示本段 VAD 结束。WS63 在中断回调里只收字节在任务中解析再把 Opus 交给 Agent 通过 WebSocket 上行。本文依据当前 OpenHarmonymini、LiteOS-M 上的 CI1302 UART ring、帧解析、Opus bridge 和 Agent 排序逻辑整理。源码分支与第 08、09 篇相同本轮重新执行了后端protocol_smoke.py确认模拟的一帧上行能够进入 ASR→LLM→TTS 流程。但这不是新的实机麦克风录音也没有覆盖串口噪声、真实 VAD 阈值和弱网堆积。UART 中断只搬运数据不在回调里解析协议ci1302_audio_listen_task.c在UART_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 payload0x0106结束当前 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.c的AGENT_UPLINK_AUDIO_DATA_MAX同样为 80。agent_feed_audio_data()把一帧复制到 staging并投递AGENT_EVT_SEND_AUDIOAgent 任务取出后构造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 连续给出最后一个0x0105和0x0106两个事件进入不同处理路径若 Agent 先发送 JSONlisten/stop服务端可能立即开始 ASR而最后一帧二进制音频还在 staging。当前代码收到0x0106后只设置s_pending_listen_stop_after_uplink。try_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 stopMANUAL 由按键或显式结束控制。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_llmok与protocol_headersok。它能证明 Python 逻辑和协议顺序没有替代 CI1302 麦克风、UART 921600、真实 Opus 内容和无线链路的实机测试。因此本文结论是“当前源码已接通 CI1302 Opus 上行与 VAD stop 排序”不是“今天完成了新的端到端语音质量验收”。