嵌入式驱动开发详解19(regmap驱动架构)

发布于:2024-12-22 ⋅ 阅读:(13) ⋅ 点赞:(0)

前言

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

RegMap 简介

  • regmap:regmap 是 Linux 内核为了减少慢速 I/O 在驱动上的冗余开销,提供了一种通用的接口来操 作硬件寄存器。
  • 问题:Linux 下使用 i2c_transfer 来读写 I2C 设备中的寄存器,SPI 接口的话使用 spi_write/spi_read 等。I2C/SPI 芯片又非常的多,因此 Linux 内核里面就会充斥了大量的 i2c_transfer 这类的冗余代码,再者,代码的复用性也会降低。
  • 解决办法:基于代码复用的原则,Linux 内核引入了 regmap 模型,regmap 将寄存器访问的共同逻辑抽象出来,驱动开发人员不需要再去纠结使用 SPI 或者 I2C 或其它接口的 API 函数,统一使用 regmap API 函数。这样的好处就是统一使用 regmap,降低了代码冗余,提高了驱动的可以移植性。
  • 特点
    1、硬件寄存器操作,比如选用通过 I2C/SPI 接口来读写设备的内部寄存器,或者需要读写 SOC 内部的硬件寄存器。
    2、提高代码复用性和驱动一致性,简化驱动开发过程。
    3、减少底层 I/O 操作次数,提高访问效率。
    4、缺点是实时性会降低。

Regmap 驱动框架

regmap 框架分为三层,如下图所示:
①、底层物理总线:regmap 就是对不同的物理总线进行封装,目前 regmap 支持的物理总 线有 i2c、i3c、spi、mmio、sccb、sdw、slimbus、irq、spmi 和 w1。
②、regmap 核心层,用于实现 regmap,我们不用关心具体实现。
③、regmap API 抽象层,regmap 向驱动编写人员提供的 API 接口,驱动编写人员使用这些 API 接口来操作具体的芯片设备,也是驱动编写人员重点要掌握的。
在这里插入图片描述
Linux 内 核 将 regmap 框架抽象为 regmap 结 构 体 , 这 个 结 构 体 定 义 在 文 件 drivers/base/regmap/internal.h 中,regmap 的初始化通过结构体 regmap_config 来 完成,这个结构体也定义在 include/linux/regmap.h 文件中。

struct regmap {
	union {
		struct mutex mutex;
		struct {
			spinlock_t spinlock;
			unsigned long spinlock_flags;
		};
	};
	regmap_lock lock;
	regmap_unlock unlock;
	void *lock_arg; /* This is passed to lock/unlock functions */

	struct device *dev; /* Device we do I/O on */
	……//此处省略一部分内容
};
struct regmap_config {
	const char *name;

	int reg_bits;
	int reg_stride;
	int pad_bits;
	int val_bits;

	bool (*writeable_reg)(struct device *dev, unsigned int reg);
	bool (*readable_reg)(struct device *dev, unsigned int reg);
	bool (*volatile_reg)(struct device *dev, unsigned int reg);
	bool (*precious_reg)(struct device *dev, unsigned int reg);
	regmap_lock lock;
	regmap_unlock unlock;
	void *lock_arg;

	int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
	int (*reg_write)(void *context, unsigned int reg, unsigned int val);

	bool fast_io;

	unsigned int max_register;
	const struct regmap_access_table *wr_table;
	const struct regmap_access_table *rd_table;
	const struct regmap_access_table *volatile_table;
	const struct regmap_access_table *precious_table;
	const struct reg_default *reg_defaults;
	unsigned int num_reg_defaults;
	enum regcache_type cache_type;
	const void *reg_defaults_raw;
	unsigned int num_reg_defaults_raw;

	u8 read_flag_mask;
	u8 write_flag_mask;

	bool use_single_rw;
	bool can_multi_write;

	enum regmap_endian reg_format_endian;
	enum regmap_endian val_format_endian;

	const struct regmap_range_cfg *ranges;
	unsigned int num_ranges;
};

以上是两个结构体内部的成员展示,当我们初始化的时候 regmap_init 函数会将regmap_config结构体成员中的值赋值到regmap结构体中。

RegMap 相关API函数

RegMap 初始化函数

regmap 支持多种物理总线,比如 I2C 和 SPI,我们需要根据所使用的接口来选 择合适的 regmap 初始化函数。Linux 内核提供了针对不同接口的 regmap 初始化函数,如下所示:

