1,主机环境:VMare下CentOS 5.5 ,1G内存。
2,集成开发环境:Elipse IDE
3,编译编译环境:arm-linux-gcc v4.4.3,arm-none-linux-gnueabi-gcc v4.5.1。
4,开发板:mini2440,2M nor flash,128M nand flash。
5,u-boot版本:u-boot-2009.08
6,linux 版本:linux-2.6.32.2
7,参考文章:
嵌入式linux应用开发完全手册,韦东山,编著。
Mini2440 之Linux 移植开发实战指南
【1】关于S3C2440 的ADC 和触摸屏接口
在 S3C2440 芯片中,AD 输入和触摸屏接口使用共同的A/D 转换器,见2440 芯片手册第16 章节,如图,其中通道7(XP或AIN7)作为触摸屏接口的X坐标输入,通道5(YP或AIN5)作为触摸屏接口的Y坐标输入。
我们从上面的结构图和数据手册可以知道,该ADC模块总共有8个通道可以进行模拟信号的输入,分别是AIN0、AIN1、AIN2、AIN3、YM、YP、XM、XP。那么ADC是怎么实现模拟信号到数字信号的转换呢?首先模拟信号从任一通道输入,然后设定寄存器中预分频器的值来确定AD转换器频率,最后ADC将模拟信号转换为数字信号保存到ADC数据寄存器0中(ADCDAT0),然后ADCDAT0中的数据可以通过中断或查询的方式来访问。对于ADC的各寄存器的操作和注意事项请参阅数据手册。
上图是mini2440上的ADC应用实例,开发板通过一个10K的电位器(可变电阻)来产生电压模拟信号,然后通过第一个通道(即:AIN0)将模拟信号输入ADC。
ADC设备在Linux中可以看做是简单的字符设备,也可以当做是一混杂设备(misc设备),这里我们就看做是misc设备来实现ADC的驱动。注意:这里我们获取AD转换后的数据将采用中断的方式,即当AD转换完成后产生AD中断,在中断服务程序中来读取ADCDAT0的第0-9位的值(即AD转换后的值)。
【2】在内核中添加ADC 驱动
Linux-2.6.32.2 内核并没有提供支持S3C2440 的ADC 驱动程序,由于《移植开发实战指南》中ADC部分代码在实际测试中始终输出-1,而无法通过测试,于是结合博主黄刚嵌入式Linux之我行——S3C2440上ADC驱动实例开发讲解的ADC驱动程序作了下修改,经过修改后有一个好处是方便地通过s3c24xx-adc.h文件中提供的宏修改通道获取采样数据,该头文件的代码也在drivers/misc目录下内容为:
#ifndef _S3C2410_ADC_H_
#define _S3C2410_ADC_H_
#define ADC_WRITE(ch, prescale) ((ch)<<16|(prescale))
#define ADC_WRITE_GETCH(data) (((data)>>16)&0x7)
#define ADC_WRITE_GETPRE(data) ((data)&0xff)
#endif /* _S3C2410_ADC_H_ */
驱动程序的文件名为:mini2440_adc.c位于drivers/misc 目录下。由上述内容可知,ADC 驱动和触摸屏驱动若想共存,就必须解决共享“A/D 转换器”资源这个问题,因此在ADC 驱动程序中声明了一个全局的“ADC_LOCK”信号量,ADC 驱动程序的内容和注解如下:
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/input.h>
#include <linux/init.h>
#include <linux/serio.h>
#include <linux/delay.h>
#include <linux/clk.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>
#include <mach/regs-clock.h>
#include <plat/regs-timer.h>
#include <plat/regs-adc.h>
#include <mach/regs-gpio.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
//;自己定义的头文件,因原生内核并没有包含
#include "s3c24xx-adc.h"
#undef DEBUG
//#define DEBUG
#ifdef DEBUG
#define DPRINTK(x...) {printk(__FUNCTION__"(%d): ",__LINE__);printk(##x);}
#else
#define DPRINTK(x...) (void)(0)
#endif
//;定义ADC 转换设备名称,将出现在/dev/adc
#define DEVICE_NAME "adc"
static void __iomem *adc_base; /*定义了一个用来保存经过虚拟映射后的内存地址*/
//;定义ADC 设备结构
typedef struct {
wait_queue_head_t wait;
int channel;
int prescale;
}ADC_DEV;
static ADC_DEV adcdev;
//;声明全局信号量,以便和触摸屏驱动程序共享A/D 转换器
DECLARE_MUTEX(ADC_LOCK);
//;ADC驱动是否拥有A/D 转换器资源的状态变量
//static volatile int OwnADC = 0;
/*用于标识AD转换后的数据是否可以读取,0表示不可读取*/
static volatile int ev_adc = 0;
/*用于保存读取的AD转换后的值,该值在ADC中断中读取*/
static int adc_data;
/*保存从平台时钟队列中获取ADC的时钟*/
static struct clk *adc_clk;
//;定义ADC 相关的寄存器
#define ADCCON (*(volatile unsigned long *)(adc_base + S3C2410_ADCCON)) //ADC control
#define ADCTSC (*(volatile unsigned long *)(adc_base + S3C2410_ADCTSC)) //ADC touch screen control
#define ADCDLY (*(volatile unsigned long *)(adc_base + S3C2410_ADCDLY)) //ADC start or IntervalDelay
#define ADCDAT0 (*(volatile unsigned long *)(adc_base + S3C2410_ADCDAT0)) //ADC conversion data 0
#define ADCDAT1 (*(volatile unsigned long *)(adc_base + S3C2410_ADCDAT1)) //ADC conversion data 1
#define ADCUPDN (*(volatile unsigned long *)(adc_base + 0x14)) //Stylus Up/Down interrupt status
#define PRESCALE_DIS (0 << 14)
#define PRESCALE_EN (1 << 14)
#define PRSCVL(x) ((x) << 6)
#define ADC_INPUT(x) ((x) << 3)
#define ADC_START (1 << 0)
#define ADC_ENDCVT (1 << 15)
//;定义“开启AD 输入”宏,因为比较简单,故没有做成函数
//#define START_ADC_AIN(ch, prescale)
#define start_adc(ch, prescale) \
do{ \
ADCCON = PRESCALE_EN | PRSCVL(prescale) | ADC_INPUT((ch)) ; \
ADCCON |= ADC_START; \
}while(0)
/*设置ADC控制寄存器,开启AD转换*/
/*static void start_adc(int ch,int prescale)
{
unsigned int tmp;
tmp = PRESCALE_EN | PRSCVL(prescale) | ADC_INPUT(ch);//(1 << 14)|(255 << 6)|(0 << 3);// 0 1 00000011 000 0 0 0
//此处writl()的原型是void writel(u32 b, volatile void __iomem *addr),addr是经过地址重映射后的地址
writel(tmp, ADCCON); //AD预分频器使能、模拟输入通道设为AIN0
tmp = readl(ADCCON);
tmp = tmp | ADC_START; //(1 << 0); // 0 1 00000011 000 0 0 1
writel(tmp, ADCCON); //AD转换开始
}
问题:此函数被调用时为什么地址映射错误?答案应该需要使用专用的函数iowrite32操作。
*/
//;ADC 中断处理函数
static irqreturn_t adc_irq(int irq, void *dev_id)
{
//;如果ADC 驱动拥有“A/D 转换器”资源,则从ADC 寄存器读取转换结果
if (!ev_adc)
{
/*读取AD转换后的值保存到全局变量adc_data中,S3C2410_ADCDAT0定义在regs-adc.h中,
这里为什么要与上一个0x3ff,很简单,因为AD转换后的数据是保存在ADCDAT0的第0-9位,
所以与上0x3ff(即:1111111111)后就得到第0-9位的数据,多余的位就都为0*/
adc_data = ADCDAT0 & 0x3ff;
/*将可读标识为1,并唤醒等待队列*/
ev_adc = 1;
wake_up_interruptible(&adcdev.wait);
}
return IRQ_HANDLED;
}
//;ADC 读函数,一般对应于用户层/应用层的设备读函数(read)
static ssize_t adc_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
/*试着获取信号量(即:加锁)*/
if (down_trylock(&ADC_LOCK))
{
return -EBUSY;
}
if(!ev_adc) /*表示还没有AD转换后的数据,不可读取*/
{
if(filp->f_flags & O_NONBLOCK)
{
/*应用程序若采用非阻塞方式读取则返回错误*/
return -EAGAIN;
}
else /*以阻塞方式进行读取*/
{
/*设置ADC控制寄存器,开启AD转换*/
start_adc(adcdev.channel, adcdev.prescale);
/*使等待队列进入睡眠*/
wait_event_interruptible(adcdev.wait, ev_adc);
}
}
/*能到这里就表示已有AD转换后的数据,则标识清0,给下一次读做判断用*/
ev_adc = 0;
/*将读取到的AD转换后的值发往到上层应用程序*/
copy_to_user(buffer, (char *)&adc_data, sizeof(adc_data));
/*释放获取的信号量(即:解锁)*/
up(&ADC_LOCK);
return sizeof(adc_data);
}
//;打开ADC设备的函数,一般对应于用户态程序的open
static int adc_open(struct inode *inode, struct file *filp)
{
int ret;
/* normal ADC */
ADCTSC = 0;
//;初始化中断队列
init_waitqueue_head(&(adcdev.wait));
adcdev.channel=0;//;缺省通道为“0”
adcdev.prescale=0xff;
/* 申请ADC中断服务,这里使用的是共享中断:IRQF_SHARED,为什么要使用共享中断,因为在触摸屏驱动中
也使用了这个中断号。中断服务程序为:adc_irq在下面实现,IRQ_ADC是ADC的中断号,这里注意:
申请中断函数的最后一个参数一定不能为NULL,否则中断申请会失败,这里传入的是ADC_DEV类型的变量*/
ret = request_irq(IRQ_ADC, adc_irq, IRQF_SHARED, DEVICE_NAME, &adcdev);
if (ret)
{
/*错误处理*/
printk(KERN_ERR "IRQ%d error %d\n", IRQ_ADC, ret);
return -EINVAL;
}
DPRINTK( "adc opened\n");
return 0;
}
static int adc_release(struct inode *inode, struct file *filp)
{
DPRINTK( "adc closed\n");
return 0;
}
static struct file_operations dev_fops = {
owner: THIS_MODULE,
open: adc_open,
read: adc_read,
release: adc_release,
};
static struct miscdevice adc_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops,
};
static int __init dev_init(void)
{
int ret;
/* 1,从平台时钟队列中获取ADC的时钟,这里为什么要取得这个时钟,因为ADC的转换频率跟时钟有关。
系统的一些时钟定义在arch/arm/plat-s3c24xx/s3c2410-clock.c中*/
adc_clk = clk_get(NULL, "adc");
if (!adc_clk) {
printk(KERN_ERR "failed to get adc clock source\n");
return -ENOENT;
}
/*时钟获取后要使能后才可以使用,clk_enable定义在arch/arm/plat-s3c/clock.c中*/
clk_enable(adc_clk);
/* 2,将ADC的IO端口占用的这段IO空间映射到内存的虚拟地址,ioremap定义在io.h中。
注意:IO空间要映射后才能使用,以后对虚拟地址的操作就是对IO空间的操作,
S3C2410_PA_ADC是ADC控制器的基地址,定义在mach-s3c2410/include/mach/map.h中,0x20是虚拟地址长度大小*/
adc_base=ioremap(S3C2410_PA_ADC,0x20);
if (adc_base == NULL) {
printk(KERN_ERR "Failed to remap register block\n");
ret = -EINVAL;
goto err_noclk;
}
/* 3,把看ADC注册成为misc设备,misc_register定义在miscdevice.h中
adc_miscdev结构体定义及内部接口函数在第2步中讲,MISC_DYNAMIC_MINOR是次设备号,定义在miscdevice.h中*/
ret = misc_register(&adc_miscdev);
if (ret)
{
/*错误处理*/
printk(KERN_ERR "Cannot register miscdev on minor=%d (%d)\n", MISC_DYNAMIC_MINOR, ret);
goto err_nomap;
}
printk(DEVICE_NAME "\tinitialized!\n");
return 0;
//以下是上面错误处理的跳转点
err_noclk:
clk_disable(adc_clk);
clk_put(adc_clk);
err_nomap:
iounmap(adc_base);
return ret;
}
static void __exit dev_exit(void)
{
free_irq(IRQ_ADC, &adcdev); //;释放中断
iounmap(adc_base); /*释放虚拟地址映射空间*/
if (adc_clk) /*屏蔽和销毁时钟*/
{
clk_disable(adc_clk);
clk_put(adc_clk);
adc_clk = NULL;
}
misc_deregister(&adc_miscdev);
}
//;导出信号量“ADC_LOCK”,以便触摸屏驱动使用
EXPORT_SYMBOL(ADC_LOCK);
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("singleboy");
MODULE_DESCRIPTION("Mini2440 ADC Driver");
说明:杂项设备(misc device)
杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的include/linux 目录下有Miscdevice.h 文件,要把自己定义的misc device 从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于misc device,其实misc_register 就是用主标号10 调用register_chrdev()的。也就是说,misc设备其实也就是特殊的字符设备。
然后打开drivers/misc/Makefile 文件,在大概24 行加入ADC 驱动程序目标模块
obj-$(CONFIG_EP93XX_PWM) += ep93xx_pwm.o
obj-$(CONFIG_C2PORT) += c2port/
obj-$(CONFIG_MINI2440_ADC) += mini2440_adc.o
obj-y += eeprom/
obj-y += cb710/
再打开drivers/char/Kconfig 文件,定位到16行附近,加入ADC 驱动配置选项:
menuconfig MISC_DEVICES
bool "Misc devices"
default y
---help---
Say Y here to get to see options for device drivers from various
different categories. This option alone does not add any kernel code.
If you say N, all options in this submenu will be skipped and disabled.
if MISC_DEVICES
config MINI2440_ADC
bool "ADC driver for FriendlyARM Mini2440 development boards"
depends on MACH_MINI2440
default y if MACH_MINI2440
help
this is ADC driver for FriendlyARM Mini2440 development boards
Notes: the touch-screen-driver required this option
config ATMEL_PWM
tristate "Atmel AT32/AT91 PWM support"
depends on AVR32 || ARCH_AT91SAM9263 || ARCH_AT91SAM9RL || ARCH_AT91CAP9
help
This option enables device driver support for the PWM channels
on certain Atmel processors. Pulse Width Modulation is used for
purposes including software controlled power-efficient backlights
on LCD displays, motor control, and waveform generation.
这样,我们就在内核中添加了ADC 驱动。
【3】确认配置选项
现在内核源代码目录的命令行执行:make menuconfig,依次选择如下子菜单项,找到刚刚添加的ADC 驱动配置选项:
Device Drivers --->
[*] Misc devices --->
如图所示,按空格键选中 ADC 配置选项
然后退出保存所选配置, 在命令行执行: make uImage , 将会生成arch/arm/boot/uImage,然后将其复制到/nfsboot目录下后启动开发板。可以在看到串口终端中启动信息:
... ...
brd: module loaded
adc initialized!
S3C24XX NAND Driver, (c) 2004 Simtec Electronics
... ...
说明ADC设备加载成功。
【4】ADC 测试程序
测试程序也是采用博主黄刚测试程序,这里作一点小小修改,加了一秒钟的睡眠,这样在旋转电位器W1时能够清楚地看到变化。代码位于linux-test/codetest/adc_test.c,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int fd;
fprintf(stderr, "press Ctrl-C to stop\n");
//以阻塞方式打开设备文件,非阻塞时flags=O_NONBLOCK
fd = open("/dev/adc", 0);
if(fd < 0)
{
printf("Open ADC Device Faild!\n");
exit(1);
}
while(1)
{
int ret;
int data;
//读设备
ret = read(fd, &data, sizeof(data));
if(ret != sizeof(data))
{
if(errno != EAGAIN)
{
printf("Read ADC Device Faild!\n");
}
continue;
}
else
{
printf("Read ADC value is: %d\n", data);
}
sleep(1);
}
close(fd);
return 0;
}
然后保存退出,之后在主机终端交叉编译
[root@localhost codetest]# arm-linux-gcc -o adc_test adc_test.c
将其复制到nfsboot目录下以便在开发板挂载该目录后能够执行
[root@localhost codetest]# cp adc_test /nfsboot/nfs
【5】挂载nfs文件系统
在目标板的串口终端中执行
[root@mini2440 /]#mount -o nolock -t nfs 10.1.0.128:/nfsboot/nfs /mnt/nfs //注意要加 -o nolock参数,否则挂载不上。
[root@mini2440 /]#cd mnt/nfs
[root@mini2440 nfs]#ls
adc_test
【7】运行测试程序
在开发板的命令行终端输入:./adc-test,然后旋转开发板上的W1 可调电阻
[root@mini2440 nfs]#./adc_test
press Ctrl-C to stop
Read ADC value is: 224
Read ADC value is: 225
Read ADC value is: 213
Read ADC value is: 379
Read ADC value is: 50
Read ADC value is: 358
Read ADC value is: 34
Read ADC value is: 0
Read ADC value is: 0
Read ADC value is: 321
Read ADC value is: 549
Read ADC value is: 738
Read ADC value is: 846
Read ADC value is: 914
Read ADC value is: 997
Read ADC value is: 1023
^C
[root@mini2440 nfs]#
可以看到ADC 转换的结果也在变动
接下来,将进行触摸屏驱动移植