嵌入式内核及驱动开发(初级)

嵌入式内核及驱动开发(初级)

一、设备环境搭建

1.交叉开发模式

交叉开发模式
具体操作步骤见嵌入式系统移植掌握

2.目标机开发方式

tftp+nfs开发

3.tftp服务器与nfs服务器配置

具体操作步骤见tftp服务器与nfs服务器配置

/*Linux驱动开发环境搭建流程*/
1,ubuntu中配置编译环境
		设置交叉工具链:
			tar -xvf gcc-4.6.4.tar.xz -C  ~/Linux_4412/toolchain
		设置环境变量:
			vim  ~/.bashrc  最后面添加
				export PATH=$PATH:/home/george/Linux_4412/toolchain/gcc-4.6.4/bin
		更新脚本:
			source ~/.bashrc

			arm-none-linux-gnueabi-gcc -v
				Using built-in specs.
				COLLECT_GCC=arm-none-linux-gnueabi-gcc
				COLLECT_LTO_WRAPPER=/home/george/Linux_4412/toolchain/gcc-4.6.4/bin/
				../libexec/gcc/arm-arm1176jzfssf-linux-gnueabi/4.6.4/lto-wrapper

2,运行开发
		a,通过tftp去启动内核
			1,将uImage和dtb文件放入到ubuntu中/tftpboot
			2,在开发板中设置uboot参数,使其能够去加载内核
				set ipaddr 192.168.7.22
				set serverip 192.168.7.21
				set bootcmd tftp 0x41000000 uImage \; tftp 0x42000000 exynos4412-fs4412.dtb \; bootm 0x41000000 - 0x42000000
				save
		
		b,通过nfs去挂载rootfs
			1,需要一个跟文件系统目录--rootfs.tar.xz,需要解压到ubuntu
				sudo tar -xvf rootfs.tar.xz  -C /opt/4412/
			
			2, 配置nfs服务器(需要安装),让/opt/4412/rootfs可以被挂载
				sudo vim /etc/exports
					 /opt/4412/rootfs                *(subtree_check,rw,no_root_squash,async)
				
				sudo service nfs-kernel-server restart  //重启nfs服务器
			
				测试:
					sudo mount -t nfs localhost:/opt/4412/rootfs   /mnt
			3,在开发中去指定内核要挂载/opt/4412/rootfs--切换到开发操作
				set bootargs console=ttySAC2,115200 init=/linuxrc root=/dev/nfs rw nfsroot=192.168.7.21:/opt/4412/rootfs ip=192.168.7.22
				save

				解释:
					bootargs 是uboot传递给内核到启动参数,是一个字符串

						console=xxx: 告诉内核启动时候到调试信息是从哪个设备输出
						init=xxx:  告诉内核linux到第一个用户进程是什么
						root=xxx : 告诉内核根文件系统在哪里
							root=/dev/nfs 表示根文件系统在网路远端
							nfsroot=ip:path
						ip=xxx :告诉内核开机的时候内核的ip地址是多少(静态分配ip)

						
				
	
