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

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

4.2 字符设备驱动

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

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

4.2.1 LED驱动程序设计

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

1.LED裸板程序
OK6410开发平台中有4个LED灯,原理图如图4.1所示。

《嵌入式Linux开发实用教程》——4.2 字符设备驱动_第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隔离带、高端内存永久映射区、高端内存固定映射区。

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

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

《嵌入式Linux开发实用教程》——4.2 字符设备驱动_第2张图片

在内核中,访问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

//kernel.h以便使用printk()等函数

include

//fs.h包含常用的数据结构,如struct file等

include

//uaccess.h 包含copy_to_user()、copy_from_user()等函数

include

//io.h 包含inl()、outl()、readl()、writel()等I/O操作函数

include

include

include

//init.h来指定你的初始化和清理函数,例如:module_init(init_function)、module_exit(cleanup_function)

include

include

include

include

include

//irq.h中断与并发请求事件

include

//下面这些头文件是I/O口在内核的虚拟映射地址,涉及I/O口的操作所必须包含的
//#include

include

include

include

include `

上面所列出的头文件即是本次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) = %xn",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 [email protected]");
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 
#include 
#include 
#include 
#include 
#include 
#include 

#define  LED_ON   0
#define  LED_OFF  1

/*
 * LED 操作说明信息输出
 */
void usage(char *exename)
{
  printf("How to use: \n");
  printf("  %s  \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 = 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点亮。

12260036.jpg本节配套视频位于光盘中“嵌入式Linux开发实用教程视频”目录下第四章01课(字符设备驱动之LED)。

4.2.2 ADC驱动程序设计

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

1.S3C6410的ADC控制寄存器简介
S3C6410控制芯片自带有4路独立专用A/D转换通道,如图4.3所示。

《嵌入式Linux开发实用教程》——4.2 字符设备驱动_第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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 

#include 
#include 
#include 

#include ```
与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 [email protected]");

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 
#include 
#include 

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 ......
……`

你可能感兴趣的:(嵌入式,前端,操作系统)