Linux驱动开发之ADC驱动与基础应用编程

目录

ADC简介

SARADC

设备树配置

IIO子系统

应用程序编写

运行测试

ADC简介

模拟量指的是表示各种实际信息的物理量,可以是电量(如电压,电流等),也可以是来自传感器的非电量(如压力,温度等)。要想使用计算机处理模拟量,就必须将其转化为数字量。ADC(Analog to Digital Converter),也即模数转换器。它可以将外部的模拟量信号转化成数字量信号。A/D转换可以分为采样、保持、量化、编码4个过程。ADC也有很多种类型,例如逐次逼近型和双积分型等。

ADC 具有以下几个比较重要的参数:

  • 测量范围:测量范围可以理解为量程,ADC测量范围决定了你外接的设备其信号输出电压范围,不能超过ADC的测量范围。
  • 分辨率:可以理解为最小测量精度,假如ADC的测量范围为0-5V,分辨率为12位,那么我们能测出来的最小电压就是5V除以2的12次方,也就是 5/4096=0.00122V。所以,分辨率越高,采集到的信号越精确。
  • 精度:是影响结果准确度的因素之一,例如ADC在12位分辨率下的最小测量值是0.00122V 但是ADC的精度最高只能到11位也就是0.00244V。也就是ADC测量出0.00244V的结果是要比0.00122V要可靠,也更准确。
  • 采样时间:当ADC在某时刻采集外部电压信号的时候,此时外部的信号应该保持不变,但实际上外部的信号是不停变化的。所以在ADC内部有一个保持电路,保持某一时刻的外部信号,这样ADC就可以稳定采集了,保持这个信号的时间就是采样时间。
  • 采样频率:也就是在一秒的时间内采集多少次。很明显,采样频率越高越好,当采样率不够的时候可能会丢失部分信息。

总之,只要是需要模拟信号转为数字信号的场合,那么肯定要用到ADC。很多数字传感器内部会集成ADC,传感器内部使用ADC来处理原始的模拟信号,最终给用户输出数字信号。

SARADC

逐次逼近型ADC也叫SARADC,全称为Successive Approximation ADC,是一种转换速度较快、转换精度较高的AD转换器。它的工作过程是采用一系列基准电压与待转换电压进行比较,就好比用天平测量物体的质量时用砝码和待测重物进行比较。比较过程由高位到低位逐位进行,依次确定转换后信号的各位是1还是0。下图为逐次逼近型ADC的组成框图:

设备树配置

在荣品RK3588开发板上使用的就是SARADC并且在很多地方有所应用,如音频codec等。SARADC设备树配置情况如下:

cpp 复制代码
saradc: saradc@fec10000 {
        compatible = "rockchip,rk3588-saradc";
        reg = <0x0 0xfec10000 0x0 0x10000>;
        interrupts = <GIC_SPI 398 IRQ_TYPE_LEVEL_HIGH>;
        #io-channel-cells = <1>;
        clocks = <&cru CLK_SARADC>, <&cru PCLK_SARADC>;
        clock-names = "saradc", "apb_pclk";
        resets = <&cru SRST_P_SARADC>;
        reset-names = "saradc-apb";
        status = "disabled";
};

&saradc {
    status = "okay";
    vref-supply = <&vcc_1v8_s0>;
};

其中,vref-supply 属性表示saradc值对应的参考电压,需根据具体的硬件环境设置,最大为1.8V,对应的saradc值为1024,且电压和adc值成线性关系。SARADC驱动文件为drivers/iio/adc/rockchip_saradc.c,其依赖于"iio"子系统框架。

IIO子系统

IIO 全称是 Industrial I/O,也就是工业 I/O,是Linux 内核为了管理日益增多的ADC类传感器而推出的子系统。IIO 子系统使用结构体 iio_dev 来描述一个具体 IIO 设备,此设备结构体定义在include/linux/iio/iio.h 文件中。

