Regmap API与IIO框架:设备驱动开发的高效工具

1. Regmap API概述

Regmap API是一种寄存器映射抽象,支持SPI和I2C协议,对于编写通用驱动程序而言是绝佳选择。其具有通用性和同质性,不同总线类型仅初始化方式有所不同,其他功能函数相同。

1.1 regmap初始化

在驱动的探测函数中,需根据支持的协议调用 regmap_init_i2c() regmap_init_spi() 来初始化regmap。在初始化之前,必须填充 regmap_config 元素。使用完regmap后,可调用 regmap_exit() 函数释放之前分配的寄存器映射。

void regmap_exit(struct regmap *map)
1.1.1 SPI初始化

SPI初始化通过 regmap_init_spi() 函数将设备访问转换为SPI命令。

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

以下是一个完整示例:

static int foo_spi_probe(struct spi_device *client)
{
    int err;
    struct regmap *my_regmap;
    struct regmap_config bmp085_regmap_config;

    /* fill bmp085_regmap_config somewhere */
    [...]
    client->bits_per_word = 8;
    my_regmap = regmap_init_spi(client, &bmp085_regmap_config);
    if (IS_ERR(my_regmap)) {
        err = PTR_ERR(my_regmap);
        dev_err(&client->dev, "Failed to init regmap: %d\n", err);
        return err;
    }
    [...]
}
1.1.2 I2C初始化

I2C初始化使用 regmap_init_i2c() 函数将设备访问转换为I2C命令。

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

以下是一个完整示例:

static int bar_i2c_probe(struct i2c_client *i2c, const struct i2c_device_id *id)
{
    struct my_struct * bar_struct;
    struct regmap_config regmap_cfg;
    /* fill regmap_cfgsome  where */
    [...]
    bar_struct = kzalloc(&i2c->dev, sizeof(*my_struct), GFP_KERNEL);
    if (!bar_struct)
        return -ENOMEM;
    i2c_set_clientdata(i2c, bar_struct);

    bar_struct->regmap = regmap_init_i2c(i2c, &regmap_config);
    if (IS_ERR(bar_struct->regmap))
        return PTR_ERR(bar_struct->regmap);
    bar_struct->dev = &i2c->dev;
    bar_struct->irq = i2c->irq;
    [...]
}

1.2 设备访问函数

Regmap API处理数据解析、格式化和传输,常用的设备访问函数有 regmap_read regmap_write regmap_update_bits

int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);
int regmap_update_bits(struct regmap *map, unsigned int reg, unsigned int mask, unsigned int val);
1.2.1 regmap_write

该函数用于向设备写入数据。若 regmap_config 中设置了 max_register ,会检查寄存器地址是否有效。若地址有效,还会调用 writeable_reg 回调函数,若回调返回 true 则继续执行写入操作,否则返回 -EIO 错误。若设置了 wr_table ,则根据寄存器地址所在范围进行相应处理。若启用了缓存( cache_type != REGCACHE_NONE ),会先更新缓存再写入硬件;若提供了 reg_write 回调函数,则使用该函数执行写入操作,否则执行通用的regmap写入函数。

1.2.2 regmap_read

该函数用于从设备读取数据,工作方式与 regmap_write 类似,使用 readable_reg rd_table 进行相应检查。若提供了 reg_read 回调函数,则使用该函数执行读取操作,否则执行通用的regmap读取函数。

1.2.3 regmap_update_bits

该函数是一个三合一函数,执行寄存器映射的读/修改/写循环。

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

其底层调用 _regmap_update_bits 函数:

static int _regmap_update_bits(struct regmap *map, unsigned int reg, unsigned int mask, unsigned int val, bool *change)
{
    int ret;
    unsigned int tmp, orig;
    ret = _regmap_read(map, reg, &orig);
    if (ret != 0)
        return ret;
    tmp = orig & ~mask;
    tmp |= val & mask;
    if (tmp != orig) {
        ret = _regmap_write(map, reg, tmp);
        *change = true;
    } else {
        *change = false;
    }
    return ret;
}

使用时,需将需要更新的位在 mask 中设置为1,在 val 中设置相应的值。

1.3 特殊的regmap_multi_reg_write函数

该函数用于向设备写入多个寄存器。

int regmap_multi_reg_write(struct regmap *map, const struct reg_sequence *regs, int num_regs)

struct reg_sequence 定义如下:

struct reg_sequence {
    unsigned int reg;
    unsigned int def;
    unsigned int delay_us;
};

以下是使用示例:

