【Leo的手记】Linux设备驱动程序手记1+2

注:笔记用
想要编写Linux设备驱动程序需要先准备好对应版本的Linux源码。如果想要进行交叉编译还需要准备对应的交叉编译工具链。注意的是,编译驱动的交叉编译工具链应该尽可能地和编译内核所使用的相同。
1.声明驱动程序入口与出口
每一个驱动程序(内核模块)在加载和卸载时都需要做一些对应的操作,这一系列操作被封装为入口函数与出口函数。其中,对于Linux内核模块,其入口与出口函数通过module_init和module_exit宏所指定。
其中入口函数的函数原型为
出口函数原型为
2.GPL协议声明
Linux内核采用GPL协议,故要求所有的内核模块源码都应该使用GPL协议进行开源。
因此必须要在Linux驱动源码中添加GPL协议声明。如下
3.为字符设备提供文件IO方法
对于Linux字符设备而言,其最常见的抽象方法便是对外抽象为一个设备文件,通过对设备文件的open,close,read,write,ioctl等方法对设备进行直接操作。因此我们需要分别实现其对应的接口。
在Linux内核源码中对于文件操作的封装是一个名为file_operations的结构体。
因此需要声明一个对应的包含了文件操作方法的函数指针的结构体。如下
在实现了对应的读写接口之后,便可以在入口中向内核注册对应的字符设备。
4.注册字符设备
在Linux中,对于一个设备而言,其需要一个主设备号和从设备号。该设备号可以指定也可以传入0通过系统分配。在此我们通过系统自动分配。
5.注销字符设备
在模块卸载的时候需要释放资源,因此最好在驱动卸载时注销字符设备。如下
6.Makefile
内核模块需要借助Linux内核源码进行编译。因此需要编写Makefile去调用内核源码的Makefile,如下
对于交叉编译,在make时需要指定ARCH和CROSS_COMPILE,同内核编译
7.用户空间与内核空间
对于Linux驱动程序而言,其作为内核模块运行于内核空间中,因此不能与用户空间地址进行直接的操作。在进行数据拷贝的操作中,需要使用copy_from_user和copy_to_user方法进行操作。
另外在内核模块中进行输出,不能使用用户态的printf等函数,内核提供了printk函数用于输出系统日志,可以在系统中通过dmesg命令进行查看。
8.映射物理地址
编写一个驱动程序,用于控制某个GPIO引脚。其他的部分和一般的字符设备驱动相同,实现文件操作接口,注册字符设备,注册设备类以及设备文件。但是对于硬件外设而言,通常需要通过控制对应的外设寄存器来实现外设的控制。外设寄存器以官方给定的物理地址存在于系统的物理内存空间内。因此想要操作对应的寄存器需要映射对应的物理内存地址到虚拟内存空间。
8.1 映射Allwinner F1c100s的GPIOA的寄存器
查看F1c100s的官方手册可以得知,对于GPIOA的操作,不考虑中断的前提下仅需要操作两个寄存器,即PA Configure Register0和PA Data Register,分别对应地址0x01c20800和0x01c20810。
在程序中声明全局静态变量PACFG0与PADAT,类型为32位无符号整型指针
在内核模块入口函数中,使用ioremap对地址进行映射
ioremap函数原型为
其中offset为物理地址偏移,size为映射的大小。
映射F1c100s PIOA相关寄存器的代码如下。
9.初始化GPIO
假定要实现GPIO输出的控制,需要配置GPIO的模式为Out模式,见手册

如要操作PA0,仅需将PACFG0的低三位配置为0b001即可。在入口函数中加入
同样在出口函数中也要对其进行对应的配置,可以在入口中保存其初始值并在出口中恢复,也可以直接关闭,在此选择直接关闭。
10.GPIO的读写
GPIO的读写仅需要对其数据寄存器的对应位进行操作即可。
附录A. 驱动程序代码
附录B. 测试程序代码