尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

异构多核SoC中RISC-V小核引导RTOS的实战指南

异构多核SoC中RISC-V小核引导RTOS的实战指南 1. 项目缘起为什么需要“小核”来引导RTOS在嵌入式开发领域尤其是涉及复杂异构多核处理器的场景里“引导”这个词往往意味着系统启动的第一步也是最关键、最容易出问题的一步。最近我在一个基于全志D1-H主核是阿里平头哥的RISC-V C906的项目上折腾目标是在C906这个大核上跑一个主流的实时操作系统RTOS比如FreeRTOS或RT-Thread。按理说直接把编译好的RTOS镜像烧录到Flash然后让C906从复位向量开始执行不就完了但现实往往没这么简单。这里就引出了“小核引导”的概念。在很多高性能的异构多核SoC里比如全志的D1、V853等除了主应用核C906外还会集成一个或多个更简单、功耗更低的小核典型的就是ARM Cortex-M系列或者RISC-V E系列的小核。这颗小核通常被设计为“系统服务处理器”或者“电源管理核心”它在芯片上电后会最先启动。让它来引导大核上的RTOS背后有几个非常实际的考量第一是启动可靠性与安全性。小核的代码通常非常精简、固化甚至运行在ROM或SRAM中不受外部Flash初始化状态、DRAM训练失败等复杂因素的影响。由它来执行最基础的芯片初始化、时钟/PLL配置、关键电源域上电、以及DDR内存的初始化训练可以确保硬件环境在一个已知、稳定的状态下再“唤醒”主核。这相当于给主核的启动提供了一个可靠的“地基”避免了主核一上电就因硬件未就绪而跑飞。第二是实现灵活的启动流程与多系统共存。小核作为先导可以读取启动介质如SPI NOR Flash、SD卡中的信息根据预设的启动列表、按键状态、甚至是网络命令来决定最终引导哪个系统镜像到主核。你想让C906跑RT-Thread还是FreeRTOS或者从SD卡启动一个Linux内核这些决策逻辑都可以放在小核的固件里实现为主核提供了灵活的启动选择这在产品开发、系统升级和恢复场景中非常有用。第三是低功耗管理与实时响应。在一些低功耗应用场景中主核C906在空闲时可以被深度休眠甚至断电。此时维持系统基本监控、响应外部中断比如按键唤醒、定时器、某些传感器事件的任务就可以交给一直在线的低功耗小核来处理。当事件发生时小核再负责重新上电并引导主核从休眠状态快速恢复。这种设计能显著降低系统整体功耗。所以“小核引导RTOS---RISC-V C906”这个标题指向的正是一个在真实产品中非常经典的架构利用一颗资源有限的协处理器小核为资源丰富的主应用处理器RISC-V C906准备好运行环境并最终将RTOS的控制权移交过去。这个过程涉及到底层硬件初始化、双核间的通信机制、镜像加载与跳转等一系列紧密耦合的软硬件技术点。接下来我就结合D1-H的实践把这个过程的里里外外拆解清楚。2. 核心硬件平台与启动流程全景要理解小核如何引导大核必须先把我们舞台的硬件背景搞清楚。我这次使用的平台是全志的D1-H芯片它是一个非常典型的异构多核SoC其核心组件如下主应用核 (Big Core) 一颗64位的RISC-V C906核心主频最高1GHz支持MMU通常用来运行富功能的操作系统如Linux或者我们这次的目标——实时操作系统RTOS。协处理器/小核 (Small Core) 一颗32位的RISC-V E907核心。E907是一个顺序执行、精简的嵌入式内核主打低功耗和实时性通常运行一个极其轻量级的RTOS甚至是裸机程序负责系统管理、电源管理、外设监控等任务。共享资源 两颗核心通过内部总线如AXI互联共享部分内存通常是SRAM、外设如UART、GPIO、定时器以及最重要的——系统控制模块。这个模块包含复位控制器、时钟控制器、电源管理单元等是小核配置大核运行环境的关键。启动介质 通常是板载的SPI NOR Flash。芯片上电后会从一个固定的地址Boot ROM开始执行代码。基于这个硬件一个完整的“小核引导C906 RTOS”的启动流程可以分为以下几个清晰的阶段阶段零芯片固有流程 (Boot ROM)芯片一上电硬件逻辑会强制从Boot ROM的起始地址通常是0x00000000取指执行。这段ROM代码是芯片出厂时固化的它非常“傻”只做最基础的事情初始化CPU核心此时只有小核E907能运行、配置最基本的时钟比如内部RC振荡器、然后尝试从预设的启动设备如SPI0读取最前面的一小段代码通常是32KB或64KB到芯片内部SRAM中并跳转到SRAM执行。这段被加载的代码就是我们的“小核固件”或“Bootloader”。阶段一小核固件执行 (E907 Bootloader)现在小核E907开始在SRAM中运行我们编写的引导程序。这个阶段是小核工作的核心它要完成以下几件大事初始化关键硬件 配置系统时钟PLL让CPU、总线、内存跑到设计频率初始化DDR控制器对板载的DDR3/DDR4内存进行训练使其稳定工作初始化必要的调试串口如UART0方便我们打印日志。加载主核镜像 从Flash或SD卡、网络等的特定位置将主核C906要运行的RTOS镜像读取到DDR内存的指定地址。这个地址必须在编译RTOS时就确定好通常是DDR的起始地址如0x40000000。配置主核启动参数 将主核的启动入口地址即RTOS镜像的入口函数地址、可能的设备树二进制文件地址等信息写入到系统控制模块的特定寄存器中。这些寄存器是硬件约定的用于告知主核“醒来后去哪里执行”。释放主核复位 最后通过写系统控制模块的复位控制寄存器解除对C906核心的复位状态。此时C906核心开始从硬件指定的复位向量地址通常映射到SRAM或DDR的某个固定地址取指执行。阶段二主核RTOS启动 (C906 RTOS)C906核心被释放后它会首先执行一段“引导头”代码可能是小核固件放在复位向量处的跳转指令也可能是RTOS镜像自带的启动代码。这段代码的责任是初始化C906核心环境 设置C906的异常向量表、栈指针、以及可能需要的核心局部资源。跳转到RTOS入口 最终跳转到我们编译的RTOS的入口函数如main()或rtthread_startup()。RTOS内核初始化 RTOS开始接管初始化自己的内核对象任务、信号量、队列等、硬件驱动、并最终创建用户任务系统开始正常运行。在整个过程中小核E907和大核C906之间的通信是单向、异步的主要通过共享内存和硬件寄存器来完成。小核把“信”RTOS镜像和启动参数放在“信箱”DDR内存和特定寄存器里然后“拍醒”释放复位大核。大核醒来后自己去“信箱”取信并执行。之后两个核可以并行运行通过中断和共享内存进行更复杂的协同工作。3. 小核引导程序的关键实现细节理解了全景流程我们聚焦到最核心的部分运行在小核E907上的引导程序Bootloader具体该如何实现。这里我以在D1-H的E907核心上编写一个简易的、用于引导C906 RTOS的裸机程序为例拆解几个关键环节。3.1 启动头与链接脚本的定制小核的代码最终要被打包成二进制文件烧录到Flash的起始位置。芯片的Boot ROM会把它加载到SRAM。因此我们的程序必须符合硬件的加载约定。首先需要一个特殊的启动头。这个头通常是一个简单的汇编文件start.S它必须放在整个二进制文件的最开头。它的核心任务就两个设置E907核心的栈指针SP然后跳转到C语言的main函数。为什么用汇编因为C语言函数调用需要栈而一开始栈指针是未定义的。// start.S 示例 (RISC-V汇编) .section .text.entry .global _start _start: // 1. 设置栈指针。需要根据你的SRAM布局来定。 // 假设SRAM地址范围是0x00020000~0x0002FFFF我们将栈顶设在0x0002FF00。 li sp, 0x0002FF00 // 2. 清零BSS段存放未初始化全局变量的区域 la a0, _bss_start la a1, _bss_end bgeu a0, a1, clear_bss_done clear_bss_loop: sw zero, (a0) addi a0, a0, 4 bltu a0, a1, clear_bss_loop clear_bss_done: // 3. 跳转到C语言主函数 call main // 4. 主函数不应返回若返回则死循环 1: j 1b其次一个精准的链接脚本linker.ld至关重要。它定义了代码、数据、栈在内存中的布局。对于小核引导程序我们通常将其全部加载到SRAM中运行因为此时DDR可能还未初始化。/* linker.ld 示例 */ MEMORY { /* D1-H 小核启动SRAM区域具体地址需查数据手册 */ SRAM (rwx) : ORIGIN 0x00020000, LENGTH 64K } SECTIONS { /* 程序入口点 */ . ORIGIN(SRAM); .text : { /* 确保启动汇编代码在最前面 */ *(.text.entry) *(.text .text.*) } SRAM .rodata : { *(.rodata .rodata.*) } SRAM .data : { _data_start .; *(.data .data.*) _data_end .; } SRAM /* BSS段存放未初始化的全局/静态变量 */ .bss : { _bss_start .; *(.bss .bss.*) *(COMMON) _bss_end .; } SRAM /* 栈空间定义在BSS段之后的高地址区域 */ .stack (NOLOAD) : { . ALIGN(16); _stack_end .; . 4K; /* 分配4KB栈空间 */ _stack_top .; } SRAM /* 全局指针用于优化访问静态/全局变量 */ _global_pointer _data_start 0x800; }注意_bss_start和_bss_end这些符号在链接脚本中定义然后在汇编和C代码中通过extern声明来使用用于在启动时清零BSS段。这是C程序能正确使用未初始化全局变量的前提。3.2 硬件初始化时钟、串口与DDR在main函数中我们需要按顺序初始化硬件。顺序很重要因为后续模块依赖前面的基础。1. 系统时钟与PLL配置这是第一步。芯片刚上电时通常运行在低速的内部时钟上。我们需要配置锁相环为系统提供稳定的高速时钟。以D1-H的CCU时钟控制单元为例我们需要使能目标PLL的电源。配置PLL的倍频因子N、分频因子M、P等。等待PLL锁定Polling锁定状态位。将系统核心时钟源切换到这个PLL的输出。// 伪代码寄存器名称和位域需查阅具体芯片手册 void clock_init(void) { // 1. 设置PLL_CPUX的配置寄存器 (例如N60, M1, P0) PLL_CPUX_CTRL_REG (60 24) | (1 8) | (0 0); // 2. 使能PLL PLL_CPUX_CTRL_REG | (1 31); // 3. 等待锁定 while(!(PLL_CPUX_CTRL_REG (1 28))); // 4. 切换CPU时钟源到PLL_CPUX输出 CPU_CLK_SRC_REG 0x2; // 假设0x2代表选择PLL_CPUX }2. 调试串口初始化在初始化DDR这种复杂且容易失败的操作之前初始化一个串口用于打印调试信息是救命稻草。配置UART的波特率、数据位、停止位并重定向printf函数到该串口。void uart_init(int baud_rate) { // 1. 配置GPIO复用功能为UART TX/RX // 2. 配置UART时钟源和分频器计算波特率除数 uint32_t divisor (APB_CLK_FREQ / (baud_rate * 16)); UART_DLH_REG (divisor 8) 0xFF; UART_DLL_REG divisor 0xFF; // 3. 配置数据格式 (8N1) UART_LCR_REG 0x03; // 4. 使能FIFO如果有 UART_FCR_REG 0x01; } // 实现putchar供printf调用 int putchar(int c) { while (!(UART_LSR_REG (1 5))); // 等待发送保持寄存器空 UART_THR_REG c; return c; }3. DDR内存初始化与训练这是小核引导程序中最复杂、最芯片相关的一步。DDR控制器需要一套严格的配置序列来启动包括设置时序参数CL、tRCD、tRP、tRAS等、配置阻抗校准、执行ZQ校准、以及最重要的——读写训练。读写训练Leveling的目的是补偿PCB走线长度差异造成的时钟与数据信号偏移。控制器会发送特定的测试模式通过调整延迟线DQS来找到数据窗口的中心点。这个过程完全依赖硬件通常由厂商提供库函数或参考代码。void ddr_init(void) { printf([E907] Start DDR Init...\n); // 1. 复位DDR控制器 DDRC_CTRL_REG | (1 0); udelay(100); DDRC_CTRL_REG ~(1 0); // 2. 配置DDR类型、时序参数、地址映射等 // 这部分参数来自芯片手册和板级设计通常是一个庞大的结构体数组 // 例如ddr_para_t ddr_timing {.cl 11, .t_rcd 11, .t_rp 11, ...}; // 调用配置函数ddr_config(ddr_timing); // 3. 执行ZQ校准阻抗匹配 ddr_zq_calibration(); // 4. 执行读写训练这是关键 int ret ddr_training(); if (ret ! 0) { printf([E907] DDR Training FAILED! Code: %d\n, ret); while(1); // 训练失败死循环。实际产品可能需要尝试安全配置或重启。 } // 5. 使能DDR控制器 DDRC_CTRL_REG | (1 1); printf([E907] DDR Init OK. Size: %d MB\n, ddr_get_capacity_mb()); }实操心得DDR初始化代码通常由芯片原厂提供不建议自己从头编写。我们的工作重点是理解流程并正确地将厂商的参考代码移植到自己的引导程序中。务必根据自己板子使用的DDR颗粒型号修改对应的时序参数。训练失败是常事通过串口打印的调试信息如果有和LED指示灯来辅助排查。3.3 加载RTOS镜像与启动参数传递DDR准备好后我们就可以把“主角”——C906的RTOS镜像——从Flash搬移到DDR了。1. 镜像加载假设我们的RTOS镜像比如一个rtthread.bin被存放在SPI Flash的0x00100000偏移地址处我们要把它加载到DDR的0x40000000地址。void load_rtos_image(void) { uint32_t load_addr 0x40000000; // DDR目标地址 uint32_t flash_offset 0x00100000; // Flash中的镜像位置 uint32_t image_size 0; // 我们需要知道镜像大小 // 方法1镜像头部包含大小信息例如前4个字节是长度 spi_flash_read(flash_offset, (void*)image_size, 4); printf([E907] RTOS Image Size: %d bytes\n, image_size); // 方法2固定大小需确保编译的RTOS不超过此大小 // image_size 512 * 1024; // 512KB // 从Flash读取镜像到DDR printf([E907] Loading RTOS image from 0x%x to 0x%x...\n, flash_offset, load_addr); spi_flash_read(flash_offset 4, (void*)load_addr, image_size); // 跳过4字节头 printf([E907] Load done.\n); // 可选校验和验证增加可靠性 if (!verify_checksum((void*)load_addr, image_size)) { printf([E907] Image checksum error!\n); while(1); } }2. 启动参数传递C906启动时需要知道两个关键信息入口地址和设备树地址如果RTOS使用设备树。这些信息需要通过约定的方式传递。入口地址就是RTOS镜像的入口函数地址。对于用GCC编译的RTOS这通常是链接脚本中定义的_start或reset_vector的地址。在我们的例子中就是0x40000000。我们需要将这个地址写入到一个C906核心在复位释放后会去读取的寄存器。在全志平台这个寄存器可能是CPUCFG_GEN_CTRL_REG或类似的“主核入口地址寄存器”。设备树地址如果RTOS如RT-Thread使用设备树我们需要将设备树二进制文件也加载到DDR比如0x41000000然后将这个地址通过另一个寄存器如CPUCFG_DTB_ADDR_REG或者约定的内存位置如入口地址前8个字节传递给C906。void setup_boot_params(uint32_t entry_addr, uint32_t dtb_addr) { // 写入主核入口地址寄存器 CPUCFG_ENTRY_REG entry_addr; // 写入设备树地址寄存器 CPUCFG_DTB_ADDR_REG dtb_addr; // 或者采用更通用的ATAGS/DTB传递方式在入口地址前预留空间存放参数 uint32_t *boot_params (uint32_t*)(entry_addr - 0x100); boot_params[0] 0xdeadbeef; // 魔数用于校验 boot_params[1] dtb_addr; // ... 其他参数 printf([E907] Boot params set. Entry: 0x%x, DTB: 0x%x\n, entry_addr, dtb_addr); }3.4 释放主核与状态同步万事俱备只欠东风。最后一步就是释放C906核心。void release_c906(void) { printf([E907] Releasing C906 core...\n); // 1. 确保所有对共享资源的配置如MMU、缓存已经完成并且数据已经同步。 // 对于简单的引导可能只需要数据内存屏障。 asm volatile(fence iorw, iorw ::: memory); // 2. 清除C906核心的复位位 // 假设CPUCFG_CPU_RST_CTRL_REG的bit1控制C906复位 (0-复位1-释放) CPUCFG_CPU_RST_CTRL_REG ~(1 1); // 3. 可选触发一个事件如SEV指令或设置一个标志通知C906可以开始运行。 // 在简单的引导中释放复位后C906会自动从复位向量开始执行。 printf([E907] C906 released. Waiting for its signal...\n); // 4. 小核可以进入等待循环或者继续执行自己的任务。 // 例如可以等待一个来自C906的“启动完成”信号通过共享内存或中断。 volatile uint32_t *boot_flag (uint32_t*)SHARED_MEM_BOOT_FLAG; while (*boot_flag ! 0xC906B007) { // 可以在这里执行一些低功耗操作 asm volatile(wfi); // 等待中断降低功耗 } printf([E907] C906 signaled ready. Dual-core system is up!\n); }至此小核的引导任务基本完成。C906核心开始执行位于0x40000000的RTOS代码而小核E907可以继续运行扮演系统看守者或实时任务处理器的角色。4. RTOS侧的适配与双核通信基础小核把RTOS镜像加载到DDR并唤醒了C906但RTOS本身也需要做一些适配才能在这个“被引导”的环境下正确启动并意识到小核的存在。4.1 RTOS启动代码的调整RTOS的启动代码通常是startup_xxx.S和board.c需要知道它运行在什么地址以及硬件已经被初始化到了什么程度。链接地址在编译RTOS时其链接脚本link.lds中的内存起始地址必须和小核加载它的地址一致即0x40000000。跳过重复初始化小核已经初始化了时钟、DDR、串口等。因此RTOS的板级初始化函数如rt_hw_board_init()中必须跳过或谨慎处理这些硬件。例如时钟可能只需要读取当前设置而不重新配置串口可能只需要设置波特率而不重新初始化GPIO和控制器。错误的二次初始化可能导致系统崩溃。获取启动参数RTOS启动后需要从小核约定的地方寄存器或特定内存地址读取设备树地址等参数。// RT-Thread 板级初始化函数示例 (C906侧) void rt_hw_board_init(void) { /* 1. 初始化硬件中断 */ rt_hw_interrupt_init(); /* 2. 系统时钟初始化 —— 注意这里可能只是设置系统tick而非重新配置PLL */ // 假设小核已将系统主频设置为1GHz uint32_t system_core_clock 1000000000; SysTick_Config(system_core_clock / RT_TICK_PER_SECOND); /* 3. 外设初始化 —— 串口可能只需配置GPIO复用已由小核完成 */ uart_reinit_for_console(); // 重新配置波特率等而非从头初始化 /* 4. 获取设备树地址并初始化 */ uint32_t dtb_addr get_dtb_addr_from_boot_param(); // 从共享内存或寄存器读取 if (dtb_addr) { rt_hw_dtb_init((void*)dtb_addr); } /* 5. 初始化动态内存堆 —— 地址范围需避开小核使用的区域 */ rt_system_heap_init((void*)HEAP_START, (void*)HEAP_END); /* ... 其他初始化 ... */ /* 6. 通知小核RTOS已启动成功 */ signal_boot_complete_to_e907(); rt_console_set_device(RT_CONSOLE_DEVICE_NAME); }4.2 建立基础的双核通信机制两个核心独立运行后需要一种机制来协同工作。最基础的通信方式是共享内存。划定共享内存区域在链接脚本或内存映射中明确划分出一段DDR区域例如0x43000000 ~ 0x4300FFFF作为共享内存。两个核心的工程都需要将这个区域排除在自己的私有堆栈之外并声明对应的数据结构。定义通信协议设计一个简单的数据结构放在共享内存开头包含命令、状态、数据等字段。// shared_mem.h - 两个核心工程共用的头文件 #define SHARED_MEM_BASE 0x43000000 typedef struct { volatile uint32_t boot_flag; // 启动状态标志 volatile uint32_t command_from_big; // 大核发给小核的命令 volatile uint32_t data_from_big; // 大核命令附带数据 volatile uint32_t command_from_small; // 小核发给大核的命令 volatile uint32_t data_from_small; // 小核命令附带数据 uint8_t message_pool[1024]; // 消息池 } shared_mem_t; static inline shared_mem_t* get_shared_mem(void) { return (shared_mem_t*)SHARED_MEM_BASE; }使用内存屏障确保数据一致性在多核系统中编译器和CPU缓存可能导致一个核心的写入另一个核心看不到。在读写共享内存中的volatile变量前后需要使用数据内存屏障指令。// C906侧发送命令 void send_cmd_to_small(uint32_t cmd, uint32_t data) { shared_mem_t *sm get_shared_mem(); asm volatile(fence w, w ::: memory); // 写屏障确保之前的写入完成 sm-command_from_big cmd; sm-data_from_big data; asm volatile(fence w, w ::: memory); // 再次屏障确保命令写入对E907可见 } // E907侧轮询命令 void poll_command_from_big(void) { shared_mem_t *sm get_shared_mem(); asm volatile(fence r, r ::: memory); // 读屏障确保读取最新值 uint32_t cmd sm-command_from_big; if (cmd ! 0) { process_command(cmd, sm-data_from_big); sm-command_from_big 0; // 清空命令 asm volatile(fence w, w ::: memory); } }基于共享内存和轮询可以实现简单的状态同步和命令传递。对于实时性要求更高的通信则需要借助核间中断。芯片通常会提供硬件机制让一个核心可以触发另一个核心的特定中断。例如E907写某个寄存器可以触发C906的软件中断反之亦然。在中断服务例程中处理共享内存中的数据效率要高得多。5. 调试技巧与常见问题排查这种底层引导和双核开发调试起来比单核复杂。分享几个我踩过坑后总结的实用技巧。1. 串口日志是生命线务必确保小核的串口最早初始化并在每个关键步骤初始化PLL成功、DDR训练开始/结束、加载镜像、释放主核后打印明确的日志。格式如[E907] 步骤方便区分日志来源。如果系统卡住最后一条日志就是线索。2. 使用LED和示波器辅助在没有串口或串口无法工作时GPIO控制的LED是简单的状态指示器。可以在不同阶段让LED闪烁不同模式。用示波器测量这个GPIO引脚可以精确判断程序执行到了哪一步卡住。例如初始化PLL前闪1下初始化DDR前闪2下加载镜像前闪3下。3. 关于DDR训练失败这是最常见的问题。现象可能是小核打印完“Start DDR Init...”后死机或者加载镜像后数据错乱。检查时序参数首先确认ddr_timing参数是否与板上DDR颗粒的型号完全匹配。不同速率、不同品牌的颗粒参数差异很大。检查电源和参考电压用万用表测量DDR芯片的VDD、VTT、VREF等电源引脚是否稳定、符合要求。降低频率尝试将DDR运行频率降低一档比如从1066MHz降到800MHz看是否能训练成功。如果成功说明PCB布线或电源质量可能在高频下不达标。查看训练状态寄存器如果厂商代码提供了训练状态信息通过串口打印出来能定位是读训练失败还是写训练失败。4. 主核不启动或启动即崩溃检查入口地址确认写入CPUCFG_ENTRY_REG的地址是否精确等于RTOS镜像的入口地址可通过readelf -a rtthread.elf | grep Entry查看。地址错一位都不行。检查镜像加载完整性在小核加载完镜像后计算一次CRC32校验和与事先存储在Flash中的校验和对比。或者让C906启动后先读取自身代码的前几个字节比如魔数并打印出来与小核加载时的数据对比。检查C906的启动环境C906启动时可能期望某些特定寄存器如mstatus处于特定状态。确保小核在释放复位前没有错误地修改过这些核心寄存器。最稳妥的方法是小核只做必要的硬件初始化和参数传递不要动C906的核心寄存器。使用JTAG调试如果条件允许用JTAG分别连接到E907和C906核心。可以先单步调试小核程序确保流程正确。然后在小核释放C906前设置断点再切换到C906的调试视角单步跟踪其启动的第一条指令这是最强大的调试手段。5. 双核通信数据不一致确保缓存一致性如果共享内存区域被缓存了一个核心的写入可能停留在缓存里另一个核心读不到。最简单的做法是在软件中将这片内存区域配置为非缓存。这通常在MMU或内存属性配置单元中设置。正确使用内存屏障如前所述在读写共享变量前后插入fence指令。使用volatile关键字防止编译器对共享变量进行优化如将读取操作优化掉。6. 优化小核固件大小小核的SRAM空间有限可能只有64KB或128KB。需要精打细算编译时使用-Os优化选项减小代码体积。避免使用大型库函数如printf的浮点格式支持。将不需要在启动阶段使用的数据如字符串常量放到Flash中运行时再读取。使用arm-none-eabi-size或riscv-none-elf-size工具分析生成的.elf文件查看.text、.data、.bss段的大小针对性优化。这个过程虽然繁琐但一旦打通你对芯片启动流程、多核架构的理解会上一个大台阶。这种“小核引导大核”的模式在追求高可靠性、低功耗、快速启动的嵌入式产品中会越来越常见。
返回列表