本文共 13247 字,大约阅读时间需要 44 分钟。
ioctl
,将应用态和内核态进行交互menuconfig
的内核源码。(与网上下的内核源码包的主要区别是menuconfig过后会生成一个.config
文件)驱动的Makefile不像是应用层makefile那么千奇百怪,可以说驱动的Makefile就是一个固定的模板,复制过来用就完事了。
# Makefileifneq ($(KERNELRELEASE),) obj-m := xyy.oelse #KERNELDIR ?= /lib/modules/$(shell uname -r)/build KERNELDIR ?= /path_to_linux_src/linux-at91-linux-2.6.39-at91-20160713 PWD := $(shell pwd)default: clean make -C $(KERNELDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- rm -rf *.o .*.cmd .tmp_versions *.mod.c modules.order Module.symvers *.ko.unsignedclean: $(RM) -r *.ko *.o .*.cmd .tmp_versions *.mod.c modules.order Module.symvers *.ko.unsignedendif
这里需要修改的总共就三处:
obj-m
后面的.o
的文件名字,改成对应的你自己驱动的.c
文件的名字;KERNELDIR
改成linux内核源码的文件夹地址即可,记住,要是menuconfig
过的。CROSS_COMPILE
这个变量改成你板子交叉编译工具链的前缀。这三处改好之后,就可以直接在makefile的当前目录输入make
执行了。
这里需要注意,为什么是obj-m
:
obj-y +=xxx.o该模块编译到zImage
obj-m +=xxx.o该模块不会编译到zImage,但会生成一个独立的xxx.ko 静态编译
如果这里使用obj-y
则make之后不会发生任何事情。
obj-m
这个玩意我们可以简单的看一个内核源码中的某个组件的Makefile
文件,这里以/arch/arm/mach-at91/Makefile
为例子:
# CPU-specific supportobj-$(CONFIG_ARCH_AT91RM9200) += at91rm9200.o at91rm9200_time.o at91rm9200_devices.oobj-$(CONFIG_ARCH_AT91SAM9260) += at91sam9260.o at91sam926x_time.o at91sam9260_devices.o sam9_smc.o at91sam9_alt_reset.oobj-$(CONFIG_ARCH_AT91SAM9261) += at91sam9261.o at91sam926x_time.o at91sam9261_devices.o sam9_smc.o at91sam9_alt_reset.oobj-$(CONFIG_ARCH_AT91SAM9G10) += at91sam9261.o at91sam926x_time.o at91sam9261_devices.o sam9_smc.o at91sam9_alt_reset.oobj-$(CONFIG_ARCH_AT91SAM9263) += at91sam9263.o at91sam926x_time.o at91sam9263_devices.o sam9_smc.o at91sam9_alt_reset.oobj-$(CONFIG_ARCH_AT91SAM9RL) += at91sam9rl.o at91sam926x_time.o at91sam9rl_devices.o sam9_smc.o at91sam9_alt_reset.oobj-$(CONFIG_ARCH_AT91SAM9G20) += at91sam9260.o at91sam926x_time.o at91sam9260_devices.o sam9_smc.o at91sam9_alt_reset.oobj-$(CONFIG_ARCH_AT91SAM9G45) += at91sam9g45.o at91sam926x_time.o at91sam9g45_devices.o sam9_smc.oobj-$(CONFIG_ARCH_AT91SAM9X5) += at91sam9x5.o at91sam926x_time.o at91sam9x5_devices.o sam9_smc.oobj-$(CONFIG_ARCH_AT91SAM9N12) += at91sam9n12.o at91sam926x_time.o at91sam9n12_devices.o sam9_smc.oobj-$(CONFIG_ARCH_AT91CAP9) += at91cap9.o at91sam926x_time.o at91cap9_devices.o sam9_smc.oobj-$(CONFIG_ARCH_AT572D940HF) += at572d940hf.o at91sam926x_time.o at572d940hf_devices.o sam9_smc.oobj-$(CONFIG_ARCH_AT91X40) += at91x40.o at91x40_time.o
可以看到有很多obj-${xxxx}
的Makefile变量,而这些变量使用的宏,实际上就是在menuconfig里进行选择的时候进行了复制,我们可以在.config
文档中找到端倪。
# Automatically generated make config: don't edit# Linux/arm 2.6.39 Kernel Configuration# Wed Mar 27 14:37:50 2019#CONFIG_ARM=yCONFIG_SYS_SUPPORTS_APM_EMULATION=yCONFIG_GENERIC_GPIO=y# CONFIG_ARCH_USES_GETTIMEOFFSET is not setCONFIG_GENERIC_CLOCKEVENTS=yCONFIG_KTIME_SCALAR=yCONFIG_HAVE_PROC_CPU=yCONFIG_STACKTRACE_SUPPORT=yCONFIG_HAVE_LATENCYTOP_SUPPORT=yCONFIG_LOCKDEP_SUPPORT=yCONFIG_TRACE_IRQFLAGS_SUPPORT=yCONFIG_HARDIRQS_SW_RESEND=yCONFIG_GENERIC_IRQ_PROBE=y
其实menuconfig
的过程就是给Makefile里的这些值进行了赋值,将不同的目标文件进行有规则的分类。俗称内核裁剪
,这部分不在这里过多赘述。
/* * @Author: Adam Xiao * @Date: 2021-03-23 19:40:28 * @LastEditors: Adam Xiao * @LastEditTime: 2021-03-25 10:39:18 * @FilePath: /test/xyy.c */// xyy.c#include#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct miscdevice misc_dev ={ .minor = MISC_DYNAMIC_MINOR, .name = "Adam",};static int __init test_init(void){ int ret = misc_register(&misc_dev); if (ret) { printk(KERN_ERR "[xyy]misc_register error\n"); } else { printk(KERN_INFO "[xyy]test driver init\n"); } return ret;}static void __exit test_exit(void){ misc_deregister(&misc_dev); printk(KERN_INFO "[xyy]test driver exit\n");}module_init(test_init);module_exit(test_exit);MODULE_AUTHOR("XXXXXBBBBB Corporation");MODULE_DESCRIPTION("XXXXXXXX machine main controller board IO test driver");MODULE_LICENSE("Dual BSD/GPL");MODULE_VERSION("1.0.0");
测试源码如上所述,直接进行make
。
xyy@test$makerm -f -r *.ko *.o .*.cmd .tmp_versions *.mod.c modules.order Module.symvers *.ko.unsignedmake -C /XXXXXXXXXXXXXXXXXXXXX/linux-at91-linux-2.6.39-at91-20160713 M=/XXXXXXXXXXXXXXXXXXXXX/test modules ARCH=arm CROSS_COMPILE=arm-at91-linux-gnueabi-make[1]: Entering directory '/XXXXXXXXXXXXXXXXXXXXX/linux-at91-linux-2.6.39-at91-20160713' CC [M] /XXXXXXXXXXXXXXXXXXXXX/test/xyy.o Building modules, stage 2. MODPOST 1 modules CC /XXXXXXXXXXXXXXXXXXXXX/test/xyy.mod.o LD [M] /XXXXXXXXXXXXXXXXXXXXX/test/xyy.komake[1]: Leaving directory '/XXXXXXXXXXXXXXXXXXXXX/linux-at91-linux-2.6.39-at91-20160713'rm -rf *.o .*.cmd .tmp_versions *.mod.c modules.order Module.symvers *.ko.unsigned
令人期待的第一个驱动新鲜出炉了。这里学习三个简单的指令
insmod
:加载对应驱动rmmod
:卸载对应驱动dmesg
:查看内核打印,-c是清除历史打印缓存,但是会显示最后一次的缓存,显示完后才会清除。我们尝试一下,刚刚编译好的驱动xyy.ko
。
# insmod xyy.ko# dmesg -c[xyy]test driver init# rmmod xyy.ko# dmesg -c[xyy]test driver exit# ls /dev/Adam -lahtcrw-rw---- 1 root root 10, 57 Mar 25 14:25 /dev/Adam
可以看到,这个驱动在加载进系统的时候,内核打印了我们编码的内容。在卸载驱动的时候,也打印了卸载驱动的相关内容。并且在/dev
目录下,多了一个名为Adam
的设备文件。
请注意10,57
这个数字,后面会详细讲解这两个数字的含义。
上面的驱动能够简单的运行起来,说明我们的框架是正确的。那么简单的讲解一下,内核里面的一些特有接口。
如同代码中最后部分有一些这样的宏,其实就是说明你的驱动的归属。
MODULE_AUTHOR("XXXXXBBBBB Corporation");MODULE_DESCRIPTION("XXXXXXXX machine main controller board IO test driver");MODULE_LICENSE("Dual BSD/GPL");MODULE_VERSION("1.0.0");
都不是强制的,但是有说法是这样的:
不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包含一行 MODULE_LICENSE: MODULE_LICENSE(“GPL”); 内核认识的特定许可有, “GPL”( 适用 GNU 通用公共许可的任何版本 ), “GPL v2”( 只适用 GPL 版本 2 ), “GPL and additional rights”, “Dual BSD/GPL”, “Dual MPL/GPL”, 和 “Proprietary”. 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊"了。 象我们在第 1 章"许可条款"中提到的, 内核开发者不会热心帮助在加载了私有模块后遇到问题的用户。
总之就是鼓励大家还是有开源精神。
module_init
和module_exit
这两个本质也是宏。用C++的概念理解这个,可以当成构造函数和析构函数去理解,驱动加载的时候运行的函数,用module_init
这个接口注册,驱动卸载的时候运行的函数,用module_exit
这个接口注册。那么在驱动进行对应的操作的时候,对应的函数就会被运行。
这两个函数,相当于驱动的入口和出口函数。
misc_register
和misc_deregister
这里先介绍两个概念,杂项设备(misc_device)和字符设备(char_device)。
杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux内核的include/linux目录下有Miscdevice.h文件,要把自己定义的miscdevice从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于miscdevice,其实misc_register就是用主标号10调用register_chrdev()的。
也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。
使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时,如果有多个设备使用该函数注册驱动程序,LED_MAJOR不能相同,否则几个设备都无法注册。如果模块使用该方式注册并且LED_MAJOR为0(自动分配主设备号),使用insmod命令加载模块时会在终端显示分配的主设备号和次设备号,在/dev目录下建立该节点,比如设备leds,如果加载该模块时分配的主设备号和次设备号为253和0,则建立节点时使用如下语句:
mknod leds c 253 0使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时要手动建立节点,否则在应用程序无法打开该设备。
杂项设备注册和加载较为方便,不用手动指定主次设备号:
insmod xyy.ko
这样就完成加载了
字符设备注册的时候,需要手动指定设备号,并且加载的时候,需要调用mknod建立节点:
insmod Adam.komknod /dev/Adam c 234 0
mknod 的标准形式为: mknod DEVNAME {b | c} MAJOR MINOR
这里需要注意的是,mknod的主次设备号需要与驱动代码中注册的时候,保持一致。
ioctl
,将应用态和内核态进行交互上面的驱动虽然是正确的,但是实际上这个驱动什么都没有做。
回到驱动的本质,驱动是要干什么呢?
驱动是要用来操控硬件的
因为用户层没有权限直接操控硬件,只有内核才能做这个事情,所以我们需要一个桥梁,来通知内核,我想操控什么硬件,用这个硬件来读取或者写入什么东西。
一般来说,读取设备或者读取芯片的值之类的东西,厂家都会给接口样例,例如:
at91_set_gpio_value(CPLD_ADDR_4,1);at91_set_gpio_value(CPLD_ADDR_1,1);
类似这样的接口其实一般字面意思就能猜出来是干了什么,上面的两个接口就是给gpio的某个管脚设置了1这个值,反正不是拉高就是拉低了电平嘛。
这种接口只能在内核态用,所以需要写一个驱动,完成一定的功能,然后把这些获取/设置的值给应用态的程序。每个驱动都是做这个事情的。
这个debug.h
主要是用来辅助打印的,将应用层和内核层的打印统一一致。
//debug.h#ifndef __DEBUG_H__#define __DEBUG_H__#define COL_DEF "\033[m"#define COL_RED "\033[0;32;31m"#define COL_GRE "\033[0;32;32m"#define COL_BLU "\033[0;32;34m"#define COL_YEL "\033[1;33m"#ifdef __KERNEL__ //for linux driver #define ERR(fmt, ...) printk(KERN_ERR COL_RED "driver error function[%s]:"\ COL_YEL fmt COL_DEF "\n", __func__, ##__VA_ARGS__) #define INFO(fmt, ...) printk(KERN_INFO COL_GRE "driver information:"\ COL_YEL fmt COL_DEF "\n", ##__VA_ARGS__) #ifdef DEBUG #define DBG(fmt, ...) printk(KERN_DEBUG COL_BLU "debug function[%s]:"\ COL_DEF fmt, __func__, ##__VA_ARGS__) #else #define DBG(fmt, ...) ({0;}) #endif#else //for linux application #define ERR(fmt, ...) printf(COL_RED "error function[%s]:"\ COL_YEL fmt COL_DEF "\n", __func__, ##__VA_ARGS__) #define INFO(fmt, ...) printf(COL_GRE "information:"\ COL_YEL fmt COL_DEF "\n", ##__VA_ARGS__) #ifdef DEBUG #define DBG(fmt, ...) printf(COL_BLU "debug function[%s]:"\ COL_DEF fmt, __func__, ##__VA_ARGS__) #else #define DBG(fmt, ...) ({0;}) #endif#endif#endif
这里是驱动的核心代码。
/* * @Author: Adam Xiao * @Date: 2021-03-23 19:40:28 * @LastEditors: Adam Xiao * @LastEditTime: 2021-03-25 17:04:13 * @FilePath: /test/xyy.c */// xyy.c#include#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "debug.h"static int io_open(struct inode *inode, struct file *filp){ INFO("[xyy] Open called!!!"); return 0;}static int io_release(struct inode *inode, struct file *file){ INFO("[xyy] close called!!!"); return 0;}struct test { int a; short b; char c;};long io_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){ INFO("cmd = %#x\n", cmd); INFO("arg:%p\n", (void *)arg); struct test t = { 0, 0, 0}; int ret = copy_from_user(&t, (struct test *)arg, sizeof(t)); INFO("ret = %d, t.a = %d, t.b = %d, t.c = %d\n", ret, t.a, t.b, t.c); t.a = 111; t.b = 222; t.c = 33; (void) copy_to_user((struct test *)arg, &t, sizeof(t)); return 0;}struct file_operations io_ops = { .owner = THIS_MODULE, .release = io_release, .open = io_open,#if 0 .read = irq_read,#endif#if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,18) .unlocked_ioctl = io_ioctl, #else .ioctl = io_ioctl,#endif};struct miscdevice misc_dev ={ .minor = MISC_DYNAMIC_MINOR, .name = "Adam", .fops = &io_ops,};static int __init test_init(void){ int ret = misc_register(&misc_dev); if (ret) { printk(KERN_ERR "[xyy]misc_register error\n"); } else { printk(KERN_INFO "[xyy]test driver init\n"); } return ret;}static void __exit test_exit(void){ misc_deregister(&misc_dev); printk(KERN_INFO "[xyy]test driver exit\n");}module_init(test_init);module_exit(test_exit);MODULE_AUTHOR("XXXXXBBBBB Corporation");MODULE_DESCRIPTION("XXXXXXXX machine main controller board IO test driver");MODULE_LICENSE("Dual BSD/GPL");MODULE_VERSION("1.0.0");
可以看到,和之前的代码相比,我们主要在杂项设备里多注册了一个file_operations
这个结构体,这个结构体是干嘛的呢?我们知道,linux
的精神就是一切东西都是文件,对于驱动的设备也一样,所以也是当做一个文件来操控。
struct file_operations io_ops = { .owner = THIS_MODULE, .release = io_release, .open = io_open,#if 0 .read = irq_read,#endif#if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,18) .unlocked_ioctl = io_ioctl, #else .ioctl = io_ioctl,#endif};
这里看成员的定义就能大胆的猜想,无非是注册了很多个动作对应的函数嘛,例如open
这个设备的时候,会执行io_open
这个函数,ioctl
的时候,会执行io_ioctl
这个函数,close
的时候,会执行io_release
这个函数。
我们简单的写个测试程序验证一下我们的猜想:
/* * @Author: Adam Xiao * @Date: 2021-03-22 16:48:27 * @LastEditors: Adam Xiao * @LastEditTime: 2021-03-25 16:20:37 * @FilePath: /test/test_Adam.c */#include#include #include #include #include #include struct test { int a; short b; char c;};int main(void){ int fd = open("/dev/Adam", O_RDONLY); if (fd < 0) { puts("open fail"); return -1; } struct test t = { 100, 20, 10}; printf("tt:%p\n", &t); int ret = ioctl(fd, 0x11223344, &t); if (ret != 0) { puts("ioctl fail"); } else { printf("ret = %d, t.a = %d, t.b = %d, t.c = %d\n", ret, t.a, t.b, t.c); } close(fd); return 0;}
可以看到,我们打开了我们驱动添加的这个/dev/Adam
这设备,并对他进行了一次ioctl
。看看这个时候,都有什么结果。
# ./test_Adamtt:0xbeecdbd0ret = 0, t.a = 111, t.b = 222, t.c = 33# dmesg[xyy]test driver initdriver information:[xyy] Open called!!!driver information:cmd = 0x11223344driver information:arg:beecdbd0driver information:ret = 0, t.a = 100, t.b = 20, t.c = 10driver information:[xyy] close called!!!
可以看到,当我们open
自己刚刚加载的驱动,/dev/Adam
这设备的时候,内核的确有打印Open Called!!!
。和我们的预期一样。这些函数的确都注册成功了,具体的还有read等函数可以注册,深入的知识这里就不展开了,大家可以自行查阅file_operations
这个结构体,所有对设备能进行的操作,这里都有对应的函数可以进行注册。
copy_from_user
和copy_to_user
细心的同学也许注意到了,驱动层面有了这两个函数非要转换一下,不知道是什么意思。
这里涉及到驱动的另一个重要概念,内核态是不能直接访问应用态数据的地址的,那么怎么办呢?内核提供了copy_from_user
和copy_to_user
这两个接口,作用就是用来在内核态和用户态传递数据。这里就不具体展开了,和我们常用的strcpy
的作用其实是类似的,无非是从内核态搬数据到用户态罢了。
完成了上面的例子,其实已经把驱动是如何在系统注册,如何在应用态打开,如何在应用态设置,如何传递到内核里,然后内核如何把想传递的东西返回给应用。这个过程简单的演示了一下,但是绝大部分驱动其实做的都是这样的事情,无非里面调用的厂家接口更多,完成的业务更复杂而已。
例如点了一个灯,或者从某个接口读出来了数据要返回出来。那么也都是在file_operations
里注册好对应的动作,无论是ioctl
也罢,read
也罢。注册好对应的函数,然后实现功能,通过copy_to_user
传递给应用态罢了。
转载地址:http://wgru.baihongyu.com/