嵌入式驱动初级-字符设备驱动基础

文章目录

  • 前言
  • 一、驱动学习预备知识
    • 1.什么是设备驱动程序
    • 2.向内核添加新功能方法
      • 2.1新功能源码与Linux内核源码不在同目录下
      • 2.2在Ubuntu下加载和删除ko文件步骤
      • 2.3在开发板下加载和删除ko文件步骤
      • 2.4内核模块基础代码解析
  • 二、字符设备驱动框架
    • 2.1Linux内核对设备的分类
    • 2.2字符设备驱动开发步骤
    • 2.3字符设备驱动代码编写
    • 2.4字符设备应用层代码编写
    • 2.5字符设备驱动添加读写
  • 三、ioctl设备操作
    • 3.1ioctl头文件定义mychar.h
    • 3.2ioctl应用层代码
    • 3.3ioctl驱动层代码


前言

记录嵌入式驱动学习笔记

一、驱动学习预备知识

学习驱动前要了解的基础内容和命令。

1.什么是设备驱动程序

一种添加到操作系统中的特殊程序,主要作用是协助操作系统完成应用程序与对应硬件设备之间数据传送的功能。简言之,设备驱动程序就是操作系统中“驱动”对应硬件设备使之能正常工作的代码。

一个驱动程序主要完成如下工作:

  1. 初始化设备,让设备做好开始工作的准备
  2. 读数据:将设备产生的数据传递给上层应用程序
  3. 写数据:将上层应用程序交付过来的数据传递给设备
  4. 获取设备信息:协助上层应用程序获取设备的属性、状态信息
  5. 设置设备信息:让上层应用程序可以决定设备的一些工作属性、模式
  6. 其它相关操作:如休眠、唤醒、关闭设备等
    其中最核心的工作就是设备数据的输入和输出,因此计算机外部设备(外设)也被称为IO设备

2.向内核添加新功能方法

向内核添加新功能的方法分为两种,一种静态加载方法,一种动态加载方法。
静态加载方法思想:即新功能源码与内核其它代码一起编译进uImage文件内,在学习驱动的阶段我们经常采用动态加载方法因此不过多介绍该方法。
动态加载方法思想:即新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko

2.1新功能源码与Linux内核源码不在同目录下

  1. cd ~/fs4412
  2. mkdir mydrivercode
  3. cd mydrivercode
  4. cp …/linux-3.14/drivers/char/myhello.c .
  5. vim Makefile
  6. make (生成的ko文件适用于主机ubuntu linux)
  7. make ARCH=arm (生成的ko文件适用于开发板linux,注意此命令执行前,开发板的内核源码已被编译)

#file命令可以查看指定ko文件适用于哪种平台,用法:
file ko文件
#结果带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux

2.2在Ubuntu下加载和删除ko文件步骤

sudo insmod ./???.ko  
#此处为内核模块文件名,将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod 
#查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod ??? 
#此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
sudo dmesg -C  #清除内核已打印的信息
dmesg #查看内核的打印信息

2.3在开发板下加载和删除ko文件步骤

#先将生成的ko文件拷贝到/opt/4412/rootfs目录下:
cp ????/???.ko  /opt/4412/rootfs
#在串口终端界面开发板Linux命令行下执行
insmod ./???.ko  #将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod #查看已被插入的内核模块有哪些
rmmod ??? #将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
内核随时打印信息,我们可以在串口终端界面随时看到打印信息,不需要dmesg命令查看打印信息

2.4内核模块基础代码解析

Linux内核的插件机制——内核模块
类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。

代码如下(示例):

#include  //包含内核编程最常用的函数声明,如printk
#include  //包含模块编程相关的宏定义,如:MODULE_LICENSE

/*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作
  被称为模块的入口函数
  
  __init的作用 : 
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text")))   实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init myhello_init(void)
{
    /*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,
    Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)
    printk不支持浮点数打印*/
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("myhello is running\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	return 0;
}

