资讯详情

一文搞懂 ADC 裸机和基于 Linux 驱动编写方法

原标题:一文理解 ADC 裸机和基于 Linux 驱动编写方法

作者:一口Linux/土豆居士(本文由作者提交)

前言

在嵌入式开发中,ADC应用频繁,本文主要讲解ADC基于基本原则和如何编写ARM裸机程序及基础Linux驱动程序。

ARM架构:Cortex-A9 Linux内核:3.14

在讲述ADC在此之前,我们需要知道模拟信号和数字信号是什么。

模拟信号

它主要是与离散的数字信号相对的连续信号。模拟信号分布在自然的每个角落,如每天的温度变化,而数字信号是人为抽象的时间不连续的信号。电模拟信号主要是指范围和相位连续的电信号,可以通过模拟电路进行放大、添加、乘坐等操作。

模拟信号是指信息的范围、频率或相位随时间不断变化,如当前广播的声音信号或图像信号。

如下图所示,正弦波从上到下次, 调幅波、 阻尼震荡波, 指数衰减波 。

3252eb2a87e534bf7d83c4dce439e379.png

数字信号

数字信号是指范围的值是离散的,这意味着范围限制在有限的值内。二进制码是一种数字信号。二进制码受噪声影响小,数字电路易于处理,因此得到了广泛的应用。

数字信号:高清数字电视,MP3,JPG,PNG文件等等。

优点:

1. 抗干扰能力强,无噪声积累

在模拟通信中,为了提高信噪比,需要在信号传输过程中及时放大衰减的传输信号,同时放大信号传输过程中不可避免的叠加噪声。

随着传输距离的增加,噪声积累越来越多,导致传输质量严重恶化。

对于数字通信,由于数字信号的振幅值是有限的离散值(通常是两个振幅值),虽然在传输过程中也受到噪声的干扰,但当信噪比恶化到一定程度时,

也就是说,判断再生的方法可以在适当的距离内再生与原始发送端相同的数字信号,无噪声干扰,从而实现长距离高质量的传输。

2. 加密方便

信息传输的安全性和保密性越来越重要,数字通信的加密处理比模拟通信容易得多。以语音信号为例,数字变换后的信号可以通过简单的数字逻辑操作进行加密和解密。

3. 便于存储、处理和交换

数字通信的信号形式与计算机使用的信号一致,是二进制代码,便于与计算机联网,也便于存储、处理和交换数字信号,

通信网络的管理和维护可以自动化和智能化。

4. 设备易于集成和微型

数字通信采用多路复用,不需要大滤波器。设备中的大部分电路都是数字电路,可以通过大规模和超大规模的集成电路实现,体积小,功耗低。

5. 综合数字网和综合业务数字网易于构成

数字交换可以通过程控数字交换设备进行,实现传输与交换的综合。

此外,电话业务和各种非话业务都可以数字化,形成综合业务数字网络。

6. 宽度占用信道频带