3,可以开始去编写代码--开发驱动
		a, 编译内核
			 tar -xvf linux-3.14.tar.xz


			 步骤:
				1,设置交叉工具链--uImage也运行arm开发板
					vim  Makefile
						ARCH = arm
						CROSS_COMPILE = arm-none-linux-gnueabi-
				2, 选择一个soc	,可以支持很多到soc,所以必须挑出针对我们到平台到代码
					make exynos_defconfig
						//  cp -raf  arch/arm/configs/exynos_defconfig   .config
				
				3,make menuconfig 内核裁剪,产生一个图像界面
						System Type  ---> 
							(2) S3C UART to use for low-level messages
				4,make uImage  : 编译内核

					//如果编译报错:缺mkimage
						sudo cp -raf mkimage  /usr/bin/
						sudo chmod 777 /usr/bin/
						重新在make uImage

				5,编译设备树文件--描述设备信息--最终要编译成dtb
					以一个默认到dts为参考,变成我们自己想要的dts

					arch/arm/boot/dts$ cp exynos4412-origen.dts exynos4412-fs4412.dts


					arch/arm/boot/dts$ vim Makefile
						70行        exynos4412-fs4412.dtb \

					回到内核源码顶层目录:
					george@ubuntu:~/Linux_4412/kernel/linux-3.14$ make dtbs
						  DTC     arch/arm/boot/dts/exynos4210-origen.dtb
						  DTC     arch/arm/boot/dts/exynos4210-smdkv310.dtb
						  DTC     arch/arm/boot/dts/exynos4210-trats.dtb
						  DTC     arch/arm/boot/dts/exynos4210-universal_c210.dtb
						  DTC     arch/arm/boot/dts/exynos4412-odroidx.dtb
						  DTC     arch/arm/boot/dts/exynos4412-origen.dtb
						  DTC     arch/arm/boot/dts/exynos4412-fs4412.dtb
						  DTC     arch/arm/boot/dts/exynos4412-smdk4412.dtb
						  DTC     arch/arm/boot/dts/exynos4412-tiny4412.dtb
						  DTC     arch/arm/boot/dts/exynos4412-trats2.dtb
						  DTC     arch/arm/boot/dts/exynos5250-arndale.dtb
						  DTC     arch/arm/boot/dts/exynos5250-smdk5250.dtb
						  DTC     arch/arm/boot/dts/exynos5250-snow.dtb
						  DTC     arch/arm/boot/dts/exynos5420-arndale-octa.dtb
						  DTC     arch/arm/boot/dts/exynos5420-smdk5420.dtb
						  DTC     arch/arm/boot/dts/exynos5440-sd5v1.dtb
						  DTC     arch/arm/boot/dts/exynos5440-ssdk5440.dtb

			使用uImag和dtb文件
				cp -raf arch/arm/boot/uImage  /tftpboot
				cp -raf arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot


		==============================================
		移植dm9000
			实际是设备树文件修改
				vim   arch/arm/boot/dts/exynos4412-fs4412.dts
			添加如下内容:
				srom-cs1@5000000 { 
					 compatible = "simple-bus"; 
					 #address-cells = <1>; 
					 #size-cells = <1>; 
					 reg = <0x5000000 0x1000000>; 
					 ranges; 

					 ethernet@5000000 { 
						  compatible = "davicom,dm9000"; 
						  reg = <0x5000000 0x2 0x5000004 0x2>; 
						  interrupt-parent = <&gpx0>; 
						  interrupts = <6 4>; 
						  davicom,no-eeprom; 
						  mac-address = [00 0a 2d a6 55 a2]; 
					 }; 
				}; 

				保存退出后,需要再次编译dts文件
					make dtbs

			配置内核:
				make menuconfig
				[*] Networking support   --->
							Networking options   --->
								<*> Packet socket
								<*> Unix domain sockets 
								[*] TCP/IP networking
								[*]    IP: kernel level autoconfiguration
									[*]     IP: BOOTP support

				Device Drivers   --->
					[*] Network device support   --->
						[*]    Ethernet driver support (NEW)   --->
							<*>    DM9000 support
				File systems   --->
						[*] Network File Systems (NEW)   --->
								<*>    NFS client support
								[*]  NFS client support for NFS version 2 
								[*]      NFS client support for NFS version 3
								[*]        NFS client support for the NFSv3 ACL protocol extension
								[*]    Root file system on NFS
				退出到时候要保存:
					再次编译内核:
						make  uImage -j2

				cp -raf arch/arm/boot/uImage  /tftpboot
				cp -raf arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot

				在开发板中到uboot设置中,添加一个参数	clk_ignore_unused		
				set bootargs  clk_ignore_unused console=ttySAC2,115200 init=/linuxrc root=/dev/nfs rw nfsroot=192.168.7.21:/opt/4412/rootfs ip=192.168.7.22 

				重新启动开发板



		b, 编写驱动代码
			1,用什么工具去写---source insight(看代码的工具)
					环境搭建\烧录镜像和工具\si_linux3.14-ori.tgz
					解压到内核源码的顶层目录:

					tar  -xvf   si_linux3.14-ori.tgz


			2,怎么写
				在souceinsght去写
				
				驱动代码需要有四个部分
					1,头文件
						#include <linux/init.h>
						#include <linux/module.h>
					2,驱动模块装载和卸载函数入口到声明
						module_init(hello_drv_init);
						module_exit(hello_drv_exit);
					3,实现模块装载和卸载函数入口
						static int __init hello_drv_init(void)
						{
							return 0;
						}

						static void __exit hello_drv_exit(void)
						{

						}
					4,GPL声明
						MODULE_LICENSE("GPL");


		c,编译驱动代码--Makefile(被读取两次: make  2,内核源码中Makefile)
			ROOTFS_DIR = /opt/4412/rootfs

			ifeq ($(KERNELRELEASE), )
			#内核源码到路径,不同环境会不一样,内核源码一定要先编译
			KERNEL_DIR = /home/george/Linux_4412/kernel/linux-3.14
			CUR_DIR = $(shell pwd)

			all :
					make -C  $(KERNEL_DIR) M=$(CUR_DIR) modules

			clean :
					make -C  $(KERNEL_DIR) M=$(CUR_DIR) clean

			install:
					cp -raf *.ko   $(ROOTFS_DIR)/drv_module


			else
			#用于指定到底编译哪个代码--hello.c
			obj-m += hello.o


			endif


		d,加载与卸载ko
			[root@farsight drv_module]# insmod hello.ko
				[ 2789.700000] -------hello_drv_init-------------

			[root@farsight drv_module]# lsmod
				hello 805 0 - Live 0xbf000000 (O)
			[root@farsight drv_module]# rmmod hello
				rmmod: can't change directory to '/lib/modules': No such file or directory
			[root@farsight drv_module]# mkdir /lib/modules

			[root@farsight drv_module]# rmmod hello
				rmmod: can't change directory to '3.14.0': No such file or directory
			[root@farsight drv_module]# mkidr /lib/modules/3.14.0

			[root@farsight drv_module]# rmmod hello
				[ 2903.230000] -------hello_drv_exit-------------

