linux字符设备驱动 LED驱动程序

1. 字符设备驱动简介

Linux的外设主要分为三类:字符设备(character device)、块设备(block device)、网络接口(network interface)。
字符设备是能像字节流一样读写操作的设备,也就是说对它的读写是以字节为单位。比如串口的收发数据,另外常见的点灯、按键、IIC、SPI、LCD的驱动都是字符设备。
如下图为Linux系统层级关系图,在Linux系统下开发字符设备驱动需要按照该框架编写程序。

linux字符设备驱动 LED驱动程序_第1张图片其中,开发字符设备驱动程序只需要关注应用程序和驱动程序层。应用程序和驱动程序层连接靠内核,我们先不深究其原理。在应用程序中,用户可以调用open、read、wirte和ioctl等函数,程序会自动调用相对应的库函数,接着程序会通过系统调用方式进入内核,内核程序调用相应的驱动程序open、read、write、ioctl函数,从而驱动硬件设备。

2. 字符设备驱动开发步骤

2.1 字符设备的注册或注销

开发字符设备驱动之前,需要注册一个字符设备驱动,相反的,删除一个字符设备,需要注销,其调用的函数原型如下:

/* 函数:register_chrdev()
 * 描述:注册一个字符设备
 * 参数:major:主设备号
 *       name:设备名字,指向一串字符串
 *       fops:结构体file_operations类型指针,指向设备的操作函数集合变量 
 * 返回:
 */
static inline int register_chrdev(unsigned int  major,
									const char  *name,
								  const struct file_operations *fops)

 /* 函数:unregister_chrdev()
 * 描述:注销一个字符设备
 * 参数:major:主设备号
 *       name:设备名字,指向一串字符串
 * 返回:
 */											
static inline void unregister_chrdev(unsigned int  major,
									   const char  *name)

说明:

  • 主设备号:每个字符设备都有一个主设备号,通过命令:“cat /proc/devices”可以查看当前使用的设备号。

  • 在字符驱动框架中,需要定义一个file_operations结构体,该结构体是字符设备的操作函数集合。

2.2 驱动模块的加载和卸载

Linux驱动有两种方式运行,一种是编译进内核,当内核运行时,会自动运行驱动;另一种是将驱动编译成模块(扩展名为:.ko),内核启动后可以使用insmod或modprobe加载驱动模块。
在上面,通过函数register_chrdev()注册了字符设备后,我们需要告诉内核有这个字符设备,即将模块加载到内核中,利用函数module_init()。模块的卸载相反,告诉内核删除这个字符模块,利用函数module_exit(),模块的加载和卸载函数如下:

module_init(xxx_init);
module_exit(xxx_exit);

注册或者卸载字符模块时,函数会调用驱动入口函数xxx_init()和驱动出口函数xxx_exit(),这两个函数的原型如下:

/* 驱动入口函数 */
static int __init xxx_init(void)
{
	/* 入口函数具体内容 */
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	/* 出口函数具体内容 */
}

说明:

  • 当执行insmod或modprobe命令时,会执行调用函数module_init(xxx_init);当执行rmmod或modprobe -r命令时,会调用函数module_exit(xxx_exit);
  • 加载模块或卸载模块会调用函数xxx_init()或函数xxx_exit(),可以在这个两个函数中分别调用注册字符设备函数register_chrdev()和删除字符设备函数unregister_chrdev(),完成字符设备的注册或注销。

3. 字符设备LED驱动开发

3.1 开发环境

开发板:JZ2440V3
Linux内核版本:2.6.22.6
编译器:arm-linux-gcc-3.4.5-glibc-2.3.6

3.2 开发底层驱动程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define DEVICE_NAME		"chrleds" 	/* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define LED_MAJOR		251     	/* 主设备号 */

static struct class *leds_class;
static struct class_device	*leds_class_dev;

/* 寄存器物理地址 
 * JZ2440V3开发板上,D10->GPF4;D11->GPF5;D12->GPF6
 */
#define GPACON_BASE	(0x56000000)
#define GPADAT_BASE	(0x56000004)
#define GPFCON_BASE	(GPACON_BASE + 0X50)
#define GPFDAT_BASE	(GPADAT_BASE + 0X50)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *GPFCON;
static void __iomem *GPFDAT;