/*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
  被称为模块的出口函数
  
  __exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text")))   实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit myhello_exit(void)
{
	printk("myhello will exit\n");
}

/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2"  "GPL and additional rights"  "Dual BSD/GPL"  "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用

其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
	myhello:module license 'unspecified' taints kernel
	Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");

/*
module_init 宏
1. 用法:module_init(模块入口函数名) 
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(myhello_init);

/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(myhello_exit);

模块三要素:入口函数 出口函数 MODULE__LICENSE
Makefile文件用于生成ko模块,代码如下

ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions
else
obj-m += hello.o
endif

内核接口头文件查询

  1. 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
  2. 找不到则更大范围的include目录下查询,命令同上

二、字符设备驱动框架

2.1Linux内核对设备的分类

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

  1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率。块设备不直接面向应用程序,一般使用一个文件系统去对接。
  3. 网络设备:针对网络数据收发的设备

嵌入式驱动初级-字符设备驱动基础_第1张图片

2.2字符设备驱动开发步骤

A:设备号
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号。dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型,,其中高 12 位为主设备号,低 20 位为次设备号。应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
嵌入式驱动初级-字符设备驱动基础_第2张图片
MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:

	dev_t devno;
	int major = 251;//主设备号
	int minor = 2;//次设备号
	devno = MKDEV(major,minor);

MAJOR宏用来从32位设备号中分离出主设备号,用法:

	dev_t devno = MKDEV(249,1);
	int major = MAJOR(devno);

MINOR宏用来从32位设备号中分离出次设备号,用法:

	dev_t devno = MKDEV(249,1);
	int minor = MINOR(devno);

如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:

//其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个
//字符设备,“200”是设备的主设备号,“0”是设备的次设备号。
	mknod /dev/chrdevbase c 200 0

B:申请和注销设备号
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数有两套如下所示:

	int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:
	from:自己指定的设备号
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:
	成功为0,失败负数,绝对值为错误码

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:
	dev:分配设备号成功后用来存放分配到的设备号
	baseminior:起始的次设备号,一般为0
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:
	成功为0,失败负数,绝对值为错误码

分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息

void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
	from:已成功分配的设备号将被释放
	count:申请成功的设备数量

释放后/proc/devices文件对应的记录消失

C:注册字符设备
在 Linux 中使用 cdev 结构体表示一个字符设备,在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备。

1.定义 cdev 结构体变量

struct cdev test_cdev;

2.用 cdev_init 函数对其进行初始化
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下

//参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

3.cdev_add 函数用于向 Linux 系统添加字符设备
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数,完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下

//参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
//数 count 是要添加的设备数量。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

4.使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del函数原型如下

//参数 p 就是要删除的字符设备。如果要删除字符设备
void cdev_del(struct cdev *p)

使用 cdev_init 函数初始化 cdev 变量的示例代码如下:

struct cdev testcdev;
/* 设备操作函数 */
	static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	 /* 其他具体的初始项 */
	};
 testcdev.owner = THIS_MODULE;
 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
 cdev_add(&testcdev, devid, 1);/* 添加字符设备 */

小结:

字符设备驱动开发步骤:

  1. 如果设备有自己的一些控制数据,则定义一个包含struct cdev cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev
  2. 定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配)
  3. 定义三个全局变量分别来表示主设备号、次设备号、设备数
  4. 定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE
  5. module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核
  6. module exit函数:a. 注销设备号 b. 从内核中移除struct cdev c. 如果如果是全局设备指针则释放其指向空间
  7. 编写各个操作函数并将函数名初始化给struct file_operations结构体变量

验证操作步骤:

  1. 编写驱动代码mychar.c
  2. make生成ko文件
  3. insmod内核模块
  4. 查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
  5. 创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
  6. 编写app验证驱动(testmychar_app.c)
  7. 编译运行app,dmesg命令查看内核打印信息

2.3字符设备驱动代码编写

#include 
#include 
#include 
#include 
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{
   printk("mychar_open is called\n");
   return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{
   printk("mychar_close is called\n");
   return 0;
}

//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{
   .owner=THIS_MODULE,
   .open=mychar_open,
   .release=mychar_close,
};

int __init mychar_init(void)
{
   //用来接收注册设备函数的返回值
   int ret = 0;
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   /*1.申请设备号*/
   //静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
   //ret = register_chrdev_region(devno,mychar_num,"mychar");
   ret = register_chrdev_region(devno,mychar_num,mycharname);
   //分配失败返回负数
   if(ret)
   {
      //动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
      //ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");
      ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);
      if(ret)
      {
         printk("get devno failed\n");
         return -1;
      }
      //如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号
      major = MAJOR(devno);
   }
   /*2.将struct cdev对象添加到内核对应的数据结构里*/
   //初始化cdev 给struct cdev对象指定操作函数集
   cdev_init(&mydev,&myops);
   mydev.owner=THIS_MODULE;
   //将字符设备添加进内核 哈希管理列表
   cdev_add(&mydev,devno,1);
   
 
   return 0;
}
   
