BES 芯片跨核通讯与共享内存设计原理
基于BES Best1600_SOC 项目代码实例分析
涉及核心:SensorHub、M33 (MCU)、M55 (DSP)
场景:陀螺仪头部追踪 → 空间音效渲染
目录
1. 系统整体架构
1.1 三核职能划分
┌─────────────────────────────────────────────────────────────────────┐
│ BES Best1600_SOC │
│ │
│ ┌──────────────┐ IPC/Core Bridge ┌──────────────────────┐ │
│ │ SensorHub │◄──────────────────────►│ M33 (MCU) │ │
│ │ │ SENS_MSG / TLV │ 主控核 / 中枢 │ │
│ │ • IMU 采集 │ │ • 协议栈 (BT/BLE) │ │
│ │ • 陀螺仪 │ │ • 业务逻辑 │ │
│ │ • 加速度计 │ │ • IPC 路由 │ │
│ │ • MotionEng │ │ • HID 报告生成 │ │
│ │ 四元数算法 │ │ • spa_algo 配置管理 │ │
│ └──────────────┘ └──────────┬───────────┘ │
│ │ │
│ Core Bridge │ Task CMD │
│ (MCU_DSP_M55_TASK_CMD_*) │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ M55 (DSP) │ │
│ │ 高性能音频处理核 │ │
│ │ • spa_algo 空间音效引擎│ │
│ │ • HRTF 滤波器 │ │
│ │ • 实时四元数输入 │ │
│ │ • 208/260 MHz 运行(按需)│ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
1.2 数据流向总览
[IMU 传感器]
│
│ SPI/I2C 硬件采样
▼
[SensorHub]
MotionEngine 算法
四元数 / 陀螺仪计算
│
│ app_core_bridge_send_cmd()
│ MCU_SENSOR_HUB_TASK_CMD_SEND_QUATERNION (0x027)
│ MCU_SENSOR_HUB_TASK_CMD_SEND_GYROSCOPE (0x028)
▼
[M33 MCU] ─────────────────────────────────
quaternion_send_receive_handler() │
gyroscope_send_receive_handler() │ BT HID 报告
│ │ app_bt_hid_process_sensor_report()
│ spa_algo_audio_quat_relay() ▼
│ app_dsp_m55_bridge_send_cmd() [手机 / 主机设备]
│ MCU_DSP_M55_TASK_CMD_RS3D_QUAT (0x194)
▼
[M55 DSP]
spa_algo 空间音效引擎
实时 HRTF 头部追踪渲染
│
│ 处理后音频数据
▼
[DAC / 扬声器输出]
2. 跨核通讯原理设计
2.1 Core Bridge 机制
BES 平台的跨核通讯基于 Core Bridge 框架,封装了底层硬件邮箱(Mailbox)中断,向上提供两类命令接口:
| 类型 | 宏名 | 特征 | 使用场景 |
|---|---|---|---|
| Task CMD | CORE_BRIDGE_TASK_COMMAND_TO_ADD |
有应答、有超时 | 需要确认的配置/数据 |
| Instant CMD | CORE_BRIDGE_INSTANT_COMMAND_TO_ADD |
无应答、即发即走 | 频率调整、状态广播 |
Core Bridge 命令注册示例
c
// 文件: services/audio_process/audio_process_spa_algo_m55_ipc.cpp
// 注册四元数转发命令(M33 → M55,无需响应)
CORE_BRIDGE_TASK_COMMAND_TO_ADD(
MCU_DSP_M55_TASK_CMD_RS3D_QUAT, // 命令码 0x194
"MCU2DSP: quaternion receive", // 调试字符串
quat_relay_tx_handler, // 发送侧 handler
nullptr, // 接收侧 handler(M33 不接收此命令)
0, // 超时 0ms
nullptr, nullptr, nullptr
);
// 注册控制命令(M33 ↔ M55 双向)
CORE_BRIDGE_TASK_COMMAND_TO_ADD(
MCU_DSP_M55_TASK_CMD_RS3D_CTRL, // 命令码 0x195
"MCU2DSP: control",
m55_ctrl_tx_handler, // TX: 发送配置
m55_ctrl_rx_handler, // RX: 处理 CFG_REQ / CFG_CONFIRM
0, nullptr, nullptr, nullptr
);
2.2 消息包格式
SensorHub ↔ M33 数据包
┌────────────────────────────────────────────────────────┐
│ app_core_bridge_data_packet_t (共 516 bytes) │
├────────────┬────────────┬───────────────────────────────┤
│ cmdcode │ cmdseq │ content[512] │
│ uint16_t │ uint16_t │ payload (实际数据) │
│ (2 bytes) │ (2 bytes) │ (最大 512 bytes) │
└────────────┴────────────┴───────────────────────────────┘
M33 ↔ M55 数据包
┌───────────────────────────────────────────────────────────┐
│ app_dsp_m55_bridge_data_packet_t (共 520 bytes) │
├────────────┬────────────┬────────────┬────────────────────┤
│ length │ cmdcode │ cmdseq │ content[512] │
│ uint32_t │ uint16_t │ uint16_t │ payload │
│ (4 bytes) │ (2 bytes) │ (2 bytes) │ (最大 512 bytes) │
└────────────┴────────────┴────────────┴────────────────────┘
源码位置:
apps/sensorhub/app_sensor_hub.cpp第 56-61 行apps/dsp_m55/app_dsp_m55.cpp第 53-59 行
2.2.1 执行流程与设计原理(M33 ↔ M55:app_dsp_m55_bridge_*)
这一节把 app_dsp_m55_bridge_send_cmd() 与 app_dsp_m55_bridge_data_received() 的执行链路拆开说明。
关键 API 与运行上下文:
- 发送侧入口(线程上下文,通常是业务线程/M33 侧调用):
c
// apps/dsp_m55/app_dsp_m55.h
int32_t app_dsp_m55_bridge_send_cmd(uint16_t cmd_code, uint8_t *p_buff, uint16_t length);
- 收包入口(IRQ 上下文,由底层 intersys/mailbox IRQ 回调进入):
c
// apps/dsp_m55/app_dsp_m55.h
unsigned int app_dsp_m55_bridge_data_received(const void* data, unsigned int len);
-
Instant vs Task 的处理差异(非常重要):
-
Instant CMD :在
app_dsp_m55_bridge_data_received()中直接调用命令表里的cmdhandler(),因此 handler 运行在 IRQ 上下文,必须"短平快"(不要打印大量日志/不要阻塞/不要做复杂 memcpy)。 -
Task CMD :在
app_dsp_m55_bridge_data_received()中只负责入队,然后由 RX 线程把数据出队并调用cmdhandler(),因此 handler 运行在线程上下文,允许做相对复杂处理。
A) 发送路径(app_dsp_m55_bridge_send_cmd → core_bridge_send → 远端)
[Caller Thread]
|
| app_dsp_m55_bridge_send_cmd(cmd_code, payload, len)
| - 复制 payload 到 tx_mailbox_heap
| - 投递到 osMailQ: app_dsp_m55_bridge_tx_mailbox
v
[core_bridge_tx_thread]
|
| osMailGet() 取出待发送消息
| 查表:app_dsp_m55_bridge_get_task_cmd_entry()/get_instant_cmd_entry()
| 调用:core_bridge_cmd_transmit_handler(payload, len)
v
[Transmit Handler (per-cmd)]
|
| (常见实现是二选一)
| app_dsp_m55_bridge_send_data_without_waiting_rsp()
| app_dsp_m55_bridge_send_data_with_waiting_rsp()
v
app_dsp_m55_bridge_transmit_data()
|
| 组包 app_dsp_m55_bridge_data_packet_t
| core_bridge_send((uint8_t*)&pkt, pkt.length + sizeof(uint32_t), trycnt)
| -> 内部调用 dsp_m55_send(),失败重试+delay
| 等待 tx_done 信号量(确保对端已取走/硬件发送完成)
v
[Remote Core IRQ] 触发接收
补充说明:
core_bridge_send()是apps/dsp_m55/app_dsp_m55.cpp内部静态封装,对dsp_m55_send()做重试与告警。app_dsp_m55_bridge_send_data_with_waiting_rsp()会在发送后等待app_dsp_m55_bridge_wait_cmd_rsp_id,由对端回MCU_DSP_M55_TASK_CMD_RSP并在本端 rsp handler 中释放信号量完成握手。
B) 接收路径(IRQ 入队/直调 → RX 线程分发 → cmdhandler)
[M55 intersys/mailbox IRQ]
|
| dsp_m55_core_register_rx_irq_handler(..., app_dsp_m55_bridge_data_received)
v
app_dsp_m55_bridge_data_received(data, len) // IRQ 上下文
|
| 解析 app_dsp_m55_bridge_data_packet_t
|
| if Instant CMD:
| - 查表 app_dsp_m55_bridge_get_instant_cmd_entry(cmdcode)
| - 直接 pInstance->cmdhandler(content, payload_len)
|
| else Task CMD:
| - app_dsp_m55_bridge_queue_push(CQueue, data, len)
| - osSignalSet(rx_thread, DATA_RECEIVED)
v
[core_bridge_rx_thread]
|
| while(queue not empty):
| - peek 长度
| - pop 到临时 buffer
| - app_dsp_m55_bridge_rx_handler(tmp, rcv_len)
v
app_dsp_m55_bridge_rx_handler() // 线程上下文
|
| 查表 app_dsp_m55_bridge_get_task_cmd_entry(cmdcode)
| 调用 pInstance->cmdhandler(content, payload_len)
v
[Command Handler]
2.2.3 回包/等待应答(Task CMD with RSP)的独立时序图
下面的时序图对应 *_send_data_with_waiting_rsp() 这一类"发起请求并等待对端应答"的模式。
A) M33 ↔ M55(app_dsp_m55_bridge_*)
M33 (Client/Caller) M33 Core-Bridge M55
| | |
| app_dsp_m55_bridge_send_cmd() | |
|---------------------------------->| |
| (enqueue tx mailbox) | |
| | core_bridge_tx_thread |
| | -> transmit_handler |
| | -> app_dsp_m55_bridge_
| | send_data_with_
| | waiting_rsp() |
| | -> transmit_data() |
| | -> core_bridge_send() |
| |----------------------->|
| | | app_dsp_m55_bridge_data_received()
| | | (Task CMD -> enqueue)
| | | core_bridge_rx_thread
| | | -> cmdhandler()
| | | -> app_dsp_m55_bridge_send_rsp(rspcode,...)
| |<-----------------------|
| | app_dsp_m55_bridge_data_received()
| | (RSP cmd -> enqueue)
| | core_bridge_rx_thread
| | -> app_dsp_m55_bridge_cmd_rsp_handler()
| | osSemaphoreRelease(wait_cmd_rsp)
|<----------------------------------| |
| (caller thread wait returns) | |
关键点:
- 发送侧真正"等待"的位置在
app_dsp_m55_bridge_send_data_with_waiting_rsp(),它等待app_dsp_m55_bridge_wait_cmd_rsp_id。 - 对端回包统一用
MCU_DSP_M55_TASK_CMD_RSP承载,payload 的开头会带上"被响应的原始 cmdcode",本端据此路由到对应rsp_handle。
B) SensorHub ↔ M33(app_core_bridge_*)
M33 (Client/Caller) M33 Core-Bridge SensorHub
| | |
| app_core_bridge_send_cmd() | |
|----------------------------------->| |
| (enqueue tx mailbox) | |
| | core_bridge_tx_thread |
| | -> transmit_handler |
| | -> app_core_bridge_
| | send_data_with_
| | waiting_rsp() |
| | -> transmit_data() |
| | -> sensor_hub_send() |
| |----------------------->|
| | | app_core_bridge_data_received()
| | | (Task CMD -> enqueue)
| | | core_bridge_rx_thread
| | | -> cmdhandler()
| | | -> app_core_bridge_send_rsp(rspcode,...)
| |<-----------------------|
| | app_core_bridge_data_received()
| | (RSP cmd -> enqueue)
| | core_bridge_rx_thread
| | -> app_core_bridge_cmd_rsp_handler()
| | osSemaphoreRelease(wait_cmd_rsp)
|<-----------------------------------| |
| (caller thread wait returns) | |
注意:SensorHub 侧 app_core_bridge_transmit_data() 内部对 sensor_hub_send() 有失败重试(最多 10 次)并延时 1ms,这是避免低层 IPC 暂时忙导致直接失败。
2.2.2 执行流程与设计原理(SensorHub ↔ M33:app_core_bridge_*)
SensorHub 通道的结构与 M55 通道类似,差别主要在于底层发送函数与 RTOS 实现(部分平台下使用 CMSIS-RTOS2 的 memory pool + message queue)。
关键 API 与运行上下文:
c
// apps/sensorhub/app_sensor_hub.h
int32_t app_core_bridge_send_cmd(uint16_t cmd_code, uint8_t *p_buff, uint16_t length);
unsigned int app_core_bridge_data_received(const void* data, unsigned int len);
A) 发送路径(app_core_bridge_send_cmd → app_sensor_hub_send → sensor_hub_send)
[Caller Thread]
|
| app_core_bridge_send_cmd(cmd_code, payload, len)
| - 复制 payload 到 tx_mailbox_heap
| - 投递到 message queue: app_core_bridge_tx_mailbox
v
[core_bridge_tx_thread]
|
| osMessageQueueGet() 取出待发送消息
| 查表:app_core_bridge_get_task_cmd_entry()/get_instant_cmd_entry()
| 调用:core_bridge_cmd_transmit_handler(payload, len)
v
app_core_bridge_transmit_data() / send_instant_cmd_data()
|
| 组包 app_core_bridge_data_packet_t
| app_sensor_hub_send(...)
| -> sensor_hub_send(...)
| -> 失败重试(最多 10 次) + osDelay(1)
| 等待 tx_done 信号量
v
[Remote Core IRQ] 触发接收
B) 接收路径(IRQ 入队/直调 → RX 线程分发 → cmdhandler)
[SensorHub intersys/mailbox IRQ]
|
| sensor_hub_core_register_rx_irq_handler(..., app_core_bridge_data_received)
v
app_core_bridge_data_received(data, len) // IRQ 上下文
|
| 解析 app_core_bridge_data_packet_t
|
| if Instant CMD:
| - 查表 app_core_bridge_get_instant_cmd_entry(cmdcode)
| - 直接 pInstance->cmdhandler(content, payload_len)
|
| else Task CMD:
| - app_core_bridge_queue_push(CQueue, data, len)
| - osThreadFlagsSet(rx_thread, DATA_RECEIVED)
v
[core_bridge_rx_thread]
|
| while(queue not empty):
| - peek 长度
| - pop 到临时 buffer
| - app_core_bridge_rx_handler(tmp, rcv_len)
v
app_core_bridge_rx_handler() // 线程上下文
|
| 查表 app_core_bridge_get_task_cmd_entry(cmdcode)
| 调用 pInstance->cmdhandler(content, payload_len)
v
[Command Handler]
Instant CMD 的注意事项同上:handler 运行在 IRQ 上下文,必须避免阻塞/耗时操作。
2.3 TLV 控制协议
SensorHub 头部追踪使能/禁用使用 TLV(Tag-Length-Value) 格式,便于协议扩展:
┌─────────┬─────────┬─────────┐
│ TAG │ LEN │ VALUE │
│ 1 byte │ 1 byte │ N byte │
└─────────┴─────────┴─────────┘
TAG::SENSOR (启停传感器):
TAG=0x01 LEN=0x01 VALUE=0x00(关) / 0x01(开) / 0x02(校准)
TAG::ARC_MODE (自动重定心):
TAG=0x02 LEN=0x01 VALUE=mode
TAG::GUID (传感器 GUID):
TAG=0x04 LEN=0x02 VALUE=guid[2]
c
// 文件: services/audio_process/audio_process_spa_algo_ipc.cpp
void spa_algo_audio_senhub_ipc_headtracking_enable(bool enable) {
uint8_t tlv[3];
tlv[0] = (uint8_t)IPC::TAG::SENSOR; // TAG
tlv[1] = IPC::SENSOR_LEN; // LEN = 1
tlv[2] = enable ? IPC::SENSOR_ON // VALUE
: IPC::SENSOR_OFF;
app_core_bridge_send_cmd(
MCU_SENSOR_HUB_TASK_CMD_MOTIONENGINE, tlv, sizeof(tlv));
}
2.4 M33 ↔ M55 配置协议(TAG 枚举)
c
// 文件: services/gaf_cc_stream/inc/gaf_cc_spa_algo_process_ipc_cmd.h
namespace M55_RS3D_IPC {
enum class TAG : uint8_t {
HEADTRACKING = 1, // 头部追踪 开/关
SPATIAL = 2, // 空间音效 开/关
SPATIAL_EQ = 3, // 空间音效均衡器配置
DRIVER_EQ = 4, // 驱动均衡器配置
PRESET = 5, // 音效预设
CFG_IND = 6, // 配置指示(M33 → M55,握手发起)
CFG_REQ = 7, // 配置请求(M55 → M33,索取参数)
CFG_SET = 8, // 配置下发(M33 → M55,参数写入)
CFG_CONFIRM = 9, // 配置确认(M55 → M33,参数生效)
};
}
2.5 四元数数据结构
c
// 四元数报告包(SensorHub → M33)
namespace IPC {
struct QuaternionRpt_s {
struct {
int16_t w, x, y, z; // 16-bit 定点四元数
} quat;
uint32_t timestamp; // 时间戳
// 总计: 12 bytes
};
}
// M55 RS3D 配置(M33 → M55)
namespace M55_RS3D_IPC {
struct Config_s {
bool spatial_en; // 空间音效总开关
bool headtracking_en; // 头部追踪开关
AUDIO_CONFIG_TYPE_T preset; // 音效预设
uint8_t hrtf[HRTF_ISB_LENGTH]; // HRTF 滤波器系数
};
struct ConfigReq_s { // 12 bytes
TAG tag; // 1 byte
uint8_t len; // 1 byte
uint16_t padding; // 2 bytes (对齐)
uint32_t sampleFreq; // 4 bytes (采样率)
Config_s *cfgPtr; // 4 bytes (配置指针)
};
}
3. 共享内存与缓冲区设计
本章先澄清两个容易混淆的概念:
- 跨核通讯(IPC) :用 Mailbox/中断把"命令/事件"从核 A 送到核 B。典型实现是 Core Bridge 的
TASK_CMD/INSTANT_CMD。 - 内存共享(Shared Memory):两核访问同一段物理 RAM,用于搬运大数据(PCM/大块参数)。共享内存本身不"传输",需要配套的同步协议(写指针/读指针/doorbell)。
工程里通常是"IPC 负责敲门 + 共享内存负责装货":
共享内存写入(Producer) ---> IPC(doorbell/通知) ---> 共享内存读取(Consumer)
3.1 缓冲区层次结构
┌─────────────────────────────────────────────────────────┐
│ M33 内存空间 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ SensorHub RX Ring Buffer (CQueue) │ │
│ │ 大小: APP_CORE_BRIDGE_RX_BUFF_SIZE = 2048 bytes │ │
│ │ 类型: 环形队列 (循环覆盖,无阻塞) │ │
│ │ 操作: │ │
│ │ 写入: EnCQueue() ← SensorHub 数据到达 │ │
│ │ 读取: DeCQueue() → 上层 handler 消费 │ │
│ │ 预查: PeekCQueue() → 无破坏性检查长度 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 四元数消息队列 (osMailQ) │ │
│ │ 容量: 4 元素 │ │
│ │ 元素类型: IPC::QuaternionRpt_s │ │
│ │ 内存池: 4 块预分配 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ M55 配置邮箱 (osMailQ) │ │
│ │ 容量: 4 元素 │ │
│ │ 元素类型: M55_RS3D_IPC::ConfigReq_s (12 bytes) │ │
│ │ 用途: M55 配置请求的异步处理 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Core Bridge TX Mailbox │ │
│ │ M33 → SensorHub: 最大 8 条目 │ │
│ │ M33 → M55: 最大 30 条目 │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
3.2 环形队列操作流程
SensorHub IPC 中断
│
▼
app_core_bridge_receive_irq_handler()
│
├─ [检查队列空间]
│
▼
EnCQueue(app_core_bridge_receive_queue, data, len)
│ ↑
│ 环形缓冲区满时
│ 丢弃最旧数据
▼
RTOS Task 调度
app_core_bridge_task()
│
├─ PeekCQueue() → 读取下一消息长度
├─ DeCQueue() → 取出消息数据
│
▼
app_core_bridge_receive_handler(cmdcode, data, len)
│
├─ cmdcode == MCU_SENSOR_HUB_TASK_CMD_SEND_QUATERNION
│ └─ quaternion_send_receive_handler()
│
├─ cmdcode == MCU_SENSOR_HUB_TASK_CMD_SEND_GYROSCOPE
│ └─ gyroscope_send_receive_handler()
│
└─ cmdcode == MCU_SENSOR_HUB_TASK_CMD_SEND_RAW_ACC
└─ raw_acc_send_receive_handler()
3.3 传感器原始数据缓冲(滑动窗口平均)
c
// 文件: services/audio_process/audio_process_spa_algo_ipc.cpp
#define GYRO_DATA_TOTAL_COUNT 5 // 滑动窗口大小
typedef struct {
uint16_t xindex, yindex, zindex; // 环形索引
int16_t x[GYRO_DATA_TOTAL_COUNT]; // X 轴历史采样
int16_t y[GYRO_DATA_TOTAL_COUNT]; // Y 轴历史采样
int16_t z[GYRO_DATA_TOTAL_COUNT]; // Z 轴历史采样
} RawGyroData_t;
// 写入:循环覆盖最旧采样
void spa_algo_gyro_data_set(int16_t gyro_data[3]) {
gyro_data_buf.x[gyro_data_buf.xindex++ % GYRO_DATA_TOTAL_COUNT] = gyro_data[0];
gyro_data_buf.y[gyro_data_buf.yindex++ % GYRO_DATA_TOTAL_COUNT] = gyro_data[1];
gyro_data_buf.z[gyro_data_buf.zindex++ % GYRO_DATA_TOTAL_COUNT] = gyro_data[2];
}
// 读取:对 5 个历史采样求均值,降噪
void spa_algo_caculate_gyro_data(void) {
for (uint8_t i = 0; i < GYRO_DATA_TOTAL_COUNT; i++) {
total.x += gyro_data_buf.x[i];
total.y += gyro_data_buf.y[i];
total.z += gyro_data_buf.z[i];
}
avg.x = total.x / GYRO_DATA_TOTAL_COUNT;
avg.y = total.y / GYRO_DATA_TOTAL_COUNT;
avg.z = total.z / GYRO_DATA_TOTAL_COUNT;
}
4. 启动与初始化流程
4.1 系统启动时序(M33 视角)
系统上电 / 复位
│
▼
M33 主程序初始化
│
├─────────────────────────────────────────────────────────┐
│ │
▼ ▼
app_sensor_hub_init() app_dsp_m55_init(user)
│ │
├─ app_core_bridge_init() ├─ app_dsp_m55_bridge_init()
│ └─ 初始化 RX 环形缓冲 (2048 bytes) ├─ 注册 RX/TX handlers
├─ 注册 SensorHub 命令 handlers ├─ dsp_m55_open()
├─ 创建接收任务 (RTOS Task) │ └─ 启动 M55 核心
└─ 等待 SensorHub ping ├─ osDelay(20ms)
│ (等待 M55 启动稳定)
├─ app_dsp_m55_init_module()
└─ osDelay(80ms)
│ │
└─────────────────┬───────────────────────────────────────┘
│
▼
spa_algo_audio_init()
│
┌──────────────┼────────────────┐
│ │ │
▼ ▼ ▼
spa_algo_audio_ spa_algo_audio_ spa_algo_audio_
senhub_ipc_ m55_ipc_ tota_init()
init() init()
│ │
├─ 创建四元数 ├─ 创建 mailQId
│ 消息队列(4) │ (4 × ConfigReq_s)
└─ 创建内存池(4) └─ 创建 spa_algo_config
_processor_thread
(priority: AboveNormal)
│
▼
spa_algo_audio_m55_ipc_cfg_ind()
(发送 CFG_IND,触发 M55 请求配置)
│
▼
ca_head_tracking_enable(true)
│
├─ spa_algo_audio_m55_ipc_headtracking_enable(true)
└─ spa_algo_audio_senhub_ipc_headtracking_enable(true)
└─ 发送 TLV SENSOR_ON → SensorHub
4.2 M55 核心的 Ping-Pong 握手
M33 M55
│ │
│──── dsp_m55_open() ─────────────►│ (M55 核启动)
│ │
│ osDelay(20ms) │ (M55 内部初始化)
│ │
│◄─── MCU_DSP_M55_TASK_CMD_PING ───│ (M55 发送心跳)
│ │
│ app_dsp_m55_ping_received_ │
│ handler() │
│ g_m55_core_runing = true │
│ │
│──── MCU_DSP_M55_TASK_CMD_RS3D ──►│ (M33 发送 CFG_IND)
│ _CTRL / TAG::CFG_IND │
│ │
│◄─── TAG::CFG_REQ ────────────────│ (M55 请求配置参数)
│ │
│ processCfgReq() │
│ ca_cfg_push() │
│ │
│──── TAG::CFG_SET ───────────────►│ (M33 下发 HRTF + 参数)
│ │
│◄─── TAG::CFG_CONFIRM ────────────│ (M55 确认配置生效)
│ │
│ ca_cfg_confirm() │
│ [进入正常工作状态] │ [开始渲染空间音效]
4.3 SensorHub 内部初始化
SensorHub 上电
│
▼
sensor_hub_core_app.c::main()
│
├─ 初始化 RTOS / 调度器
├─ 初始化 IPC 通道
├─ 加载 MotionEngine 算法库
│ └─ fme_Mobile_init()
├─ 注册传感器事件回调
│ ├─ on_quaternion_ready()
│ ├─ on_gyroscope_ready()
│ └─ on_accelerometer_ready()
└─ 启动传感器采样
│
▼
[等待 M33 的 SENSOR_ON TLV]
│
▼
开始上报四元数 / 陀螺仪数据
5. 时序执行流程
5.1 正常工作时序图(头部追踪 → 空间音效)
SensorHub M33 (MCU) M55 (DSP)
│ │ │
│ 每 10ms 采样一次 │ │
│ │ │
[IMU 硬件中断] │ │
│ │ │
MotionEngine 计算 │ │
四元数 Q(w,x,y,z) │ │
│ │ │
│──QUAT(0x027)────►│ │
│ (12 bytes) │ │
│ │ quaternion_send_ │
│ │ receive_handler() │
│ │ │
│ │ [条件检查] │
│ │ sa_effect_side== │
│ │ SA_ON_EAR && │
│ │ m55_is_running() && │
│ │ ca_cfg_is_confirmed()│
│ │ │
│ │──RS3D_QUAT(0x194)───►│
│ │ (8 bytes 四元数) │
│ │ │ spa_algo
│ │ │ 头部追踪计算
│ │ │ HRTF 滤波渲染
│ │ │
│ │──RS3D_QUAT(0x194)───►│ (下一帧)
│ [连续 100Hz 更新] │ │
│ │ │
│ │ [同时生成 HID 报告] │
│ │ → app_bt_hid_process │
│ │ _sensor_report() │
│ │ → 发送到手机 │
5.2 配置更新时序图
用户操作 / APP 命令
│
▼
M33: ca_spatial_enable(true)
│
├─ audio_spatial_en = true
├─ hal_cmu_axi_freq_req(M55, 208MHz) ← 请求提升 M55 频率(由 M33 落地)
├─ spa_algo_audio_m55_ipc_spatial_enable(true)
│ └─ 压入 mailQId (TAG::SPATIAL)
└─ pendVsEngineConfig = 2
│
[RTOS 调度: spa_algo_config_processor_thread 唤醒]
│
osMailGet(mailQId) → 取出配置项
│
switch(TAG):
case SPATIAL:
ca_cfg_push(cfgPtr, sampleFreq)
发送 CFG_SET → M55
│
▼
M55 接收 CFG_SET
spa_algo 更新参数
发送 CFG_CONFIRM → M33
│
▼
M33: ca_cfg_confirm()
[配置生效,四元数转发开始]
5.3 音频处理 Tick 时序
音频 DMA 中断 (每帧 ~5ms)
│
▼
spa_algo_audio_process(buf, len)
│
├─ [pendVsEngineConfig > 0 ?]
│ ├─ --pendVsEngineConfig
│ └─ [== 0] vsengineConfig() ← 应用延迟配置
│
├─ spa_algo_audio_senhub_ipc_check()
│ └─ 读取 SensorHub 消息队列
│ └─ [有数据] 调用对应 handler
│
├─ spa_algo_audio_m55_ipc_check()
│ └─ osMailGet(mailQId, 0) ← 非阻塞轮询
│ └─ 处理 CFG_REQ / CFG_CONFIRM
│
└─ spa_algo_audio_tota_check()
└─ 处理 TOTA 调试通道消息
6. 核心代码路径索引
| 功能 | 文件路径 | 关键函数 |
|---|---|---|
| 空间音频主入口 | services/audio_process/audio_process_spa_algo.cpp |
spa_algo_audio_init(), spa_algo_audio_process() |
| 头部追踪使能 | services/audio_process/audio_process_spa_algo.cpp:295 |
ca_head_tracking_enable() |
| 空间音效使能 | services/audio_process/audio_process_spa_algo.cpp:256 |
ca_spatial_enable() |
| SensorHub IPC | services/audio_process/audio_process_spa_algo_ipc.cpp |
spa_algo_audio_senhub_ipc_init() |
| 四元数接收 | services/audio_process/audio_process_spa_algo_ipc.cpp:355 |
quaternion_send_receive_handler() |
| 陀螺仪接收 | services/audio_process/audio_process_spa_algo_ipc.cpp:455 |
gyroscope_send_receive_handler() |
| 四元数转发到 M55 | services/audio_process/audio_process_spa_algo_m55_ipc.cpp:46 |
spa_algo_audio_quat_relay() |
| M55 IPC 初始化 | services/audio_process/audio_process_spa_algo_m55_ipc.cpp |
spa_algo_audio_m55_ipc_init() |
| M55 配置处理线程 | services/audio_process/audio_process_spa_algo_m55_ipc.cpp:131 |
spa_algo_config_processor_thread() |
| SensorHub App | apps/sensorhub/app_sensor_hub.cpp |
app_sensor_hub_init() |
| M55 App | apps/dsp_m55/mcu_dsp_m55_app.cpp |
app_dsp_m55_init() |
| M55 命令定义 | apps/dsp_m55/app_dsp_m55.h |
MCU_DSP_M55_TASK_CMD_* |
| SensorHub 命令定义 | apps/sensorhub/app_sensor_hub.h |
MCU_SENSOR_HUB_TASK_CMD_* |
| M55 RS3D IPC 协议 | services/gaf_cc_stream/inc/gaf_cc_spa_algo_process_ipc_cmd.h |
M55_RS3D_IPC::TAG |
7. 主频调度策略
7.1 频率档位
| 档位常量 | 频率 | 使用场景 |
|---|---|---|
HAL_CMU_AXI_FREQ_208M |
208 MHz | 空间音效正常处理 |
HAL_CMU_AXI_FREQ_260M |
260 MHz | 复杂 HRTF 计算(按需) |
| 默认低频 | < 100 MHz | 空闲 / 无音效 |
说明:本项目不支持 340M 主频档。芯片制程/规格不允许在量产配置下把 M55 主频设置到该档位。
7.2 频率调度逻辑
这里要强调一点:M55 的主频最终由 M33 侧控制。
- M33 作为系统主控负责实际调用
hal_cmu_axi_freq_req()。 - M55 侧如果需要提升/降低算力,通常通过 IPC 发起"频率请求"(instant cmd),由 M33 侧落地成真正的 CMU 配置。
- 若使能
SUBSYS_RMT_SYSFREQ_REQ,则hal_sysfreq会把"设置频率"动作通过回调转交给系统主控侧(用于多子系统/远端请求场景)。
c
// 文件(示例): services/audio_process/audio_process_spa_algo.cpp
void ca_spatial_enable(bool enable) {
if (enable) {
// 空间音效开启 → 请求提升 M55 主频(由 M33 落地)
hal_cmu_axi_freq_req(HAL_CMU_AXI_FREQ_USER_M55,
HAL_CMU_AXI_FREQ_208M);
} else {
// 空间音效关闭 → 释放频率请求,降回默认
hal_cmu_axi_freq_req(HAL_CMU_AXI_FREQ_USER_M55,
HAL_CMU_AXI_FREQ_ANY);
}
}
7.3 频率-功耗权衡
低功耗模式 高性能模式
│ │
M55 off / sleep M55 @ 208/260 MHz
SensorHub 低频采样 spa_algo 全功率
无空间音效 100Hz 头部追踪
│ │
触发条件: 触发条件:
- 耳机摘下 (off-ear) - 耳机戴上 (on-ear)
- 用户关闭空间音效 - 用户开启空间音效
- 无 BT 连接 - BT 连接 + A2DP/LE Audio 播放
8. 方案设计优势分析
8.1 多核异构分工优势
| 核心 | 特点 | 分工理由 |
|---|---|---|
| SensorHub | 低功耗,专用传感器接口 | 持续运行,功耗敏感;避免唤醒主核 |
| M33 | 通用控制,实时性强 | 协议栈、路由、HID、业务逻辑;不适合密集 DSP |
| M55 | 高性能 DSP,SIMD 指令 | HRTF 卷积计算密集,专用 DSP 效率远高于 M33 |
8.2 Core Bridge 设计优势
- 解耦发送与接收 : Handler 注册机制,收发逻辑分离,新命令只需
CORE_BRIDGE_TASK_COMMAND_TO_ADD一行注册 - 命令去重与序列化 :
cmdseq序列号防止重复处理,硬件邮箱保序 - 两类命令满足不同需求: Task CMD(有确认)用于配置,Instant CMD(无确认)用于高频数据
- 调试友好: 每个命令带字符串标识,日志追踪清晰
8.3 TLV 协议扩展优势
传统方式(固定结构体):
struct Cmd { uint8_t type; bool on_off; uint8_t mode; }
→ 新增字段必须修改结构体,破坏二进制兼容
TLV 方式:
[TAG][LEN][VALUE][TAG][LEN][VALUE]...
→ 接收方只解析认识的 TAG,忽略未知 TAG
→ 协议向前兼容,双核固件可独立升级
8.4 邮箱 + RTOS 信号异步处理优势
同步方式 (阻塞):
M33 发送 CFG_SET → 等待 M55 响应 → 阻塞 1~5ms
影响音频 DMA 中断响应,导致卡顿
异步方式 (邮箱 + 信号):
M33 发送 CFG_SET → 立即返回
spa_algo_config_processor_thread 独立处理响应
音频 DMA 不受影响
8.5 环形缓冲区优势
固定数组:
- 读写必须同步,需要 mutex
- 溢出处理复杂
CQueue 环形缓冲:
- 单生产者单消费者天然无锁
- 满时丢弃旧数据,保持实时性
- 2KB 足够缓冲突发传感器数据
8.6 滑动窗口平均降噪
原始陀螺仪数据存在高频抖动噪声:
[128, 130, -5(噪声), 131, 129] → 直接取会产生跳变
滑动窗口平均(5点):
avg = (128+130-5+131+129) / 5 = 102.6 → 有噪声仍影响
实际效果:
多数时刻噪声点稀少,5点窗口足以平滑
计算量极低(加法 + 移位),不引入延迟
9. 关键数据结构速查
9.1 命令码一览
c
// SensorHub → M33
MCU_SENSOR_HUB_TASK_CMD_PING = 0x037 // 心跳
MCU_SENSOR_HUB_TASK_CMD_SEND_QUATERNION = 0x027 // 四元数 (12 bytes)
MCU_SENSOR_HUB_TASK_CMD_SEND_GYROSCOPE = 0x028 // 陀螺仪 (6 bytes)
MCU_SENSOR_HUB_TASK_CMD_SEND_RAW_ACC = 0x029 // 原始加速度 (6 bytes)
MCU_SENSOR_HUB_TASK_CMD_SEND_RAW_GYRO = 0x033 // 原始陀螺仪 (6 bytes)
// M33 → SensorHub
MCU_SENSOR_HUB_TASK_CMD_MOTIONENGINE = 0x034 // 运动引擎控制 (TLV)
// M33 → M55
MCU_DSP_M55_TASK_CMD_RS3D_QUAT = 0x194 // 四元数转发 (8 bytes)
MCU_DSP_M55_TASK_CMD_RS3D_CTRL = 0x195 // 控制/配置 (TLV)
// M33 → M55 (无应答)
MCU_DSP_M55_INSTANT_CMD_AXI_SYSFREQ_REQ = 0x182 // 频率请求
MCU_DSP_M55_INSTANT_CMD_DISABLE_M55 = 0x183 // 禁用 M55
9.2 缓冲区尺寸汇总
| 缓冲区名称 | 大小 | 位置 |
|---|---|---|
| SensorHub RX 环形缓冲 | 2048 bytes | M33 内存 |
| 数据包最大 payload | 512 bytes | 每条消息 |
| 四元数消息队列 | 4 元素 × 12 bytes | M33 内存 |
| M55 配置邮箱 | 4 元素 × 12 bytes | M33 内存 |
| M33→M55 TX Mailbox | 30 条目 | Core Bridge |
| M33→SensorHub TX Mailbox | 8 条目 | Core Bridge |
| 陀螺仪滑动窗口 | 5 采样 × 3轴 × 2 bytes = 30 bytes | M33 内存 |
9.3 RTOS 任务优先级
| 任务 | 优先级 | 说明 |
|---|---|---|
spa_algo_config_processor_thread |
AboveNormal | M55 配置处理,优先响应 |
app_core_bridge_task (SensorHub) |
Normal | SensorHub 消息分发 |
app_dsp_m55_bridge_task |
Normal | M55 消息分发 |
附录:完整数据流一图概览
┌─────────────────────────────────────────────────┐
│ BES Best1600_SOC 空间音频系统 │
└─────────────────────────────────────────────────┘
[IMU 传感器] [SensorHub] [M33 MCU]
加速度计 ──────SPI/I2C──► MotionEngine 算法 ─QUAT(0x027)──► quaternion_handler()
陀螺仪 ──────SPI/I2C──► 四元数计算 ─GYRO(0x028)──► gyroscope_handler()
原始数据打包 ─ACC (0x029)──► raw_acc_handler()
│
TLV SENSOR_ON ◄────┘
(0x034 MOTIONENGINE)
│
├── HID 报告 ──► 手机
│
│ RS3D_QUAT(0x194)
│ (条件: on-ear &&
│ m55_running &&
│ cfg_confirmed)
│
▼
[M55 DSP]
spa_algo 引擎
HRTF 头部追踪渲染
208MHz 高速运行
│
▼
[DAC 输出 → 耳机]
3D 空间音效
基于 Best1600_SOC 项目 develop 分支代码分析
涉及文件: services/audio_process/, apps/dsp_m55/, apps/sensorhub/, tests/dsp_m55/, tests/sensor_hub/
补充:共享内存的落地形态(@utils/cfifo / @services/mcpp / @services/gaf_cc_stream)
A. @utils/cfifo:共享内存环形 FIFO 的基本语义
@utils/cfifo 提供一个轻量 ring buffer:
cfifo_put():写入连续字节流;空间不够返回FIFO_ERR。cfifo_pop():读出连续字节流;数据不够返回FIFO_ERR。cfifo_peek():不移动读指针,返回两段指针(buf1/len1+buf2/len2),用于 wrap-around 场景减少一次 memcpy。
它内部用 write/read 单调递增做索引(用 % size 映射到环上),并使用内存屏障保证读写顺序。
跨核使用约束(必须遵守):
- 推荐单生产者/单消费者模型(SPSC):一核只写、一核只读,避免双写/双读引入锁。
- 共享 buffer 必须位于两核都可见的 RAM 映射区。
- 若启用 DCache:共享区应尽量放到 non-cacheable 区域;否则需要在"写完通知前 clean cache / 读前 invalidate cache",否则可能读到旧数据。
B. @services/mcpp:跨核 RPC + 共享缓冲区(大块参数/PCM)
@services/mcpp 的典型形态是"RPC 命令"走跨核消息通道,但大块内存通过共享缓冲区承载。
示例:框架提供 MCPP_FRAMEWORK_ALLOC_MEM_CTL 控制命令,让服务端分配一块共享 buffer,并把地址通过 ctl 参数返回给客户端使用。
要点:
- 这个"指针返回"只在两核共享同一地址空间/同一物理 RAM 映射时成立。
- 若两核 cache 属性不同(例如一侧 cacheable、一侧 non-cacheable),必须定义清晰的 cache 维护策略。
- 指针只是一种"共享内存句柄"概念,如果后续要支持不共享地址空间的平台,应抽象成 offset/handle 机制。
C. @services/gaf_cc_stream:音频流处理与算法服务的跨核组合
@services/gaf_cc_stream 更偏"流式数据路径":
- 控制类配置:用 IPC 下发(例如算法开关、参数更新)。
- 数据类 payload:尽量通过共享内存 FIFO/块缓冲承载,IPC 仅做 doorbell。
- 当开启算法处理(例如基于 @services/mcpp 的算法服务)时,GAF CC stream 侧会组织好帧长/采样率/通道映射等配置,再把数据送到算法侧。
补充:SUBSYS_RMT_SYSFREQ_REQ 与 M55 频率请求链路
当定义 SUBSYS_RMT_SYSFREQ_REQ 时,hal_sysfreq 会暴露注册接口 hal_sysfreq_subsys_set_freq_register(cb)。
典型用法是让"系统主控侧"(通常是 M33)注册回调:
- 子系统(例如 M55 / BTH / SensorHub)触发频率变化需求。
hal_sysfreq不直接在子系统里落地 CMU 配置,而是通过回调把目标频率交给主控侧处理。- 主控侧统一调用
hal_cmu_axi_freq_req()等接口完成实际主频调整。
这样做的核心目标:避免多个子系统各自改 CMU 导致冲突,也符合"主控统一电源/时钟域管理"的系统设计。
补充:DSP_M55 驱动层(@platform/drivers/dsp_m55)与 Core-Bridge 的对接
本节从驱动视角解释 dsp_m55_open() / dsp_m55_send() / IRQ 回调如何支撑上层 Core-Bridge。
1) 驱动 API 概览
c
// platform/drivers/dsp_m55/dsp_m55.h
typedef unsigned int (*DSP_M55_RX_IRQ_HANDLER)(const void *data, unsigned int len);
typedef void (*DSP_M55_TX_IRQ_HANDLER)(const void *data, unsigned int len);
int dsp_m55_open(DSP_M55_RX_IRQ_HANDLER rx_hdlr, DSP_M55_TX_IRQ_HANDLER tx_hdlr);
int dsp_m55_close(void);
int dsp_m55_send(const void *data, unsigned int len);
int dsp_m55_send_seq(const void *data, unsigned int len, unsigned int *seq);
int dsp_m55_tx_active(unsigned int seq);
对应关系:
dsp_m55_send()是 Core-Bridge 发送的最底层出口(例如apps/dsp_m55/app_dsp_m55.cpp里的core_bridge_send()最终调用它)。dsp_m55_open(rx, tx)把上层的"收包回调/发送完成回调"注册到底层 IPC(hal_rmt_ipc_open)。
2) dsp_m55_open() 做了什么
platform/drivers/dsp_m55/dsp_m55.c::dsp_m55_open() 大体分三步:
- 电源/时钟/复位域准备 :
hal_psc_sys_m55_enable()、hal_cmu_m55_clock_enable()、hal_cmu_m55_reset_clear()。 - 加载 M55 镜像并设置入口 :通过
subsys_loader_load_image()/subsys_check_boot_struct()得到code_start,设置向量表(SP/ENTRY),最后hal_cmu_m55_start_cpu()拉起 M55。 - 建立 IPC 通道并启动接收 :
hal_rmt_ipc_open(HAL_RMT_IPC_CORE_SYS_M55C0, HAL_RMT_IPC_CHAN_0, rx_hdlr, tx_hdlr, ..., &rmt_ipc_ep)hal_rmt_ipc_start_recv(rmt_ipc_ep)
这里的关键是第 3 步:驱动把 rx_hdlr/tx_hdlr 绑定到 remote IPC 的中断路径,从而让上层在收到对端消息时被"同步回调"。
3) RX/TX 回调如何上接 Core-Bridge
M33 侧通常在 apps/dsp_m55/mcu_dsp_m55_app.cpp 里调用 dsp_m55_open(dsp_m55_rx_handler, dsp_m55_tx_handler),其作用是把驱动回调再分发到 Core-Bridge:
dsp_m55_rx_handler():根据优先级选择core_rx_handler[prio],默认 low prio 时core_rx_handler[LOW] = app_dsp_m55_bridge_data_received。dsp_m55_tx_handler():默认 low prio 时core_tx_handler[LOW] = app_dsp_m55_bridge_data_tx_done,用于释放发送侧等待的tx_done信号量。
因此链路是:
hal_rmt_ipc IRQ -> dsp_m55_rx_handler() -> app_dsp_m55_bridge_data_received()
hal_rmt_ipc TX done -> dsp_m55_tx_handler() -> app_dsp_m55_bridge_data_tx_done()
4) send/tx_done 的设计点
dsp_m55_send()本质是hal_rmt_ipc_send(rmt_ipc_ep, data, len)。- 上层
core_bridge_send()在dsp_m55_send()失败时做了重试与延迟退避,避免瞬时 busy 导致直接失败。 - 上层在成功调用
dsp_m55_send()后仍会等待tx_done信号量(见app_dsp_m55_bridge_transmit_data()),更准确地说是等待"对端已rx_done应答"(本端收到了DATA_DONEIRQ 并在 tx IRQ handler 中回调tx_hdlr),从而避免上层过早复用/释放发送缓冲。
补充:SensorHub 驱动层(@platform/drivers/sensor_hub)对照
SensorHub 驱动与 M55 驱动形态基本一致:
sensor_hub_open(rx, tx)内部同样使用hal_rmt_ipc_open(..., rx_hdlr, tx_hdlr, ...)+hal_rmt_ipc_start_recv()。- 上层
apps/sensorhub/mcu_sensor_hub_app.cpp的sensor_hub_rx_handler()会把数据交给app_core_bridge_data_received(),sensor_hub_tx_handler()则调用app_core_bridge_data_tx_done()释放 tx_done。
补充:驱动层 IPC 的数据通道(@platform/hal/hal_rmt_ipc*)
这一节把"驱动层 IPC"再往下扒开一层,解释 hal_rmt_ipc 的真实工作方式:它并不是传统意义上的"硬件 FIFO 逐字节发送",而是 共享内存里的消息链表 + 两类 mailbox 中断(doorbell)。
关键源码:
platform/hal/hal_rmt_ipc_common.cplatform/hal/hal_rmt_ipc_pri.hplatform/hal/best1600/hal_mcu2dsp_best1600.c(以 Best1600 的 MCU↔DSP 线路为例)
1) 术语对齐:Mailbox / 共享通道 / 两类 IRQ
hal_rmt_ipc 抽象了两个核心概念:
- 共享通道 (channel) :
HAL_RMT_IPC_CHAN_0..3是同一对核之间并行的逻辑通道。每个通道都有独立的"发送记录池(send_record)"和"消息链表指针"。 - Mailbox/doorbell 中断 :每个通道至少有两类事件(在
hal_rmt_ipc_pri.h里定义):
c
enum HAL_RMT_IPC_IRQ_TYPE_T {
HAL_RMT_IPC_IRQ_DATA_IND, // 对端有新数据可取(doorbell: data indication)
HAL_RMT_IPC_IRQ_DATA_DONE, // 对端已处理完成并应答(doorbell: data done / ack)
};
在 Best1600 的实现里,这两类事件对应 CMU 里的不同 IRQ SET 位(见 hal_mcu2dsp_irq_active() 等函数)。
直观理解:
- DATA_IND:发送方敲对端门铃,让对端去共享内存里"取信"。
- DATA_DONE:接收方处理完后敲回门铃,告诉发送方可以释放发送记录/复用 buffer。
2) "Mailbox"到底装了什么:共享内存里的指针
hal_rmt_ipc 的"消息"本体定义在 hal_rmt_ipc_pri.h:
c
struct HAL_RMT_IPC_MSG_T {
struct HAL_RMT_IPC_MSG_T *next;
unsigned int len;
const void *data; // 注意:这里是指针,不是拷贝后的 payload
};
这意味着:
hal_rmt_ipc_send()不会把data[len]拷贝到硬件 FIFO,它只是把{data 指针, len}挂到一条"共享链表"上。- 接收方 IRQ 里读取的是这个链表节点,然后把
data/len原样交给上层rx_irq_handler(data, len)。 - 因为
data是指针,发送缓冲必须对两核可见且在 tx_done 前保持有效 。这就是为什么 Core-Bridge 发送后要等tx_done信号量再复用tx_mailbox_heap。
这也解释了工程里常见的两条纪律:
- IPC 负责敲门(doorbell),数据尽量放共享 RAM。
- 发送侧 buffer 生命周期至少覆盖到"对端 rx_done"。
3) open 发生了什么:交换"配置块"地址
hal_rmt_ipc_open() 会把当前 core 的 HAL_RMT_IPC_CFG_T 地址写到一个双方约定的固定地址槽 里(不同链路对应不同 *_CFG_PTR_LOC)。
关键代码在 platform/hal/hal_rmt_ipc_common.c::hal_rmt_ipc_open():
- 初始化并清零
cfg->chan_cfg[chan]。 - 给该通道分配发送记录池:
c
cfg->chan_cfg[chan].send_record = cfg->send_record + chan * cfg->rec_num_per_chan;
cfg->chan_cfg[chan].send_rec_num = cfg->rec_num_per_chan;
- 把本端 cfg 地址发布出去,让对端可读:
c
*cfg->local_cfg_pp = ADDR_CPU_TO_DEV(cfg);
__DSB();
对端在第一次收到 RX IRQ 时,会懒加载 recv_msg_list_pp:
c
all_chan_cfg[chan].recv_msg_list_pp =
(const struct HAL_RMT_IPC_MSG_T **)&(*cfg->peer_cfg_pp)->chan_cfg[chan].send_msg_list_p;
这一步非常关键:它把"从哪里读对端的发送链表头指针"建立起来。
4) 发送路径:send_seq / seq 的真实语义
发送 API:
c
int hal_rmt_ipc_send_seq(HAL_RMT_IPC_EP_T ep, const void *data, unsigned int len, unsigned int *seq);
它的实现(hal_rmt_ipc_common.c::hal_rmt_ipc_send_seq())核心逻辑是:
- 从
send_record[]里找一个in_use == false的槽位。 - 把
{len, data 指针}写进该槽位的record->msg。 - 如果当前
send_msg_list_p == NULL(通道空闲),直接把该 msg 作为"当前可见的发送链表头",然后触发对端DATA_IND:
c
chan_cfg->send_msg_list_p = ADDR_CPU_TO_DEV(&record->msg);
__DSB();
cfg->peer_tx_irq_set(chan); // 对端 DATA_IND doorbell
- 如果通道当前已有正在发送/等待应答的链表,则把新 msg 挂到
send_pending_list_p(等待下一轮)。
因此:
- seq 就是 send_record 的索引 (
*seq = i),用于后续查询该条发送是否已被对端应答。 send_seq()能否成功,取决于该通道的send_record池是否还有空位;池满会返回失败(上层通常会重试)。
5) RX 流控:manual_rx_done / 回调返回值的语义
接收侧 IRQ 入口是 hal_rmt_ipc_rx_irq_handler()(platform/hal/hal_rmt_ipc_common.c),它支持两种"收包完成"模式:
- 自动 rx_done(默认) :
hal_rmt_ipc_open(..., manual_rx_done=false, ...)。 - 手动 rx_done :
hal_rmt_ipc_open(..., manual_rx_done=true, ...),需要上层在合适时机调用hal_rmt_ipc_rx_done(ep)才会向对端发DATA_DONE。
对应到代码里的关键字段(hal_rmt_ipc_pri.h):chan_manual_rx_done / chan_rx_busy / recv_pending_head。
更重要的是:rx 回调的返回值可以实现"分段消费"。
processed = rx_irq_handler(data, len)。- 若
processed < len,驱动会把"剩余未处理的数据"挂到recv_pending_head,并暂停继续推进消息链表,等后续被rx_irq_resume()重新触发。 - 当未完成处理时,如果是自动 rx_done 模式,驱动不会立刻
rx_done();如果是手动模式,则完全由上层调用hal_rmt_ipc_rx_done()来决定何时应答。
这套机制用于两类场景:
- IRQ handler 需要把数据搬到更安全的 buffer,但一次搬不完/资源不够(用
processed做 backpressure)。 - 上层希望把"应答时机"延后到真正消费完成之后(manual rx_done)。
6) tx_active 的真实语义:是否已收到对端应答
驱动层 API:
c
int hal_rmt_ipc_tx_active(HAL_RMT_IPC_EP_T ep, unsigned int seq);
其实现非常直接(hal_rmt_ipc_common.c::hal_rmt_ipc_tx_active()):
seq < send_rec_num:返回send_record[seq].in_use。seq == HAL_RMT_IPC_ALL_SEND_SEQ:遍历所有 record,只要有任何in_use就认为 TX 仍 active。
所以 tx_active() 并不表示"硬件仍在移位发送",而是表示:
- 这条发送记录还没被回收。
- 换句话说:对端还没对这条消息完成
rx_done应答,本端还不能复用它所引用的 buffer。
7) tx_done 从哪里来:对端 rx_done → 本端 TX IRQ
hal_rmt_ipc_tx_irq_handler()(hal_rmt_ipc_common.c)在收到 DATA_DONE IRQ 后会:
- 清除本端 TX IRQ 源(
cfg->local_tx_irq_clear(chan))。 - 遍历
send_msg_list_p链表,逐条调用上层注册的tx_irq_handler(data, len),并把对应send_record.in_use = false(回收槽位)。 - 如果存在
send_pending_list_p,则把 pending 提升为新的send_msg_list_p,并再次触发对端DATA_IND。
因此从系统视角看,一条消息的"完成"是这样的握手:
Sender: 写共享链表头 + doorbell(DATA_IND) ───────────────► Receiver
Receiver: 读取链表 -> 上抛 rx_handler
Receiver: 处理完 -> rx_done() -> doorbell(DATA_DONE) ◄────── Sender
Sender: TX IRQ 回收 record -> 回调 tx_handler -> tx_active 变 false
这也解释了为什么 Core-Bridge 里 tx_done 信号量能作为"发送完成"的判据:它等的是"对端已确认接收并处理完成"的语义。
8) 通道选择的工程习惯(CHAN_0/1/...)
工程里一般按"用途隔离"分配 channel:
CHAN_0:主消息通道(例如dsp_m55_open(..., HAL_RMT_IPC_CHAN_0, ...))。CHAN_1:trace / log 等旁路通道(例如platform/drivers/dsp_m55/rx_dsp_m55_trc.c的RMT_TRC_CHAN_ID)。
隔离的好处是:trace 的突发不会挤占业务消息的 send_record 池,也不会让业务通道的 doorbell 更频繁。