(二三)、驱动模块开发

在这里插入图片描述

1. 参数传递

加载ko:  insmod hello.ko myname="george" myvalue=33

用途: wifi驱动,wifi硬件中内部也运行内部代码,原厂开发,这些代码叫做固件--firmware.bin
	  装载wifi驱动,必须告诉固件到文件在哪里
		 insmod  rtxxx.ko path=/lib/modules/firmware/xxx.bin

在代码如何处理参数:
module_param(name, type, perm)
参数1:表示参数到名字,比如myname, myvalue
参数2:参数到类型, charp, int
参数3: /sys/modules/表示文件到权限: 0666

用法:
	module_param(myvalue, int, 0666);
	module_param(myname, charp, S_IRUGO|S_IWUGO|S_IXUGO);

2,符号导出

#include <linux/module.h>
#include <linux/init.h>



	//不需要模块加载和卸载到入口声明,直接定义好一些封装的函数

	int my_add(int a, int b)
	{
		return a+b;
	}

	EXPORT_SYMBOL(my_add);

	int my_sub(int a, int b)
	{
		return a-b;
	}

	EXPORT_SYMBOL(my_sub);

	MODULE_LICENSE("GPL");

(四五六)、字符设备驱动

1. 字符设备驱动开发框架

字符设备驱动开发框架

  • 作为字符设备驱动要素:
    1,必须有一个设备号,用在众多到设备驱动中进行区分
    2,用户必须知道设备驱动对应到设备节点(设备文件)
    linux把所有到设备都看成文件
    crw-r----- 1 root root 13, 64 Mar 28 20:14 event0
    crw-r----- 1 root root 13, 65 Mar 28 20:14 event1
    crw-r----- 1 root root 13, 66 Mar 28 20:14 event2
    3,对设备操作其实就是对文件操作,应用空间操作open,read,write的时候
    实际在驱动代码有对应到open, read,write

2. 申请设备号和创建设备节点

1,作为驱动必须有一个主设备号–向系统申请

  • int register_chrdev(unsigned int major, const char * name, const struct file_operations * fops)
    参数1:主设备号
    设备号(32bit–dev_t)==主设备号(12bit) + 次设备号(20bit)
    主设备号:表示一类设备–camera
    次设备号: 表示一类设备中某一个:前置,后置
    给定到方式有两种:
    1,动态–参数1直接填0
    2,静态–指定一个整数,250
    参数2: 描述一个设备信息,可以自定义
    /proc/devices列举出所有到已经注册的设备
    参数3: 文件操作对象–提供open, read,write
    返回值: 正确返回0,错误返回负数
  • void unregister_chrdev(unsigned int major, const char * name)
    参数1:主设备号
    参数2: 描述一个设备信息,可以自定义

2,创建设备节点:
1,手动创建–缺点/dev/目录中文件都是在内存中,断电后/dev/文件就会消失
mknod /dev/设备名 类型 主设备号 次设备号
比如:

mknod  /dev/chr0  c  250 0
[root@farsight drv_module]# ls /dev/chr0 -l
crw-r--r--    1 0        0         250,   0 Jan  1 00:33 /dev/chr0

2,自动创建(通过udev/mdev机制)
//创建一个类

  • struct class *class_create(owner, name)
    参数1: THIS_MODULE
    参数2: 字符串名字,自定义
    返回一个class指针

