Linux驱动
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
Linux驱动
驱动
1.驱动课程大纲
内核模块
字符设备驱动
中断
2.ARM裸机代码和驱动有什么区别
共同点都能够操作硬件 (都操作寄存器)
不同点
裸机就是用C语言给对应的寄存器里面写值驱动是按照一定的框架格式往寄存器里面写值
arm裸机单独编译单独执行驱动依赖内核编译依赖内核执行根据内核指定好的架构和配置去实现
arm裸机同时只能执行一份代码驱动可以同时执行多分代码且当要操作串口的时候内核写的一部分代码咱们程序员就不用去写了比较方便
arm裸机只需要一个main就可以了在main函数中写相应的逻辑代码即可驱动是依赖内核的框架和操作硬件的过程。
驱动里面操作LED灯的寄存器驱动模块是依赖内核框架执行代码
3.linux系统组成
0-3G的用户空间是每个进程单独拥有0-3G的空间
系统调用(软中断swi)----应用层通过系统调用与底层交互swi将应用层切换到内核层。
注1G的物理内存映射成04G的虚拟内存每个进程都可以访问内核03G是每个进程单独拥有的3G~4G是所有的共有的。代码运行在物理内存上向虚拟内存上面写值其实是写在物理内存上面的
kernel : 【3-4G】
内核5大功能
进程管理进程的创建销毁调度等功能
注可中断不可中断就是是否被信号打断。从运行状态怎样改到可中断等待态和不可中断等待态操作系统开始会对每个进程分配一个时间片当进程里面写了sleep函数进程由运行到休眠态但是此时CPU不可能等着。有两种方法1根据时间片CPU自动跳转2程序里面自己写能引起CPU调度的代码就可以
文件管理通过文件系统ext2/ext3/ext4 yaff jiffs等来组织管理文件
网络管理通过网络协议栈OSITCP对数据进程封装和拆解过程数据发送和接收是通过网卡驱动完成的网卡驱动不会产生文件在Linux系统dev下面没有相应的文件所以不能用open等函数,而是使用的socket。
内存管理通过内存管理器对用户空间和内核空间内存的申请和释放
设备管理 设备驱动的管理(驱动工程师所对应的)
字符设备驱动: led 鼠标 键盘 lcd touchscreen触摸屏
1.按照字节为单位进行访问顺序访问(有先后顺序去访问)
2.会创建设备文件open read write close来访问
块设备驱动 :camera u盘 emmc
1.按照块512字节扇区来访问可以顺序访问可以无序访问
2.会创建设备文件open read write close来访问
网卡设备驱动:猫
-
按照网络数据包来收发的。
4.宏内核、微内核 (了解)
宏内核将进程网络文件设备内存等功能集成到一个内核中
特点代码运行效率高。
缺点如果有一个部分出错整个内核就崩溃了。
eg:ubuntu Android
微内核只将进程内存机制集成到这个内核中文件设备驱动在操作系统之外。通过API接口让整个系统运行起来。
缺点效率低 优点稳定性强华为手机
eg:鸿蒙 -
驱动模块驱动三要素入口出口许可证
入口:资源的申请 (安装)
出口:资源的释放 释放
许可证:GPL写一个模块需要开源因为Linux系统是开源的所以需要写许可协议
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
__init可以不指定及可以不写但是正常是写的
//_init _eixt 指定放到内存的那个位置。
//__init将hello_init放到.init.text段中
{
return 0;
}
static void __exit hello_exit(void)
//存储类型 数据类型 指定存放区域 函数名(形参)
//__exit将hello_exit放到.exit.text段中
{
}
module_init(hello_init);
//告诉内核驱动的入口地址(函数名为函数首地址)
module_exit(hello_exit);
//告诉内核驱动的出口地址
MODULE_LICENSE("GPL");
//许可证
Makefile
KERNELDIR:= /lib/modules/KaTeX parse error: Expected 'EOF', got '#' at position 40: … //Ubuntu内核的路径 #̲KERNELDIR:= /ho…(shell pwd)//驱动文件的路径
打开一个终端看终端的路径
all: //目标
make -C
(
K
E
R
N
E
L
D
I
R
)
M
=
(KERNELDIR) M=
(KERNELDIR)M=(PWD) modules
-C进入顶层目录
注进入内核目录下执行make modules这条命令
如果不指定 M=
(
P
W
D
)
会把内核目录下的
.
c
文件编译生成
.
k
o
M
=
(PWD) 会把内核目录下的.c文件编译生成.ko M=
(PWD)会把内核目录下的.c文件编译生成.koM=(PWD) 想编译模块的路径
clean:
make -C
(
K
E
R
N
E
L
D
I
R
)
M
=
(KERNELDIR) M=
(KERNELDIR)M=(PWD) clean
obj-m:=hello.o //指定编译模块的名字
make工具作用 是什么 使用格式 什么特点 怎么用
make是工程管理器对多个文件进行管理可以根据文件的时间戳自动发现更新的文件
追代码
创建索引文件
ctags -R
在终端上
vi -t xxx
在代码中跳转
ctrl + ]
ctrl + t
Ubuntu内核所对应的内核路径
6.命令
sudo insmod hello.ko 安装驱动模块
sudo rmmod hello 卸载驱动模块
lsmod 查看模块
dmesg 查看消息
sudo dmesg -C 直接清空消息不回显
sudo dmesg -c 回显后清空
7.内核中的打印函数。
搜索函数搜到以后在里面任意找到一个看函数原形就OK
printk(打印级别 “内容”)
printk(KERN_ERR “Fail%d”,a);
printk(KERN_ERR “%s:%s:%d\n”,FILE,func,LINE);
驱动在哪一个文件哪一个函数哪一行
printk(“%s:%s:%d\n”,FILE,func,LINE);/
vi -t KERN_ERR查看内核打印级别
include/linux/printk.h
#define KERN_EMERG "<0> /* system is unusable /系统不用
#define KERN_ALERT “<1>” / action must be taken immediately /被立即处理
#define KERN_CRIT “<2>” / critical conditions /临界条件临界资源
#define KERN_ERR “<3>” / error conditions /出错 //kern_err
#define KERN_WARNING “<4>” / warning conditions /警告
#define KERN_NOTICE “<5>” / normal but significant condition /提示
#define KERN_INFO “<6>” / informational /打印信息时候的级别
#define KERN_DEBUG “<7>” / debug-level messages */ 调试级别
0 ------ 7
最高的 最低的
Hq@ubuntu:~$ cat /proc/sys/kernel/printk 4 4 1 7
终端的级别 消息的默认级别 终端的最大级别 终端的最小级别
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])
只有当消息的级别大于终端级别消息才会被显示
但对与咱们的这个Ubuntu被开发者修改过来所有消息不会主动回显。
修改系统默认的级别
su root
echo 4 3 1 7 > /proc/sys/kernel/printk
虚拟机的默认情况
板子的默认情况
如果想修改开发板对应的打印级别
vi rootfs/etc/init.d/rcS //当系统重新启动rcS中的命令会全部重新执行一遍
echo 4 3 1 7 > /proc/sys/kernel/printk
在rootfs/etc/init.d/rcS里面添加上以后再起板子板子的级别就为如下
rootfs/etc/init.d/rcS一些启动虚拟机需要启动的东西都可以放在这个文件中启动系统时同时启动。echo 4 3 1 7 > /proc/sys/kernel/printk 放到板子跟文件系统对应这个文件中。
安装驱动和卸载驱动时消息会打印。
8.驱动多文件编译
hello.c add.c
Makefile
obj-m:=demo.o
demo-y+=hello.o add.o
(-y作用将hello.o add.o放到demo.o中)
最终生成demo.ko文件
9.模块传递参数
命令传递的方式 (终端传递值)
sudo insmod demo.ko hello world
* Standard types are:
* byte, short, ushort, int, uint, long, ulong (没有找到char!!!!!!!!) //char -->byte
* charp: a character pointer //一个字符指针
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
module_param(name, type, perm)
功能接收命令行传递的参数
参数
@name :变量的名字
@type :变量的类型
@perm :权限 0664 0775其它用户对我的只有读和执行权限没有写的权限
modinfo hello.ko(查看变量情况)
MODULE_PARM_DESC(_parm, desc) //module_parm_desc
功能对变量的功能进行描述
参数
@_parm:变量
@desc :描述字段
只能传十进制不可以写十六进制
练习
1.byte类型如何使用
2.如何给一个指针传递一个字符串
sudo insmod hello.ko a=20 b=30 c=65 p="hello_world"
注意传字符的时候写ASCII码值传递字符串的时候不能有空格
module_param_array(name, type, nump, perm)
功能接收命令行传递的数组
参数
@name :数组名
@type :数组的类型
@nump :保存输入元素个数的地址
@perm :权限
sudo insmod hello.ko a=121 b=10 c=65 p=“hello” ww=1,2,3,4,5
复习
1.模块
三要素
入口
static int __init hello_init(void)
{
return 0;
}
module_init(hello_init);
出口
static void __exit hello_exit(void)
{
}
module_exit(hello_exit)
许可证
MODULE_LICENSE(“GPL”);
多文件编译
obj-m:=demo.o
demo-y+=hello.o add.o
内核中的打印
printk(打印级别 “打印的内容”);
printk(“打印的内容”);
/proc/sys/kernel/printk
4 4 1 7
出现这个错误提示说明scripts下没有生成相应的文件cd到kernel所在目录执行 make scripts
然后 make 就可以编译了
字符设备驱动
linux系统中一切皆文件
应用层 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 {
in¬t (*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函数会有一个返回值文件描述符fdread函数通过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,成功主设备号失败返回错误码负数
void unregister_chrdev(unsigned int major, const char *name)
功能注销一个字符设备驱动
参数
@major:主设备号
@name:名字
返回值无
手动创建设备文件
sudo mknod hello (路径是任意) c/b 主设备号 次设备号
sudo –rf hello 删除的时候记得加-rf
通过字符设备驱动点亮板子上的led灯
app: test.c char buf[3]
1 0 0
0 1 0
0 0 1
------------------|------------------------
kernel: led_driver.c
-------------------|------------------------
hardware: RGB_led
应用程序如何将数据传递给驱动读写的方向是站在用户的角度来说的
#include <linux/uaccess.h>
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)
返回值成功返回虚拟地址失败返回NULLvoid *0;
void iounmap(void addr)
功能取消映射
参数
@addr 虚拟地址
返回值无
#define ENOMEM 12 / Out of memory */
释放资源是按申请资源的倒序来释放
Eg:点灯
软件编程控制硬件的思想
只需要向控制寄存器中写值或者读值就可以让我们处理器完成一定的功能。
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
GPIOxALTFN1function寄存器 0xC001A024
一个寄存器36个字节
green:gpioe13
0xC001e000
blue :gpiob12
0xC001b000
练习
1.字符设备驱动实现流水灯30分钟
//读改写
writel(v,c)
功能:向地址中写一个值
参数
@ v :写的值
@ c :地址
readl©
功能读一个地址将地址中的值给返回
参数
@c :地址
设备节点创建问题udev/mdev
mknod hello c 243 0手动创建设备节点hello
宏有返回值为最后一句话执行的结果
#include <linux/device.h>
自动创建设备节点
struct class *cls;
cls = class_create(owner, name) /void class_destroy(struct class *cls)//销毁
功能向用户空间提交目录信息内核目录的创建
参数
@owner :THIS_MODULE看到owner就添THIS_MODULE
@name :目录名字
返回值成功返回struct class *指针
失败返回错误码指针 int (-5)
if(IS_ERR(cls)){
return PTR_ERR(cls);PTR_ERR把错误码指针转换成错误码
}
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, …)内核文件的创建每个文件对应一个外设硬件设备
/void device_destroy(struct class *class, dev_t devt)//销毁
功能向用户空间提交文件信息
参数
@class :目录名字
@parent:NULL
@devt :设备号 major<<12 |0 < = > MKDEV(major,0)
@drvdata :NULL
@fmt :文件的名字
返回值成功返回struct device *指针
失败返回错误码指针 int (-5)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/device.h>
#define NAME "led_dev"
struct class *cls = NULL;
struct device *dev = NULL;
// 定义宏名代替物理地址
#define RED_BASE 0xc001a000
#define GREEN_BASE 0xc001e000
#define BLUE_BASE 0xc001b000
unsigned int major = 0; // 主设备号
char kbuf[32] = "";
int ret;
// 定义指针保存映射后得到的虚拟地址
unsigned int *red_addr = NULL;
unsigned int *green_addr = NULL;
unsigned int *blue_addr = NULL;
int myopen(struct inode *inode_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t myread(struct file *file_t, char __user *ubuf, size_t size, loff_t *off)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
if (size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_to_user(ubuf, kbuf, size);
if (ret != 0)
{
printk("kernel copy data to user failed.\n");
return -EINVAL;
}
return 0;
}
ssize_t mywrite(struct file *file_t, const char __user *ubuf, size_t size, loff_t *off)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
// 将用户空间数据拷贝到内核空间
if (size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_from_user(kbuf, ubuf, size);
if (ret != 0)
{
printk("user copy to kbuf failed.\n");
return -EINVAL;
}
if (kbuf[0] == 1) // 红灯
*(red_addr) |= 1 << 28;
else
*(red_addr) &= ~(1 << 28);
if (kbuf[1] == 1) // 绿灯
*(green_addr) |= 1 << 13;
else
*(green_addr) &= ~(1 << 13);
if (kbuf[2] == 1) // 蓝灯
*(blue_addr) |= 1 << 12;
else
*(blue_addr) &= ~(1 << 12);
return 0;
}
int myclose(struct inode *inode_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
// 点等法结构体赋值
struct file_operations fops = {
.open = myopen,
.read = myread,
.write = mywrite,
.release = myclose,
};
static int __init mycdev_init(void)
{
// 注册字符设备驱动(得到字符设备驱动的框架)
major = register_chrdev(major, NAME, &fops);
if (major < 0)
{
printk("register_chrdev error.\n");
return -EINVAL;
}
// 建立led灯操作物理地址和虚拟地址之间的映射
// 1.映射红灯
red_addr = (unsigned int *)ioremap(RED_BASE, 40);
if (red_addr == NULL)
{
printk("ioremap red failed.\n");
return -EINVAL;
}
// 2.映射绿灯
green_addr = (unsigned int *)ioremap(GREEN_BASE, 40);
if (green_addr == NULL)
{
printk("ioremap green failed.\n");
return -EINVAL;
}
// 3.映射蓝灯
blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40); // 这里40个字节和36个字节都行
if (blue_addr == NULL)
{
printk("ioremap blue failed.\n");
return -EINVAL;
}
// 初始化灯操作寄存器
// 红
*(red_addr + 9) &= ~(3 << 24);
*(red_addr + 1) |= 1 << 28;
*(red_addr) &= ~(1 << 28); // 初始灭
// 绿
*(green_addr + 8) &= ~(3 << 26);
*(green_addr + 1) |= 1 << 13;
*(green_addr) &= ~(1 << 13); // 初始灭
// 蓝
*(blue_addr + 8) &= ~(1 << 24);
*(blue_addr + 8) |= 1 << 25;
*(blue_addr + 1) |= 1 << 12;
*(blue_addr) &= ~(1 << 12); // 初始灭
// 设置自动创建设备节点
// 向用户空间提交目录信息
cls = class_create(THIS_MODULE, NAME);
if (IS_ERR(cls))
{
printk("class create failed.\n");
return PTR_ERR(cls);
}
// 提交文件信息
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, NAME);
if (IS_ERR(dev))
{
printk("device create failed.\n");
return PTR_ERR(dev);
}
return 0;
}
static void __exit mycdev_exit(void)
{
device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
// 取消映射就是释放的时候是和映射时倒过来的
iounmap(blue_addr);
iounmap(green_addr);
iounmap(red_addr);
// 注销字符设备驱动
unregister_chrdev(major, NAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
int fd = open("/dev/led_dev", O_RDWR);
if (fd < 0)
{
perror("open error.");
return -1;
}
char buf[32] = ""; // 让初始值全部初始化为0
while (1)
{
write(fd, buf, 32);
buf[0] = 1;
write(fd, buf, 32);
sleep(1);
buf[0] = 0;
buf[1] = 1;
write(fd, buf, 32);
sleep(1);
buf[1] = 0;
buf[2] = 1;
write(fd, buf, 32);
sleep(1);
buf[2] = 0;
buf[0] = 1;
buf[1] = 1;
write(fd, buf, 32);
sleep(1);
buf[0] = 0;
buf[1] = 1;
buf[2] = 1;
write(fd, buf, 32);
sleep(1);
buf[1] = 0;
buf[2] = 0;
}
close(fd);
return 0;
}
ioctl函数 ******思想非常重要
注用户程序所作的只是通过命令码告诉驱动程序它想做什么至于怎么解释这些命令和怎么实现这些命令这都是驱动程序要做的事情。驱动程序提供了对ioctl的支持用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。
如前边我们对灯的控制是一个或两个灯如果我们有一万个灯。没有办法统一控制。
引入ioctl的原因从用户的角度出发用户只需要告诉你指令底层如何实现和我无关。
功能input output 的控制
user: ioctl是应用层使用的函数底层需要识别实现功能
#include <sys/ioctl.h>
int ioctl(int fd, int request, …);RED_ON
让点灯的代码变得简洁
参数//请求码和打开的文件有关
@fd : 打开文件产生的文件描述符
@request: 请求码(读写|第三个参数传递的字节的个数)(如开灯关灯)
在sys/ioctl.h中有这个请求码的定义方式。
@… :可写、可不写如果要写写一个内存的地址
Kernel
在驱动程序中实现的ioctl函数体内实际上是有一个switch{case}结构每一个case对应一个命令码做出一些相应的操作。怎么实现这些操作这是每一个程序员自己的事情
fops: //这个函数在注册设备的时候file_operations结构体中
long (*unlocked_ioctl) (struct file *file, unsigned int request, unsigned long args);
//request就是应用层发下来的请求码需要传对应类型值。我能识别的是 _IO(type,lr)
计算后的值。
对于使用ioctl函数时主要的就是请求码的设计请求码主要在sys/ioctl.h文件里面进行了设计。
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);老版本的内核用这个
unsigned int (*poll) (struct file *, struct poll_table_struct *);//应用层的poll、epoll调用的是底层的poll函数。
//从宏定义可以看出底层已经和应用层有了对应控制规定相当于定义 了标志
表示我本次是读还是写的字节的大小再往下看当调用_IOC的时候怎样把四个域组合在一起的。
一个一个看鼠标放在_IOC_DIRSHIFT进行跳转出现下面的同学
#define _IO(type,nr)
_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)
_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define RDE_LED _IO(type,nr)
如果不涉及用copy_to_user和copy_from_user应用层和底层数据传递单纯只是点灯就用_IO最简单的就可以。
这些宏是帮助你完成请求码的封装的。
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
dir << 30 | size<<16 | type << 8 | nr << 0
2 14 8 8
方向 大小 类型 序号
方向00 01 10 11读写相关
(大小sizeof变量名)
类型组合成一个唯一的不重合的整数,一般传一个字符
序号表示同类型中的第几个当开灯的时候写0那关的时候就不写0。
详解https://blog.csdn.net/JCfyw/article/details/116349738
//通过一个数的某几位控制内容不同来实现不同的操作命令
#define RLED_ON _IOWR(‘a’,0,int)//亮灯
#define RLED_OFF _IOWR(‘a’,1,int) //灭灯
//ioctl之后read和write已经可以不用了他们可以识别命令这相当于进程间通信IPC通信中获取唯一key值一样这的0 1开关灯也需要通过宏函数获取一个唯一操作指令。
经使用的命令码的域在如下文档中已经声明了。
vi kernel-3.4.39/Documentation/ioctl$ vi ioctl-number.txt
2^32次方 = 4G的数字所以可以使用内核的想法是每一个数字代表一个功能和数字一一对应但是不一样的驱动使用的时候相同也是可以的
详解https://blog.csdn.net/JCfyw/article/details/116349738
练习
- ioctl函数的使用
用ioctl的实现电灯思路站在用户角度
- ioctl实现点灯首先需要输入命令码(应用层可以随便设计)但是命令码对应操作需要给底层发送对应的数字这个数字不能随便发送因为底层需要识别那我使用的时候需要根据底层的提示传送对应的值。
- 编写驱动时首先写应用层代码调用ioctl。底层根据应用层输出做出对应操作。
Linux内核中断
Eg:
ARM里当按下按键的时候他首先会执行汇编文件start.s里面的异常向量表里面的irq,在irq里面进行一些操作再跳转到C的do_irq(); //Linux内核和裸机特点一样只是做了一些操做需要跟着linux框架才能操作。通用的东西系统会先自己写好。
进行操作1判断中断的序号2处理中断3清除中断
Linux内核实现和ARM逻辑实现中断的原理是一样的。
内核当按键按下后依然到异常向量表再到handler_irq函数写死的在handler_irq里面定义了一个数组数组中每个成员里面存放的是结构体在结构体里面有个函数指针这个函数指针就指向了咱们自己提交函数的名字数组的下标是Linux内核的软中断号它和硬件中断号之间有个映射关系。内核实现中断时在handler_irq函数里面把中断的寄存器都初始化好了咱们只需要拿到软中断号绑定我的中断处理函数就可以
//终端寄存器都自己映射好了你只需要在这个中断来你做什么样的操作就可以了
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char name, void dev)
功能注册中断
参数
@irq : 软中断号 对于中断如按键当按这个按键产生一个中断我们知道按键所对应的真实的物理CPIONO号即需要将真实的物理GPIONO号映射到内存中
gpio的软中断号
软中断号 = gpio_to_irq(gpiono号); //将GPIONO号转为软中断号
gpiono = m32+nn:组内的序号//GPIONO号 的计算方式 针对于按键
m:那一组 A B C D E5组
0 1 2 3 4
gpioa28 = 032+28
gpiob8 =132+8 gpiob16 = 132+16
Gpioc5=2*32+5
使用中断步骤
- 先注册中断有这个中断
- 找到GPIONO号
控制器中断号ADC控制器对应的中断、内核中已经给你定义好了
find -name irqs.h在内核源码中找
find -name irqs.h ./arch/arm/mach-s5p6818/include/mach/irqs.h
find -name s5p6818_irq.h ./arch/arm/mach-s5p6818/include/mach/s5p6818_irq.h
#define IRQ_PHY_ADC (41 + 32) //IRQ_PHY_ADC软中断号
@handler: 中断的处理函数 //*****这个函数中实现逻辑
irqreturn_t (*irq_handler_t)(int irqno, void *dev);
IRQ_NONE //中断没有处理完成
IRQ_HANDLED //中断正常处理完成
//中断处理函数返回的时IRQ_NONE表示中断没有处理完成
@flags 中断的触发方式
#define IRQF_DISABLED 0x00000020 //快速中断在处理函数里面写了他就先处理这个中断
#define IRQF_SHARED 0x00000080 //共享中断中断的接口较少但是器件都想要中断那管脚需要外接两个寄存器里面有中断状态标志位看中断状态标志位有没有置位。一个口不可以链接两个按键按键没办法区分
#define IRQF_TRIGGER_RISING 0x00000001上升沿触发
#define IRQF_TRIGGER_FALLING 0x00000002下降沿触发
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
@name :名字 cat /proc/interrupts
@dev :向中断处理函数中传递参数 不想传就写为NULL
返回值成功0失败返回错误码
void free_irq(unsigned int irq, void *dev_id)
功能注销中断
参数
@irq :软中断号
@dev_id:向中断处理函数中传递的参数不想传就写为NULL
Eg按键所对应的中断号是多少及找所对应的GPIO
第一步找底板原理图找到按键
第二步拷贝网络标号到核心板
及对应的软中断号为gpio_to_irq gpiob8 = 132+8gpio_to_irq gpiob16 = 132+16
ARRAY_SIZE计算数组里面元素的个数 //代码完成后按键一次可能会打印多次硬件设备按键一次可能会设别多次
问题解决方法
[root@farsight]#insmod farsight_irq.ko
[ 21.262000] request irq146 error
insmod: can’t insert ‘farsight_irq.ko’: Device or resource busy
通过 cat /proc/interrupts
146: GPIO nxp-keypad
154: GPIO nxp-keypad
说明中断号已经被占用了
解决办法在内核中将这个驱动删掉
如何确定驱动文件的名字是谁
grep “nxp-keypad” * -nR
arch/arm/mach-s5p6818/include/mach/devices.h:48:
#define DEV_NAME_KEYPAD "nxp-keypad"
grep “DEV_NAME_KEYPAD” * -nR
drivers/input/keyboard/nxp_io_key.c:324: .name = DEV_NAME_KEYPAD,
驱动文件的名字是nxp_io_key.c
找宏的名字在Makefine里面知道
如何从内核中将他去掉
选项菜单的名字Kconfig
config KEYBOARD_NXP_KEY
tristate "SLsiAP push Keypad support"
make menuconfig
<>SLsiAP push Keypad support
去掉图形化界面里面的*号后可以把nxp_io_key.o删除掉这样再次编译内核的时候就可以看出来nxp_io_key.c是否备编译如果被编译就有对应的.o生成如果不被编译就不会生成nxp_io_key.o文件。
make uImage 重新编译内核
cp arch/arm/boot/uImage ~/tftpboot
重新启动板子
安装驱动
然后按键进行测试
eg:
- 按下按键将红灯状态取反
- 尝试使用定时器将中断的抖动消除
为什么不用sleep(1)消抖
Linux内核定时器 (按键一次打印多次消抖)
定时器的当前时间如何获取
jiffies:内核时钟节拍数
jiffies是在板子上电这一刻开始计数只要
板子不断电这个值一直在增加64位(工业上直接使用可以在设置板子时进行修改)。在驱动代码中直接使用即可。
//每次当定时器中断发生时内核内部通过一个64位的变量jiffies_64做加一计数
定时器加1代表走了多长时间
在内核顶层目录下有.config
CONFIG_HZ=1000
周期 = 1/CONFIG_HZ
周期是1ms;//1000ms=1s
//总步骤有对象、初始化、添加对应逻辑、操作完成注销
分配的对象
struct timer_list mytimer;
对象的初始化
struct timer_list {
unsigned long expires; //定时的时间
void (*function)(unsigned long); //定时器的处理函数
unsigned long data; //向定时器处理函数中填写的值
};
void timer_function(unsigned long data) //定时器的处理函数
{
}
//初始化自己需要的三个
mytimer.expries = jiffies + 1000; //1s
mytimer.function = timer_function;
mytimer.data = 0;
init_timer(&mytimer); //内核帮你填充你未填充的对象
对象的添加定时器
void add_timer(struct timer_list *timer);
//同一个定时器只能被添加一次
//在你添加定时器的时候定时器就启动了,只会执行一次
int mod_timer(struct timer_list *timer, unsigned long expires)
//再次启动定时器 jiffies+1000
4.对象的删除
int del_timer(struct timer_list *timer)
//删除定时器
Int gpio_get_value(int gpiono);//通过gpiono获取当权gpio的所处状态
注定时器的申请需要放在中断申请之前。重启定时器在检测到下降沿就可以重启。
当检测到下降沿进行中断处理调用中断处理函数同时检测到下降沿还需要重启定时器所以重启定时器应该放在中断处理函数中。
定时器处理函数中检测是否是低电平再进行对应操作。定时时间到检测一次。
模块导出符号表
思考1应用层两个app程序app1中拥有一个add函数app1运行时app2是否可以调用app1中的add函数 不行因为应用层app运行的空间是私有的(0-3G)没有共享。
思考2两个驱动模块module1中的函数module2是否可以调用可以他们公用3-4G内核空间只是需要找到函数的地址就可以。好处减少代码冗余性代码不会再内存上被重复加载。代码更精简一些代码可以不用写直接调用别人写好的函数就可以。
编写驱动代码找到其他驱动中的函数需要用模块导出符号表将函数导出被人才可以使用这个函数。他是一个宏函数。
在驱动的一个模块中向使用另外一个模块中的函数/变量只需要使用EXPORT_SYMBOL_GPL将变量或者函数的地址给导出。使用者就可以用这个地址来调用它了。
EXPORT_SYMBOL_GPL(sym)
sym变量名或函数名
代码举例1两个独立的代码驱动模块
代码举例2提供者为内核已经安装使用的驱动
总结
编译
1.先编译提供者编译完成之后会产生一个Module.symvers
2.将Module.symvers拷贝到调用者的目录下
3.编译调用者即可
安装
先安装提供者
再安装调用者
卸载
先卸载调用者
再卸载提供者
如果调用者和提供者时两个独立(xx.ko)驱动模块他们间传递地址的时候是通过Module.symvers传递的。
如果提供者是内核的模块uImage此时调用者和提供者间就不需要Module.symvers文件传递信息。
补充-裸机实现按键中断
中断
按键中断实验
1.打开按键原理图。
2.核心板
由于我们需要将外边的信号输入到芯片中即将GPIOB8设置伪GPIO功能同时设置为输入功能。
3.芯片手册
设置信号检测为下降沿触发中断使能引脚检测功能。
将信号发给cpu(使能中断功能)。
按键产生中断信号代码部分
中断管理器GIC可以控制产生160种中断
中断是外部的硬件给cpu发送的一个信号让cpu停下来先完成自己的事情。如 按键打断cpu让音响声音放大。
解决问题产生的中断不可能直接给cpu运行的。
–》1.如果外部硬件同时产生中断cpu没有办法同时处理。
2.cpu正在处理一个信号现在其他硬件又产生了中断这个中断信号cpu无法接收处理。
3.一个中断产生如果有多个cpu送个那个cpu。
4.中断信号送给cpucpu无法识别是那种中断。
即产生的中断是不能直接送给cpu处理的而是在中断管理器GIC中拦截注册分配优先级排队等待中断管理器中给160种中断进行了优先级划分 可以通过寄存去重新自己配置优先级这个优先级只决定排队高优先级不能打断低优先级的中断。可以任意打断关闭中断不给cpu送过去。
/* 中断管理器-介于外部设备和CPU之间,能检测和接收外部设备产生的中断信号
并且对所有的中断信号进行统一管理和协调并将其转发给合适的CPU去处理*/
1.多个外设同时产生中断信号时中断管理器可以对多个信号进行排队,优先级高的先转发给CPU,优先级低的继续排队
2.中断管理器可以为每一个中断分配一个优先级
3.一个中断正在处理的同时另一个外设产生了中断信号中断管理器可以先将其进行注册等待等上一个处理完成后再将其转发给CPU处理
4.中断管理器可以给每一个外设产生的中断信号选择一个合适的CPU来处理
5.CPU在接收到中断信号后并不知道是哪个外设产生的,这时CPU可以通过查询中断管理器来获取中断信息
6.任意打开或者关闭一个中断
… …
了解–》
重点
查看寄存器写代码
情况1led在一直闪烁着的情况下遇到中断灯灭处理中断完成之后会返回main函数继续执行main函数但是发现一直在重复的执行中断处理。原因
代码如下
#include"s5p6818.h"
void Delay(unsigned int Time)
{
unsigned int i,j;
for(i=0;i<Time;i++)
for(j=0;j<2000;j++);
}
void do_irq(void)
{
printf(“This is GPIOB8\n”);
Delay(1000);
/清除中断标志位,防止处理完中断后外设还向中断管理器发送中断信号,写1清0,写0不变/
GPIOB.DET = (1 << 8);//GPIOxDET
}
int main()
{
/外设层次 - 设置对应的外设(硬件),使其产生一个有效的中断信号/
/1.将GPIOB8设置成GPIO功能 GPIOB.ALTFN0/
GPIOB.ALTFN0 = GPIOB.ALTFN0 & (~(0x3 << 16));
/2.将GPIOB8设置成input功能 GPIOB.OUTENB/
GPIOB.OUTENB = GPIOB.OUTENB & (~(1 << 8));
/3.将GPIOB8的检测模式设置成检测下降沿 GPIOB.DETMODEEX + GPIOB.DETMODE0/
GPIOB.DETMODEEX = GPIOB.DETMODEEX & (~(1 << 8));
GPIOB.DETMODE0 = GPIOB.DETMODE0 & (~(0x3 << 16)) | (0x2 << 16);
/4.使能GPIOB8引脚的检测功能 GPIOB.DETENB/
GPIOB.DETENB = GPIOB.DETENB | (1 << 8);
/5.使能GPIOB8检测到有效信号后(下降沿)产生中断信号 GPIOB.INTENB/
GPIOB.INTENB = GPIOB.INTENB | (1 << 8);
/*中断管理器层次 -
* 一方面检测和接收外部设备产生的中断信号,并对其统一管理排队,另一方面将其转发给CPU*/
/*6.全局使能中断管理器接收第0组的中断信号*/
GICD_CTRL |= 0x1;
/*7.在中断管理器中使能86号中断,使其能够转发到对应的CPU*/
GICD_ISENABLER.ISENABLER2 = GICD_ISENABLER.ISENABLER2 | (0x1 << 22);
/*8.在中断管理器中设置86号中断转发给CPU0处理*/
GICD_ITARGETSR.ITARGETSR21 = GICD_ITARGETSR.ITARGETSR21 & (~(0xFF << 16)) | (0x1 << 16);
/*9.使能中断信号能通过CPU接口连接到CPU*/
GICC_CTRL |= 0x01;
/*初始化LED*/
GPIOA.ALTFN1 = GPIOA.ALTFN1 & (~(0x3 << 24)) | (0x0 << 24);
GPIOA.OUTENB = GPIOA.OUTENB | (1 << 28);
while(1)
{
GPIOA.OUT = GPIOA.OUT | (1 << 28);
Delay(200);
GPIOA.OUT = GPIOA.OUT & (~(1 << 28));
Delay(200);
}
return 0;
}
只要出现产生中断不同的中断信号来都会调转到do_irq执行这个中断处理函数如何区分是哪一类中断信号读寄存器GICC_IAR --》cpu不知道是那种中断信号但是中断管理器转过来的中断信号它知道。
代码部分如下
情况2当第一次接收到中断信号读GICC_IAR判断是那种中断进行处理再次来中断不识别。–》原因刚开始不出现这个情况是因为GICC_IAR没有读那种中断信号所以中断处理器认为这个中断信号没有处理继续给cpu送信号而现在读了中断处理器认为你还在处理这个中断还在忙。
中断处理器只负责送信号不知道cpu有没有处理这个信号。但是cpu现在问中断处理器处理的是几号信号这个时候中断处理器就认为你一直在处理这个信号再来这个信号也不会往cpu送。哪怕cpu已经把这个信号处理完了。
代码部分如下
最终代码中断管理器
#include"s5p6818.h"
void Delay(unsigned int Time)
{
unsigned int i,j;
for(i=0;i<Time;i++)
for(j=0;j<2000;j++);
}
void do_irq(void)
{
unsigned int IrqNum;
/从中获取当前的中断的中断号,从该指令执行后中断管理器就认为CPU要处理中断,所以就不再送入新的中断/
IrqNum = GICC_IAR & 0x000003FF;
switch(IrqNum)
{
case 0:
/*0号中断处理程序*/
break;
case 1:
/*1号中断处理程序*/
break;
/* ... ... */
case 86:
printf("This is GPIOB8\n");
Delay(1000);
/*清除中断标志位,防止处理完中断后外设还向中断管理器发送中断信号,写1清0,写0不变*/
GPIOB.DET = (1 << 8);
break;
/* ... ... */
case 159:
/*159号中断处理程序*/
break;
/* ... ... */
}
/*告诉中断管理器当前的中断已经处理完成,可以送入新的中断*/
GICC_EOIR = GICC_EOIR & (~(0x3FF)) | IrqNum;
}
int main()
{
/外设层次 - 设置对应的外设(硬件),使其产生一个有效的中断信号/
/1.将GPIOB8设置成GPIO功能 GPIOB.ALTFN0/
GPIOB.ALTFN0 = GPIOB.ALTFN0 & (~(0x3 << 16));
/2.将GPIOB8设置成input功能 GPIOB.OUTENB/
GPIOB.OUTENB = GPIOB.OUTENB & (~(1 << 8));
/3.将GPIOB8的检测模式设置成检测下降沿 GPIOB.DETMODEEX + GPIOB.DETMODE0/
GPIOB.DETMODEEX = GPIOB.DETMODEEX & (~(1 << 8));
GPIOB.DETMODE0 = GPIOB.DETMODE0 & (~(0x3 << 16)) | (0x2 << 16);
/4.使能GPIOB8引脚的检测功能 GPIOB.DETENB/
GPIOB.DETENB = GPIOB.DETENB | (1 << 8);
/5.使能GPIOB8检测到有效信号后(下降沿)产生中断信号 GPIOB.INTENB/
GPIOB.INTENB = GPIOB.INTENB | (1 << 8);
/*中断管理器层次 -
* 一方面检测和接收外部设备产生的中断信号,并对其统一管理排队,另一方面将其转发给CPU*/
/*6.全局使能中断管理器接收第0组的中断信号*/
GICD_CTRL |= 0x1;
/*7.在中断管理器中使能86号中断,使其能够转发到对应的CPU*/
GICD_ISENABLER.ISENABLER2 = GICD_ISENABLER.ISENABLER2 | (0x1 << 22);
/*8.在中断管理器中设置86号中断转发给CPU0处理*/
GICD_ITARGETSR.ITARGETSR21 = GICD_ITARGETSR.ITARGETSR21 & (~(0xFF << 16)) | (0x1 << 16);
/*9.使能中断信号能通过CPU接口连接到CPU*/
GICC_CTRL |= 0x01;
/*初始化LED*/
GPIOA.ALTFN1 = GPIOA.ALTFN1 & (~(0x3 << 24)) | (0x0 << 24);
GPIOA.OUTENB = GPIOA.OUTENB | (1 << 28);
while(1)
{
GPIOA.OUT = GPIOA.OUT | (1 << 28);
Delay(200);
GPIOA.OUT = GPIOA.OUT & (~(1 << 28));
Delay(200);
}
return 0;
}