
前一篇Part 1里我写完了怎么把全志SoC主系统通过U-BootSPL拉起来、串口看到Linux控制台出来的全过程。当时评论区很多人问的最多的一个问题就是板子上明明还有一颗RISC-V协处理器什么时候把它也点亮其实Part 1里我只解决了板子能跑的问题异构RISC-V核心还躺在复位状态里根本没参与到系统里来。这篇文章就是把这块补完——从单核启动推进到真正的Heterogeneous系统主核跑Linux、RISC-V小核跑裸机或RTOS两边的内存共享、消息通道、中断路由全部打通。这篇文章面向的对象是做过至少一次嵌入式Linux bring-up、对全志SoC有一定了解、但还没碰过AMP异构开发的人。如果你已经在D1、T113、R528这类板子上把Linux跑起来了那这正好是下一步。如果你跑的是其他带RISC-V协处理器的全志SoC比如H616系列原理也是一样的只是寄存器地址和核型号不同而已。1. 异构bring-up和普通单核启动的本质区别很多从Part 1过来的朋友会觉得不就是把另一个核心也跑起来吗跟当初启动主核有啥本质不同还真不一样。主核启动是BootROM规定的固定流程你只需要提供正确的镜像格式、加载地址和启动参数剩下的硬件都帮你安排好了。异构小核启动完全没有这么省心绝大多数事情要你自己来。SMP和AMP是两码事。常见的八核ARM板子跑Linux属于SMP对称多处理所有核心地位平等共享同一个操作系统、同一套内存管理单元调度器统一分配任务。全志这类芯片上的RISC-V协处理器属于AMP非对称多处理主核和小核跑的是完全不同的软件——主核跑Linux小核上跑的可能是裸机程序、FreeRTOS或RT-Thread两边没有共享的调度器没有共享的地址映射甚至没有共享的中断控制器。这不叫多核这叫两台用一根网线连起来的独立电脑。这个区别带来一个最直接的后果主核的地址映射和管理规则在小核那边统统不作数。主核上你用虚拟地址访问内存、用MMU做页表翻译小核大多数情况下没有MMU或者MMU直接被关闭所有访问都是物理地址直通。你主核上devmem读到的某个寄存器地址到小核那边就要看它自己地址空间里这个位置是什么。同一个物理地址在不同核心的视角里可能完全是不同的含义。另一个被低估的点是异构的核心不会自己启动。主核启动时BootROM会从固定的Boot Media地址取代码、跳转执行这一切都是芯片设计时定死的。小核呢全志的设计里RISC-V协处理器通常处于复位状态需要主核主动给它配置时钟、解除复位、把固件放到约定地址、然后手动触发启动。这个过程出了问题小核看起来就是死的——而怎么死的完全不会给你任何日志因为串口那边根本没它的事。动手之前先把这个思维转变过来主核是主人小核是客人。客人住哪、吃什么、怎么样被叫醒全都要主人安排。2. 全志SoC异构骨架主核、RISC-V小核与Mailbox的分工先把芯片层面的事情说清楚。我用T113/R528这一系做例子——主核是双核ARM Cortex-A7小核是RISC-V架构的E907。D1/D1s那一类主核本身是RISC-V C906的区别只是主核变了小核的存在形式和联动思路完全一致。2.1 主核负责管资源小核只认一张简单地图主核侧的资源包括DDR控制器、MMC控制器、网络、显示、USB等等Linux的全套驱动都在这里。小核E907没有MMU它运行所需的代码和数据放在一个主核预先划出来的内存区域里。这块区域必须在两个核的地址空间里都能访问通常的做法是从DDR里预留一块物理内存或者在芯片的SRAM里放。DDR预留比较有代表性。主核在设备树里给Linux划出一块reserved-memory这块内存对Linux而言是不可用状态——它绝不能被页表分配走绝不能被其他驱动挪用否则小核的固件数据会被主核踩掉或者小核写回来的数据污染了Linux的某个进程。这是异构开发最常见的崩溃来源后面讲到踩坑我会详细说。2.2 Mailbox两边的门铃内存共享是纸面上的共享真正要通知对方我有数据给你了靠的是硬件Mailbox——这是全志SoC里专门给异构核间通讯准备的一组寄存器。它的作用就是门铃主核往某个寄存器写一个值小核那边就会收到一个中断反过来也一样。一个容易误会的点是Mailbox本身不传输业务数据它只传注意我有事这六个字。真正的数据放在共享内存里Mailbox只负责触发中断让对方来读。所以Mailbox设计的好坏直接决定了异构系统的响应速度和稳定性。全志sunxi-msgbox这套硬件通道数量有限每一路通道可以配置方向主核到小核/小核到主核、可以附带一个简单的消息内容但是不能传大块数据。把大块数据走Mailbox传属于新手最常见的资源浪费。2.3 时钟、复位和中断域小核虽然是个独立CPU但它挂在SoC的时钟树和复位域里。主核Linux的时钟驱动clk subsystem可以控制小核的时钟频率复位驱动控制它的复位引脚。上电初期小核的时钟是关闭的复位信号是拉着的不把这两件事办完小核就是一块石头。中断方面则是另一个关键点。主核ARM用的中断控制器是GIC小核RISC-V用的是自己的PLIC。两边都能产生中断但对方的核不见得认。实际项目里最省事的方案是小核的中断处理全部走Mailbox触发的那一路小核产生的中断如果需要主核知道也通过Mailbox反向通知而不是直接物理串线到GIC的SPI上。下面这个表总结了我最常用的对应关系你可以对照自己的板子改一改资源主核ARM A7小核RISC-V E907操作系统Linux裸机/FreeRTOS/RT-ThreadMMU有无地址访问虚拟地址物理地址仅物理地址中断控制器GICPLIC数据交互共享内存DDR预留或SRAM同一块共享内存通知机制Mailbox写通道收中断同一Mailbox其他通道3. 把RISC-V协处理器拉起来的完整启动链路铺垫了这么多现在进入正题实际操作中怎么把那个死的小核弄醒。整个过程分三个阶段引导阶段U-Boot里做、设备树描述阶段让Linux知道小核的存在、固件管理阶段Linux运行后如何加载/重启小核。3.1 U-Boot阶段放固件、配时钟、解复位小核固件是一段独立编译的二进制通常在PC上用RISC-V交叉编译器编出来比如riscv-none-embed-gcc。编译产物会是一个纯二进制文件里面是小核的代码和只读数据。这段二进制本身没有任何ELF头、没有加载器帮你搬运主核要把它的每一条指令原样放到小核指定的入口地址上。入口地址的选择非常关键。不同芯片不一样规范的做法是查芯片手册里RISC-V核心的Boot Address。以E907为例常见做法是把固件放到DDR的某个固定地址比如0x48000000附近具体以你板子的内存布局为准然后把小核的复位向量设置成这个地址。谁来设置还是主核——通过复位控制寄存器里的BOOT_ADDR字段。U-Boot里的典型操作流程# 假设固件已经打包在boot分区里 load mmc 0:1 0x48000000 e907_fw.bin # 或者如果你已经把固件编进了U-Boot镜像里可以直接用cp命令 # cp.b 0x100000 0x48000000 0x10000 # 确认复位控制寄存器当前状态 md.l 0x07000400 4 # 设置小核入口地址并解除复位寄存器偏移以实际芯片手册为准 mw.l 0x07000400 0x48000000做完这几步之后小核的PC会跳到0x48000000开始执行。但是这里有个很多新手栽进去的坑写完复位寄存器不等于时钟已经就绪。在很多全志方案上必须先通过CCUClock Control Unit把小核的时钟打开并配到合适频率然后操作复位释放。顺序反了或者时钟没配小核要么永远不跑要么跑起来也是错的指令流。3.2 内核侧用remoteproc管理还是直接devmem到了Linux这一步小核的管理方式有两条路。第一是正经的remoteproc框架——这是内核里专门管理辅助处理器的框架加载固件、启动、停止、异常重启都有标准接口。如果你用的是厂商改过的内核通常他们也已经把全志的RISC-V remoteproc驱动放进去了。# 把内核编译时默认打包的固件加载到小核 modprobe riscv_remoteproc echo start /sys/class/remoteproc/remoteproc0/state第二是用devmem直接操作全部寄存器手动完成加载和启动。这条路不依赖内核驱动适合早期调试阶段。很多在厂商SDK里干活的人反而更习惯这个方式因为小核固件动不动要改每次重新编内核太痛苦了。# 1. 将固件复制到预留内存假设预留区起始0x48000000 dd if/path/to/e907_fw.bin of/dev/mem bs1 seek$((0x48000000)) convnotrunc # 2. 配置时钟 devmem 0x0700002c 32 0x00040001 # 3. 设置入口地址并释放复位 devmem 0x07000400 32 0x48000000我个人的建议调试期用devmem跑正式方案再移植到remoteproc。原因很简单devmem不依赖DTS、不依赖驱动哪一步出问题一眼就能定位而且早期小核固件的加载地址、启停逻辑频繁变动直接写进remoteproc驱动里每次都要重编内核太浪费生命。3.3 设备树里怎么描述异构关系设备树是Linux和固件之间的契约书异构系统在这份契约书里要写明三件事哪块内存是小核的、Mailbox设备在哪个地址、中断路由怎么走。以下是一个典型全志T113的异构描述片段基于厂商BSP实际设备树精简而来reserved-memory { #address-cells 2; #size-cells 2; ranges; rproc_reserved: rproc48000000 { no-map; reg 0x0 0x48000000 0x0 0x1000000; }; }; mailbox: mailbox3002000 { compatible allwinner,sunxi-msgbox; reg 0x0 0x03002000 0x0 0x1000; interrupts GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH; #mbox-cells 1; }; riscv_rproc: riscv-rproc0 { compatible allwinner,sunxi-riscv-rproc; memory-region rproc_reserved; mboxes mailbox 0, mailbox 1; mbox-names rx, tx; clocks ccu CLK_RISCV; resets ccu RST_RISCV; status okay; };注意reserved-memory里的no-map属性。这个属性意思是Linux的MMU完全不碰这块区间——不建立页表映射。很多人在这个属性上犯过错误不加no-map内核会认为这是普通内存的一部分分配器可能在不知不觉中把页表填进去然后某天某个进程访问到这片地址结果读出来的是小核正在维护的数据整个系统行为变得不可预测。4. 通讯机制消息通道与中断路由的真实打开方式把小核跑起来只是第一步真正干活靠的是通讯。尤其在全志这类SoC上小核的功能往往是实时控制类任务——比如PWM输出、实时IO采样、音频通路管理——这些任务需要频繁和主核交互通讯链路不够稳整个系统的价值就大打折扣。4.1 Mailbox通道分配原则sunxi-msgbox硬件上有多条通道常见的是主核-小核方向、小核-主核方向各若干条。双向上各用一条就够了。双向各自独立还有一个现实原因收发各自独立可以避免互锁问题。如果只用一条通道既收又发主核往通道写数据、同时等待对方回包而对方也往同一通道写回两边方向相反的数据在同一通道上交错处理很容易出现双方都以为对方会先读的死锁局面。关于通道与中断号的对应关系不同型号的芯片有各自的中断路由。对于T113/R528这一类小核的Mailbox中断会接到PLIC的某个IRQ编号上小核侧程序里需要正确初始化PLIC的对应中断源这是参考全志SDK和芯片手册得到的实践具体编号请以你的芯片手册为准。4.2 共享内存协议比想象中容易踩雷的设计数据本身走共享内存。最基础的设计是定义一套简单的消息头struct message { uint32_t magic; /* 魔数防止读到未初始化的内存 */ uint32_t type; /* 消息类型 */ uint32_t length; /* 数据长度 */ uint32_t seq; /* 序号 */ uint8_t payload[64]; };主核往共享内存写入消息后通过Mailbox发出一个新消息到的中断小核收到中断后读取共享内存处理完再通过反向Mailbox回一个处理完毕通知。听起来很简单但这里我吃过一次亏主核对共享内存的写入不一定立刻对小核可见。原因是主核ARM侧有cache小核直读DDR时读到的可能是cache line滞后的旧数据。解决这个问题有两种方式。一是在写数据后、发Mailbox中断前执行cache clean操作dma_map_single或dma_sync_single_for_device。二是绕开cache问题——直接把共享内存放SRAM。全志芯片内部有SRAM主核和小核都能访问SRAM通常是不带缓存直连的不存在一致性问题。代价是SRAM容量小比如T113的SRAM大概几十到几百KB量级只能放轻量级交互数据。我自己的做法是交互频繁的小消息走SRAM大块数据走DDR预留区并配合cache同步。4.3 一个最简单的mbox收发示例U-Boot阶段验证用调试通讯时我不会一上来就写完整驱动而是先做最小验证主核写个死循环等小核中断小核跑起来后发一个固定数据包过来。这个验证放在U-Boot阶段做因为这里没有Linux的复杂内存管理出问题容易排查。/* 以全志常见mbox寄存器布局为例具体偏移以手册为准 */ #define MBOX_BASE 0x03002000 static void mbox_test_wait(void) { volatile uint32_t *reg (uint32_t *)MBOX_BASE; unsigned long timeout 1000000; /* 清中断标志 */ writel(0x1, reg 0x10); /* 使能小核-主核方向中断 */ writel(0x1, reg 0x18); while (timeout--) { if (readl(reg 0x14) 0x1) { printf(mbox: received interrupt.\n); /* 读共享内存消息 */ uint32_t *msg (uint32_t *)0x48000000; printf(mbox: msg magic0x%08x type%d\n, msg[0], msg[1]); break; } } }这个阶段的核心目的只是确认中断通路内存通路是好的。确认无误后再往Linux驱动和业务协议上平移基本不会走弯路。5. 调试路上的五个深坑与对应解法异构bring-up的调试比普通启动调试更让人头疼原因我之前说过小核死了没有任何输出你只有没反应一个症状。这里把我实际踩过的几个坑完整列出来每个都是真金白银换来的教训。5.1 坑一固件加载地址没对上小核从头到尾没跑现象U-Boot阶段该写的寄存器都写了时钟开了、复位解了但小核什么动静都没有。排查时先确认一件事入口地址到底对不对。我犯过的错误是直接照抄别的板子的Boot Address结果那款芯片小核的复位向量跟T113不一样。这种问题排查方法很简单但很多人不会先查看编译出来的固件反汇编第一条指令是什么。如果固件的第一条指令出现在文件偏移0那它的加载入口就是Link地址本身如果你的固件开头有段跳转表通常是j指令那要确保跳转目标跟实际加载地址匹配。还有一个隐蔽点全志的小核固件如果是基于厂商SDK编译的它的内存布局里往往包含对SRAM地址的引用而你把固件load到了DDR。在这种情况下固件虽然开始了但内部访问某个外设寄存器时用的还是SRAM地址空间可能直接跑到无效区域产生异常。这时候建议把固件的链接脚本打开看清楚确认text段和数据段的地址是否都在你预留的DDR区间内。5.2 坑二核跑起来了但Mailbox中断永远不来现象小核固件在跑比如你让小核点个灯灯亮了但主核等Mailbox中断等半天等不到。这个坑的根子几乎都在中断控制器配置上。全志的Mailbox硬件会向两个方向产生中断但每个方向的中断要分别enable到对应的目标中断控制器。你光把主核这边的GIC中断使能了但小核那边根本不知道我要发一个中断给对面它的软件里必须也对Mailbox的发送方向做对应的初始化。另一个常见病是主核这边使能了GIC中断但没清掉硬件上pending的中断标志。如果之前有过一次中断没处理标志位一直挂着第二次中断来的时候硬件层面无法再次触发表现为只有第一次中断之后再也不来。5.3 坑三时钟配好就解复位顺序反了直接卡死现象小核看起来上电就卡死每次想重跑都要断电重启否则怎么解复位都没用。这是时钟和复位的顺序问题。全志的手册里对RISC-V核的推荐流程一般是先打开小核时钟、等时钟稳定然后再解除复位。如果你在时钟还没就绪时就把复位撤了小核内部逻辑在无时钟状态下被强行释放状态机可能进入一个奇奇怪怪的锁死状态。更典型的场景是你把小核时钟配成某个PLL分频值后发现频率不对直接改时钟树配置但没有先拉复位小核在时钟瞬间的毛刺里跑飞了。正确顺序配时钟 - 拉复位 - 确认时钟稳定 - 解复位 - 写入口地址。等等入口地址应该在解复位之前还是之后我的经验是入口地址寄存器本身是个静态配置项你可以在解复位前写进去保证小核解复位的一瞬间PC取的就是正确地址。把地址放在复位释放之后才写会有小核已经跑了几个指令周期、但PC指向位置还是错的或者全是0的窗口期。5.4 坑四主核串口和RISC-V小核串口被复用现象小核跑起来了但主核串口控制台突然出现乱码或者彻底不输出。这个坑在R528/T113这类的板子上特别常见因为小核的调试串口和控制台串口可能是同一组UART引脚。U-Boot阶段你手动操作寄存器把小核时钟打开、解除复位之后小核固件自己的printf会往它的调试串口写数据。如果它的调试串口和主核用同一个物理UART两边同时写就会互相干扰控制台输出自然就乱了。解决办法在正式规划里给小核分配独立的调试串口不要在同一个物理串口上debug两个核。实在只有一个串口那就只在调试阶段用小核日志跑稳定以后关掉小核的printf用Mailbox把日志发到主核这边来打印。5.5 坑五用完小核想重启结果整个系统挂了现象固件异常后你echo stop再echo start想重启小核结果主控也可能一起崩掉。这类问题大部分出在共享内存没有正确清理。小核崩溃的时候它的最后状态、寄存器的痕迹、内存里的消息队列数据都停留在共享内存里。重启小核后小核固件从入口重新跑但消息队列还是旧的于是重复处理旧消息、或者序列号错乱行为变得很古怪。正确的重启姿势停掉小核复位后主核要把共享内存区域清零重新初始化Mailbox软状态清pending中断、重置序列号然后才重新加载固件。另外强烈建议在共享内存里保留一个小核异常状态区小核崩溃前把异常类型写到固定偏移这样主核读取后能知道它死在哪一段代码上。6. 异构bring-up操作清单与我的个人经验最后沉淀一份操作清单按照我实际走通的顺序排列。不敢说每个芯片百分之百适用但流程框架对全志系列基本是通用的你只需要对着自己的《芯片用户手册》把寄存器地址换掉就行。Pull小核固件阶段编译小核固件确认链接地址尤其text段和data段落在预留内存区域在U-Boot环境变量里定义固件加载地址变量例如setenv e907_loadaddr 0x48000000加载固件后md.b看一下固件头确认不是全0或全FF时钟复位阶段打开小核时钟配置PLL分频写入口地址到复位控制寄存器解除复位等约1ms按实际PLL稳定时间调整观察小核是否开始执行早期可以用小核点灯、或者写一个固定值到共享内存标志位内核阶段设备树配置reserved-memory no-map确认#define CONFIG_REMOTEPROC和相关驱动编入内核用devmem手动加载固件确认小核能起来起稳定后切换remoteproc管理验证echo start/echo stop通讯阶段从SRAM里放一个简单消息结构小核启动后主动往上写主核读SRAM验证数据再写回一个确认值跑通Mailbox收发中断再扩展业务消息我自己吃过最大的亏是太急着上业务代码。第一次做T113异构开发的时候有点兴奋Linux刚起来没花多久就把小核固件跑通了然后直接开始设计复杂消息协议结果中间某个片段的cache未同步问题排查了快一周后来才意识到是预留区没做cache clean导致小核读到的永远落后半拍。从那以后我养成了一个习惯每做一个异构步骤都先做最小验证验证通过再往前走。这个习惯在排错时救了我无数次。最后分享一个实用技巧小核固件调试时不要让它一上来就跑全功能先把大循环里加一个自增计数器写到共享内存固定偏移。这样主核每隔几秒读一次该地址如果数值一直在涨说明小核活着如果停了说明卡死——这个计数器就是小核的心跳灯比任何调试器都直观。用这种方式先验证固件主循环再逐步打开外设驱动你会发现自己定位问题的速度提升一大截。