//创建一个设备文件

  • struct device *device_create(struct class * class, struct device * parent, dev_t devt,
    void * drvdata, const char * fmt,…)
    参数1: class结构体,class_create调用之后到返回值
    参数2:表示父亲,一般直接填NULL
    参数3: 设备号类型 dev_t
    dev_t devt
    #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
    #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
    #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
    参数4:私有数据,一般直接填NULL
    参数5和6:表示可变参数,字符串,表示设备节点名字

//销毁动作:

  • void device_destroy(devcls, MKDEV(dev_major, 0));
    参数1: class结构体,class_create调用之后到返回值
    参数2: 设备号类型 dev_t

  • void class_destroy(devcls);
    参数1: class结构体,class_create调用之后到返回值

3,在驱动中实现文件io的接口,应用程序可以调用文件io

a,驱动中实现文件io操作接口:struct file_operations

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 (*iterate) (struct file *, struct dir_context *);
		unsigned int (*poll) (struct file *, struct poll_table_struct *);
		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 *, loff_t, loff_t, 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 **);
		long (*fallocate)(struct file *file, int mode, loff_t offset,
				  loff_t len);
		int (*show_fdinfo)(struct seq_file *m, struct file *f);
		}; //函数指针的集合,其实就是接口,我们写驱动到时候需要去实现
const struct file_operations my_fops = {
				.open = chr_drv_open,
				.read = chr_drv_read,
				.write = chr_drv_write,
				.release = chr_drv_close,
		};

b,应用程序如何去调用文件io去控制驱动–open,read,…

fd = open("/dev/chr2", O_RDWR);
			if(fd < 0)
			{
				perror("open");
				exit(1);
			}

			read(fd, &value, 4);

			write(fd, &value, 4);


			close(fd);

3. 用户控制驱动和驱动控制硬件方式

用户空间和内核空间到数据交互
1,应用程序需要传递数据给驱动

  • int copy_to_user(void __user * to, const void * from, unsigned long n)
    //将数据从内核空间拷贝到用户空间,一般是在驱动中chr_drv_read()用
    参数1:应用驱动中的一个buffer
    参数2:内核空间到一个buffer
    参数3:个数
    返回值:大于0,表示出错,剩下多少个没有拷贝成功
    等于0,表示正确
  • int copy_from_user(void * to, const void __user * from, unsigned long n)
    //将数据从用户空间拷贝到内核空间,一般是在驱动中chr_drv_write()用
    参数1:内核驱动中的一个buffer
    参数2:应用空间到一个buffer
    参数3:个数

2, 控制外设,其实就是控制地址,内核驱动中是通过虚拟地址操作
内核控制外设到方法

  • void *ioremap(cookie, size)
    参数1: 物理地址
    参数2: 长度
    返回值: 虚拟地址

    去映射–解除映射

  • void iounmap(void __iomem *addr)
    参数1: 映射之后到虚拟地址

3, 通过驱动控制led灯:
led— GPX2_7 — GPX2CON ==0x11000C40
GPX2DAT ==0x11000C44

将0x11000C40映射成虚拟地址
对虚拟地址中到[32:28] = 0x1

4,应用程序和驱动扮演的是什么角色

  • 用户态:应用程序
    玩策略: 怎么去做
    1, 一闪一闪
    2,10s闪一次,也可以1s闪一次
    3,一直亮
    4,跑马灯
    控制权是在应用程序(程序员)

  • 内核态:驱动
    玩机制: 能做什么
    led:亮 和 灭
    5,编写字符设备驱动到步骤和规范

  • 步骤:
    1,实现模块加载和卸载入口函数
    module_init(chr_dev_init);
    module_exit(chr_dev_exit);
    2,在模块加载入口函数中
    a, 申请主设备号 (内核中用于区分和管理不同字符设备)
    register_chrdev(dev_major, “chr_dev_test”, &my_fops);
    b,创建设备节点文件 (为用户提供一个可操作到文件接口–open())
    struct class *class_create(THIS_MODULE, “chr_cls”);
    struct device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, “chr2”);
    c, 硬件的初始化
    1,地址的映射
    gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
    2,中断到申请
    3,实现硬件的寄存器到初始化
    // 需要配置gpio功能为输出
    *gpx2conf &= ~(0xf<<28);
    *gpx2conf |= (0x1<<28);
    e,实现file_operations
    const struct file_operations my_fops = {
    .open = chr_drv_open,
    .read = chr_drv_read,
    .write = chr_drv_write,
    .release = chr_drv_close,
    };

  • 规范:
    1,面向对象编程思想
    用一个结构体来表示一个对象
    //设计一个类型,描述一个设备的信息

struct led_desc{
				unsigned int dev_major; //设备号
				struct class *cls;
				struct device *dev; //创建设备文件
				void *reg_virt_base;
			};

