9. Linux内核Regmap API

在Linux内核驱动开发中,各类外设(如SPI、I2C、MMIO(内存映射I/O)等)的寄存器操作是驱动实现的核心环节。 传统驱动开发中,开发者需要针对不同外设的通信协议(如SPI的读写时序、I2C的地址传输), 重复编写大量寄存器读写代码,不仅增加了开发工作量,还导致驱动代码冗余、可维护性差。 为解决这一问题,Linux内核从3.1版本开始引入了Regmap(Register Map)机制,它是一种通用的寄存器访问抽象层, 能够统一各类外设的寄存器操作接口,屏蔽不同通信协议的底层差异,同时支持寄存器缓存、原子操作、批量读写等高级特性, 大幅简化驱动开发流程、提升代码复用性和稳定性。

9.1. Regmap概念

Regmap(Register Map,寄存器映射)是Linux内核提供的一套通用寄存器访问抽象层,其核心目的是将不同外设(SPI、I2C、MMIO等)的寄存器操作, 封装成统一的API接口,让开发者无需关注底层通信协议的细节,只需调用统一的函数即可完成寄存器的读、写、批量操作等功能。

Regmap本质上是通过“寄存器地址-值”的映射关系,对设备寄存器进行管理,同时内置缓存机制、锁机制和错误处理机制,既简化了驱动开发,又提升了寄存器操作的效率和可靠性。

核心优势:

  • 统一接口,简化开发:屏蔽SPI、I2C、MMIO等不同外设的通信差异,提供统一的寄存器读写API,开发者无需重复编写底层通信时序代码,大幅减少驱动开发工作量。

  • 内置缓存,提升效率:支持寄存器值缓存,对于只读寄存器或不常变化的寄存器,无需每次都访问硬件,直接从缓存中读取,减少硬件访问次数,降低CPU占用率。

  • 原子操作,保证安全:内置自旋锁/互斥锁机制,确保寄存器操作的原子性,避免多线程、中断上下文并发访问导致的寄存器值错乱。

  • 支持批量操作:提供批量读写API,可一次性完成多个连续寄存器的读写操作,适用于需要批量配置寄存器的场景,如外设初始化。

  • 可扩展性强:支持自定义寄存器访问逻辑,可适配各类特殊外设的寄存器操作需求,同时支持多种通信总线的扩展。

9.2. Regmap API基础知识

9.2.1. Regmap核心结构体

Regmap的核心功能依赖于两个关键结构体:Regmap配置结构体(struct regmap_config)和Regmap实例结构体(struct regmap),是Regmap API使用的基础。

9.2.1.1. Regmap配置结构体

Regmap配置结构体(struct regmap_config)用于配置Regmap的核心参数,如寄存器地址位宽、数据位宽、缓存策略等, 开发者在初始化Regmap前需先配置该结构体,指定Regmap的工作特性。