static const struct reg_sequence foo_default_regs[] = {
    { FOO_REG1,          0xB8 },
    { BAR_REG1,          0x00 },
    { FOO_BAR_REG1,      0x10 },
    { REG_INIT,          0x00 },
    { REG_POWER,         0x00 },
    { REG_BLABLA,        0x00 },
};
static int probe ( ...)
{
    [...]
    ret = regmap_multi_reg_write(my_regmap, foo_default_regs, ARRAY_SIZE(foo_default_regs));
    [...]
}

1.4 其他设备访问函数

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);

1.5 regmap与缓存

Regmap支持缓存,是否使用缓存系统取决于 regmap_config cache_type 字段的值。支持的缓存类型有:

enum regcache_type {
    REGCACHE_NONE,
    REGCACHE_RBTREE,
    REGCACHE_COMPRESSED,
    REGCACHE_FLAT,
};

默认值为 REGCACHE_NONE ,表示禁用缓存。其他值定义了缓存的存储方式。设备某些寄存器可能有预定义的上电复位值,可将这些值存储在 reg_defaults 数组中,以加快设备访问速度。

struct reg_default {
    unsigned int reg;
    unsigned int def;
};

cache_type 设置为 REGCACHE_NONE ,则 reg_defaults 将被忽略。若未设置 default_reg 但启用了缓存,会自动创建相应的缓存结构。以下是 LTC3589 调节器驱动的示例:

static const struct reg_default ltc3589_reg_defaults[] = {
    { LTC3589_SCR1,   0x00 },
    { LTC3589_OVEN,   0x00 },
    { LTC3589_SCR2,   0x00 },
    { LTC3589_VCCR,   0x00 },
    { LTC3589_B1DTV1, 0x19 },
    { LTC3589_B1DTV2, 0x19 },
    { LTC3589_VRRCR,  0xff },
    { LTC3589_B2DTV1, 0x19 },
    { LTC3589_B2DTV2, 0x19 },
    { LTC3589_B3DTV1, 0x19 },
    { LTC3589_B3DTV2, 0x19 },
    { LTC3589_L2DTV1, 0x19 },
    { LTC3589_L2DTV2, 0x19 },
};
static const struct regmap_config ltc3589_regmap_config = {
    .reg_bits = 8,
    .val_bits = 8,
    .writeable_reg = ltc3589_writeable_reg,
    .readable_reg = ltc3589_readable_reg,
    .volatile_reg = ltc3589_volatile_reg,
    .max_register = LTC3589_L2DTV2,
    .reg_defaults = ltc3589_reg_defaults,
    .num_reg_defaults = ARRAY_SIZE(ltc3589_reg_defaults),
    .use_single_rw = true,
    .cache_type = REGCACHE_RBTREE,
};

1.6 搭建regmap子系统的步骤

  1. 根据设备特性设置 struct regmap_config ,包括寄存器范围、默认值、缓存类型等。若需要自定义读写函数,可将其传递给 reg_read/reg_write 字段。
  2. 在探测函数中,根据总线类型(I2C或SPI)使用 regmap_init_i2c regmap_init_spi 分配regmap。
  3. 需要读写寄存器时,调用 regmap_[read|write] 函数。
  4. 使用完regmap后,调用 regmap_exit 释放探测函数中分配的寄存器映射。

1.7 regmap示例

以下是一个模拟SPI设备驱动的示例:

/* mandatory for regmap */
#include <linux/regmap.h>
/* Depending on your need you should include other files */
static struct private_struct
{
    /* Feel free to add whatever you want here */
    struct regmap *map;
    int foo;
};
static const struct regmap_range wr_rd_range[] =
{
    {
        .range_min = 0x20,
        .range_max = 0x4F,
    },
    {
        .range_min = 0x60,
        .range_max = 0x7F
    },
};
struct regmap_access_table drv_wr_table =
{
    .yes_ranges =   wr_rd_range,
    .n_yes_ranges = ARRAY_SIZE(wr_rd_range),
};
struct regmap_access_table drv_rd_table =
{
    .yes_ranges =   wr_rd_range,
    .n_yes_ranges = ARRAY_SIZE(wr_rd_range),
};
static bool writeable_reg(struct device *dev, unsigned int reg)
{
    if (reg >= 0x20 && reg <= 0x4F)
        return true;
    if (reg >= 0x60 && reg <= 0x7F)
        return true;
    return false;
}
static bool readable_reg(struct device *dev, unsigned int reg)
{
    if (reg >= 0x20 && reg <= 0x4F)
        return true;
    if (reg >= 0x60 && reg <= 0x7F)
        return true;
    return false;
}
static int my_spi_drv_probe(struct spi_device *dev)
{
    struct regmap_config config;
    struct custom_drv_private_struct *priv;
    unsigned char data;
    /* setup the regmap configuration */
    memset(&config, 0, sizeof(config));
    config.reg_bits = 8;
    config.val_bits = 8;
    config.write_flag_mask = 0x80;
    config.max_register = 0x80;
    config.fast_io = true;
    config.writeable_reg = drv_writeable_reg;
    config.readable_reg = drv_readable_reg;
    /*
     * If writeable_reg and readable_reg are set,
     * there is no need to provide wr_table nor rd_table.
     * Uncomment below code only if you do not want to use
     * writeable_reg nor readable_reg.
     */
    //config.wr_table = drv_wr_table;
    //config.rd_table = drv_rd_table;
    /* allocate the private data structures */
    /* priv = kzalloc */
    /* Init the regmap spi configuration */
    priv->map = regmap_init_spi(dev, &config);
    /* Use regmap_init_i2c in case of i2c bus */
    /*
     * Let us write into some register
     * Keep in mind that, below operation will remain same
     * whether you use SPI or I2C. It is and advantage when
     * you use regmap.
     */
    regmap_read(priv->map, 0x30, &data);
    [...] /* Process data */
    data = 0x24;
    regmap_write(priv->map, 0x23, data); /* write new value */
    /* set bit 2 (starting from 0) and 6 of register 0x44 */
    regmap_update_bits(priv->map, 0x44, 0b00100010, 0xFF);
    [...] /* Lot of stuff */
    return 0;
}

2. IIO框架概述

Industrial I/O (IIO) 是一个内核子系统,专门用于模拟 - 数字转换器 (ADC) 和数字 - 模拟转换器 (DAC)。随着具有模拟 - 数字或数字 - 模拟转换能力的传感器数量不断增加,且代码实现分散在不同的内核源文件中,IIO框架以通用和同质的方式将这些设备整合在一起。自2009年以来,Jonathan Cameron和Linux IIO社区一直在开发该框架。加速度计、陀螺仪、电流/电压测量芯片、光传感器、压力传感器等都属于IIO设备家族。

2.1 IIO模型架构

IIO模型基于设备和通道架构:
- 设备 :代表芯片本身,是层次结构的顶层。
- 通道 :代表设备的单个采集线路,一个设备可能有一个或多个通道。例如,加速度计是一个具有三个通道的设备,分别对应X、Y和Z轴。

IIO芯片作为物理和硬件传感器/转换器,在用户空间以字符设备(支持触发缓冲时)和 sysfs 目录条目的形式暴露,其中一些文件代表通道,单个通道由单个 sysfs 文件条目表示。

2.2 用户空间与IIO驱动的交互方式

  • /sys/bus/iio/iio:deviceX/ :代表传感器及其通道。
  • /dev/iio:deviceX :是一个字符设备,用于导出设备的事件和数据缓冲区。

2.3 IIO框架架构和布局

IIO框架在内核和用户空间之间的组织方式如下:驱动程序管理硬件并将处理结果报告给IIO核心,使用IIO核心提供的一组工具和API。IIO子系统通过 sysfs 接口和字符设备将底层机制抽象给用户空间,用户可以在其上执行系统调用。

2.4 IIO API头文件

IIO API分布在多个头文件中:

#include <linux/iio/iio.h>    /* mandatory */
#include <linux/iio/sysfs.h>  /* mandatory since sysfs is used */
#include <linux/iio/events.h> /* For advanced users, to manage iio events */
#include <linux/iio/buffer.h> /* mandatory to use triggered buffers */
#include <linux/iio/trigger.h>/* Only if you implement trigger in your driver (rarely used)*/

2.5 IIO数据结构

IIO设备在内核中由 struct iio_dev 实例表示,并由 struct iio_info 结构描述。所有重要的IIO结构都在 include/linux/iio/iio.h 中定义。

2.5.1 iio_dev结构

该结构描述了IIO设备和驱动程序,包含以下重要信息:

struct iio_dev {
    [...]
    int modes;
    int currentmode;
    struct device dev;
    struct iio_buffer *buffer;
    int scan_bytes;
    const unsigned long *available_scan_masks;
    const unsigned long *active_scan_mask;
    bool scan_timestamp;
    struct iio_trigger *trig;
    struct iio_poll_func *pollfunc;
    struct iio_chan_spec const *channels;
    int num_channels;
    const char *name;
    const struct iio_info *info;
    const struct iio_buffer_setup_ops *setup_ops;
    struct cdev chrdev;
};
  • modes :表示设备支持的不同模式,包括 INDIO_DIRECT_MODE (设备提供 sysfs 类型接口)、 INDIO_BUFFER_TRIGGERED (设备支持硬件触发)、 INDIO_BUFFER_HARDWARE (设备有硬件缓冲区)和 INDIO_ALL_BUFFER_MODES (前两者的并集)。
  • currentmode :表示设备实际使用的模式。
  • dev :表示IIO设备关联的 struct device
  • buffer :数据缓冲区,在启用触发缓冲模式时推送到用户空间。
  • scan_bytes :捕获并馈送到缓冲区的字节数。
  • available_scan_masks :可选的允许位掩码数组,用于限制触发缓冲时可启用的通道。
  • active_scan_mask :启用通道的位掩码,仅将这些通道的数据推送到缓冲区。
  • scan_timestamp :指示是否将捕获时间戳推送到缓冲区。
  • trig :当前设备的触发器(支持缓冲模式时)。
  • pollfunc :触发器收到时运行的函数。
  • channels :通道规范结构表,描述设备的每个通道。
  • num_channels :通道数量。
  • name :设备名称。
  • info :驱动程序的回调和常量信息。
  • setup_ops :在缓冲区启用/禁用前后调用的回调函数集。
  • chrdev :IIO核心创建的关联字符设备。
2.5.2 内存分配、注册和注销

使用 iio_device_alloc() 函数为IIO设备分配内存:

struct iio_dev *devm_iio_device_alloc(struct device *dev, int sizeof_priv)

分配内存后,填充不同字段,然后使用 iio_device_register() 函数将设备注册到IIO子系统:

int iio_device_register(struct iio_dev *indio_dev)

使用完设备后,使用 iio_device_unregister() 函数注销设备:

void iio_device_unregister(struct iio_dev *indio_dev)

2.6 IIO框架操作流程

以下是使用IIO框架的主要操作流程:

graph TD;
    A[分配IIO设备内存] --> B[填充iio_dev字段];
    B --> C[注册IIO设备];
    C --> D[处理设备操作];
    D --> E[注销IIO设备];

2.7 总结

Regmap API和IIO框架为设备驱动开发提供了强大而高效的工具。Regmap API简化了SPI和I2C设备的寄存器操作,通过统一的接口和缓存机制提高了开发效率和设备访问速度。IIO框架则为模拟 - 数字和数字 - 模拟转换设备提供了统一的管理和交互方式,使得不同类型的传感器能够更方便地集成到内核中。掌握这两个工具,开发者可以更轻松地开发出高质量的设备驱动程序。

3. Regmap API与IIO框架的综合应用及深入分析

3.1 Regmap API与IIO框架的协同工作

在实际的设备驱动开发中,Regmap API和IIO框架可以协同工作,以实现更复杂的功能。例如,对于一个支持SPI或I2C接口的传感器设备,我们可以使用Regmap API来进行寄存器的读写操作,同时使用IIO框架来管理设备的通道和数据采集。

以下是一个简单的示例,展示了如何将两者结合使用:

#include <linux/regmap.h>
#include <linux/iio/iio.h>

// 假设这是一个SPI设备
static struct spi_device *my_spi_device;
static struct iio_dev *my_iio_dev;
static struct regmap *my_regmap;

// 初始化Regmap
static int init_regmap(void) {
    struct regmap_config config = {
       .reg_bits = 8,
       .val_bits = 8,
        // 其他配置...
    };
    my_regmap = regmap_init_spi(my_spi_device, &config);
    if (IS_ERR(my_regmap)) {
        return PTR_ERR(my_regmap);
    }
    return 0;
}

// 初始化IIO设备
static int init_iio_device(void) {
    my_iio_dev = iio_device_alloc(sizeof(struct my_private_data));
    if (!my_iio_dev) {
        return -ENOMEM;
    }
    // 填充iio_dev字段
    my_iio_dev->name = "my_iio_device";
    my_iio_dev->modes = INDIO_DIRECT_MODE;
    // 其他配置...

    // 注册IIO设备
    return iio_device_register(my_iio_dev);
}

// 读取传感器数据
static int read_sensor_data(unsigned int reg, unsigned int *val) {
    return regmap_read(my_regmap, reg, val);
}

// 主初始化函数
static int my_driver_init(void) {
    int ret;
    ret = init_regmap();
    if (ret) {
        return ret;
    }
    ret = init_iio_device();
    if (ret) {
        regmap_exit(my_regmap);
        return ret;
    }
    return 0;
}

