目录 一、驱动认知 1.1 为什么要学习写驱动 1.2 文件名与设备号 1.3 open函数打通上层到底层硬件的详细过程 二、基于框架编写驱动代码 2.1 编写上层应用代码 2.2 修改内核驱动框架代码 2.3 部分代码解读 2.3.1 static的作用 2.3.2 结构体成员变量赋值方式 2.3.3 结构体file_operations(最终加载到内核驱动链表) 2.3.4 手动生成设备 三、驱动代码编译和测试 3.1 驱动框架的模块编译并发送至树莓派 ①Makefile内添加生成.o命令 ②模块编译生成.ko文件 ③把.ko文件发送至树莓派 3.2 上层代码交叉编译发送至树莓派 3.3 树莓派装载驱动并运行 ①树莓派加载内核驱动(insmod) ②运行上层代码(无权限) ③增加访问权限再运行 ④检查是否执行成功:demsg指令查看内核打印信息
一、驱动认知 1.1 为什么要学习写驱动 树莓派开发简单是因为有厂家提供的wiringPi库,实现超声波,实现继电器操作,做灯的点亮…都非常简单。 但未来做开发时,不一定都是用树莓派,则没有wiringPi库可以用。但只要能运行Linux,linux的标准C库一定有。 学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路图…就能做开发。 用树莓派学习的目的不仅是为是体验其强大便捷的wiringPi库,更要通过树莓派学会linux内核开发,驱动编写等,做一个属于自己的库。 1.2 文件名与设备号 linux一切皆为文件,其设备管理同样是和文件系统紧密结合。在目录/dev下都能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动,那么open怎样区分这些硬件呢? 依靠文件名与设备号。在/dev下ls -l可以看到 设备号又分为:主设备号用于区别不同种类的设备;次设备号区别同种类型的多个设备。 内核中存在一个驱动链表,管理所有设备的驱动。 驱动开发无非以下两件事:
编写完驱动程序 加载到内核 用户空间open后,调用驱动程序(驱动程序就是操作寄存器来驱动IO口,单片机51,32就是这种操作)
驱动插入到链表的位置(顺序)由设备号检索。 1.3 open函数打通上层到底层硬件的详细过程 用户空间调用open(比如open("/dev/pin4",O_RDWR))产生一个软中断(中断号是0x80),进入内核空间调用sys_call,这个sys_call在内核里面是汇编的,用Source Insight搜索不到。
sys_calll真正调用的是sys_open(属于VFS层虚拟文件系统,因为磁盘的分区和引脚分区不一样,为了实现上层统一化),根据你的设备名比如pin4去到内核的驱动链表,根据其主设备号与次设备号找到相关驱动函数。
调用驱动函数里面的open,这个open就是对寄存器的操作,从而设置IO口引脚电平。这件事对于单片机来说特变容易,就两句话搞定:
sbit pin4 = P1^4;
pin4=1;
(对应下图的粉色笔迹) 二、基于框架编写驱动代码 2.1 编写上层应用代码 目的是用简单的例子展示从用户空间到内核空间的整套流程。
根据上面提到的驱动认知,有个大致的概念,以open为例子: 上层open→sys_call→sys_open→内核驱动链表节点→执行节点里的open 当然,没有装载驱动的话这个程序执行一定会报错。只有在内核装载了驱动并且在/dev下生成了“pin4”这样一个设备才能运行。
接下来介绍最简单的字符设备驱动框架。
2.2 修改内核驱动框架代码 所谓框架,就是在往驱动链表里面加驱动的时候要符合内核规则,它是定死的东西,基本的语句必须要有,少一个都不行。
虽然有这么多的代码,但核心运行的就两个printk。
zd@ubuntu:~/SYSTEM/linux-rpi-4.14.y/drivers/char$ vi pin4driver2.c
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/io.h>
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno;
static int major =231;
static int minor =0;
static char *module_name="pin4";
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n");
return 0;
}
static ssize_t pin4_write(struct file *file1,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_write\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major,minor);
ret = register_chrdev(major, module_name,&pin4_fops);
pin4_class=class_create(THIS_MODULE,"myfirstdemo");
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name);
}
module_init(pin4_drv_init);
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
2.3 部分代码解读 2.3.1 static的作用 内核代码数量庞大,为了防止与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。内核源码里面运用了大量的static,因为内核源码众多,一万五千多个C文件,很容易造成代码命名的冲突。
2.3.2 结构体成员变量赋值方式
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
这是内核代码中常见的对结构体的操作方式,单独给指定结构体某些元素赋值。
注意:在keil的编译工具环境中不允许这样写,linux可以。
2.3.3 结构体file_operations(最终加载到内核驱动链表) 在SourceInsight中查看结构体file_operations,可以发现很多的函数指针(指向函数的指针,函数内进行一些程序的执行),这些函数名跟系统上层对文件的操作差不多。(read,write,llseek)(在课程视频9:36) 如果上层想要实现read,就复制过来,按照格式改一改就能使用。 上层对应底层,上层想要用read,底层就要有read的支持。
2.3.4 手动生成设备 框架中有自动生成设备的代码,那么手动生成设备是怎么样的呢?(一般不这样干,麻烦,仅作为演示)
进入/dev目录,查看帮助可知道创建规则 sudo mknod 设备名称 设备类型 主设备号 次设备号 使用如下命令创建名称为zhu,主设备号为8,次设备号为1的字符设备。
sudo mknod zhu c 8 1
用 ls -l可以看到已经创建成功 三、驱动代码编译和测试 3.1 驱动框架的模块编译并发送至树莓派 在ubuntu中,进入Linux内核源码(前一章节编译好的)字符设备驱动目录linux-rpi-4.14.y/drivers/char(IO口属于字符设备驱动)。进入源码目录下的原因是,写驱动必须要链接到源码(源码定义好了结构体等等),必须要有源码。 拷贝上文分析过的驱动框架代码,拿到这个文件夹下 ,并创建成名字为pin4drive.c的文件
①Makefile内添加生成.o命令 进行配置,使得工程编译时可以编译这个文件 当然不一定要放在/char下。但注意:放在哪个文件夹下,就修改那个文件夹的Makefile即可。
模仿这些文件的编译方式,以编译成模块的形式(还有一个方式为编译进内核)编译pin4drive.c 在Makefile里面添加:
obj-m += pin4drive2.o
m就是模块的形式 ②模块编译生成.ko文件 之前编译内核镜像的时候用的是这个命令: 现在只需进行模块编译,不需要生成zImage,dtbs文件; 回到源码目录/linux-rpi-4.14.y再执行下面指令 注:如果说编译中途提示出错,照着错误提示去修改.c文件即可,和上层编译类似。 编译完成生成的一些文件如下: ③把.ko文件发送至树莓派
scp pin4drive.ko pi@192.168.101.19:/home/pi(别人视频的地址)
之前犯的一个小错误是树莓派和ubuntu的ip地址一样,导致连接不上,修改树莓派的ip地址即可
3.2 上层代码交叉编译发送至树莓派 拷贝上文分析的上层代码到ubuntu中,此处我命名为pin4test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
printf("open failed\n");
perror("reson");
}else{
printf("open success\n");
}
fd = write(fd,'1',1);
return 0;
}
使用交叉编译工具在Linux进行编译
arm-linux-gnueabihf-gcc pin4drivertest.c -o pin4test
发送至树莓派
scp pin4test pi@192.168.101.19:/home/pi
3.3 树莓派装载驱动并运行 ①树莓派加载内核驱动(insmod)
sudo insmod pin4drive2.ko
查看是否已经成功添加驱动
可以去设备下查看
ls /dev/pin4 -l
看到驱动添加成功,主设备号231,次设备号0,和内核里面的代码对应上。 或者lsmod查看内核挂载的驱动
如果需要卸载驱动,就sudo rmmod pin4drive
②运行上层代码(无权限)
./pin4test
发现没有对设备pin4的访问权限 crw是超级用户所拥有的权限,而框中两类用户则无读写的权限(下面有详细说明) ③增加访问权限再运行 解决方法1:加超级用户
sudo ./pin4test
解决方法2:增加“所有用户都可以访问的权限”(建议)
sudo chmod 666 /dev/pin4
运行成功: 拓展 >> chmod 命令用于更改文件/文件夹的属性(读,写,执行)
permission to: user(u) group(g) other(o)
/ˉˉˉ\ /ˉˉˉ\ /ˉˉˉ\
octal: 6 6 6
binary: 1 1 0 1 1 0 1 1 0
what to permit: r w x r w x r w x
what to permit - r: read, w: write, x: execute
permission to - user: the owner that create the file/folder
group: the users from group that owner is member
other: all other users
EG: chmod 744 仅允许用户(所有者)执行所有操作,而组和其他用户只允许读。
④检查是否执行成功:demsg指令查看内核打印信息 用dmesg命令显示内核缓冲区信息,并通过管道筛选与pin4相关信息
dmesg | grep pin4
|