Linux操作系统将所有的设备都会看成是文件,因此当我们需要访问设备时,都是通过操作文件的方式进行访问。对字符设备的读写是以字节为单位进行的。
对字符设备驱动程序的学习过程,主要以两个具有代表性且在OK6410开发平台可实践性的字符驱动展开分析,分别为:LED驱动程序、ADC驱动程序。
1.1.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( )内存空间被划分成为两个部分:用户空间(3G)和内核空间(1G),大小分别为0~3G和3~4G。如图4. 2所示。
3~4G之间的内核空间中,从低地址到高地址依次为:物理内存映射区、隔离带、vmalloc虚拟内存分配区、隔离带、高端内存映射区、专用页面映射区。
用户进程通常情况下,只能访问用户空间的虚拟地址,不能访问到内核空间。
每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。
在内核中,访问IO内存之前,我们只有IO内存的物理地址,这样是无法通过软件直接访问的,需要首先用ioremap()函数将设备所处的物理地址映射到内核虚拟地址空间(3GB~4GB)。然后,才能根据映射所得到的内核虚拟地址范围,通过访问指令访问这些IO内存资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,如下所示:
void * ioremap(unsigned long phys_addr, unsigned longsize,
unsignedlong flags);
iounmap函数用于取消ioremap()所做的映射,如下所示:
void iounmap(void *addr);
到这里应该明白,像GPMCON(0x7F008820 )这个物理地址是不能直接操控的,必须通过映射到内核的虚拟地址中,才能进行操作。
现在开始设计第一个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()等IO操作函数
#include
#include
#include
//init.h来指定你的初始化和清理函数,例如:module_init(init_function)、module_exit(cleanup_function)
#include
#include
#include
#include
#include
//irq.h中断与并发请求事件
//下面这些头文件是IO口在内核的虚拟映射地址,涉及IO口的操作所必须包含
//#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 inti;
/*设置GPM0~GPM3为输出模式*/
for (i = 0;i < 4; i++)
{
s3c_gpio_cfgpin(S3C64XX_GPM(i),S3C_GPIO_OUTPUT);
printk("TheGPMCON %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, unsignedint 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, unsignedint 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)) );
return0;
/*如果是熄灭LED(argv) */
caseLED_OFF:
gpio_set_value(S3C64XX_GPM(argv),1);
printk("LED OFF n");
printk("S3C64XX_GPM(i) = %x n",gpio_get_value(S3C64XX_GPM(argv)) );
return0;
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");
return0;
}
这是驱动程序的核心控制,各个函数指针所对应的回调函数:
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 errorn","led");
return -1;
}
printk("OK!n");
return0;
}
__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_DESCRIPTION("OK6410(S3C6410) LEDDriver");
完成驱动程序的设计之后,将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$ makemodules
……
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 intled_number;
if (argc !=3) {
gotoerr;
}
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) {
gotoerr;
}
/* 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 {
gotoerr;
}
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:以可读写方式打开文件。
上述三种常用的旗标是互斥使用,但可与其他的旗标进行或运算符组合。
第三个参数mode是使用该文件的权限。比如777,755等。
通过这个应用程序实现对LED驱动程序的控制,为了更加方便快捷编译这个应用程序,为其写一个Makefile文件,如下所示:
#交叉编译链安装路径
CC = /usr/local/arm/4.4.1/bin/arm-linux-gcc
zzq_led_app:zzq_led_app.o
$(CC) -ozzq_led_appzzq_led_app.o
zzq_led_app.o:zzq_led_app.c
$(CC) -czzq_led_app.c
clean :
rm zzq_led_app.ozzq_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 -czzq_led_app.c
/usr/local/arm/4.4.1/bin/arm-linux-gcc -o zzq_led_appzzq_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点亮。
注:本节配套视频位于光盘中“嵌入式Linux实用教程视频”目录下第四章01课(字符设备驱动之LED)。