Linux驱动开发实战指南-下

第 18 章 SPI 子系统--oled 屏实验

SPI(串行外设接口)是高速、全双工、同步的通信总线,仅需四根管脚,管脚利用率高。

本章围绕 SPI 驱动开发展开,含五部分核心内容:

SPI 基础:讲解物理总线、通信时序及工作模式。

驱动框架:分析核心数据结构与 SPI 驱动整体架构。

核心组件:拆解 SPI 总线驱动、核心层及控制器的工作机制。

关键函数:介绍同步、异步等驱动开发常用函数。

实战实验:以 SPI 接口 OLED 显示屏为例,讲解驱动程序编写。

18.1 spi 基本知识

18.1.1 spi 物理总线

spi 总线都可以挂载多个设备,spi 支持标准的一主多从,全双工半双工通信等。其中四根控制线包括:

• SCK:时钟线,数据收发同步

• MOSI:数据线,主设备数据发送、从设备数据接收

• MISO:数据线,从设备数据发送,主设备数据接收

• NSS:片选信号线

I2C 通过设备地址选择通信设备,SPI 则通过片选引脚选中目标设备。

SPI 可借助自身片选引脚或外部 GPIO 扩展从设备数量,具体数量由片选引脚数决定。

不同片选引脚的使用规则

  • 若使用 SPI 接口自带片选引脚,总线驱动会自动处理设备选中时机。
  • 若使用外部 GPIO 作为片选引脚,需在 SPI 设备驱动中设置选中时机,或在配置 SPI 时指定所用引脚。

通常无特殊要求时,优先使用 SPI 接口自带的片选引脚。

自带片选引脚由 SPI 控制器直接管理,外部 GPIO 需由 CPU 或驱动程序单独控制。

18.1.2 spi 时序

• 起始信号:NSS 信号线由高变低

• 停止信号:NSS 信号由低变高

• 数据传输:在 SCK 的每个时钟周期 MOSI 和 MISO 同时传输一位数据,高/低位传输没有硬

性规定

-- 传输单位:8 位或 16 位

-- 单位数量:允许无限长的数据传输

18.1.3 spi 通信模式

根据总线空闲时 SCK 的时钟状态以及数据采样时刻,SPI 的工作模式分为四种:
• 时钟极性 CPOL:指 SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号:
-- CPOL=0 时,SCK 在空闲状态时为低电平
-- CPOL=1 时,SCK 在空闲状态时为高电平
• 时钟相位 CPHA:数据的采样的时刻:
-- CPHA=0 时,数据在 SCK 时钟线的"奇数边沿"被采样
-- CPHA=1 时,数据在 SCK 时钟线的"偶数边沿"被采样

SCK 空闲状态为低电平时 CPOL=0,为高电平时 CPOL=1;

CPHA=0 时,数据在 SCK 奇数边沿采样(CPOL=0 对应上升沿,CPOL=1 对应下降沿)。Linux 内核定义了相关 SPI 通讯模式,常用模式 0 和模式 3。

cpp 复制代码
/*
 * SPI 通信模式由时钟极性 (CPOL) 和时钟相位 (CPHA) 组合定义。
 * - CPOL (Clock Polarity):定义了时钟信号在空闲状态时的电平。
 * - CPHA (Clock Phase):定义了数据采样是在时钟的第一个边沿还是第二个边沿。
 */

#define SPI_CPHA 0x01  /* 时钟相位:1 表示数据在时钟第二个边沿采样 */
#define SPI_CPOL 0x02  /* 时钟极性:1 表示时钟空闲时为高电平 */

/*
 * 四种基本 SPI 模式组合:
 * - 模式 0 和模式 3 是最常用的两种模式。
 */
#define SPI_MODE_0  (0x00)        /* CPOL=0, CPHA=0:时钟空闲低,第一个边沿(上升沿)采样 */
#define SPI_MODE_1  (SPI_CPHA)    /* CPOL=0, CPHA=1:时钟空闲低,第二个边沿(下降沿)采样 */
#define SPI_MODE_2  (SPI_CPOL)    /* CPOL=1, CPHA=0:时钟空闲高,第一个边沿(下降沿)采样 */
#define SPI_MODE_3  (SPI_CPOL | SPI_CPHA) /* CPOL=1, CPHA=1:时钟空闲高,第二个边沿(上升沿)采样 */

18.2 spi 驱动框架

SPI 分为总线驱动和设备驱动,总线驱动由芯片厂商提供,设备驱动需自行编写,其实现涉及字符设备驱动、SPI 核心层及 SPI 主机驱动。

各模块核心功能

SPI 核心层:提供控制器驱动、设备驱动的注册 / 注销方法,以及上层 API 接口,支持 SPI 主机、驱动、设备的初始化注册与退出注销。

SPI 主机驱动(控制器驱动):控制 SPI 适配器(控制器),实现 SPI 总线硬件访问操作。

SPI 设备驱动:对应 SPI 设备端程序,通过主机驱动与 CPU 完成数据交换。

18.2.1 关键数据结构

18.2.1.1 spi_master

spi_master 是 SPI 控制器接口,实际也就是 spi_controller 结构体。

cpp 复制代码
/*内核源码*/
#define spi_master spi_controller
18.2.1.2 spi_controller
cpp 复制代码
struct spi_controller {
    struct device dev;              /* 继承自设备模型的通用设备结构体 */

    struct list_head list;          /* 用于将此控制器链接到全局SPI控制器列表 */
    s16 bus_num;                    /* SPI总线号 (如 spi0, spi1) */
    u16 num_chipselect;             /* 控制器支持的片选线数量 */

    struct spi_message *cur_msg;    /* 当前正在处理的SPI消息 */

    /*
     * 旧的传输接口函数指针,用于兼容和简化。
     * - setup: 在传输前配置SPI设备的模式、速率等参数。
     * - transfer: 提交一个SPI消息进行异步传输。
     * - cleanup: 在设备移除时进行清理工作。
     */
    int (*setup)(struct spi_device *spi);
    int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
    void (*cleanup)(struct spi_device *spi);

    /*
     * 用于处理传输的内核线程工作队列。
     * 现代SPI驱动倾向于使用这种异步方式来处理数据传输。
     */
    struct kthread_worker kworker;
    struct task_struct *kworker_task;
    struct kthread_work pump_messages;

    struct list_head queue;         /* 等待处理的SPI消息队列 */
    // struct spi_message *cur_msg; /* 注意:原代码中此成员重复定义了 */

    /*
     * 现代SPI主机驱动推荐实现的接口。
     * - transfer_one_message: 传输一个完整的SPI消息,是最重要的入口点。
     * - transfer_one: 传输一个单独的spi_transfer片段(由核心层调用)。
     * - prepare_transfer_hardware: 在开始一批传输前准备硬件(如申请DMA通道)。
     * - set_cs: 控制指定SPI设备的片选信号(低电平有效或高电平有效)。
     */
    int (*transfer_one_message)(struct spi_controller *ctlr, struct spi_message *mesg);
    int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *transfer);
    int (*prepare_transfer_hardware)(struct spi_controller *ctlr);
    void (*set_cs)(struct spi_device *spi, bool enable);

    int *cs_gpios;                  /* 指向一个数组,包含用于片选的GPIO编号。
                                       如果使用控制器硬件片选,则此指针为NULL。 */
};
18.2.1.3 spi_driver 结构体
cpp 复制代码
struct spi_driver {
    const struct spi_device_id *id_table;  // 设备ID匹配表
    int (*probe)(struct spi_device *spi);  // 设备初始化函数
    int (*remove)(struct spi_device *spi); // 设备移除清理函数
    void (*shutdown)(struct spi_device *spi); // 系统关机处理函数
    struct device_driver driver;           // 内嵌通用驱动结构体
};

id_table:用来和 spi 进行配对。

.probe: spi 设备和 spi 驱动匹配成功后,回调该函数指针。

spi_driver 与 i2c_driver、platform_driver 结构一致,核心用法相同,

均通过内嵌 device_driver 结构体、匹配表及 probe/remove 等生命周期函数实现设备驱动管理。

18.2.1.4 spi_device 设备结构体

spi_device 结构体代表一个具体的 SPI 设备,存储其配置信息。

当驱动与设备(如设备树节点)匹配成功后,可在 .probe 函数参数中获取该结构体,用于访问设备详情。

cpp 复制代码
struct spi_device {
    struct device dev;                     // 内嵌的通用设备结构体,是Linux设备模型的基础
    struct spi_controller *controller;     // 指向该设备所连接的SPI控制器
    struct spi_controller *master;         // 兼容旧版代码的别名,作用与controller相同
    u32 max_speed_hz;                      // 设备支持的最大传输速率,单位为赫兹(Hz)
    u8 chip_select;                        // 芯片选择线(CS)的索引
    u8 bits_per_word;                      // 每次传输的数据位数(例如8位、16位)
    u16 mode;                              // SPI通信模式的位掩码,由以下宏定义组合而成
#define SPI_CPHA 0x01                     // 时钟相位(Clock Phase)
#define SPI_CPOL 0x02                     // 时钟极性(Clock Polarity)
#define SPI_MODE_0 (0|0)                  // 模式0:CPOL=0, CPHA=0
#define SPI_MODE_1 (0|SPI_CPHA)           // 模式1:CPOL=0, CPHA=1
#define SPI_MODE_2 (SPI_CPOL|0)           // 模式2:CPOL=1, CPHA=0
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)    // 模式3:CPOL=1, CPHA=1
#define SPI_CS_HIGH 0x04                  // 片选信号为高电平有效
#define SPI_LSB_FIRST 0x08                // 数据传输时最低位(LSB)在前
#define SPI_3WIRE 0x10                    // 使用三线模式(SCLK, SI/SO, CS)
#define SPI_LOOP 0x20                     // 回环模式,用于自测试
#define SPI_NO_CS 0x40                    // 不需要片选信号(总线上只有一个设备)
#define SPI_READY 0x80                    // 从设备准备好信号
#define SPI_TX_DUAL 0x100                 // 启用双线传输模式(仅发送)
#define SPI_TX_QUAD 0x200                 // 启用四线传输模式(仅发送)
#define SPI_RX_DUAL 0x400                 // 启用双线接收模式
#define SPI_RX_QUAD 0x800                 // 启用四线接收模式
    int irq;                              // 分配给该设备的中断号
    void *controller_state;               // SPI控制器的私有状态数据
    void *controller_data;                // SPI控制器的私有数据
    char modalias[SPI_NAME_SIZE];         // 设备的模块别名,用于驱动匹配
    int cs_gpio;                          // 用于片选的GPIO引脚编号(若使用GPIO模拟CS)

    /* 统计信息 */
    struct spi_statistics statistics;     // 用于记录SPI传输的统计信息
};
18.2.1.5 spi_transfer 结构体

spi_transfer 结构体是 SPI 数据传输的核心配置单元,用于定义单次 SPI 传输的具体参数和数据缓冲区。

cpp 复制代码
struct spi_transfer {
    /*
     * tx_buf 和 rx_buf 可以指向同一个缓冲区(全双工)。
     * 在 MicroWire 模式下,必须有一个缓冲区为 NULL(半双工)。
     * 缓冲区必须能被 dma_*map_single() 函数正确映射,
     * 除非 spi_message 的 is_dma_mapped 标志已设置。
     */
    const void *tx_buf;        // 发送数据缓冲区指针
    void *rx_buf;              // 接收数据缓冲区指针
    unsigned len;              // 发送/接收数据的长度(字节数)

    dma_addr_t tx_dma;         // 发送数据的 DMA 地址
    dma_addr_t rx_dma;         // 接收数据的 DMA 地址
    struct sg_table tx_sg;     // 用于 scatter-gather DMA 的发送 scatter list
    struct sg_table rx_sg;     // 用于 scatter-gather DMA 的接收 scatter list

    unsigned cs_change:1;      // 传输结束后是否切换片选状态 (0: 不切换, 1: 切换)
    unsigned tx_nbits:3;       // 发送数据的线数 (1, 2, 或 4)
    unsigned rx_nbits:3;       // 接收数据的线数 (1, 2, 或 4)
#define SPI_NBITS_SINGLE 0x01 /* 1线传输模式 */
#define SPI_NBITS_DUAL 0x02   /* 2线传输模式 */
#define SPI_NBITS_QUAD 0x04   /* 4线传输模式 */

    u8 bits_per_word;          // 本次传输的每字位数(覆盖spi_device的默认值)
    u16 delay_usecs;           // 传输结束后,片选切换前的延时(微秒)
    u32 speed_hz;              // 本次传输的速率(Hz)(覆盖spi_device的默认值)

    struct list_head transfer_list; // 用于将多个spi_transfer链接成一个 spi_message
};
18.2.1.6 spi_message 结构体

spi_transfer 结构体保存单次 SPI 传输的具体数据和配置(如收发缓冲区、长度等)。

而在 SPI 驱动中,数据传输以 "消息"(spi_message 结构体)为单位,一个消息可包含多个 spi_transfer(构成连续传输序列)。

发送一个 SPI 消息的核心四步:

定义 spi_message 结构体实例;

调用 spi_message_init() 初始化该消息;

通过 spi_message_add_tail() 将一个或多个已配置好的 spi_transfer 绑定到消息中;

调用 spi_sync()(同步)或 spi_async()(异步)执行消息发送,完成所有绑定的 spi_transfer 传输。

cpp 复制代码
struct spi_message {
    struct list_head transfers;  // 指向一个 spi_transfer 结构体链表的头部

    struct spi_device *spi;      // 该消息要发送到的目标 SPI 设备

    unsigned is_dma_mapped:1;    // 标记消息的缓冲区是否已进行 DMA 映射

    /* 完成回调函数 */
    void (*complete)(void *context); // 消息传输完成后调用的回调函数
    void *context;               // 传递给 complete 回调函数的上下文指针

    unsigned frame_length;       // 总帧数(通常等于总字节数,取决于 bits_per_word)
    unsigned actual_length;      // 实际成功传输的字节数
    int status;                  // 消息传输的最终状态(0 表示成功)

    /* 用于驱动内部管理 */
    struct list_head queue;      // 用于将消息链接到 SPI 控制器的等待队列
    void *state;                 // SPI 控制器驱动用于保存消息状态的私有数据
};

struct spi_message 关键说明:

该结构体的大多数成员由内核在 spi_message_init() 和 spi_message_add_tail() 等函数内部处理,驱动开发者通常无需直接操作。

核心关注成员:struct spi_device *spi;

作用:明确指定当前消息要发送到的目标 SPI 设备。

该指针通常指向在 probe 函数中获得的 spi_device 实例。

18.2.2 SPI 核心层

18.2.2.1 spi 总线注册

linux 系统在开机的时候就会执行,自动进行 spi 总线注册。

cpp 复制代码
/**
 * spi_init - SPI 子系统的核心初始化函数
 *
 * 该函数在 kernel 启动时被调用,用于注册 SPI 总线类型和 SPI 控制器的设备类。
 * 这是 SPI 驱动模型能够正常工作的基础。
 *
 * 返回:
 *  0 - 初始化成功
 *  非0 - 初始化失败(通常是总线或类注册失败)
 */
static int __init spi_init(void)
{
    int status;

    // ... (可能存在的其他初始化代码,如分配资源等)

    // 1. 注册 SPI 总线类型
    //    这使得内核可以管理 SPI 设备和驱动的匹配过程
    status = bus_register(&spi_bus_type);
    if (status < 0)
        goto out; // 如果注册失败,则跳转到错误处理流程

    // ... (可能存在的其他初始化代码)

    // 2. 注册一个名为 "spi_master" 的设备类
    //    这个类在 sysfs 中创建 /sys/class/spi_master 目录
    //    用于统一管理所有的 SPI 控制器设备
    status = class_register(&spi_master_class);
    if (status < 0)
        goto out_unregister_bus; // 如果类注册失败,则需要先注销已经注册的总线

    // ... (可能存在的其他初始化代码)

out_unregister_bus:
    bus_unregister(&spi_bus_type); // 回滚操作:注销已注册的总线
out:
    return status; // 返回最终的初始化状态
}

总线注册成功后,sys/bus/ 目录下会生成 spi 总线,

sys/class/ 目录下会新增 spi_master 设备类。

18.2.2.2 spi 总线定义

spi_bus_type 总线定义,会在 spi 总线注册时使用。

cpp 复制代码
/**struct bus_type spi_bus_type - SPI 总线类型结构体*/
struct bus_type spi_bus_type = {
    .name       = "spi",                  // 总线名称
    .dev_groups = spi_dev_groups,          // 设备属性组
    .match      = spi_match_device,        // 设备与驱动的匹配函数
    .uevent     = spi_uevent,              // 生成 uevent 事件
};

.match = spi_match_device 设定了 SPI 设备与驱动的匹配规则,

具体匹配逻辑由 spi_match_device 函数实现。

18.2.2.3 spi_match_device() 函数
cpp 复制代码
/**
 * spi_match_device - SPI 总线的设备-驱动匹配函数
 * @dev: 指向待匹配的设备(通用 device 结构体)
 * @drv: 指向待匹配的驱动(通用 device_driver 结构体)
 *
 * 该函数按照优先级顺序尝试多种匹配方式,成功匹配则返回非零值,否则返回 0。
 * 匹配优先级:1. 设备树匹配  2. ACPI 匹配  3. ID 表匹配  4. 名称字符串匹配
 *
 * 返回:
 *  1 - 匹配成功
 *  0 - 匹配失败
 */
static int spi_match_device(struct device *dev, struct device_driver *drv)
{
    // 将通用设备/驱动指针转换为 SPI 专用类型
    const struct spi_device *spi = to_spi_device(dev);
    const struct spi_driver *sdrv = to_spi_driver(drv);

    // 1. 优先使用设备树(OF)匹配
    //    比较设备节点的 compatible 属性和驱动的 of_match_table
    if (of_driver_match_device(dev, drv))
        return 1; // 匹配成功

    // 2. 其次使用 ACPI 匹配(用于 ACPI 表描述的硬件)
    if (acpi_driver_match_device(dev, drv))
        return 1; // 匹配成功

    // 3. 然后使用 ID 表匹配
    //    如果驱动提供了 id_table,则用它与设备的 modalias 进行比较
    if (sdrv->id_table)
        return !!spi_match_id(sdrv->id_table, spi); // 匹配成功返回 1,失败返回 0

    // 4. 最后使用名称字符串匹配(最传统的方式)
    //    直接比较设备的 modalias 和驱动的 name
    return strcmp(spi->modalias, drv->name) == 0;
}

spi_match_device 函数提供四种匹配方式,

按优先级依次为:

设备树匹配、

ACPI 匹配、

ID 表匹配,若均失败,则 fallback 到

设备名(modalias 与驱动名)匹配。

18.2.3 spi 控制器驱动

RK3568 芯片含 4 个 SPI 控制器,其设备树中对应设有 4 个 SPI 节点。

cpp 复制代码
spi3: spi@fe640000 {
    compatible = "rockchip,rk3066-spi";  // 兼容性字符串,用于匹配驱动
    reg = <0x0 0xfe640000 0x0 0x1000>;   // 寄存器地址和大小
    interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>; // 中断号和触发方式
    #address-cells = <1>;                // 子节点地址 cells 数量
    #size-cells = <0>;                   // 子节点大小 cells 数量
    
    clocks = <&cru CLK_SPI3>, <&cru PCLK_SPI3>; // 时钟源
    clock-names = "spiclk", "apb_pclk";  // 时钟名称
    
    dmas = <&dmac0 26>, <&dmac0 27>;     // DMA 通道
    dma-names = "tx", "rx";              // DMA 名称
    
    pinctrl-names = "default", "high_speed"; // pin 配置名称
    pinctrl-0 = <&spi3m0_cs0 &spi3m0_cs1 &spi3m0_pins>; // 默认 pin 配置
    pinctrl-1 = <&spi3m0_cs0 &spi3m0_cs1 &spi3m0_pins_hs>; // 高速 pin 配置
    
    status = "disabled";                 // 节点状态:禁用
};

• reg :为 spi3 寄存器组相关的起始地址为 0xfe640000,寄存器长度为 0x1000。

