韦东山 嵌入式linux教程 笔记

资源链接

1、QEMU开发板在线文档: http://wiki.100ask.org/Qemu
2、QEMU开发板网盘资源:http://weidongshan.gitee.io/informationdownloadcenter/boards/Sim/qemu/index.html

3、QEMU开发板镜像git资源:
http://wiki.100ask.org/Download_ubuntu-18.04_imx6ul_qemu
http://wiki.100ask.org/Download_ubuntu-16.04_imx6ul_qemu
4、QEMU开发板源码git:
考虑到代码仓库过多,特使用repo工具管理代码。
先用git clone下载repo工具,再用repo工具下载源码:

book\@100ask:\~\$ git clone https://e.coding.net/codebug8/repo.git
book\@100ask:\~\$ mkdir -p 100ask_imx6ull-qemu && cd 100ask_imx6ull-qemu
book\@100ask:\~/100ask_imx6ull-qemu\$ ../repo/repo init -u https://e.coding.net/weidongshan/manifests.git -b linux-sdk -m  imx6ull/100ask-imx6ull_qemu_release_v1.0.xml --no-repo-verify
book\@100ask:\~/100ask_imx6ull-qemu\$ ../repo/repo sync -j2

如果一切正常,你在/home/book目录下创建了一个100ask_imx6ull-qemu目录,里有如下一行内容:
在这里插入图片描述

在这里插入图片描述

一、常用命令

格式: 命令 [选项] [参数] eg:ls -l /home
1、选项和参数不一定存在
2、选项以"-"来指明的
3、以空格隔开,多个空格视为一个空格

pwd:显示路径
ls: 显示文件
cd:切换路径
cd …:返回上一级路径
mkdir:新建目录
touch:新建文件
cp test1.txt test2.txt:复制文件1到文件2
gedit:编辑文件
cat:显示文件内容到terminal
rm:删除文件
rmdir:删除目录
clear:清屏
man + 命令:查看命令选项
mv 旧文件名 新文件名
mv 旧目录名 新目录名
mv 文件名 新目录名

简化版指令:
cd ~:回到home目录
cd .:切换到当前目录
cd …:切换到上级目录
cd …/… :切换到上上级目录

二、shell

shell就是一个应用程序,通过键盘串口给它发送命令,回车后它就会解析指令、参数,并执行命令。
1、接收键盘数据并回显;
2、解析输入的字符串,寻找对应程序,并执行对应程序
a、去哪里找?去PATH环境变量所指示的位置找

三、如何更改PATH?

1、临时设置:只对当前终端有效:
2、永久设置方法1:
①、sudo gedit /etc/environment
②、“”里加上 :/home/solo
3、永久设置方法2:
export PATH=$PATH:/home/solo

四、路径

1、绝对路径 /home/solo
2、相对路径
当前路径为 /bin, 那么./pwd就是执行 /bin/pwd

当前路径为/home/solo/aaa,
cd …/video就到达 /home/solo/video
cd …/…就到达 /home

五、vi编辑器

1、配置
cd /etv/vim
cp vimrc ~/.vimrc
cd ~
gedit .vimrc

在.vimrc最后加入以下内容:
"关闭兼容内容
set nocompatible
"显示行号
set number
"backspace设置为两个空格
set backspace=2
"tab键设置为4个空格
set tabstop=4
"设置自动对齐为4个空格
set shiftwidth=4
"搜索时不区分大小写
set ignorecase
"搜索时高亮显示
set hlsearch

2、三种模式 (【第2篇】环境搭建、Linux基本操作、工具使用 – P25详细介绍了快捷操作)
a、一般模式:光标移动、复制、粘贴、删除: i、a、o进入
一般模式,HJKL可代替上下左右
b、编辑模式
c、查找模式
不想保存 :q!

在这里插入图片描述

六、进阶命令

1、find 查找文件
find /home/solo/ -name “test.txt”
2、grep 查找文件中符合条件的字符串
grep -rn “string” filename
3、file 识别文件类型
file ~/.vimrc
4、which和whereis查找命令或程序的位置
which gcc
whereis pwd
5、 无损压缩
a、单文件压缩: gzip 和bzip2
b、
c、

七、NAT配置 网络 2-P34

ubuntu采用NAT连接;获取ubuntuIP;
在win上配置与ubuntuIP的虚拟连接;
利用mobaxterm连接ubuntu
在这里插入图片描述
2、windows操作
在这里插入图片描述
在这里插入图片描述
3、ubuntu操作
①修改 Ubuntu 的 mountd 端口
在 NAT 网络下,要想开发板能通过 NFS 挂载 Ubuntu,需要修改 mountd 端口为 9999
如果你还不会用 vi 命令,可以在 Ubuntu 桌面启动终端,执行以下命令,用 GUI 工具修改/etc/services:

sudo gedit /etc/services

添加 2 行:

mountd 9999/tcp
mountd 9999/udp

在这里插入图片描述
② NFS 重启:
sudo /etc/init.d/nfs-kernel-server restart
③ 查看端口:
sudo rpcinfo -p
请参考如下图操作:
在这里插入图片描述

八、开发板挂载 Ubuntu 的 NFS 目录

什么 nfs 协议?
NFS 实现了一个跨越网络的文件访问功能,整个架构为 Client-Server架构,客户端和服务端通过 RPC 协议进行通信,RPC 协议可以简单的理解为一个基于 TCP 的应用层协议,它简化命令和数据的传输。

NFS 最大的特点是将服务端的文件系统目录树映射到客户端,而在客户端访问该目录树与访问本地文件系统没有任何差别,客户端并不知道这个文件系统目录树是本地的还是远在另外一台服务器。

