00. 目录
01. Linux设备分类
Linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。 在windows系统中,设备大家很好理解,像硬盘,磁盘指的是实实在在硬件。 而在文件系统的linux下面,都有对于文件与这些设备关联的,访问这些文件就可以访问实际硬件。 像访问文件那样去操作硬件设备,一切都会简单很多,不需要再调用以前com,prt等接口了。 直接读文件,写文件就可以向设备发送、接收数据。 按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。
字符设备:指应用程序按字节/字符来读写数据的设备。 这些设备节点通常为传真、虚拟终端和串口调制解调器、键盘之类设备提供流通信服务, 它通常不支持随机存取数据。字符设备在实现时,大多不使用缓存器。系统直接从设备读取/写入每一个字符。 例如,键盘这种设备提供的就是一个数据流,当你敲入“cnblogs”这个字 符串时, 键盘驱动程序会按照和输入完全相同的顺序返回这个由七个字符组成的数据流。它们是顺序的,先返回c,最后是s。
块设备:通常支持随机存取和寻址,并使用缓存器。 操作系统为输入输出分配了缓存以存储一块数据。当程序向设备发送了读取或者写入数据的请求时, 系统把数据中的每一个字符存储在适当的缓存中。当缓存被填满时,会采取适当的操作(把数据传走), 而后系统清空缓存。它与字符设备不同之处就是,是否支持随机存储。字符型是流形式,逐一存储。 典型的块设备有硬盘、SD卡、闪存等,应用程序可以寻址磁盘上的任何位置,并由此读取数据。 此外,数据的读写只能以块的倍数进行。
网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。
Linux内核中处处体现面向对象的设计思想,为了统一形形色色的设备,Linux系统将设备分别抽象为struct cdev, struct block_device,struct net_devce三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作
字符设备驱动程序适合于大多数简单的硬件设备,而且比起块设备或网络驱动更加容易理解, 因此我们选择从字符设备开始,从最初的模仿,到慢慢熟悉,最终成长为驱动界的高手。
02. 字符设备抽象
Linux内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象, cdev记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations), 在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的cdev, 当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。
C语言中没有面向对象语言的继承的语法,但是我们可以通过结构体的包含来实现继承,这种抽象提取了设备的共性, 为上层提供了统一接口,使得管理和操作设备变得很容易。
在硬件层,我们可以通过查看硬件的原理图、芯片的数据手册,确定底层需要配置的寄存器,这类似于裸机开发。 将对底层寄存器的配置,读写操作放在文件操作接口里面,也就是实现file_operations结构体。
其次在驱动层,我们将文件操作接口注册到内核,内核通过内部散列表来登记记录主次设备号。
在文件系统层,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件的文件操作接口来设置底层寄存器
实际上,在Linux上写驱动程序,都是做一些“填空题”。因为Linux给我们提供了一个基本的框架, 我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。有句成语工欲善其事,必先利其器, 在理解这个框架之前我们得花点时间来学习字符设备驱动相关概念及数据结构。
03. 硬件原理图分析
当GPIO4_C6的引脚为高电平的时候,蜂鸣器响,当GPIO4_C6的引脚为低电平的时候,蜂鸣器不响。
04. 芯片手册分析
参考文档: Rockchip_RK3399TRM_V1.4_Part-20170408.pdf
因为我们的内核里面已经有了蜂鸣器的驱动了,像复用关系的寄存器,电气属性的寄存器,就可以不用设置了,直接设置数据寄存器就可以了,我们在此文档中查找引脚 CPIO4_C6,如下图所示:
通过查询 Rockchip_RK3399TRM_V1.4 手册 1.1 Address Mapping 章节可知,CPIO4 的基地址为 FF79_0000,如下图所示:
然后在 Rockchip_RK3399TRM_V1.4 手册里面找到 Chapter 20 GPIO 章节的 20.4.1,我们找到 Registers Summary 表格,通过表格我们可知,数据寄存器和方向寄存器的地址偏移地址分别为 0x0000 和 0x0004,如下图所示:
通过计算我们可以得到,CPIO4 的数据寄存器的地址为 0xFF790000+0x0000 等于 0xFF790000,有了数据寄存器的地址我们就可以给他赋值了。
在驱动开发调试过程,我们可以通过 io 命令访问寄存器,如需判断 iomux, gpio direction/电平,提高/减小驱动强度,使能施密特触发,这是一个高效又准确的调试手段。
但是需要注意的一点是,访问寄存器的前提是 clk 已经被使能。否则通过 io 命令无法设置某 gpio 的电平值,是因为其 clock 没有打开。 pinctrl 驱动为了省功耗,设计理念是有使用才打开 clock。
使用命令 cat /sys/kernel/debug/clk/clk_summary |grep gpio 查看 GPIO4 的时钟是否被使能,如下图所示
[root@rk3399:/]# cat /sys/kernel/debug/clk/clk_summary |grep gpio
pclk_gpio4 1 1 100000000 0 0
pclk_gpio3 0 1 100000000 0 0
pclk_gpio2 0 1 100000000 0 0
pclk_gpio1_pmu 2 1 48285715 0 0
pclk_gpio0_pmu 1 1 48285715 0 0
[root@rk3399:/]#
其中,1表示使能时钟了,0 为没有使能时钟,需要打开,使用命令 echo 1 > /sys/kernel/debug/clk/pclk_gpio4/clk_enable_count
刚才我们已经通过数据手册,确定了 GPIO4 的数据寄存器的地址为 0xFF790000,使用 io 命令查看这个寄存器的值为 42000000,如下图所示:
[root@rk3399:/]# io -4 -r 0xff790000
ff790000: 62000000
[root@rk3399:/]#
使用命令 io -4 -r 0xff790000 0x42000000 把数据寄存器设置成 0x42000000,蜂鸣器即可发出响声,如下图所示:
[root@rk3399:/]# io -4 -r 0xff790000 0x42400000
[root@rk3399:/]#
05. 蜂鸣器驱动示例
test.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define GPIO5_DR 0xff790000
unsigned int *vir_gpio5_dr;
int my_open(struct inode *inode, struct file *fp)
{
printk("open......\n");
return 0;
}
ssize_t my_read(struct file *fp, char __user *puser, size_t size, loff_t *off)
{
printk("read......\n");
return 0;
}
ssize_t my_write(struct file *fp, const char __user *puser, size_t size, loff_t *off)
{
char kbuf[64] = {0};
if (copy_from_user(kbuf, puser, size) != 0)
{
printk("copy_from_user failed.....\n");
return -1;
}
printk("kbuf: %d\n", kbuf[0]);
if (1 == kbuf[0])
{
*vir_gpio5_dr = 0x4a400000;
}
else if (0 == kbuf[0])
{
*vir_gpio5_dr = 0x4a000000;
}
printk("write......\n");
return 0;
}
int my_close(struct inode *inode, struct file *fp)
{
printk("close......\n");
return 0;
}
static struct file_operations misc_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_close,
};
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "misc_dev",
.fops = &misc_fops,
};
static int __init test_init(void)
{
int ret = 0;
printk("test_init.....\n");
//注册杂项设备
ret = misc_register(&misc_dev);
if (0 != ret)
{
printk("misc_register failed.....\n");
return 1;
}
printk("misc_register OK....\n");
//将物理地址转化为虚拟地址
vir_gpio5_dr = ioremap(GPIO5_DR, 4);
if (NULL == vir_gpio5_dr)
{
printk("GPIO5_DR ioremap is error....\n");
return EBUSY;
}
return 0;
}
static void __exit test_exit(void)
{
//注销杂项设备
misc_deregister(&misc_dev);
iounmap(vir_gpio5_dr);
printk("test_exit......\n");
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jin.Deng");
MODULE_INFO(intree, "Y");
app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int ret = -1;
int fd = -1;
char buf[32];
fd = open("/dev/misc_dev", O_RDWR);
if (-1 == fd)
{
perror("open");
return 1;
}
while(1)
{
buf[0] = 1;
buf[1] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
}
close(fd);
return 0;
}
运行结果
[root@rk3399:/mnt/kernel/5th/5buzzer]# insmod test.ko
[ 753.759866] test_init.....
[ 753.762948] misc_register OK....
[root@rk3399:/mnt/kernel/5th/5buzzer]# ./app
[ 755.098327] open......
[ 755.100757] kbuf: 1
[ 755.102957] write......
[ 756.105846] kbuf: 0
[ 756.108033] write......
[ 757.110916] kbuf: 1
[ 757.113111] write......
[ 758.115982] kbuf: 0
[ 758.118181] write......