cpp 复制代码
struct iio_dev {
    int             id;
    struct module           *driver_module;

    int             modes;
    int             currentmode;
    struct device           dev;
    struct iio_buffer       *buffer;
    int             scan_bytes;
    struct mutex            mlock;
    const unsigned long     *available_scan_masks;
    unsigned            masklength;
    const unsigned long     *active_scan_mask;
    bool                scan_timestamp;
    unsigned            scan_index_timestamp;
    struct iio_trigger      *trig;
    bool                trig_readonly;
    struct iio_poll_func        *pollfunc;
    struct iio_poll_func        *pollfunc_event;
    struct iio_chan_spec const  *channels;
    int             num_channels;
    const char          *name;
    const char          *label;
    const struct iio_info       *info;
    clockid_t           clock_id;
    struct mutex            info_exist_lock;
    const struct iio_buffer_setup_ops   *setup_ops;
    struct cdev         chrdev;
#define IIO_MAX_GROUPS 6
    const struct attribute_group    *groups[IIO_MAX_GROUPS + 1];
    int             groupcounter;
    unsigned long           flags;
    void                *priv;
};

其中,modes为设备支持的模式;buffer为缓冲区;available_scan_masks为可选的扫描位掩码,使用触发缓冲区的时候可以通过设置掩码来确定使能哪些通道,使能以后的通道会将捕获到的数据发送到IIO缓冲区;channels为IIO设备通道,为iio_chan_spec结构体类型;info 为iio_info结构体类型,这个结构体里面有很多函数,需要驱动开发人员编写,用户空间读取IIO设备内部数据,最终调用的就是iio_info里面的函数。

同样,在使用iio_dev之前需要先申请,申请函数如下:

cpp 复制代码
struct iio_dev *iio_device_alloc(int sizeof_priv)

其中,sizeof_priv为私有数据内存空间大小,一般会将自定义的设备结构体变量作为iio_dev的私有数据,这样可以直接通过iio_device_alloc函数同时完成iio_dev和设备结构体变量的内存申请。申请成功以后可以使用iio_priv函数来得到自定义的设备结构体变量首地址。释放iio_dev的函数如下:

cpp 复制代码
void iio_device_free(struct iio_dev *indio_dev)

在申请好后,接下来就需要初始化各种成员变量,初始化完成以后就需要将iio_dev注册到内核中,需要用到int iio_device_register(struct iio_dev *indio_dev)函数,注销iio_dev则使用void iio_device_unregister(struct iio_dev *indio_dev)函数。

iio_info结构体指针变量定义如下,同样定义在include/linux/iio/iio.h 文件中。

cpp 复制代码
struct iio_info {
    const struct attribute_group    *event_attrs;
    const struct attribute_group    *attrs;

    int (*read_raw)(struct iio_dev *indio_dev,
            struct iio_chan_spec const *chan,
            int *val,
            int *val2,
            long mask);
    int (*read_raw_multi)(struct iio_dev *indio_dev,
            struct iio_chan_spec const *chan,
            int max_len,
            int *vals,
            int *val_len,
            long mask);
    int (*read_avail)(struct iio_dev *indio_dev,
              struct iio_chan_spec const *chan,
              const int **vals,
              int *type,
              int *length,
              long mask);
    int (*write_raw)(struct iio_dev *indio_dev,
             struct iio_chan_spec const *chan,
             int val,
             int val2,
             long mask);
    int (*write_raw_get_fmt)(struct iio_dev *indio_dev,
             struct iio_chan_spec const *chan,
             long mask);
    int (*read_event_config)(struct iio_dev *indio_dev,
                 const struct iio_chan_spec *chan,
                 enum iio_event_type type,
                 enum iio_event_direction dir);
    int (*write_event_config)(struct iio_dev *indio_dev,
                  const struct iio_chan_spec *chan,
                  enum iio_event_type type,
                  enum iio_event_direction dir,
                  int state);
    int (*read_event_value)(struct iio_dev *indio_dev,
                const struct iio_chan_spec *chan,
                enum iio_event_type type,
                enum iio_event_direction dir,
                enum iio_event_info info, int *val, int *val2);
    int (*write_event_value)(struct iio_dev *indio_dev,
                 const struct iio_chan_spec *chan,
                 enum iio_event_type type,
                 enum iio_event_direction dir,
                 enum iio_event_info info, int val, int val2);
    int (*validate_trigger)(struct iio_dev *indio_dev,
                struct iio_trigger *trig);
    int (*update_scan_mode)(struct iio_dev *indio_dev,
                const unsigned long *scan_mask);
    int (*debugfs_reg_access)(struct iio_dev *indio_dev,
                  unsigned reg, unsigned writeval,
                  unsigned *readval);
    int (*of_xlate)(struct iio_dev *indio_dev,
            const struct of_phandle_args *iiospec);
    int (*hwfifo_set_watermark)(struct iio_dev *indio_dev, unsigned val);
    int (*hwfifo_flush_to_buffer)(struct iio_dev *indio_dev,
                      unsigned count);
};