我们为什么要挂载 ubuntu 的 nfs 目录?
我们有些时候需要多次调试开发板文件系统内的某个应用程序,这就需要多次进行编译拷贝等操作,所以我们在前期进行调试时可以直接让开发板使用 ubuntu 的 nfs 目录下文件系统来进行远程调试,用以提高调试效率,加快研发速度。

在这里插入图片描述

1、确定windows使用的网卡和IP;

在这里插入图片描述
2、确保开发板与Windows 能 ping 通 后,假设 Windows 的 IP 是 192.168.1.17,在开发板上执行以下命令挂载 NFS:

mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.1.17:/home/book/nfs_rootfs /mnt

注意:必须指定 port 为 2049,mountport 为 9999。
mount 命令用来挂载各种支持的文件系统协议某个目录下。
mount 成功之后 , 开 发 板 在 /mnt 目 录 下 读 写 文 件 时 , 实 际 上 访 问 的 就 是 Ubuntu 中 的/home/book/nfs_rootfs 目录,所以开发板和 Ubuntu 之间通过 NFS 可以很方便地共享文件。
在开发过程中,在 Ubuntu 中编译好程序后放入/home/book/nfs_rootfs 目录,开发板 mount nfs 后就可以直接使用/mnt 下的文件。

总结:使用 NAT 时,开发板要想访问 Ubuntu 是通过 Windows 进行中转的。所以ubuntuIP,windowsIP都要用到。
在这里插入图片描述
在这里插入图片描述

九、编程前的准备工作

能够给开发板编译第 1 个 APP、第 1 个驱动,并运行起来,就算成功搭好环境了。
给 PC 机编译程序时用的命令是:

gcc -o hello hello.c

给开发板编译程序时用的命令类似(不同开发板 gcc 的前缀可能不同):

arm-linux-gcc -o hello hello.c

所以,在给开发板编译程序之前,需要先编写项目源码( APP 和驱动程序的源码用 git命令下载,内核源码和工具链用 repo 命令下载),再安装交叉编译工具链,就是 arm-linux-gcc 这类工具。
1、用git管理源码,可在ubuntu直接执行git下载

$ git clone https://e.coding.net/weidongshan/01_all_series_quickstart.git

2、repo是 Git 之上构建的工具。Repo 帮助管理许多 Git 存储库。Repo 只是为了更易于使用 Git。repo 命令是可执行的 Python脚本,您可以将其放置在路径中的任何位置。
在这里插入图片描述
a、repo配置
要设置git的邮箱和用户名,git邮箱和用户名请根据个人情况进行配置。

book@100ask:~$ git config --global user.email “[email protected]
book@100ask:~$ git config --global user.name “100ask”

在这里插入图片描述
b、repo下载,不同开发板下载地址不同

3、配置交叉编译工具链
交叉编译工具链用来在Ubuntu主机上编译应用程序,而这些应用程序是在ARM等其他平台上运行

设置交叉编译工具主要是设置PATH, ARCH和CROSS_COMPILE三个环境变量。
建议使用“永久生效”的方法。使用多种开发板,使用“临时生效”的方法。
a、永久生效
执行:gedit ~/.bashrc或者vi ~/.bashrc
在行尾添加下面3行:

export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin

设置完毕后,要执行 source ~/.bashrc 命令使其生效,这条命令是加载这些设置的环境变量

b、临时生效
执行“export”命令设置环境变量,该设置只对当前打开的terminal终端有效。
执行以下3个命令,第3个命令很长,这里使用小字体方便大家复制:

export ARCH=arm

export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-

export PATH=$PATH:/home/book/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin

c、 测试交叉编译工具链
(1) 测试环境变量:

book@100ask:~$ echo $ARCH
arm
book@100ask:~$ echo $CROSS_COMPILE
arm-buildroot-linux-gnueabihf- 

(2) 测试交叉编译器:
执行以下命令,结果见后图:

book@100ask:~$ arm-buildroot-linux-gnueabihf-gcc -v

4、开发驱动程序时,驱动程序中用到的函数都是来自内核

十、开发板上运行第一个APP

前提:使用 GIT 下载源码、使用 repo 下载工具链,并配置了交叉编译工具链

1、在ubuntu编译程序
gcc 编译是给 PC 机编译的,里面的机器指令是 x86 的。
我们要想给 ARM 板编译出 hello 程序,需要使用交叉编译工具链,比如:

$ arm-linux-gnueabihf-gcc -o hello hello.c

2、把编译生成的 hello 文件拷贝到 Ubuntu nfs 服务目录下,备用:

$ cp hello /home/book/nfs_rootfs

3 、在开发板上拷贝文件
开发板启动后通过 nfs 挂载 Ubuntu 目录的方式,将相应的文件拷贝到开发板上。
a、假设使用的是VMware NAT方式,Windows IP为192.168.1.100,在开发板上执行以下命令(注意:必须指定port为2049、mountport为9999):

[root@board:~]# mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.1.100:/home/book/nfs_rootfs /mnt
[root@board:~]# cp /mnt/hello ./hello

b、假设使用的是VMware桥接方式,Ubuntu IP为192.168.1.100,在开发板上执行以下命令:

[root@board:~]# mount -t nfs -o nolock,vers=3 192.168.1.100:/home/book/nfs_rootfs /mnt
[root@board:~]# cp /mnt/hello ./hello

4、最后,在开发板上执行如下操作添加可执行权限,并运行程序:

[root@board:~]# chmod +x hello
[root@board:~]# ./hello

十一、开发板上运行第一个驱动

1、需要编译内核、模块,并放到开发板上去
为什么编译驱动程序之前要先编译内核?
① 驱动程序要用到内核文件:
比如驱动程序中这样包含头文件:#include <asm/io.h>,其中的 asm 是一个链接文件,指向 asm-arm或 asm-mips,这需要先配置、编译内核才会生成 asm 这个链接文件。
② 编译驱动时用的内核、开发板上运行的内核,要一致:
两个内核不一致时会导致一些问题。所以我们编译驱动程序前,要把自己编译出来到内核放到板子上去,替代开发板原来的内核。
③ 更换板子上的内核后,板子上的其他驱动也要更换:
板子使用新编译出来的内核时,板子上原来的其他驱动也要更换为新编译出来的。

