linux驱动开发

发布于:2025-09-11 ⋅ 阅读:(20) ⋅ 点赞:(0)

开发环境搭建

1、gcc的交叉编译工具链,详细看我的另一篇博客。

2、安装常用包

sudo apt install bison flex libssl-dev lzop libncurses-dev

开发板需要烧录一些文件来运行Linux

1、需要一个后缀为bin的文件,这个文件是通过编译u-boot得来的。它的功能是

  1. 加载内核可执行文件到内存运行

  2. 给待运行的内核准备好启动参数

  3. 加载二进制设备树文件到内存

  4. 安装系统

2、Linux内核的裸机可执行文件,由Linux源码编译生成。

3、设备树文件,ARM-Linux内核启动、运行过程中需要一些来自各芯片手册的编程依据,该文件专门用于记录这些依据。设备树文件有两种格式:

  1. .dts、.dtsi:文本形式,便于书写、修改

  2. .dtb:二进制形式,由.dts文件经专门工具处理后生成

4、根分区文件,Linux内核运行成功后,需要运行第一个应用程序(即祖先进程)以及后续其它应用程序。而任何应用程序的运行需要各种文件的支持,如:可执行文件、库文件、配置文件、资源文件。这些文件的持久保存和按路径访问需要外存分区特定文件系统的支持。rootfs就是Linux系统根目录所在的分区,其内包含根分区下众多常用app所需的文件。

Linux源码目录介绍

arch和init目录是和启动有关的目录,drivers和设备驱动相关,fs和文件系统相关,mm是和内存管理相关,net是和网络协议栈相关,kernal和ipc和任务相关,lib目录主要存放公用的一些函数接口,Linux内核没有标准c库,lib可以理解为Linux内核中的标准c库,crypto和security它两个存放了加解密算法它们和安全相关,sound是声音设备驱动的框架,block是外存驱动框架,include存放了内核的头文件。其他的目录大多不是内核源码,比如Documentation存放的是一些文档,scripts存放的是和内核编译相关的东西。

linux内核的编译

一般开发板厂商会给一个sdk工具,这个工具会包括u-boot源码和Linux内核源码和rootfs等等开发需要用到的各种工具以及源码,通常厂商会有说明如何编译linux内核,按照厂商的说明做即可。

Linux驱动开发常用的命令行命令

sudo insmod ./文件名.ko ;将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod;查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod 文件名.ko ;此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件

sudo dmesg -C ;清除内核已打印的信息
sudo dmesg ;查看内核的打印信息

cat /proc/devices;用于查看设备对应的设备号。

sudo mknod 设备文件名 设备种类(c为字符设备,b为块设备)  主设备号  次设备号 ;用来创建设备文件并将设备文件与设备号进行关联,注意设备文件通常存放在/dev目录下,比如sudo mknod /dev/mytest c 289 0;

向Linux内核添加新功能

静态方法

在此举例说明,第一步在Linux内核的驱动目录下char目录下编写hello.c,代码如下。

#include <linux/module.h>
#include <linux/kernel.h>