• compatible 属性值与主机驱动匹配;

cpp 复制代码
// 在文件: drivers/spi/spi-rockchip.c 中

/**
 * rockchip_spi_dt_match - Rockchip SPI 控制器的设备树匹配表
 *
 * 这个数组中的每一项都包含一个 compatible 字符串,
 * 当内核解析设备树时,会用这些字符串去匹配各个 SPI 控制器节点的 compatible 属性。
 * 如果匹配成功,对应的 rockchip_spi 驱动就会被绑定到该硬件上。
 */
static const struct of_device_id rockchip_spi_dt_match[] = {
    { .compatible = "rockchip,px30-spi", },
    { .compatible = "rockchip,rv1108-spi", },
    { .compatible = "rockchip,rv1126-spi", },
    { .compatible = "rockchip,rk3036-spi", },
    { .compatible = "rockchip,rk3066-spi", },  // 与 RK3568 SPI 节点匹配的项
    { .compatible = "rockchip,rk3188-spi", },
    { .compatible = "rockchip,rk3228-spi", },
    { .compatible = "rockchip,rk3288-spi", },
    { .compatible = "rockchip,rk3368-spi", },
    { .compatible = "rockchip,rk3399-spi", },
    // ... (可能还有更多其他 Rockchip 芯片的 compatible 项)
    { }, // 数组结束标记
};
MODULE_DEVICE_TABLE(of, rockchip_spi_dt_match);

驱动控制器通过下面 module_platform_driver() 注册:

cpp 复制代码
// 在文件: drivers/spi/spi-rockchip.c 中

/**
 * struct platform_driver rockchip_spi_driver - Rockchip SPI 平台驱动结构体
 * @driver: 内嵌的通用驱动结构体
 * @driver.name: 驱动名称,用于 sysfs 和驱动匹配
 * @driver.pm: 指向电源管理操作结构体的指针
 * @driver.of_match_table: 指向设备树匹配表的指针
 * @probe: 驱动的探测函数,当设备与驱动匹配时调用
 * @remove: 驱动的移除函数,当设备被移除时调用
 */
static struct platform_driver rockchip_spi_driver = {
    .driver = {
        .name = DRIVER_NAME,                     // 驱动名称
        .pm = &rockchip_spi_pm,                  // 电源管理_ops
        .of_match_table = of_match_ptr(rockchip_spi_dt_match), // 设备树匹配表
    },
    .probe = rockchip_spi_probe,                  // 探测函数
    .remove = rockchip_spi_remove,                // 移除函数
};

// 宏:注册平台驱动,替代传统的 platform_driver_register/unregister
module_platform_driver(rockchip_spi_driver);

控制器驱动源码中, module_platform_driver() 宏:

cpp 复制代码
/**
 * module_platform_driver() - 平台驱动模块注册宏
 * @__platform_driver: 平台驱动结构体实例指针
 *
 * 该宏用于静态定义平台驱动的初始化和退出函数,
 * 并将驱动注册到内核平台总线。
 * 等效于:
 * static int __init driver_init_func(void)
 * {
 *     return platform_driver_register(&__platform_driver);
 * }
 * module_init(driver_init_func);
 *
 * static void __exit driver_exit_func(void)
 * {
 *     platform_driver_unregister(&__platform_driver);
 * }
 * module_exit(driver_exit_func);
 *
 * 使用此宏可大幅简化平台驱动模块的代码编写。
 */
#define module_platform_driver(__platform_driver) \
    module_driver(__platform_driver, platform_driver_register, \
                 platform_driver_unregister)

module_driver() 展开如下:

cpp 复制代码
#define module_driver(driver,             //模块名称或结构体变量名
                    register,             //注册模块的函数名
                    unregister, ...) \    //注销模块的函数名
static int __init driver##_init(void) \
{ \
    return register(&(driver), ##__VA_ARGS__); \
} \
module_init(driver##_init);


/*
__init:这是一个内核宏,用于标记初始化函数。内核在模块加载时会调用这个函数,并在初始化完成后可能将其从内存中移除以节省空间。
*/
/*
driver##_init:这是通过宏拼接生成的初始化函数名。
## 是宏拼接操作符,将 driver 参数和 _init 拼接成一个完整的函数名。
*/
cpp 复制代码
// 在文件: drivers/spi/spi-rockchip.c 中

/**
 * rockchip_spi_probe - SPI 控制器驱动的探测与初始化函数
 * @pdev: 指向平台设备结构体的指针
 *
 * 当设备树中的节点与驱动匹配时,此函数被调用。
 * 它负责初始化 SPI 控制器的硬件,包括申请资源、配置时钟、中断、
 * DMA、GPIO 等,并最终将控制器注册到 SPI 子系统中。
 *
 * 返回: 成功返回 0,失败返回负值错误码。
 */
static int rockchip_spi_probe(struct platform_device *pdev)
{
    int ret;
    struct rockchip_spi *rs;              // Rockchip 私有的 SPI 控制器结构体
    struct spi_controller *ctlr;          // 通用的 SPI 控制器结构体
    struct resource *mem;                 // 用于获取设备内存资源
    struct device_node *np = pdev->dev.of_node; // 指向设备树节点
    u32 rsd_nsecs;
    bool slave_mode;                      // 标记控制器是主模式还是从模式
    struct pinctrl *pinctrl = NULL;       // 用于引脚配置

    // 1. 检查设备树中是否设置了从模式属性
    slave_mode = of_property_read_bool(np, "spi-slave");

    // 2. 分配并初始化通用的 SPI 控制器结构体
    if (slave_mode)
        ctlr = spi_alloc_slave(&pdev->dev, sizeof(struct rockchip_spi));
    else
        ctlr = spi_alloc_master(&pdev->dev, sizeof(struct rockchip_spi)); // 分配主模式控制器
    if (!ctlr)
        return -ENOMEM;

    // 3. 将 SPI 控制器结构体保存为平台设备的私有数据,方便后续在其他函数中获取
    platform_set_drvdata(pdev, ctlr);

    // 4. 获取 Rockchip 私有 SPI 控制器结构体(它被嵌入在 ctlr 内部)
    rs = spi_controller_get_devdata(ctlr);
    ctlr->slave = slave_mode;

    // ... (省略了大量代码,包括:获取并映射内存资源、配置时钟、申请复位等)

    // 5. 禁用芯片(在配置完成前)
    spi_enable_chip(rs, false);

    // 6. 获取中断号并请求中断服务
    ret = platform_get_irq(pdev, 0);
    if (ret < 0)
        goto err_disable_spiclk;
    ret = devm_request_threaded_irq(&pdev->dev, ret, rockchip_spi_isr,
                                    NULL, IRQF_ONESHOT, dev_name(&pdev->dev), ctlr);
    if (ret)
        goto err_disable_spiclk;

    // ... (省略了设置 FIFO 阈值、配置传输模式等代码)

    // 7. 启用运行时电源管理
    pm_runtime_set_active(&pdev->dev);
    pm_runtime_enable(&pdev->dev);

    // 8. 填充 SPI 控制器结构体的关键成员
    ctlr->auto_runtime_pm = true;             // 启用自动运行时 PM
    ctlr->bus_num = pdev->id;                 // 设置总线号
    ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | SPI_LSB_FIRST | SPI_CS_HIGH; // 支持的模式
    if (slave_mode) {
        ctlr->mode_bits |= SPI_NO_CS;
        ctlr->slave_abort = rockchip_spi_slave_abort;
    } else {
        ctlr->flags = SPI_MASTER_GPIO_SS;    // 允许使用 GPIO 作为片选
    }
    ctlr->num_chipselect = ROCKCHIP_SPI_MAX_CS_NUM; // 支持的最大片选数
    ctlr->dev.of_node = pdev->dev.of_node;   // 关联设备树节点
    ctlr->bits_per_word_mask = SPI_BPW_MASK(16) | SPI_BPW_MASK(8) | SPI_BPW_MASK(4); // 支持的位宽
    ctlr->min_speed_hz = rs->freq / BAUDR_SCKDV_MAX; // 最小频率
    ctlr->max_speed_hz = min(rs->freq / BAUDR_SCKDV_MIN, MAX_SCLK_OUT); // 最大频率

    // 9. 设置 SPI 控制器的操作函数指针
    ctlr->set_cs = rockchip_spi_set_cs;       // 设置片选的函数
    ctlr->setup = rockchip_spi_setup;         // 为特定设备进行设置的函数
    ctlr->cleanup = rockchip_spi_cleanup;     // 清理函数
    ctlr->transfer_one = rockchip_spi_transfer_one; // 执行单次传输的核心函数
    ctlr->max_transfer_size = rockchip_spi_max_transfer_size; // 最大传输大小
    ctlr->handle_err = rockchip_spi_handle_err; // 错误处理函数

    // 10. 请求 DMA 通道(如果设备树配置了的话)
    ctlr->dma_tx = dma_request_chan(rs->dev, "tx");
    if (IS_ERR(ctlr->dma_tx)) {
        // 如果请求失败且不是因为需要延迟探测,则打印警告并禁用 DMA
        if (PTR_ERR(ctlr->dma_tx) != -EPROBE_DEFER)
            dev_warn(rs->dev, "Failed to request TX DMA channel\n");
        ctlr->dma_tx = NULL;
    }
    ctlr->dma_rx = dma_request_chan(rs->dev, "rx");
    if (IS_ERR(ctlr->dma_rx)) {
        if (PTR_ERR(ctlr->dma_rx) != -EPROBE_DEFER)
            dev_warn(rs->dev, "Failed to request RX DMA channel\n");
        ctlr->dma_rx = NULL;
    }
    if (ctlr->dma_tx && ctlr->dma_rx) {
        // 如果 DMA 通道有效,则设置 DMA 地址并指定判断是否可用 DMA 的函数
        rs->dma_addr_tx = mem->start + ROCKCHIP_SPI_TXDR;
        rs->dma_addr_rx = mem->start + ROCKCHIP_SPI_RXDR;
        ctlr->can_dma = rockchip_spi_can_dma;
    }

    // ... (省略了设置延时等代码)

    // 11. 获取并配置引脚(pinctrl)
    pinctrl = devm_pinctrl_get(&pdev->dev);
    if (!IS_ERR(pinctrl)) {
        // 尝试查找高速模式的引脚配置状态
        rs->high_speed_state = pinctrl_lookup_state(pinctrl, "high_speed");
        if (IS_ERR_OR_NULL(rs->high_speed_state)) {
            dev_warn(&pdev->dev, "no high_speed pinctrl state\n");
            rs->high_speed_state = NULL;
        }
    }

    // 12. 将配置好的 SPI 控制器注册到内核的 SPI 子系统中
    ret = devm_spi_register_controller(&pdev->dev, ctlr);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to register controller\n");
        goto err_free_dma_rx;
    }

    return 0; // 初始化成功

    // ... (省略了错误处理的 goto 标签和代码)
}

18.2.4 spi 设备驱动

SPI 总线驱动由硬件供应商提供,我们只需了解和学习其原理。在 SPI 设备驱动中,我们会使用以下函数:

注册函数:在驱动的入口函数中调用,用于注册 SPI 设备。

注销函数:在驱动的出口函数中调用,用于注销 SPI 设备。

这与平台设备驱动和 I2C 设备驱动的注册和注销方式相同。

cpp 复制代码
// 注册 SPI 驱动
int spi_register_driver(struct spi_driver *sdrv);

// 注销 SPI 驱动
static inline void spi_unregister_driver(struct spi_driver *sdrv);

对比 i2c 设备的注册和注销函数,不难发现把"spi"换成"i2c"就是 i2c 设备的注册和注销函数了,并且用法相同。

spi_driver 类型的结构体 (spi 设备驱动结构体),一个 spi_driver 结构体就代表了一个 spi

设备驱动

18.2.4.1 spi_setup() 函数

函数通过调用master->setup设置SPI设备的片选信号、传输单位和最大传输速率等参数。

在rockchip_spi_probe()函数中,初始化了ctlr->setup = rockchip_spi_setup。

cpp 复制代码
// 设置SPI设备的参数(如片选信号、传输单位、最大传输速率等)
int spi_setup(struct spi_device *spi);
18.2.4.2 spi_message_init() 函数
cpp 复制代码
// 初始化SPI消息结构体
static inline void spi_message_init(struct spi_message *m) {
    memset(m, 0, sizeof(*m));          // 清零结构体
    spi_message_init_no_memset(m);     // 进一步初始化
}
18.2.4.3 spi_message_add_tail() 函数
cpp 复制代码
// 将SPI传输添加到消息队列尾部
static inline void spi_message_add_tail(struct spi_transfer *t, // SPI传输结构体指针
                                         struct spi_message *m) // SPI消息结构体指针
{
    list_add_tail(&t->transfer_list, &m->transfers);
}

这个函数就是将 spi_transfer 结构体添加到 spi_message 队列的末尾。

18.2.5 spi 同步与互斥

spi_message通过queue成员将多个spi_message串联,

首个spi_message挂在struct list_head queue下。

spi_message还包含 struct list_head transfers成员,用于串联spi_transfer。

18.2.5.1 SPI 同步传输数据

spi_sync() 通过在内部调用 __spi_sync(),并利用 mutex_lock() 和 mutex_unlock() 实现互斥访问,从而阻塞当前线程以完成 SPI 数据传输。

cpp 复制代码
// 阻塞式SPI同步传输,通过互斥锁保证总线独占
int spi_sync(struct spi_device *spi,        // SPI从设备指针
             struct spi_message *message)   // 包含传输数据和设置的消息

{
    int ret;                                 // 返回值

    mutex_lock(&spi->controller->bus_lock_mutex); // 锁定总线互斥锁
    ret = __spi_sync(spi, message);              // 执行实际传输
    mutex_unlock(&spi->controller->bus_lock_mutex); // 解锁总线互斥锁

    return ret; // 返回传输结果
}

__spi_sync() 函数实现如下:

cpp 复制代码
// __spi_sync - spi_sync()的内部实现,完成实际的同步传输
static int __spi_sync(struct spi_device *spi,        // SPI从设备指针
                      struct spi_message *message)   // 要传输的消息
{
    int status;                                     // 函数返回状态
    struct spi_controller *ctlr = spi->controller;   // SPI控制器指针,缓存起来方便使用
    unsigned long flags;                             // 用于保存中断状态

    // 1. 验证SPI设备和消息的合法性
    status = __spi_validate(spi, message);
    if (status != 0)
        return status;

    // 2. 设置消息的完成回调函数和上下文
    message->complete = spi_complete;  // 设置传输完成时的回调函数
    message->context = &done;          // 设置回调函数的上下文(一个completion对象)
    message->spi = spi;                // 绑定消息到当前SPI设备
    ...
    // 3. 根据控制器的传输类型,选择不同的传输路径
    if (ctlr->transfer == spi_queued_transfer) {
        // 3.1 对于支持队列的控制器:
        // 关闭本地中断并获取spinlock,保护总线访问
        spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags);

        trace_spi_message_submit(message); // 跟踪/调试:记录消息提交事件

        // 将消息提交到控制器的传输队列
        status = __spi_queued_transfer(spi, message, false);

        // 恢复中断状态并释放spinlock
        spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
    } else {
        // 3.2 对于不支持队列的控制器:
        // 直接在当前上下文中执行异步传输(但会等待其完成)
        status = spi_async_locked(spi, message);
    }


    // 4. 等待传输完成
    if (status == 0) {
        ...
        // 阻塞在这里,直到message的complete回调被调用(即传输完成)
        wait_for_completion(&done);
        // 从message中获取最终的传输状态
        status = message->status;
    }

    // 5. 清理现场
    message->context = NULL; // 清除上下文指针,避免悬空引用

    return status; // 返回最终的传输状态
}
18.2.5.2 SPI 异步传输数据
cpp 复制代码
// SPI异步传输的对外接口,会进行参数检查,然后调用内部函数__spi_async发起传输
int spi_async(struct spi_device *spi,        // 指向SPI从设备结构体的指针
              struct spi_message *message)  // 指向包含传输数据和回调的消息结构体的指针
{
    ... // 此处为参数检查等准备工作
    ret = __spi_async(spi, message);
    ... // 此处为可能的后续处理
}

驱动中调用 async 会将 message 结构体加入 SPI 控制器的队列,并触发一个内核工作来异步处理该 message,不阻塞当前进程。

cpp 复制代码
// 发起SPI异步传输,调用控制器transfer方法,不阻塞,通过message回调通知结果
static int __spi_async(struct spi_device *spi,        // 指向SPI从设备的指针
                       struct spi_message *message)  // 包含传输数据和回调信息的指针
{
    struct spi_controller *ctlr = spi->controller;
    ...
    return ctlr->transfer(spi, message);
}

18.3 oled 屏幕驱动实验

18.3.1 硬件介绍

18.3.1.1 硬件连接

OLED 驱动中,lubuncat2 采用 rk3568 的 spi3,其引脚可参考 rk3568 数据手册。

oled 屏和板卡引脚对应连接如下表:

18.3.1.2 设备树插件

在嵌入式 Linux 开发中,用户需要编写的部分主要取决于你使用的开发板和内核版本

一般来说,芯片厂商(如 Rockchip)会提供一个基础的设备树(通常称为xxx.dtsi),其中包含了芯片内部所有外设(如 SPI 控制器)的基本定义 。这个文件你不应该去修改它。

用户需要做的,是在一个针对具体开发板的设备树文件(通常称为xxx.dts)中,对这些外设进行使能和配置