2、编译内核(不同开发板源码不同):

book@100ask:~/100ask_stm32mp157_pro-sdk$ cd Linux-5.4
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ make 100ask_stm32mp157_pro_defconfig
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ make uImage LOADADDR=0xC2000040 -j4
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ make dtbs
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ cp arch/arm/boot/uImage ~/nfs_rootfs
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ cp arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb ~/nfs_rootfs

3、编译内核模块

进入内核源码目录后,就可以编译内核模块了:

book@100ask:~$ cd 100ask_stm32mp157_pro-sdk/Linux-5.4
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ make modules -j4
book@100ask:~/100ask_stm32mp157_pro-sdk/Linux-5.4$ sudo make INSTALL_MOD_PATH=/home/book/nfs_rootfs modules_install

最后一条命令是把模块安装到 /home/book/nfs_rootfs 目 录 下 备 用 , 会 得 到
/home/book/nfs_rootfs/lib/modules目录。
安装模块后的/home/book/nfs_rootfs/目录结构如下图所示。
注意:下图用到tree命令,如果提示没有该命令,需要执行“sudo apt install tree”命令安装tree
工具(前提是Ubuntu能上网)。
在这里插入图片描述
4、安装内核和模块到开发板上:
执行上述步骤后,在Ubuntu的/home/book/nfs_rootfs目录下已经有了zImage或uImage、dtb文件,并且有lib/modules子目录(里面含有各种模块)。

如果你使用的是VMware NAT方式,假设Windows IP为192.168.1.100,在开发板启动进入Linux后,输入root登录,然后执行以下命令(注意:必须指定port为2049、mountport为9999):

mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.1.100:/home/book/nfs_rootfs /mnt
cp /mnt/zImage /boot 或 cp /mnt/uImage /boot 
cp /mnt/*.dtb /boot
cp /mnt/lib/modules /lib -rfd
sync
reboot

如果你使用的是VMware桥接方式,假设Ubuntu IP为192.168.1.100,在开发板上执行以下命令:

mount -t nfs -o nolock,vers=3 192.168.1.100:/home/book/nfs_rootfs /mnt
cp /mnt/zImage /boot 或 cp /mnt/uImage /boot 
cp /mnt/*.dtb /boot
cp /mnt/lib/modules /lib -rfd
sync
reboot

最后reboot开发板,它就使用新的zImage或uImage、dtb、模块了。

5、体验第 1 个驱动程序
①把第 1 个驱动程序 01_hello_drv 上传到 Ubuntu 后,修改它的 Makefile,设置其中的 KERN_DIR 变量为内核的源码目录,以 IMX6ULL 为例,如下:

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

②编译:在01_hello_drv目录下执行make命令即可编译驱动程序及测试程序,如下
在这里插入图片描述

③运行:
启动开发板后,通过 nfs 挂载 Ubuntu 目录的方式,将上面两个文件拷贝到开发板上。
VMware NAT方式,Windows IP为192.168.1.100,在开发板上执行以下命令(注意:必须指定port为2049、mountport为9999):
[root@board:~]# mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.1.100:/home/book/nfs_rootfs /mnt
VMware桥接方式,Ubuntu IP为192.168.1.100,在开发板上执行以下命令:

[root@board:~]# mount -t nfs -o nolock,vers=3 192.168.1.100:/home/book/nfs_rootfs /mnt

挂载 NFS 成功后,把驱动和测试程序复制到开发板上:

[root@board:~]# cp /mnt/hello_drv.ko ./
[root@board:~]# cp /mnt/hello_drv_test ./

然后安装驱动:

[root@board:~]# insmod hello_drv.ko

执行lsmod查看所有驱动:
在这里插入图片描述
执行“cat /proc/devices”:
在这里插入图片描述
执行“ls -l /dev/hello”,可以发现有这个设备节点,并且它的主设备号跟上图一样:
在这里插入图片描述
最后执行测试程序:

./hello_drv_test -w www.100ask.net
./hello_drv_test -r

在这里插入图片描述
如果不想看到驱动调试信息,可以先执行以下命令用来关闭内核的打印信息:

echo "1 4 1 7" > /proc/sys/kernel/printk

在这里插入图片描述
安装驱动程序时,如果有以下提示信息,原因就是板子上运行的内核太老了,解决方法就是:
先编译内核、
替换板能上的内核,
再重新编译、
安装驱动程序:
在这里插入图片描述

十二、开发板使用手册:

在这里插入图片描述
百问网 IMX6ULL-QEMU 虚拟开发板的使用也很简单,打开 WIKI 首页后,点击类似上图的“百问网 imx6ullqemu”进去,即可看到使用方法。


第 4 篇 嵌入式 Linux 应用开发基础知识


一、编译:

一个 C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等4 步才能变成可执行文件。

1、 怎么编译多个文件
① 一起编译、链接:

gcc -o test main.c sub.c

② 分开编译,统一链接:

gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
gcc -o test main.o sub.o

二、makefile的使用:

hello: hello.c
	gcc -o hello hello.c
clean:
	rm -f hello

1、想达到什么样的效果?
修改源文件或头文件,只需要重新编译牵涉到的文件,就可以重新生成 APP

2、 Makefile 其实挺简单
一个简单的 Makefile 文件包含一系列的“规则”,其样式如下:

目标(target): 依赖(prerequiries)<tab>命令(command)

命令被执行的 2 个条件
依赖文件目标文件新,
目标文件还没生成。

3、 先介绍 Makefile 的 2 个函数
A、 $(foreach var,list,text)
简单地说,就是 for each var in list, change it to text。 对 list 中的每一个元素,取出来赋给 var,然后把 var 改为 text 所描述的形式。
例子:

objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终 dep_files := .a.o.d .b.o.d

B、 $(wildcard pattern)
pattern 所列出的文件是否存在,把存在的文件都列出来。
例子:

src_files := $( wildcard *.c) // 最终 src_files 中列出了当前目录下的所有.c 文件

4、 一步一步完善 Makefile
第 1 个 Makefile,简单粗暴,效率低:

test : main.c sub.c sub.h
	gcc -o test main.c sub.c

第 2 个 Makefile,效率高,相似规则太多太啰嗦,不支持检测头文件:

test : main.o sub.o
	gcc -o test main.o sub.o
main.o : main.c
	gcc -c -o main.o main.c
sub.o : sub.c
	gcc -c -o sub.o sub.c
clean:
	rm *.o test -f

第 3 个 Makefile,效率高,精炼,不支持检测头文件:

test : main.o sub.o
	gcc -o test main.o sub.o
%.o : %.c
	gcc -c -o $@ $<
clean:
	rm *.o test -f

第 4 个 Makefile,效率高,精炼,支持检测头文件(但是需要手工添加头文件规则):

test : main.o sub.o
	gcc -o test main.o sub.o
%.o : %.c
	gcc -c -o $@ $<
sub.o : sub.h
clean:
	rm *.o test -f 

第 5 个 Makefile,效率高,精炼,支持自动检测头文件:

objs := main.o sub.o

test : $(objs)
	gcc -o test $^
	
#需要判断是否存在依赖文件
#.main.o.d .sub.o.d
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))

#把依赖文件包含进来
ifneq ($(dep_files),)
	include $(dep_files)
endif

%.o : %.c
	gcc -Wp,-MD,.$@.d -c -o $@ $<
	
clean:
	rm *.o test -f
distclean:
	rm $(dep_files) *.o test -f

5、makefile解析

可以使用“-f”选项指定make的文件,不再使用名为“Makefile”的文件,比如:

make -f Makefile.build

可以使用“-C”选项指定make的目录,切换到其他目录里去make,比如:

make -C a/ -f Makefile.build  //去a/目录执行Makefile.build

可以指定目标,不再默认生成第一个目标:

make -C a/ -f Makefile.build other_target

B. 即时变量、延时变量:
变量的定义语法形式如下:

A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一次定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果 D 在前面是延时变量,那么现在它还是延时变量;

GNU make 中对变量的赋值有两种方式:延时变量、立即变量
上图中,变量 A 是延时变量,它的值在使用时才展开、才确定。比如:

A = $@
test:
	@echo $A

上述 Makefile 中,变量 A 的值在执行时才确定,它等于 test,是延时变量。
如果使用“A := $@”,这是立即变量,这时$@为空,所以 A 的值就是空。

C. 变量的导出(export)
在编译程序时,我们会不断地使用“make -C dir”切换到其他目录,执行其他目录里的 Makefile。如果想让某个变量的值在所有目录中都可见,要把它 export 出来。
比如“CC = $(CROSS_COMPILE)gcc”,这个 CC 变量表示编译器,在整个过程中都是一样的。定
义它之后,要使用“export CC”把它导出来。

D、放置第 1 个目标:
执行 make 命令时如果不指定目标,那么它默认是去生成第 1 个目标。
有时候不太方便把第 1 个目标完整地放在文件前面,这时可以在文件的前面直接放置目标,在后面再完善它的依赖与命令。比如:

First_target: // 这句话放在前面
.... // 其他代码,比如 include 其他文件得到后面的 xxx 变量
First_target : $(xxx) $(yyy) // 在文件的后面再来完善
	command

F. 假想目标:
我们的 Makefile 中有这样的目标:

clean:
	rm -f $(shell find -name "*.o")
	rm -f $(TARGET)

如果当前目录下恰好有名为“clean”的文件,那么执行“make clean”时它就不会执行那些删除命令。
这时我们需要使用下面的语句把“clean”设置为假想目标:

.PHONY : clean

G. 常用的函数:
i. $(foreach var,list,text)
简单地说,就是 for each var in list, change it to text。 对 list 中的每一个元素,取出来赋给 var,然后把 var 改为 text 所描述的形式。
例子:

objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终 dep_files := .a.o.d .b.o.d

在这里插入图片描述
在这里插入图片描述

ii. $(wildcard pattern):展开指定目录
pattern 所列出的文件是否存在,把存在的文件都列出来。
例子:

src_files := $( wildcard *.c) // 最终 src_files 中列出了当前目录下的所有.c 文件

$(notdir $(var)):去掉路径

src_files := $( wildcard *.c) // 最终 src_files 中列出了当前目录下的所有.c 文件
file = $(notdir $(src_files))

$(notdir $(var)):只含路径

iii. $(filter pattern…,text)
把 text 中符合 pattern 格式的内容,filter(过滤)出来、留下来。
例子:

obj-y := a.o b.o c/ d/
DIR := $(filter %/, $(obj-y)) //结果为:c/ d/

iv. $(filter-out pattern…,text)
把 text 中符合 pattern 格式的内容,filter-out(过滤)出来、扔掉。
例子:

obj-y := a.o b.o c/ d/
DIR := $(filter-out %/, $(obj-y)) //结果为:a.o b.o

vi. $(patsubst pattern,replacement,text):替换文件后缀
寻找text’中符合格式pattern’的字,用replacement’替换它们。pattern’和`replacement’
中可以使用通配符。
比如:

subdir-y := c/ d/
subdir-y := $(patsubst %/, %, $(subdir-y)) // 结果为:c d

makefile – P495开始略

推荐《跟我一起写makefile》

1、伪目标 .PHONY

假如当前目录已有文件叫clean,那么make clean不会执行,这时需要将clean设为伪目标:

CC = gcc
OBJ = main.o add.o sub.o
output: $(OBJ)
	$(CC) -o $@ $^
%.o: %.c
	$(CC) -c $<

.PHONY:clean
clean:
	rm $(OBJ) output

2、makefile变量和赋值

通过$()来完成变量的引用

1、(:=)立即赋值: var1 := aaa
2、(=)延迟赋值:var1 = aaa /整个makefile使用到var1的都会使用makefile里var1最后被指定的值
3、(?=):前面赋值 :如果var前面赋值了,就用前面的值;如果前面没有赋值,就用?指定的值

在这里插入图片描述
4、(+=)追加赋值:如果var前面已经赋值aaa,再用+=赋值bbb,那么var最后值为aaa bbb(中间有个空格)

3、自动化变量

$@ 表示所有目标文件
$<
$^ 所有文件依赖的列表
%
在这里插入图片描述

在这里插入图片描述

三、文件IO

在 Linux 系统中,一切都是“文件”:普通文件、驱动程序、网络通信等等。所有的操作,都是通过“文件 IO”来操作的。要掌握文件操作的常用接口。

1、open write

头文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

函数原型:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

函数说明:
① pathname 表示打开文件的路径;
② Flags 表示打开文件的方式,常用的有以下 6 种,
a. O_RDWR 表示可读可写方式打开;
b. O_RDONLY 表示只读方式打开;
c. O_WRONLY 表示只写方式打开;
d. O_APPEND 表示如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面;
e. O_TRUNC 表示如果这个文件中本来是有内容的,则原来的内容会被丢弃,截断;
f. O_CREAT 表示当前打开文件不存在,我们创建它并打开它,通常与 O_EXCL 结合使用,当没有文件时
创建文件,有这个文件时会报错提醒我们;
③ Mode 表示创建文件的权限,只有在 flags 中使用了 O_CREAT 时才有效,否则忽略。
④ 返回值:打开成功返回文件描述符,失败将返回-1。在这里插入图片描述

四、Framebuffer

1、LCD 操作原理

Linux 系统中通过 Framebuffer 驱动程序来控制 LCD。Framebuffer 就是一块内存,里面保存着一帧图像。

假设 LCD 的分辨率是 1024x768,每一个像素的颜色用 32 位来表示,那么 Framebuffer 的大小就是:
1024x768x32/8=3145728 字节。

简单介绍 LCD 的操作原理:
① 驱动程序设置好 LCD 控制器
根据 LCD 的参数设置 LCD 控制器的时序、信号极性;
根据 LCD 分辨率、BPP 分配 Framebuffer。
② APP 使用 ioctl 获得 LCD 分辨率、BPP
③ APP 通过 mmap 映射 Framebuffer,在 Framebuffer 中写入数据
在这里插入图片描述

2、 ioctl 函数

头文件:

#include <sys/ioctl.h>

函数原型:

int ioctl(int fd, unsigned long request, ...);

函数说明:
① fd 表示文件描述符;
② request 表示与驱动程序交互的命令,用不同的命令控制驱动程序输出我们需要的数据;
③ … 表示可变参数 arg,根据 request 命令,设备驱动程序返回输出的数据。
④ 返回值:打开成功返回文件描述符,失败将返回-1。
ioctl 的作用非常强大、灵活。不同的驱动程序内部会实现不同的 ioctl,APP 可以使用各种 ioctl 跟驱动程序交互:可以传数据给驱动程序,也可以从驱动程序中读出数据。

3、 mmap函数

头文件:

#include <sys/mman.h>

函数原型:

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

函数说明:
① addr 表示指定映射的內存起始地址,通常设为 NULL 表示让系统自动选定地址,并在成功映射后返回该地址;
② length 表示将文件中多大的内容映射内存中;
③ prot 表示映射区域的保护方式,可以为以下 4 种方式的组合
a. PROT_EXEC 映射区域可被执行
b. PROT_READ 映射区域可被读出
c. PROT_WRITE 映射区域可被写入
d. PROT_NONE 映射区域不能存取
④ Flags 表示影响映射区域的不同特性,常用的有以下两种
a. MAP_SHARED 表示对映射区域写入的数据会复制回文件内,原来的文件会改变
b. MAP_PRIVATE 表示对映射区域的操作会产生一个映射文件的复制,对此区域的任何修改不会写回原来的文件内容中。
⑤ 返回值:若成功映射,将返回指向映射的区域的指针,失败将返回-1。

4、Framebuffer 程序分析

1 首先,像打开文件一样打开设备节点:

fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
    
    
	printf("can't open /dev/fb0\n");
	return -1;
}

2 获取 LCD 可变参数

 static struct fb_var_screeninfo var; /* Current var */
