0. 本文会讲解的内容
- 自己写驱动需要的材料
- 简单的介绍编译驱动的Makefile文件
- 写一个简单的驱动,并告知如何查看是否正确加载
- 介绍驱动常用的几个回调指针
- 写一个驱动,使用
ioctl
,将应用态和内核态进行交互
1. 自己写驱动需要的材料
- 对应平台的交叉工具链,本文中的工具链是(arm-at91-linux-gnueabi-gcc)
- 对应平台的可编译的内核,即进行过
menuconfig
的内核源码。(与网上下的内核源码包的主要区别是menuconfig过后会生成一个.config
文件)
2. 简单的介绍编译驱动的Makefile文件
驱动的Makefile不像是应用层makefile那么千奇百怪,可以说驱动的Makefile就是一个固定的模板,复制过来用就完事了。
驱动的Makefile
# Makefile
ifneq ($(KERNELRELEASE),)
obj-m := xyy.o
else
#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.unsigned
clean:
$(RM) -r *.ko *.o .*.cmd .tmp_versions *.mod.c modules.order Module.symvers *.ko.unsigned
endif
简单讲讲这个Makefile如何用
这里需要修改的总共就三处:
- 就是
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 support
obj-$(CONFIG_ARCH_AT91RM9200) += at91rm9200.o at91rm9200_time.o at91rm9200_devices.o
obj-$(CONFIG_ARCH_AT91SAM9260) += at91sam9260.o at91sam926x_time.o at91sam9260_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9261) += at91sam9261.o at91sam926x_time.o at91sam9261_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9G10) += at91sam9261.o at91sam926x_time.o at91sam9261_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9263) += at91sam9263.o at91sam926x_time.o at91sam9263_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9RL) += at91sam9rl.o at91sam926x_time.o at91sam9rl_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9G20) += at91sam9260.o at91sam926x_time.o at91sam9260_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9G45) += at91sam9g45.o at91sam926x_time.o at91sam9g45_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91SAM9X5) += at91sam9x5.o at91sam926x_time.o at91sam9x5_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91SAM9N12) += at91sam9n12.o at91sam926x_time.o at91sam9n12_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91CAP9) += at91cap9.o at91sam926x_time.o at91cap9_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT572D940HF) += at572d940hf.o at91sam926x_time.o at572d940hf_devices.o sam9_smc.o
obj-$(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=y
CONFIG_SYS_SUPPORTS_APM_EMULATION=y
CONFIG_GENERIC_GPIO=y
# CONFIG_ARCH_USES_GETTIMEOFFSET is not set
CONFIG_GENERIC_CLOCKEVENTS=y
CONFIG_KTIME_SCALAR=y
CONFIG_HAVE_PROC_CPU=y
CONFIG_STACKTRACE_SUPPORT=y
CONFIG_HAVE_LATENCYTOP_SUPPORT=y
CONFIG_LOCKDEP_SUPPORT=y
CONFIG_TRACE_IRQFLAGS_SUPPORT=y
CONFIG_HARDIRQS_SW_RESEND=y
CONFIG_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 <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <asm/gpio.h>
#include <asm/uaccess.h>
#include <linux/delay.h>
#include <linux/spinlock.h>
#include <linux/miscdevice.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/timer.h>
#include <linux/timex.h>
#include <linux/rtc.h>
#include <linux/workqueue.h>
#include <asm/atomic.h>
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$make
rm -f -r *.ko *.o .*.cmd .tmp_versions *.mod.c modules.order Module.symvers *.ko.unsigned
make -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.ko
make[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 -laht
crw-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_init
和module_exit
这两个本质也是宏。用C++的概念理解这个,可以当成构造函数和析构函数去理解,驱动加载的时候运行的函数,用module_init
这个接口注册,驱动卸载的时候运行的函数,用module_exit
这个接口注册。那么在驱动进行对应的操作的时候,对应的函数就会被运行。
这两个函数,相当于驱动的入口和出口函数。
4.3 misc_register
和misc_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.ko
mknod /dev/Adam c 234 0
mknod 的标准形式为: mknod DEVNAME {b | c} MAJOR MINOR
- DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,就需要先用mkdir在dev目录下新建一个目录;
- b和c 分别表示块设备和字符设备:
b表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;
c表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据; - 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 <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <asm/gpio.h>
#include <asm/uaccess.h>
#include <linux/delay.h>
#include <linux/spinlock.h>
#include <linux/miscdevice.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/timer.h>
#include <linux/timex.h>
#include <linux/rtc.h>
#include <linux/workqueue.h>
#include <asm/atomic.h>
#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 <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
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_Adam
tt:0xbeecdbd0
ret = 0, t.a = 111, t.b = 222, t.c = 33
# dmesg
[xyy]test driver init
driver information:[xyy] Open called!!!
driver information:cmd = 0x11223344
driver information:arg:beecdbd0
driver information:ret = 0, t.a = 100, t.b = 20, t.c = 10
driver information:[xyy] close called!!!
可以看到,当我们open
自己刚刚加载的驱动,/dev/Adam
这设备的时候,内核的确有打印Open Called!!!
。和我们的预期一样。这些函数的确都注册成功了,具体的还有read等函数可以注册,深入的知识这里就不展开了,大家可以自行查阅file_operations
这个结构体,所有对设备能进行的操作,这里都有对应的函数可以进行注册。
5.2 copy_from_user
和copy_to_user
细心的同学也许注意到了,驱动层面有了这两个函数非要转换一下,不知道是什么意思。
这里涉及到驱动的另一个重要概念,内核态是不能直接访问应用态数据的地址的,那么怎么办呢?内核提供了copy_from_user
和copy_to_user
这两个接口,作用就是用来在内核态和用户态传递数据。这里就不具体展开了,和我们常用的strcpy
的作用其实是类似的,无非是从内核态搬数据到用户态罢了。
6. 后续要做的概述
完成了上面的例子,其实已经把驱动是如何在系统注册,如何在应用态打开,如何在应用态设置,如何传递到内核里,然后内核如何把想传递的东西返回给应用。这个过程简单的演示了一下,但是绝大部分驱动其实做的都是这样的事情,无非里面调用的厂家接口更多,完成的业务更复杂而已。
例如点了一个灯,或者从某个接口读出来了数据要返回出来。那么也都是在file_operations
里注册好对应的动作,无论是ioctl
也罢,read
也罢。注册好对应的函数,然后实现功能,通过copy_to_user
传递给应用态罢了。