struct led_desc *led_dev;//表示一个全局的设备对象
// 0, 实例化全局的设备对象–分配空间
// GFP_KERNEL 如果当前内存不够用到时候,该函数会一直阻塞(休眠)

//  #include <linux/slab.h>
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
	if(led_dev == NULL)
	{
		printk(KERN_ERR "malloc error\n");
		return -ENOMEM;
	}
	led_dev->dev_major = 250;

2,做出错处理
在某个位置出错了,要将之前申请到资源进行释放

led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);	
					led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);
			if(led_dev->dev_major < 0)
			{
				printk(KERN_ERR "register_chrdev error\n");
				ret = -ENODEV;
				goto err_0;
			}
			err_0:
				kfree(led_dev);
				return ret;

6, 操作寄存器地址到方式:
1, volatile unsigned long *gpxcon;
*gpxcon &= ~(0xf<<28);
2, readl/writel();
u32 readl(const volatile void __iomem *addr)//从地址中读取地址空间到值
void writel(unsigned long value , const volatile void __iomem *add)
// 将value的值写入到addr地址
例子:

// gpio的输出功能的配置
			u32 value = readl(led_dev->reg_virt_base);
			value &= ~(0xf<<28);
			value |= (0x1<<28)
			writel(value, led_dev->reg_virt_bas);	

或者:

*gpx2dat |= (1<<7);
		替换成:
			writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );

(七~十一)、中断编程

1、中断号和中断申请

1,中断号–就是一个号码,需要通过一定的方式去获取到
在3.14.0内核中,从设备树中获取
中断处理框架

获取中断号到方法:
1, 宏定义
IRQ_EINT(号码)
2,设备树文件中
arch/arm/boot/dts/exynos4412-fs4412.dts

硬件连接:
key ---- gpx1_2— EINT10
设备树文件:arch/arm/boot/dts/exynos4x12-pinctrl.dtsi
gpx1: gpx1 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
interrupt-parent = <&gic>;
interrupts = <0 24 0>, <0 25 0>, <0 26 0>, <0 27 0>,
<0 28 0>, <0 29 0>, <0 30 0>, <0 31 0>;
#interrupt-cells = <2>;
};

在编程过程中,需要定义自己的节点–描述当前设备用的中断号
arch/arm/boot/dts/exynos4412-fs4412.dts +51

 key_int_node{
                compatible = "test_key";
                interrupt-parent = <&gpx1>;
                interrupts = <2 4>;
			};

编译设备树文件:
make dtbs
更新dtbs文件:
cp -raf arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot/

中断号的描述
2,在驱动中去通过代码获取到中断号,并且申请中断(实现中断处理方法)

a,获取到中断号码:
	int get_irqno_from_node(void)
	{
		// 获取到设备树中到节点
		struct device_node *np = of_find_node_by_path("/key_int_node");
		if(np){
			printk("find node ok\n");
		}else{
			printk("find node failed\n");
		}

		// 通过节点去获取到中断号码
		int irqno = irq_of_parse_and_map(np, 0);
		printk("irqno = %d\n", irqno);
		
		return irqno;
	}
b,申请中断
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void * dev)
	参数1: 设备对应的中断号
	参数2: 中断的处理函数
			typedef irqreturn_t (*irq_handler_t)(int, void *);
	参数3:触发方式
			#define IRQF_TRIGGER_NONE	0x00000000  //内部控制器触发中断的时候的标志
			#define IRQF_TRIGGER_RISING	0x00000001 //上升沿
			#define IRQF_TRIGGER_FALLING	0x00000002 //下降沿
			#define IRQF_TRIGGER_HIGH	0x00000004  // 高点平
			#define IRQF_TRIGGER_LOW	0x00000008 //低电平触发
	参数4:中断的描述,自定义,主要是给用户查看的
			/proc/interrupts
	参数5:传递给参数2中函数指针的值
	返回值: 正确为0,错误非0


	参数2的赋值:
	irqreturn_t key_irq_handler(int irqno, void *devid)
	{
		return IRQ_HANDLED;
	}


	
	
释放中断:
		void free_irq(unsigned int irq, void *dev_id)
		参数1: 设备对应的中断号
		参数2:与request_irq中第5个参数保持一致

3,实现字符设备驱动的框架

// 1,设定一个全局的设备对象
key_dev = kzalloc(sizeof(struct key_desc),  GFP_KERNEL);

// 2,申请主设备号
key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);

