Linux ADXL345 驱动分析

/ 1评 / 0

为什么分析这个器件呢,因为他涉及的知识点比较多,而且很多人都用过,既支持I2C接口,也支持SPI接口,在Linux中当然推荐用regmap统一管理,这里分析的是由Eva Rachel Retuya大佬写的驱动,实际上这个驱动也很简单但是很经典,所以值得分析.

首先在adxl345_spi.c和adxl345_i2c.c分别实现platform driver引入.

static struct i2c_driver adxl345_i2c_driver = {
	.driver = {
		.name	= "adxl345_i2c",
		.of_match_table = adxl345_of_match,
		.acpi_match_table = adxl345_acpi_match,
	},
	.probe_new	= adxl345_i2c_probe,
	.id_table	= adxl345_i2c_id,
};
module_i2c_driver(adxl345_i2c_driver);

static struct spi_driver adxl345_spi_driver = {
	.driver = {
		.name	= "adxl345_spi",
		.of_match_table = adxl345_of_match,
		.acpi_match_table = adxl345_acpi_match,
	},
	.probe		= adxl345_spi_probe,
	.id_table	= adxl345_spi_id,
};
module_spi_driver(adxl345_spi_driver);

他们分别也只是单独实现了自己的probe函数,先看看两个初始化的区别.

static const struct regmap_config adxl345_i2c_regmap_config = {
	.reg_bits = 8,
	.val_bits = 8,
};

static int adxl345_i2c_probe(struct i2c_client *client)
{
	struct regmap *regmap;

	regmap = devm_regmap_init_i2c(client, &adxl345_i2c_regmap_config);
	if (IS_ERR(regmap))
		return dev_err_probe(&client->dev, PTR_ERR(regmap), "Error initializing regmap\n");

	return adxl345_core_probe(&client->dev, regmap);
}

static const struct regmap_config adxl345_spi_regmap_config = {
	.reg_bits = 8,
	.val_bits = 8,
	 /* Setting bits 7 and 6 enables multiple-byte read */
	.read_flag_mask = BIT(7) | BIT(6),
};

static int adxl345_spi_probe(struct spi_device *spi)
{
	struct regmap *regmap;

	/* Bail out if max_speed_hz exceeds 5 MHz */
	if (spi->max_speed_hz > ADXL345_MAX_SPI_FREQ_HZ)
		return dev_err_probe(&spi->dev, -EINVAL, "SPI CLK, %d Hz exceeds 5 MHz\n",
				     spi->max_speed_hz);

	regmap = devm_regmap_init_spi(spi, &adxl345_spi_regmap_config);
	if (IS_ERR(regmap))
		return dev_err_probe(&spi->dev, PTR_ERR(regmap), "Error initializing regmap\n");

	return adxl345_core_probe(&spi->dev, regmap);
}

我们知道如果单独操作SPI驱动,可以用spi_message_xxx做结构然后用spi_sync发送,如果单独做I2C驱动用i2c_transfer,不管哪种,他对于这种多接口器件也不友好,而且更底层的接口不会优化太多的性能,反而增加后续很多麻烦,比如ADXL345的SPI读写寄存器需要用最高位来决定,还要用MB位决定是否连续读取.

如果使用传统的SPI方法我们就要先行操作reg,最后还要做复制,明明都是很常见的代码,要是每个设备都这么做,必然大量代码冗余.

// 结构
struct spi_message m;
struct spi_device *spi = (struct spi_device *)dev->private_data;

// 内存申请
t = kzalloc(sizeof(struct spi_transfer),GFP_KERNEL);

// 数据填充
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){
    return -ENOMEM;
}

memcpy(buf,rxdata+1,len);

在regmap会简单成怎样?

regmap_read(dev->regmap,reg,&data);

regmap目前支持很多接口,除了I2C,SPI之类,包括I3C之类也是支持的,可以说是只要操作的设备自身也有寄存器,通信方式比较正常,都可以支持,看一开始SPI初始化时候会套用一个参数read_flag_mask,这个参数就是说当我需要读寄存器时候,自动置位BIT7/BIT6,那么我们就不用自己txdata[0] = reg | 0x8这样置位了.

OK,初始化后,一切实际驱动都会在adxl345_core实现,进来后简单校验然后就开始创建IIO设备.

int adxl345_core_probe(struct device *dev, struct regmap *regmap)
{
	enum adxl345_device_type type;
	struct adxl345_data *data;
	struct iio_dev *indio_dev;
	const char *name;
	u32 regval;
	int ret;

	type = (uintptr_t)device_get_match_data(dev);
	switch (type) {
	case ADXL345:
		name = "adxl345";
		break;
	case ADXL375:
		name = "adxl375";
		break;
	default:
		return -EINVAL;
	}

	// 读取寄存器,如果读取不到,可能dts或者device驱动匹配不对的地址,如果在SPI怎么都能过这一步,毕竟没有ACK状态.
	ret = regmap_read(regmap, ADXL345_REG_DEVID, &regval);
	if (ret < 0)
		return dev_err_probe(dev, ret, "Error reading device ID\n");

	// 比较Device ID是否相等.
	if (regval != ADXL345_DEVID)
		return dev_err_probe(dev, -ENODEV, "Invalid device ID: %x, expected %x\n",
				     regval, ADXL345_DEVID);

	// 创建一个IIO设备(工业IO)
	indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
	if (!indio_dev)
		return -ENOMEM;

IIO设备可以理解成所有涉及ADC,DAC转换,最后以此上报数据,不好分类为其他设备的,都可以加入这里,比如驱动这里实现了3个函数.

static const struct iio_info adxl345_info = {
	.attrs		= &adxl345_attrs_group,
	.read_raw	= adxl345_read_raw,
	.write_raw	= adxl345_write_raw,
	.write_raw_get_fmt	= adxl345_write_raw_get_fmt,
};

这里有一个特别的知识点,用户无法传递浮点数到内核,所以read_raw和read_raw分别有val1,val2,write_raw_get_fmt决定了用户用那种格式写数据到内核驱动,比如当写入IIO_CHAN_INFO_CALIBBIAS的时候,支持IIO_VAL_INT,也就是val1是整数,val2无意义,当写入IIO_CHAN_INFO_SAMP_FREQ时候,小数部分扩大10^9倍,则用户写1.234,此时val1是1,val2是0.234*10^9.

static int adxl345_write_raw_get_fmt(struct iio_dev *indio_dev,
				     struct iio_chan_spec const *chan,
				     long mask)
{
	switch (mask) {
	case IIO_CHAN_INFO_CALIBBIAS:
		return IIO_VAL_INT;
	case IIO_CHAN_INFO_SAMP_FREQ:
		return IIO_VAL_INT_PLUS_NANO;
	default:
		return -EINVAL;
	}
}

当然我们返回的数据也要报告一个格式,这样用户空间才知道怎么显示,数据读写我们就有了,难道还要用户自己用户空间挨个转换读取,查查寄存器手册,明显太低效率了,所以还要定义iio_chan_spec,下面定义进行了部分展开,具体源码可以看官方源码库.

static const struct iio_chan_spec adxl345_channels[] = {
    {					
        .type = IIO_ACCEL, // 比如IIO_LIGHT(光传感) IIO_VOLTAGE(电压ADC) 等等
        .modified = 1, // 当设置为1,channel2为通道描述符
        .channel2 = IIO_MOD_X, // 这个是iio_modifier定义的,他定义决定了用户空间下的文件名称
        .address = 0, // 地址(不一定是寄存器地址)
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |BIT(IIO_CHAN_INFO_CALIBBIAS),	// 通道属性
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) | BIT(IIO_CHAN_INFO_SAMP_FREQ), // 通道属性			
    }
	ADXL345_CHANNEL(1, Y),
	ADXL345_CHANNEL(2, Z),
};

为什么这里address设置成0呢,查表看到地址应该是这样的.

所以实际读取时候也进行了转换.

#define ADXL345_REG_DATAX0		0x32

#define ADXL345_REG_DATA_AXIS(index)	\
	(ADXL345_REG_DATAX0 + (index) * sizeof(__le16))

