一、驱动课程大纲
- 内核模块
- 字符设备驱动
- 中断
二、ARM裸机代码和驱动有什么区别?
1、共同点:
都能够操作硬件
2、不同点:
1)裸机就是用C语言给对应的寄存器里面写值,驱动是按照一定的套路往寄存器里面写值
2)arm裸机单独编译单独执行,驱动依赖内核编译,依赖内核执行(根据内核指定好的架构和配置去实现)
3)arm裸机同时只能执行一份代码,驱动可以同时执行多分代码(且当要操作串口的时候,内核写的一部分代码咱们程序员就不用去写了,比较方便)
4)arm裸机只需要一个main就可以了,在main函数中写相应的逻辑代码即可驱动是依赖内核的框架和操作硬件的过程。
(驱动里面操作LED灯的寄存器)(驱动模块是依赖内核框架执行代码)
三、linux系统组成
1、0-3G空间
的用户空间是每个进程单独拥有0-3G的空间
2、系统调用(软中断swi)----
(应用层通过系统调用与底层交互,swi,将应用层切换到内核层。
注:1G的物理内存映射成0~4G的虚拟内存,每个进程都可以访问内核,0~3G是每个进程单独拥有的,3G~4G是所有的共有的。代码运行在物理内存上,向虚拟内存上面写值,其实是写在物理内存上面的
3、kernel : 【3-4G】内核
内核的5大功能:
1)进程管理:进程的创建,销毁,调度等功能
注:可中断,不可中断,就是是否被信号打断。从运行状态怎样改到可中断等待态,和不可中断等待态操作系统开始会对每个进程分配一个时间片,当进程里面写了sleep函数,进程由运行到休眠态,但是此时CPU不可能等着。有两种方法,1:根据时间片,CPU自动跳转,2:程序里面自己写能引起CPU调度的代码就可以
2)文件管理:通过文件系统ext2/ext3/ext4 yaff jiffs等来组织管理文件
3)网络管理:通过网络协议栈(OSI,TCP)对数据进程封装和拆解过程(数据发送和接收是通过网卡驱动完成的,网卡驱动不会产生文件(在Linux系统dev下面没有相应的文件),所以不能用open等函数,而是使用的socket)。
4)内存管理:通过内存管理器对用户空间和内核空间内存的申请和释放
5)设备管理: 设备驱动的管理(驱动工程师所对应的)
字符设备驱动: (led 鼠标 键盘 lcd touchscreen(触摸屏))
1.按照字节为单位进行访问,顺序访问(有先后顺序去访问)
2.会创建设备文件,open read write close来访问
块设备驱动 :(camera u盘 emmc)
1.按照块(512字节)(扇区)来访问,可以顺序访问,可以无序访问
2.会创建设备文件,open read write close来访问
网卡设备驱动:(猫)
1.按照网络数据包来收发的。
4、宏内核、微内核
1)宏内核:将进程,网络,文件,设备,内存等功能集成到一个内核中
特点:代码运行效率高。
缺点:如果有一个部分出错整个内核就崩溃了。
eg:ubuntu Android
2)微内核:只将进程,内存机制集成到这个内核中,文件,设备,驱动在操作系统之外。通过API接口让整个系统运行起来。
缺点:效率低 优点:稳定性强(华为手机)
eg:鸿蒙
5.驱动模块(三要素:入口;出口;许可证)
1)入口:资源的申请
2)出口:资源的释放
3)许可证:GPL(写一个模块需要开源,因为Linux系统是开源的,所以需要写许可协议)
#include <linux/init.h>
#include <linux/module.h>
1)驱动函数框架
申请资源函数
static int __init hello_init(void)
//(__init可以不指定,及可以不写,但是正常是写的)
//__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");//许可证
2)Makefile函数
Makefile:
KERNELDIR:= /lib/modules/$(shell uname -r)/build/ //Ubuntu内核的路径
#KERNELDIR:= /home/linux/kernel/kernel-3.4.39/(板子内核路径)
PWD:=$(shell pwd)//驱动文件的路径(打开一个终端看终端的路径)
all: //目标
make -C $(KERNELDIR) M=$(PWD) modules(-C:进入顶层目录执行)
//注:进入内核目录下执行make modules这条命令
//如果不指定 M=$(PWD) 会把内核目录下的.c文件编译生成.ko
M=$(PWD) 想编译模块的路径(驱动模块所在路径)
clean:
make -C $(KERNELDIR) M=$(PWD) clean
obj-m:=hello.o //指定编译模块的名字
3)make运行结果
4)追代码命令
注:在内核路径下进行创建索引,追踪时也需要在内核路径下追踪
make tags
创建索引文件
ctags -R
在终端上
vi -t xxx
在代码中跳转
ctrl + ]
ctrl + t
Ubuntu内核所对应的内核路径
6.命令:
1、sudo insmod hello.ko 安装驱动模块
2、sudo rmmod hello 卸载驱动模块
3、lsmod 查看模块
4、dmesg 查看消息
5、sudo dmesg -C 直接清空消息不回显
6、sudo dmesg -c 回显后清空
7.内核中的打印函数printk
搜索函数,搜到以后,在里面任意找到一个,看函数原形就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__);
8.查看内核打印级别:vi -t KERN_ERR(查看内核打印级别)
1)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 */(出错)
#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
最高的 最低的
2)linux@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被开发者修改过来,所有消息不会主动回显。
3)修改系统默认的级别
su root
echo 4 3 1 7 > /proc/sys/kernel/printk
虚拟机的默认情况:
板子的默认情况:
4)如果想修改开发板对应的打印级别
vi rootfs/etc/init.d/rcS
echo 4 3 1 7 > /proc/sys/kernel/printk
在rootfs/etc/init.d/rcS里面添加上以后再起板子,板子的级别就为如下:
安装驱动和卸载驱动时,消息会打印。
9、驱动多文件编译
hello.c add.c
Makefile
demo-y+=hello.o
demo-y+=add.o
(-y作用:将hello.o add.o放到demo.o中:可放置多个文件,打包为一个文件)
obj-m:=demo.o
最终生成demo.ko文件
1)hello.c
#include<linux/module.h>
#include<linux/init.h>
#include<linux/printk.h>
#include"add.h"
//申请资源函数
//存储类型 数据类型 指定存放位置 函数名(行参)
//(__init可以不指定,及可以不写,但是正常是写的)
static int __init hello_init(void)//__init将hello_init放到.init.text段中
{
printk(KERN_ERR "HELLO WORLD add=%d\n",add(2,3));//KERN_ERR后必须加空格
printk("%s %s %d\n",__FILE__,__func__,__LINE__);
return 0;
}
//释放资源函数
static void __exit hello_exit(void) //__exit将hello_exit放到.exit.text段中
{
printk(KERN_INFO "welcome......sub=%d\n",sub(2,3));
printk("%s %s %d\n",__FILE__,__func__,__LINE__);
}
//入口
module_init(hello_init);//告诉内核驱动的入口地址(函数名为函数首地址)
//出口
module_exit(hello_exit);//告诉内核驱动的出口地址
//许可证
MODULE_LICENSE("GPL");//许可证
2)add.c
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
3)add.h
#ifndef ADD_H
#define __ADD_H__
int add(int a,int b);
int sub(int a,int b);
#endif
4)makefile
#KERNEL_PATH=/lib/modules/4.15.0-142-generic/build/ #Ubuntu内核的路径
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39/ #(板子内核路径)
PWD=$(shell pwd) #将shell命令pwd执行的结果赋值给pwd,驱动文件的路径(想编译模块的路径)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#(-C进入顶层目录)
#注:进入内核目录下执行make modules这条命令,如果不指定M=$(pwd)会把内核目录下的.c文件编译生成.ko
#M=路径:指定需要编译的驱动代码所在的路径
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
#将hello.c和add.c编译成二进制文件打包到一起生成一个驱动模块
demo-y += hello.o
demo-y += add.o
obj-m = demo.o #指定编译模块的名字
注意修改路径,可在开发板或ubuntu下运行,开发板下需先在ubuntu下编译再到板子上运行
10、模块传递参数
命令传递的方式
sudo insmod demo.ko hello world
---------------------------------------------------------
* Standard types are:
* byte, short, ushort, int, uint, long, ulong (没有找到char!!!!!!!!)
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
- 接收命令行传递的参数module_param
module_param(name, type, perm)
功能:接收命令行传递的参数
参数:
@name :变量的名字
@type :变量的类型
@perm :权限 0664 0775(其它用户对我的只有读和执行权限,没有写的权限)
modinfo hello.ko(查看变量情况)
2)对变量的功能进行描述MODULE_PARM_DESC
MODULE_PARM_DESC(_parm, desc)
功能:对变量的功能进行描述
参数:
@_parm:变量
@desc :描述字段
只能传十进制,不可以写十六进制
练习:
1.byte类型如何使用 (传递参数用ascii)
2.如何给一个指针传递一个字符串
sudo insmod hello.ko a=20 b=30 c=65 p="hello_world"
3)接收命令行传递的数组module_param_array
module_param_array(name, type, nump, perm)
功能:接收命令行传递的数组
参数:
@name :数组名
@type :数组的类型
@nump :参数的个数,变量的地址
@perm :权限
注意:传字符的时候写ASCII码值;传递字符串的时候,不能有空格
字符串不能有空格,数组元素以逗号“ ,”间隔。
sudo insmod hello.ko a=121 b=10 c=65 p="hello" ww=1,2,3,4,5
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/moduleparam.h>
int a=100;
module_param(a,int,0775);
MODULE_PARM_DESC(a,"this is int number.");
char ch='A';
module_param(ch,byte,0775);
MODULE_PARM_DESC(ch,"this is char.");
char *sp=NULL;
module_param(sp,charp,0775);
MODULE_PARM_DESC(sp,"this is char pointe.");
int st[10];
int num;
module_param_array(st,int, &num, 0775);
MODULE_PARM_DESC(st,"this is int array.");
//驱动三要素
//存储类型 数据类型 指定存放的位置 函数名(行参)
static int __init hello_init(void)
{
int i;
printk(KERN_ERR "hello world\n");
printk("%s %s %d a=%d\n",__FILE__,__func__,__LINE__,a);
printk("%d %c %s\n",a,ch,sp);
for( i=0;i<num;i++)
{
printk("st[%d]=%d\n",i,st[i]);
}
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "welcome ……\n");
printk(KERN_ERR "%s %s %d\n",__FILE__,__func__,__LINE__);
}
//入口:申请资源
module_init(hello_init);
//出口:释放资源
module_exit(hello_exit);
//许可证
MODULE_LICENSE("GPL");
makefile
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板的路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build #虚拟机路径
PWD=$(shell pwd) #将shell命令pwd执行的结果赋值给PWD变量
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
# -C 路径:指定到切换到那个路径,执行make modules命令
# M=路径:指定需要编译的驱动代码所在的路径
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m += hello.o
#要编译生成驱动模块的目标程序