0 目录

  • 1 前言
  • 2 驱动开发认识
    • 2.1 驱动
      • 2.1.1 设备驱动程序的主要功能
      • 2.1.2 驱动程序的主要类型
      • 2.1.3 设备文件
      • 2.1.4 sys文件系统:
  • 3 基础编程
    • 3.1 内核模块
      • 3.1.1 设备驱动的编译和加载方式
      • 3.1.2 一个模块被插入时的主要工作
    • 3.2 内核编程
      • 3.2.1 内核模块编程模板
      • 3.3 字符驱动程序模板
  • 4 总结

1 前言

已经有段时间没好好地写博客了,最近在研究安卓底层,所以想写写我对安卓底层的认识和总结。本篇是安卓底层学习总结系列的第一篇,驱动开发基础。

2 驱动开发认识

安卓系统,想必我也不用作太多介绍,这里我要提及的是安卓系统和嵌入式系统十分接近,所编写的驱动程序实际上大多也可以认为是嵌入式驱动程序。并且安卓的内核是Linux,所以写安卓驱动程序实际上和写Linux内核模块差不多,我门这篇主要认识PC中的Linux驱动。

2.1 驱动

所谓驱动,就是内核与外部设备的媒介,下面介绍有关驱动需要知道的知识。

2.1.1 设备驱动程序的主要功能

  • 对设备初始化和释放
  • 内核与硬件的数据交互
  • 应用程序和硬件的数据交互
  • 硬件的错误检测

2.1.2 驱动程序的主要类型

  • 字符设备
    – 使用自己制定的数据大小,通常以字节为单位输入输出
  • 块设备
    – 以块为单位输入输出
    – 对块设备读写时,利用系统内存作缓冲区,当用户进程对设备请求能满足用户的要求就返回请求的数据
  • 网络设备

2.1.3 设备文件

在shell中查看这个目录

ls -l /dev

可以看到所有的设备文件节点,通常为以下格式

crw-r--r--  1 root    root       10,   235 3月  29 08:12 autofs
  • 文件类型
    – 上面格式的第一个字符c代表了这个设备文件的文件类型为字符设备,b就是块设备,网络设备没有设备文件

  • 主设备号
    – 设备类型和主设备号唯一确定设备文件的驱动程序和界面。在上述格式中10, 235的10就是代表了主设备号。

  • 次设备号
    – 说明目标设备是同类设备的第几个,在上述格式中10, 235的235就是代表了次设备号。
    例:
    crw------- 1 root root 10, 59 3月 29 08:12 cpu_dma_latency
    crw------- 1 root root 10, 203 3月 29 08:12 cuse

    上面两个字符设备同属于一种设备,但不是一个设备。

2.1.4 sys文件系统:

统一管理查看内核功能参数和设备模型

/sys/block # 所有块设备
/sys/bus # 按总线类型分层放置的目录结构
/sys/class # 按设备功能放置
/sys/class/mem # mem目录包含各个设备的链接,指向devices各个具体设备
/sys/devices # 分层次放置
/sys/dev # 字符设备和块设备的主次号
/sys/fs # 描述所有文件系统
/sys/kernel # 内核所有可调整参数位置
/sys/module # 所有模块信息
/sys/power # 系统电源选项

3 基础编程

驱动程序通常是以内核模块的方式编写,并且插入到系统内核进行执行,所以我们得先了解什么是内核模块。

3.1 内核模块

Linux是一个单体内核系统,分成5个子系统,整个内核在一个地址空间。Linux提供了模块机制,来为其增加设备;只需编译模块,再插入内核就可以完成设备增加。而内核模块就是可以在系统运行期间动态安装和拆卸的内核功能单元。

3.1.1 设备驱动的编译和加载方式

  • 直接编译进内核,随同Linux启动时加载。
  • 编译成可加载删除模块,insmod加载,rmmod删除