可以看出该结构体中基本都是一些函数定义,是用户空间对设备的具体操作的最终反映,类似于file_operation。其中,attrs是通用的设备属性。read_raw和 write_raw这两个函数就是最终读写设备内部数据的操作函数,indio_dev是需要读写的IIO设备,chan是需要读取的通道,val,val2是读取/写入设备的数据,val表示整数部分,val2表示小数部分。但是val2是对具体的小数部分扩大N倍后的整数值,因为不能直接从内核向应用程序返回一个小数值。且扩大的倍数我们不能随便设置,而是要使用 Linux 定义的倍数。mask为掩码,用于指定我们读取的是什么数据,Linux 内核使用 IIO_CHAN_INFO_RAW 和 IIO_CHAN_INFO_SCALE 这两个宏来表示原始值以及分辨率,这两个宏就是掩码。write_raw_get_fmt 用于设置用户空间向内核空间写入的数据格式,该函数决定了wtite_raw函数中val和val2的意义。

IIO的核心就是通道,一个传感器可能有多路数据,比如一个ADC芯片支持8路采集,那么这个ADC就有8个通道。Linux 内核使用 iio_chan_spec 结构体来描述通道,定义在 include/linux/iio/iio.h 文件中。

cpp 复制代码
struct iio_chan_spec {
    enum iio_chan_type  type;
    int         channel;
    int         channel2;
    unsigned long       address;
    int         scan_index;
    struct {
        char    sign;
        u8  realbits;
        u8  storagebits;
        u8  shift;
        u8  repeat;
        enum iio_endian endianness;
    } scan_type;
    long            info_mask_separate;
    long            info_mask_separate_available;
    long            info_mask_shared_by_type;
    long            info_mask_shared_by_type_available;
    long            info_mask_shared_by_dir;
    long            info_mask_shared_by_dir_available;
    long            info_mask_shared_by_all;
    long            info_mask_shared_by_all_available;
    const struct iio_event_spec *event_spec;
    unsigned int        num_event_specs;
    const struct iio_chan_spec_ext_info *ext_info;
    const char      *extend_name;
    const char      *datasheet_name;
    unsigned        modified:1;
    unsigned        indexed:1;
    unsigned        output:1;
    unsigned        differential:1;
};

其中,type为通道类型,iio_chan_type是一个枚举类型,列举了可以选择的所有通道类型,定义在include/uapi/linux/iio/types.h里面。

