STM32MP157驱动开发——Regmap API


0.前言

  在前面学习 I2C 和 SPI 驱动的时候,针对 I2C 和 SPI 设备寄存器的操作都是通过相关的 API 函数进行操作的,这样 Linux 内核中就会充斥着大量的重复、冗余代码。但本质上都是对外设寄存器的操作,所以为了方便内核开发人员统一访问 I2C/SPI 设备,引入了 Regmap 子系统。这节就学习一下如何使用。

一、Regmap API 简介

  Linux 下大部分设备的驱动开发都是操作其内部寄存器,比如 I2C/SPI 设备的本质都是一样,通过 I2C/SPI 接口读写芯片内部寄存器。主控芯片内部寄存器也是同样的道理,比如 STM32MP157 的 PWM、TIM 等外设初始化等。
  Linux 下使用 i2c_transfer 来读写 I2C 设备中的寄存器,SPI 接口的话使用 spi_write/spi_read 等。但由于 I2C/SPI 芯片有很多,这种操作会产生大量的冗余代码,除此之外,一些芯片既支持 I2C 接口,也支持 SPI 接口,比如 icm20608 芯片,如果在开发过程中,需要更换通信接口,那么就需要大幅修改相关驱动。
  基于代码复用的原则, Linux 内核引入了 regmap 模型,regmap 将寄存器访问的共同逻辑抽象出来,驱动开发人员不需要再去纠结使用 SPI 或者 I2C 接口 API 函数,统一使用 regmap API 函数。这样的好处就是统一使用 regmap,降低了代码冗余,提高了驱动的可以移植性。此外,通过 regmap 模型提供的统一接口函数来访问器件的寄存器,SOC 内部的寄存器也可以使用 regmap 接口函数来访问。
  regmap 在驱动和硬件之间添加了 cache,降低了低速 I/O 的操作次数,提高了访问效率,缺点是实时性会降低。一般在如下几种情况会使用 regmap:

① 硬件寄存器操作,比如选用通过 I2C/SPI 接口来读写设备的内部寄存器,或者需要读写 SOC 内部的硬件寄存器。
② 提高代码复用性和驱动一致性,简化驱动开发过程。
③ 减少底层 I/O 操作次数,提高访问效率

1.Regmap 驱动框架

在这里插入图片描述
regmap 框架分为三层:

① 底层物理总线:regmap 就是对不同的物理总线进行封装,目前 regmap 支持的物理总线有 i2c、i3c、spi、mmio、sccb、sdw、slimbus、irq、spmi 和 w1
② regmap 核心层,用于实现 regmap,此处不用关心具体实现
③ regmap API 抽象层,regmap 向驱动编写人员提供的 API 接口,驱动编写人员使用这些 API 接口来操作具体的芯片设备

2.regmap 结构体

Linux 内核将 regmap 框架抽象为 regmap 结构体,这个结构体定义在文件 include/linux/regmap.h 中,部分内容如下:

49 struct regmap {
    
    
50   union {
    
    
51 		struct mutex mutex;
52 		struct {
    
    
53 			spinlock_t spinlock;
54 			unsigned long spinlock_flags;
55   	};
56   };
57   regmap_lock lock;
58   regmap_unlock unlock;
59   void *lock_arg; /* This is passed to lock/unlock functions */
60   gfp_t alloc_flags;
......
89   unsigned int max_register;
90   bool (*writeable_reg)(struct device *dev, unsigned int reg);
91   bool (*readable_reg)(struct device *dev, unsigned int reg);
92   bool (*volatile_reg)(struct device *dev, unsigned int reg);
93   bool (*precious_reg)(struct device *dev, unsigned int reg);
94   bool (*writeable_noinc_reg)(struct device *dev, unsigned int reg);
95   bool (*readable_noinc_reg)(struct device *dev, unsigned int reg);
96   const struct regmap_access_table *wr_table;
97   const struct regmap_access_table *rd_table;
98   const struct regmap_access_table *volatile_table;
99   const struct regmap_access_table *precious_table;
100  const struct regmap_access_table *wr_noinc_table;
101  const struct regmap_access_table *rd_noinc_table;
102
103  int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
104  int (*reg_write)(void *context, unsigned int reg, unsigned int val);
105  int (*reg_update_bits)(void *context, unsigned int reg,
106  unsigned int mask, unsigned int val);
......
159
160  struct rb_root range_tree;
161  void *selector_work_buf; /* Scratch buffer used for selector */
162
163  struct hwspinlock *hwlock;
164 };

