欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

程序员文章站 2022-07-14 16:25:40
...

一、嵌入式linux字符设备驱动框架

写应用程序的人 不应该去看电路图,但是如何操作硬件呢:调用驱动程序里的open,read,write等来实现。

C库里实现了 open 、read、write上层函数

调用open等:swi val—引发一个异常中断,进入内核异常处理函数。

系统调用接口:根据发生中断的原因,调用处理函数(sys_open,sys_read等sys_open等函数会执行与open相关各种初始化函数,通知调用自己写好的open函数,这里注意应用程序的open不仅仅是调用驱动中的open,其他函数类似,是调用sys_open,sys_open里包含了驱动中实现的open)。

sys_open:根据调用的不同设备,执行不同的函数。

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

总结:

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

APP应用程序 调用open read(c库中实现,swi),swi val—引发一个异常中断,进入内核异常处理函数,根据发生中断的原因,调用处理函数(sys_open,sys_read等)。sys_open:根据调用的不同设备的属性,执行不同的函数。

问题:

App应用程序调用open等,最终调用的是相应设备的open,是如何对应起来的呢?依赖于驱动程序框架。

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

1.简单的字符驱动程序框架

主要包括以下几个部分,这一节我们先看看这几部分都有什么。第三节有完整的驱动框架代码。

(1)写出led_open,led_read等

static int first_drv_open(struct inode *inode, struct file *file)
{
	printk("first_drv_open\n");
	return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	printk("first_drv_write\n");
	return 0;
}

(2)怎么告诉内核呢?定义一个file_operations结构体,填充它:

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};
(3)怎么用起来这个结构体,就是怎么告诉内核?

//注册驱动程序

int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核
	printk("first_drv_init\n");
	return 0;
}
static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载
}

参数:主设备号,名字,结构体

(4)谁来调用这个函数呢:驱动的入口函数,需要一个宏来修饰一下:

module_init(first_drv_init);
module_exit(first_drv_exit);//函数指针

二、函数对应过程分析:

查看下设备节点信息:
07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

C表示字符设备    4 主设备号 63次设备号

-常规文件

D目录.

当APP使用open的时候 首先判断设备属性是字符设备,然后根据主设备号 去数组中找到对应的file_operation结构体。

用设备类型+设备号(字符设备+主设备号)就可以找到fileoperation结构

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

register_chrdev最简单的实现方式:以Major为索引,把file_operation写进去,实现注册。
07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

VFS如何根据要打开的东西,找到驱动程序:

VFS:根据字符设备数组,根据设备号找到file_operation结构

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

file_operation里:

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

入口函数里调用 regist_chrdev根据主设备号把file_operation放到数组里。

三、简单的字符驱动程序框架如下:

(1)可挂载驱动程序框架代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static int first_drv_open(struct inode *inode, struct file *file)
{
	printk("first_drv_open\n");
	return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	printk("first_drv_write\n");
	return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};


int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核
	printk("first_drv_init\n");
	return 0;
}

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载
}

module_init(first_drv_init);
module_exit(first_drv_exit);
(2)Makefile
KERN_DIR = /home/book/yangfei/linux-2.6.22.6

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= first_drv.o
驱动编译依赖于内核文件,因此需要提前解压内核源码并编译。

(3)执行make,生成.ko文件 并拷贝至NFS文件夹

make

cp first_drv.ko/work/nfs_root/drivers_and_test/

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

(4)手动加载驱动程序

cat /proc/devices  :

insmod first_drv.ko


07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

(5)编写驱动测试应用程序:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/* firstdrvtest on
  * firstdrvtest off
  */
int main(int argc, char **argv)
{
	int fd;
	int val = 1;
	fd = open("/dev/xxx", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}
	write(fd, &val, 4);
	return 0;
}

arm-linux-gcc -ofirstdrvtest firstdrvtest.c

cp firstdrvtest/work/nfs_root/

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

挂载nfs服务器

mount -t nfs -o nolock192.168.1.102:/work/nfs_root /mnt


07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

创建设备节点:

# mknod /dev/xxx   c    252   0     ---》/dev会出现xxx设备

ls /dev/

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

测试环境:

格式化nand,烧写光盘自带linux2.26、qt yaffs、uboot1.1.6,gcc:3.4.5.

四、思考与改进

(1)主设备号怎么定 

  • cat/proc/devices 找一个没用的     手工指定主设备号
  • 写0,由系统分配                         自动分配主设备号

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

(2)dev/xxx怎么来的

应用程序

打开某个设备:open(dev/xxx)

首先要:

  • 手工创建设备节点:mknod /dev/xxx c 252 0

但是当我们使用自动分配设备号的时候 ,我们都要cat /proc/devices查看主设备号名字,然后再创建设备节点吗?这样显然不合适。

  • 自动创建设备节点,udex机制(mdev机制)

注册设备的时候,会在/sys下生成设备信息,

mdev:自动根据系统信息创建设备节点。

驱动程序里,提供设备信息,mdev就可以自动创建设备节点。

如何提供设备信息呢:

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

入口函数,先创建一个类—》类下面创建一个设备(提供系统信息)

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

