ARTICLE DETAIL

资讯详情

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

Allwinner异构RISC-V双核Bring-up实战:从固件到核间通信

Allwinner异构RISC-V双核Bring-up实战:从固件到核间通信 1. 先接上 Part 1 的线这一篇要解决什么问题1.1 上次把大核跑通这次轮到小核Allwinner 异构 RISC-V bring-up 这个系列写到 Part 2说明 Part 1 那点铺垫已经不够用了。Part 1 我主要讲了怎么确认芯片型号、怎么理解这颗 SoC 上 ARM 主核的启动链路BROM → SPL → U-Boot以及怎么在串口上看到 U-Boot 正常打印。做完这些芯片的“大路”算是通了大核能跑、DDR 能访问、基础外设能操作。但异构芯片的价值恰恰不在大核上。以 V853 这类方案为例除了 Cortex-A7 之外芯片内部还挂着一颗 T-Head E907这是个 RISC-V 小核。主核上跑 Linux小核被设计用来干那些不适合 Linux 干的活低功耗待机管理、音频编解码辅助、传感器融合预处理甚至安全启动的协助。Part 2 要做的就是让这颗小核从“datasheet 上存在”变成“实际能跑、能通信”。所以这篇不是什么高深的理论文章就是一份怎么把 RISC-V 副核从零拉起来的实战记录。我会把工具链搭建、链接脚本、启动汇编、固件加载、核间通信、问题排查这几个环节全部过一遍。整个过程不依赖厂商闭源 SDK 的魔法而是基于开源工具链和寄存器操作你能在自己的板子上一步步复现。1.2 这一篇的三个具体目标在动手之前我先把目标拆清楚避免越写越散。第一把 RISC-V 副核的固件工程搭起来包括交叉工具链的选择、link.ld 链接脚本的编写、启动汇编的写法这是所有调试验证的基础。第二用 ARM 主核把固件搬运到副核能访问的内存配置时钟和复位控制寄存器释放复位让副核真正跑起来。第三建立一套主核和副核之间的共享内存通信机制先从最简单的“我活着”状态开始再到命令收发。这篇文章适合两类人。一类是手里正好有 Allwinner 异构方案的板子比如 V853、T113-S3、R329 这类 A7 RISC-V 组合的可以照着流程直接复现。另一类是没有对应板子但想理解异构双核 bring-up 通用套路的朋友。工具链怎么选、链接脚本为什么容易翻车、共享内存协议怎么写、缓存一致性怎么处理这些经验放到任何 ARM RISC-V 双核组合上都成立不局限于 Allwinner。2. 先搞清楚对手Allwinner 异构方案里的 RISC-V 到底是个什么角色2.1 厂商为什么要往 SoC 里塞一个 RISC-V 小核很多人第一次接触“异构 SoC”这个概念是从手机芯片的“大核小核”开始的。但 Allwinner 这类做嵌入式/边缘 AI 方案的厂商塞 RISC-V 小核的思路不太一样。手机上的大小核是同架构、不同性能档位而 Allwinner 是直接把两种指令集架构放进一颗芯片ARM 主核负责跑 Linux 和复杂业务RISC-V 小核负责跑裸机固件或 RTOS。这么设计的理由很实际。低功耗待机场景下整个 Linux 系统没有必要保持运行主核可以进入深睡眠由一颗功耗极低的 RISC-V 核来维持基本功能比如检测唤醒事件、维持 RTC 计时、轮询传感器。还有就是实时性要求高的场景Linux 的调度延迟不可控把中断密集、对抖动敏感的任务放到小核上更稳妥。另外还有一个隐形理由RISC-V 核配合独立 TCM 内存天然适合做安全启动或安全服务的载体和主核在物理上隔离出了问题也不会把整个系统拖垮。我个人的体会是异构 bring-up 最容易犯的错就是把小核当成“另一个 Cortex-M”来对待。实际上它在总线矩阵里的位置、复位时序、时钟来源、和主核共享的外设每一样都值得先翻手册确认再动手写代码。尤其是总线拓扑小核能不能访问 DDR、能不能访问某个外设寄存器往往决定了你的固件设计。2.2 几款典型芯片的异构结构对比我在实际项目里接触过的 Allwinner 异构方案大致可以分成三类。第一类是 R329 这种 ARM A53 RISC-V DSP 的组合RISC-V 侧偏重音频/AI 加速。第二类是 V853、T113-S3 这种 Cortex-A7 E907 的组合这也是本文主要讨论的对象。第三类是 H616/H618 这类主核是 Cortex-A53内部还有一个 RISC-V 安全协处理器早期主要跑 BROM 固件。这三种方案的 bring-up 路径不完全一样但共性很强先确认 RISC-V 核在复位后的默认状态再确认它的程序入口是怎么决定的最后确认它和主核共享哪些内存和寄存器。下表整理了几个关键差异点芯片ARM 侧RISC-V 侧主要定位RISC-V 启动方式D1/D1s无全 RISC-VC906主核常规启动流程V853Cortex-A7E907辅助/低功耗ARM 释放复位T113-S3双 Cortex-A7E907辅助/实时任务ARM 释放复位R329双 Cortex-A53RISC-V DSP音频/AIARM/SPL 加载H616/H618四 Cortex-A53E906 安全核安全启动辅助BROM/固件接管项目标题里说的 heterogeneous RISC-V on Allwinner SoCs指的就是后面几行的场景一颗 ARM 主核和一颗 RISC-V 副核共存需要你手动把副核“拉起来”。这和 D1 那种整颗芯片都是 RISC-V 的情况有本质区别——D1 只要管好一条启动链而异构方案里有两条启动链它们之间存在依赖关系。2.3 上电瞬间RISC-V 核看到的是什么样的世界搞清楚复位后的初始状态是 bring-up 的第一个关键点。对于 V853/T113 这类芯片E907 在芯片上电后会处于复位状态程序计数器停在复位向量地址但能不能取指、能访问哪些内存取决于 ARM 侧有没有把时钟和总线权限配好。这一点和独立 MCU 完全不一样——MCU 上电自己就跑异构 SoC 的副核上电后是“锁住”的必须由主核解锁。内存视角也很重要。E907 通常有自己的 TCMTightly Coupled Memory也有对片内 SRAM 的访问权限但对 DDR 的访问往往要等 ARM 侧完成 DRAM 初始化之后才可用。第一次 bring-up 我强烈建议只跑 TCM/SRAM不碰 DDR这样能隔离掉一堆内存初始化、缓存一致性相关的问题。还有一个容易忽略的点RISC-V 小核的异常向量和中断控制器。E907 这类 T-Head 内核中断模型和 SiFive 的标准 CLINT/PLIC 略有差异外部中断可能直接映射到特定的中断号。这意味着你不能照抄一份其他 RISC-V 芯片的 trap 代码就指望能用得根据目标核的编程手册确认 mtvec 的设置方式和外部中断的路由。这些细节到了第五阶段调试时都会变成“坑”提前了解能省很多时间。3. 固件工程三件套工具链、链接脚本、启动汇编3.1 工具链选择与 Makefile 搭建RISC-V 小核的固件是裸机程序没有操作系统帮忙所以工具链要选“裸机”风格的。我用的是 riscv64-unknown-elf 工具链配合 multilib通过 -march 参数指定为 rv32imac、-mabi 指定为 ilp32。E907 是 RV32 内核指令集通常包含整数乘法除法、原子操作和压缩指令但不一定带浮点单元所以 -marchrv32imac 是最稳的选择。如果你不想折腾 multilib也可以直接用 riscv32-unknown-elf 的专用工具链或者用 Buildroot 编译一套 riscv32-nommu 的交叉工具链。工具链版本之间差异不大关键是要保证 -march 和 -mabi 和目标核匹配否则编译出来的指令集不对芯片一跑就进 illegal instruction。下面是我常用的 Makefile 骨架麻雀虽小五脏俱全CROSS : riscv64-unknown-elf- CC : $(CROSS)gcc OBJCOPY : $(CROSS)objcopy OBJDUMP : $(CROSS)objdump CFLAGS : -marchrv32imac -mabiilp32 -nostdlib -fno-common -ffreestanding -O2 -Wall LDFLAGS : -Tlink.ld -nostdlib OBJS : startup.o main.o mailbox.o firmware.elf: $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $ $(OBJS) firmware.bin: firmware.elf $(OBJCOPY) -O binary $ $ firmware.lst: firmware.elf $(OBJDUMP) -D $ $ %.o: %.S $(CC) $(CFLAGS) -c -o $ $ %.o: %.c $(CC) $(CFLAGS) -c -o $ $ clean: rm -f *.o *.elf *.bin *.lst编译完后我会习惯性地用riscv64-unknown-elf-objdump -D firmware.elf看一遍反汇编重点确认第一条指令是不是从预期地址开始、跳转目标是否正确、有没有意外生成浮点指令。这习惯帮我抓出过好几次 march/mabi 配置错误算是个低成本高回报的检查手段。3.2 link.ld给一颗裸核画好“内存地图”网上搜“riscv link.ld”能翻到一大堆示例但十有八九拿过来不能直接用。原因很简单RISC-V 裸机固件的链接脚本必须回答三个问题——代码放哪、数据放哪、栈放哪而这三个问题的答案完全取决于目标芯片的内存布局。Allwinner 给 E907 分配的 TCM 基地址、大小、是否支持读写都写在芯片手册的 memory map 章节里不同芯片可能差很远。我的链接脚本一般这样写OUTPUT_ARCH(riscv) ENTRY(_start) MEMORY { TCM (rwx) : ORIGIN 0x200000, LENGTH 128K SHMEM (rw) : ORIGIN 0x400000, LENGTH 64K } SECTIONS { .text : { *(.text.init) *(.text*) } TCM .rodata : { *(.rodata*) } TCM .data : { _data_start .; *(.data*) _data_end .; } TCM .bss (NOLOAD) : { _bss_start .; *(.bss*) *(COMMON) _bss_end .; } TCM .stack (NOLOAD) : { . ALIGN(16); . . 8K; _stack_top .; } TCM _end .; }这里面的 ORIGIN 地址只是示意实际要以你的芯片手册为准。几个容易翻车的细节说一下。一是_start必须最先被链接到 TCM 的基地址因为副核释放复位后就是从那个固定地址开始取指的所以我用*(.text.init)保证启动汇编排在最前面。二是 BSS 段要标记NOLOAD并且要在启动代码里手动清零不然全局变量的初始值全是垃圾。三是栈要单独划一段并且要按 16 字节对齐RISC-V 的 ABI 对栈对齐有要求没对齐跑到函数调用就崩。3.3 启动汇编第一行指令之前的事启动汇编是整个固件里最不能出错的部分因为它不依赖任何运行时环境全靠手动把该设的寄存器设好。我的 startup.S 大概长这样.section .text.init .globl _start _start: /* 关闭所有中断 */ csrw mie, zero csrw mstatus, zero /* 建立栈指针 */ la sp, _stack_top /* 设置异常向量表 */ la t0, trap_entry csrw mtvec, t0 /* 刷新指令缓存防止加载器写入的固件是旧数据 */ fence.i /* 清零 BSS 段 */ la a0, _bss_start la a1, _bss_end 1: bgeu a0, a1, 2f sw zero, 0(a0) addi a0, a0, 4 j 1b 2: /* 关闭 U/I/S 模式相关的中断委托这里只用 M 模式 */ csrw medeleg, zero csrw mideleg, zero /* 跳转到 main */ call main 1: j 1b几个要点解释一下。fence.i这行很多人会漏掉固件是由 ARM 核搬运到 TCM 的如果不做指令缓存同步RISC-V 核可能取到旧的、缓存过的指令表现出来就是寄存器值看起来对但程序行为完全不可控。这个我在第五部分会再展开。medeleg和mideleg清零也很关键。如果你不打算在固件里支持 U 模式最好把异常和中断全部留在 M 模式处理避免出现“中断来了但不知道去哪”的尴尬。当然如果你的方案需要跑类 RTOS 用户态那就得另说了。3.4 编译阶段最常见的三个错误第一次搭这个工程的人基本都会在我标出的三个位置踩坑。第一个是编译参数和内核不匹配最常见的是用-marchrv64imac -mabilp64去编一个 RV32 核心的固件编译期可能不报错但一上板就 illegal instruction。第二个是漏了-nostdlib导致链接器尝试链接主机端库函数报一堆乱七八糟的 undefined reference其实你只需要一个裸机程序。第三个是链接脚本里 TCM 地址写错固件被链接到错误基地址副核释放复位后从真正的 TCM 基地址取指拿到的字节全是乱的。还有一个值得单独说的不要开-O0裸奔。固件跑在 TCM 里容量本来就紧张-O0会生成大量加载/保存指令既浪费空间又拖慢执行。-O2在裸机环境里很稳配合-fno-common避免公共变量段落到意想不到的位置。如果你担心优化引入 bug可以先在 QEMU 的 riscv32 virt 机器上跑一遍逻辑测试再放到真实芯片上。4. 把固件灌进去加载、释放复位与核间通信4.1 时钟与复位控制解锁副核的第一步固件编好之后接下来就是主核侧的工作。首先要找到这一颗芯片的 CCUClock Control Unit和 RSTReset相关寄存器把 RISC-V 核的时钟门打开、把它的复位状态释放掉。这一步说难不难说简单也容易翻车——有些芯片的时钟使能和复位释放写在同一组寄存器里而且有先后顺序要求。我处理 V853/T113 这类芯片的经验是先使能时钟等待几个周期再解除复位。如果反过来可能触发异常状态表现为副核看起来“没反应”。具体寄存器位号每颗芯片不同但命名规律通常类似手册里的RISCV_CLK_GATE和RISCV_RST_CTRL搜一下就找得到。如果你手头没有完整寄存器手册另一个办法是去翻厂商闭源 SDK 里跑 RISC-V 固件的示例代码比如 Tina SDK 里的rtos相关目录里面的初始化顺序基本就是芯片设计方推荐的顺序。4.2 搬运固件与内存一致性处理复位释放后副核第一时间会从 TCM 基地址取指所以固件必须在此之前完整落到 TCM 里。这里有个常见问题如果你的固件体积大于 TCM或者你图省事想直接放到 DDR 里运行那就得确保 DDR 已经被正确初始化而且 ARM 侧的缓存不会把数据“藏住”。我的建议是第一次 bring-up 用最朴素的方式把固件放到 TCM。ARM 核通过普通内存映射访问 TCM写完加一个DSB ISH内存屏障确保写操作真正到达 TCM 而不是停留在写缓冲然后再释放复位。如果固件必须放 DDR那 ARM 侧在写完固件后要做 cache cleanRISC-V 侧启动时做 cache invalidate 或fence.i两边配合起来才不容易出数据错乱。另外提一个实用检查方法ARM 侧写完固件后读回 TCM 内容做 CRC 或逐字节比对确认和编译出的.bin一致再释放复位。这一步看起来笨但能快速区分“固件没加载进去”和“固件加载了但跑不起来”两类问题调试效率提高一大截。4.3 共享内存协议设计从“我活着”到“干活”副核跑起来之后主核怎么知道它活了最简单的办法是在约定的共享内存地址放一个魔法数。我在项目里常用的结构是这样/* 共享内存区放在 ARM 和 RISC-V 都能访问的 SRAM */ #define SHM_BASE 0x400000 #define SHM_MAGIC 0xB0B0C0DE #define SHM_MAGIC_ACK 0xDEADC0DE struct amp_shm { volatile uint32_t magic; volatile uint32_t cmd; volatile uint32_t status; volatile uint32_t counter; volatile char logbuf[512]; };主核先把共享内存区域清零写入期望的魔法数然后释放复位。副核启动后第一件事就是检查魔法数如果匹配就把自己的魔法数改成 ACK 并递增 counter。主核轮询 counter发现它变了就说明副核已经跑起来并且能看到共享内存。这个“魔法数握手”是核间通信的最基本形态几乎所有后续功能都建立在这套机制上。在此基础上可以逐步扩展命令协议主核写 cmd 字段然后给副核发一个软件中断副核收到中断就读取 cmd、执行、写回 status。比这更复杂的 OpenAMP 远程处理器框架也是同样的思路只不过把共享内存换成了 vring把命令换成了 virtio 消息。4.4 主核侧与副核侧的代码骨架主核侧的核心逻辑大概是这样static void amp_load_firmware(void) { extern uint8_t _binary_firmware_bin_start[]; extern uint8_t _binary_firmware_bin_end[]; /* 1. 将固件复制到 TCM */ memcpy((void *)TCM_BASE, _binary_firmware_bin_start, _binary_firmware_bin_end - _binary_firmware_bin_start); /* 2. 初始化共享内存 */ volatile struct amp_shm *shm (void *)SHM_BASE; memset((void *)shm, 0, sizeof(*shm)); shm-magic SHM_MAGIC; /* 3. 内存屏障确保写操作对副核可见 */ dsb(ISH); /* 4. 释放 RISC-V 核复位 */ writel(RISCV_RST_RELEASE, RST_BASE RISCV_RST_CTRL); /* 5. 等待副核握手 */ while (shm-magic ! SHM_MAGIC_ACK) { udelay(10); printf(waiting for riscv core...\n); } printf(riscv core alive, counter%lu\n, shm-counter); }副核侧 main 函数对应的逻辑就是先读共享内存、写 ACK、然后进入主循环int main(void) { volatile struct amp_shm *shm (void *)SHM_BASE; if (shm-magic ! SHM_MAGIC) { /* 魔法数不对说明共享内存位置或初始化时序有问题 */ while (1); } shm-magic SHM_MAGIC_ACK; shm-counter; while (1) { if (shm-cmd ! 0) { switch (shm-cmd) { case CMD_LED_ON: gpio_set(1); break; case CMD_LED_OFF: gpio_set(0); break; } shm-status shm-cmd; shm-cmd 0; } wfi(); } }注意副核侧所有共享内存字段都加了volatile这是必须的。编译器可能把多次读取优化成一次导致副核永远看不到主核新写入的命令。如果你以后引入 RTOS还要考虑多个任务同时访问共享内存的互斥但第一步裸机轮询模式可以先把协议调通再想这些。5. 调试实录两次“核消失”和一张问题速查表5.1 实战一寄存器写了副核就是不跑第一次在 V853 上尝试释放 E907 复位我遇到过一个让人很抓狂的现象固件确定已经写进 TCM复位寄存器也写了但副核一点反应都没有——共享内存里魔法数纹丝不动。我先怀疑是固件有问题反汇编看了三遍启动代码没问题又怀疑是 TCM 基地址写错了对照手册确认也没错。后来查了一圈发现问题出在时钟使能上。这颗 SoC 的 RISC-V 核有两个独立控制点一个控制总线时钟一个控制核心时钟而我只打开了其中一个。核心时钟没开的时候寄存器操作看起来“成功”了实际上内核根本没有时钟自然不会跑。把两个时钟位都置上再配合复位释放副核立刻活了。这个经历给两个教训第一释放复位前务必确认时钟树所有相关节点都已使能不要只看名字带 RST 的寄存器第二在没有仿真器和 trace 的情况下可以用共享内存魔法数作为副核“是否跑到 main 之前”的探针如果再配合 GPIO 翻转能把调试粒度切得更细。就这样从“完全无反应”逐渐缩小范围比瞎猜快得多。5.2 实战二跑起来了但数据是花的第二次是在 T113-S3 上副核倒是跑起来了魔法数也能收到但和主核通信传数据时偶尔出现随机损坏。一开始我怀疑是传输协议的问题加了好几层校验还是有概率出错。后来把共享内存从 DDR 挪到片内 SRAM问题直接消失了。原因就是缓存一致性。我之前图省事把共享内存放到了 DDRARM 侧写数据后虽然加了 DSB但 CPU 缓存还有一份 stale 的数据副核读取时拿到的是 DDR 里的旧值或脏数据。片内 SRAM 在 Allwinner 这套方案里通常是不带缓存的ARM 和 RISC-V 两侧访问的都是同一个物理存储天然一致省掉一堆 cache maintenance 的麻烦。这个经历让我养成了两个习惯。一是异构核间通信的共享内存优先选片内 SRAM容量不够再考虑 DDR但一定要把 cache 操作写对。二是共享内存字段全部使用 32 位自然对齐避免跨越总线宽度边界这样可以减少一个总线上的字节序/原子性风险点。5.3 常见问题速查表把这一路遇到的典型问题整理成表格方便你调试时快速定位方向现象优先排查方向常见原因解决建议副核完全无反应时钟、复位、固件核心时钟未使能固件未落到正确地址先读回固件校验再查时钟复位寄存器跑飞、PC 跳到异常地址栈、启动汇编栈指针没设置BSS 未清零单步反汇编启动代码确认 _stack_topIllegal instruction工具链参数-march/-mabi 与内核不匹配统一 rv32imac/ilp32查反汇编有无浮点指令共享数据随机损坏缓存一致性、共享内存位置放 DDR 未做 cache 操作挪到片内 SRAM或补 cache clean/invalidate中断进不来中断使能、路由mstatus.MIE 没开外部中断号映射错先轮询再中断仔细读中断控制器手册看门狗复位喂狗机制调试时主核没喂狗导致全局复位定位阶段先关闭或延长看门狗超时取指拿到旧数据指令缓存固件搬运后未做 fence.i启动开头加 fence.i搬运后从 ARM 侧也做同步这张表不是全能的但它覆盖了异构 bring-up 里频率最高的几类问题。遇到新问题的时候我的习惯是先判断“哪个环节最可疑”复位之前是加载问题复位之后是运行问题通信之后是一致性问题。按这个顺序排查效率最高。5.4 调试自检清单最后给一份可以在每次实验前过一遍的自检清单。第一固件 .bin 是不是最新的编完有没有重新复制到烧写工具或代码里这个问题听起来弱智但真的反复发生。第二TCM 基地址和链接脚本的 ORIGIN 是否一致不一致必然起不来。第三共享内存区域的地址在 ARM 侧和 RISC-V 侧看到的是不是同一个物理地址有些芯片的总线矩阵会做地址重映射。第四复位释放之前要不要先确认副核的中断控制器处于干净状态历史 pending 的中断有时会让副核一启动就进 trap。第五串口日志和 GPIO 指示能省出大量瞎猜时间哪怕是点个灯都行。6. 从“能跑”到“能干”后续集成方向与我的体会6.1 用 Linux 的 remoteproc 框架把它接起来如果只是验证固件逻辑主核侧用 U-Boot 里的裸机代码手动加载就够了。但产品化阶段你不可能让 Linux 主核在用户态直接用寄存器去释放一个协处理器这时候就要请出 Linux 的 remoteproc 框架。它的工作就是把“加载固件、释放复位、维护生命周期、处理异常”这些事情抽象成标准接口让应用侧只需要打开一个设备节点就能和副核通信。具体到 Allwinner 方案你可以在 Linux 侧写一个简单的 remoteproc 驱动把前面讲的时钟/复位/共享内存操作封装进去。固件可以放在文件系统里也可以编译进内核的 firmware 目录由驱动的rproc-ops-start()回调负责加载和释放。再配合 rpmsg/virtio主核和副核之间就能使用标准消息通道而不再是一堆裸指针和魔法数。这一步的工程量不小但收益也明显副核变成 Linux 下一个管理良好的设备掉线可以恢复固件可以热更新业务侧完全不用关心底层细节。我个人建议在主核侧先把裸机通信协议调通之后再动手接 remoteproc不要一步到位因为 remoteproc 的报错信息往往会掩盖底层时序问题。6.2 适合放给小核干的活副核真正发挥价值是在它接管了主核不擅长的任务之后。我实际试过这么几类一类是低功耗待机管理主核进入 suspend 后副核轮询按键和传感器有事件才唤醒主核这一套能显著降低待机功耗。另一类是实时性要求高的 IO 处理比如特定协议的波形采集副核用 GPIO 中断加 TIMER抖动比 Linux 侧小得多。还有一类是系统健康监控副核周期性读取电源电压、温度在 Linux 无响应时执行恢复动作相当于一个低成本硬件看门狗加监控器。音频相关任务也是常见选择部分 Allwinner 方案本身就带音频 DSP 的 RISC-V 核把音频编解码的一部分搬到小核能释放主核算力。但这类任务需要音频框架层面的配合纯裸机开发的工作量比前几类大不少。如果你刚把副核跑通建议先做低功耗唤醒或健康监控这类边界清晰的任务积累信心也积累代码。6.3 我的一些个人体会从 Part 1 到 Part 2最深的感受是异构 bring-up 的本质不是“写代码”而是“建立确定性的可观测过程”。代码本身没多少行难的是每一步都能确认前一环已经正确完成固件确实在 TCM 里、时钟确实在翻转、复位确实释放了、共享内存确实两边看到的是同一个地址。当你把这一连串“确实”建立起来副核自然就跑起来了。最后分享一个小技巧在处理异构芯片时预留一个 GPIO 作为副核的心跳输出副核每次主循环翻转一次。这个 GPIO 用逻辑分析仪一看就能直观判断副核是否在跑、主循环周期是多少比任何日志都直观。我第一次调通握手协议时盯着逻辑分析仪上的方波看了好久——那一刻才真正觉得这颗 RISC-V 小核“活”了。希望这篇记录也能帮你早点看到属于你的方波。
返回列表