4. Linux系统裁剪与定制
好,咱们进入第四章。这一章是网关开发的核心环节——你要让Linux内核认识你的硬件,并且只保留你需要的东西。说白了,就是给网关“瘦身”和“配型”。
我刚开始做网关项目时,直接拿了个通用内核就往上烧,结果启动要30秒,Flash空间还差一大截。后来才明白,嵌入式Linux不是桌面Linux,每一KB空间都得精打细算。
4.1 内核裁剪:只留你需要的
内核裁剪的目的很简单:去掉不需要的功能,减小镜像体积,加快启动速度。工业网关通常只需要网络、串口、GPIO、I2C/SPI等基础外设,什么蓝牙、Wi-Fi(除非网关自带)、显卡驱动,统统可以砍掉。
核心原则: 裁剪不是删代码,而是通过配置菜单关闭不需要的模块。内核源码树下的 make menuconfig 是你最常用的工具。
具体步骤我一般这样走:
- 确定目标硬件:CPU型号、外设接口、存储大小。比如我用的是TI的AM335x系列,那就要选对应的SoC支持。
- 执行
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig:进入图形化配置界面。 - 逐项检查:
- 关闭所有非必要的驱动(如显卡、声卡、USB HID)。
- 关闭调试选项(
Kernel hacking里很多调试功能可以关掉)。 - 文件系统只保留需要的(比如只留
ext4和squashfs)。
- 保存为
.config,然后编译。
我的习惯: 每次裁剪前,先把默认配置备份一份。我曾经因为手快,把网络协议栈整个关掉了,编译出来的内核连ping都不通……嗯,从那以后我学会了 cp .config .config.bak。
4.2 内核编译:从源码到镜像
裁剪完配置,接下来就是编译。嵌入式内核编译和桌面版不太一样,你得指定架构和交叉编译器。
一个典型的编译命令序列如下:
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
make mrproper
make am335x_evm_defconfig # 使用厂商默认配置作为起点
make menuconfig # 手动裁剪
make -j4 uImage LOADADDR=0x80008000
make -j4 dtbs # 编译设备树
make -j4 modules # 编译内核模块
这里有几个关键点:
mrproper:清理所有中间文件,确保从干净状态开始。LOADADDR:指定内核加载地址,不同SoC不一样,查手册。-j4:并行编译,加快速度。我一般设成CPU核心数+1。
注意: 编译时如果报错说缺少头文件或库,先检查交叉编译器是否安装完整。我遇到过最坑的一次是编译器版本太新,内核源码不兼容,最后换回Linaro 7.5才搞定。
4.3 设备树(Device Tree)语法与实战
设备树,说白了就是描述硬件信息的“配置文件”。它告诉内核:CPU是什么、内存多大、外设挂在哪里、中断号是多少。没有设备树,内核就不知道该怎么跟硬件打交道。
你想想看,以前ARM Linux用大量板级文件(board-*.c)来写硬件信息,每次换板子都要改内核代码。设备树把硬件描述和内核代码分开了,换板子只需要换一个.dts文件。
4.3.1 基本语法
设备树源文件(.dts)的语法很直观,我直接拿一个例子说明:
/dts-v1/;
/ {
model = "My Industrial Gateway Board";
compatible = "mycompany,gateway-v1", "ti,am335x";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB RAM
};
leds {
compatible = "gpio-leds";
led0: led@0 {
label = "status:green";
gpios = <&gpio1 16 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
&i2c0 {
status = "okay";
clock-frequency = <100000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
};
关键概念:
| 元素 | 说明 | 示例 |
|---|---|---|
| 节点 | 用 / 开头,表示一个设备或总线 |
leds { ... } |
| 属性 | 键值对,描述节点特征 | compatible = "gpio-leds"; |
| reg | 地址和大小,格式为 <地址 大小> |
reg = <0x80000000 0x10000000>; |
| 中断 | 用 interrupt-parent 和 interrupts 描述 |
interrupts = <0 67 4>; |
| 引用 | 用 &label 引用其他节点 |
&i2c0 { ... }; |
4.3.2 实战:添加一个自定义外设
假设我们的网关板子上外挂了一个ADC芯片(比如MCP3208),通过SPI接口连接。我需要让内核识别它。
第一步,在设备树中添加SPI节点下的子节点:
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>;
adc@0 {
compatible = "microchip,mcp3208";
reg = <0>; // SPI片选0
spi-max-frequency = <1000000>;
vref-supply = <&vref_3v3>;
};
};
第二步,在内核中确认驱动是否已启用。MCP3208的驱动在 drivers/iio/adc/ 下,通过 make menuconfig 找到并勾选:
Device Drivers --->
Industrial I/O support --->
Analog to digital converters --->
<*> Microchip MCP3208
第三步,重新编译设备树和内核,烧录后启动。如果一切正常,你会在 /sys/bus/iio/devices/ 下看到新的ADC设备。
避坑指南: 我曾经在设备树里写错了SPI片选号,结果ADC和另一个设备冲突,系统启动时直接挂死。排查了半天才发现是 reg = <0> 写成了 reg = <1>。所以,每次改设备树后,建议先用 dtc -I dtb -O dts 反编译一下,确认生成的二进制文件和你写的一致。
4.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。从硬件板子到最终运行的内核,设备树是中间的桥梁。
4.5 添加自定义外设驱动:一个完整案例
如果内核里没有现成的驱动怎么办?那就自己写一个。我以最简单的GPIO控制为例,演示如何添加一个自定义驱动。
假设我们有一个LED灯,接在GPIO1_16上,想通过 /sys/class/leds/ 控制它。其实内核已经有 gpio-leds 驱动,但为了教学,我们写一个最小化的字符驱动。
4.5.1 驱动源码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#define GPIO_LED 48 // GPIO1_16 对应的编号
static int led_open(struct inode *inode, struct file *file) {
return 0;
}
static ssize_t led_write(struct file *file, const char __user *buf,
size_t len, loff_t *off) {
char kbuf[4];
if (copy_from_user(kbuf, buf, len))
return -EFAULT;
if (kbuf[0] == '1')
gpio_set_value(GPIO_LED, 1);
else
gpio_set_value(GPIO_LED, 0);
return len;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
static int __init led_init(void) {
int ret;
ret = gpio_request(GPIO_LED, "my_led");
if (ret) {
pr_err("Failed to request GPIO\n");
return ret;
}
gpio_direction_output(GPIO_LED, 0);
register_chrdev(0, "my_led", &led_fops);
pr_info("My LED driver loaded\n");
return 0;
}
static void __exit led_exit(void) {
gpio_free(GPIO_LED);
unregister_chrdev(0, "my_led");
pr_info("My LED driver unloaded\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
4.5.2 编译与测试
把源码放到 drivers/char/ 下,修改 Kconfig 和 Makefile,然后重新编译内核或编译为模块。加载后:
# echo 1 > /dev/my_led // 点亮
# echo 0 > /dev/my_led // 熄灭
注意: 这只是演示用的最小驱动。实际产品中,你应该使用内核现有的子系统(如LED子系统、GPIO子系统),而不是直接操作 gpio_set_value。我见过不少新手直接操作GPIO寄存器,结果移植到新内核时全废了。
4.6 本章小结
内核裁剪让你去掉冗余,设备树让你描述硬件,自定义驱动让你扩展功能。这三者加起来,就是嵌入式Linux网关开发的“三板斧”。
我个人觉得,设备树是这里面最值得花时间啃的。你想想看,一个设备树写对了,硬件和内核的“沟通”就顺畅了;写错了,轻则外设不工作,重则系统起不来。所以,每次改完.dts,我都会用 dtc -I dtb -O dts 反编译检查一遍,确保没有低级错误。