学习链接:https://xuesong.blog.youkuaiyun.com/article/details/109522945?spm=1001.2014.3001.5502
Linux 字符设备是Linux 三大设备之一(另外两种是块设备,网络设备),字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备, 本文主要记录字符设备驱动相关知识
一、应用程序和驱动的交互原理
1、驱动就是获取外设、或者传感器数据,控制外设。数据会提交给应用程序。Linux驱动编译既要编写一个驱动,还要我们编写一个简单的测试应用程序,APP。单片机下驱动和应用都是放到一个文件里面,也就是杂糅到一起。Linux下驱动和应用是完全分开的。
用户空间(用户态)和内核空间(内核态):
Linux操作系统内核和驱动程序运行在内核空间、应用程序运行在用户空间。
应用程序想要访问内核资源,怎么办,有三种方法:系统调用、异常(中断)和陷入。
应用程序不会直接调用系统调用,而是通过API函数来间接的调用系统调用,比如POSIX、API和C库等。unix类操作系统中最常用的编程接口就是POSIX。
应用 程序使用open函数 打开一个设备文件。
每个系统调用都有一个系统调用号。
系统调用处于内核空间,应用程序无法直接访问,因此需要“陷入“到内核,方法就是软中断。陷入内核以后还要指定系统调用号。
二、字符设备驱动开发流程
字符设备驱动
应用层: APP1 APP2 ...
fd = open("led驱动的文件",O_RDWR);
read(fd);
write();
close();
-------------------------------------------------------------------------------------------------------------------------
内核层:
对灯写一个驱动
led_driver.c
driver_open();
driver_read();
driver_write();
driver_close();
struct file_operations {
int (*open) (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 *);
int (*release) (struct inode *, struct file *);(close)
}
cdev:
设备号1 设备号2 设备号n
设备驱动1 设备驱动2 .... 设备驱动n
设备号:32位,无符号数字
高12位 :主设备号 :区分哪一类设备
低20位 :次设备号 :区分同类中哪一个设备
-------------------------------------------------------------------------------------------------------------------------
硬件层: LED uart ADC PWM
每个驱动里面都有对应的file_operations
open的过程:
open打开文件,这个文件与底层的驱动的设备号有关系,
通过设备号访问设备驱动中的struct file_operations里面的open函数。
read的过程:
open函数会有一个返回值,文件描述符fd,read函数通过fd
找到驱动中的struct file_operations里面的read函数。
字符设备驱动的注册
int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
功能:注册一个字符设备驱动
参数:
@major:主设备号
:如果你填写的值大于0,它认为这个就是主设备号
:如果你填写的值为0,操作系统给你分配一个主设备号
@name :名字 cat /proc/devices
当注册一个字符设备驱动的时候。
如果成功的话,当你使用cat /proc/devices 命令查看的时候可以看到系统自动分配的主设备号和这个名字
@fops :操作方法结构体
返回值:major>0 ,成功返回0,失败返回错误码(负数) vi -t EIO
major=0,成功主设备号,失败返回错误码(负数)
手动创建设备文件
sudo mknod hello (路径是任意) c/b 主设备号 次设备号
sudo –rf hello 删除的时候记得加-rf
字符设备驱动应用--点亮LED灯
应用程序将数据传递给驱动
int copy_from_user(void *to, const void __user *from, int n)
功能:从用户空间拷贝数据到内核空间(用户需要写数据的时候)
参数:
@to :内核中内存的首地址
@from:用户空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
int copy_to_user(void __user *to, const void *from, int n)
功能:从内核空间拷贝数据到用户空间(用户开始读数据)
参数:
@to :用户空间内存的首地址
@from:内核空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
驱动操作寄存器
rgb_led灯的寄存器是物理地址,在linux内核启动之后,在使用地址的时候,操作的全是虚拟地址。需要将物理地址转化为虚拟地址。在驱动代码中操作的虚拟地址就相当于操作实际的物理地址。
物理地址<------>虚拟地址
void * ioremap(phys_addr_t offset, unsigned long size)
(当__iomen告诉编译器,取的时候是一个字节大小)功能:将物理地址映射成虚拟地址
参数:
@offset :要映射的物理的首地址
@size :大小(字节)(映射是以业为单位,一页为4K,就是当你小于4k的时候映射的区域都为4k)
返回值:成功返回虚拟地址,失败返回NULL((void *)0);
void iounmap(void *addr)
功能:取消映射
参数:
@addr :虚拟地址
返回值:无
软件编程控制硬件
只需要向控制寄存器中写值或者读值,就可以让我们处理器完成一定的功能。
RGB_led
1》GPIOxOUT:控制引脚输出高低电平
RED_LED--->GPIOA28
GPIOAOUT ---> 0xC001A000
GPIOA28输出高电平:
GPIOAOUT[28] <--写-- 1
GPIOA28输出低电平:
GPIOAOUT[28] <--写-- 0
2》GPIOxOUTENB:控制引脚的输入输出模式
GPIOAOUTENB ---> 0xC001A004
设置GPIOA28引脚为输出模式:
GPIOAOUTENB[28] <--写-- 1
3》GPIOxALTFN:控制引脚功能的选择
GPIOAALTFN1 ---> 0xC001A024
设置GPIOA28引脚为GPIO功能:
GPIOAALTFN1[25:24] <--写-- 0b00
00 = ALT Function0
01 = ALT Function1
10 = ALT Function2
11 = ALT Function3
GPIO引脚功能的选择:每两位控制一个GPIO引脚
red :gpioa28
GPIOXOUT :控制高低电平的 0xC001A000
GPIOxOUTENB:输入输出模式 0xC001A004
GPIOxALTFN1:function寄存器 0xC001A024
(一个寄存器36个字节)
green:gpioe13
0xC001e000
blue :gpiob12
0xC001b000
Linux里面一切皆文件,驱动设备表现就是一个/dev/下的文件,/dev/led。应用程序调用open函数打开设备,比如led。应用程序通过write函数向/dev/led写数据,比如写1表示打开,写0表示关闭。如果要关闭设备那么就是close函数。
编写驱动的 时候也需要编写驱动对应的open、close,write函数。字符设备驱动fileoptions_struct.
驱动最终是被应用调用的,在写驱动的时候要考虑应用开发的便利性。
驱动是分驱动框架的,要按照驱动框架来编写,对于字符设备驱动来说,重点编写应用程序对应的open、close、read、write等函数。