shell是什么:
shell是一个应用程序,在命令行中输入的命令就是传入shell的参数;
对于不同的命令,shell会去哪里找该命令所对应的程序:
去PATH环境变量所指示的路径找到程序,找到程序后会启动程序并传入参数即传入命令行窗口输入的命令;
这么设置PATH:
假设你所要执行的程序存放在book里面
1.临时设置
export PATH=$PATH:/home/book #只对当前终端有效,在终端执行该命令修改PATH
2.只对当前用户有效
gedit ~/.bashrc #执行该命令打开.bashrc文件
export PATH=$PATH:/home/book #在文件末尾添加该命令修改PATH,保存并关闭文件,重新打开当前终端才能执行
3.永久设置
sudo gedit /etc/environment #打开environment文件
:/home/book #在文件末尾添加新的路径
文件操作命令:
目录文件操作命令:
/ 是根目录是最顶级目录,~ 通常表示当前用户的主目录,例如用户名为user1,那么 ~ 就指向 home/user1
命令 --help #查看命令的使用方法
在登陆虚拟机时通常不使用root用户登录,对虚拟机的破坏性太大,要使用其他用户登录临时需要root权限在命令前加 sudo 。
1.pwd
print working directory
打印当前所在路径
change directory
切换路径
示例:cd /home/ #切换到home路径
cd .. #切换上一级目录
./hello #执行当前目录中的hello
cd - #切回上一次的目录
3.mkdir
make directory
创建目录
示例:mkdir dir0 #创建dir0目录
mkdir -p dir1/dir2 #创建目录及子目录
4.rmdir
remove directory
删除目录
示例:rmdir dir1 #删除一个空目录,该命令不能删除非空目录
list
列出目录内容
示例:ls #显示当前目录下的文件
ls -a #显示当前文件下的所有文件包括隐藏文件
ls -l #显示文件更完整信息
ls -la #上面两个的综合命令
6.cp
示例:cp -rfd dir_a dir_b #复制dir_a,复制文件保存在当前文件夹下
recursive,递归地,即复制所有文件
force,强制覆盖
d,如果源文件为链接文件,也只是把它作为链接文件复制过去,而不是复制实际文件
7.rm
remove
删除文件或目录
示例:rm -rf dir_1 #recursive 递归的,force,强制删除,即删除所有文件
8.cat
串联文件的内容并打印出来
示例:cat file1.txt #打印文件中的内容
9.touch
修改文件的时间,如果文件不存在则创建空文件
改变文件权限和属性:
1.chmod
改变文件的权限,文件信息中最前面一位表示文件类型,再前三位表示拥有用户的权限,再中间三位表示同组用户的权限,后三位表示其他用户的权限。w 可写、r 可读、x 可执行、- 无。对应位有权限表示1,无权限表示0,使用三组三位二进制数表示文件的权限。例:-wrxwr-wrx 767
示例:chmod a+x hello #给所有用户加上x权限
chmod a-x hello #给所有用户减去x权限
chmod u+x hello #给拥有者加上x权限
chmod u-x hello #给拥有者减去x权限
chmod 767 hello #给将权限改为-wrxwr-wrx
2.chown 改变文件拥有者
示例:sudo chown book:book hello #改变为book用户所有、book用户组
查找/是搜索命令:
1.find 目录名 选项 查找条件
示例:find /home/book/dira -name "test1.txt" #在目录/home/book/dira中找名为test1.txt的文件
find /home/book/dira -name "*.txt" #在/home/book/dira目录中找.txt结尾的文件,*是通配符
find /home/book/dira -name "dira" #在目录/home/book/dira中找名为dira的的目录或文件
注意:不指定目录命就是在当前目录中找;
2.grep 选项 查找模式 文件名
示例:grep -rn "abc" test1.txt #n number ,在 test1.txt 中查找字符串abc ,文件名改为*表示在当前文件中找
压缩/解压缩命令:
1.gzip
-l(list) 列出压缩文件的内容
-k(keep) 在压缩或解压缩时,保留原来的文件
-d(decompress) 将压缩文件进行解压缩
示例:gzip -kd mypwd.1 #压缩文件mypwd.1,最后会生成mypwd.1.gz 同时保留mypwd.1
注意:该命令只能压缩单个文件,不能压缩目录
2.bzip2
-k 在压缩或解压缩是,保留原来的文件
-d 将文件精选解压缩
注意:相较于gzip,bzip2的压缩率更高,常用与大文件
3.tar
-c(create) 表示创建用来生成文件包
-x 表示提取,从文件包中提取文件
-t 可以查看压缩的文件
-z 使用gzip方式进行处理,与c结合就表示压缩,与x结合就表示解压缩
-j 使用bzip2方式处理,与c结合就表示压缩,与x结合就表示解压缩
-v(verbose) 详细报告tar处理的信息
-f(file) 表示文件,后面接着一个文件名 -C <指定目录> 解压到指定目录
示例: tar czvf dira.tar.gz dira #把目录 dira 压缩、打包为 dira.tar.gz 文件
tar tvf dira.tar.gz #查看压缩文件
tar xzvf dira.tar.gz #解压到当前目录
tar xzvf dira.tar.gz -C /home/book #解压到/home/book
tar cjvf dira.tar.bz2 dira #把目录 dira 压缩、打包为 dira.tar.bz2 文件
tar tvf dira.tar.bz2 #查看压缩文件
tar xjvf dira.tar.bz2 #解压到当前目录:
tar xjvf dira.tar.bz2 -C /home/book #解压到/home/book
vi编辑器的使用:
一般模式下的命令:
i 进入编辑模式
方向键 移动光标
Ctrl+f 向下翻页
Ctrl+b 向上翻页
ngg 光标移到n行行首
G 转至文件末尾
cc 删除整行,并且修改整行内容
dd 删除该行,不提供修改功能
ndd 删除当前行及其下面的n-1行
nyy 复制当前行及其下面的n-1行
p 粘贴最近复制的内容
u 撤销上一步操作
在一般模式下,按下 : 键,此时 Vi 编辑器底部会出现一个冒号 :,表示进入了命令行模式。
命令行模式:
:%s/p1/p2/g 将文件中所有p1替换成p2
:%s/p1/p2/gc 将文件中所有p1替换成p2,替换时需确认
:wq 保存并退出
:q! 不保存退出
:q 如果文件未修改,直接退出。
:/key-word 查找关键字
在虚拟机中和开发板通信:
OTG(USB On-The-Go) = 开发板上那个 Micro USB / USB Type-C 接口(硬件)
adb(Android Debug Bridge) = Android / 嵌入式 Linux 里的调试通信协议(软件)
关系:adb 通常就是通过 OTG 接口来和电脑通信的,纯 Linux(非 Android)系统本身没有adb,
板子上的Linux 系统 + OTG 硬件 + 系统内自带 adbd(ADB Daemon) 服务 = 可以用 adb 。
在虚拟机终端使用 adb devices 命令查看是否成功连接开发板, adb shell 命令登录开发板,即可在虚拟机终端操作开发板上的Linux系统。
git仓库资料下载:
在windows中利用 git bash 工具,使用命令行操作git仓库,windows下进入bash,开始->所有程序->git bash。
下载及后续更新本地源码:
1.进入下载路径
cd /d/abc/
2.从远程下载仓库(只需要做一次)
git clone https://e.coding.net/weidongshan/01_all_series_quickstart.git
3.进入目录
cd /d/abc/01_all_series_quickstart/
4.查看状态是否有更新,local out of date 表示有更新
git remote show origin
5.拉取更新部分(可跳过上步直接执行这一步)
git pull origin
6.查看更新内容,f前翻,b后翻,q退出
git log
驱动程序的执行过程:
1.先修改驱动程序中的MakeFile,把内核源码路径改成开发板对应的内核编译路径 /home/book/100ask_imx6ull-sdk/Linux-4.9.88 且一定要在Ubuntu上编译内核传到开发板的 /home/book 路径的前提下进行;
2.在虚拟机使用 make 命令在驱动程序的目录下使用编译出驱动程序(.ko)和测试程序(hello_drv_test),将两程序使用adb传到开发板;
3.在开发板执行该命令安装驱动程序 insmod hello_drv.ko (注意:一定要使用在Ubuntu上刚编译出的开发板内核(zlmage)放到/boot然后重启板子再安装驱动程序)
4.在开发板执行测试程序 ./hello_drv_test
GCC的使用:
gcc -o test main.c sub.c
链接两个源文件生成可执行文件test。
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
gcc -o test main.o sub.o
若直接链接成可执行文件,在面对多源文件时只对单个文件进行修改就需要全部重新编译成汇编文件。所以需要分别编译成汇编文件再将汇编文件链接。
gcc -c -o main.o main.c -v
在汇编过程中增加-v选项查看头文件、库文件 路径(其他过程也可以使用-v选项)(<>里面的会去编译链指定的默认路径中寻找头文件,""里面的会在当前路径下寻找)。
gcc -o test main.c -I ./
在编译时添加 -I 选项将 ./ 当前目录加入到头文件的搜索路径。
在main.c中会调用sub.c的内容若,只编译main.c就会显示main中使用sub的部分未定义,除了手动执行链接命令外,还可以将sub.c制作成库。
制作静态库:
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
ar crs libsub.a sub.o sub1.o sub2.o #可以使用多个.o生成静态库
gcc -o test main.o libsub.a
制作动态库:
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
ar sheared libsub.a sub.o sub1.o sub2.o #可以使用多个.o生成动态库
链接动态库:
gcc -o test1 main.o -L ./ -lsub // -L ./ 指定当前路径为库的搜索路径,-l 指定具体找哪一个库 sub 省略了lib和.a
在使用动态库的情况下运行可执行文件需要临时添加运行库的路径:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ // ./ 将当前路径添加搜索路径中
列出头文件目录、库目录对交叉编译链同理 echo 'main(){}' arm-linux-gcc -E -v -
echo 'main(){}' gcc -E -v -
补充:
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
编译内核前要先配置环境变量。
ARCH环境变量,指定编译目标的体系架构。CROSS_COMPILE指定了交叉编译工具链的前缀。PATH变量定义了系统在执行命令时搜索可执行文件的目录列表。
Makefile
**Makefile规则:**如果目标文件不存在或依赖文件比目标文件新就执行命令。
目标文件:依赖文件1 依赖文件2 ...
tab\] 命令
> test:a.o b.o
>
> gcc -o test a.o b.o
>
> a.o : a.c
>
> gcc -c -o a.o a.c
>
> b.o : b.c
>
> gcc -c -o b.o b.c
**Makefile语法:**
% 通配符
$\^ 表示所有依赖文件
$\< 表示第一个依赖文件
$@ 表示目标文件
make 命令的使用:make \[目标文件\] 示例:make clean (如果make后面没有目标文件就默认当前目录下Makefile中的第一个定义的非隐含目标文件,如果有就执行目标文件)。
若当前目录下存在名为clean的文件,需要将Makefile中的 clean 定为假想目标(.PHONY: clean)才能成功执行Makefile中的目标文件 clean 对应的命令。
> test:a.o b.o c.o
>
> gcc -o test $\^
>
> %.o : %.c
>
> gcc -c -o $@ $\<
>
> clean:
>
> rm \*.o test
>
>
> .PHONY: clean
**变量的使用:**
:= 即时变量,其值在定义的时候就定了,
= 延时变量,其值在使用的时候才会确定。
?= 延时变量,如果是第一次定义时才起效,如果前面该变量已定义则忽略这句。
+= 附加,他是即时变量还是延时变量取决于前面的定义。
$(B) 变量B
> A := $(C)
>
> B = $(C)
>
> C = abc
>
> D = 100
>
> D ?= hello
>
> all:
>
> @echo A = $(A)
>
> @echo B = $(B)
>
> @echo D = $(D)
>
> C += 123
>
> 输出结果:
>
> A = //由于在执行第一句时C没有定义,所以为空
>
> B = abc 123
>
> D = 100 //后面对D的定义方式不会覆盖前面的定义
**Makefile中的函数:**
a. $(foreach var,list,text) # 在 list 中取出符合 text 的变量
b. $(filter pattern...,text) # 在 text 中取出符合 patten 格式的值
$(filter-out pattern...,text)} # 在 text 中取出不符合 patten 格式的值
c. $(wildcard pattern)} # pattern定义了文件名的格式, wildcard 取出其中存在的文件
d. $(patsubst pattern,replacement,$(var)) # 从列表中取出每一个值
# 如果符合pattern$\\quad$
# 则替换为 replacement
> A = a b c
>
> B = $(foreach f, $(A), $(f).o)
>
> C= a b c d/
>
> D = $(filter %/, $(C))
>
> E = $(filter-out %/, $(C))
>
> filtes = $(wildcard \*.c)
>
> files2 = a.c b.c c.c d.c e.c abc
>
> files3 = $(wildcard $(files2))
>
> dep_files = $(patsubst %.c,%.d,$(files2))
>
> all:
>
> @echo B = $(B)
>
> @echo D = $(D)
>
> @echo E = $(E)
>
> @echo files = $(files)
>
> @echo files3 = $(files3)
>
> @echo dep_files = $(dep_files)
>
> 目录下存放的内容:
>
> Makefile a.c b.c c.c
>
> 输出结果:
>
> B = a.o b.o c.o
>
> D = d/
>
> E = a b c
>
> files = a.c b.c c.c
>
> files3 = a.c b.c c.c
>
> dep_files = a.d b.d c.d d.d e.d abc
> 命令:
>
> gcc -M c.c # 打印出依赖
>
> gcc -M -MF c.d c.c # 把依赖写进文件c.d
>
> gcc -c -o c.o c.c -MD -MF c.d # 编译c.c,把依赖写入文件c.d
**添加编译标签:**
> CFLAGS = -Werror -Iinclude //添加编译标签,一般将头文件都放到include文件下
>
> %.o : %.c
>
> gcc $(CFLAGS) -c -o $@ $\< -MD -MF .$@.d
>
>
> 在编译过程中的打印内容:
>
> gcc -Werror -Iinclude -c -o a.o a.c -MD -MF .a.o.d
**Makefile的使用:**
将子目录下所以需要编译的程序链接成 built-in.o 文件保存在子目录下,将顶层文件中需要编译的程序和子目录中的 built-in.o 文件链接成顶层文件中的built-in.o ,那么顶层目录下的 built-in.o 就代表了该程序所以涉及的文件,最终只需将顶层目录中的 built-in.o 链接成app。
**怎么编译子目录:**
顶层目录中的 Makefile.build 包含子目录中的 Makefile 。
补充:
内核源码路径 KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
> 使用不同的开发板一定要修改 KERN_DIR 。

