11 Linux 设备驱动

11 Linux 设备驱动

  • 1、Linux软件特点
    • 1.1 用户空间
    • 1.2 内核空间
  • 2、Linux程序
    • 2.1 应用程序
    • 2.2 内核程序
      • 2.2.1 编程
      • 2.2.2 编译
    • 2.3 内核命令行传参
      • 2.3.1 应用程序的命令行传参
      • 2.3.2 内核程序命令行传参
    • 2.4 内核程序符号导出
      • 2.4.1 应用程序多文件之间的访问调用
      • 2.4.1 内核多文件之间的访问调用
    • 2.5 内核打印函数printk
      • 2.5.1 printk和printf对比
      • 2.5.2 printk函数特点
      • 2.5.3 linux内核默认打印输出级别
  • 3、linux内核提供的GPIO操作库函数
  • 4、系统调用函数执行流程:
  • 5、linux内核字符设备驱动
    • 5.1 设备驱动定义和特点
    • 5.2 linux内核设备驱动分类
    • 5.3 字符设备文件属性特点:
    • 5.4 设备号,主设备号,次设备号
    • 5.5 内核字符设备驱动涉及的两大结构体和相关配套函数
    • 5.6 驱动案例
    • 5.7 字符设备驱动硬件操作接口
      • 5.7.1 write接口
      • 5.7.2 read接口
      • 5.7.3 ioctl接口
    • 5.8 文件自动创建和自动删除
    • 5.9 struct inode和struct file
    • 5.10 综合案例
  • 6、Linux内核混杂设备驱动
    • 6.1 混杂设备定义

1、Linux软件特点

linux系统(软件范畴)的用户空间(用户态)和内核空间(内核态)特点

1.1 用户空间

用户空间特点:

  • 用户空间包含的软件就是各种用户命令,应用程序所需的动态库,用户自己编写的应用程序(UC,QT)等
  • 用户空间的软件在运行的时候对应的CPU核工作模式为User用户模式
  • 用户空间的软件不允许直接访问内核空间的代码,地址和数据,如果用户空间的软件访问内核空间,只能通过系统调用
  • linux系统4G虚拟地址空间划分:
    用户空间占前3G,地址范围:0x00000000~0xBFFFFFFF,用户虚拟地址
    内核空间占后1G,地址范围:0xC0000000~0xFFFFFFFF,内核虚拟地址
    所以应用程序要想访问内核空间的地址,必须通过系统调用,来间接访问
  • 用户空间的软件如果进行了非法的内存访问,不会造成操作系统奔溃,反而是操作系统直接干掉应用程序(例如:segment fault)
    例如:*(int *)0 = 0;
  • 用户空间的软件不允许直接访问外设的物理地址
    如果要访问,必须提前将外设的物理地址映射到用户虚拟地址,或者内核虚拟地址,一旦完成映射,访问用户虚拟地址或者内核虚拟地址就是在访问外设的物理地址
  • 用户空间的软件类似网络编程中客户端,内核空间的软件将来要时刻服务于用户空间软件

1.2 内核空间

内核空间特点:

  • 内核空间的软件就是uImage(包含了七大子系统)
  • 内核空间的软件在运行的时候对应的CPU核工作模式为SVC管理模式
  • 内核空间的软件如果对内存进行非法的访问,操作系统直接崩溃(类似windows蓝屏),内核崩溃又称吐核
    例如:*(int *)0 = 0;
  • 内核空间的软件同样不允许直接访问外设的物理地址
    如果要访问,必须提前将外设的物理地址映射到用户虚拟地址或者内核虚拟地址,一旦完成映射,访问用户虚拟地址或者内核虚拟地址就是在访问外设的物理地址
  • 内核空间软件类似网络编程的服务器
    切记:内核程序一定要给应用程序服务
  • 内核空间和用户空间如果要进行数据传输,通信只能通过系统调用

2、Linux程序

2.1 应用程序

#include <stdio.h> //位于标准C库中
//main:入口函数
int main(int argc/*参数个数*/, char *argv[]/*参数信息*/, char *envp[]){printf("hello,world\n"); //标准C库函数exit(0); //出口函数
}

2.2 内核程序

2.2.1 编程

vim helloworld.c 添加如下内容:
#include <linux/init.h>
#include <linux/module.h>
static int helloworld_init(void){printk("%s\n", __func__);return 0;
}
static void helloworld_exit(void){printk("%s\n", __func__);
}
module_init(helloworld_init);
module_exit(helloworld_exit);   
MODULE_LICENSE("GPL");说明:- 内核程序使用的头文件一律不是标准C库的,内核程序使用的头文件位于内核源码中(例如:/home/kernel)- 用module_init宏修饰的函数为内核程序的入口函数,类似main函数,例如:helloworld_init当内核程序和uImage分开编译,当执行insmod安装内核程序到uImage命令时,内核uImage会自动执行此入口函数如果内核程序和uImage编译在一起,当内核uImage启动的时候内核uImage会自动执行此函数要求入口函数的返回值为int类型,形参为void,入口函数执行成功返回0,执行失败返回负值注意:执行成功表示内核程序安装成功,其生命周期才刚刚开始永驻内存,静静为应用程序服务执行失败表示内核程序安装失败,内存也不会存在这个内核程序- 用module_exit修饰的函数为内核程序的出口函数,例如:helloworld_exit要求出口函数的返回值和形参都是void当系统重启或者执行rmmod卸载命令时,内核uImage自动执行,一旦卸载完毕,内存就不会存在这个内核程序,也不会再服务于应用程序- 内核打印函数用printk,此函数代码定义不是位于标准C库,而是位于内核源码中- MODULE_LICENSE("GPL")表示告诉内核uImage,此内核程序同样遵循GPL开源软件协议,如果不遵循,内核源码中有些函数无权调用!切记:任何内核程序(.c文件)必须添加这句话

2.2.2 编译

使用Makefile进行编译
vim Makefile 添加如下内容:

obj-m += helloworld.o #表示将helloworld.c单独编译生成helloworld.ko  m=module=模块
all:make -C /home/kernel SUBDIRS=$(PWD) modules
clean:make -C /home/kernel SUBDIRS=$(PWD) modules说明
# 当执行make或者make all时,执行对应的编译命令:make -C /home/kernel SUBDIRS=$(PWD) modules
# -C /home/kernel:到/home/kernel目录下,类似:cd /home/kernel
# make -C /home/kernel:到/home/kernel目录下make
# make -C /home/kernel modules:到/home/kernel目录下执行make modules
# make modules:将内核源码中(/home/kernel)的所有.c编译生成对应的.ko
# PWD:当前所在的路径:/home/drivers
# SUBDIRS=$(PWD):告诉linux内核,在内核源码之外还有一个 /home/drivers目录,请把这个目录下的helloworld.c单独编译

接下来单独编译helloworld.c内核程序

1 执行Makefile
cd /home/drivers
make
2 创建内核程序二进制文件存放目录
mkdir /home/rootfs/home/drivers/
3 将内核程序复制
cp helloworld.ko /home/rootfs/home/drivers/
4 清除生成的目标文件
make clean 
5 将根文件系统烧写到开发板中
6 下位机系统启动完毕执行:
cd /home/drivers/
echo 8 > /proc/sys/kernel/printk //将默认打印级别调低
insmod helloworld.ko //安装内核程序到uImage,此时内核uImage执行入口函数helloworld_init
lsmod //查看内核程序是否安装完毕
rmmod helloworld //从内核uImage中卸载内核程序,此时内核执行出口函数helloworld_exit

在这里插入图片描述

2.3 内核命令行传参

2.3.1 应用程序的命令行传参

