ARTICLE DETAIL

资讯详情

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

Allwinner SoC异构RISC-V实战:remoteproc与核间通信详解

Allwinner SoC异构RISC-V实战:remoteproc与核间通信详解 1. 异构RISC-V在Allwinner SoC上的整体设计思路1.1 为什么要在Allwinner SoC上跑异构RISC-VAllwinner这几年的SoC产品线里越来越多型号开始集成一颗甚至多颗RISC-V核。比如大家熟悉的D1、T113、A523这些芯片主CPU是ARM Cortex-A系列但片内还塞了一个叫C906或E907的RISC-V小核。这个核通常不跑Linux而是跑RTOS或者裸机固件专门负责一些实时性要求高、功耗敏感的杂活——音频DSP处理、传感器数据采集、电源管理、摄像头预处理等等。问题来了ARM核跑LinuxRISC-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: mailbox3003000 { 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: remoteproc3004000 { 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: rproc48000000 { 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核有cacheRISC-V核也有cache如果共享内存区域没有正确配置cache属性会出现“ARM核写了数据RISC-V核读到的还是旧值”的情况。解决办法有两种一是把共享内存区域配置成非缓存的二是使用硬件cache一致性协议如果SoC支持的话。非缓存配置在设备树里这样写rproc_ddr: rproc48000000 { 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字段是不是1num字段是不是和实际资源数量一致。如果是“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核启动时先交换版本号如果不匹配就报错。这样可以避免固件和驱动版本不一致导致的诡异问题。我在实际项目中遇到过好几次因为版本不匹配导致通信失败的情况加了版本号检查之后问题定位快了很多。
返回列表