> `-C`选项告诉`make`切换到变量`$(KERN_DIR)指定的目录`下执行后续的`make`命令。
>
> \`M\`选项指定了模块源代码所在的目录。
>
> \`pwd\`是一个Shell命令,用于打印当前工作目录。
>
> `$(CROSS_COMPILE)`变量用于指定交叉编译工具前缀。
>
> **modules 是所指定的路径中的Makefile中的一个目标文件,这个目标定义了编译内核模块的一系列操作。**

> 第一句:定义了组成100ask_led模块的对象文件列表。
>
> 第二句:编译成一个可加载的内核模块。
>
> 最终将三个.o文件编译成一个.ko文件

> 内核构建系统会根据 `obj - m` 中列出的对象文件,自动查找对应的 `.c` 文件,最终将三个.o文件分别编译成.ko文件。
## 文件句柄如何和具体文件挂钩:

> \& 表示后台执行程序, //这样就可以继续操作终端
>
> ps (Process Status) //显示当前进程状态
>
> cd /proc/27753/fd //进入当前进程目录,fd(file descriptors)

每个进程默认打开三个文件描述符 0 (stdin) 1 (stdout) 2 (stderr)
每个进程有自己的文件句柄空间,对于0、1、2...文件句柄都是属于这个进程的。对于要打开文件句柄3所对应的文件,要添加打开哪个进程所对应的文件句柄3的限定词。
## adb的使用:
adb devices #显示挂载的设备
adb push 1.txt /root # 把Ubuntu的文件放到开发板的/root目录
adb pull /root/1.txt 2.tx # 把开发板的/root/1.txt下载并改名为2.txt
adb shell # 登录开发板

## 输入系统框架及调试:
驱动程序上报type(哪类,比如EV_KEY,按键类)、code(哪个,比如KEY_A)、value(值,比如0按下1松开)这三项表示所有输入事件的所有数据。
使用 **ls /dev/input/\* -l** 或 **ls /dev/event\* -l** 查看设备节点。

使用**cat /proc/bus/input/devices** 查看设备节点对应的硬件。

I:id of the devices 由结构体 input_id 来描述

N:name of devices
P:physical path to the device in the system hierarchy
S:sysfs path
U:unique identification code for the device(if device has it) 设备唯一识别码
H:list of input handles associated with the device 与设备关联的输入句柄列表。
B:bitmaps(位图)
**PROP:device properties and quirks(****设备属性****)**
**EV:types of events supported by the device(****设备支持的事件类型****)**
**KEY:keys/buttons this device has(****此设备具有的键****/****按钮****)**
**MSC:miscellaneous events supported by the device(****设备支持的其他事件****)**
**LED:leds present on the device(****设备上的指示灯****)**
例如 B:EV=3 ,3的二进制为011,表示该设备支持0、1这两类事件。
对于 B:ABS=265800 3 这是两个32位数字组成一个64位数字"0x2658000,00000003",数值为 1 的位有:0、1、47、48、50、53、54,即0、1、0x2f、0x30、0x32、0x35、0x36,对应的宏。
使用 **hexdump /dev/input/event0** 查看输入设备触发的事件,所显示的十六进制数是事件宏所对应的编号。

## 使用freetype绘制字符:
在使用freetype绘制字符时需要先交叉编译freetype并将头文件、库文件放到工具链目录中。同tslib的交叉编译过程相似。
> arm-buildroot-linux-gnueabihf-gcc -o freetype_show_font freetype_show_font.c -lfreetype
-lfreetype是链接库的意思(link library),下面命令表示在链接阶段链接器会在库的搜索路径中寻找libfreetype这个库;也可以通过 -L 来指定搜索路径 例:-L/path/to/library #会将/path/to/library添加到库的搜索路径中。
**使用freetype绘制一行文字:**
当前字符的原点(origin)加上当前字符的步长(advance)就是下一个字符的起点,当前字符的原点不一定在当前字符的外框上。要移动这一行文字,改变的是这一行文字外框原点的位置。
## man命令的使用:
> ```
> man [选项] [节号] 命令/主题
> ```
一些常见的选项包括:
* `-f`:显示与指定关键字相关的手册页面。
* `-k`:搜索手册页中与关键字匹配的条目。
* `-a`:显示所有匹配的手册页面。
* `-w`:仅显示手册页的位置,而不显示其内容。
常见的节号包括:
* 1:用户命令
* 2:系统调用
* 3:C库函数
* 4:设备和特殊文件
* 5:文件格式和约定
* 6:游戏和演示
* 7:杂项
* 8:系统管理命令
实例
要查看 ls 命令的手册页面,可以执行以下命令:
> man ls
要查看 C 语言标准库函数 printf 的手册页面,可以执行:
> man 3 printf
要搜索包含特定关键字的手册页面条目,可以使用 -k 选项:
> man -k keyword
## 文件IO

fopen、fread等都是标准IO相关的函数,open、read等都是系统调用IO的函数。标准IO相对于系统调用IO在其内部增加了用户buffer,读写操作先经过buffer,必要时才会调用系统调用IO向内核发起操作。
## open函数:
**int open(const char \*pathname, int flags);**
作用:打开或创建一个文件。
pathname:文件路径名或文件名
flags:表示打开文件所采用的操作
O_RDONLY:只读模式
O_WRONLY:只写模式
O_RDWR:可读可写
O_APPEND:表示追加,如果原来文件里面有内容,则这次写入会 写在文件的最末尾。
O_CREAT:表示当前打开文件不存在,我们创建它并打开它,通常与O_EXCL 结合使用,当没有文件时创建文件,有这个文件时会报错提醒我们。
......
**int open(const char \*pathname, int flags, mode_t mode);**
**查看umask的值**

Mode 表示创建文件的权限,只有在 flags 中使用了 O_CREAT 时才有效, 否则忽略。
也可以通过open函数创建新文件,mode的值与umask的值共同决定是新文件的权限, **mode** 的值要与 **umask** 的**取反值按位与**的出来的值才是文件的权限的值。
## write函数:

将buf中count字节数据写入fd指定的文件。

再次调用write函数会在上一次位置之后写入新的内容。


若使用lseek函数重新定位读/写文件偏移,则写入的内容会覆盖文件中原有的内容,而不会在中间插入新内容。
## read函数:


在内核中会记录读的位置,下一次读就会从文件pos=N+M的位置开始读。
## dup函数
int dup(int oldfd);

echo 123 \> 1.txt #创建文本文件,内容为123

程序打开的是一个文件,但是fd、fd2对应不同的文件句柄,在内核中对应不同的file结构体,所以第二个read函数读数据的位置不会受到第一个read函数的影响,依然是从第0个位置开始读,所以buf和buf2的数据是一样的。
对于使用dup函数fd3依然是一个新的文件句柄,但是fd3与fd的文件句柄指向的是同一个file结构体,由于文件句柄3是复制文件句柄1得到的,所以读的是第二个字节。
dup2函数
int dup2(int oldfd, int newfd);


文件句柄1所对应的文件是stdout,hello,world本来应该打印到文件句柄1所对应的文件,但由于使用了dup2函数,使得文件句柄1所对应的文件被重定向了,所以hello,world就会打印到fd所对应的文件。
在内核中,首先会关闭文件句柄1所对应的文件,然后让file\*指针指oldfd文件句柄所对应的file结构体。
## Framebuffer:
BPP:bit per piexl

## 访问硬件的四种方式:
**1.2.使用查询方式或休眠-唤醒方式读取输入数据:**

查询方式:APP调用read函数时,如果驱动程序中有数据,read函数会返回数据,否则会立刻返回错误。
休眠唤醒方式:驱动程序中有数据调用的函数返回数据,否则APP就会在内核态休眠,当有数据时驱动程序会唤醒APP再返回数据。
若在使用open函数是传入noblock参数则是查询方式,否则就是休眠-唤醒方式读取数据。
**3.poll方式:**

相较于上面两种方式,poll增加了超时时间,在超时时间内数据正常返回,超过timeout返回错误。

events是所监测的事件,revents是实际触发的事件。

为什么使用poll方式时open节点使用非阻塞方式:
因为点击一次触摸屏会有很多数据,为的是在使用read函数多读几次poll函数返回的数据,在没有数据的时候不想让APP休眠,保证将数据读完。

在开发板运行时将设备节点作为参数传入该程序。
**select方式:**

**4.异步通知方式:**
APP忙自己的事,当驱动程序用数据时会主动给APP发信号,这会导致APP执行信号处理函数。
1.编写信号处理函数,注册信号处理函数

2.打开驱动程序

将设备节点作为参数传入APP

3.把APP的进程号告诉驱动程序
4.使能异步通知


## 使用tslib库访问触摸屏设备:
tslib是一个触摸屏的开源库,使用他来访问触摸屏设备。下面是tslib的框架

核心在于plugins目录中的插件或称module。read读取单点触摸屏数据,read_mt读取多点触摸屏数据。
**确定工具链中头文件和库文件目录:**
// 对于 IMX6ULL,命令如下
>
> echo 'main(){}'\| arm-buildroot-linux-gnueabihf-gcc -E -v -
这是头文件搜索目录:

这是lib的搜索目录:

其中的冒号时路径分隔符,链接阶段会在这两个路径中搜索库文件。
**交叉编译tslib:**
> **tar xJf tslib-1.21.tar.xz**
> **cd tslib-1.21**
> **./configure --host=arm-buildroot-linux-gnueabihf --prefix=/**
> **make**
**make install DESTDIR=$PWD/tmp**
在执行压缩包中的 **configure** 脚本时,**--host、** **--prefix**都是作为configure脚本的配置选项,
**--host** 指定了交叉编译的平台为 **arm-buildroot-linux-gnueabihf**
**--prefix**指定的是在执行 make install 时软件会安装在--prefix所指定路径为根的目录下。例如库文件就会安装在 /lib ,配置文件安装在 /etc ,头文件安装在 /bin 。

这是configure(ts_config.c)脚本的源码,如果没有设置环境变量 TSLIB_CONFFILE 就会从TS_CONF 这个宏来获取配置文件的名字,而这个宏是在配置时定义的。
使用 Makefile中的变量 **DESTDIR** 来指定安装目录,改成 **PWD/tmp**(当前目录中的tmp目录)。
**以上操作为的是后续制作的开发板文件系统镜像能在正确路径(如 `/lib` 对应镜像内实际路径)找到库文件。**
**把头文件、库文件放到工具链目录下:**
> **cd tslib-1.21/tmp/**
> **cp include/\* /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_s****dk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include**
**cp -d lib/\*so\* /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabih****f_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-b****uildroot-linux-gnueabihf/lib**
**把库文件放到开发板上:**
**运行程序不需要头文件只需要库文件。**
> **cd tslib-1.21/tmp**
> **adb push lib/ts /lib**
> **adb push lib/\*so\* /lib**
> **adb push bin/\* /bin**
**adb push etc/ts.conf /etc**
## **网络通信:**
数据传输三要素:源、目的、长度
### **TCP:**
**服务端:**
**socket函数:**

> fd = socket
>
> 无三要素,获取句柄。
> **domain**是网络程序所在的主机采用的通讯协族。AF_UNIX 只能够用于单一的 Unix 系统进程间通信,而 AF_INET 是针对 Internet 的,因而可以允许远程通信使用。
> **type**是网络程序所采用的通讯协议。SOCK_STREAM 表明用的是 TCP 协议,SOCK_DGRAM 表明用的是 UDP 协议。
> **protocol**,由于指定了 type,所以这个地方一般只要用 0 来代替就可以了。
> 函数成功时返回文件描述符。

参数 **AF_INET**的含义:

**bind函数:**



> 把fd和ip、端口绑定起来
> **sockfd**是由 socket 函数调用返回的文件描述符。
> **my_addr**是一个指向 sockaddr 的指针。
> **addrlen**是 sockaddr 结构的长度。
> 为什么使用 sockaddr_in 代替 sockaddr?
为了更加清晰的表示sa_data中的14个字节数据都代表什么。sin_port、sin_addr、sin_zero也是14个字节。sa_data中的数据就是这三个数据。

htons将主机字节序转为网络字节序,此处将端口号做了转换。INADDR_ANY表示本机上的所有ip。
**linsten函数:**

> 启动监听数据
> **sockfd**是 bind 后的文件描述符。
> **backlog**设置请求排队的最大长度。当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度。最大监听多少路连接。

> BACKLOG = 10
**accept函数:**

> 等待、接受客户端的连接。
> **sockfd**是 listen 后的文件描述符。
> **addr****,****addrlen**是客户端的ip信息,参考结构体内容。

> inet_ntoa(net to assicii) 将结构体成员 sin_addr 中的地址变成常见的ip样式,iClientNum 是连接的客户端的数量。
**send函数:**

> 客户端和服务端都使用send函数发数据。
> **sockfd**指定发送端套接字描述符;
> **buf**指明一个存放应用程序要发送数据的缓冲区;
> **len**指明实际要发送的数据的字节数;
> **flags**一般置 0。
**recv函数:**

> 客户端和服务端都使用recv函数收数据。
> **sockfd**指定接收端套接字描述符;
> **buf**指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据;
> **len**指明 buf 的长度;
> **flags**一般置 0。

> fork函数会复制当前进程创建一个子进程,由于可能出现多个客户端连接的情况,每一个客户端连接都会使用fork函数创建一个子进程,子进程走if分支,父进程走else分支。
**客户端**

> fd = socket 获取句柄,
**connect函数:**

> 建立连接
> **sockfd**是 socket 函数返回的文件描述符。
> **serv_addr**储存了服务器端的连接信息,其中 sin_add 是服务端的地址。
> **addrlen**是 serv_addr 的长度
> connect 函数是客户端用来同服务端连接的.成功时返回 0,sockfd 是同服
> 务端通讯的文件描述符,失败时返回-1。

**僵死进程**

在退出client程序后使用 **ps -A** 命令查看进程,发现由fork函数创建的子进程变成了僵死进程(\
理解:
%[ ] 指定字符集,即加了字符规则的%s
^ 取反
%[^,] 取非','的内容,直到遇到','
I2C:
数据位只能在SCL是高电平的时候才能变化。
SDA线是半双工通信,SDA在1位开始位和7位地址位由主设备驱动,在第10位应答位由从设备驱动。SCL会有等待时间等待从设备的应答。
写操作:
白色是主设备->从设备,黑色是反方向。

读操作:

SMBus 是I2C的子集,相比于I2C有更加严格的要求,例如I2C没有规定发送的数据位的具体内容。
通过结构体i2c_adapter 来描述i2c外设,通过结构体 i2c_client 来描述与i2c相连的设备,传输的数据用结构体i2c_msg表示。
可以使用i2c_adapte.algo.master_xfer 这个传输函数来传输 i2c_msg ,但如果不想很深的去使用这个接口,也可以使用下面这个函数传输 i2c_msg。

该函数从 adap 中找到结构体中的传输函数,将 msgs 传输到从设备,由于 msgs 中会保存从设备地址,所以在这个函数中不需要使用 i2c_client这个结构体。

i2cdetect -l # 查看开发板上有多少条i2c总线。

UU:地址为0x1a这个设备是存在的,并且内核中已经有他的驱动程序了。
1e:地址为0x1e的这个设备是存在的,但是内核中没有他的驱动程序。
使用I2C-Tools(需要交叉编译到开发板)操作传感器AP3216C:
1.使用SMBus协议:


向0号总线的 0x1e 设备地址的0号寄存器写入0x4复位设备,0号寄存器写入0x3使能设备。
-f 强制写入,对于内核中有驱动程序的设备,如果不加 -f 就会写入失败。
-y 不询问。


i2cget -f -y 0 0x1e 0xc w #读取光照信息
i2cget -f -y 0 0x1e 0xe w #读取高度信息
2.使用i2c协议:

i2ctransfer -f -y 0 w2@0x1e 0 0x4 DESC: 写入两字节数据到0x1e
i2ctransfer -f -y 0 w1@0x1e 0xc r2 DESC: 写入一字节数据到0x1e,然后读出两字节数据 (r2 由于前面已经写了设备地址这里可以省略)


无论使用 I2C 还是 SMBus 都只会去调用open、write、read、ioctl这些上层接口。通过 open 对应的i2c控制器的设备节点来指定使用哪个i2c控制器,通过从设备在总线上的设备地址指定从设备,通过i2c或SMBus两种方式传输数据。
编写驱动程序(不涉及硬件):

编写驱动程序最重要的就是 file_operations 这个结构体,APP通过open、read函数调用drv_open、drv_read,使用 ioremap(phy) 将硬件寄存器的物理地址映射为虚拟地址,驱动程序去操作硬件,而drv_open、drv_read就保存在file_operations 这个结构体中,装载驱动程序时,内核根据主设备号将该结构体放到内核中该主设备号对应的位置,卸载驱动程序时再将该结构体从内核中抹去。
source insight 中使用lookup reference查找功能
1.确定主设备号,也可以让内核分配定义自己的 file_operations 结构体

major=0 让内核为其分配主设备号
2.定义自己的 file_operations 结构体

增加 static 避免命名污染,通过指定结构体成员的方式定义 hello_drv 。file_operations 这个结构体包含了一系列函数指针,这些函数指针指向驱动程序对文件操作的具体实现。内核通过主、次设备号找到对应的file_operations结构体。
3.实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体

函数写在结构体定义的上面省去函数声明的步骤。copy_to_user 从kernel_buf复制到用户buf,为的是确保数据的一致性。
4.把 file_operations 结构体告诉内核:register_chrdev
在入口函数中实现。
5.谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数

register_chrdev (character device)把结构体告诉内核,返回主设备号。驱动程序通过访问设备节点来操作设备,class_create 提供设备信息, device_create 自动创建设备节点,其中的hello是设备名称。在 class_create 的基础上执行 device_create 。
6.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev

注销在入口函数中创建的内容。
7.其他完善:提供设备信息,自动创建设备节点:class_create, device_create

module_init 将 hello_init 修饰成入口函数,module_exit 将 hello_exit 修饰成出口函数,遵循GPL协议。


cat /proc/devices
列出字符设备 (character devices)和块设备 (block device)的主设备号,以及分配到这些设备号的设备名称。

insmod hellow_drv.ko 装载驱动
lsmod 显示内核中已载入的驱动程序。
rmmod hello_drv 卸载驱动 hello_drv
对于涉及硬件的驱动程序(led):

对保存寄存器地址的指针添加 volatile 防止指针被优化,强调该指针不能轻易修改。

使用 ioremap 函数将寄存器的物理地址映射成虚拟地址,地址:baseaddr + offset 。后续通过该指针操作寄存器。其中100ask_led这个名称是在 /proc/devices 文件中注册驱动设备,myled 这个名称用于在 /dev 目录下创建设备节点,所有在出口函数中卸载驱动设备时要使用的名称是 100ask_led 。


通过指针修改寄存器。
led驱动程序框架:



由device_create创建多个设备节点,根据次设备号i确定要操作哪个led。由iminor(node)获取当前打开设备文件对应的次设备号。将次设备号传递给p_led_opr->init函数和p_led_opr->ctl函数初始化和控制对应的led。

使用#ifndef、#define、#endif避免在编译时多次引用该头文件。


在驱动程序的入口函数中使用get_board_led_opr,在驱动程序的.c文件中使用该函数返回底板.c文件中定义的结构体 board_demo_led_opr ,在之后为适配不同的开发板在board_demo_led_init和board_demo_led_ctl函数中添加对应开发板的寄存器操作来控制硬件。
为什么不直接定义指针指向该结构体而要使用函数封装的原因:
1.该结构体有static声明,只能在当前.c文件中使用
2.为适配多板使用,程序用到其他开发板时只需在get_board_led_opr(返回类型是结构体指针)中添加为其他板子创建的结构体。否则就得修改所有用到该指针的地方。
#ifdef CONFIG_BOARD_A
return &board_A_led_opr;
#elif CONFIG_BOARD_B
return &board_B_led_opr;

在驱动程序的.c文件中定义的 led_drv 关注的是用户空间对设备的通用访问接口,其中的函数实现的是内核与用户空间进行文件操作(如opne、read),而 p_led_opr 关注的是具体硬件设备的操作实现。
在Makefiel中实现将驱动.c和底板.c编译成一个.ko文件。
总线设备驱动模型:
bus_type结构体:


name:指定总线的名称,当新注册一种总线类型时,会在 /sys/bus 目录创建一个新的目录,目录名就是该参数的值;
bus_groups、dev_groups、drv_groups:分别表示 总线、设备、驱动的属性。
通常会在对应的 /sys 目录下在以文件的形式存在,对于驱动而言,在目录 /sys/bus//driver/ 存放了驱动的默认属性;设备则在目录 /sys/bus//devices/ 中。这些文件一般是可读写的,用户可以通过读写操作来获取和设置这些 attribute 的值。
match:当向总线注册一个新的设备或者是新的驱动时,会调用该回调函数。该设备主要负责匹配工作。
uevent:总线上的设备发生添加、移除或者其它动作时,就会调用该函数,来通知驱动做出相应的对策。
probe:当总线将设备以及驱动相匹配之后,执行该回调函数,最终会调用驱动提供的probe 函数。
remove:当设备从总线移除时,调用该回调函数。
suspend、resume:电源管理的相关函数,当总线进入睡眠模式时,会调用suspend回调函数;而resume回调函数则是在唤醒总线的状态下执行。
pm:电源管理的结构体,存放了一系列跟总线电源管理有关的函数,与 device_driver 结构体中的 pm_ops 有关。
p:该结构体用于存放特定的私有数据,其成员 klist_devices 和 klist_drivers 记录了挂载在该总线的设备和驱动。
platform_driver结构体:

platform_device结构体:

内核中的 platform_match 函数,此函数很重要,此函数就是完成设备和驱动之间匹配的,总线就是使用 platform_match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。match 函数有两个参数:dev 和 drv,这两个参数分别为 device 和 device_driver 类型,也就是设备和驱动。
platform_match 函数的匹配规则:最先比较:
platform_device.driver_override 和 platform_driver.driver.name 可以设置 platform_device 的 driver_override,强制选择某个 platform_driver。
然后比较:
platform_device. name 和 platform_driver.id_table[i].name
Platform_driver.id_table 是"platform_device_id"指针,表示该 drv 支持若干 个 device,它里面列出了各个 device 的{.name, .driver_data},其中的"name"表示该 drv 支持的设备的名字,driver_data 是些提供给该 device 的私有数据。
最后比较:
platform_device.name 和 platform_driver.driver.name
platform_driver.id_table 可能为空, 这时可以根据 platform_driver.driver.name 来寻找同名的 platform_device。
框架简述(重要):
1.
使用platform_device_register函数和platform_driver_register函数将自己定义的 platform_device 结构体和 platform_driver 结构体注册进内核,一旦注册进内核对应的device和driver就会进行匹配,当匹配成功后,系统将调用 probe 函数来完成设备的初始化工作。(分配、设置、注册file_operation结构体,核心还是这个结构体)(注册和匹配过程如下)class_create会在内核的/sys/class目录下创建一个类,方便对设备进行分类管理;而device_create基于这个类在/dev目录下创建设备节点,使得应用程序能够通过设备节点访问设备
2.
在 platform _device.resource 指定硬件资源,platform _driver.probe 会记录platform _device.resource中的资源和执行device_create、class_create 。
3.
在驱动的底层中完成注册和匹配,APP就可以调用驱动的上层open函数操作设备,整个程序就通了。
platform_device_register和platform_driver_register函数的调用过程:
platform_device_register
platform_device_add
device_add
bus_add_device // 放入链表
bus_probe_device // probe 枚举设备,即找到匹配的(dev, drv)
device_initial_probe
__device_attach
bus_for_each_drv(...,__device_attach_driver,...)
__device_attach_driver
driver_match_device(drv, dev) // 是否匹配
driver_probe_device // 调用 drv 的 probe
platform_driver_register
__platform_driver_register
driver_register
bus_add_driver // 放入链表
driver_attach(drv)
bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
__driver_attach
driver_match_device(drv, dev) // 是否匹配
driver_probe_device // 调用 drv 的 probe
EXPORT_SYMBOL的作用:

需要使用多个.c文件生成.ko文件时,使用EXPORT_SYMBOL()表示只有先加载该.c文件,其他.c文件才能使用该文件中的led_device_create。
设备树的语法:
node的格式:
label: node_name[@unit-address] {
[properties definitions]
[child nodes]
};
node是设备树中的基本单元,lable是标号可以省略,是为了方便引用node。
在根节点外使用以下两种方式修改uart@fe001000:
/dts-v1/; //表示版本号
/ { // "/"表示根节点
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
//使用lable引用node
&uart0 {
status = "disabled";
};
//或在根节点之外使用全路径:
&{/uart@fe001000} {
status = "disabled";
};
propertises的格式:
其实就是name=value
//格式1
label: property_name = value;
//格式2,没有值
[label:] property-name;
//3种取值
arrays of cells(1 个或多个 32 位数据, 64 位数据使用 2 个 32 位数据表示),
string(字符串),
bytestring(1 个或多个字节)
示例:
interrupts = <17 0xc>; //32位数据用尖括号围起来
clock-frequency = <0x00000001 0x00000000>; //64位数据用两个32位表示
compatible = "simple-bus"; //字符串用双引号
local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
local-mac-address = [000012345678]; // 每个 byte 使用 2 个 16 进制数来表示
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";//组合值
常用属性:
#address-cells**、****#size-cells、reg**
/ {
#address-cells = <1>; //address要用1个32位数表示
#size-cells = <1>; //size要用1个32为数来表示
memory {
reg = <0x80000000 0x20000000>; //0x80000000寄存器表示地址 0x20000000表示寄存器大小
};
};
reg 属性的值,是一系列的"address size",用多少个 32 位的数来表示address 和 size,由**其父节点(这里是根节点)**的#address-cells、#size-cells 决定。
compatible
//对于led,A、B、C三个驱动程序都兼容他。内核启动时会按照这个顺序找到A、B、C
led {
compatible = "A", "B", "C";
};
根节点下也有 compatible 属性,用来选择哪一个"machine desc":一个 内核可以支持 machine A,也支持 machine B,内核启动后会根据根节点的 compatible 属性找到对应的 machine desc 结构体,执行其中的初始化函数。 compatible 的值,建议取这样的形式:"manufacturer,model",即"厂家名,模块名"。compatible 属性在匹配过程中,优先级最高。
modul
{
compatible = "samsung,smdk2440", "samsung,mini2440"; //可以兼容内核中esmdk2440和mini2440驱动
model = "jz2440_v3"; //是什么板
};
用来准确定义这个硬件是什么。比如有2款板子配置基本一致**,它们的compatible是一样**
**那么就通过model来分辨这2****款板子。**
status
&uart1 {
status = "disabled";
};
dtsi 文件中定义了很多设备,但是在你的板子上某些设备是没有的。这时你可以给这个设备节点添加一个 status 属性,设置为"disabled"。
okay 设备正常运行,disabled 设备不可操作但后边可以恢复操作,fail 发生了严重错误需修复,fail-sss 发生了严重错误需修,sss表示错误信息。
常用节点:
根节点
dts文件中必须包含一个根节点,且根节点中必须包含#address-cells、#size-cells、compatiable、model这些属性。
CPU节点
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
.......
}
};
一般在dtsi文件中都定义好了。
memory节点
memory {
reg = <0x80000000 0x20000000>;
};
dtsi文件由芯片厂商写好,不会有内存的大小,所以要依照板子内存的大小设置。
chosen节点
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
我们可以通过设备树文件给内核传入一些参数,这要在 chosen 节点中设置 bootargs 属性。
设备树模板位置:
/dts-v1/;
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
在内核的 arch/arm/boot/dts 目录下就有了能用的设备树模板,一般命名为 xxxx.dtsi。"i"表示"include",被别的文件引用的。
使用dtc工具手工编译dts与反编译dtb文件:
./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts //编译dts为dt
b
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb //反编译dtb****为
dts
内核目录下 scripts/dtc/dtc 是设备树的编译工具,直接使用它的话,包含其他文件时不能使用"#include",而必须使用"/incldue"。
相较于总线设备驱动的形式设备树形式有什么区别:
设备树dts文件(源码)编译成设备树dtb文件(二进制),再由内核生成platform_device结构体,该结构体的生成过程如下:
dtb文件中的设备节点已经被内核处理成device_node结构体,设备树中的每一个节点在内核中都有一个device_node, 再将device_node结构体转换为platform_device结构体,再去匹配paltform_driver.
其实就是内核通过解析设备树获得硬件资源再将其保存在platform_device当中。
内核源码中 include/linux/目录下有很多 of 开头的头文件,of 表示"open firmware"即开放固件。
对于不会生成platform_device的节点怎么访问?
使用内核提供的函数,根据节点在 device_node 中的定义的属性寻找(例如:name、device_type、compatible、phandle:dts编译成dtb时的数字id)。
使用设备树写驱动程序:
设备树节点如何与platform_driver匹配?
驱动文件:

dts文件:

设备树要存在compatible属性,且platform_driver.of_match_table.compatible要与其相同。驱动要求设备树提供什么,就要按照这个要求去编写设备树。
编程步骤:
1.在设备树的根节点下添加led的设备节点

设备树文件位置:内核源码目录中 arch/arm/boot/dts/100ask_imx6ull-14x14.dts
修改、编译后得到 a****rch/arm/boot/dts/100ask_imx6ull-14x14.dtb
替换开发板上的/boot/100ask_imx6ull-14x14.dtb文件。
2.修改platform_driver的源码

添加.of_match_table成员用来匹配设备树中对应的节点。修改.probe如下。

if (!np) 判断如果节点不存在,因为该 platform_driver 对应的platform_device不一定来自设备树。使用of_property_read_u32函数从np节点里读出pin属性保存在变量led_pin中。
不使用设备树的特征(结合 总线设备驱动模型 章节的 框架简述 共同理解):
使用设备树编写驱动程序,要编译驱动上层文件leddrv.c(file_operations),驱动底层文件board_A_led.c(platform_device)和chip_demo_gpio.c(platform_driver) 。在chip_demo_gpio.c定义的结构体用来执行对硬件的操作和读取platform_device中的硬件资源,将结构体返回到leddrv.c。
使用设备树就替代了board_A_led.c的作用只需要编译其余两个文件和设备树文件。
调试技巧:
1.设备树信息

cd /sys/firmware/devicetree/base/
以上目录对应设备树的根节点,可以从此进去找到自己定义的节点。
节点是目录,属性是文件。
属性值是字符串时,用 cat 命令可以打印出来;属性值是数值时,用 hexdump 命令可以打印出来。
2.platform_device信息
/sys/devices/platform
以上目录含有注册进内核的所有 platform_device ,一个设备对应一个目录,进入某个目录后,如果它有"driver"子目录,就表示这个 platform_device 跟某个 platform_driver 配对了。
3.platform_device信息
/sys/bus/platform/drivers
以上目录含有注册进内核的所有 platform_driver ,一个 driver 对应一个目录,进入某个目录后,如果它有配对的设备,可以直接看到。上图imx-i2c与2个平台设备配对了21a0000.i2c 和 21a4000.i2c 。
我的理解:
Pinctrl子系统:
内核的Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt
pin controller、client device。
前者提供服务:可以用它来复用引脚、配置引脚。
后者使用服务:声明自己要使用哪些引脚的哪些功能,怎么配置它们。
用pinctrl子系统管理引脚的复用和配置。左边是pin controller节点,可以由芯片厂商提供的工具生成,对于mx6ull可有Pin Tool生成,是pinctrl子系统对所选设备进行配置。右边是client device节点,该节点中的内容会被填充到 platform_device 结构体。
GPIO子系统
1.确定某个 GPIO Controller 的基准引脚号(base number):


所以 gpio4 这组引脚的基准引脚号就是 96,这也可以"cat base"来再次确认。02a8000就是这个GPIO控制器的基地址。
假设按键对应的引脚号是GPIO4_14,

2.使用在自己定义probe中使用gpiod_get函数从设备树中获取GPIO资源:


设备树中的配置信息
pinctrl_leds: ledgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x000110A0
>;
};
在根节点下添加所定义设备节点的信息
myled {
compatible = "cumtchw,leddrv";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_leds>;
led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
};
gpiod_get
gpiod_get_index
of_find_gpio
dev是定义的platform_device结构体。con_id是在设备树的根节点下添加的设备信息,属性名称有命名规范,只需要传入"led",gpios会自动补全,如下图。flags用来控制 GPIO 的初始化行为、电平极性、方向等核心特性,是连接驱动代码和设备树GPIO_ACTIVE_LOW等属性的关键桥梁。返回句柄,
命名的补全
3.在自己定义的remove函数中使用gpiod_put函数释放在probe函数中获得的句柄:
Linux对中断的处理:
arm对中断的使用过程:
1.初始化
a.设置中断源
b.设置中断控制器(屏蔽、优先级)
c.设置cpu总开关(使能中断)
2.执行程序
3.产生中断:按下按键->中断控制器->cpu
4.cpu每执行一条指令都会检测有无异常或中断。
5.cpu执行一条跳转指令(异常向量),跳转地址保存的是另一条跳转指令,对于不同的异常再跳转不同的地址执行程序处理异常。
6.这些处理函数保存现场、处理异常(分辨中断源、调用对应函数)、恢复现场。

3、4、5都是硬件处理,6是软件处理
执行中断时栈的使用:


对于有MMU的系统,每个进程的空间相互隔离,进程A无法修改进程B的代码和数据,但对于没有MMU的系统就无法保证进程A的内容不会遭到其他进程的破坏。
对于ARM为避免出现上述情况,cpu会将寄存器的内容放在栈中(保护现场 )再去执行其他进程,当需要再次执行这个进程时cpu会将之前保存在栈中的寄存器内容取出在执行程序(恢复现场)。
Linux中断处理原则:
1.不能嵌套,如下图的中断处理框图中,当 CPU 正在处理一个中断(执行中断处理函数handle_irq)时,会关闭本地 CPU 的中断响应 ,直到这个中断的handle_irq流程执行完毕、恢复中断后,才会响应下一个硬件中断。
2.中断处理越快越好
耗时中断的处理方式:
1.分为上半部分\下半部分
![]()
对于中断框图的情景描述:
情况一:被同一个中断打断
中断A的执行过程中,执行到下半部时又被新的中断A打断,这时会从头开始执行再次执行中断上半部到4判断直接完成处理,然后恢复前一个A中断,继续执行中断下半部。
情况二:被其他中断打断
中断A执行到下半部时被中断B打断,返回1重新处理,到4时preempt_count=1直接完成处理,然后恢复中断A的现场执行中断下半部。
上述中断被其他中断打断发生在开中断之前,否则就违背了中断不能嵌套的处理原则。
如上述中断的下半部只执行了一次如何完整的执行完所有中断?
使用软件处理中断实现下半部,处理的是所有软件中断的下半部。
上半部分确实要求尽量快速执行,且在执行过程中通常不能被打扰 ,而下半部分相对来说执行时间较长,并且在执行时可以被当前中断或其他中断打断,所以中断下半部分存放耗时的工作。
2.用内核线程处理中断
构造work.func内核线程放入queue(队列)处理中断下半部。进一步可以对每一个中断都创建一个内核线程,这样在有多个cpu时就可以在不同的cpu执行不同的work队列,不至于多个中断都集中在一个work线程中集中在一个cpu上(线程化的中断 )。
Linux中断重要的数据结构:
中断结构图

中断处理函数可能存在于GIC、GPIO模块、外部设备。

irp_desc结构体在 include/linux/irqdesc.h 中定义,上图为部分内容。

中断处理的分层调用(并不是嵌套中断)遍历的是同一个action链表
struct irq_desc 数组
irq_data
chip
domain
ops
xlate 函数,解析设备树节点。
map 函数,将硬件设备号转换为虚拟中断号保存在platform_device中。
hwirq_max
revmap_size
linear_remap[] 这三个保存hwirq和irq及其之间的关系。
handle_irq
action 列表保存每个中断动作描述符
handler 中断处理上半部,需要紧急处理的内容
thread_fn 线程处理函数
thread 内核创建的线程
对于既提供handler又提供thread_fn就是中断上半部和下半部。只提供handler就是一般的request_irq函数。只提供thread_fn就完全由内核线程来处理中断。
对于A号中断,他的action链表是空的,他的handle_irq只是分辨发生的是哪一个GPIO中断,他会读取GPIO寄存器得到hwirq,根据hwirq得到之前映射的irq。然后去调用对应的GPIO的handle_irq函数,此时GPIO的handle_irq会调用在它所在struct irq_desc数组中的action链表中的用户提供的中断处理函数来执行。
软件中断号和硬件中断号:
引入软件中断号:
由 void raise_softirq(unsigned int nr) 触发软件中断。
由 void open_softirq(int nr, void (*action)(struct softirq_action *)) 设置软件中断的处理函数。
通过该函数为某个中断注册处理函数。irq既是软件中断号。
在设备树中指定硬件中断号:
interrupt-parent = <&gpio1> //指定是GPIO1的中断
interrupt = <5 RISING> //指定5号中断,5既是硬件中断号hwirq
对于硬件中断号只有指定了parent才有意义,说明他是属于某个域的(domain)。内核会根据设备树中的 interrupt-parent 找到对应的 irq_domain 结构体,结构体中的 xlate 函数会将设备树中的硬件设备号转换成虚拟设备号。
中断下半部:
对于硬件触发的中断是中断的上半部,要在中断的上半部的中断服务函数中初始化和使能中断的下半部( tasklet_init**)并调用中断下半部。在中断上半部会将中断下半部的中断处理函数放入链表中(** tasklet_schedule**),在执行到下半部时再去链表中调用服务函数。**

state 有两位,bit0为1表示已经将tasklet_schedule 把该 tasklet 放入队列了tasklet_schedule 函数会判断该位,如果已经等于 1 那么它就不会再次把 tasklet 放入队列。bit1等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0。
count 表示该 tasklet 是否使能:等于 0 表示使能了,非 0 表示被禁止了。对于 count 非 0 的 tasklet,里面的 func 函数不会被执行。
在设备树中指定中断,在代码中获得中断:
在设备树中描述硬件框图
设备树中中断部分的编写规则
驱动工程师只需要在自己的设备节点指明interrupt-parent和**interrupts**这两个属性,即可使用对应interrupt-controller的中断。
1.对于一些设备树中可以转换成 platform_device 的节点,就使用platform_get_resource获取中断号。
2.对于不能转换的节点,例如I2C设备、SPI设备。对于 I2C 设备节点,I2C 总线驱动在处理设备树里的 I2C 子节点时,也会处 理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里,代码如下(drivers/i2c/i2c-core.c):
对于 SPI 设备节点,SPI 总线驱动在处理设备树里的 SPI 子节点时,也会处理其中的中断信息。一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里,代码如下(drivers/spi/spi.c):

3.如果既不能转换为platform_devive,也没有写好的驱动程序,那就用 of_irq_get() 自己去解析设备树得到中断号。
4.对于GPIO可以使用**gpiod_to_irq()** 获取虚拟中断号
然后使用request_irq或者devm_request_irq,还有一些线程化处理函数的申请函数等等在虚拟中断号对应的irq_desc的action链表中添加对应中断处理函数即可
echo "7 4 1 7" > /proc/sys/kernel/printk
打印内核信息
中断上下文:
(1)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。
(2)中断下文:执行在内核空间的中断服务程序。
中断上下文注意事项:
运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。所以中断处理程序代码要受到一些限制,在中断代码中不能出现实现下面功能的代码:
(1)睡眠或者放弃CPU。 因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。牢记:中断服务子程序一定不能睡眠(或者阻塞)。
(2)尝试获得信号量 如果获得不到信号量,代码就会睡眠,导致(1)中的结果。
(3)执行耗时的任务 中断处理应该尽可能快,因为如果一个处理程序是IRQF_DISABLED类型,他执行的时候会禁止所有本地中断线,而内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。中断处理程序的任务尽可能放在中断下半部执行。
(4)访问用户空间的虚拟地址。因为中断运行在内核空间。
休眠与唤醒:
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait); //用这个宏来初始化 gpio_key_wait 这个队列
wait_event_interruptible(gpio_key_wait, g_key);
//在 gpio_key_drv_read 函数中将当前线程放到 gpio_key_wait 这个队列进行休眠
wake_up_interruptible(&gpio_key_wait); //从队列中取出上面的线程并唤醒
唤醒线程后会返回线程中继续执行休眠函数下面的代码。
POLL机制:
在drv_poll中需要实现
1.将线程挂入队列 poll_wait()。
2.返回设备状态,查询是否有数据可读。
对于阻塞或非阻塞,其程序实现还是在驱动程序当中,驱动程序中存在这个功能但是APP不一定会使用。
定时器:
初始化定时器的结构体:

expires 超时时间 function 定时器处理函数
setup_timer(timer, fn, data) 初始化定时器结构体参数
void add_timer(struct timer_list *timer) 将定时器添加进内核
int mod_timer(struct timer_list *timer, unsigned long expires) 修改超时时间,放在中断服务函数中
int del_timer(struct timer_list *timer) 删除定时器
定时器的超时时间是基于 jiffies 这个全局变量来递增的,expires = jiffies + HZ/5 每经过 HZ/5 的时间定时器触发一次,对于定时器也是在中断的上下文中执行,不能休眠要尽快返回(在中断前cpu会关闭进程调度此时不能调用其他进程,此时睡眠会导致系统死掉),与滴答定时器不同,定时器属于软件中断。
工作队列:
工作队列使用的是内核线程,work_struct中的函数工作在内核上下文。构造work函数,使用schedule_work()将函数添加进队列。

中断的线程化处理:
中断下半部用一个内核线程来处理。使用request_threaded_irq注册中断,使用free_irq卸载中断。

irq 中断号
handler 上半部中断服务函数,可以为空。若不提供则使用 irq_default_primary_handler ,直接返回 IRQ_WAKE_THREAD 唤醒线程去执行thread_fn ,如果提供返回值必须是 IRQ_WAKE_THREAD 才能唤醒线程。
thread_fn 在线程中执行的函数,如果中断被正确处理应该返回 IRQ_HANDLED 。
mmap:
应用程序不能直接读写驱动程序中的buffer,需要在用户态buffer和内核态buffer之间进行一次数据拷贝。但是在读写数据量大时不能接受所有使用地址映射的方式在应用与内核间建立联系,让APP 在用户态直接读写。
驱动程序需要做的三件事:
1.确定物理地址
2.确定属性:是否使用buffer、cache
3.建立映射关系
查看映射关系:
ps 查看pid
cat /proc/pid/maps 得到映射关系
内存的映射数据结构:
每个APP在内核中都有一个 task_struct 结构体用来描述一个进程。该结构体中的 mm_struct 用来管理该进程占用的内存。
mm_struct.mmap用来描述虚拟地址,mm_struct.pgd(Page Global Directory,页目录)描述物理地址 ,内核用一系列的 vm_area_struct 来描述APP中不同的地址空间例如代码段、数据段、BSS 段、栈等等,还有共享库。vm_area_struct 表示APP的一块虚拟内存空间,其中的 vm_start、vm_end 是虚拟地址。
每一个 APP 的虚拟地址可能相同,物理地址不相同,这些对应关系保存在 pgd 中。
arm架构内存映射:
要提前设置页表的使用方式。
1.只是用一级页表
CPU 发出虚拟地址 vaddr,假设为 0x12345678,MMU 根据 vaddr[31:20]找到一级页表项,即虚拟地址空间的第0x123个页表项,页表项中保存物理基地址,结合偏移地址0x45678找到物理地址。
2.使用二级页表
依然是一级页表的第0x123个页表项,根据一级页表项中的内容得知其是一个二级页表项和二级页表项的物理地址,vaddr[19:12]表示的是二级页表项中的索引 index 即 0x45,在二级页表项中找到第 0x45 项,其中含有物理基地址,他跟vaddr[11:0] 偏移地址组合得到物理地址。








