s3c2410多通道adc驱动及测试程序

网上流行很多基于2410的ADC驱动及测试程序。本文所使用的是开发板光盘中自带的经过修改后的adc驱动。笔者在这个基础上再作一点修改。由于那个文件已经删除了版权信息(但还是能找到这些代码与网上流行的驱动的一些联系),这里也不知道如何添加了,可以肯定的是,它使用了GPL,这里公开源代码,也算是GPL了。

原来的代码默认使用ADC第0个通道,本文将添加ioctl接口,可以通过应用层的ioctl来选择多个通道。

与原来的代码相比,添加了如下几个方面:
1、添加头文件<linux/ioctl.h>,不过经测试,没有也可以通过编译。
2、修改原来的调试信息为:

1
2
3
4
5
6
7
#define DEBUG
#ifdef DEBUG /* define it in Makefile of somewhere */
/* KERN_INFO */
#define DPRINTK(fmt, ...) printk(KERN_DEBUG fmt, ##__VA_ARGS__)
#else
#define DPRINTK(fmt, ...)
#endif

这个便于查看调试信息。
3、添加ioctl相关宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* ioctl */
#ifndef u16
#define u16 unsigned short
#endif

#define ADC_IOC_MAGIC 0xd2

#define ADC_SET_CHANNEL _IOW(ADC_IOC_MAGIC, 0, u16)
#define ADC_SET_CLKDIV _IOW(ADC_IOC_MAGIC, 1, u16)

#define ADC_MAX_IOC 2 /* we only have 2 ioctl commands */
#define MAX_ADC  4 /* we have 4 adc chnnels */
/* end of ioctl */

4、添加ioctl接口:

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
/* ioctl发生错误,返回-1,并设置errno,这个便是驱动中ioctl中的那个。
     网上有资料说要返回-ENOTTY,不过个人认为这里返回-EINVAL更恰当一些。  
  */
static int s3c2410_adc_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
        if ((_IOC_TYPE(cmd) != ADC_IOC_MAGIC) || (_IOC_NR(cmd) > ADC_MAX_IOC))
                return -EINVAL;

        switch (cmd) {
        /* set channel */
        case ADC_SET_CHANNEL:
                arg &=3;  //多此一举??
                if (arg > 3)
                        arg = 3;
                adcdev.channel=arg;
                break;
        case ADC_SET_CLKDIV:
                arg &= 0xff; // ??
                if (arg > 0xff)
                        arg = 0xff;
                adcdev.prescale=arg;
                break;
        default:
                return -EINVAL;
                break;
        }
        return 0;
}

当然,也要在这个文件的file_operations结构体添加这个接口:

1
.ioctl   = s3c2410_adc_ioctl,

5、copy_to_user
原来的代码使用了sprintf将ADC转换的结果转换为字符串类型,不过却在后面添加一个“\n”,不知是何意。

1
2
3
4
//len = sprintf(str, "%d\n", value); // why '\n'?
len = sprintf(str, "%d", value);
……
copy_to_user(buffer, str, len);

也正是这个原因,在测试程序中要使用sscanf将字符串转换成整数类型才能得到正常的结果。 其它的修改不影响使用。
测试代码也简单,如下:

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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <linux/ioctl.h>

#define ADC "/dev/adc"

#ifdef DEBUG
#define debug(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#define debug(fmt, ...)
#endif

#ifndef u16
#define u16        unsigned short
#endif

// 控制字,与驱动一致
#define ADC_IOC_MAGIC        0xd2

#define ADC_SET_CHANNEL        _IOW(ADC_IOC_MAGIC, 0, u16)
#define ADC_SET_CLKDIV        _IOW(ADC_IOC_MAGIC, 1, u16)

// 特意的错误控制字
// test
#define AAA 333
// test end
int fd;

// 由于是死循环,需要捕获SIGINT信号
void sig_handle(int sig)
{
        //debug("you press ^C: %d/n", sig);
        close(fd);        /* we colse it here */
        exit(0);
}
int main(void)
{
        char buff[10];
        int val;
        int len;
        int i;
        signal(SIGINT, sig_handle);

        debug("Test of ADC. ^C to exit/n");
        fd = open(ADC, O_RDWR);
        if (fd < 0){
                perror("Open /dev/adc failed");
                exit(1);
        }
        // test
        /* it will return -EINVAL(which specified in ADC driver) */
        if (ioctl(fd, AAA,0) < 0)
                perror("ioctl");

        while (1) {
                /* we have 4 channels ADC*/
                for (i=0; i<4; i++) {
                        ioctl(fd, ADC_SET_CHANNEL, i);
                        len = read(fd, buff,sizeof(buff));
                        if (len < 0) {
                                perror("read adc device failed");
                                exit(0);
                        } else {
                                buff[len] = '/0';
                                sscanf(buff, "%d", &val);
                                printf("read AIN[%d]: %d value:%d/n", i, len, val);
                        }
                        sleep(1); // 每隔1秒采1个通道
                }
                //sleep(1); // 每隔1秒采集4个通道
        }

       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
./myadc
read AIN[0]: 3 value:629
read AIN[1]: 3 value:316
read AIN[2]: 3 value:257
read AIN[3]: 3 value:303
read AIN[0]: 1 value:0
read AIN[1]: 1 value:0
read AIN[2]: 3 value:259
read AIN[3]: 3 value:297
……
read AIN[1]: 3 value:468
read AIN[2]: 3 value:299
read AIN[3]: 3 value:327
read AIN[0]: 3 value:629
read AIN[1]: 1 value:0
……
read AIN[0]: 3 value:629
read AIN[1]: 3 value:287
read AIN[2]: 3 value:273
read AIN[3]: 1 value:0
read AIN[0]: 3 value:629
read AIN[1]: 3 value:296
read AIN[2]: 3 value:271
read AIN[3]: 3 value:306
read AIN[0]: 3 value:629
read AIN[1]: 3 value:281
read AIN[2]: 1 value:0
read AIN[3]: 3 value:269

上面显示0的就是用导线将开发板上几个ADC通道分别接触板子上GND出现的。如果接VCC,值应该是1023。就是说,ADC的值为0~1023,ADC驱动将转换结果与0x3ff相与,这可以看datasheet。
至于测试代码中使用sscanf,是因为驱动中使用了sprintf。
驱动的debug信息如下(举例,与前面显示无直接关系):

1
2
3
4
5
6
7
8
9
10
11
# dmesg | tail
AIN[3] = 0x012c, 1
AIN[0] = 0x0275, 1
AIN[1] = 0x0146, 1
AIN[2] = 0x0126, 1
AIN[3] = 0x0150, 1
AIN[0] = 0x0275, 1
AIN[1] = 0x012d, 1
AIN[2] = 0x0109, 1
AIN[3] = 0x0126, 1
adc closed

本人虽然学过一段时间的单片机,但是对于电机控制、PWM、ADC等等兴趣不大,了解也不多。其实我也不知这个多通道的ADC能做什么。
文中ioctl控制字参考《ARM徽处理器与应用开发》一书,电子工业出版社出版。
完整的驱动程序如下:

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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#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/ioctl.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>

#define DEBUG
#ifdef DEBUG /* define it in Makefile of somewhere */
/* KERN_INFO */
#define DPRINTK(fmt, ...) printk(KERN_DEBUG fmt, ##__VA_ARGS__)
#else
#define DPRINTK(fmt, ...)
#endif

#define DEVICE_NAME        "adc"

static void __iomem *base_addr;

typedef struct {
        wait_queue_head_t wait;
        int channel;
        int prescale;
}ADC_DEV;

DECLARE_MUTEX(ADC_LOCK);
/* you may need to change is to the following */
//DEFINE_SEMAPHORE(ADC_LOCK);

static int OwnADC = 0;

static ADC_DEV adcdev;
static volatile int ev_adc = 0;
static int adc_data;

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)
/* ioctl */
#ifndef u16
#define u16        unsigned short
#endif

#define ADC_IOC_MAGIC        0xd2

#define ADC_SET_CHANNEL        _IOW(ADC_IOC_MAGIC, 0, u16)
#define ADC_SET_CLKDIV        _IOW(ADC_IOC_MAGIC, 1, u16)

#define ADC_MAX_IOC        2 /* we only have 2 ioctl commands */
#define MAX_ADC                4 /* we have 4 adc chnnels */
/* end of ioctl */

static irqreturn_t adcdone_int_handler(int irq, void *dev_id)
{
        if (OwnADC) {
                adc_data = ADCDAT0 & 0x3ff;

                ev_adc = 1;
                wake_up_interruptible(&adcdev.wait);
        }

        return IRQ_HANDLED;
}

static ssize_t s3c2410_adc_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
        char str[20];
        int value;
        size_t len;
        if (down_trylock(&ADC_LOCK) == 0) {
                OwnADC = 1;
                START_ADC_AIN(adcdev.channel, adcdev.prescale);
                wait_event_interruptible(adcdev.wait, ev_adc);

                ev_adc = 0;

                DPRINTK("AIN[%d] = 0x%04x, %d/n", adcdev.channel, adc_data, ADCCON & 0x80 ? 1:0);

                value = adc_data;
                #if 0
                sprintf(str,"%5d", adc_data);
                copy_to_user(buffer, (char *)&adc_data, sizeof(adc_data));
                #endif
                OwnADC = 0;
                up(&ADC_LOCK);
        } else {
                value = -1;
        

        //len = sprintf(str, "%d/n", value); // why '/n'?
        len = sprintf(str, "%d", value);
        if (count >= len) {
                int r = copy_to_user(buffer, str, len);
                return r ? r : len;
        } else {
                return -EINVAL;
        }
}

static int s3c2410_adc_open(struct inode *inode, struct file *filp)
{
        init_waitqueue_head(&(adcdev.wait));

        adcdev.channel=0;
        adcdev.prescale=0xff;

        DPRINTK( "adc opened/n");
        return 0;
}

static int s3c2410_adc_release(struct inode *inode, struct file *filp)
{
        DPRINTK( "adc closed/n");
        return 0;
}

static int s3c2410_adc_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
        if ((_IOC_TYPE(cmd) != ADC_IOC_MAGIC) || (_IOC_NR(cmd) > ADC_MAX_IOC))
                return -EINVAL;

        switch (cmd) {
        /* set channel */
        case ADC_SET_CHANNEL:
                arg &=3; //??
                if (arg > 3)
                        arg = 3;
                adcdev.channel=arg;
                break;
        case ADC_SET_CLKDIV:
                arg &= 0xff; // ??
                if (arg > 0xff)
                        arg = 0xff;
                adcdev.prescale=arg;
                break;
        default:
                return -EINVAL;
                break;
        }
        return 0;
}
static struct file_operations dev_fops = {
        .owner   = THIS_MODULE,
        .open    = s3c2410_adc_open,
        .read    = s3c2410_adc_read,
        .release = s3c2410_adc_release,
        .ioctl   = s3c2410_adc_ioctl,
};

static struct miscdevice misc = {
        .minor = MISC_DYNAMIC_MINOR,
        .name  = DEVICE_NAME,
        .fops  = &dev_fops,
};

static int __init dev_init(void)
{
        int ret;

        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 */
        DPRINTK("ioremap: %x ADCCON: %x/n", base_addr, 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;

        ret = request_irq(IRQ_ADC, adcdone_int_handler, IRQF_SHARED, DEVICE_NAME, &adcdev);
        if (ret) {
                iounmap(base_addr);
                return ret;
        }

        ret = misc_register(&misc);

        printk (DEVICE_NAME"/tinitialized/n");
        return ret;
}

static void __exit dev_exit(void)
{
        free_irq(IRQ_ADC, &adcdev);
        iounmap(base_addr);

        if (adc_clock) {
                clk_disable(adc_clock);
                clk_put(adc_clock);
                adc_clock = NULL;
        }

        misc_deregister(&misc);
}

EXPORT_SYMBOL(ADC_LOCK);
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");

注:文中代码注释出现了中文,是为了方便文章的阅读(根据经验,有清楚注释的代码才算代码),在实际代码中是没有中文注释的。
后续之事:修改网上流行的adc驱动,使用write或ioctl从应用层传递数据到驱动。