先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题
一 前言
在正式开始今天的文章之前,先谈谈个人学习linux开发中的心得,将自己学习过程中遇到的问题呈现到大家面前,以便给大家一些启发,让大家少走一些弯路。
之前我学习linux开发的过程,基本是沿用自己学习单片机开发的路径来的:准备好开发板,准备好开发环境,准备好芯片手册pdf,就可以开始开发学习了。我想大多数从单片机开发,嵌入式开发转到嵌入式linux开发,应该所走的路都跟我相似。
但是这条路好像在嵌入式linux开发的过程中实现起来会比较困难,尤其是对于初学者来说,当然对于一些高手来讲,如果按照嵌入式单片机的开发路径,那么他完全可以称为嵌入式linux开发的全栈工程师了,这种人要么是公司的技术大拿,要么就是公司的底层框架设计师了,业务能力水平那是相当厉害了。
但是,我想对初学者来说,我们不适合这样的路径,因为这样会让我们的精力很分散,不能精通于某个角度的学习。还是拿我自己举例,我在开始进入linux内核模块编程之前,大量的时间花在了如何在一个嵌入式开发板平台上搭建好linux环境,从uboot,busybox,再到linux内核移植,我花了很长很长时间才能搭建好这个环境,期间遇到一些自己明白并且能搞定的,也遇到一些自己不明白但是能搞定的,还有一些自己不明白也没法搞定的,最害怕的就是最后一种情况,自己不明白又搞不定,完全卡壳在这里,因为linux环境还没跑起来啊,也就无法做linux内核模块编程了,搞到自己相当郁闷,也开始怀疑自己的业务能力。后期,没有办法,在各大论坛,qq群潜水、提问题、发文章,最后虽然解决了问题,但是对于自己的linux内核模块编程来说,真的是事倍功半,或者说因为是野路子出生,很多基础知识不扎实。
希望我的这些弯路能给大家一些启发,在大家开始看接下来的内容之前,先反思一下自己的学习方法是否正确。当然,我给大家的建议是,如果是做linux内核模块编程的话,先不要把时间花在uboot,如何移植linux搭建linux环境上,也不要在开发板上学习,最简单方便实惠的方法就是创建好本地ubuntu虚拟机后,直接基于此开始linux内核模块编程的学习吧。
二 what
先问自己一个问题:什么是字符驱动设备?或者先抛去字符两字,什么是linux系统驱动?
linux系统驱动是用户访问底层硬件设备的桥梁,它将用户访问的底层硬件进行封装,使得用户不必关心底层硬件的操作,用户层将这些设备完全当做文件来进行读写等操作。
针对这些各种各样的设备驱动,linux系统将它们分为了三类:字符设备,块设备,网络设备(后两个在本篇中将不做介绍),如下图
字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
三 why
- 为什么需要字符驱动设备程序?我自己写一个驱动不就好了吗?
我想这个问题也是做嵌入式单片机开发的人,最容易会问的问题,因为从他们的工作经验来看,一个跑在设备上的驱动完全是可以由他一个人来进行的,很多时候也没有区分用户空间和内核空间,当然很多人如果有一个很好的框架,他是对app程序和驱动程序有一个比较明显的区分的。
越是庞大的系统,越涉及多层次的协作,如果上层应用层开发人员完全没有底层硬件驱动的概念,如果没有这些设备驱动程序,他们将完全不懂如何开发。
在linux系统下,有一句很经典的话,就是"一切皆文件",上层业务开发人员可以把所有需要访问的硬件资源,当成文件一样去操作,那么对于文件的操作一般都有:打开文件、关闭文件、读文件、写文件,所以linux驱动开发需要对这些硬件资源实现这些操作:打开操作、关闭操作、读操作、写操作,linux系统会帮我们封装好这些调用流程,所以这里不得不说linux系统的强大,以字符驱动设备为例,调用流程大致如下所示:
四 how
在正式实现一个字符驱动设备编程之前,先给大家普及一个知识,因为linux内核版本的不断更新,很多接口API的实现以及名字都在不停变化,这样造成的一个结果是,如果你完全不知道自己的内核版本,也不知道我开发时所用的内核版本,直接将我下面的源代码拷贝回去编译的话,会有一大堆报错,相信新手此时也会一头雾水,所以给大家如下建议
1. 先确认自己Ubuntu当前的linux内核版本,在console下输入命令 uname -r,如下面的截图
2. linux内核官网:https://elixir.bootlin.com/linux/v4.15/source
3. 如果你的linux内核版本和我不一致,当你直接拷贝我下面的函数到你的环境下编译出错时,不要慌张,一般都是我调用的API在你的linux内核下面可能没有或者名字不对了,到上面的官网上根据这些关键字搜索即可
4. 再次重申,下面的示例程序,linux内核版本是4.15,请大家一定注意内核版本的差异
再次强调,请大家一定注意内核版本的差异,我下面的示例程序是在4.15的linux内核版本下开发的。
根据之前的文章《linux内核模块编程》,我们知道linux内核编程的一般框架是init和exit
#include <linux/module.h>
#include <linux/kernel.h>
int test_init(void)
{
......
return 0;
}
void test_exit(void)
{
......
}
MODULE_LICENSE("GPL");
module_init(test_init);
module_exit(test_exit);
但是因为我们今天实现的字符驱动设备开发,除了init和exit之外,我们还需要实现open,read,write以及close,今天先实现open,read和close,源代码如下
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
static int major;
static struct class *chardev_class;
static struct device *chardrv_class_dev;
static int chardev_drv_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "chardev_drv_open\n");
return 0;
}
static ssize_t chardev_drv_read (struct file *filp, char __user *buf,
size_t size, loff_t *ppos)
{
printk(KERN_INFO "chardev_drv_read\n");
return 0;
}
int chardev_drv_close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "chardev_drv_close\n");
return 0;
}
static struct file_operations chardev_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = chardev_drv_open,
.read = chardev_drv_read,
.release = chardev_drv_close,
};
static int chardev_drv_init(void)
{
printk(KERN_INFO "already chardev_drv_init\n");
major = register_chrdev(0, "chardev_drv", &chardev_drv_fops);
chardev_class = class_create(THIS_MODULE, "chardrv");
chardrv_class_dev = device_create(chardev_class, NULL, MKDEV(major, 0), NULL, "chardev_drv"); /* /dev/xyz */
return 0;
}
static void chardev_drv_exit(void)
{
printk(KERN_INFO "new exit chardev_drv\n");
unregister_chrdev(major, "chardev_drv");
device_unregister(chardrv_class_dev);
class_destroy(chardev_class);
}
module_init(chardev_drv_init);
module_exit(chardev_drv_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m:=char_dev.o
KDIR:= /lib/modules/$(shell uname -r)/build
PWD:= $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
五 test
- 编译make
- 加载驱动
sudo insmod char_dev.ko
- 查看打印
demsg | tail
,发现内核加载成功
- 因为我们创建了一个字符驱动设备,这个字符驱动设备在/dev路劲下,开头的"c"表示字符设备
- 卸载`sudo rmmod char_dev
- 再次查看/dev,
ls -al | grep timer
,发现已经没有这个字符设备了。