#include <stdio.h> 
int main(int argc, char *argv[])
{int a;int b;if(argc != 3) {printf("用法:%s <a> <b>\n", argv[0]);return -1;}a = atoi(argv[1]);b = atoi(argv[2]);printf("a = %d, b = %d\n", a, b);exit(0); 
}

编译:gcc -o helloworld helloworld.c

# 运行
gcc -o helloworld helloworld.c
./helloworld 
argc = 1
argv[0] = "./helloworld"./helloworld 100 200  
argc=3
argv[0] = "./helloworld"
argv[1] = "100"
argv[2] = "200"

2.3.2 内核程序命令行传参

只需遵循以下三个原则即可:
1:内核程序接收参数的变量只能是全局变量
2:内核程序接收参数的全局变量的数据类型必须是以下类型:

bool,invbool
char,uchar
short,ushort
int,uint
long,ulong  //ulong=unsigned long
charp(char *)

注意:内核程序不建议处理浮点数(float,double) 如果内核程序要处理浮点数,两个方法:
1.浮点数变整数(缺失精度) 3.2*2.3=>32*23/100
2.浮点数运算可以让应用程序完成
3:内核程序接收参数的全局变量必须用以下宏进行传参声明:
module_param(name, type, perm);
说明:
1.name:全局变量名
2.type: 全局变量的数据类型
3.perm:全局变量的访问权限,一般用8进制数表示 例如:0664
注意:不允许有可执行权限(r/w/x)
注意:
1:如果权限非0,将来在/sys/module/内核程序名/parameters目录下会创建一个同名的文件,文件里面的内容就是变量的值,将来内核程序安装之后可以通过修改文件的内容来间接修改变量的值(非常灵活)
2:如果权限为0,不会出现同名的文件,此变量只能在安装时传递参数,如果没有安装之后传递参数的需求,权限一律给0,目的节省内存资源,这是因为/sys目录下的内容存储在于内存中!

#include <linux/init.h>
#include <linux/module.h>
// 定义全局变量
static int  frq;
static char* pstring;
// 传参声明
module_param(frq,int,0664);
module_param(pstring,charp,0);static int helloworld_init(void){printk("%s frq = %d pstring  = %s \n",__func__,frq,pstring);return 0;
}static void helloworld_exit(void){printk("%s frq = %d pstring  = %s \n",__func__,frq,pstring);
}module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");

编写Makefile进行编译
在下位机进行验证
在这里插入图片描述

如果直接修改frq文件,就间接修改了程序中frq的值。

2.4 内核程序符号导出

符号(symbol):就是程序中的变量名和函数名
符号导出目的:就是让别的内核程序能够访问到变量和函数也就是多文件之间的混合调用

2.4.1 应用程序多文件之间的访问调用

上位机执行:
vim test.h 添加如下内容:

#ifndef __TEST_H
#define __TEST_H
extern void my_test(void);
#endif

vim test.c 添加如下内容:

#include <stdio.h>
void my_test(void)
{printf("%s\n", __func__);
}

vim main.c 添加如下内容

#include <stdio.h>
#include "test.h"
void main(void)
{my_test();return 0;
}

编译

arm-cortex_a9-linux-gnueabi-gcc -shared -fpic -o libtest.so test.c
arm-cortex_a9-linux-gnueabi-gcc -o main main.c -L. -ltest

下位机测试

2.4.1 内核多文件之间的访问调用

步骤和应用程序的一样,只不过多了一个将函数或者变量导出方法:
EXPORT_SYMBOL(函数名或者变量名)或者EXPORT_SYMBOL_GPL(函数名或者变量名);
说明:
前者导出的函数或者变量,不管这个内核程序是否遵循GPL协议,都可以访问
后者导出的函数或者变量,只能给那些遵循GPL协议的内核程序访问调用
编写test.c和test.h

vim test.h#ifndef __TEST_
#define __TEST_#include <linux/init.h>
#include <linux/module.h>
extern void my_test(void);#endifvim tset.c
#include "test.h"
void my_test(void){printk("%s\n",__func__);
}// 显示导出函数
EXPORT_SYMBOL_GPL(my_test);
// EXPORT_SYMBOL(my_test);
MODULE_LICENSE("GPL");

编写main.c和Makefile

// main.c
#include "test.h"
static int  main_init(void){printk("%s\n",__func__);my_test();return 0;
}static void main_exit(void){printk("%s\n",__func__);my_test();
}module_init(main_init);
module_exit(main_exit);MODULE_LICENSE("GPL");// Makefile
obj-m += main.o
obj-m += test.o
all:make -C /home/arm/kernel SUBDIRS=$(PWD) modules
clean:make -C /home/arm/kernel SUBDIRS=$(PWD) modules

上位机验证
在这里插入图片描述

2.5 内核打印函数printk

2.5.1 printk和printf对比

  • 相同点:都是用于打印,用法一致
  • 不同点:前者用于内核空间,后者用于用户空间

2.5.2 printk函数特点

精髓在于printk能够指定打印输出级别,级别共八级:

#define KERN_EMERG		"<0>"	/*系统奔溃*/
#define KERN_ALERT		"<1>"	/*事件必须立马处理*/
#define KERN_CRIT		"<2>"	/*严重情形*/
#define KERN_ERR		"<3>"	/*错误情形*/
#define KERN_WARNING	"<4>"	/*警告*/
#define KERN_NOTICE		"<5>"	/*正常但是还需要引起注意*/
#define KERN_INFO		"<6>"	/*普通信息*/
#define KERN_DEBUG		"<7>"	/*调试信息*/	

结论:数字越小,打印级别越高,表示当时的情形非常紧急,非常危险
用法:格式:printk(级别 “打印输出的内容”);

printk(KERN_ERR "this is a error msg.\n");
// 或者
printk("<3>" "this is a error msg.\n");

2.5.3 linux内核默认打印输出级别

功能:类似开关,决定着printk打印输出是否生效
例如:假设linux内核当前默认的打印输出级别是4

printk("<2>" "...."); //输出
printk("<4>" "...."); //不输出
printk("<5>" "...."); //不输出

结论:printk指定的打印输出级别对应的数字要大于等于默认打印级别数字信息一律屏蔽不输出,否则输出

问:linux内核默认打印输出级别如何设置呢?
答:设置方法两种:

  • 方法1:通过修改配置文件:/proc/sys/kernel/printk
    编写内核程序,编译,并下载到下位机
#include <linux/init.h>
#include <linux/module.h>
static int printk_init(void){printk("<0>""this is printk 0\n");printk("<1>""this is printk 1\n");printk("<2>""this is printk 2\n");printk("<3>""this is printk 3\n");printk("<4>""this is printk 4\n");printk("<5>""this is printk 5\n");printk("<6>""this is printk 6\n");printk("<7>""this is printk 7\n");return 0;
}
static void print_exit(void){printk(KERN_EMERG"this is printk 0\n");printk(KERN_ALERT"this is printk 1\n");printk(KERN_CRIT"this is printk 2\n");printk(KERN_ERR"this is printk 3\n");printk(KERN_WARNING"this is printk 4\n");printk(KERN_NOTICE"this is printk 5\n");printk(KERN_INFO"this is printk 6\n");printk(KERN_DEBUG"this is printk 7\n");
}
module_init(printk_init);
module_exit(print_exit);
MODULE_LICENSE("GPL"); 

下位机测试:

insmod my_printk.ko // 默认的打印级别
echo 8 > /proc/sys/kernel/printk  //将默认打印级别修改为8
rmmod my_printk

在这里插入图片描述

注意:此方法有缺陷,随着系统重启,之前的配置将会无效,因为/proc目录下内容创建于内存,且此方法无法决定内核启动时的打印信息,如果懒得每次系统启动之后手动echo,干脆将echo这条命令放到启动脚本rcS或者profile中即可,系统启动自动设置默认的打印输出级别

  • 方法2:通过设置内核启动参数来指定默认打印输出级别
    设置内核启动参数
    重启下位机,进入uboot命令行执行:
setenv bootargs root/dev/nfs nfsroot=192.168.1.9:/home/rootfs ip=192.168.1.165:192.168.1.9:192.168.1.1:255.255.255.0 init=/linuxrc console=ttySAC0,115200 maxcpus=1 debug

boot //启动系统
在这里插入图片描述

结论:内核启动参数如果用debug,将来默认打印级别为10, 内核启动参数如果用loglevel=数字,将来默认打印级别为对应的数字
产品研发阶段用debug,系统启动速度慢
产品发布用quiet,系统速度快

3、linux内核提供的GPIO操作库函数

相关函数,内核相关的函数没有手册,所以函数的返回值和头文件根据其他人写的代码全盘拷贝即可
gpio_request

int gpio_request(unsigned gpio, const char *label)
- 功能:在linux内核中,处理器的任何GPIO硬件对于linux内核来说,都是一种宝贵的资源,内核程序要想访问操作某个GPIO硬件,必须利用此函数先向内核申请这个GPIO资源,类似:malloc
- 参数- gpio:在linux内核中,内核给每个GPIO硬件都指定分配唯一的一个软件编号,简称GPIO编号,类似GPIO硬件的身份证号- GPIO硬件对应的软件编号定义:GPIO硬件 			GPIO编号GPIOC12				PAD_GPIO_C + 12GPIOB26				PAD_GPIO_B + 26- label:随意指定一个标签名称即可,表示申请的GPIO硬件的标识

gpio_free

void gpio_free(unsigned gpio)
- 功能:如果内核程序不再使用某个GPIO硬件,记得要释放资源

gpio_direction_output

gpio_direction_output(unsigned gpio, int value);
- 功能:配置GPIO为输出功能同时输出value(1/0)

gpio_direction_input

gpio_direction_input(unsigned gpio);
- 功能:配置GPIO为输入功能

gpio_set_value

gpio_set_value(unsigned gpio, int value);
- 功能:设置GPIO的输出值为value(1/0) 此函数使用的前提是提前配置为输出功能

gpio_get_value

int gpio_get_value(unsigned gpio);
- 功能:获取GPIO的电平状态,返回值保存状态,此函数对输入还是输出无要求
  • 案例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h> // gpio_* 声明
#include <mach/platform.h> // PAD_GPIO_C 声明
struct led_gpio{char * name; // 灯的名称int gpio;// 灯的编号
};
static struct led_gpio led_info[]={{.name="led0",.gpio=PAD_GPIO_C+12},{.name="led1",.gpio=PAD_GPIO_C+7},{.name="led2",.gpio=PAD_GPIO_C+11},{.name="led3",.gpio=PAD_GPIO_B+26}
};
static int led_init(void){int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_request(led_info[i].gpio,led_info[i].name);gpio_direction_output(led_info[i].gpio,0);printk("%s:第%s个灯打开\n",__func__,led_info[i].name);}return 0;
}
static void led_exit(void){int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_request(led_info[i].gpio,led_info[i].name);gpio_direction_output(led_info[i].gpio,1);printk("%s:第%s个灯关闭\n",__func__,led_info[i].name);}
}
module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");

4、系统调用函数执行流程:

以write函数为例,其调用过程如下:
1:当应用程序调用write,首先调用到标准C库中write函数定义
2:标准C库中write函数作两件事:
2.1:保存write函数的系统调用号到R7寄存器
“系统调用号”:linux内核给每个系统调用函数都分配一个唯一的软件编号,类似系统调用函数的身份证号
定义在内核源码/arch/arm/include/asm/unistd.h 例如: #define __NR_write 4
2.2: 调用svc/swi指令,触发软中断异常
2.3: 一旦触发软中断异常,CPU核立马要处理异常
CPU核硬件上做四件事:
1. 备份cpsr:spsr_svc=cpsr
2. 设置cpsr:bit[7:0]=11010011
3. 保存返回地址lr_svc=pc-4(此时CPU核还在当前进程地址空间运行)
4. 设置pc=0xFFFF0008(内核空间异常向量表软中断处理入口地址)
至此用户进程由用户空间“陷入”内核空间继续运行,CPU核跑到0xFFFF0008地址运行
软件处理软中断异常流程:
1. 提前建议异常向量表
内核把异常向量表建立在0xFFFF0000起始地址(软件可以改),linux系统中,异常向量表位于内核空间,异常处理也是位于内核空间
2. 到了0xFFFF0008地址以后,先做保护现场
3. 然后调用软中断异常处理函数,此函数做三件事:
3.1. 首先从R7寄存器中取出write系统调用号4
3.2. 然后以系统调用号4为下标在内核的系统调用表(数组)中找到一个对应的内核函数sys_write
“系统调用表”:本质就是一个大数组,下标就是系统调用号,数组内容就是一个内核函数,内核函数都是以sys_开头,定义在内核源码/arch/arm/kernel/calls.S
3.3. 一旦找到此函数sys_write,进程然后调用此内核函数
4. 内核函数sys_write调用完毕,进程然后恢复现场
状态恢复和跳转返回,进程又回到用户空间继续运行,至此应用write函数调用完毕
在这里插入图片描述

5、linux内核字符设备驱动

5.1 设备驱动定义和特点

定义:能够使硬件工作的软件
驱动两大核心:
1. 必须操作硬件
2. 必须给用户应用提供操作接口,根据操作接口访问硬件(服务),驱动最终势必要服务于应用程序

5.2 linux内核设备驱动分类

1: 字符设备驱动:操作的硬件访问按照字节流形式访问
例如:LED,蜂鸣器,按键,LCD显示屏(RGB),触摸屏(XY绝对坐标),鼠标(XY相对坐标),UART接口外设(BT,GPS,GPRS等),各种传感器等
2.块设备驱动:操作的硬件访问按照数据块访问,例如:一次访问512字节
例如:硬盘,U盘,TF卡,SD卡,EMMC,Norflash,Nandflash,注意这类驱动:内核基本完美支持
3.网络设备驱动:网卡驱动,有线网卡和无线网卡 注意这类驱动:要不由内核完美支持要不芯片厂家写好

5.3 字符设备文件属性特点:

1.字符设备文件存在于根文件系统rootfs必要目录dev目录下
2.包含关键属性:
在这里插入图片描述

说明:
c:表示此文件对应的硬件为字符设备
204:表示字符设备文件的主设备号
64~67:表示字符设备文件的次设备号
ttySAC0:表示第一个UART串口硬件对应的字符设备文件名 
ttySAC1:表示第二个UART串口硬件对应的字符设备文件名
ttySAC2:表示第三个UART串口硬件对应的字符设备文件名
ttySAC3:表示第四个UART串口硬件对应的字符设备文件名

3.访问字符设备文件就是在访问字符设备硬件
访问字符设备文件的方式是通过系统调用函数
例如:
1.打开第一个串口
int fd = open(“/dev/ttySAC0”, O_RDWR);
注意:一旦打开成功,将来字符设备文件后续无需使用,只需用其代理:fd,所以:fd就是代表字符设备文件,就是代表字符设备硬件
2.向第一个UART串口发送字符串
write(fd, “hello,world\n”, strlen(“hello,world\n”));
3.从第一个UART串口读取数据
char buf[1024] = {0};
read(fd, buf, 1024);
4.关闭串口
close(fd);
4.字符设备文件的创建方式:两种
1.手动创建:只需mknod命令
格式:mknod /dev/字符设备文件名 c 主设备号 次设备号
例如:mknod /dev/myled c 250 0
2.自动创建

