ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

T527异构开发实战:唤醒玄铁C906 RISC-V核并跑通裸机程序

T527异构开发实战:唤醒玄铁C906 RISC-V核并跑通裸机程序 T527 这颗芯片在嵌入式圈子里讨论度不低但绝大多数人盯着的都是那四个 Cortex-A55 大核很少有人注意到它内部还藏着一颗玄铁XuanTieRISC-V 协处理核。我之前在评估 T527 做边缘计算方案时也是先把 Linux 跑起来再说直到某天在一个国产化项目的需求文档里看到一行“需具备 RISC-V 异构计算能力”才重新把目光放回到这颗被冷落的核上。这颗核不是摆设它是平头哥的 C906RV64 架构带 MMU能干不少正经活。这篇是系列的第一篇重点是把这颗 RISC-V 核“唤醒”的全过程确认它在系统里的位置、理解启动方式、搭好交叉编译环境、跑通第一个裸机程序并且把它从“能跑”推进到“能通信”。适合手里有 T527 开发板、想折腾异构核的嵌入式开发者和体系结构爱好者。1. 为什么要在 T527 上折腾这颗 RISC-V 核1.1 异构计算不是噱头是真能分担任务的做嵌入式这么久我对“异构计算”这个词一直比较谨慎因为很多芯片宣传页上的异构实际用起来要么文档稀碎要么工具链难产最后沦为跑分软件里的一行字。但 T527 的这颗 C906 不同它是一颗完整的 64 位 RISC-V 核心不是那种只能做安全隔离的小控制器而是可以独立运行程序、访问 DDR、甚至能跑轻量级 RTOS 的通用处理器核。在 T527 上A55 主核跑 Linux 处理复杂业务逻辑、网络协议栈、AI 推理调度C906 从核可以承担三类典型任务实时控制类电机控制、传感器采集、PWM 输出这类对中断延迟敏感的工作放在自带缓存的 RISC-V 核上响应时间比在 Linux 里跟调度器抢时间靠谱得多。低功耗常驻任务系统待机时 A55 可以进低功耗状态让 C906 保持一个轻量循环去监听唤醒事件功耗能降一个量级这是手机平台常驻协处理核的成熟思路。安全与隔离场景两颗核跑在独立的地址空间里可以通过硬件隔离做关键数据的独立处理甚至把一部分安全逻辑物理上隔离在 A55 的 Linux 之外。这些场景不是画饼C906 的运算能力和外设访问权限完全可以支撑。真正的前提是你得先能把它跑起来。1.2 主从核的协作模式先理解 AMP 而非 SMP很多人一提多核就想到 SMP——多个核跑同一个操作系统共享内存靠原子操作和自旋锁同步。但 T527 的 A55 和 C906 走的是另一条路AMP非对称多处理。在这种模式下两颗核各自运行独立的程序比如 A55 跑 LinuxC906 跑裸机或 RTOS通过共享内存和中断进行协作彼此之间没有调度器层面的耦合。这里有个非常关键的技术点T527 系统上电后默认只有 A55 作为主核启动并引导 Linux而 C906 处于复位状态需要主核侧主动“释放”它。从 A55 Linux 的角度看C906 更像一个“可加载固件的外设”你可以通过两种方式让它复位退出并跳转到指定地址执行U-Boot 阶段加载在 U-Boot 命令行下直接把固件拷贝到 DDR 的某个地址然后通过特定命令释放 C906 复位。这种方式适合调试阶段改完代码重新下载就行不碰内核。Linux 运行时加载通过内核的 remoteproc 框架把固件作为资源加载并启动 C906。这是产品化路径适合正式部署。Part 1 我先采取第一种方式把流程跑通理解启动链路之后再往 remoteproc 方向走。2. 先摸清 C906 的“脾气”架构、内存与工具链2.1 C906 是什么水平的核C906 属于平头哥玄铁系列是一条 9 级流水线、单发射、顺序执行的 64 位 RISC-V 内核指令集基线是 RV64IMAFDC也就是整数、乘除、原子操作、单双精度浮点都有还支持压缩指令。对于裸机开发来说这套指令集已经很够用了不需要额外扩展。它的 Level 1 Cache 是可配置的在 T527 上的具体配置以芯片手册为准一般来说 32KB 左右的 I-Cache 和 D-Cache 起步。比较关键的是它自带 MMU所以理论上你可以给它跑一个完整的 Linux但我们做裸机开发时会把 MMU 关掉直接用物理地址访问外设省去页表配置的复杂度开发效率最高。技术规格上要特别注意一点C906 支持 RISC-V 标准的中断控制器 PLICPlatform-Level Interrupt Controller以及核内可编程中断控制器 CLINT。这意味着写中断处理时不涉及黑科技全是标准规范里的套路。2.2 内存视图它眼里看到的世界是什么样的在裸机开发里最大的坑往往不是代码本身而是地址映射。C906 作为从核它访问的物理地址空间和 A55 是共享的也就是说 A55 能用 DDRC906 也能访问。这在方便数据交换的同时也带来一个责任你不能随便写内存写坏了 A55 那边的数据就是事故。从系统地址布局看典型 T527 方案中 DDR 的高端地址或特定区域会被保留给 RISC-V 核使用具体基地址需要查阅《Allwinner T527 用户手册》的“内存映射”章节。这是动手前的必修课我见过不少人直接默认从 0x40000000 开始写裸机代码结果 A55 侧数据被踩得面目全非排查了一晚上才发现是地址撞车。稳妥的做法是在 DDR 里划一块区域专门留给 C906比如 A55 的 Linux 内核通过 device tree 里的reserved-memory节点把这部分内存预留出来让 Linux 永远不会分配给普通进程。裸机固件放在这块预留内存里加载地址、链接地址、运行地址全部对齐到同一个值。2.3 工具链选型别用错交叉编译器为 C906 编译裸机代码首选是平头哥官方提供的Xuantie-900-gcc工具链它的目标三元组通常是riscv64-unknown-elf-gcc。如果搞不到官方工具链用通用的riscv64-unknown-elf-gcc也可以因为 C906 的指令集是标准的不需要特殊的 vendor 扩展。注意不要用riscv64-linux-gnu-gcc编译裸机程序这套工具链是给跑 Linux 的 user space 用的编译时会隐式链接 Linux 的 C 库生成的可执行文件依赖操作系统的系统调用接口没有操作系统就跑不了。裸机必须用-elf后缀的“裸金属工具链”编译产物直接是固件镜像不依赖任何内核。我实际测试下来平头哥 GCC 10.x 系列在编译 C906 代码时优化效果最好建议用-O2配合-marchrv64imafdc -mabilp64d这两个参数。3. 动手前必须吃透的链接脚本RISC-V 裸机的基石3.1 链接地址错一位整块板子陪你熬夜热词里反复出现 “risc-v link.ld”这是有道理的。RISC-V 裸机开发中链接脚本是最重要也最容易出错的文件。很多人第一回跑裸机程序代码编译零错误一上板就“死机”串口没反应JTAG 也连不上十有八九就是链接地址和实际加载地址不一致。我们得先明确三个地址的关系。**链接地址Link Address**是编译器生成代码时假定的运行地址跳转指令、内存访问、绝对寻址都会按这个地址来算**加载地址Load Address**是固件实际被放到内存里的位置**运行地址Run Address**是 CPU 真正取指执行时的地址。如果这三个值不一致程序一跑就废因为所有绝对跳转都指向了错误的位置。我们的操作策略很明确把三地址统一。在 U-Boot 里把固件加载到预留内存区域的起始地址比如CONFIG_RISCV_FIRMWARE_BASE然后让 C906 从同一地址开始执行。链接脚本里的ORIGIN就写这个地址一分一毫都不能差。3.2 一个能用的 link.ld 应该是这样的我从一个实际验证过的工程里抽出来一个可用的链接脚本骨架结构拆开讲。OUTPUT_ARCH(riscv) ENTRY(_start) MEMORY { /* 这里的 ORIGIN 必须和 U-Boot 里 go 命令跳转的地址一致 */ DDR (rwx) : ORIGIN 0x48000000, LENGTH 4M } SECTIONS { .text : { KEEP(*(.text._start)) *(.text*) *(.rodata*) . ALIGN(16); _etext .; } DDR .data : { _data_start .; *(.sdata*) *(.data*) . ALIGN(16); _data_end .; } DDR .bss : { _bss_start .; *(.sbss*) *(.bss*) *(COMMON) . ALIGN(16); _bss_end .; } DDR . ALIGN(16); _end .; /* 栈顶放在镜像末尾向下增长 */ _stack_top _end 0x1000; }三点经验KEEP(*(.text._start))的写法很关键。把启动汇编的入口函数单独放在代码段最前面确保第一条指令就是复位入口链接器不会乱排。_stack_top不是符号变量是地址常量。它代表栈顶地址本身取它的地址就是取数值。对应到汇编里用la sp, _stack_top初始化栈指针。.bss段必须由启动代码清零。加载固件时 DDR 里的初始值是不可预测的不把未初始化全局变量清成零后面访问全局变量就是毒数据。这里的 ORIGIN 我用 0x48000000 做示例地址真实板子以你查到的 T527 手册预留内存地址为准。拿到原文后第一件事就是核对这个值。3.3 启动汇编与 C 语言的交接有了链接脚本接着是最小启动代码。RISC-V 的裸机启动汇编比 ARM 简单没有那么多特权模式切换核心动作就三个设栈、清 bss、跳 main。.section .text._start .globl _start _start: /* 关闭全局中断避免初始化过程中被打断 */ csrci mstatus, 0x8 /* 设置栈指针链接脚本里定义的栈顶 */ la sp, _stack_top /* 清零 bss 段 */ la t0, _bss_start la t1, _bss_end 1: bgeu t0, t1, 2f sd zero, 0(t0) addi t0, t0, 8 j 1b 2: /* 跳转到 C 主函数 */ call main 1: /* 主函数返回后死循环兜底 */ wfi j 1b这段代码没有半点多余。csrci mstatus, 0x8是把全局中断使能位MIE清零裸机初始化过程中串口都没配好中断进来只有死机一条路。.bss清零用sd一次写 8 字节效率比sb高一个量级。用一个循环兜底是为了防止 main 函数 return 后 PC 不知道跑到哪里去。加了wfi指令还能顺带让核进入低功耗等待比空转j优雅得多。4. 实操环境搭建与第一个裸机固件4.1 开发环境的具体搭建步骤先说环境。我在 Ubuntu 22.04 上实测整个搭建过程十分钟以内前提是网络畅通。安装裸机工具链推荐直接从平头哥官网下载预编译的Xuantie-900-gcc解压后配置环境变量即可wget https://occ.t-head.cn/community/download # 以实际下载页为准 tar -xzf Xuantie-900-gcc-elf-newlib-x86_64.tar.gz export PATH$PWD/Xuantie-900-gcc/bin:$PATH验证工具链是否可用跑一下版本号即可确认riscv64-unknown-elf-gcc -v顺便检查一下工具链支持的架构扩展确定它能生成rv64imafdc指令集的代码riscv64-unknown-elf-gcc -marchrv64imafdc -mabilp64d -print-libgcc-file-name如果这条命令能打印出 libgcc 的路径说明工具链支持该指令集可以编译裸机程序。4.2 一个最小程序的完整工程结构我建议按下面的目录结构组织工程为后面的复杂项目打底xuantie-c906-demo/ ├── start.S ├── link.ld ├── main.c ├── uart.c ├── uart.h └── Makefilemain.c最核心的逻辑是从串口打印一行信息。这里要提个醒C906 访问片上外设用的基地址一定是从 T527 手册里面查来的不同封装、不同复用配置下 UART 的基址和时钟源可能不一样。我写代码时用宏定义把基址提出来方便按板子适配。#define UART_BASE 0x05000000UL // 以 T527 手册实际 UART 基地址为准 static void uart_putc(char c) { volatile unsigned int *dr (unsigned int *)(UART_BASE 0x00); volatile unsigned int *sr (unsigned int *)(UART_BASE 0x14); /* 等待发送 FIFO 不满 */ while (*sr (1U 1)) ; *dr c; } void uart_puts(const char *s) { while (*s) uart_putc(*s); } int main(void) { uart_puts(Hello from XuanTie C906 on T527!\r\n); /* 死循环防止 main 返回 */ while (1) ; return 0; }这个版本没有做中断、没有开 DMA纯粹验证“核活了串口能输出”。我第一次上板时跑的就是这个看到串口打印出字符串的那一刻心里那块石头才落地因为后面的所有复杂功能都是建立在这条输出之上的。4.3 Makefile 与编译参数Makefile 的核心编译参数要盯紧指令集架构和 ABI 不匹配是编译期最常见的翻车点CROSS : riscv64-unknown-elf- CC : $(CROSS)gcc OBJCOPY : $(CROSS)objcopy CFLAGS : -marchrv64imafdc -mabilp64d -O2 -ffreestanding -nostdlib -fno-builtin LDFLAGS : -T link.ld -nostdlib -static all: firmware.bin start.o: start.S $(CC) $(CFLAGS) -c start.S -o $ main.o: main.c $(CC) $(CFLAGS) -c main.c -o $ uart.o: uart.c $(CC) $(CFLAGS) -c uart.c -o $ firmware.elf: start.o main.o uart.o $(CC) $(LDFLAGS) $^ -o $ firmware.bin: firmware.elf $(OBJCOPY) -O binary firmware.elf $ clean: rm -f *.o *.elf *.bin几个参数的用途说明一下-ffreestanding告诉编译器这是一个独立环境程序不依赖宿主操作系统printf 这类标准库函数不会被依赖。-nostdlib不链接标准启动代码和标准 C 库链接入口完全由我们自己控制。-fno-builtin防止编译器自作主张用内建函数替换你的代码。-static静态链接生成独立的、不依赖动态库的固件。编译完检查一下生成的反汇编确认入口第一句就是_startriscv64-unknown-elf-objdump -d firmware.elf | head -30如果第一行不是_start:说明链接脚本的ENTRY没生效或者KEEP没写对返回去检查。5. 上板加载从 U-Boot 到“Hello World”5.1 U-Boot 下的加载命令序列编译出firmware.bin之后把文件放到 tftp 服务器或者 SD 卡里进入 U-Boot 命令行。以 SD 卡 fatload 的方式举例U-Boot# fatload mmc 0:1 0x48000000 firmware.bin U-Boot# go 0x48000000go命令告诉 CPU 直接跳转到指定地址执行不传参、不检查格式非常适合裸机调试。RAM 里的地址要和 link.ld 的ORIGIN完全一致差一位都是跑飞。注意一个细节T527 的 U-Boot 一般默认只启动了 A55C906 此时还处于复位状态。go命令本身不会自动释放 C906 的复位你需要先通过特定寄存器序列把它的复位释放掉。不同板卡 BSP 对这个的处理方式不尽相同有些板子在 Linux 内核启动时会顺便初始化好两颗核之间的关系有些则需要手动操作。我建议先查一下你的板卡 BSP 的 U-Boot 源码搜索c906或者riscv相关代码确认有没有现成的释放流程。这一块如果文档缺失最直接的办法是用 JTAG 连接 C906直接在调试器里控制复位并设置 PC。这也是为什么我强烈建议有调试器就插上调试器没有调试器裸调从核真的是地狱难度。5.2 运行结果与验证手段一切顺利的话串口上会打印出Hello from XuanTie C906 on T527!如果没看到输出按下面顺序排查串口硬件占用C906 打印用的 UART 可能和 A55 的 console 是同一个外设两边同时用会产生竞争。调试阶段最好的办法是让 A55 先别拿这个串口打印或者用另一个空闲 UART。时钟门控很多 SoC 的外设默认在时钟门控关闭状态C906 代码访问 UART 寄存器前需要先打开对应时钟。这步要在启动汇编里配或者由 A55 侧提前配好。加载地址错误go的地址和 link.ld 的 ORIGIN 不一致程序已经在内存里跑飞了。用 JTAG 读 PC 就能立刻确认。5.3 从“能打印”到“能干活”共享内存的预热串口打印说明核已经跑起来了但真正的产品功能离不开数据交换。这里先留一个最简单的通道设计思路在两颗核之间划一块共享内存C906 往里写数据A55 侧的 Linux 应用通过mmap映射同一块物理内存来读取。共享内存区域建议选在 Linux 的reserved-memory里定义。在 device tree 里预留之后A55 侧任何程序都无法通过正常内存管理接触到这块区域就避免了双方踩踏。C906 侧只需要把共享区地址定义为一个宏启动后在固定的偏移位置写一个魔数A55 侧轮询这个魔数是否变化来判断 C906 是否活着。这是 Part 1 能做的最大闭环满足了“有计算、有通信”的验证目标。6. 上板运行时的经典翻车现场与排查手册6.1 我踩过的几个坑列成速查表以下问题都是我实际调试过程中遇到过的按出现频率排序现象大概率原因排查方法串口无任何输出C906 未释放复位程序根本没跑JTAG 连上读 PC停在 0 则确认复位未释放串口输出乱码UART 波特率配置错误检查 UART 时钟源分频系数用示波器量 TX 引脚电平宽度串口输出一次后死机未等发送 FIFO 空就返回或者栈溢出检查 uart_putc 的等待条件确认栈指针初始化无误程序好像跑起来了但行为诡异bss 段没清零全局变量初始值不确定在 main 入口断点查全局变量值清零逻辑看汇编一执行 go 命令整个系统挂链接地址写到了 Linux 正在用的内存检查预留内存区域是否在 device tree 里正确配置反汇编第一句不是 _startlink.ld 里 KEEP 没生效确认.text._start段名和汇编里.section完全一致6.2 两条保命级的调试建议如果你只是想把代码跑起来上面的内容已经够了。但如果你想继续做下去下面两条建议值得尽早知道。第一条把 JTAG 调试环境配好再动手。裸机开发没有调试器等于蒙眼狂奔。用 JTAG 连上 C906 之后你可以直接控制核心的复位、设置 PC 值、单步执行、读写寄存器任何“程序到底跑没跑”的争论都能一句话解决。T527 的调试接口在 BSP 文档里有明确说明配套的 OpenOCD 配置通常也能在社区找到。第二条先在 QEMU 上验证一遍再上板。QEMU 支持 RISC-V virt 平台你几乎可以不改任何代码就把这个裸机程序跑起来串口也能看到输出。上板前先在模拟器上确认链接脚本正确、内存布局合理、启动流程通畅能过滤掉一半以上的低级错误。我就是靠这个习惯把上板后的调试时间从三小时压缩到了二十分钟。第三条工具链版本要锁定。不用的 GCC 版本对启动代码的处理有细微差别比如新版本可能要求更完整的功能属性声明或者对.section的解析更严格。同样一段 start.S在 GCC 10 下编译正常在 GCC 12 下可能直接报错。建议把官方工具链的版本固定住不要轻易升级裸机工程的工具链“能用就不动”。Part 1 到这里核心流程已经走通了从理解 T527 的异构架构到搭好工具链再到写链接脚本、启动代码、UART 输出最后在 U-Boot 下把固件加载到预留内存并跳转执行。个人体会最深的一点是这套流程里真正花时间的不是写代码而是搞清楚地址映射和复位控制链路。很多开发者在从核开发上栽跟头不是代码能力问题是对 SoC 级的内存规划缺乏整体意识。C906 这颗核的潜力很大裸机点亮只是第一步后续往 RTOS、往 remoteproc、往核间通信协议方向走都非常值得投入。下一篇可以聊聊怎么把 FreeRTOS 迁移到 C906 上以及如何用共享内存和 Mailbox 建立一套稳定的核间通信通道这些都是真正产品化的必经之路。
返回列表