拆开全志Allwinner T527的数据手册大多数人第一眼盯上的都是那几颗Cortex-A55和Cortex-A75我反而对藏在SoC下层的玄铁RISC-V核心更感兴趣。这颗核心在公开宣传里通常只有一句“安全协处理器”带过但真正把它当独立主控用起来的人非常少。这篇是“Unlocking the XuanTie RISC-V Core on Allwinner T527”系列的第一篇目标很直接在不破坏原有安全启动功能的前提下把这颗核心“借”出来让它跑我们自己编译的裸机程序并且能和主核交换数据。适合手里有T527开发板、想做异构多核实验或者对RISC-V裸机开发感兴趣的嵌入式Linux玩家。整个探索过程我会分成三条主线来讲核心到底挂在哪个地址空间、怎么给它解锁时钟和复位、第一个固件怎么构建以及如何跟A核通信。1. 先看清 T527 里这颗 RISC-V 核心到底是什么来头1.1 玄铁 C906 的关键参数从公开手册和固件特征来判断T527 里的这颗核心属于平头哥玄铁 C906 这一档扩展组合是 RV64IMAFDC也就是整数、乘除、原子、单精度浮点和双精度浮点都带还可能有可选的向量扩展。对裸机开发来说最需要记住的是三点。第一它默认是小端模式启动。玄铁核本身可以配置成大端或小端但全志的集成方式是小端所以链接脚本和编译参数里都不需要额外处理字节序。第二C906 的异常模型和标准 RISC-V 保持一致mstatus、mtvec、mepc、mcause 这些 CSR 都在早期的 trap 处理可以完全按照标准规范来写。第三它虽然带 MMU但裸机阶段我建议直接把satp清零跑在物理地址模式。这样省掉页表调试时看到的地址就是真实总线地址不用来回换算。这三个参数直接决定了你的工具链选项和启动汇编写法。网上很多 RISC-V 教程默认你用 qemu 或者开发板但内置在 SoC 里的核心有一个很大的不同没有现成的固件加载器帮你初始化环境复位之后它直接取指执行栈指针、BSS 清零、向量表都要你自己来。1.2 它在 SoC 内部的位置和角色从系统框图来看这颗核心挂在 R_BUS 域里。全志的惯用做法是A 核跑 LinuxRISC-V 核做安全岛或者实时控制岛。也就是说它并不参与 Linux 的日常计算任务而是被厂商固件当作一个独立的主控来用可能负责安全启动校验、密钥管理、待机唤醒这类工作。我们解锁它不是要让 Linux 直接“调度”它而是要在另一个核的世界里给它喂固件让它和 A 核并行工作。它有自己的缓存和私有 SRAM也有和片外 DDR 通信的路径所以严格来说是一个完整的处理器不是某个外设。理解这一点很重要后续所有开发都可以沿用“独立单片机”的思维而不是“Linux 驱动外设”的思维。1.3 为什么 Linux 侧默认看不见它很多人第一个反应是去查/proc/cpuinfo但你当然看不到它。原因是 C906 不在 ARMv8 的 CPU 拓扑里它更像是挂在系统总线上的一台从设备。正常的访问路径是主核准备好固件放到约定地址然后通过系统控制寄存器给核心上电、开时钟、去复位。因此“解锁”这个词的本质就是我们要自己接管这几步操作。厂商 BootROM 可能会在启动早期把它管理起来也可能直接放着不用不同批次和不同开发板的表现还不一样。我在 T527 上观察到的默认状态是核心的时钟被关闭复位线保持拉低对应 SRAM 区域在总线层面对非安全世界的访问也有限制需要先做权限切换。这三点串起来就是解锁的全部内容。2. 三条线索定位核心手册、设备树与固件2.1 从芯片手册锁定 SRAM 与系统控制寄存器第一步永远是翻手册。重点看“Memory Map”和“System Control”两章。我的找法非常机械先定位关键词C906、RISC-V或Security Core找到核心对应的 SRAM 区域。全志这颗核心通常有一个私有 SRAM几十 KB 到一两百 KB这就是裸机固件的安身之处。接着找C906 soft reset和C906 clock gate相关的寄存器一般在 CCU 模块里带RISC_V或RV前缀。如果你手里的手册版本比较老里面可能直接写的是E906之类的代号。别被名字骗了本质上是同一类东西。真正确认的方法是看寄存器描述里有没有提到“RISC-V core”或“C906”字样。把下面这几项记到一份表格里后续所有代码都围绕它们展开项目手册章节建议备注SRAM 基址和大小Memory Map决定固件的链接地址核心时钟门控寄存器CCU决定核心能不能取指核心复位控制寄存器CCU决定核心从哪开始跑SRAM 访问权限控制SMPU/SRAM Controller决定主核能不能读写可能的邮箱中断号Device Tree / GIC后续通信用2.2 设备树里可能留下的痕迹如果你用的是厂商的 Linux BSP不要急着自己发明寄存器地址先翻 DTS。不少全志 T5 系列的内核里都会有类似下面这样的节点虽然节点名字可能是c906、rv_core或security-core但功能上都是把核心的寄存器基址和中断号暴露给驱动。下面这段是形状示例T527 上的真实地址要以你自己拿到的 DTS 和手册为准rv_core: rv-core2c000000 { compatible allwinner,sunxi-rv-core; reg 0x0 0x2c000000 0x0 0x40000, 0x0 0x07000400 0x0 0x100; resets ccu RST_RISCV_CORE; clocks ccu CLK_RISCV_CORE; };看到resets和clocks两个属性基本可以确定这颗核心由 CCU 管理。只要把这两个属性对应的寄存器摸清楚解锁就完成了大半。另外留意interrupts属性后面做邮箱中断时要用到。2.3 从厂商固件里提取入口向量手册和 DTS 只能告诉我们“核心在哪里”但厂商固件能告诉我们“核心怎么启动”。这一步经常被跳过但它其实是最有价值的一条线索。我习惯的做法是把整个升级固件拉下来用binwalk拆开找到里面比较大的独立镜像块。很多全志固件里都会有一段 RISC-V 可执行程序用file命令可以直接得到类似ELF 64-bit LSB executable, UCB RISC-V的输出。拿到 ELF 之后不要急着反汇编整个程序先做两件事用riscv64-unknown-elf-readelf -h查入口地址确认厂商固件是在哪个基址上运行的用riscv64-unknown-elf-readelf -S看.text段的对齐方式推测核心对向量表对齐的要求。这一步非常有用。我最初调试 T527 时就是因为没看厂商固件盲目把代码链到自认为对的 SRAM 地址核心复位后一点反应都没有。看了固件入口地址之后才明白这颗核心的启动地址其实就是它私有 SRAM 区域的起始地址。早期版本厂商固件甚至可以从字符串或者符号表里看到内部模块名对我们理解它的软件架构很有帮助。3. 解锁实操电源、时钟、复位和访问权限3.1 先把供电和时钟打开硬件上如果核心默认没有供电后面的复位操作全是白搭。在很多全志设计里RISC-V 核心和一部分 SRAM 共用电源域因此需要先确保对应的 PMIC 或 SoC 内部 LDO 处于输出状态。如果全程使用内核驱动最稳妥的方式是通过设备模型的power-domains属性间接管理简单粗暴一点也可以在模块初始化里用iomap操作 PMIC 寄存器。这里有一个必须遵守的时序电源先于时钟时钟先于复位。顺序反了可能出现总线错误而且这个错误往往只发生在第一次操作核心的时候很难排查。我踩过一次很深的坑电源域没确认就急着去写 CCU 时钟寄存器结果readl直接返回 0xFFFFFFFF整个内核模块 insmod 的时候卡死。后来加了dev_pm_domain_attach()才解决。建议你们在解锁前先确认电源域状态这个步骤看着不起眼却能省掉一晚上的崩溃调试。3.2 CCU 寄存器的操作要点CCU 里通常有一组 32 位寄存器每一位控制一颗核心的时钟门或复位线。读取时用readl写入时用writel这个过程放在内核模块里不能再简单static void __iomem *ccu_base; static void rv_core_enable(void) { u32 val; val readl(ccu_base CLK_RISCV_CORE); val | BIT(0); writel(val, ccu_base CLK_RISCV_CORE); val readl(ccu_base RST_RISCV_CORE); val ~BIT(0); writel(val, ccu_base RST_RISCV_CORE); }注意复位寄存器通常是高有效表示复位去复位就是清位但不同芯片也有反逻辑的情况务必以手册为准。我实际操作中发现有些 BSP 已经把时钟和复位在设备树里暴露成了 reset controller那么更省事的做法是直接调用reset_control_deassert()接口。不过手动操作的好处是能一步步观察效果排查问题时更清楚。3.3 SRAM 访问权限最容易卡住的一环这一步是解锁过程中最容易卡住的地方。就算时钟和复位都操作对了一旦 SRAM 区域在总线层面对主核访问受限memcpy_toio就写不进去就算强行写读回来也全是 0xFF 或者直接触发总线错误。全志很多 SoC 都有内存保护单元可以把某一整块 SRAM 的访问权限配给特定 CPU。解锁前需要把对应 SRAM 区域的主核读写权限打开。操作路径一般是找到 SMPU 或 SRAM Controller 里的“slave port access control”寄存器把影响目标 SRAM 的位设置为允许 A 核访问。这个寄存器位置每个 SoC 差异很大但判断方法是一致的读 SRAM 某个地址如果总是全 0xFF 或全 0x00先怀疑供电和时钟如果供电时钟正常但读写失败再怀疑 SMPU 权限如果 SMPU 权限已开但还是失败最后检查是不是被安全世界隔离了。T527 上我最终是修改了MEM_ACCESS_CTL相关位才让ioremap之后的地址可读可写。这一步没有捷径只能对着手册的安全控制章节逐位核对。3.4 加载固件并释放复位当电源、时钟、权限都准备好剩下就是两件事把固件二进制拷进 SRAM再去复位。void *rv_sram ioremap(RV_SRAM_BASE, RV_SRAM_SIZE); memcpy_toio(rv_sram, rv_firmware_bin, sizeof(rv_firmware_bin)); __iowmb(); // 释放复位 writel(0, ccu_base RST_RISCV_CORE);__iowmb()是很多人会漏掉的关键屏障。拷贝固件后如果不做内存屏障复位释放后核心可能读到一半的数据直接在第一条指令上跑飞。核心启动后最简单的验证方法是让固件在共享 SRAM 的固定位置写一个 magic 值主核这边循环读。等不到 magic 就按前面的顺序倒查权限、时钟、复位、链接地址。4. 第一个裸机程序工具链与 link.ld 细节4.1 裸机工具链为什么选 riscv64-unknown-elf如果你用厂商 SDK里面可能自带编译脚本。但自己写裸机程序时我更推荐riscv64-unknown-elf-前缀的工具链而不是riscv64-linux-gnu-。原因很实在前者是裸机导向的链接时不会引入 Linux 的启动文件、动态库和平台头文件编译参数更干净。下载安装之后先确认工具链目标架构riscv64-unknown-elf-gcc -v # Target: riscv64-unknown-elf riscv64-unknown-elf-gcc -marchrv64imafdc -mabilp64d -O2 -nostdlib \ -ffreestanding -c main.c -o main.o这里-marchrv64imafdc表示启用整数、乘除、原子、单精度浮点和双精度浮点。C906 在 T527 上是完整实现了 F/D 扩展的可以放心用。如果你看到cc1: error: ABI conflict之类的报错多半是-march里的扩展和-mabi不匹配比如用了rv64i却指定lp64d改成对应组合就行。4.2 link.ld 里最容易翻车的三个点网上搜risc-v link.ld能找到一大堆模板但给 SoC 内置核写链接脚本有三个点必须自己确认。第一个是入口符号一定要写ENTRY(_start)。C906 复位后直接跳到自己 SRAM 的起始地址不会帮你初始化栈不会帮你清 bss。如果入口符号不明确链接器可能把第一条指令放到奇怪的位置。第二个是 VMA 和 LMA 的关系。对这种“把代码直接放到 SRAM 运行”的场景VMA 等于 LMA把段地址指向 SRAM 基址即可。下面是一份我实测能用的模板OUTPUT_ARCH(riscv) ENTRY(_start) MEMORY { sram (rwx) : ORIGIN 0x20000000, LENGTH 128K } SECTIONS { . ORIGIN(sram); .text : { KEEP(*(.text.entry)) *(.text*) } sram .rodata : { *(.rodata*) } sram .data : { *(.data*) } sram .bss (NOLOAD) : { __bss_start .; *(.bss*); __bss_end .; } sram . ALIGN(0x1000); __vector_base .; . 0x1000; __stack_top .; }第三个点是栈顶和向量表对齐。玄铁 C906 的异常向量表在使能向量模式时需要按入口间隔排列为了省事我直接把mtvec基址对齐到 4KB向量区间预留 4KB。栈顶放在向量区后面确保栈不会往代码段反向生长。上面ORIGIN 0x20000000只是示例T527 上真实 SRAM 基址请以手册为准不要照抄。4.3 启动汇编与最小 trap 框架裸机_start只需要做四件事确认 hartid、设栈指针、清 bss、调 main。再补一个 trap stub防止中断异常直接跑飞。.section .text.entry .globl _start _start: csrr t0, mhartid bnez t0, 1f # 多核环境只让 hart0 继续 la sp, __stack_top # 清 bss la t0, __bss_start la t1, __bss_end 2: bgeu t0, t1, 3f sd zero, 0(t0) addi t0, t0, 8 j 2b 3: call main 1: wfi j 1btrap stub 可以很轻量但至少要保存现场再跳转分发的 C 函数.section .text .globl trap_entry trap_entry: addi sp, sp, -16*8 sd x1, 0(sp) sd x2, 8(sp) sd x3, 16(sp) # 保存必要寄存器后在这里读取 mcause 做分发 # 或者直接把 mcause 和 mepc 传给 C 函数 csrr a0, mcause csrr a1, mepc call handle_trap ld x1, 0(sp) ld x2, 8(sp) ld x3, 16(sp) addi sp, sp, 16*8 mret想省事的话第一版可以先在main里只做一件事往共享内存写递增计数。先把“核心能跑起来”验证了再引入中断。我甚至建议第一版不要开任何中断把全部注意力放在链路搭建上。4.4 把二进制塞进去从 objcopy 到内核模块编译完成后把 ELF 转成纯二进制riscv64-unknown-elf-objcopy -O binary rv_core.elf rv_core.binrv_core.bin的大小不要超过 SRAM 容量也不要覆盖共享内存约定区域。然后把数组声明进内核模块或者干脆用request_firmware从文件系统加载。推荐request_firmware因为调试周期短。每改一次固件只要把文件拷到/lib/firmware/再重新加载模块即可不用重新编译模块本体。经常有人问为什么核心跑不起来最后发现是强制用request_firmware时固件路径拼错了。这里建议在加载前后都打印一下firmware-size确认固件确实被读到再继续往下查。5. 给主核发消息共享内存和同步细节5.1 为什么第一版先用共享内存异构核之间通信有很多现成框架比如 RPMsg、OpenAMP。但在验证“核心能不能跑”的第一版里我强烈建议只用一个简单的共享内存结构体struct rv_msg { uint32_t magic; // 0x52564300 uint32_t cmd; uint32_t payload[8]; uint32_t ack; };结构体放在 SRAM 的固定偏移比如固件代码段结束之后。A 核写cmdRISC-V 核读到后处理再把结果写到payload并置位ack。A 核这边拿轮询读ack。没有任何框架却最容易排查问题。为什么不做 RPMsg因为 RPMsg 功能完善但一旦出问题你分不清是核心没起来、mailbox 没配、还是共享内存布局错了。先把裸协议跑通再迁移到成熟框架总成本反而最低。5.2 内存序和编译优化是最大的坑共享内存在两个核之间编译器会做优化CPU 也会做乱序。主核写完cmd之后必须保证写操作真的落到内存RISC-V 核才能看到。主核侧最简单的做法是加wmb()RISC-V 侧用fence rw, rw。C 代码里直接写内联汇编static inline void rv_fence(void) { __asm__ __volatile__(fence rw, rw ::: memory); }读取方也要配合volatile防止编译器把读缓存到寄存器while (volatile_read_ack(msg-ack) 0) ;我第一次实测时主核这边数据已经写了RISC-V 核的循环里读到的永远是旧值。加上fence和volatile之后立刻正常。这是这颗核心最容易迷惑新手的点不是时序太慢而是内存序没处理好。5.3 从轮询升级到邮箱中断轮询验证通过后再考虑真正的邮箱。全志在很多 A 系列 SoC 里提供软件中断或 mailbox 寄存器RISC-V 核写一个特定寄存器主核那边就会收到一个 SPI 中断中断号可以在设备树里查到。邮箱驱动的关键是把地址配成“主核能写、RISC-V 核能读”的共享区域否则会出现“RISC-V 核发了中断但主核没感知”的现象。此时优先查两件事中断号在 GIC 里是否被 mask以及邮箱寄存器所在的电源域有没有被关掉。6. 复盘踩过的坑哪些做法值得固定下来6.1 把解锁步骤脚本化别凭记忆敲寄存器调试过程中我经历了反复加载、反复开电源、反复去复位。每一次都手动敲devmem太痛苦。建议整理成两个文件一个内核模块负责把解锁步骤封装成 init 函数一个加载脚本负责把固件放到/lib/firmware/并加载模块把共享内存的关键地址打成日志。最终我手里的脚本大概五六十行没有任何优雅设计但每次断电重启都能在十秒内回到“核心起来、共享内存看到 magic”的状态。嵌入式调试里“可重复”比“简洁”重要得多。这一步做完再回头写驱动就从容多了。6.2 这些经验可以平移到哪里全志采用玄铁 RISC-V 核心的 SoC 不止 T527T507、V853、F133 等都有类似布局。本文的方法论并不依赖某一颗芯片的精确寄存器而是依赖一套排查顺序先手册找 SRAM 和 CCU再 DTS 找现有驱动再固件提取入口向量最后按“电源、时钟、权限、复位”的顺序解锁。这套顺序在类似架构的 SoC 上基本是通用的。我在这个过程中最大的体会是解锁并不是最难的部分难的是在目标核上跑逻辑时同时兼顾两个核的公序内存模型。每次写共享数据都要停下来想一遍另一个核看到的是什么这种思维切换才是异构开发的真正门槛。下一部分我会重点讲怎么把 FreeRTOS 移植到这颗核心上以及怎么用向量计算单元跑一些轻量级信号处理任务。如果你手头正好有 T527 开发板建议先把手动点亮核心的流程走通再来一起折腾操作系统级的东西。