前言

荔枝派nano这块板子,从本章开始,将会发挥它最大的价值,藉由它来带领我们进入嵌入式linux驱动开发的大门。

想必大家在玩linux类型的板子之前应该或多或少的都尝试过其他类型的板载系统的开发,诸如裸跑C语言程序的51单片机;基于Arduino开发套件的ESP8266,亦或是一直大火的STM32等等,不一而足。虽然说芯片不一样,开发模式不一样,但是从中或多或少我们能够窥见针对不同芯片的驱动开发思路,哪怕是一个小小的LED等,在这些不同芯片上的驱动方式或多或少都有相同或者不同的地方。

而基于linux平台的嵌入式驱动开发,则和上面几种驱动开发模式,都不太一样。

我们知道,在进行linux内核编译的时候,我们可以看到有很多文件,然后通过设置arm平台框架和交叉编译链等选项,来使得这些源码文件构建成镜像文件。但是假如我们是设备制作商,现在设计了一款基于linux的板子,也针对linux做了定制化设计并提供了一些扩展提供给用户,其实最简单的方式,无非就是我写好了驱动文件,然后放到内核中和内核文件一起编译,然后烧写到板子上。这种方式非常简便,当需求设计不多的时候,这种做法更直接有效,但是一旦需要实现的驱动设计需求较多而且改动频繁的时候,这种思想显然是费时费力的。

在软件架构领域,目前一直在推崇微服务化等思想,其核心目的就是让服务尽可能的小,尽可能的去中心化,然后来实现各个模块的解耦。而这种思想,其实对于嵌入式linux驱动开发来说,也具有相当强的影响力。与其将驱动和内核绑定在一起,那么有没有什么机制,让驱动实现和内核剥离,驱动可以随意实现,内核只需要加载驱动就行了呢?

答案是肯定的。而且这个设计思想很早就在linux系统上进行了实现,而且保留至今。

在Linux系统中,主要将存储器和外设分为三种类型,即:字符设备、块设备和网络设备。其中字符设备是指那些必须以串行顺序依次进行访问的设备,比如触摸屏,鼠标等等。块设备则可以按照任意顺序进行访问,以块为单位进行操作,比如硬盘,eMMC等。字符设备和块设备的驱动设计有很大的差别,但是对于用户而言,他们都要使用文件系统的操作接口,比如open(),close(),read(),write()等进行访问。而网络设备是面向数据包的传输设计的,并不倾向于对应于文件系统的节点。

接下来,针对这三种类型,说下统一的嵌入式开发思路吧。

linux内核模块结构

一个Linux内核模块主要由以下几个部分组成

 

1. 模块加载函数

当通过insmode或者modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。

Linux内核模块加载函数,一般以__init标记来声明,典型的模块加载函数形式如下:

static int __init hello_init(void)
{
        printk(KERN_EMERG "hello, init\n");
        return 0;
}
module_init(hello_init);

模块加载函数以“module_init(函数名)”的形式被指定。它返回int型。若初始化成功,返回0,初始化失败时,则返回相应的错误编码。在Linux内核中,错误编码是一个接近于0的负值。

在Linux内核中,可以使用request_module函数加载内核模块,驱动开发人员可以通过调用如下代码:

request_module(module_name);

来灵活的加载其他模块。

在Linux中,所有标识为__init的函数,如果直接编译进入内核,成为内核镜像的一部分,在连接的时候,都会放在.init.text这个区段内。

 

2. 模块卸载函数

当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。

典型代码如下:

static void __exit hello_exit(void)
{
        printk(KERN_EMERG "hello, exit\n");
}
module_exit(hello_exit);

模块卸载函数在模块卸载的时候执行,而不返回任何职,且必须以"module_exit"的形式来指定。通常来说,模块卸载函数需要完成与模块加载函数相反的功能。

我们用__exit来修饰模块卸载函数,可以告诉内核如果相关的景象被直接编译进内核(即built-in),则cleanup_function函数会被忽略,直接不链接进最后的景象,既然模块被内置了,就不可能卸载它了,卸载函数也就没有存在的必要了。

 

3. 模块许可证声明

许可证(LICENSE)声明描述内核模块的许可权限,如果不生命LICENSE,模块被加载时,将收到内核被污染(Kenrel Tainted)的警告。在Linux内核模块领域,可以接受的LICENSE包括"GPL","GPL v2","GPC and additional rights","Dual BSD/GPL","Dual MPL/GPL"和"Proprietary"(关于模块是否可以采用非GPL许可权,如"Proprietary",这个在学术界和法律界都有争议)。大多数情况下,内核模块应该遵循GPC兼容协议许可,Linux内核模块最常见的是以MODULE_LICENSE(“GPL v2”)语句声明模块来表明采用GPL v2许可。

 