第 90~101 行有很多的函数以及 table,这些需要驱动编写人员根据实际情况选择性的初始化,regmap 的初始化通过结构体 regmap_config 来完成。

3.regmap_config 结构体

regmap_config 结构体就是用来初始化 regmap 的,这个结构体也定义在 include/linux/regmap.h 文件中:

352 struct regmap_config {
    
    
353   const char *name;
354
355   int reg_bits;
356   int reg_stride;
357   int pad_bits;
358   int val_bits;
359
360   bool (*writeable_reg)(struct device *dev, unsigned int reg);
361   bool (*readable_reg)(struct device *dev, unsigned int reg);
362   bool (*volatile_reg)(struct device *dev, unsigned int reg);
363   bool (*precious_reg)(struct device *dev, unsigned int reg);
364   bool (*writeable_noinc_reg)(struct device *dev, unsigned int reg);
365   bool (*readable_noinc_reg)(struct device *dev, unsigned int reg);
366
367   bool disable_locking;
368   regmap_lock lock;
369   regmap_unlock unlock;
370   void *lock_arg;
371
372   int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
373   int (*reg_write)(void *context, unsigned int reg, unsigned int val);
374
375   bool fast_io;
376
377   unsigned int max_register;
378   const struct regmap_access_table *wr_table;
379   const struct regmap_access_table *rd_table;
380   const struct regmap_access_table *volatile_table;
381   const struct regmap_access_table *precious_table;
382   const struct regmap_access_table *wr_noinc_table;
383   const struct regmap_access_table *rd_noinc_table;
384   const struct reg_default *reg_defaults;
385   unsigned int num_reg_defaults;
386   enum regcache_type cache_type;
387   const void *reg_defaults_raw;
388   unsigned int num_reg_defaults_raw;
389
390   unsigned long read_flag_mask;
391   unsigned long write_flag_mask;
392   bool zero_flag_mask;
393
394   bool use_single_read;
395   bool use_single_write;
396   bool can_multi_write;
397
398   enum regmap_endian reg_format_endian;
399   enum regmap_endian val_format_endian;
400
401   const struct regmap_range_cfg *ranges;
402   unsigned int num_ranges;
403
404   bool use_hwlock;
405   unsigned int hwlock_id;
406   unsigned int hwlock_mode;
407 };

在内核源码中,对这些变量都进行了详细解释,这里主要看几个重要属性。
name:名字
reg_bits:寄存器地址位数,必填字段
reg_stride:寄存器地址步长
pad_bits:寄存器和值之间的填充位数
val_bits:寄存器值位数,必填字段
writeable_reg:可选的可写回调函数,寄存器可写的话此回调函数就会被调用,并返回 true
readable_reg:可选的可读回调函数,寄存器可读的话此回调函数就会被调用,并返回 true
volatile_reg:可选的回调函数,当寄存器值不能缓存的时候此回调函数就会被调用,并返回 true
precious_reg:当寄存器值不能被读出来的时候此回调函数会被调用,比如很多中断状态寄存器读清零,读这些寄存器就可以清除中断标志位,但是并没有读出这些寄存器内部的值
reg_read:可选的读操作回调函数,所有读寄存器的操作此回调函数就会执行
reg_write:可选的写操作回调函数,所有写寄存器的操作此回调函数就会执行
fast_io:快速 I/O,使用 spinlock 替代 mutex 来提升锁性能
max_register:有效的最大寄存器地址,可选
wr_table:可写的地址范围,为 regmap_access_table 结构体类型。后面的 rd_table、volatile_table、precious_table、wr_noinc_table 和 rd_noinc_table 同理
reg_defaults:寄存器模式值,为 reg_default 结构体类型,此结构体有两个成员变量:reg 和 def,reg 是寄存器地址,def 是默认值
num_reg_defaults:默认寄存器表中的元素个数
read_flag_mask:读标志掩码
write_flag_mask:写标志掩码

4.Regmap 操作函数

①Regmap 申请与初始化