5.4 设备号,主设备号,次设备号

  • 设备号:同时包含了主设备号和次设备号
    设备号数据类型:dev_t(本质unsigned int,32位,4字节)
    设备号的高12位保存着主设备号的值
    设备号的低20位保存着次设备号的值
    • 相关的内核操作宏:
      设备号=MKDEV(已知主设备号,已知次设备号)
      主设备号=MAJOR(已知设备号)
      次设备号=MINOR(已知设备号)
      主设备号:应用程序根据设备文件的主设备号在内核中找到唯一对应的驱动程序,所以一个驱动程序仅有唯一的主设备号
      次设备号:应用根据主设备号找到驱动,驱动然后根据次设备号,找到对应的唯一的硬件外设,所以一个硬件设备仅有唯一的次设备号
      结论:设备号对于linux内核是一种宝贵的资源,如果某个驱动要关联某个设备号信息,必须向内核申请
      申请和释放的方法如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 
- 功能:向内核申请设备号
- 参数:- dev:保存内核给你分配的设备号注意:主设备号就一个,其中的次设备号是起始的次设备号- baseminor:希望起始的次设备号,一般给0- count:次设备号的个数例如:count=4并且baseminor=0,那么次设备号:0,1,2,3- name:设备名称(不是设备文件名,两码事)用于调试:执行cat /proc/devices能够看到申请的设备号信息
void unregister_chrdev_region(dev_t from, unsigned count)	
- 功能:释放设备号资源
- 参数:- from:传递之前申请的设备号- count:次设备号的个数
  • 案例 加载驱动,申请设备号,卸载驱动,释放设备号
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h> 
#include <linux/cdev.h>// 定义设备号对象
static dev_t dev;static int led_init(void){// 申请设备号alloc_chrdev_region(&dev,0,3,"zpyl");printk("%s: MAJOR = %d, MINOR = %d\n", __func__, MAJOR(dev), MINOR(dev));return 0;
}static void led_exit(void){// 移除设备号unregister_chrdev_region(dev,3);
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");

下位机测试:

cd /home/drivers/
insmod led_drv.ko 
cat /proc/devices //查看申请到的主设备号

5.5 内核字符设备驱动涉及的两大结构体和相关配套函数

1:描述硬件操作接口的结构体:

struct file_operations {int (*open) (struct inode *, struct file *); //打开设备接口int (*release) (struct inode *, struct file *); //关闭设备接口...
};

2:接口和应用调用关系:
应用open->C库open->软中断->内核的sys_open->驱动的open接口
应用close->C库close->软中断->内核的sys_close->驱动的release接口

3:描述字符设备驱动属性的结构体:

struct cdev  {dev_t  dev; //保存字符设备驱动申请的设备号信息int count; //保存字符设备驱动申请的次设备号个数struct file_operations *ops; //通过ops指针将来可以给此结构体关联一个file_opertions结构体...
};

4:配套的操作函数

初始化函数:cdev_init(struct cdev *dev, const struct file_operations *fops)
注册函数:cdev_add(struct cdev *dev, dev_t dev, unsigned count);
卸载函数:cdev_del(struct cdev *dev);	

5.6 驱动案例

.linux内核字符设备驱动涉及的两大结构体和相关配套函数
编写LED字符设备驱动,要求:打开open设备:开灯,关闭close设备:关灯

  • 应用程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){// 打开设备 // 应用程序open 》C库open 》 软中断 》内核sys_open 》驱动led_open 》开灯int fd = open("/dev/zpyl",O_RDWR);if(fd<0){printf("zpyl dervice is failed\n");return -1;}sleep(3);// 关闭设备// 应用程序close 》C库close 》软中断 》内核sys_close 》驱动led_close 》关灯close(fd);return 0;
}
  • 驱动程序
    书写驱动程序时遵循
    1.搭框架
    编写头文件,入口函数和出口函数(先不要写内容)
    2.各种该
    该声明的声明
    该定义的定义
    该初始化的初始化
    先搞硬件再搞软件
    3.各种填
    填充入口函数和出口函数
    先写注释后塞代码
    4.写接口
    根据用户的需求最后完成各个接口函数的功能