cpp 复制代码
enum iio_chan_type {
    IIO_VOLTAGE,
    IIO_CURRENT,
    IIO_POWER,
    IIO_ACCEL,
    IIO_ANGL_VEL,
    IIO_MAGN,
    IIO_LIGHT,
    IIO_INTENSITY,
    IIO_PROXIMITY,
    IIO_TEMP,
    IIO_INCLI,
    IIO_ROT,
    IIO_ANGL,
    IIO_TIMESTAMP,
    IIO_CAPACITANCE,
    IIO_ALTVOLTAGE,
    IIO_CCT,
    IIO_PRESSURE,
    IIO_HUMIDITYRELATIVE,
    IIO_ACTIVITY,
    IIO_STEPS,
    IIO_ENERGY,
    IIO_DISTANCE,
    IIO_VELOCITY,
    IIO_CONCENTRATION,
    IIO_RESISTANCE,
    IIO_PH,
    IIO_UVINDEX,
    IIO_ELECTRICALCONDUCTIVITY,
    IIO_COUNT,
    IIO_INDEX,
    IIO_GRAVITY,
    IIO_POSITIONRELATIVE,
    IIO_PHASE,
    IIO_MASSCONCENTRATION,
#ifdef CONFIG_NO_GKI
    IIO_SIGN_MOTION,
    IIO_STEP_DETECTOR,
    IIO_STEP_COUNTER,
    IIO_TILT,
    IIO_TAP,
    IIO_TAP_TAP,
    IIO_WRIST_TILT_GESTURE,
    IIO_GESTURE,
#endif
};

可以看出,Linux内核支持的传感器类型非常丰富,其中ADC对应于IIO_VOLTAGE类型。回到iio_chan_spec 结构体,当成员变量indexed为1的时候,channel为通道索引。当成员变量modified为1的时候,channel2为通道修饰符,如X,Y,Z轴修饰符,通道修饰符主要影响sysfs下的通道文件名称。address成员变量用户可以自定义,但是一般会设置为此通道对应的芯片数据寄存器的地址。output表示为输出通道。differential表示为差分通道。

IIO框架主要用于ADC类的传感器,比如陀螺仪、加速度计、磁力计、光强度计等,这些传感器基本都是IIC或者SPI接口的。因此IIO驱动的基础框架就是IIC或者SPI,有些SOC 内部的ADC也会使用IIO框架,那么这个时候驱动的基础框架就是platfrom。

荣品RK3588开发板中SARADC驱动已经编写好了,我们只需使能相关内核配置即可。

编译并烧录内核,系统启动后使用命令cat /sys/bus/iio/devices/iio\:device0/in_voltage0_raw即可获取channel0的ADC值。

应用程序编写

cat虽然能获取对应文件的内容,但是要连续不断的读取传感器数据就不能用cat命令了。像in_voltage0_raw这样的传感器数据文件称为流文件,也叫标准文件I/O流,因此打开、读写此类文件要使用文件流操作函数。打开文件流函数为

cpp 复制代码
FILE *fopen(const char *pathname, const char *mode)

其中,pathname为需要打开的文件流路径,mode为打开方式,打开错误返回NULL,成功则返回FILE类型的文件流指针。关闭文件流则使用函数int fclose(FILE *stream),返回0表示关闭成功。读取文件流函数为

cpp 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

其中,ptr为要读取的数组中首个对象的指针。size为每个对象的大小。nmemb为要读取的对象个数。stream为要读取的文件流。读取成功返回读取的对象个数,如果出现错误或到文件末尾,那么返回一个短计数值 (或者 0)。 向文件流写入数据,使用size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)函数,参数和返回值含义同上。

cpp 复制代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>

/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atof(str);\
 
/* 字符串转数字,将整数字符串转换为整数数值 */
#define SENSOR_INT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atoi(str);\
/* iio框架下对应的文件路径 */
static char *file_path[] = {
    "/sys/bus/iio/devices/iio:device0/in_voltage_scale",
    "/sys/bus/iio/devices/iio:device0/in_voltage1_raw",
};
/* 文件路径索引,要和file_path里面的文件顺序对应 */
enum path_index {
    IN_VOLTAGE_SCALE = 0,
    IN_VOLTAGE_RAW,
};

struct adc_dev{
    int raw;
    float scale;
    float act;
};
struct adc_dev saradc;

