裸机和嵌入式 Linux 的本质区别
前面聊的 STM32、FreeRTOS 都是“裸机或 RTOS”路线:你的程序直接跑在 CPU 上,自己管所有资源。嵌入式 Linux 完全换了个思路:Linux 内核先启动,它来管 CPU、内存、外设,你的程序只是内核之上的一个普通进程。
裸机是“你直接开一辆车”,嵌入式 Linux 是“你打网约车”——车由司机(内核)开,你只要说去哪(调用系统调用)。代价是多了一层开销,好处是操作系统替你把难的事都做了。
什么时候该上 Linux?需要跑网络协议栈、数据库、多进程 GUI、或者硬件资源够(几百 MB 内存以上)的时候,Linux 就是正确选择。如果是电池供电、KB 级内存、微秒级实时响应的传感器节点,裸机更合适。
交叉编译:在 PC 上编译 ARM 程序
开发板性能弱,不会在板子上跑编译器。常规做法是在 x86 的开发机上编译出 ARM 架构的可执行文件,再拷到板子上跑——这就是交叉编译。工具链里 gcc 前面会带个前缀,比如 arm-linux-gnueabihf-gcc。
# 用交叉编译器编译一个 hello world
arm-linux-gnueabihf-gcc -o hello hello.c
# 拷到开发板运行
scp hello root@192.168.1.100:/root/
ssh root@192.168.1.100
# 在板子上
./hello
Hello, Embedded Linux!
- arm-linux-gnueabihf-gcc: not found工具链没装或没加到 PATH。
- 报错找不到 libxxx.so依赖的库没交叉编译,要把库也一起编进去。
- 板子上报 Exec format error架构对不上,编译的不是板子用的 ARM 版本。
- Yocto 方案生产环境用 Yocto/Buildroot 统一构建整个根文件系统,省心但学习曲线陡。
Bootloader、内核与根文件系统
一块嵌入式 Linux 板子上电后,三个东西按顺序跑:Bootloader(U-Boot)先初始化硬件、加载内核;内核解压启动、挂载根文件系统;根文件系统上跑 init,拉起用户态进程。这三段任何一个坏了,板子都起不来。
| 组件 | 职责 | 常见位置 |
|---|---|---|
| U-Boot | 初始化 DDR、加载内核、引导启动 | SD/eMMC 前几扇区 |
| Kernel | 管理硬件、调度进程、提供系统调用 | boot 分区 zImage |
| DTB | 描述板子硬件资源 | boot 分区 .dtb |
| Rootfs | 根文件系统、/bin、/lib、配置 | root 分区 |
调试板子起不来,就是按这三段分段排查:U-Boot 有没有打印、内核有没有解压到一半卡住、根文件系统挂载失败在哪一步。
设备树:告诉内核板子上有什么
以前内核源码里写死板子硬件,换个板子就得改内核。设备树(DTS)把硬件描述从内核代码里剥出来,写成一个独立的 .dtb 文件。内核启动时读 DTB,就知道这块板子接了什么芯片、挂在哪个总线、用哪些引脚。
// 一个简化的设备树片段
&uart4 {
pinctrl-names = "default";
pinctrl-0 = &uart4_pins;
status = "okay"; /* 改成 "disabled" 就关掉这个串口 */
};
&i2c1 {
clock-frequency = <100000>;
status = "okay";
sensor@48 {
compatible = "ti,tmp1075";
reg = <0x48>; /* I2C 地址 */
};
};
板级硬件改动(换个传感器、调个引脚)改 DTS 重新编译就行,不用动内核源码。这是嵌入式 Linux 比老派 BSP 灵活得多的地方。
字符设备驱动:写第一个内核模块
最简单的驱动模型是字符设备:在内核里注册一个设备节点,用户态用 open/read/write/ioctl 操作它。下面是一个最简骨架。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
static int major;
static char buf[64];
static ssize_t my_read(struct file *f, char __user *u,
size_t n, loff_t *off) {
return copy_to_user(u, buf, n);
}
static ssize_t my_write(struct file *f, const char __user *u,
size_t n, loff_t *off) {
return copy_from_user(buf, u, n);
}
static struct file_operations fops = {
.read = my_read,
.write = my_write,
};
static int __init my_init(void) {
major = register_chrdev(0, "mydev", &fops);
return 0;
}
static void __exit my_exit(void) {
unregister_chrdev(major, "mydev");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
insmo 加载后,mknod 建一个 /dev/mydev 节点,用户态程序就能像读写文件一样和这个驱动通信。真实驱动再加中断、DMA、电源管理,但骨架就是这个。
用户态与内核态怎么通信
用户态程序不能直接碰硬件寄存器,必须经过内核。常见通道有三种:字符设备的 read/write/ioctl、netlink 广播、sysfs 属性节点。
- read/write:流式数据,比如传感器读数、串口收发。
- ioctl:结构化命令,“设置采样率”“读寄存器”这类带参数的控制。
- sysfs:内核把状态直接暴露到 /sys/class/xxx/value,cat 就能读,echo 就能写,调试最方便。
- netlink:内核主动往用户态推事件,比如热插拔、状态变化通知。
嵌入式 Linux 学习路径
嵌入式 Linux 是个深坑,别想着一步到位。推荐这个顺序:
- 第一步:找一块能跑 Linux 的开发板(树莓派、全志、瑞芯微都行),先把官方镜像烧进去用起来。
- 第二步:学会交叉编译一个自己写的 C 程序,scp 到板子上跑。
- 第三步:改一个设备树节点(比如加一个 LED),看板子上灯的反应。
- 第四步:写一个最简字符设备驱动模块,insmod 加载。
- 第五步:用 Buildroot 或 Yocto 自己编一个根文件系统,把前面这些塞进去。
裸机做扎实了再上 Linux。连 STM32 的寄存器都没摸熟就直接啃内核源码,容易劝退。
如果你已经能熟练写 STM32 外设驱动,嵌入式 Linux 入门比你想象的快——硬件操作那部分你已经会了,新东西只是“内核怎么帮你管这些硬件”。