尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

树莓派3B移植64位RT-Thread:从ARMv8启动到最小系统构建

树莓派3B移植64位RT-Thread:从ARMv8启动到最小系统构建 1. 项目缘起为什么要在树莓派3B上折腾64位RT-Thread最近在整理手头的开发板翻出了吃灰已久的树莓派3B。这块板子性能说强不强说弱不弱跑个完整的Linux桌面有点吃力但用来做嵌入式开发或者轻量级服务器又绰绰有余。我就在想能不能给它换个“芯”让它跑一个更轻量、更实时、启动更快的系统于是RT-Thread这个优秀的国产实时操作系统就进入了我的视线。但树莓派3B的博通BCM2837 SoC其Cortex-A53核心是支持64位AArch64指令集的。我们平时用的Raspberry Pi OS原Raspbian默认是32位系统这其实没有完全发挥出硬件的潜力。尤其是在处理一些需要大内存寻址或特定64位优化的任务时64位系统有其天然优势。而RT-Thread作为一个高度可裁剪的RTOS其64位版本针对Cortex-A核能更好地利用这个特性比如更高效的原子操作、更大的虚拟地址空间以及对未来一些64位专属库和工具链的更好支持。所以这个项目的目标就很明确了将RT-Thread操作系统以64位AArch64的模式移植到树莓派3B开发板上并让最基本的系统成功运行起来。这不仅仅是让一个系统跑起来更是一次深入理解ARMv8架构启动流程、RT-Thread内核移植、以及树莓派硬件底层的好机会。整个过程会涉及到交叉编译工具链、启动引导、设备树、外设驱动等多个环节踩坑是必然的但收获也会非常扎实。2. 前期准备工具链与源码环境搭建工欲善其事必先利其器。在开始动手修改代码之前一个稳定可靠的开发环境是成功的一半。这个环节看似基础但很多问题都源于环境配置不当。2.1 交叉编译工具链的选择与安装树莓派3B的CPU是ARM Cortex-A53我们要编译的是AArch64架构的代码因此必须使用对应的交叉编译工具链。这里我选择的是Linaro提供的aarch64-linux-gnu-工具链它成熟稳定社区支持好。在Ubuntu或Debian系的开发机上可以直接通过apt安装sudo apt update sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu安装完成后使用aarch64-linux-gnu-gcc -v命令验证应该能看到类似Target: aarch64-linux-gnu的输出。注意有些教程会使用gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf这类none-elf工具链。对于RT-Thread这种最终需要加载到Linux通过U-Boot等引导的“裸机”程序使用linux-gnu工具链通常是更合适的选择因为它包含了针对Linux ABI应用二进制接口的库和头文件在链接阶段会更顺畅。而none-elf更偏向于纯粹的、无操作系统的嵌入式环境。2.2 获取RT-Thread源码RT-Thread的源码托管在Gitee上。我们直接克隆官方仓库并切换到我们需要的分支或标签。为了稳定性我选择了一个长期支持版本LTS的分支。git clone https://gitee.com/rtthread/rt-thread.git cd rt-thread git checkout lts-v4.1.x # 以实际LTS分支名为准例如 lts-v4.1.x2.3 认识RT-Thread的BSP板级支持包结构RT-Thread采用BSPBoard Support Package的方式来组织不同硬件平台的代码。在rt-thread/bsp目录下你可以看到很多芯片厂商和开发板的目录如stm32,gd32,imx6ull等。我们的目标是在bsp目录下为树莓派3B创建一个新的目录比如raspi3-64。但更高效的做法是参考一个已有的、最相似的BSP进行修改。树莓派3B和树莓派4B在底层外设如Mini UART、GPIO、系统定时器上有很高的相似性且树莓派4B已经有社区维护的BSP可能是32位的。因此我选择先研究bsp/raspi4如果存在或bsp/raspi3通常是32位的代码结构。一个标准的RT-Thread BSP通常包含以下关键目录和文件applications/: 用户应用代码目录。drivers/: 板级外设驱动如UART、GPIO、SD卡等。libraries/: 芯片厂商提供的底层库如树莓派的bsps库但RT-Thread通常自己实现或使用HAL。rtconfig.py: Python脚本用于通过scons工具生成rtconfig.h系统配置头文件和进行工程构建。SConscript: Scons构建系统的脚本定义了如何编译本BSP下的所有源文件。board.c/board.h: 板级硬件初始化代码如内存布局、时钟初始化、串口初始化等。linker_scripts.lds: 链接脚本定义了代码、数据、堆栈等在内存中的布局。这是64位移植的核心文件之一。3. 内核移植核心链接脚本与启动代码的适配这是整个移植过程中技术含量最高、也最容易出错的部分。我们需要让RT-Thread内核知道它运行在AArch64环境下并且正确地放在树莓派3B内存的合适位置。3.1 理解树莓派3B的启动流程与内存映射树莓派的启动过程比较特殊上电后SoC中的GPUVideoCore首先运行从SD卡的特定分区FAT32加载引导文件bootcode.bin,start.elf。start.elf会读取config.txt和cmdline.txt然后加载我们指定的内核镜像比如kernel8.img用于64位模式。GPU将CPU从复位状态释放CPU从内存地址0x80000开始执行我们的内核代码。对于AArch64模式这个入口地址同样是0x80000。树莓派3B的物理内存起始地址是0x00000000但GPU会占用最开始的几十MB。通常我们将内核加载到0x80000而将可用内存的起始地址设定在稍后的位置例如0x2000002MB之后以避免与GPU或固件使用的区域冲突。RAM的总大小是1GB0x40000000。3.2 编写AArch64的链接脚本 (linker_scripts.lds)链接脚本决定了程序各个段section在内存中的布局。我们需要创建一个针对AArch64的链接脚本。可以参考RT-Thread其他ARMv8-A BSP如qemu-virt64-aarch64的链接脚本。一个简化版的核心内容如下ENTRY(_start) SECTIONS { . 0x80000; /* 内核加载地址 */ .text : { KEEP(*(.vectors)) /* 异常向量表AArch64有新的布局 */ *(.text*) } .rodata : { *(.rodata*) } .data : { *(.data*) } .bss (NOLOAD) : { __bss_start .; *(.bss*) *(COMMON) __bss_end .; } . ALIGN(8); __heap_start .; . . 64M; /* 定义64KB的堆空间可根据需要调整 */ __heap_end .; . ALIGN(4096); __stack_top .; . . 0x4000; /* 定义16KB的栈空间例如给主线程 */ __stack_bottom .; /DISCARD/ : { *(.comment) *(.gnu*) } }关键点解析ENTRY(_start): 指定程序入口点为_start符号这将在启动汇编代码中定义。. 0x80000;: 设置当前地址计数器为内核加载地址。KEEP(*(.vectors)): 强制保留异常向量表即使它没有被引用。AArch64的异常向量表地址由VBAR_EL3或VBAR_EL1寄存器指定但链接时仍需将其放在正确的位置。__bss_start/__bss_end,__heap_start/__heap_end,__stack_top/__stack_bottom: 这些符号将在C代码中被引用用于初始化BSS段清零、初始化堆管理器和设置栈指针。3.3 编写AArch64的启动汇编代码 (startup_gcc.S)启动代码需要完成从汇编世界到C世界的过渡并为C语言运行环境做好准备。AArch64的汇编语法与ARMv7有所不同。主要任务包括定义入口点_start。设置异常向量表AArch64的异常向量表有16个条目4个异常级别 x 4种路由状态我们需要设置EL1内核运行的特权级的向量表。初期可以放置简单的死循环后续再完善异常处理。初始化CPU设置EL1所需的控制寄存器如SCTLR_EL1禁用EL0和EL1的浮点与SIMD陷阱设置CPACR_EL1使能指令和数据缓存。设置栈指针从链接脚本中获取__stack_top的地址赋值给SP寄存器。清零BSS段利用链接脚本中定义的__bss_start和__bss_end将这一段内存清零。这是C语言中未初始化全局变量初始值为0能正常工作的前提。跳转到C入口函数最后调用rtthread_startup()函数进入RT-Thread的初始化世界。下面是一段关键步骤的示例代码片段.globl _start _start: /* 1. 设置异常向量基地址到VBAR_EL1 */ ldr x0, vector_table msr VBAR_EL1, x0 /* 2. 配置CPU为EL1并禁用不需要的特性 */ mrs x0, SCTLR_EL1 bic x0, x0, #(1 12) // 禁用指令缓存初期为调试方便 bic x0, x0, #(1 2) // 禁用数据缓存初期为调试方便 bic x0, x0, #(1 1) // 禁用对齐检查 orr x0, x0, #(1 0) // 使能MMU稍后配置初期可先关闭 msr SCTLR_EL1, x0 isb // 指令同步屏障 /* 3. 设置栈指针 */ ldr x0, __stack_top mov sp, x0 /* 4. 清零BSS段 */ ldr x0, __bss_start ldr x1, __bss_end mov x2, #0 clear_bss_loop: cmp x0, x1 b.ge clear_bss_done str x2, [x0], #8 b clear_bss_loop clear_bss_done: /* 5. 跳转到C语言主函数 */ bl rtthread_startup /* 6. 如果C函数返回则进入死循环 */ loop: b loop4. 板级初始化与最小系统驱动实现当启动汇编代码成功跳转到rtthread_startup()后RT-Thread内核就开始接管了。但在这之前我们需要在板级初始化中为内核准备好最基本的运行环境主要是串口驱动这是我们唯一的调试输出窗口。4.1 实现串口控制台驱动树莓派3B的Mini UARTPL011 UART被蓝牙占用但可通过配置切换是最简单的输出设备。我们需要为其实现rt_hw_console_output()函数该函数会被RT-Thread的日志系统rt_kprintf调用。首先要了解Mini UART的寄存器映射。树莓派的外设寄存器基地址是0x3F000000这是总线地址在MMU未开启时物理地址等于总线地址。Mini UART的偏移地址是0x00215040。在drivers/目录下创建drv_uart.c#include rtthread.h #include rthw.h #define AUX_ENABLE (*(volatile rt_uint32_t*)(0x3F215004)) #define AUX_MU_IO (*(volatile rt_uint32_t*)(0x3F215040)) #define AUX_MU_IER (*(volatile rt_uint32_t*)(0x3F215044)) #define AUX_MU_IIR (*(volatile rt_uint32_t*)(0x3F215048)) #define AUX_MU_LCR (*(volatile rt_uint32_t*)(0x3F21504C)) #define AUX_MU_MCR (*(volatile rt_uint32_t*)(0x3F215050)) #define AUX_MU_LSR (*(volatile rt_uint32_t*)(0x3F215054)) #define AUX_MU_MSR (*(volatile rt_uint32_t*)(0x3F215058)) #define AUX_MU_SCRATCH (*(volatile rt_uint32_t*)(0x3F21505C)) #define AUX_MU_CNTL (*(volatile rt_uint32_t*)(0x3F215060)) #define AUX_MU_STAT (*(volatile rt_uint32_t*)(0x3F215064)) #define AUX_MU_BAUD (*(volatile rt_uint32_t*)(0x3F215068)) void rt_hw_console_output(const char *str) { while (*str) { /* 等待发送缓冲区为空 */ while (!(AUX_MU_LSR 0x20)) { ; } /* 发送一个字符 */ AUX_MU_IO *str; } } /* 串口初始化函数需要在board.c中调用 */ void rt_hw_uart_init(void) { /* 1. 使能Mini UART功能 */ AUX_ENABLE | 1; // 使能UART1 /* 2. 禁用中断初期 polling 模式*/ AUX_MU_IER 0; /* 3. 设置数据格式8位数据无校验 */ AUX_MU_LCR 3; // 8位模式 /* 4. 设置RTS为高禁用流控*/ AUX_MU_MCR 0; /* 5. 清除FIFO */ AUX_MU_IIR 0xC6; /* 6. 设置波特率。公式baud sys_clk / (8 * (baud_reg 1))。 假设系统时钟为250MHz目标波特率115200。 计算baud_reg (250000000 / (8 * 115200)) - 1 ≈ 270 - 1 269 */ AUX_MU_BAUD 269; /* 7. 使能发送和接收 */ AUX_MU_CNTL 3; }踩坑记录波特率计算是第一个大坑。树莓派3B的核心时钟频率 (sys_clk) 默认是250MHz但它是可变的。上述计算基于默认值。如果串口输出乱码首要怀疑对象就是波特率。可以通过在config.txt中添加core_freq250来锁定核心时钟频率排除这个变量。4.2 系统时钟Tick源配置RT-Thread作为实时操作系统需要一个高精度、稳定的定时器来产生系统心跳Tick。树莓派有一个ARM Core专属的系统定时器System Timer它非常稳定适合作为Tick源。这个定时器的寄存器基地址是0x3F003000。我们需要实现rt_hw_timer_init()函数配置一个定时器通道例如通道1使其每隔一段时间例如10ms产生一次中断。#include rtthread.h #include rthw.h #define TIMER_BASE (0x3F003000) #define TIMER_CS (*(volatile rt_uint32_t*)(TIMER_BASE 0x00)) #define TIMER_CLO (*(volatile rt_uint32_t*)(TIMER_BASE 0x04)) #define TIMER_CHI (*(volatile rt_uint32_t*)(TIMER_BASE 0x08)) #define TIMER_C1 (*(volatile rt_uint32_t*)(TIMER_BASE 0x10)) #define TIMER_INTERVAL (10000) // 10ms 1MHz (系统定时器时钟为1MHz) void rt_hw_timer_init(void) { rt_uint64_t current_time; /* 读取64位当前计时值低32位在CLO高32位在CHI */ do { rt_uint32_t hi TIMER_CHI; rt_uint32_t lo TIMER_CLO; if (hi ! TIMER_CHI) { // 防止读取过程中CHI变化 continue; } current_time ((rt_uint64_t)hi 32) | lo; } while(0); /* 设置通道1的比较值 当前时间 间隔 */ TIMER_C1 (rt_uint32_t)(current_time TIMER_INTERVAL); /* 清除通道1的中断标志 */ TIMER_CS | (1 1); /* 使能系统定时器中断到CPU需要配置GIC见下文 */ }定时器中断产生后需要通过ARM的通用中断控制器GIC来路由到CPU。树莓派3B使用GIC-400。配置GIC是另一个复杂点需要设置中断号、优先级、目标CPU等。这里先不展开但必须完成否则系统Tick无法工作。4.3 内存管理初始化RT-Thread需要知道可用的内存空间在哪里、有多大。我们在链接脚本中定义了堆的起止地址__heap_start和__heap_end。需要在board.c的rt_hw_board_init()函数中调用rt_system_heap_init()来初始化动态内存堆。void rt_hw_board_init(void) { /* 1. 初始化硬件串口以便后续打印日志 */ rt_hw_uart_init(); /* 2. 打印启动信息 */ rt_kprintf(\n\nRT-Thread for Raspberry Pi 3B (AArch64) starting...\n); /* 3. 初始化系统堆内存。 注意这里传入的地址必须是内存的“虚拟地址”。 在MMU启用前虚拟地址等于物理地址。我们使用链接脚本中定义的符号。*/ extern void *__heap_start; extern void *__heap_end; rt_system_heap_init((void*)__heap_start, (void*)__heap_end); /* 4. 初始化系统定时器Tick */ rt_hw_timer_init(); /* 5. 初始化板载LED可选用于指示系统状态*/ rt_hw_led_init(); /* 6. 初始化调度器启动系统 */ rt_system_scheduler_init(); /* 7. 初始化系统定时器线程 */ rt_system_timer_thread_init(); /* 8. 初始化空闲线程 */ rt_thread_idle_init(); /* 9. 启动调度器永不返回 */ rt_system_scheduler_start(); }5. 构建、配置与镜像生成环境、代码都准备好了现在需要把它们编译成一个树莓派能识别的内核镜像。5.1 配置RT-Thread内核与组件RT-Thread使用menuconfig工具进行配置。在BSP根目录下执行scons --menuconfig会调用Env工具或直接打开配置界面。关键配置包括架构选择在RT-Thread Kernel - Target Architecture中选择aarch64。优化等级Build Opt Level可以先选择-O0便于调试稳定后改为-O2。内核对象名称建议开启调试时方便。控制台设备将RT-Thread Device Drivers - Using console for rt_kprintf的设备名设置为我们的串口设备名例如uart1。Shell组件开启RT-Thread Components - Command shell这样系统启动后就能通过串口输入命令了。需要指定shell使用的设备同样指向uart1。配置完成后保存退出。配置会被保存到rtconfig.h文件中。5.2 使用Scons构建项目在BSP根目录下直接运行scons命令。Scons会根据SConscript和rtconfig.py调用我们之前安装的aarch64-linux-gnu-gcc工具链进行编译。如果一切顺利会生成一个名为rtthread.elf的文件。这就是我们编译出的可执行文件。5.3 生成树莓派可引导镜像 (kernel8.img)树莓派的引导程序start.elf期望的64位内核镜像是一个纯二进制文件并且不带ELF头。我们需要使用工具链中的objcopy来转换。aarch64-linux-gnu-objcopy -O binary rtthread.elf kernel8.img这条命令将ELF格式的rtthread.elf转换为原始的二进制文件kernel8.img。kernel8这个文件名是树莓派固件约定的用于64位内核。5.4 准备SD卡启动文件准备一张TF卡格式化为FAT32格式。需要从树莓派官网下载最新的固件文件bootcode.bin,start.elf,fixup.dat等或者从任何一个可用的Raspberry Pi OS SD卡中复制这些文件。将以下文件放入TF卡的根目录bootcode.binstart.elffixup.datbcm2710-rpi-3-b.dtb(树莓派3B的设备树文件可从Linux内核源码或官方固件中获取)config.txtcmdline.txt(可选RT-Thread可能不需要)kernel8.img(我们刚刚生成的内核镜像)关键的config.txt文件需要正确配置# 启用64位模式 arm_64bit1 # 指定内核文件名 kernelkernel8.img # 禁用GPU侧的内存给ARM更多内存可选 gpu_mem16 # 固定核心频率确保串口波特率稳定 core_freq250 # 启用Mini UART作为主控制台并修复其时钟重要 enable_uart1 # 禁用蓝牙将PL011 UART释放给GPIO14/15如果需要更稳定的串口可选 # dtoverlaydisable-bt6. 上电调试与问题排查将准备好的SD卡插入树莓派3B连接串口线GPIO14-TXD, GPIO15-RXD注意电平是3.3V上电。打开串口终端如PuTTY、minicom、picocom设置波特率115200数据位8停止位1无校验。6.1 常见的启动失败场景与对策无任何输出检查电源确保使用5V/2.5A以上的电源板子上的红色LED常亮。检查串口连接TX/RX是否接反地线是否连接串口终端参数是否正确检查config.txtenable_uart1是否设置kernelkernel8.img是否正确检查镜像文件kernel8.img是否成功拷贝到SD卡根目录可以尝试用一个已知可用的64位Linux内核如kernel8.img从官方系统复制测试串口和启动流程。输出乱码波特率不匹配这是最常见的原因。确认config.txt中设置了core_freq250并且代码中计算波特率寄存器的公式正确。可以尝试将波特率降低到9600测试。时钟源错误Mini UART的时钟源是核心时钟(core_freq)而核心时钟默认是变动的。core_freq250将其锁定在250MHz。输出部分字符串后停止或重启内存布局错误检查链接脚本中的加载地址(0x80000)是否正确。检查堆栈地址是否与GPU保留内存或其他区域冲突。可以尝试将链接地址和内存起始地址调大一些如0x200000。未处理的中断如果系统定时器中断或其它中断被触发但没有正确的中断服务程序(ISR)CPU可能进入异常状态导致复位。在初期可以在中断向量表中所有条目都放置一个简单的死循环或错误处理函数看看系统是否在触发中断后跑飞。C运行时环境未正确建立确保BSS段清零、栈指针设置正确。可以在rtthread_startup()的最开始加一个打印如果能执行到这里说明启动汇编部分基本正常。链接错误或编译错误工具链不匹配确保所有代码包括库都是用aarch64-linux-gnu-工具链编译的。汇编语法AArch64的GNU汇编语法与ARMv7有区别注意指令后缀和寄存器命名x0-x30, w0-w30。6.2 成功启动的标志如果一切顺利你将在串口终端看到类似以下的输出heap: 0x20080000 - 0x24080000 \ | / - RT - Thread Operating System / | \ 4.1.0 build May 10 2024 2006 - 2022 Copyright by rt-thread team Hello, Raspberry Pi 3B! msh /看到RT-Thread的Logo和版本信息以及命令行提示符msh /恭喜你最艰难的一步已经完成了你现在拥有了一个运行在树莓派3B上的64位RT-Thread最小系统。7. 功能完善与后续探索让内核跑起来只是第一步接下来可以丰富这个系统完善驱动GPIO驱动实现标准的PIN设备驱动可以控制LED、读取按键。SD卡驱动与文件系统实现SDHCI驱动并挂载FAT32或LittleFS文件系统这是运行复杂应用的基础。网络驱动树莓派3B的有线网卡是SMSC LAN9514实现其USB和MAC层驱动并集成LwIP协议栈让开发板具备网络功能。图形显示虽然RT-Thread本身不强调图形但可以移植LVGL等轻量级图形库到FrameBuffer上实现简单的UI。启用MMU与缓存初期为了简化我们禁用了MMU和缓存。要获得最佳性能必须正确配置MMU页表使能缓存。这需要建立页表将物理地址映射到虚拟地址通常采用恒等映射并正确设置SCTLR_EL1寄存器。优化系统性能Tickless模式当系统空闲时让CPU进入低功耗的WFIWait For Interrupt状态甚至通过配置定时器唤醒实现Tickless进一步降低功耗。中断优化完善GIC驱动支持中断嵌套、优先级管理。集成软件包RT-Thread强大的软件包生态系统通过pkgs --update命令访问可以让你轻松添加MQTT、WebSocket、TLS、数据库等各种功能模块快速构建物联网终端。移植的过程就是一个不断遇到问题、分析数据手册、查阅ARM架构参考手册、调试代码的过程。每一次问题的解决都会让你对硬件、对操作系统、对ARM64架构的理解更深一层。当最终看到那个熟悉的RT-Thread Logo出现在自己移植的板子上时那种成就感是无与伦比的。这个64位的RT-Thread系统可以作为一个高性能、高实时的嵌入式应用基础去驱动更复杂的项目。
返回列表