STM32MP157驱动开发——Linux 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

中间的参数为使能浮点运算。

测试结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45682654/article/details/128591539