先看测试代码,目的打开树莓派/dev/pin4模块,向其中写入一个整型数。
测试程序 pin4test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<stdio.h>
int main()
{
int data=0;
int fd;
fd=open("/dev/pin4",O_RDWR);
if(fd<0){
printf("opne pin4 defeat!\n");
}
else{
printf("opne success!\n");
}
printf("please input a int num:\n");
scanf("%d",&data);
printf("data=%d\n",data);
write(fd,&data,sizeof(data));
return 0;
}
编译测试程序: gcc pin4test.c -o pin4test
直接运行测试程序./pin4test一定是不会成功的,因为/dev下根本就没有pin4这个模块,因此我们接下来的工作就是让dev下产生pin4这个模块。
首先我们需要根据驱动框架编写好自己的驱动代码,代码如下。
这个驱动是作用数将树莓派的第四个引脚配置成输出模式,并根据上层传递过来的整型数来控制输出高低电平。传入1输出高电平,传入0输出低电平。
驱动框架pin4driver.c
#include <linux/fs.h> // 声明了文件操作相关的函数,比如文件打开,读写,关闭等
#include <linux/module.h> // 声明模块初始化和退出相关的函数
#include <linux/init.h> // 声明了初始化相关的宏,比如__init和__exit
#include <linux/device.h> // 设备相关的结构体和函数
#include <linux/uaccess.h> // 用户空间和内核空间数据交换相关的函数
#include <linux/types.h> // 定义了设备号等数据类型
#include <asm/io.h> // IO内存映射相关的函数
// 定义了一个结构体指针变量pin4_class,用于表示设备所属的类别
static struct class *pin4_class;
// 定义了一个设备结构体指针变量pin4_class_dev,用于表示设备本身
static struct device *pin4_class_dev;
// 定义了一个设备号变量devno
static dev_t devno; //设备号
// 定义了主设备号major
static int major =231; //主设备号
// 定义了次设备号minor
static int minor =0; //次设备号
// 定义了一个字符指针变量module_name,用于存储模块名“pin4”
static char *module_name="pin4"; //模块名
volatile unsigned int *GPFSET0=NULL;//根据手册 GPFSET0寄存器 用来选择引脚
volatile unsigned int *GPSET0 =NULL;//根据手册 GPFSET0寄存器 用来置高电平
volatile unsigned int *GPCLR0 =NULL;//根据手册 GPFSET0寄存器 用来清0
// 参数inode代表打开的文件,file代表打开文件对应的file结构体
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
//根据手册把GPFSET0第12~14位配置成001可以将树莓派第四个引脚配置成输出模式(output)
*GPFSET0 &= ~(0x6<<12); //将二进制数0110左移十二位取反,然后与等于,将寄存器第13、14位置0
*GPFSET0 |=(0x1<<12);//根让寄存器的第12位置1
return 0;
}
// 参数file代表写入的文件,buf是写入的数据缓冲区,count是写入的字节数,ppos是写入的文件位置指针
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;
printk("pin4_write\n");
copy_from_user(&userCmd,buf,count);//内核编程中常用的函数,它的主要作用是从用户空间复制数据到内核空间
if(userCmd==1){
printk("set 1\n");
*GPSET0 |=0x01<<4; //将树莓派第四个引脚置为高电平
}
else if(userCmd==0){
printk("set 0\n");
*GPCLR0 |=0x01<<4; //将树莓派第四个引脚置为低电平
}
else{
printk("undo\n");
}
return 0;
}
// 定义了一个文件操作结构体变量pin4_fops,用于存储文件操作函数
static struct file_operations pin4_fops = {
.owner = THIS_MODULE, // 表示文件操作的所有者是当前模块
.open = pin4_open, // 表示文件打开时调用的函数是pin4_open
.write = pin4_write, // 表示文件写入时调用的函数是pin4_write
};
// pin4_drv_init函数的函数声明,这个函数是模块的初始化函数
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major,minor); // 使用MKDEV宏生成设备号,这个宏在types.h中定义,主设备号和次设备号相加得到设备号devno
ret = register_chrdev(major, module_name,&pin4_fops); // 使用register_chrdev函数注册字符设备驱动,告诉内核这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo"); // 使用class_create函数创建设备类别,类别名是"myfirstdemo"
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); // 使用device_create函数创建设备文件,参数包括类别、父设备、设备号、名称等
GPFSET0=(volatile unsigned int *)ioremap(0x3f200000,4);//将物理地址映射成虚拟地址,内核和上层空间都是访问虚拟地址的
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001c,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
return 0;
}
// pin4_drv_exit函数的函数声明,这个函数是模块的退出函数
void __exit pin4_drv_exit(void)
{
iounmap(GPFSET0);//将之前通过ioremap函数映射的IO地址空间解除映射
iounmap(GPSET0);
iounmap(GPCLR0);
device_destroy(pin4_class,devno); // 使用device_destroy函数销毁创建设备文件
class_destroy(pin4_class); // 使用class_destroy函数销毁创建的设备类别
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口宏
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
将这个代码在虚拟机上拷贝到linux内核的/drivers/char字符驱动目录下,为了使该驱动文件能够被编译到,这里需要修改Makefile,Makefile文件的意义在于自动化构建项目,也就是为了更简便地执行程序,Makefile文件描述了整个工程所有文件的编译顺序、编译规则,它存在于项目的根目录下,并且可以在项目的任何子目录中存在,但通常在项目的根目录下创建或维护。
使用以下命令编辑Makefile,模仿其他的文件加入下面的语句,文件的后缀名改为.0,前面的-m表示以模块的方式加载到内核。
vi Makefile
然后回到内核目录下:编译内核模块。
还记的编译内核的代码吗,如下所示。这里只需要编译modules即可,因此使用如下第一个命令即可。
使用这个:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
"ARCH": "定义目标体系结构,这里是arm,表示你正在为基于ARM的处理器构建内核。",
"CROSS_COMPILE": "定义交叉编译器的前缀,这里是指用于ARM架构的GNU编译器的前缀,包括编译器、链接器和其它工具。它的形式通常是 'arm-linux-gnueabihf-'.",
"KERNEL": "定义内核的版本或者名称,这里是kernel7。",
"make -j4": "使用make命令并且并行编译的数量为4,这可以大大提高编译速度。'-j'参数代表并行编译,后面的数字4代表同时编译4个文件。",
"zImage": "这是编译的内核镜像文件的名称,'zImage'是压缩内核镜像的简称。",
"modules": "这部分是编译所有的内核模块。",
"dtbs": "这个部分是编译设备树源文件(.dts)并生成设备树二进制文件(.dtb)和设备树自解压文件(.dtbs)。设备树描述了硬件的配置信息。"
编译通过以后会在linux-rpi-4.14.y/drivers/char路径下生成pin4driver.ko文件。
然后将这个文件传到树莓派的工作目录下/home/pi。
接着加载驱动,命令如下:
sudo insmod pin4driver.ko
命令执行完成后可以执行lsmod命令,它用于列出已加载的内核模块 这里可以看到pin4driver已经被加载进去了。
同时树莓派/dev目录下会多一个 pin4 的文件
名字的来源为字符驱动框架程序中的如下语句
static char *module_name="pin4"; //模块名
使用命令 ls pin4 -l可以查看文件的详细信息,231为主设备号,0为从设备号。生成的主次设备号由在驱动代码的变量major minor决定的。
既然已经在/dev目录下生成了pin4模块,那么这里就可以执行测试程序pin4test了。
这里看到运行 ./pin4test运行结果是打开失败,是因为加载驱动的时候使用的是超级用户权限,导致普通用户没有pin4的权限,所以这里需要使用超级用户权限改变/dev/pin4的权限,命令如下
sudo chmod 666/dev/pin4
然后在执行./pin4test就会正常运行。
使用dmesg命令可以打印内核的状态,内核成功执行了驱动中的 pin4_open函数,成功打印了pin4_open。