公司动态

树莓派2移植RT-Thread实战:从零构建BSP与驱动开发

📅 2026/8/19 9:56:40
树莓派2移植RT-Thread实战:从零构建BSP与驱动开发
1. 项目缘起为什么要在树莓派2上折腾RT-Thread如果你手头恰好有一块吃灰多年的树莓派2又对实时操作系统RTOS感兴趣那么把RT-Thread移植上去绝对是一个能让你深入理解硬件、操作系统和驱动开发的绝佳练手项目。这不仅仅是让老硬件焕发新生更是一次从零开始亲手构建一个可运行、可调试的嵌入式系统的完整旅程。树莓派2作为一款经典的ARM Cortex-A7多核开发板其硬件生态成熟资料丰富但通常运行的是Linux这类复杂的通用操作系统。而RT-Thread作为一个开源、小巧、高度可裁剪的实时操作系统其设计哲学与Linux截然不同。将RT-Thread移植到树莓派2上意味着我们要抛开庞大的Linux内核和驱动框架直面最底层的硬件从CPU上电后的第一条指令开始设置异常向量表、初始化内存管理单元MMU、配置系统时钟Timer、处理中断控制器GIC再到点亮一颗LED、驱动串口UART进行调试输出。这个过程会让你对“操作系统究竟是如何在硬件上跑起来的”有一个前所未有的清晰认知。对于嵌入式开发者而言掌握RTOS的移植是进阶的必经之路。它考验的不仅仅是对C语言的熟练度更是对计算机体系结构、ARM汇编、硬件手册阅读能力的综合挑战。成功在树莓派2上运行RT-Thread后你获得的将不仅仅是一个能跑RTOS的开发板更是一套可复用的、针对Cortex-A系列处理器的BSP板级支持包开发方法论。无论是后续为其他外设编写驱动还是将这套BSP适配到其他类似架构的芯片上你都会感到得心应手。2. 移植前的核心准备工具链、源码与硬件认知动手之前充分的准备是成功的一半。这个阶段的目标是搭建好开发环境并深入理解我们即将面对的“对手”——树莓派2的硬件和RT-Thread的源码结构。2.1 开发环境搭建与工具链选型首先需要一个Linux开发环境。Windows用户可以通过WSL2Windows Subsystem for Linux安装Ubuntu等发行版或者使用虚拟机。macOS用户则可以直接使用终端。我强烈推荐在Linux原生环境下进行可以避免很多因环境差异导致的诡异问题。核心工具是交叉编译工具链。因为我们的宿主机比如x86_64的PC需要编译出能在ARM架构的树莓派2上运行的代码。这里我们选择arm-none-eabi-gcc。这个工具链不包含操作系统库专门用于裸机或RTOS开发非常轻量。在Ubuntu/Debian上安装命令很简单sudo apt update sudo apt install gcc-arm-none-eabi安装完成后可以通过arm-none-eabi-gcc -v来验证版本。我使用的是10.3.1版本比较稳定。接下来是获取RT-Thread源码。前往RT-Thread的GitHub仓库https://github.com/RT-Thread/rt-thread克隆最新代码或者下载稳定版本如v4.1.x。建议使用git clone方便后续同步更新和查阅提交历史。git clone --recursive https://github.com/RT-Thread/rt-thread.git cd rt-thread--recursive参数很重要它会同时拉取子模块比如一些内置的软件包。2.2 深入剖析树莓派2的硬件启动流程这是移植中最关键、也最容易让人困惑的部分。树莓派2的启动过程与常见的MCU如STM32有很大不同。上电与BootROM树莓派2的SoCBCM2836内部有一块固化的BootROM。上电后CPU的ARM核处于复位状态由VideoCore GPU一个博通的多媒体协处理器首先运行BootROM代码。BootROM会依次从SD卡或eMMC的特定分区寻找启动文件。它首先寻找bootcode.bin第二阶段引导程序然后是start.elfGPU固件最后才是内核镜像kernel7.img对于Cortex-A7内核。我们的RT-Thread最终就要被命名为kernel7.img并放在SD卡根目录。ARM核的接管start.elf会初始化一些基础硬件如SDRAM控制器然后将kernel7.img加载到内存地址0x8000处并释放ARM核的复位。此时ARM核开始执行我们镜像文件的第一条指令。请注意CPU此时处于ARM模式MMU关闭仅有部分核心寄存器可用运行在特权模式通常是SVC模式。我们的移植代码就要从这里开始接手。内存映射树莓派2的物理内存是1GB。但CPU访问外设如GPIO、UART、Timer是通过“内存映射I/O”MMIO的方式。这些外设的寄存器被映射到以0x3F000000开始的高端地址空间具体基址可能因型号略有差异需查阅BCM2836手册。在初始化MMU之前我们需要通过物理地址直接访问这些寄存器来控制硬件。理解这个流程就知道我们的启动代码通常是startup.c和相关的汇编文件需要做什么设置栈指针、初始化BSS段清零未初始化的全局变量、拷贝数据段然后跳转到RT-Thread的入口函数rtthread_startup。而在这之前或之后我们必须尽快初始化最基础的调试输出——串口UART否则系统将运行在“黑暗”中任何错误都难以排查。2.3 初识RT-Thread的BSP骨架RT-Thread的BSP板级支持包有相对固定的目录结构。在rt-thread/bsp目录下我们可以看到很多其他芯片或开发板的BSP。我们的目标就是在bsp下创建一个raspi2或类似名称的目录。一个最简化的BSP通常包含以下关键部分rtconfig.py项目的灵魂配置文件。它通过SCons构建系统定义了本BSP使用的CPU架构、编译工具链、编译参数、需要包含的组件如FinSH控制台、设备驱动框架、网络协议栈等。这是我们移植过程中需要反复修改和调整的文件。board目录板级硬件相关代码的聚集地。这里存放着链接脚本linker_scripts目录下的.lds文件、启动文件startup.c和可能的.S汇编文件、时钟配置drv_clk.c、引脚配置、以及最关键的board.c和board.h。board.c中的rt_hw_board_init函数是硬件初始化的总入口系统启动后会最早调用它。drivers目录设备驱动存放处。我们需要在这里实现树莓派2的各类外设驱动例如drv_uart.c串口、drv_gpio.c通用输入输出、drv_spi.c等。RT-Thread提供了完善的设备驱动框架rtdevice.h我们的驱动需要按照这个框架进行封装注册为rt_device从而可以被上层应用统一访问。applications目录 用户应用程序的起点通常包含一个main.c里面定义了用户线程入口。libraries目录 可能需要放置树莓派2相关的底层库或头文件比如Broadcom提供的bcm2836.h或bcm2837.h其中定义了外设寄存器的结构体映射但更常见的做法是直接在自己的驱动代码中定义这些寄存器地址。在开始编码前我建议先仔细阅读一个成熟的BSP比如stm32系列下的某个型号理解各个文件的作用和调用关系。这能帮你建立起清晰的代码组织思路。3. 从零构建BSP启动、内存与最小系统现在我们进入实战阶段从创建一个空的BSP目录开始。3.1 创建BSP目录与基础工程配置首先在rt-thread/bsp目录下创建raspi2文件夹并复制一份相对简单的BSP作为模板。我选择复制qemu-vexpress-a9因为它同样是Cortex-A内核且结构清晰。cd rt-thread/bsp cp -r qemu-vexpress-a9 raspi2 cd raspi2然后我们需要大刀阔斧地修改这个模板使其适应树莓派2。第一步修改rtconfig.py。这是构建的指挥中枢。打开它你需要重点关注和修改以下部分from building import * # 1. 指定交叉编译工具链前缀 CROSS_TOOL gcc if CROSS_TOOL gcc: PLATFORM gcc EXEC_PATH /usr/bin # 你的 arm-none-eabi-gcc 所在路径如果已在PATH中可留空或注释 # 关键指定工具链前缀 TARGET arm-none-eabi- # 2. 定义设备类型和CPU DEVICE -marcharmv7-a -mtunecortex-a7 -mfpuneon-vfpv4 -mfloat-abihard ARCH arm CPU cortex-a7 # 3. 编译标志 CFLAGS DEVICE -Wall AFLAGS -c DEVICE -x assembler-with-cpp LFLAGS DEVICE -nostartfiles -Wl,--gc-sections,-Maprtthread.map,-cref,-u,system_vectors -T board/linker_scripts/link.lds # 4. 指定链接后处理工具用于生成kernel7.img所需的纯二进制文件 POST_ACTION OBJCPY -O binary $TARGET rtthread.bin\n POST_ACTION SIZE $TARGET \n # 树莓派需要的是kernel7.img本质上就是一个二进制文件我们可以直接重命名 POST_ACTION cp rtthread.bin kernel7.img\nPOST_ACTION部分很重要它会在编译链接完成后使用objcopy工具将ELF格式的可执行文件转换成纯二进制镜像并复制为我们需要的kernel7.img。第二步准备或编写链接脚本。链接脚本.lds告诉链接器如何把代码、数据等段放到内存的哪个位置。树莓派2的加载地址是0x8000。我们需要在board/linker_scripts下创建或修改link.lds。一个简化的链接脚本核心内容如下ENTRY(reset_handler) /* 指定入口点为复位处理函数 */ MEMORY { RAM (rwx) : ORIGIN 0x8000, LENGTH 1024M /* 从0x8000开始长度1GB */ } SECTIONS { . 0x8000; /* 定位计数器从0x8000开始 */ .text : { KEEP(*(.vectors)) /* 异常向量表必须放在最前面 */ *(.text*) /* 所有代码段 */ *(.rodata*) /* 只读数据段 */ } RAM .data : { /* 已初始化数据段 */ *(.data*) } RAM .bss : { /* 未初始化数据段启动代码需要清零 */ *(.bss*) *(COMMON) } RAM /* 其他段... */ . ALIGN(4); _end .; /* 用于计算BSS段结束 */ }这个脚本定义了程序从0x8000开始存放并规定了代码、数据、BSS段的布局。ENTRY(reset_handler)指定了程序入口这个reset_handler就是我们启动汇编代码中的标签。3.2 编写启动汇编与板级初始化启动代码是系统上电后运行的第一段代码通常用汇编或内联汇编编写因为它需要执行一些C语言环境尚未建立时的低级操作。在board目录下我们需要一个启动文件比如startup.c它可能包含汇编和C的混合。核心任务包括设置异常向量表ARM Cortex-A7在复位后异常向量表Exception Vector Table必须放置在内存的特定地址默认是0x0或通过VBAR寄存器设置的高地址。由于我们的镜像被加载到0x8000我们需要将向量表放在这里。向量表里是一系列跳转指令分别对应复位、未定义指令、软中断、预取指中止、数据中止、IRQ、FIQ等异常。复位向量就指向我们的reset_handler。复位处理函数reset_handler这是真正的起点。它需要设置各模式下的栈指针SP至少设置SVC和IRQ模式的栈。关闭中断IRQ和FIQ。初始化BSS段清零。BSS段的起止地址通常在链接脚本中通过符号导出如_bss_start,_bss_end。复制数据段DATA段从只读存储器如镜像中的初始值到RAM中。DATA段的源地址和目标地址也由链接脚本定义。可选初始化MMU和缓存。对于初期调试为了简化我们可以先不开启MMU直接使用物理地址访问内存和外设。跳转到C语言入口函数通常是rt_hw_board_init或直接到rtthread_startup。一个极度简化的reset_handler伪代码逻辑如下实际需要完整的汇编实现void reset_handler(void) { /* 1. 设置栈指针 */ set_sp_svc_mode(/* 栈顶地址 */); set_sp_irq_mode(/* 栈顶地址 */); /* 2. 关闭中断 */ disable_interrupts(); /* 3. 清零BSS段 */ extern unsigned int _bss_start, _bss_end; unsigned int *dst _bss_start; while (dst _bss_end) { *dst 0; } /* 4. 拷贝数据段 */ extern unsigned int _data_loadaddr, _data_start, _data_end; unsigned int *src _data_loadaddr; dst _data_start; while (dst _data_end) { *dst *src; } /* 5. 跳转到C世界 */ rt_hw_board_init(); }rt_hw_board_init()函数这个函数在board.c中实现是C语言环境下的第一个硬件初始化函数。它的首要任务必须在任何其他复杂操作之前完成的就是初始化串口UART。因为没有串口我们无法看到任何打印信息调试将如同盲人摸象。void rt_hw_board_init() { /* 1. 初始化硬件定时器为操作系统时钟滴答提供来源 */ rt_hw_timer_init(); // 配置ARM Core的私有定时器ARM Peripheral Timer或系统定时器 /* 2. 初始化系统堆 */ rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END); // HEAP_BEGIN/END需要在链接脚本或board.h中定义 /* 3. 初始化板载UART设备并注册为控制台 */ rt_hw_uart_init(); // 实现UART引脚、波特率等初始化 rt_console_set_device(RT_CONSOLE_DEVICE_NAME); // 例如 uart0 /* 4. 打印RT-Thread版本信息 */ rt_show_version(); /* 5. 初始化其他组件调度器、定时器线程等 */ rt_thread_idle_init(); rt_system_timer_init(); rt_system_scheduler_init(); /* 6. 初始化应用程序线程 */ rt_application_init(); /* 7. 启动调度器系统开始运行 */ rt_system_scheduler_start(); }注意rt_hw_timer_init()和rt_hw_uart_init()需要我们自己根据树莓派2的硬件手册来实现。这是移植初期最硬核的两个驱动。4. 驱动实现攻坚定时器、串口与GPIO系统能跑起来依赖于两个最基础的驱动系统时钟滴答SysTick和调试输出UART。我们逐一攻破。4.1 系统定时器Timer驱动操作系统的心跳RT-Thread作为实时操作系统需要一个稳定的时基来驱动任务调度、软件定时器等。这个时基通常由硬件定时器中断提供。树莓派2的Cortex-A7核心有一个ARM Generic Timer这是一个符合ARM架构规范的私有定时器精度高最适合用作系统时钟源。我们需要在drivers目录下创建drv_timer.c。核心任务是配置这个定时器使其以固定的频率例如1000Hz即1ms中断一次产生中断并在中断服务程序ISR中调用RT-Thread的时钟滴答函数rt_tick_increase()。关键步骤获取定时器基址和频率ARM Generic Timer的寄存器是CP15协处理器的一部分需要通过MRC/MCR汇编指令访问。但更简单的方法是树莓派的系统定时器System Timer一个GPU管理的定时器也提供了一个全局的64位计数器我们可以用它作为时钟源。然而ARM Generic Timer是更标准的选择。为了简化许多移植选择使用ARM Core的私有定时器ARM Peripheral Timer但其基址需要从设备树或固定地址获取。这里我们以系统定时器为例因为它地址固定0x3F003000且容易操作。配置比较寄存器系统定时器有一个计数器CLO不断自增。我们可以读取当前计数值加上一个间隔值比如系统时钟频率/1000写入比较寄存器C1。当计数器达到这个比较值时就会触发中断。编写中断服务程序在ISR中首先清除中断标志向CS寄存器的对应位写1然后调用rt_tick_increase()。最后重新设置下一次的比较值当前计数值间隔以实现周期性中断。一个极其重要的坑中断控制器的配置。树莓派2使用ARM的GIC-400中断控制器。系统定时器的中断号是1这是GPU端的IRQ ID对应ARM的IRQ ID需要查表通常是1 64这里容易出错。实际上树莓派的系统定时器中断是通过GPU路由的其ARM IRQ号是1在IRQ basic pending寄存器中。你需要仔细阅读BCM2836 ARM-local peripherals手册正确配置GIC和GPU中断使能才能让定时器中断送达ARM核心。这一步配置错误系统将没有任何时钟滴答调度器无法工作表现为系统“卡死”在启动阶段。我的经验是先实现一个简单的忙等待延时函数确保串口能工作然后再集中精力攻克定时器中断。可以暂时注释掉调度器启动让程序只运行一个简单的打印循环验证基础驱动。4.2 串口UART驱动调试的生命线串口驱动是我们的“眼睛”。树莓派2的UART0PL011 UART引脚是GPIO14TX和GPIO15RX。我们需要在drivers目录下创建drv_uart.c。实现要点引脚复用树莓派的GPIO引脚有多种功能Alt0-Alt5。需要将GPIO14和15设置为Alt0功能以启用UART。#define GPFSEL1 (*(volatile unsigned int*)(GPIO_BASE 0x04)) // GPIO14是FSEL1的[14:12]位GPIO15是[17:15]位设置为ALT0 (0b100) GPFSEL1 (GPFSEL1 ~(7 12)) | (4 12); // GPIO14 ALT0 GPFSEL1 (GPFSEL1 ~(7 15)) | (4 15); // GPIO15 ALT0配置UART参数访问UART0的寄存器基址0x3F201000。需要关闭UART清除CR寄存器设置波特率IBRD和FBRD分频寄存器计算依赖于核心时钟频率clock_freq通常是250MHz数据位8、停止位1、无校验。然后重新使能UART和发送/接收功能。// 波特率计算BAUDDIV (clock_freq / (16 * baud_rate)) // IBRD BAUDDIV的整数部分 FBRD round(64 * 小数部分) unsigned int baud_divisor clock_freq / (16 * 115200); UART0_IBRD baud_divisor; UART0_FBRD ((clock_freq * 4 / (115200 * 64)) 2) / 4; // 一种近似计算实现rt_device接口RT-Thread的设备驱动需要实现一个rt_device结构体并填充open,close,read,write,control等操作函数。对于控制台UART主要实现write发送字符和read接收字符可阻塞或非阻塞。write函数循环检查FR寄存器的TXFF位发送FIFO满如果不满就将字符写入DR寄存器。read函数检查FR寄存器的RXFE位接收FIFO空如果不空就从DR寄存器读取字符。注册设备在驱动初始化函数中调用rt_hw_uart_init()里面完成硬件初始化和设备注册rt_device_register(uart_device, uart0, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_STREAM)。踩坑实录最让人头疼的问题是“没有输出”。除了检查引脚复用、波特率计算一定要确认start.elf是否正确加载了你的内核并且没有启用miniUART覆盖了PL011。在config.txt中加入enable_uart1和core_freq250可以确保PL011 UART使用正确的时钟源。另外早期调试可以用一个最简单的汇编循环点亮GPIO连接的LED来验证代码是否真的在运行排除串口驱动本身的问题。4.3 GPIO驱动点亮第一盏灯有了串口我们可以进行更直观的调试。实现一个简单的GPIO驱动来点亮树莓派2板载的ACT LED在树莓派2上通常是GPIO47不对树莓派2的ACT LED连接在GPIO16上但需要通过config.txt配置。最稳妥的方式是外接一个LED到某个GPIO比如GPIO17。GPIO驱动相对简单功能选择通过GPFSEL寄存器将对应引脚设置为输出模式001。输出控制通过GPSET寄存器置位输出高电平点亮LED通过GPCLR寄存器清零输出低电平熄灭LED。我们可以将这个GPIO操作封装成RT-Thread的PIN设备驱动这样就可以使用RT-Thread的PIN设备APIrt_pin_mode(),rt_pin_write()来控制它方便后续应用程序使用。5. 系统集成、构建与调试实战当基础驱动就绪后我们需要将它们集成到BSP中并完成最终的构建和调试。5.1 配置RT-Thread内核与组件回到rtconfig.h或通过menuconfig工具生成我们需要裁剪和配置RT-Thread内核。初期为了最小化系统可以关闭所有不必要的组件只保留核心调度器、线程管理、定时器、信号量等基本功能以及FinSH控制台这样我们就可以通过串口输入命令了。使用scons --menuconfig命令可以调出图形化配置界面。确保以下配置正确RT_USING_CONSOLE开启并正确设置了控制台设备名如uart0。RT_USING_DEVICE开启。RT_USING_PIN开启如果你实现了PIN驱动。RT_TICK_PER_SECOND设置为1000对应1ms的定时器中断频率。内存堆的大小RT_HEAP_SIZE要根据树莓派2的可用内存合理设置初期可以设置一个较小的值如8MB。5.2 构建、生成镜像与上电测试在BSP根目录raspi2下执行scons命令开始编译。如果一切配置正确你会看到编译过程最后生成rtthread.elf、rtthread.bin和kernel7.img。制作可启动的SD卡将一张SD卡格式化为FAT32格式。将树莓派官方的启动文件bootcode.bin,start.elf,fixup.dat复制到SD卡根目录。这些文件可以从官方GitHubraspberrypi/firmware的boot目录下获取。将我们编译生成的kernel7.img复制到SD卡根目录覆盖可能已存在的同名文件。可选创建或修改config.txt文件添加enable_uart1、core_freq250、arm_64bit0确保32位模式等配置。将SD卡插入树莓派2连接串口调试器USB to TTL到GPIO14/15上电。串口调试波特率设置为115200。如果看到RT-Thread的启动Logo和版本信息以及FinSH的命令提示符msh 那么恭喜你移植成功了你可以尝试输入list_thread查看当前线程free查看内存使用情况或者运行你写在main.c里的测试程序比如闪烁LED。5.3 常见问题与调试技巧无任何输出检查硬件连接TX接RXRX接TXGND接GND确认串口调试器本身工作正常。检查启动文件确认kernel7.img是最新编译的并且SD卡上的start.elf等文件是兼容树莓派2的版本。简化代码注释掉所有复杂初始化在启动汇编的最开始写一段汇编代码让某个GPIO引脚周期性高低变化用示波器或LED观察确认CPU确实在执行你的代码。检查UART时钟确认config.txt中core_freq设置正确并且UART波特率分频计算无误。输出乱码几乎是波特率不匹配的典型症状。仔细核对核心时钟频率和分频计算。可以尝试其他标准波特率如9600测试。系统在rt_system_scheduler_start()后卡死最大的嫌疑是定时器中断没有正确配置或中断服务程序ISR没有正确清除中断标志导致中断不断触发或者根本没有触发导致调度器没有时钟滴答。在定时器ISR入口点加一个GPIO翻转操作用示波器看是否有周期性脉冲可以判断中断是否发生。检查GIC和GPU中断的使能位、中断号配置。查阅BCM2836 ARM-local peripherals手册的Interrupt章节。内存分配失败检查链接脚本中堆heap区域的设置HEAP_BEGIN和HEAP_END是否在有效的RAM范围内并且没有和其他段重叠。在rt_hw_board_init中打印出堆的起始和结束地址确认其合理性。移植是一个反复试错的过程。最有效的方法是增量开发和充分利用硬件调试手段。先让串口打印一个“Hello”再初始化定时器并让一个GPIO在定时器中断里翻转最后再开启调度器加载多线程。每完成一步就固化一个可工作的版本这样当问题出现时你就能快速定位到最近的一次修改。