struct regmap *devm_regmap_init_i2c(struct i2c_client *i2c,
				    const struct regmap_config *config);
struct regmap *devm_regmap_init_spi(struct spi_device *dev,
				    const struct regmap_config *config);
struct regmap *devm_regmap_init_spmi_base(struct spmi_device *dev,
					  const struct regmap_config *config);
struct regmap *devm_regmap_init_spmi_ext(struct spmi_device *dev,
					 const struct regmap_config *config);
struct regmap *devm_regmap_init_mmio_clk(struct device *dev, const char *clk_id,
					 void __iomem *regs,
					 const struct regmap_config *config);
struct regmap *devm_regmap_init_ac97(struct snd_ac97 *ac97,
				     const struct regmap_config *config);

接下来以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。

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

void regmap_exit(struct regmap *map) 

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

RegMap 读写函数

int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);
int regmap_write_async(struct regmap *map, unsigned int reg, unsigned int val);
int regmap_raw_write(struct regmap *map, unsigned int reg,
		     const void *val, size_t val_len);
int regmap_bulk_write(struct regmap *map, unsigned int reg, const void *val,
			size_t val_count);
int regmap_multi_reg_write(struct regmap *map, const struct reg_default *regs,
			int num_regs);
int regmap_multi_reg_write_bypassed(struct regmap *map,
				    const struct reg_default *regs,
				    int num_regs);
int regmap_raw_write_async(struct regmap *map, unsigned int reg,
			   const void *val, size_t val_len);

如上所示,展示了RegMap提供的写函数,read函数也是这几个类型的,不管是什么接口的设备,读和写都是通过这一套函数完成。各个函数分别应对什么场景可自行分析。

RegMap 掩码设置

问题:当使用 spi 接口的时候,读取 icm20608 寄存器的 时候地址最高位必须置 1,写内部寄存器的是时候地址最高位要设置为 0。因此这里就涉及到对 寄存器地址最高位的操作。
解决方案:当我们使用 regmap 的时候就不需要手动将寄存器地址的 bit7 置 1,在初始化 regmap_config 的时候直接将 read_flag_mask 设置为 0X80 即可,这样通过 regmap 读取 SPI 内部寄存器的时候就会将寄存器地址与 read_flag_mask 进行或运算,结果就是将 bit7 置 1,但是整个过程不需要 我们来操作,全部由 regmap 框架来完成的。
注意事项:以SPI的初始化为例,如下所示,如果我们设置了regmap_config的read_flag_mask,系统会通过regmap_config 中的读写掩码来初始化 regmap_bus 中的掩码。如果没有的话,系统就会用 regmap_spi 默认将 read_flag_mask 设置为 0X80(这里可以查看bus对应的结构体),当你所使用的 SPI 设备不需要读掩码,在初始 化 regmap_config 的时候一定要将 read_flag_mask 设置为 0X00。

struct regmap *regmap_init(struct device *dev,
			   const struct regmap_bus *bus,
			   void *bus_context,
			   const struct regmap_config *config)
{
	……
	if (config->read_flag_mask || config->write_flag_mask) {
		map->read_flag_mask = config->read_flag_mask;
		map->write_flag_mask = config->write_flag_mask;
	} else if (bus) {
		map->read_flag_mask = bus->read_flag_mask;
	}
	……
}

RegMap 程序测试

regmap结构体定义

regmap 框架的核心就是 regmap 和 regmap_config 结构体,我们一般都是在自定义的设备结构体里面添加这两个类型的成员变量。

regmap初始化

一般在 probe 函数中初始化 regmap,以本专栏前面所讲解的I2C设备ap3216c和spi设备icm20608为例。
I2C设备ap3216c:

static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{   
	……
    ap3216cdev.regmap_config.reg_bits = 8;
    ap3216cdev.regmap_config.val_bits = 8;
    ap3216cdev.regmap = regmap_init_i2c(client,&ap3216cdev.regmap_config);
	……
}

spi设备icm20608:

static int icm20608_probe(struct spi_device *spi)
{
	……
    icm20608dev.regmap_config.reg_bits = 8;
    icm20608dev.regmap_config.val_bits = 8;
    icm20608dev.regmap_config.read_flag_mask = 0x80;
    icm20608dev.regmap = regmap_init_spi(spi,&icm20608dev.regmap_config);
	……
}

之所以ap3216c不用设置read_flag_mask,是因为该设备读写操作是通过i2c架构里的msg.flags标志位控制的,而icm20608是通过寄存器地址的最高位实现的。

