
在嵌入式圈子里,有一类板子特别招人喜欢:明明是主流架构的SoC,却偏偏藏着一颗不太为人知的协处理核心。手里这块基于Allwinner T527的工业级板卡,四核Cortex-A55之外,竟然还集成了一颗平头哥的玄铁C906 RISC-V核。标题里用了Unlocking这个词,听起来有点夸张,但当你发现这颗核默认被厂商固件占用、Linux侧完全不可见时,你会发现解锁这个动作是真实存在的。这篇文章是系列的第一篇,我先把探索思路完整过一遍:为什么T527里会有RISC-V核、它在启动链路里处在什么位置、如何从Linux侧确认它的存在、以及怎么做最基础的固件编译与加载准备。真正把C906跑成协处理器的完整实现,我会放在Part 2里展开。如果你是做嵌入式底层开发、对RISC-V好奇、或者手里正好有T527相关板卡的开发者,这篇应该能帮你省下不少查手册和试错的功夫。1. 当一颗C906藏在A55集群里——T527的异构布局1.1 XuanTie C906并非玩具内核平头哥玄铁系列的定位很清楚:C906不是那种只能跑点玩具代码的最小RISC-V实现,它是一颗完整的64位顺序双发处理器,支持RV64IMAFDC指令集,带有矢量扩展和内存管理单元。C906的IPC虽然没法跟Cortex-A55比,但它胜在结构简单、功耗可控、启动逻辑透明,非常适合做SoC里的辅助处理单元。在T527的芯片框图里,这颗C906通常被标成信号处理协处理器或者安全/管理工作核。全志把它放在四核A55旁边的目的,并不是让它跟A55抢通用计算任务,而是承担三类工作:一是低功耗场景下的常驻裸机任务,二是需要向量运算的前处理/后处理,三是可以独立于Linux运行的时间关键型控制任务。这颗核在微架构层面的设计很有意思。三级流水线配合紧凑的取指单元,没有复杂的分支预测器,所有的硬件资源都往确定性上倾斜。也就是说,你把它当作一颗可以完全掌控的实时协处理核,比把它当成一颗小号A55要合理得多。它的向量部件支持RVV 0.7.1版本,虽然跟最新的RVV 1.0有差异,但在这类工业SoC里,C906的向量计算能力已经足够处理音频滤波、FFT、图像预处理这类负载。1.2 解锁到底在解锁什么如果你刚拿到T527的开发板,直接把SD卡烧上主线Linux内核启动,你大概率不会在系统里看到任何RISC-V的痕迹。C906默认被出厂配置接管:可能是全志自带的某个FEL固件,也可能是芯片初始化代码里的一个死循环。总之,这颗核在默认状态下是一块不可编程的保留区域。解锁动作实际上包含三件事:第一,从硬件层面确认C906确实存在,并且搞清楚它的复位和启动矢量由哪个寄存器控制。这一步是关键中的关键,因为不同批次的T527寄存器映射可能存在差异,不能只凭一份旧手册就想当然。第二,绕过或者替换默认的固件加载路径,让C906的入口地址指向我们自己编译的裸机程序。这里涉及启动链路的改动,后面我会详细讲。第三,打通A55核与C906核之间的通信通道。RISC-V核有自己的CLINT(定时器与核间中断)和PLIC(平台级中断控制器),跟A55侧的GIC是两套独立的东西,如何建立最小可用的消息机制,是解锁完成后最优先要考虑的问题。这也是为什么我坚持把它分成Part 1和Part 2。Part 1解决它在哪、怎么找到它、怎么编译一个能跑的最小固件,Part 2才做完整的固件业务逻辑和核间通信框架。你如果跳过第一步直接去写C906的业务代码,很容易因为基础链路没打通,排错排到怀疑人生。2. 启动链条上的RISC-V:固件链路与核加载次序2.1 从BROM到Linux,C906在哪一步被唤醒T527的启动流程跟全志平台其他芯片大致类似:芯片内部的BROM(只读启动ROM)先从SD卡、eMMC或SPI NOR里加载SPL,SPL接着拉起ATF和U-Boot,U-Boot最终引导Linux内核。整个过程中,RISC-V核的激活时机比较微妙。根据全志平台常见的设计思路,这颗RISC-V核通常是由BROM或者SPL在最早阶段就完成初始化的,芯片会从一段固定地址把小核固件拷贝到RISC-V核的专用SRAM或共享DRAM里,然后设置启动矢量并解除复位。也就是说,你还没看到U-Boot的logo,C906可能已经在一个死循环里等待指令了。但这里有个变数:厂商引导固件可能会根据启动介质、安全启动标志或者eFUSE状态,决定要不要激活C906。如果你手里的板子配置了安全启动,C906的入口地址可能会被锁定在一个受保护的区域,这种情况下解锁的难度会大不少。实际使用中,建议先通过串口把完整的SPL日志抓出来,看看里面有没有关于riscv或c906的打印信息。我在调试时发现,部分T527的SPL日志里会打印一行类似riscv core start at 0x...的信息,这正是确认激活时机的第一手证据。如果日志里没有,那就需要从芯片的寄存器层面去查C906的当前状态。2.2 设备树视角:系统如何看见异构核Linux内核不会因为某个核存在就自动识别,它需要设备树里明确描述。在T527的设备树中,如果厂商的BSP把C906节点编进去了,你会看到类似这样的片段:cpus { #address-cells 1; #size-cells 0; riscv0: cpu0 { compatible riscv; device_type cpu; riscv,isa rv64imafdc; reg 0; status disabled; }; };注意status disabled,这是关键。厂商默认把它禁掉,内核自然不认。我们需要把这个状态改成okay,并且确保配套的reserved-memory区域没有被其他节点占用。否则即使内核看到了节点,也会因为内存冲突而拒绝启用。这时候Linux的cpuinfo里不会出现这个CPU核,因为RISC-V的hart(硬件线程)信息在Linux的进程调度器看来属于非标准CPU,需要由对应的驱动去接管。设备树里的compatible riscv只是一个标记,真正让C906驱动起来的,是后面要讲的remoteproc框架。2.3 ATF与U-Boot对C906的初始化细节在前面的启动流程里,ATF(可信固件)通常会负责把C906的启动地址寄存器设置成一个约定值。这块逻辑分布在不同固件阶段,不同厂商的实现也不同,没有统一的代码路径。我在Part 1里建议你做的,是在U-Boot阶段用md命令直接读几个关键寄存器,确认C906的复位状态。比如,假定某颗T527的C906控制寄存器基址为0x0300B000,启动矢量寄存器偏移为0x14,复位控制偏移为0x1c,可以这样验证:# 在U-Boot命令行下读取C906相关寄存器 md.l 0x0300B000 0x10 # 如果能看到启动矢量已经被设置,说明固件已经初始化过C906需要特别说明,这里的具体寄存器地址必须根据你能拿到的芯片手册来确认。不同批次、不同封装,寄存器偏移都可能不同。我在这里写这个例子,是想说明排查思路:你不需要一开始就去逆向固件,先看寄存器值有没有变化,就能知道C906走到了哪一步。U-Boot阶段还有一个重要的点:内存映射。C906通过AXI总线和A55共享DRAM,但它的地址空间里,除了DRAM之外还有一段私有SRAM。通常厂商会把RISC-V核的固件放在私有SRAM里,因为这样不占用主内存,也不容易被Linux内核破坏。如果你解锁后想让C906跑更复杂的程序,就得把固件或共享内存挪到DRAM区域,这时候reserved-memory的规划就变得非常重要。3. 观测实操:在Linux侧确认C906核的存在与状态3.1 cpuinfo、设备树与系统日志的交叉验证直接进Linux系统,用几个简单命令就能判断这颗核当前是什么状态。我建议交叉验证,不要只看一个来源。# 查看设备树里的CPUs节点 ls -l /proc/device-tree/cpus/ # 读取某个CPU节点的isn属性(如果存在) cat /proc/device-tree/cpus/riscv0/riscv,isa 2/dev/null # 搜索内核日志里的riscv相关输出 dmesg | grep -i riscv # 查看remoteproc设备是否存在 ls /sys/class/remoteproc/ 2/dev/null | grep rproc我实测时的情况是:设备树里有节点但状态是disabled,dmesg完全没有任何riscv打印,/sys/class/remoteproc目录为空。这说明C906在固件层面已经被初始化,但Linux侧完全没有接管逻辑。当你看到这个组合时,解锁的第一步就成了:让一个标准remoteproc框架认领它。3.2 时钟、复位与电源域的访问路径即使C906的启动矢量已经设置,如果它的时钟没开或者还被按在复位状态,程序也跑不起来。全志平台的时钟和复位控制通常在CCU(时钟控制单元)里,通过设备树clk和reset属性描述。我采用的方法比较直接:先看Linux里已经暴露的时钟列表,找到riscv相关条目:# 列出系统里所有时钟 ls /sys/kernel/debug/clk/ # 找到riscv/cluster/c906相关的时钟名 find /sys/kernel/debug/clk/ -iname *riscv* -o -iname *c906* -o -iname *rproc* 2/dev/null如果内核驱动没有使能C906的时钟,你需要写一个小驱动或者在U-Boot阶段直接把时钟配置好。更稳妥的做法是在U-Boot阶段接管,因为那里没有Linux电源管理框架的干扰。全志平台不少板卡默认会在ATF阶段把RISC-V核的时钟打开,但状态可能不是CLK_ON,而是CLK_AUTO_OFF。这种情况下,核在空闲时会掉电,你打RISC-V调试口看寄存器,会以为硬件坏了。3.3 RISC-V核的独立调试通道C906支持标准的RISC-V调试接口,如果板卡引出了JTAG,你可以用T-HEAD官方的调试器直接连上去。不过工业级板卡大多不会专门引出RISC-V核的JTAG,所以实测阶段我更多依赖共享内存这个软通道。一个很实用的验证技巧:在共享内存区域写一个固定的魔数,让C906的主循环轮询它。只要C906在跑,它就更新另一块内存里的状态字;A55侧循环读这个状态字,就能确认C906活着。这个机制也是后面做核间通信的雏形。# 假设共享内存区域在0x70000000,用devmem写入魔数 devmem 0x70000000 32 0xDEADBEEF # 然后循环读取0x70000004看C906是否回应 devmem 0x70000004 32实测时我看到状态字从全0变成非0,才敢说C906确实活了。这种内存回环的方式比任何调试器都直接,而且不依赖额外的硬件。4. 让C906真正跑起来:固件编译与加载方案4.1 拉取工具链并编译一个最小固件C906是标准RV64,这意味着你不需要特殊魔改工具链,直接使用通用的riscv64-unknown-elf-gcc就可以编译裸机固件。如果你的发行版没带这个工具链,可以去bootlin工具链网站下或者用apt装交叉编译包:# 安装riscv工具链(Ubuntu/Debian示例) apt install gcc-riscv64-unknown-elf # 确认工具链可用 riscv64-unknown-elf-gcc --version接下来写一个最基础的固件,不依赖任何库,直接在C906的UART上打印一行字符。为了方便演示,我简化了寄存器地址,重点展示代码骨架:// c906_mini.c void kernel_main(void) { // 假设UART地址为0x02500000,这是一个示意 volatile unsigned int *uart (volatile unsigned int *)0x02500000; const char *msg hello from c906\r\n; while (*msg) { *uart *msg; msg; } while (1) { __asm__ volatile(wfi); } }对应的链接脚本需要定义入口地址。这里使用0x70000000作为示例,实际地址要根据你规划的共享内存和固件加载区域来定:OUTPUT_ARCH(riscv) ENTRY(kernel_main) SECTIONS { . 0x70000000; .text : { *(.text.entry) *(.text.*) } .data : { *(.data.*) *(.sdata.*) } .bss : { *(.bss.*) } }编译命令:riscv64-unknown-elf-gcc -marchrv64imafdc -mabilp64d -nostdlib -T c906.ld c906_mini.c -o c906_mini.elf # 生成纯二进制固件 riscv64-unknown-elf-objcopy -O binary c906_mini.elf c906_mini.bin很多人在这一步就踩坑了:用-marchrv64imac编译,导致生成的固件里没有浮点指令,这本身没问题,但如果C906的向量扩展没开启,某些基础库里的内联汇编会触发非法指令异常。建议直接用rv64imafdc,跟设备树里写的isa保持一致。4.2 reserved-memory与共享内存规划C906跑起来之后,它的代码和堆栈必须待在固定的物理地址,不能被Linux内核动态分配走。最规范的解决办法是在设备树里预留内存。reserved-memory { #address-cells 2; #size-cells 2; ranges; c906_fw_reserved: c906-fw70000000 { reg 0x0 0x70000000 0x0 0x1000000; no-map; }; c906_shared_reserved: c906-shared71000000 { reg 0x0 0x71000000 0x0 0x100000; no-map; }; };no-map属性非常关键,它告诉内核这段内存不要做页表映射、不要缓存管理。如果没有no-map,Linux可能会对这段地址做缓存映射,你和C906操作同一块内存时,会因为缓存一致性问题拿到完全不同的值。这里的内存地址我是按常见做法举例的,具体选哪段地址取决于你板卡的DRAM映射。我建议优先选那些不参与Linux内存映射的空洞区域,比如某些SoC在0x70000000附近有一段保留给协处理器的空间。选择低地址段的预留内存还有个好处:可以用devmem直接访问,不需要等内核驱动准备好。4.3 remoteproc框架接入remoteproc是Linux内核里管理协处理器的标准框架,原本是给DSP、GPU、单片机器件准备的。T527的C906完全可以被纳入这个模型:主核(Host)通过remoteproc的接口加载固件、启动协处理核、处理崩溃事件,协处理核则通过rpmsg或自定协议通信。要让remoteproc认领C906,设备树里需要增加一个remoteproc节点:c906_rproc { compatible allwinner,sun8i-t527-rproc; reg 0x0 0x00000000 0x0 0x1000; memory-region c906_fw_reserved, c906_shared_reserved; resets ccu 216; reset-names rst; clock-names clk; clocks ccu 215; status disabled; };驱动的关键逻辑其实并不复杂:通过触摸复位控制和启动矢量寄存器,把固件拷贝到指定的memory-region,然后解除复位。RISC-V核的boot方式跟很多DSP类似,都是主机写入口地址,核自行取指运行。生产级别的驱动代码里,还需要处理异常报告和资源表解析,但最小可用的驱动几十行就够了。实际加载固件的方式有两种:内核加载和直接操作寄存器。如果你不想写驱动,可以先在U-Boot阶段把固件拷进内存,然后让C906的启动矢量指向它,然后手动复位。这种野路子适合验证,不适合产品。我在Part 1里更推荐把remoteproc框架的设备树节点准备好,哪怕先不做完整驱动,至少把框架的轮廓搭好,后面Part 2可以直接填核心逻辑。4.4 打印、中断与门铃:通信机制的最小闭环当你的C906固件能在共享内存的一个标志位上回应主核时,第一步验证就算成功了。但这离能用还差两步:给C906一个中断通道,以及让它能主动向主核发信号。先看最简单的门铃机制。RISC-V核之间、或者RISC-V与A55之间,通常通过一组doorbell寄存器互相发中断。A55侧往doorbell寄存器写一个bit,会触发C906的PLIC中断;反过来,C906往另一个doorbell寄存器写数据,A55侧会收到一个IRQ。在Part 1里,我不打算把这个机制完整实现——那需要写两个驱动模块。但我建议你先在共享内存里规划好通信结构:#define SHARED_MEM_BASE 0x71000000 struct c906_comms { uint32_t magic; // 固定魔数,用于校验共享内存是否可用 uint32_t status; // 0: idle, 1: busy, 2: done uint32_t command; // 主核下发的命令 uint32_t data[16]; // 数据区 };C906固件循环检查magic是否被主核写入,一旦发现command ! 0,就执行对应的操作,然后把status从busy改成done。这个轮询机制虽然不如中断高效,但它是调试阶段最简单可靠的办法——不需要处理中断优先级、不需要配置PLIC路由,只靠内存读写就能把两个核串起来。我实测的感受是,轮询模型下,从A55写命令到C906返回状态,延迟大概在几微秒量级,这在很多控制场景下已经够用。如果你需要更低的延迟,再上中断模型。5. 常见问题与排查技巧实录5.1 现象:C906没有任何输出,但系统整体正常这是最典型的情况,通常不是解锁失败,而是你根本没让它跑。排查顺序非常重要,强烈建议按下面这个顺序走:第一,确认C906的时钟使能。用clk调试接口查看,如果时钟还在disabled状态,写一个小脚本把它打开。第二,确认启动矢量寄存器。如果启动矢量指向0,那它一被唤醒就跑到零地址去了,自然没有输出。第三,确认UART是否复用正确。C906的调试串口通常是独立的物理引脚,需要配置pinmux才能输出,而很多板卡的pinmux默认没有给RISC-V核的UART分配。第四,检查固件本身的链接地址。如果链接脚本里入口地址跟启动矢量不一致,C906可能取指到错误位置,直接hang死。5.2 现象:固件load进去就hang,主系统都被影响这个问题多半是内存惹的祸。C906的固件区域如果没有用no-map或者没有跟内核内存管理隔离,Linux可能会在启动过程中往这片区域写入页表结构,直接覆盖你的固件。另一个隐蔽原因是cache一致性问题。你在A55侧读状态字读到的是cache里的旧值,而C906写的是DRAM里的新值。解决方法是把共享内存的页表属性设为非缓存(Non-Cacheable),或者用dmb/fence指令做显式的同步。在设备树里正确配置reserved-memory的no-map,同时在内核驱动里使用dma_alloc_coherent来分配共享缓冲区,是最省心的做法。5.3 现象:寄存器写入毫无反应,仿佛这颗核不存在如果寄存器的值写进去再读出来还是0,基本可以锁定在三个方向:总线访问权限、电源域、或芯片版本差异。总线访问权限这个问题经常被忽略。T527上有安全世界的总线隔离,EL1或EL2的代码访问某些寄存器会被静默丢弃。你可以试试在ATF阶段或者用一个安全侧驱动去访问,再看值有没有变化。电源域这个方向也很常见:C906所在的电源域可能被Linux的cpu idle代码关掉了。你在应用层写寄存器时它没上电,自然没反应。芯片版本差异则需要特别提醒:同一型号的不同批次,寄存器布局可能有小范围偏移。建议找两份不同版本的芯片手册交叉核对,或者直接反汇编出厂固件,找到它操作的寄存器地址作为参考。5.4 排查思路总结:从Boot Log倒推C906的遭遇在T527这类平台上,排查C906问题最忌讳盯着Linux内核日志看。内核日志只能告诉你主核侧的情况;RISC-V核的状态,要去SPL和ATF的启动阶段找线索。我习惯把完整串口日志分成三段:SPL阶段、ATF阶段、Linux阶段。在SPL阶段看有没有RISC-V相关的初始化打印;在ATF阶段看有没有访问C906控制寄存器的动作;在Linux阶段看设备树加载和remoteproc的probe情况。三段日志一拼,问题的范围瞬间缩小。我也建议在调试阶段先关掉C906的动态电源管理,不让它自动睡眠。很多解不开的问题,其实是这核睡过去了,并不是硬件坏了。现象主要检查点快速定位命令/方法C906无输出时钟、复位、启动矢量、pinmuxclk调试文件、U-Bootmd读寄存器固件load后hang内存预留、cache一致检查reserved-memory是否no-map,用devmem写读一致性寄存器无响应总线权限、电源域、版本差异对比不同阶段串口日志,核对多版本手册无法确认核活着共享内存魔数轮询状态字的循环测试最后聊几句经验这一次解锁T527上的C906,我的体会是:这类异构协处理核真正难的点不在于写固件,而在于启动链路的透明度和内存布局的严谨性。只要把它是被谁拉起来的、它看内存的视角是什么、它如何被唤醒这三个问题想明白了,后面写业务代码就跟开发一颗普通MCU没什么区别。先把你板子上的dmesg和U-Boot日志完整存一份,照着上面第3节的命令把C906的当前状态摸清楚,再动手编译固件,成功率会高很多。我犯过的错就是用编译好的固件直接试,结果日志里找不到C906的线索,白白折腾了两个晚上。希望你在看完本文之后绕开这些坑,期待Part 2见。