第三章:Linux内核与设备树基础——内核编译、DTS语法与CAN控制器节点配置

做嵌入式Linux工控网关,内核和设备树是绕不开的两座山。很多初学者觉得内核编译是个黑盒子,设备树更是天书。其实没那么玄乎。我刚开始接触时也一头雾水,后来在几个项目里反复折腾,才摸清了门道。今天咱们就把这两块掰开揉碎了讲清楚。

3.1 内核编译:从源码到镜像的完整流程

内核编译说白了就是把Linux内核源码变成能在你板子上跑起来的二进制文件。工控网关对实时性和稳定性要求高,所以内核配置要精打细算,不能一股脑全选上。

3.1.1 获取内核源码

我习惯从官方内核仓库拉取长期支持版本(LTS)。比如5.10.y或6.1.y系列,这些版本维护周期长,社区支持好。

# 以5.10.200为例
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.200.tar.xz
tar -xf linux-5.10.200.tar.xz
cd linux-5.10.200

3.1.2 配置内核选项

配置内核有三种常用方式:

配置方式 命令 适用场景
默认配置 make defconfig 快速生成通用配置
基于现有配置 make menuconfig 交互式调整,最常用
板级配置 make ARCH=arm imx_v7_defconfig 特定开发板官方配置

我个人强烈推荐 make menuconfig。它基于ncurses的图形界面,可以逐项搜索、勾选、取消。工控网关必须开启的选项包括:

  • CAN总线子系统:Networking support → CAN bus subsystem support
  • 实时抢占补丁(如果使用PREEMPT_RT):General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)
  • 设备树支持:Device Drivers → Device Tree and Open Firmware support
我的习惯:每次修改配置后,把生成的.config文件备份一份。我曾经因为误操作覆盖了配置,不得不从头再来,浪费了半天时间。

3.1.3 编译与安装

# 编译内核镜像和设备树
make -j$(nproc) zImage
make -j$(nproc) dtbs

# 编译内核模块
make -j$(nproc) modules

# 安装模块到根文件系统
make modules_install INSTALL_MOD_PATH=/path/to/rootfs

这里有个坑:-j参数指定并行编译线程数,一般设为CPU核心数的1.5到2倍。但如果你在虚拟机里编译,别开太多,否则内存容易爆。我有一台4核的笔记本,开-j8直接卡死,后来老老实实改成-j4才稳定。

3.2 设备树(DTS)语法:硬件描述的语言

设备树是Linux用来描述硬件拓扑结构的数据结构。它把板级细节从内核源码里剥离出来,变成一种树形文本文件。你想想看,如果没有设备树,每换一个板子就要改内核代码,那得多麻烦?

3.2.1 基本结构

一个典型的设备树文件(.dts)长这样:

/dts-v1/;

/ {
    model = "工控网关CAN采集板";
    compatible = "mycompany,iot-gateway-v1";

    chosen {
        stdout-path = &uart1;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB
    };

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a7";
            reg = <0x0>;
        };
    };

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart1: serial@02020000 {
            compatible = "fsl,imx6ul-uart";
            reg = <0x02020000 0x4000>;
            interrupts = <0 26 4>;
            status = "okay";
        };
    };
};

关键概念:

  • 节点(Node):用/开头表示根节点,子节点用{}包裹
  • 属性(Property):键值对,描述节点特征
  • 标签(Label):如uart1:,方便在其他地方引用
  • 引用(Reference):用&uart1引用标签

3.2.2 常用属性

属性 含义 示例
compatible 设备兼容性字符串,驱动匹配依据 "fsl,imx6ul-uart"
reg 寄存器地址和大小 <0x02020000 0x4000>
interrupts 中断号、类型 <0 26 4>
status 设备状态:okay、disabled "okay"
clocks 时钟源引用 <&clks IMX6UL_CLK_UART1>
注意:reg属性的地址和大小由父节点的#address-cells和#size-cells决定。如果这两个值配错了,内核解析出来的地址会完全不对。我曾经在这个问题上排查了整整一个下午。

3.3 CAN控制器节点配置:让硬件说话

工控网关的核心功能之一就是CAN总线数据采集。在设备树里配置CAN控制器节点,是让CAN硬件正常工作的前提。