/* 函数:led_open()
 * 描述:应用程序执行open(...)时,就会调用该函数
 * 参数:inode:传递给驱动的inode
 *       filp:设备文件,file结构体有个叫做private_data的成员变量
 * 			   一般在open的时候将private_data指向设备结构体。
 * 返回: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{	
	int val;

	// 配置GPF3引脚为输出
	val = readl(GPFCON);
	val &= ~(0x3<<(4*2));
	val |= (1<<(4*2));
	writel(val, GPFCON);
	
	// 配置GPF4引脚为输出
	val = readl(GPFCON);
	val &= ~(0x3<<(5*2));
	val |= (1<<(5*2));
	writel(val, GPFCON);

	// 配置GPF5引脚为输出
	val = readl(GPFCON);
	val &= ~(0x3<<(6*2));
	val |= (1<<(6*2));
	writel(val, GPFCON);

	// 都输出0
	val = readl(GPFDAT);
	val &= ~(1<<4);
	val &= ~(1<<5);
	val &= ~(1<<6);
	writel(val, GPFDAT);
	return 0;
}

/* 函数:led_read()
 * 描述:应用程序执行read(...)时,就会调用该函数,从设备读取数据 
 * 参数:filp:要打开的设备文件(文件描述符)
 *       buf:返回给用户空间的数据缓冲区
 * 		 cnt:要读取的数据长度
 * 		offt:相对于文件首地址的偏移
 * 返回:读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int  err;
	err = copy_to_user(buf, (const void *)&leds_status, 1);
		if(err < 0) {
		printk("kernel read failed!\r\n");
		return -EFAULT;
	}
	return 0;
}

/* 函数:led_write()
 * 描述:应用程序执行write(...)时,就会调用该函数,向设备写数据 
 * 参数:filp:设备文件,表示打开的文件描述符
 *       buf:要写给设备写入的数据
 * 			 cnt:要写入的数据长度
 * 		  offt:相对于文件首地址的偏移
 * 返回:写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	char ledstat;
	int val;
	char err;
	
	err = copy_from_user(&ledstat, buf, 1);
	if(err < 0) 
	{
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	printk("%d   \r\n",ledstat);

	if(ledstat == 0x0) 
	{
		/* 关闭所有LED灯 */
		val = readl(GPFDAT);
		val |= (1 << 4);	
		val |= (1 << 5);
		val |= (1 << 6);
		writel(val, GPFDAT);

		printk("Close All leds!\r\n");
	} 
	else
	{
		/* 打开所有LED灯 */
		val = readl(GPFDAT);
		val &= ~(1 << 4);	
		val &= ~(1 << 5);
		val &= ~(1 << 6);
		writel(val, GPFDAT);

		printk("Open All leds!\r\n");
	}

	return 0;
}

/* 
 * 这个结构是字符设备驱动程序的核心
 * 当应用程序操作设备文件时所调用的open、read、write等函数
 * 最终会调用这个结构中指定的对应函数
 */
static struct file_operations chrled_fops = {
	.owner = THIS_MODULE,
	.open  = led_open,
	.read  = led_read,
	.write = led_write,
};

/* 函数:led_init()
 * 描述:执行insmod命令时就会调用这个函数 
 * 参数:无
 * 返回:无
 */
static int __init led_init(void)
{
	int val = 0;

	/* 1、寄存器地址映射 */
	GPFCON = ioremap(GPFCON_BASE, 4);
	if (!GPFCON) {
		return -EIO;
	}

  GPFDAT = ioremap(GPFDAT_BASE, 4);
	if (!GPFDAT) {
		return -EIO;
	}

    /* 注册字符设备
     * 参数为主设备号、设备名字、file_operations结构;
     * 这样,主设备号就和具体的file_operations结构联系起来了,
     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
     * LED_MAJOR可以设为0,表示由内核自动分配主设备号
     */
    val = register_chrdev(LED_MAJOR, DEVICE_NAME, &chrled_fops);
    if (val < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return val;
    }

	/* 新建一个类 */
	leds_class = class_create(THIS_MODULE, DEVICE_NAME);
	if (IS_ERR(leds_class))
		return PTR_ERR(leds_class);

	/* 在类下创建一个设备 */
	leds_class_dev = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL,DEVICE_NAME);
	if (unlikely(IS_ERR(leds_class_dev)))
			return PTR_ERR(leds_class_dev);

	printk(DEVICE_NAME " initialized!\n");

	return 0;
}

/* 函数:led_exit()
 * 描述:执行rmmod命令时就会调用这个函数 
 * 参数:无
 * 返回:无
 */
