目录
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 6const 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_GKIIIO_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驱动进行了对比测试。