#include <linux/init.h>
#include <linux/module.h>#include <linux/fs.h> // struct file_operations
#include <linux/cdev.h> // struct cdev
#include <linux/gpio.h>
#include <mach/platform.h>// 声明描述led硬件信息结构体
struct led_gpio{int gpio;// 灯的编号char *name;// 灯的名称
};// 定义初始化led硬件信息对象
static struct led_gpio led_info[] = {{.name = "led0",.gpio = PAD_GPIO_C+12},{.name = "led1",.gpio = PAD_GPIO_C+7}
};
// 定义设备号对象
static dev_t dev;
// 定义字符设备对象
static struct cdev led_cdev;// 操作函数
static int led_open(struct inode *inode, struct file *file){int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_set_value(led_info[i].gpio,0);}return 0;
}static int led_close(struct inode *inode,struct file *file){int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_set_value(led_info[i].gpio,1);}return 0;
}// 定义初始化操作接口对象
static struct file_operations led_fops={.open = led_open,// 打开设备.release = led_close // 关闭设备
};
// 入口函数
static int led_init(void){// 申请GPIO资源int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){// 申请GPIOgpio_request(led_info[i].gpio,led_info[i].name);// 配置为输出gpio_direction_output(led_info[i].gpio,1);printk("%s led open\n",led_info[i].name);} // 申请设备号alloc_chrdev_region(&dev,0,1,"zpyl");printk("%d %d\n",MAJOR(dev),MINOR(dev));// 初始化字符设备对象给其关联操作接口cdev_init(&led_cdev,&led_fops);// 向内核注册字符设备对象,等待应用访问cdev_add(&led_cdev,dev,1);return 0;
}
// 出口函数
static void led_exit(void){int i=0;// 卸载字符设备对象cdev_del(&led_cdev);// 释放设备号unregister_chrdev_region(dev,1);// 输出为1,释放GPIO资源for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_direction_output(led_info[i].gpio,1);gpio_free(led_info[i].gpio);printk("%s led free\n",led_info[i].name);}
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");
  • 针对驱动程序书写Makefile
obj-m += led_drv.o
all:make -C /home/arm/kernel SUBDIRS=$(PWD) modules
clean:make -C /home/arm/kernel SUBDIRS=$(PWD) modules
  • 针对应用程序使用交叉编译器
arm-cortex_a9-linux-gnueabi-gcc -o led_test led_test.c 
  • 下位机测试
# 安装驱动程序
insmod led_drv.ko
# 查看设备号
cat /proc/devices
# 创建字符设备文件
mknod /dev/zpyl c 244 0
# 执行应用程序
./led_test
# 卸载驱动程序
rmmod led_drv

5.7 字符设备驱动硬件操作接口

5.7.1 write接口

应用程序端的write

ssize_t write(int fd, const void *buf, size_t count);
- 功能:向硬件设备写入数据
- 参数:- fd:设备文件描述符,对应的就是/dev/myled,对应的就是硬件设备- buf:传递用户缓冲区的内存首地址(注意:用户缓冲区的地址范围:0x0000000~0xBFFFFFFF),此缓冲区存储的是将来向硬件设备写入的数据- count:希望要写入的字节数
- 返回值:返回实际写入的字节数,写入失败返回-1

底层驱动write接口

struct file_operations {ssize_t (*write) (struct file *file, const char __user *buf, size_t count, loff_t *ppos);
};
- 功能:负责将用户缓冲区中要写入的数据(字符串,整型数字,数组,结构体)最终写入到硬件,就是一个桥梁的作用,连接用户和硬件,类似搬运工: 用户缓冲区的数据->驱动write->硬件外设
- 参数:- file指针:文件指针,跟应用程序的write的fd第一个参数有亲戚关系,通过fd能够找到file指针- buf:由于用__user修饰,表示buf指针变量只能保存用户缓冲区的首地址,等于应用write的第二个参数切记:底层驱动不能直接利用此buf指针访问用户缓冲区的内存数据,而是需要使用内核提供的内存拷贝函数copy_from_user- count:等于应用write系统调用函数的第三个参数,表示希望要写入的字节数- ppos:记录上一次的读写位置,初始值为0
  • 读写位置步骤:
    1.先获取上一次的读写位置:
    loff_t pos = *ppos;
    2.假设这次又写入了100个字节,当底层驱动write返回之前记得更新读写位置
    *ppos = pos + 100;
    注意:多次写入操作
  • 调用关系:
    应用write->C库write->软中断->内核的sys_write->驱动的write接口
int copy_from_user(void *to, const void __user *from, int n)
- 功能:将用户缓冲区的数据拷贝到内核缓冲区中,此函数会帮你做安全性的检查
- 参数:- to:指定内核缓冲区首地址- from:指定用户缓冲区的首地址- n:指定要拷贝的字节数
  • write 案例
    向LED硬件写1开灯,写0关灯
  • 驱动程序
#include <linux/init.h>
#include <linux/module.h>#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h> //copy_from_user// 声明led硬件结构体
struct led_gpio{int gpio;char *name;
};
// 定义灯的硬件信息
static struct led_gpio led_info[]={{.name = "led0",.gpio = PAD_GPIO_C+12},{.name = "led1",.gpio = PAD_GPIO_C+7}
};
// 定义设备号
static dev_t dev;
// 定义字符设备对象
static struct cdev led_cdev;// 声明操作led灯的结构体类型
struct led_ops{int id; // 编号int cmd;// 操作命令
};
// 定义操作函数
static ssize_t led_write(struct file *file,const char __user *buf,size_t count,loff_t *ppos){struct led_ops nled;// 内核缓冲区printk("write\n");if(copy_from_user(&nled,buf,count)){return -EFAULT;}// 操作灯gpio_set_value(led_info[(nled.id)%ARRAY_SIZE(led_info)].gpio,!nled.cmd);return count;
}
// 定义初始化操作接口函数
static struct file_operations led_fops={.write=led_write
};
// 入口函数
static int led_init(void){// 申请GPIO资源int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_request(led_info[i].gpio,led_info[i].name);gpio_direction_output(led_info[i].gpio,1);}// 申请设备号alloc_chrdev_region(&dev,0,1,"zpyl");printk("%d dev %d\n",MAJOR(dev),MINOR(dev));// 关联操作接口cdev_init(&led_cdev,&led_fops);// 向内核注册设备对象cdev_add(&led_cdev,dev,1);return 0;
}
// 出口函数
static void led_exit(void){int i;// 卸载字符设备对象cdev_del(&led_cdev);// 释放设备号unregister_chrdev_region(dev,1);// 释放GPIOfor(i=0;i<ARRAY_SIZE(led_info);i++){gpio_direction_output(led_info[i].gpio,1);gpio_free(led_info[i].gpio);printk("%s led free\n",led_info[i].name);}
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
  • 应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>// 灯的结构体
struct led_info{int id;//灯的编号int cmd; // 命令
};
int main(int argc ,char*argv[]){int fd;struct led_info led_cmd;//用户输入的命令if(argc <3){printf("user input num < 3\n");return -1;}printf("11\n");fd = open("/dev/zpyl",O_RDWR);if(fd <0){printf("open zpyl failed\n");return -1;}printf("%d %s %s\n",argc,argv[1],argv[2]);if(!strcmp(argv[1],"on")){led_cmd.cmd = 1;}else if(!strcmp(argv[1],"off")){led_cmd.cmd = 0;}else{printf("cmd input error\n");close(fd);return -1;}led_cmd.id =atoi(argv[2]);write(fd,&led_cmd,sizeof(led_cmd));close(fd);return 0;
}
  • 测试
# 安装驱动 测试程序
./led_test on 1

5.7.2 read接口

应用程序read

ssize_t  read(int fd, void *buf, size_t count)
- 功能:从硬件设备中读取数据
- 参数:- fd:设备文件描述符,对应就是文件,本质最终对应硬件外设- buf:用于保存数据的用户缓冲区首地址- count:希望读取的字节数
- 返回值:返回实际读取的字节数

驱动的read

struct file_operations {ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *ppos);
};
- 功能:负责从硬件外设中读取数据,然后将读取到的数据拷贝给应用程序,拷贝到用户缓冲区中,起到了一个桥梁的作用,连接应用和硬件硬件数据->read->应用用户缓冲区
- 参数:- file:文件指针,file和应用read的第一个参数fd是亲戚关系,通过fd能够找到file- buf:保存用户缓冲区的首地址,同样底层驱动不能直接访问,必须利用内核提供的内存拷贝函数:copy_to_user,将内核缓冲区中的数据拷贝到用户缓冲区中- count:等于应用程序read的第三个参数(例如:count=sizeof(status)),保存希望读取的字节数- ppos:记录着上一次的读写位置,用于多次读取
- 返回值:返回实际读取的字节数,失败返回-1

调用关系:应用read->C库read->触发软中断->内核的sys_read->底层驱动read接口

int  copy_to_user(void __user *to, void *from, int n)
- 功能:将内核缓冲区的数据拷贝到用户缓冲区中,帮你做地址的安全性检查
- 参数:- to:用户缓冲区的首地址- from:内核缓冲区的首地址- n:要拷贝的字节数

5.7.3 ioctl接口

应用程序的ioctl

int ioctl(int fd, unsigned long cmd, ...); 
- 功能:1.利用此函数可以向硬件设备发送控制命令(有种write感觉)  2.利用此函数还可以跟硬件进行数据的交互(又有读又有写的感觉)
- 参数:- fd:设备文件描述符- cmd:给硬件设备发送的控制命令命令由驱动工程师自行定义,命令的值尽量大点,建议10以内的数字不要用例如:#define  LED_ON		(0x100001) //开灯命令#define  LED_OFF	(0x100002) //关灯命令- ...:如果应用程序要和硬件进行读或者写操作,第三个参数只需传递用户缓冲区的首地址即可,类似read/write的第二个参数void *buf,将来驱动程序利用copy_from_user/copy_to_user可以对用户缓冲区进行读或者写数据操作
- 返回值:成功返回0,失败返回-1

内核驱动程序ioctl

struct file_operations {long (*unlocked_ioctl)(struct file *file, unsigned int cmd, unsigned long buf);
};
- 功能:1.根据用户发送来的命令操作硬件2.如果应用ioctl传递三个参数,表示应用想利用ioctl实现和硬件的数据交互
- 参数:- file:文件指针,跟应用ioctl的第一个参数fd是亲戚关系- cmd:其值就是应用ioctl发送来的命令例如:cmd=LED_ON/LED_OFF,将来底层驱动解析cmd命令,各种判断- buf:如果应用ioctl传递三个参数,那么此buf保存的就是应用程序ioctl的第三个参数,底层驱动将来对buf进行访问时,记得数据类型要转换,同样利用copy_to_user和copy_from_user进行内存的拷贝,不能直接访问
  • 案例 使用ioctl实现读写操作
    应用程序
#include <stdio.h>
#include <unistd.h>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>// 灯的结构体
struct led_info{int id;//灯的编号int cmd; // 命令
};// 灯的状态
struct led_state{int index;// 灯的编号int state; // 灯的状态
};#define LED_ON  0x100001 //开灯命令
#define LED_OFF 0x100002 //关灯命令
#define LED_READ 0x100003 // 读取命令
int main(int argc ,char*argv[]){int fd;struct led_info led_cmd;//用户输入的命令struct led_state led_read;// led 状态if(argc <3){printf("user input num < 3\n");return -1;}printf("11\n");fd = open("/dev/zpyl",O_RDWR);if(fd <0){printf("open zpyl failed\n");return -1;}printf("%d %s %s\n",argc,argv[1],argv[2]);led_cmd.id=atoi(argv[2]);if(!strcmp(argv[1],"on")){led_cmd.cmd = 1;ioctl(fd,LED_ON,&led_cmd);}else if(!strcmp(argv[1],"off")){led_cmd.cmd = 0;ioctl(fd,LED_OFF,&led_cmd);}else if(!strcmp(argv[1],"read")){led_read.index = led_cmd.id;ioctl(fd,LED_READ,&led_read);printf("第%d灯的状态是:%s\n",led_read.index,led_read.state?"关":"开");}else {printf("cmd input error\n");close(fd);return -1;}close(fd);return 0;
}

驱动程序

nclude <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h> //copy_from_user// 声明led硬件结构体
struct led_gpio{int gpio;char *name;
};
// 定义灯的硬件信息
static struct led_gpio led_info[]={{.name = "led0",.gpio = PAD_GPIO_C+12},{.name = "led1",.gpio = PAD_GPIO_C+7}
};
// 定义设备号
static dev_t dev;
// 定义字符设备对象
static struct cdev led_cdev;// 声明操作led灯的结构体类型
struct led_ops{int id; // 编号int cmd;// 操作命令
};// 声明读取灯的结构体
struct led_state{int index;int state;
};
// 定义操作函数
#define LED_ON  0x100001 //开灯命令
#define LED_OFF 0x100002 //关灯命令
#define LED_READ 0x100003 // 读取命令 
static long led_ioctl(struct file *file,unsigned int cmd,unsigned long buf){struct led_ops nled;// 内核缓冲区 struct led_state led; // 内核缓冲区switch(cmd){case LED_ON:if(copy_from_user(&nled,(struct led_ops*)buf,sizeof(nled))){printk("led_ops is fault\n");return -EFAULT;}gpio_set_value(led_info[nled.id].gpio,!nled.cmd);break;case LED_OFF:if(copy_from_user(&nled,(struct led_ops*)buf,sizeof(nled))){printk("led_ops is fault\n");return -EFAULT;}gpio_set_value(led_info[nled.id].gpio,!nled.cmd);break;case LED_READ:if(copy_from_user(&led,(struct led_state*) buf,sizeof(led))){return -EFAULT;};led.state = gpio_get_value(led_info[(led.index)%ARRAY_SIZE(led_info)].gpio);if(copy_to_user((struct led_state* )buf,&led,sizeof(led))){return -EFAULT;}break;default :break;}return 0;
};
// 定义初始化操作接口函数
static struct file_operations led_fops={.unlocked_ioctl = led_ioctl
};
// 入口函数
static int led_init(void){// 申请GPIO资源int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_request(led_info[i].gpio,led_info[i].name);gpio_direction_output(led_info[i].gpio,1);}// 申请设备号alloc_chrdev_region(&dev,0,1,"zpyl");printk("%d dev %d\n",MAJOR(dev),MINOR(dev));// 关联操作接口cdev_init(&led_cdev,&led_fops);// 向内核注册设备对象cdev_add(&led_cdev,dev,1);return 0;
}
// 出口函数
static void led_exit(void){int i;// 卸载字符设备对象cdev_del(&led_cdev);// 释放设备号unregister_chrdev_region(dev,1);// 释放GPIOfor(i=0;i<ARRAY_SIZE(led_info);i++){gpio_direction_output(led_info[i].gpio,1);gpio_free(led_info[i].gpio);printk("%s led free\n",led_info[i].name);}
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

5.8 文件自动创建和自动删除

insmod安装驱动时设备文件自动创建好
rmmod卸载驱动时设备文件自动删除
驱动程序只需调用以下四个函数最终完成设备文件的自动创建和自动删除

struct class *cls;  //定义设备类指针
cls = class_create(THIS_MODULE, "tarena1"); //创建设备类对象
//THIS_MODULE:内核常量
//"tarena1":对象名 将来在/sys/class/tarena1目录(生成)
device_create(cls,  NULL, dev, NULL, "myled"); //在tarena1设备类下自动创建设备文件/dev/myled,设备号是dev
//此函数内部会帮你解析/proc/sys/kernel/hotplug文件,找到/sbin/mdev
//然后把设备号dev和设备文件名myled给/sbin/mdev,mdev自动帮你创建
//创建设备文件的原材料:设备号dev和设备文件名myled将来会放在/sys/class/tarena1目录下device_destroy(cls, dev);//自动删除设备文件,
class_destroy(cls);//自动删除设备类对象
  • 修改之前的驱动代码
#include <linux/module.h>
#include <linux/module.h>#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h> //copy_from_user
#include <linux/device.h>// 定义设备号
static dev_t dev;
// 定义字符设备对象
static struct cdev led_cdev;
// 定义初始化操作接口函数
static struct file_operations led_fops={.unlocked_ioctl = led_ioctl
};
// 定义设备类指针
struct class *cls;
// 入口函数
static int led_init(void){// 申请设备号alloc_chrdev_region(&dev,0,1,"zpyl");printk("%d dev %d\n",MAJOR(dev),MINOR(dev));// 关联操作接口cdev_init(&led_cdev,&led_fops);// 向内核注册设备对象cdev_add(&led_cdev,dev,1);// 创建设备类对象cls = class_create(THIS_MODULE,"zpyl1");// 在zpyl1设备下自动创建设备文件/dev/zpyl,设备号为devdevice_create(cls,NULL,dev,NULL,"zpyl");return 0;
}
// 出口函数
static void led_exit(void){int i;// 卸载字符设备对象cdev_del(&led_cdev);// 释放设备号unregister_chrdev_region(dev,1);// 自动删除设备文件device_destroy(cls,dev);// 自动删除设备类对象class_destroy(cls);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

5.9 struct inode和struct file

  • struct inode
struct inode {dev_t	 i_rdev;//保存设备号信息struct cdev  *i_cdev; //指向字符设备对象,例如:led_cdev...
};
- 功能:描述一个文件的物理上的信息,例如:ls -l  /dev/myled //看到的各种属性都是用inode来描述
生命周期:一旦创建文件成功(mknod,vim,touch,dd,cp,mv等),linux内核就会帮你自动创建一个inode对象,用此对象来描述创建的新文件各种属性信息,一旦删除文件(rm),内核也会帮你自动删除对应的inode对象
结论:1.一个文件仅有一个inode对象2.字符设备驱动操作接口
struct file_operations {int (*open)(struct inode *inode, ....);int (*release)(struct inode *inode, ....);
};
这些接口的第一个形参inode指针就是指向内核自动帮你创建的inode对象,将来驱动open,release接口可以通过inode指针来获取对应的文件信息
重点关注设备号:inode->i_rdev这个成员,能够提取主次设备号:MAJOR(inode->i_rdev), MINOR(inode->i_rdev);
  • struct file
struct file { const struct file_operations	*f_op;//指向字符设备驱动硬件操作接口对象,例如:led_fops...
};
- 功能:描述一个文件被成功打开open以后的各种属性(权限啦,读写位置信息等)
生命周期:文件一旦被open成功打开,linux内核(sys_open)自动帮你创建一个file对象,来描述文件打开以后的属性,文件一旦被关闭close,内核自动销毁对应的file对象
结论:1.一个文件可以有多个file对象2.字符设备驱动操作接口:
struct file_operations {int (*open)(struct inode *inode, struct file *file);int (*release)(struct inode *inode, struct file *file);int (*read)(struct file *file,...);int (*write)(struct file *file, ...);int (*unlocked_ioctl)(struct file *file, ...);
};
这些接口的file指针指向内核创建的file对象
问:如何通过file指针来获取对应文件的inode对象指针呢?
答:参见fbmem.c(LCD显示屏驱动)
struct inode *inode = file->f_path.dentry->d_inode;
再通过inode就可以获取设备号了!

5.10 综合案例

案例:编写LED字符设备驱动,利用ioctl实现开关某个灯,目前要求:将四个LED灯作为四个硬件个体,又由于他们的硬件特性是一致的,所以他们共用一个驱动程序,驱动将来通过次设备号来区分LED硬件个体

  • 应用程序
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>// 命令
#define LED_ON    0x10001 // 开灯命令
#define LED_OFF   0x10002 // 关灯命令
#define LED_READ 0x10003 // 读取命令int main(){char cmd[10];int index;int i=0;char *dev[] = {"/dev/zpyl0","/dev/zpyl1","/dev/zpyl2","/dev/zpyl3"};int fd[4] ={1,2,3,4};int led_stat;for(i=0;i<4;i++){fd[i]=open(*(dev+i),O_RDWR);if(fd[i]<0){printf("open fault\n");return -1;}}while(1){scanf("%d %s",&index,cmd);if(index<0 || index>3){printf("index : 0~3\n");continue;}if(!strcasecmp(cmd,"on")){ioctl(fd[index],LED_ON);	}else if(!strcasecmp(cmd,"off")){ioctl(fd[index],LED_OFF);}else if(!strcasecmp(cmd,"read")){ioctl(fd[index],LED_READ,&led_stat);printf("%d灯的状态为%s\n",index,(led_stat?"关":"开"));}}for(i=0;i<4;i++){close(fd[i]);}return 0;
}
  • 驱动程序
#include <linux/init.h>
#include <linux/module.h>#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h>
#include <linux/device.h>
// 声明GPIO
struct led_gpio{int gpio;char* name;
};
// 定义led的GPIO
static struct led_gpio led_info[]={{.name = "led0",.gpio = PAD_GPIO_C+12},{.name = "led1",.gpio = PAD_GPIO_C+7},{.name = "led2",.gpio = PAD_GPIO_C+11},{.name = "led3",.gpio = PAD_GPIO_B+26}	
};
// 定义设备号
static dev_t dev;
// 定义字符设备对象
static struct cdev led_cdev;// 声明操作命令
#define LED_ON   0x10001 // 开灯命令
#define LED_OFF  0x10002 // 关灯命令
#define LED_READ 0x10003 // 读取命令// 定义操作函数
static long led_ioctl(struct file *file,unsigned int cmd,unsigned long buf){int led_stat;// 灯的状态//通过file获取文件对应的inode指针struct inode *inode = file->f_path.dentry->d_inode;//通过inode来获取次设备号int minor = MINOR(inode->i_rdev); switch(cmd){case LED_ON:gpio_set_value(led_info[minor].gpio,0);printk("%s 开\n",led_info[minor].name);break;case LED_OFF:gpio_set_value(led_info[minor].gpio,1);printk("%s 关\n",led_info[minor].name);break;case LED_READ:led_stat =  gpio_get_value(led_info[minor].gpio);if(copy_to_user((int *)buf,&led_stat,sizeof(led_stat))){return  -EFAULT;}printk("%s 读取\n",led_info[minor].name);break;default : break;}return 0;
};
// 定义操作接口对象
static struct file_operations led_fops={.unlocked_ioctl = led_ioctl
};// 定义设备类指针
struct class *cls;// 入口函数
static int led_init(void){// 初始化GPIO 并输出为1int i=0;for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_request(led_info[i].gpio,led_info[i].name);gpio_direction_output(led_info[i].gpio,1);printk("%s init success\n",led_info[i].name);}// 申请设备号alloc_chrdev_region(&dev,0,4,"zpyl");printk("MAJOR = %d\n",MAJOR(dev));// 关系设备对象操作接口cdev_init(&led_cdev,&led_fops);// 注册设备对象cdev_add(&led_cdev,dev,4);// 创建设备类对象cls = class_create(THIS_MODULE,"zpyl1");// 创建设备文件device_create(cls,NULL,MKDEV(MAJOR(dev),0),NULL,"zpyl0");device_create(cls,NULL,MKDEV(MAJOR(dev),1),NULL,"zpyl1");device_create(cls,NULL,MKDEV(MAJOR(dev),2),NULL,"zpyl2");device_create(cls,NULL,MKDEV(MAJOR(dev),3),NULL,"zpyl3");return 0;
};// 出口函数
static void led_exit(void){int i=0;// 卸妆字符设备对象cdev_del(&led_cdev);// 释放GPIO资源for(i=0;i<ARRAY_SIZE(led_info);i++){gpio_free(led_info[i].gpio);printk("%s free\n",led_info[i].name);}// 释放设备号unregister_chrdev_region(dev,4);// 删除设备文件device_destroy(cls,MKDEV(MAJOR(dev),0));device_destroy(cls,MKDEV(MAJOR(dev),1));device_destroy(cls,MKDEV(MAJOR(dev),2));device_destroy(cls,MKDEV(MAJOR(dev),3));// 删除设备类对象class_destroy(cls);
};module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");
  • Makefile
obj-m += led_drv.o
all:make -C /home/arm/kernel SUBDIRS=$(PWD) modulescp led_drv.ko /home/rootfs/home/drivers/arm-cortex_a9-linux-gnueabi-gcc -o led_test led_test.ccp led_test /home/rootfs/home/drivers/
clean:make -C /home/arm/kernel SUBDIRS=$(PWD) modules

6、Linux内核混杂设备驱动

6.1 混杂设备定义

  • 从软件实现角度去看,混杂设备本质还是字符设备,只是它的主设备号由内核已经定义好为10,各个混杂设备驱动通过次设备号来区分
  • linux内核描述混杂设备的结构体
struct miscdevice {const char *name;int minor;const struct file_operations *fops;
};
- 功能:描述混杂设备属性name: 混杂设备文件名,并且由linux自动帮你创建,无需调用四个函数minor:给混杂设备指定的次设备号,一般指定宏:MISC_DYNAMIC_MINOR,表示让linux内核帮你分配一个次设备号fops:混杂设备的硬件操作接口
  • 相关函数
misc_register(& 混杂设备对象)
功能:向内核注册一个混杂设备对象
misc_deregister(& 混杂设备对象);
功能:从内核中卸载混杂设备对象
  • 案例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <mach/platform.h>// 声明gpio
struct key_gpio{int gpio;char* name;
};
// 定义GPIO
static struct key_gpio key_info[]={{.name="key_1",.gpio=PAD_GPIO_A+28},{.name="key_2",.gpio=PAD_GPIO_B+9},{.name="key_3",.gpio=PAD_GPIO_B+30},{.name="key_4",.gpio=PAD_GPIO_B+31}
};static ssize_t key_read(struct file* file,char __user* buf,size_t count,loff_t *ppos){int key_state;// 内核保存key的状态key_state = gpio_get_value(key_info[0].gpio);	copy_to_user(buf,&key_state,count);return count;
};
// 操作接口函数
static struct file_operations key_fops={.read=key_read
};
// 混杂设备
static struct miscdevice key_device={.name="zp",// 驱动名称.minor=MISC_DYNAMIC_MINOR,// 混杂设备的主设备号是10,次设备号由系统分配.fops=&key_fops// 操作函数
};
static int key_init(void){// 申请GPIO资源int i=0;for(i=0;i<ARRAY_SIZE(key_info);i++){gpio_request(key_info[i].gpio,key_info[i].name);gpio_direction_input(key_info[i].gpio);printk("%s init success\n",key_info[i].name);}// 注册misc_register(&key_device);return 0;
};static void key_exit(void){	int i=0;for(i=0;i<ARRAY_SIZE(key_info);i++){gpio_free(key_info[i].gpio);printk("%s free success\n",key_info[i].name);}// 卸载misc_deregister(&key_device);
};module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/400802.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1Panel应用推荐:KubePi开源Kubernetes管理面板

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…

H7-TOOL混合脱机烧录以及1拖4不同的通道烧录不同的程序操作说明(2024-08-07)

【应用场景】 原本TOOL的1拖4是用于同时烧录相同程序给目标板&#xff0c;但有时候一个板子上有多个不同的MCU&#xff0c; 客户希望仅通过一个TOOL就可以完成对板子上多个MCU的烧录&#xff0c;也就是1拖4不同的通道烧录不同的程序&#xff0c;此贴为此制作。 【实验目标】…

序列建模之循环和递归网络 - 循环神经网络篇

序言 在探索序列数据的深层规律时&#xff0c;循环神经网络&#xff08; RNN \text{RNN} RNN&#xff09;以其独特的设计思想成为了序列建模领域的中流砥柱。与传统的神经网络不同&#xff0c; RNN \text{RNN} RNN引入了循环结构&#xff0c;使得网络能够处理任意长度的序列数…

winform 大头针实现方法——把窗口钉在最上层

平时我们再使用成熟的软件的时候&#xff0c;会发现有个大头针的功能挺不错的。就是点一下大头针&#xff0c;窗口就会钉住&#xff0c;一直保持在最上面一层&#xff0c;这样可以一边设置参数&#xff0c;一边观察这个窗口里面的变化&#xff0c;比较方便。下面我就来简单实现…

移动APP测试有哪些注意事项?专业APP测试报告如何获取?

移动APP在其生命周期中有不同的阶段&#xff0c;从开始到投入目标市场再到被淘汰。移动APP的成功有多种因素&#xff0c;例如创建、部署、推广、粘性等。但是&#xff0c;创建出色APP的关键在于它的测试&#xff0c;软件测试负责为客户提供安全有效的产品&#xff0c;因此移动A…

大数据-83 Spark 集群 RDD编程简介 RDD特点 Spark编程模型介绍

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Golang 与 Java:编程语言比较及如何选择

Golang 与 Java&#xff1a;哪种语言更好&#xff1f;我们的详细比较指南涵盖了语法、性能和流行度方面的主要差异&#xff0c;以帮助您做出决定。 在规划项目时&#xff0c;有许多编程语言可供选择。但一开始就选择正确的语言是成功启动或交付的关键。选择错误的语言&#xff…

Apache Tomcat 信息泄露漏洞排查处理CVE-2024-21733)

一、漏洞描述 Apache Tomcat作为一个流行的开源Web服务器和Java Servlet容器并用于很多中小型项目的开发中。其中,Coyote作为Tomcat的连接器组件,是Tomcat服务器提供的供客户端访问的外部接口,客户端通过Coyote与服务器建立链接、发送请求并且接收响应。 近日发现Apache To…

【python】OpenCV—Optical Flow

文章目录 1、光流2、Opencv 中光流的实现3、稀疏光流4、密集光流4.1、farneback4.2、lucaskanade_dense4.3、rlof 5、涉及到的库5.1、cv2.goodFeaturesToTrack5.2、cv2.calcOpticalFlowPyrLK5.3、cv2.optflow.calcOpticalFlowSparseToDense5.4、cv2.calcOpticalFlowFarneback5.…

CentOS7.9上通过KVM安装Centos虚拟机

目录 1 开发前准备&#xff08;先确保服务器可以虚拟化&#xff09;&#xff1a; 2、安装KWM环境 3、创建镜像文件存放目录 4、创建镜像文件存放目录 5、安装桥连接虚拟网络 6、安装虚拟机 7、配置操作系统 8、虚拟机配置网卡地址 9、克隆虚拟机执行 1开发前准备&am…

Unity教程(十)Tile Palette搭建平台关卡

Unity开发2D类银河恶魔城游戏学习笔记 Unity教程&#xff08;零&#xff09;Unity和VS的使用相关内容 Unity教程&#xff08;一&#xff09;开始学习状态机 Unity教程&#xff08;二&#xff09;角色移动的实现 Unity教程&#xff08;三&#xff09;角色跳跃的实现 Unity教程&…

IDEA 创建类时自动生成注释

一、背景 在开发的过程中&#xff0c;公司都会要求开发针对自己创建的类进行一些描述说明&#xff0c;为了便于程序员在创建类时快速生成注释。 二、如何配置? 打开File -> Settings -> Editor -> File and Code Templates -> Includes&#xff0c;在File Header…

Unity新输入系统结构概览

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 在学习新输入系统之前&#xff0c;我们需要对其构成有个印象 1.输入动作&#xff08;Inputaction&#xff09; 是定义输…

一次caffeine引起的CPU飙升问题

背景 背景是上游服务接入了博主团队提供的sdk&#xff0c;已经长达3年&#xff0c;运行稳定无异常&#xff0c;随着最近冲业绩&#xff0c;流量越来越大&#xff0c;直至某一天&#xff0c;其中一个接入方&#xff08;流量很大&#xff09;告知CPU在慢慢上升且没有回落的迹象&…

2分钟搭建一个简单的WebSocket服务器

你好同学&#xff0c;我是沐爸&#xff0c;欢迎点赞、收藏和关注。个人知乎 如何用2分钟在本地搭建一个简单的 WebSocket 服务器&#xff1f;其实使用 Node.js&#xff0c;加上一些流行的库&#xff0c;是很容易实现的。前端同学通过自己搭建 WebSocket 服务器&#xff0c;对于…

百问网全志系列开发板音频ALSA配置步骤详解

8 ALSA 8.1 音频相关概念 ​ 音频信号是一种连续变化的模拟信号&#xff0c;但计算机只能处理和记录二进制的数字信号&#xff0c;由自然音源得到的音频信号必须经过一定的变换&#xff0c;成为数字音频信号之后&#xff0c;才能送到计算机中作进一步的处理。 ​ 数字音频系…

系统重装简记

写在文章开头 因为固态损毁而更换固态&#xff0c;所以需要进行系统重装&#xff0c;由于系统重装都是固定的繁琐的步骤&#xff0c;所以就以这篇文章来记录一下系统重装的一些日常步骤&#xff0c;希望对你有帮助。 Hi&#xff0c;我是 sharkChili &#xff0c;是个不断在硬核…

《Linux运维总结:基于x86_64架构CPU使用docker-compose一键离线部署etcd 3.5.15容器版分布式集群》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;《Linux运维篇&#xff1a;Linux系统运维指南》 一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面对不同的客户部署业务系统&#xff0…

【网编】——UDP编程

宏观操作 服务器&#xff1a;socket创套接字—bind绑定连接—recvfrom接收数据/sendto发送数据 客户端&#xff1a;socket创套接字—sendto发送数/recvfrom接收数据—close关闭套接字 函数 recv ssize_t recvfrom ( int sockfd , void * buf , size_t len , int flags , str…

链接Mysql 报错connection errors; unblock with ‘mysqladmin flush-hosts‘错误的解决方法!亲测有效!

文章目录 前言一、使用 mysqladmin flush-hosts 命令解锁 IP 地址二、增加 max_connect_errors 参数三、检查连接错误的原因 前言 今天正常的对各大的测试服进行重启的时候发现每台服务器都启动失败&#xff01;查看日志发现每台服务器都报一下的错误 java.sql.SQLException:…