SPI接口的ADC驱动调试

背景

最近在学习IIO子系统,顺带调试了个SPI接口的ADC驱动,所以在这简单记录下。
这里只简单介绍了适配一个简单SPI接口ADC驱动的流程,不过多深入框架子系统,更多关于IIO子系统的介绍,请见下一篇:

驱动开发

dts

dts主要修改或新增以下几点:

  • spi master控制器相关配置
  • spi设备节点配置
  • 引脚复用配置

如spi节点配置例子:

&spi0 {
    status = "okay";
    num-cs = <1>;  /* Total number of chip selects */

    aaa: aaa@0 {
        status = "okay";
        reg = <0>;  /* Chip select used by the device. */
        compatible = "yyy,xxx";
        wakeup-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_HIGH>; /* gpio, interrupt... */
        spi-cpol;   /* The device requires inverse clock polarity (CPOL) mode. */
        spi-cpha;   /* The device requires shifted clock phase (CPHA) mode. */
        spi-tx-bus-width = <1>; /* Bus width to the SPI bus used for write transfers. */
        spi-rx-bus-width = <1>;
        spi-max-frequency = <5000000>;
    };
};

更多spi配置信息可参见内核文档spi相关部分[^spi]

说明:
SPI的四种模式,即上面dts配置中spi-cpolspi-cpha属性,这个需要根据spi从设备的时序特性来配置

SPI模式 CPOL CPHA 空闲状态时钟 数据输入/出时钟相位
SPI0 0 0 低电平 上升沿输入,下降沿输出
SPI1 0 1 低电平 下升沿输入,上降沿输出
SPI2 1 0 高电平 下升沿输入,上降沿输出
SPI3 1 1 高电平 上升沿输入,下降沿输出

这里有更具体详细介绍:https://blog.csdn.net/buhuidage/article/details/121180622

配置

主要有以下几方面的驱动配置:

  • spi core的配置
  • soc相关spi master控制器驱动配置
  • 如果使用IIO子系统,还需打开IIO的配置
  • 如果使用regmap子系统,还需打开regmap的配置
  • 如果使用spidev,还需打开spidev相关配置,spidev相关介绍可见另一篇博文:https://notes.z-dd.online/2023/04/17/spidev%E7%9B%B8%E5%85%B3/
#spi core
CONFIG_SPI=y
CONFIG_SPI_MASTER=y

#spi master控制器
CONFIG_SPI_XXX=y 

#iio
CONFIG_IIO=y
CONFIG_IIO_BUFFER=y
CONFIG_IIO_BUFFER_CB=y
CONFIG_IIO_KFIFO_BUF=y
CONFIG_IIO_TRIGGERED_BUFFER=y
CONFIG_IIO_TRIGGER=y
CONFIG_IIO_CONSUMERS_PER_TRIGGER=2

#regmap
CONFIG_REGMAP=y
CONFIG_REGMAP_SPI=y
CONFIG_REGMAP_MMIO=y
CONFIG_REGMAP_IRQ=y

#spidev
CONFIG_SPI_SPIDEV=y

驱动代码实现

其实有很多方式来实现该驱动:

伪代码实现如下:

...

/* 通道属性,根据器件硬件属性配置 */
#define IIO_ADC_VOLTAGE_CHAN(num) \
    { \
        .type = IIO_VOLTAGE, \
        .indexed = 1, \
        .channel = (num), \
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) \
    }

static const struct iio_chan_spec iio_adc_channels[] = {
    IIO_ADC_VOLTAGE_CHAN(0),
    IIO_ADC_VOLTAGE_CHAN(1),
};

static int iio_adc_sensor_read(struct iio_adc *dev, int ch, int *val)
{
    /* 通过spi_write()/spi_read()/spi_write_then_read()等相关spi接口操作寄存器 */
    ...
}