// 主退出函数
static void my_driver_exit(void) {
    iio_device_unregister(my_iio_dev);
    regmap_exit(my_regmap);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

3.2 Regmap API的性能优化

Regmap API提供了一些机制来优化设备的读写性能,特别是在使用缓存时。以下是一些性能优化的建议:
- 合理使用缓存 :根据设备的特点和使用场景,选择合适的缓存类型。例如,如果设备的寄存器值变化频繁,使用 REGCACHE_NONE 可能更合适;如果寄存器值变化较少,使用 REGCACHE_RBTREE 或其他缓存类型可以减少对硬件的访问次数。
- 批量读写操作 :使用 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);
  • 自定义读写函数 :如果设备有特殊的读写要求,可以提供自定义的 reg_read reg_write 回调函数,以提高读写效率。

3.3 IIO框架的高级应用

IIO框架除了基本的设备和通道管理外,还支持一些高级功能,如触发缓冲和事件管理。

3.3.1 触发缓冲支持

触发缓冲允许在特定事件触发时采集数据,提高数据采集的效率。以下是使用触发缓冲的基本步骤:
1. 分配和初始化IIO设备。
2. 启用触发缓冲功能,设置相关参数。
3. 注册触发器和轮询函数。
4. 开始数据采集。

// 启用触发缓冲
static int enable_triggered_buffer(struct iio_dev *indio_dev) {
    struct iio_buffer_setup_ops setup_ops = {
       .preenable = my_preenable,
       .postenable = my_postenable,
       .predisable = my_predisable,
       .postdisable = my_postdisable,
       .validate_scan_mask = my_validate_scan_mask,
    };
    indio_dev->setup_ops = &setup_ops;
    return iio_triggered_buffer_setup(indio_dev, my_trigger, my_poll_func);
}

// 主初始化函数
static int my_driver_init(void) {
    struct iio_dev *indio_dev;
    int ret;

    indio_dev = iio_device_alloc(sizeof(struct my_private_data));
    if (!indio_dev) {
        return -ENOMEM;
    }

    // 填充iio_dev字段
    // ...

    ret = iio_device_register(indio_dev);
    if (ret) {
        iio_device_free(indio_dev);
        return ret;
    }

    ret = enable_triggered_buffer(indio_dev);
    if (ret) {
        iio_device_unregister(indio_dev);
        iio_device_free(indio_dev);
        return ret;
    }

    return 0;
}
3.3.2 事件管理

IIO框架还支持事件管理,允许设备在特定条件下触发事件。例如,当传感器数据超过某个阈值时触发事件。以下是事件管理的基本步骤:
1. 定义事件类型和处理函数。
2. 注册事件处理函数。
3. 在设备驱动中触发事件。

// 事件处理函数
static int my_event_handler(struct iio_dev *indio_dev,
                            const struct iio_chan_spec *chan,
                            int64_t timestamp) {
    // 处理事件
    return 0;
}

// 注册事件处理函数
static int register_event_handler(struct iio_dev *indio_dev) {
    struct iio_event_spec event_spec = {
       .type = IIO_EV_TYPE_THRESH,
       .direction = IIO_EV_DIR_RISING,
       .channel = 0,
       .handler = my_event_handler,
    };
    return iio_add_event_spec(indio_dev, &event_spec);
}

3.4 常见问题及解决方案

在使用Regmap API和IIO框架时,可能会遇到一些常见问题,以下是一些解决方案:

问题描述 解决方案
Regmap初始化失败 检查设备连接、配置参数是否正确,查看错误码进行调试。
IIO设备注册失败 检查内存分配是否成功,iio_dev字段是否填充正确。
数据读写错误 检查寄存器地址、数据格式是否正确,使用调试工具查看通信数据。
触发缓冲不工作 检查触发器和轮询函数是否正确注册,触发条件是否满足。

3.5 总结与展望

Regmap API和IIO框架为设备驱动开发提供了强大的支持,通过统一的接口和高效的机制,简化了开发过程,提高了设备的管理和数据采集效率。在未来的开发中,随着硬件设备的不断发展,这两个工具也将不断完善和扩展,为开发者提供更多的功能和便利。

开发者可以进一步探索这两个工具的高级特性,结合实际项目需求,开发出更加高效、稳定的设备驱动程序。同时,关注社区的发展动态,学习其他开发者的经验和技巧,不断提升自己的开发水平。

通过本文的介绍,希望读者能够对Regmap API和IIO框架有更深入的理解,并能够在实际项目中灵活运用这两个工具,为设备驱动开发带来更多的可能性。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