regmap 支持多种物理总线,比如 I2C 和 SPI,需要根据所使用的接口来选择合适的 regmap 初始化函数。一般会在 probe 函数中初始化 regmap_config,然后申请并初始化 regmap。Linux 内核提供了针对不同接口的 regmap 初始化函数,SPI 接口初始化函数为 regmap_init_spi:
原型

struct regmap * regmap_init_spi(struct spi_device *spi,
								const struct regmap_config *config)

参数
spi:需要使用 regmap 的 spi_device
config:regmap_config 结构体,由开发人员初始化一个 regmap_config 实例,然后将其地址赋值给此参数
返回值:申请到的并进过初始化的 regmap

I2C 接口的 regmap 初始化函数为 regmap_init_i2c:
原型

struct regmap * regmap_init_i2c(struct i2c_client *i2c,
								const struct regmap_config *config)

参数
spi:需要使用 regmap 的 i2c_client
config:regmap_config 结构体,由开发人员初始化一个 regmap_config 实例,然后将其地址赋值给此参数
返回值:申请到的并进过初始化的 regmap

还有很多其他物理接口对应的 regmap 初始化函数,这里就不介绍了,直接查阅 Linux内核即可,基本和 SPI/I2C 的初始化函数相同。

②Regmap释放

在退出驱动的时候需要释放掉申请到的 regmap,不管是什么接口,全部使用 regmap_exit 函数:
原型

void regmap_exit(struct regmap *map)

参数
map:需要释放的 regmap
返回值:无

③regmap 设备访问 API 函数

不管是 I2C 还是 SPI 等接口,还是 SOC 内部的寄存器,对于寄存器的操作就两种:读和写。regmap 提供了最核心的两个读写操作:regmap_read 和 regmap_write。
原型

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

参数
map:要操作的 regmap。
reg:要读/写的寄存器。
val:读/写的寄存器值。
返回值
0:读取/写入成功
其他:读取/写入失败

在 regmap_read 和 regmap_write 的基础上还衍生出了其他一些 regmap 的 API 函数:

regmap_update_bits 函数
原型

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

功能:用来修改寄存器指定的 bit 位的值。
参数
map:要操作的 regmap。
reg:要操作的寄存器。
mask:掩码,需要更新的位必须在掩码中设置为 1。
val:需要更新的位值。
返回值
0:写入成功
其他:写入失败

regmap_bulk_read函数 和 regmap_bulk_write函数
原型

int regmap_bulk_read(struct regmap *map,
					 unsigned int reg,
					 void *val,
					 size_t val_count)
int regmap_bulk_write(struct regmap *map,
					  unsigned int reg,
					  const void *val,
					  size_t val_count)

功能:读取/写入多个寄存器的值。
参数
map:要操作的 regmap。
reg:要读取/写入的第一个寄存器。
val:要读取/写入的数据缓冲区。
val_count:要读取/写入的寄存器数量。
返回值
0:写入成功
其他:写入失败

5. regmap_config 掩码设置

  结构体 regmap_config 中有三个关于掩码的成员变量:read_flag_mask、write_flag_mask 和 zero_flag_mask。
  以之前学习的 icm20608 为例,该芯片支持 i2c 和 spi 接口,但是当使用 spi 接口时,如果需要读取 icm20608 寄存器数据,地址最高位必须置 1,写内部寄存器的时,地址最高位要设置为 0,这里就涉及到对寄存器地址最高位的操作。在之前编写的驱动代码中,使用手动修改该位置数据的方式:

1 static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
2 {
    
    
3
......
21 		txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址 bit7 要置 1 */
22 		t->tx_buf = txdata; /* 要发送的数据 */
23 		t->rx_buf = rxdata; /* 要读取的数据 */
24 		t->len = len+1; /* t->len=发送的长度+读取的长度 */
25 		spi_message_init(&m); /* 初始化 spi_message */
26 		spi_message_add_tail(t, &m);
27 		ret = spi_sync(spi, &m); /* 同步发送 */
......
39 		return ret;
40 }

其中第 21 行,将寄存器的地址 bit7 置 1,表示这是一个读操作。
如果使用 regmap,在初始化 regmap_config 的时候直接将 read_flag_mask 设置为 0X80 即可,这样通过 regmap 读取 SPI 内部寄存器时就会将寄存器地址与 read_flag_mask 进行或运算,结果就是将 bit7 置 1。
同理 write_flag_mask 用法也是一样,只是 write_flag_mask 用于写寄存器操作。

打开 regmap-spi.c 文件,这个文件就是 regmap 的 spi 总线文件,其中有以下内容:

101 static const struct regmap_bus regmap_spi = {
    
    
102 		.write = regmap_spi_write,
103 		.gather_write = regmap_spi_gather_write,
104 		.async_write = regmap_spi_async_write,
105 		.async_alloc = regmap_spi_async_alloc,
106 		.read = regmap_spi_read,
107 		.read_flag_mask = 0x80,
108 		.reg_format_endian_default = REGMAP_ENDIAN_BIG,
109 		.val_format_endian_default = REGMAP_ENDIAN_BIG,
110 };
111
112 struct regmap *__regmap_init_spi(struct spi_device *spi,
113 								 const struct regmap_config *config,
114 								 struct lock_class_key *lock_key,
115 								 const char *lock_name)
116 {
    
    
117 		return __regmap_init(&spi->dev, &regmap_spi, &spi->dev, config, lock_key, lock_name);
118 }

第 101~110 行初始化了一个 regmap_bus 实例:regmap_spi,第 107 行中 read_flag_mask 默认为 0X80。注:这里是将 regmap_bus 的 read_flag_mask 成员变量设置为 0X80
第 112~119 行__regmap_init_spi 函数,被 regmap_init_spi 函数调用,用于并申请一个 SPI 总线的 regmap,该函数也只是__regmap_init 函数的简单封装。在__regmap_init 函数中有以下内容:

812 if (config->read_flag_mask ||
813     config->write_flag_mask ||
814     config->zero_flag_mask) 
815 {
    
    
816   	map->read_flag_mask = config->read_flag_mask;
817   	map->write_flag_mask = config->write_flag_mask;
818 } else if (bus) {
    
    
819 	map->read_flag_mask = bus->read_flag_mask;
820 }

第 812~817 行就是用 regmap_config 中的读写掩码来初始化 regmap_bus 中的掩码。注意:如果所使用的 SPI 设备不需要读掩码,在初始化 regmap_config 的时候一定要将 read_flag_mask 设置为 0X00,并且也要将 zero_flag_mask 设置为 true。

二、驱动开发

本节就对之前开发的 icm20608 驱动进行改写,所以不需要修改有关的设备树。

1.修改设备结构体,添加 regmap 和 regmap_config

修改完后的 icm20608_dev 结构体如下:

#include <linux/regmap.h>

struct icm20608_dev {
    
    
	struct spi_device *spi;		/* spi设备 */
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	struct device_node	*nd; 	/* 设备节点 */
	signed int gyro_x_adc;		/* 陀螺仪X轴原始值 	 */
	signed int gyro_y_adc;		/* 陀螺仪Y轴原始值		*/
	signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 		*/
	signed int accel_x_adc;		/* 加速度计X轴原始值 	*/
	signed int accel_y_adc;		/* 加速度计Y轴原始值	*/
	signed int accel_z_adc;		/* 加速度计Z轴原始值 	*/
	signed int temp_adc;		/* 温度原始值 			*/

	struct regmap *regmap;
	struct regmap_config regmap_config;
};

添加了regmap 指针变量,regmap 需要使用 regmap_init_spi 函数来申请和初始化,所以这里是指针类型。
regmap_config 结构体成员变量用来配置 regmap。

2.初始化 regmap

一般在 probe 函数中初始化 regmap,修改后内容如下:

/*probe函数*/
static int icm20608_probe(struct spi_device *spi)
{
    
    
	int ret;
	struct icm20608_dev *icm20608dev;
	
	/* 分配icm20608dev对象的空间 */
	icm20608dev = devm_kzalloc(&spi->dev, sizeof(*icm20608dev), GFP_KERNEL);
	if(!icm20608dev)
		return -ENOMEM;

	/* 初始化 regmap_config 设置 */
	icm20608dev->regmap_config.reg_bits = 8; /* 寄存器长度 8bit */
	icm20608dev->regmap_config.val_bits = 8; /* 值长度 8bit */
	icm20608dev->regmap_config.read_flag_mask = 0x80; /* 读掩码 */
	/* 初始化 IIC 接口的 regmap */
	icm20608dev->regmap = regmap_init_spi(spi, &icm20608dev->regmap_config);
	if (IS_ERR(icm20608dev->regmap)) {
    
    
		return PTR_ERR(icm20608dev->regmap);
	}

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	ret = alloc_chrdev_region(&icm20608dev->devid, 0, ICM20608_CNT, ICM20608_NAME);
	if(ret < 0) {
    
    
		pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", ICM20608_NAME, ret);
        goto del_regmap;
	}
......
	return 0;
	
destroy_class:
	device_destroy(icm20608dev->class, icm20608dev->devid);
del_cdev:
	cdev_del(&icm20608dev->cdev);
del_unregister:
	unregister_chrdev_region(icm20608dev->devid, ICM20608_CNT);
	return -EIO;
del_regmap:
	regmap_exit(icm20608dev->regmap);
	return -EIO;
}

①regmap_config 的初始化,icm20608 的寄存器地址地址长度为 8bit,寄存器值也是 8bit,因此 reg_bits 和 val_bits 都设置为 8。由于 icm20608 通过 SPI 接口读取的时候地址寄存器最高位要设置为 1,因此 read_flag_mask 设置为 0X80。
②通过 regmap_init_spi 函数来申请并初始化 SPI 总线的 regmap。
③如果要删除 regmap 就使用 regmap_exit 函数。

3.卸载驱动时移除regmap

在 remove 函数中删除 probe 里申请的 regmap,修改后内容如下:

static int icm20608_remove(struct spi_device *spi)
{
    
    
	struct icm20608_dev *icm20608dev = spi_get_drvdata(spi);
	/* 注销字符设备驱动 */
	/* 1、删除cdev */
	cdev_del(&icm20608dev->cdev);
	/* 2、注销设备号 */
	unregister_chrdev_region(icm20608dev->devid, ICM20608_CNT); 
	/* 3、注销设备 */
	device_destroy(icm20608dev->class, icm20608dev->devid);
	/* 4、注销类 */
	class_destroy(icm20608dev->class); 
	/* 5、删除 regmap */
	regmap_exit(icm20608dev->regmap);
	
	return 0;
}

使用 regmap_exit 删除掉 probe 函数中申请的 regmap。

4.读写设备内部寄存器

以前使用 spi 驱动框架编写读写函数,现在直接使用 regmap_read、regmap_write 的函数即可:

/*读取icm20608指定寄存器值,读取一个寄存器*/
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
    
    
	u8 ret;
	unsigned int data;

	ret = regmap_read(dev->regmap, reg, &data);
	return (u8)data;
}
/*写入icm20608指定寄存器值,读取一个寄存器*/
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
    
    
    regmap_write(dev->regmap, reg, &value);
}

/*读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、三轴加速度计和内部温度*/
void icm20608_readdata(struct icm20608_dev *dev)
{
    
    
	u8 ret;
	unsigned char data[14];

	ret = regmap_bulk_read(dev->regmap, ICM20_ACCEL_XOUT_H, data, 14);

	dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
}

①icm20608_read_onereg 函数用于读取 icm20608 内部单个寄存器,这里使用 regmap_read 函数来完成寄存器读取操作
②icm20608_write_onereg 函数用于向 icm20608 指定寄存器写入数据,使用regmap_write 函数来完成
③icm20608_readdata 函数用于读取 icm20608 内部陀螺仪、加速度计和温度计的数据,从 ICM20_ACCEL_XOUT_H 寄存器开始,连续读取 14 个寄存器。这里直接使用 regmap_bulk_read 函数来读取多个寄存器。

三、运行测试

与之前的 SPI 总线测试方式基本相同,编译出.ko模块文件,放入开发板的相应目录,然后使用之前的测试程序进行测试即可。
在这里插入图片描述

四、I2CRegmap API开发

IIC 总线的 regmap 框架基本和 SPI 一样,只是需要使用 regmap_init_i2c 来申请并初始化对应的 regmap,同样都是使用 regmap_read 和 regmap_write 来读写 I2C 设备内部寄存器。
将 SPI 相关的 API 更换成 IIC 的 API 即可,这里就不再赘述。

猜你喜欢

转载自blog.csdn.net/weixin_45682654/article/details/128556146