1

Linux ADXL345 驱动分析

 1 year ago
source link: https://www.taterli.com/8768/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Linux ADXL345 驱动分析

为什么分析这个器件呢,因为他涉及的知识点比较多,而且很多人都用过,既支持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呢,查表看到地址应该是这样的.

image-6.png

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

#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 驱动开发实际上就是和各种子系统打交道,真是一个最优美的架构.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK