1、软件系统分为:应用程序、库、操作系统(内核)、驱动程序,开发人员专注某一层,了解邻层的接口。如,应用程序调用库函数open,库根据open传入的参数执行swi指令引起CPU异常进入内核。内核的异常处理函数根据参数找到相应驱动程序。内核与驱动程序没有界限,因为驱动程序最终是要编进内核。驱动程序从不主动运行。在有MMU的系统中,应用程序处于用户空间,驱动程序处于内核空间。
2、Linux外设分为:字符设备(读写以字节方式进行)、块设备(数据读写以块方式,数据有格式)、网络接口(数据读写是大小不固定的块)。
3、Linux设备驱动程序开发步骤
(1)初始化驱动程序,如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序。
(2)设计要实现的操作函数,如open等。
(3)实现中断服务
(4)编译驱动程序到内核,如果动态编译为模块,则用insmod rmmod命令进行加载和卸载。加载:调用模块的初始化函数,向内核注册驱动程序。卸载:调用模块清除函数。
(5)测试
4、应用程序使用统一的接口函数(系统调用)调用硬件启动程序。字符设备驱动程序的函数集合在 include/linux/fs.h 的file_operations 结构中
//为驱动函数规定统一以文件操作的接口,如open、read ....
//当应用程序使用open函数打开某个设备,就会调用 file_operations 中的open函数。从这个角度,编写字符设备驱动程序就是为具体硬件的file_operations 结构编写需要的函数。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
};
5、安装驱动程序时调用初始化函数,把驱动出现的file_operations 结构和主设备号向内核注册。内核为字符设备保存一个数组,主设备号为数组标号。加载即在对于标号填入对应设备file_operations 地址,卸载相反。
//对于字符设备使用如下函数进行注册
int register_chrdev(unsigned int major, consr char *name, struct file_operations *fops);
//之后当应用程序操作设备时,Linux系统就会根据设备文件类型、主设备号找到file_operations
简单的驱动程序编写(不涉及中断、select机制、fasync异步通知机制)
(1)编写驱动初始化函数
(2)构造file_operations 中成员函数
6、LED驱动程序分析
(1)初始化,指定加载和卸载函数
//执行 insmod s3c24xx_leds 就会调用该函数
static int __init s3c24xx_leds_init(void)
{
int ret;
/* 注册字符设备驱动程序
* 参数为主设备号、设备名字、file_operations结构;
* 这样,主设备号就和具体的file_operations结构联系起来了,
* 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
* LED_MAJOR可以设为0,表示由内核自动分配主设备号
*/
ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
if (ret < 0) {
printk(DEVICE_NAME " can't register major number\n");
return ret;
}
printk(DEVICE_NAME " initialized\n");
return 0;
}
// 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数
static void __exit s3c24xx_leds_exit(void)
{
/* 卸载驱动程序 */
unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}
/* 这两行指定驱动程序的初始化函数和卸载函数 */
//要是不适用这2行就要把加载和卸载函数名改为init_module和cleanup_module
module_init(s3c24xx_leds_init);
module_exit(s3c24xx_leds_exit);
(2)file_operations 结构
static struct file_operations s3c24xx_leds_fops =
{
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = s3c24xx_leds_open,
.ioctl = s3c24xx_leds_ioctl,
};
(3)应用程序对设备文件/dev/leds执行open和ioclt时会调用的函数
// 应用程序对设备文件/dev/leds执行open(...)时,就会调用s3c24xx_leds_open函数
//open即设置LED引脚为输出
static int s3c24xx_leds_open(struct inode *inode, struct file *file)
{
int i;
for (i = 0; i < 4; i++)
{
// 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能,该函数在内核中实现
s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
}
return 0;
}
/* 应用程序对设备文件/dev/leds执行ioclt(...)时,
* 就会调用s3c24xx_leds_ioctl函数
*/
//根据命令,实现LED的开和关,open返回inode和file给ioctl
static int s3c24xx_leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
if (arg > 4) {
return -EINVAL;
}
switch(cmd) {
case IOCTL_LED_ON:
// 设置指定引脚的输出电平为0,该函数在内核中实现
s3c2410_gpio_setpin(led_table[arg], 0);
return 0;
case IOCTL_LED_OFF:
// 设置指定引脚的输出电平为1,该函数在内核中实现
s3c2410_gpio_setpin(led_table[arg], 1);
return 0;
default:
return -EINVAL;
}
}
7、驱动编译,编译在PC上,PC上有未编译的linux目录
cat /proc/devices 查看已有设备
(1)在drivers\char下加入文件s3c24xx_leds.c;
(2)在drivers\char\Makefile中加一行
obj-m += s3c24xx_leds.o
(3)在内核根目录下执行make modules生成drivers\chars\s3c24xx_leds.ko
(4)把s3c24xx_leds.ko(以网络传输等方式)放到开发板文件系统/lib/modules/2.6.22.6/下
(5)这是就可以使用insmod s3c24xx_leds 和rmmod s3c24xx_leds命令进行加载和卸载。
8、测试程序(应用程序)
(1)在PC上编译生成可执行文件,把它放到开发板文件系统/user/bin目录下
(2)在开发板文件系统中建立设备文件
mknod /dev/leds c 231 0
(3)用命令测试
led_test 1 on
led_test 2 off
应用程序的open和ioctl等系统调用,他们的参数和驱动程序中相应函数的参数不是一一对应的,经过了内核文件层的转换。
#include
#include
#include
#include
#define IOCTL_LED_ON 0
#define IOCTL_LED_OFF 1
void usage(char *exename)
{
printf("Usage:\n");
printf(" %s \n" , exename);
printf(" led_no = 1, 2, 3 or 4\n");
}
int main(int argc, char **argv)
{
unsigned int led_no;
int fd = -1;
if (argc != 3)
goto err;
fd = open("/dev/leds", 0); // 打开设备,根据/dev/leds提取设备类型、主设备号,根据这些可以找到对应file_operations
if (fd < 0) {
printf("Can't open /dev/leds\n");
return -1;
}
led_no = strtoul(argv[1], 0, 0) - 1; // 操作哪个LED?
if (led_no > 3)
goto err;
if (!strcmp(argv[2], "on")) {
ioctl(fd, IOCTL_LED_ON, led_no); // 点亮它
} else if (!strcmp(argv[2], "off")) {
ioctl(fd, IOCTL_LED_OFF, led_no); // 熄灭它
} else {
goto err;
}
close(fd);
return 0;
err:
if (fd > 0)
close(fd);
usage(argv[0]);
return -1;
}
9、Makefile,把开发版根文件挂载到PC,PC直接编译无需下载
KERN_DIR = /work/linux-2.6//内核目录
all:
make -C $(KERN_DIR ) M = 'pwd' module
// -C表示到KERN_DIR 目录下执行Makefile
boj-m += s3c24xx_led.o