一路模拟电话的频带为4kHz带宽,一路数字电话约占64kHz,这就是模拟通信仍然具有活力的主要原因。随着宽频带信道(光缆、数字微波)的广泛使用(一对光缆可以打开数千个电话)和数字信号处理技术的发展(数字电话的数字率可以从64开始kb/s压缩到32kb/s数字电话的带宽问题不再是主要问题,甚至要问题。

常用的数字信号编码不归零(NRZ)编码、 曼彻斯特(Manchester)曼彻斯特的编码和差异(Differential Manchester)编码。

转换数字信号和模拟信号

模拟信号和数字信号可以相互转换:模拟信号通常通过PCM脉码调制(Pulse Code Modulation)将方法量化为数字信号,

也就是说,模拟信号的不同范围对应于不同的二进制值。例如,模拟信号可以用8位编码量化为2^8=256个量级,实用中常采用24位或30位编码;

数字信号通常通过对载波移相(Phase Shift)二进制数字信号用于计算机、计算机局域网和城域网,

目前在计算机广域网中实际传送的则既有二进制数字信号,也有由数字信号转换而得的模拟信号。但是更具应用发展前景的是数字信号。

PCM脉冲编码调制

脉冲编码调制是将连续的模拟信号转换为时间离散,然后在信道中传输数字信号。

脉冲编码调制是先抽样模拟信号,然后量化样值范围, 编码过程。

抽样:

定期扫描模拟信号,将时间上的连续信号变成时间上的离散信号。

抽样后,模拟信号还应包含原始信号中的所有信息,即可以无失真地恢复原始模拟信号。

量化:

即通过抽样获得的瞬时值离散其范围,即用一组规定的电平表示瞬时抽样值,通常用二进制表示。

编码:

用一组二进制码组来表示每个具有固定电平的量化值。但实际上量化是在编码过程中同时完成的,所以编码过程也叫模/数变换,可以记录下来A/D。

ADC

ADC,Analog-to-Digital Converter缩写,指模/数转换器或模数转换器。是指将连续变化的模拟信号转换为离散数字信号的装置。真实世界的模拟信号,如温度、压力、声音或图像,需要转换为更容易存储、处理和发射的数字形式。该功能可以通过模/数转换器实现,并且可以在各种产品中找到。

ADC最早用于将无线信号转换为数字信号。如电视信号、长短播电台接收等。

对应DAC,Digital-to-Analog Converter,它是ADC模数转换的逆向过程。

目前市场上所有的电子产品都集成了传感器传感器需要收集数据,必须在其内部结构中使用ADC,常见传感器如下:

温湿度:温度传感器,DHT11

声音:录制音频芯片,WM8906

图像:索尼IMX386/IMX283传感器 Exynos4412 A/D转换器

三星的Exynos4412模块结构图如下:

Adc集成控制器exynos4412 soc中间,中断线连接到中断控制器combiner,然后路由到GIC(Generic Interrupt Controller),连接滑动变阻器adc控制器通道3。

ADC控制器

参考《Exynos 4412 SCP》 的datasheet。

ADC10或12位控制器CMOS再循环模拟数字转换器有10个通道输入,可将模拟量转换为10位或12位二进制数。Mhz A/D 转换时钟,最大1Msps转换速度。A/D具有片上采样保持功能的转换,也支持待机工作模式。

ADC接口包括以下特性。

10bit/12bit可选输出位。

微分误差 1.0LSB。

积分误差 2.0LSB。

最大转换率5Msps.

功耗低,电压输入1.8V。

电压输入范围 0~1.8V。

支持偏上样本保持功能。

通用转换模式。

模块图

4412 A/D控制器接口框图如下:

我们不需要注意原理,只需要知道。

通道选择

从上图可以看出,A/D控制器有4个通道,通用寄存器地址为0x126c0000。

A/D控制器寄存器

对ADC控制器的操作主要是通过配置寄存器来实现的datasheet,必须掌握使用寄存器。A/D控制器寄存器汇总。

1、A/D控制寄存器ADCCON

RES : 选择A/D转换精度0:分为1024份 1:划分成4096份

ECFLG :转换是否结束 0:转换中 1:转换完成;轮询模式需要根据位置判断数据是否转换。

PRSCEN:A/D能否转换预分频?

PRSCVL:预分频的值,转换公式见下面

STANDBY:待机模式 0:正常工作模式 1.待机模式。在待机模式下PRSCEN设置为0

READ_START: A/D转换由读取操作触发,设置为1后,每次读取A/D值的操作会触发一次A/D转换。

ENABLE_START: 单次开启A/D转换后,该位置将自动清零READ_START当设置为1时,该位置无效。

设置值通常为(1) << 16 | 1 << 14 | 99 <<6 | 1 << 1)。

2、A/D转换数据寄存器ADCDAT0

注意寄存器的值只有低12位有效。

3、A/D清中断寄存器CLRINTADC

可以看出,中断例程负责清中断,中断后写入任何值即可清中断。

4、A/D通道选择寄存器ADCMUX

每次操作都要先设置通道,因为 4个通道是共用同一套寄存器,如果有其他任务也在使用A/D,就会产生混乱。在此我们选择通道3,置3即可。

5、ADC中断ID

参见9.2.2GIC Interrupt Table