不管是什么设备,最终卸载驱动函数里面都要释放regmap,通过regmap_exit(struct regmap *map) 实现。

regmap数据读写

此时就不需要自己封装函数了,只需要利用前面所提到的读写API函数即可实现数据的读取,这也就是regmap的意义所在。这里来做一下对比,我们之前实现传输一个I2C数据和一个SPI数据是怎么实现的,现在是怎么平替的:
之前的I2C数据读写如下:

static int ap3216c_read_regs(struct ap3216c_dev *dev, unsigned char reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;
    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].buf = ®
    msg[0].len = 1;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].buf = val;
    msg[1].len = len;

    ret = i2c_transfer(client->adapter,msg,2);

    if(ret == 2){
        return 0;
    }else{
        printk("i2c read error\r\n");
        return -EREMOTEIO;
    }
}

static int ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len) 
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *) dev->private_data;
    b[0] = reg; // 寄存器首地址 
    memcpy(&b[1],buf,len); // 将要写入的数据拷贝到数组 b 里面 
    msg.addr = client->addr; // ap3216c 地址 
    msg.flags = 0;
    msg.buf = b; // 要写入的数据缓冲区 
    msg.len = len + 1; // 要写入的数据长度 
    return i2c_transfer(client->adapter, &msg, 1);
}

static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{ 
    u8 data = 0;
    ap3216c_read_regs(dev, reg, &data, 1);
    return data;
}

static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{ 
    ap3216c_write_regs(dev, reg, &buf, 1);
}

之前的SPI数据读写如下:

static int icm20608_read_regs(struct icm20608_dev *dev, unsigned char reg, void *val, int len)
{
    int ret;
    unsigned char txdata[1];
    unsigned char *rxdata;
    struct spi_message m;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    if(!t){
        ret = -ENOMEM;
        goto error1;
    }
    rxdata = kzalloc(sizeof(char) * (len+1), GFP_KERNEL);
    if(!rxdata){
        ret = -ENOMEM;
        goto error2;
    }
    txdata[0] = reg | 0x80;
    t->tx_buf = txdata;
    t->rx_buf = rxdata;
    t->len = len + 1;

    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);
    if(ret ){
        goto error3;
    }
    memcpy(val,rxdata+1,len);
    return 0;

error3:
    kfree(rxdata);    
error2:
    kfree(t);
error1:
    return ret;
}

static int icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len) 
{
    int ret;
    unsigned char *txdata;
    struct spi_message m;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    if(!t){
        ret = -ENOMEM;
        goto error1;
    }
    txdata = kzalloc(sizeof(char) * (len+1), GFP_KERNEL);
    if(!txdata){
        ret = -ENOMEM;
        goto error2;
    }
    *txdata = reg & ~0x80;
    memcpy(txdata+1,buf,len);
    t->tx_buf = txdata;
    t->len = len + 1;
    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);
    if(ret ){
        goto error2;
    }
    return 0;
error2:
    kfree(txdata);
error1:
    kfree(t);
    return ret;
}

static unsigned char icm20608_read_reg(struct icm20608_dev *dev, u8 reg)
{
    u8 data = 0;
    icm20608_read_regs(dev,reg,&data,1);
    return data;
}

static void icm20608_write_reg(struct icm20608_dev *dev, u8 reg, u8 data)
{
    icm20608_write_regs(dev,reg,&data,1);
}

现在读写数据方式如下,不管是I2C还是SPI方式都是一样的:

static unsigned char icm20608_read_reg(struct icm20608_dev *dev, u8 reg)
{
    u8 ret;
    u8 data = 0;
    ret = regmap_read(dev->regmap,reg,&data);
    return data;
}

static void icm20608_write_reg(struct icm20608_dev *dev, u8 reg, u8 data)
{
    regmap_write(dev->regmap,reg,data);
}

后续

由此可见,regmap机制在Linux内核中提供了一种高效、统一的方法来访问和操作硬件寄存器,它通过抽象化硬件寄存器访问细节,简化了驱动程序的开发,并支持多种物理总线,使内核能够以标准化的方式与各种硬件设备进行交互,提高了代码的可维护性和系统的稳定性。

参考文献

  1. 个人专栏系列文章
  2. 正点原子嵌入式驱动开发指南
  3. 对代码有兴趣的同学可以查看链接https://github.com/NUAATRY/imx6ull_dev