一、LED 驱动回顾
对于 LED,APP 调用 open 函数导致驱动程序的 led_open 函数被调用。在 里面,把 GPIO 配置为输出引脚。安装驱动程序后并不意味着会使用对应的硬件, 而 APP 要使用对应的硬件,必须先调用 open 函数。所以建议在驱动程序的 open 函数中去设置引脚。
APP 继续调用 write 函数传入数值,在驱动程序的 led_write 函数根据该 数值去设置 GPIO 的数据寄存器,从而控制 GPIO 的输出电平。
怎么操作寄存器?从芯片手册得到对应寄存器的物理地址,在驱动程序中使 用 ioremap 函数映射得到虚拟地址。驱动程序中使用虚拟地址去访问寄存器。
1.1 回顾 LED 驱动
二、 按键驱动编写思路
GPIO 按键的原理图一般有如下 2 种:
图 2.1 按键原理图示意
按键没被按下时,上图中左边的 GPIO 电平为高,右边的 GPIO 电平为低。
按键被按下后,上图中左边的 GPIO 电平为低,右边的 GPIO 电平为高。
编写按键驱动程序最简单的方法如图 14.3 所示:
2.2 按键驱动编写
回顾一下编写驱动程序的套路:
2.3 驱动程序编写套路
对于使用查询方式的按键驱动程序,我们只需要实现 button_open、 button_read。
三、编程:先写框架
我们的目的写出一个容易扩展到各种芯片、各种板子的按键驱动程序,所以驱动程序分为上下两层:
(1)button_drv.c 分配/设置/注册 file_operations 结构体
起承上启下的作用,向上提供 button_open,button_read 供 APP 调用。
而这 2 个函数又会调用底层硬件提供的 p_button_opr 中的 init、read 函数操作硬件。
(2)board_xxx.c 分配/设置/注册 button_operations 结构体
这个结构体是我们自己抽象出来的,里面定义单板 xxx 的按键操作函数。
这样的结构易于扩展,对于不同的单板,只需要替换 board_xxx.c 提供自己的 button_operations 结构体即可。
3.1 button_drv.c (上层)
#include <linux/module.h> #include <linux/errno.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/sched.h> #include <linux/slab.h> #include <linux/fcntl.h> #include <linux/fs.h> #include <linux/signal.h> #include <linux/mutex.h> #include <linux/mm.h> #include <linux/timer.h> #include <linux/wait.h> #include <linux/skbuff.h> #include <linux/proc_fs.h> #include <linux/poll.h> #include <linux/capi.h> #include <linux/kernelcapi.h> #include <linux/init.h> #include <linux/device.h> #include <linux/moduleparam.h> #include "button_drv.h" static int major = 0; static struct button_operations *p_button_opr; static struct class *button_class; static int button_open (struct inode *inode, struct file *file) { int minor = iminor(inode); p_button_opr->init(minor); return 0; } static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off) { unsigned int minor = iminor(file_inode(file)); char level; int err; level = p_button_opr->read(minor); err = copy_to_user(buf, &level, 1); return 1; } static struct file_operations button_fops = { .open = button_open, .read = button_read, }; void register_button_operations(struct button_operations *opr) { int i; p_button_opr = opr; for (i = 0; i < opr->count; i++) { device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i); } } void unregister_button_operations(void) { int i; for (i = 0; i < p_button_opr->count; i++) { device_destroy(button_class, MKDEV(major, i)); } } EXPORT_SYMBOL(register_button_operations); EXPORT_SYMBOL(unregister_button_operations); int button_init(void) { major = register_chrdev(0, "100ask_button", &button_fops); button_class = class_create(THIS_MODULE, "100ask_button"); if (IS_ERR(button_class)) return -1; return 0; } void button_exit(void) { class_destroy(button_class); unregister_chrdev(major, "100ask_button"); } module_init(button_init); module_exit(button_exit); MODULE_LICENSE("GPL");
第26行:确定主设备号(让系统随机分配)
static int major = 0;
第29行:创建类,让设备自动创建设备号
static struct class *button_class;
定义一个file_operations结构体
static struct file_operations button_fops = { .open = button_open, .read = button_read, };
第55~64行:创建register_button_operations来注册按钮操作
void register_button_operations(struct button_operations *opr) { int i; p_button_opr = opr; for (i = 0; i < opr->count; i++) { device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i); } }
button_operations *opr中定义了count就可以知道有几个按键
用一个for循环遍历每个按键调用device_create 在button_class下面虚拟文件中构造设备节点,父亲为NULL,主设备为major,次设备号为i,格式为100ask_button%d。
在这个类下面创造了设备节点,主设备号为major,次设备号为i,名字是100ask_buttoni
第66~74行:unregister_button_operations,销毁上面创建的按钮操作,注销按钮操作
void unregister_button_operations(void) { int i; for (i = 0; i < p_button_opr->count; i++) { device_destroy(button_class, MKDEV(major, i)); } }
销毁掉创建的类
device_destroy(button_class, MKDEV(major, i));
EXPORT_SYMBOL 是一个常用于 Linux 内核编程的宏,它用于将一个符号(例如一个函数或变量)标记为“导出的”,这意味着这个符号可以在其他模块中被引用或链接。在 Linux 内核中,模块是一种可以动态加载和卸载的代码段,而 EXPORT_SYMBOL 使得一个模块中的函数或变量可以被其他模块所使用。
EXPORT_SYMBOL(register_button_operations); EXPORT_SYMBOL(unregister_button_operations);
这俩个函数是给别人用的,我们需要在button.drv头文件里面声明一下
void register_button_operations(struct button_operations *opr); void unregister_button_operations(void);
第81~90行:定义入口函数init()
int button_init(void) { major = register_chrdev(0, "100ask_button", &button_fops); button_class = class_create(THIS_MODULE, "100ask_button"); if (IS_ERR(button_class)) return -1; return 0; }
在入口函数中把file_operations结构体 button_fops告诉内核
major = register_chrdev(0, "100ask_button", &button_fops);
在入口函数创建类
button_class = class_create(THIS_MODULE, "100ask_button");
在这个类下创造dev,需要有真实的dev时候才能创建,由底层 button_operations *opr提供
第92~96行:定义出口函数 exit()
void button_exit(void) { class_destroy(button_class); unregister_chrdev(major, "100ask_button"); }
卸载主设备号
unregister_chrdev(major, "100ask_button");
销毁掉入口函数中创建的类
class_destroy(button_class);
第98~100行:向内核声明函数
module_init(button_init); module_exit(button_exit); MODULE_LICENSE("GPL");
框架搭建完毕,现在我们实现file_operations结构体中的button_open和 button_read
主设备号用来寻找驱动程序,次设备号是提供给我们用的,想怎么用就怎么用,想用次设备号寻找哪个按键就用哪个次设备号
第83行 button_init中注册主设备号
major = register_chrdev(0, "100ask_button", &button_fops); //主设备号
第31~36行 :编写button_open函数
static int button_open (struct inode *inode, struct file *file) { int minor = iminor(inode); p_button_opr->init(minor); return 0; }
调用底层提供的p_button_opr结构体中的init()函数来初始化引脚,配置为输入引脚,传入次设备号minor,从inode节点中获得次设备号
int minor = iminor(inode);
p_button_opr->init(minor)
第38~47行 :编写button_read函数
static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off) { unsigned int minor = iminor(file_inode(file)); char level; int err; level = p_button_opr->read(minor); err = copy_to_user(buf, &level, 1); return 1; }
从file中获得次设备号
unsigned int minor = iminor(file_inode(file));
调用底层提供的p_button_opr结构体中的read()函数来把GPIO的电平读回来
level = p_button_opr->read(minor);
读回来后传回用户空间,从level中将数据拷贝到buf中,拷贝1字节,return 1表示读了1字节
err = copy_to_user(buf, &level, 1); return 1;
问题来了,底层提供的p_button_opr结构体哪里来的呢?
是底层硬件相关的代码提供的,现在让我们看一下底层代码
3.2 button_drv.h
#ifndef _BUTTON_DRV_H #define _BUTTON_DRV_H struct button_operations { int count; void (*init) (int which); int (*read) (int which); }; void register_button_operations(struct button_operations *opr); void unregister_button_operations(void); #endif
count:有多少个按键
设置button_operations结构体提供上层init()函数和read()函数
在 button_drv.c (上层)第28行中定义全局变量,就可以使用底层p_button_opr
static struct button_operations *p_button_opr;
在button_drv.c (上层)第55~64行中提供注册函数:
void register_button_operations(struct button_operations *opr) { int i; p_button_opr = opr; for (i = 0; i < opr->count; i++) { device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i); } }
由底层向上一层提供p_button_opr结构体
3.3 board_xxx.c(底层)
#include <linux/module.h> #include <linux/errno.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/sched.h> #include <linux/slab.h> #include <linux/fcntl.h> #include <linux/fs.h> #include <linux/signal.h> #include <linux/mutex.h> #include <linux/mm.h> #include <linux/timer.h> #include <linux/wait.h> #include <linux/skbuff.h> #include <linux/proc_fs.h> #include <linux/poll.h> #include <linux/capi.h> #include <linux/kernelcapi.h> #include <linux/init.h> #include <linux/device.h> #include <linux/moduleparam.h> #include "button_drv.h" static void board_xxx_button_init_gpio (int which) { printk("%s %s %d, init gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which); } static int board_xxx_button_read_gpio (int which) { printk("%s %s %d, read gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which); return 1; } static struct button_operations my_buttons_ops ={ .count = 2, .init = board_xxx_button_init_gpio, .read = board_xxx_button_read_gpio, }; int board_xxx_button_init(void) { register_button_operations(&my_buttons_ops); return 0; } void board_xxx_button_exit(void) { unregister_button_operations(); } module_init(board_xxx_button_init); module_exit(board_xxx_button_exit); MODULE_LICENSE("GPL");
第26~29行:初始化引脚
static void board_xxx_button_init_gpio (int which) { printk("%s %s %d, init gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which); }
打印一句话
第31~35行:读取引脚
static int board_xxx_button_read_gpio (int which) { printk("%s %s %d, read gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which); return 1; }
打印一句话并且返回引脚电平
第37~41行:定义一个 button_operations 结构体
static struct button_operations my_buttons_ops ={ .count = 2, .init = board_xxx_button_init_gpio, .read = board_xxx_button_read_gpio, };
count表明有2个按键
init表明初始化引脚的函数
read表明读取引脚的函数
第43~47行:注册函数
int board_xxx_button_init(void) { register_button_operations(&my_buttons_ops); return 0; }
入口函数中注册了一个my_buttons_ops结构体
第49~52行:卸载函数
void board_xxx_button_exit(void) { unregister_button_operations(); }
出口函数中销毁一个my_buttons_ops结构体
3.4 button_test.c(测试程序)
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> /* * ./button_test /dev/100ask_button0 * */ int main(int argc, char **argv) { int fd; char val; /* 1. 判断参数 */ if (argc != 2) { printf("Usage: %s <dev>\n", argv[0]); return -1; } /* 2. 打开文件 */ fd = open(argv[1], O_RDWR); if (fd == -1) { printf("can not open file %s\n", argv[1]); return -1; } /* 3. 写文件 */ read(fd, &val, 1); printf("get button : %d\n", val); close(fd); return 0; }
3.5 Makefile
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR # 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量: # 2.1 ARCH, 比如: export ARCH=arm64 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu- # 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin # 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同, # 请参考各开发板的高级用户使用手册 KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88 all: make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o button_test button_test.c clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order rm -f ledtest # 参考内核源码drivers/char/ipmi/Makefile # 要想把a.c, b.c编译成ab.ko, 可以这样指定: # ab-y := a.o b.o # obj-m += ab.o obj-m += button_drv.o obj-m += board_xxx.o
四、上机测试
4.1编译
编译程序,把代码上传代服务器后执行 make 命令。
cp *.ko button_test ~/nfs_rootfs/
4.2 挂载到开发板
在开发板上挂载 NFS
4.3 测试
1. [root@100ask:~]# echo "7 4 1 7" > /proc/sys/kernel/printk // 打开内核的打印信息,有些 2. 板子默认打开了
提示找不到符号,所以我们需要先装载button_drv.ko
4.4 测试结果:
大佬觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄
💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍
🔥🔥🔥任务在无形中完成,价值在无形中升华,让我们一起加油吧!🌙🌙🌙