void __exit mychar_exit(void)
{
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   //注销字符设备驱动 入口参数:设备号 设备名称
   //unregister_chrdev(devno,"mychar");
   //将字符设备从内核移除
   cdev_del(&mydev);
   unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

2.4字符设备应用层代码编写

#include 
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"

int main(int argc, char *argv[])
{
	int fd = -1;
	if(argc < 2){
		printf("Error Usage!\r\n");
		return 1;
	}
	fd = open(argv[1],O_RDONLY);
	if(fd < 0){
		printf("Can't open file %s\r\n", argv[1]);
		return 2;
	}
	close(fd);
	fd = -1;
	return 0;
}

2.5字符设备驱动添加读写

ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
/*
完成功能:读取设备产生的数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
    pbuf:指向用户空间一块内存,用来保存读到的数据
    count:用户期望读取的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
 返回值:
    本次成功读取的字节数,失败返回-1
*/
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)

ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
/*
完成功能:向设备写入数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
    pbuf:指向用户空间一块内存,用来保存被写的数据
    count:用户期望写入的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
 返回值:
    本次成功写入的字节数,失败返回-1
*/
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)

添加读写的字符设备驱动代码

#include 
#include 
#include 
#include 
#include 

#define BUF_LEN 100
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
//全局数组 字符设备产生的数据存放
char mydev_buf[BUF_LEN];
//数组下标从零开始
int curlen=0;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{
   printk("mychar_open is called\n");
   return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{
   printk("mychar_close is called\n");
   return 0;
}
//读数据是对于应用层来说 把内核数据拷贝到应用层来读取
/*
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
    pbuf:指向用户空间一块内存,用来保存读到的数据
    count:用户期望读取的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
 返回值:
    本次成功读取的字节数,失败返回-1
*/
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{
   int ret = 0;
   //要读的数据大于数据的长度 修改要读数据的长度
   int size = 0;
   if(count > curlen)
   {
      size = curlen;
   }else
   {
      size=count;
   }
   //将内核空间数据复制到用户空间 用户空间:puser 内核空间:mydev_buf
   ret = copy_to_user(puser,mydev_buf,size);
   if(ret)
   {
      printk("copy_to_user failed\n");
      return -1;
   }
   //将剩下的数据移动到数组起始 即删除已读数据
   memcpy(mydev_buf,mydev_buf + size,curlen - size);
   curlen = curlen - size;
   return size;
}
//写数据对于应用层来说 把数据写进驱动
/*
ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
完成功能:向设备写入数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
    pbuf:指向用户空间一块内存,用来保存被写的数据
    count:用户期望写入的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
 返回值:
    本次成功写入的字节数,失败返回-1
*/
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
   int size = 0;
   int ret = 0;
   //如果剩余数组长度小于要写入的数据长度 则把要写入的数据修改 
   if(count > BUF_LEN - curlen)
   {
      size = BUF_LEN - curlen;
   }else
   {
      size = count;
   }
   //从用户空间拷贝数据到内核空间
   ret = copy_from_user(mydev_buf + curlen,puser,size);
   if(ret)
   {
      printk("copy_from_user failed\n");
      return -1;
   }
   curlen = curlen + size;
   return size;
}
//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{
   .owner=THIS_MODULE,
   .open=mychar_open,
   .release=mychar_close,
   .read=mychar_read,
   .write=mychar_write,
};

int __init mychar_init(void)
{
   //用来接收注册设备函数的返回值
   int ret = 0;
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   /*1.申请设备号*/
   //静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
   //ret = register_chrdev_region(devno,mychar_num,"mychar");
   ret = register_chrdev_region(devno,mychar_num,mycharname);
   //分配失败返回负数
   if(ret)
   {
      //动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
      //ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");
      ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);
      if(ret)
      {
         printk("get devno failed\n");
         return -1;
      }
      //如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号
      major = MAJOR(devno);
   }
   /*2.将struct cdev对象添加到内核对应的数据结构里*/
   //初始化cdev 给struct cdev对象指定操作函数集
   cdev_init(&mydev,&myops);
   mydev.owner=THIS_MODULE;
   //将字符设备添加进内核 哈希管理列表
   cdev_add(&mydev,devno,1);
   
 
   return 0;
}
   