class_create会在/sys下创建 firstdrv这个类,class_create类下创建xyz这个设备,mdev会自动创建一个dev/xyz设备节点

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

出口函数:

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

编译报错:

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

解决方法:

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

五、嵌入式linux   led字符设备驱动

前面写好了字符设备驱动程序框架,现在实现led字符设备驱动:
1、需要做什么
  • 搭框架
  • 完善硬件操作

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

(1)opne中实现端口输入输出配置

(2)入口函数:完成虚拟地址映射

(3)write中实现LED操作

copy_from_user 用户空间到内核空间传递数据。

copy_to_user();  内核空间到用户空间传递数据

2代码实现

(1)驱动完整代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *firstdrv_class;
static struct class_device	*firstdrv_class_dev;

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;


static int first_drv_open(struct inode *inode, struct file *file)
{
	//printk("first_drv_open\n");
	/* 配置GPF4,5,6为输出 */
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;

	//printk("first_drv_write\n");

	copy_from_user(&val, buf, count); //	copy_to_user();

	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};


int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	firstdrv_class = class_create(THIS_MODULE, "firstdrv");

	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	return 0;
}

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载

	class_device_unregister(firstdrv_class_dev);
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}

module_init(first_drv_init);
module_exit(first_drv_exit);


MODULE_LICENSE("GPL");
(2)编译程序,加载驱动

07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

3 改进

当我们想实现对硬件的多种情况操作时:

(1)根据传入参数,执行多种操作

(2)创建多个设备节点,执行不同的硬件操作

代码参考:

drivers_and_test\leds\myleds.c

/*
 * 执行insmod命令时就会调用这个函数 
 */
static int __init s3c24xx_leds_init(void)
//static int __init init_module(void)

{
    int ret;
	int minor = 0;

    gpio_va = ioremap(0x56000000, 0x100000);
	if (!gpio_va) {
		return -EIO;
	}

    /* 注册字符设备
     * 参数为主设备号、设备名字、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;
    }

	leds_class = class_create(THIS_MODULE, "leds");
	if (IS_ERR(leds_class))
		return PTR_ERR(leds_class);
    


	leds_class_devs[0] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "leds"); /* /dev/leds */
	
	for (minor = 1; minor < 4; minor++)  /* /dev/led1,2,3 */
	{
		leds_class_devs[minor] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, minor), NULL, "led%d", minor);
		if (unlikely(IS_ERR(leds_class_devs[minor])))
			return PTR_ERR(leds_class_devs[minor]);
	}
        
    printk(DEVICE_NAME " initialized\n");
    return 0;
}

驱动测试应用程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/*
  *  ledtest <dev> <on|off>
  */

void print_usage(char *file)
{
    printf("Usage:\n");
    printf("%s <dev> <on|off>\n",file);
    printf("eg. \n");
    printf("%s /dev/leds on\n", file);
    printf("%s /dev/leds off\n", file);
    printf("%s /dev/led1 on\n", file);
    printf("%s /dev/led1 off\n", file);
}
int main(int argc, char **argv)
{
    int fd;
    char* filename;
    char val;

    if (argc != 3)
    {
        print_usage(argv[0]);
        return 0;
    }
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf("error, can't open %s\n", filename);
        return 0;
    }

    if (!strcmp("on", argv[2]))
    {
        // 亮灯
        val = 0;
        write(fd, &val, 1);
    }
    else if (!strcmp("off", argv[2]))
    {
        // 灭灯
        val = 1;
        write(fd, &val, 1);
    }
    else
    {
        print_usage(argv[0]);
        return 0;
    }
    
    
    return 0;
}
07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-LED字符设备驱动

到此,我们实现了简单的字符设备驱动框架,并在框架上加入了led硬件操作,实现了led字符设备驱动的编写与驱动应用程序的编写。

六、需要注意的几点

(1)以下两个函数实现了在内核中加入设备信息,mdev就可以根据/sys下生成的设备信息,自动创建设备节点

firstdrv_class = class_create(THIS_MODULE, "firstdrv");

firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */
(2)查看设备驱动设备:cat /proc/devices   (主设备号+设备名称)

查看设备节点:ls /dev/xxx 

(3)卸载驱动

rmmod first_drv

(4)卸载设备节点

rm  /dev/xxx

(5)APP应用程序打开的open函数,打开的设备节点,而不是驱动程序。

(6)arm单板和虚拟机之间通过网络文件系统NFS传递文件:

mount -t nfs -o nolock192.168.1.102:/work/nfs_root /mnt

(7)编译驱动程序的内核和单板运行的内核要一致

 编译驱动测试程序的工具链版本和文件系统的版本要一致,不然应用程序可能会缺少某些库而无法运行

(8)驱动程序框架有自己的函数,就是这么用,记住就可以,所以入门应该是比较简单的。

(9)应用程序的open是如何找到驱动程序的open并执行的。

注册驱动程序:在数组中以主设备号为索引,file_operation为内容,进行注册

open的时候,根据要打开的设备节点的类型,主设备号在数组中找到对应的file_operation结构体,并使用里面对应的open函数。

(11)自动创建设备节点的时候,会在/sys下产生设备信息,以便以创建设备节点。