由此可知,ADC中断号对应的SPI值是10,inturrupt ID 为42。对于终端查询方式和编写终端的驱动需要知道SPI id和inturrupt ID,后面讲解基于Linux驱动还会再分析设备树节点如何填写。

6、Combiner中断控制器

combiner的配置寄存器:IMSRn、IECRn、ISERn、ISTRn,类似于GPIO 对中断源分组。只有中断模式才需要考虑combiner中断控制器的操作。

7、Combiner分组

参考章节:10.2.1Interrupt Combiner Table 10-1Interrupt Groups of Interrupt Combiner

可见ADC在INTG10,即第10组。

8、Combiner IESR2

参考章节:10.4.2.9IESR2

如果要用中断模式设置为1即可。

9、Combiner IECR2

参考章节:10.4.2.10IECR2

此处用于关闭中断,采用默认值即可,注意,如果设置了1,那么中断功能就关闭了。

10、A/D转换的转换时间计算

例如:PCLK为100MHz,PRESCALER = 65 ;所有10位转换时间为

100MHz/(99+1) = 1MHz

转化时间为1/(1MHz/5 cycles) = 5us。

完成一次A/D转换需要5个时钟周期。A/D转换器的最大工作时钟为5MHz,所以最大采样率可以达到1Mit/s.

电路连接图

由该电路图可知,外设是一个滑动变阻器,根据接触点的不同,会导致输入电压的模拟值不同。连接的A/D控制器通道为3。该电路利用一个电位计输出电压到4412的AIN3管脚。输入的电压范围为0~1.8V。

ADC裸机开发程序实例

ADC数据的读取通常由2种方法:中断模式、轮询模式。

轮询模式

轮询模式读取数据步骤如下:

1.要读取数据首先向ADC寄存器ADCCON的bit:1写1,发送转换命令,采用读-启动模式来开启转换。

2.当ADC控制器转换完毕会将ADCCON的bit:15设置为1,

3.轮询检测ADCCON的bit:15是否设置为1,如果设置为1,就读走数据,否则继续等待。

这种方式比较占用CPU资源。

//注:这里使用读-启动模式

/***********************ADC ******************/

# defineADC_CFG __REG(0x10010118)

# defineADCCON __REG(0x126C0000)

# defineADCDLY __REG(0x126C0008)

# defineADCDAT __REG(0x126C000C)

# defineCLRINTADC __REG(0x126C0018)

# defineADCMUX __REG(0x126C001C)

# include"exynos_4412.h"

# include"pwm.h"

# include"uart.h"

unsignedchartable[ 10] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

voidmydelay_ms( inttime)

{

inti, j;

while(time--)

{

for(i = 0; i < 5; i++)

for(j = 0; j < 514; j++);

}

}

adc_init( inttemp)

{

ADCCON = ( 1<< 16| 1<< 14| 99<< 6| 1<< 1);

ADCMUX = 3;

temp = ADCDAT & 0xfff;

}

/*

* 裸机代码,不同于LINUX 应用层, 一定加循环控制

*/

intmain( void)

{

unsignedcharbit4,bit3,bit2,bit1;

unsignedinttemp = 0;

uart_init;

adc_init(temp);

puts( "开始转换n");

while( 1)

{

while(!(ADCCON & 0x8000));

temp = ADCDAT & 0xfff;

printf( "U = %dn",temp);

temp = 1.8* 1000* temp/ 0xfff;

bit4 = temp / 1000;

putc(table[bit4]);

bit3 = (temp % 1000)/ 100?;

putc(table[bit3]);

bit2 = ((temp % 1000)% 100)/ 10;

putc(table[bit2]);

bit1 = ((temp % 1000)% 100)% 10;

putc(table[bit1]);

puts( "mV");

putc( 'n');

mydelay_ms( 1000);

}

return0;

}

中断模式

中断模式读取数据步骤如下:

1.要读取数据首先向ADC寄存器ADCCON的bit:0写1,发送转换命令;

2.当ADC控制器转换完毕会通过中断线向CPU发送中断信号;

3.在中断处理函数中,读走数据,并清中断.