void __exit mychar_exit(void)
{
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   //注销字符设备驱动 入口参数:设备号 设备名称
   //unregister_chrdev(devno,"mychar");
   //将字符设备从内核移除
   cdev_del(&mydev);
   unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);



三、ioctl设备操作

为了处理设备非数据的操作,内核将对设备的控制委派给了ioctl接口

long xxx_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);
功能:对相应设备做指定的控制操作(各种属性的设置获取等等)
参数:
	filp:指向open产生的struct file类型的对象,表示本次ioctl对应的那次open
	cmd:用来表示做的是哪一个操作
    arg:和cmd配合用的参数
返回值:成功为0,失败-1

cmd的命令需要遵守一种编码规则,命令按照以下方式组成

bit位	含义
31-30	00-命令不带参
		10-命令需要从驱动中获取数据,读方向
		01-命令需要向驱动中写数据,写方向
		11-命令既要写入数据又要获取数据,读写双向
29-16	如果命令带参则指定参数所占的内存空间大小 size
15-8	每个驱动全局唯一的幻数 type
7-0		命令码 nr

嵌入式驱动初级-字符设备驱动基础_第3张图片
内核提供了一组宏来定义、提取命令中的字段信息,定义命令所使用的最底层的宏是_IOC,他将4个部分通过移位合并在一起

#define _IOC(dir,type,nr,size) (((dir)<<_IOC_DIRSHIFT)| \
                               ((type)<<_IOC_TYPESHIFT)| \
                               ((nr)<<_IOC_NRSHIFT)| \
                               ((size)<<_IOC_SIZESHIFT))
/* used to create numbers */

// 定义不带参数的 ioctl 命令
#define _IO(type,nr)   _IOC(_IOC_NONE,(type),(nr),0)

//定义带读参数的ioctl命令(copy_to_user) size为类型名
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

//定义带写参数的 ioctl 命令(copy_from_user) size为类型名
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

//定义带读写参数的 ioctl 命令 size为类型名
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

/* used to decode ioctl numbers */
#define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)       (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)      (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

3.1ioctl头文件定义mychar.h

#ifndef MY_CHAR_H
#define MY_CHAR_H

#include 
//幻数使用k
#define MY_CHAR_MAGIC 'k'
/*
//定义带读参数的ioctl命令(copy_to_user) size为类型名
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
*/
//获取最大长度 type:幻数 nr:命令编码 size:命令带参时参数所占的内存大小
#define MYCHAR_IOCTL_GET_MAXLEN _IOR(MY_CHAR_MAGIC,1,int*)
//获取当前长度
#define MYCHAR_IOCTL_GET_CURLEN _IOR(MY_CHAR_MAGIC,2,int*)


#endif

3.2ioctl应用层代码

#include 
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "mychar.h"
#include 
int main(int argc, char *argv[])
{
	int fd = -1;
	char buf[8] = "";
	int max = 0;
	int cur = 0;
	if(argc < 2){
		printf("Error Usage!\r\n");
		return 1;
	}
	fd = open(argv[1],O_RDWR);
	if(fd < 0){
		printf("Can't open file %s\r\n", argv[1]);
		return 2;
	}
	ioctl(fd,MYCHAR_IOCTL_GET_MAXLEN,&max);
	printf("max len is %d\n",max);
	//往内核写6个
	write(fd,"hello",6);
	ioctl(fd,MYCHAR_IOCTL_GET_CURLEN,&cur);
	printf("cur len is %d\n",cur);
	//从内核读8个
	read(fd,buf,8);
	printf("buf=%s\n",buf);
	close(fd);
	fd = -1;
	return 0;
}

3.3ioctl驱动层代码

