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