《嵌入式Linux开发实用教程》——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 ......
……
时间: 2024-09-20 14:39:33

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

《嵌入式Linux开发实用教程》——4.1 设备驱动概述

4.1 设备驱动概述 嵌入式Linux开发实用教程Linux系统将设备分成3种基本类型:字符设备.块设备.网络接口. (1)字符设备 字符设备是一个能够像字节流一样被访问的设备,字符终端(/dev/console)和串口(/dev/ttys0)就是两个字符设备.字符设备可以通过文件系统节点来访问,比如/dev/tty1和/dev/lp0等.这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道. (2)块设备 块设备和字符设备相

《嵌入式Linux开发实用教程》——1.1 Linux基本命令

1.1 Linux基本命令 嵌入式Linux开发实用教程 在学习嵌入式Linux开发的过程中,将经常使用到Linux的操作命令.实际上,Linux系统中的命令也是为实现特定的功能而编写的,而且绝大数的命令是用C语言编写的.有些实用性强的程序被广泛使用和传播,逐渐地演变成Linux的标准命令.但是Linux的操作命令繁多,本节将在U-Boot.Linux移植过程中常用到的Linux操作命令罗列出来进行讲解,为后续的学习做良好的铺垫.读者不要认为这是Linux简单命令则不屑一顾,嵌入式Linux学习

《嵌入式Linux开发实用教程》——4.3 块设备驱动

4.3 块设备驱动 嵌入式Linux开发实用教程 块设备和字符设备从字面上理解最主要的区别在于读写的基本单元不同,块设备的读写基本单元为数据块,数据的输入输出都是通过一个缓冲区来完成的.而字符设备不带有缓冲,直接与实际的设备相连而进行操作,读写的基本单元为字符.从实现的角度来看,块设备和字符设备是两种不同的机制,字符设备的read.write的API直接到字符设备层,但是块设备相对复杂,是先到文件系统层,然后再由文件系统层发起读写请求. 数据块指的是固定大小的数据,这个值的大小由内核来决定.一般

《嵌入式Linux开发实用教程》——导读

前言 嵌入式Linux开发实用教程 2012年11月,当我看到论坛中的同龄大学生在学习嵌入式Linux寸步难行,我就计划将我学习嵌入式Linux的点点滴滴记录下来,从一个学生的角度去写,或许更能让初学者接受.2013年1月,当写完初稿再重新审视的时候,总感觉不尽如意.2013年3月,我联系了我的师弟李强,两人打算以一个全新的思维重新完成这本书. 2013年6月,书稿终于定型. 本书一共有6章,从Linux指令基础到Linux常用软件:从U-Boot移植到Linux移植:从Linux驱动程序设计到

《嵌入式Linux开发实用教程》——1.2 Makefile基本知识

1.2 Makefile基本知识 嵌入式Linux开发实用教程 Makefile如今能得以广泛应用,这还得归功于它被包含在UNIX系统中.在make诞生之前,UNIX系统的编译系统主要由"make"."install"shell脚本程序和程序的源代码组成.它可以把不同目标的命令组成一个文件,而且可以抽象化依赖关系的检查和存档.这是向现代编译环境发展的重要一步.1977年,斯图亚特·费尔德曼在贝尔实验室里制作了这个软件.2003年,斯图亚特·费尔德曼因发明了这样一个重

《嵌入式Linux开发实用教程》——第1章 嵌入式Linux基础 1.1 Linux基本命令

第1章 嵌入式Linux基础 1.1 Linux基本命令 在学习嵌入式Linux开发的过程中,将经常使用到Linux的操作命令.实际上,Linux系统中的命令也是为实现特定的功能而编写的,而且绝大数的命令是用C语言编写的.有些实用性强的程序被广泛使用和传播,逐渐地演变成Linux的标准命令.但是Linux的操作命令繁多,本节将在U-Boot.Linux移植过程中常用到的Linux操作命令罗列出来进行讲解,为后续的学习做良好的铺垫.读者不要认为这是Linux简单命令则不屑一顾,嵌入式Linux学习

《嵌入式Linux开发实用教程》——1.5 嵌入式Linux移植常用软件

1.5 嵌入式Linux移植常用软件 在进行嵌入式Linux学习与开发的过程中,需要使用到一些常用的开发工具,熟练使用这些软件,能让学习与开发达到事半功倍的效果. 1.5.1 SecureCRT SecureCRT是可以在Window环境下登录UNIX和Linux服务器主机的软件,它不仅支持SSH1.SSH2,而且支持TeInet和rlogin协议. 在Ubuntu宿主机上安装SSH. zhuzhaoqi@zhuzhaoqi-desktop:~/sudo apt-get install open

《嵌入式Linux开发实用教程》——1.3 arm-linux交叉编译链

1.3 arm-linux交叉编译链 平常我们做的编译叫本地编译,也就是在当前平台编译,编译得到的程序也是在本地执行.相对而言的交叉编译指的是在一个平台上生成另一个平台的可执行代码. 常见的交叉编译有以下3种. 在Windows PC上,利用ADS(ARM 开发环境),使用armcc编译器,编译出针对ARM CPU的可执行代码. 在Linux PC上,利用arm-linux-gcc编译器,编译出针对Linux ARM平台的可执行代码. 在Windows PC上,利用cygwin环境,运行arm-

《嵌入式Linux开发实用教程》——第4章 Linux设备驱动程序设计 4.1 设备驱动概述

第4章 Linux设备驱动程序设计 4.1 设备驱动概述 Linux系统将设备分成3种基本类型:字符设备.块设备.网络接口. (1)字符设备 字符设备是一个能够像字节流一样被访问的设备,字符终端(/dev/console)和串口(/dev/ttys0)就是两个字符设备.字符设备可以通过文件系统节点来访问,比如/dev/tty1和/dev/lp0等.这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道. (2)块设备 块设备和字