Linux驱动-ADC篇-ADC基本知识点
文章目录
- 前言
- 一、初识ADC
- ADC
- ADC 分辨率
- RK3568开发板ADC接口了解
- SARADC TSADC 区别和联系
- 按键ADC外设
- 板载剩余ADC 说明
- SARADC工作原理
- 二、操作ADC
- 通过sysfs接口操作ADC
- 通过C系统程序编程操作ADC
- 三、ADC驱动程序-编写
- ADC涉及到的函数
- iio_channel_get()函数
- iio_read_channel_raw()函数
- ADC编写设备树
- ADC驱动程序编写
- 驱动程序编写
- 应用程序的编写
- 测试验证
- 总结
前言
先搞清楚 ADC 要学习什么
一、初识ADC
ADC
ADC 是 Analog-to-Digital Converter 的缩写, 可以翻译为模数转换器, 它是一种电子设备或电路模块, 用于将连续变化的模拟信号转换为离散的数字信号。
模拟信号是一种连续变化的信号, 其值可以在一定范围内取任意值, 它在时间上和幅度上都是连续的, 能够精确地反映自然界中的物理量变化。 而数字信号是一种离散的信号, 其值只能在有限的范围内取特定的数值, 通常以二进制形式表示, 它在时间上和幅度上都是离散的
ADC 应用领域
- (1) 传感器信号采集: 温度传感器、 压力传感器、 光敏传感器等采集到的环境数据通常以模拟信号形式存在。
- (2) 音频处理: 麦克风拾取的声音信号是模拟信号。
- (3) 图像处理: 摄像头捕捉的光信号本质上是模拟信号。
- (4) 工业自动化: 各种物理量(如速度、 位置、 压力等) 的测量通常依赖于传感器输出的模拟信号。
-(5) 医疗设备: 许多生物信号(如心电图、 脑电图等) 是以模拟信号形式采集的。
ADC 分辨率
ADC 分辨率指的是模数转换器能够区分的最小信号变化的能力
ADC 的分辨率通常用位数来表示, 位数决定了 ADC 输出的离散数值数量, n 位 ADC 可以表示 2^n 个不同的数值。 例如, 8 位 ADC 可以表示 2^8=256 个不同的数值, 位数越高, 分辨率越高, 能够区分的信号变化越小
分辨率可以通过以下公式计算:
- (1) 量程是指 ADC 输入信号的最大范围;
- (2) n 是 ADC 的位数;
- (3) 2^n 表示 ADC 能够区分的离散数值总数。
假设一个 3 位 ADC 的参考电压(量程) 为 2V, 计算他的分辨率是多少?
- (1) 题干 1: 量程= 2V
- (2) 题干 2: 位数为 3, 2^n = 2^3 = 8;
代入分辨率计算公式可得 2V/8= 0.25V。 这意味着该 ADC 能够将输入信号分成 8 个离散的等级, 每个等级之间的步长为 0.25V。
除了分辨率之外, ADC 还有另外几个重要的参数
RK3568开发板ADC接口了解
打开文件Rockchip RK3568 Datasheet V1.0-20201210.pdf,框图如下:
也就是说 这个RK3568 使用手册上面 描述的是8路SARADC,实际应该还是6路SARADC+2路TS-ADC。
SARADC TSADC 区别和联系
| 特性 | SARADC | TSADC |
|---|---|---|
| 全称 | Successive Approximation Register ADC(逐次逼近型 ADC) Temperature Sensor ADC(温度传感器 ADC) | |
| 核心用途 | 通用模拟信号采集。用于测量外部电压(如电位器、光敏电阻、传感器输出的模拟电压)。 | 芯片内部测温。用于监控 SoC 自身的温度,防止过热烧毁。 |
| 分辨率 | 10 bit(RK3568 典型值) | 温度分辨率 5°C(通常输出 10 位或 12 位数字量对应温度) |
| 输入范围 | 0 ~ 1.8V(外部引脚输入) | -20°C ~ 120°C(内部 PN 结温度) |
| 通道数 | 8 路(8 个外部引脚,可接 8 个不同传感器) | 双路(通常监控芯片内部两个不同位置的温度) |
| 采样率 | 最高 1MS/s(每秒 100 万次) | 最高 50KS/s(每秒 5 万次) |
| 驱动子系统 | IIO(Industrial I/O)子系统 | Thermal(热管理)子系统 |
| sysfs 路径 | /sys/bus/iio/devices/iio:deviceX/ | /sys/class/thermal/thermal_zoneX/ |
按键ADC外设
由于 TS-ADC 只负责检测 CPU 和 GPU 的温度所以处理器并未引出, 因此接下来介绍在iTOP-RK3568 开发板上使用 SARADC 的外设, 首先是 ADC 按键, ADC 按键的工作原理是将多个按键连接到同一个 SARADC 引脚上, 并通过不同的分压电阻产生不同的电压, 通过 ADC 测量该电压, 并与预设的电压范围进行比对, 从而判断哪个按键被按下。 按键原理图如下:
板载剩余ADC 说明
板载剩余的 4 路 SARADC 被连接到了 6pin 的座子上, 相应原理图如图
SARADC工作原理
SARADC 全称为逐次逼近型 ADC(Successive Approximation Register ADC) , 是一种常见的模数转换器, 其工作原理通过逐步比较输入模拟信号与内部生成的参考电压, 最终确定一个接近输入信号的数字值
二、操作ADC
通过sysfs接口操作ADC
首先进入:开发板/sys/bus/iio/devices/iio:device0目录下, 目录内容如图
- (1) in_voltageX_raw 属性表示 ADC 某个通道的原始采样值, 其中 X 是通道编号(如 0, 1,
2, …, 7) 。 它以整数形式存储 ADC 转换后的原始采样值。 - (2) in_voltage_scale 属性表示将 ADC 原始采样值转换为实际电压值的缩放因子, 以浮点
数形式表示, 单位可以是毫伏(mV) 或伏特(V) , 具体取决于硬件实现。 将 in_voltageX_raw
属性原始值乘以 in_voltage_scale 属性值, 即可得到对应 ADC 通道的实际电压值。
SARADC 的通道 7 为例进行演示, 对应的硬件为 iTOP-RK3568 开发板底板的 J24 接口的第 5 脚, 相关原理图如图 239.2 所示
使用杜邦线将 ADC 的通道 7 接到 GND 上, 重新使用 cat 命令读取 in_voltage7_raw 属性, 读取结果如图 239.5 所示, 可以看到采集的原始数据为 15, 计算得到实际电压值为 0.02V,趋近于 0V, 因为是把 ADC 连接到了 GND 上, 所以趋近于 0 说明 ADC 采样的值正确
除了 SARADC 之外, 还有用来测量 CPU 和 GPU 温度的 TSADC, 可以在开发板上使用cat /sys/devices/virtual/thermal/thermal_zone0/temp
可以得到现在 GPU 的温度为 43.75 摄氏度
通过C系统程序编程操作ADC
本小节将会通过 C 系统编程来实现获取并打印 SARADC 通道 7 采集到的电压, 核心原理为使用 C 代码通过 sysfs 接口读取 ADC 的值
#include<stdio.h>#include<stdlib.h>#include<math.h>intmain(intargc,char*argv[]){intscale=0;// 用于存储从文件中读取的原始 ADC 值FILE*fp=NULL;// 文件指针, 用于打开和读取文件floatvalue=0.0;// 用于存储计算后的实际电压值// 打开 ADC 原始值文件fp=fopen("/sys/bus/iio/devices/iio:device0/in_voltage7_raw","r");if(fp==NULL){// 检查文件是否成功打开printf("fopen error\n");return-1;}//从文件中读取ADC原始值fscanf(fp,"%d",&scale);// 计算实际电压值// 假设参考电压为 1.8V, ADC 分辨率为 10 位(2^10 = 1024)value=(1.8/1024)*(float)scale;// 输出计算结果, 保留两位小数printf("ADC value is %.2fV\n",value);fclose(fp);return0;// 程序正常结束}scale 是读取到的原始值, 而(1.8/1024)是缩放因子, 缩放因子可以通过计算的方式得到也可以直接获取 in_voltage_scale 的值,最后编译:aarch64-linux-gnu-gcc -o adc adc.c
根据打印结果可以得到 ADC 第 7 通道默认情况下的电压是 1.79V, 然后通过杜邦线与 GND相连, 重新运行该程序
三、ADC驱动程序-编写
ADC涉及到的函数
iio_channel_get()函数
这个函数的作用是在驱动中建立与设备树(DTS)中定义的 IIO 通道的连接。
函数原型:struct iio_channel *iio_channel_get(const char *name, const char *channel_name);
参数说明:
- name: 表示 IIO 设备的名称或 ID。 如果为 NULL, 则表示默认设备。
- channel_name: 表示要获取的通道名称。
返回值:成功时返回一个指向 struct iio_channel 的指针, 表示获取到的 IIO 通道对象。
返回值struct iio_channel结构体用于描述 IIO 通道, 其定义如代码清单 所示:
struct iio_channel{struct iio_device*indio_dev;// 关联的 IIO 设备constchar*channel_name;// 通道名称intchannel_index;// 通道索引bool is_output;// 是否为输出通道};当获取的通道对象不再被需要时, 需要使用 iio_channel_release()函数释放相关资源。
设备树(DTS)的配合
adc_demo:adc_demo{compatible="firefly,rk356x-adc";io-channels=<&saradc 2>;// 获取 saradc 的通道 2// 如果有多个通道:// io-channels = <&saradc 2>, <&saradc 3>;// io-channel-names = "channel2", "channel3";};驱动中调用iio_channel_get(&pdev->dev, NULL)就会自动解析并获取到&saradc 2这个通道。
iio_read_channel_raw()函数
这个函数用于从已获取的通道中读取未经处理的原始 ADC 数值。
参数理解
channel:iio_channel_get()返回的通道指针。- val:输出参数,用于存放读取到的原始值(如 10 位 ADC 的 0~1023)。
intret,raw_val;ret=iio_read_channel_raw(adc->chan,&raw_val);if(ret<0){dev_err(&pdev->dev,"Failed to read raw value: %d\n",ret);returnret;}printk("Raw ADC value: %d\n",raw_val);iio_read_channel_raw()读到的只是原始数字量(比如 983)。要得到真实的电压值(mV),需要结合in_voltage_scale进行换算:实际电压(mV) = raw_val × scale(scale 值可以通过iio_read_channel_processed()自动换算,或者手动读取 sysfs 的in_voltage_scale节点获取)。
ADC编写设备树
路径:arch/arm64/boot/dts/rockchip/rk3568-evb.dtsi,添加如下代码:
adc7:adc-test{compatible="myadc";io-channels=<&saradc 7>;};该节点的重点在 io-channels 属性, io-channels 属性表示要使用 ADC 的第 7 个通道, 然后重新编译内核源码, 编译完成之后烧写到开发板即可。
ADC驱动程序编写
驱动程序编写
注册一个杂项设备,在 dev 目录下生成一个名为 adc 的设备节点, 通过对该节点进行操作可以得到 ADC 通道采集到的电压值
#include<linux/module.h>#include<linux/fs.h>#include<linux/uaccess.h>#include<linux/ioctl.h>#include<linux/platform_device.h>#include<linux/iio/consumer.h>#include<linux/miscdevice.h>#include<linux/of.h>// 添加对设备树的支持#defineCMD_READ_SCALE_IOR('A',0x1,int)// 定义 ioctl 命令staticstruct iio_channel*adc_chan;// IIO 通道指针// 文件操作:ioctl 实现staticlongadc_dev_ioctl(struct file*file,unsignedintcmd,unsignedlongarg){intscale=0;switch(cmd){caseCMD_READ_SCALE:// 从 ADC 通道读取原始值if(iio_read_channel_raw(adc_chan,&scale)<0){pr_err("Failed to read ADC channel raw value\n");return-EIO;}// 将数据复制到用户空间if(copy_to_user((int*)arg,&scale,sizeof(scale))){pr_err("Failed to copy data to user space\n");return-EFAULT;}break;default:pr_err("Invalid ioctl command\n");return-EINVAL;}return0;}// 文件操作结构体staticconststruct file_operations adc_dev_fops={.owner=THIS_MODULE,.unlocked_ioctl=adc_dev_ioctl,};// 杂项设备结构体staticstruct miscdevice adc_dev={.minor=MISC_DYNAMIC_MINOR,// 动态分配次设备号.name="adc",// 设备名称.fops=&adc_dev_fops,// 文件操作接口};// 设备树匹配表staticconststruct of_device_id adc_driver_match[]={{.compatible="myadc"},{}};MODULE_DEVICE_TABLE(of,adc_driver_match);// 平台设备探测函数staticintadc_driver_probe(struct platform_device*pdev){// 获取 IIO 通道adc_chan=iio_channel_get(&pdev->dev,NULL);if(IS_ERR(adc_chan)){pr_err("Failed to get IIO channel\n");returnPTR_ERR(adc_chan);}// 注册杂项设备if(misc_register(&adc_dev)){pr_err("Failed to register misc device\n");iio_channel_release(adc_chan);// 释放 IIO 通道return-ENODEV;}pr_info("ADC driver probed successfully\n");return0;}// 平台设备移除函数staticintadc_driver_remove(struct platform_device*pdev){// 注销杂项设备misc_deregister(&adc_dev);// 释放 IIO 通道iio_channel_release(adc_chan);pr_info("ADC driver removed successfully\n");return0;}// 平台设备驱动结构体staticstruct platform_driver adc_driver={.probe=adc_driver_probe,.remove=adc_driver_remove,.driver={.name="myadc",// 驱动名称.owner=THIS_MODULE,.of_match_table=adc_driver_match,},};// 模块初始化函数staticint__initadc_driver_init(void){intret;// 注册平台设备驱动ret=platform_driver_register(&adc_driver);if(ret){pr_err("Failed to register platform driver\n");returnret;}pr_info("ADC driver module loaded\n");return0;}// 模块退出函数staticvoid__exitadc_driver_exit(void){// 注销平台设备驱动platform_driver_unregister(&adc_driver);pr_info("ADC driver module unloaded\n");}module_init(adc_driver_init);module_exit(adc_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("topeet");首先利用杂项驱动框架注册了一个字符设备(设备节点为/dev/adc), 用户空间可通过该设备节点与驱动交互, 在 probe 函数中通过 iio_channel_get()函数获取 ADC 通道, 并使用 iio_read_channel_raw()函数读取 ADC 原始值, 除此之外定义了一个 ioctl 接口命令 CMD_READ_SCALE,用于在用户空间触发 ADC 数据读取操作, 在 ioctl 实现中调用 IIO API 获取数据并通过 copy_to_user 将结果传递给用户空间, 从而实现简单的 ADC 数据采集功能。
应用程序的编写
编写与驱动程序匹配的应用程序, 该应用程序通过/dev/adc 设备节点读取ADC 值,编写完成的应用程序
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<sys/ioctl.h>#defineCMD_READ_SCALE_IOR('A',1,int)intmain(intargc,char*argv[]){intscale=0;intfd;floatvalue=0.0;// 用于存储计算后的实际电压值// 打开设备文件fd=open("/dev/adc",O_RDWR);if(fd<0){printf("open error\n");return-1;}// 使用 ioctl 读取 scale 值if(ioctl(fd,CMD_READ_SCALE,&scale)<0){printf("ioctl error\n");close(fd);return-1;}value=(1.8/1024)*(float)scale;// 打印读取到的 scale 值printf("val is %lf\n",value);// 关闭设备文件close(fd);return0;}该程序的重点在于 ioctl 函数的使用, 应该程序可以通过 ioctl 系统调用向 ADC 驱动发送自定义命令 CMD_READ_SCALE 触发驱动读取 ADC 数据, 并将结果存储到用户空间变量 scale 中
测试验证
编译驱动,加载到修改好设备树的开发版上
insmod adc.ko驱动成功加载之后会在/dev 目录下生成一个名为 adc 的设备节点如图
然后使用./app 运行测试程序
以看到默认情况下 adc 引脚的电压是 1.8V, 然后通过杜邦线将 ADC 第 7 通道的引脚接到GND 上, 然后重新运行该程序, 运行结果如图
获取到的结果趋近于 0V, 证明测试结果是准确的
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。