注:中断对应寄存器的设置,后续会更新对应的文档。

voiddo_irq( void)

{

intirq_num;

irq_num = CPU0.ICCIAR & 0x3ff;

switch(irq_num)

{

case42:

adc_num = ADCDAT& 0xfff;

printf( "adc = %dn",adc_num);

CLRINTADC = 0;

// IECR2 = IECR2 | (1 << 19); 打开的话只能读取一次,

//42/32

ICDICPR.ICDICPR1 = ICDICPR.ICDICPR1 | ( 1<< 10);【清GIC中断标志位类似于 ICDISER】

break;

}

CPU0.ICCEOIR = CPU0.ICCEOIR & (~ 0x3ff) | irq_num;

}

voidadc_init( void)

{ //12bit 使能分频 分频值 手动

ADCCON = ( 1<< 16) | ( 1<< 14) | ( 0xff<< 6) | ( 1<< 0);

ADCMUX = 3;

}

voidadcint_init( void)

{

IESR2 = IESR2 | ( 1<< 19);

ICDDCR = 1; //使能分配器

//42/32

ICDISER.ICDISER1 = ICDISER.ICDISER1 | ( 1<< 10); //使能相应中断到分配器

ICDIPTR.ICDIPTR10 = ICDIPTR.ICDIPTR10 &(~( 0xff<< 16)) | ( 0x1<< 16); //发送到相应CPU接口

CPU0.ICCPMR = 255; //设置中断屏蔽优先级

CPU0.ICCICR = 1; //全局使能开关

}

intmain( void)

{

adc_init;

adcint_init;

while( 1)

{

ADCCON = ADCCON | 1;

delay_ms( 1000);

}

return0;

}

基于Linux驱动编写

设备树

编写基于Linux的ADC外设驱动,首先需要编写设备树节点信息,在裸机程序中,我们只用到了寄存器地址,而编写基于Linux的驱动,我们需要用到中断功能。所以编写设备树节点需要知道ADC要用到的硬件资源主要包括:寄存器资源和中断资源。

关于中断的使用我们在后续文章中会继续分析,现在我们只需要知道中断信息如何填写即可。

ADC寄存器信息填写

由上可知,寄存器基地址为0x126c0000,其他寄存器只需要根据基地址做偏移即可获取,所以设备树的reg属性信息如下:

reg = <0x126C0000 0x20>;

ADC中断信息填写

描述中断连接需要四个属性:

父节点提供以下信息

interrupt-controller - 一个空的属性定义该节点作为一个接收中断信号的设备。

interrupt-cells - 这是一个中断控制器节点的属性。它声明了该中断控制器的

中断指示符中【interrupts】 cell 的个数(类似于 #address-cells 和 #size-cells)。

子节点描述信息

interrupt-parent - 这是一个设备节点的属性,包含一个指向该设备连接的中断控制器的

phandle。那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性。

iterrupts - 一个设备节点属性,包含一个中断指示符的列表,对应于该设备上的

每个中断输出信号。【设备的中断信息放在该属性中】

父节点

首先我们必须知道ADC控制器的中断线的父节点:

由上图可知ADC控制器位于soc内,4个ADC通道公用一根中断线,该中断线连接在combiner上,所以我们需要查找到combiner这个父节点的说明:

进入设备树文件所在目录:archarmbootdts

grep combiner *.* -n

经过筛选得到以下信息:

因为我们使用的板子是exynos4412,而exynos系列通用的平台设备树文件是exynos4.dtsi,查看该文件:

上图列举了combiner控制器的详细信息:

interrupt-cells ;

interrupt-cells =<2>;

所以ADC控制器中断控制器的interrupts属性应该有两个cell。

interrupts属性填写

而设备的中断信息填写方式由内核的以下文档提供:

Documentationdevicetreebindingsinterrupt-controllerinterrupts.txt

69. b) two cells

70. ------------

71. The #interrupt-cells property is set to 2 and the first cell 72. defines the

73. index of the interrupt within the controller, whilethe second cell is used

74. to specify any of the following flags:

75. - bits[3:0] trigger typeand level flags

76. 1 = low-to-high edge triggered

