裸机编程基础:无标准库环境配置

说实话,很多嵌入式新手一上来就问我:「为什么我的Rust程序在STM32上跑不起来?」

答案往往很简单——你还在用标准库。我刚开始做裸机开发时也踩过这个坑。Rust的标准库依赖操作系统,而嵌入式环境里,根本没有操作系统这回事。

裸机编程,说白了就是「光着膀子干活」。没有OS给你分配内存,没有文件系统,甚至连基本的println!都用不了。但正是这种「什么都没有」的环境,给了我们最大的控制权。

核心库 vs 标准库

这两个概念,我当年花了整整一周才彻底搞明白。简单来说:

  • 标准库(std):依赖操作系统,提供文件、网络、线程等功能
  • 核心库(core):与平台无关,只提供语言本身的基础功能

在裸机环境下,我们只能用core库。它包含了基本类型、迭代器、闭包等,但没有堆分配、没有I/O。

关键区别一览:

功能 std core
Vec、String
Box、Rc
基本类型
迭代器、闭包
panic处理 需自定义

你想想看,没有Vec和String,我们怎么管理数据?嗯,这就是接下来要解决的问题。

全局分配器:自己动手管内存

在标准库中,全局分配器由操作系统提供。但在裸机上,你得自己写一个。

我曾经在一个IoT项目中,因为全局分配器写得不够好,导致设备运行三天后突然死机。排查了整整两天,才发现是内存碎片问题。

一个最简单的全局分配器实现:

// 一个极简的 bump allocator
use core::alloc::{GlobalAlloc, Layout};

struct BumpAllocator {
    heap_start: usize,
    heap_end: usize,
    next: usize,
}

unsafe impl GlobalAlloc for BumpAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let align = layout.align();
        let size = layout.size();
        
        // 对齐当前指针
        let current = (self.next + align - 1) & !(align - 1);
        
        if current + size > self.heap_end {
            return core::ptr::null_mut(); // 内存不足
        }
        
        self.next = current + size;
        current as *mut u8
    }
    
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
        // bump allocator 不支持释放单个对象
        // 实际项目中需要更复杂的实现
    }
}

⚠️ 注意:上面的分配器不支持释放内存。真实项目中,我建议使用linked_list_allocator或buddy_system_allocator这类现成的crate。自己手写分配器,坑太多了。

panic处理:当程序崩溃时

标准库的panic会打印错误信息然后退出。但在裸机上,没有标准输出,也没有退出这个概念。

你需要自己定义panic行为。我个人习惯的做法是:

use core::panic::PanicInfo;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // 在嵌入式环境中,通常点亮一个LED或触发看门狗
    // 或者直接进入死循环
    loop {
        // 可以在这里添加调试信息输出
        // 比如通过串口打印 panic 信息
    }
}

我记得有一次调试一个电机控制程序,panic后设备直接卡死,连调试信息都看不到。后来我加了一个简单的串口输出,才定位到问题——一个数组越界访问。

💡 小技巧:在开发阶段,可以让panic触发一个断点指令(如ARM的BKPT),这样调试器能自动停下来。发布版本再改成死循环。

启动文件与链接脚本

这部分,嗯,可以说是裸机编程中最容易出问题的地方。我见过太多人在这里卡住。

启动文件负责:

  • 设置栈指针
  • 初始化.data段(全局变量)
  • 清零.bss段(未初始化全局变量)
  • 调用main函数

链接脚本则告诉链接器:

  • 代码放在哪里(Flash地址)
  • 数据放在哪里(RAM地址)
  • 各个段的大小和位置

一个典型的链接脚本片段:

MEMORY
{
    FLASH : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text : {
        *(.vector_table)
        *(.text*)
    } > FLASH
    
    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT > FLASH
    
    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM
}

这里有个坑:.data段需要从Flash复制到RAM。启动文件里必须实现这个复制逻辑。我曾经因为忘记处理这个,导致全局变量全是默认值,排查了整整一个下午。

知识体系总览

下面这张图,是我做裸机开发时总结的核心逻辑:

裸机编程核心知识体系 裸机Rust应用 #![no_std] core库 全局分配器 panic处理 无操作系统依赖 直接硬件访问 基本类型、迭代器 无堆分配、无I/O Bump / 链表分配器 内存碎片管理 自定义panic行为 调试信息输出 启动文件 + 链接脚本 栈初始化、段复制 内存布局定义 中断向量表

这张图把整个裸机编程的脉络理清楚了。从最顶层的应用,到四个核心组件,再到底层的启动和链接。每个环节都环环相扣。

说实话,裸机编程就像搭积木。标准库帮你搭好了所有积木,你只需要拼装。而裸机编程,你得从烧制砖块开始。但正是这种「从零开始」的过程,让你对系统有了最深刻的理解。

我在做第一个裸机项目时,光是链接脚本就改了几十次。每次编译失败,都意味着我对硬件理解又加深了一层。所以,别怕出错,出错才是学习最快的方式。

专注资料整理