前言
(1)个人邮箱:zhangyixu02@gmail.com
(2)本文用于从未接触过Linux驱动,但是了解Linux命令行操作,电脑上已经装配好Ubuntu系统的同学学习使用。
(3)微信公众号:风正豪
几种Linux源码的区别
(1)做Linux驱动开发肯定需要对Linux的几种源码由一个简单的了解,一般来说,Linux的源码分为三种,Linux官网的源码,当前使用的半导体厂家的源码,当前使用的开发板厂家的源码。
<1>Linux官网下的Linux源码是由linus引导下的Linux源码,这源码是给内核开发者或者半导体厂商使用的。
<2>例如正点原子,北京迅为,韦东山,野火这些开发板厂家,他们会根据半导体厂家退出的官方开发板以及源码,自己生产一款开发板。
<3>对于一些公司,他们不具备定制开发板的能力,于是会购买开发板厂家的板卡然后再自己根据需求扩展业务。
Linux驱动运行
Linux驱动两种运行
(1)Linux驱动有两种运行方式,第一种是将驱动程序编译进入Linux内核,第二种是将驱动成内核模块。
(2)编译进入内核:
<1>当驱动程序编写完成之后,我们可以根据需求是否决定是否要编译进入内核。如果编译进入内核,那么当Linux内核启动的时候会自动加载驱动程序。
<2>对于使用频率较少或者是暂时不需要使用的驱动程序,不建议编译进入内核。因为这样会增大Linux内核体积,拖慢Linux启动速度。这个就好比某一些流氓软件,强行在你的windows电脑中设置成开机自启动。每次打开电脑,电脑就会告诉你,成功击败全国1%的用户。
<3>作为Linux驱动的初学者,不建议将驱动编译进入内核,因为每次编译内核,重新烧录内核是一个非常麻烦的过程,需要重启系统。初学者编写驱动程序,肯定会经常出问题,就算是老手也不能保证一次成功。所以将驱动程序编译进入内核的事情,要等你百分之百确定这个驱动可行的时候,而且需要经常使用这个驱动程序的时候,再进行讨论。
(3)Linux内核模块:
<1>Linux内核模块是Linux系统中的一种特殊的机制,可以将一些使用频率很少,或者暂时不适用的功能编译成模块,需要的时候再进行加载进入内核。
<2>使用内核模块可以减少内核的体积,加快Linux启动速度,并且在系统运行的时候插入或者卸载驱动无需重启系统。内核文件的后缀是.ko。
<3>作为入门选手,肯定是只会使用到这个功能的,所以上面哪个等我们实力达标的时候再进行了解也不迟。
内核模块加载和卸载指令
insmod加载命令
(1)一般我们常用的驱动加载,命令是insmod 模块名。
(2)例如本文的模块名字为hello_drv.ko。那么我们将会使用insmod hello_drv.ko命令加载驱动。
(3)加载驱动会自动触发驱动文件中module_init()这个宏所定义的函数里面。(注意,这里涉及到后面的内容,看不懂很正常,先简单提醒一下)
insmod hello_drv.ko
modprobe加载命令
(1)insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。
(2)但是如果是modprobe 就不会存在这个问题, modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe 命令相比 insmod 要智能一些。
(3)虽然modprobe命令比insmod指令更加智能,但是我本人并不推荐采用modprobe命令。
<1>因为他需要自己在/lib/modules/< kernel-version>目录下创建一个文件,然后把模块都放进去,才可以使用。
<2>不仅如此,modprobe命令的配置也是一个麻烦的过程。对于新手而言,编写的模块根本就不会有什么驱动模块之间的依赖,就算有,也就一两个模块之间有依赖。根本没有必要在这个modprobe加载命令上浪费过多的时间。
rmmod卸载模块
(1)rmmod 模块名,可以用于卸载已经加载的内核模块
(2)例如本文的模块名字为hello_drv.ko。那么我们将会使用rmmod hello_drv或者rmmod hello_drv.ko命令都可以卸载驱动。
(3)卸载驱动会自动触发驱动文件中module_exit()这个宏所定义的函数里面。(注意,这里涉及到后面的内容,看不懂很正常,先简单提醒一下)
rmmod hello_drv.ko
lsmod列出已加载进入Linux的内核模块
(1)输入lsmod可以直接查看到模块加载情况
lsmod
modinfo查看内核信息
(1)modinfo可以查看到作者信息,版本信息,模块位置,编译出来是文件类型。
modinfo hello_drv.ko
实操
驱动程序——hello_drv.c
(1)学习就需要由浅入深。下面是一个最简单的驱动程序。
(2)作为一个最简单的驱动程序,无论如何需要具有如下5个元素
<1>Linux相关头文件引用
<2>入口函数
<3>出口函数
<4>module_init(),这个宏用于确定入口函数是哪一个。当你加载驱动程序的时候,Linux内核会根据这个宏来确定驱动的入口函数。这个宏里面写入口函数的函数名。
<5>module_exit(),这个宏用于确定驱动的出口函数,当你卸载驱动的时候,Linux内核根据这个宏确定驱动的出口函数。这个宏里面写出口函数的函数名。
<6>MODULE_LICENSE(“GPL”); 指定GPL协议,Linux是遵顼GPL的开源协议,如果你要使用Linux的其他函数,就必须遵守GPL协议,如果没有写这一行,编译将不会被通过。
(3)下面这两个可有可无,看你本人心情.
<1>MODULE_AUTHOR()这个宏用于指定这个驱动程序的作者是谁。
<2>MODULE_VERSION()这个宏用于指定当前驱动程序的版本。
(4)
<1>在Linux驱动程序编写的时候,我们打印不能使用printf()函数,只能使用printk()函数。
<2>为什么呢?看下图,在Linux中调用关系如下,printf()函数属于内核上面的库函数部分。肯定都是上层调用下层,怎么可能下层调用上层的。因此,printf()函数不能在驱动程序中使用,驱动程序的打印要使用printk()。
/*******************************************************************************
* @文件名 : 驱动入门
* @作者 : zhangyixu
* @CSDN_ID : qq_63922192
* @CSDN_昵称 : 风正豪
* @日期 : 2023年09月01日
* @TABsize : 一个TAB为4空格
* @编码格式 : UTF-8
* @版权声明 : 仅供参考学习,未经允许禁止商用
********************************************************************************/
#include <linux/module.h>
#include <linux/init.h>
//入口函数
static int hello_init(void)
{
printk("hello world\r\n");
return 0;
}
//出口函数
static void hello_exit(void)
{
printk("good bye\r\n");
}
module_init(hello_init); //确认驱动入口函数
module_exit(hello_exit); //确认驱动出口函数
/*最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加
*这个协议要求我们代码必须免费开源,Linux遵循GPL协议,他的源代码可以开放使用,那么你写的内核驱动程序也要遵循GPL协议才能使用内核函数
*因为指定了这个协议,你的代码也需要开放给别人免费使用,同时可以根据这个协议要求很多厂商提供源代码
*但是很多厂商为了规避这个协议,驱动源代码很简单,复杂的东西放在应用层
*/
MODULE_LICENSE("GPL"); //指定模块为GPL协议
MODULE_AUTHOR("CSDN:qq_63922192"); //表明作者,可以不写
MODULE_VERSION("1.0"); //驱动版本申明,可以不写
Makefile编写
(1)这个Makefile编写新手不用过于纠结,只需要知道如下几点:
<1>KERN_DIR 需要修改成你的Linux内核路径。
<2>obj-m += 驱动文件名.o
<3>配置了交叉编译工具链,如果不清楚自己有没有配置交叉编译工具链,可以输入vim ~/.bashrc,进入VIM编辑器之后,输入: /PATH。如果能够跳转,并且找到如下图的三行文字,就表示交叉工具链已经被配置好了。如果没有进行跳转,说明没有被配置好。那么就按照需求配置好
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihfexport-
PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += hello_drv.o
开始实验
(1)准备好上面两个文件之后,在Ubuntu中输入make进行编译。
(2)
<1>将.ko文件拷贝到开发板中,因为我的/home/book/nfs_rootfs/目录是被挂载到了开发板上,所以不需要拷贝。不过在开发板中需要输入下面这条挂载命令。
<2>需要注意的一点是,这里的开发板需要和Ubuntu能够通过网卡进行互相ping通。192.168.5.11是Ubuntu的IP地址,我们将Ubuntu的/home/book/nfs_rootfs 挂载到开发板的/mnt目录下。因此,当我们在Ubuntu的/home/book/nfs_rootfs/drivers_projects/01_0_hello_simple_drv目录下修改,在开发板的/mnt/drivers_projects/01_0_hello_simple_drv也会被同步修改。
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
(3)
<1>输入insmod hello_drv.ko装载驱动,即可看到hello world的打印信息。因为我们装载驱动的时候,会根据module_init()这个宏进入hello_init()函数,然后打印hello world。
<2>如果装载驱动的时候没有看到hello world的打印信息,就说明没有打开内核打印。需要输入echo “7 4 1 7” > /proc/sys/kernel/printk。
<3>我们进行玩实验之后,会发现打开内核打印之后界面会弹出一大堆的打印信息,看的很烦人,于是我们可以输入echo “0 4 1 7” > /proc/sys/kernel/printk 关闭内核打印。
echo "0 4 1 7" > /proc/sys/kernel/printk #关闭内核打印
echo "7 4 1 7" > /proc/sys/kernel/printk #打开内核打印
insmod hello_drv.ko
(4)输入lsmod即可查看驱动的加载情况。
(5)输入rmmod hello_drv.ko装载驱动,即可看到good bye的打印信息。因为我们卸载驱动的时候,会根据module_exit()这个宏进入module_exit()函数,然后打印good bye。
(6)输入lsmod即可查看驱动的卸载情况。