// 3,创建设备节点文件
key_dev->cls = class_create(THIS_MODULE, "key_cls");
key_dev->dev = device_create(key_dev->cls, NULL, 
								MKDEV(key_dev->dev_major,0), NULL, "key0");

4,驱动中将硬件所产生的数据传递给用户

a,硬件如何获取数据
	key: 按下和抬起: 1/0
	读取key对应的gpio的状态,可以判断按下还是抬起
	
	读取key对应gpio的寄存器--数据寄存器
	
	//读取数据寄存器
	int value = readl(key_dev->reg_base + 4) & (1<<2);

b,驱动如何传递给用户
	在中断处理中填充数据:
		key_dev->event.code = KEY_ENTER;
		key_dev->event.value = 0;


	在xxx_read中奖数据传递给用户
		ret = copy_to_user(buf, &key_dev->event,  count);

c,用户如何拿到--编写应用程序
	while(1)
	{
		read(fd, &event, sizeof(struct key_event));

		if(event.code == KEY_ENTER)
		{
			if(event.value)
			{
				printf("APP__ key enter pressed\n");
			}else{
				printf("APP__ key enter up\n");
			}
		}
	}

2、IO模型

1,实现文件IO模型之一阻塞,等同于休眠

文件io模型:
		1,非阻塞
		2,阻塞
		3,多路复用--select/poll
		4, 异步信号通知faync

阻塞: 当进程在读取外部设备的资源(数据),资源没有准备好,进程就会休眠
	linux应用中,大部分的函数接口都是阻塞
		scanf();
		read();
		write();
		accept();
驱动中需要调用
	1,将当前进程加入到等待队列头中
		add_wait_queue(wait_queue_head_t * q, wait_queue_t * wait)
	2,将当前进程状态设置成TASK_INTERRUPTIBLE
		set_current_state(TASK_INTERRUPTIBLE)
	3,让出调度--休眠
		schedule(void)

更加智能的接口,等同于上面的三个接口:
	wait_event_interruptible(wq, condition)

驱动如何去写代码
	1,等待队列头
			wait_queue_head_t

			init_waitqueue_head(wait_queue_head_t *q);
	
	2,在需要等待(没有数据)的时候,进行休眠
		wait_event_interruptible(wait_queue_head_t wq, condition) // 内部会构建一个等待队列项/节点wait_queue_t
		参数1: 等待队列头
		参数2: 条件,如果是为假,就会等待,如果为真,就不会等待
				可以用一标志位,来表示是否有数据


	3,在一个合适的时候(有数据),会将进程唤醒
		wake_up_interruptible(wait_queue_head_t *q)

		用法:
			wake_up_interruptible(&key_dev->wq_head);
			//同时设置标志位
			key_dev->key_state  = 1;

2, 非阻塞: 在读写的时候,如果没有数据,立刻返回,并且返回一个出错码
用的会比较少,因为比较耗资源

open("/dev/key0", O_RDWR|O_NONBLOCK);
------------------------------------
驱动中需要去区分,当前模式是阻塞还是非阻塞
//如果当前是非阻塞模式,并且没有数据,立马返回一个出错码
if(filp->f_flags & O_NONBLOCK && !key_dev->key_state)
	return -EAGAIN;

3,多路复用–select和poll

poll的应用:
1, 需要打开多个文件(多个设备)

2, 利用poll来实现监控fd的读,写,出错
	#include <poll.h>

   int poll(struct pollfd *fds, nfds_t nfds, int timeout);
   参数1: 表示多个文件描述符集合
		struct pollfd描述的是文件描述符到信息
		struct pollfd {
           int   fd;  //文件描述符
           short events;   //希望监控fd的什么事件:读,写,出错
						POLLIN 读,
						POLLOUT 写,
						POLLERR出错
           short revents;    //结果描述,表示当前的fd是否有读,写,出错
						//用于判断,是内核自动赋值
						POLLIN 读,
						POLLOUT 写,
						POLLERR出错
		};
	参数2:被监控到fd的个数
	参数3: 监控的时间:
				正: 表示监控多少ms
				负数: 无限的时间去监控
				0: 等待0ms,类似于非阻赛
	返回值: 负数:出错
			大于0,表示fd中有数据
			等于0: 时间到

4,如果应用中使用poll对设备文件进行了监控,那么设备驱动就必须实现poll接口

unsigned int key_drv_poll(struct file *filp, struct poll_table_struct *pts)
{
	
	// 返回一个mask值
	unsigned int mask;
	// 调用poll_wait,将当前到等待队列注册系统中
	poll_wait(filp, &key_dev->wq_head, pts);
	
	// 1,当没有数据到时候返回一个0
	if(!key_dev->key_state)
		mask = 0;

	// 2,有数据返回一个POLLIN
	if(key_dev->key_state)
		mask |= POLLIN;

	return mask;
	
}