static int adxl345_read_raw(struct iio_dev *indio_dev,
			    struct iio_chan_spec const *chan,
			    int *val, int *val2, long mask)
{
	struct adxl345_data *data = iio_priv(indio_dev);
	__le16 accel;
	long long samp_freq_nhz;
	unsigned int regval;
	int ret;

	switch (mask) {
	case IIO_CHAN_INFO_RAW:
		/*
		 * Data is stored in adjacent registers:
		 * ADXL345_REG_DATA(X0/Y0/Z0) contain the least significant byte
		 * and ADXL345_REG_DATA(X0/Y0/Z0) + 1 the most significant byte
		 */
		ret = regmap_bulk_read(data->regmap,
				       ADXL345_REG_DATA_AXIS(chan->address),
				       &accel, sizeof(accel));
		if (ret < 0)
			return ret;

		*val = sign_extend32(le16_to_cpu(accel), 12);

如果不想这么做也可以这么设置.

static const struct iio_chan_spec adxl345_channels[] = {
	ADXL345_CHANNEL(ADXL345_REG_DATAX0, X),
	ADXL345_CHANNEL(ADXL345_REG_DATAY0, Y),
	ADXL345_CHANNEL(ADXL345_REG_DATAZ0, Z),
};

还有一些杂项,比如采样率必然不是连续可调的,所以也可以预先定义.

static IIO_CONST_ATTR_SAMP_FREQ_AVAIL(
"0.09765625 0.1953125 0.390625 0.78125 1.5625 3.125 6.25 12.5 25 50 100 200 400 800 1600 3200"
);

static struct attribute *adxl345_attrs[] = {
	&iio_const_attr_sampling_frequency_available.dev_attr.attr,
	NULL
};

static const struct attribute_group adxl345_attrs_group = {
	.attrs = adxl345_attrs,
};

现在已经差不多了,回到主线继续注册IIO设备.

	// 创建一个IIO设备(工业IO)
	indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
	if (!indio_dev)
		return -ENOMEM;

	// 从dev中要一块私有数据的内存,这里的私有数据当然是adxl345_data了.
	data = iio_priv(indio_dev);
	data->regmap = regmap; // 来自regmap驱动,不存起来后续可是很麻烦.
	data->type = type; // 这个驱动本体识别两个不同的驱动.
	/* Enable full-resolution mode */
	data->data_range = ADXL345_DATA_FORMAT_FULL_RES;

	ret = regmap_write(data->regmap, ADXL345_REG_DATA_FORMAT,
			   data->data_range);
	if (ret < 0)
		return dev_err_probe(dev, ret, "Failed to set data range\n");

	// 实际IIO套结构
	indio_dev->name = name;
	indio_dev->info = &adxl345_info; // 关联read_raw/write_raw/write_raw_get_fmt
	indio_dev->modes = INDIO_DIRECT_MODE; // 提供sysfs接口模式,还可以选择INDIO_BUFFER_TRIGGERED(硬件触发),INDIO_BUFFER_SOFTWARE(软件触发),INDIO_BUFFER_HARDWARE(硬件缓冲区)
	indio_dev->channels = adxl345_channels; // 各种通道
	indio_dev->num_channels = ARRAY_SIZE(adxl345_channels);

	/* Enable measurement mode */
	ret = adxl345_powerup(data->regmap);
	if (ret < 0)
		return dev_err_probe(dev, ret, "Failed to enable measurement mode\n");

	ret = devm_add_action_or_reset(dev, adxl345_powerdown, data->regmap);
	if (ret < 0)
		return ret;

	// 注册设备
	return devm_iio_device_register(dev, indio_dev);
}

为了用,当然要dts声明他.

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

i2c0 {
    #address-cells = <1>;
    #size-cells = <0>;

    /* Example for a I2C device node */
    accelerometer@2a {
        compatible = "adi,adxl345";
        reg = <0x53>;
        interrupt-parent = <&gpio0>;
        interrupts = <0 IRQ_TYPE_LEVEL_HIGH>;
    };
};

spi0 {
    #address-cells = <1>;
    #size-cells = <0>;

    /* Example for a SPI device node */
    accelerometer@0 {
        compatible = "adi,adxl345";
        reg = <0>;
        spi-max-frequency = <5000000>;
        spi-cpol;
        spi-cpha;
        interrupt-parent = <&gpio0>;
        interrupts = <0 IRQ_TYPE_LEVEL_HIGH>;
    };
};

现在到用户空间看,命名规则主要看industrialio-core.c,主要结构如下:

static const char * const iio_direction[] = {
	[0] = "in",
	[1] = "out",
};

static const char * const iio_chan_type_name_spec[] = {
	[IIO_VOLTAGE] = "voltage",
	[IIO_CURRENT] = "current",
	[IIO_POWER] = "power",
	[IIO_ACCEL] = "accel",
...
}

static const char * const iio_modifier_names[] = {
	[IIO_MOD_X] = "x",
	[IIO_MOD_Y] = "y",
	[IIO_MOD_Z] = "z",
...
}

static const char * const iio_chan_info_postfix[] = {
	[IIO_CHAN_INFO_RAW] = "raw",
	[IIO_CHAN_INFO_PROCESSED] = "input",
	[IIO_CHAN_INFO_SCALE] = "scale",
	[IIO_CHAN_INFO_OFFSET] = "offset",
	[IIO_CHAN_INFO_CALIBSCALE] = "calibscale",
	[IIO_CHAN_INFO_CALIBBIAS] = "calibbias",
...
}

我们设备刚好是输入且类型是IIO_ACCEL,且channel2定义是IIO_MOD_X,并且数据格式是IIO_CHAN_INFO_RAW,IIO_CHAN_INFO_CALIBBIAS,IIO_CHAN_INFO_SCALE,IIO_CHAN_INFO_SAMP_FREQ,所以他会生成3个(后面2个共享)名称,其中第一个不用质疑就是in_accel_x_raw.

另外驱动中中断似乎没用?需要的话要自行写2E寄存器,然后绑定中断函数.

Linux 驱动开发实际上就是和各种子系统打交道,真是一个最优美的架构.

  1. 假如说道:

    有个项目想看下博主是否能做

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注