3.3.1 常见的CAN控制器

嵌入式平台常用的CAN控制器有两种:

  • SoC内置CAN:如i.MX6的FlexCAN、STM32MP1的bxCAN
  • 外部SPI转CAN模块:如MCP2515、MCP2518

咱们以i.MX6UL的FlexCAN为例,它在设备树里的配置是这样的:

&flexcan1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_flexcan1>;
    xceiver-supply = <®_can_3v3>;
    status = "okay";
};

&flexcan2 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_flexcan2>;
    xceiver-supply = <®_can_3v3>;
    status = "okay";
};

对应的引脚复用配置:

pinctrl_flexcan1: flexcan1grp {
    fsl,pins = <
        MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX    0x1b020
        MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX    0x1b020
    >;
};

pinctrl_flexcan2: flexcan2grp {
    fsl,pins = <
        MX6UL_PAD_UART2_RTS_B__FLEXCAN2_RX    0x1b020
        MX6UL_PAD_UART2_CTS_B__FLEXCAN2_TX    0x1b020
    >;
};

3.3.2 关键配置项说明

配置项 作用 注意事项
pinctrl-0 指定引脚复用配置 必须与原理图一致,RX/TX不能接反
xceiver-supply CAN收发器电源控制 如果收发器独立供电,需要添加regulator节点
clock-frequency CAN控制器时钟频率 影响波特率计算,通常为30MHz或40MHz
bosch,mram-cfg M_CAN的MRAM配置(仅M_CAN) 配置邮箱数量和大小
核心要点:设备树里的CAN节点配置,本质上是告诉内核三件事——CAN控制器在哪个地址、用哪些引脚、时钟从哪里来。这三件事缺一不可。

3.3.3 外部SPI转CAN模块配置

如果SoC没有内置CAN控制器,可以用MCP2515这类SPI转CAN芯片。配置方式略有不同:

&ecspi1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi1>;
    cs-gpios = <&gpio4 9 GPIO_ACTIVE_LOW>;
    status = "okay";

    can0: mcp2515@0 {
        compatible = "microchip,mcp2515";
        reg = <0>;
        clocks = <&clk16m>;
        interrupt-parent = <&gpio4>;
        interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
        spi-max-frequency = <10000000>;
        status = "okay";
    };
};

这里要注意:

  • spi-max-frequency不要设太高,MCP2515最高支持10MHz
  • 中断引脚必须支持边沿触发,我遇到过用电平触发导致中断丢失的情况
  • 外部晶振频率要准确,否则波特率会偏

3.4 知识体系总览

下面这张图把本章的核心逻辑串起来了:

Linux内核与设备树知识体系 内核编译 源码 → 配置 → 编译 设备树(DTS) 硬件描述语言 CAN控制器配置 FlexCAN / MCP2515 编译流程 1. 获取LTS源码 2. make menuconfig 3. make zImage dtbs 4. 安装模块 DTS核心元素 • 节点(Node) • 属性(Property) • 标签(Label) • 引用(Reference) 配置要点 • 引脚复用(pinctrl) • 时钟频率 • 收发器电源 • 中断配置 工控网关CAN数据采集系统 内核 + 设备树 + CAN驱动

3.5 避坑指南与调试技巧

做CAN设备树配置,有几个坑我踩过,分享给你:

坑1:引脚复用冲突
我曾经在i.MX6UL上同时配置了UART2和FlexCAN2,结果两个都用不了。查了半天才发现它们复用了同一组引脚。解决方案:仔细看芯片手册的引脚复用表,确保功能不冲突。
坑2:设备树编译错误
make dtbs时报语法错误,最常见的原因是漏了分号或括号不匹配。我习惯用dtc -I dts -O dtb -o test.dtb test.dts单独编译检查,比在内核树里编译快得多。
调试技巧:内核启动后,用ls /proc/device-tree/查看设备树是否被正确解析。如果CAN节点没出现,说明设备树没加载或者节点状态不是okay。

嗯,到这里内核编译、设备树语法和CAN控制器配置的核心内容就讲完了。这些知识是后面实现CAN数据采集的基础,你可以在自己的板子上动手试试,把FlexCAN或MCP2515配起来,看看能不能用ip link set can0 up type can bitrate 250000把CAN接口点亮。