字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux应用程序对驱动的调用顺序如下图所示:
驱动程序主要任务就是“打通”内核与硬件设备之间的通道,最终形成统一的接口(open、write、read...)供内核调用,编写LED驱动程序实际上就是填充这些接口,下面就开始一步一步编写一个LED的驱动程序。
引脚为LCD_D23 低电平点亮
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include <../arch/arm/mach-mx28/mx28_pins.h>
#define DEVICE_NAME "imx283_led"//驱动名称
#define major 200 //主设备号 仅静态分配时需要
#define LED_GPIO MXS_PIN_TO_GPIO(PINID_LCD_D23) //for 283 287A/B
static int led_open(struct inode *inode ,struct file *flip)
{
int ret = -1;
gpio_free(LED_GPIO);
ret = gpio_request(LED_GPIO, "LED1");
printk("gpio_request = %d\r\n",ret);
return 0;
}
该函数主要实现了向内核申请这个 GPIO 端口,同时把该引脚配置成 GPIO 工作模式。
static int led_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
int ret = -1;
unsigned char databuf[1];
ret = copy_from_user(databuf,buf,1);
if(ret < 0 )
{
printk("kernel write error \n");
return ret;
}
gpio_direction_output(LED_GPIO, databuf[0]);
return ret;
}
在该函数的传入参数中有 buf 和 count 参数。buf 表示应用程序调用 write 时,要写入数
据的缓冲区;count 表示缓冲区中有效数据的长度。
gpio_direction_output函数作用是让指定的IO口输出0或者1.
注意:buf 缓冲区是在用户空间的,而 LED 驱动程序是运行在内核空间的,并且在内核空间的代码是不能直接访问用户空间的缓冲区的,所以需要使用 copy_from_user 宏把用户空间 buf 中要写入的数据复制到 data 缓冲区中。
static int led_read(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
return 0;
}
对于一个LED灯我们只需要操作它输出0或1即可,所以read操作直接返回0即可。
static int led_release(struct inode *inode ,struct file *flip)
{
gpio_free(LED_GPIO);
return 0;
}
该函数的作用是释放GPIO。
file_operations结构体定义在Linux内核源码include/linux/fs.h中
/*
* NOTE:
* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
* can be called without the big kernel lock held in all filesystems.
*/
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 *, 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 (*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 (*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);
int (*setlease)(struct file *, long, struct file_lock **);
};
在上面的成员函数指针的形参中,struct file 表示一个打开的文件。Struct inode表示一个磁盘上的具体文件。
对于一个简单的LED驱动只需要实现基本的open、read、write、release接口即可,实际需要实现哪些接口需要根据驱动的需求来决定。
static struct file_operations led_fops=
{
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.read = led_read,
.release = led_release,
};
owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
上面已经做好了驱动的准备工作,但是内核还不知道这个设备,所以接下来需要“告诉”内核我们编写的这个设备驱动,也就是向内核注册设备。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
对于字符设备,我们一般调用register_chrdev函数注册,major是需要注册设备的主设备号,name是设备名称,fops就是上面第3步填充的file_operations结构体。
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
应用程序对设备文件进行读写操作时,都是通过主设备号找到这个设备的驱动文件,然后在驱动文件里取得具体操作的函数,最后再进行相关操作。
我们可以使用cat /proc/devices 看我们系统中哪些设备号已经被使用了。
上图就是已经被使用的设备号,我们最好不要再使用。
对于LED驱动,我们再一开始就定义了主设备号为200 设备名称为imx283_led,下面就需要实现设备的初始化函数。
static int __init led_init(void)
{
int ret = -1;
ret = register_chrdev(major,DEVICE_NAME, &led_fops );
if(ret < 0)
{
printk("register chrdev failed!\n");
return ret;
}
printk("module init ok \n");
return ret;
}
注意:内核中只能用printk打印,不可使用printf
led_init函数将在驱动模块被初始化(insmod)时调用。
同样的,有注册设备,就一定有注销设备,函数unregister_chrdev就是注销(卸载)一个字符设备,它的原型如下:
static inline void unregister_chrdev(unsigned int major, const char *name)
它只需要提供设备的主设备号和设备名称即可。
我们还需要实现设备的注销函数:
static void __exit led_exit(void)
{
unregister_chrdev(major,DEVICE_NAME);
printk("module exit ok\n");
}
register_chrdev函数第一个参数为0,则表示需要内核动态分配主设备号,其合法返回值(大于0)就是分配的主设备号。
因此我们可以通过如下方式让内核动态分配主设备号,而不需要我们手动设置。
static int MAJOR = 0;
static int __init led_init(void)
{
MAJOR = register_chrdev(0,DEVICE_NAME, &led_fops );
printk("major=%d\n",MAJOR);
printk("module init ok \n");
return 0;
}
static void __exit led_exit(void)
{
unregister_chrdev(MAJOR,DEVICE_NAME);
printk("module exit ok\n");
}
由于我们是以模块的形式加载或者卸载设备驱动,我们还需要向内核注册模块的加载和卸载函数,就是说上面实现的led_init和led_exit两个函数也需要向内核注册,不然执行insmod或rmmod时,是不会调用led_init或led_exit函数的。
模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
参数 xxx_init 就是需要注册的具体函数,这里就是刚刚说的led_init和led_exit函数。
module_init(led_init);
module_exit(led_exit);
LICENSE信息必不可少,否则编译会报错。
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("imx283 first driver");//描述信息
MODULE_AUTHOR("xzx2020");//作者信息
到此,一个LED驱动就完成了,接下来就是编译和编写测试程序(应用程序)。
/*obj-m:内核模块文件,指将myleds.o编译成myleds.ko*/
obj-m:=led_driver.o
PWD:=$(shell pwd)
KDIR:=/ZLG_linux/linux-2.6.35.3
all:
$(MAKE) -C $(KDIR) M=$(PWD)
/*M=pwd :指定当前目录*/
/*make -C $(KERN_DIR) 表示将进入(KERN_DIR)目录,执行该目录下的Makefile*/
clean:
rm -rf *.ko *.order *.symvers *.cmd *.o *.mod.c *.tmp_versions .*.cmd .tmp_versions
led_driver是驱动文件名(我这里命名为led_driiver.c)
KDIR 是内核源码的主目录
保证驱动文件(.c文件)和MakeFile在同一目录下,执行make就可以生成驱动的.ko文件。
最终生成的led_driiver.ko就是我们需要的文件。
测试程序很简单,就是打开led设备文件,然后控制led间隔200ms闪烁一段时间,最后再关闭该设备文件。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = -1,i;
char buf[1]={0};
fd = open("/dev/imx283_led",O_RDWR);
if(fd < 0)
{
printf("open /dev/imx283_led fail fd=%d\n",fd);
}
for(i=0;i<50;i++)
{
buf[0] = 0;
write(fd,buf,1);
usleep(200000);
buf[0] = 1;
write(fd,buf,1);
usleep(200000);
}
fd = close(fd);
if(fd < 0)
{
printf("led test error\n");
}
return 0;
}
同样的,我们也可以写一个测试程序的MakeFile编译测试程序,或者直接用命令
arm-fsl-linux-gnueabi-gcc led_test.c -o led_test
即可编译生成测试程序的可执行文件。
测试程序makefile:
EXEC = ./led_test
OBJS = led_test.o
CROSS = arm-fsl-linux-gnueabi-
CC = $(CROSS)gcc
STRIP = $(CROSS)strip
CFLAGS = -Wall -g -O2
all: clean $(EXEC)
$(EXEC):$(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
$(STRIP) $@
clean:
-rm -f $(EXEC) *.o
这时候,把上面生成的.ko文件和led_test文件想办法弄到开发板上就可以测试了(我这里用的是U盘拷贝)。
首先执行insmod加载驱动。
可以看到,模块加载成功,此时执行cat /proc/devices 应该能看到我们刚刚加载的设备:
接着我们需要手动创建设备节点,因为此时/dev目录下是没有我们这个imx283_led设备的。
//格式 mknod /dev/xxx 设备类型 主设备号 次设备号
//主设备号是cat /proc/devices里看到的 次设备号需要我们手动填写这里设置为0 最大255
mknod /dev/imx283_led c 200 0
接着再查看/dev下有没有生成LED设备节点。
ls -l /dev|grep led
可以看到/dev下已经生成了imx283_led设备节点,主设备号200,次设备号0,名称imx283_led。
这时候就可以执行测试程序了。
不出意外的话,开发板上的led灯应该已经开始闪烁了。
最后,再执行rmmod卸载设备驱动
卸载也没有什么问题。
鉴于笔者水平有限,同时也是Linux驱动初学者,以上只是个人学习的总结,难免会有错误纰漏之处,望各位网友多多批评指教。
由于现在较新的Linux内核(2.6以上)的字符设备驱动开发已经不提倡这种注册方式,所以下一篇博客已对此驱动作了一些改进:i.MX283开发板第一个Linux驱动-LED驱动改进
本文参考:
1.《嵌入式Linux应用完全开发手册》
2.《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0》
3.《EasyARM-iMX28xx Linux开发指南 20150901 V1.03》