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的工作特性。
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可选取值如下:
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的所有状态信息(如缓存、锁、配置、底层通信接口等), 后续所有寄存器操作都需要通过该结构体指针完成。
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通过该结构体屏蔽不同总线的底层差异。
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.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. 驱动代码详解¶
核心数据结构定义
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引脚等所有资源,便于统一管理和释放。
驱动初始化
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配置结构体及写入回调函数
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写命令函数
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批量写命令函数
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写数据函数
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批量写数据函数
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函数
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子系统实验一样:
应用程序启动后,OLED屏先清屏(全屏黑屏),持续1秒;
随后在屏幕中间位置显示“hello world”字符串;
字符串显示3秒后,再次清屏,循环重复上述过程。
显示的“hello world”字符串如下图:
9.4.1.5. 实验注意事项¶
硬件连接正确性:OLED的SCLK、MOSI、CS0引脚需正确连接到开发板SPI对应的引脚,DC引脚需连接到与设备树配置一致的引脚;电源VCC须接3.3V,GND接GND,反接会烧毁OLED屏。
不能使用devm_regmap_init_spi:原因SSD1306的SPI通信协议无寄存器地址字段,仅需传输纯数据(命令/数据),而devm_regmap_init_spi的默认实现会遵循“1 字节地址 + 1 字节数据”的标准SPI寄存器操作规范,额外发送1字节地址字节会被SSD1306误解析为命令/数据,导致通信失败、屏幕无法初始化。
Regmap配置:regmap_config中必须指定.val_bits=8,这是Regmap API的必填配置,SSD1306无实际寄存器地址,本实验使用devm_regmap_init自定义reg_write回调可以不填.reg_bits
额外注意: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从设备,完整代码如下:
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接口对应的物理引脚,如下图:
9.4.2.2. 驱动代码详解¶
核心数据结构定义
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控制引脚。
驱动初始化
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配置结构体
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写命令和批量写命令函数
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写数据和批量写数据函数
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文件同级目录下,以进行设备树插件的编译。
然后在内核源码顶层目录执行以下命令编译设备树插件:
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之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。
重启板卡可以在uboot启动信息中看到设备树插件加载。
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”字符串如下图:
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初始化失败。