基于ARM的裸机程序和基于Linux的驱动程序

前言
在嵌入式开发中,adc应用比较频繁,本文主要讲解adc的基本原理以及如何编写基于arm的裸机程序和基于linux的驱动程序。
arm架构:cortex-a9linux内核:3.14
在讲述adc之前,我们需要先了解什么是模拟信号和数字信号。
模拟信号
主要是与离散的数字信号相对的连续的信号。模拟信号分布于自然界的各个角落,如每天温度的变化,而数字信号是人为的抽象出来的在时间上不连续的信号。电学上的模拟信号是主要是指幅度和相位都连续的电信号,此信号可以被模拟电路进行各种运算,如放大,相加,相乘等。
模拟信号是指用连续变化的物理量表示的信息,其信号的幅度,或频率,或相位随时间作连续变化,如目前广播的声音信号,或图像信号等。
如下图所示从上到下一次是正弦波、 调幅波、 阻尼震荡波、 指数衰减波 。
数字信号
数字信号指幅度的取值是离散的,幅值表示被限制在有限个数值之内。二进制码就是一种数字信号。二进制码受噪声的影响小,易于有数字电路进行处理,所以得到了广泛的应用。
数字信号:高清数字电视,mp3,jpg,png文件等等。
优点:
1. 抗干扰能力强、无噪声积累
在模拟通信中,为了提高信噪比,需要在信号传输过程中及时对衰减的传输信号进行放大,信号在传输过程中不可避免地叠加上的噪声也被同时放大。
随着传输距离的增加,噪声累积越来越多,以致使传输质量严重恶化。
对于数字通信,由于数字信号的幅值为有限个离散值(通常取两个幅值),在传输过程中虽然也受到噪声的干扰,但当信噪比恶化到一定程度时,
即在适当的距离采用判决再生的方法,再生成没有噪声干扰的和原发送端一样的数字信号,所以可实现长距离高质量的传输。
2. 便于加密处理
信息传输的安全性和保密性越来越重要,数字通信的加密处理的比模拟通信容易得多,以话音信号为例,经过数字变换后的信号可用简单的数字逻辑运算进行加密、解密处理。
3. 便于存储、处理和交换
数字通信的信号形式和计算机所用信号一致,都是二进制代码,因此便于与计算机联网,也便于用计算机对数字信号进行存储、处理和交换,
可使通信网的管理、维护实现自动化、智能化。
4. 设备便于集成化、微型
数字通信采用时分多路复用,不需要体积较大的滤波器。设备中大部分电路是数字电路,可用大规模和超大规模集成电路实现,因此体积小、功耗低。
5. 便于构成综合数字网和综合业务数字网
采用数字传输方式,可以通过程控数字交换设备进行数字交换,以实现传输和交换的综合。
另外,电话业务和各种非话业务都可以实现数字化,构成综合业务数字网。
6. 占用信道频带较宽
一路模拟电话的频带为4khz带宽,一路数字电话约占64khz,这是模拟通信目前仍有生命力的主要原因。随着宽频带信道(光缆、数字微波)的大量利用(一对光缆可开通几千路电话)以及数字信号处理技术的发展(可将一路数字电话的数码率由64kb/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。adc控制器是10位或12位cmos再循环式模拟数字转换器,它具有10个通道输入,并可将模拟量转换至10位或12位二进制数。5mhz 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 combinertable 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 ******************/#define   adc_cfg  __reg(0x10010118)#define  adccon  __reg(0x126c0000)#define  adcdly  __reg(0x126c0008)#define  adcdat  __reg(0x126c000c)#define  clrintadc __reg(0x126c0018)#define  adcmux  __reg(0x126c001c)#include exynos_4412.h#include pwm.h#include uart.hunsigned char table[10] = {'0','1','2','3','4','5','6','7','8','9'};void mydelay_ms(int time){  int i, j;  while(time--)  {    for (i = 0; i < 5; i++)    for (j = 0; j < 514; j++);  }}adc_init(int temp){  adccon = (1 << 16 | 1 << 14 | 99 <<6 | 1 << 1);  adcmux = 3;  temp = adcdat & 0xfff;}/* *  裸机代码,不同于linux 应用层, 一定加循环控制 */int main (void){  unsigned char bit4,bit3,bit2,bit1;  unsigned int temp = 0;    uart_init();  adc_init(temp);  puts(开始转换);  while(1)  {    while(!(adccon & 0x8000));    temp = adcdat & 0xfff;    printf(u = %d,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('');    mydelay_ms(1000);  }  return 0;} 中断模式
中断模式读取数据步骤如下:
1.要读取数据首先向adc寄存器adccon的bit:0写1,发送转换命令;
2.当adc控制器转换完毕会通过中断线向cpu发送中断信号;
3.在中断处理函数中,读走数据,并清中断.
注:中断对应寄存器的设置,后续会更新对应的文档。
void do_irq(void)
{       int irq_num;       irq_num = cpu0.icciar &0x3ff;       switch(irq_num)       {         case 42:              adc_num = adcdat&0xfff;              printf(adc = %d,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;}void adc_init(void){    //12bit   使能分频       分频值                 手动       adccon = (1 << 16) | (1 << 14) | (0xff << 6) | (1 << 0);       adcmux = 3;}void adcint_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;  //全局使能开关}int main (void){   adc_init();       adcint_init();       while(1)       {              adccon = adccon | 1;              delay_ms(1000);       }   return 0;} 基于linux驱动编写
设备树
编写基于linux的adc外设驱动,首先需要编写设备树节点信息,在裸机程序中,我们只用到了寄存器地址,而编写基于linux的驱动,我们需要用到中断功能。所以编写设备树节点需要知道adc要用到的硬件资源主要包括:寄存器资源和中断资源。
关于中断的使用我们在后续文章中会继续分析,现在我们只需要知道中断信息如何填写即可。
adc寄存器信息填写
在这里插入图片描述
由上可知,寄存器基地址为0x126c0000,其他寄存器只需要根据基地址做偏移即可获取,所以设备树的reg属性信息如下:
reg = ;
adc中断信息填写
描述中断连接需要四个属性:父节点提供以下信息
interrupt-controller - 一个空的属性定义该节点作为一个接收中断信号的设备。interrupt-cells      - 这是一个中断控制器节点的属性。它声明了该中断控制器的中断指示符中【interrupts】 cell 的个数(类似于 #address-cells 和 #size-cells)。 子节点描述信息
interrupt-parent - 这是一个设备节点的属性,包含一个指向该设备连接的中断控制器的 phandle。那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性。interrupts       - 一个设备节点属性,包含一个中断指示符的列表,对应于该设备上的每个中断输出信号。【设备的中断信息放在该属性中】 父节点
首先我们必须知道adc控制器的中断线的父节点:
由上图可知adc控制器位于soc内,4个adc通道公用一根中断线,该中断线连接在combiner上,所以我们需要查找到combiner这个父节点的说明:
进入设备树文件所在目录:archarmootdts
grep combiner *.* -n 经过筛选得到以下信息,
因为我们使用的板子是exynos4412,而exynos系列通用的平台设备树文件是exynos4.dtsi,查看该文件:
上图列举了combiner控制器的详细信息:
interrupt-cells ;
interrupt-cells =;
所以adc控制器中断控制器的interrupts属性应该有两个cell。
interrupts属性填写
而设备的中断信息填写方式由内核的以下文档提供:
documentationdevicetreeindingsinterrupt-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, while the second cell is used 74.  to specify any of the following flags: 75.    - bits[3:0] trigger type and 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表示中断的触发方式
*. 1:上升沿触发*. 2:下降沿触发*. 3:高电平触发*. 4:低电平触发
那么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 = ; adc外设设备树信息
fs4412-adc{    compatible = fs4412,adc;    reg = ;    interrupt-parent = ;    interrupts = ;}; 本文默认大家会使用设备树,不知道如何使用设备树的朋友,后续会开一篇单独讲解设备树。【注意】在不支持设备树内核中,以cortex-a8为例,中断信息填写在以下文件中
内部中断,irqs.h (archarmmach-s5pc100includemach)外部中断在irqs.h (archarmplat-s5pincludeplat) adc属于内部中断,位于archarmmach-s5pc100includemachirqs.h中。
寄存器信息填写在以下位置:
archarmmach-s5pc100mach-smdkc100.cstatic 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#ifdef config_plat_s3c24xxstatic struct resource s3c_adc_resource[] = {  [0] = define_res_mem(s3c24xx_pa_adc, s3c24xx_sz_adc),  [1] = define_res_irq(irq_tc),  [2] = define_res_irq(irq_adc),};struct platform_device s3c_device_adc = {  .name    = s3c24xx-adc,  .id    = -1,  .num_resources  = array_size(s3c_adc_resource),  .resource  = s3c_adc_resource,};#endif /* config_plat_s3c24xx */#if defined(config_samsung_dev_adc)static struct resource s3c_adc_resource[] = {  [0] = define_res_mem(samsung_pa_adc, sz_256),  [1] = define_res_irq(irq_tc),  [2] = define_res_irq(irq_adc),};struct platform_device s3c_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 static int major = 250;  static wait_queue_head_t wq;static int have_data = 0;static int adc;static struct resource *res1;static struct resource *res2;static void *adc_base; #define adccon 0x0000#define adcdly 0x0008#define adcdat 0x000c#define clrintadc 0x0018#define adcmux 0x001c  static  irqreturn_t adc_handler(int irqno, void *dev){  have_data = 1;   printk(11111);  /*清中断*/  writel(0x12,adc_base + clrintadc);  wake_up_interruptible(&wq);  return irq_handled;}static int adc_open (struct inode *inod, struct file *filep){   return 0;}static ssize_t adc_read(struct file *filep, char __user *buf, size_t len, loff_t *pos){    writel(0x3,adc_base + adcmux);  writel(1<<0 | 1<<14 | 0x1<<16 | 0xffstart,res2->end-res2->start);   register_chrdev( major, adc, &adc_ops);  init_waitqueue_head(&wq);    return 0;}static int hello_remove(struct platform_device *pdev){  free_irq(res1->start,null);  free_irq(res2->start,null);    unregister_chrdev( major, adc);  return 0;} static struct of_device_id adc_id[]={  {.compatible = fs4412,adc },}; static struct platform_driver hello_driver={    .probe = hello_probe,  .remove = hello_remove,  .driver ={    .name = bigbang,    .of_match_table = adc_id,  },}; static int hello_init(void){  printk(hello_init);  return platform_driver_register(&hello_driver);}static void hello_exit(void){  platform_driver_unregister(&hello_driver);  printk(hello_exit );  return;}module_license(gpl);module_init(hello_init);module_exit(hello_exit); 测试程序
test.c


PROFINET自动化通信技术在工业自动化领域的应用研究
关于排线线束,它的自身特性是什么
马云新型餐饮模式开启,AR无人餐厅将引燃“云栖大会”
三星超越诺基亚成全球手机老大
直击现场 | 西井科技孙作雷博士受邀参与“顶级科技集群”高级别圆桌会议
基于ARM的裸机程序和基于Linux的驱动程序
世界杯火爆进行,中科极光真激光电视解锁缤纷观赛季
关于Wi-Fi AP存在的干扰问题分析
5G 将如何改变路测的未来
燃料电池质子交换膜制备工艺
HPE推出新一代安全性能强劲的ProLiant服务器
BM1582百力微35V-2A恒压转换器
维修电路板要用到哪些工具
Northvolt公司计划在欧洲市场大规模生产环保电池
高亮度红光Micro LED实现量产
iPhone XR 价格太贵翻车,库克信心满满:不在意,会继续推广
中国重庆重橙网络科技有限公司与Adobe合作
Z-Wave SiP模块提供高集成开发体验
Python中互换X和Y的值
论电源滤波器对于音响的重要性