s3c2410多通道adc驱动及测试程序(使用write控制多通道)

免责声明:
本文所涉及的驱动是在SeonKon Choi于2003年编写的适合2.4版本内核的ADC驱动基础上结合网络能找得到的代码片段经过自己的修改并实践通过的。测试过程中的ADC并不涉及其它驱动(比如与ADC有很密切关系的触摸屏驱动)。本文作者本着GNU共享精神发表此文并附录源代码——其实公布源代码是必须的,因为代码使用GPL(而如何行文则是作者的自由)。至于如何使用文中知识点及代码,无论正确与否,概与文章作者无关,风险自担。不过,趁文章作者还有激情研究驱动之时,欢迎一起讨论,大家共同学习,天天向上。

杂项: 本文上接《s3c2410多通道adc驱动及测试程序》,里面承诺了用write来传递数据到内核中。那篇文章使用ioctl来控制通道,而本文使用write控制通道。而两者的驱动,在本质上是一致的——除了针对内核版本不同而有所修改外。

收获:
在修改人家驱动代码过程中,对于一些不确定的函数(比如位于哪个头文件,每个函数参数的含义),逐一google之,并查看内核源代码。个人认为,学习代码、驱动这种事情,最好在实践中学习,看书与实践结合起来。话虽如此,真正做到的人却不多。如果不去动手写,看再多的LDD3、ELDD、ULK、APUE、CSAPP也收获不丰,印象不深。说来惭愧,正是这个观点,让我一直不去看书,至今连LDD3也没看完,APUE也刚开了个头。

未尽及将来可能会做的事:
1、与TS共享中断,使用信号量进行互斥访问。
2、其它的。  

正文:
如想直接抄代码,可忽略行文,直接到文章后面下载附录的工程压缩包或源代码。
1、对于头文件,不必说太多,2.4版本的内核有些头文件已经删除或修改或合并等等,比如像<linux/config.h>已经不在了。

2、使用ioremap,这使得代码整洁、易看。举一例:
网上使用如下代码片段(示意性代码)设置通道,读取ADC数据:

1
2
3
writel((1<<14) | PRSCVL(adcdev.prescale) | ADC_INPUT(adcdev.channel) | 0x01 | 0x01, S3C2410_ADCCON);

ret = readl(S3C2410_ADCDAT0);

使用宏后:

1
2
3
START_ADC_AIN(adcdev.channel, adcdev.prescale);

ret = ADCDAT0;

其中START_ADC_AIN将设置通道、启动ADC等合在一起,ADCDAT0是经过ioremap后加上一偏移得到的“寄存器”。
如此一来,是不是觉得后面的代码很直观很容易看?不过这也有一点不好,封闭得太多了,对于想知道其中原理的人来说可不是件好事。比如声明一个信号量,使用DECLARE_MUTEX(ADC_LOCK)或DEFINE_SEMAPHORE(ADC_LOCK),这很好懂,但是这个宏到底做了些什么呢?这就要深入到这个宏定义所在文件中研究了(在<linux/semaphore.h>文件中)。
对于这两宏,想说几句,因为最近一次移植新版本的内核(2.6.37.3),就是因为DECLARE_MUTEX不再使用而导致我浪费了几天的时间。据本人考查,最早说要将DECLARE_MUTEX改为DEFINE_SEMAPHORE是在08年10月底,参见地址http://lwn.net/Articles/304725/。而在Linux Cross Reference(网站如http://lxr.free-electrons.com/等等)中发现,直到2.6.36才出现DEFINE_SEMAPHORE,而且这个版本中两个宏同时存在。但是到了2.6.37,DECLARE_MUTEX这个宏却不存在了。
不由得想到某个论坛的签名,原文是英文的,原文及出自一时不知道去哪找了。大意是说UNIX最好的地方是什么?答曰有社区,再问UNIX不好的地方是什么?答曰有太多的社区。像Linux内核这东西,版本更新太快了,各种结构体名称变化、成员变化及新增函数、删除函数,让人目不暇接。虽说一味追求新版本不好,但是心理作用,还是选择新的版本来移植。新的东西当然有它的好,像买台android的手机,就得不停的刷机,不断折腾,这跟搞Linux在某种程度上是一样的。

3、硬件寄存器 ADC驱动涉及的寄存器不多,都可以在datasheet中找到说明的。如:

1
2
3
#define PRSCVL(x)              ((x) << 6)
#define ADC_INPUT(x) ((x) << 3)
#define ADC_START (1 << 0)

第一个是设置AD转换器预定频率(注:该术语可能不标准),左移6位,是因为ADCCON寄存器的613位就是这个值。而ADC_INPUT是选择通道的,位于ADCCON寄存器的35位。最后一行是使能ADC(启动ADC),位于ADCCON第0位,1表示开始转换(注:根据datasheet英文理解)。
其实这些不难理解的,看多代码了,自然也知道了。自从当年学AVR后,我对这种方式的移位特别有好感,甚至一度上瘾,常常跟别人说,不过没多少人理解,后来就算了,自己独自享受这种便捷了。

4、与原来代码的改动:
1)、去掉MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT,这个据说在2.6中不再使用了。
2)、对于request_irq函数,将IRQ_ADC_DONE改为IRQ_ADC,最后一个参数改为&adcdev。
3)、将adcdone_int_handler函数返回类型由原来的void改为irqreturn_t。
4)、另外添加cdev类型的变量,因为注册字符设备需要使用到。
5)、判断copy_from_user、copy_to_user两个函数的返回值,正常返回0,出错则返回没有复制成功的字节数。出错时返回-EFAULT。如果不判断,会出现一个警告,比如:

1
warning: ignoring return value of 'copy_to_user', declared with attribute warn_unused_result

内核中很多地方都使用这两个函数,都作了判断。
其它的修改不一一道来了。
测试过程:
测试手段很多,由于本人的网络驱动一直不成功,不能使用方便快捷的NFS方式来测试。将编译好的驱动模块和交叉编译后的测试程序拷贝到U盘,再用U盘复制到开发板上,在这个过程中,文件丢失了可执行性,又得在开发板上用chmod恢复可执行性。很是麻烦。
原想将未修改的代码与修改后的代码作一个patch的,不过有10KB那么大,就不帖出来了(附件里有)。下面附上工程目录及源代码。如果网页显示与工程文件不一致,以工程目录中的文件为准。实际代码中没有中文注释,由此带来的各种质问及不解,本人不作解释。
整个工程下载(压缩文件名称为s3c2410-adc-latelee.org.tar.bz2,md5sum为0aa975c62eaefb8d05430bca8d7df088): s3c2410-adc-latelee.org.tar.bz2
应用层的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**************************************************************Late Lee from http://www.latelee.org
2011-05-18
on host: $ arm-linux-gcc -DDEBUG my-adc-test-new.c -o adc-test-new
on target # insmod adc.ko# mknod /dev/myadc c 252 0/1/2 (252 is from # cat /proc/devices)
# ./adc-test-new
Test of ADC. ^C to exit
AIN[0]: 1023 3.30
AIN[1]: 350 1.13
AIN[2]: 281 0.91
AIN[3]: 304 0.98
# dmesg
adc opened
set adc channel=0, prescale=0xff
AIN[0] = 0x03ff, 1
set adc channel=1, prescale=0xff
AIN[1] = 0x015e, 1
set adc channel=2, prescale=0xff
AIN[2] = 0x0119, 1
set adc channel=3, prescale=0xff
AIN[3] = 0x0130, 1
adc closed
****************************************************************/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>#define ADC "/dev/myadc"
#ifdef DEBUG
#define debug(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#define debug(fmt, ...)
#endif
/* for write */
#define ADC_WRITE(ch, prescale) ((ch)<<16|(prescale))
int fd;
void sig_handle(int sig)
{
debug("you press ^C: %dn", sig);
close(fd); /* we colse it here */
exit(0);
}
int main(void)
{
int data;
int prescale = 0xff;
int val; /* do not use 'float val' */
int i;
signal(SIGINT, sig_handle);
debug("Test of ADC. ^C to exitn");
fd = open(ADC, O_RDWR);
if (fd < 0){
perror("Open /dev/adc failed");
exit(1);
}
while (1) {
/* we have 4 channels ADC*/
for (i=0; i<4; i++) {
data = ADC_WRITE(i, prescale); // 将通道及预设定频率合成一个数。
write(fd, &data, sizeof(data)); // 赋给驱动
read(fd, &val, sizeof(val)); // 读取转换后的数据
printf("AIN[%d]: %d %4.2fn", i, val, val * 3.3 / 1023.0); /* 3.3只是一个测试值。1023? 1024? */
sleep(1);
}
//sleep(1);
}
close(fd); /* just kidding */
return 0;
}

