当前位置: 首页 > article >正文

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设备树配置情况如下:

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 文件中。

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之前需要先申请,申请函数如下:

struct iio_dev *iio_device_alloc(int sizeof_priv)

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

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 文件中。

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 文件中。

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里面。

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流,因此打开、读写此类文件要使用文件流操作函数。打开文件流函数为

FILE *fopen(const char *pathname, const char *mode)

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

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)函数,参数和返回值含义同上。

#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驱动进行了对比测试。


http://www.kler.cn/a/568314.html

相关文章:

  • 《基于Python的服务器实时监控运维系统的设计与开发》开题报告
  • P8697 [蓝桥杯 2019 国 C] 最长子序列
  • 浅显易懂HashMap的数据结构
  • 【AI大模型】使用Python调用DeepSeek的API,原来SDK是调用这个,绝对的一分钟上手和使用
  • Spring Boot的无缝衔接:深入解析与实践
  • C# Dictionary 使用指南
  • 解读DeepSeek开源的flashMLA项目的意义
  • 逻辑回归-乳腺癌肿瘤预测
  • 【PID】STM32通过闭环PID控制电机系统
  • k8s拉取harbor镜像部署
  • golang介绍,特点,项目结构,基本变量类型与声明介绍(数组,切片,映射),控制流语句介绍(条件,循环,switch case)
  • 海洋cmsv9报错注入,order by 和limit注入
  • NFC拉起微信小程序申请URL scheme 汇总
  • JavaScript 简单类型与复杂类型-简单类型传参
  • Spring Boot拦截器(Interceptor)与过滤器(Filter)详细教程
  • EtherCAT总线学习笔记
  • 【03】STM32F407 HAL 库框架设计学习
  • openEuler环境下GlusterFS分布式存储集群部署指南
  • 前缀和 C++
  • 【pytest框架源码分析三】pluggy源码分析之hook注册调用流程