《嵌入式Linux开发实用教程》——4.2 字符设备驱动

    xiaoxiao2024-03-17  125

    本节书摘来自异步社区《嵌入式Linux开发实用教程》一书中的第4章,第4.2节,作者 朱兆祺,李强,袁晋蓉,更多章节内容可以访问云栖社区“异步社区”公众号查看。

    4.2 字符设备驱动

    Linux操作系统将所有的设备都会看成是文件,因此当我们需要访问设备时,都是通过操作文件的方式进行访问。对字符设备的读写是以字节为单位进行的。

    对字符设备驱动程序的学习过程,主要以两个具有代表性且在OK6410开发平台可实践性的字符驱动展开分析,分别为LED驱动程序、ADC驱动程序。

    4.2.1 LED驱动程序设计

    为了展现LED的裸板程序和基于Linux系统的LED驱动程序的区别与减少难度梯度,在写LED驱动程序之前很有必要先看一下LED的裸板程序是怎样设计的。

    1.LED裸板程序

    OK6410开发平台中有4个LED灯,原理图如图4.1所示。

    从图4.1中可知,4个LED采用的是共阳极连接方式,GPM0~GPM3分别控制着LED1~LED4。而GPMCON寄存器地址为:0x7F008820;GPMDAT寄存器地址为:0x7F008824。那么GPM中3个寄存器宏定义为:

    /*=============================================================== **  基地址的定义 ===============================================================*/ #define  AHB_BASE    (0x7F000000) /**************************************************************** ** GPX的地址定义 ****************************************************************/ #define  GPX_BASE    (AHB_BASE+0x08000) …… /**************************************************************** **    GPM寄存器地址定义 ****************************************************************/ #define  GPMCON    (*(volatile unsigned long *)(GPX_BASE + 0x0820)) #define  GPMDAT    (*(volatile unsigned long *)(GPX_BASE + 0x0824)) #define  GPMPUD    (*(volatile unsigned long *)(GPX_BASE + 0x0828))

    将GPM0~GPM3设置为输出功能:

    /* GPM0,1,2,3设为输出引脚 */ /* **  每一个GPXCON的引脚有 4位二进制进行控制 **  0000-输入   0001-输出 */ GPMCON = 0x1111;

    点亮LED1,则是让GPM3~GPM0输出:1110。

    GPMDAT = 0x0e;

    点亮LED3,则是让GPM3~GPM0输出:1011。

    GPMDAT = 0x0b;

    2.LED驱动程序

    有了LED裸板程序的基础,那么移植到Linux系统LED驱动设备程序的难度也不会很大了。但是在Linux中,特别注意《s3c6410用户手册》提供的GPM寄存器地址不能直接用于Linux中。

    在一般情况下,Linux系统中,进程的4GB(232)内存空间被划分成为两个部分:用户空间(3G)和内核空间(1GB),大小分别为0~3GB和3~4GB,如图4.2所示。

    在3~4GB之间的内核空间中,从低地址到高地址依次为:系统物理内存映射区、VMALLOC_OFFSET、vmalloc用来分配物理地址非连续的内存空间、8KB隔离带、高端内存永久映射区、高端内存固定映射区。

    在通常情况下,进程只能访问用户空间的虚拟地址,不能访问内核空间。

    每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射的,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。

    在内核中,访问I/O内存之前,我们只有I/O内存的物理地址,这样是无法通过软件直接访问的,需要首先用ioremap()函数将设备所处的物理地址映射到内核虚拟地址空间(3GB~4GB)。然后才能根据映射所得到的内核虚拟地址范围,通过访问指令访问这些I/O内存资源。

    一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚拟地址空间内(通过页表),然后才能根据映射所得到的核心虚拟地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚拟地址空间(3GB~4GB)中,如下所示:

    void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

    iounmap函数用于取消ioremap()所做的映射,如下所示:

    void iounmap(void * addr);

    到这里应该明白,像GPMCON(0x7F00 8820)这个物理地址是不能直接操控的,必须通过映射到内核的虚拟地址中,才能进行操作。

    现在开始设计第一个LED驱动程序。

    字符驱动程序所要包含的头文件主要位于include/linux及/arch/arm/mach-s3c64xx /include/mach目录下,如下LED驱动程序所包含的头文件:

    /* * head file */ //moudle.h 包含了大量加载模块需要的函数和符号的定义 #include <linux/module.h> //kernel.h以便使用printk()等函数 #include <linux/kernel.h> //fs.h包含常用的数据结构,如struct file等 #include <linux/fs.h> //uaccess.h 包含copy_to_user()、copy_from_user()等函数 #include <linux/uaccess.h> //io.h 包含inl()、outl()、readl()、writel()等I/O操作函数 #include <linux/io.h> #include <linux/miscdevice.h> #include <linux/pci.h> //init.h来指定你的初始化和清理函数,例如:module_init(init_function)、module_exit(cleanup_function) #include <linux/init.h> #include <linux/delay.h> #include <linux/device.h> #include <linux/cdev.h> #include <linux/gpio.h> //irq.h中断与并发请求事件 #include <asm/irq.h> //下面这些头文件是I/O口在内核的虚拟映射地址,涉及I/O口的操作所必须包含的 //#include <mach/gpio.h> #include <mach/regs-gpio.h> #include <plat/gpio-cfg.h> #include <mach/hardware.h> #include <mach/map.h>

    上面所列出的头文件即是本次LED驱动程序所需要包含的头文件。

    #define DEVICE_NAME   "led" #define LED_MAJOR    240          /*主设备号*/

    这是LED驱动程序的驱动名称和主设备号。

    设备节点位于/dev目录下,如下所示,例举出了ubuntu系统/dev/vcs*的设备节点:

    zhuzhaoqi@zhuzhaoqi-desktop:~$ ls -l /dev/vcs* …… crw-rw---- 1 root tty 7,  7 2013-04-09 20:56 /dev/vcs7 crw-rw---- 1 root tty 7, 128 2013-04-09 20:56 /dev/vcsa ……

    /dev/vcs7设备节点的主设备号为:7,次设备号为:7;/dev/vcsa设备节点的主设备号为:7,次设备号为:128。

    #define LED_ON      0 #define LED_OFF     1

    这是LED灯打开或者关闭的宏定义,由于OK6410开发平台的4个LED是共阳连接,所以输出1即为熄灭LED,输出0为点亮LED。

    字符驱动程序中实现了open、close、read、write等系统调用。

    open函数指针的声明位于fs.h的file_operations结构体中,如下所示:

    struct file_operations {   …… int (*open) (struct inode* , struct file *);   …… };

    open函数指针的回调函数led_open()完成的任务是设置GPM的输出模式。

    static int led_open(struct inode *inode,struct file *file) {   unsigned int i;   /*设置GPM0~GPM3为输出模式*/   for (i = 0; i < 4; i++)   {     s3c_gpio_cfgpin(S3C64XX_GPM(i),S3C_GPIO_OUTPUT); printk("The GPMCON %x is %x \n",i,s3c_gpio_getcfg(S3C64XX_GPM(i)) );   }   printk("Led open... \n");   return 0; }

    s3c_gpio_cfgpin()函数原型位于gpio-cfg.h中,如下:

    extern int s3c_gpio_cfgpin(unsigned int pin, unsigned int to);

    内核对这个函数是这样注释的:s3c_gpio_cfgpin()函数用于改变引脚的GPIO功能。参数pin是GPIO的引脚名称,参数to是需要将GPIO这个引脚设置成为的功能。

    GPIO的名称在arch/arm/mach-s3c6400/include/mach/gpio.h进行了宏定义:

    /* S3C64XX GPIO number definitions. */ #define S3C64XX_GPA(_nr)  (S3C64XX_GPIO_A_START + (_nr)) #define S3C64XX_GPB(_nr)  (S3C64XX_GPIO_B_START + (_nr)) #define S3C64XX_GPC(_nr)  (S3C64XX_GPIO_C_START + (_nr)) #define S3C64XX_GPD(_nr)  (S3C64XX_GPIO_D_START + (_nr)) #define S3C64XX_GPE(_nr)  (S3C64XX_GPIO_E_START + (_nr)) #define S3C64XX_GPF(_nr)  (S3C64XX_GPIO_F_START + (_nr)) #define S3C64XX_GPG(_nr)  (S3C64XX_GPIO_G_START + (_nr)) #define S3C64XX_GPH(_nr)  (S3C64XX_GPIO_H_START + (_nr)) #define S3C64XX_GPI(_nr)  (S3C64XX_GPIO_I_START + (_nr)) #define S3C64XX_GPJ(_nr)  (S3C64XX_GPIO_J_START + (_nr)) #define S3C64XX_GPK(_nr)  (S3C64XX_GPIO_K_START + (_nr)) #define S3C64XX_GPL(_nr)  (S3C64XX_GPIO_L_START + (_nr)) #define S3C64XX_GPM(_nr)  (S3C64XX_GPIO_M_START + (_nr)) #define S3C64XX_GPN(_nr)  (S3C64XX_GPIO_N_START + (_nr)) #define S3C64XX_GPO(_nr)  (S3C64XX_GPIO_O_START + (_nr)) #define S3C64XX_GPP(_nr)  (S3C64XX_GPIO_P_START + (_nr)) #define S3C64XX_GPQ(_nr)  (S3C64XX_GPIO_Q_START + (_nr))

    S3C64XX_GPIO_M_START的定义如下:

    enum s3c_gpio_number {   S3C64XX_GPIO_A_START = 0,   S3C64XX_GPIO_B_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_A),   S3C64XX_GPIO_C_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_B),   S3C64XX_GPIO_D_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_C),   S3C64XX_GPIO_E_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_D),   S3C64XX_GPIO_F_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_E),   S3C64XX_GPIO_G_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_F),   S3C64XX_GPIO_H_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_G),   S3C64XX_GPIO_I_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_H),   S3C64XX_GPIO_J_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_I),   S3C64XX_GPIO_K_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_J),   S3C64XX_GPIO_L_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_K), S3C64XX_GPIO_M_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_L),   S3C64XX_GPIO_N_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_M),   S3C64XX_GPIO_O_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_N),   S3C64XX_GPIO_P_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_O),   S3C64XX_GPIO_Q_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_P), };

    S3C64XX_GPIO_NEXT的定义:

    #define S3C64XX_GPIO_NEXT(__gpio) \   ((__gpio##_START) + (__gpio##_NR) + CONFIG_S3C_GPIO_SPACE + 1)

    宏定义一层一层很多,但是通过这个设置,可以很方便地选择想要的任何一个GPIO口进行操作。

    GPIO功能设置位于gpio-cfg.h中:

    #define S3C_GPIO_SPECIAL_MARK  (0xfffffff0) #define S3C_GPIO_SPECIAL(x)  (S3C_GPIO_SPECIAL_MARK | (x)) /* Defines for generic pin configurations */ #define S3C_GPIO_INPUT  (S3C_GPIO_SPECIAL(0)) #define S3C_GPIO_OUTPUT  (S3C_GPIO_SPECIAL(1)) #define S3C_GPIO_SFN(x)  (S3C_GPIO_SPECIAL(x))

    通过上面的宏定义可知,GPIO的引脚功能有输入、输出,和你想要的任何可以实现的功能设置,S3C_GPIO_SFN(x)这个函数即是通过设定x的值,实现任何存在功能的设置。如果要设置GPM0~GPM3为输出功能,则:

    for (i = 0; i < 4; i++) { s3c_gpio_cfgpin(S3C64XX_GPM(i),S3C_GPIO_OUTPUT); }

    通过这样的操作,设置就显得比较简洁实用。

    s3c_gpio_getcfg(S3C64XX_GPM(i))

    这行代码的作用是获取GMP(argv)的当前值。这个函数的原型在include/linux/gpio.h中:

    static inline void gpio_get_value(unsigned int gpio) {   __gpio_get_value(gpio); }

    完成端口模式设定,接下来的程序是完成LED操作。在fs.h的file_operations结构体中,有unlocked_ioctl函数指针的声明,如下所示:

    struct file_operations { ……   long (*unlocked_ioctl) (struct file *,unsigned int,unsigned long); ……  };

    unlocked_ioctl函数指针所要回调的led_ioctl()函数即是需要实现应用层对LED1~LED4的控制操作。

    static long led_ioctl ( struct file *file, unsigned int cmd, \             unsigned long argv ) {   if (argv > 4) {     return -EINVAL;   } printk("LED ioctl... \n"); /* 获取应用层的操作 */   switch(cmd) { /* 如果是点亮LED(argv) */   case LED_ON:     gpio_set_value(S3C64XX_GPM(argv),0);     printk("LED ON \n");   printk( "S3C64XX_GPM(i) = %x\n",gpio_get_value(S3C64XX_GPM(argv)) );     return 0; /* 如果是熄灭LED(argv) */   case LED_OFF:     gpio_set_value(S3C64XX_GPM(argv),1);     printk("LED OFF \n");     printk( "S3C64XX_GPM(i) = %x \n",gpio_get_value(S3C64XX_GPM(argv)) );     return 0;   default:     return -EINVAL;   } }

    本函数调用了GPIO端口值设定函数。

    gpio_set_value(S3C64XX_GPM(argv),1);

    这是设定GMP(argv)输出为1。函数的原型位于include/linux/gpio.h中:

    static inline void gpio_set_value(unsigned int gpio, int value) {   __gpio_set_value(gpio, value); }

    release函数指针所要回调的函数led_release ()函数:

    static int led_release(struct inode *inode,struct file *file) {     printk("zhuzhaoqi >>> s3c6410_led release \n");     return 0; }

    这是驱动程序的核心控制,各个函数指针所对应的回调函数:

    struct file_operations led_fops = {     .owner     = THIS_MODULE,     .open      = led_open,     .unlocked_ioctl = led_ioctl,     .release    = led_release, };

    由于Linux3.8.3内核中没有ioctl函数指针,取而代之的是unlocked_ioctl函数指针实现对led_ioctl()函数的回调。

    驱动程序的加载分为静态加载和动态加载,将驱动程序编译进内核称为静态加载,将驱动程序编译成模块,使用时再加载称为动态加载。动态加载模块的扩展名为:.ko,使用insmod命令进行加载,使用rmmod命令进行卸载。

    static int __init led_init(void) {     int rc;     printk("LEDinit... \n");     rc = register_chrdev(LED_MAJOR,"led",&led_fops);     if (rc < 0)     {         printk("register %s char dev error\n","led");         return -1;     }     printk("OK!\n");     return 0; }

    _init修饰词对内核是一种暗示,表明该初始化函数仅仅在初始化期间使用,在模块装载之后,模块装载器就会将初始化函数释放掉,这样就能将初始化函数所占用的内存释放出来以作他用。

    当使用insmod命令加载LED驱动模块时,led_init()初始化函数将被调用,向内核注册LED驱动程序。

    static void __exit led_exit(void) {     unregister_chrdev(LED_MAJOR,"led");     printk("LED exit...\n"); }

    _exit这个修饰词告诉内核这个退出函数仅仅用于模块卸载,并且仅仅能在模块卸载或者系统关闭时被调用。

    当使用rmmod命令卸载LED驱动模块时,led_exit ()清除函数将被调用,向内核注册LED驱动程序。

    module_init(led_init); module_exit(led_exit);

    module_init和module_exit是强制性使用的,这个宏会在模块的目标代码中增加一个特殊的段,用于说明函数所在位置。如果没有这个宏,则初始化函数和退出函数永远不会被调用。

    MODULE_LICENSE("GPL");

    如果没有声明LICENSE,模块被加载时,会给出处理内核被污染(kernel taint)的警告。如果在zzq_led.c中没有许可证(LICENSE),则会给出如下提示:

    [YJR@zhuzhaoqi 3.8.3]# insmod zzq_led.ko zzq_led: module license 'unspecified' taints kernel. Disabling lock debugging due to kernel taint

    Linux遵循GNU通用公共许可证(GPL),GPL是由自由软件基金会为GNU项目设计,它允许任何人对其重新发布甚至销售。

    当然,也许程序还会有驱动程序的作者和描述信息:

    MODULE_AUTHOR("zhuzhaoqi jxlgzzq@163.com"); MODULE_DESCRIPTION("OK6410(S3C6410) LED Driver");

    完成驱动程序的设计之后,将zzq_led.c驱动程序放置于/drivers/char目录下,打开Makefile文件:

    zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3/drivers/char$ gedit Makefile

    在Makefile中添加LED驱动:

    obj-m              += zzq_led.o

    回到内核的根目录执行make modules命令生成LED驱动模块:

    zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3$ make modules ……  CC [M] drivers/char/zzq_led.o ……

    编译完成之后在/drivers/char目录下会生成zzq_led.ko模块,将其拷贝到文件系统下面的/lib/modules/3.8.3(如果没有3.8.3目录,则建立)目录下。

    加载LED驱动模块:

    [YJR@zhuzhaoqi]\# cd lib/module/3.8.3/ [YJR@zhuzhaoqi]\# ls zzq_led.ko [YJR@zhuzhaoqi]\# insmod zzq_led.ko LED init... OK!

    根据信息输出可知加载zzq_led.ko驱动模块成功。通过lsmod查看加载模块:

    [YJR@zhuzhaoqi]\# lsmod zzq_led 1548 0 - Live 0xbf000000

    在/dev目录下建立设备文件,进行如下操作:

    [YJR@zhuzhaoqi]\# mknod /dev/led c 240 0

    是否建立成功,可以查看/dev下的节点得知:

    [YJR@zhuzhaoqi]\# ls /dev/l* /dev/led      /dev/log      /dev/loop-control

    说明LED设备文件已经成功建立。

    3.LED应用程序

    驱动程序需要应用程序对其操控。程序如下:

    #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define  LED_ON   0 #define  LED_OFF  1 /* * LED 操作说明信息输出 */ void usage(char *exename) {   printf("How to use: \n");   printf("  %s <LED Number><on/off> \n", exename);   printf("  LED Number = 1, 2, 3 or 4 \n"); } /*  * 应用程序主函数 */ int main(int argc, char *argv[]) {   unsigned int led_number;   if (argc != 3) {     goto err;   }   int fd = open("/dev/led",2,0777);   if (fd < 0) {     printf("Can't open /dev/led \n");     return -1;   }   printf("open /dev/led ok ... \n");   led_number = strtoul(argv[1], 0, 0) - 1;   if (led_number > 3) {     goto err; }   /* LED ON */   if (!strcmp(argv[2], "on")) {     ioctl(fd, LED_ON, led_number);   }   /* LED OFF */   else if (!strcmp(argv[2], "off")) {     ioctl(fd, LED_OFF, led_number);   }   else {     goto err;   }   close(fd);   return 0; err:   if (fd > 0) {     close(fd);   }   usage(argv[0]);   return -1; }

    在main()函数中,涉及了open()函数,其原型如下:

    int open( const char * pathname,int flags, mode_t mode);

    当然,很多open函数中的入口参数也只有2个,原型如下:

    int open( const char * pathname, int flags);

    第一个参数pathname是一个指向将要打开的设备文件途径的字符串。

    第二个参数flags是打开文件所能使用的旗标,常用的几种旗标有:

    O_RDONLY:以只读方式打开文件 O_WRONLY:以只写方式打开文件 O_RDWR:以可读写方式打开文件

    上述3种常用的旗标是互斥使用,但可与其他的旗标进行或运算符组合。

    第3个参数mode是使用该文件的权限。比如777、755等。

    通过这个应用程序实现对LED驱动程序的控制,为了更加方便快捷地编译这个应用程序,为其写一个Makefile文件,如下所示:

    #交叉编译链安装路径 CC = /usr/local/arm/4.4.1/bin/arm-linux-gcc zzq_led_app:zzq_led_app.o     $(CC) -o zzq_led_appzzq_led_app.o zzq_led_app.o:zzq_led_app.c     $(CC) -c zzq_led_app.c clean :     rm zzq_led_app.o zzq_led_app

    执行Makefile之后会生成zzq_led_app可执行应用文件,如下:

    zhuzhaoqi@zhuzhaoqi-desktop:~/LDD/linux-3.8.3/zzq_led$ make /usr/local/arm/4.4.1/bin/arm-linux-gcc -c zzq_led_app.c /usr/local/arm/4.4.1/bin/arm-linux-gcc -o zzq_led_app zzq_led_app.o zhuzhaoqi@zhuzhaoqi-desktop:~/LDD/linux-3.8.3/zzq_led$ ls Makefile zzq_led_app zzq_led_app.c zzq_led_app.o zzq_led.c

    将生成的zzq_led_app可执行应用文件拷贝到根文件系统的/usr/bin目录下,执行应用文件,如下操作:

    [YJR@zhuzhaoqi]\# ./zzq_led_app How to use:   ./zzq_led_app <LED Number><on/off>   LED Number = 1, 2, 3 or 4

    根据信息提示可以进行对LED驱动程序的控制,点亮LED1,则如下:

    [YJR@zhuzhaoqi]\# ./zzq_led_app 1 on The GPMCON 0 is fffffff1 The GPMCON 1 is fffffff1 The GPMCON 2 is fffffff1 The GPMCON 3 is fffffff1 zhuzhaoqi >>> LED open... LED ioctl... LED ON S3C64XX_GPM(i) = 0 LED release... open /dev/led ok ...

    此时可以看到LED1点亮。

    4.2.2 ADC驱动程序设计

    A/D转换即是将模拟量转换为数字量,在物联网迅速发展的今天,作为物联网的感知前端传感器也随之迅速更新,压力、温度、湿度等众多模拟信号的处理都需要涉及A/D转换,因此A/D驱动程序在学习嵌入式中占据着重要地位。

    1.S3C6410的ADC控制寄存器简介

    S3C6410控制芯片自带有4路独立专用A/D转换通道,如图4.3所示。

    通过三星公司提供的《S3C6410用户手册》可知,ADCCON为ADC控制寄存器,地址为:0x7E00 B0000。ADCCON的复位值为:0x3FC4,即为:0011 1111 1100 0100。

    #define S3C_ADCREG(x)       (x) #define S3C_ADCCON       S3C_ADCREG(0x00)

    ADCCON控制寄存器具有16位,每一位都能通过赋值来实现其相对应的功能。

    ADCCON[0]:ENABLE_START,A/D 转换开始启用。如果READ_START 启用,这个值是无效的。ENABLE_START = 0,无行动;ENABLE_START = 1,A/D 转换开始和该位被清理后开启。ADCCON[0]的复位值为0,即复位之后默认为无行动。

    #define S3C_ADCCON_NO_ENABLE_START    (0<<0) #define S3C_ADCCON_ENABLE_START    (1<<0)

    ADCCON[1]:READ_START,A/D 转换开始读取。READ_START = 0,禁用开始读操作;READ_START = 1,启动开始读操作。ADCCON[1]的复位值为0,禁用开始读操作。

    #define S3C_ADCCON_NO_READ_START    (0<<1) #define S3C_ADCCON_READ_START    (1<<1)

    ADCCON[2]:STDBM,待机模式选择。STDBM = 0,正常运作模式;STDBM = 1,待机模式。ADCCON[2]的复位值为1,待机模式。

    #define S3C_ADCCON_RUN    (0<<2) #define S3C_ADCCON_STDBM    (1<<2)

    ADCCON[5:3]:SEL_MUX,模拟输入通道选择。SEL_MUX = 000,AIN0;SEL_MUX = 001,AIN1;SEL_MUX = 010,AIN2;SEL_MUX = 011,AIN3;SEL_MUX = 100,YM;SEL_MUX = 101,YP;SEL_MUX = 110,XM;SEL_MUX = 111,XP。ADCCON[5:3]的复位值为000,选用AIN0通道。

    #define S3C_ADCCON_RESSEL_10BIT_1  (0x0<<3) #define S3C_ADCCON_RESSEL_12BIT_1  (0x1<<3) #define S3C_ADCCON_MUXMASK    (0x7<<3) #define S3C_ADCCON_SELMUX(x)    (((x)&0x7)<<3) //任意通道的选择

    ADCCON[13:6]:PRSCVL,ADC 预定标器值0xFF。数据值:5~255。ADCCON[13:6]的复位值为1111 1111,即为0xFF。

    #define S3C_ADCCON_PRSCVL(x)    (((x)&0xFF)<<6) // 任意值设定 #define S3C_ADCCON_PRSCVLMASK    (0xFF<<6) //复位值

    ADCCON[14]:PRSCEN,ADC预定标器启动。PRSCEN = 0,禁用;PRSCEN = 0,启用。ADCCON[14]的复位值为0,禁用ADC预定标器。

    #define S3C_ADCCON_NO_PRSCEN    (0<<14) #define S3C_ADCCON_PRSCEN    (1<<14)

    ADCCON[15]:ECFLG,转换的结束标记(只读)。ECFLG = 0,A/D 转换的过程中;ECFLG = 1,A/D 转换结束。ADCCON[15]的复位值为0,A/D 转换的过程中。

    #define S3C_ADCCON_ECFLG_ING    (0<<15) #define S3C_ADCCON_ECFLG    (1<<15)

    ADCDAT0寄存器为ADC 的数据转换寄存器。地址为:0x7E00B00C。

    ADCDAT0[9:0]:XPDATA,X 坐标的数据转换(包括正常的ADC 的转换数据值)。数据值: 0x000~0x3FF。

    ADCDAT0[11:10]:保留。当启用12位AD时作为转换数据值使用。

    #define S3C_ADCDAT0_XPDATA_MASK    (0x03FF) #define S3C_ADCDAT0_XPDATA_MASK_12BIT  (0x0FFF)

    上面所介绍的是专用A/D转换通道常用寄存器,LCD触摸屏A/D转换有另外的A/D通道。

    2.ADC驱动程序

    A/D转化驱动由于也属于字符设备驱动,所以其程序设计流程和LED驱动大体一致。在linux-3.8.3/drivers/char目录下新建zzqadc.c驱动文件,当然也可写好之后再拷贝到linux-3.8.3/ drivers/char目录下。

    zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3/drivers/char$ vim zzqadc.c

    头文件是必不可少的,A/D驱动程序所要包含的头文件如下所示:

    #include <linux/module.h> #include <linux/kernel.h> #include <linux/slab.h> #include <linux/input.h> #include <linux/init.h> #include <linux/errno.h> #include <linux/serio.h> #include <linux/delay.h> #include <linux/clk.h> #include <linux/sched.h> #include <linux/cdev.h> #include <linux/miscdevice.h> #include <asm/io.h> #include <asm/irq.h> #include <asm/uaccess.h> #include <mach/map.h> #include <mach/regs-clock.h> #include <mach/regs-gpio.h> #include <plat/regs-adc.h>

    与LED驱动程序所包含的头文件相比较,多了ADC专用的头文件,如regs-adc.h,这个头文件位于linux-3.8.3/arch/arm/plat-samsung/include/plat目录下。

    static void __iomem *base_addr; static struct clk *adc_clock; #define  __ADCREG(name)  (*(unsigned long int *)(base_addr + name))

    自从linux-2.6.9版本开始便把_iomem加入内核,_iomem是表示指向一个I/O的内存空间。将_iomem加入linux,主要是考虑到驱动程序的通用性。由于不同的CPU体系结构对I/O空间的表示可能不同,但是当使用_iomem时,就会忽略对变量的检查,因为_iomem使用的是void。

    #define  S3C_ADCREG(x)   (x) #define  S3C_ADCCON     S3C_ADCREG(0x00) #define  S3C_ADCDAT0    S3C_ADCREG(0x0C) /* ADC contrl */ #define  ADCCON       _ADCREG(S3C_ADCCON) /* read the ADdata */ #define  ADCDAT0      _ADCREG(S3C_ADCCON)

    声明ADC控制寄存器的地址。

    /* The set of ADCCON */ #define  S3C_ADCCON_ENABLE_START      (1 << 0) #define  S3C_ADCCON_READ_START       (1 << 1) #define  S3C_ADCCON_RUN          (0 << 2) #define  S3C_ADCCON_STDBM         (1 << 2) #define  S3C_ADCCON_SELMUX(x)       ( ((x)&0x7) << 3 ) #define  S3C_ADCCON_PRSCVL(x)       ( ((x)&0xFF) << 6 ) #define  S3C_ADCCON_PRSCEN         (1 << 14) #define  S3C_ADCCON_ECFLG         (1 << 15) /* The set of ADCDAT0 */ #define  S3C_ADCDAT0_XPDATA_MASK      (0x03FF) #define  S3C_ADCDAT0_XPDATA_MASK_12BIT  (0x0FFF)

    根据上一小节对ADCCON和ADCDAT0的介绍,可以很容易写出上面的宏定义。

    在使用ADC之前,先得对ADC进行初始化设置,由于OK6410开发平台自带的A/D电压采样电路选用的是AIN0通道,则这里需要对AIN0进行初始化。初始化阶段需要完成的事情为:A/D 转换开始和该位被清理后开启、正常运作模式、模拟输入通道选择AIN0、ADC 预定标器值0xFF、ADC预定标器启动。

    /* * AIN0 init  */ static int adc_init(void) {   ADCCON = S3C_ADCCON_PRSCEN | S3C_ADCCON_PRSCVL(0xFF) | \  S3C_ADCCON_SELMUX(0x00) | S3C_ADCCON_RUN; ADCCON |=S3C_ADCCON_ENABLE_START;   return 0; }

    open函数指针的实现函数adc_open():

    /* * open dev */ static int adc_open(struct inode *inode, struct file *filp) {   adc_init();   return 0; }

    release函数指针的实现函数adc_release():

    /* * release dev */ static int adc_release(struct inode *inode,struct file *filp) {   return 0; }

    read()函数指针的实现函数adc_read(),这个函数的作用是读取ADC采样数据。

    /* * adc_read */ static ssize_t adc_read(struct file *filp, char __user *buff, size_t size, loff_t *ppos) {   ADCCON |= S3C_ADCCON_READ_START;   /* check the adc Enabled ,The [0] is low*/   while(ADCCON & 0x01);   /* check adc change end */   while(!(ADCCON & 0x8000));    /* return the data of adc */   return (ADCDAT0 & S3C_ADCDAT0_XPDATA_MASK); }

    ADC驱动程序的核心控制部分:

    static struct file_operations dev_fops = {   .owner  = THIS_MODULE,   .open  = adc_open,   .release = adc_release,   .read  = adc_read, }; static struct miscdevice misc = {   .minor = MISC_DYNAMIC_MINOR,   .name = “zzqadc“,   .fops = &dev_fops, };

    加载insmod驱动程序,如下所示:

    static int __init dev_init() {   int ret;   /* Address Mapping */   base_addr = ioremap(SAMSUNG_PA_ADC,0X20);   if(base_addr == NULL)   {     printk(KERN_ERR"failed to remap \n");     return -ENOMEM;   }   /* Enabld acd clock */   adc_clock = clk_get(NULL,"adc");   if(!adc_clock)   {     printk(KERN_ERR"failed to get adc clock \n");     return -ENOENT;   }   clk_enable(adc_clock);   ret = misc_register(&misc);   printk("dev_init return ret: %d \n", ret);   return ret; }

    加载insmod驱动程序,这里使用到了ioremap()函数。在内核驱动程序的初始化阶段,通过ioremap()函数将物理地址映射到内核虚拟空间;在驱动程序的mmap系统调用中,使用remap_page_range()函数将该块ROM映射到用户虚拟空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。

    ioremap()宏定义在asm/io.h内:

      #define ioremap(cookie,size)      __ioremap(cookie,size,0)

    _ioremap函数原型为(arm/mm/ioremap.c):

      void _iomem * _ioremap(unsigned long phys_addr, size_t size, unsigned longflags);

    phys_addr:要映射的起始的I/O地址;

    size:要映射的空间的大小;

    flags:要映射的I/O空间和权限有关的标志。

    该函数返回映射后的内核虚拟地址(3GB~4GB),接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

    base_addr = ioremap(SAMSUNG_PA_ADC,0X20);

    这行代码即是将SAMSUNG_PA_ADC(0x7E00 B000)映射到内核,返回内核的虚拟地址给base_addr。

    clk_get(NULL,"adc")可以获得adc时钟,每一个外设都有自己的工作频率,PRSCVL是A/D转换器时钟的预分频功能时A/D时钟的计算公式,A/D时钟 = PCLK / (PRSCVL+1)。

    注意:AD时钟最大为2.5MHz并且应该小于PCLK的1/5。

      adc_clock = clk_get(NULL,"adc");

    即为获取adc的工作时钟频率。

    ret = misc_register(&misc);

    创建杂项设备节点。这里使用到了杂项设备,杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的include/linux目录下有miscdevice.h文件,要把自己定义的misc device从设备定义到这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。

    卸载rmmod驱动程序:

    static void __exit dev_exit() {   iounmap(base_addr);   /* disable ths adc clock */   if(adc_clock)   {     clk_disable(adc_clock);     clk_put(adc_clock);     adc_clock = NULL;   }   misc_deregister(&misc); }

    许可证声明、作者信息、调用加载和卸载程序:

    MODULE_LICENSE("GPL"); MODULE_AUTHOR("zhuzhaoqi jxlgzzq@163.com"); module_init(dev_init); module_exit(dev_exit);

    在/linux-3.8.3/drivers/char目录下的Makefile中添加:

    obj-m              += zzqadc.o

    回到/linux-3.8.3根目录下:

    /home/zhuzhaoqi/Linux/linux-3.8.3# make modules

    将/linux-3.8.3/drivers/char目录下生成的zzqadc.ko拷贝到文件系统的/lib/module/3.8.3目录中。

    3.ADC应用程序

    ADC应用程序也是相对简单,打开设备驱动文件之后进行数据读取即可。

    #include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() {   int fp,adc_data,i;   fp = open("/dev/zzqadc",O_RDWR);   if (fp < 0)   {     printf("open failed! \n");   }   printf("opened ... \n");   for ( ; ; i++)   {     adc_data = read(fp,NULL,0);     printf("Begin the NO. %d test... \n",i);     printf("adc_data = %d \n",adc_data);     printf("The Value = %f V \n" , ( (float)adc_data )* 3.3 / 1024);     printf("End the NO. %d test ...... \n \n",i);     sleep(1);   }   close(fp);   return 0; }

    由于本次使用的A/D转换是10位,则数据转换值即为1024,而OK6410的参考电压是3.3V,则A/D采集数据和电压之间的转换公式为:(float)adc_data )* 3.3 / 1024。

    为ADC应用程序编写Makefile:

    CC = /usr/local/arm/4.4.1/bin/arm-linux-gcc zzqadcapp:zzqadcapp.o     $(CC) -o zzqadcapp zzqadcapp.o zzqadcapp.o:zzqadcapp.c     $(CC) -c zzqadcapp.c clean :     rm zzqadcapp.o zzqadcapp

    将生成的zzqadcapp应用文件拷贝到文件系统/usr/bin文件夹下。

    加载zzqadc.ko设备:

    [YJR@zhuzhaoqi 3.8.3]# insmod zzqadc.ko dev_init return ret: 0 [YJR@zhuzhaoqi]\# ls -l /dev/zzqadc crw-rw----  1 root   root   10, 60 Jan 1 08:00 /dev/zzqadc

    在/dev目录下存在zzqadc设备节点,则说明ADC驱动加载成功。

    执行ADC应用程序,电压采样如下所示:

    [YJR@zhuzhaoqi]\# ./zzqadcapp opened ... …… Begin the NO. 10 test... adc_data = 962 The Value = 3.100195 V End the NO. 10 test ...... …… 相关资源:嵌入式Linux应用程序开发标准教程(华清远见)
    最新回复(0)