……
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
    
    
	printf("can't get var\n");
	return -1;
}

3 映射 Framebuffer
要映射一块内存,
需要知道它的地址──这由驱动程序来设置,
需要知道它的大小──这由应用程序决定。
代码如下:

line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
//fb_base 为映射内存的首地址;screen_size为映射内存大小;fd_fb 为设备描述符
fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fb_base == (unsigned char *)-1)
{
    
    
	printf("can't mmap\n");
	return -1;
}

screen_size 是整个 Framebuffer 的字节大小;PROT_READ | PROT_WRITE 表示该区域可读、可写;MAP_SHARED 表示该区域是共享的,APP 写入数据时,会直达驱动程序,这个参数的更深刻理解可以参考后面驱动基础中讲到的 mmap 知识。

*UNICODE编码实现

1、UTF-8

使用 UTF8 编码时,即使 TXT 文件中丢失了某些数据,也只会影响到当前字符的显示,后面的字符不受影响。

对于非 ASCII 字符,使用变长的编码:每一个字节的高位都自带长度信息。请看下图:
在这里插入图片描述
上图中,
0xe4 的二进制是“11100100”,高位有 3 个 1,表示从当前字节起有 3 字节参与表示 UNICODE;
0xb8 的二进制是“10111000”,高位有 1 个 1,表示从当前字节起有 1 字节参与表示 UNICODE; 0xad 的二进制是“10101101”,高位有 1 个 1,表示从当前字节起有 1 字节参与表示 UNICODE;