const struct file_operations key_fops = {
	.poll = key_drv_poll,
	
};

5,异步信号通知: 当有数据到时候,驱动会发送信号(SIGIO)给应用,就可以异步去读写数据,不用主动去读写
a,应用–处理信号,主要是读写数据

	 void catch_signale(int signo)
	{
		if(signo ==  SIGIO)
		{
			printf("we got sigal SIGIO");
			// 读取数据
			read(fd, &event, sizeof(struct key_event));
			if(event.code  == KEY_ENTER)
			{
				if(event.value)
				{
					printf("APP__ key enter pressed\n");
				}else
				{
					printf("APP__ key enter up\n");
				}
			}
		}

	}

	// 1,设置信号处理方法
	signal(SIGIO,catch_signale);
	// 2,将当前进程设置成SIGIO的属主进程
	fcntl(fd, F_SETOWN, getpid());

	// 3,将io模式设置成异步模式
	int flags  = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, flags | FASYNC );

b,驱动--发送信号
	1,需要和进程进行关联--记录信号该发送给谁
		实现一个fasync的接口

		int key_drv_fasync(int fd, struct file *filp, int on)
		{
			//只需要调用一个函数记录信号该发送给谁
			return fasync_helper(fd, filp, on,  &key_dev->faysnc);

		}
	2,在某个特定的时候去发送信号,在有数据的时候
		//发送信号
		kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);

3、中断下半部和timer定时器的实现

1、中断的下半部
1,softirq: 处理比较快,但是内核级别的机制,需要修改整个内核源码,不推荐也不常用
2,tasklet: 内部实现实际调用了softirq
3, workqueue: 工作队列
下半部的由来

1,tasklet:
	struct tasklet_struct
	{
		struct tasklet_struct *next;
		unsigned long state;
		atomic_t count;
		void (*func)(unsigned long); // 下半部的实现逻辑
		unsigned long data; // 传递给func
	};

	a, 初始化
		struct tasklet_struct mytasklet;

		tasklet_init(struct tasklet_struct * t, void(* func)(unsigned long), unsigned long data)

		例子:
		void key_tasklet_half_irq(unsigned long data)
		{
			// 表示有数据,需要去唤醒整个进程/等待队列
			wake_up_interruptible(&key_dev->wq_head);
			//同时设置标志位
			key_dev->key_state  = 1;

			//发送信号
			kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);
		}

		tasklet_init(&key_dev->mytasklet, key_tasklet_half_irq, 45);


	b,在上半部中放入到内核线程中--启动
		// 启动下半步
		tasklet_schedule(&key_dev->mytasklet);

	c,模块卸载的时候:
		tasklet_kill(&key_dev->mytasklet);


2,工作队列和工作
	typedef void (*work_func_t)(struct work_struct *work);

	struct work_struct {
		atomic_long_t data;
		struct list_head entry;
		work_func_t func;
	};

	a, 初始化
		
		void work_irq_half(struct work_struct *work)
		{
			printk("-------%s-------------\n", __FUNCTION__);
			// 表示有数据,需要去唤醒整个进程/等待队列
			wake_up_interruptible(&key_dev->wq_head);
			//同时设置标志位
			key_dev->key_state  = 1;

			//发送信号
			kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);
			
		}
		struct work_struct mywork;

		INIT_WORK(struct work_struct *work, work_func_t func);

	b, 在上半部中放入到内核线程中--启动

		schedule_work(&key_dev->mywork);

下半部的实现基本逻辑

(十二~十五)、平台总线开发

驱动模型

  • 设备驱动模型:bus, driver, device
    struct bus_type :总线对象,描述一个总线,管理device和driver,完成匹配
    struct bus_type {
    const char *name;
    int (*match)(struct device *dev, struct device_driver *drv);
    }
  • 注册和注销
    int bus_register(struct bus_type *bus)
    void bus_unregister(struct bus_type *bus)

