rust-raspberrypi-OS-tutorials链式加载器:UART传输内核实现
rust-raspberrypi-OS-tutorials链式加载器:UART传输内核实现
为什么需要链式加载器?
每次开发都需要将内核镜像写入SD卡是一件繁琐的事情。链式加载器(Chainloader)作为最后一个需要烧录到SD卡的程序,能够通过UART接口接收新的内核镜像并执行,大幅提升开发效率。本文将详细介绍如何通过UART实现Raspberry Pi内核的链式加载功能。
链式加载器工作原理
链式加载器的核心实现分为两个关键阶段:自重定位和内核接收执行。
1. 自重定位过程
链式加载器启动时首先需要将自身从固件加载地址(0x80000)重定位到更高的内存区域(0x2000000),为后续接收新内核腾出空间。这一过程通过位置无关代码(Position Independent Code)实现:
// src/_arch/aarch64/cpu/boot.s
.L_relocate_binary:
ADR_REL x0, __binary_nonzero_start // 加载地址
ADR_ABS x1, __binary_nonzero_start // 链接地址
ADR_ABS x2, __binary_nonzero_end_exclusive
.L_copy_loop:
ldr x3, [x0], #8
str x3, [x1], #8
cmp x1, x2
b.lo .L_copy_loop
链接脚本src/bsp/raspberrypi/kernel.ld中定义了重定位相关的符号:
/* 设置链接地址为32MB */
. = 0x2000000;
__binary_nonzero_start = .;
.text : { *(.text*) }
.data : { *(.data*) }
. = ALIGN(8);
__binary_nonzero_end_exclusive = .;
2. UART传输协议实现
重定位完成后,加载器通过UART与主机建立通信,接收新内核镜像:
// src/main.rs
const MINILOAD_LOGO: &str = r#"
__ __ _ _ _ _
| \/ (_)_ _ (_) | ___ __ _ __| |
| |\/| | | ' \| | |__/ _ \/ _` / _` |
|_| |_|_|_||_|_|____\___/\__,_\__,_|
"#;
fn kernel_main() -> ! {
println!("{}", MINILOAD_LOGO);
println!("{:^37}", bsp::board_name());
println!("[ML] Requesting binary");
// 发送启动信号
for _ in 0..3 {
console().write_char(3 as char); // ASCII ETX字符
}
// 读取内核大小(4字节)
let mut size: u32 = u32::from(console().read_char() as u8);
size |= u32::from(console().read_char() as u8) << 8;
size |= u32::from(console().read_char() as u8) << 16;
size |= u32::from(console().read_char() as u8) << 24;
// 确认接收
console().write_char('O');
console().write_char('K');
// 接收内核数据
let kernel_addr: *mut u8 = bsp::memory::board_default_load_addr() as *mut u8;
unsafe {
for i in 0..size {
core::ptr::write_volatile(kernel_addr.offset(i as isize), console().read_char() as u8)
}
}
// 跳转到新内核
let kernel: fn() -> ! = unsafe { core::mem::transmute(kernel_addr) };
kernel()
}
硬件连接与测试
1. 串口连接
使用USB转TTL适配器连接Raspberry Pi的UART接口:
2. 编译与烧录
# 编译链式加载器
cd 06_uart_chainloader
make BSP=rpi3
# 将生成的kernel8.img烧录到SD卡
3. 加载内核镜像
通过minipush工具发送内核镜像:
# 发送演示内核
make chainboot
成功执行后将显示:
__ __ _ _ _ _
| \/ (_)_ _ (_) | ___ __ _ __| |
| |\/| | | ' \| | |__/ _ \/ _` / _` |
|_| |_|_|_||_|_|____\___/\__,_\__,_|
Raspberry Pi 3
[ML] Requesting binary
[MP] ⏩ Pushing 7 KiB ==========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now
关键代码模块解析
1. UART驱动
UART驱动src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs实现了基础的字符读写功能:
fn read_char(&mut self, blocking_mode: BlockingMode) -> Option<char> {
// 检查接收缓冲区状态
if self.registers.FR.matches_all(FR::RXFE::SET) {
if let BlockingMode::NonBlocking = blocking_mode {
return None;
}
while self.registers.FR.matches_all(FR::RXFE::SET) {}
}
// 读取字符
let ret = self.registers.DR.get() as u8 as char;
self.chars_read += 1;
Some(ret)
}
2. 内存地址定义
src/bsp/raspberrypi/memory.rs定义了Raspberry Pi的内存布局:
/// 固件默认加载地址
pub const BOARD_DEFAULT_LOAD_ADDRESS: usize = 0x8_0000;
/// 获取默认加载地址
#[inline(always)]
pub fn board_default_load_addr() -> *const u64 {
BOARD_DEFAULT_LOAD_ADDRESS as _
}
调试与扩展
1. QEMU调试
使用QEMU模拟链式加载过程:
# 带汇编输出的QEMU模拟
make qemuasm
将显示重定位过程的指令执行序列:
0x00080030: 58000140 ldr x0, #0x80058
0x00080034: 9100001f mov sp, x0
0x00080038: 58000141 ldr x1, #0x80060
0x0008003c: d61f0020 br x1
...
0x02000070: 9400044c bl #0x20011a0 ; 重定位后的代码
2. 自动化测试
测试脚本tests/chainboot_test.rb验证了链式加载功能:
# 测试预期输出
EXPECTED_PRINT = 'Echoing input now'
# 启动QEMU并验证加载过程
def run
setup
@console_subtests.prepend(PowerTargetRequestTest.new(@qemu_cmd, pty_main))
execute_subtests
finish
end
总结与展望
链式加载器通过自重定位和UART通信,解决了嵌入式开发中频繁烧录SD卡的痛点。这一机制为后续教程中的内核开发提供了高效的测试环境。下一阶段可以扩展实现:
- 内核镜像校验功能
- 传输进度指示
- 多内核版本管理
完整实现可参考06_uart_chainloader目录下的源代码,官方文档请参见README.md。
点赞收藏本文,关注后续Rust嵌入式开发教程!
更多推荐

所有评论(0)