维度 SPI 控制器节点(如 &spi3 PINCTRL 节点(如 &pinctrl
作用层面 描述 SPI 控制器的功能配置(如传输模式、频率、片选等)。 描述 SPI 引脚的物理属性(如引脚复用、电气特性、上下拉等)。
关注内容 SPI 的逻辑功能 :- 是否使能控制器(status = "okay");- 片选引脚映射(cs-gpios);- 传输参数(spi-max-frequency);- 子设备挂载(如spi_oled@0)。 引脚的物理配置 :- 引脚复用功能(如将 GPIO 配置为 SPI_CLK/MOSI/MISO);- 驱动能力(drv_level);- 上下拉电阻(pull-up/pull-down);- 引脚编号映射(如rockchip,pins = <4 RK_PC2 2>)。
依赖关系 依赖 PINCTRL 提供的引脚配置,否则 SPI 控制器无法使用引脚。 不依赖 SPI 控制器,仅负责引脚本身的硬件配置,可被其他外设复用。
配置目标 让 SPI 控制器能正常工作(逻辑层面)。 让 SPI 引脚符合电气规范(物理层面)。
cpp 复制代码
/* 在你的开发板.dts文件中 */

#include "rk3568.dtsi" // 引入芯片厂商提供的基础设备树

/* ... */

&spi3 {
    status = "okay";  // 1. 使能SPI3控制器

    pinctrl-names = "default", "high_speed";
    pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;     // 2. 引用默认模式的引脚配置
    pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>; // 3. 引用高速模式的引脚配置

    cs-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>; // 4. 指定片选引脚

    /* 5. 在这里添加你的OLED设备节点 */
    spi_oled@0 {
        status = "okay";
        compatible = "fire,spi_oled"; /* 必须与你的驱动匹配 */
        reg = <0>;
        spi-max-frequency = <24000000>;
        dc_control_pin = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>; /* 自定义的DC引脚 */

        /* 如果DC引脚需要特殊配置,在这里引用 */
        pinctrl-names = "default";
        pinctrl-0 = <&spi_oled_pin>;
    };
};

/* 6. 如果有自定义的引脚(如OLED的DC引脚),需要在pinctrl节点中定义 */
&pinctrl {
    spi_oled {
        spi_oled_pin: spi_oled_pin {
            rockchip,pins = <
                3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none
            >;
        };
    };
};


在spi设备节点中我们自己定义了DC引脚,为什么只有DC引脚需要自己定义呢 因为DC引脚并不是SPI协议的一部分

以下部分通常是芯片厂商已经写好的 ,位于 .dtsi 文件中,用户无需关心:

SPI 控制器的基本定义:

cpp 复制代码
/*
 * 在 rk3568.dtsi 中
 * SPI3 控制器的基础定义 (厂商提供,用户不应修改)
 * 该节点描述了 SPI3 控制器的硬件特性和资源,但默认处于禁用状态。
 */
spi3: spi@fe6b0000 {
    compatible = "rockchip,rk3568-spi", "rockchip,rk3066-spi"; // 兼容性声明,用于匹配驱动
    reg = <0x0 0xfe6b0000 0x0 0x1000>;                         // SPI3 控制器的寄存器地址和大小
    interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;            // 中断号和触发方式
    clocks = <&cru SCLK_SPI3>, <&cru PCLK_SPI3>;               // 时钟源 (SPI时钟和APB总线时钟)
    clock-names = "spiclk", "apb_pclk";                         // 时钟名称
    resets = <&cru SRST_SPI3>;                                  // 复位控制器
    reset-names = "spi";                                        // 复位名称
    #address-cells = <1>;                                       // 子节点地址 cells 数量 (SPI从设备地址)
    #size-cells = <0>;                                          // 子节点大小 cells 数量 (无)
    dmas = <&dmac 26>, <&dmac 27>;                              // DMA 通道 (发送和接收)
    dma-names = "tx", "rx";                                     // DMA 通道名称
    pinctrl-names = "default";                                  // 默认引脚配置名称
    pinctrl-0 = <&spi3m0_pins>;                                 // 默认引脚配置 (通常为一种模式)
    status = "disabled";                                        // 默认状态为禁用
};

标准的引脚配置:

cpp 复制代码
/* 在 rk3568.dtsi 中 */
spi3 {
    spi3m1_pins: spi3m1-pins {
        rockchip,pins =
            <4 RK_PC2 2 &pcfg_pull_none>, /* spi3_clkm1 */
            <4 RK_PC5 2 &pcfg_pull_none>, /* spi3_misom1 */
            <4 RK_PC3 2 &pcfg_pull_none>; /* spi3_mosim1 */
    };
    spi3m1_cs0: spi3m1-cs0 {
        rockchip,pins =
            <4 RK_PC6 2 &pcfg_pull_none>; /* spi3_cs0m1 */
    };
    /* ... 其他模式和片选的定义 ... */
};
设备树插件对oled设备节点的描述

设备树插件书写格式不变,我们重点讲解 spi_oled 设备节点。

spi_oled 设备树插件,linux_driver/SPI_OLED/lubancat-spi3-m1-oled-overlay.dts。

cpp 复制代码
/*
 * Copyright (C) 2022 - All Rights Reserved by
 * EmbedFire LubanCat
 */
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/clock/rk3568-cru.h>
#include <dt-bindings/interrupt-controller/irq.h>

/*
 * &spi3: 引用并配置RK3568的SPI3控制器
 */
&spi3 {
    status = "okay";                      // 使能SPI3控制器

    pinctrl-names = "default", "high_speed"; // 定义两种引脚配置状态
    pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;  // "default"状态下使用的引脚
    pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>; // "high_speed"状态下使用的引脚

    cs-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>; // 指定CS(Chip Select)引脚为GPIO4_PC6,低电平有效

    /*
     * spi_oled@0: 在SPI3总线上定义一个OLED设备节点
     * @0: 表示该设备连接在SPI3的片选0上
     */
    spi_oled@0 {
        status = "okay";                  // 使能这个OLED设备节点

        compatible = "fire,spi_oled";     // 设备兼容属性,用于匹配Linux内核中的驱动程序

        reg = <0>;                       // 设备地址,对应SPI的片选线(CS0)

        spi-max-frequency = <24000000>;   // SPI通信的最大频率为24MHz

        dc_control_pin = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>; // OLED的DC(Data/Command)控制引脚

        pinctrl-names = "default";        // 定义该设备的引脚配置状态
        pinctrl-0 = <&spi_oled_pin>;      // "default"状态下使用的引脚,这里特指DC引脚
    };
};

/*
 * &pinctrl: 引用并配置引脚控制器
 */
&pinctrl {
    /*
     * spi_oled: 定义一个新的引脚配置组
     */
    spi_oled {
        /*
         * spi_oled_pin: 定义具体的引脚配置
         */
        spi_oled_pin: spi_oled_pin {
            rockchip,pins = <
                3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none
            >;
            /*
             * 3 RK_PA7: GPIO3组的PA7引脚
             * RK_FUNC_GPIO: 将该引脚配置为GPIO功能
             * &pcfg_pull_none: 引脚配置为无上拉/下拉
             */
        };
    };
};

设备树节点中使能 spi3 控制器,并通过 pinctrl 配置其使用的引脚(包括默认和高速模式),同时指定了 cs 引脚。

cpp 复制代码
14  status = "okay";
15  pinctrl-names = "default", "high_speed";
16  pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;        //spi3m1_cs0定义了 cs引脚配置
                                                   //spi3m1_pins定义了 SCLK(时钟)、MOSI(主发从收)、MISO(主收从发)等核心信号引脚的配置
17  pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>;
18  cs-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>;//指定一个 GPIO 引脚作为 spi3 的片选信号(CS)
                                               //gpio4 组的 RK_PC6 引脚,并且是低电平有效(GPIO_ACTIVE_LOW)
设备树pinctrl对oled引脚的描述

设备树 pinctrl 描 述 (内 核 源码/arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi)

cpp 复制代码
/* SPI3 引脚配置(普通模式) */
spi3 {
    /* 普通模式引脚组:SCLK、MISO、MOSI */
    spi3m1_pins: spi3m1-pins {
        rockchip,pins =
            /* spi3_clkm1:GPIO4_PC2,复用为SPI3_CLK,无上下拉 */
            <4 RK_PC2 2 &pcfg_pull_none>,
            /* spi3_misom1:GPIO4_PC5,复用为SPI3_MISO,无上下拉 */
            <4 RK_PC5 2 &pcfg_pull_none>,
            /* spi3_mosim1:GPIO4_PC3,复用为SPI3_MOSI,无上下拉 */
            <4 RK_PC3 2 &pcfg_pull_none>;
    };

    /* 普通模式片选0(CS0)引脚 */
    spi3m1_cs0: spi3m1-cs0 {
        rockchip,pins =
            /* spi3_cs0m1:GPIO4_PC6,复用为SPI3_CS0,无上下拉 */
            <4 RK_PC6 2 &pcfg_pull_none>;
    };

    /* 普通模式片选1(CS1)引脚 */
    spi3m1_cs1: spi3m1-cs1 {
        rockchip,pins =
            /* spi3_cs1m1:GPIO4_PD1,复用为SPI3_CS1,无上下拉 */
            <4 RK_PD1 2 &pcfg_pull_none>;
    };
};

/* SPI3 引脚配置(高速模式) */
spi3-hs {
    /* 高速模式引脚组:SCLK、MISO、MOSI(提升驱动能力) */
    spi3m1_pins_hs: spi3m1-pins {
        rockchip,pins =
            /* spi3_clkm1:GPIO4_PC2,复用为SPI3_CLK,上拉+驱动等级1 */
            <4 RK_PC2 2 &pcfg_pull_up_drv_level_1>,
            /* spi3_misom1:GPIO4_PC5,复用为SPI3_MISO,上拉+驱动等级1 */
            <4 RK_PC5 2 &pcfg_pull_up_drv_level_1>,
            /* spi3_mosim1:GPIO4_PC3,复用为SPI3_MOSI,上拉+驱动等级1 */
            <4 RK_PC3 2 &pcfg_pull_up_drv_level_1>;
    };

    /* 高速模式片选0(CS0)引脚(提升驱动能力) */
    spi3m1_cs0_hs: spi3m1-cs0 {
        rockchip,pins =
            /* spi3_cs0m1:GPIO4_PC6,复用为SPI3_CS0,上拉+驱动等级1 */
            <4 RK_PC6 2 &pcfg_pull_up_drv_level_1>;
    };

    /* 高速模式片选1(CS1)引脚(提升驱动能力) */
    spi3m1_cs1_hs: spi3m1-cs1 {
        rockchip,pins =
            /* spi3_cs1m1:GPIO4_PD1,复用为SPI3_CS1,上拉+驱动等级1 */
            <4 RK_PD1 2 &pcfg_pull_up_drv_level_1>;
    };
};

引脚配置参考:向 pinctrl 子系统添加引脚的具体操作,可参考 GPIO 子系统章节。

引脚对应关系:设备树中 SPI3 的引脚(SCLK、MOSI、CS)需与 OLED 屏的对应引脚连接,具体功能和开发板位置参考前文表格。

MISO 引脚处理:OLED 屏无 MISO 引脚,无需连接,设备树中无需额外配置。

DC 引脚功能:OLED 需额外连接 DC 引脚(数据 / 命令选择),高电平表示数据,低电平表示控制命令,需在设备树中定义该引脚的 GPIO 配置。
不同的设备树节点中对 reg属性的定义不同 在 iic和spi 节点中,reg 表示片选通道

18.3.2 实验代码讲解

18.3.2.1 编程思路
18.3.2.2 驱动的入口和出口函数实现

SPI OLED 驱动的入口 / 出口函数与 I2C MPU6050 驱动结构相似,仅将 I2C 相关接口替换为 SPI 对应接口,核心逻辑一致。

驱动入口函数实现(linux_driver/SPI_OLED/spi_oled.c)

cpp 复制代码
/* 指定 ID 匹配表 */
static const struct spi_device_id oled_device_id[] = {
    {"fire,spi_oled", 0},  // 传统匹配方式:匹配的设备名称
    {}                      // 必须以空条目结尾
};

/* 指定设备树匹配表 */
static const struct of_device_id oled_of_match_table[] = {
    {.compatible = "fire,spi_oled"}, // 设备树匹配方式:与设备树中的 compatible 属性对应
    {}                               // 必须以空条目结尾
};

/* spi 总线设备结构体 */
struct spi_driver oled_driver = {
    .probe = oled_probe,             // 设备探测函数,匹配成功后调用
    .remove = oled_remove,           // 设备移除函数,驱动卸载时调用
    .id_table = oled_device_id,      // 指向传统 SPI 设备 ID 匹配表
    .driver = {
        .name = "spi_oled",          // 驱动名称,用于 sysfs 和驱动绑定
        .owner = THIS_MODULE,        // 驱动模块所有者,用于模块计数
        .of_match_table = of_match_ptr(oled_of_match_table), // 指向设备树匹配表
    },
};

/*
 * 驱动初始化函数
 * 模块加载时内核自动调用,负责注册字符设备和 SPI 驱动
 */
static int __init oled_driver_init(void)
{
    int error;
    int ret = -1; // 保存错误状态码

    pr_info("oled_driver_init\n");

    /* --------------------- 注册字符设备部分 ----------------- */

    // 1. 动态分配设备编号 (主设备号, 次设备号起始, 设备数量, 设备名称)
    ret = alloc_chrdev_region(&oled_devno, 0, DEV_CNT, DEV_NAME);
    if (ret < 0) {
        printk("fail to alloc oled_devno\n");
        goto alloc_err; // 分配失败,跳转到错误处理
    }

    // 2. 初始化 cdev 结构体,并关联文件操作接口
    oled_chr_dev.owner = THIS_MODULE;
    cdev_init(&oled_chr_dev, &oled_chr_dev_fops);

    // 3. 将字符设备添加到内核,使其对系统可见
    ret = cdev_add(&oled_chr_dev, oled_devno, DEV_CNT);
    if (ret < 0) {
        printk("fail to add cdev\n");
        goto add_err; // 添加失败,跳转到错误处理
    }

    /* 创建类 */
    // 在 /sys/class/ 目录下创建一个名为 DEV_NAME 的目录
    class_oled = class_create(THIS_MODULE, DEV_NAME);

    /* 创建设备 */
    // 在 /dev/ 目录下创建一个名为 DEV_NAME 的设备节点
    device_oled = device_create(class_oled, NULL, oled_devno, NULL, DEV_NAME);

    /* --------------------- 注册 SPI 驱动部分 ----------------- */

    // 向 SPI 总线子系统注册我们的驱动
    error = spi_register_driver(&oled_driver);
    if (error < 0) {
        // SPI 驱动注册失败,需要清理前面已创建的字符设备相关资源
        device_destroy(class_oled, oled_devno); // 清除 /dev 下的设备节点
        class_destroy(class_oled);             // 清除 /sys/class 下的类
        cdev_del(&oled_chr_dev);               // 从内核中删除字符设备
        unregister_chrdev_region(oled_devno, DEV_CNT); // 注销设备编号
    }

    return error; // 返回 SPI 驱动注册的结果

add_err:
    // cdev_add 失败时,注销已分配的设备编号
    unregister_chrdev_region(oled_devno, DEV_CNT);
    printk(" error! \n");

alloc_err:
    // alloc_chrdev_region 失败时,直接返回错误码
    return -1;
}

/*
 * 驱动注销函数
 * 模块卸载时内核自动调用,负责释放所有已申请的资源
 */
static void __exit oled_driver_exit(void)
{
    pr_info("oled_driver_exit\n");

    // 1. 从 SPI 总线子系统注销驱动
    spi_unregister_driver(&oled_driver);

    // 2. 释放可能使用的 GPIO 引脚
    gpio_free(oled_control_pin_number);

    /* 删除设备 */
    // 按与创建时相反的顺序释放资源
    device_destroy(class_oled, oled_devno); // 清除 /dev 下的设备节点
    class_destroy(class_oled);             // 清除 /sys/class 下的类
    cdev_del(&oled_chr_dev);               // 从内核中删除字符设备
    unregister_chrdev_region(oled_devno, DEV_CNT); // 注销设备编号
}

xxx_device_id,传统驱动模型,通过设备名称进行匹配
xxx_of_match_table,设备树驱动模型,通过设备树节点的 compatible属性匹配

spi_driver 结构体

xxx_driver_init 入口函数负责
【字符设备的注册】、
【SPI驱动的注册】 xxx_driver_exit 出口函数负责,
注销注册的驱动,
注销注册的字符设备并清理

传统设备模型和设备树模型的区别

对比维度 传统驱动模型 设备树模型
硬件信息存储位置 驱动代码中(硬编码)或 platform_device 结构体。 独立的设备树文件(.dts/.dtb),与驱动分离。
驱动与硬件的耦合度 高耦合:硬件信息变更需修改驱动代码,重新编译驱动。 低耦合:硬件信息变更仅需修改设备树文件,无需改动驱动。
匹配方式 通过 id_table(设备名称)或平台设备名称匹配。 通过 compatible 属性(设备树节点与驱动的 of_match_table 匹配)。
硬件拓扑描述 无法描述复杂总线拓扑(如 SPI 子设备、I2C 从设备的层级关系)。 支持复杂总线拓扑,可清晰描述设备间的连接关系(如 SPI 控制器下挂多个 OLED 设备)。
资源配置方式 驱动代码中直接指定资源(如 request_mem_region 申请寄存器地址)。 驱动从设备树节点中获取资源(如 of_address_to_resource 解析寄存器地址)。
适用硬件复杂度 适合简单硬件(如单个 GPIO、UART 设备)。 适合复杂硬件(如多核心 CPU、外设密集型嵌入式系统)。
内核依赖 依赖内核版本较低,无设备树解析模块。 依赖内核支持设备树(CONFIG_OF 配置),需内核解析 .dtb 文件。示例

示例(以 SPI OLED 驱动为例)

传统驱动模型

传统驱动模型硬件信息和驱动匹配代码可以在一个文件中也可以在不同文件中,具体看代码组织方式。这也降低了耦合性。

硬件信息硬编码:在驱动代码中直接定义 SPI 设备的片选引脚、时钟频率等

cpp 复制代码
// 传统驱动:硬编码 SPI 设备信息
static struct spi_board_info oled_board_info = {
    .modalias = "fire,spi_oled",  // 与驱动的 id_table 匹配
    .max_speed_hz = 1000000,      // SPI 时钟频率
    .bus_num = 0,                 // SPI 总线号
    .chip_select = 0,             // 片选引脚
};

// 注册平台设备
static int __init oled_init(void) {
    spi_register_board_info(&oled_board_info, 1);
    return 0;
}

驱动匹配:驱动通过 id_table 匹配设备名称:

cpp 复制代码
static const struct spi_device_id oled_device_id[] = {
    {"fire,spi_oled", 0},  // 与平台设备的 modalias 匹配
    {}
};

设备树模型

硬件信息在设备树中定义(xxx.dts 文件):

cpp 复制代码
// 设备树:描述 SPI 控制器和 OLED 子设备
spi@10000000 {  // SPI 控制器节点
    compatible = "vendor,spi-controller";
    reg = <0x10000000 0x1000>;  // 寄存器地址和大小
    interrupts = <10>;           // 中断号
    #address-cells = <1>;
    #size-cells = <0>;

    oled@0 {  // OLED 子设备节点
        compatible = "fire,spi_oled";  // 与驱动的 of_match_table 匹配
        reg = <0>;                     // 片选编号
        spi-max-frequency = <1000000>; // SPI 最大时钟频率
        reset-gpios = <&gpio 1 0>;     // 复位 GPIO 引脚
    };
};
cpp 复制代码
// 驱动:从设备树节点解析资源
static int oled_probe(struct spi_device *spi) {
    struct device *dev = &spi->dev;
    struct device_node *node = dev->of_node;
    int reset_gpio;

    // 从设备树获取复位 GPIO
    reset_gpio = of_get_named_gpio_flags(node, "reset-gpios", 0, NULL);
    if (!gpio_is_valid(reset_gpio)) {
        dev_err(dev, "failed to get reset gpio\n");
        return -EINVAL;
    }

    // 申请 GPIO
    gpio_request(reset_gpio, "oled_reset");
    gpio_direction_output(reset_gpio, 1);  // 复位引脚拉高

    // 其他初始化...
    return 0;
}

// 驱动匹配设备树的 compatible 属性
static const struct of_device_id oled_of_match_table[] = {
    {.compatible = "fire,spi_oled"},
    {}
};
18.3.2.3 .prob 函数实现

在.prob 函数中完成两个主要工作是,申请 gpio 控制 D/C 引脚和初始化 spi。

cpp 复制代码
/**
 * @brief 驱动的设备探测函数
 * @param spi 指向与驱动匹配成功的 SPI 设备结构体的指针
 * @return 成功返回 0,失败返回负数错误码
 */
static int oled_probe(struct spi_device *spi)
{
    // 从 SPI 设备结构体中获取对应的设备树节点指针
    struct device_node *node = spi->dev.of_node;

    printk(KERN_EMERG "match successed \n");

    /* --------------------- 1. 初始化控制 GPIO (D/C 引脚) ----------------- */

    // 1.1 从设备树节点 "dc_control_pin" 属性中获取 GPIO 编号
    oled_control_pin_number = of_get_named_gpio(node, "dc_control_pin", 0);

    printk("oled_control_pin_number = %d,\n ", oled_control_pin_number);

    // 1.2 向内核申请使用该 GPIO 引脚
    gpio_request(oled_control_pin_number, "dc_control_pin");

    // 1.3 将该 GPIO 引脚设置为输出模式,并设置初始电平为高电平
    gpio_direction_output(oled_control_pin_number, 1);

    /* --------------------- 2. 初始化 SPI 通信参数 ----------------- */

    // 2.1 将 spi 设备结构体指针保存到全局变量,以便其他函数(如 file_operations)使用
    oled_spi_device = spi;

    // 2.2 设置 SPI 通信模式为 MODE_0 (CPOL=0, CPHA=0)
    oled_spi_device->mode = SPI_MODE_0;

    // 2.3 设置 SPI 通信的最大时钟频率为 2MHz
    oled_spi_device->max_speed_hz = 2000000;

    // 2.4 将上述配置应用到 SPI 控制器
    spi_setup(oled_spi_device);

    /* --------------------- 3. 打印调试信息 ----------------- */

    printk("max_speed_hz = %d\n", oled_spi_device->max_speed_hz);
    printk("chip_select = %d\n", (int)oled_spi_device->chip_select);
    printk("bits_per_word = %d\n", (int)oled_spi_device->bits_per_word);
    printk("mode = %02X\n", oled_spi_device->mode);
    printk("cs_gpio = %02X\n", oled_spi_device->cs_gpio);

    // 返回 0,表示探测成功
    return 0;
}
18.3.2.4 字符设备操作函数集实现

字符设备操作函数集是驱动对外接口,实现三个核心函数:

.open 初始化 spi_oled,

.write 写入显示数据,

.release 关闭设备。

.open 函数实现

在 open 函数中完成 spi_oled 的初始化,

cpp 复制代码
/*
 * 字符设备 open 操作函数
 * 用户空间调用 open 时触发 OLED 初始化。
 */
static int oled_open(struct inode *inode, struct file *filp)
{
    spi_oled_init(); // 调用 OLED 初始化函数
    return 0; // 返回成功
}

/*
 * OLED 屏幕核心初始化函数
 * 发送初始化指令并清屏。
 */
void spi_oled_init(void)
{
    // 发送预定义的初始化指令序列
    oled_send_command(oled_spi_device, oled_init_data, sizeof(oled_init_data));

    // 初始化完成后清屏(0x00 通常为黑色)
    oled_fill(0x00);
}

/*
 * 通过 SPI 总线向 OLED 控制器发送命令序列。
 * @param spi_device: 指向已初始化的 SPI 设备结构体的指针
 * @param commands: 指向待发送命令字节数组的指针
 * @param length: 命令序列的长度(字节数)
 * @return: 成功返回 0,失败返回 -1
 */
static int oled_send_command(struct spi_device *spi_device, u8 *commands, u16 length)
{
    int error = 0;
    struct spi_message message;  // SPI 消息结构体
    struct spi_transfer transfer; // SPI 传输结构体

    // 1. 申请内核内存
    message = kzalloc(sizeof(struct spi_message), GFP_KERNEL);
    transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);

    // 2. 设置 D/C 引脚为低电平(表示接下来发送的是命令)
    gpio_direction_output(oled_control_pin_number, 0);

    // 3. 填充 SPI 传输和消息结构体
    transfer->tx_buf = commands; // 要发送的命令缓冲区
    transfer->len = length;      // 命令长度
    spi_message_init(message);   // 初始化消息
    spi_message_add_tail(&transfer, message); // 将传输添加到消息队列

    // 4. 同步发送 SPI 消息
    error = spi_sync(spi_device, message);

    // 5. 释放内存
    kfree(message);
    kfree(transfer);

    // 6. 检查结果并返回
    if (error != 0) {
        printk("spi_sync error! \n");
        return -1;
    }
    return error;
}

open 函数调用 spi_oled_init 函数,后者通过 oled_send_command 初始化 spi_oled 并调用清屏函数。oled_send_command 核心流程如下:

定义 spi_message 和 spi_transfer 结构体;

用 kzalloc 分配内核空间(约 100 字节,节省栈空间);

置 D/C 引脚为低电平(标识发送命令);

初始化传输与消息结构体,添加传输到消息队列,调用 spi_sync 同步发送;

释放已分配空间。

.write 函数实现

.write 函数用于接收来自应用程序的数据,并显示这些数据。

cpp 复制代码
/* 字符设备 write 操作:将用户空间数据写入 OLED 并显示 */
static int oled_write(
    struct file *filp,                 // 文件指针
    const char __user *buf,            // 用户空间数据缓冲区
    size_t cnt,                        // 数据长度
    loff_t *off                        // 文件偏移量(未使用)
)
{
    int copy_number = 0;
    oled_display_struct *write_data;   // 指向显示数据结构体的指针

    /* 1. 在内核中申请内存 */
    write_data = (oled_display_struct*)kzalloc(cnt, GFP_KERNEL);
    if (!write_data) {
        return -ENOMEM; // 内存分配失败
    }

    /* 2. 从用户空间拷贝数据到内核空间 */
    copy_number = copy_from_user(write_data, buf, cnt);

    /* 3. 调用显示函数,将数据显示到 OLED 指定位置 */
    oled_display_buffer(write_data->display_buffer, write_data->x, write_data->y, write_data->length);

    /* 4. 释放内核内存 */
    kfree(write_data);

    return 0;
}

/* 将指定缓冲区的数据显示到 OLED 屏幕的指定位置 (函数声明) */
static int oled_display_buffer(
    u8 *display_buffer,  // 待显示的数据缓冲区
    u8 x,                // 显示起始 X 坐标
    u8 y,                // 显示起始 Y 坐标
    u16 length           // 待显示数据的长度
);

/**
 * 自定义 OLED 显示数据结构体
 * 这是一个变长结构体,display_buffer 的大小不固定。
 */
typedef struct oled_display_struct
{
    u8 x;                 // 显示起始 X 坐标
    u8 y;                 // 显示起始 Y 坐标
    u32 length;           // 要显示的数据长度
    u8 display_buffer[];  // 柔性数组成员,存储实际显示数据
} oled_display_struct;
.release 函数实现

release 函数功能仅仅是向 spi_oled 显示屏发送关闭显示命令

cpp 复制代码
/* 字符设备 release 操作函数 */
static int oled_release(struct inode *inode, struct file *filp)
{
    // 发送 0xAE 命令关闭 OLED 显示
    oled_send_command(oled_spi_device, 0xae); 
    return 0; // 成功关闭
}
18.3.2.5 编写测试应用程序
Makefile 文件
bash 复制代码
# 定义输出文件名
out_file_name = "test_app"

# 默认目标:编译所有文件
all: test_app.c oled_code_table.c
	# 使用 aarch64 交叉编译器编译测试程序
	aarch64-linux-gnu-gcc $^ -o $(out_file_name)

# 伪目标:清理编译产物
.PHONY: clean
clean:
	# 删除生成的可执行文件
	rm $(out_file_name)
测试程序源码 (test_app.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// 点阵数据(从 oled_code_table.c 中引入)
extern unsigned char F16x16[];      // 16x16 点阵汉字数据
extern unsigned char F6x8[][6];     // 6x8 点阵ASCII字符数据
extern unsigned char F8x16[][16];   // 8x16 点阵ASCII字符数据
extern unsigned char BMP1[];        // 图片点阵数据

// 屏幕尺寸定义(需与实际OLED匹配)
#define X_WIDTH 128
#define Y_WIDTH 64

// 函数声明
void oled_fill(int fd, int x1, int y1, int x2, int y2, unsigned char dot);
void oled_show_F16X16_letter(int fd, int x, int y, unsigned char *letter, int num);
void oled_show_F8X16_string(int fd, int x, int y, char *str);
void oled_show_F6X8_string(int fd, int x, int y, char *str);
void show_bmp(int fd, int x, int y, unsigned char *bmp, int length);

/**
 * @brief 主测试函数
 */
int main(int argc, char *argv[])
{
    int fd;

    // 1. 打开 OLED 设备节点
    fd = open("/dev/spi_oled", O_RDWR);
    if (fd < 0) {
        perror("Failed to open device"); // 使用 perror 打印更详细的错误信息
        return -1;
    }

    // 2. 循环执行测试用例
    while(1)
    {
        // 2.1 显示图片
        show_bmp(fd, 0, 0, BMP1, X_WIDTH * Y_WIDTH / 8);
        sleep(2); // 显示2秒
        oled_fill(fd, 0, 0, X_WIDTH-1, Y_WIDTH-1, 0x00); // 清屏

        // 2.2 显示不同字体的文字
        oled_show_F16X16_letter(fd, 0, 0, F16x16, 4); // 显示4个16x16汉字
        oled_show_F8X16_string(fd, 0, 2, "F8X16:THIS IS SPI TEST APP"); // 显示8x16字符串
        oled_show_F6X8_string(fd, 0, 6, "F6X8:THIS IS SPI TEST APP");   // 显示6x8字符串
        sleep(2); // 显示2秒
        oled_fill(fd, 0, 0, X_WIDTH-1, Y_WIDTH-1, 0x00); // 清屏

        // 2.3 显示测试完成提示
        oled_show_F8X16_string(fd, 0, 0, "Testing is completed");
        sleep(2); // 显示2秒
        oled_fill(fd, 0, 0, X_WIDTH-1, Y_WIDTH-1, 0x00); // 清屏
    }

    // 3. 关闭设备文件(注:由于上面是无限循环,此处代码实际上不会被执行)
    close(fd);
    return 0;
}

18.3.3 实验准备

18.3.3.1 编译设备树插件

一、编译命令(按芯片系列选择)

  1. RK3588 系列(如 lubancat4)

在内核源码根目录执行:

bash 复制代码
# 1. 加载默认配置
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig

# 2. 编译设备树插件
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

二、编译产物

编译成功后,生成的 OLED 设备树插件文件为:

cpp 复制代码
内核源码目录/arch/arm64/boot/dts/rockchip/overlays/lubancat-spi3-m1-oled-overlay.dtbo

(.dtbo 是设备树插件的标准后缀,用于动态加载扩展硬件配置)

18.3.3.2 编译驱动程序

将 linux_driver/SPI_OLED/ 放到内核源码同级目录,执行里面的 MakeFile,生成 spi_oled.ko。

18.3.3.3 编译应用程序

18.3.4 程序运行结果

将前面生成的设备树插件、驱动程序、应用程序通过 scp 等方式拷贝到开发板。

18.3.4.1 加载设备树和驱动文件

加载驱动陈程序使用命令 sudo insmod spi_oled.ko , 将会打印 match successed 和 spi 相关信息:

18.3.4.2 测试效果

驱动加载成功后直接运行测试应用程序 sudo ./test_app ,正常情况下显示屏会显示并自动切换设定的内容,如下所示:

第 19 章 linux 电源管理

19.1 Suspend

bash 复制代码
#查看系统支持的电源状态
cat /sys/power/state

从用户视角,系统休眠(sleep/hibernate)核心是保存上下文、挂起(suspend)系统,后续可通过唤醒(wakeup)恢复。

四种电源状态说明(按功耗节省递增、唤醒耗时递增排序)

freeze:冻结 I/O 设备至低功耗,处理器进入空闲(S2Idle 状态),设备中断即可唤醒。

standby:冻结 I/O 设备 + 暂停系统,核心单元上电不丢状态,恢复需平台设置唤醒源。

mem(STR):状态数据存内存,仅内存自刷新保数据,其他设备断电,恢复需重新配置,需平台设置唤醒源。

disk(STD):上下文存非易失磁盘后完全掉电,唤醒过程最慢(如按电源键唤醒)。

状态 核心操作 数据存储位置 设备供电情况 唤醒条件 / 方式 唤醒速度 功耗节省程度
freeze 冻结 I/O 设备,处理器进入空闲(S2Idle) 无额外存储 I/O 设备低功耗,处理器空闲,核心上电 设备中断即可唤醒 最快 最低
standby 冻结 I/O 设备 + 暂停系统 无额外存储 核心单元上电,I/O 设备低功耗 需平台设置唤醒源 较快 较低
mem(STR) 保存状态数据,挂起系统 内存 仅内存自刷新供电,其他设备断电 需平台设置唤醒源 较慢 较高
disk(STD) 保存上下文数据,系统完全掉电 磁盘(非易失) 全系统断电 按电源键等硬件触发 最慢 最高

处理器空闲就是CPU任务可执行,仅 CPU 层面省电,系统核心仍在维持基础运行

暂停系统就是处理器停止运行非必要任务,核心逻辑暂停运行。

状态类型 CPU 状态 系统核心状态 供电情况 状态保留 唤醒方式
处理器进入空闲 无任务时低功耗待命 正常运行 全系统正常供电 完全保留 设备中断、新任务调度即可触发
暂停系统 停止非必要任务 暂停运行 核心单元上电,外设低功耗 完全保留 需平台预设唤醒源(如特定按键、定时器)
挂起系统(mem) 停止工作 停止运行 仅内存供电,其他断电 内存保留 需平台预设唤醒源(如特定按键、定时器)
完全掉电(disk) 停止工作 停止运行 全系统断电 磁盘保留 硬件触发(如按下电源键)

19.2 Regulator Framework

Regulator(调节器,含电压 / 电流调节器)是 Linux 内核电源管理的底层基础设施,是抽象概念。Regulator Framework 框架通过动态调整输出电压 / 电流实现省电,为设备(consumer,使用者)提供获取电压、使能 / 关闭电源等统一接口,也支持电源提供者(provider)注册及驱动接口扩展。

框架核心分为四部分:

machine:定义 regulator 硬件制约与映射关系;

regulator:即 regulator 驱动;

consumer:regulator 的服务对象(用电设备);

sys-class-regulator:用户空间操作接口。

19.2.1 Regulator 驱动

Regulator 驱动核心是注册电源提供者(如 PMIC)及相关操作函数,

struct regulator_desc 结构体用于静态描述 PMIC 提供的单个调节器。

regulator_desc结构体
cpp 复制代码
/*
对一个物理调节器(如PMIC中的一个电压输出)的静态描述,
定义了它的能力、特性和如何与硬件交互。
*/
struct regulator_desc {
    const char *name;               // 调节器的名称,用于标识和查找
    const char *supply_name;        // 父调节器的名称,用于级联供电
    const char *of_match;           // 用于匹配设备树中 regulator 节点的名称
    const char *regulators_node;    // 设备树中用于自动解析初始化数据的节点路径
    /* ... 其他成员 ... */
    int id;                         // 调节器在其提供者(如PMIC)内部的唯一标识ID

    unsigned n_voltages;            // 可配置的电压档位数量(固定电压源设为1)
    const struct regulator_ops *ops;// 指向该调节器的操作函数集
    int irq;                        // 与该调节器相关的中断号
    enum regulator_type type;       // 类型:电压调节器或电流调节器
    struct module *owner;           // 模块所有者,通常为 THIS_MODULE
    unsigned int min_uV;            // 最小输出电压(单位:µV)
    unsigned int uV_step;           // 电压调节步长(单位:µV)
    unsigned int ramp_delay;        // 电压变化后的稳定时间(单位:通常为µs)
    /* ... 其他成员 ... */
};
regulator_register/unregister
cpp 复制代码
/**
 * regulator_register - 注册一个 regulator 到内核框架
 */
struct regulator_dev *regulator_register(
    struct regulator_desc *regulator_desc, // 指向描述该 regulator 能力的静态结构体
    const struct regulator_config *config  // 指向包含动态配置信息的结构体
);

/**
 * regulator_unregister - 从内核框架中注销一个 regulator
 */
void regulator_unregister(
    struct regulator_dev *rdev // 指向由 regulator_register() 返回的 regulator_dev 对象
);
regulator_dev
cpp 复制代码
struct regulator_dev {
    const struct regulator_desc *desc;    // 指向该regulator的静态描述符
    int exclusive;                        // 独占使用标记
    u32 use_count;                        // 被使用次数计数器
    u32 open_count;                       // 被打开次数计数器
    u32 bypass_count;                     // Bypass模式计数器

    /* lists we belong to */
    struct list_head list;                // 链接所有regulator的链表头

    /* lists we own */
    struct list_head consumer_list;       // 链接所有使用该regulator的consumer链表头

    struct coupling_desc coupling_desc;   // 与其他regulator的耦合描述

    struct blocking_notifier_head notifier; // 通知链,用于向consumer发送事件
    struct mutex mutex;                   // 保护consumer操作的互斥锁
    struct task_struct *mutex_owner;      // 互斥锁当前所有者
    int ref_cnt;                          // 引用计数器
    struct module *owner;                 // 模块所有者
    struct device dev;                    // 内核设备模型中的设备结构体
    struct regulation_constraints *constraints; // 运行时约束条件
    struct regulator *supply;             // 父regulator(供电树)
    const char *supply_name;              // 父regulator名称
    struct regmap *regmap;                // 用于访问硬件寄存器的regmap
    struct delayed_work disable_work;     // 延迟禁用工作队列
    int deferred_disables;                // 延迟禁用计数器
    void *reg_data;                       // 驱动私有数据

    /* ... 其他成员 ... */
};
regulation_constraints
cpp 复制代码
/*regulator的运行时安全限制*/
struct regulation_constraints {
    const char *name;           // 约束名称

    /* 电压输出范围 */
    int min_uV;                 // 最小输出电压 (µV)
    int max_uV;                 // 最大输出电压 (µV)

    int uV_offset;              // 电压偏移量 (µV)

    /* 电流输出范围 */
    int min_uA;                 // 最小输出电流 (µA)
    int max_uA;                 // 最大输出电流 (µA)
    int ilim_uA;                // 电流限制 (µA)

    int system_load;            // 系统负载 (用于耦合调节器)
    int max_spread;             // 最大电压偏差 (用于耦合调节器)

    /* 标志对这个 regulator 有效的操作模式 */
    unsigned int valid_modes_mask; // 有效模式掩码

    /* regulator 有效的操作 */
    unsigned int valid_ops_mask;   // 有效操作掩码

    /* regulator input voltage - only if supply is another regulator */
    int input_uV;               // 输入电压 (仅当供电来自另一个regulator时使用)

    /* ... 其他成员 ... */

    /* 约束标志位 */
    unsigned always_on:1;          // 系统开启时,regulator始终保持开启
    unsigned boot_on:1;            // 由bootloader或固件使能
    unsigned apply_uV:1;           // 当min_uV == max_uV时,强制应用此固定电压
    unsigned ramp_disable:1;       // 禁用电压斜坡延迟
    unsigned soft_start:1;         // 启用软启动(缓慢提升电压)
    unsigned pull_down:1;          // 当regulator关闭时,启用下拉电阻
    unsigned over_current_protection:1; // 启用过流保护(过流时自动关闭)
};

struct regulation_constraints 是对 regulator 的安全限制(如电压 / 电流输出范围),其成员通常与设备树属性对应(如 min_uV 对应 regulator-min-microvolt),约束信息一般从设备树中解析获取。

19.2.2 consumer 接口函数

regulator结构体

consumer 是 regulator 提供服务的对象,使用者。每个 consumer 都有个 regulator 结构体:

cpp 复制代码
struct regulator {
    struct device *dev;                // 指向使用该regulator的设备(consumer)
    struct list_head list;             // 用于将regulator链接到rdev的consumer_list
    unsigned int always_on:1;          // 指示该consumer希望regulator始终保持开启
    unsigned int bypass:1;             // 指示该consumer希望regulator进入bypass模式
    int uA_load;                       // consumer请求的负载电流(µA)
    struct regulator_voltage voltage[REGULATOR_STATES_NUM]; // 不同状态下请求的电压
    const char *supply_name;           // 电源供应器名称
    struct device_attribute dev_attr;  // sysfs属性
    struct regulator_dev *rdev;        // 关联的regulator_dev(实际的regulator提供者)
    struct dentry *debugfs;            // debugfs目录项
};

struct regulator 是 regulator 消费者(consumer)使用的结构体,代表 consumer 对 regulator 的请求和状态。

它不直接操作硬件,而是通过 rdev 指针 与实际的 r**egulator_dev(provider 注册的)**交互。

当 consumer 需请求电压、电流或开关 regulator 时,会填充该结构体对应字段并传递给内核 regulator framework。

常见consumer 接口函数
cpp 复制代码
/* 获取和释放 */

/** 获取指定的 regulator 并返回其结构体指针 */
struct regulator *regulator_get(
    struct device *dev,  // 请求 regulator 的设备指针
    const char *id       // regulator 的名称或标识符
);

/** 释放之前通过 regulator_get 获取的 regulator */
void regulator_put(
    struct regulator *regulator  // 要释放的 regulator 结构体指针
);

/* 使能和关闭 */

/** 使能指定的 regulator */
int regulator_enable(
    struct regulator *regulator  // 要使能的 regulator
);

/** 关闭指定的 regulator */
int regulator_disable(
    struct regulator *regulator  // 要关闭的 regulator
);

/* 设置和获取电压/电流 */

/** 设置 regulator 的输出电压范围 */
int regulator_set_voltage(
    struct regulator *regulator,  // 目标 regulator
    int min_uV,                   // 最小电压 (µV)
    int max_uV                    // 最大电压 (µV)
);

/** 获取 regulator 当前的输出电压 */
int regulator_get_voltage(
    struct regulator *regulator  // 目标 regulator
);

/** 设置 regulator 的输出电流限制范围 */
int regulator_set_current_limit(
    struct regulator *regulator,  // 目标 regulator
    int min_uA,                   // 最小电流限制 (µA)
    int max_uA                    // 最大电流限制 (µA)
);

/* 模式控制 */

/** 根据预期负载电流设置 regulator 到最佳工作模式 */
int regulator_set_optimum_mode(
    struct regulator *regulator,  // 目标 regulator
    int load_uA                   // 预期负载电流 (µA)
);

/** 强制设置 regulator 到指定的工作模式 */
int regulator_set_mode(
    struct regulator *regulator,  // 目标 regulator
    unsigned int mode             // 目标模式 (如 REGULATOR_MODE_NORMAL)
);

/** 获取 regulator 当前的工作模式 */
unsigned int regulator_get_mode(
    struct regulator *regulator  // 目标 regulator
);

19.2.3 用户空间 sysfs 接口

切换到/sys/class/regulator 目录下,使用命令:

该目录下可见已注册的 regulator,均为链接文件,指向平台设备下的具体文件。

切换到其中一个 regulator 目录(如 regulator.14,对应 tcs4525),可看到一系列相关文件。

19.3 源码简单分析

以 lubancat2 为例,rk3568 由 rk809 和 tcs4525 两款 PMIC 供电,通过 I2C 总线控制,其设备树描述(有省略)如下:

tcs4525 设备树描述(内核源码路径:arch/arm64/boot/dts/rockchip/rk3568-lubancat2.dts):

cpp 复制代码
&i2c0 {
    status = "okay";  // 使能 i2c0 总线

    vdd_cpu: tcs4525@1c {  // 定义一个名为 vdd_cpu 的 regulator 节点,对应 i2c 地址 0x1c 的 tcs4525 芯片
        compatible = "tcs,tcs452x";  // 与 tcs452x 系列驱动匹配
        reg = <0x1c>;                // I2C 设备地址

        vin-supply = <&vcc5v0_sys>;  // 该 regulator 的输入电源,由 vcc5v0_sys 提供

        /* 以下为 regulator 约束属性,对应 struct regulation_constraints */
        regulator-name = "vdd_cpu";  // 该 regulator 的名称,用于内核中识别和管理
        regulator-min-microvolt = <712500>;  // 最小输出电压:712.5 mV
        regulator-max-microvolt = <1390000>; // 最大输出电压:1390 mV
        regulator-ramp-delay = <2300>;       // 电压变化率:2300 uV/us (伏/微秒)

        fcs,suspend-voltage-selector = <1>;  // 厂商特定属性,用于选择休眠时的电压

        regulator-boot-on;   // 标记为在 bootloader 阶段就已使能
        regulator-always-on; // 标记为系统运行期间必须始终保持开启,不能被关闭

        // 定义在不同系统状态下的行为
        regulator-state-mem {
            regulator-off-in-suspend; // 在系统休眠(suspend to RAM)时,关闭该 regulator
        };
    };

    /* ... 其他 i2c 设备 ... */
};

I2C0 节点下的 tcs4525 被描述为 regulator,其 regulator-* 前缀字段为 regulator 特有属性。

compatible 属性匹配 I2C 驱动 fan53555_regulator_driver,

触发 fan53555_regulator_probe() 初始化,最终通过 devm_regulator_register() 完成 regulator 注册(详见内核源码 drivers/regulator/fan53555.c)。

内核源码路径:arch/arm64/boot/dts/rockchip/rk3568-lubancat2.dts

cpp 复制代码
&i2c0 {
    /* ... 其他设备 ... */

    rk809: pmic@20 {  // 定义一个名为 rk809 的 pmic 节点,对应 i2c 地址 0x20 的芯片
        compatible = "rockchip,rk809";  // 与 rockchip rk809 驱动匹配
        reg = <0x20>;                   // I2C 设备地址

        interrupt-parent = <&gpio0>;    // 中断父控制器为 gpio0
        interrupts = <3 IRQ_TYPE_LEVEL_LOW>; // 中断引脚为 GPIO0_3,低电平触发

        // 定义不同状态下的引脚配置
        pinctrl-names = "default", "pmic-sleep", "pmic-power-off", "pmic-reset";
        pinctrl-0 = <&pmic_int>;                  // 默认状态引脚配置
        pinctrl-1 = <&soc_slppin_slp>, <&rk817_slppin_slp>; // 睡眠状态
        pinctrl-2 = <&soc_slppin_gpio>, <&rk817_slppin_pwrdn>; // 关机状态
        pinctrl-3 = <&soc_slppin_gpio>, <&rk817_slppin_rst>; // 复位状态

        /* ... 其他配置 ... */

        // 定义 rk809 内部各电源轨的输入电源(均由 vcc3v3_sys 提供)
        vcc1-supply = <&vcc3v3_sys>;
        vcc2-supply = <&vcc3v3_sys>;
        vcc3-supply = <&vcc3v3_sys>;
        vcc4-supply = <&vcc3v3_sys>;
        vcc5-supply = <&vcc3v3_sys>;
        vcc6-supply = <&vcc3v3_sys>;
        vcc7-supply = <&vcc3v3_sys>;
        vcc8-supply = <&vcc3v3_sys>;
        vcc9-supply = <&vcc3v3_sys>;

        pwrkey {         // 电源键配置
            status = "okay"; // 使能电源键功能
        };

        /* ... 其他子节点 ... */

        //  regulators 子节点,定义了 rk809 内部的所有电源输出
        //  包含 5 路大电流 BUCK,9 个 LDO、2 个 SWITCH
        regulators {
            vdd_logic: DCDC_REG1 { // 定义名为 vdd_logic 的 DCDC 输出
                regulator-always-on;      // 始终保持开启
                regulator-boot-on;        //  bootloader 阶段使能
                regulator-min-microvolt = <500000>;  // 最小输出电压:500mV
                regulator-max-microvolt = <1350000>; // 最大输出电压:1350mV
                regulator-init-microvolt = <900000>; // 初始输出电压:900mV
                regulator-ramp-delay = <6001>;      // 电压变化率
                regulator-initial-mode = <0x2>;     // 初始工作模式

                regulator-state-mem {
                    regulator-off-in-suspend; // 休眠时关闭
                };
            };

            /* ... 其他 regulator 定义 ... */
        };

        //  codec 子节点,描述 rk809 集成的音频编解码器
        rk809_codec: codec {
            #sound-dai-cells = <0>;
            compatible = "rockchip,rk809-codec", "rockchip,rk817-codec";
            clocks = <&cru I2S1_MCLKOUT_TX>;
            clock-names = "mclk";
            assigned-clocks = <&cru I2S1_MCLKOUT_TX>, <&cru I2S1_MCLK_TX_IOE>;
            assigned-clock-rates = <12288000>;
            assigned-clock-parents = <&cru CLK_I2S1_8CH_TX>, <&cru I2S1_MCLKOUT_TX>;
            pinctrl-names = "default";
            pinctrl-0 = <&i2s1m0_mclk>;
            hp-volume = <20>;    // 耳机音量
            spk-volume = <3>;    // 扬声器音量
            //mic-in-differential; // 注释掉了差分麦克风输入
            status = "okay";     // 使能音频 codec
        };
    };
};

vcc1-supply 等系列节点指定 rk809 内部各 regulator 的输入电源,均为 vcc3v3_sys。

其中前缀 vcc1 对应内部 DCDC_REG1 regulator 的 supply_name(即父节点标识),vcc3v3_sys 节点如下:

regulator-fixed 节 点 (内 核 源 码 arch/arm64/boot/dts/rockchip/rk3568-lubancat2.dts)

cpp 复制代码
/* 定义一个名为 dc_5v 的固定电压 regulator,表示板卡的 5V 输入电源 */
dc_5v: dc-5v {
    compatible = "regulator-fixed";      // 标识为固定电压调节器
    regulator-name = "dc_5v";            // 名称为 "dc_5v"
    regulator-always-on;                 // 始终保持开启
    regulator-boot-on;                   // 从 bootloader 阶段就开启
    regulator-min-microvolt = <5000000>; // 输出电压为 5V (5000000 µV)
    regulator-max-microvolt = <5000000>; // 最小和最大值相同,说明是固定电压
};

/* 定义一个名为 vcc5v0_sys 的固定电压 regulator,由 dc_5v 供电 */
vcc5v0_sys: vcc5v0-sys {
    compatible = "regulator-fixed";      // 标识为固定电压调节器
    regulator-name = "vcc5v0_sys";       // 名称为 "vcc5v0_sys"
    regulator-always-on;                 // 始终保持开启
    regulator-boot-on;                   // 从 bootloader 阶段就开启
    regulator-min-microvolt = <5000000>; // 输出电压为 5V
    regulator-max-microvolt = <5000000>;
    vin-supply = <&dc_5v>;               // 其输入电源来自于上面定义的 dc_5v
};

/* 定义一个名为 vcc3v3_sys 的固定电压 regulator,由 vcc5v0_sys 供电 */
vcc3v3_sys: vcc3v3-sys {
    compatible = "regulator-fixed";      // 标识为固定电压调节器
    regulator-name = "vcc3v3_sys";       // 名称为 "vcc3v3_sys"
    regulator-always-on;                 // 始终保持开启
    regulator-boot-on;                   // 从 bootloader 阶段就开启
    regulator-min-microvolt = <3300000>; // 输出电压为 3.3V (3300000 µV)
    regulator-max-microvolt = <3300000>;
    vin-supply = <&vcc5v0_sys>;          // 其输入电源来自于上面定义的 vcc5v0_sys
};

RK809 是一款多功能 PMIC,

除了核心的 regulator 功能,还集成了 RTC、电源键(pwrkey)和音频编解码器(codec)等。因此,其设备树节点下包含了 pwrkey、regulators 和 rk809_codec 等多个子节点。

其中,regulators 子节点比较特殊,它本身没有 compatible 属性,而是包含了一系列代表具体电源输出的子节点。

这些 regulator 的注册过程如下:

顶层节点的 compatible = "rockchip,rk809" 属性匹配到 I2C 驱动 rk808_i2c_driver。

驱动的 probe 函数 (rk808_probe) 被调用,完成基础初始化。

在 probe 函数中,通过 devm_mfd_add_devices 将 RK809 拆分为多个多功能设备(MFD),其中就包括 rk808-regulator 平台设备。

这个新的平台设备会与对应的平台驱动 rk808_regulator_driver 匹配。

最终,在 rk808_regulator_probe 函数中,遍历设备树 regulators 子节点,调用 devm_regulator_register 完成所有 regulator 的注册。

这些注册好的 regulator 为整个系统提供电力。例如,RK3568 的电源管理单元(PMU)会使用它们来管理芯片内部的各个电源域,包括用于控制 IO 电平的 IO 电源域。这些 IO 电源域通常由 PMIC 提供的 LDO 供电。

cpp 复制代码
&pmu_io_domains {
    status = "okay";  // 使能 PMU IO 电源域控制器

    // 为不同的 IO 域指定供电的 regulator
    pmuio1-supply = <&vcc3v3_pmu>;  // PMU 内部 IO1 域电源
    pmuio2-supply = <&vcc3v3_pmu>;  // PMU 内部 IO2 域电源

    vccio1-supply = <&vccio_acodec>; // 外部 IO1 域 (Acodec) 电源
    vccio3-supply = <&vccio_sd>;     // 外部 IO3 域 (SD卡) 电源
    vccio4-supply = <&vcc_1v8>;      // 外部 IO4 域电源 (1.8V)
    vccio5-supply = <&vcc_3v3>;      // 外部 IO5 域电源 (3.3V)
    vccio6-supply = <&vcc_1v8>;      // 外部 IO6 域电源 (1.8V)
    vccio7-supply = <&vcc_3v3>;      // 外部 IO7 域电源 (3.3V)
};

系统中已注册 regulator 与 consumer 的关联、regulator 之间的层级关系,

可通过 /sys/kernel/debug/regulator/regulator_summary 文件查看,

下图展示了 IO 电源域与对应 regulator 的关联情况。

19.4 实验

下面我们简单编写一个驱动,向内核注册一个 regulators,电压范围为 500000µV 到 1350000µV。

19.4.1 驱动代码

regulator_test.c(linux_driver/power_management/下)

cpp 复制代码
static int regulator_driver_probe(struct platform_device *pdev)
{
 struct regulator_config config = { };
 int ret;

 config.dev = &pdev->dev; // 指向该 regulator 所属的设备
 config.init_data = &my_regulator_initdata; // 指向 regulator 的初始化数据

 // 注册 regulator 设备
 my_regulator_test_rdev = regulator_register(&my_regulator_desc, &config);
 if (IS_ERR(my_regulator_test_rdev)) {
 ret = PTR_ERR(my_regulator_test_rdev);
 pr_err("Failed to register regulator: %d\n", ret);
 return ret;
 }

 return 0;
}

// 定义平台驱动结构体
static struct platform_driver my_regulator_driver = {
 .probe = regulator_driver_probe, // 驱动的 probe 函数
 .driver = {
 .name = "my_regulator", // 驱动名称,用于匹配设备
 .owner = THIS_MODULE,
 },
};

static struct platform_device *regulator_pdev; // 平台设备指针

static int my_regulator_test_init(void)
{
 int ret;

 // 1. 分配一个平台设备
 regulator_pdev = platform_device_alloc("my_regulator", -1);
 if (!regulator_pdev) {
 pr_err("Failed to allocate dummy regulator device\n");
 return -1;
 }

 // 2. 将平台设备添加到系统中
 ret = platform_device_add(regulator_pdev);
 if (ret != 0) {
 pr_err("Failed to register dummy regulator device: %d\n", ret);
 platform_device_put(regulator_pdev);
 return -1;
 }

 // 3. 注册平台驱动
 ret = platform_driver_register(&my_regulator_driver);
 if (ret != 0) {
 pr_err("Failed to register dummy regulator driver: %d\n", ret);
 platform_device_unregister(regulator_pdev);
 return -1;
 }

 return 0;
}

static void my_regulator_test_exit(void)
{
 // 按与注册相反的顺序卸载资源
 regulator_unregister(my_regulator_test_rdev); // 注销 regulator
 platform_device_unregister(regulator_pdev); // 注销平台设备
 platform_driver_unregister(&my_regulator_driver); // 注销平台驱动
}

module_init(my_regulator_test_init); // 指定模块初始化函数
module_exit(my_regulator_test_exit); // 指定模块退出函数
MODULE_LICENSE("GPL"); // 模块许可证声明

19.4.2 测试结果

编译设备树插件和内核模块(参考前文环境搭建),生成 regulator_test.ko。

加载设备树插件后,执行 sudo insmod regulator_test.ko 加载驱动,

加载成功后,/sys/class/regulator 目录下会生成对应 regulator 目录(如 regulator.31,后缀数字依板卡而定)。

在/sys/kernel/debug/regulator/regulator_summary 文件中记录系统 regulator 和 consumer 之间的关系。

第 20 章 DRM 图形显示框架

Linux 图像子系统软件框架较为复杂,涵盖 GUI、3D 应用、DRM/KMS 及硬件等层级,可通过下图初步了解(感兴趣可进一步探索 DRI 直接渲染基础设施框架):

Linux 显示驱动开发常涉及 FBDEVDRM/KMS 子系统

FBDEV 可快速实现基础显示驱动,但无法很好支持芯片新特性(如显示覆盖、GPU 加速、硬件光标),且通过 /dev/fb 暴露显存易引发应用访问冲突、安全性不足。

随着芯片性能提升、3D 渲染及 GPU 普及,FBDEV 已显落伍,由此 DRM(Direct Rendering Manager,直接图形管理器)作为现代图形显示框架应运而生。

20.1 DRM 框架简述

DRM 框架通过分层设计和模块化管理解决了 FrameBuffer 的困境,其核心是将显示相关操作规范化,避免直接显存访问的冲突,同时支持硬件新特性。以下是关键架构:

DRM/KMS 常被用来指代整个 DRM 子系统,

但 KMS(内核模式设置)和 DRM driver(硬件相关驱动)仅为其部分组件,

完整子系统还包含 DRM core(通用框架)、GEM(显存管理)等核心模块。

20.1.1 Libdrm

Libdrm 是 DRM 框架在用户空间的封装库,核心是对底层 IOCTL 接口进行封装,向上提供统一、通用的 API。

用户或应用程序通过调用这些库函数,即可安全访问、管理显示资源(如显存、显示模式)。

其优势在于:

统一接口:为不同硬件提供一致的操作方式,简化应用开发。

避免冲突:应用不再直接操作硬件或显存,所有请求都通过 Libdrm 传递给内核 DRM 驱动,由驱动统一调度和处理,有效避免了多应用间的资源访问冲突,提升了系统安全性和稳定性。

20.1.2 KMS(Kernel Mode Setting)

KMS(Kernel Mode Setting)是 DRM 框架的核心模块,负责显示参数设置 (如分辨率、刷新率)和显示画面控制(如帧缓冲切换、输出管理)两大基础功能。为适配现代显示设备逻辑,KMS 进一步拆分出多个子模块协同工作,确保功能模块化和适配灵活性。

20.1.2.1 DRM FrameBuffer

DRM FrameBuffer 是 DRM 框架中硬件无关的软件抽象,核心描述图层显示的关键信息(宽、高、像素格式、行间距等)。

20.1.2.2 Planes

Planes 是 DRM 框架的基本显示控制单位,每个图像对应一个 Plane,其属性(显示区域、翻转、色彩混合等)决定图像显示效果;最终图像经 Planes 传递至 CRTC 组件,实现多图混合或单图独立显示功能。

20.1.2.3 CRTC

CRTC 负责将待显示图像转换为硬件时序信号,兼具帧切换、电源控制、色彩调整等功能,可连接多个 Encoder 实现屏幕复制。

20.1.2.4 Encoder

Encoder(编码器)是 DRM 框架中 CRTC 与 Connector 间的中间组件,负责电源管理,核心功能是将 CRTC 输出的像素数据,转换为 HDMI、MIPI 等特定显示接口所需的信号格式。

20.1.2.5 Connector

Connector(连接器)负责对接 HDMI、VGA 等外部显示设备,可获取设备 EDID 信息及 DPMS 连接状态。

上述的这些组件,最终完成了一个完整的 DRM 显示控制过程,
DRM Framebuffer 提供显示内容信息,
经 Planes 控制显示属性后,
由 CRTC 转换为硬件时序,
再通过 Encoder 进行信号转换,
最后经 Connector 输出到显示器,呈现 DRM Framebuffer 内容。

上面 CRTC、Planes、Encoder、Connector 这些组件是对硬件的抽象,即使没有实际的硬件与之对应,在软件驱动中也需要实现这些,否则 DRM 子系统无法正常运行。

20.1.3 GEM(generic DRM memory-management)

GEM 负责对 DRM 使用的内存 (如显存) 进行管理, 是一个软件抽象。

GEM 框架提供的功能包括:

• 内存分配和释放

• 命令执行

• 执行命令时的管理

20.2 驱动简述

片上显示(On-Chip Display)驱动通常由芯片厂商(如 Rockchip)开发和维护,

这类驱动被称为 DRM-Host 驱动。

其代码通常位于内核源码的 drivers/gpu/drm/<厂商名>/ 目录下

(例如 drivers/gpu/drm/rockchip/)。

若想深入研究源码,可查阅该目录下的具体实现。

值得注意的是,Rockchip 的 DRM 驱动采用了 Component 框架,其中显示主驱动作为 master ,而其控制下的各个硬件模块(如 VOP、HDMI 控制器等)则作为 component

arch/arm64/boot/dts/rockchip/rk3568.dtsi

cpp 复制代码
/* 显示子系统的顶层节点 */
display_subsystem: display-subsystem {
    compatible = "rockchip,display-subsystem"; /* 标识为Rockchip显示子系统 */

    /* 指定用于显示的特殊内存区域 */
    memory-region = <&drm_logo>, <&drm_cubic_lut>;
    memory-region-names = "drm-logo", "drm-cubic-lut"; /* 内存区域名称 */

    ports = <&vop_out>; /* 连接到VOP(视频输出处理器)的输出端口 */

    devfreq = <&dmc>; /* 关联动态内存控制器,用于频率调节 */

    /* 显示路由和策略配置 */
    route {
        /* DSI0显示路径的配置 */
        route_dsi0: route-dsi0 {
            status = "disabled"; /* 默认禁用 */

            /* 不同阶段的Logo配置 */
            logo,uboot = "logo.bmp";      /* U-Boot阶段显示的Logo */
            logo,kernel = "logo_kernel.bmp"; /* Kernel阶段显示的Logo */
            logo,mode = "center";         /* Logo显示模式:居中 */
            charge_logo,mode = "center";  /* 充电Logo显示模式:居中 */

            connect = <&vp0_out_dsi0>; /* 定义该路径的终点是vp0_out_dsi0 */
            /*..................*/
        };
        /* 其他显示路径... */
    };
    /*..................*/

    /* VOP (Video Output Processor) 节点 */
    vop: vop@fe040000 {
        compatible = "rockchip,rk3568-vop"; /* 标识为RK3568的VOP */
        reg = <0x0 0xfe040000 0x0 0x3000>, /* 寄存器地址和大小 */
              <0x0 0xfe044000 0x0 0x1000>;
        reg-names = "regs", "gamma_lut"; /* 寄存器区域名称 */

        rockchip,grf = <&grf>; /* 指向全局寄存器文件 */
        interrupts = <GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH>; /* 中断配置 */

        /* 时钟配置 */
        clocks = <&cru ACLK_VOP>,
                 <&cru HCLK_VOP>,
                 <&cru DCLK_VOP0>,
                 <&cru DCLK_VOP1>,
                 <&cru DCLK_VOP2>;
        clock-names = "aclk_vop", "hclk_vop", "dclk_vp0", "dclk_vp1", "dclk_vp2";

        iommus = <&vop_mmu>; /* 关联IOMMU用于内存管理 */
        power-domains = <&power RK3568_PD_VO>; /* 关联电源域 */
        status = "disabled"; /* 默认禁用,由驱动根据需要启用 */

        /* VOP的输出端口,用于连接到Encoder/Connector */
        vop_out: ports {
            #address-cells = <1>;
            #size-cells = <0>;

            vp0: port@0 { /* VP0 (Video Plane 0) 的输出端口 */
                /*..................*/
            };
            vp1: port@1 { /* VP1 的输出端口 */
                /*..................*/
            };
            vp2: port@2 { /* VP2 的输出端口 */
                /*..................*/
            };
        };
    };
};

drivers/gpu/drm/rockchip/rockchip_drm_drv.c

cpp 复制代码
#include <linux/platform_device.h>
#include <drm/drm_component.h>

// 1. Component Master 操作集
// 定义了 Component 框架的绑定和解绑函数
static const struct component_master_ops rockchip_drm_ops = {
    .bind = rockchip_drm_bind,    // 所有子组件准备就绪后,调用此函数进行总初始化
    .unbind = rockchip_drm_unbind,// 驱动卸载时,调用此函数进行资源释放
};

// 2. Platform Driver Probe 函数
// 当平台设备(对应设备树中的 display-subsystem)被匹配时,此函数被调用
static int rockchip_drm_platform_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct component_match *match = NULL; // 用于匹配子组件的规则集合
    int ret;

    // 平台相关的早期探测(具体实现依赖芯片)
    ret = rockchip_drm_platform_of_probe(dev);
#if !IS_ENABLED(CONFIG_DRM_ROCKCHIP_VVOP)
    if (ret)
        return ret;
#endif

    // 动态添加子组件的匹配规则
    // 例如:VOP、HDMI、DSI 等控制器
    match = rockchip_drm_match_add(dev);
    if (IS_ERR(match))
        return PTR_ERR(match);

    // 将当前驱动注册为 Component Master
    // 并指定子组件的匹配规则
    ret = component_master_add_with_match(dev, &rockchip_drm_ops, match);
    if (ret < 0) {
        rockchip_drm_match_remove(dev); // 注册失败,清理匹配规则
        return ret;
    }

    // 设置设备的 DMA 一致性掩码为 64 位
    // 允许访问超过 4GB 的内存地址
    dev->coherent_dma_mask = DMA_BIT_MASK(64);

    return 0;
}

// 3. Platform Driver 结构体
static struct platform_driver rockchip_drm_platform_driver = {
    .probe = rockchip_drm_platform_probe,  // 探测函数
    .remove = rockchip_drm_platform_remove,// 移除函数
    .shutdown = rockchip_drm_platform_shutdown,// 关机函数
    .driver = {
        .name = "rockchip-drm",            // 驱动名称,用于匹配设备
        .of_match_table = rockchip_drm_dt_ids,// 设备树匹配表
        .pm = &rockchip_drm_pm_ops,        // 电源管理操作集
    },
};

下面我们以 lubancat2 为例,其 rk3568 支持多种显示接口:
该图展示了多媒体接口模块,
包含 VOP(3 个显示端口,2 个高清用于 LCD/HDMI,
1 个标清用于 BT656 转 CVBS)、
HDMI2.0a、eDP1.3、
单 LVDS / 双 MIPI-DSI_TX、
并行 RGB 接口、
电子墨水接口、
16 位摄像头接口及 4 通道 MIPI-CSI_RX

lubancat2 引出的接口有 MIPI DSI 和 HDMI。

以 HDMI 显示接口为例,默认的设备树 (rk3568-lubancat2.dts) 是开启了 HDMI,设备树中关于 HDMI 的描述如下:

arch/arm64/boot/dts/rockchip/rk3568.dtsi

cpp 复制代码
/* HDMI 控制器节点 */
hdmi: hdmi@fe0a0000 {
    compatible = "rockchip,rk3568-dw-hdmi"; /* 标识为RK3568的DW-HDMI控制器 */
    reg = <0x0 0xfe0a0000 0x0 0x20000>;     /* 寄存器基地址和大小 */
    interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>; /* HDMI中断配置 */

    /* HDMI所需时钟 */
    clocks = <&cru PCLK_HDMI_HOST>, /* 主机接口时钟 */
             <&cru CLK_HDMI_SFR>,   /* 状态机和寄存器时钟 */
             <&cru CLK_HDMI_CEC>,   /* CEC总线时钟 */
             <&pmucru PLL_HPLL>,    /* 参考时钟源 */
             <&cru HCLK_VOP>;       /* VOP接口时钟 */
    clock-names = "iahb", "isfr", "cec", "ref", "hclk"; /* 时钟名称 */

    power-domains = <&power RK3568_PD_VO>; /* 关联VO电源域 */
    reg-io-width = <4>; /* 寄存器访问宽度为4字节 */
    rockchip,grf = <&grf>; /* 指向全局寄存器文件 */

    #sound-dai-cells = <0>; /* 音频DAI链接配置 */

    /* 引脚配置 */
    pinctrl-names = "default"; /* 默认引脚状态 */
    pinctrl-0 = <&hdmitx_scl &hdmitx_sda &hdmitxm0_cec>; /* I2C和CEC引脚 */

    status = "disabled"; /* 默认禁用 */

    /* 端口连接 */
    ports {
        #address-cells = <1>;
        #size-cells = <0>;

        /* HDMI输入端口,用于连接VOP */
        hdmi_in: port {
            reg = <0>;
            #address-cells = <1>;
            #size-cells = <0>;

            /* 与VOP的VP0端口连接 */
            hdmi_in_vp0: endpoint@0 {
                reg = <0>;
                remote-endpoint = <&vp0_out_hdmi>; /* 指向VOP的VP0输出端点 */
                status = "disabled";
            };
            /* 与VOP的VP1端口连接 */
            hdmi_in_vp1: endpoint@1 {
                reg = <1>;
                remote-endpoint = <&vp1_out_hdmi>; /* 指向VOP的VP1输出端点 */
                status = "disabled";
            };
        };
    };
};

HDMI 的平台驱动在 rockchip_drm_init 中注册,当匹配设备树时,

会调用 dw_hdmi_rockchip_probe 函数,

该函数通过 component_add 注册一个 component,

并设置其 component_ops 操作函数集。

当 DRM Master(rockchip_drm)的 bind 回调被内核调用后,内核会接着调用这个 HDMI component 的 bind 回调,即 dw_hdmi_rockchip_bind 函数,以完成 HDMI 控制器的最终初始化和与 DRM 框架的集成。

drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c

cpp 复制代码
#include <linux/platform_device.h>
#include <drm/drm_component.h>

// 1. Component 操作集
// 定义了该 component 的绑定和解绑函数
static const struct component_ops dw_hdmi_rockchip_ops = {
    .bind = dw_hdmi_rockchip_bind,    // 当 Master 准备好后,调用此函数进行绑定和初始化
    .unbind = dw_hdmi_rockchip_unbind,// 驱动卸载时,调用此函数进行资源释放
};

// 2. Platform Driver Probe 函数
// 当平台设备(对应设备树中的 hdmi 节点)被匹配时,此函数被调用
static int dw_hdmi_rockchip_probe(struct platform_device *pdev)
{
    // 启用运行时电源管理
    pm_runtime_enable(&pdev->dev);
    // 获取设备使用权,确保设备处于活动状态
    pm_runtime_get_sync(&pdev->dev);

    // 将当前设备注册为一个 DRM component
    // 并指定其操作集
    return component_add(&pdev->dev, &dw_hdmi_rockchip_ops);
}

// 3. Platform Driver 结构体
static struct platform_driver dw_hdmi_rockchip_pltfm_driver = {
    .probe = dw_hdmi_rockchip_probe,   // 探测函数
    .remove = dw_hdmi_rockchip_remove, // 移除函数
    .shutdown = dw_hdmi_rockchip_shutdown, // 关机函数
    .driver = {
        .name = "dwhdmi-rockchip",     // 驱动名称,用于匹配设备
        .of_match_table = dw_hdmi_rockchip_dt_ids, // 设备树匹配表
        .pm = &dw_hdmi_pm_ops,         // 电源管理操作集
    },
};

在 dw_hdmi_rockchip_bind 函数中会对 hdmi 进行初始化,寻找 crtc,初始化 Encoder 等。

20.3 屏幕显示测试

基于 DRM 驱动框架测试 HDMI 显示效果,采用 rk3568-lubancat2.dts 设备树,其默认已启用 HDMI 功能。

20.3.1 Libdrm

我们使用 libdrm 官方测试工具 modetest 验证 DRM 驱动。

cpp 复制代码
git clone https://gitlab.freedesktop.org/mesa/drm
sudo apt -y install python3-pip cmake git ninja-build
python3 -m pip install meson
meson . build && ninja -C build

20.3.2 实验操作

将 modetest 上传至开发板,添加可执行权限并关闭图形界面:

bash 复制代码
# 上传文件(以 scp 为例)
scp modetest root@[开发板IP]:/root/

# 添加可执行权限
chmod +x /root/modetest

# 关闭图形界面(停止显示管理器,根据系统调整,如 gdm3、lightdm 等)
systemctl stop gdm3  # 或 systemctl stop lightdm
bash 复制代码
# 命令关闭图形界面
sudo systemctl set-default multi-user.target
sudo reboot
# 测试完后,开启图像界面使用命令:
sudo systemctl set-default graphical.target

直接使用命令 ./modetest 运行 modetest 程序:

待程序检测执行完毕,会列举出开发板上的 DRM 框架下的显示设备。其中一些字样如 Encoders、Connectors、CRTCs 经过前面的介绍,大家都应该有了一点印象。
DRM显示过程

我们要做的,就是找出前面终端中打印的 HDMI 屏幕对应的 connectors、CRTCs 的 ID,使用命令:

bash 复制代码
./modetest -M rockchip -e
./modetest -M rockchip -c

根据识别到的 DRM 设备信息,使用以下命令进行测试:

bash 复制代码
# -M rockchip:指定 DRM 驱动模块
# -s 150@71:1920x1080:设置连接器 150 绑定 CRTC 71,分辨率 1920x1080
./modetest -M rockchip -s 150@71:1920x1080

退出测试:在终端按回车即可退出 modetest。

补充知识:DRM 框架支持模拟 Framebuffer 设备(以兼容传统应用),Rockchip 驱动中对应实现代码为 drivers/gpu/drm/rockchip/rockchip_drm_fb.c,最终会生成 /dev/fb0 设备节点。

传统 FB 测试命令:

bash 复制代码
# 向 /dev/fb0 写入随机数据,屏幕会显示花屏效果,验证 FB 模拟功能
cat /dev/random > /dev/fb0

第 21 章 SMP(Symmetrical Multi-Processing)

21.1 处理器的发展过程

处理器核心性能取决于 IPC(每时钟周期指令数)和主频。单核时代,架构改良对 IPC 的提升有限,性能提升主要依赖提高主频,但主频升高会导致功耗激增(每增 1G 主频功耗升 25 瓦,超 150 瓦后风冷散热不足),"主频为王" 时代终结,多核心处理器应运而生。

多核心处理器将两个及以上独立处理器封装于单一 IC,分两类:

同构多核:集成相同架构 CPU,共享系统资源,广泛应用于台式机、笔记本;

异构多核:集成通用处理器、DSP、FPGA 等,适配复杂实时场景(如机器人装配线的图像处理与电机控制)。

早期手机 SoC 采用 ARM"Big.Little" 架构,集成高性能 big core(承担高负载任务)和低性能 little core(处理日常负载),通过合理调度平衡性能与功耗,提升手机续航。

DynamIQ 是 big.LITTLE 技术的演进扩展,big.LITTLE 仅为其支持的功能之一;它通过合并大小核心集群,形成兼具大小 CPU 的单一集成化 CPU 集群。

21.2 相关知识介绍

启动过程分析前,先了解核心前置知识:

ARMv8 异常等级(EL0~EL3) :数字越小,权限越低,直接决定处理器运行权限

TF-A(Trusted Firmware-A):ARM Profile A 架构的可信固件参考实现,核心是通过签名认证构建启动信任链(Trust Chain),保障启动安全;

PSCI(Power State Coordination Interface):ARM 定义的电源管理接口规范,由固件实现,Linux 需通过 smc/hvc 指令 切换异常等级调用,适配 ARMv8 虚拟化、安全等特性,支撑 CPU 启动、休眠 / 唤醒(suspend/resume) 等核心操作。

21.3 rk3568 处理器基本介绍

LubanCat2 板卡基于 RK3568 芯片,集成四核 Cortex-A55 CPU,最高主频 2GHz。

bash 复制代码
#查看cpu信息
lscpu

21.4 linux SMP 启动过程

上述仅简要介绍了 SMP 启动流程,实际还需明确两个核心问题:

内核如何识别 CPU 核心数量?CPU0 如何唤醒其他从核?

内核在 kernel/cpu.c 中定义了 cpumask 类型结构体变量,用于描述各 CPU 核心的工作状态,为核心识别与唤醒提供基础。

cpumask 类型的结构体变量 (位于文件 kernel/cpu.c)

cpp 复制代码
#include <linux/cpumask.h>

/*
 * 1. __cpu_possible_mask
 * - 含义:系统中物理上存在的所有 CPU 核心的集合。
 * - 初始化:在系统启动早期,由平台代码(如设备树或 ACPI)根据硬件配置静态设置。
 * - 生命周期:一旦设置,在整个系统运行期间通常不会改变。
 * - 示例:如果一个 4 核处理器,其值为 0b1111 (二进制)。
 */
struct cpumask __cpu_possible_mask __read_mostly;
EXPORT_SYMBOL(__cpu_possible_mask);

/*
 * 2. __cpu_online_mask
 * - 含义:当前处于在线状态、可以执行任务的 CPU 核心的集合。
 * - 动态性:通过 CPU 热插拔(hotplug)机制可以动态地添加或移除。
 * - 关系:是 __cpu_possible_mask 的子集。
 * - 示例:4 核处理器中,如果 CPU1 被手动下线,其值为 0b1101。
 */
struct cpumask __cpu_online_mask __read_mostly;
EXPORT_SYMBOL(__cpu_online_mask);

/*
 * 3. __cpu_present_mask
 * - 含义:当前物理上存在于系统中且已被内核检测到的 CPU 核心的集合。
 * - 与 possible 的区别:possible 是理论上的最大值,present 是当前实际存在的。
 *   在大多数固定硬件(如手机、嵌入式设备)上,两者通常是相同的。
 *   在支持热插拔 CPU 槽位的服务器上,可能存在差异。
 */
struct cpumask __cpu_present_mask __read_mostly;
EXPORT_SYMBOL(__cpu_present_mask);

/*
 * 4. __cpu_active_mask
 * - 含义:当前活跃的、可以被调度器用来运行用户任务的 CPU 核心的集合。
 * - 与 online 的区别:一个 CPU 可能 online,但因某些原因(如处于 idle 状态或被隔离)暂时不 active。
 *   通常情况下,active_mask 是 online_mask 的一个超集或子集,具体取决于调度策略。
 */
struct cpumask __cpu_active_mask __read_mostly;
EXPORT_SYMBOL(__cpu_active_mask);

/*
 * 5. __cpu_isolated_mask
 * - 含义:被内核隔离(isolated)的 CPU 核心的集合。
 * - 用途:这些 CPU 不会被调度器分配任何普通用户任务,通常用于:
 *   - 运行特定的实时任务(通过 isolcpus 内核参数)。
 *   - 用于虚拟化中的特定虚拟机。
 *   - 硬件错误隔离。
 */
struct cpumask __cpu_isolated_mask __read_mostly;
EXPORT_SYMBOL(__cpu_isolated_mask);

struct cpumask 结构体(位于文件 include/linux/cpumask.h)

cpp 复制代码
/* 
 * 注释:不要直接赋值或返回此结构体实例!
 * 原因:其大小由 NR_CPUS 决定,可能非常大,直接操作会导致效率低下或栈溢出。
 * 正确用法:应通过指针(cpumask_t *)或引用(const cpumask_t &)来传递和操作。
 */
typedef struct cpumask { 
    DECLARE_BITMAP(bits, NR_CPUS); // 核心:使用位图存储CPU状态
} cpumask_t;

/*
 * 宏:DECLARE_BITMAP
 * 功能:动态声明一个位图数组。
 * 参数:
 *   - name: 要声明的位图数组的变量名。
 *   - bits: 该位图需要支持的总位数(即需要表示的CPU最大数量)。
 * 实现原理:
 *   - BITS_TO_LONGS(bits) 是一个辅助宏,用于计算存储 'bits' 个比特位
 *     需要多少个 'unsigned long' 类型的元素。
 *   - 例如,在32位系统上,要表示32个CPU,只需要1个unsigned long元素。
 *     要表示40个CPU,则需要2个unsigned long元素。
 *   - 最终,此宏会展开为:unsigned long name[计算出的数组大小];
 */
#define DECLARE_BITMAP(name, bits) \
    unsigned long name[BITS_TO_LONGS(bits)]

/sys/devices/system/cpu 目录记录了系统中所有的 CPU 核以及上述各变量的内容,

例如文件 present,对应于 __cpu_present_mask 变量,执行以下命令,可以查看当前系统中所有的 CPU 核编号。

bash 复制代码
cat /sys/devices/system/cpu/present

我们通过文件 /sys/devices/system/cpu/cpu1/online 在用户空间控制一个 CPU 核运行与否。

bash 复制代码
#启用cpu1
echo 1 > /sys/devices/system/cpu/cpu1/online

#关闭cpu1
echo 0 > /sys/devices/system/cpu/cpu1/online

内核是如何建立 CPU 之间的关系的。

设备树根节点下有个/cpus 的子节点,其内容如下

/cpus 节点和/psci 节点 (位于arch/arm64/boot/dts/rockchip/rk3568.dtsi)

cpp 复制代码
/*
 * 1. cpus 节点
 * - 功能:作为所有 CPU 核心节点的父容器,描述系统中的 CPU 硬件拓扑。
 * - #address-cells = <2>; #size-cells = <0>;:
 *   - 子节点(cpu@...)的地址需要用 2 个 32 位值来表示。
 *   - 子节点没有大小(size),因为它们是抽象的 CPU 设备。
 */
cpus {
    #address-cells = <2>;
    #size-cells = <0>;

    /*
     * 2. cpu0 节点 (主处理器)
     * - 描述:定义了第一个 CPU 核心 (CPU0)。
     * - device_type = "cpu";:告知操作系统此节点代表一个 CPU。
     * - compatible = "arm,cortex-a55";:标识 CPU 型号,用于内核匹配驱动和特性。
     * - reg = <0x0 0x0>;:CPU 的标识符,在多簇(cluster)系统中,
     *   第一个值是簇 ID,第二个值是簇内的 CPU ID。
     * - enable-method = "psci";:【关键】指定了启动/关闭此 CPU 的方法是 PSCI。
     * - clocks, operating-points-v2, cpu-idle-states: 与 CPU 时钟、
     *   工作电压/频率、休眠状态相关的配置。
     */
    cpu0: cpu@0 {
        device_type = "cpu";
        compatible = "arm,cortex-a55";
        reg = <0x0 0x0>;
        enable-method = "psci";
        clocks = <&scmi_clk 0>;
        operating-points-v2 = <&cpu0_opp_table>;
        cpu-idle-states = <&CPU_SLEEP>;
        #cooling-cells = <2>;
        dynamic-power-coefficient = <187>;
    };

    /*
     * 3. cpu1, cpu2, cpu3 节点 (从处理器)
     * - 描述:定义了其余的 CPU 核心。
     * - 结构与 cpu0 基本相同。
     * - reg = <0x0 0x100>; <0x0 0x200>; <0x0 0x300>;:
     *   它们与 cpu0 位于同一个簇 (簇 ID 为 0),CPU ID 分别为 1, 2, 3。
     * - enable-method = "psci";:【关键】所有 CPU 都使用 PSCI 进行电源管理。
     */
    cpu1: cpu@100 {
        device_type = "cpu";
        compatible = "arm,cortex-a55";
        reg = <0x0 0x100>;
        enable-method = "psci";
        clocks = <&scmi_clk 0>;
        operating-points-v2 = <&cpu0_opp_table>;
        cpu-idle-states = <&CPU_SLEEP>;
    };

    cpu2: cpu@200 {
        device_type = "cpu";
        compatible = "arm,cortex-a55";
        reg = <0x0 0x200>;
        enable-method = "psci";
        clocks = <&scmi_clk 0>;
        operating-points-v2 = <&cpu0_opp_table>;
        cpu-idle-states = <&CPU_SLEEP>;
    };

    cpu3: cpu@300 {
        device_type = "cpu";
        compatible = "arm,cortex-a55";
        reg = <0x0 0x300>;
        enable-method = "psci";
        clocks = <&scmi_clk 0>;
        operating-points-v2 = <&cpu0_opp_table>;
        cpu-idle-states = <&CPU_SLEEP>;
    };

    /* ... 其他可能的节点,如 L2 缓存 ... */
};

/*
 * 4. psci 节点
 * - 功能:定义了 PSCI (Power State Coordination Interface) 的实现细节。
 * - compatible = "arm,psci-1.0";:指定了兼容的 PSCI 版本。
 * - method = "smc";:【关键】告知内核通过 SMC (Secure Monitor Call)
 *   指令来触发 PSCI 调用。内核会构造特定的寄存器值,
 *   执行 SMC 指令,将控制权交给安全世界(EL3)的 TF-A 固件。
 */
psci {
    compatible = "arm,psci-1.0";
    method = "smc";
};

psci 节点定义了电源管理接口的版本和调用方式:

compatible = "arm,psci-1.0":表示兼容 PSCI 1.0 标准

method = "smc":通过 SMC 指令陷入 EL3 异常等级,调用 TF-A 固件提供的电源管理功能。

cpp 复制代码
[    0.000000] psci: probing for conduit method from DT.
[    0.000000] psci: PSCIv1.1 detected in firmware.
[    0.000000] psci: Using standard PSCI v0.2 function IDs
[    0.000000] psci: Trusted OS migration not required
[    0.000000] psci: SMC Calling Convention v1.2

cpus 节点中的 operating-points-v2 = <&cpu0_opp_table> 属性:

指向 cpu0_opp_table 节点,该节点定义了 CPU 支持的工作频率、电压等性能参数用于 CPU 动态调频(DVFS)功能。

/cpu0_opp_table 节 点 (位于arch/arm64/boot/dts/rockchip/rk3568.dtsi)

cpp 复制代码
/*
 * cpu0_opp_table 节点
 * - 功能:定义 CPU0 的工作点(频率-电压组合)表,用于动态调频(DVFS)。
 * - compatible = "operating-points-v2";:标识此节点遵循 v2 版工作点规范。
 * - opp-shared;:表示此表中的工作点可被多个 CPU 核心共享(如所有 A55 核心)。
 */
cpu0_opp_table: cpu0-opp-table {
    compatible = "operating-points-v2";
    opp-shared;

    /* 以下为与温度、漏电相关的高级配置,用于更精细的电压调整 */
    mbist-vmin = <825000 900000 950000>;
    nvmem-cells = <&cpu_leakage>, <&core_pvtm>, <&mbist_vmin>;
    nvmem-cell-names = "leakage", "pvtm", "mbist-vmin";
    rockchip,pvtm-voltage-sel = <
        0 84000 0
        84001 91000 1
        91001 100000 2
    >;
    rockchip,pvtm-freq = <408000>;
    rockchip,pvtm-volt = <900000>;
    rockchip,pvtm-ch = <0 5>;
    rockchip,pvtm-sample-time = <1000>;
    rockchip,pvtm-number = <10>;
    rockchip,pvtm-error = <1000>;
    rockchip,pvtm-ref-temp = <40>;
    rockchip,pvtm-temp-prop = <26 26>;
    rockchip,thermal-zone = "soc-thermal"; /* 绑定温度传感器区域 */
    rockchip,temp-hysteresis = <5000>;     /* 温度迟滞,防止频繁调整 */
    rockchip,low-temp = <0>;
    rockchip,low-temp-adjust-volt = <
        /* MHz MHz uV */
        0 1608 75000
    >;

    /*
     * 工作点 1:408 MHz
     * - opp-hz: CPU 频率(408,000,000 Hz)。
     * - opp-microvolt: 对应电压范围(最小 850,000 uV,典型 850,000 uV,最大 1,150,000 uV)。
     * - opp-microvolt-Lx: 不同性能等级(L0/L1/L2)对应的电压。
     * - clock-latency-ns: 频率切换的延迟(40,000 ns)。
     */
    opp-408000000 {
        opp-hz = /bits/ 64 <408000000>;
        opp-microvolt = <850000 850000 1150000>;
        opp-microvolt-L0 = <850000 850000 1150000>;
        opp-microvolt-L1 = <825000 825000 1150000>;
        opp-microvolt-L2 = <825000 825000 1150000>;
        clock-latency-ns = <40000>;
    };

    /*
     * 工作点 2:600 MHz
     */
    opp-600000000 {
        opp-hz = /bits/ 64 <600000000>;
        opp-microvolt = <850000 825000 1150000>;
        opp-microvolt-L0 = <850000 850000 1150000>;
        opp-microvolt-L1 = <825000 825000 1150000>;
        opp-microvolt-L2 = <825000 825000 1150000>;
        clock-latency-ns = <40000>;
    };

    /*
     * 工作点 3:816 MHz
     * - opp-suspend: 标记此工作点为休眠状态下使用的低功耗频率。
     */
    opp-816000000 {
        opp-hz = /bits/ 64 <816000000>;
        opp-microvolt = <850000 850000 1150000>;
        opp-microvolt-L0 = <850000 850000 1150000>;
        opp-microvolt-L1 = <825000 825000 1150000>;
        opp-microvolt-L2 = <825000 825000 1150000>;
        clock-latency-ns = <40000>;
        opp-suspend;
    };

    /* ... 其他工作点(如 1.0 GHz, 1.2 GHz 等)... */
};

OPP 驱动根据芯片版本设定 CPU 的电压和频率。

在 **SMP(对称多处理)** 初始化前,内核会先初始化 present_mask,通常是将 possible_mask 的值复制给它。随后,内核根据 present_mask 中的 CPU 列表,逐一启动这些 CPU,使其加入系统并开始工作。

初始化 present_mask(位于arch/arm64/kernel/smp.c)

cpp 复制代码
/*
 * 函数:smp_prepare_cpus
 * 功能:在启动第二个及以后的CPU(从CPU)之前,完成多核系统的准备工作。
 * 参数:max_cpus - 系统允许启动的最大CPU数量。
 */
void __init smp_prepare_cpus(unsigned int max_cpus)
{
    int err;
    unsigned int cpu;
    unsigned int this_cpu;

    /* 1. 初始化CPU拓扑结构 */
    init_cpu_topology(); // 解析设备树的/cpus节点,构建CPU的拓扑关系(如核数、簇等)

    this_cpu = smp_processor_id(); // 获取当前正在执行的CPU(即引导CPU,通常是CPU0)的ID
    store_cpu_topology(this_cpu);  // 存储引导CPU的拓扑信息到per-CPU变量中
    numa_store_cpu_info(this_cpu); // 存储引导CPU的NUMA(非统一内存访问)信息
    numa_add_cpu(this_cpu);        // 将引导CPU添加到NUMA节点中

    /*
     * 如果通过"nosmp"或"maxcpus=0"参数强制单核运行,
     * 则不设置任何从CPU的present状态,直接返回。
     */
    if (max_cpus == 0)
        return;

    /*
     * 2. 遍历所有可能的CPU(possible_mask中的CPU),为启动它们做准备。
     *    这包括初始化硬件和设置__cpu_present_mask。
     */
    for_each_possible_cpu(cpu) { // 遍历在possible_mask中的每一个CPU

        per_cpu(cpu_number, cpu) = cpu; // 设置该CPU的逻辑编号

        if (cpu == smp_processor_id()) // 如果是当前正在运行的CPU(CPU0),则跳过
            continue;

        if (!cpu_ops[cpu]) // 如果该CPU没有对应的操作函数集,则跳过
            continue;

        /* 3. 调用该CPU的准备函数 */
        err = cpu_ops[cpu]->cpu_prepare(cpu); // 执行CPU特定的准备工作(如初始化硬件)
        if (err) // 如果准备失败,则跳过
            continue;

        /* 4. 设置CPU的present状态 */
        set_cpu_present(cpu, true); // 将该CPU标记为"已存在",更新__cpu_present_mask
        numa_store_cpu_info(cpu);   // 存储该CPU的NUMA信息
    }
}

函数 smp_init 和 cpu_up(位于内核文件 kernel/smp.c,kernel/cpu.c)

cpp 复制代码
/*
 * 函数:smp_init
 * 功能:SMP(对称多处理)初始化的主函数,由引导处理器(CPU0)调用,用于激活系统中的其他所有CPU。
 */
void __init smp_init(void)
{
    unsigned int cpu; // 循环变量,代表CPU编号

    /* 省略部分代码 */

    /*
     * 1. 初始化CPU热插拔线程
     * - 为系统中的每个CPU创建一个名为 "cpuhp/%u" 的内核线程。
     * - 这些线程负责处理CPU上线(online)和下线(offline)的各种状态转换。
     * - 对于当前正在运行的CPU0,会创建 "cpuhp/0" 线程。
     */
    cpuhp_threads_init();

    pr_info("Bringing up secondary CPUs ...\n"); // 打印日志,提示开始启动从CPU

    /* FIXME: This should be done in userspace --RR */
    /*
     * 2. 遍历所有已准备好的CPU
     * - for_each_present_cpu(cpu):遍历 __cpu_present_mask 中的所有CPU。
     *   这些CPU是在 smp_prepare_cpus 函数中被标记为 "present" 的。
     */
    for_each_present_cpu(cpu) {
        /*
         * 检查是否已达到用户设定的最大CPU数量(由 maxcpus= 内核参数指定)
         */
        if (num_online_cpus() >= setup_max_cpus)
            break;

        /*
         * 如果CPU尚未在线(即不是CPU0),则启动它
         */
        if (!cpu_online(cpu))
            cpu_up(cpu); // 核心调用:启动指定的CPU
    }

    /* 省略后续代码,如打印启动结果等 */
}

/*
 * 函数:cpu_up
 * 功能:请求将指定的CPU上线。这是一个用户态和内核态都可以调用的接口。
 * 参数:cpu - 要上线的CPU编号。
 * 返回:成功返回0,失败返回错误码。
 */
int cpu_up(unsigned int cpu)
{
    /*
     * 调用 do_cpu_up 执行实际的上线操作。
     * CPUHP_ONLINE 是一个目标状态,表示要将CPU激活到完全可用的在线状态。
     */
    return do_cpu_up(cpu, CPUHP_ONLINE);
}

smp_init() 遍历 present_mask,对未在线的 CPU 调用 cpu_up()。

cpu_up() 通过 cpu_psci_ops 触发 smc 指令陷入 EL3,请求 PSCI 启动从核。

从核启动后返回 EL1,执行 secondary_entry(arch/arm64/kernel/head.S),完成启动。
OFFLINE →
BRINGUP_CPU →
AP_OFFLINE →
AP_ONLINE →
AP_ACTIVE,
最终完成初始化

CPU 状态值枚举(位 于 文 件 include/linux/cpuhotplug.h)

cpp 复制代码
/*
 * 注释:CPU 上线(CPU-up)与下线(CPU-down)状态流转关系
 * BP:引导处理器(初始化主核),AP:应用处理器(从核)
 */
/*
CPU-up          CPU-down
             
BP              AP          BP              AP
OFFLINE         OFFLINE
  |              ^
  v              |
BRINGUP_CPU->AP_OFFLINE    BRINGUP_CPU <- AP_IDLE_DEAD (idle线程/play_dead状态)
  |                → AP_OFFLINE
  v (关中断)        ,---------------^
AP_ONLINE          | (stop_machine机制)
  |                TEARDOWN_CPU <- AP_ONLINE_IDLE
  |                ^
  v                |
AP_ACTIVE         AP_ACTIVE
*/

// CPU 热插拔状态枚举
enum cpuhp_state {
    CPUHP_INVALID = -1,          // 无效状态
    CPUHP_OFFLINE = 0,           // 离线状态(初始状态)
    /* 省略部分中间状态代码 */
    CPUHP_AP_ONLINE_DYN_END = CPUHP_AP_ONLINE_DYN + 30, // 动态上线状态结束标记
    CPUHP_AP_X86_HPET_ONLINE,    // X86架构HPET定时器上线状态
    CPUHP_AP_X86_KVM_CLK_ONLINE, // X86架构KVM时钟上线状态
    CPUHP_AP_ACTIVE,             // AP从核活跃状态(完成初始化)
    CPUHP_ONLINE,                // 在线状态(可调度任务)
};
阶段 BP 状态 AP 状态 说明
初始状态 OFFLINE OFFLINE 系统上电后,BP 开始执行初始化,AP 处于暂停状态。
上线过程 BRINGUP_CPU AP_OFFLINE BP 完成自身初始化,开始唤醒 AP;AP 被唤醒后执行基础硬件初始化。
BRINGUP_CPU AP_ONLINE AP 完成初始化,关闭本地中断(IRQ-off),等待融入系统。
AP_ACTIVE AP_ACTIVE AP 完全在线,可被调度器分配任务;BP 也进入活跃状态,多核协同工作。
下线过程 AP_ACTIVE AP_ONLINE_IDLE AP 完成当前任务,进入空闲状态,准备关闭。
BRINGUP_CPU TEARDOWN_CPU 内核通过 stop_machine 机制暂停其他任务,AP 执行关闭前清理工作。
BRINGUP_CPU AP_IDLE_DEAD AP 执行 play_dead 函数,进入低功耗 "死亡" 状态。
AP_ACTIVE OFFLINE BP 确认 AP 已关闭,将其状态标记为 OFFLINE,自身回到活跃状态。

关键说明:

状态依赖:AP 的状态转换依赖 BP 的协调(如唤醒、确认关闭),BP 始终扮演 "管理者" 角色。

安全机制 :下线过程中通过 stop_machine 暂停其他任务,避免并发冲突;AP 关闭前会执行清理工作,确保系统稳定性。

最终状态 :上线后 BP 和 AP 均为 AP_ACTIVE(可正常工作);下线后 AP 回到 OFFLINE(暂停状态)。

do_cpu_up 调用 _cpu_up 并传入目标状态 CPUHP_ONLINE,

_cpu_up 比较后返回较小值 CPUHP_BRINGUP_CPU,

触发 CPU 从 OFFLINE 转为 BRINGUP_CPU 状态。

_cpu_up 函数 (位于文件 kernel/smp.c)

cpp 复制代码
/*
 * 函数:_cpu_up
 * 功能:执行 CPU 上线的实际操作,按顺序调用一系列回调函数。
 * 参数:
 *   cpu: 要上线的 CPU 编号。
 *   tasks_frozen: 指示系统是否处于冻结状态(用于休眠/恢复)。
 *   target: 目标状态(最终希望 CPU 达到的状态)。
 * 返回:成功返回 0,失败返回错误码。
 */
static int _cpu_up(unsigned int cpu, int tasks_frozen, enum cpuhp_state target)
{
    int ret, st; // st 用于保存当前 CPU 状态

    /* 省略部分代码:
     * 1. 检查 CPU 状态是否合法。
     * 2. 获取 CPU 当前的状态 (st)。
     * 3. 对状态转换进行一些安全性检查。
     */

    /*
     * 关键点:将目标状态调整为不超过 CPUHP_BRINGUP_CPU。
     * 这意味着,无论调用者传入的 target 是什么(比如 CPUHP_ONLINE),
     * 本次 _cpu_up 调用最多只执行到 BRINGUP_CPU 阶段。
     * 后续阶段(如 AP_ONLINE -> AP_ACTIVE)将由其他机制触发。
     */
    target = min((int)target, CPUHP_BRINGUP_CPU);

    /*
     * 核心调用:执行从当前状态 (st) 到目标状态 (target) 的所有回调函数。
     * cpuhp_up_callbacks 会遍历一个预定义的回调函数数组,
     * 依次执行状态 st+1, st+2, ..., target 对应的回调。
     * 这些回调函数完成了 CPU 上线所需的各种具体工作。
     */
    ret = cpuhp_up_callbacks(cpu, st, target);

out:
    cpus_write_unlock(); // 释放 CPU 写入锁
    arch_smt_update();   // 更新 SMT (Simultaneous Multithreading) 相关信息
    cpu_up_down_serialize_trainwrecks(tasks_frozen); // 处理一些并发问题
    return ret;
}

cpuhp_up_callbacks 函数的作用就是调用 cpuhp_hp_states 数组注册的初始化回调函数,以完成 CPU 上线过程中各状态的转换工作。

cpuhp_up_callbacks 函数 (位于文件 kernel/cpu.c)

cpp 复制代码
/*
 * 函数:cpuhp_up_callbacks
 * 功能:按照状态顺序,依次调用 `cpuhp_hp_states` 数组中注册的上线回调函数。
 * 参数:
 *   cpu: 要上线的 CPU 编号。
 *   st: 指向该 CPU 的状态信息结构体 (struct cpuhp_cpu_state)。
 *   target: 本次调用希望达到的目标状态。
 * 返回:
 *   0: 成功到达 target 状态。
 *   非0: 在某个状态回调中失败,返回错误码。
 */
static int cpuhp_up_callbacks(unsigned int cpu, struct cpuhp_cpu_state *st,
                              enum cpuhp_state target)
{
    enum cpuhp_state prev_state = st->state; // 记录初始状态,用于失败时回滚
    int ret = 0;

    /*
     * 状态循环:从当前状态开始,逐步向目标状态推进。
     * st->state 是当前状态,每次循环自增1,直到达到 target。
     */
    while (st->state < target) {
        st->state++; // 进入下一个状态

        /*
         * 核心调用:执行与当前状态 (st->state) 关联的回调函数。
         * - 这个函数会从 cpuhp_hp_states[st->state].start 中获取函数指针并执行。
         * - 第四个参数 "true" 表示这是一个 "bringup" (上线) 操作。
         */
        ret = cpuhp_invoke_callback(cpu, st->state, true, NULL, NULL);

        if (ret) { // 如果回调函数执行失败
            if (can_rollback_cpu(st)) { // 检查是否可以回滚
                st->target = prev_state; // 设置回滚的目标状态
                undo_cpu_up(cpu, st);    // 执行回滚操作,撤销已完成的状态初始化
            }
            break; // 中断状态推进,返回错误
        }
    }

    return ret; // 返回最终结果
}

AP 上电默认状态为 CPUHP_OFFLINE。

cpuhp_up_callbacks 会按顺序执行 cpuhp_hp_states 数组中从 CPUHP_OFFLINE+1 到 CPUHP_BRINGUP_CPU 的所有回调函数 ,完成 AP 核的启动

启动后 AP 核运行空闲任务 ,同时 BP 核唤醒 cpuhp/0 进程

推动 AP 从 CPUHP_AP_ONLINE_IDLE 状态完成到 CPUHP_ONLINE状态 的转换。

cpuhp_thread_fun 函数 (位于文件 kernel/smp.c)

cpp 复制代码
/*
 * 函数:cpuhp_thread_fun
 * 功能:CPU 热插拔线程的主工作函数,负责执行特定状态的回调函数。
 * 参数:
 *   cpu: 目标 CPU 的编号。
 *   state: 要处理的目标状态(cpuhp_state)。
 *   bringup: 布尔值,true 表示上线操作,false 表示下线操作。
 *   st: 指向 CPU 热插拔状态信息结构体 (struct cpuhp_cpu_state)。
 */
static void cpuhp_thread_fun(unsigned int cpu, enum cpuhp_state state,
                             bool bringup, struct cpuhp_cpu_state *st)
{
    /*
     * 判断当前要处理的状态是否为"原子状态"。
     * 原子状态的回调函数需要在关闭中断的情况下执行,以保证操作的原子性,
     * 避免在执行过程中被中断打断,导致状态不一致。
     */
    if (cpuhp_is_atomic_state(state)) {
        local_irq_disable(); // 关闭本地中断

        /*
         * 调用与目标状态关联的回调函数。
         * 这个函数是实际执行 CPU 状态转换工作的地方。
         * 结果会保存在 st->result 中。
         */
        st->result = cpuhp_invoke_callback(cpu, state, bringup, st->node, &st->last);

        local_irq_enable(); // 重新开启本地中断

        WARN_ON_ONCE(st->result); // 如果回调执行失败(result != 0),打印警告信息
    } else {
        /*
         * 如果不是原子状态,则直接在开启中断的上下文下调用回调函数。
         * 这类回调通常不要求严格的原子性,可以被中断。
         */
        st->result = cpuhp_invoke_callback(cpu, state, bringup, st->node, &st->last);
    }
}

cpuhp 线程 最终通过 cpuhp_invoke_callback 完成回调,使 AP 达到 CPUHP_ONLINE 状态,从而可被内核调度 ,与 BP 共同承担系统负载。若存在多个 AP,内核会为每个 AP 重复执行上述启动流程,直至所有 AP 均启动成功

第 22 章 Sysfs

Sysfs 是与 configfs、debugfs 同类的内存文件系统,核心作用是向用户空间导出内核对象,呈现 kobject 层级结构。

它既支持用户查看和配置内核参数,又能清晰展示设备、驱动、总线等硬件相关的层级关系,是 Linux 统一设备模型的重要管理载体,也是驱动测试中常用的 /sys 目录底层支撑。

22.1 Sysfs 的目录结构

Sysfs 是虚拟文件系统 ,文件仅存在于内存,不对应硬盘存储。其默认挂载点为 /sys,也可通过命令挂载至其他位置,是内核向用户空间暴露硬件和驱动信息的核心接口

bash 复制代码
mount -t sysfs sysfs /sys

一般情况下/sys 的目录结构:

cpp 复制代码
total 0
drwxr-xr-x 2 root root 0 Oct 19 09:49 block
drwxr-xr-x 33 root root 0 Oct 19 09:49 bus
drwxr-xr-x 69 root root 0 Oct 19 09:49 class
drwxr-xr-x 4 root root 0 Oct 19 09:49 dev
drwxr-xr-x 11 root root 0 Oct 19 09:49 devices
drwxr-xr-x 3 root root 0 Oct 19 09:49 firmware
drwxr-xr-x 8 root root 0 Oct 19 09:49 fs
drwxr-xr-x 12 root root 0 Oct 19 09:49 kernel
drwxr-xr-x 151 root root 0 Oct 19 09:49 module
drwxr-xr-x 3 root root 0 Oct 19 09:49 power

struct kobject 在 <linux/kobject.h> 中定义,它包含了很多成员,但核心的有几个:

cpp 复制代码
struct kobject {
    const char      *name;      // 对象的名称,会成为 sysfs 中的目录名
    struct list_head    entry;   // 用于将 kobject 链接到其所属的 kset 中
    struct kobject      *parent; // 指向父 kobject,形成层级结构
    struct kset     *kset;      // 指向该 kobject 所属的 kset(一组同类 kobject)
    struct kobj_type    *ktype;  // 指向该 kobject 的类型描述符
    struct sysfs_dirent *sd;     // 对应 sysfs 中的目录项
    struct kref     kref;       // 引用计数
    ...
};

kobject 是 Linux 内核中一个非常底层和基础的结构。它虽然简单,但通过它构建起来的 对象模型sysfs 文件系统 ,极大地简化了内核对各种硬件和软件实体的管理,并为用户空间提供了一个统一、直观的方式来与内核进行交互

22.1.1 block 目录

这是 /sys/block 目录的列表及其说明:

cpp 复制代码
cat@lubancat:/sys/block$ ls -l
total 0
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop0 -> ../devices/virtual/block/loop0
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop1 -> ../devices/virtual/block/loop1
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop2 -> ../devices/virtual/block/loop2
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop3 -> ../devices/virtual/block/loop3
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop4 -> ../devices/virtual/block/loop4
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop5 -> ../devices/virtual/block/loop5
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop6 -> ../devices/virtual/block/loop6
lrwxrwxrwx 1 root root 0 Oct 19 10:12 loop7 -> ../devices/virtual/block/loop7
lrwxrwxrwx 1 root root 0 Oct 19 10:12 mmcblk0 -> ../devices/platform/fe310000.sdhci/mmc_host/mmc0/mmc0:0001/block/mmcblk0
lrwxrwxrwx 1 root root 0 Oct 19 10:12 mmcblk0boot0 -> ../devices/platform/fe310000.sdhci/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0boot0
lrwxrwxrwx 1 root root 0 Oct 19 10:12 mmcblk0boot1 -> ../devices/platform/fe310000.sdhci/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0boot1
lrwxrwxrwx 1 root root 0 Oct 19 10:12 mmcblk1 -> ../devices/platform/fe2b0000.dwmmc/mmc_host/mmc1/mmc1:aaaa/block/mmcblk1
lrwxrwxrwx 1 root root 0 Oct 19 10:12 ram0 -> ../devices/virtual/block/ram0
lrwxrwxrwx 1 root root 0 Oct 19 10:12 zram0 -> ../devices/virtual/block/zram0

22.1.2 bus、devices、class 目录

22.1.3 firmware 目录

/sys/firmware 目录包含与固件相关的子目录和属性文件,用于内核固件加载机制及固件驱动的交互。

cpp 复制代码
cat@lubancat:/sys/firmware$ ls -l
total 0
drwxr-xr-x 3 root root 0 Oct 19 11:34 devicetree
-r-------- 1 root root 163840 Oct 19 11:34 fdt

权限\] \[链接数\] \[所有者\] \[所属组\] \[大小\] \[修改日期\] \[名称


devicetree 大小为 0因为它本质是文件夹而不是文件,
fdt 有size而 firewares的 total又是0,
这是因为虚拟文件系统占用的是内存,不是磁盘空间

bash 复制代码
# 查看 fdt 文件的前 64 个字节
hexdump -C -n 64 /sys/firmware/fdt
cpp 复制代码
00000000  d0 0d fe ed 00 00 01 00  00 00 00 38 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

左侧 00000000 是字节偏移量(十六进制)。

中间 d0 0d fe ed ... 是每个字节的十六进制表示。

右侧 |...........8....| 是对应的 ASCII 字符(. 表示不可打印字符)。

22.1.4 fs 目录

这是 /sys/fs 目录的列表及其说明:

cpp 复制代码
cat@lubancat:/sys/fs$ ls -l
total 0
drwx-----T 2 root root 0 Oct 19 14:41 bpf
drwxr-xr-x 10 root root 280 Oct 19 14:41 cgroup
drwxr-xr-x 5 root root 0 Oct 19 16:58 ext4
drwxr-xr-x 3 root root 0 Oct 19 14:41 fuse
drwxr-x--- 2 root root 0 Oct 19 14:41 pstore
drwxr-xr-x 3 root root 0 Oct 19 16:58 xfs

22.1.5 kernel 目录

/sys/kernel 目录是内核暴露可调整参数的位置,但部分参数仍在 /proc/sys/kernel 接口中。

22.1.6 module 目录

/sys/module 目录包含系统中所有内核模块(无论是内置还是外部加载)的信息。

外部模块 (.ko):

加载后会在该目录下创建一个子目录,包含其版本、状态、参数等属性文件。

内置模块:

仅当模块有非零默认值的参数时,才会出现其目录,主要用于暴露这些可调整的参数(位于 <module_name>/parameters/ 下)。

bash 复制代码
# 查看 printk 模块的 time 参数当前值
cat /sys/module/printk/parameters/time
# 输出: Y (表示启用时间戳)

# 动态修改该参数,关闭时间戳
echo 0 | sudo tee /sys/module/printk/parameters/time

# 再次查看确认修改
cat /sys/module/printk/parameters/time
# 输出: N (表示已关闭)

此接口提供了在系统运行时动态调整内核模块参数的能力,是重要的系统调试和优化工具。

内置模块的参数也可在 kernel 启动时通过命令行参数(如 printk.time=1)预设。

22.1.7 power 目录

/sys/power 目录提供电源管理子系统的统一接口,部分属性文件可用于控制系统电源状态(如关机、重启)。

22.2 Sysfs 使用

sysfs 模型的核心是 struct kobject(定义于 <linux/kobject.h>),它是内核对象的抽象表示,与 sysfs 目录项紧密绑定 ------ 每个 kobject 对应 sysfs 中的一个目录 ,其属性对应目录下的文件,对象间关系对应目录链接。

简单介绍下设备模型核心数据结构 kobject:

kobject(include/linux/kobject.h)

cpp 复制代码
struct kobject {
    const char      *name;       // 对象名称,对应 sysfs 目录名
    struct list_head    entry;   // 用于将 kobject 链接到所属 kset 的链表项
    struct kobject      *parent; // 父 kobject 指针,构成 sysfs 目录层级
    struct kset     *kset;      // 所属的 kset(同类 kobject 集合)
    struct kobj_type    *ktype;  // 对象类型描述符,定义属性和操作
    struct kernfs_node  *sd;     // 对应 sysfs 的目录项(kernfs 节点)
    struct kref     kref;       // 引用计数,管理对象生命周期
    /*....*/
};

22.2.1 创建一个目录

cpp 复制代码
/** 创建并初始化一个kobject对象,并将其添加到指定父对象下,同时在sysfs中创建对应目录。 */
struct kobject *kobject_create_and_add(
    const char *name,        // kobject的名称,将作为sysfs中的目录名
    struct kobject *parent   // 父kobject指针,指定sysfs中的父目录。为NULL则创建在sysfs根目录
);

22.2.2 创建一个文件

cpp 复制代码
/** 在指定kobject对应的sysfs目录中创建一个文件。 */
int sysfs_create_file(
    struct kobject *kobj,    // 关联的kobject,文件将创建在其sysfs目录下
    const struct attribute *attr // 描述要创建的文件的属性(如名称、权限、show/store方法)
);

22.3 简单实验

22.3.1 程序源码

inux_driver/Sysfs/sys_test.c

cpp 复制代码
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/err.h>

// 1. 定义一个内核变量,将通过 sysfs 暴露给用户空间
static volatile int test_value = 0;

// 2. 声明一个 kobject 指针,用于在 sysfs 中创建目录
static struct kobject *kobj_test;

// 3. sysfs 文件的 "读" 操作处理函数
//    当用户空间读取 /sys/sysfs_test/test_value 文件时,此函数被调用
static ssize_t sysfs_show(struct kobject *kobj, 
                          struct kobj_attribute *attr, 
                          char *buf)
{
    pr_info("sysfs file is read\n");
    // 将 test_value 的值格式化为字符串并写入到 buf 中,用户空间即可读取
    return sprintf(buf, "test_value = %d\n", test_value);
}

// 4. sysfs 文件的 "写" 操作处理函数
//    当用户空间向 /sys/sysfs_test/test_value 文件写入时,此函数被调用
static ssize_t sysfs_store(struct kobject *kobj, 
                           struct kobj_attribute *attr,
                           const char *buf, 
                           size_t count)
{
    pr_info("sysfs file is written\n");
    // 从用户空间传入的 buf 中解析出整数,并存入 test_value
    sscanf(buf, "%d", &test_value);
    return count; // 返回写入的字节数,表示成功
}

// 5. 使用 __ATTR 宏来定义一个 kobj_attribute 对象
//    这个宏会帮助我们初始化一个 struct kobj_attribute 结构体
//    参数分别是:文件名、文件权限、读函数、写函数
static struct kobj_attribute sysfs_test_attr = 
    __ATTR(test_value, 0664, sysfs_show, sysfs_store);

// 6. 模块初始化函数,当模块被加载时执行
static int __init sysfs_test_driver_init(void)
{
    int ret;

    // 7. 创建一个名为 "sysfs_test" 的 kobject,并将其添加到 sysfs 的根目录下
    //    这会在 /sys/ 目录下创建一个名为 sysfs_test 的目录
    kobj_test = kobject_create_and_add("sysfs_test", NULL);
    if (!kobj_test) {
        pr_err("kobject create failed\n");
        return -ENOMEM; // 如果创建失败,返回错误
    }

    // 8. 在刚刚创建的 /sys/sysfs_test/ 目录下,创建一个名为 test_value 的文件
    //    这个文件的行为由上面定义的 sysfs_test_attr 决定
    ret = sysfs_create_file(kobj_test, &sysfs_test_attr.attr);
    if (ret) {
        pr_err("sysfs create file failed\n");
        goto error_sysfs; // 如果失败,跳转到错误处理流程
    }

    pr_info("sysfs test driver initialized successfully\n");
    return 0;

error_sysfs:
    // 9. 错误处理:如果文件创建失败,需要先释放已经创建的 kobject
    kobject_put(kobj_test); 
    return ret;
}

// 10. 模块退出函数,当模块被卸载时执行
static void __exit sysfs_test_driver_exit(void)
{
    // 11. 清理工作:移除在 sysfs 中创建的文件
    sysfs_remove_file(kobj_test, &sysfs_test_attr.attr);

    // 12. 减少 kobject 的引用计数,当计数为 0 时,内核会自动销毁它
    //     这会移除 /sys/sysfs_test 目录
    kobject_put(kobj_test);

    pr_info("sysfs test driver unloaded successfully\n");
}

// 13. 宏定义,指定模块的初始化和退出函数
module_init(sysfs_test_driver_init);
module_exit(sysfs_test_driver_exit);

// 14. 模块的元信息
MODULE_LICENSE("GPL");              // 声明模块遵循 GPL 协议
MODULE_AUTHOR("Embedfire");         // 模块作者
MODULE_DESCRIPTION("A simple sysfs driver example"); // 模块描述

内核的高层驱动模型(如 cdev、bus、device)通过包含一个 kobject 成员,来自动获得在 sysfs 中创建文件和目录的能力,以及统一的对象生命周期管理。

开发者在使用这些高层结构体时,就间接利用了 kobject 和 sysfs 的功能。

22.3.2 测试

使用交叉编译器将驱动代码编译为 sys_test.ko 文件。

bash 复制代码
# 假设你的 Makefile 已正确配置
make

传输模块:将生成的 sys_test.ko 文件传输到目标板卡的文件系统中。

bash 复制代码
scp sys_test.ko root@<board_ip_address>:/path/on/board/

登录板卡:通过 SSH 或串口登录到目标板卡。

加载模块:在板卡上使用 insmod 命令加载内核模块。

bash 复制代码
sudo insmod /path/on/board/sys_test.ko

读写操作

读取文件:使用 cat 命令读取 /sys/sysfs_test/test_value 文件的内容。

bash 复制代码
cat /sys/sysfs_test/test_value

预期结果:会显示 test_value 的当前值(初始为 0),例如:test_value = 0。

同时,内核日志会打印出 读 sysfs!(可通过 dmesg 查看)。

写入文件 :使用 echo 命令向 /sys/sysfs_test/test_value 文件写入一个新值。

cpp 复制代码
echo 123 | sudo tee /sys/sysfs_test/test_value

tee 命令用于将标准输入写入到文件中,sudo 是必需的,因为该文件通常只有 root 用户才有写权限。

内核日志会打印出 写 sysfs!(可通过 dmesg 查看)。

验证写入:再次读取文件以确认值已更新。

bash 复制代码
cat /sys/sysfs_test/test_value

预期结果:会显示你刚刚写入的值,例如:test_value = 123。

卸载模块(可选)

bash 复制代码
sudo rmmod sys_test

卸载后,/sys/sysfs_test 目录会被自动移除。

关于dmesg

dmesg 命令能够看到你在驱动中用 pr_info 打印的日志,是因为 Linux 内核有一个专门的 ** 环形缓冲区(Ring Buffer)** 来存储内核消息。

/var/log/dmesg 文件:在很多 Linux 发行版中,

系统启动时会将 dmesg 的内容保存到 /var/log/dmesg 文件 中,方便日后查看启动时的内核信息。但实时的日志 还是需要通过 dmesg 命令查看。

journalctl -k

在使用 systemd 的现代 Linux 系统上,journalctl -k 或 journalctl --dmesg 是查看内核消息更强大和推荐的方式。systemd-journald 服务 会持续收集内核消息(以及其他系统日志),并提供更丰富的查询和过滤功能。

printk 与 printf 的区别:printf 是 C 标准库函数,用于用户空间程序向其控制终端输出。printk 是内核函数,用于内核向其日志缓冲区输出。内核中没有标准库,所以不能使用 printf。

bash 复制代码
-k 是 --dmesg 的 "快捷方式",打字更简洁;
--dmesg 是 "全称",可读性更强,适合不熟悉短参数的用户。
-kf 查看实时内核消息
相关推荐
学嵌入式的长路1 天前
正点原子imx6ull移植lvgl v8.3及触摸屏调试
linux·驱动开发·lvgl·imx6ull·触摸屏
DeeplyMind1 天前
Guest → QEMU → Virglrenderer 调用逻辑分析
linux·驱动开发·虚拟化·virtio-gpu·virglrenderer
x***J3482 天前
测试驱动开发:从单元测试到集成测试
驱动开发·单元测试·集成测试
赖small强4 天前
【Linux驱动开发】Linux MMC子系统技术分析报告 - 第二部分:协议实现与性能优化
linux·驱动开发·mmc
Saniffer_SH4 天前
通过近期测试简单聊一下究竟是直接选择Nvidia Spark还是4090/5090 GPU自建环境
大数据·服务器·图像处理·人工智能·驱动开发·spark·硬件工程
赖small强4 天前
【Linux驱动开发】Linux电源管理系统架构及驱动实现详细分析
linux·驱动开发·suspend·cpufreq·cpuidle·runtime pm
赖small强4 天前
【Linux驱动开发】Linux设备驱动中内存与I/O访问的底层机制及技术实现深度解析
linux·驱动开发·内存与io访问
赖small强4 天前
【Linux驱动开发】Linux网络设备驱动底层原理与实现详解
linux·驱动开发·socket·net_device·sk_buff
骑猪兜风2334 天前
大厂集体押注 SDD!阿里、腾讯、亚马逊都在用的规范驱动开发,优势在哪?坑怎么避?
人工智能·驱动开发·经验分享·langchain·ai编程