1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
第三十八章 Linux ADC驱动实验
上一章节我们讲解了如何给AP3216C编写IIO驱动,AP3216C本质就是ADC,因此纯粹的ADC驱动也是IIO驱动框架的。本章我们就来学习一下如何使用RK3568内部的ADC,并且在学习巩固一下IIO驱动。
38.1 ADC简介
38.1.1 什么是ADC
ADC,Analog to Digital Converter的缩写,中文名称模数转换器。它可以将外部的模拟信号转化成数字信号。对于GPIO口来说高于某个电压值,它读出来的只有高电平,低于就是低电平。假如我想知道具体的电压数值就要借助于ADC的帮助,它可以将一个范围内的电压精确的读取出来。假设我们的GPIO口只要高于1.7V的都认为是高电平,例如,比如某个IO口上外接了一个设备它能提供0-2V的电压变化,我们在这个IO口上使用GPIO模式去读取的话我们只能获得0和1两个数据,但是我们使用ADC模式去读取就可以获得0-2V之间连续变化的数值。
ADC有几个比较重要的参数:
测量范围:测量范围对于ADC来说就好比尺子的量程,ADC测量范围决定了你外接的设备其信号输出电压范围,不能超过ADC的测量范围。如果所使用的外部传感器输出的电压信号范围和所使用的ADC测量范围不符合,那么就需要自行设计相关电压转换电路。
分辨率:就是尺子上的能量出来的最小测量刻度,例如我们常用的厘米尺它的最小刻度就是1毫米,表示最小测量精度就是1毫米。假如ADC的测量范围为0-5V,分辨率设置为12位,那么我们能测出来的最小电压就是5V除以2的12次方,也就是5/4096=0.00122V。很明显,分辨率越高,采集到的信号越精确,所以分辨率是衡量ADC的一个重要指标。
精度:是影响结果准确度的因素之一,比如在厘米尺上我们能测量出大概多少毫米的尺度但是毫米后一点点我们却不能准确的量出。经过计算我们ADC在12位分辨率下的最小测量值是0.00122V但是我们ADC的精度最高只能到11位也就是0.00244V。也就是ADC测量出0.00244V的结果是要比0.00122V要可靠,也更准确。
采样时间:当ADC在某时刻采集外部电压信号的时候,此时外部的信号应该保持不变,但实际上外部的信号是不停变化的。所以在ADC内部有一个保持电路,保持某一时刻的外部信号,这样ADC就可以稳定采集了,保持这个信号的时间就是采样时间。
采样率:也就是在一秒的时间内采集多少次。很明显,采样率越高越好,当采样率不够的时候可能会丢失部分信息,所以ADC采样率是衡量ADC性能的另一个重要指标
总之,只要是需要模拟信号转为数字信号的场合,那么肯定要用到ADC。很多数字传感器内部会集成ADC,传感器内部使用ADC来处理原始的模拟信号,最终给用户输出数字信号。
38.1.2 RK3568 ADC简介
ATK-DLRK3568开发板上有两种AD接口,分别是温度传感器(TSADC)和逐次逼近ADC(SARADC)。TSADC具有高达50KS/s的采样率,支持两通道,温度范围在-20℃~120℃和5℃温度分辨率。SARADC具有高达1MS/s的采样率,支持八通道单端10位,时针频率要小于13MHZ。这里就简单的介绍一下ADC的特征,有关RK3568 ADC更多详细的介绍,请参考开发板光盘A盘-基础资料\08、RK官方文档。
38.2 ADC驱动源码分析
38.2.1 设备树下的ADC节点
Linux SDK里的内核采用I/O子系统来控制ADC,该子系统主要是为了方便AD/DA转换来设计的。下面的实验以SARADC来实现ADC的基本配置和实战。rk3568.dtsi文件中定义了SARADC的节点信息,如下所示:
1 saradc: saradc@fe720000 {
2 compatible = "rockchip,rk3568-saradc", "rockchip,rk3399-saradc";
3 reg = <0x0 0xfe720000 0x0 0x100>;
4 interrupts = <GIC_SPI 93 IRQ_TYPE_LEVEL_HIGH>;
5 #io-channel-cells = <1>;
6 clocks = <&cru CLK_SARADC>, <&cru PCLK_SARADC>;
7 clock-names = "saradc", "apb_pclk";
8 resets = <&cru SRST_P_SARADC>;
9 reset-names = "saradc-apb";
10 status = "disabled";
11 };
第2行,compatible属性值为“rockchip,rk3568-saradc”,所以在整个Linux源码里面搜索这个字符串即可找到RK3568的ADC驱动核心文件,这个文件就是drivers/iio/adc/rockchip_saradc.c。
有关RK3568的SARADC节点更为详细的信息请参考对应的绑定文档:Documentation/devicetree/bindings/iio/adc/rockchip-saradc.txt。
38.2.2 ADC驱动源码分析
前面我们也提及到了,通过SARADC的节点信息compatible属性可以找到rockchip-saradc.c文件,文件的主体框架是platform,配合IIO驱动框架实现ADC驱动。
1、rockchip_saradc结构体
此结构体用来描述SARADC模块的数据结构,用于配置和控制SARADC模块的相关参数,可以实现对其的初始化、通道选择、采样率设置等操作。
示例代码38.2.2.1 rockchip_saradc结构体
1 struct rockchip_saradc {
2 void __iomem *regs;
3 struct clk *pclk;
4 struct clk *clk;
5 struct completion completion;
6 struct regulator *vref;
7 int uv_vref;
8 struct reset_control *reset;
9 const struct rockchip_saradc_data *data;
10 u16 last_val;
11 bool suspended;
12 #ifdef CONFIG_ROCKCHIP_SARADC_TEST_CHN
13 struct timer_list timer;
14 bool test;
15 u32 chn;
16 spinlock_t lock;
17 #endif
18 };
2、rockchip_saradc_probe函数
接下来看一下rockchip_saradc_probe函数,内容如下(有省略):
示例代码38.2.2.2 rockchip_saradc_probe函数
1 static int rockchip_saradc_probe(struct platform_device *pdev)
2 {
3 struct rockchip_saradc *info = NULL;
4 struct device_node *np = pdev->dev.of_node;
5 struct iio_dev *indio_dev = NULL;
6 struct resource *mem;
7 const struct of_device_id *match;
8 int ret;
9 int irq;
10
11 if (!np)
12 return -ENODEV;
13
14 indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*info));
15 if (!indio_dev) {
16 dev_err(&pdev->dev, "failed allocating iio device\n");
17 return -ENOMEM;
18 }
19 info = iio_priv(indio_dev);
......
48
49 init_completion(&info->completion);
50
51 irq = platform_get_irq(pdev, 0);
52 if (irq < 0) {
53 dev_err(&pdev->dev, "no irq resource?\n");
54 return irq;
55 }
56
57 ret = devm_request_irq(&pdev->dev, irq, rockchip_saradc_isr,
58 0, dev_name(&pdev->dev), info);
59 if (ret < 0) {
60 dev_err(&pdev->dev, "failed requesting irq %d\n", irq);
61 return ret;
62 }
......
136
137 platform_set_drvdata(pdev, indio_dev);
138
139 indio_dev->name = dev_name(&pdev->dev);
140 indio_dev->dev.parent = &pdev->dev;
141 indio_dev->dev.of_node = pdev->dev.of_node;
142 indio_dev->info = &rockchip_saradc_iio_info;
143 indio_dev->modes = INDIO_DIRECT_MODE;
144
145 indio_dev->channels = info->data->channels;
146 indio_dev->num_channels = info->data->num_channels;
147
148 #ifdef CONFIG_ROCKCHIP_SARADC_TEST_CHN
149 spin_lock_init(&info->lock);
150 timer_setup(&info->timer, rockchip_saradc_timer, 0);
151 ret = sysfs_create_group(&pdev->dev.kobj, &rockchip_saradc_attr_group);
152 if (ret)
153 return ret;
154
155 ret = devm_add_action_or_reset(&pdev->dev,
156 rockchip_saradc_remove_sysgroup, pdev);
157 if (ret) {
158 dev_err(&pdev->dev, "failed to register devm action, %d\n",
159 ret);
160 return ret;
161 }
162 #endif
163 return devm_iio_device_register(&pdev->dev, indio_dev);
164 }
第14行,调用devm_iio_device_alloc函数申请iio_dev,这里连rockchip_saradc内存一起申请了。
第19行,调用iio_priv函数从iio_dev里面得到rockchip_saradc首地址。
第51行,调用platform_get_irq获取中断号。
第57行,调用 devm_request_irq函数申请中断。
第139~146行,初始化iio_dev,重点是第142行的rockchip_saradc_iio_info,因为用户空间读取ADC数据最终就是由rockchip_saradc_iio_info来完成的。
第163行,devm_iio_device_register函数向内核注册iio_dev。
可以看出rockchip_saradc_probe函数核心就是初始化ADC,然后建立ADC的IIO驱动框架。
3、rockchip_saradc_iio_info结构体
rockchip_saradc_iio_info结构体内容如下所示:
示例代码38.2.2.3 rockchip_saradc_iio_info结构体
1 static const struct iio_info rockchip_saradc_iio_info = {
2 .read_raw = rockchip_saradc_read_raw,
3 };
重点来看一下第2行的rockchip_saradc_read_raw函数,因为此函数是最终向用户空间发送ADC原始数据,函数内容如下:
示例代码38.2.2.4 rockchip_saradc_read_raw函数
1 static int rockchip_saradc_read_raw(struct iio_dev *indio_dev,
2 struct iio_chan_spec const *chan,
3 int *val, int *val2, long mask)
4 {
5 struct rockchip_saradc *info = iio_priv(indio_dev);
6
7 #ifdef CONFIG_ROCKCHIP_SARADC_TEST_CHN
8 if (info->test)
9 return 0;
10#endif
11 switch (mask) {
12 case IIO_CHAN_INFO_RAW:
13 mutex_lock(&indio_dev->mlock);
14
15 if (info->suspended) {
16 mutex_unlock(&indio_dev->mlock);
17 return -EBUSY;
18 }
19
20 reinit_completion(&info->completion);
21
22 /* 8 clock periods as delay between power up and start cmd */
23 writel_relaxed(8, info->regs + SARADC_DLY_PU_SOC);
24
25 /* Select the channel to be used and trigger conversion */
26 writel(SARADC_CTRL_POWER_CTRL
27 | (chan->channel & SARADC_CTRL_CHN_MASK)
28 | SARADC_CTRL_IRQ_ENABLE,
29 info->regs + SARADC_CTRL);
30
31 if (!wait_for_completion_timeout(&info->completion,
32 SARADC_TIMEOUT)) {
33 writel_relaxed(0, info->regs + SARADC_CTRL);
34 mutex_unlock(&indio_dev->mlock);
35 return -ETIMEDOUT;
36 }
37
38 *val = info->last_val;
39 mutex_unlock(&indio_dev->mlock);
40 return IIO_VAL_INT;
41 case IIO_CHAN_INFO_SCALE:
42 /* It is a dummy regulator */
43 if (info->uv_vref < 0)
44 return info->uv_vref;
45
46 *val = info->uv_vref / 1000;
47 *val2 = info->data->num_bits;
48 return IIO_VAL_FRACTIONAL_LOG2;
49 default:
50 return -EINVAL;
51 }
52}
第12~36行,主要是读取ADC原始数据值。writel_relaxed函数设置power up开始采样的间距为8哥sclk周期。writel函数配置采样通道、使用中断、开启转换,最后等待转换完成中断发生。
第41~48行,主要是获取ADC对应的分辨率。
关于ADC驱动源码就讲解到这里,接下来我们学习如何使能ADC,然后编写应用程序读取ADC采集到的值。
38.3 硬件原理图分析
ATKDLRK3568开发板ADC硬件原理图如图38.3.1所示:
图38.3.1 ADC原理图
本章实验我们使用ATK-DLRK3568的ADC3接口来采集VR可调电位器的电压,注意:ADC的采集电压绝对值最大是1.8V,请不要超过1.8V,否则可能对芯片造成损坏。如图38.3.2所示:
图38.3.2 开发板ADC位置
38.4 ADC驱动编写
38.4.1 修改设备树
ADC驱动RK官方已经编写好了,我们只需要修改设备树即可。在rk3568-evb.dtsi文件中使能saradc节点即可。
示例代码38.4.1.1 PA5引脚配置信息
1 &saradc {
2 status = "okay";
3 vref-supply = <&vcca_1v8>;
4 };
第3行,saradc值对应的参考电压,需要根据具体的硬件环境设置,最大为1.8V,对应的saradc值为1024,电压和adc值成线性关系。
38.4.2 使能ADC驱动
RK官方默认已经使能了ADC驱动,所以不需要我们修改,但是为了学习,我们还是学习一下如何使能Linux内核自带的ADC驱动。打开Linux内核配置界面,配置路径如下:
-> Device Drivers
-> Industrial I/O support (IIO [=y])
-> Analog to digital converters
-> <*>Rockchip SARADC driver //使能ADC
如图38.4.2.1所示:
图38.4.2.1 ADC配置项
38.4.3 编写测试APP
原子的出厂系统默认已经适配好了,进入/sys/bus/iio/devices目录下,此目录下就有ADC对应的IIO设备:iio:deviceX,本章例程如图38.4.3.1所示:
图38.4.3.1 ADC iio设备
图38.4.3.1中的“iio:device0”就是ADC设备,因此此时并没有加载其他的IIO设备驱动,只有一个ADC。如果大家还加载了其他IIO设备驱动,那么就要依次进入iio设备目录,查看一下都对应的是什么设备。
进入“iio:device0”目录,内容如图38.4.3.2所示:
图38.4.3.2 iio:device0目录文件
标准的IIO设备文件目录,我们只关心两个文件:
in_voltage3_raw:ADC3原始值文件。
in_voltage_scale:ADC比例文件(分辨率),单位为mV。
实际电压值(mV)= in_voltage3_raw * in_voltage_scale。
我们的开发板此时的in_voltage3_raw和in_voltage_scale这两个文件内容如下:
图38.4.3.3 当前电压
经过计算,图38.4.3.3中实际电压:1.757812500*326≈573.046875,也就是0.57V左右。
本实验对应的例程路径为:开发板光盘1、程序源码3、Linux驱动例程25_adc。
接下来就是编写测试APP,新建adcApp.c文件,然后在里面输入如下所示内容:
示例代码38.4.3.4 adcApp.c文件内容
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : adcApp.c
作者 : 正点原子Linux团队
版本 : V1.0
描述 : adc测试应用文件。
其他 : 无
使用方法 :./adcApp
论坛 : www.openedv.com
日志 : 初版V1.0 2023/08/15 正点原子Linux团队创建
***************************************************************/
1 #include "stdio.h"
2 #include "unistd.h"
3 #include "sys/types.h"
4 #include "sys/stat.h"
5 #include "sys/ioctl.h"
6 #include "fcntl.h"
7 #include "stdlib.h"
8 #include "string.h"
9 #include <poll.h>
10 #include <sys/select.h>
11 #include <sys/time.h>
12 #include <signal.h>
13 #include <fcntl.h>
14 #include <errno.h>
15
16 /* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
17 #define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\
18 ret = file_data_read(file_path[index], str);\
19 dev->member = atof(str);\
20
21 /* 字符串转数字,将整数字符串转换为整数数值 */
22 #define SENSOR_INT_DATA_GET(ret, index, str, member)\
23 ret = file_data_read(file_path[index], str);\
24 dev->member = atoi(str);\
25
26
27 /* adc iio框架对应的文件路径 */
28 static char *file_path[] = {
29 "/sys/bus/iio/devices/iio:device0/in_voltage_scale",
30 "/sys/bus/iio/devices/iio:device0/in_voltage3_raw",
31 };
32
33 /* 文件路径索引,要和file_path里面的文件顺序对应 */
34 enum path_index {
35 IN_VOLTAGE_SCALE = 0,
36 IN_VOLTAGE3_RAW,
37 };
38
39 /*
40 * adc数据设备结构体
41 */
42 struct adc_dev{
43 int raw;
44 float scale;
45 float act;
46 };
47
48 struct adc_dev adc;
49
50 /*
51 * @description : 读取指定文件内容
52 * @param - filename : 要读取的文件路径
53 * @param - str : 读取到的文件字符串
54 * @return : 0 成功;其他 失败
55 */
56 static int file_data_read(char *filename, char *str)
57 {
58 int ret = 0;
59 FILE *data_stream;
60
61 data_stream = fopen(filename, "r"); /* 只读打开 */
62 if(data_stream == NULL) {
63 printf("can't open file %s\r\n", filename);
64 return -1;
65 }
66
67 ret = fscanf(data_stream, "%s", str);
68 if(!ret) {
69 printf("file read error!\r\n");
70 } else if(ret == EOF) {
71 /* 读到文件末尾的话将文件指针重新调整到文件头 */
72 fseek(data_stream, 0, SEEK_SET);
73 }
74 fclose(data_stream); /* 关闭文件 */
75 return 0;
76 }
77
78 /*
79 * @description : 获取adc数据
80 * @param - dev : 设备结构体
81 * @return : 0 成功;其他 失败
82 */
83 static int sensor_read(struct adc_dev *dev)
84 {
85 int ret = 0;
86 char str[50];
87
88 SENSOR_FLOAT_DATA_GET(ret, IN_VOLTAGE_SCALE, str, scale);
89 SENSOR_INT_DATA_GET(ret, IN_VOLTAGE3_RAW, str, raw);
90
91 /* 转换得到实际的电压值 */
92 dev->act = (dev->scale * dev->raw)/1000.f;
93 return ret;
94 }
95
96 /*
97 * @description : main主程序
98 * @param - argc : argv数组元素个数
99 * @param - argv : 具体参数
100 * @return : 0 成功;其他 失败
101 */
102 int main(int argc, char *argv[])
103 {
104 int ret = 0;
105
106 if (argc != 1) {
107 printf("Error Usage!\r\n");
108 return -1;
109 }
110
111 while (1) {
112 ret = sensor_read(&adc);
113 if(ret == 0) { /* 数据读取成功 */
114 printf("\r\n原始值:\r\n");
115 printf("ADC原始值:%d,电压值:%.3fV\r\n", adc.raw, adc.act);
116
117 }
118 usleep(100000); /*100ms */
119 }
120 return 0;
121 }
adcApp.c就是在上一章的应用程序上修改而来的,由于只读取一路ADC,因此内容反而更简单,这里就不做介绍了。
38.5 运行测试
38.5.1 编译驱动程序和测试APP
由于不需要我们编写ADC驱动程序,因此也就不需要编译驱动程序。设备树前面已经编译过了,所以这里就只剩下编译测试APP。由于adcApp.c用到了浮点运算,因此我们编译的时候要使能硬件浮点,输入如下编译adcApp.c这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc adcApp.c -o adcApp
编译成功以后就会生成adcApp这个应用程序。
38.5.2 运行测试
输入如下命令,使用adcApp测试程序:
./adcApp
测试APP会不断的读取ADC值并输出到终端,我们可以通过调节开发板上的电位器来改变电压值,如图38.5.2.1所示:
图38.5.2.1 测试结果
从图38.5.2.1可以看到ADC原始值以及对应的电压值,因为ATK-DLRK3568的ADC可采集电压范围为01.8V,因此当我们扭动开发板上的电位器的时候,电压会在01.8V之间变化。