五、input_event

1程序运行的一些基础知识

  1. 编译程序时去哪找头文件?
    系统目录:就是交叉编译工具链里的某个 include 目录;
    也可以自己指定:编译时用 “ -I dir ”选项指定。
  2. 链接时去哪找库文件?
    系统目录:就是交叉编译工具链里的某个 lib 目录; 也可以自己指定:链接时用 “ -L dir ”选项指定。
  3. 运行时去哪找库文件?
    系统目录:就是板子上的/lib、/usr/lib 目录; 也可以自己指定:运行程序用环境变量 LD_LIBRARY_PATH 指定。
  4. 运行时不需要头文件,所以头文件不用放到板子上

2 输入系统框架

在这里插入图片描述
假设用户程序直接访问/dev/input/event0设备节点,或者使用tslib访问设备节点,数据的流程如下:
① APP 发起读操作,若无数据则休眠;
② 用户操作设备,硬件上产生中断;
③ 输入系统驱动层对应的驱动程序处理中断:
读取到数据,转换为标准的输入事件,向核心层汇报。
所谓输入事件就是一个“struct input_event”结构体。
④ 核心层可以决定把输入事件转发给上面哪个 handler 来处理:
从handler的名字来看,它就是用来处输入操作的。有多种handler,比如:evdev_handler、kbd_handler、joydev_handler 等等。
最常用的是 evdev_handler:它只是把 input_event 结构体保存在内核 buffer 等,APP 来读取时就原原本本地返回。它支持多个 APP 同时访问输入设备,每个 APP 都可以获得同一份输入事件。
当 APP 正在等待数据时,evdev_handler 会把它唤醒,这样 APP 就可以返回数据。
⑤ APP 对输入事件的处理:
APP 获得数据的方法有 2 种:直接访问设备节点(比如/dev/input/event0,1,2,…),或者通过 tslib、 libinput 这类库来间接访问设备节点。这些库简化了对数据的处理。

3 输入事件 input_event

重要的是:type(哪类事件)、code(哪个事件)、value(事件值),细讲如下:
type:表示哪类事件
比如 EV_KEY 表示按键类、EV_REL 表示相对位移(比如鼠标),EV_ABS 表示绝对位置(比如触摸屏)。有这几类事件(参考 Linux 内核头文件):
code:表示该类事件下的哪一个事件
比如对于 EV_KEY(按键)类事件,它表示键盘。键盘上有很多按键,比如数字键 1、2、3,字母键 A、B、 C 里等。对于触摸屏,它提供的是绝对位置信息,有 X 方向、Y 方向,还有压力值。
value:表示事件值
对于按键,它的 value 可以是 0(表示按键被按下)、1(表示按键被松开)、2(表示长按);
对于触摸屏,它的 value 就是坐标值、压力值。
同步事件
APP 读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报 X、Y 位置信息,也可能会上报压力值。
APP 怎么知道它已经读到了完整的数据?
驱动程序上报完一系列的数据后,会上报一个“同步事件”,表示数据上报完毕。APP 读到“同步事件”时,就知道已经读完了当前的数据。
同步事件也是一个 input_event 结构体,它的 type、code、value 三项都是 0。

六、APP 访问硬件的 4 种方式

1 查询方式

APP 调用 open 函数时,传入“O_NONBLOCK”表示“非阻塞”。
APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read 函数会返回数据,否则也会立刻返回错误。

2 休眠-唤醒方式

APP 调用 open 函数时,不要传入“O_NONBLOCK”。
APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read 函数会返回数据;否则 APP就会在内核态休眠,当有数据时驱动程序会把 APP 唤醒,read 函数恢复执行并返回数据给 APP。

3 POLL/SELECT 方式

POLL 机制、SELECT 机制是完全一样的,只是 APP 接口函数不一样。
简单地说,它们就是“定个闹钟”:在调用 poll、select 函数时可以传入“超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到“超时时间”结束时返回错误。
用法如下。
APP 先调用 open 函数时。
APP 不是直接调用 read 函数,而是先调用 poll 或 select 函数,这 2 个函数中可以传入“超时时间”。它们的作用是:如果驱动程序中有数据,则立刻返回;否则就休眠。在休眠期间,如果有人操作了硬件,驱动程序获得数据后就会把 APP 唤醒,导致 poll 或 select 立刻返回;如果在“超时时间”内无人操作硬件,则时间到后 poll 或 select 函数也会返回。APP 可以根据函数的返回值判断返回原因:有数据?无数据超时返回?
APP 根据 poll 或 select 的返回值判断有数据之后,就调用 read 函数读取数据时,这时就会立刻获得数据。poll/select 函数可以监测多个文件,可以监测多种事件:
在调用 poll 函数时,要指明:
① 你要监测哪一个文件:哪一个 fd
② 你想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT
最后,在 poll 函数返回时,要判断状态。

4 异步通知方式

应用程序要做的事情有这几件:
① 编写信号处理函数:

static void sig_func(int sig)
{
    
    
	int val;
	read(fd, &val, 4);
	printf("get button : 0x%x\n", val);
}

② 注册信号处理函数:

signal(SIGIO, sig_func);

③ 打开驱动:

fd = open(argv[1], O_RDWR);

