无标题
start_proc(IPC_PROC_ID_DSP, 1, 0x00100000); |
参数含义是:
| 字段 | 值 | 含义 |
|---|---|---|
| proc_id | IPC_PROC_ID_DSP,即 3 | 要启动 DSP,也就是这里所说的 Q8 |
| core_mask | 1 | Q8 的 core mask |
| img_addr | 0x00100000 | Q8 固件已加载到的物理地址 |
这里 Q8 使用的是:
所以代码中没有单独定义 IPC_PROC_ID_Q8,而是用 IPC_PROC_ID_DSP 表示 Q8。
2. start_proc() 构造发给 MCU0 的 IPC 消息
start_proc() 首先取得 IPC 共享 payload 区:
uio_pld_mem_t pld_mem;
if (ipc_req_get_pld(&pld_mem) != 0) {
LOG_ERROR(“START: failed to get IPC payload memory\n”);
return -1;
}
这个 payload 区是在程序启动时通过 MCU0 的 requestor channel 初始化的。初始化路径在 lidar_a55_app/files/src/main.c:82:
config_get_ipc_chan_num(IPC_ROLE_REQUESTER, IPC_PROC_ID_MCU0,
&req_chan_num);
ipc_req_init(req_chan_num);
当前 config.c 中该通道号固定为:
#define IPC_CHAN_REQ_MCU0 2
因此,A55 的启动请求走的是 mailbox channel 2,目标是 MCU0。
然后,代码在栈上创建一个 32 字节的 bcm_msg:
bcm_msg msg = { 0 }; |
消息格式为:
typedef struct s_bcm_msg { |
其 payload 被解释成 Loader 命令需要的结构:
typedef struct s_loader_init_proc_in { |
随后设置消息头:
msg.hdr = BCM_MSG_HDR(BCM_MSG_HDR_MSGID_NONE,
LOADER_SRV_FUNC_ID_INIT_PROCESSOR,
BCM_SRV_ID_LOADER,
IPC_PROC_ID_MCU0,
BCM_MSG_HDR_REPLY_NONE);
这几个字段非常关键:
message ID = 0
function ID = 0x06
service ID = 0x06,即 LOADER service
processor ID = 0,即 MCU0
reply = none
换句话说,消息头表达的是:
请 MCU0 的 Loader 服务执行 function 0x06:
INIT_PROCESSOR
消息头里的 processor ID 是 MCU0,而不是 MCU1/Q8。因为这里的 processor ID 指的是“IPC 消息的目标处理器”,真正要启动的处理器由 payload 中的 proc_id 指定。
3. 目标处理器写在 payload 中
ldr_pld_ptr->proc_id = proc_id;
ldr_pld_ptr->core_mask = core_mask;
ldr_pld_ptr->img_addr = img_addr;
启动 MCU1 时,payload 大致是:
offset 0: proc_id = 1
offset 1: core_mask = 1
offset 2: img_with_hdr = 0
offset 3: reserved = 0
offset 4: img_addr = 0x00040000
offset 8: flags[0..4] = 0
启动 Q8 时,payload 大致是:
offset 0: proc_id = 3
offset 1: core_mask = 1
offset 2: img_with_hdr = 0
offset 3: reserved = 0
offset 4: img_addr = 0x00100000
offset 8: flags[0..4] = 0
其中 img_with_hdr 没有显式设置,但因为:
bcm_msg msg = { 0 }; |
所以它的值为 0,表示当前发送的镜像被认为是不带额外镜像头的 raw image。flags 也全部保持为 0。
4. 把消息复制到 IPC 共享内存
memcpy(pld_mem.virt_addr, &msg, sizeof(bcm_msg));
这里的 pld_mem.virt_addr 是 uiolib 映射出来的 mailbox payload 虚拟地址。消息内容从 A55 的栈变量复制到 MCU0 能看到的共享 payload 区域。
5. 通过 doorbell 通知 MCU0
if (ipc_req_send_recv(0) != 0)
return -1;
ipc_req_send_recv(0) 的实现位于 lidar_a55_app/files/src/ipc_req.c:64。
因为传入的 cmd_id 是 0,所以函数先把共享 payload 的虚拟地址转换成物理地址:
if (!cmd_id)
cmd_id = uio_mbox_virt_to_phy(&dev_ctx, pld_mem.virt_addr);
然后发送:
uio_mbox_send_msg(&dev_ctx, (cmd_id | 1));
因此,doorbell 的值是:
共享 payload 物理地址 | 1
最低位的 1 用来表示这是一个“地址形式”的 doorbell,而不是直接编码的 command。MCU0 收到 mailbox doorbell 后,根据这个物理地址找到共享内存中的 bcm_msg,读取:
hdr
payload.proc_id
payload.core_mask
payload.img_addr
接着由 MCU0 的 Loader 服务处理 LOADER_SRV_FUNC_ID_INIT_PROCESSOR = 0x06,对指定目标处理器进行初始化/启动。
6. A55 等待 MCU0 返回结果
发送 doorbell 后:
status = uio_mbox_get_status(&dev_ctx, true);
这里会阻塞等待 MCU0 的响应中断。收到响应后,start_proc() 从同一个共享 payload 中读取返回码:
msg_ptr = (bcm_msg *)pld_mem.virt_addr;
retcode = BCM_MSG_GET_RETCODE(msg_ptr);
判断 MCU0 的 Loader 是否成功:
if (retcode != 0) {
LOG_ERROR(“START: remote returned %d\n”, retcode);
return -1;
}
所以每个处理器的启动都具有同步语义:
发送 MCU1 启动请求
↓
等待 MCU0 响应
↓
检查 retcode
↓
只有成功后才发送 Q8 启动请求
如果 MCU1 启动失败,Q8 请求不会发送;如果 Q8 启动失败,cmd_start() 返回错误,也不会打印 datapath started。
7. 固件是在 init 阶段预先装载的
cmd_start() 本身不加载固件,只把固件地址告诉 MCU0。固件加载发生在 cmd_init() 中:
if (load_image(q8_img, q8_size, Q8_IMG_LOAD_ADDR) != 0)
…
Q8 镜像被复制到:
0x00100000
然后 MCU1 镜像被复制到:
0x00040000
对应代码:
#define Q8_IMG_LOAD_ADDR 0x00100000
#define MCU1_IMG_LOAD_ADDR 0x00040000
load_image() 通过 /dev/mem 将文件内容 mmap 到目标物理地址:
mem_fd = open(“/dev/mem”, O_RDWR | O_SYNC);
map = mmap(NULL, size, PROT_WRITE, MAP_SHARED,
mem_fd, load_addr);
read(fd, map, size);
因此完整时序是:
init
│
├─ 校验 Q8 镜像
├─ 校验 MCU1 镜像
├─ 将 Q8 镜像写入 0x00100000
├─ 将 MCU1 镜像写入 0x00040000
├─ 启动 A55 的 IPC responder
└─ 发布共享配置
│
├─ 发 INIT_PROCESSOR(proc_id=MCU1, img_addr=0x00040000) 给 MCU0
A55 -> MCU0 Loader service -> 请求启动 MCU1/Q8 |
结合你补充的 MCU0 源码,可以把整个链路落实为:
A55 cmd_start() |
这里的关键点是:A55 发给 MCU0 的只是一个 LOADER 服务请求,MCU0 的 Loader 再调用 MCU 驱动去操作 MCU1/Q8 的 reset、vector 和 enable 寄存器。
1. A55 发送的消息
A55 的 cmd_start() 依次调用:
start_proc(IPC_PROC_ID_MCU1, CORE_MASK_MCU1,
MCU1_IMG_LOAD_ADDR);
start_proc(IPC_PROC_ID_DSP, CORE_MASK_Q8,
Q8_IMG_LOAD_ADDR);
因此两条请求分别是:
请求 1:
service = LOADER
function = INIT_PROCESSOR (0x06)
target = MCU0
payload.proc_id = MCU1 (1)
payload.core_mask = 1
payload.img_addr = 0x00040000
请求 2:
service = LOADER
function = INIT_PROCESSOR (0x06)
target = MCU0
payload.proc_id = Q8 (3)
payload.core_mask = 1
payload.img_addr = 0x00100000
MCU0 侧同样定义了对应的 Loader payload:
typedef struct sLOADER_SrvInitProcInType {
uint8_t procId;
uint8_t coreMask;
uint8_t imgWithHdr;
uint8_t reserved;
uint32_t imgAddr;
uint32_t flags[5];
} LOADER_SrvInitProcInType;
该定义位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/init/loader/lib/loader_srv_cmds.h:250
A55 侧的本地结构与 MCU0 侧结构布局一致,所以 A55 填好的 payload 可以直接被 MCU0 Loader 解释。
需要注意:
msg.hdr = BCM_MSG_HDR(…,
LOADER_SRV_FUNC_ID_INIT_PROCESSOR,
BCM_SRV_ID_LOADER,
IPC_PROC_ID_MCU0,
BCM_MSG_HDR_REPLY_NONE);
这里 header 中的 proc_id = IPC_PROC_ID_MCU0 表示“这条 IPC 消息发给 MCU0”。
真正要启动谁,不在 header 中,而是在 payload 中:
ldr_pld_ptr->proc_id = proc_id;
也就是说:
header.proc_id = MCU0 // 消息接收方
payload.proc_id = MCU1/Q8 // Loader 要启动的目标处理器
2. A55 的 doorbell 如何到达 MCU0
A55 的 ipc_req_send_recv(0) 最终会调用 uiolib:
uio_mbox_send_msg(&dev_ctx, (cmd_id | 1));
cmd_id = uio_mbox_virt_to_phy(&dev_ctx, pld_mem.virt_addr);
之后写入:
payload physical address | 1
MCU0 IPC 驱动的发送路径使用同样的 doorbell 约定。在:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/system/drivers/ipc/lib/ipcdrv.c:612
MCU0 侧发送消息时是:
ret = BCM_Map2GlobalAddr(txTblElt->msgCpy, &globalAddr);
if (BCM_ERR_OK == ret) {
globalAddr |= IPC_DOORBELL_FULL_MASK;
IPCDRV_Regs->doorbell[txTblElt->chIdx] = globalAddr;
}
A55 侧的 | 1 与 MCU0 侧的 IPC_DOORBELL_FULL_MASK 本质上都是将 doorbell 标识为“full/address form”,具体 mask 宏由两侧各自的 uiolib/IPC 头文件定义。
3. MCU0 IPC 驱动收到消息
MCU0 端真正接收 A55 消息的是:
void IPCDRV_IRQHandlerTx(void)
位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/system/drivers/ipc/lib/ipcdrv.c:975
它的处理逻辑是:
const uint32_t txMask = IPCDRV_Regs->ipc_tx;
for (chIdx = 0UL; chIdx < IPC_NUM_CHANNELS; chIdx++) {
if ((txMask & (1UL << chIdx)) != 0UL &&
IPCDRV_RxState.pendingStates[chIdx].origMsg == NULL) {
const uint32_t msgRemoteAddr =
IPCDRV_Regs->doorbell[chIdx] &
~IPC_DOORBELL_FULL_MASK;
...
}
}
对 A55 发来的请求,主要做四件事。
3.1 从 doorbell 取消息地址
const uint32_t msgRemoteAddr =
IPCDRV_Regs->doorbell[chIdx] &
~IPC_DOORBELL_FULL_MASK;
doorbell 的高位/标志位被清掉后,剩下的就是 A55 共享消息地址。
3.2 将 A55 地址转换为 MCU0 地址
BCM_MapRemoteEpMemAddrToLocal(
IPCDRV_RxState.pendingStates[chIdx].msgOrigEp.cpuID,
msgRemoteAddr,
&msgAddr);
MCU0 地址转换实现位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/architecture/abstract/lib/bcm_msg.c:73
BCM8915X 上的规则是:
if (aRemoteMemAddr < 0x200000UL) {
/* SRAM */
localMemAddr = aRemoteMemAddr | 0x01000000UL;
}
所以 A55 使用的低地址:
0x00040000 -> MCU0 local 0x01040000
0x00100000 -> MCU0 local 0x01100000
这也解释了为什么 A55 的固件装载地址是 0x00040000 和 0x00100000,而 MCU0 可以把这些地址映射到自己的 SRAM window 中访问。
如果是 DRAM 地址:
if ((0x80000000UL <= aRemoteMemAddr) &&
(0xE0000000UL > aRemoteMemAddr)) {
/* DRAM, no address translation needed */
}
则地址保持不变。
3.3 根据消息头识别 Loader 服务
映射成功后:
BCM_MsgType *const msg = (BCM_MsgType *const)msgAddr;
const BCM_SrvIdType srvID =
BCM_MSG_HDR_GET_SRV_ID(msg->hdr);
const BCM_MsgEPType *const dstEp =
BCM_MsgEpTbl.ep[srvID];
A55 发出的 header 中:
service ID = BCM_SRV_ID_LOADER = 0x06
MCU0 于是将这条消息判断为 Loader service 请求。
如果 service 不支持、没有 endpoint 或者 service 被 block,则执行:
IPCDRV_SrvReqComplete(chIdx, msg, BCM_BOOL_TRUE);
并返回错误。
如果合法,则:
IPCDRV_Regs->ipc_tx_msk &= ~(1UL << chIdx);
IPCDRV_RxState.pendingStates[chIdx].origMsg = msg;
IPCDRV_RxState.pendingStates[chIdx].readComplete =
BCM_BOOL_FALSE;
IPCDRV_ChannelInfo[chIdx].cbFunc(srvID);
这里先关闭该 channel 的 TX interrupt mask,防止同一个 channel 在当前请求完成之前又接收下一条请求。
3.4 调用 IPC callback
callback 是通过:
IPCDRV_ChannelInfo[chIdx].cbFunc(srvID);
调用的。
在 Service Runtime 中注册的 callback 是:
SRVRT_IpcChannelCb
其实现位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/architecture/srvrt/os/common/srvrt_osil_common.c:1372
void SRVRT_IpcChannelCb(BCM_SrvIdType aSrvId) |
因此 IPC 中断处理函数本身并不直接执行 Loader,也不直接执行 MCU_InitProcessor()。它只负责把 Loader service 对应的 waker 唤醒。
4. srvrt_osil_common.c 如何把消息交给 Loader
Service Runtime 初始化时会:
ret = IPCDRV_Init();
然后读取 PTU 中的 IPC channel 配置:
ret = PTU_GetAuxImgInfoById(
PTU_AUX_IMG_ID_IPCC,
0UL,
&auxImgInfo);
再调用:
enIpcSrvMask =
SRVRT_SetupIpcChannel(
ipcChanCfgEntry,
SRVRT_IpcChannelCb);
相关代码位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/architecture/srvrt/os/common/srvrt_osil_common.c:1400
SRVRT_SetupIpcChannel() 对每个 PTU IPC channel 配置执行:
IPCDRV_SetChanConfig(
cfgIdx,
chanCfg->targetCpuId,
chanCfg->srcCpuIdMask,
chanCfg->privilegedWrite);
如果当前 CPU 是该 channel 的 target,再注册对应 service:
IPCDRV_RegisterService(
cfgIdx,
srvIdMask,
aCbFunc);
这说明 MCU0 的实际 IPC channel 与 service 映射不是在 srvrt_osil_common.c 里硬编码完成的,而是来自 PTU 的 IPCC 配置。
对于 Loader service,生成的 Service Runtime 配置中可以看到:
.srvId = BCM_SRV_ID_LOADER,
.taskId = SYSTEM_Task,
.execId = 0UL,
.funcTbl = &LOADER_SrvFuncTbl,
也就是说:
BCM_SRV_ID_LOADER
│
▼
SYSTEM_Task
│
▼
LOADER_SrvFuncTbl
LOADER_SrvFuncTbl 的定义在:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/init/loader/lib/loader_srv.c:3490
其中请求处理函数是:
.srvReqKickoffFn = LOADER_SrvReqKickoff,
.srvReqPollFn = LOADER_SrvReqPoll,
5. SRVRT_IPCPollInternal() 把消息放入 Loader 队列
被唤醒的 SYSTEM task 在 event loop 中调用:
SRVRT_IPCPoll(execInfo);
最终进入:
SRVRT_IPCPollInternal(aSrvId)
位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_BL_REL_1.5_src/architecture/srvrt/os/common/srvrt_osil_common.c:1271
核心代码是:
if (BCM_ERR_OK == IPCDRV_RecvMsg(
aSrvId,
&ipcMsg,
&ipcMsgSrcEp)) {
ret = SRVRT_MsgQueueAllocPoolElt(
queue,
&msgPoolIdx);
if (BCM_ERR_OK == ret) {
queue->msgBundlePool[msgPoolIdx].msg = ipcMsg;
queue->msgBundlePool[msgPoolIdx].msgEp =
ipcMsgSrcEp;
SRVRT_MsgQueueListAdd(
&queue->pendingListInfo,
&queue->state->pendingListState,
msgPoolIdx);
}
}
IPCDRV_RecvMsg() 本身只是在 RX pending state 中找到刚才接收的消息:
if ((rxState->origMsg != NULL) &&
(aSrvID == BCM_MSG_HDR_GET_SRV_ID(
rxState->origMsg->hdr)) &&
(rxState->readComplete == BCM_BOOL_FALSE)) {
*aMsg = rxState->origMsg;
*aMsgOrigEp = &rxState->msgOrigEp;
rxState->readComplete = BCM_BOOL_TRUE;
}
因此消息实际没有被重新拷贝到 MCU0 的 Loader buffer 中,Loader 使用的是 IPC 驱动映射后的原始共享消息指针。
6. Loader service 识别 INIT_PROCESSOR
消息进入 Loader 队列后,Service Runtime 调用:
LOADER_SrvReqKickoff()
其中保存:
LOADER_SrvCtx.msg = aMsg;
LOADER_SrvCtx.msgSrcEp = aMsgEp;
LOADER_SrvCtx.waker = aWaker;
LOADER_SrvCtx.msgPld.pldPtr = &aMsg->pld;
LOADER_SrvCtx.funcId =
BCM_MSG_HDR_GET_FNC_ID(aMsg->hdr);
相关代码位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/init/loader/lib/loader_srv.c:3200
对于 A55 的请求:
funcId = 0x06
对应:
LOADER_SRV_FUNC_ID_INIT_PROCESSOR
在 LOADER_SrvReqPoll() 的映射表中:
const LOADER_SrvFuncId2SmMapType smInfo[] = {
…
{
LOADER_SRV_FUNC_ID_INIT_PROCESSOR,
&BCM_SM_INFO1(LOADER_SrvInitProcessorSM)
},
};
所以最终调用:
LOADER_SrvInitProcessorSM()
7. Loader 状态机具体做什么
核心实现位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/init/loader/lib/loader_srv.c:2959
7.1 检查目标处理器
static const char * const procName[MCU_PROCESSOR_ID_MAX+1UL] = {
[MCU_PROCESSOR_ID_MCU1] = “MCU1”,
[MCU_PROCESSOR_ID_A55] = “A55”,
[MCU_PROCESSOR_ID_Q8] = “Q8”,
};
然后检查:
if ((inParam->procId > MCU_PROCESSOR_ID_MAX) ||
(procName[inParam->procId] == NULL)) {
ret = BCM_ERR_INVAL_PARAMS;
}
因此 payload 中的:
procId = 1 -> MCU1
procId = 3 -> Q8
7.2 解析镜像地址
ret = BCM_MapRemoteEpMemAddrToLocal(
LOADER_SrvCtx.msgSrcEp->cpuID,
inParam->imgAddr,
&localAddr);
对于 A55 发来的地址:
MCU1: 0x00040000 -> MCU0 local 0x01040000
Q8: 0x00100000 -> MCU0 local 0x01100000
imgWithHdr 当前由 A55 结构清零,因此通常为 0:
bcm_msg msg = { 0 }; |
对于 imgWithHdr == 0,Loader 不读取镜像头中的 entry point,而是直接使用:
uint32_t entryPt = inParam->imgAddr;
也就是说当前 A55 代码的启动入口地址就是 img_addr 本身:
MCU1 entry point = 0x00040000
Q8 entry point = 0x00100000
如果以后 imgWithHdr 设置为非零,则 Loader 会从镜像头读取:
fsImgHdr->hdrAuthPld.entryPoint
并进行签名、entry point 范围等检查。
7.3 Reset 目标处理器
状态机执行到 LOADER_SRV_STATE_INIT_PROC_EXEC 后:
ret = MCU_ResetProcessor(
inParam->procId,
inParam->coreMask);
MCU 驱动实现位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/system/drivers/mcu/lib/bcm8915x/mcu_drv.c:1478
然后根据处理器类型执行不同操作。
MCU1 reset
if (aProcId == MCU_PROCESSOR_ID_MCU1) {
if ((aCoreMask & 1UL) != 0UL) {
MCU_REG_CLKNRST->cpu_dma_sys_reset_control |=
RIG_CLKNRST_CDSRC_CPU_DMA_SYS_MCU1_RSTB_FRC_MASK;
MCU_REG->misc_ctl &= ~MCU_MISC_CTL_U_MCU_EN_MASK;
MCU_REG->misc_ctl |= MCU_MISC_CTL_U_MCU_WAIT_MASK;
BCM_CpuNDelay(2000UL);
}
}
即:
- 强制 MCU1 reset
- 清除 MCU enable
- 设置 MCU wait
- 延时
Q8 reset
} else if (MCU_PROCESSOR_ID_Q8 == aProcId) {
uint32_t coreIdx;
for (coreIdx = 0UL; coreIdx < 4UL; coreIdx++) {
if (0UL != (aCoreMask & (1UL << coreIdx))) {
MCU_SysResetReleaseQ8(
coreIdx,
BCM_BOOL_TRUE);
}
}
}
当前 coreMask = 1,因此只处理:
Q8 core 0
如果 coreMask = 0xF,则会处理 Q8 的 4 个 core。
8. 初始化并释放目标处理器
Reset 成功后,Loader 调用:
ret = MCU_InitProcessor(
inParam->procId,
inParam->coreMask,
entryPt,
inParam->flags);
对应实现位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/system/drivers/mcu/lib/bcm8915x/mcu_drv.c:1966
MCU1 的启动动作
if (aProcId == MCU_PROCESSOR_ID_MCU1) {
if ((aCoreMask & 1UL) != 0UL) {
MCU_REG->u_initvtor = aEntryPt;
MCU_REG->misc_ctl |= MCU_MISC_CTL_U_MCU_EN_MASK;
MCU_REG->misc_ctl &= ~MCU_MISC_CTL_U_MCU_WAIT_MASK;
MCU_REG_CLKNRST->cpu_dma_sys_reset_control &=
~RIG_CLKNRST_CDSRC_CPU_DMA_SYS_MCU1_RSTB_FRC_MASK;
}
}
因此 MCU1 的启动顺序是:
MCU_REG->u_initvtor = 0x00040000
设置 MCU_EN
清除 MCU_WAIT
释放 MCU1 reset
可以概括为:
MCU1 vector = 0x00040000
MCU1 enable = 1
MCU1 wait = 0
MCU1 reset = release
Q8 的启动动作
} else if (MCU_PROCESSOR_ID_Q8 == aProcId) {
uint32_t coreIdx;
for (coreIdx = 0UL; coreIdx < 4UL; coreIdx++) {
if (0UL != (aCoreMask & (1UL << coreIdx))) {
ret = MCU_SysInitQ8(
coreIdx,
aEntryPt);
if (BCM_ERR_OK != ret) {
break;
}
}
}
}
MCU_SysInitQ8() 位于:
/home/hjk/workspace/11.bcm8915x/BCM8915X_LIDAR_REF_SW_REL_0_1/BCM8915X_BL_REL_1.5_src/system/drivers/mcu/lib/bcm8915x/mcu_drv.c:1793
它会先调用:
MCU_SysResetReleaseQ8(
aCoreIdx,
BCM_BOOL_FALSE);
释放对应 Q8 core 的 reset,然后继续配置 Q8 core 的启动向量/相关寄存器。具体 reset 控制由 MCU_SysResetReleaseQ8() 根据 core index 选择对应的:
Q8_TOP_APBSLAVE_RSTB_n
Q8_TOP_CRSTB_n
Q8_TOP_PRESETN_n
等 reset control 位。
所以对于当前代码:
coreMask = 1
entryPt = 0x00100000
实际含义是:
初始化 Q8 core 0
使用入口地址 0x00100000
释放 Q8 core 0 的 reset
开始执行 Q8 镜像
9. MCU0 如何返回 response 给 A55
Loader 状态机完成后,如果返回值不是 BCM_ERR_BUSY:
if (ret != BCM_ERR_OK) {
BCM_MemSet(
&LOADER_SrvCtx.msg->pld,
0U,
sizeof(LOADER_SrvCtx.msg->pld));
}
BCM_MSG_SET_RETCODE(
LOADER_SrvCtx.msg,
ret);
LOADER_SrvCtx.msg = NULL;
也就是说,MCU0 会把返回码写入原始 IPC message 的 payload。
Service Runtime 随后完成消息时,会调用:
IPCDRV_SendResp(aMsg);
在 IPC 驱动内部:
IPCDRV_SrvReqComplete(
chIdx,
rxState->origMsg,
BCM_BOOL_FALSE);
IPCDRV_SrvReqComplete() 会:
aMsg->hdr |= BCM_MSG_HDR_REPLY_MASK;
IPCDRV_Regs->doorbell[aChIdx] = 0x0UL;
也就是:
- 设置消息 header 的 reply bit
- 清除 MCU0 侧 doorbell
- 清除 RX pending state
- 重新使能 channel 的 TX interrupt
A55 侧等待:
status = uio_mbox_get_status(&dev_ctx, true);
收到 MCU0 的响应后,读取共享 payload 中的返回码:
retcode = BCM_MSG_GET_RETCODE(msg_ptr);
最终:
retcode == 0
表示对应目标处理器的 reset/init 操作成功。
10. 这段代码实际完成的启动顺序
最终可以精确总结为:
MCU1
A55:
payload.proc_id = 1
payload.core_mask = 1
payload.img_addr = 0x00040000
MCU0 Loader:
Map 0x00040000 -> MCU0 local 0x01040000
使用 entryPt = 0x00040000
MCU0 MCU driver:
MCU_ResetProcessor(MCU1, 1)
- assert MCU1 reset
- clear MCU_EN
- set MCU_WAIT
MCU_InitProcessor(MCU1, 1, 0x00040000, flags)
- u_initvtor = 0x00040000
- set MCU_EN
- clear MCU_WAIT
- release MCU1 reset
Q8
A55:
payload.proc_id = 3
payload.core_mask = 1
payload.img_addr = 0x00100000
MCU0 Loader:
Map 0x00100000 -> MCU0 local 0x01100000
使用 entryPt = 0x00100000
MCU0 MCU driver:
MCU_ResetProcessor(Q8, 1)
- assert Q8 core 0 reset
MCU_InitProcessor(Q8, 1, 0x00100000, flags)
- 初始化 Q8 core 0
- 配置 entry point = 0x00100000
- release Q8 core 0 reset
并且 cmd_start() 是严格串行的:
先启动 MCU1
↓
等待 MCU0 返回成功
↓
再启动 Q8 core 0
↓
等待 MCU0 返回成功
↓
打印 datapath started
因此更准确的描述是:
cmd_start() 并不是通过 IPC 直接通知 MCU0 “启动 MCU1 和 Q8”这样的一条复合命令,而是连续发送两条 BCM_SRV_ID_LOADER / LOADER_SRV_FUNC_ID_INIT_PROCESSOR 请求。MCU0 IPC 驱动负责接收和路由消息,srvrt_osil_common.c 负责唤醒 SYSTEM_Task 并把消息投递给 Loader service,Loader 的
LOADER_SrvInitProcessorSM() 再分别调用 MCU_ResetProcessor() 和 MCU_InitProcessor(),最终由 MCU 驱动设置 MCU1/Q8 的入口地址、reset 和 enable 寄存器。
• 结论先说:
当前这套代码下,四个 Q8 不能直接复用 A55 侧现有的“一份镜像 + 一个地址 + core_mask = 1”配置。
从现有 MCU 驱动测试代码来看,推荐方式是:
- A55 将四个 Q8 镜像分别加载到四个不同的地址;
- 通过 MCU0 分别对 Q8 core0~core3 发送四次 INIT_PROCESSOR;
- 每次请求使用对应的 coreMask 和对应的 imgAddr。
当前 cmd_start() 实际只启动了 Q8 core0。
1. 当前应用只启动一个 Q8
A55 应用中目前定义的是:
#define Q8_IMG_LOAD_ADDR 0x00100000
#define CORE_MASK_Q8 1
并且 cmd_init() 只加载一份 Q8 镜像:
load_image(q8_img, q8_size, Q8_IMG_LOAD_ADDR);
cmd_start() 只发送一次 Q8 启动请求:
start_proc(IPC_PROC_ID_DSP, CORE_MASK_Q8,
Q8_IMG_LOAD_ADDR);
所以当前实际请求等价于:
proc_id = Q8
core_mask = 0x1
img_addr = 0x00100000
core_mask = 0x1 只表示:
bit 0 = 1 -> 只启动 Q8 core0
bit 1 = 0 -> 不启动 Q8 core1
bit 2 = 0 -> 不启动 Q8 core2
bit 3 = 0 -> 不启动 Q8 core3
因此,当前 lidar_a55_app 并没有启动四个 Q8,只启动了 Q8 core0。
2. MCU0 确实支持四个独立的 Q8 core
MCU0 驱动中有四组 Q8 CSR:
Q8_RDBType * const MCU_REG_Q8[4] = {
((Q8_RDBType *)Q8_CSR_0_BASE),
((Q8_RDBType *)Q8_CSR_1_BASE),
((Q8_RDBType *)Q8_CSR_2_BASE),
((Q8_RDBType *)Q8_CSR_3_BASE),
};
也就是说,core0~core3 分别有自己的寄存器控制块。
MCU_SysInitProcessor() 对 Q8 的处理是:
} else if (MCU_PROCESSOR_ID_Q8 == aProcId) {
uint32_t coreIdx;
for (coreIdx = 0UL; coreIdx < 4UL; coreIdx++) {
if (0UL != (aCoreMask & (1UL << coreIdx))) {
ret = MCU_SysInitQ8(coreIdx, aEntryPt);
if (BCM_ERR_OK != ret) {
break;
}
}
}
}
因此,从驱动接口形式上看:
MCU_InitProcessor(Q8, 0x1, entry, …)
表示初始化 core0;
MCU_InitProcessor(Q8, 0x2, entry, …)
表示初始化 core1;
MCU_InitProcessor(Q8, 0x4, entry, …)
表示初始化 core2;
MCU_InitProcessor(Q8, 0x8, entry, …)
表示初始化 core3;
MCU_InitProcessor(Q8, 0xF, entry, …)
表示依次初始化四个 core。
3. 现有测试代码明确使用四个不同地址
MCU 驱动测试代码 MCU_TestQ8BootMask() 给出了目前最直接的参考实现:
static const MCU_TestQ8CoreInfoType MCU_TestQ8Cores[] = {
{
.load_addr = 0x00100000,
…
},
{
.load_addr = 0x00110000,
…
},
{
.load_addr = 0x00120000,
…
},
{
.load_addr = 0x00130000,
…
}
};
四个 Q8 的镜像地址分别是:
Q8 core 镜像地址
━━━━━━━━━ ━━━━━━━━━━━━
core0 0x00100000
───────── ────────────
core1 0x00110000
───────── ────────────
core2 0x00120000
───────── ────────────
core3 0x00130000
测试代码随后分别加载四个镜像:
for (c = 0; c < 4; c++) {
const MCU_TestQ8CoreInfoType *core =
&MCU_TestQ8Cores[c];
uint8_t * const dstPtr =
(uint8_t *)(uintptr_t)
(core->load_addr | 0x01000000UL);
if (c == 0) {
readBootImgData(..., "q8_image_core0.bin");
} else if (c == 1) {
readBootImgData(..., "q8_image_core1.bin");
} else if (c == 2) {
readBootImgData(..., "q8_image_core2.bin");
} else if (c == 3) {
readBootImgData(..., "q8_image_core3.bin");
}
}
这段代码说明当前设计意图是:
每个 Q8 core 有自己的镜像
每个 Q8 core 有自己的 load address
测试中使用的四个镜像文件也是独立的:
q8_image_core0.bin
q8_image_core1.bin
q8_image_core2.bin
q8_image_core3.bin
所以,对于你这个 LiDAR 应用,不能只把同一份 lidar_q8_app.bin 写入 0x00100000,然后认为四个 Q8 会自动各自得到一份代码。
4. 测试代码也是分别启动四个 core
测试代码不是一次调用:
MCU_InitProcessor(Q8, 0xF, …);
而是:
for (c = 0; c < 4; c++) {
if ((aCoreMask & (1UL << c)) != 0) {
ret = MCU_InitProcessor(
MCU_PROCESSOR_ID_Q8,
(1UL << c),
MCU_TestQ8Cores[c].load_addr,
opt);
}
}
这非常关键。因为每个 core 使用不同的入口地址:
core0 -> MCU_InitProcessor(Q8, 0x1, 0x00100000, …)
core1 -> MCU_InitProcessor(Q8, 0x2, 0x00110000, …)
core2 -> MCU_InitProcessor(Q8, 0x4, 0x00120000, …)
core3 -> MCU_InitProcessor(Q8, 0x8, 0x00130000, …)
这样每条请求都能携带不同的:
coreMask
imgAddr
而 Loader 的 IPC payload 只有一个 imgAddr:
typedef struct sLOADER_SrvInitProcInType {
uint8_t procId;
uint8_t coreMask;
uint8_t imgWithHdr;
uint8_t reserved;
uint32_t imgAddr;
uint32_t flags[5];
} LOADER_SrvInitProcInType;
因此,一条 INIT_PROCESSOR 消息只能表达:
一组 coreMask + 一个 imgAddr
不能表达:
core0 -> addr0
core1 -> addr1
core2 -> addr2
core3 -> addr3
所以如果四个 core 使用不同镜像/不同入口地址,就必须发送多条请求。
5. “四个一起启动”分两种情况
情况一:四个 Q8 使用不同镜像或不同地址
这种情况下应当:
A55 load 四个镜像到不同地址
│
├─ 请求 1:Q8 core0,addr = 0x00100000
├─ 请求 2:Q8 core1,addr = 0x00110000
├─ 请求 3:Q8 core2,addr = 0x00120000
└─ 请求 4:Q8 core3,addr = 0x00130000
每条请求都经过:
A55
-> MCU0 IPC driver
-> SRVRT
-> Loader service
-> MCU_InitProcessor()
-> MCU_SysInitQ8(coreN, addrN)
A55 侧可以严格串行等待,也可以从协议层面设计异步批量请求,但当前 ipc_req_send_recv() 是同步等待 response 的,因此现有框架下自然是四次串行请求。
建议的 A55 侧逻辑类似:
start_proc(IPC_PROC_ID_DSP, 0x1, 0x00100000);
start_proc(IPC_PROC_ID_DSP, 0x2, 0x00110000);
start_proc(IPC_PROC_ID_DSP, 0x4, 0x00120000);
start_proc(IPC_PROC_ID_DSP, 0x8, 0x00130000);
不过正式修改时最好不要简单写死四次,而是定义每个 Q8 core 的镜像路径和加载地址数组。
情况二:四个 Q8 使用相同镜像和相同入口地址
这种情况下,理论上可以只发送一条请求:
proc_id = Q8
core_mask = 0xF
img_addr = 某一个公共入口地址
因为 MCU0 驱动内部会执行:
for (coreIdx = 0; coreIdx < 4; coreIdx++) {
if (aCoreMask & (1UL << coreIdx)) {
MCU_SysInitQ8(coreIdx, aEntryPt);
}
}
所以 core_mask = 0xF 会覆盖四个 Q8 core。
但是这有几个前提:
- 四个 Q8 必须能够执行同一份镜像;
- 四个 Q8 必须允许使用同一个 entry point;
- 镜像必须正确处理每个 core 的身份;
- 代码和数据不能因为四个 core 同时使用而产生冲突;
- 镜像放置地址必须是四个 Q8 都可以访问的公共地址;
- Q8 firmware 本身必须是按这种多核启动方式设计的。
而当前测试代码没有采用这种模式。测试代码使用四份镜像、四个地址、四次初始化,因此不能假设当前 lidar_q8_app.bin 能直接用 core_mask = 0xF 启动四核。
6. core_mask = 0xF 并不是严格的并行启动
即使使用:
MCU_InitProcessor(MCU_PROCESSOR_ID_Q8, 0xF, entry, opt);
底层也不是四个 core 同时完成初始化,而是:
for (coreIdx = 0; coreIdx < 4; coreIdx++) {
if (aCoreMask & (1UL << coreIdx)) {
MCU_SysInitQ8(coreIdx, aEntryPt);
}
}
也就是依次执行:
初始化 core0
↓
初始化 core1
↓
初始化 core2
↓
初始化 core3
每个 MCU_SysInitQ8() 还包括:
释放 Q8 外部 reset
设置 altresetvec
配置 general_ctrl1
执行 DTCM 初始化
执行 D-cache A 初始化
执行 I-cache 初始化
执行 D-cache B 初始化
重启 core
解除 stall
因此:
core_mask = 0xF
表示“由一次 MCU 驱动调用覆盖四个 Q8 core”,并不表示四个 core 完全并行、同时释放 reset。
如果每个 core 地址不同,仍然必须分别调用,因为一次调用只有一个 aEntryPt:
MCU_SysInitQ8(coreIdx, aEntryPt);
7. 当前 Loader 层还有一个重要限制
LOADER_SrvInitProcessorSM() 中最终执行:
ret = MCU_ResetProcessor(
inParam->procId,
inParam->coreMask);
if (BCM_ERR_OK == ret) {
ret = MCU_InitProcessor(
inParam->procId,
inParam->coreMask,
entryPt,
inParam->flags);
}
这里的 entryPt 对整个 coreMask 只有一个值。
因此:
INIT_PROCESSOR(procId=Q8, coreMask=0xF, imgAddr=0x00100000)
只能代表:
Q8 core0 -> 0x00100000
Q8 core1 -> 0x00100000
Q8 core2 -> 0x00100000
Q8 core3 -> 0x00100000
它不能代表:
Q8 core0 -> 0x00100000
Q8 core1 -> 0x00110000
Q8 core2 -> 0x00120000
Q8 core3 -> 0x00130000
后者需要四个 IPC 请求,或者需要扩展 Loader 的 payload 协议,让一条消息携带四个 entry address。但从现有代码和测试来看,没有必要扩展协议,四次请求即可完成。
8. 对当前 LiDAR 应用应如何修改
当前配置只有一个 Q8 镜像字段:
Q8ImageFileRaw = ./img/lidar_q8_app.bin
Q8ImageFileFFT = ./img/lidar_q8_app.bin
Q8ImageFileToF = ./img/lidar_q8_app.bin
Q8ImageFileFMCW = ./img/lidar_q8_app.bin
如果 LiDAR firmware 的设计确实要求四个 Q8 分别运行独立镜像,建议改成类似:
Q8Core0ImageFileRaw = ./img/lidar_q8_core0_app.bin
Q8Core1ImageFileRaw = ./img/lidar_q8_core1_app.bin
Q8Core2ImageFileRaw = ./img/lidar_q8_core2_app.bin
Q8Core3ImageFileRaw = ./img/lidar_q8_core3_app.bin
或者如果四个 core 运行同一份二进制,也可以由 A55 读取同一文件四次,分别写入:
0x00100000
0x00110000
0x00120000
0x00130000
但是否能直接复制同一份镜像,取决于 Q8 firmware 的链接地址、代码是否位置相关、各 core 的数据区是否独立,以及镜像是否包含按 core 区分的配置。不能只根据 MCU 驱动接口判断这一点。
A55 侧的启动流程应大致变为:
static const uint32_t q8_core_masks[4] = {
1U << 0,
1U << 1,
1U << 2,
1U << 3,
};
static const uint32_t q8_load_addrs[4] = {
0x00100000,
0x00110000,
0x00120000,
0x00130000,
};
初始化阶段:
for (i = 0; i < 4; i++) {
load_image(q8_img[i], q8_size[i], q8_load_addrs[i]);
}
启动阶段:
for (i = 0; i < 4; i++) {
start_proc(IPC_PROC_ID_DSP,
q8_core_masks[i],
q8_load_addrs[i]);
}
9. 还需要注意地址空间和镜像大小
测试代码中四个地址间隔为:
0x00110000 - 0x00100000 = 0x10000
也就是每个 core 的镜像区域间隔 64 KiB。
而当前 A55 应用限制:
#define Q8_IMG_MAX_SZ 0xC000
即最大 48 KiB,小于 64 KiB 间隔,这与测试地址布局是匹配的。
但如果正式 Q8 镜像大于 0x10000,或者包含额外 header/data,就可能覆盖下一个 core 的镜像区域,需要重新规划地址。
另外,MCU0 的地址转换会将 A55 看到的低地址转换到 MCU0 SRAM window:
A55/Q8 global address:
0x00100000 -> MCU0 local 0x01100000
0x00110000 -> MCU0 local 0x01110000
0x00120000 -> MCU0 local 0x01120000
0x00130000 -> MCU0 local 0x01130000
所以四个地址必须落在 MCU0 BCM_MapRemoteEpMemAddrToLocal() 支持的地址范围内。
最终判断
对你当前这套工程,最可靠的结论是:
当前 cmd_start():
只启动 Q8 core0
如果需要启动四个 Q8,并且四个 Q8 使用不同镜像或不同 load address:
A55 侧加载四份镜像到不同地址
MCU0 侧发送四次 INIT_PROCESSOR
每次只带一个 core mask 和一个 img_addr
推荐映射:
core0: coreMask = 0x1, imgAddr = 0x00100000
core1: coreMask = 0x2, imgAddr = 0x00110000
core2: coreMask = 0x4, imgAddr = 0x00120000
core3: coreMask = 0x8, imgAddr = 0x00130000
如果四个 Q8 明确设计成运行同一份镜像、使用相同入口地址,则可以:
A55 加载一份公共镜像
MCU0 发送一次 coreMask = 0xF
但这只是接口层面可行;现有 MCU 测试代码采用的是“四份镜像、四个地址、四次启动”,因此在没有确认 lidar_q8_app.bin 的多核设计之前,应按四个独立 core 分别加载、分别启动来实现。
─ Worked for 3m 30s ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
─ Conversation recap ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
目标是确认四个Q8的启动方式;现有A55代码仅配置单镜像、单地址和core_mask=1,不能直接支持四核。需确认硬件拓扑及各Q8加载地址和启动掩码。

