博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ARM Linux 驱动入门及手把手教你简单驱动书写
阅读量:111 次
发布时间:2019-02-26

本文共 13247 字,大约阅读时间需要 44 分钟。

0. 本文会讲解的内容

  1. 自己写驱动需要的材料
  2. 简单的介绍编译驱动的Makefile文件
  3. 写一个简单的驱动,并告知如何查看是否正确加载
  4. 介绍驱动常用的几个回调指针
  5. 写一个驱动,使用ioctl,将应用态和内核态进行交互

1. 自己写驱动需要的材料

  • 对应平台的交叉工具链,本文中的工具链是(arm-at91-linux-gnueabi-gcc)
  • 对应平台的可编译的内核,即进行过menuconfig的内核源码。(与网上下的内核源码包的主要区别是menuconfig过后会生成一个.config文件)

2. 简单的介绍编译驱动的Makefile文件

驱动的Makefile不像是应用层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

简单讲讲这个Makefile如何用

这里需要修改的总共就三处

  1. 就是obj-m后面的.o的文件名字,改成对应的你自己驱动的.c文件的名字;
  2. KERNELDIR改成linux内核源码的文件夹地址即可,记住,要是menuconfig过的。
  3. 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里的这些值进行了赋值,将不同的目标文件进行有规则的分类。俗称内核裁剪,这部分不在这里过多赘述。

3. 写一个简单的驱动,并告知如何查看是否正确加载

/* * @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这个数字,后面会详细讲解这两个数字的含义。

4. 介绍驱动程序的接口,以及常用的几个回调指针

上面的驱动能够简单的运行起来,说明我们的框架是正确的。那么简单的讲解一下,内核里面的一些特有接口。

4.1 MODULE_AUTHOR系列宏

如同代码中最后部分有一些这样的宏,其实就是说明你的驱动的归属。

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 章"许可条款"中提到的, 内核开发者不会热心帮助在加载了私有模块后遇到问题的用户。

总之就是鼓励大家还是有开源精神。

4.2 module_initmodule_exit

这两个本质也是宏。用C++的概念理解这个,可以当成构造函数和析构函数去理解,驱动加载的时候运行的函数,用module_init这个接口注册,驱动卸载的时候运行的函数,用module_exit这个接口注册。那么在驱动进行对应的操作的时候,对应的函数就会被运行。

这两个函数,相当于驱动的入口和出口函数。

4.3 misc_registermisc_deregister

这里先介绍两个概念,杂项设备(misc_device)和字符设备(char_device)。

几种设备类型

  • 杂项设备 misc_device

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux内核的include/linux目录下有Miscdevice.h文件,要把自己定义的miscdevice从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于miscdevice,其实misc_register就是用主标号10调用register_chrdev()的。

也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。

  • 字符设备(char_device)

使用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

  1. DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,就需要先用mkdir在dev目录下新建一个目录;
  2. b和c 分别表示块设备和字符设备:
    b表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;
    c表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据;
  3. MAJOR和MINOR分别表示主设备号和次设备号:
    为了管理设备,系统为每个设备分配一个编号,一个设备号由主设备号和次设备号组成。主设备号标示某一种类的设备,次设备号用来区分同一类型的设备。linux操作系统中为设备文件编号分配了32位无符号整数,其中前12位是主设备号,后20位为次设备号,所以在向系统申请设备文件时主设备号不好超过4095,次设备号不好超过2^20 -1。

这里需要注意的是,mknod的主次设备号需要与驱动代码中注册的时候,保持一致。

5. 写一个驱动,使用ioctl,将应用态和内核态进行交互

上面的驱动虽然是正确的,但是实际上这个驱动什么都没有做。

回到驱动的本质,驱动是要干什么呢?

驱动是要用来操控硬件的

因为用户层没有权限直接操控硬件,只有内核才能做这个事情,所以我们需要一个桥梁,来通知内核,我想操控什么硬件,用这个硬件来读取或者写入什么东西。

一般来说,读取设备或者读取芯片的值之类的东西,厂家都会给接口样例,例如:

at91_set_gpio_value(CPLD_ADDR_4,1);at91_set_gpio_value(CPLD_ADDR_1,1);

类似这样的接口其实一般字面意思就能猜出来是干了什么,上面的两个接口就是给gpio的某个管脚设置了1这个值,反正不是拉高就是拉低了电平嘛。

这种接口只能在内核态用,所以需要写一个驱动,完成一定的功能,然后把这些获取/设置的值给应用态的程序。每个驱动都是做这个事情的。

5.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这个结构体,所有对设备能进行的操作,这里都有对应的函数可以进行注册。

5.2 copy_from_usercopy_to_user

细心的同学也许注意到了,驱动层面有了这两个函数非要转换一下,不知道是什么意思。

这里涉及到驱动的另一个重要概念,内核态是不能直接访问应用态数据的地址的,那么怎么办呢?内核提供了copy_from_usercopy_to_user这两个接口,作用就是用来在内核态和用户态传递数据。这里就不具体展开了,和我们常用的strcpy的作用其实是类似的,无非是从内核态搬数据到用户态罢了。

6. 后续要做的概述

完成了上面的例子,其实已经把驱动是如何在系统注册,如何在应用态打开,如何在应用态设置,如何传递到内核里,然后内核如何把想传递的东西返回给应用。这个过程简单的演示了一下,但是绝大部分驱动其实做的都是这样的事情,无非里面调用的厂家接口更多,完成的业务更复杂而已。

例如点了一个灯,或者从某个接口读出来了数据要返回出来。那么也都是在file_operations里注册好对应的动作,无论是ioctl也罢,read也罢。注册好对应的函数,然后实现功能,通过copy_to_user传递给应用态罢了。

7. 参考文档

转载地址:http://wgru.baihongyu.com/

你可能感兴趣的文章