77. 2 = high-to-low edge triggered

78. 4 = active high level-sensitive

79. 8 = active low level-sensitive

由以上信息可知,中断的第一个cell是该中断源所在中断控制器的index,第二个cell表示中断的触发方式

上升沿触发

下降沿触发

高电平触发

低电平触发

那么index应该是多少呢?

详见datasheet的9.2.2 GIC Interrupt Table 节:

此处我们应该是填写左侧的SPI ID:10 还是填写INTERRUPT ID:42呢?

此处我们可以参考LCD节点的interrupts填写方法:

通过查找父节点为combiner的设备信息。

继续grep combiner . -n

由此可见lcd这个设备的interrupts属性index值是11,所以可知ADC控制器中断线的index是10。中断信息如下:

interrupt-parent = ;

interrupts = <10 3>;

ADC外设设备树信息

fs4412-adc{

compatible = "fs4412,adc";

reg = <0x126C0000 0x20>;

interrupt-parent = ;

interrupts = <10 3>;

};

本文默认大家会使用设备树,不知道如何使用设备树的朋友,后续会开一篇单独讲解设备树。

【注意】在不支持设备树内核中,以Cortex-A8为例,中断信息填写在以下文件中

内部中断,Irqs.h (archarmmach-s5pc100includemach)

外部中断在Irqs.h (archarmplat-s5pincludeplat)

ADC属于内部中断,位于archarmmach-s5pc100includemachIrqs.h中。

寄存器信息填写在以下位置:

archarmmach-s5pc100Mach-smdkc100.c

static struct platform_device *smdkc100_devices[] __initdata = {

&s3c_device_adc,

&s3c_device_cfcon,

&s3c_device_i2c0,

&s3c_device_i2c1,

&s3c_device_fb,

&s3c_device_hsmmc0,

&s3c_device_hsmmc1,

&s3c_device_hsmmc2,

&samsung_device_pwm,

&s3c_device_ts,

&s3c_device_wdt,

&smdkc100_lcd_powerdev,

&s5pc100_device_iis0,

&samsung_device_keypad,

&s5pc100_device_ac97,

&s3c_device_rtc,

&s5p_device_fimc0,

&s5p_device_fimc1,

&s5p_device_fimc2,

&s5pc100_device_spdif,

};

结构体s3c_device_adc定义在以下文件:

archarmplat-samsungDevs.c

# ifdefCONFIG_PLAT_S3C24XX

staticstructresources3c_adc_resource[] = {

[ 0] = DEFINE_RES_MEM(S3C24XX_PA_ADC, S3C24XX_SZ_ADC),

[ 1] = DEFINE_RES_IRQ(IRQ_TC),

[ 2] = DEFINE_RES_IRQ(IRQ_ADC),

};

structplatform_devices3c_device_adc= {

.name = "s3c24xx-adc",

.id = -1,

.num_resources = ARRAY_SIZE(s3c_adc_resource),

.resource = s3c_adc_resource,

};

# endif/* CONFIG_PLAT_S3C24XX */

# ifdefined(CONFIG_SAMSUNG_DEV_ADC)

staticstructresources3c_adc_resource[] = {

[ 0] = DEFINE_RES_MEM(SAMSUNG_PA_ADC, SZ_256),

[ 1] = DEFINE_RES_IRQ(IRQ_TC),

[ 2] = DEFINE_RES_IRQ(IRQ_ADC),

};

structplatform_devices3c_device_adc= {

.name = "samsung-adc",

.id = -1,

.num_resources = ARRAY_SIZE(s3c_adc_resource),

.resource = s3c_adc_resource,

};

# endif/* CONFIG_SAMSUNG_DEV_ADC */

由代码可知,平台驱动对应的platform_device具体内容由宏CONFIG_PLAT_S3C24XX、CONFIG_SAMSUNG_DEV_ADC来控制。

驱动编写架构和流程如下

read

{

1、向adc设备发送要读取的命令

ADCCON 1<<0 | 1<<14 | 0X1<<16 | 0XFF<<6

2、读取不到数据就休眠

wait_event_interruptible;

3、等待被唤醒读数据

havedata = 0;

}

