本文实现的是一个低级驱动开发案例。简单通过编写内核驱动模块由开发板(sp6818)的按键key控制板子led灯
大致流程如下
编写驱动文件keyled.c
编写应用文件app.c
编写makefile
make制作出.ko文件
将.ko移至开发板中
insmod 将设备加入驱动序列中
mknod /dev/keyled c 87 0 将主设备号与节点相关联
驱动文件编写
//keyled.c
#include <linux/kernel.h> // printk
#include <linux/init.h> // __init/__exit
#include <linux/module.h> // module_init
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/io.h> // arch/arm/include/asm/io.h
#include <linux/ioport.h>
#include <linux/interrupt.h>
#include <linux/wait.h> //休眠唤醒机制
#include <linux/kthread.h>
wait_queue_head_t myqueue;
int flag= 1;
struct resource *res = NULL;
unsigned long *ptr = NULL;
unsigned long *ptr_b = NULL;
int keyled_open(struct inode *node, struct file *file)
{
printk(KERN_INFO "keyled open\n");//KERN_INFO为输出优先级
//led init
*(ptr+1) |= (0x1<<7);
*(ptr) |= (0x1<<7);
//key_init
*(ptr_b + 1) &= ~(0x1<<30);
return 0;
}
int keyled_close(struct inode *node, struct file *file)
{
//keyled_init();
printk(KERN_INFO "keyled close\n");
return 0;
}
int kbuffer;
ssize_t keyled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
printk(KERN_INFO "keyled write\n");
//memcpy(&kbuffer, ubuf, size);
//copy_to_user();
copy_from_user(&kbuffer, ubuf, size);//将用户空间数据拷贝给内核空间
printk(KERN_INFO "kbuffer: %d\n", kbuffer);
if(kbuffer == 0)
{
printk(KERN_INFO "led on\n");
*(ptr) &= ~(0x1<<7);
}else
{
printk(KERN_INFO "led off\n");
*(ptr) |= (0x1<<7);
}
return 0;
}
int key_status;
ssize_t keyled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
//printk(KERN_INFO "keyled read...\n");
//线程开始休眠
//wait_event(myqueue, flag != 1);
key_status = (*(ptr_b + 6) & (0x1<<30)) >> 30;
//printk(KERN_INFO "key state: %d\n", key_status);
copy_to_user(ubuf, &key_status, size);//将内核空间数据拷贝给用户空间
return 0;
}
struct file_operations keyled_fops = {
.open = keyled_open,
.release = keyled_close,
.write = keyled_write,
.read = keyled_read,
};
static int __init led_drv_init(void)//_init的作用是在insmod加载内核时调用该函数,之后忽略掉这个函数,并释放内存。
{
printk(KERN_INFO "-----------------led driver init .....................\n");
int ret;
//注册字符设备驱动
ret = register_chrdev(87, "key-led", &keyled_fops);//将驱动与主设备号相关联
if(ret < 0)
{
printk(KERN_INFO "register_chrdev failed\n");
return -1;
}
res = request_mem_region(0xc001c000, 0x1000,"myled-iomap");//request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。
if(res == NULL)
{
printk(KERN_INFO "request_mem_region failed\n");
return -1;
}
printk(KERN_INFO "request_mem_region success\n");
ptr = (unsigned long *)ioremap(0xc001c000, 0x1000);//ioremap主要是检查传入地址的合法性,建立页表(包括访问权限),完成物理地址到虚拟地址的转换。
if(ptr == NULL)
{
printk(KERN_INFO "ioremap failed\n");
return -1;
}
printk(KERN_INFO "ioremap success\n");
ptr_b = (unsigned long *)ioremap(0xc001b000, 0x1000);//只读的地址似乎不需要request_mem_region
if(ptr_b == NULL)
{
printk(KERN_INFO "key ioremap failed\n");
return -1;
}
printk(KERN_INFO "key ioremap success\n");
//初始队列头结构
init_waitqueue_head(&myqueue);
return 0;
}
static void __exit led_drv_exit(void)//remmod时调用
{
printk(KERN_INFO "-----------------led driver exit .....................\n");
iounmap(ptr);//释放映射
iounmap(ptr_b);//释放映射
release_mem_region(0xc001c000, 0x1000);//释放地址声明
unregister_chrdev(87, "key-led");//注销
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");//版本
MODULE_AUTHOR("zbzyjya ");//作者
MODULE_DESCRIPTION("8250 serial probe module for Accent Async cards");
本文会按照驱动程序内部的启动顺序与调用顺序来讲解
先关注这两个函数(_init和_exit)
static int __init led_drv_init(void)
static void __exit led_drv_exit(void)
先分别说说修饰符_init和_exit的作用:
_init的作用是调用完该函数之后,就忽略掉这个函数,在初始化完成后丢弃该函数并收回所占内存,
__exit修饰词标记函数只在模块卸载时使用。
_init和_exit的作用
通过
module_init(led_drv_init);
module_exit(led_drv_exit);
在内核加载(insmod)和卸载(remmod)时会调用对应的函数。
这种函数存在的意义是,将一部分不常用的代码或者只调用一次的代码,动态编译进内核当中,使用完之后可以立即释放该函数的内存,使得内存利用更加合理。
关注一下led_drv_init内的几个api
//注册字符设备驱动
ret = register_chrdev(87, "key-led", &keyled_fops);//将驱动与主设备号相关联
这个函数将驱动与设备号关联起来,第三个参数传入的是一个file_operations的结构体,这个结构体十分重要,后文再讲解。
res = request_mem_region(0xc001c000, 0x1000,"myled-iomap");
//request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。
request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。第一个参数为led灯的物理地址,第二个参数为读取大小。
ioremap主要是检查传入地址的合法性,建立页表(包括访问权限),完成物理地址到虚拟地址的转换。要操作led灯的话必须要有这两步声明与映射。
在调用insmod keyled.ko时驱动会执行led_drv_init,完成上述各种初始化与注册操作。与其相应的是调用remmod的时候会执行 led_drv_exit,执行该函数内部的驱动注销与映射释放等操作。
应用文件编写
再来看看app.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#define LED_ON 0x0
#define LED_OFF 0x1
int main()
{
int fd;
int wr_data;
int rd_state;
fd = open("/dev/keyled", O_RDWR);
if(fd < 0)
{
printf("open failed\n");
return -1;
}
printf("open keyled success\n");
while(1)
{
read(fd, &rd_state, 4);
if(rd_state == 0)
{
usleep(30000);
read(fd, &rd_state, 4);
if(rd_state == 0)
{
//按键弹开
do {
read(fd, &rd_state, 4);
}while(rd_state == 0);
//
printf("led change state....\n");
}
}
}
close(fd);
return 0;
}
这段代码为应用层代码,先简单说说应用层是如何通过API系统调用的,在应用层中,open了一个驱动节点(/dev/keyled)之后,应用层对这个文件描述符的open、read、write等API操作,最后都会找到这个驱动内部file_operations结构体(参考上面keyled.c)中的.open、.read、.write(均为函数指针)所指向的函数来执行。
struct file_operations keyled_fops = {
.open = keyled_open,
.release = keyled_close,
.write = keyled_write,
.read = keyled_read,
};
如当应用层执行fd = open("/dev/keyled", O_RDWR);时,驱动就会找到keyled_open来执行。file_operations结构体是驱动开发中很重要的结构体,注意在字符驱动注册时要作为第三个参数传入。
那么同理应用层调用write和read时对应会调用
ssize_t keyled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
printk(KERN_INFO "keyled write\n");
//memcpy(&kbuffer, ubuf, size);
//copy_to_user();
copy_from_user(&kbuffer, ubuf, size);//将用户空间数据拷贝给内核空间
printk(KERN_INFO "kbuffer: %d\n", kbuffer);
if(kbuffer == 0)
{
printk(KERN_INFO "led on\n");
*(ptr) &= ~(0x1<<7);
}else
{
printk(KERN_INFO "led off\n");
*(ptr) |= (0x1<<7);
}
return 0;
}
int key_status;
ssize_t keyled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
//printk(KERN_INFO "keyled read...\n");
//线程开始休眠
//wait_event(myqueue, flag != 1);
key_status = (*(ptr_b + 6) & (0x1<<30)) >> 30;
//printk(KERN_INFO "key state: %d\n", key_status);
copy_to_user(ubuf, &key_status, size);//将内核空间数据拷贝给用户空间
return 0;
}
app.c中的write是如何把内容写进fd里的呢?
该函数的第二个传参ubuf便是用户写入的内容,通过
将用户空间数据拷贝给内核空间,把ubuf里的内容传入kbuffer中。
keyled_read同理,用copy_to_user将内核空间数据拷贝给用户空间,完成读写。有一道面试题问的是,内核空间和用户空间如何通讯,其答案之一就是通过copy_from_user和copy_to_user来实现。
有了这个功能,就能通过读取开关状态来判断是否点亮led灯。
说说Makefile
如何用makefile制作出对应的keyled.ko和app呢?
这里给出参考与博主自己的理解
obj-m += keyled.o
KERNEL_DIR = /opt/wkspace/build_plat/kernel-3.4.39/
all:
make modules CROSS_COMPILE=/usr/local/arm/arm-eabi-4.8/bin/arm-eabi- -C $(KERNEL_DIR) M=`pwd`
arm-linux-gcc app.c -o app
cp keyled.ko app /nfs
clean:
make modules clean M=`pwd` -C $(KERNEL_DIR) CROSS_COMPILE=/usr/local/arm/arm-eabi-4.8/bin/arm-eabi-
把keyled.c编译成keyled.ko文件
驱动开发makefile
红色是编译内核驱动的时候要加的指令 黄色是指定交叉编译链 蓝色是跳转到内核源码里去 绿色是回到当前目录。
值得注意的是-C是跳转到指定路径当中去 M是回来。由于驱动开发需要依赖内核一些库,KERNEL_DIR指定的是内核源码所在的路径。
制作出.ko和app之后,就能把他们拷到开发板上。
insmod和mknod
拷到开发板之后,在.ko当前文件夹下,命令行输入
insmod keyled.ko
实现将设备加入驱动序列中。通过cat /proc/devices或者lsmod可以查看驱动是否加入成功。除了insmod之外还可以用modprobe keyled.ko来将设备加入,modprode的功能更多,有兴趣的读者可以自己了解。
设备加入驱动序列之后,还需要
mknod /dev/keyled c 87 0 将主设备号与节点相关联 87是主设备号 0是副设备号
通过mknod将主设备号与节点相关联。可以这样理解,insmod之后驱动中只有一个设备号和设备相关联,但应用层里找不到这样一个设备。mknod的作用是给这个设备生成一个/dev文件夹下的节点,类似于给应用层代码插一个指路的路牌,这样就把节点-设备-设备号相关联了起来,能让应用层代码顺利找到该设备。
完成这一步之后就能./app执行可执行文件了。执行完之后可以remmod掉.ko,再次lsmod查看设备就已经被删除了。
小弟刚入行不久,若有问题请指正。