④ 把进程 ID 告诉驱动:

fcntl(fd, F_SETOWN, getpid());

⑤ 使能驱动的 FASYNC 功能:

flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);

第五篇 Linux驱动开发基础知识


一、基础工作

1、驱动程序依赖于 Linux 内核,你为开发板 A 开发驱动,那就先在 Ubuntu 中得到、配置、编译开发板 A所使用的 Linux 内核。

硬件部分:
① 开发板接线:串口线、电源线、网线
② 开发板烧写系统
软件部分:
① 下载 Linux 内核,Windows 和 Ubuntu 下各放一份
② Windows 下:使用 Source Insight 创建内核源码的工程,这是用来浏览内核、编辑驱动
③ Ubuntu 下:安装工具链,配置编译 Linux 内核

2、不同芯片使用的内核版本

rk3399 linux 4.4.154
rk3288 linux 4.4.154
imx6ul linux 4.9.88
am3358 linux 4.9.168

二、hello驱动

1结构体file和file_operations

APP 使用open函数打开文件时,open函数会返回一个整数,这个整数被称为文件句柄
对于 APP 的每一个文件句柄,在内核里面都有一个“struct file”与之对应。

int open(const char *pathname, int flags, mode_t mode);

在这里插入图片描述
我们使用 open 打开文件时,传入的 flags、mode 等参数会被记录在内核中对应的 struct
file 结构体里(f_flags、f_mode);
去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的 f_pos 成员里。
在这里插入图片描述
结构体 struct file_operations 的定义如下:当APP执行open/read/write等操作时,linux根据设备号在chrdevs[]中找到f_op中的对应函数。
在这里插入图片描述

2 怎么编写驱动程序********

① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核:register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

3 写驱动程序

参考 driver/char 中的程序,包含头文件,写框架,传输数据:
A. 驱动中实现 open, read, write, release,APP 调用这些函数时,都打印内核信息
B. APP 调用 write 函数时,传入的数据保存在驱动中
C. APP 调用 read 函数时,把驱动中保存的数据返回给 APP

入口函数 static int __init hello_init(void)
主要工作就是向内核注册一个 file_operations 结构体:hello_drv,这就是字符设备驱动程序的核心。

file_operations 结构体static struct file_operations hello_drv = { ...};
里面提供了 open/read/write/release 成员,应用程序调用 open/read/write/close 时就会导致这些成员函数被调用。

file_operations 结构体 hello_drv 中的成员函数都比较简单,大多数只是打印而已。要注意的是,驱动程序应用程序之间传递数据要使用 copy_from_user/copy_to_user 函数。

总结:
编写read/write/open/close函数—>添加到file_operations hello_drv 结构体—>入口函数注册f_op结构体—>出口函数和其他函数;

4 驱动编写思想

1、面向对象:各种驱动,都是实现file_operations;
2、上下分层:上层实现与硬件无关的操作,比如注册字符设备驱动;下层实现与硬件相关的操作;
3、左右分离:下层再实现左右分离,层层调用,根据实际资源情况选择不同的函数。

三、QEMU虚拟开发板使用教程

本节内容如果资源过时了,最新资源见网页
http://wiki.100ask.org/Qemu

1、准备工作

1、电脑下载VMwareubuntu镜像(链接见首节)
在这里插入图片描述
在这里插入图片描述

2、电脑安装VMware后,使用VMware workstation 15 Player打开下载好的ubuntu18镜像,可省去安装步骤;并设置虚拟机网络为NAT模式;

3、安装KVM加速qemu运行

$ sudo apt-get update
$ sudo apt-get install qemu qemu-kvm libvirt-bin bridge-utils virt-manager

4、在视频中演示的方法可能是直接下载qemu镜像压缩包,但是由于GIT仓库改版,只能使用GIT clone方式下载:
请在ubuntu终端下执行如下命令,直接在线克隆整个仓库

book@100ask:~$  git  clone  https://e.coding.net/weidongshan/ubuntu-18.04_imx6ul_qemu_system.git

在这里插入图片描述

2、运行

1、首次运行QEMU,需要安装SDL环境(无法运行时,指令前面加sudo)

$ ./install_sdl.sh

如果出现报错 Package xxxx is not installed.,可使用apt命令修复:

$ sudo apt --fix-broken install

2、运行带 GUI 的 imx6ul 模拟器
① 模拟百问网 imx6ull-qemu 开发板

$ ./qemu-imx6ull-gui.sh // 启动后,登录名是 root,无需密码

② 模拟野火 imx6ull-pro 开发板

$ ./qemu-imx6ull-gui.sh fire // 启动后,登录名是 root,无需密码

③ 模拟正点原子 imx6ull-alpha 开发板

$ ./qemu-imx6ull-gui.sh atk // 启动后,登录名是 root,无需密码

③ 运行不带 GUI 的 imx6ull 模拟器

$ ./qemu-imx6ull-nogui.sh // 启动后,登录名是 root,无需密码

3、操作LCD
/root 目录下有自带例程,执行:

[root@qemu_imx6ul:~]# fb-test
或
[root@qemu_imx6ul:~]# myfb-test /dev/fb0

4、操作 LED —— /root下
我们模拟的 IMX6ULL 板子,它的 Linux 系统中已经带有 LED 驱动和测试命令,可以执行以下命令测试:

[root@qemu_imx6ul:~]# cd led_driver_qemu/
[root@qemu_imx6ul:~/led_driver_qemu]# insmod 100ask_led.ko 
[root@qemu_imx6ul:~/led_driver_qemu]# ./ledtest /dev/100ask_led0 off
[root@qemu_imx6ul:~/led_driver_qemu]# ./ledtest /dev/100ask_led0 on

5、使用按键来控制 LED —— /root下
首先在“QEMU 设备管理器”中打开按键的界面,然后执行以下命令测试:

[root@qemu_imx6ul:~]# cd led_driver_qemu/
[root@qemu_imx6ul:~/led_driver_qemu]# insmod 100ask_led.ko 
[root@qemu_imx6ul:~/led_driver_qemu]# cd ../button_driver_qemu/
[root@qemu_imx6ul:~/button_driver_qemu]# insmod button_drv.ko 
[root@qemu_imx6ul:~/button_driver_qemu]# insmod board_100ask_qemu_imx6ull.ko
[root@qemu_imx6ul:~/button_driver_qemu]# ./button_led_test

6、读写 I2C EEPROM AT24C02 —— /root下
首先在“QEMU 设备管理器”中打开 at24c02 的界面,然后执行以下命令测试:

// 0x50 是 AT24C02 的 I2C 设备地址
[root@qemu_imx6ul:~]# i2c_usr_test /dev/i2c-0 0x50 r 0 // 读地址 0 data: , 0, 0x00 
[root@qemu_imx6ul:~]# i2c_usr_test /dev/i2c-0 0x50 w 1 0x58 // 写地址 1,写入 0x58

3、下载内核源码和压缩 ——100ask_imx6ull-qemu 目录

考虑到代码仓库过多,特使用 repo 工具管理代码。
先用 git clone 下载 repo 工具,再用 repo 工具下载源码:

最新版见网页:http://wiki.100ask.org/Qemu#.E8.8E.B7.E5.8F.96.E6.BA.90.E7.A0.81

book\@100ask:\~\$ git clone https://e.coding.net/codebug8/repo.git
book\@100ask:\~\$ mkdir -p 100ask_imx6ull-qemu && cd 100ask_imx6ull-qemu
book\@100ask:\~/100ask_imx6ull-qemu\$ ../repo/repo init -u https://e.coding.net/weidongshan/manifests.git -b linux-sdk -m  imx6ull/100ask-imx6ull_qemu_release_v1.0.xml --no-repo-verify
book\@100ask:\~/100ask_imx6ull-qemu\$ ../repo/repo sync -j4

如果一切正常,你在/home/book 目录下创建了一个 100ask_imx6ull-qemu 目录,里有如下内容:
在这里插入图片描述

Ubuntu下压缩命令为(最好是下载之后马上压缩,不要编译内核后再压缩,否则文件太大了):

sudo tar cjf Linux-4.9.88.tar.bz2 linux-4.9.88

可将压缩后的linux内核文件利用filezila传到windows,解压后供学习阅读。


4、ubuntu设置交叉编译工具链

如需永久修改,请修改用户配置文件。在 Ubuntu 系统下,修改如下:

book@100ask:~$ vi ~/.bashrc

在行尾添加:
注意PATH要为实际位置:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-qemu/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin

注意:设置完环境变量后需要执行以下命令或重启UBUNTU才能使环境变量生效:

source ~/.bashrc

5、编译内核及设备树(编译失败,有知道注意事项的请告知)

前面我们下载了源码,设置好工具链后,即可编译:

book@100ask:~/100ask_imx6ull-qemu$ cd linux-4.9.88
book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make mrproper
book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make 100ask_imx6ull_qemu_defconfig
book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make zImage -j2 //编译 zImage 内核镜像,其中 N 参数可以根据 CPU 个数,来加速编译系统。
book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make dtbs //编译设备树文件

编译成功后,可以得到如下文件:

arch/arm/boot/zImage // 内核
arch/arm/boot/dts/100ask_imx6ull_qemu.dtb // 设备树

6、替换开发板的内核和设备树

用上面 2 个文件去替换 QEMU中的 zImage 和 100ask_imx6ull_qemu.dtb

7、修改文件系统

进入 QEMU目录 后,有一个 imx6ull-system-image 目录,里面有名为 rootfs.img 的文件,它就是根文件系统
你可以在 Ubuntu 下直接修改 rootfs.img,不过要先挂载,执行以下命令:

sudo mount -o loop rootfs.img /mnt

你就可以在/mnt 目录下操作其中的文件了,也可以把 Ubuntu 中的文件复制进去。
注意:修改完毕后,要执行以下命令:

sudo umount /mnt

8、QEMU模拟器使用NFS ******

如果你的 Ubuntu 未安装 NFS 服务,执行以下命令:

sudo apt-get install nfs-kernel-server

然后,修改/etc/exports,添加类似以下的内容,
下面的例子里允许开发板通过 NFS访问 Ubuntu 的/home/book/nfs_rootfs目录:

sudo gedit /etc/exports


/home/book/nfs_rootfs *(rw,nohide,insecure,no_subtree_check,async,no_root_squash)

最后,重启 NFS 服务,在 Ubuntu 上执行以下命令:

sudo /etc/init.d/nfs-kernel-server restart

可以在 Ubuntu 上通过 NFS 挂载自己,验证一下 NFS 可用:

sudo mount -t nfs -o nolock,vers=3 127.0.0.1:/home/book /mnt
ls /mnt

9、挂载NFS

注意:qemu模拟器是ping不通ubuntu或者PC的!!!!!!!但可以nfs挂载!!!!!!

QEMU可以改自己IP:

[root@qemu_imx6ul:~]# ifconfig eth0 10.0.2.15

对于QEMU来说,它分配给ubuntu的IP为10.0.2.2,所以挂载这个IP,
挂载ubuntu的nfs_rootfs目录到QEMU虚拟的/mnt目录:

[root@qemu_imx6ul:~]# mount -t nfs -o nolock,vers=3 10.0.2.2:/home/book/nfs_rootfs /mnt

如果一切正常,在开发板上就可以通过/mnt 目录访问 Ubuntu 的/home/book/nfs_rootfs目录了。

猜你喜欢

转载自blog.csdn.net/LIU944602965/article/details/115134094