3.1.2 一个模块被插入时的主要工作

  1. 打开要安装的模块(·ko文件),读进用户空间。
  2. 链接其他函数到内核。即把外部函数的地址填入访问指令和数据结构中
  3. 在内核创建module数据结构,申请系统空间
  4. 将完成链接的模块映像装入内核空间,并在内核登记模块相关的数据结构(里面有相关操作的函数指针)

3.2 内核编程

要编写一个内核模块就要先了解一下基本函数。
首先,内核与用户之间数据是不互通的,要互相使用数据得经过系统调用,系统调用中有着一些基本函数,用来完成基本任务。
比如:

- copy_to_user主要用于将内核段中的数据拷贝到用户段的内存中去
- copy_from_user主要用于将用户段内存中的数据拷贝到内核中

这些函数在用户态是无法使用的,也就是说,在外部写的.c程序库中是不包含这两个函数的。所以编写内核程序是与编写普通c程序是有所区别的。

3.2.1 内核模块编程模板

下面贴出一个简单的helloworld内核程序,我们在具体程序中进行解释。

#include<linux/init.h>  // 定义了module_init等函数
#include<linux/module.h> // 最基本的头文件,其中定义了MODULE_LICENSE等宏// 当插入内核模块时,系统将调用下面的module_init宏,然后通过module_init调用此函数
static int hello_init(void){/***printk在函数内部,有代码申请了一块静态缓冲区,当与控制台建立连接时,将缓冲区打印到终端*注意:它不支持浮点数,记得打印时+\n,不然的话不会立即打印,打印级别数字越小级别越高*KERN_CRIT表示 critical conditions级别的调试级别,级别数字为2**/printk(KERN_CRIT "HELLO WORLD!!!\n"); // \n用处很大,最好不要省return 0;
}
// 与hello_init对应,在移除该内核模块时调用module_exit宏,然后调用此函数
static void hello_exit(void){// KERN_WARNING级别数字为4printk(KERN_WARNING "bye bye!!\n");return;
}
// 下面都是宏,在加载卸载模块时调用
module_init(hello_init);
module_exit(hello_exit);// 下面的内容是必须的,用于表明该模块的信息,用modinfo *.ko即可查看
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alexander");
MODULE_DESCRIPTION("一个简单的内核模块测试");

接下来编写Makefile文件,具体请自行查看资料

obj-m:=hello_module.o
PWD:=$(shell pwd)
default:$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

下面,我们对其进行测试,在shell中输入以下命令

 makesudo insmod hello_module.kodmesg

即可查看信息,其中:
insmod用于插入内核模块。
dmesg用于打印内核日志信息。
sudo dmesg -C可以清空日志信息。
modinfo *.ko查看模块信息。
sudo rmmod hello_module卸载模块。

最后我的内核日志打印信息为:

[ 9806.210068] HELLO WORLD!!!
[10004.819841] perf: interrupt took too long (3137 > 3130), lowering kernel.perf_event_max_sample_rate to 63750
[10097.027480] bye bye!!

至此我们完成了一个简单的内核模块编程模板。

3.3 字符驱动程序模板

上面我们已经简单介绍了内核模块编写,下面我们来正式写一个有基本输入输出和基本测试程序的字符驱动程序模板。

