i.MX283开发板第一个Linux驱动-LED驱动

  • 字符设备驱动开发

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

Linux应用程序对驱动的调用顺序如下图所示:

驱动程序主要任务就是“打通”内核与硬件设备之间的通道,最终形成统一的接口(open、write、read...)供内核调用,编写LED驱动程序实际上就是填充这些接口,下面就开始一步一步编写一个LED的驱动程序。

1.查看硬件电路,确定LED对应的GPIO口以及工作条件

引脚为LCD_D23  低电平点亮

2.编写open、write、read、release函数

  • 首先需要引用相关头文件、定义相关宏
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/bcd.h>
#include <linux/capability.h>
#include <linux/rtc.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#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
  • open调用实现
static int led_open(struct inode *inode ,struct file *flip)
{

   int ret = -1;
   ret = gpio_request(LED_GPIO, "LED1");
   printk("gpio_request = %d\r\n",ret);
   return 0;
}

该函数主要实现了向内核申请这个 GPIO 端口,同时把该引脚配置成 GPIO 工作模式。

  • write调用实现
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 缓冲区中。

  • read调用实现
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即可。

  • release调用实现
static int led_release(struct inode *inode ,struct file *flip)
{
  gpio_free(LED_GPIO);
  return 0;  
}

该函数的作用是释放GPIO。

3.填充file_operations结构体

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。

4.注册与注销字符设备

  • 静态分配主设备号:

上面已经做好了驱动的准备工作,但是内核还不知道这个设备,所以接下来需要“告诉”内核我们编写的这个设备驱动,也就是向内核注册设备。

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);

5.添加作者和LICENSE信息

LICENSE信息必不可少,否则编译会报错。

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("imx283 first driver");//描述信息
MODULE_AUTHOR("xzx2020");//作者信息

到此,一个LED驱动就完成了,接下来就是编译和编写测试程序(应用程序)。

6.驱动程序Makefile编译脚本

/*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就是我们需要的文件。

7.编写测试程序

测试程序很简单,就是打开led设备文件,然后控制led间隔200ms闪烁一段时间,最后再关闭该设备文件。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <limits.h>
#include <asm/ioctls.h>
#include <time.h>
#include <pthread.h>


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盘拷贝)。

8.测试

首先执行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卸载设备驱动

卸载也没有什么问题。

9.总结

鉴于笔者水平有限,同时也是Linux驱动初学者,以上只是个人学习的总结,难免会有错误纰漏之处,望各位网友多多批评指教。

本文参考:

1.《嵌入式Linux应用完全开发手册》 

2.《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0》

3.《EasyARM-iMX28xx Linux开发指南 20150901 V1.03》

发布了44 篇原创文章 · 获赞 70 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_24835087/article/details/104092156