device对象:设备对象,描述设备信息,包括地址,中断号,甚至其他自定义的数据
struct device {
struct kobject kobj;  //所有对象的父类
const char *init_name;
// 在总线中会有一个名字,用于做匹配,在/sys/bus/mybus/devices/名字
struct bus_type *bus; //指向该device对象依附于总线的对象
void *platform_data; // 自定义的数据,指向任何类型数据

注册和注销的方法:
int device_register(struct device *dev)
void device_unregister(struct device *dev)

driver对象:描述设备驱动的方法(代码逻辑)
struct device_driver {
const char *name;
// 在总线中会有一个名字,用于做匹配,在/sys/bus/mybus/drivers/名字
struct bus_type *bus;//指向该driver对象依附于总线的对象
int (*probe) (struct device *dev); // 如果device和driver匹配之后,driver要做的事情
int (*remove) (struct device *dev); // 如果device和driver从总线移除之后,driver要做的事情
}
注册和注销:
int driver_register(struct device_driver *drv)
void driver_unregister(struct device_driver *drv)

如何实现总线匹配,匹配成功之后会自动调用driver的probe方法:
1, 实现bus对象中 match方法
2, 保证driver和device中名字要一样

====================================================================
平台总线模型

  • 平台总线模型:
    为什么会有平台总线:
    用于平台升级:三星: 2410, 2440, 6410, s5pc100 s5pv210 4412
    硬件平台升级的时候,部分的模块的控制方式,基本上是类似的
    但是模块的地址是不一样

      	gpio控制逻辑: 1, 配置gpio的输入输出功能: gpxxconf
      				   2, 给gpio的数据寄存器设置高低电平: gpxxdata
      				逻辑操作基本上是一样的
      				但是地址不一样
      	
      	uart控制:1,设置8n1,115200, no AFC
      				UCON,ULCON, UMODOEN, UDIV
      			
      			逻辑基本上是一样的
      			但是地址不一样
    

    问题:
    当soc升级的时候, 对于相似的设备驱动,需要编写很多次(如果不用平台总线)
    但是会有大部分重复代码

    解决:引入平台总线
    device(中断/地址)和driver(操作逻辑) 分离
    在升级的时候,只需要修改device中信息即可(中断/地址)
    实现一个driver代码能够驱动多个平台相似的模块,并且修改的代码量很少

  • 平台总线中的三元素:
    1, bus
    platform_bus:不需要自己创建,开机的时候自动创建

    struct bus_type platform_bus_type = {
    .name = “platform”,
    .dev_groups = platform_dev_groups,
    .match = platform_match,
    .uevent = platform_uevent,
    .pm = &platform_dev_pm_ops,
    };
    匹配方法:
    1,优先匹配pdriver中的id_table,里面包含了支持不同的平台的名字
    2,直接匹配driver中名字和device中名字

      struct platform_device *pdev = to_platform_device(dev);
      struct platform_driver *pdrv = to_platform_driver(drv);
      if (pdrv->id_table)// 如果pdrv中有idtable,平台列表名字和pdev中的名字
      	return platform_match_id(pdrv->id_table, pdev) != NULL;
    
      /* fall-back to driver name match */
      return (strcmp(pdev->name, drv->name) == 0);
    

2,device对象:
struct platform_device {
const char *name; //用于做匹配
int id; // 一般都是直接给-1
struct device dev; // 继承了device父类
u32 num_resources; // 资源的个数
struct resource *resource; // 资源:包括了一个设备的地址和中断
}
注册和注销
int platform_device_register(struct platform_device * pdev);
void platform_device_unregister(struct platform_device * pdev)

3,driver对象
struct platform_driver {
int (*probe)(struct platform_device *); //匹配成功之后被调用的函数
int (*remove)(struct platform_device *);//device移除的时候调用的函数
struct device_driver driver; //继承了driver父类
|
const char *name;
const struct platform_device_id *id_table; //如果driver支持多个平台,在列表中写出来
}
注册和注销
int platform_driver_register(struct platform_driver *drv);
void platform_driver_unregister(struct platform_driver *drv)

==========================================

编写代码: 编写一个能在多个平台下使用的led驱动
1,注册一个platform_device,定义资源:地址和中断
struct resource {
resource_size_t start; // 开始
resource_size_t end; //结束
const char *name; //描述,自定义
unsigned long flags; //区分当前资源描述的是中断(IORESOURCE_IRQ)还是内存(IORESOURCE_MEM)
struct resource *parent, *sibling, *child;
};

2,注册一个platform_driver,实现操作设备的代码
注册完毕,同时如果和pdev匹配成功,自动调用probe方法:
probe方法: 对硬件进行操作
a,注册设备号,并且注册fops–为用户提供一个设备标示,同时提供文件操作io接口
b, 创建设备节点
c, 初始化硬件
ioremap(地址); //地址从pdev需要获取
readl/writle();
d,实现各种io接口: xxx_open, xxx_read, …
获取资源的方式:
//获取资源
// 参数1: 从哪个pdev中获取资源
// 参数2: 资源类型
// 参数3: 表示获取同种资源的第几个
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值