static int __init myhello_init(void)
{
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
    printk("myhello is running\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	return 0;
}

static void __exit myhello_exit(void)
{
	printk("myhello will exit\n");
}
MODULE_LICENSE("GPL");
module_init(myhello_init);
module_exit(myhello_exit);

第二步,在驱动目录下修改Makfile和Kconfig文件,这里就不介绍怎么修改了,就是向其中加点内容就行,按照里面格式添加就行,很简单。

第三步,在内核顶层目录下make menuconfig一下,选择上那个新功能,最后按照厂商方法编译内核。

动态方法

在此只介绍新功能源码与Linux内核源码不在同一目录结构下的情况,这也是比较推荐的。

第一步:在任意目录中新建一个目录,比如新建的目录为mydriver,在mydriver目录中新建新功能源码的文件,比如hello.c,在里面编写代码,代码同静态方法的代码。然后在mydriver目录中新建Makefile文件,文件内容如下,不过需要适当修改KERNELDIR对应的路径和obj-m对应的文件名,路径必须为Linux内核源码顶层目录的绝对路径。编译时在命令行输入make表示编译出来的ko文件适用于当前Ubuntu系统,make ARCH=arm表示编译出来的ko文件适用于开发板

ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/linux/fs4412/linux-3.14
ROOTFS ?= /opt/4412/rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions

else
obj-m += hello.o


endif

编译多个动态模块方法

ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/linux/fs4412/linux-3.14
ROOTFS ?= /opt/4412/rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions

else
obj-m += hello.o
obj-m += xyz.o
xyz-objs =test.o func.o

endif

动态方法常用命令行命令

sudo insmod ./文件名.ko ;将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod;查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod 文件名.ko ;此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件

sudo dmesg -C ;清除内核已打印的信息
sudo dmesg ;查看内核的打印信息

cat /proc/devices;用于查看设备对应的设备号。

内核模块的信息宏

MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明

MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明

MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名

这些模块信息,用法:

modinfo  模块文件名

给模块传参(用的比较少)

module_param(name,type,perm);//将指定的全局变量设置成模块参数
/*
name:全局变量名
type:
    使用符号      实际类型                传参方式
    bool         bool           insmod xxx.ko  变量名=0 或 1
    invbool      bool           insmod xxx.ko  变量名=0 或 1
    charp        char *         insmod xxx.ko  变量名="字符串内容"
    short        short          insmod xxx.ko  变量名=数值
    int          int            insmod xxx.ko  变量名=数值
    long         long           insmod xxx.ko  变量名=数值
    ushort       unsigned short insmod xxx.ko  变量名=数值
    uint         unsigned int   insmod xxx.ko  变量名=数值
    ulong        unsigned long  insmod xxx.ko  变量名=数值
perm:给对应文件 /sys/module/name/parameters/变量名 指定操作权限,一般都是给传0664。
    #define S_IRWXU 00700
    #define S_IRUSR 00400
    #define S_IWUSR 00200
    #define S_IXUSR 00100
    #define S_IRWXG 00070
    #define S_IRGRP 00040
    #define S_IWGRP 00020
    #define S_IXGRP 00010
    #define S_IRWXO 00007
    #define S_IROTH 00004
    #define S_IWOTH 00002  //不要用 编译出错
    #define S_IXOTH 00001
*/

给数组传参

module_param_array(name,type,&num,perm);
/*
name、type、perm同module_param,type指数组中元素的类型
&num:存放数组大小变量的地址,可以填NULL(确保传参个数不越界)
    传参方式 insmod xxx.ko  数组名=元素值0,元素值1,...元素值num-1  
*/

例如

#include <linux/module.h>
#include <linux/kernel.h>


int a=10;
char *astr="hello";
int garr[5]={1,2,3,4,5};

module_param(a,int,0664);
module_param(astr,0664);
module_param_array(garr,int,NULL,0664);

static int __init myhello_init(void)
{
    printk("%d\n",a);
    printk("%s\n",astr);
    for(int i=0;i<5;i++)
    {
        printk("%d\n",garr[i]);
    }
    printk("\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
    printk("myhello is running\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	return 0;
}

static void __exit myhello_exit(void)
{
	printk("myhello will exit\n");
}
MODULE_LICENSE("GPL");
module_init(myhello_init);
module_exit(myhello_exit);

使用方式

sudo insmod ./hello.ko a=100 astr="hi" garr=5,6,7,8,9

模块依赖(详细请看书18-19页)

模块依赖需要用到的宏有EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL(),前者用于导出变量,后者用于导出函数。导出后在使用它们时只需要extern一下即可,比如extern int expval;extern void expfun(void);

注意:如果模块a用到了模块b,即模块a依赖于模块b,那么要在Makefile先编译b再编译a,先加载b模块再加载a模块,先卸载a模块再卸载b模块。如果模块a和模块不在同一目录那么先编译b,然后会有一个后缀为.sysvers的文件,将这个文件拷贝到模块a的目录中再编译模块a。

EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()会将其变量或函数名添加到内核符号表中以实现内核模块间符号的共享。

设备号

可以参考书32-33页代码看看如何使用下面的各种函数和宏。

设备号是32位的,前12位是主设备号,后20位是次设备号。主设备号用来表示同类设备,它们共用一套驱动,次设备号用来表示具体设备,不同设备会有不同的次设备号。

与设备号相关的宏

MKDEV;用来将主设备号和次设备号合成设备号,返回类型为dev_t。

MAJOR;输入设备号返回主设备号。

MINOR;输入设备号返回次设备号

与设备号相关的命令行

sudo mknod 设备文件名 设备种类(c为字符设备,b为块设备)  主设备号  次设备号 ;用来创建设备文件,注意设备文件通常存放在/dev目录下,比如sudo mknod /dev/mytest c 289 0;

与设备号相关的函数

int mknod(const char *pathname,mode_t mode,dev_t dev);和上面一样用于创建设备文件,参数和上面的一样,返回值为0和-1,0表示成功,-1表示失败。

int register_chrdev_region(dev_t from, unsigned count, const char *name);静态申请设备号,from:自己指定的设备号,count:申请的设备数量,name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号,返回值:成功为0,失败负数,绝对值为错误码。

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name);动态申请设备号,dev:分配设备号成功后用来存放分配到的设备号,baseminior:起始的次设备号,一般为0,count:申请的设备数量,name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号,返回值:成功为0,失败负数,绝对值为错误码。

void unregister_chrdev_region(dev_t from, unsigned count);释放设备号,from:已成功分配的设备号将被释放,count:申请成功的设备数量。

关联设备和设备操作函数,将设备和设备号关联起来以及不关联起来。

void cdev_init(struct cdev *cdev,const struct file_operations *fops);初始化cdev设备,给cdev设备赋予操作函数,对于cdev和file_operations结构体在下面列举出来了。

int cdev_add(struct cdev *p,dev_t dev,unsigned int count);将指定字符设备关联设备号并添加到内核,p:指向被添加的设备,dev:设备号,count:设备数量,一般填1。

void cdev_del(struct cdev *p);从内核中移除一个字符设备并将设备和设备号取消关联,p:指向被移除的字符设备。

cdev_init可能会参考下面的结构体

struct cdev
{
    struct kobject kobj;//表示该类型实体是一种内核对象
    struct module *owner;//填THIS_MODULE,表示该字符设备从属于哪个内核模块
    const struct file_operations *ops;//指向空间存放着针对该设备的各种操作函数地址
    struct list_head list;//链表指针域
    dev_t dev;//设备号
    unsigned int count;//设备数量
};

struct file_operations 
{
   struct module *owner;           //填THIS_MODULE,表示该结构体对象从属于哪个内核模块
   int (*ope/n) (struct inode *, struct file *);    //打开设备
   int (*release) (struct inode *, struct file *);    //关闭设备
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);    //读设备
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备
   loff_t (*llseek) (struct file *, loff_t, int);        //定位
   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
   unsigned int (*poll) (struct file *, struct poll_table_struct *);    //POLL机制,实现多路复用的支持
   int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层
   int (*fasync) (int, struct file *, int); //信号驱动
   //......
};

使用驱动函数

应用层调用open函数-》内核中的syscall_open函数-》内核驱动中的drv_open函数


网站公告

今日签到

点亮在社区的每一天
去签到