static int iio_adc_read_raw(struct iio_dev *indio_dev,
                struct iio_chan_spec const *chan,
                int *val, int *val2,
                long mask)
{
    int ret;
    struct iio_adc *dev = iio_priv(indio_dev);
            
    switch (mask) {
    case IIO_CHAN_INFO_RAW: /* 读取传感器数据 */
        if (chan->type == IIO_VOLTAGE) {
            ret = iio_adc_sensor_read(dev, chan->channel, val);
            if (ret < 0)
                return ret;
        }
        return IIO_VAL_INT;
    case IIO_CHAN_INFO_SCALE: /* SCALE根据实际调整 */
        *val = 3600 / 1000; 
        *val2 = 12;
        return IIO_VAL_FRACTIONAL_LOG2;
    default:
        ret = -EINVAL;
        break;
    }

    return ret;
}

/* 实际使用的接口需根据器件功能自行添加 */
static const struct iio_info iio_adc_info = {
    .read_raw = &iio_adc_read_raw,
};

static int iio_adc_probe(struct spi_device *spi)
{
    int ret = 0;
    struct iio_adc *iio_adc_dev;
    struct iio_dev *iiodev;

    /* 向内核申请分配iio_dev内存, 包括同时分配的iio_adc_dev内存 */
    iiodev = devm_iio_device_alloc(&spi->dev, sizeof(struct iio_adc));
    if (!iiodev) {
        return -ENOMEM;
    }
    spi_set_drvdata(spi, iiodev);

    /* 把已分配的indio_dev内存结构的私有数据赋给iio_adc_dev */
    iio_adc_dev = iio_priv(iiodev);
    iio_adc_dev->spi = spi;
    mutex_init(&iio_adc_dev->lock);

    /* 设置iio_dev的主要成员变量 */
    iiodev->name = "iio_adc";
    //iiodev->dev.parent = &spi->dev;
    iiodev->info = &iio_adc_info;
    iiodev->modes = INDIO_DIRECT_MODE; /* 更多模式说明请见iio子系统的文章 */
    iiodev->channels = iio_adc_channels;
    iiodev->num_channels = ARRAY_SIZE(iio_adc_channels);

    /* 注册iio_dev */
    ret = iio_device_register(iiodev);
    if (ret < 0) {
        dev_err(&spi->dev, "iio_device_register failed\n");
        goto err_iio_register;
    }
    
    /* vref regulator相关操作:devm_regulator_get() regulator_enable() */
    /* GPIO 等硬件相关操作 */

    /* 初始化从设备内部寄存器 */
    iio_adc_reg_init(iio_adc_dev);

    return 0;
}

static int iio_adc_remove(struct spi_device *spi)
{
    struct iio_dev *iiodev = spi_get_drvdata(spi);
    /* 注销IIO */
    iio_device_unregister(iiodev);
    return 0;
}

static const struct of_device_id iio_adc_of_match[] = {
    {.compatible = "yyy,xxx"},
    { /*  */}
};
MODULE_DEVICE_TABLE(of, iio_adc_of_match);

static struct spi_driver iio_adc_driver = {
    .probe  = iio_adc_probe,
    .remove = iio_adc_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name  = "iio_adc",
        .of_match_table = iio_adc_of_match,
    },
    //.id_table = iio_adc_id,
};

static int __init iio_adc_init(void)   
{   
    /* 注册spi 驱动 */
    return spi_register_driver(&iio_adc_driver); 
}  
static void __exit iio_adc_exit(void) 
{  
    spi_unregister_driver(&iio_adc_driver);
}  

...

核外操作

这里以读取adc通道0的电压为例,其他类似。

  • 命令行方式:

    #读取
    cat /sys/bus/iio/devices/iio\:device0/in_voltage0_raw
  • C程序方式:
    可直接通过fopen,read等文件操作接口来操作设备节点

参考

  • drivers/iio/adc/ti-adc128s052.c

  • Documentation/devicetree/bindings/iio/adc/ti-adc128s052.txt

  • Documentation/devicetree/bindings/spi/spi-controller.yaml