☰
Allwinner SoC异构RISC-V实战:remoteproc与核间通信详解
2026/10/7 20:00:23 网站建设 项目流程

1. 异构RISC-V在Allwinner SoC上的整体设计思路

1.1 为什么要在Allwinner SoC上跑异构RISC-V

Allwinner这几年的SoC产品线里,越来越多型号开始集成一颗甚至多颗RISC-V核。比如大家熟悉的D1、T113、A523这些芯片,主CPU是ARM Cortex-A系列,但片内还塞了一个叫C906或E907的RISC-V小核。这个核通常不跑Linux,而是跑RTOS或者裸机固件,专门负责一些实时性要求高、功耗敏感的杂活——音频DSP处理、传感器数据采集、电源管理、摄像头预处理等等。

问题来了:ARM核跑Linux,RISC-V核跑RTOS,两者之间怎么通信?怎么让Linux这边把固件加载到RISC-V核上?怎么在运行时互相传数据?这就是异构多核要解决的核心问题。Allwinner的方案是走remoteproc框架,配合mailbox和共享内存,把RISC-V核当成一个“远程处理器”来管理。

我当初接触这个项目,是因为手头一块T113-S3的开发板,想用它的RISC-V核做音频前处理,ARM核跑Linux做上层应用。翻了一遍全志的SDK和主线内核的文档,发现这块内容散落在各个地方,踩了不少坑。这篇文章就把整个流程拆开讲清楚,从设备树配置到固件加载,再到通信调试,尽量让后来的人少走弯路。

1.2 异构架构的核心组件拆解

整个异构方案里,有几个关键组件必须搞清楚,不然后面配设备树的时候会一头雾水。

remoteproc子系统是Linux内核提供的一套框架,专门用来管理“非主CPU”的处理器。它的核心职责包括:加载固件到远程核、启动/停止远程核、监控远程核状态、处理崩溃恢复。对于RISC-V核来说,remoteproc就是Linux这边的“遥控器”。

mailbox框架负责核间中断。ARM核和RISC-V核之间需要一种“敲门”机制,告诉对方“我有数据给你了”或者“我处理完了”。Allwinner SoC里通常用MSGBOX硬件模块来实现,每个核有独立的收发通道,通过寄存器写入触发对方中断。

共享内存是数据实际存放的地方。两个核约定好一块物理地址区域,ARM核写、RISC-V核读,或者反过来。共享内存本身不产生中断,需要配合mailbox来通知对方“数据准备好了”。

资源表是固件里的一个数据结构,告诉Linux内核这个远程核需要哪些资源:需要多少内存、需要哪个中断号、需要哪些设备。remoteproc在加载固件时会解析这个表,把资源分配好。

这四个组件配合起来,才能让两个不同架构的核在同一个芯片里“和平共处”。下面这张表总结了它们的分工:

组件职责硬件依赖软件位置
remoteproc固件加载、启停控制、状态监控无特定硬件要求Linux内核
mailbox核间中断通知MSGBOX硬件模块Linux内核+RTOS
共享内存数据缓冲区DDR/SRAM物理内存双方约定
资源表资源需求描述无固件内部

1.3 方案选型的几个关键考量

为什么Allwinner选remoteproc而不是自己写一套私有框架?我分析下来有几个原因。

第一,主线内核支持。remoteproc从Linux 3.4就进入主线了,API稳定,文档齐全。全志如果自己搞一套,维护成本高,社区也不买账。第二,标准化接口。remoteproc提供了统一的sysfs接口,用户空间可以通过/sys/class/remoteproc/目录下的文件来启动、停止远程核,不需要写额外的驱动。第三,生态兼容。很多RTOS厂商(比如RT-Thread、FreeRTOS)已经提供了remoteproc的资源表模板,固件开发这边可以直接复用。

不过remoteproc也不是没有缺点。它的固件加载流程比较固定,要求固件必须是ELF格式,而且资源表必须放在特定的段里。对于资源受限的RISC-V小核来说,ELF格式的固件体积会比纯二进制大一些。另外,remoteproc的崩溃恢复机制在异构场景下有时候会显得笨重,远程核挂了之后重新加载固件需要的时间比较长。

但综合来看,对于Allwinner这种“ARM主核+ RISC-V从核”的架构,remoteproc仍然是最稳妥的选择。自己造轮子的话,光是核间同步和内存管理就够喝一壶了。

2. 设备树配置与硬件资源分配

2.1 设备树节点的完整写法

设备树是异构方案落地的第一步,配错了后面全白搭。Allwinner的RISC-V核在设备树里通常挂在msgbox和remoteproc两个节点下面。以T113-S3为例,核心配置大概长这样:

msgbox: mailbox@3003000 { compatible = "allwinner,sun8i-msgbox"; reg = <0x03003000 0x1000>; interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>; clocks = <&ccu CLK_BUS_MSGBOX>; resets = <&ccu RST_BUS_MSGBOX>; #mbox-cells = <1>; }; rproc: remoteproc@3004000 { compatible = "allwinner,sun8i-riscv-remoteproc"; reg = <0x03004000 0x1000>; interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>; clocks = <&ccu CLK_BUS_RISCV>; resets = <&ccu RST_BUS_RISCV>; memory-region = <&rproc_ddr>; mboxes = <&msgbox 0>; mbox-names = "arm-kick"; firmware-name = "riscv-firmware.elf"; status = "okay"; }; reserved-memory { #address-cells = <1>; #size-cells = <1>; ranges; rproc_ddr: rproc@48000000 { reg = <0x48000000 0x800000>; no-map; }; };

这里有几个点需要特别注意。memory-region指向的保留内存必须是no-map的,意思是Linux内核不能把这部分内存映射到自己的地址空间里,否则两边同时访问会出问题。mboxes和mbox-names指定了mailbox通道,arm-kick这个名字是驱动里约定的,不能随便改。

firmware-name指定了固件文件名,remoteproc驱动会从/lib/firmware/目录下去找这个文件。如果你把固件放在别的地方,需要修改这个属性或者用符号链接。

2.2 内存布局与地址映射的坑

内存布局是异构方案里最容易出问题的地方。ARM核和RISC-V核看到的物理地址是一样的,但虚拟地址映射可能不同。共享内存的物理地址必须在两个核的地址空间里都能访问到。

Allwinner的RISC-V核通常只能访问DDR的低地址区域,具体范围取决于芯片型号。T113-S3的RISC-V核可以访问0x40000000到0x4FFFFFFF这段DDR。所以保留内存必须落在这个范围内,否则RISC-V核根本读不到。

另外,cache一致性是个大坑。ARM核有cache,RISC-V核也有cache,如果共享内存区域没有正确配置cache属性,会出现“ARM核写了数据,RISC-V核读到的还是旧值”的情况。解决办法有两种:一是把共享内存区域配置成非缓存的,二是使用硬件cache一致性协议(如果SoC支持的话)。

非缓存配置在设备树里这样写:

rproc_ddr: rproc@48000000 { reg = <0x48000000 0x800000>; no-map; /* 非缓存属性 */ dma-coherent; };

dma-coherent告诉内核这块内存是硬件一致的,不需要软件维护cache。但要注意,不是所有Allwinner SoC都支持这个属性,具体要看芯片手册。

2.3 中断与mailbox通道分配

mailbox通道的分配需要两边约定好。Allwinner的MSGBOX模块通常有多个通道,每个通道对应一个方向。比如通道0是ARM发给RISC-V的,通道1是RISC-V发给ARM的。

在设备树里,mboxes = <&msgbox 0>表示使用通道0。RISC-V固件那边也要配置对应的通道。如果两边通道号对不上,中断就触发不了。

中断号的分配也要注意。interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>里的1是SPI中断号,这个值在不同SoC上可能不同。配错了会导致remoteproc驱动加载失败,内核日志里会报“failed to request interrupt”之类的错误。

提示:设备树修改完之后,一定要用dtc工具反编译检查一遍,确认没有语法错误。我见过好几次因为少了一个分号导致整个设备树编译失败的情况。

3. 固件加载与remoteproc驱动实操

3.1 RISC-V固件的编译与打包

RISC-V核的固件通常用RT-Thread或者裸机代码编写,编译工具链是riscv64-unknown-elf-gcc或者riscv64-linux-gnu-gcc。编译出来的ELF文件需要包含资源表,否则remoteproc加载时会报错。

资源表的定义在remoteproc.h里,一个典型的资源表长这样:

struct resource_table { u32 ver; u32 num; u32 reserved[2]; u32 offset[0]; }; struct fw_rsc_carveout { u32 type; u32 da; u32 pa; u32 len; u32 flags; u32 reserved; u8 name[32]; };

type字段指定资源类型,RSC_CARVEOUT表示内存区域,RSC_DEVMEM表示设备内存,RSC_TRACE表示调试缓冲区。da是设备地址(RISC-V核看到的地址),pa是物理地址(ARM核看到的地址),len是长度。

编译的时候,资源表需要放在一个独立的段里,通常叫.resource_table。链接脚本里要加上:

.resource_table : { KEEP(*(.resource_table)) }

这样remoteproc驱动才能找到资源表。

3.2 固件加载的完整流程

固件加载的流程分几步走。第一步,把编译好的ELF文件放到/lib/firmware/目录下,文件名要和设备树里的firmware-name一致。第二步,通过sysfs接口启动远程核:

echo start > /sys/class/remoteproc/remoteproc0/state

执行这条命令之后,内核会做以下几件事:从/lib/firmware/读取ELF文件、解析资源表、分配内存、把固件的各个段拷贝到对应地址、释放RISC-V核的复位、触发启动。

如果一切正常,state文件会变成running。如果出错,dmesg里会有详细的错误信息。常见的错误包括:固件文件找不到、资源表格式不对、内存分配失败、中断请求失败。

停止远程核用:

echo stop > /sys/class/remoteproc/remoteproc0/state

停止之后,RISC-V核会被复位,占用的内存会被释放。

3.3 启动失败的排查思路

启动失败是最常见的问题,我整理了一个排查流程。

先看dmesg里的错误信息。如果是“firmware not found”,检查文件路径和文件名。如果是“invalid resource table”,检查资源表的ver字段是不是1,num字段是不是和实际资源数量一致。如果是“failed to allocate memory”,检查保留内存的大小是否足够,地址是否在RISC-V核可访问范围内。

还有一个隐蔽的问题:固件的入口地址。ELF文件头里指定了入口地址,remoteproc会跳转到这个地址执行。如果入口地址配错了,RISC-V核会跑飞,表现为“启动后没有任何输出”。这时候需要用JTAG调试器连上RISC-V核,看看PC指针停在哪里。

注意:调试异构问题的时候,串口输出是最重要的信息来源。建议在RISC-V固件里加一个早期的串口初始化代码,把启动日志打出来。Allwinner的RISC-V核通常有独立的调试串口,具体引脚要看原理图。

4. 核间通信与共享内存实战

4.1 mailbox中断的收发机制

mailbox中断的收发机制其实很简单:发送方往MSGBOX的某个寄存器写一个值,硬件就会触发接收方的中断。接收方在中断处理函数里读取寄存器,获取发送方传来的值。

Linux这边的发送代码大概是这样:

struct mbox_chan *chan; struct mbox_client cl; cl.dev = dev; cl.tx_block = true; cl.tx_tout = 1000; cl.knows_txdone = false; chan = mbox_request_channel(&cl, 0); mbox_send_message(chan, &msg);

mbox_send_message会把msg的指针传给底层驱动,底层驱动把值写到MSGBOX寄存器里。接收方在中断里调用mbox_chan_received_data回调,处理收到的数据。

RISC-V固件那边的代码类似,只是没有Linux的mbox框架,需要直接操作寄存器。Allwinner的MSGBOX寄存器布局在芯片手册里有详细说明,通常是MSGBOX_IRQ_PEND寄存器记录哪个通道有中断,MSGBOX_DATA寄存器存放数据。

4.2 共享内存的数据结构设计

共享内存的数据结构设计要考虑几个问题:数据对齐、读写同步、缓冲区管理。

最简单的方案是环形缓冲区。定义一个结构体:

struct shared_ring { volatile u32 head; volatile u32 tail; u8 data[4096]; };

head是写指针,tail是读指针。发送方写数据到data[head],然后更新head,再通过mailbox通知接收方。接收方从data[tail]读数据,更新tail。

这里的关键是内存屏障。在更新head之前,必须确保数据已经写入内存。在ARM架构上,需要加dmb指令;在RISC-V架构上,需要加fence指令。不加屏障的话,编译器或者CPU可能会重排指令,导致接收方读到旧数据。

/* 发送方 */ memcpy(&ring->data[ring->head], buf, len); dmb(); /* 确保数据写入完成 */ ring->head = (ring->head + len) % sizeof(ring->data); mbox_send_message(chan, &msg);

4.3 通信协议的设计与调试

通信协议的设计要简单可靠。我一般用“命令+长度+数据”的格式:

字段长度说明
cmd1字节命令码
len2字节数据长度
data变长实际数据
crc1字节校验和

命令码用来区分不同的操作,比如0x01是音频数据,0x02是控制命令,0x03是状态查询。长度字段告诉接收方要读多少数据。CRC校验用来检测传输错误。

调试的时候,可以在共享内存里加一个调试区域,双方都可以往里面写日志。Linux这边用dev_dbg输出,RISC-V那边直接往内存里写字符串。出问题的时候,用devmem工具读取共享内存的内容,就能看到双方的交互记录。

提示:共享内存的地址和大小一定要在两边保持一致。我建议在头文件里定义宏,两边都引用同一个头文件,避免手写地址出错。

5. 常见问题与排查技巧实录

5.1 启动阶段的高频问题

启动阶段最常见的问题是固件加载失败。除了前面提到的文件路径和资源表问题,还有一个容易被忽略的点:固件的段对齐。remoteproc要求固件的每个段都按页对齐(通常是4KB),如果段没有对齐,加载时会报“misaligned segment”错误。

解决办法是在链接脚本里加上对齐指令:

. = ALIGN(4096); .text : { *(.text) }

另一个高频问题是时钟和复位。RISC-V核需要独立的时钟和复位信号,如果设备树里没有正确配置clocks和resets属性,核根本起不来。检查方法是看/sys/kernel/debug/clk/clk_summary里有没有RISC-V相关的时钟,以及/sys/kernel/debug/reset里复位状态是否正常。

5.2 运行时的通信故障

运行时的通信故障通常表现为“发送方发了数据,接收方没反应”。排查思路如下:

先确认mailbox中断有没有触发。在Linux这边,可以看/proc/interrupts里MSGBOX中断的计数有没有增加。如果没有增加,说明硬件层面就没有触发中断,检查MSGBOX寄存器的配置。

如果中断触发了但数据不对,检查共享内存的cache属性。用devmem读取共享内存的物理地址,看看数据是不是发送方写入的值。如果读到的是旧值,说明cache没有同步,需要加屏障指令或者把内存配置成非缓存。

还有一种情况是中断风暴。如果发送方发得太快,接收方来不及处理,中断会不断触发,导致CPU占用率飙升。解决办法是在接收方加一个限流机制,比如每处理完一批数据再重新使能中断。

5.3 崩溃恢复与稳定性优化

RISC-V核跑飞是难免的,关键是要能快速恢复。remoteproc提供了崩溃恢复机制:当远程核崩溃时,驱动会收到通知,然后自动重新加载固件。

要启用这个机制,需要在设备树里加上watchdog节点,或者在固件里定期喂狗。Allwinner的RISC-V核通常有一个WDT模块,配置好之后,如果固件超过一定时间没有喂狗,WDT会触发复位,remoteproc检测到复位后重新加载固件。

稳定性优化方面,我总结了几个经验:第一,共享内存加锁。如果多个线程同时访问共享内存,必须加自旋锁或者互斥锁。第二,消息队列深度限制。环形缓冲区满了之后,发送方要么等待,要么丢弃,不能无限写入。第三,心跳机制。双方定期通过mailbox发送心跳包,超过一定时间没收到就认为对方挂了,触发恢复流程。

问题现象可能原因排查方法解决方案
固件加载失败文件路径错误检查/lib/firmware/修正文件名
启动后无输出入口地址错误JTAG查看PC指针修正链接脚本
中断不触发MSGBOX配置错误查看/proc/interrupts修正设备树
数据不一致cache未同步devmem读取内存加屏障或非缓存
中断风暴发送过快查看CPU占用率加限流机制
核跑飞固件bug查看WDT日志加喂狗和恢复

5.4 性能调优的几点心得

性能调优方面,有几个参数可以调整。共享内存的大小直接影响吞吐量,但太大会浪费DDR。我一般根据实际数据量来定,音频场景2MB足够,视频场景可能需要8MB以上。

mailbox的触发频率也要控制。每次mailbox中断都有开销,如果数据量很小但发送很频繁,中断开销会超过数据处理本身。解决办法是批量发送:攒够一定数量的数据再触发一次中断。

RISC-V核的主频也可以调整。Allwinner的RISC-V核通常支持动态调频,在负载低的时候降频省电,负载高的时候升频提性能。调频策略可以在RTOS里实现,通过共享内存和ARM核协商。

最后再分享一个小技巧:在共享内存的头部放一个版本号字段。ARM核和RISC-V核启动时先交换版本号,如果不匹配就报错。这样可以避免固件和驱动版本不一致导致的诡异问题。我在实际项目中遇到过好几次因为版本不匹配导致通信失败的情况,加了版本号检查之后,问题定位快了很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询