#include<linux/init.h> // 定义了module_init
#include<linux/module.h> // 最基本的头文件,其中定义了MODULE_LICENSE等宏
#include<linux/fs.h>  // file_operations结构体所在static const char *dev_name = "first_cdev"; // 设置设备名,之后可以在/proc/devices中查看该设备
static unsigned int major = 55; // 设置主设备号/* open函数,用于打开设备文件
* 注:在linux中,一切皆文件,驱动设备文件也不例外,只不过设备文件是一种
* 特殊的文件,而对驱动程序的操作其实也是基于文件操作的。
*/
static int first_cdev_open(struct inode *inode, struct file *file){printk("open\n");return 0;
}// 必须关闭设备文件
static int first_cdev_close(struct inode *inode, struct file *file)
{printk(KERN_DEBUG "close\n");return 0;
}// 读取设备文件
static ssize_t first_cdev_read(struct file *file, char *buf,size_t count, loff_t *offset)
{printk(KERN_DEBUG "read :%ld", count);if(count >= sizeof(unsigned int)){ // 如果读到了来自内核的数据// 复制数据到用户程序进行输出if(copy_to_user((void __user *)buf, (void *)(&file->private_data), sizeof(unsigned int))) return -EFAULT;}return count;
}// ioctl操作,主要用于对驱动设备进行命令控制
// 被注释的这种方法已经被废弃static int first_cdev_ioctl(struct inode *inode, struct file *file,
/*
注意:在2.6.36以后ioctl函数已经不存在了,用unlocked_ioctl和compat_ioctl两个函数代替。参数去除了原来ioctl中的struct inode参数,返回值也发生了改变。
1、compat_ioctl:支持64bit的driver必须要实现的ioctl,当有32bit的用户程序调用64bit内核的ioctl的时候,这个callback会被调用到。如果没有实现compat_ioctl,那么32位的用户程序在64位的kernel上执行ioctl时会返回错误:Not a typewriter
2、如果是64位的用户程序运行在64位的kernel上,调用的是unlocked_ioctl,如果是32位的APP运行在32位的kernel上,调用的也是unlocked_ioctl
*/
static long first_cdev_ioctl(struct file *file,unsigned int cmd, unsigned long arg)
{char argk[4]; // 定义一个字符数组,存放一些字符argk[0] = 0;argk[1] = 1;argk[2] = 2;argk[3] = 3;printk(KERN_DEBUG "ioctl:%x\n", cmd);switch(cmd){ // 根据传来的命令指示进行操作case 0: // 指令 0printk(KERN_DEBUG "ctl NO.0\n");// 将用户态程序的数据覆盖本地定义的字符数组,并打印从用户态程序获取的数据if(copy_from_user(argk, (void __user *)arg, 4))return -EFAULT;printk("arg:%x,%x,%x,%x\n", argk[0], argk[1], argk[2], argk[3]);break;case 1: // 指令 1printk(KERN_DEBUG "ctl NO.1\n");// 将数据传入用户态应用程序if(copy_to_user((void __user *)arg, argk, 4))return -EFAULT;break;default:break; }return 0;
}// write函数,当向内核程序写数据时调用
static ssize_t first_cdev_write(struct file *file,const char __user *buf, size_t size, loff_t *ppos){printk("write\n");return 0;
}// 在file_operations中注册open和write等函数
static struct file_operations first_cdev_fo = {.owner = THIS_MODULE,.open = first_cdev_open,.release = first_cdev_close,.read = first_cdev_read,// .ioctl= first_cdev_ioctl,.unlocked_ioctl = first_cdev_ioctl,.write = first_cdev_write,
};// 插入模块时调用
static int first_cdev_init(void){// 注册设备,将file_operations结构体放到内核的特定数组中// major作为主设备号int res;// 注册设备res = register_chrdev(major, dev_name, &first_cdev_fo);if(res < 0){printk(KERN_DEBUG "register fail\n");return res;}//if(dev_id < 0){//  printk("error\n");//} printk(KERN_CRIT "hello character devices!!\n");return 0;
}// 卸载模块时调用
static void first_cdev_exit(void){// 注销设备unregister_chrdev(major, dev_name);printk(KERN_INFO "bye,character devices\n");return;
}module_init(first_cdev_init);
module_exit(first_cdev_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("alexander");
MODULE_DESCRIPTION("第一个字符驱动模块编写");

下面是测试程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>// 要调用的设备文件名
#define DEF_FILE_NAME "/dev/xxx" int main(int argc, char **argv)
{int fd, size;char readbuf[8];char writebuf[8] = "writebuf";char ioarg[4];char *dev_file;if(1 == argc){ // 从命令行获取还是使用本地定义的设备文件名dev_file = DEF_FILE_NAME;} else {dev_file = argv[1];}printf("<<<<<<test file name:%s>>>>>\n", dev_file);printf("test write:\n");fd = open("/dev/xxx", O_RDWR); // 以读写方式打开设备文件if(fd < 0){printf("can't open device\n");}size = write(fd, writebuf, sizeof(writebuf)); // 向设备文件写入数据close(fd); // 关闭设备文件printf("test read:\n");fd = open(dev_file, O_RDONLY); // 以只读方式打开设备文件size = read(fd, readbuf, sizeof(readbuf)); // 从设备文件读取数据// close(fd)printf("read size:%d\n",size);for(int i=0; i<size; i++){printf("readbuf[%d]:%x\n", i, (unsigned char)readbuf[i]);}close(fd);printf("test ioctl:\n");fd = open(dev_file, O_RDWR); // 以读写方式打开设备文件// 设置初始数组数据ioarg[0] = 0xf0;ioarg[1] = 0xf1;ioarg[2] = 0xf2;ioarg[3] = 0xf3;printf("ioctl test 0\n");ioctl(fd, 0, ioarg); // 执行0号命令,将数组写入设备文件printf("ioctl test 1\n");ioctl(fd, 1, ioarg); // 执行1号命令,从设备文件读printf("arg:%x, %x, %x, %x\n", ioarg[0], ioarg[1], ioarg[2], ioarg[3]);close(fd);return 0;
}

Makefile文件

obj-m:=first_cdev.o
PWD:=$(shell pwd)
default:$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

下面进行测试

make
gcc test.c -o test
sudo mknod /dev/xxx c(设备类型) 55(主设备号) 0(次设备号)
sudo insmod *.ko
./test

注:mknod 用来创建设备文件,指定设备文件主设备号为55,在制定前最好用 ls -l /dev 查看是否主设备号重复.
执行完insmod命令就可以用cat /proc/devices查看设备是否加载成功了。
接下来我们运行测试程序,以下是输出内容

<<<<<<test file name:/dev/xxx>>>>>
test write:
can't open devive
test read:
read size:8
readbuf[0]:0
readbuf[1]:0
readbuf[2]:0
readbuf[3]:0
readbuf[4]:9d
readbuf[5]:55
readbuf[6]:0
readbuf[7]:0
test ioctl:
ioctl test 0
ioctl test 1
arg:fffffff0, fffffff1, fffffff2, fffffff3

dmesg查看内核日志

[11955.133491] hello character devices!!
[11960.960142] open
[11960.960148] read :8
[11960.960205] close

最后,卸载驱动程序

sudo rmmod first_cdev
sudo rm /dev/xxx

至此,我们完成了一个有输入输出功能的字符设备驱动程序模板。

4 总结

总的来说,编写驱动程序并不难,但驱动程序主要与硬件相关,编写具体的驱动会需要特定硬件的芯片手册,所以以上只是Linux的驱动程序基础,编写驱动程序还需要进一步学习,比如学习系统的启动、设备树、硬件引脚等概念,学完后希望能在安卓开发板子上动手实践,下篇文章,我将对系统启动流程进行总结介绍。

本系列链接传送:
【Android底层学习总结】2. 安卓系统内核的启动
【Android底层学习总结】3. 内核中driver_init函数源码解析

【Android底层学习总结】1. 驱动开发基础相关推荐

  1. Android深度探索--HAL与驱动开发----第五章读书笔记

    第五章主要学习了搭建S3C6410开发板的测试环境.首先要了解到S3C6410是一款低功耗.高性价比的RISC处理器它是基于ARMI1内核,广泛应用于移动电话和通用处理等领域. 开发板从技术上说与我们 ...

  2. 【Android底层学习总结】2. 安卓系统内核的Bring Up

    0 目录 1 前言 2 简介 3 启动流程 3.1 上电 3.2 Boot Loader 3.3 Kernel的初始化 4 总结 1 前言 上节我们学习了驱动开发基础,这节我们继续学习,这节我们主要来 ...

  3. Android深度探索--HAL与驱动开发----第一章读书笔记

    1.1   Android拥有非常完善的系统构架可以分为四层: 第一层:Linux内核.主要包括驱动程序以及管理内存.进程.电源等资源的程序 第二层:C/C++代码库.主要包括Linux的.so文件以 ...

  4. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED驱动框架--面向对象、分层设计思想

    文章目录 前言 1.LED驱动程序框架 1.1.对于LED驱动,我们想要什么样的接口? 1.2.LED驱动要怎么写,才能支持多个板子?分层写 1.3.程序分析 驱动程序 应用程序 Makefile 1 ...

  5. 驱动开发基础知识——设备树

    BSP开发工程师[原来BSP就是那些被指臃肿的文件啊 BSP的出生 Linux经过不断的发展,原先嵌入式系统的三层结构逐步演化成为一种四层结构. 这个新增加的中间层次位于操作系统和硬件之间,包含了系统 ...

  6. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之Pinctrl子系统和GPIO子系统的使用

    文章目录 前言 1.Pinctrl子系统 1.1.为什么有Pinctrl子系统 1.2.重要的概念 1.3.代码中怎么引用pinctrl 2.GPIO子系统 2.1.为什么有GPIO子系统 2.2.在 ...

  7. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之按键驱动框架

    文章目录 前言 1.APP怎么读取按键值 1.1.查询方式 1.2.休眠-唤醒方式 1.3.poll方式 1.3.异步通知方式 1.5. 驱动程序提供能力,不提供策略 2.按键驱动程序框架--查询方式 ...

  8. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED模板驱动程序的改造:设备树

    文章目录 前言 1.驱动的三种编写方法 2.怎么使用设备树写驱动程序 2.1.设备树节点要与platform_driver能匹配 2.2.修改platform_driver的源码 3.实验和调试技巧 ...

  9. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之设备树模型

    文章目录 前言 1.设备树的作用 2.设备树的语法 2.1.设备树的逻辑图和dts文件.dtb文件 2.1.1.1Devicetree格式 1DTS文件的格式 node的格式 properties的格 ...

最新文章

  1. JS正则表达式验证数字
  2. 深入分析Linux内核源码oss.org.cn/kernel-book/
  3. ABAP 引用类型介绍
  4. day03 基本数据类型
  5. 12306加密传输_三大运营商发5G消息白皮书:短消息服务升级,支持加密传输
  6. python用什么来写模块-史上最详细的python模块讲解
  7. mir2disease:miRNA相关疾病数据库
  8. 便宜的前端培训班都有哪些?
  9. FusionSphere 物理CPU与VCPU的关系梳理总结
  10. 联想拯救者Y7000P2020 RTX2060显卡 AX201网卡 安装Ubuntu16.04采坑记录
  11. SQL Server 无法启动WMI服务
  12. EGO Planner代码解析bspline_optimizer部分(3)
  13. AI公开课:03月26日未来十年 AI如何进化—圆桌探讨(乌镇智库理事长、CSDN 创始人董事长、智源人工智能研究院副院长)之《AI:昨天 · 今天 · 明天》
  14. 《网络神采4》技术大揭密之:DedeCMS存储过程
  15. Solana 区块链数据抓取
  16. 如何结交阿里P9,腾讯T4这样的大佬?
  17. JAVA垃圾收集器之Parallel Scavenge收集器
  18. 阿里云服务器docker安装网心云容器魔方
  19. 神经网络可以解决的问题,人工神经网络通过调整
  20. 快递单打印专家 免费

热门文章

  1. 图像迁移风格保存模型_一种图像风格迁移方法与流程
  2. 招标采购评标专家管理数智化解决方案
  3. flash初学(转)
  4. 《The Wiley Handbook of Human Computer Interaction》Part V Input / Output 以身体为中心的听觉反馈设计原则 翻译
  5. 圣戈班发布全新本地化生物工艺袋产品
  6. MIT 计算机操作环境导论Missing Semester Lesson 9 安全和密码学
  7. 如何在区块链领域用技术赚钱
  8. ubuntu 18.04 安装NVIDIA 显卡驱动
  9. thinkpad x61-lg2装XP
  10. #(四)、(五)拟合数学方法的发展简介