regmap_config结构体(内核源码/include/linux/regmap.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct regmap_config {
    /* 设备名称 */
    const char *name;

    /* 寄存器地址位宽(单位:bit),常见值:8、16、32,如SPI外设常用8位地址 */
    int reg_bits;
    /* 寄存器数据位宽(单位:bit),常见值:8、16、32,与外设寄存器数据宽度一致 */
    int val_bits;

    /* 是否禁用Regmap内置的锁机制,true=禁用,默认false;启用锁,可保证操作原子性 */
    bool disable_locking;

    /* 自定义读寄存器函数,用于特殊外设的寄存器访问逻辑,默认NULL,使用Regmap默认实现 */
    int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
    /* 自定义写寄存器函数,用于特殊外设的寄存器访问逻辑,默认NULL,使用Regmap默认实现 */
    int (*reg_write)(void *context, unsigned int reg, unsigned int val);

    /* 是否使用快速I/O操作,适用于MMIO等高速访问场景,true=启用 */
    bool fast_io;

    /* 最大寄存器地址,用于边界检查,避免访问非法寄存器地址 */
    unsigned int max_register;
    /* 可写寄存器表,指定哪些寄存器可写 */
    const struct regmap_access_table *wr_table;
    /* 可读寄存器表,指定哪些寄存器可读 */
    const struct regmap_access_table *rd_table;
    /* 缓存类型,指定Regmap使用的缓存机制 */
    enum regcache_type cache_type;

    /* 是否支持批量写操作,true=支持,默认false */
    bool can_multi_write;

    /* 设置寄存器地址的字节序 */
    enum regmap_endian reg_format_endian;
    /* 设置寄存器数值的字节序 */
    enum regmap_endian val_format_endian;


    /* 省略部分成员 */
};

其中cache_type和val_format_endian可选取值如下:

regcache_type和val_format_endian枚举(内核源码/include/linux/regmap.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum regcache_type {
    REGCACHE_NONE,           /* 关闭寄存器缓存,所有操作直接访问硬件I2C/SPI,实时性最高,如ADC/传感器 */
    REGCACHE_RBTREE,         /* 红黑树结构缓存,适用于寄存器数量多、地址不连续的复杂设备,如Codec/PMIC */
    REGCACHE_COMPRESSED,     /* 压缩格式缓存,节省内存,用于超大规模寄存器的专用设备 */
    REGCACHE_FLAT,           /* 扁平数组缓存,适用于寄存器地址连续、数量较少的简单设备 */
};


enum regmap_endian {
    REGMAP_ENDIAN_DEFAULT = 0,/* 默认字节序,由regmap框架根据总线/设备自动配置 */
    REGMAP_ENDIAN_BIG,        /* 大端序,高字节优先, */
    REGMAP_ENDIAN_LITTLE,     /* 小端序,低字节优先 */
    REGMAP_ENDIAN_NATIVE,     /* 本地CPU字节序,自动适配当前平台的CPU大小端模式 */
};

9.2.1.2. Regmap实例结构体

Regmap实例结构体(struct regmap)是Regmap的核心实例,由Regmap初始化函数返回,封装了Regmap的所有状态信息(如缓存、锁、配置、底层通信接口等), 后续所有寄存器操作都需要通过该结构体指针完成。

regmap结构体(内核源码/drivers/base/regmap/internal.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct regmap {
    /* 并发保护机制:支持互斥锁和自旋锁,保证寄存器操作原子性 */
    union {
        struct mutex mutex;               /* 互斥锁,用于进程上下文并发保护 */
        struct {
            spinlock_t spinlock;          /* 自旋锁,用于中断上下文并发保护 */
            unsigned long spinlock_flags; /* 自旋锁标志位 */
        };
    };
    /* 自定义锁/解锁函数,用于替换Regmap默认锁机制 */
    regmap_lock lock;
    regmap_unlock unlock;
    void *lock_arg;                 /* 传递给锁/解锁函数的上下文参数 */

    /* 设备及核心操作相关 */
    struct device *dev;             /* 关联的设备结构体,用于I/O操作关联 */
    void *work_buf;                 /* 用于格式化I/O操作的临时缓存区 */
    const struct regmap_bus *bus;   /* 底层通信总线接口,屏蔽SPI/I2C/MMIO差异 */
    void *bus_context;              /* 总线上下文指针,如SPI/I2C设备指针 */
    const char *name;               /* Regmap实例名称 */

    /* 核心寄存器操作函数,可自定义实现特殊访问逻辑 */
    int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
    int (*reg_write)(void *context, unsigned int reg, unsigned int val);
    int (*reg_update_bits)(void *context, unsigned int reg,
                        unsigned int mask, unsigned int val);

    /* 寄存器范围及访问控制 */
    unsigned int max_register;      /* 最大寄存器地址,用于边界检查 */
    bool (*writeable_reg)(struct device *dev, unsigned int reg); /* 自定义可写判断 */
    bool (*readable_reg)(struct device *dev, unsigned int reg);  /* 自定义可读判断 */
    const struct regmap_access_table *wr_table; /* 可写寄存器表 */
    const struct regmap_access_table *rd_table; /* 可读寄存器表 */

    /* 读写标志及地址配置 */
    unsigned long read_flag_mask;   /* 读操作附加标志掩码 */
    unsigned long write_flag_mask;  /* 写操作附加标志掩码 */
    int reg_shift;                  /* 寄存器地址移位位数 */
    int reg_stride;                 /* 寄存器地址步长 */

    /* 缓存相关,Regmap核心优势相关成员 */
    enum regcache_type cache_type;      /* 缓存类型,如写透、回写 */
    struct reg_default *reg_defaults;   /* 寄存器默认值数组 */
    const void *reg_defaults_raw;       /* 原始格式寄存器默认值 */
    void *cache;                        /* 寄存器缓存存储区 */
    bool cache_dirty;                   /* 缓存是否脏标记 */
    unsigned int num_reg_defaults;      /* 寄存器默认值个数 */

    /* 批量操作控制 */
    bool can_multi_write;           /* 是否支持批量写操作 */
    bool use_single_read;           /* 是否将批量读转为单次读 */
    bool use_single_write;          /* 是否将批量写转为单次写 */

    size_t max_raw_read;            /* 最大原始读操作长度限制 */
    size_t max_raw_write;           /* 最大原始写操作长度限制 */

    /* 省略部分成员 */
};

9.2.2. Regmap总线类型

Regmap支持多种底层通信总线,每种总线对应一个struct regmap_bus结构体,该结构体封装了对应总线的寄存器读写实现,Regmap通过该结构体屏蔽不同总线的底层差异。

regmap_bus结构体(内核源码/drivers/base/regmap/internal.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct regmap_bus {
    /* 是否支持快速I/O操作,适用于MMIO等高速访问场景,true=启用 */
    bool fast_io;

    /* 核心总线写操作函数,实现底层总线的写逻辑 */
    regmap_hw_write write;
    /* 核心总线读操作函数,实现底层总线的读逻辑 */
    regmap_hw_read read;

    /* 寄存器级写操作函数,封装单寄存器写逻辑,适配Regmap统一接口 */
    regmap_hw_reg_write reg_write;
    /* 寄存器级读操作函数,封装单寄存器读逻辑,适配Regmap统一接口 */
    regmap_hw_reg_read reg_read;

    /* 最大原始读操作长度限制,指定单次批量读的最大字节数 */
    size_t max_raw_read;
    /* 最大原始写操作长度限制,指定单次批量写的最大字节数 */
    size_t max_raw_write;

    /* 省略部分成员 */
};

9.3. Regmap API驱动核心函数

Regmap API提供了一系列通用函数,涵盖Regmap初始化、寄存器读写、批量操作、缓存管理、注销等功能,所有函数均定义于内核源码include/linux/regmap.h, 实现于内核源码/drivers/base/regmap/目录下。

9.3.1. Regmap初始化函数

初始化函数用于创建Regmap实例,不同总线对应不同的初始化函数,核心差异在于底层总线接口的绑定。

9.3.1.1. SPI总线Regmap初始化(devm_regmap_init_spi)

devm_regmap_init_spi函数专门用于SPI总线外设的Regmap实例初始化,将Regmap配置参数与SPI设备关联,创建并返回Regmap实例; 支持devm资源管理,设备卸载时自动释放Regmap相关资源,无需手动注销。

函数原型:

1
2
struct regmap *devm_regmap_init_spi(struct spi_device *spi,
                                const struct regmap_config *config);

参数说明:

  • spi:指向struct spi_device的指针,SPI外设设备实例。

  • config:指向struct regmap_config的指针,Regmap配置参数。

返回值:

  • 成功返回struct regmap实例指针;失败返回错误指针,可通过IS_ERR()判断,PTR_ERR()获取错误码。

9.3.1.2. I2C总线Regmap初始化(devm_regmap_init_i2c)

devm_regmap_init_i2c函数专门用于I2C总线外设的Regmap实例初始化,关联I2C从设备与Regmap配置,创建并返回Regmap实例; 支持devm资源管理,简化驱动资源释放流程。

函数原型:

1
2
struct regmap *devm_regmap_init_i2c(struct i2c_client *client,
                                const struct regmap_config *config);

参数说明:

  • client:指向struct i2c_client的指针,I2C外设设备实例。

  • config:指向struct regmap_config的指针,Regmap配置参数。

返回值:

  • 成功返回struct regmap实例指针;失败返回错误指针,可通过IS_ERR()判断,PTR_ERR()获取错误码。

9.3.1.3. MMIO总线Regmap初始化(devm_regmap_init_mmio)

devm_regmap_init_mmio函数专门用于MMIO接口外设的Regmap实例初始化,将MMIO映射后的虚拟地址与Regmap配置关联,创建并返回Regmap实例; 支持devm资源管理,适用于平台设备等使用MMIO接口的外设。

函数原型:

1
2
3
struct regmap *devm_regmap_init_mmio(struct device *dev,
                                void __iomem *base,
                                const struct regmap_config *config);

参数说明:

  • dev:指向struct device的指针,关联的设备实例(如平台设备)。

  • base:MMIO地址映射后的虚拟地址指针(通过ioremap获取)。

  • config:指向struct regmap_config的指针,Regmap配置参数。

返回值:

  • 成功返回struct regmap实例指针;失败返回错误指针,可通过IS_ERR()判断,PTR_ERR()获取错误码。

9.3.1.4. 通用Regmap初始化(devm_regmap_init)

devm_regmap_init函数是通用型Regmap实例初始化函数,适用于自定义总线、特殊外设或未被内核内置支持的总线场景, 需手动指定底层总线接口(struct regmap_bus),灵活适配各类非标准外设的Regmap初始化需求;支持devm资源管理。

函数原型:

1
2
3
4
struct regmap *devm_regmap_init(struct device *dev,
                            const struct regmap_bus *bus,
                            void *bus_context,
                            const struct regmap_config *config);

参数说明:

  • dev:关联的设备实例。

  • bus:指向struct regmap_bus的指针,底层总线接口。

  • bus_context:总线上下文指针(如SPI/I2C设备指针)。

  • config:Regmap配置参数。

返回值:

  • 成功返回struct regmap实例指针;失败返回错误指针,可通过IS_ERR()判断,PTR_ERR()获取错误码。

9.3.2. 寄存器读写函数

寄存器读写是Regmap的核心功能,提供单寄存器读写和批量读写,支持缓存机制,常用函数如下:

9.3.2.1. 单寄存器写(regmap_write)

regmap_write函数用于向指定寄存器写入一个单个值,是Regmap最基础的写操作函数; 会根据Regmap配置的缓存策略(如写透、回写)同步更新缓存(若缓存启用),同时进行寄存器地址合法性检查,确保写操作安全有效。

函数原型:

1
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);

参数说明:

  • map:Regmap实例指针。

  • reg:寄存器地址,需符合配置的reg_bits位宽。

  • val:要写入的值,需符合配置的val_bits位宽。

返回值:

  • 成功返回0;失败返回负数错误码。

9.3.2.2. 单寄存器读(regmap_read)

regmap_read函数用于读取指定寄存器的单个值,是Regmap最基础的读操作函数;若缓存启用,优先从缓存中读取数据(缓存命中),减少硬件访问次数; 若缓存未命中或缓存禁用,则直接访问硬件读取,并同步更新缓存(若缓存启用)。

函数原型:

1
int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);

参数说明:

  • map:Regmap实例指针。

  • reg:寄存器地址。

  • val:指向存储读取结果的指针,需提前分配内存。

返回值:

  • 成功返回0;失败返回负数错误码。

9.3.2.3. 批量写寄存器(regmap_bulk_write)

regmap_bulk_write函数用于从指定起始寄存器地址开始,批量写入多个连续的寄存器值,适用于外设初始化时批量配置多个寄存器的场景(如一次性配置外设的工作模式、参数等); 支持缓存同步(若缓存启用),相比多次调用regmap_write,可大幅提升操作效率,减少总线通信次数。

函数原型:

1
2
int regmap_bulk_write(struct regmap *map, unsigned int reg,
                 const void *val, size_t count);

参数说明:

  • map:Regmap实例指针。

  • reg:起始寄存器地址。

  • val:指向存储要写入数据的缓冲区指针。

  • count:要写入的数据个数(每个数据的位宽由val_bits指定)。

返回值:

  • 成功返回0;失败返回负数错误码。

9.3.2.4. 批量读寄存器(regmap_bulk_read)

regmap_bulk_read函数用于从指定起始寄存器地址开始,批量读取多个连续的寄存器值,适用于需要一次性获取多个相关寄存器数据的场景(如读取外设的状态、数据采集结果等); 支持缓存机制,缓存命中时直接读取缓存,未命中时访问硬件并同步缓存,提升读取效率。

函数原型:

1
2
int regmap_bulk_read(struct regmap *map, unsigned int reg,
                void *val, size_t count);

参数说明:

  • map:Regmap实例指针。

  • reg:起始寄存器地址。

  • val:指向存储读取结果的缓冲区指针,需提前分配足够内存。

  • count:要读取的数据个数(每个数据的位宽由val_bits指定)。

返回值:

  • 成功返回0;失败返回负数错误码。

9.3.2.5. 寄存器位操作(regmap_update_bits)

regmap_update_bits函数用于对寄存器的指定位进行精准修改,不影响其他未指定的位,是驱动开发中常用的原子操作函数; 操作流程为“读-改-写”:先读取寄存器当前值,根据掩码(mask)修改指定位,再将修改后的值写回寄存器; 支持缓存同步,确保缓存与硬件状态一致,适用于使能/禁用外设功能、修改参数位等场景。

函数原型:

1
2
int regmap_update_bits(struct regmap *map, unsigned int reg,
                  unsigned int mask, unsigned int val);

参数说明:

  • map:Regmap实例指针。

  • reg:寄存器地址。

  • mask:位掩码,指定要修改的位(掩码位为1的位将被修改,0的位保持不变)。

  • val:要写入的位值(仅掩码位对应的位有效,其他位无效)。

返回值:

  • 成功返回0;失败返回负数错误码。

9.3.3. Regmap注销函数

9.3.3.1. regmap_exit函数

当驱动卸载时,需注销Regmap实例,释放相关资源。若使用devm_前缀的初始化函数,内核会自动注销,无需手动调用;若未使用devm_前缀,需手动调用regmap_exit函数。

函数原型:

1
void regmap_exit(struct regmap *map);

参数说明:

  • map:Regmap实例指针。

9.4. Regmap API实验

本实验所使用的OLED屏幕驱动芯片为SSD1306,该芯片支持SPI和I2C两种主流通信方式,可根据实际硬件需求灵活选择接口。 本实验围绕该OLED屏幕,分别实现SPI和I2C总线的Regmap API实验,详细讲解基于Regmap封装的驱动程序核心逻辑。

Regmap API的核心优势之一便是能够统一两种总线的寄存器操作逻辑,基于该API编写驱动时,无需大幅修改核心代码, 仅需调整总线相关的初始化配置和设备树参数,即可实现SPI与I2C接口的快速切换,极大提升了驱动的可移植性和开发效率。

9.4.1. SPI子系统Regmap API实验

本实验在SPI子系统实验基础上进行修改,通过Regmap写命令/写数据,设备树插件和SPI子系统实验的完全一致,驱动仅说明修改部分。

SPI接口OLED模块参考链接: 野火【OLED屏_SPI_0.96寸】模块

本章的示例代码目录为: linux_driver/26_spi_regmap_api

9.4.1.1. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* OLED 私有设备结构体,统一管理字符设备、SPI设备、GPIO、设备号等资源 */
struct oled_dev {
    dev_t devno;               /* 字符设备号 */
    struct cdev cdev;          /* 字符设备结构体 */
    struct class *class;       /* 设备类 */
    struct device *device;     /* 设备节点 */
    struct spi_device *spi;    /* SPI设备句柄 */
    struct regmap *regmap;     /* Regmap句柄 */
    int dc_gpio;               /* DC控制引脚编号 */
};
  • struct oled_dev:私有结构体,整合了字符设备、SPI设备、Regmap句柄、GPIO引脚等所有资源,便于统一管理和释放。

驱动初始化

驱动初始化(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static int oled_spi_probe(struct spi_device *spi)
{
    ....

    /* 配置SPI参数 */
    spi->mode = SPI_MODE_0;
    ret = spi_setup(spi);
    if (ret) {
        printk(KERN_ERR "spi setup failed\n");
        goto free_gpio;
    }

    /* 通用Regmap初始化,绑定自定义SPI回调 */
    oled_dev.regmap = devm_regmap_init(&spi->dev, NULL, spi, &oled_regmap_config);
    if (IS_ERR(oled_dev.regmap)) {
        ret = PTR_ERR(oled_dev.regmap);
        printk(KERN_ERR "regmap init failed\n");
        goto free_gpio;
    }

    /* 保存SPI设备句柄到私有结构体 */
    oled_dev.spi = spi;

    ...
}
  • 第14行:使用devm_regmap_init函数进行Regmap实例初始化,参数:&spi->dev(设备指针)、NULL(无regmap类型,手动绑定回调)、spi(上下文,传递给自定义回调)、&oled_regmap_config(Regmap配置,绑定自定义写入回调)。

注意

此处不能使用devm_regmap_init_spi函数进行Regmap实例初始化,因为其内部实现了默认的SPI写入逻辑,该逻辑遵循“标准SPI寄存器操作规范”,即默认发送1字节寄存器地址 + 1字节数据,但SSD1306的SPI通信协议不需要寄存器地址,其仅需接收纯数据(命令或显示数据),多余的1字节地址会被SSD1306误解析为命令/数据,导致通信失败、显示异常。

Regmap配置结构体及写入回调函数

Regmap配置结构体及写入回调函数(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 自定义Regmap SPI写入回调,仅发数据,不发地址 */
static int oled_regmap_spi_write(void *context, unsigned int reg, unsigned int val)
{
    struct spi_device *spi = context;
    u8 data = val;
    // 只发1字节纯数据,匹配SSD1306 SPI协议
    return spi_write(spi, &data, 1);
}

/* Regmap配置结构体 */
static const struct regmap_config oled_regmap_config = {
    .val_bits = 8,                      // 寄存器数据位宽:8位
    .can_multi_write = true,            // 支持多字节批量写入
    .reg_write = oled_regmap_spi_write, // 绑定自定义SPI写入
};
  • regmap_config:Regmap的核心配置,指定寄存器数据位宽为8位,can_multi_write = true表示支持批量写入,提升效率。

  • 通过自定义SPI写入回调函数(oled_regmap_spi_write),并将其绑定到Regmap配置的.reg_write字段,替代devm_regmap_init_spi的默认写入逻辑。

  • 自定义SPI写入回调函数(oled_regmap_spi_write)仅发送1字节纯数据(val),不发送任何地址字节(reg参数被忽略),匹配SSD1306的SPI通信协议,因此必须使用通用的devm_regmap_init函数,手动绑定回调,而非专用的devm_regmap_init_spi。

OLED写命令函数

OLED写命令函数(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 函数功能:OLED 写命令
* spi:SPI设备句柄
* cmd:要写入的命令
* 返回值:0-成功,负数-失败
*/
static int oled_write_cmd(struct spi_device *spi, u8 cmd)
{
    int ret;
    /* 设置DC引脚为低电平,表示传输命令 */
    gpio_set_value(oled_dev.dc_gpio, 0);

    /* Regmap单字节写入命令 */
    ret = regmap_write(oled_dev.regmap, 0x00, cmd);
    if (ret) {
        printk(KERN_ERR "spi_oled write cmd failed\n");
        return ret;
    }

    /* 恢复DC引脚为高电平 */
    gpio_set_value(oled_dev.dc_gpio, 1);
    return 0;
}
  • 第14行:使用regmap_write写入单字节,此处reg=0x00仅为占位符,SSD1306无寄存器地址,只认DC引脚(DC=0 命令,DC=1 数据)。

OLED批量写命令函数

OLED批量写命令函数(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*
* 函数功能:OLED 批量写命令
* spi:SPI设备句柄
* cmds:命令缓冲区
* len:命令长度
* 返回值:0-成功,负数-失败
*/
static int oled_write_cmds(struct spi_device *spi, const u8 *cmds, u16 len)
{
    int i, ret;

    /* 循环逐字节写入命令 */
    for (i = 0; i < len; i++) {
        ret = oled_write_cmd(spi, cmds[i]);
        if (ret)
            return ret;
    }
    return 0;
}

由于SSD1306 SPI每个命令都需要独立的DC引脚时序,必须先拉低DC引脚,再发送,最后恢复DC引脚,因此无法使用regmap_bulk_write批量写入命令,只能循环调用oled_write_cmd。

OLED写数据函数

OLED写数据函数(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 函数功能:OLED 写单字节数据
* spi:SPI设备句柄
* data:要写入的数据
* 返回值:0-成功,负数-失败
*/
static int oled_write_data(struct spi_device *spi, u8 data)
{
    int ret;

    /* 设置DC引脚为高电平,表示传输数据 */
    gpio_set_value(oled_dev.dc_gpio, 1);

    /* Regmap单字节写入数据 */
    ret = regmap_write(oled_dev.regmap, 0x00, data);
    if (ret) {
        printk(KERN_ERR "spi_oled write data failed\n");
        return ret;
    }
    return 0;
}
  • 第15行:使用regmap_write写入单字节,此处reg=0x00仅为占位符,SSD1306无寄存器地址,只认DC引脚(DC=0 命令,DC=1 数据)。

OLED批量写数据函数

OLED批量写数据函数(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 函数功能:OLED 批量写数据
* spi:SPI设备句柄
* buf:数据缓冲区
* len:数据长度
* 返回值:0-成功,负数-失败
*/
static int oled_write_datas(struct spi_device *spi, const u8 *buf, u16 len)
{
    int ret;

    /* 设置DC引脚为高电平,表示传输数据 */
    gpio_set_value(oled_dev.dc_gpio, 1);

    /* Regmap批量写入数据 */
    ret = regmap_bulk_write(oled_dev.regmap, 0x00, buf, len);
    if (ret) {
        printk(KERN_ERR "spi_oled write datas failed\n");
        return ret;
    }

    return 0;
}
  • 第16行,调用regmap_bulk_write批量写入多个字节,此处reg=0x00仅为占位符,数据传输时,DC引脚保持高电平,通过regmap_bulk_write批量写入多个字节,相比循环调用oled_write_data,代码更简洁、效率更高。

remove函数

remove函数(位于linux_driver/26_spi_regmap_api/spi_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int oled_spi_remove(struct spi_device *spi)
{
    /* 打印平台驱动移除信息 */
    printk(KERN_INFO "spi_oled driver remove\n");

    /* 销毁设备节点 */
    device_destroy(oled_dev.class, oled_dev.devno);

    /* 删除字符设备 */
    cdev_del(&oled_dev.cdev);

    /* 释放设备号 */
    unregister_chrdev_region(oled_dev.devno, DEV_CNT);

    /* 销毁设备类 */
    class_destroy(oled_dev.class);

    /* 释放GPIO引脚 */
    gpio_free(oled_dev.dc_gpio);

    /* 清空设备句柄 */
    oled_dev.spi = NULL;
    oled_dev.regmap = NULL;

    return 0;
}

驱动卸载的时候内核自动释放regmap资源,但不会清空指针,需手动设置为NULL。

9.4.1.2. 编译设备树和驱动

此部分和SPI子系统实验完全一致不作过多说明。

编译得到设备树插件lubancat-spi-oled-overlay.dtb、驱动模块spi_oled_regmap.ko和应用程序spi_oled_app。

9.4.1.3. 模块接线说明

通过杜邦线连接SPI OLED模块和板卡,和SPI子系统实验接线完全一致,具体接线如下:

1
2
3
4
5
6
7
SPI OLED模块        板卡
   VCC   --------   3.3V
   GND   --------   GND
   CLK   --------   SPI_CLK
   MOSI  --------   SPI_MOSI
   CS    --------   SPI_CS
   D/C   --------   自己选定的DC脚

9.4.1.4. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

9.4.1.4.1. 实验操作

使用以下命令加载驱动和运行测试程序,加载驱动前需确保SPI OLED模块已经连接到板卡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#加载驱动
sudo insmod spi_oled_regmap.ko

#信息输出如下
[   80.894731] spi_oled driver init
[   80.895399] spi_oled driver probe
[   80.895571] major=236, minor=0
[   80.896537] SPI max_speed: 2000000Hz
[   80.896685] SPI mode: 0x00
[   80.896751] SPI chip_select = 0
[   80.896818] SPI bits_per_word = 8

信息打印和SPI子系统实验一样,SPI的最大速度为2MHz,工作模式为模式0,片选信号为CS0,每个数据宽度为8位。

使用以下命令运行应用程序进行显示:

1
2
3
4
5
6
7
8
#运行应用程序
sudo ./spi_oled_app /dev/spi_oled_regmap

#信息打印如下
[  159.619994] spi_oled device open
设备打开成功,开始显示...
hello world 显示!
hello world 显示!

运行应用程序后,OLED屏会出现以下现象,和SPI子系统实验一样:

  1. 应用程序启动后,OLED屏先清屏(全屏黑屏),持续1秒;

  2. 随后在屏幕中间位置显示“hello world”字符串;

  3. 字符串显示3秒后,再次清屏,循环重复上述过程。

显示的“hello world”字符串如下图:

../_images/subsystem_spi_subsystem_17.jpg

9.4.1.5. 实验注意事项

  1. 硬件连接正确性:OLED的SCLK、MOSI、CS0引脚需正确连接到开发板SPI对应的引脚,DC引脚需连接到与设备树配置一致的引脚;电源VCC须接3.3V,GND接GND,反接会烧毁OLED屏。

  2. 不能使用devm_regmap_init_spi:原因SSD1306的SPI通信协议无寄存器地址字段,仅需传输纯数据(命令/数据),而devm_regmap_init_spi的默认实现会遵循“1 字节地址 + 1 字节数据”的标准SPI寄存器操作规范,额外发送1字节地址字节会被SSD1306误解析为命令/数据,导致通信失败、屏幕无法初始化。

  3. Regmap配置:regmap_config中必须指定.val_bits=8,这是Regmap API的必填配置,SSD1306无实际寄存器地址,本实验使用devm_regmap_init自定义reg_write回调可以不填.reg_bits

  4. 额外注意:can_multi_write = true仅适用于数据批量传输,命令传输因需单独控制DC引脚,仍需循环调用单字节写入函数,不可批量发送。

9.4.2. I2C子系统Regmap API实验

本实验在SPI子系统Regmap API实验基础上进行修改,修改为I2C子系统 + Regmap API,通过Regmap写命令/写数据。

I2C接口OLED模块参考链接: 野火【OLED屏_I2C_0.96寸】模块

注解

补充说明:I2C接口的OLED和SPI接口的OLED的差异是,I2C无读写方向DC引脚,用reg地址区分命令/数据:0x00=命令,0x40=数据,设备I2C默认地址为0x3c,详细可参考模块配套资料。

本章的示例代码目录为: linux_driver/27_i2c_regmap_api

9.4.2.1. 设备树插件详解

本实验设备树插件(lubancat-i2c-oled-overlay.dts)用于配置I2C适配器和I2C OLED从设备,完整代码如下:

设备树插件(位于linux_driver/27_i2c_regmap_api/lubancat-i2c-oled-overlay.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>


/ {
    fragment@0 {
        target = <&i2c3>;

        __overlay__ {
            clock-frequency = <100000>;
            pinctrl-names = "default";
            pinctrl-0 = <&i2c3m1_xfer>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            i2c_oled@3c {
                compatible = "fire,i2c_oled";
                reg = <0x3c>;
                status = "okay";
            };
        };
    };
};

关键配置说明:

  • target = &i2c3:指定要修改的I2C适配器为I2C3,需与实际要使用的硬件接口一致;

  • clock-frequency = <100000>:配置I2C通信速率为100KHz,I2C OLED支持标准模式(100KHz)和快速模式(400KHz);

  • pinctrl-0 = <&i2c3m1_xfer>:此处使用的具体I2C引脚为i2c3m1;

  • i2c_oled@3c:从设备节点名称,@后的0x3c是I2C OLED的7位I2C地址,必须与传感器实际地址一致(默认0x3c);

  • compatible = “fire,i2c_oled”;:驱动匹配的核心标识,需与I2C驱动中of_match_table的属性完全一致,否则驱动无法匹配设备;

  • reg = <0x3c>:明确I2C OLED的I2C从设备地址,内核通过该属性识别I2C总线上的具体设备。

注意

以上设备树插件是以I2C3_M1为例,需根据板卡实际接口进行修改!

如果不清楚自己板卡有哪些可用的I2C接口,可在板卡执行以下命令确认:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#查看配置文件可用i2c插件
cat /boot/uEnv/uEnv.txt | grep i2c

#以LubanCat2-V2板卡为例,信息打印如下
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m1-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c5-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m0-rtc-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m1-rtc-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c5-m0-rtc-overlay.dtbo

从可用的设备树插件可以确认,LubanCat2-V2支持i2c3-m0、i2c3-m1、i2c5-m0,从内核源码中找到对应的设备树插件源码如下,以i2c3-m1为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#查看i2c3-m1设备树插件源码内容
cat kernel/arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-i2c3-m1-overlay.dts

#信息打印如下
/dts-v1/;
/plugin/;

/ {
        compatible = "rockchip,rk3568";

        fragment@0 {
                target = <&i2c3>;

                __overlay__ {
                        status = "okay";
                        pinctrl-names = "default";
                        pinctrl-0 = <&i2c3m1_xfer>;
                };
        };
};

可知,如果LubanCat2-V2使用i2c3-m1接口,lubancat-i2c-oled-overlay.dts配置的target和pinctrl-0就是 target = <&i2c3>;pinctrl-0 = <&i2c3m1_xfer>;

结合板卡配套的快速使用手册的40pin引脚对照图章节,可确认i2c3-m1接口对应的物理引脚,如下图:

../_images/subsystem_i2c_subsystem_1.jpg

9.4.2.2. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/27_i2c_regmap_api/i2c_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
* OLED 私有设备结构体,统一管理字符设备、I2C设备、设备号等资源
* I2C无读写方向引脚,用reg地址区分命令/数据:0x00=命令,0x40=数据
*/
struct oled_dev {
    dev_t devno;               /* 字符设备号 */
    struct cdev cdev;          /* 字符设备结构体 */
    struct class *class;       /* 设备类 */
    struct device *device;     /* 设备节点 */
    struct i2c_client *client; /* I2C客户端句柄 */
    struct regmap *regmap;     /* Regmap句柄 */
};

oled_dev结构体中用struct i2c_client *client替代SPI驱动的struct spi_device *spi,用于管理I2C设备资源,同时删除不需要的dc_gpio控制引脚。

驱动初始化

驱动初始化(位于linux_driver/27_i2c_regmap_api/i2c_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int oled_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    ...

    /* 创建设备节点 */
    oled_dev.device = device_create(oled_dev.class, &client->dev,
                                    oled_dev.devno, NULL, DEV_NAME);
    if (IS_ERR(oled_dev.device)) {
        printk(KERN_ERR "fail to create device\n");
        ret = PTR_ERR(oled_dev.device);
        goto destroy_class;
    }

    /* 标准I2C Regmap初始化 */
    oled_dev.regmap = devm_regmap_init_i2c(client, &oled_regmap_config);
    if (IS_ERR(oled_dev.regmap)) {
        ret = PTR_ERR(oled_dev.regmap);
        printk(KERN_ERR "regmap init failed\n");
        goto destroy_device;
    }

    /* 保存I2C客户端句柄到私有结构体 */
    oled_dev.client = client;

    /* 初始化OLED硬件 */
    oled_hw_init();

    return 0;

    ...
}
  • 第15行:Regmap初始化函数为devm_regmap_init_i2c,绑定I2C客户端和Regmap配置。I2C OLED设备本身支持寄存器地址区分命令/数据,Regmap默认写入逻辑与SSD1306的I2C通信协议匹配。

Regmap配置结构体

Regmap配置结构体(位于linux_driver/27_i2c_regmap_api/i2c_oled_regmap.c)
1
2
3
4
5
6
/* Regmap配置结构体 */
static const struct regmap_config oled_regmap_config = {
    .reg_bits = 8,              // 寄存器地址位数:8位
    .val_bits = 8,              // 寄存器数据位宽:8位
    .can_multi_write = true,    // 支持多字节批量写入
};
  • regmap_config:Regmap的核心配置,指定寄存器地址位数和寄存器数据位宽为8位,can_multi_write = true表示支持批量写入,提升效率。

OLED写命令和批量写命令函数

OLED写命令和批量写命令函数(位于linux_driver/27_i2c_regmap_api/i2c_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* 函数功能:OLED 写命令
* client:I2C设备句柄
* cmd:要写入的命令
* 返回值:0-成功,负数-失败
*/
static int oled_write_cmd(struct i2c_client *client, u8 cmd)
{
    int ret;

    /* SSD1306 I2C 写命令:reg=0x00 */
    ret = regmap_write(oled_dev.regmap, 0x00, cmd);
    if (ret) {
        printk(KERN_ERR "i2c_oled write cmd failed\n");
        return ret;
    }

    return 0;
}

/*
* 函数功能:OLED 批量写命令
* client:I2C设备句柄
* cmds:命令缓冲区
* len:命令长度
* 返回值:0-成功,负数-失败
*/
static int oled_write_cmds(struct i2c_client *client, const u8 *cmds, u16 len)
{
    int ret;

    /* SSD1306 I2C 批量写命令:reg=0x00 */
    ret = regmap_bulk_write(oled_dev.regmap, 0x00, cmds, len);
    if (ret) {
        printk(KERN_ERR "i2c_oled bulk write cmds failed\n");
        return ret;
    }

    return 0;
}
  • I2C OLED无需DC引脚,通过Regmap的寄存器地址(0x00=命令,0x40=数据)区分传输类型,写命令时reg=0x00。

  • 批量写命令时可直接使用regmap_bulk_write函数批量写入。

OLED写数据和批量写数据函数

OLED写数据和批量写数据函数(位于linux_driver/27_i2c_regmap_api/i2c_oled_regmap.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* 函数功能:OLED 写单字节数据
* client:I2C设备句柄
* data:要写入的数据
* 返回值:0-成功,负数-失败
*/
static int oled_write_data(struct i2c_client *client, u8 data)
{
    int ret;

    /* SSD1306 I2C 写数据:reg=0x40 */
    ret = regmap_write(oled_dev.regmap, 0x40, data);
    if (ret) {
        printk(KERN_ERR "i2c_oled write data failed\n");
        return ret;
    }
    return 0;
}

/*
* 函数功能:OLED 批量写数据
* client:I2C设备句柄
* buf:数据缓冲区
* len:数据长度
* 返回值:0-成功,负数-失败
*/
static int oled_write_datas(struct i2c_client *client, const u8 *buf, u16 len)
{
    int ret;

    /* SSD1306 I2C 批量写数据:reg=0x40 */
    ret = regmap_bulk_write(oled_dev.regmap, 0x40, buf, len);
    if (ret) {
        printk(KERN_ERR "i2c_oled write datas failed\n");
        return ret;
    }

    return 0;
}
  • 写数据和写命令的区别是写数据时reg=0x40。

注解

除了以上内容,和SPI子系统Regmap API实验其他差异部分仅是I2C子系统相关,不作详细说明,可直接对比俩实验驱动源码自行确认。

9.4.2.3. 应用代码详解

应用代码和SPI子系统Regmap API实验的应用代码完全一致,仅换了文件名字。

9.4.2.4. 编译设备树和驱动

9.4.2.4.1. 编译设备树

修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件,添加我们编辑好的设备树插件,并把设备树插件文件放在和Makefile文件同级目录下,以进行设备树插件的编译。

../_images/subsystem_regmap_api_0.jpg

然后在内核源码顶层目录执行以下命令编译设备树插件:

1
2
3
#这里以rk356x系列4.19.232内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

提示

其余系列板卡参考 使用内核的构建脚本编译设备树插件 章节进行编译。

9.4.2.4.2. 加载设备树

编译出来的设备树插件位于 内核源码/arch/arm64/boot/dts/rockchip/overlay/lubancat-i2c-oled-overlay.dtbo, 将设备树插件先传到板卡,再拷贝到板卡的 /boot/dtb/overlay/ 目录下。

1
2
3
4
#先传输到板卡

#再拷贝到板卡的/boot/dtb/overlay/目录下
sudo cp -f lubancat-i2c-oled-overlay.dtbo /boot/dtb/overlay/

然后在 /boot/uEnv/uEnv.txt 按照格式添加我们的设备树插件,需要在#overlay_start和#overlay_end之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。

../_images/subsystem_regmap_api_1.jpg

重启板卡可以在uboot启动信息中看到设备树插件加载。

../_images/subsystem_regmap_api_2.jpg
9.4.2.4.3. 编译驱动

在实验目录下输入 make 即可编译驱动和应用程序,编译得到内核模块i2c_oled_regmap.ko和应用程序i2c_oled_app。

9.4.2.5. 模块接线说明

通过杜邦线连接I2C OLED模块和板卡,接线如下:

1
2
3
4
5
I2C OLED模块        板卡
   VCC   --------   3.3V
   GND   --------   GND
   SCL   --------   SCL
   SDA   --------   SDA

9.4.2.6. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

9.4.2.6.1. 实验操作

使用以下命令加载驱动和运行测试程序,加载驱动前需确保I2C OLED模块已经连接到板卡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#先确认设备地址存在,使用i2cdetect命令,其中3表示是i2c3,需要根据实际使用的i2c修改
i2cdetect -a -y 3

#信息打印如下,可以看到68地址
    0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --


#加载驱动
sudo insmod i2c_oled_regmap.ko

#信息输出如下
[  455.798026] i2c_oled driver init
[  455.798353] i2c_oled driver probe
[  455.798455] major=236, minor=0

使用以下命令运行应用程序进行显示:

1
2
3
4
5
6
7
8
9
#运行测试程序
sudo ./i2c_oled_app /dev/i2c_oled_regmap

#信息打印如下
[  583.827056] i2c_oled device open
设备打开成功,开始显示...
hello world 显示!
hello world 显示!
hello world 显示!

运行应用程序后,OLED屏会出现以下现象,和SPI子系统Regmap API实验一样:

  • 应用程序启动后,OLED屏先清屏(全屏黑屏),持续1秒;

  • 随后在屏幕中间位置显示“hello world”字符串;

  • 字符串显示3秒后,再次清屏,循环重复上述过程。

显示的“hello world”字符串如下图:

../_images/subsystem_regmap_api_3.jpg

9.4.2.7. 实验注意事项

  • 硬件连接正确性:OLED的SDA和SCL引脚需正确连接到开发板SDA和SCL对应的引脚,电源VCC须接3.3V,GND接GND,反接会烧毁OLED屏。

  • I2C Regmap初始化:可正常使用devm_regmap_init_i2c函数,I2C OLED通过Regmap的reg参数(0x00=命令、0x40=数据)区分传输类型,无需额外GPIO控制,与SPI驱动的DC引脚控制逻辑不同。

  • Regmap配置要求:regmap_config结构体中,reg_bits和val_bits必须设为8位,否则Regmap初始化失败。