adc_handler

{

1、清中断 ADC使用中断来通知转换数据完毕的

2、状态位置位;

havedata=1;

3、唤醒阻塞进程

wake_up

}

probe

{

1、读取中断号,注册中断处理函数

2、读取寄存器的地址,ioremap

3、字符设备的操作

}

驱动需要首先捕获中断信号后再去寄存器读取相应的数据,在ADC控制器没有准备好数据之前,应用层需要阻塞读取数据,所以在读取数据的函数中,需要借助等待队列来实现驱动对应用进程的阻塞。驱动程序

驱动程序对寄存器的操作参考裸机程序,只是基地址需要通过ioremap做映射,对寄存器的读写操作需要用readl、writel。

driver.c

# include

# include

# include

# include

# include

# include

# include

# include

# include

staticintmajor = 250;

staticwait_queue_head_twq;

staticinthave_data = 0;

staticintadc;

staticstructresource* res1;

staticstructresource* res2;

staticvoid*adc_base;

# defineADCCON 0x0000

# defineADCDLY 0x0008

# defineADCDAT 0x000C

# defineCLRINTADC 0x0018

# defineADCMUX 0x001C

staticirqreturn_t adc_handler( intirqno, void*dev)

{

have_data = 1;

printk( "11111n");

/*清中断*/

writel( 0x12,adc_base + CLRINTADC);

wake_up_interruptible(&wq);

returnIRQ_HANDLED;

}

staticintadc_open(struct inode *inod, struct file *filep)

{

return0;

}

staticssize_t adc_read(struct file *filep, char__user *buf, size_tlen, loff_t*pos)

{

writel( 0x3,adc_base + ADCMUX);

writel( 1<< 0| 1<< 14| 0X1<< 16| 0XFF<< 6,adc_base +ADCCON );

wait_event_interruptible(wq, have_data== 1);

/*read data*/

adc = readl(adc_base+ADCDAT)& 0xfff;

if(copy_to_user(buf,&adc, sizeof( int)))

{

return-EFAULT;

}

have_data = 0;

returnlen;

}

staticintadc_release(struct inode *inode, struct file *filep)

{

return0;

}

staticstructfile_operationsadc_ops=

{

.open = adc_open,

.release = adc_release,

.read = adc_read,

};

staticinthello_probe(struct platform_device *pdev)

{

intret;

printk( "match 0k n");

res1 = platform_get_resource(pdev,IORESOURCE_IRQ, 0);

res2 = platform_get_resource(pdev,IORESOURCE_MEM, 0);

ret = request_irq(res1->start,adc_handler,IRQF_DISABLED, "adc1", NULL);

adc_base = ioremap(res2->start,res2->end-res2->start);

register_chrdev( major, "adc", &adc_ops);

init_waitqueue_head(&wq);

return0;

}

staticinthello_remove(struct platform_device *pdev)

{

free_irq(res1->start, NULL);

free_irq(res2->start, NULL);

unregister_chrdev( major, "adc");

return0;

}

staticstructof_device_idadc_id[]=

{

{.compatible = "fs4412,adc"},

};

staticstructplatform_driverhello_driver=

{

.probe = hello_probe,

.remove = hello_remove,

.driver ={

.name = "bigbang",

.of_match_table = adc_id,

},

};

staticinthello_init( void)

{

printk( "hello_init");

returnplatform_driver_register(&hello_driver);

}

staticvoidhello_exit( void)

{

platform_driver_unregister(&hello_driver);

printk( "hello_exit n");

return;

}

MODULE_LICENSE( "GPL");

module_init(hello_init);

module_exit(hello_exit);

测试程序

test.c

# include

# include

# include

# include

main

{

intfd,len;

intadc;

fd = open( "/dev/hello",O_RDWR);

if(fd< 0)

{

perror( "open fail n");

return;

}

while( 1)

{

read(fd,&adc, 4);

printf( "adc%0.2f V n",( 1.8*adc)/ 4096);

}

close(fd);

责任编辑:

标签: 1612s3tc接近传感器0931s3pc接近传感器1545s3pc接近传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台