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