static void __exit led_exit(void)
{
  	/* 卸载驱动程序 */
  	unregister_chrdev(LED_MAJOR, DEVICE_NAME);

	class_device_unregister(leds_class_dev);

	class_destroy(leds_class);

	/* 取消映射 */
	iounmap(GPFCON);
	iounmap(GPFDAT);
}

/* 指定驱动程序的初始化函数和卸载函数 */
module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("https://me.csdn.net/qq_31782183");
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");

在驱动程序开发中,不能直接操作物理地址,而是用虚拟地址映射到物理地址,程序中操作虚拟地址,利用函数ioremap()完成映射;相反,解除映射关系利用函数iounmap(),函数的原型如下:

#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)

 /* 函数:__arm_ioremap()
 * 描述:完成虚拟地址到物理地址的映射
 * 参数:phys_addr:物理地址起始地址
 * 			 size:要映射内存的空间大小
 *          mtype:ioremap类型	MT_DEVICE
 * 							    MT_DEVICE_NONSHARED
 * 							    MT_DEVICE_CACHED
 * 							    MT_DEVICE_WC
 * 返回:虚拟地址的起始地址
 */	
void __iomem * __arm_ioremap(phys_addr_t phys_addr, 
							      size_t size,
							unsigned int mtype)
{
	return arch_ioremap_caller(phys_addr, size, mtype,
	__builtin_return_address(0));
}

/* 函数:iounmap()
 * 描述:完成虚拟地址到物理地址的映射
 * 参数:addr:虚拟地址起始地址
 * 返回:无
 */
void iounmap (volatile void __iomem *addr)

使用函数ioremap()将寄存器的物理地址映射到虚拟地址后,我们就可以通过指针访问虚拟地址,从而访问物理地址,建议使用如下函数:

读函数:

u8  readb(const volatile void __iomem *addr)	/* 对应8bit写操作 */
u16 readw(const volatile void __iomem *addr)	/* 对应16bit写操作 */
u32 readl(const volatile void __iomem *addr)	/* 对应32bit写操作 */

写函数:

/* value:写入的数值
 * addr:写入的地址(虚拟地址)
 */
void writeb(u8 value,  volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

3.3 开发应用程序

#include 
#include 
#include 
#include 

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char **argv)
{
	int fd;
	char *filename;
	char dat[1];

	if(argc != 3)
	{
		printf("Please input:\n");
		printf("%s /dev/chrleds on\n", argv[0]);
    	printf("%s /dev/chrleds off\n", argv[0]);
		return 0;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0)
	{
		printf("file %s open failed!\r\n", filename);
		return 0;
	}

	if(!strcmp("on",argv[2]))
	{
		dat[0] = 1;
        write(fd, dat, 1);
	}
	else if (!strcmp("off", argv[2]))
    {
        dat[0] = 0;
        write(fd, dat, 1);
    }
    else
    {
		printf("Please input:\n");
		printf("%s /dev/chrleds on\n", argv[0]);
    	printf("%s /dev/chrleds off\n", argv[0]);
    }

	return 0;
}

3.4 编写Makeile文件

内容如下:

KERNELDIR := /home/lib/linux/systemcode/linux-2.6.22.6

CURRENT_PATH := $(shell pwd)

obj-m := chrleds.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
	rm -rf modules.order

3.5 编译和测试

3.5.1 编译驱动模块:
make

编译完成,会生成chrleds.ko的驱动模块文件。

3.5.2 编译APP测试程序:
arm-linux-gcc chrledsApp.c -o chrledsApp

编译完成后,会生成chrledsApp应用程序。

3.5.3 挂接nfs文件系统:
mount -t nfs -o tcp,nolock 192.168.1.1:/home/lib/linux/nfs/fs_mini_mdev /mnt

启动开发板后,执行该语句,将自己的nfs系统挂接,方便测试。

3.5.4 加载模块:

将chrleds.ko和chrledsApp两个文件拷贝到挂接系统的/lib/modules/2.6.22.6 目录下,输入以下命令:

insmod chrleds.ko

驱动加载成功后,会创建"/dev/chrleds"设备节点。

3.5.5 输入如下命名测试LED灯:
./chrledsApp /dev/chrleds on	/* 打开LED灯 */./chrledsApp /dev/chrleds off	/* 关闭LED灯 */

如果要卸载模块,输入如下命令:

rmmod chrleds.ko

你可能感兴趣的:(Linux驱动)