博客:http://blog.csdn.net/muyang_ren
实现功能:开发板动态加载adc驱动模块并能通过测试程序
系统:Ubuntu 14.04 驱动交叉编译内核:linux-2.6.32.2 开发板:mini2440
建立交叉编译请点击,烧写linux到开发板请点击,Linux RootFs 选择rootfs_rtm_2440.img (光盘目录:image/linux/rtm )
linux文件目录:/opt/FriendlyARM/mini2440/linux-2.6.32.2
自己驱动目录 : /home/lianghuiyong/my2440drivers
注意:我的开发板内核被我裁剪了adc驱动部分,包括之后要写的一些驱动,裁剪内核驱动很简单,make menuconfig菜单后对应的驱动空格为空就行了
ADC原理
ADC模块共有8个模拟信号通道(XP、XM、YP、YM,A[ 3 : 0 ]),我们使用最多的是触摸屏,这时设置XP、XM、YP、YM选择为触摸屏的引脚,XM、YM则分别为X、Y方向接地线,为低电平。XP、YP使能后,笔尖等让触摸屏接触点受压产生电压变化来计算产生的X 、Y轴坐标,并将坐标值分别存储于ADCDAT0,ADCDAT1中。触摸屏就先这样简单介绍下,当触摸屏引脚为禁止时,这四个端口(XP、XM、YP、YM)可被用于ADC的模拟输入端口(AIN4、AIN5、AIN6和AIN7);
原理:这里模拟信号源选择开发板上的可调电位器,从电位器电路图中可知模拟信号输出端为AIN0,可调电位器阻值的改变产生电压的变化,设置MUX多路模拟信号选择器为AIN0,进行模数转换,并将产生的数据存储于ADCDAT0中。顺便说一下,MUX(8选1)可选择XP、XM、YP、YM作为模拟信号源,但此时不是触摸屏信号,而是AIN4、AIN5、AIN6或者AIN7的模拟信号
驱动部分
adc.c
/*************************************************************************
> File Name: adc.c
> Author: 梁惠涌
> Mail:
> Created Time: 2014年10月08日 星期三 21时01分48秒
************************************************************************/
//在 linux-2.6.32.2/arch/arm/mach-s3c2410/include/mach 目录下
#include<mach/regs-gpio.h> // 和GPIO相关的宏定义
#include<mach/hardware.h> //S3C2410_gpio_cfgpin等gpio函数定义
//在 linux-2.6.32.2/include/linux 目录下
#include<linux/miscdevice.h> //注册 miscdevide 结构体成员变量
#include<linux/delay.h> //这个应该是时延函数了
#include<linux/kernel.h> //内核,不用说
#include<linux/module.h> //驱动加载卸载函数定义
#include<linux/init.h> //初始化头文件
#include<linux/mm.h>
#include<linux/fs.h> //注册file_operations结构体变量
#include<linux/types.h>
#include<linux/moduleparam.h>
#include<linux/slab.h>
#include<linux/errno.h>
#include<linux/ioctl.h>
#include<linux/cdev.h>
#include<linux/string.h>
#include<linux/list.h>
#include<linux/pci.h>
#include<linux/gpio.h> //gpio口定义文件(虚拟地址)
#include<linux/clk.h> //系统时钟频率初始化文件
#include<linux/sched.h>
#include<linux/interrupt.h> //宏定义 IRQ_HANDLED 等
//在 linux-2.6.32.2/arch/arm/include/asm 目录下
#include<asm/io.h>
#include<asm/irq.h>
#include<asm/uaccess.h>
#include<asm/atomic.h>
#include<asm/unistd.h>
//在 linux-2.6.32.2/arch/arm/plat-s3c/include/plat 目录下
#include<plat/regs-adc.h> //定义ADCCON ADCDAT0等寄存器
#define DEVICE_NAME "adc"
static DECLARE_WAIT_QUEUE_HEAD(adc_waitq);
static void __iomem *adc_base;
static struct clk *adc_clk;
static int adc_data;
static volatile int ev_adc =0;
/* (1) 定义等待变量adc_waitq
* (2) 定义虚拟地址指针 adc_base, __iomem是2.6.9之后加入的特性,用来表示
* 指向一个 I/O 的虚拟内存空间,void可以使编译器忽略对变量的检查。
* (3) 定义一个clk类型的指针变量adc_clk,用于存储头文件中adc时钟的设置,
* (4) clk结构体是定义在 s3c2410-clock.c文件中。
* (5) 中volatile确保读取ev_adc的值不会被编译器的优化忽略
* */
static ssize_t adc_read(struct file *fp, char *buf, size_t count, loff_t *ppos)
{
unsigned int tmp;
tmp = (1<<14)|(255<<6)|(0<<3)|(1<<0);
writel(tmp, adc_base + S3C2410_ADCCON); //AD转换转换开始
wait_event_interruptible(adc_waitq, ev_adc);
ev_adc =0;
copy_to_user(buf, (char *)&adc_data, sizeof(adc_data));
return sizeof(adc_data);
}
/* (1) adc_read函数中,ssize_t是表示读写数据块的大小,类型是无符号整形
* (2) (1<<14) 预分频使能,(255<<6)预分频值设为255,(0<<3)模拟输入通道AIN0
* (1<<0)使能AD转换,根据开发板启动信息(S3C244X:core 405 MHZ 等)可以知道开
* 发板 PCLK为50.625MHZ,A/D转换频率=50.625MHZ /(255+1)=0.197MHZ,A/D转换时间
* =1/(0.197/5)=26uS。
* (3) writel函数是将tmp的值写入adc虚拟控制寄存器
* (4) wait_event_interruptible使等待队列进入睡眠,应该是为了copy_to_user
* 传递的不是一个正在改变的值的值,adc_data值读取在adc_irq函数实现
* (5) copy_to_user,将读取到的值发送到测试程序(用户层)返回adc_data的字节数
* */
static irqreturn_t adc_irq(int irq, void *dev_id){
if(!ev_adc){
adc_data=readl(adc_base + S3C2410_ADCDAT0) & 0x3ff;
ev_adc= 1;
wake_up_interruptible( &adc_waitq);
}
return IRQ_HANDLED;
}
/* (1)adc_irq在下面函数被引用的,irqreturn_t在irqreturn.h中定义枚举类型
* (2)adc_data存储ADCDAT0虚拟数据寄存器后十位值
* (3)wake_up_interruptible唤醒adc_waitq队列
* (4)IRQ_HANDLED宏定义为1,表示接收到了准确中断信号,并作了相应正确的处理
* */
static int adc_open(struct inode *inode, struct file *fp){
int ret;
ret=request_irq(IRQ_ADC, adc_irq, IRQF_SHARED, DEVICE_NAME, &adc_data);
if(ret){
printk(KERN_ERR"IRQ %d error %d \n", IRQ_ADC , ret);
}
return 0;
}
/* (1) request_irq中断申请函数
* 参数1:中断源, 参数2:中断处理函数,
* 参数3:中断方式, 参数4:设备名字, 参数5:设备ID
* (2) 其中设备ID不能使用NULL,否则ret会为1值,报错,设备ID一般为驱动结构体
* 地址,但我没有使用结构体,直接使用&adc_data;
* (3) 看过一些文章说写1等,但是会有warning,原因是设备ID参数是一个指针,需
* 要写一个地址才行。
* */
static struct file_operations dev_fops={
.owner = THIS_MODULE,
.open = adc_open,
.read = adc_read,
};
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops,
};
static int __init adc_init(void){
int ret;
adc_base = ioremap(S3C2410_PA_ADC, 4);
adc_clk=clk_get(NULL, "adc");
if(!adc_clk){
printk(KERN_ERR "failed to find adc clock source! \n");
return -ENOENT;
}
printk(KERN_ALERT "Hello! mini2440 adc module is installed\n");
clk_enable(adc_clk);
ret = misc_register(&misc);
return ret;
}
/* (1) 函数__init是一个属性标志,当驱动模块被加载时,__init函数就被执行了
* (2) ioremap函数是将物理地址映射成虚拟地址并传递于adc_base
* (3) clk_get是从头文件中获取adc时钟设置,定义在s3c2410-clock.c,由于内核
* 并没有adc驱动,所以adc的时钟并没初始化,clk_enable(adc_clk)初始化 adc
* 时钟misc_register函数是注册混杂设备驱动
* */
static void __exit adc_exit(void){
printk(KERN_ALERT "Good-bye ,mini2440 adc module is removed! \n");
misc_deregister(&misc);
}
/* (1) __exit和__init一样是module属性标志
* (2) misc_deregister函数是注销混杂设备驱动
* */
module_init(adc_init);
module_exit(adc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lianghuiyong Inc.");
Makefile
ifeq ($(KERNELRELEASE),)
KDIR ?= /opt/FriendlyARM/mini2440/linux-2.6.32.2/
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
else
obj-m:= adc.o
endif
/* 第1行:判断 KERNELRELEASE 变量是否为空,只有make命令该变量才不为空。
* 第2、3行:KDIR是内核路径,PWD是当前模块路径。
* 第4行:是makefile的功能选项,冒号结尾。
* 第5行:是执行模块的编译,语法是“Make -C 内核路径 M=模块路径 modules”。
* 第6、7行和第4、5行一样。
* 第8行:删除多余文件功能标识。
* 第9行:是删除编译过程的中间文件的命令。
* 第11行:如果make命令没有带功能标识(modules、modules_install、clean)使用,
* 就将adc.o编译成adc.ko模块
* */
测试模块
adcceshi.c
/*************************************************************************
> File Name: adcceshi.c
> Author: 梁惠涌
> Mail:
> Created Time: 2014年10月10日 星期五 23时41分26秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
void delay(int j){
int i;
for(;j>0;j--)
for(i=10000;i>0;i--);
}
int main (int argc, char **argv){
int fd;
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 : %d \n",data);
}
delay(5000);
}
close(fd);
return 0;
}
/* (1) read(fd,&data,sizeof(data)) 函数将跳转到驱动file_openration
* 中的.read =adc_read,进入adc_read函数,通过copy_to_user将驱动程
* 序中adc_data 传递给用户层测试程序data,return返回data字节数
* (2) 其中驱动中 adc_data的值是在驱动程序 adc_irq函数中通过readl从
* ADCDAT0虚拟地址获取.
* (3) delay函数通过多次空循环实现时间的延迟
* */
测试程序使用命令:arm-linux-gcc -g adcceshi.c -o adcceshi 来生成测试程序adcceshi
开发板运行截图
其中rmmod: module 'adc' not found提示是因为是没把驱动程序放在/lib/modules/2.6.32.2-FriendlyARM下,我直接在挂载的文件夹下使用,
这个提示可以忽略
错误笔记
1 、error :‘TASK_INTERRUPTIBLE' undeclared (first use in this function)
包含头文件: #include<linux/sched.h>
2、 error: 'IRQF_SHARED' undeclared
包含头文件:#include<linux/interrupt.h>
3、挂载驱动后运行测试程序出现:
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c3934000
[00000000] *pgd=33969031, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#2]
last sysfs file: /sys/devices/virtual/misc/adc/dev
Modules linked in: adc [last unloaded: adc]
CPU: 0 Tainted: G D (2.6.32.2-FriendlyARM #2)
PC is at adc_read+0x20/0x10c [adc]
LR is at vfs_read+0xac/0xe0
pc : [<bf00c0e0>] lr : [<c008ec10>] psr: a0000013
sp : c393bf20 ip : c393bf58 fp : c393bf54
r10: 00000000 r9 : c393a000 r8 : c0023088
r7 : bec9fd34 r6 : c393bf78 r5 : bec9fd34 r4 : bf00c470
r3 : 00000000 r2 : 00007fc1 r1 : bec9fd34 r0 : c390e380
Flags: NzCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: c000717f Table: 33934000 DAC: 00000015
Process adcceshi (pid: 664, stack limit = 0xc393a270)
Stack: (0xc393bf20 to 0xc393c000)
bf20: 00000000 c390e380 c399b904 00000000 c393bf64 c393bf40 c390e380 bec9fd34
bf40: c393bf78 00000003 c393bf74 c393bf58 c008ec10 bf00c0d0 c390e380 00000000
bf60: 00000000 00000003 c393bfa4 c393bf78 c008ed1c c008eb74 00000000 00000000
bf80: 00000005 00000000 c393bfa4 00000000 00000000 00000000 00000000 c393bfa8
bfa0: c0022ee0 c008ece0 00000000 00000000 00000003 bec9fd34 00000004 bec9fd34
bfc0: 00000000 00000000 00000000 00000003 00000000 00000000 40024000 bec9fd44
bfe0: 00000000 bec9fd28 00008550 400daebc 60000010 00000003 00000000 00000000
Backtrace:
[<bf00c0c0>] (adc_read+0x0/0x10c [adc]) from [<c008ec10>] (vfs_read+0xac/0xe0)
r7:00000003 r6:c393bf78 r5:bec9fd34 r4:c390e380
[<c008eb64>] (vfs_read+0x0/0xe0) from [<c008ed1c>] (sys_read+0x4c/0x84)
r7:00000003 r6:00000000 r5:00000000 r4:c390e380
[<c008ecd0>] (sys_read+0x0/0x84) from [<c0022ee0>] (ret_fast_syscall+0x0/0x28)
r6:00000000 r5:00000000 r4:00000000
Code: e59f40e0 e59f20e0 e5943004 e1a07001 (e5832000)
---[ end trace 929b7bc9d9ae4dda ]---
Segmentation fault
只需注意上面中:PC is at adc_read+0x20/0x10c [adc] 出错的是 adc_read函数,从 Unable to handle kernel NULL pointer dereference at virtual address 00000000
可看出是指针的问题,看整个驱动发现是因为adc_base没有赋予虚拟地址值,在驱动加载函数中增加:adc_base= ioremap(S3C2410_PA_ADC, 0x20);adc物理地址转换成虚拟地址并存储于adc_base 中
4、IRQ 80 error -22 错误
其中irq中断函数 ret = request_irq(IRQ_ADC, adc_irq, IRQF_SHARED, DEVICE_NAME,NULL); 最后一个参数不能为NULL,我修改后为 &adc_data