本人得到的原始的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/** s3c2410-adc.c
*
* S3C2410 ADC
* exclusive with s3c2410-ts.c
*
* Author: SeonKon Choi <bushi@mizi.com>
* Date : $Date: 2003/01/20 14:24:49 $
*
* $Revision: 1.1.2.6 $
*
2004-6-14 add a device by threewater<threewater@up-tech.com>
Fri Dec 03 2002 SeonKon Choi <bushi@mizi.com>- initial
*
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file COPYING in the main directory of this archive
* for more details.
*/
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>#include <linux/sched.h>
#include <linux/irq.h>
#include <linux/delay.h>
#include <asm/hardware.h>
#include <asm/semaphore.h>
#include <asm/uaccess.h>
#include "s3c2410-adc.h"
#undef DEBUG
//#define DEBUG
#ifdef DEBUG
#define DPRINTK(x...) {printk(__FUNCTION__"(%d): ",__LINE__);printk(##x);}
#else
#define DPRINTK(x...) (void)(0)
#endif
#define DEVICE_NAME "s3c2410-adc"
#define ADCRAW_MINOR 1
static int adcMajor = 0;
typedef struct {
struct semaphore lock;
wait_queue_head_t wait;
int channel;
int prescale;
}ADC_DEV;
static ADC_DEV adcdev;
#define START_ADC_AIN(ch, prescale)
do{
ADCCON = PRESCALE_EN | PRSCVL(prescale) | ADC_INPUT((ch)) ;
ADCCON |= ADC_START;
}while(0)
static void adcdone_int_handler(int irq, void *dev_id, struct pt_regs *reg)
{
wake_up(&adcdev.wait);
}
static ssize_t s3c2410_adc_write(struct file *file, const char *buffer, size_t count, loff_t * ppos)
{
int data;
if(count!=sizeof(data)){
//error input data size
DPRINTK("the size of input data must be %dn", sizeof(data));
return 0;
}
copy_from_user(&data, buffer, count);
adcdev.channel=ADC_WRITE_GETCH(data);
adcdev.prescale=ADC_WRITE_GETPRE(data);
DPRINTK("set adc channel=%d, prescale=0x%xn", adcdev.channel, adcdev.prescale);
return count;
}
static ssize_t s3c2410_adc_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
int ret = 0;
if (down_interruptible(&adcdev.lock))
return -ERESTARTSYS;
START_ADC_AIN(adcdev.channel, adcdev.prescale);
interruptible_sleep_on(&adcdev.wait);
ret = ADCDAT0;
ret &= 0x3ff;
DPRINTK("AIN[%d] = 0x%04x, %dn", adcdev.channel, ret, ADCCON & 0x80 ? 1:0);
copy_to_user(buffer, (char *)&ret, sizeof(ret));
up(&adcdev.lock);
return sizeof(ret);
}
static int s3c2410_adc_open(struct inode *inode, struct file *filp)
{
init_MUTEX(&adcdev.lock);
init_waitqueue_head(&(adcdev.wait));
adcdev.channel=0;
adcdev.prescale=0xff;
MOD_INC_USE_COUNT;
DPRINTK( "adc openedn");
return 0;
}
static int s3c2410_adc_release(struct inode *inode, struct file *filp)
{
MOD_DEC_USE_COUNT;
DPRINTK( "adc closedn");
return 0;
}
static struct file_operations s3c2410_fops = {
owner: THIS_MODULE,
open: s3c2410_adc_open,
read: s3c2410_adc_read,
write: s3c2410_adc_write,
release: s3c2410_adc_release,
};
#ifdef CONFIG_DEVFS_FS
static devfs_handle_t devfs_adc_dir, devfs_adcraw;
#endif
int __init s3c2410_adc_init(void)
{
int ret;
/* normal ADC */
ADCTSC = 0; //XP_PST(NOP_MODE);
ret = request_irq(IRQ_ADC_DONE, adcdone_int_handler, SA_INTERRUPT, DEVICE_NAME, NULL);
if (ret) {
return ret;
}
ret = register_chrdev(0, DEVICE_NAME, &s3c2410_fops);
if (ret < 0) {
printk(DEVICE_NAME " can't get major numbern");
return ret;
}
adcMajor=ret;
#ifdef CONFIG_DEVFS_FS
devfs_adc_dir = devfs_mk_dir(NULL, "adc", NULL);
devfs_adcraw = devfs_register(devfs_adc_dir, "0raw", DEVFS_FL_DEFAULT,
adcMajor, ADCRAW_MINOR, S_IFCHR | S_IRUSR | S_IWUSR, &s3c2410_fops, NULL);
#endif
printk (DEVICE_NAME"tinitializedn");
return 0;
}
module_init(s3c2410_adc_init);
#ifdef MODULE
void __exit s3c2410_adc_exit(void)
{
#ifdef CONFIG_DEVFS_FS
devfs_unregister(devfs_adcraw);
devfs_unregister(devfs_adc_dir);
#endif
unregister_chrdev(adcMajor, DEVICE_NAME);
free_irq(IRQ_ADC_DONE, NULL);
}
module_exit(s3c2410_adc_exit);
MODULE_LICENSE("GPL");
#endif