4. 模块参数(可选)

模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。

我们可以用“module_param(参数名,参数类型,参数读写权限)”为模块定义一个参数,例如下列diamante定义了1个整型参数和1个字符指针参数:

static char * book_name  = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

参数类型可以是byte, short, ushort, int, uint, long, ulong, charp, bool或者invbool(布尔的反),在模块编译时,会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。

除此之外,模块也可以拥有参数数组,形式为"module_param_array"(数组名,数组类型,数组长,参数读写权限).

 

5. 模块导出符号(可选)

内核模块可以导出的符号(symbol,对应于函数或者变量),若导出,其他模块则可以使用本模块中的变量或者函数。

Linux的"/proc/kallsyms"文件对应着内核符号表,它记录了符号以及符号所在的内存地址。

模块可以使用如下宏导出符号到内核符号表中:

EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);

导出的符号可以被其他模块使用,只需要使用前提前声明一下即可。

 

6. 模块作者等信息声明(可选)

在Linux内核模块中,我们可以使用MODULE_AUTHOR,MODULEDESCRIPTION,MODULE_VERSION,MODULE_DEVICE_TABLE,MODULE_ALIAS分别标明模块作者,描述,版本,设备表和别名,例如:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("feixiaoxing");
MODULE_DESCRIPTION("This is just a hello module!\n");

对于USB,PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,以标明该驱动模块所支持的设备。

模块编译

看完了模块的组成,这里我们以一个简单的Hello.c的例子来展开,代码如下:

#include <linux/init.h>
#include <linux/sched.h>
#include <linux/module.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cxsr");
MODULE_DESCRIPTION("a hello module!\n");
 
static int __init hello_init(void)
{
        printk(KERN_EMERG "hello, init\n");
        return 0;
}
 
static void __exit hello_exit(void)
{
        printk(KERN_EMERG "hello, exit\n");
}
 
module_init(hello_init);
module_exit(hello_exit);

之后我们在源码同级目录创建一个Makefile,注意名称大小写, 内容如下:

ifneq ($(KERNELRELEASE),)
obj-m := hello.o
 
else
PWD  := $(shell pwd)
KDIR := /home/scy/linux-mi/linux-f1c100s-480272lcd-test/
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm
clean:
	rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
endif

需要特别注意的是, all命令和clean命令,下面的命令行,前面一定要TAB缩进,不能是空格缩进,如果是空格缩进,会报如下的错误:

Makefile:8: *** missing separator. Stop.

这个时候,我们可以利用如下命令打开Makefile,然后删掉命令行前面的空格,用TAB替代即可:

nano Makefile

这里还需要注意的就是,KDIR目录,一定是我们编译到板子上的linux内核的目录,而all命令中的交叉编译链,必须是我们进行linux内核编译的交叉编译链,否则将会导致模块不能被加载。

完毕之后,在终端输入如下命令:

make

此时,就可以看到模块被制作:

同时,可以看到,原来目录下有俩文件,现在有了多个:

这里,我们需要拷贝到板子上的文件就是hello.ko文件了。

 

ko文件拷贝与安装

文件拷贝,我们可以使用minicom,也可以直接手动拷贝,由于我这里比较懒,就手动拷贝了。

插上usb插卡器,然后执行如下命令,进行拷贝:

sudo mkdir /mnt/sdb2             //创建一个临时目录
sudo mount /dev/sdb2 /mnt/sdb2   //将sdb2挂载到此临时目录
sudo cp hello.ko /mnt/sdb2/media //拷贝到sdb2/media目录下
sudo sync
sudo umount /dev/sdb2

之后,我们就可以拔掉tf卡,插到开发板上,进入media目录,进行模块的安装:

可以看到,我们写的简单的字符驱动成功打印出来了。

如果出现如下错误:

	 insmod: can't insert 'hello.ko': invalid module format

此种错误,则表明内核版本和makefile使用的内核版本不一致,一定要保证板子的linux内核版本和Makefile中的内核版本一致且交叉编译链一致,即可。

参考

《Linux设备驱动开发详解:基于最新的Linux 4.0内核》

posted on 2021-06-12 06:51  程序诗人  阅读(643)  评论(0编辑  收藏  举报