前言
最近跟着韦东山老师的课程学习驱动基础知识,韦东山老师讲的非常好,大家想深入学习驱动知识的可以去看看韦东山老师的驱动教学视频。
一、字符设备驱动程序框架
图 6.1 中驱动层访问硬件外设寄存器依靠的是 ioremap 函数去映射到寄存 器地址,然后开始控制寄存器。
编写驱动程序的步骤:
- 确定主设备号,也可以让内核分配
- 定义自己的 file_operations 结构体
- 实现对应的 drv_open/drv read/drv write 等函数,填入 file operations 结构体
- 把 file_operations 结构体告诉内核: register_chrdev
- 谁来注册驱动程序啊? 得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
- 有入口函数就应该有出口函数: 卸载驱动程序时,出口函数调用unregister_chrdev
- 其他完善:提供设备信息,自动创建设备节点: class_create,device_create
对于 LED 驱动,我们想要什么样的接口?
LED 驱动能支持多个板子的基础:分层思想
把驱动拆分为通用的框架(leddrv.c)、具体的硬件操作(board_X.c):
以面向对象的思想,改进代码,抽象出一个结构体:
每个单板相关的 board_X.c 实现自己的 led_operations 结构体,供上层 的 leddrv.c 调用:
二、Linux驱动如何指向一个GPIO
在编写驱动程序的时候,如果我们需要进行点灯操作,首先要知道控制的是哪一个引脚才可以真正的操作LED。我们通过在原理图中查找发现,LED是由GPIO5_3控制。
直接通过寄存器来操作GPIO
(1)我们在学习入门视频的时候,常常会看到他们使用ioremap()函数对寄存器进行映射,然后直接操作寄存器。不再要使用这个寄存器的时候,就调用iounmap()函数进行释放。
(2) 这样编写毫无疑问,非常原始,就像是在写51单片机的程序。但是不同的在于,51单片机的寄存器并不多,所以直接操作寄存器并不麻烦。而i.max6ull这个级别的芯片,寄存器一大堆,再直接进行寄存器操作,无疑非常麻烦。
(3)我上一篇写的博客就是直接利用寄存器来操作GPIO,链接如下:Linux驱动入门 —— LED点灯驱动程序-CSDN博客
想要深入学习的,可以看看韦东山老师的驱动教学视频,韦东山老师讲的非常好。
利用引脚号操作GPIO
(1)从上面的原理图,我们知道了LED是由GPIO5_3控制之后,就可以直接开始操作了吗?
(2)不对,在 Linux 中,GPIO 的标识和控制通常是通过引脚号来进行的,引脚号是用于唯一标识特定的 GPIO 引脚。
(3)如果我们有stm32,msp430这种裸机开发经验,就会发现,不同的芯片对于GPIO的名字定义是不同的。比如STM32将引脚定义成PA0,PB4这种。但是如果是MSP430单片机,他对引脚的定义是P2.3,P1.0这样的。不同厂家对自己的芯片GPIO名字不同。
(4)这种GPIO名字不同,会导致什么结果呢?这样会让驱动开发人员总是要记,不同的芯片的命名规则,显然是非常麻烦的。于是,Linux规定了,我不管你是什么芯片厂家,不管你怎么命名,你如果要跑Linux,就必须将引脚变成一个数字,这个数字叫做引脚号。驱动开发人员,只需要知道这个引脚对应的引脚号,就可以进行操作了。而把这个引脚变成引脚号的过程,就是由芯片原厂的工程师来做了。
(5)关于引脚号的获取,最简单的办法就是,直接联系厂家询问。比如如下是飞腾的芯片,他们的引脚映射表。
IMX6ULL引脚获取
现在我的这个IMX6ULL开发板要控制GPIO5_3,而且我找不到他们的引脚映射表,那么他这个引脚号是多少呢?
如果找不到映射表,我们连接上开发板
输入指令:cat /sys/kernel/debug/gpio
即可获得GPIO的映射表,以及他的起始地址。
(1)这个时候有人可能就会认为,这里搜索到的gpiochip5就是GPIO5了。答案是否定的。
(2)为什么这么说呢?因为我上面说了,不同厂家对GPIO的命名是不同的,他们厂家的工程师最终会将这些GPIO抽象成一个引脚号。在这个抽象的过程中驱动中的命名可能会和原理图上的命名有些许出入,比如imax6ull开发板的GPIO5就是gpiochip4,因为imax6ull开发板是从GPIO1开始进行计算,而驱动程序中,是从gipo0开始的。
(3)如何确定是这样的呢?首先我们看上图,指导gpio0的地址为209c000,那么直接打开芯片手册,可以看到GPIO1的起始地址为209c000,正好对应
(4)现在知道GPIO5对应gpio4了,然后从终端中可以指导,gpio4的起始引脚号为128,那么GPIO5_3的引脚号就是128+3=131。
三、Linux的统一接口 — GPIO子系统
为什么需要统一接口
(1)讲解Linux的GPIO子系统之前,我先拿单片机开发做引子。
(2)对于绝大多数人而言,学习嵌入式开发,都是从51单片机开始的。STC89C52作为51单片机的一款经典单片机,大家多多少少都有些许了解。
(3)在编写STC89C52单片机的程序时候,我们都是直接对寄存器进行操作的,比如下面这个串口初始化程序。
void UartInit(void) //9600bps@11.0592MHz { SCON = 0x50; //8位数据,可变波特率 AUXR &= 0xBF; //定时器1时钟为Fosc/12,即12T AUXR &= 0xFE; //串口1选择定时器1为波特率发生器 TMOD &= 0x0F; //设定定时器1为16位自动重装方式 TL1 = 0xE8; //设定定时初值 TH1 = 0xFF; //设定定时初值 ET1 = 0; //禁止定时器1中断 TR1 = 1; //启动定时器1 }
(4)学习完51单片机之后,大多数人开始进阶STM32F103这款芯片。因为STM32F103这款芯片的寄存器很多,直接使用寄存器开发,查手册会相当的麻烦。于是ST公司就封装了一些库,如下为GPIO操作部分的库函数。
(5)我们会发现,不同的芯片,他的库函数大概率是不一样的。假如我们编写了一个业务程序,在STM32上跑的好好的。如果因为某些事情,我要换一款芯片,而这款芯片的库函数和STM32的不一样。最终会导致什么结果?很明显,所有业务程序需要重写编写!这是非常麻烦的事情!
(6)为了防止出现这种情况,Linux规定了,不管你是啥芯片,你只要想跑Linux,就必须给我统一接口!管你什么厂家,你的芯片让GPIO设置为输出的函数,名字必须叫做int gpio_direction_output()!
(7)这样做,存在什么好处呢?显而易见,我们业务代码不需要更改了,如果我们想换一款芯片,只需要底层稍微的改动一下即可。这也是为什么有些人说的,没跑Linux,阶级分明,跑了Linux,众生平等。
四、GPIO子系统函数介绍
Linux的GPIO子系统中可以通过如下函数配置GPIO。
int gpio_request(unsigned gpio, const char *label); void gpio_free(unsigned gpio); int gpio_direction_input(unsigned gpio); int gpio_direction_output(unsigned gpio, int value); int gpio_get_value(unsigned gpio); void gpio_set_value(unsigned gpio, int value);
(1)gpio_request()
/****** 函数介绍 ******/ /* 作用 : 向Linux 内核中用于请求申请一个 GPIO 引脚 * 传入参数 : * gpio : 要请求的 GPIO 引脚号 * label : 给GPIO起一个名字 * 返回参数 : 如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误 */ int gpio_request(unsigned gpio, const char *label);
- 作用: 向Linux 内核中用于请求申请一个 GPIO 引脚的函数。如果我们想对一个引脚进行操作,需要最先调用 gpio_request()这个函数。
- gpio : 要请求的 GPIO 引脚号。这个引脚号可以自己直接给出,还可以通过 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息(设备树的内容)
- label : 给GPIO起一个名字,因为直接一个引脚号不方便人阅读,所以可以给这个引脚号起一个名字。随便起名字,只要你自己喜欢,不影响。
- 返回值 : 如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误。
(2)gpio_free()
/****** 函数介绍 ******/ /* 作用 : 如果不使用某个GPIO了,那么就需要调用 gpio_free 函数进行释放 * 传入参数 : * gpio : 要释放的GPIO引脚号 * 返回参数 : 无 */ void gpio_free(unsigned gpio);
- 作用 : 如果不使用某个 GPIO 了, 那么就需要调用 gpio_free 函数进行释放。
- gpio : 要释放的GPIO引脚号。与gpio_request的GPIO引脚号是同一个东西。
- 返回参数 : 无
(3)gpio_direction_input()
/****** 函数介绍 ******/ /* 作用 : 设置某个 GPIO 为输入 * 传入参数 : * gpio : 要设置为输入的GPIO 引脚号 * 返回参数 : 设置成功返回 0; 设置失败返回负值 */ int gpio_direction_input(unsigned gpio);
- 作用 : 将GPIO配置为输入方向。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输入
- gpio : 要设置为输入的GPIO 引脚号
- 返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输入模式。返回负数,表示出错或无法设置 GPIO 引脚。
(4)gpio_direction_output()
/****** 函数介绍 ******/ /* 作用 : 设置某个 GPIO 为输出,并且设置默认输出值 * 传入参数 : * gpio : 要设置为输出的GPIO 引脚号 * value : GPIO 默认输出值 * 返回参数 : 设置成功返回 0; 设置失败返回负值 */ int gpio_direction_output(unsigned gpio, int value);
- 作用 : 将GPIO配置为输出方向,并且设置默认输出值。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输出
- gpio : 设置为输出的GPIO 引脚号
- value : GPIO 默认输出值。如果GPIO初始化成功之后,默认输出的电压。
- 返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输出模式。返回负数,表示出错或无法设置 GPIO 引脚。
(5)gpio_get_value()
/****** 函数介绍 ******/ /* 作用 : 获取指定GPIO的电平值 * 传入参数 : * gpio : 要获取电平值的GPIO标号 * 返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值 */ int gpio_get_value(unsigned gpio);
- 作用 : 获取指定GPIO的电平信息
- gpio : 要获取电平值的GPIO标号
- 返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值。
(6)gpio_set_value()
- 作用 : 设置指定GPIO的电平值
- gpio : 要设置指定GPIO的电平值
- value : 要设置的电平值,如果传入0,则表示将GPIO设置为低电平。传入一个非0值,表示将GPIO设置为高电平
- 返回参数 : 无
/****** 函数介绍 ******/ /* 作用 : 获取指定GPIO的电平值 * 传入参数 : * gpio : 要设置指定GPIO的电平值 * value : 要获取电平值的GPIO标号 * 返回参数 : 无 */ void gpio_set_value(unsigned gpio, int value);
五、LED驱动代码
驱动层代码:
这里只展现代码,详细介绍参考大佬博客 Linux驱动入门 —— LED点灯驱动程序-CSDN博客
led_drv.c
#include "asm-generic/errno-base.h" #include "asm-generic/gpio.h" #include "asm/uaccess.h" #include <linux/module.h> #include <linux/poll.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/miscdevice.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h> #include <linux/gpio/consumer.h> #include <linux/platform_device.h> #include <linux/of_gpio.h> #include <linux/of_irq.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/slab.h> #include <linux/fcntl.h> #include <linux/timer.h> // 描述一个引脚 struct gpio_desc{ int gpio; // 引脚编号 char *name; // 名字 }; static struct gpio_desc gpios[] = { {131, "led0", }, // 引脚编号,名字 }; /* 1. 确定主设备号 */ static int major = 0; static struct class *gpio_class; // 一个类,用于创建设备节点 /* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */ static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { char tmp_buf[2]; // 存放驱动层和应用层交互的信息 int err; // 没有使用,用于存放copy_from_user和copy_to_user的返回值,消除报错 int count = sizeof(gpios)/sizeof(gpios[0]); // 记录定义的最大引脚数量 // 应用程序读的时候,传入的值如果不是两个,那么返回一个错误 if (size != 2) return -EINVAL; /* 作用 : 驱动层得到应用层数据 * tmp_buf : 驱动层数据 * buf : 应用层数据 * 1 :数据长度为1个字节(因为我只需要知道他控制的是那一盏灯,所以只需要传入一个字节数据) */ err = copy_from_user(tmp_buf, buf, 1); //第0项表示要操作哪一个LED,如果操作的LED超出,表示失败 if (tmp_buf[0] >= count) return -EINVAL; //将引脚电平读取出来 tmp_buf[1] = gpio_get_value(gpios[(int)tmp_buf[0]].gpio); /* 作用 : 驱动层发数据给应用层 * buf : 应用层数据 * tmp_buf : 驱动层数据 * 2 :数据长度为2个字节 */ err = copy_to_user(buf, tmp_buf, 2); return 2; } static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { unsigned char ker_buf[2]; int err; // 应用程序读的时候,传入的值如果不是两个,那么返回一个错误 if (size != 2) return -EINVAL; /* 作用 : 驱动层得到应用层数据 * tmp_buf : 驱动层数据 * buf : 应用层数据 * size :数据长度为size个字节 */ err = copy_from_user(ker_buf, buf, size); // 如果要操作的GPIO不在规定范围内,返回错误 if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0])) return -EINVAL; // 设置指定引脚电平 gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]); return 2; } /* 2. 定义自己的file_operations结构体 */ static struct file_operations gpio_led_drv= { .owner = THIS_MODULE, .read = gpio_drv_read, .write = gpio_drv_write, }; /* 4. 把file_operations结构体告诉内核:注册驱动程序 */ /* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */ static int __init gpio_drv_init(void) { int err; //用于保存函数返回值,用于判断函数是否执行成功 int i; //因为存在多个GPIO可能要申请,所以建立一个i进行for循环 int count = sizeof(gpios)/sizeof(gpios[0]); //统计有多少个GPIO /*__FILE__ :表示文件 *__FUNCTION__ :当前函数名 *__LINE__ :在文件的哪一行 */ printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); for (i = 0; i < count; i++) { /* 设置为输出引脚 */ //申请指定GPIO引脚,申请的时候需要用到名字 err = gpio_request(gpios[i].gpio, gpios[i].name); // 如果返回值小于0,表示申请失败 if (err < 0) { //如果GPIO申请失败,打印出是哪个GPIO申请出现问题 printk("can not request gpio %s %d\n", gpios[i].name, gpios[i].gpio); return -ENODEV; } // 如果GPIO申请成功,设置输出高电平 gpio_direction_output(gpios[i].gpio, 1); } /* 注册file_operations */ // 注册字符驱动程序 major = register_chrdev(0, "100ask_led", &gpio_led_drv); /* /dev/gpio_desc */ /******这里相当于命令行输入 mknod /dev/100ask_gpio c 240 0 创建设备节点*****/ // 创建类,为THIS_MODULE模块创建一个类,这个类叫做gpio_class gpio_class = class_create(THIS_MODULE, "100ask_led_class"); if (IS_ERR(gpio_class)) //如果返回错误 { /*__FILE__ :表示文件 *__FUNCTION__ :当前函数名 *__LINE__ :在文件的哪一行 */ printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); // 注销字符驱动程序 unregister_chrdev(major, "100ask_led_class"); // 返回错误 return PTR_ERR(gpio_class); } /*输入参数是逻辑设备的设备名,即在目录/dev目录下创建的设备名 *参数一 : 在gpio_class类下面创建设备 *参数二 : 无父设备的指针 *参数三 : 主设备号+次设备号 *参数四 : 没有私有数据 */ device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led"); /* /dev/100ask_gpio */ // 如果执行到这里了,说明LED驱动装载完成 printk("LED driver loading is complete\n"); return err; } /* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */ static void __exit gpio_drv_exit(void) { int i; int count = sizeof(gpios)/sizeof(gpios[0]); /*__FILE__ :表示文件 *__FUNCTION__ :当前函数名 *__LINE__ :在文件的哪一行 */ printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); // 销毁gpio_class类下面的设备节点 device_destroy(gpio_class, MKDEV(major, 0)); // 销毁gpio_class类 class_destroy(gpio_class); // 注销驱动 unregister_chrdev(major, "100ask_led"); for (i = 0; i < count; i++) { // 将GPIO释放 gpio_free(gpios[i].gpio); } // 如果执行到这里了,说明LED驱动卸载完成 printk("The LED driver is uninstalled\n"); } /* 7. 其他完善:提供设备信息,自动创建设备节点 */ module_init(gpio_drv_init); // 确认入口函数 module_exit(gpio_drv_exit); // 确认出口函数 /*最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加 *这个协议要求我们代码必须免费开源,Linux遵循GPL协议,他的源代码可以开放使用,那么你写的内核驱动程序也要遵循GPL协议才能使用内核函数 *因为指定了这个协议,你的代码也需要开放给别人免费使用,同时可以根据这个协议要求很多厂商提供源代码 *但是很多厂商为了规避这个协议,驱动源代码很简单,复杂的东西放在应用层 */ MODULE_LICENSE("GPL"); // 指定模块为GPL协议 MODULE_AUTHOR("CSDN:qq_919426896"); // 表明作者,可以不写
应用层代码:
strtol()函数是将字符转换为数字。
因为我们在命令行中输入的1,其实是字符1,而不是数字1。为了和驱动层统一数据类型,所以这里需要调用这个函数。
ledtest.c
#include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <poll.h> #include <signal.h> static int fd; // int led_on(int which); // int led_off(int which); // int led_status(int which); /* 可执行文件名 | 表示要操作哪一盏灯 | 灯状态 | 效果 * ./led_test | <0|1|2|..> | on |硬件上开灯 * ./led_test | <0|1|2|..> | off |硬件上关灯 * ./led_test | <0|1|2|..> | |读取led状态,并且显示在终端 */ int main(int argc, char **argv) { int ret; // 存放函数返回值,用于判断函数是否正常执行 char buf[2]; // 存放命令行的后两个字符(<0|1|2|...> [on | off]) // 如果传入参数少于两个,打印文件用法 if (argc < 2) { printf("Usage: %s <0|1|2|...> [on | off]\n", argv[0]); return -1; } // 打开文件,因为在驱动层中,device_create()函数创建的设备节点名字叫做100ask_led,而设备节点都存放在/dev目录下,所以这里是/dev/100ask_led fd = open("/dev/100ask_led", O_RDWR); // 如果无法打开,返回错误 if (fd == -1) { printf("can not open file /dev/100ask_led\n"); return -1; } // 如果传入了三个参数,表示写入 if (argc == 3) { /* write */ /* 作用 : 将字符串转化为一个整数 * argv[1] : 要转换为长整数的字符串 * NULL :如果提供了 endptr 参数,则将指向解析结束位置的指针存储在 endptr 中。endptr 可以用于进一步处理字符串中的其他内容 * 0 : 设置为 0,则会根据字符串的前缀(如 "0x" 表示十六进制,"0" 表示八进制,没有前缀表示十进制)来自动判断进制 */ buf[0] = strtol(argv[1], NULL, 0); // 判断是否为打开 if (strcmp(argv[2], "on") == 0) buf[1] = 0; //因为LED外接3.3V,所以输出低电平才是开灯 else buf[1] = 1; //因为LED外接3.3V,所以输出高电平才是关灯 // 向字符驱动程序中写入 ret = write(fd, buf, 2); } // 否则表示读取电平信息 else { /* read */ /* 作用 : 将字符串转化为一个整数 * argv[1] : 要转换为长整数的字符串 * NULL :指向第一个不可转换的字符位置的指针 * 0 : 表示默认采用 10 进制转换 */ buf[0] = strtol(argv[1], NULL, 0); // 读取电平,从驱动层读取两个数据 ret = read(fd, buf, 2); // 如果返回值为2,表示正常读取到了电平。(为什么是2,看驱动程序的gpio_drv_read) if (ret == 2) { //打印引脚信息 printf("led %d status is %s\n", buf[0], buf[1] == 0 ? "on" : "off"); } } close(fd); return 0; }
Makefile
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88 all: make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o ledtest ledtest.c clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order rm -f ledtest obj-m += led_drv.o
上机测试:
可参考上一篇博客 Linux驱动入门 —— LED点灯驱动程序-CSDN博客
在ubuntu上执行make
挂载 NFS 目录,参考 开发板挂载 Ubuntu 的 NFS 目录-CSDN博客
打开开发板,这里使用的是imx6ull
执行 insmod led_drv.ko
开始测试
执行 ./ledtest 0 on 使开发板亮灯
测试结束执行 rmmod led_drv.ko 卸载驱动