目录
一. Linux内核
用户级别(User Level): 用户级别是操作系统中最高的级别,也称为用户空间。在用户级别中,运行着应用程序和用户空间的库。用户级别的代码通常是以用户身份运行的,没有直接访问系统资源或硬件设备的权限。应用程序通过系统调用接口与内核进行通信来请求操作系统提供的服务和资源。在用户级别,应用程序可以执行一般的计算任务和逻辑操作。
内核级别(Kernel Level): 内核级别位于用户级别和硬件级别之间。它包含了操作系统的内核空间,也称为内核态。在内核级别中,运行着操作系统的内核代码,负责管理系统资源、提供各种系统服务,并执行系统调用的实际操作。内核级别具有更高的特权,可以直接访问和控制硬件设备,以及处理中断和异常。内核级别的代码处理底层的任务,如进程管理、内存管理、文件系统、网络协议栈等。
硬件级别(Hardware Level): 硬件级别是操作系统中最低的级别,它包含了计算机的物理硬件。硬件级别包括处理器、内存、输入输出设备等。在硬件级别上,真正执行指令和操作的是处理器和其他硬件设备。操作系统通过内核级别与硬件交互,管理硬件资源并提供服务。
1.1 系统调用接口
系统调用是Linux内核提供给用户空间应用程序的接口,它允许应用程序请求操作系统提供的服务和资源。通过系统调用,上层的应用程序可以与底层的内核进行通信,从而实现软硬件的对话。
系统调用接口隐藏了底层的复杂性,使得上层应用程序可以独立于具体的硬件平台。这为开发者提供了更高的可移植性,他们可以编写与操作系统无关的代码,并在不同的硬件上运行。这种分离还使得应用程序的开发更加简化,因为开发者可以专注于应用程序的逻辑,而不必过多考虑底层的细节。
此外,库函数也是通过系统调用来实现模块化的功能。库函数是一组封装了常用操作的函数集合,它们提供了高级的抽象层,使得开发者可以更方便地使用一些常见的功能,如文件操作、网络通信等。库函数通过封装系统调用,提供了更友好和易用的接口,使得开发者能够更快速地开发应用程序。
Shell作为Linux的命令行解释器,提供了一个用户界面,允许用户与操作系统进行交互。通过Shell,用户可以通过命令行输入指令来执行各种操作,包括调用应用程序、执行脚本等。脚本是一系列的Shell命令的集合,它们可以被组合成一个可执行的程序,以实现复杂的操作和自动化任务。
总的来说,Linux利用内核实现了软硬件的对话,通过系统调用接口将上层的应用程序与底层的内核分离,同时库函数和Shell提供了更高级别的抽象和用户界面,使得开发者和用户能够更方便地使用和控制系统。
1.2 用户态与内核态的交互
在用户空间,应用程序通过调用C库提供的API来与操作系统进行交互。这些API可以执行诸如打开文件、读取数据、写入数据、创建进程、创建线程和建立网络连接等操作。C库是操作系统的标准库,因此只要运行的是Linux系统,就可以使用C库。
然而,在某些情况下,特定平台的厂商可能没有提供所需的库,比如wiringPi库。这时候,我们就需要自己实现这个库,以便在应用层调用驱动。
在内核空间,Linux遵循一切皆文件的原则。当用户空间调用open函数时,会触发一个软中断,软中断号为0x80,表示发生了系统调用。系统调用会进入内核空间的sys_call,并调用虚拟文件系统的sys_open函数。接着,内核会根据被调用的驱动文件,在驱动链表中查找相应的驱动。驱动文件中包含设备名和设备号等信息。通过设备驱动函数,可以操作寄存器来驱动I/O口进行相关的操作。
总结起来,用户空间通过调用C库的API与内核进行交互,而内核空间负责处理系统调用并操作驱动来完成具体的任务。
二. Linux驱动
Linux驱动是用于在Linux操作系统中管理和控制硬件设备的软件模块。它们负责与硬件设备进行交互,提供统一的接口供用户空间应用程序调用,以实现对硬件设备的访问和操作。
Linux驱动可以分为两类:
字符设备驱动:用于管理字符设备,如串口、终端、键盘等。字符设备驱动通过提供读取和写入字符流的接口来与用户空间进行交互。它们通常使用字符设备文件(如/dev/tty)来表示设备,并通过系统调用(如open、read、write和close)来访问设备。
块设备驱动:用于管理块设备,如硬盘、闪存等。块设备驱动提供了对块级数据的读取和写入操作。块设备驱动通过提供块设备文件(如/dev/sda)来表示设备,并使用块设备接口(如读取和写入块数据)来与用户空间进行交互。
开发Linux驱动需要了解设备的硬件特性和寄存器的操作方式。驱动程序通常由设备的初始化、资源分配、中断处理、数据传输等部分组成。编写驱动程序时,需要遵循Linux内核的编程规范和API,如使用适当的数据结构、函数调用和内核接口。
此外,Linux还提供了许多其他类型的驱动程序,如网络设备驱动、USB设备驱动、声卡驱动等,用于管理和控制各种不同类型的硬件设备。
2.1 主设备号和次设备号
在Linux系统中,设备管理是通过文件系统来实现的,各种设备以文件的形式存在于/dev目录下,这些文件被称为设备文件。应用程序可以像操作普通的数据文件一样打开、关闭和读写这些设备文件,从而完成对设备的操作。
为了管理这些设备文件,系统为每个设备分配了一个设备号,设备号由主设备号和次设备号组成。 主设备号用于区分不同种类的设备,次设备号则用于区分同一类型的多个设备。
举个例子:
主设备号可以类似于Iphone这个品牌;次设备号可以类似于Iphone13,Iphone14。
主设备号用于标识特定的驱动程序,它指示了设备所使用的驱动程序或模块。不同的设备类型通常会有不同的主设备号,以便内核能够将设备请求正确地路由给相应的驱动程序。
次设备号用于区分同一类型的多个设备,它表示使用相同驱动程序的不同设备实例。
例如:
如果有两个LED指示灯,可以编写一个字符设备驱动程序来控制它们。可以将该驱动程序的主设备号注册为10号设备,并为每个LED灯分配不同的次设备号,例如1和2。这样,通过打开对应的设备文件并使用相应的次设备号,应用程序可以独立地控制每个LED灯的打开和关闭操作。
注意:
设备号的分配和注册是由驱动程序开发者负责的,开发者需要确保主设备号和次设备号的唯一性,并在驱动程序中正确处理和解析这些设备号。此外,设备号的分配也需要遵循一定的规范和约定,以便其他系统组件能够正确地识别和使用这些设备。
三. 编写适合树莓派4B的驱动代码
当用户空间程序执行打开设备的系统调用时,会触发软中断,进入内核态。在内核态中,虚拟文件系统(VFS)会解析设备名并调用相应的驱动程序来处理该设备的操作。
如果内核中没有对应的驱动程序,那么打开设备的系统调用就会失败并返回错误码。因此,为了能够成功地打开设备,需要在内核中添加相应的驱动程序,并将其注册到内核的驱动链表中。
在驱动程序中,需要实现设备操作函数,如读、写、控制等,以及初始化和清理函数。同时,需要定义设备号和设备名,并将其注册到内核中,以便用户空间程序可以通过设备名来打开设备。
3.1 手动创建设备节点
在Linux系统中,使用mknod命令可以创建设备节点。 sudo mknod blue c 222 33 查看设备节点 ls -l /dev/设备节点名称
3.2 设备驱动的基本代码框架
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/cdev.h> // 定义设备驱动的结构体 struct my_device_data { // 设备数据 }; // 定义设备文件操作函数 static int my_device_open(struct inode *inode, struct file *filp) { // 打开设备时的操作 } static int my_device_release(struct inode *inode, struct file *filp) { // 关闭设备时的操作 } static ssize_t my_device_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { // 读取设备数据的操作 } static ssize_t my_device_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { // 写入设备数据的操作 } // 定义设备文件操作结构体 static struct file_operations my_device_fops = { .owner = THIS_MODULE, .open = my_device_open, .release = my_device_release, .read = my_device_read, .write = my_device_write, }; // 定义设备驱动模块加载函数 static int __init my_device_init(void) { // 初始化设备驱动时的操作 // 注册字符设备驱动 if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) { printk(KERN_ALERT "Failed to allocate character device region\n"); return -1; } // 创建设备类 my_device_class = class_create(THIS_MODULE, "my_device_class"); if (IS_ERR(my_device_class)) { unregister_chrdev_region(dev, 1); printk(KERN_ALERT "Failed to create device class\n"); return PTR_ERR(my_device_class); } // 创建设备文件 my_device_device = device_create(my_device_class, NULL, dev, NULL, "my_device"); if (IS_ERR(my_device_device)) { class_destroy(my_device_class); unregister_chrdev_region(dev, 1); printk(KERN_ALERT "Failed to create device file\n"); return PTR_ERR(my_device_device); } // 注册设备文件操作 cdev_init(&my_device_cdev, &my_device_fops); if (cdev_add(&my_device_cdev, dev, 1) < 0) { device_destroy(my_device_class, dev); class_destroy(my_device_class); unregister_chrdev_region(dev, 1); printk(KERN_ALERT "Failed to add character device\n"); return -1; } return 0; } // 定义设备驱动模块卸载函数 static void __exit my_device_exit(void) { // 卸载设备驱动时的操作 // 移除设备文件操作 cdev_del(&my_device_cdev); // 销毁设备文件 device_destroy(my_device_class, dev); // 销毁设备类 class_destroy(my_device_class); // 注销字符设备驱动 unregister_chrdev_region(dev, 1); } // 注册设备驱动模块加载和卸载函数 module_init(my_device_init); module_exit(my_device_exit); MODULE_LICENSE("GPL"); //内核模块的发行许可证 MODULE_AUTHOR("Your Name"); //指定了模块的作者姓名 MODULE_DESCRIPTION("Sample Device Driver"); //提供了对模块的简要描述
四. 编写驱动代码及编译,测试
4.1 编写驱动树莓派4B的IO口的代码
#include <linux/fs.h> //file_operations声明 #include <linux/module.h> //module_init module_exit声明 #include <linux/init.h> //__init __exit 宏定义声明 #include <linux/device.h> //class devise声明 #include <linux/uaccess.h> //copy_from_user 的头文件 #include <linux/types.h> //设备号 dev_t 类型声明 #include <asm/io.h> //ioremap iounmap的头文件 // 定义class和device结构体指针变量 static struct class *BCM5_class; static struct device *BCM5_class_dev; static dev_t devno; // 定义设备号变量 static int major = 222; // 定义主设备号变量 static int minor = 0; // 定义次设备号变量 static char *module_name="BCM5"; // 定义字符指针变量,表示模块名 // 定义GPIO寄存器的指针变量 volatile unsigned int *GPFSEL0 = NULL; volatile unsigned int *GPSET0 = NULL; volatile unsigned int *GPCLR0 = NULL; // 定义BCM5_open函数,用于打开设备 static int BCM5_open(struct inode *inode,struct file *file) { // 内核的打印函数和printf类似 printk("BCM5_open\n"); // 配置pin5引脚位输出引脚,bit 15--17 配置成001 *GPFSEL0 &= ~(0x6 << 15);//把bit17 bit16配置成0 *GPFSEL0 |= (0x1 << 15);//把bit15配置成1 return 0; } // 定义BCM5_write函数,用于向设备写入数据 static ssize_t BCM5_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { char userCmd; printk("BCM5_write\n"); // 获取上层write函数的值 copy_from_user(&userCmd,buf,count); // 根据值来操作io口,高电平或者低电平 if(userCmd == 1){ printk("set 1\n"); *GPSET0 |= 0x1 << 5; }else if(userCmd == 0){ printk("set 0\n"); *GPCLR0 |= 0x1 << 5; }else { printk("undo\n"); } return 0; } // 定义file_operations结构体变量,用于注册驱动 static struct file_operations BCM5_fops = { .owner = THIS_MODULE, .open = BCM5_open, .write = BCM5_write, }; // 定义模块初始化函数,用于加载模块 int __init BCM5_drv_init(void) { printk("insmod drive BCM5 success\n"); int ret; // 将主设备号设置为222,次设备号设置为0,并将它们组合成一个设备号。 devno = MKDEV(major,minor); // 注册字符设备驱动程序,并将其添加到内核的字符设备驱动链表中 ret = register_chrdev(major, module_name,&BCM5_fops); // 创建一个新的设备类 BCM5_class=class_create(THIS_MODULE,"demo"); // 创建一个新的设备文件 BCM5_class_dev =device_create(BCM5_class,NULL,devno,NULL,module_name); //创建设备文件 // 将物理地址映射到内核虚拟地址空间中,以便内核可以访问设备的寄存器。 GPFSEL0 = (volatile unsigned int*)ioremap(0xfe200000,4); GPSET0 = (volatile unsigned int*)ioremap(0xfe20001c,4); GPCLR0 = (volatile unsigned int*)ioremap(0xfe200028,4); return 0; } // 定义模块退出函数,用于卸载模块 void __exit BCM5_drv_exit(void) { // 释放内核虚拟地址空间 iounmap(GPFSEL0); iounmap(GPSET0); iounmap(GPCLR0); // 销毁设备文件 device_destroy(BCM5_class,devno); // 销毁设备类 class_destroy(BCM5_class); // 卸载驱动 unregister_chrdev(major, module_name); } // 定义模块初始化函数和退出函数 module_init(BCM5_drv_init); module_exit(BCM5_drv_exit); // 模块许可证声明 MODULE_LICENSE("GPL v2");
4.2 修改Makefile
obj-m += BCM5.o表示要编译名为BCM5.o的内核模块。这会告诉Makefile编译器,在编译内核时,需要将BCM5.o编译成内核模块,并将其作为一个独立的文件加载到内核中。
这样做的目的是将设备驱动程序作为内核模块编译,以便在需要时可以方便地加载和卸载设备驱动程序,而不需要重新编译整个内核。
4.3 编译内核模块
根据指定的内核源代码目录和相关的编译选项,将模块源代码编译成可加载的内核模块文件(.ko文件)
在树莓派4BLinux源码的树目录下进行 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7l make modules
4.4 编写测试代码并进行交叉编译
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd; int cmd; int data; fd=open("/dev/BCM5",O_RDWR); if(fd < 0){ printf("open failed\n"); perror("reson:"); }else{ printf("open success\n"); } printf("input commnd : 0/1 \n 1:set pin5 high\n 0:set pin5 low\n"); scanf("%d",&cmd); if(cmd == 1){ data = 1; } if(cmd == 0){ data = 0; } printf("data = %d\n",data); fd = write(fd,&data,1); return 0; }
4.5 将.ko文件与可执行文件拷贝至树莓派4B
4.6 测试
1. 在树莓派4B底下,把驱动模块文件加载进树莓派内核:
sudo insmod BCM5.ko
2. 运行测试代码之前,想要给驱动代码文件加一个权限:
sudo chmod 666 /dev/BCM5
3. 执行可执行文件:
4. 查看系统启动过程中的内核消息,包括驱动加载信息
dmesg
5. 查看IO口状态
gpio readall
这样我们就实现了一个简单的驱动了。
五. BCM2711芯片手册
链接:https://pan.baidu.com/s/1j0aMO4v22TNi919qz7a_uA
提取码:8888