static int file_data_read(char *filename, char *str)
{
    int ret = 0;
    FILE *data_stream;
    data_stream = fopen(filename, "r"); 
    if(data_stream == NULL) {
        printf("can't open file %s\r\n", filename);
        return -1;
    }
    
    ret = fscanf(data_stream, "%s", str);
    if(!ret) {
        printf("file read error!\r\n");
    } else if(ret == EOF) {
        /* 读到文件末尾时将文件指针重新调整到文件头 */
        fseek(data_stream, 0, SEEK_SET); 
    }
    fclose(data_stream); 
    return 0;
}

static int adc_read(struct adc_dev *dev)
{
    int ret = 0;
    char str[50];
    SENSOR_FLOAT_DATA_GET(ret, IN_VOLTAGE_SCALE, str, scale);
    SENSOR_INT_DATA_GET(ret, IN_VOLTAGE_RAW, str, raw);
    /* 转换为实际电压值,单位mV */
    dev->act = (dev->scale * dev->raw)/1000.f;
    return ret;
}

int main(int argc, char *argv[])
{
    int ret = 0;
    if (argc != 1) {
        printf("Error Usage!\r\n");
        return -1;
    }
    while (1) {
    ret = adc_read(&saradc);
    if(ret == 0) { 
        printf("ADC 原始值:%d,电压值:%.3fV\r\n", saradc.raw,saradc.act);
    }
    usleep(100000); 
    }
    return 0;
}

其中,使用atof函数将浮点字符串转换为具体的浮点数值,使用atoi函数将整数字符串转换为具体的整数数值。

运行测试

此次测试采用龙芯2k0300久久派开发板进行测试,由于其4.19的内核没有支持ADC驱动,故需要参考其5.10内核的源码进行ADC驱动的移植,具体可参考龙芯LS2K0300之ADC驱动。从新编译的内核启动,选定开发板上的ADC通道1进行测试,将其与开发板GND引脚相连,使用cat命令查看ADC值大小。交叉编译测试程序并拷入开发板运行,观察ADC值的打印并进行对比。

可以看出,测试程序的输出结果与命令获取的ADC值基本一致。除此之外,还可以对ADC引脚进行加压测试,对比测试结果是否准确,注意不要超过开发板ADC引脚的参考电压值。

总结:本篇详细介绍了ADC的相关基础知识以及Linux内核的IIO子系统框架,并编写测试程序对开发板ADC驱动进行了对比测试。

相关推荐
m0_748254884 分钟前
【华为OD机考】2024E+D卷真题【完全原创题解 详细考点分类 不断更新题目 六种主流语言Py+Java+Cpp+C+Js+Go】
java·c语言·华为od
Tlog嵌入式18 分钟前
[项目]基于FreeRTOS的STM32四轴飞行器: 三.电源控制
c语言·单片机·mcu·iot
黎明晓月29 分钟前
‌CentOS 7.9 安装 Docker 步骤
linux·docker·centos
菜鸟xy..38 分钟前
winhex软件简单讲解,虚拟磁盘分区介绍
linux·运维·服务器
网硕互联的小客服41 分钟前
如何排查服务器内存泄漏问题
linux·运维·服务器·安全·ssh
Evoxt 益沃斯1 小时前
How to enable Qemu Guest Agent for Virtual Machines
linux·运维·服务器·qemu
钟离墨笺1 小时前
【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)
linux·服务器·网络
solomonzw1 小时前
C++ 学习(八)(模板,可变参数模板,模板专业化(完整模板专业化,部分模板专业化),类型 Traits,SFINAE(替换失败不是错误),)
c语言·开发语言·c++·学习
llkk星期五1 小时前
ubuntu 22.04附加驱动安装NVIDIA显卡驱动重启后无WiFi蓝牙等问题
linux·ubuntu
CVer儿1 小时前
ubuntu挂载固态硬盘
linux·运维·ubuntu