经过修改后的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/** s3c2410-adc.c
*
* S3C2410 ADC
* exclusive with s3c2410-ts.c
*
* Author: SeonKon Choi <bushi@mizi.com>
* Date : $Date: 2003/01/20 14:24:49 $
*
* $Revision: 1.1.2.6 $
*
2004-6-14 add a device by threewater<threewater@up-tech.com>
Fri Dec 03 2002 SeonKon Choi <bushi@mizi.com>- initial
*
2011-05-16 modified this file for 2.6.30.2 kernel by Late Lee<latelee@163.com>
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file COPYING in the main directory of this archive
* for more details.
*/
/*#include <linux/config.h>*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>#include <linux/sched.h>
/*#include <linux/irq.h>*/
#include <linux/delay.h>
/*#include <asm/hardware.h>*/
#include <linux/semaphore.h>
/*#include <asm/uaccess.h>*/
#include <linux/fs.h> /* file_operations */
#include <linux/interrupt.h> /* request_irq */
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/clk.h> /* clk_get */
#include <asm/io.h> /* ioremap */
#include <mach/regs-clock.h>
#include <plat/regs-adc.h> /* S3C2410_ADCCON etc */
#include "s3c2410-adc.h"
#ifdef DEBUG /* I define it in Makefile */
/* KERN_INFO */
#define DPRINTK(fmt, ...) printk(KERN_DEBUG fmt, ##__VA_ARGS__)
#else
#define DPRINTK(fmt, ...)
#endif
#define DEVICE_NAME "adc"
#define ADCRAW_MINOR 1
static int adcMajor = 0; /* auto */
static int adcMinor = 0;
static int adc_nr_devs = 3; /* 3 for testing */
static void __iomem *base_addr;
static struct clk *adc_clock;
#define ADCCON (*(volatile unsigned long *)(base_addr + S3C2410_ADCCON)) //ADC control
#define ADCTSC (*(volatile unsigned long *)(base_addr + S3C2410_ADCTSC)) //ADC touch screen control
#define ADCDLY (*(volatile unsigned long *)(base_addr + S3C2410_ADCDLY)) //ADC start or Interval Delay
#define ADCDAT0 (*(volatile unsigned long *)(base_addr + S3C2410_ADCDAT0)) //ADC conversion data 0
#define ADCDAT1 (*(volatile unsigned long *)(base_addr + S3C2410_ADCDAT1)) //ADC conversion data 1
#define ADCUPDN (*(volatile unsigned long *)(base_addr + 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)
#define START_ADC_AIN(ch, prescale) do{
ADCCON = PRESCALE_EN | PRSCVL(prescale) | ADC_INPUT((ch)) ;
ADCCON |= ADC_START;} while(0)
typedef struct {
struct semaphore lock;
wait_queue_head_t wait;
int channel;
int prescale;
}ADC_DEV;
static ADC_DEV adcdev;
static struct cdev adc_cdev;
//DECLARE_MUTEX(ADC_LOCK);
static irqreturn_t adcdone_int_handler(int irq, void *dev_id, struct pt_regs *reg)
{
wake_up(&adcdev.wait);
return IRQ_HANDLED;
}
static ssize_t s3c2410_adc_write(struct file *file, const char *buffer, size_t count, loff_t * ppos)
{
int data;
if(count!=sizeof(data)){
//error input data size
DPRINTK("the size of input data must be %d\n", sizeof(data));
return 0;
}
if ( copy_from_user(&data, buffer, count) )
return -EFAULT; // by me
adcdev.channel=ADC_WRITE_GETCH(data); /* get the channel */
adcdev.prescale=ADC_WRITE_GETPRE(data); /* get the prescale */
DPRINTK("set adc channel=%d, prescale=0x%x\n", adcdev.channel, adcdev.prescale);
return count;
}
static ssize_t s3c2410_adc_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
int ret = 0;
if (down_interruptible(&adcdev.lock))
return -ERESTARTSYS;
START_ADC_AIN(adcdev.channel, adcdev.prescale);
interruptible_sleep_on(&adcdev.wait);
ret = ADCDAT0;
ret &= 0x3ff;
DPRINTK("AIN[%d] = 0x%04x, %d\n", adcdev.channel, ret, ADCCON & 0x80 ? 1:0);
if ( copy_to_user(buffer, (char *)&ret, sizeof(ret)) )
return -EFAULT; // by me
up(&adcdev.lock);
return sizeof(ret);
}
static int s3c2410_adc_open(struct inode *inode, struct file *filp)
{
init_MUTEX(&adcdev.lock);
init_waitqueue_head(&(adcdev.wait));
adcdev.channel=0;
adcdev.prescale=0xff;
//MOD_INC_USE_COUNT;
DPRINTK( "adc opened\n");
return 0;
}
static int s3c2410_adc_release(struct inode *inode, struct file *filp)
{
//MOD_DEC_USE_COUNT;
DPRINTK( "adc closed\n");
return 0;
}
static struct file_operations s3c2410_fops = {
.owner = THIS_MODULE,
.open = s3c2410_adc_open,
.read = s3c2410_adc_read,
.write = s3c2410_adc_write,
.release = s3c2410_adc_release,
};
int __init s3c2410_adc_init(void)
{
int ret;
dev_t devno = 0;
devno = MKDEV(adcMajor, adcMinor);
base_addr=ioremap(S3C2410_PA_ADC,0x20);
if (base_addr == NULL) {
printk(KERN_ERR "failed to remap register block\n");
return -ENOMEM;
}
/* test: ioremap: c4876000(kernel space) ADCCON: 3fc4 */
/* another test: ioremap: c48ee000 ADCCON: 3fc4 */
DPRINTK("ioremap: %x ADCCON: %x\n", (unsigned int)base_addr, (unsigned int)ADCCON);
adc_clock = clk_get(NULL, "adc");
if (!adc_clock) {
printk(KERN_ERR "failed to get adc clock source\n");
return -ENOENT;
}
clk_enable(adc_clock);
/* normal ADC */
ADCTSC = 0; //XP_PST(NOP_MODE);
//ret = request_irq(IRQ_ADC_DONE, adcdone_int_handler, SA_INTERRUPT, DEVICE_NAME, NULL);
ret = request_irq(IRQ_ADC, adcdone_int_handler, IRQF_SHARED, DEVICE_NAME, &adcdev);
if (ret) {
// return ret;
return -EAGAIN;
}
//ret = register_chrdev(0, DEVICE_NAME, &s3c2410_fops);
if (adcMajor) {
ret = register_chrdev_region(devno, adc_nr_devs, DEVICE_NAME);
} else {
ret = alloc_chrdev_region(&devno, adcMinor, adc_nr_devs, DEVICE_NAME);
adcMajor = MAJOR(devno);
}
if (ret < 0) {
printk(DEVICE_NAME " can't get major number\n");
return ret;
}
//adcMajor=ret;
cdev_init(&adc_cdev, &s3c2410_fops); /* init */
adc_cdev.owner = THIS_MODULE;
ret = cdev_add(&adc_cdev, devno, adc_nr_devs);
if (ret < 0) {
printk(KERN_ERR "failed to add adc");
return ret;
}
DPRINTK("init adc ok, major:%d\n", adcMajor);
printk (DEVICE_NAME"tinitialized\n");
return 0;
}
void __exit s3c2410_adc_exit(void)
{
//unregister_chrdev(adcMajor, DEVICE_NAME);
//free_irq(IRQ_ADC_DONE, NULL);
free_irq(IRQ_ADC, &adcdev);
iounmap(base_addr);
if (adc_clock) {
clk_disable(adc_clock);
clk_put(adc_clock);
adc_clock = NULL;
}
cdev_del(&adc_cdev);
unregister_chrdev_region(MKDEV(adcMajor, adcMinor), adc_nr_devs);
DPRINTK("exit adc ok\n");
}
module_init(s3c2410_adc_init);
module_exit(s3c2410_adc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Chiangchin Li");