#include 
#include 
#include 
#include 
#include 
#include "mychar.h"
// 该程序思路 应用层先往内核mydev_buf内写 hello 然后再从内核里读出来
#define BUF_LEN 100
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//设备操作函数避免使用太多全局变量 将变量写入一个结构体
struct mychar_dev
{
   //创建cdev类型的对象 cdevinit函数要用到 包含在结构体mychar_dev相当于子类继承父类
   struct cdev mydev;
   //全局数组 字符设备产生的数据存放
   char mydev_buf[BUF_LEN];
   //数组下标从零开始
   int curlen;
}; 
//实例化一个 mychar_dev类型的对象
struct mychar_dev gmydev;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{
   //将设备结构体作为私有数据添加到设备文件中
   /*已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称)*/
   pfile->private_data =(void *) (container_of(pnode->i_cdev,struct mychar_dev,mydev));
   printk("mychar_open is called\n");
   return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{
   printk("mychar_close is called\n");
   return 0;
}
//读数据是对于应用层来说 把内核数据拷贝到应用层来读取
/*
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
    pbuf:指向用户空间一块内存,用来保存读到的数据
    count:用户期望读取的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
 返回值:
    本次成功读取的字节数,失败返回-1
*/
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{
   struct mychar_dev *pmydev = (struct mychar_dev *) pfile->private_data;
   int ret = 0;
   //要读的数据大于数据的长度 修改要读数据的长度
   int size = 0;
   if(count > pmydev->curlen)
   {
      size = pmydev->curlen;
   }else
   {
      size=count;
   }
   //将内核空间数据复制到用户空间 用户空间:puser 内核空间:mydev_buf
   ret = copy_to_user(puser,pmydev->mydev_buf,size);
   if(ret)
   {
      printk("copy_to_user failed\n");
      return -1;
   }
   //将剩下的数据移动到数组起始 即删除已读数据
   memcpy(pmydev->mydev_buf,pmydev->mydev_buf + size,pmydev->curlen - size);
   pmydev->curlen -= size;
   return size;
}
//写数据对于应用层来说 把数据写进驱动
/*
ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
完成功能:向设备写入数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
    pbuf:指向用户空间一块内存,用来保存被写的数据
    count:用户期望写入的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
 返回值:
    本次成功写入的字节数,失败返回-1
*/
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
   int size = 0;
   int ret = 0;
   struct mychar_dev *pmydev = (struct mychar_dev *) pfile->private_data;
   //如果剩余数组长度小于要写入的数据长度 则把要写入的数据修改 
   if(count > BUF_LEN - pmydev->curlen)
   {
      size = BUF_LEN - pmydev->curlen;
   }else
   {
      size = count;
   }
   //从用户空间拷贝数据到内核空间
   ret = copy_from_user(pmydev->mydev_buf + pmydev->curlen,puser,size);
   if(ret)
   {
      printk("copy_from_user failed\n");
      return -1;
   }
   pmydev->curlen += size;
   return size;
}
//设备属性控制函数
long mychar_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg)
{
   int  __user *pret = (int *)arg;
   int maxlen = BUF_LEN;
   int ret = 0;
   struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
   switch(cmd)
   {
      case MYCHAR_IOCTL_GET_MAXLEN:
         ret = copy_to_user(pret,&maxlen,sizeof(int));
         if(ret)
         {
            printk("copy_to_user MAXLEN failed\n");
            return -1;
         }
         break;
      case MYCHAR_IOCTL_GET_CURLEN:
         ret = copy_to_user(pret,&pmydev->curlen,sizeof(int));
         if(ret)
         {
            printk("copy_to_user CURLEN failed\n");
            return -1;
         }
         break;
      default:
         printk("cmd is unknow\n");
         return -1;
   }
   return 0;
}
//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{
   .owner=THIS_MODULE,
   .open=mychar_open,
   .release=mychar_close,
   .read=mychar_read,
   .write=mychar_write,
   .unlocked_ioctl=mychar_ioctl,
};

int __init mychar_init(void)
{
   //用来接收注册设备函数的返回值
   int ret = 0;
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   /*1.申请设备号*/
   //静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
   //ret = register_chrdev_region(devno,mychar_num,"mychar");
   ret = register_chrdev_region(devno,mychar_num,mycharname);
   //分配失败返回负数
   if(ret)
   {
      //动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
      //ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");
      ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);
      if(ret)
      {
         printk("get devno failed\n");
         return -1;
      }
      //如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号
      major = MAJOR(devno);
   }
   /*2.将struct cdev对象添加到内核对应的数据结构里*/
   //初始化cdev 给struct cdev对象指定操作函数集
   cdev_init(&gmydev.mydev,&myops);
   gmydev.mydev.owner=THIS_MODULE;
   //将字符设备添加进内核 哈希管理列表
   cdev_add(&gmydev.mydev,devno,1);
   
 
   return 0;
}
   
void __exit mychar_exit(void)
{
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   //注销字符设备驱动 入口参数:设备号 设备名称
   //unregister_chrdev(devno,"mychar");
   //将字符设备从内核移除
   cdev_del(&gmydev.mydev);
   unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);



你可能感兴趣的:(嵌入式驱动,linux,驱动开发)