STM32MP157驱动开发——Linux ADC驱动
- 0.前言
- 一、ADC 简介
- 1.ADC 简介
- 2.STM32MP157 ADC简介
- 二、ADC 驱动源码解析
- 1.设备树下的 ADC 节点
- 2.ADC 驱动源码分析
- 1)stm32_adc 结构体
- 2)stm32_adc_probe 函数
- 3)stm32_adc_iio_info 结构体
- 三、驱动开发
- 1.修改设备树
- 2.使能 ADC 驱动
- 四、运行测试
0.前言
上一节学习了 Linux 下的 IIO 驱动框架,并使用 IIO 子系统对板载的 icm20608 设备进行了驱动开发。此外,由于传感器采集数据的速率较快,还引入了数据缓冲区以及文件流操作的读写方式。
本节就学习如何使用 STM32MP1 内部的 ADC 设备,也借此巩固一下 IIO 子系统的使用方式。
一、ADC 简介
1.ADC 简介
ADC,Analog to Digital Converter 的缩写,也就是数模转换器,可以将外部的模拟信号转化成数字信号,将一个范围内的电压精确的读取出来。ADC 有几个比较重要的参数:
测量范围:测量范围对于 ADC 来说就好比尺子的量程,ADC 测量范围决定了外接的设备其信号输出电压范围,如果所使用的外部传感器输出的电压信号范围和所使用的 ADC 测量范围不符合,那么就需要自行设计相关电压转换电路。
分辨率:就是尺子上的能量出来的最小测量刻度,假如 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 性能的另一个重要指标。
2.STM32MP157 ADC简介
STM32MP157 有两个 ADC:ADC1 和 ADC2,ADC1 和 ADC2 紧密耦合,可在双重模式下运行(ADC1 为主器件)。每个 ADC 由一个 16 位逐次逼近模数转换器组成,每个 ADC 有 20 个通道,每个通道支持单次、连续、扫描或不连续采样模式。转换结果存储在一个左对齐或右对齐的 32 位数据寄存器中。ADC 主要特性如下:
① 多达 2 个 ADC,可在双重模式下运行
② 可以配置为 16、14、12、10 或 8 位分辨率
③ 自校准
④ 可独立配置各通道采样时间
二、ADC 驱动源码解析
1.设备树下的 ADC 节点
STM32MP157 有 2 个 ADC,因此对应 2 个 ADC 控制器,在设备树里就有 2 个 ADC控制器节点。这 2 个 ADC 的设备树节点内容都是一样的,除了 reg 属性不同。本节使用 PA5 引脚来完成 ADC 驱动开发,该引脚是 ADC1_INP19 通道引脚。stm32mp151.dtsi 文件中的 adc 节点信息如下:
根据 compatible 属性值“st,stm32mp1-adc-core”,可以找到驱动核心文件为 drivers/iio/adc/stm32-adc-core.c。另一个 compatible 属性值“st,stm32mp1-adc”,可以找到 ADC 驱动文件 drivers/iio/adc/stm32-adc.c。ADC 相关的绑定文档为 Documentation/devicetree/bindings/iio/adc/st,stm32-adc.txt。根据该文档中的要求,就可以创建自己的 ADC 节点。
ADC 首先需要创建一个根节点,属性如下:
必要属性:
- compatible:兼容性属性,必须的,可以设置为“st,stm32mp1-adc-core”
- reg:ADC 控制器寄存器信息
- interrupts:中断属性,ADC1 和 ADC2 各对应一个中断信息
- clocks:时钟属性
- clock-names:时钟名字,可选“adc”或“bus”
- interrupt-controller:中断控制器
- vdda-supply:此属性对应 vdda 输入模拟电压句柄
- vref-supply:此属性对应 vref 参考电压句柄
- interrupt-cells:设置为 1
- address-cells:设置为 1
- size-cells:设置为 0
可选属性:
- pinctrl 引脚配置信息
- booster-supply:嵌入式 booster 调节器句柄
- vdd-supply:vdd 输入电压句柄
- st,syscfg:系统配置控制器句柄
- st,max-clk-rate-hz:最大时钟
STM32MP157 有两个 ADC,每个 ADC 对应一个子节点,ADC 子节点相关属性如下:
必要属性:
- compatible:兼容性属性,必须的,可以设置为“st,stm32mp1-adc”
- reg:不同 ADC 控制器寄存器地址偏移信息。
- interrupts:中断线信息,adc@0 为 0,adc@100 为 1。
- st,adc-channels:ADC 通道信息,可以设置 0~19,分别对应 20 个通道
- st,adc-diff-channels:ADC 差分通道信息 (如果使用差分 ADC 功能)
- io-channel-cells:设置为1
可选属性:
- dmas:DMA 通道句柄
- dma-names:dma 名字,必须设置成“rx”
- assigned-resolution-bits:ADC 分辨率,可以设置为 8、10、12、14 或 16
- st,min-sample-time-nsecs:最小采样时间,单位 ns
2.ADC 驱动源码分析
STM32MP157 ADC 驱动文件有两个:stm32-adc-core.c 和 stm32-adc.c。stm32-adc-core.c 是 ADC 核心层,主要用于 ADC 电源等初始化,stm32-adc.c 主体框架是 platform,配合 IIO 驱动框架实现 ADC 驱动 (重点关注此驱动)。
1)stm32_adc 结构体
196 struct stm32_adc {
197 struct stm32_adc_common *common; /* ADC 通用数据 */
198 u32 offset; /* ADC 控制器偏移地址 */
199 const struct stm32_adc_cfg *cfg; /* 配置信息 */
200 struct completion completion; /* 单次转换完成量 */
201 u16 buffer[STM32_ADC_MAX_SQ]; /* 数据缓冲区 */
202 struct clk *clk; /* 时钟 */
203 int irq; /* 中断 */
204 spinlock_t lock; /* 自旋锁 */
206 unsigned int num_conv; /* 扫描转换编号 */
205 unsigned int bufi; /* 数据缓冲区索引 */
207 u32 res; /* 数据分辨率 */
208 u32 trigger_polarity; /* 外部触发优先级 */
209 struct dma_chan *dma_chan; /* dma 通道 */
210 u8 *rx_buf; /* dma 接收缓冲区 */
211 dma_addr_t rx_dma_buf; /* dma 接收缓冲总线地址 */
212 unsigned int rx_buf_sz; /* dma 接收缓冲大小 */
213 u32 difsel; /* 单次结束/差分掩码 */
214 u32 pcsel; /* 预选通道掩码 */
215 u32 smpr_val[2]; /* 采样时间(smpr1 和 smpr2)*/
216 struct stm32_adc_calib cal; /* 校准值 */
217 char chan_name[STM32_ADC_CH_MAX][STM32_ADC_CH_SZ];/* 通道名字 */
218 };
2)stm32_adc_probe 函数
1 static int stm32_adc_probe(struct platform_device *pdev)
2 {
3 struct iio_dev *indio_dev;
4 struct device *dev = &pdev->dev;
5 irqreturn_t (*handler)(int irq, void *p) = NULL;
6 struct stm32_adc *adc;
7 int ret;
8
9 if (!pdev->dev.of_node)
10 return -ENODEV;
11
12 indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*adc));
13 if (!indio_dev)
14 return -ENOMEM;
15
16 adc = iio_priv(indio_dev);
17 adc->common = dev_get_drvdata(pdev->dev.parent);
18 spin_lock_init(&adc->lock);
19 init_completion(&adc->completion);
20 adc->cfg = (const struct stm32_adc_cfg *)of_match_device(dev->driver->of_match_table, dev)->data;
21
22
23 indio_dev->name = dev_name(&pdev->dev);
24 indio_dev->dev.parent = &pdev->dev;
25 indio_dev->dev.of_node = pdev->dev.of_node;
26 indio_dev->info = &stm32_adc_iio_info;
27 indio_dev->modes = INDIO_DIRECT_MODE | INDIO_HARDWARE_TRIGGERED;
28
29 platform_set_drvdata(pdev, adc);
30
31 ret = of_property_read_u32(pdev->dev.of_node, "reg", &adc->offset);
32 if (ret != 0) {
33 dev_err(&pdev->dev, "missing reg property\n");
34 return -EINVAL;
35 }
36
37 adc->irq = platform_get_irq(pdev, 0);
38 if (adc->irq < 0)
39 return adc->irq;
40
41 ret = devm_request_threaded_irq(&pdev->dev, adc->irq, stm32_adc_isr, stm32_adc_threaded_isr, 0, pdev->name, adc);
44 if (ret) {
45 dev_err(&pdev->dev, "failed to request IRQ\n");
46 return ret;
47 }
48
49 adc->clk = devm_clk_get(&pdev->dev, NULL);
50 if (IS_ERR(adc->clk)) {
51 ret = PTR_ERR(adc->clk);
52 if (ret == -ENOENT && !adc->cfg->clk_required) {
53 adc->clk = NULL;
54 } else {
55 dev_err(&pdev->dev, "Can't get clock\n");
56 return ret;
57 }
58 }
59
60 ret = stm32_adc_of_get_resolution(indio_dev);
61 if (ret < 0)
62 return ret;
63
64 ret = stm32_adc_chan_of_init(indio_dev);
65 if (ret < 0)
66 return ret;
67
68 ret = stm32_adc_dma_request(indio_dev);
69 if (ret < 0)
70 return ret;
71
72 if (!adc->dma_chan)
73 handler = &stm32_adc_trigger_handler;
74
75 ret = iio_triggered_buffer_setup(indio_dev, &iio_pollfunc_store_time, handler, &stm32_adc_buffer_setup_ops);
78 if (ret) {
79 dev_err(&pdev->dev, "buffer setup failed\n");
80 goto err_dma_disable;
81 }
82
83 /* Get stm32-adc-core PM online */
84 pm_runtime_get_noresume(dev);
85 pm_runtime_set_active(dev);
86 pm_runtime_set_autosuspend_delay(dev, STM32_ADC_HW_STOP_DELAY_MS);
87 pm_runtime_use_autosuspend(dev);
88 pm_runtime_enable(dev);
89
90 ret = stm32_adc_hw_start(dev);
91 if (ret)
92 goto err_buffer_cleanup;
93
94 ret = iio_device_register(indio_dev);
95 if (ret) {
96 dev_err(&pdev->dev, "iio dev register failed\n");
97 goto err_hw_stop;
98 }
......
123 return ret;
124 }
第 12 行,调用 devm_iio_device_alloc 函数申请 iio_dev,这里也连 stm32_adc 内存一起申请了。
第 16 行,调用 iio_priv 函数从 iio_dev 里面的到 stm32_adc 首地址。
第 23~27 行,初始化 iio_dev,重点是第 26 行的 stm32_adc_iio_info,因为用户空间读取 ADC 数据最终就是由 stm32_adc_iio_info 来完成的。
第 37 行,调用 platform_get_irq 获取中断号。
第 41 行,调用 devm_request_threaded_irq 函数申请中断,这里使用的是中断线程化。
第 60 行,调用 stm32_adc_of_get_resolution 函数获取 ADC 的分辨率。
第 64 行,调用 stm32_adc_chan_of_init 函数初始化 ADC 通道。
第 68 行,调用 stm32_adc_dma_request 函数初始化 DMA。
第 75 行,调用 iio_triggered_buffer_setup 函数设置 IIO 触发缓冲区。
第 90 行,调用 stm32_adc_hw_start 函数开启 ADC。
第 94 行,调用 iio_device_register 函数向内核注册 iio_dev。
可以看出 stm32_adc_probe 函数核心就是初始化 ADC,然后建立 ADC 的 IIO 驱动框架。
3)stm32_adc_iio_info 结构体
1 static const struct iio_info stm32_adc_iio_info = {
2 .read_raw = stm32_adc_read_raw,
3 .validate_trigger = stm32_adc_validate_trigger,
4 .hwfifo_set_watermark = stm32_adc_set_watermark,
5 .update_scan_mode = stm32_adc_update_scan_mode,
6 .debugfs_reg_access = stm32_adc_debugfs_reg_access,
7 .of_xlate = stm32_adc_of_xlate,
8 };
第2行的 stm32_adc_read_raw 函数就是最终向用户空间发送 ADC 原始数据的。
碍于篇幅限制,这里就不在赘述,可以自行查阅相关代码。
三、驱动开发
原理图:
JP2 是一个 3P 的排针,1 脚连接到 STM32MP157 的 DAC 引脚上(PA4),2 脚连接到 ADC 引脚上(PA5),3 脚连接到 VR1 这个可调电位器上。
本节使用 ADC 来采集 VR1 可调电位器的电压,所以将2-3连接。
1.修改设备树
ADC 驱动已经由 ST 编写好,只需要修改设备树即可。
在 stm32mp15-pinctrl.dtsi 中添加 ADC 使用的 PA5 引脚配置信息:
adc1_in19_pins_a: adc1-in19 {
pins {
pinmux = <STM32_PINMUX('A', 5, ANALOG)>;
};
};
然后在 stm32mp157d-atk.dts 文件中向根节点添加 vdd 子节点:
vdd: regulator-vdd {
compatible = "regulator-fixed";
regulator-name = "vdd";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
regulator-boot-on;
};
最后在 stm32mp157d-atk.dts 文件中向 adc 节点追加一些内容:
&adc {
pinctrl-names = "default";
pinctrl-0 = <&adc1_in19_pins_a>;
vdd-supply = <&vdd>;
vdda-supply = <&vdd>;
vref-supply = <&vdd>;
status = "okay";
adc1: adc@0 {
st,adc-channels = <19>;
st,min-sample-time-nsecs = <10000>;
assigned-resolution-bits = <16>;
status = "okay";
};
};
①配置adc引脚
②设置电压属性
③adc1 子节点,st,adc-channels 属性设置 adc 通道为 19,st,min-sample-time-nsecs 属性设置最小采样时间为 10000ns;设置分辨率为16位
2.使能 ADC 驱动
在 Linux 内核的 menuconfig 中,使能自带的 ADC 驱动:
修改完成后,就可以编译出设备树文件和系统镜像文件,启动开发板
四、运行测试
启动开发板后,在 /sys/bus/iio/devices 目录下,会存在 ADC 对应的 iio 设备,在该目录中存在以下文件:
- in_voltage19_raw:ADC1 通道 19 原始值文件
- in_voltage_offset:ADC1 偏移文件
- in_voltage_scale:ADC1 比例文件(分辨率),单位为 mV。实际电压值(mV)=in_voltage19_raw* in_voltage_scale
测试App:
#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); \
/* adc iio 框架对应的文件路径 */
static char *file_path[] = {
"/sys/bus/iio/devices/iio:device0/in_voltage_scale",
"/sys/bus/iio/devices/iio:device0/in_voltage19_raw",
};
/* 文件路径索引,要和 file_path 里面的文件顺序对应 */
enum path_index {
IN_VOLTAGE_SCALE = 0,
IN_VOLTAGE_RAW,
};
/*
* ADC 数据设备结构体
*/
struct adc_dev{
int raw;
float scale;
float act;
};
struct adc_dev stm32adc;
/*
* @description : 读取指定文件内容
* @param – filename : 要读取的文件路径
* @param - str : 读取到的文件字符串
* @return : 0 成功;其他 失败
*/
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;
}
/*
* @description : 获取 ADC 数据
* @param - dev : 设备结构体
* @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;
}
/*
* @description : main 主程序
* @param – argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int ret = 0;
if (argc != 1) {
printf("Error Usage!\r\n");
return -1;
}
while (1) {
ret = adc_read(&stm32adc);
if(ret == 0) { /* 数据读取成功 */
printf("ADC 原始值: %d,电压值: %.3fV\r\n", stm32adc.raw,
stm32adc.act);
}
usleep(100000); /*100ms */
}
return 0;
}
编译测试App:
arm-none-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard adcApp.c -o adcApp
中间的参数为使能浮点运算。
测试结果: