ARM嵌入式学习笔记——《设备驱动基础》

Linux设备驱动基础

  • 明确:Linux系统的两个空间:用户态(用户空间)和内核态(内核空间)。
  • 推荐:
  • 《Unix环境高级编程》第三版
  • 《Linux设备驱动程序》第三版
  • 《Linux内核设计与实现》第三版

用户空间的特点

  • 用户空间包含的软件就是各种应用程序(ls/cd/,),也包括自己编写的UC程序,QT程序,静态库,动态库等等,这些软件在运行的时候就运行在用户空间。
  • 用户空间的软件在运行的时候,对应的CPU核的工作模式就是user用户模式。
  • 用户空间的软件不允许直接访问内核空间的代码,地址和数据。要想访问内核空间必须只能通过系统调用。
  • Linux系统的4G虚拟地址空间中,用户空间每个进程独占前3G,用户虚拟地址范围:0x0000000xBFFFFFF。内核空间进程共享后1G,内核虚拟地址为:0xC0000000xCFFFFFF。
  • 用户空间的软件同样不允许直接访问外设的物理地址,要想访问外设的物理地址必须将物理地址映射到用户空间的虚拟地址上(简称用户虚拟地址),将来访问虚拟映射的用户虚拟地址就是在访问物理地址。
  • 用户空间的软件如果对内存进行非法访问,不会造成Linux系统的奔溃,但是进程将会被系统干掉。
  • 用户空间软件类似网络编程的客户端,它总是向Linux内核空间的软件发起一个服务请求,然后Linux内核空间根据请求完成需求,并且最终的结果返回给用户空间的软件。

Linux内核空间特点

  • 内核空间包含的软件就是uImage。

    • 包含七大子系统的内容,
    • 设备驱动程序(.ko)属于内核空间软件。
  • 内核空间的软件在运行的适合,CPU核的工作模式为SVC管理模式,内核空间的软件权限最高。

  • 内核空间的软件同样不允许直接访问外设的物理地址,要想访问外设的物理地址必须将物理地址映射到内核虚拟地址上,0XC000000~0XFFFFFFF。

  • 内核空间的软件如果对内存进行非法的访问,直接会导致Linux系统奔溃,类似Windows蓝屏。

Linux内核驱动开发基础:

int module_init(void);
void module_exit(void);
MODULE_LICENSE("GPL");
  • 内核程序使用的头文件位于Linux内核源码中。

  • 内核程序的入口函数要用module_init宏来修饰,并且入口函数的返回数据必须是int型。

    • 入口函数的形参列表为void。
    • 入口函数执行成功返回0,失败返回负值。
    • 当执行insmod安装内核程序到内核uImage中,或者采用编译在一起的方式(选择),当内核uImage在启动的时候,这两种情况内核uImage会自动调用入口函数。
    • 切记:入口函数执行成功,这个内核程序的生命周期才刚刚开始,并且内核程序会存储在内存中等待应用程序的访问(通过系统调用来间接访问)。
  • 内核程序的出口函数用module_exit修饰,出口函数的返回值和形参都是void,当执行rmmod从内核uImage中卸载内核程序或者采用不选择的方式编译uImage后。这两种情况内核uImage会自动调用出口函数。

    • 切记:一旦出口函数被调用,这个内核程序将会从内存中彻底清除,不能再为应用程序服务。
  • 任何内核程序,只要是一个.c文件,必须添加MODULE_LICENSE(“GPL”),否则内核会出错并且部分内核函数及变量无法使用。

  • 内核打印函数printk,只要是内核程序调用的函数,不是自己编写的就是内核源码提供的,一律不允许调用标准C库的函数。

Linux内核程序的编译

  • 内核程序的编译一定要关联内核源码(kernel),头文件和调用的函数都是位于内核源码中。

  • 回顾内核程序的编译步骤:

    • 拷贝内核程序到内核源码的某个目录下。
    • 修改Kconfig添加配置菜单。
    • 修改Makefile添加编译支持。
    • 对内核源码使用make menuconfig选择添加模块。
    • make uImage; make modules;
  • 总结:这种内核程序的编译过程极其繁琐,简化只需要一个Makefile即可解决。

编写解决问题的Makefile
cd /opt/drivers/day01/
vim Makefile
#将helloworld.o和uImage分开编译,单独编译
obj-m += helloworld.o
#all:伪目标,对应的命令当前执行make或者make all时执行。
#-C /opt/kernel :进入/opt/kernel目录下。
#SUBDIRS:子目录。
all:
    make -C /opt/kernel SUBDIRS=$(PWD) modules
    
#当执行make clean时进行清除操作
clean:
    make -C /opt/kernel SUBDIRS=$(PWD) clean
    
//将内核程序目标文件拷贝到根文件系统进行安装和卸载。
mkdir /opt/rootfs/home/drivers
cp helloworld.ko /opt/rootfs

#下位机测试:
#重启下位机,进入uboot命令执行:
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs  ip=192.168.1.110:192.168.1.8:192.168.1.1:255.255.255.0  init=/linxurc console=ttySAC0,115200 maxcpus=1

saveenv
tftp 0x48000000 uImage
bootm 0x48000000

#下位机Linux系统启动后执行:
cd /home/drivers
ls
    helloworld.ko
#将内核程序安装到uImage中
insmod helloworld.ko
lsmod
rmmod helloworld #从内核中卸载内核程序。
lsmod

Linux内核程序的命令传递参数

  • 明确:要想给Linux内核传递参数,必须遵循以下三个原则:
    • 接收参数的内核变量必须是全局变量。
      • 内核变量:此变量定义在内核空间,也就是此变量定义在内核程序内,也就是此变量的地址一定位于0xC0000000~0xFFFFFFFF.
    • 接收参数的内核全局变量的数据类型一定是以下类型:
      • 内核全局变量:明确其生命周期,当insmod安装内核程序时,内核会给他分配内存空间,永驻内存,直到rmmod从内核卸载。
      • bool 、invbool:布尔类型。
      • char、uchar
      • short、ushort
      • int、uint
      • long、ulong
      • charp(char*)
      • 切记:内核要求变量不允许是浮点数,CPU核处理浮点数的速度特别慢,所以不能使用浮点数。
    • 接收参数的内核全局变量接收参数之前必须用以下宏进行传参声明:
      • module_param(name, type, perm);
      • name:内核全局变量名。
      • type:内核全局变量的数据类型。
      • perm:内核全局变量的访问权限,一般用8进制来表示权限,不允许用可执行权限。
案例:编写内核程序,实现Linux内核程序的命令行传参。
  • 上位机执行:
mkdir /opt/drivers/day01/3.0
cd /opt/drivers/day01/3.0
vim helloworld.c
vim Makefile
make
cp helloworld.ko /opt/rootfs/home/drivers/
  • 下位机测试:
cd /home/drivers
echo 8 > /proc/sys/kernel/printk
insmod helloworld.ko
rmmod helloworld

安装内核程序时,传递参数
#include <linux/init.h>
#include <linux/module.h>

static int irq;
static char *pstring;

//传参声明
module_param(irq, int, 0664);
module_param(pstring, charp, 0);
//insmod时执行
static int helloworld_init(void)
{
    printk("%s: irq = %d, pstring = %s \n", __func__, irq, pstring):
    return 0;
}
//rmmod时执行
static void helloworld_exit(void)
{
    printk("%s: irq = %d, pstring = %s\n", __func__, irq, pstring);
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");

#编写Makefile
obj-m += helloworld.o
all:
    make -C /opt/kernel SUBDIRS=$(PWD) modules
    
clean:
    make -C /opt/kernel SUBDIRS=$(PWD) clean
    
#使用执行:
insmod helloworld.ko irq-250 pstring=china
//安装内核程序后,传递参数
#安装内核后,传递参数执行:
#查看当前参数值:
cat /sys/module/helloworld/parameters/irq
#修改参数值:
echo 2555 > /sys/module/helloworld/parameters/irq
总结:
  • 如果权限非零,将来在/sys/…/目录下会生成一个跟变量名同名的文件,将来通过修改文件的值来间接的修改变量的值,将来安装以后也能再次传递参数。
  • 如果权限为0,不会有同名的文件,此变量的传参只能在安装时传递参数,安装后不能再次传递参数。

Linux内核符合导出

  • 明确:何为符号(symbol)

    • 符合就是变量名或者函数名。
    • 符号导出目的:为了让别人的程序能够访问调用。
  • 回顾应用程序符号导出(多文件之间的访问,多文件,使用头文件声明和包含)

Linux内核符号导出(内核多文件之间的访问调用)
  • 明确:Linux内核程序多文件之间的访问和应用程序一模一样,都需要声明、定义和调用,但是内核要求导出的变量或者函数需要显示的导出一下。
  • 用以下宏导出即可:
    • EXPORT_SYMBOL(变量名或者函数名);
    • 或者——EXPORT_SYMBOL_GPL(变量名或者函数名);
    • 前者导出的函数或者变量,不管调用的内核程序是否遵循GPL协议,这个内核程序都可以调用。
    • 后者导出的函数或者变量只能给那些遵循GPL协议的内核程序访问。
    • 内核程序遵循GPL协议的代码:MODULE_LICENSE(“GPL”);
案例:helloworld调用test的变量
  • test.h
#ifndef __TEST_H
#define __TEST_H

//函数声明
extern void my_test(void);

#endif
  • test.c
#include <linux/init.h>
#include <linux/module.h>

//函数定义
void my_test(void)
{
    printk("%s\n", __FUNCTION__);
}

//显式将函数进行导出
EXPORT_SYMBOL(my_test);
MODULE_LICENSE("GPL");
  • helloworld.c
#include <linux/init.h>
#include <linux/module.h>
#include "test.h"

static int helloworld_init(void)
{
    //调用test
    my_test();
    printk("%s\n", __func__);
    return 0;
}

static void helloworld_exit(void)
{
    //调用test
    my_test();
    printk("%s\n", __func__);
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");
  • 编译调用:
vim Makefile
obj-m += helloworld.o
obj-m += test.o
all:
    make -C /opt/kernel SUBDIRS=$(PWD) modules
    
clean:
    make -C /opt/kernel SUBDIRS=$(PWD) clean
    
#复制到下位机上:
cp test.ko helloworld.ko /opt/rootfs/home/drivers

printk vs printf

  • 相同点:

    • 都是用于打印调试信息的。
    • 用法一模一样。
  • 不同点:

    • printf 只能用于用户空间
    • printk 只能用于内核空间
printk 特点
  • 能够指定打印输出级别,共8级。分别是:

    • KERN_EMERG <0> 系统奔溃
    • KERN_ALERT <1> 情况紧急必须立刻处理
    • KERN_CRIT <2> 严重情况
    • KERN_ERR <3> 错误情况
    • KERN_WARNING <4> 警告信息
    • KERN_NOTICE <5> 正常情况但是需要注意
    • KERN_INFO <6> 信息
    • KERN_DEBUG <7> 调试信息
  • 结论:数字越小,危险系数越高。此信息越应该打印输出。

    • 数字越小,打印级别越高。
  • 用法:

    • printk(KERN_ERR “this is a error msg!\n”);
    • printk("<3>" “this is a error msg!\n”);
实例:测试打印

test_printk.c

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

static int printk_all_init(void)
{
    printk("<0>", "level 0\n");
    printk("<1>", "level 1\n");
    printk("<2>", "level 2\n");
    printk("<3>", "level 3\n");
    printk("<4>", "level 4\n");
    printk("<5>", "level 5\n");
    printk("<6>", "level 6\n");
    printk("<7>", "level 7\n");
    
    return 0;
}

static void printk_all_exit(void)
{
    printk("<0>", "level 0\n");
    printk("<1>", "level 1\n");
    printk("<2>", "level 2\n");
    printk("<3>", "level 3\n");
    printk("<4>", "level 4\n");
    printk("<5>", "level 5\n");
    printk("<6>", "level 6\n");
    printk("<7>", "level 7\n");
    
}
module_init(printk_all_init);
module_exit(printk_all_exit);
MODULE_LICENSE("GPL");
printk 函数的默认打印输出级别的配置
  • printk 函数默认的打印阈值的配置
  • 如果printk函数指定的打印输出级别对应的数字小于默认的打印阈值的数据,此信息输出,否则屏蔽。
  • 问:内核默认的打印输出级别(阈值)如果配置?
  • 两种方法:
    • 通过修改配置文件/proc/sys/kernel/printk来修改默认的打印输出级别。(此方法解决不了Linux启动的时候打印信息的控制)
    • 在内核的启动参数中设置默认的打印输出级别(推荐使用此方法)

linux内核GPIO操作库函数

1.明确:

  • “GPIO操作”:ARM处理器引脚具有复用功能,使用前
    记得先配置为GPIO功能
    一旦配置为GPIO功能,即可输入或者输出操作
    GPIO操作又分:输入操作和输出操作
  • “输入操作”:此GPIO引脚的电平由外设来决定
  • “输出操作”: 此GPIO引脚的电平由CPU来决定
  • “库函数”:linux内核已经帮你实现,你只需调用即可
  • linux内核提供的库函数的实现定义在内核源码中

2.linux内核提供的GPIO操作的库函数如下:

int gpio_request(unsigned gpio, const char *label)

  • 函数功能:CPU的任何一个GPIO引脚硬件资源对于linux内核来说都是一种宝贵的资源,如果某个内核程序要向访问这个GPIO引脚硬件资源,首先必须向linux内核申请资源(类似malloc)
  • 参数说明:
    • gpio:GPIO引脚硬件在linux内核中的软件编号,也就是对于任何一个GPIO引脚,linux内核都给分配一个唯一的软件编号(类似GPIO引脚的身份证号)
      GPIO硬件 GPIO软件编号
      GPIOC12 PAD_GPIO_C+12
      GPIOB11 PAD_GPIO_B+11
      … …
  • label:给申请的硬件GPIO引脚指定的名称,随便取
  • 返回值:看内核大神的代码如何判断即可,照猫画虎
  • 涉及头文件:只需将大神的代码使用的头文件全盘拷贝过来即可注意""包含的头文件不做参考,找别的代码

void gpio_free(unsigned gpio)

  • 功能:内核程序如果不再使用访问GPIO硬件资源
    记得要将硬件资源归还给linux内核,类似free
  • 参数:
    • gpio:要释放的GPIO硬件资源对应的软件编号

int gpio_direction_output(unsigned gpio, int value)

  • 功能:配置GPIO引脚为输出功能,并且输出一个value值(1高电平/0低电平)
  • 参数:
    • gpio:GPIO硬件对应的软件编号
    • value:输出的值

int gpio_direction_input(unsigned gpio)

  • 功能:配置GPIO为输入功能

int gpio_set_value(unsigned gpio, int value)

  • 功能:设置GPIO引脚的输出值为value(1:高/0:低)
  • 前提是必须首先将GPIO配置为输出功能

int gpio_get_value(unsigned gpio)

  • 功能:获取GPIO引脚的电平状态,返回值就是引脚的
  • 电平状态(返回1:高电平;返回0:低电平)
  • 此引脚到底是输入还是输出没关系!
案例:利用linux内核GPIO库函数实现加载驱动开灯

卸载驱动关灯
回顾:标准C的结构体使用

  	//声明描述LED硬件信息的数据结构
  	struct led_resource {
  		int gpio;  //LED灯对应GPIO引脚的软件编号
  		char *name;//LED灯的名称
  		int state; //LED的状态
  	};
  	
  	//定义初始化四个LED灯的硬件信息对象  
  	struct led_resource led_info[] = {
  			{PAD_GPIO_C+12, "LED1", 0},
  			{PAD_GPIO_C+11, "LED2", 0},
  			{PAD_GPIO_C+10, "LED3", 0},
  			{PAD_GPIO_C+9, "LED4", 0}
  	}; 
  • 以上初始化的缺点是state字段无需初始化,但是按照传统的结构体初始化方式,state字段是必须要初始化的否则编译报错!
    如何解决以上问题呢?也就是只初始化gpio和name字段而不用初始化state呢?
  • 答:利用结构体的标记初始化方式即可
    //定义初始化四个LED灯的硬件信息对象  
  	struct led_resource led_info[] = {
  			{
  				.name = "LED1",
  				.gpio = PAD_GPIO_C+12
  			},
  			{
  			  .name = "LED2",
  				.gpio = PAD_GPIO_C+11
  			},
  			{
  				.name = "LED3",
  				.gpio = PAD_GPIO_C+10
  			},
  			{
  				.name = "LED4",
  				.gpio = PAD_GPIO_C+9
  			}
  	};  

结构体的标记初始化方式可以不用按照顺序,并且不用全部进行对成员初始化!

  	上位机实施步骤:
  	mkdir /opt/drivers/day02/1.0 -p
  	cd /opt/drivers/day02/1.0
  	vim led_drv.c
  	vim Makefile
  	make
  	cp led_drv.ko /opt/rootfs/home/drivers
  	
  	下位机测试:
  	cd /home/drivers
  	insmod led_drv.ko //开灯
  	rmmod led_drv //关灯

linux内核系统调用实现原理(了解)

面试题:谈谈对linux系统调用的理解
以write系统调用函数为例,掌握系统调用的实现过程:
例如:

  int main(void)
  {
  	write(1, "hello,world\n", 12);
  	printf("hello,world\n");
  	return 0;
  }
  • 1.当应用程序(进程)调用write系统调用函数, 首先会跑到
    C库的write函数的定义实现的地方

  • 2.C库的write函数将会做两件事

    • 1.首先保存write函数的系统调用号到R7寄存器
      “系统调用号”:linux内核给每一个系统调用函数分配唯一的软件编号(类似系统调用函数的身份证号)
      定义内核源码的arch/arm/include/asm/unistd.h
      例如:#define __NR_write (0+4)
    • 2.然后调用swi指令触发一个软中断异常
      新版本的ARM核(ARMV6开始)触发软中断的指令为svc
      老版本的ARM核触发软中断的指令是swi,现在的新版编译器
      同样支持swi指令!
  • 3.一旦触发软中断异常,CPU核立马要处理软中断异常
    硬件上自动将做:

    • 备份CPSR到SPSR_SVC
    • 设置CPSR
      • MODE=SVC_MODE:切换SVC管理模式
      • T=0:切换到ARM状态
      • IF=11:禁止FIQ/IRQ中断
      • 保存返回地址LR_SVC=PC-4
    • 设置PC=0x08,至此CPU跑到0x08软中断处理的入口地址
    • 至此开启了软件处理软中断异常的流程
    • 软中断的处理入口地址里相关的代码有linux内核来实现也就是说当前进程由用户空间"陷入"内核空间
  • 4.linux内核软中断处理的入口地址相关的代码将做
    以下工作:

    • 1.从R7寄存器取出之前保存的write函数的系统调用号4
    • 2.以write系统调用号4为下标在内核的系统调用表(数组)中找到对应的一个内核函数sys_write,一旦找到对应的内核函数,继续调用此函数,调用完毕,最后原路返回到用户空间,至此write函数返回!
    • “系统调用表”:本质就是一个数组,数组元素就是函数指针数组元素的下标就是系统调用号其定义在内核源码的arch/arm/kernel/calls.S
  • 5.切记:要边说边画图!


linux内核设备驱动

8.1.何为设备驱动?

答:设备驱动的两大核心内容

  • 1.必须有对硬件的操作访问
  • 2.必须有硬件操作接口,用户能够通过这些接口来访问硬件

8.2.linux内核设备驱动的分类

字符设备驱动:对字符设备的访问按照字节流形式访问
例如:LED,按键,蜂鸣器,GPS(UART),GPRS(UART),BT(UART)
触摸屏(XY绝对坐标),LCD显示屏(像素点)
声卡,摄像头,各种硬件传感器(三轴,重力,光线,距离,温度等)
EEPROM存储器(I2C接口)
块设备驱动:对块设备的访问按照数据块进行,比如一次操作512字节
例如:硬盘,U盘,TF卡,SD卡,Nandflash,Norflash,EMMC
网络设备驱动:对网络设备驱动一般按照网络协议进行
例如:有线网卡和无线网卡

8.3.明确:linux系统的理念

“一切皆文件”:任何硬件外设都是以文件的形式存放用户访问文件本质上就是在访问对应的硬件外设字符设备对应的文件称之为字符设备文件块设备对应的文件称之为块设备文件网络设备无设备文件,通过socket套接字进行访问

8.4.字符设备文件特点属性

明确:字符设备文件本身代表的就是字符设备硬件本身
明确:字符设备文件存在于根文件系统必要目录的dev目录下
当然块设备文件也存于dev目录下
举例子:查看下位机的UART设备的字符设备文件
ls /dev/ttySAC* -lh 得到以下信息:
crw-rw---- 204, 64 /dev/ttySAC0
crw-rw---- 204, 65 /dev/ttySAC1
crw-rw---- 204, 66 /dev/ttySAC2
crw-rw---- 204, 67 /dev/ttySAC3
说明:
c:表示此设备文件对应的设备为字符设备
204:表示串口的主设备号
64/65/66/67:分别表示第一个,第二个,第三个,第四个串口的次设备号
ttySAC0:表示第一个串口的设备文件名
ttySAC1:表示第二个串口的设备文件名
ttySAC2:表示第三个串口的设备文件名
ttySAC3:表示第四个串口的设备文件名
注意:一个硬件外设个体有唯一的一个设备文件名

8.5.字符设备文件创建方法有两种:

  • 1.手动创建,只需mknod命令
    语法:
    mknod /dev/字符设备文件名 c 主设备号 次设备号
    例如:
    mknod /dev/zhangsan c 250 0
  • 2.自动创建
    后续课程慢慢来

8.6.字符设备文件的访问

明确:访问字符设备文件本质就是在访问字符设备硬件
明确:字符设备文件的访问必须利用系统调用函数
例如:
打开UART0:注意:使用绝对路径!
	int fd = open("/dev/ttySAC0", O_RDWR)	
	if (fd < 0)
		return -1;	

从UART0读取数据:
char buf[1024] = {0};
read(fd, buf, 1024);

向UART0写入数据:
write(fd, "hello,world\n", 12);

关闭UART0:	     
close(fd); 	

8.7.主设备号,次设备号,设备号

主设备号作用:应用程序根据字符设备文件的主设备号
在茫茫的内核驱动中找到对应的唯一的
设备驱动,一个设备驱动仅有唯一的主设备号
次设备号作用:设备驱动根据次设备号能够找到应用程序
要访问的硬件外设个体
总结:一个驱动仅有一个主设备号,一个硬件个体仅有一个次设备号
应用根据主设备号找驱动,驱动根据次设备号找硬件个体
所以:主,次设备号对于linux内核是一个宝贵的资源,某个
设备驱动必须要想linux内核申请主,次设备号
问:如何申请呢?
设备号:设备号包含主,次设备号
linux内核用dev_t(unsigned int)数据类型描述设备号信息
设备号的高12位用来描述主设备号
设备号的低20位用来描述次设备号
设备号和主,次设备号之间的转换操作宏:
设备号=MKDEV(已知的主设备号,已知的次设备号);
主设备号=MAJOR(已知的设备号);
次设备号=MINOR(已知的设备号);
作业:认真研读掌握MINOR宏的源码实现过程!
问:既然设备号对于linux内核是一种宝贵的资源,设备驱动
如何向内核申请资源呢?
答:利用以下函数即可申请
int alloc_chrdev_region(dev_t *dev,
unsigned baseminor,
unsigned count,
const char *name
);
函数功能:向内核申请设备号
参数:
dev:保存申请的设备号
包括主设备号和起始的次设备号
baseminor:希望申请的起始次设备号,一般给0
count:申请的次设备号的个数
如果baseminor=0,count=2,那么申请的次设备号分别是0和1
name:申请设备号指定的名称,随便取
将来通过执行cat /proc/devices查看

设备驱动一旦不再使用设备号,记得要将设备号资源归还给linux内核:
void unregister_chrdev_region(dev_t dev,
unsigned count);
功能:释放申请的设备号资源
dev:申请的设备号
count:申请的次设备号的个数

8.8.自行设计数据结构来描述字符设备驱动属性

//描述字符设备驱动
      struct char_device {
      		dev_t dev; //描述申请的设备号
      		int count; //描述申请的次设备号的个数
      		int (*open)(...); //给用户提供打开设备的接口
      		int (*close)(...); //给用户提供关闭设备的接口
      		int (*read)(...); //给用户提供读设备接口
      		int (*write)(...); //给用户提供写设备接口
      };

浮想联翩,应用程序和驱动接口调用关系:
应用程序open->软中断->内核的sys_open->驱动的open接口
应用程序close->软中断->内核的sys_close->驱动的close接口
应用程序read->软中断->内核的sys_read->驱动的read接口
应用程序write->软中断->内核的sys_write->驱动的write接口

  • 优化:
			//描述字符设备驱动给用户提供的操作接口
			struct file_operations {
				  int (*open)(...); //给用户提供打开设备的接口
      		int (*close)(...); //给用户提供关闭设备的接口
      		int (*read)(...); //给用户提供读设备接口
      		int (*write)(...); //给用户提供写设备接口
			};
			
			//描述字符设备驱动
			struct char_device {
      		dev_t dev; //描述申请的设备号
      		int count; //描述申请的次设备号的个数
      		struct file_operations *ops;//给用户提供的操作接口
      };

8.9.Linux内核描述字符设备驱动的数据结构

Linux内核描述给用户提供操作接口的数据结构

       			//描述字符设备驱动给用户提供的操作接口
			struct file_operations {
				  int (*open) (struct inode *, struct file *); //给用户提供打开设备的接口
      		int (*release) (struct inode *, struct file *); //给用户提供关闭设备的接口
					...
			};
			字符设备驱动和应用程序调用关系:
			应用程序open->软中断->内核的sys_open->驱动的open接口
      应用程序close->软中断->内核的sys_close->驱动的release接口
			
			//描述字符设备驱动
			struct cdev {
      		dev_t dev; //描述申请的设备号
      		int count; //描述申请的次设备号的个数
      		struct file_operations *ops;//给用户提供的操作接口
      		...
      };

配套函数:
cdev_init(strcut cdev *cdev,
struct file_operations *fops)
函数功能:初始化字符设备驱动对象,就是给字符设备
驱动对象添加一个硬件操作接口
cdev:要初始化的字符设备对象
fops:给用户提供的硬件操作接口
cdev_add(struct cdev *cdev, dev_t dev, int count)
函数功能:向内核注册添加一个字符设备对象,一旦添加完毕
内核就有一个真实的字符设备驱动
cdev:要注册的字符设备对象
dev:申请的设备号
count:申请的次设备号的个数
cdev_del(struct cdev *cdev)
函数功能:从内核中卸载字符设备对象,一旦卸载完毕
内核就不会有一个真实的字符设备驱动

8.10.编写字符设备驱动步骤

  • 1.定义初始化硬件操作接口对象
    struct file_operations led_fops = {
    .open = led_open, //打开设备接口
    .release = led_close, //关闭设备接口
    };

  • 2.定义初始化字符设备对象
    struct cdev led_cdev; //定义字符设备对象
    //led_cdev.ops = &led_fops
    cdev_init(&led_cdev, &led_fops);//给字符设备对象添加硬件操作接口

  • 3.最终向内核注册字符设备对象
    cdev_add(&led_dev, 申请的设备号,次设备号的个数);
    一旦注册成功,内核就有一个真实的字符设备驱动,并且
    给用户提供硬件操作接口(open/release)

  • 4.从内核卸载字符设备对象
    cdev_del(&led_cdev);

  • 5.最后编写led_open/led_close接口
    具体内容根据用户需求来定

案例:编写LED字符设备驱动,实现打开设备开灯,关闭设备关灯

  • 注意:将四个LED灯作为一个硬件设备个体
  • 实施步骤:
    上位机执行:
			mkdir /opt/drivers/day02/2.0
			cd /opt/drivers/day02/2.0
			vim led_drv.c //驱动
			vim led_test.c //应用
			vim Makefile
			make
			cp led_drv.ko /opt/rootfs/home/drivers
			arm-cortex_a9-linux-gnueabi-gcc -o led_test led_test.c
			cp led_test /opt/rootfs/home/drivers
			
		下位机测试:
		cd /home/drivers
		insmod led_drv.ko
		cat /proc/devices //查看申请的主设备号
			Character devices:
  		主设备号   设备名称
  		1 					mem
  		5 				/dev/tty
			...
			250 			tarena
	  mknod /dev/myled c 申请的主设备号 0 //创建设备文件myled
	  ./led_test //运行

编写一个字符设备驱动的完整步骤

  • 1.先写头文件
  • 2.搭建框架
    • 入口函数
    • 出口函数
    • 各种修饰
    • GPL
    • 入口和出口函数里面先不要写代码
  • 3.该声明的声明,该定义的定义,该初始化的初始化
    • 先搞定硬件然后软件
  • 4.填充入口和出口函数
    • 先写注释
    • 后写代码
  • 5.最后根据用户需求编写各个硬件操作接口函数
    • 可以放到前面写
    • 可以放到后面写,但要前面做声明
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值