内容来自韦东山老师《嵌入式Linux应用开发完全手册》
欢迎关注我的微信公众号【万能的小江江】
git clone https://e.coding.net/weidongshan/01_all_series_quickstart.git #先下载本次全部代码和资料
1.HelloWorld背后没那么简单
1.1 交叉编译hello.c
源码路径:01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\01_hello
hello.c
源码
#include <stdio.h>
/* 执行命令: ./hello weidongshan
* argc = 2
* argv[0] = ./hello
* argv[1] = weidongshan
*/
int main(int argc, char **argv)
{
if (argc >= 2)
printf("Hello, %s!\n", argv[1]);
else
printf("Hello, world!\n");
return 0;
}
在Ubuntu中编译
$ gcc -o hello hello.c #hello是生成的可执行文件目标名,hello.c是源文件名
$ ./hello #执行可执行文件hello
Hello,world!
$./hello InImpasse
Hello,InImpasse!
Ubuntu中生成的可执行文件hello可以在Ubuntu中运行(假设Ubuntu是x86版本的),不能在ARM中运行,因为x86的gcc是给pc机编译的
使用交叉编译工具链,给ARM板编译hello程序
arm-buildroot-linux-gnueabihf-gcc -o hello hello.c
1.2 几个问题
头文件有什么用?
声明declare
头文件在哪里?
默认路径
进入交叉编译器目录,执行find -name "stdio.h"
,可以看到它位于一个include
目录下的根目录
编译器中的include目录
指定路径
#include "目录/xxx.h" //头文件中这样定义
编译时用
-I
选项指定
printf函数在哪里?
库
默认路径
编译器中的lib
目录
找默认路径
进入交叉编译器目录,执行find -name lib
,可以得到xxxx/lib、xxxx/usr/lib
,一般来说这两个目录就是要找的路径(如果很多lib,有很多so文件的一般就是要找的路径)
指定路径
编译时用-L
选项指定目录
编译时用-l
选项指定库
别的C文件
c文件有什么用?
定义、实现define
2.GCC编译器的使用
pc上常见的编译工具链为gcc、ld、objcpy、objdump等,这些编译工具链编译出来的程序能在x86平台运行
如果要编译在ARM平台运行的程序,必须使用交叉编译工具链xxx-gcc、xxx-ld等(不同版本的编译器前缀都不一样,比如arm-linux-gcc)
GCC的编译过程(精简)
C/C++文件要经过预处理preprocessing
、编译compilation
、汇编assembly
和链接linking
4步才能变成可执行文件
预处理
文件格式:.c/.cpp
xxx-gcc -E -o hello.i hello.c
编译
文件格式:.i
xxx-gcc -S -o hello.s hello.i
汇编(as命令)
文件格式:.s
xxx-gcc -c -o hello.o hello.s
链接(collect2命令)
文件格式:.o
xxx-gcc -o hello hello.o
常用编程选项
-E
:预处理,开发过程中想要快速确定某个宏可以用-E
-dM
-c
:把预处理、编译、都做了,但是不链接
-o
:指定输出文件
-I
:指定头文件目录
-L
:指定链接时库文件目录
-I
:指定连接哪个库的文件
怎么编译多个文件
1.同时编译、链接:
gcc -o test main.c sub.c
2.分开编译、统一链接:
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
gcc -c -o sub.o sub.c
gcc -shared -o libsub.so sub.o sub2.0 sub3.o #可以使用多个.o生成动态库
gcc -o test main.o -lsub -L #libsub.so所在目录
运行:
先把libusb.so放到PC或板子上的/lib目录,然后就可以运行test程序
如果不想把libsub.so放到/lib,也可以放在某个目录,比如/a,然后执行下面的指令:
export LD_LIBRARY_PATH = $LD_LIBRARY_PATH:/a ./test
制作、使用静态库
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
ar crs libsub.a sub.o sub2.o sub3.o #可以使用多个.o生成动态库
gcc -o test main.o libsub.a #如果.a不在当前目录喜爱,需要指定它的绝对或相对路径
运行:
不需要把静态库libsub.a放到板子上
注:执行arm-linux-gnueabihf-gcc -c -o sub.o sub.c
交叉编译需要在最后加上-fPIC
参数
很有用的选项
gcc -E main.c #查看预处理的结果,比如头文件是哪个
gcc -E -dM main.c > 1.txt #把所有的宏都展开,存在1.txt里
gcc -Wp,-MD,abc,dep -c -o main.o main.c #生成依赖文件abc.dep,后面Makefile会用到
GCC编译过程(详细)
C/C++文件要经过预处理preprocessing
、编译compilation
、汇编assembly
和链接linking
4步才能变成可执行文件
日常一般用编译
来统称这4个步骤,不同的交叉编译器工具链前缀可能不同,比如arm-linux-gcc
预处理
C/C++文件中,#
开头的命令被称为预处理命令,比如命令#include
、宏定义命令#define
、条件编译命令#if
、#ifdef
等
预处理就是把这些包含include
的文件插入原文件
、把宏定义展开
、再根据条件编译命令
选择要使用的代码,最后把这些东西输出到.i
文件中等待进一步处理
编译
把C/C++代码(比如.i
文件)翻译成汇编代码
,所用到的工具是ccl
(x86和ARM都有自己独特的ccl命令)
汇编
把汇编代码翻译成符合一定格式的机器代码,Linux系统上一般表现为ELF目标文件
(OBJ文件
),用到的工具是as
。(x86和ARM也都有自己独特的as命令,比如arm-linux-as
)
链接
把前面生成的OBJ文件
和系统库OBJ文件
、库文件
链接起来,最终生成可以在特定平台运行的可执行文件,用到的工具是ld
或collect2
例子
编译程序的时候加上-v选项可以看到上面这几个步骤,例如:
gcc -0 hello hello.c -v
会有很多输出结果,主要输出如下:
ccl hello.c -o /tmp/cctETob7.s #编译命令,预处理hello.c生成的输出文件转化为汇编代码
as -o /tmp/ccvv2KbL.o /tmp/cctETob7.s #汇编命令,把汇编代码转化为符合一定格式的机器码
collect2 -o hello crt1.0 crti.o crtbegin.o /tmp/ccvv2KbL.o cretend.o crtn.o #链接生成可执行文件
预处理和编译被放在了ccl中进行,可以拆分为以下两步:
cpp -o hello.i hello.c #预处理生成包含宏定义,条件编译指令等的.i文件
ccl hello.i -o /tmp/cctETob7.s #编译.i文件生成.s汇编文件
一些常用选项
其实不用单独执行cpp、ccl、colloect2
等命令,直接执行gcc指定不同参数就可以了
GCC总体选项(Overall Option)
(这部分需要加深理解!!!)
-c
预处理、编译、汇编源文件,但是不做链接(生成.o文件,不生成可执行文件),编译器根据源文件生成OBJ文件
-S
编译后即停止,不进行汇编(把c加各种定义命令转成汇编语言,不转机器码)
对于每个输入的非汇编语言文件,输出结果是汇编语言文件
-E
预处理后即停止,不进行编译(就直加上各种定义命令。不转汇编语言)
-o file
指定输出文件为file,这个选项不限制是在预处理、编译、汇编还是链接,都可以使用
-v
显示制作GCC工具自身时的配置命令;同时显示编译器驱动程序、预处理器、编译器的版本号
例子
//File: main.c
#include <stdio.h>
#include "sub.h"
int main(int argc, char *argv[])
{
int i;
printf("Main fun!\n");
sub_fun();
return 0;
}
//File: sub.h
void sub_fun(void);
//File: sub.c
void sub_fun(void)
{
printf("Sub fun!\n");
}
用gcc编译、链接
编译
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
gcc -o test main.o sub.o
链接
./test
#Main fun!
#Sub fun!
预处理+编译
gcc -S -o main.s main.c
查找某个宏定义
gcc -E main.c
什么是ld
arm-linux-ld
是一个链接程序工具,其作用主要是将汇编过的多个二进制文件进行链接,成为一个可执行的二进制文件,这个命令的选项有好多,具体用到的时候大家可以使用–help 选项来查看具体的选项用法
警告选项(Warning Option)
-Wall
这个选项基本打开了所以需要注意的警告信息,如没有指定类型的声明、在声明之前就使用的函数、局部变量声明之后就没有再使用等等
上面的main.c
文件中,第6行的变量i没有被使用,但是使用gcc -c -o main.o main.c
进行编译时并没有出现提示
可以加上-Wall
选项,例子如下:
gcc -Wall -c main.c
警告信息如下:
main.c: In function 'main':
main.c: 6: warning: unused varianle 'i'
这种警告没有坏的影响,但是有些警告需要特别关注,比如类型匹配的警告等
调试选项(Debugging Option)
-g
以操作系统本地格式(stabs,coff,xcoff,dwarf
)产生调试信息,GDB能够使用这些调试信息。在大多数使用stabs
格式的系统上,-g选项加入只有GDB才使用的额外调试信息。可以使用下面的选项来生成额外的信息:”-gstabs+”, “-gstabs”, “-gxcoff+”,”-gxcoff”,”-gdwarf+或”gdwarf”,具体用法需要参考GCC手册(没有很看懂…)
优化选项(Optimization Option)
具体细节等到用到了再加以补充(P302)
-0或-01
-02
-03
-00
不优化
链接器选项(Linker Option)
object-file-name
-llibrary
-nostrartfiles
-nostdlib
-static
-shared
-Xlinker option
-Wl,option
-u symbol
目录选项(Directory Option)
-Idir
-I-
-Ldir
-Bprefix
ld/objdump/objcopy选项
在开发APP时,一般不需要直接调用这3个命令;在开发裸机、bootloader或者调试APP的时候会涉及,到时候再加以展开
3.Makefile的使用
在了Linux中使用make
命令来编译程序,尤其是编译大程序;make
命令所指向的动作依赖于Makefile
文件,最简单的Makefile文件如下:
hello: hello.c
gcc -o hello hello.c #此行必须用Tab键缩进,不能以空格键缩进,下同
clean:
rm -f hello
把该Makefile文件放入01_hello
目录下,之后直接执行make
命令即可编译程序,执行make clean
即可清除编译结果
make命令会根据文件更新的时间戳来决定哪些文件需要重新编译,可以避免编译已经编译过的、没有变化的程序,以提高编译效率
需要更加完整了解Makefile的规则,可以参考《GUN Make使用手册》或者其他资料
配套视频内容大纲
Makefile规则与示例
为什么需要Makefile
可以更高效地编译程序
Makefile其实挺简单的
一个简单的Makefile文件包含一系列的“规则”,样式如下:
目标(target)...:依赖(prerequiries)... <tab>命令(command)
如果“依赖文件”比“目标文件”更新,就可以执行“命令“以重新生成”目标文件“
命令执行的2个条件:1.依赖文件比目标文件新;2.目标文件尚未生成
介绍Makefile的2个函数
$(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
$(wildcard pattern)
pattern
所列出的文件是否存在,把存在的文件都列出来举例:
src_files := $(wildcard *.c) #最终src_files中列出了当前目录下所有的.c文件
一步步完善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,效率高,精炼,支持检测头文件(but需要手工添加头文件规则)
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 := $(fboreach f, $(objs), .$(f).d) dep_files := $(wildcard $(dep_files)) #把依赖文件包含进来 ifneq ($(dep_files),) include $(dep_files) endif %.o : %.c gcc -Wp,-MD,.[email protected] -c -o $@ $< clean: rm *.o test -f distclean: rm $(dep_files) *.o test -f
通用Makefile的使用
功能如下:
- 支持多个目录、多层目录、多个文件
- 支持给所有的文件设置编译选项
- 支持给某个目录设置编译选项
- 支持给某个文件单独设置编译选项
- 简单好用
下载链接:
通用Makefile的解析
零星知识点:
make
命令的使用 执行
make
命令时,它会去当前目录下查找名为Makefile
的文件,并根据它的值去执行操作,生成第一个目标 我们可以用
-f
选项指定文件,不再使用名为Makefile
的文件,如:make -f Makefile.build
我们也可以使用
-C
选项指定目录,切换到其他目录去make -C a/ -f Makefile.build
也可以指定目标,不再默认生成第一个目标
make -C a/ -f Makefile.build other_target
即时变量、延时变量
变量的定义语法形式如下:
A = xxx //延时变量 B ?= xxx //延时变量,只有第一次定义时赋值才成功;如果之前定义过,则此赋值无效 C := xxx //立即变量 D += yyy //如果D在前面是延时变量,那么它现在还是延时变量;如果D在前面是立即变量,那么现在它还是立即变量(感觉绕起来好好笑哈哈哈)
在GNU make中对变量的赋值有两种方式:延时变量、立即变量
由上可知,变量A是延时变量,它的值在使用时才展开、才确定,比如:
A = $@ test: @echo $A
上述Makefile中,变量A的值在执行时才确定,等于
test
,是延时变量 如果使用
A := $@
这是立即变量,这时¥@
为空,所以A的值就是空变量的导出(export)
在编译程序时,我们不断地使用
make -C dir
切换到其他目录,执行其他目录里的Makefile
,如果想让某个变量的值在所有的目录中都可见,可以把它export
出来 比如
CC [ $(CROSS_COMPILE)gcc]
,这个CC变量表示编译器,在整个过程中都是一样的。定义它之后,要使用export CC
把它导出来Makefile中可以使用的shell命令
比如:
TOPDIR := $(shell pwd)
这是一个立即变量,TOPDIR等于shell命令pwd结果
在Makefile中怎么放置第1个目标
执行
make
命令时如果不指定目标,那么它默认是去生成第1个目标
所以
第1个目标
的位置
很重要。有时候不太方便把第1个目标
完整地放在文件前面,可以在文件的前面直接放置目标,在后面再完善它的依赖和命令 比如:
First_target: //这句话放在前面 ... //其他代码,比如include其他文件得到后面的xxx变量 First_target : $(xxx) $(xxx) //在文件的后面再来完善 command
假想目标:
我们的Makefile中有这样的目标:
clean: rm -f $(shell find -name "*.o") rm -f $(TARGET)
如果当前目录下恰好有名为
clean
的文件,那么执行make clean
时它就不会执行那些删除命令 如果我们需要把
clean
这个目标设置为假想目标,这样可以确保执行make clean
时删除的命令肯定能够得到执行 使用下面的语句把clean设置为假想目标:
.PHONY : clean
常用的函数
$(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
$(wildcard pattern)
pattern
所列出的文件是否存在,把存在的文件都列出来src_files := $(wildcard *.c) #最终src_files中列出了当前目录下所有的.c文件
$(filter pattern…,text)
把
text
中符合pattern
格式的内容,filter
(过滤)出来、留下来obj-y := a.o b.o c/ d/ DIR := $(filter %/, $(obj-y)) //结果为:c/ d/
$(filter-out pattern…,text)
把
text
中符合pattern
格式的内容,filter-out
(过滤)出来、扔掉obj-y := a.0 b.o c/ d/ DIR := $(filter-out %/, $(obj-y)) //结果为:a.o b.o
$(patsubst pattern,replacement,text)
寻找
text
中符合格式pattern
的字,用replacement
替换它们。pattern
和replacement
中可以使用通配符subdir-y := c/ d/ subdir-y := $(patsubst %/, %, $(subdir-y)) //结果为:c d
通用Makefile的设计思想
在Makefile文件中确定要编译的文件、目录,比如:
obj-y += main.o obj-y += a/
Makefile
文件总是被Makefile.build
包含的在
Makefile.build
中设置编译规则,有3条编译规则怎么编译子目录?进入子目录编译:
$(subdir-y): make -C $@ -f $(TOPDIR)/Makefile.build
怎么编译当前目录中的文件?
%.o : %.c $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
当前目录下的
.o
和子目录下的built-in.o
要打包起来:built-in.o : $(cur_objs) $(subdir_objs) $(LD) -r -o $@ $^
顶层Makefile中把顶层目录的
built-in.o
链接成APP:$(TARGET) : built-in.o $(CC) $(LDFLAGS) -o $(TARGET) built-in.o
情景演绎
#顶层Makefile obj-y += mian.o obj-y += sub.o obj-y += a/ all: start_recursive build$(TARGET) $echo $(TARGET) has been built! #执行make,导致顶层Makefile的第1个目标“all”被处理 #"start_recursive_build"使用Makefile.build处理顶层目录 start_recursive_build: make -C ./ -f $(TOPDIR)/Makefile.build $(TARGET) : built-in.o $(CC) $(LDFLAGS) -o $(TARGET) built-in.o
#Makefile.build obj-y := subdir-y := EXTRA_CFLAGS := #上面3个,变量清零 包含Makefile,里面有obj-y,确定文件目录 include Makefile PHONY += $(subdir-y) __build : $(subdir-y) built-in.o # $(subdir-y)先处理子目录"a/",假设已成功得到"a/built-in.o" #怎么处理子目录"a/"? 跟处理顶层目录一样 make -c a/ -f Makeile.build (Makefile.build包含"a/ Makefile" 里面有"obj-y += sub2.o和obj-y += sub3.o"没有目录,这些.o被链接为"a/built-in.o") $(subdir-y): make -C $@ -f $(TOPDIR)/Makefile.build #编译main.o,sub.o 把main.o,sub.o,a/built-in.o一起链接得到顶层目录的built-in.o built-in.o : $(cur_objs) $(subdir_objs) $(LD) -r -o $@ $^
Makefile规则
Makefile文件里的赋值方法
Makefile常用函数
后续再进行补充
文件IO
第一本更适合初学者,第二本可以当作字典来翻
在Linux系统中,一切都是文件
:普通文件、驱动程序、网络通信等…所有的操作,都是通过文件IO
来操作的
文件从哪里来?
磁盘、Flash、SD卡、U盘
真是的文件,以某种格式(FAT32,exFAT等)保存在某个存储设备上(/dev/xxx),要先mount
Linux内核提供的虚拟文件系统(也要先mount)
特殊文件:
/dev/xxx
,设备节点(手持设备、块设备)、FIFD、Socket
怎么访问文件?
通用的IO模型:open/read/write/lseek/close
copy.c
源码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
/*
* ./copy 1.txt 2.txt
* argc = 3
* argv[0] = "./copy"
* argv[1] = "1.txt"
* argv[2] = "2.txt"
*/
int main(int argc, char **argv)
{
int fd_old, fd_new;
char buf[1024];
int len;
/* 1. 判断参数 */
if (argc != 3)
{
printf("Usage: %s <old-file> <new-file>\n", argv[0]);
return -1;
}
/* 2. 打开老文件 */
fd_old = open(argv[1], O_RDONLY);
if (fd_old == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
/* 3. 创建新文件 */
fd_new = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fd_new == -1)
{
printf("can not creat file %s\n", argv[2]);
return -1;
}
/* 4. 循环: 读老文件-写新文件 */
while ((len = read(fd_old, buf, 1024)) > 0)
{
if (write(fd_new, buf, len) != len)
{
printf("can not write %s\n", argv[2]);
return -1;
}
}
/* 5. 关闭文件 */
close(fd_old);
close(fd_new);
return 0;
}
这个源码可以在Ubuntu上测试,结果与ARM板上类似
执行下面的命令编译、运行:
gcc -o copy copy.c
./copy copy.c new.c
不是通用的函数:ioctl/mmap
在Linux中,我们可以把一个文件的所有内容映射到内存,然后直接读写内存就可以读写文件
copy_mmap.c
的源码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* ./copy 1.txt 2.txt
* argc = 3
* argv[0] = "./copy"
* argv[1] = "1.txt"
* argv[2] = "2.txt"
*/
int main(int argc, char **argv)
{
int fd_old, fd_new;
struct stat stat;
char *buf;
/* 1. 判断参数 */
if (argc != 3)
{
printf("Usage: %s <old-file> <new-file>\n", argv[0]);
return -1;
}
/* 2. 打开老文件 */
fd_old = open(argv[1], O_RDONLY);
if (fd_old == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
/* 3. 确定老文件的大小 */
if (fstat(fd_old, &stat) == -1)
{
printf("can not get stat of file %s\n", argv[1]);
return -1;
}
/* 4. 映射老文件 */
buf = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd_old, 0);
if (buf == MAP_FAILED)
{
printf("can not mmap file %s\n", argv[1]);
return -1;
}
/* 5. 创建新文件 */
fd_new = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fd_new == -1)
{
printf("can not creat file %s\n", argv[2]);
return -1;
}
/* 6. 写新文件 */
if (write(fd_new, buf, stat.st_size) != stat.st_size)
{
printf("can not write %s\n", argv[2]);
return -1;
}
/* 5. 关闭文件 */
close(fd_old);
close(fd_new);
return 0;
}
这个源码可以在Ubuntu上测试,结果与ARM板上类似
执行下面的命令编译、运行:
gcc -o copy_mmap copy_mmap.c
./copy_mmap copy_mmap.c new2.c
怎样知道函数的用法?
Linux下有3大帮助方法:help、man、info
想要查看某个命令的用法时,比如查看ls命令的用法,可以执行:
ls --help
help
只能用于查看某个命令的用法,而man手册
既可以查看命令的用法,也可以查看函数的详细介绍等,它有9大分类,如下:
Executable programs or shell commands
//命令System calls (functions provided by the kernel)
//系统调用,比如man 2 open
Library calls (functions within program libraries)
//函数库调用Special files (ususally found in /dev)
//特殊文件,比如man 4 tty
File formats and conventions eg /etc/passwd
//文件格式和约定,比如man 5 passwd
Games
//游戏Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
//杂项System administration commands (usually only for root)
//系统管理命令Kernel routines[Non standard]
//内核例程 想要查看
open函数
的用法时,可以执行man open
,发现这不是想要的内容时,可以再执行man 2 open
在
man命令
中可以及时按h
查看帮助信息了解快捷键。常用的快捷键是:f #往前翻一页 b #往后翻一页 /patten #往前搜 ?patten #往后搜
就内容来说,
info手册
会比man手册
编写得更全面一些,但man手册
使用起来更容易一些
info手册
相当于一本书的一章,里面有很多节;man手册
相当于一本书里面的一节 就个人而言,用
info命令
会比较少 可以直接执行
info
命令后,输入H
查看它的快捷键,在info手册
中,某一节被称为node
常见的快捷键如下:
Up #Move up one line Down #Move down one line PgUp #Scroll backward one screenful PgDn #Scroll forward one screenful Home #Go to the beginning of this node End #Go to the end of this node TAB #Skip to the next hypertext link RET #Follow the hypertext link under the cursor 1 #Go back to the last node seen in this window [ #Go to the previous node in the document ] #Go to the next node in the document p #Go to the previous node on this level n #Go to the next node on this level u #Go up one level t #Go to the top node of this document d #Go to the main 'directory' node
系统调用函数怎么进入内核?
内核的sys_open、sys_read会做什么?
Framebuffer应用编程
LCD操作原理
在Linux系统中通过Fremebuffer驱动程序来控制LCD。Frame是帧的意思,buffer是缓冲的意思,意味着Framebuffer就是一块内存,里面保存着一帧图像
Freambuffer中保存着一帧图像的每一个像素的颜色值,假设LCD的分辨率是1024x768
,每个像素的颜色为32位
,Framebuffer的大小为1024x768x32/8=3145728
字节
简单介绍LCD的操作原理
驱动程序设置好LCD控制器:
根据LCD参数设置LCD控制器的
时序、信号极性
根据
LCD分辨率
、BPP
分配FramebufferAPP使用
ioctl
获得LCD分辨率
、BPP
APP通过
mmap
映射Framebuffer,在Framebuffer中写入数据
假设需要设置LCD中左边(x,y)
处像素的颜色,要先找到这个像素对应的内存,再根据它的BPP值
设置颜色
假设fb_base
是APP执行mmap
后得到的Freamebuffer地址,如下图所示:
可以用下面的公式计算出(x,y)坐标处像素对于的Framebuffer地址:
(X,y)像素起始地址 = fb_base+(xres*bbp/8)*y + x*bpp/8
像素的颜色是用RGB三原色(红绿蓝)来表示的,在不同的BPP格式中,用不同的位来分别表示RGB
对于32BPP,一般只设置低24位表示颜色,高8位表示透明度,一般的LCD都不支持
对于24BPP,硬件上为了方便处理,在Framebuffer中也是用32位来表示,效果和32BPP一样
对于16位BPP,常用的是RGB565;很少的场合会用到RGB555,这可以通过ioctl
读取驱动程序中的RGB位偏移来确定使用哪一种格式
涉及的API函数
本程序的目的是:打开LCD设备节点,获取分辨率等参数,映射Framebuffer,最后实现描点函数
open函数
在Ubuntu中执行man 2 open
,可以看到open函数
的说明
头文件:
#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种:- O_RDWR 表示可读可写方式打开
- O_RDONLY 表示只读方式打开
- O_WRONLY 表示只写方式打开
- O_APPEND 表示如果这个文件中本身是有内容的,则新写入的内容会接续到原来的内容后面
- O_TRUNC 表示如果这个文件中本来是有内容的,则原来的内容会被丢弃,截断
- O_CREAT 表示当前打开文件不存在,我们创建它并打开它,通常与O_EXCL结合使用,当没有文件的时候创建文件,有这个文件的时候会报错提醒我们
- Mode表示创建文件的权限,只有在flags中使用了O_CREAT时才有效,否则忽略
- 返回值:打开成功返回文件描述符,失败将返回-1
ioctl函数
在Ubuntu中执行man ioctl
,可以看到ioctl函数
的说明
头文件:
#include <sys/ioctl.h>
函数原型:
int ioctl(int fd,unsigned long request,...);
函数说明:
fd表示文件描述符
request表示与驱动程序交互的命令,用不同的命令控制驱动程序输出我们需要的数据
…表示可变参数arg,根据request命令,设备驱动程序返回输出的数据
返回值:打开成功返回文件描述符,失败将返回-1
ioctl的作用非常强大、灵魂。不同的驱动程序内部会实现不同的ioctl,APP可以使用各种ioctl跟驱动程序交互:可以传数据给驱动程序,也可以从驱动程序中读出数据
mmap函数
在Ubuntu中执行man mmap
,可以看到mmap函数的说明
如果想要更深刻理解mmap内部的机制,可以看《嵌入式Linux驱动开发基础知识》中关于mmap的介绍。在APP开发中,只需要知道它的用法即可
头文件:
#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种方式的组合PROT_EXEC
映射区域可被执行PROT_READ
映射区域可被读出PROT_WRITE
映射区域可被写入PROT_NONE
映射区域不能被存取
Flags
表示影响映射区域的不同特性,常见的有以下两种MAP_SHARED
表示对映射区域写入的数据会复制回文件内,原来的文件会改变MAP_PRIVATE
表示对映射区域的操作会产生一个映射文件的复制,对此区域的任何修改都不会写回原来的文件内容中
- 返回值:若成功映射,将返回指向映射的区域的指针,失败将返回
-1
Freambuffer程序分析
源码目录:
打开设备
首先,打开设备节点
fd_fb = open("/dev/fb0",0_RDWR);
if (fd_fb < 0)
{
printf("can't open /dev/fb0\n");
return -1;
}
获取LCD参数
LCD驱动程序给APP提供2类参数:
可变的参数
fb_var_screeninfo
固定的参数
fb_fix_screeninfo
编写应用程序的时候主要关心可变参数,它的结构体定义如下(
#include <linux/fb.h>
):struct fb_var_screeninfo{ __u32 xres; //visible resolution __u32 yres; //分辨率 __u32 xres_virtual; //visible resolution __u32 yres_virtual; __u32 xoffset; //offset from virtual to visible __u32 yoffset; //resolution __u32 bits_per_pixel; //bpp,guess what __u32 grayscale; //0 = color,1 = grayscale //>1 = FOURCC struct fb_bitfield red; //bitfield in fb mem if true color struct fb_bitfield green; //else only length is significant struct fb_bitfield blue; //RGB分别用多少位来表示从哪一位开始 struct fb_bitfield transp; //transparency __u32 nonstd; //!=0 Non standard pixel format __u32 activate; //see FB_ACTIVATE __u32 height; // height of picture in mm __u32 width; //width of picture in mm __u32 accel_flags; //(OBSOLETE) see fb_info.flags //Timing:All values in pixclocks,except pixclock (of course) __u32 pixclock; //pixel clock in ps (pico seconds) __u32 left_margin; //time from sync to picture __u32 right_margin; //time from picture to sync __u32 upper_margin; //time from sync to picture __u32 lower_margin; __u32 hsync_len; //length of horizontal sync(synchronization同步) __u32 vsync_len; //length of vertical sync __u32 sync; //see FB_SYNC __u32 vmode; //see FB_VMODE_ __u32 rotate; //angle we rotate counter clockwise __u32 colorspace; //colorspace for FORCC-based modes __u32 reserved[4]; //Reserved for future compatibility }<<end fb_vat_screeninfo>>;
可以使用以下的代码获取
fb_var_screeninfo
static struct fb_var_screeninfo var; //Current var ...... if (ioctl(fd_fb,FBIOGET_VSCREENINFO,&var)) { printf("can't get var\n"); return -1; }
注:ioctl里面用的参数是FBIOGET_VSCREENINFO,它表示get var screen info,获得屏幕的可变信息;当然也可以使用FBIOPUT_VSCREENINFO来调整这些参数,但是比较少用到
对于固定的参数fb_fix_screeninfo,在应用编程中比较少用到。它的结构体定义如下:
struct fb_fix_screeninfo{ char id[16]; //identification string eg "TT Builtin" unsigned long smem_start; //Start of frame buffer mem //(physical address) __u32 smem_len; //Length of frame buffer mem __u32 type; //see FB_TYPE_ __u32 type_aux; //Interleave for interleaved Planes __u16 xpanstep; //zero if no hardware panning __u16 ypanstep; //zero if no hardware panning __u16 ywrapstep; //zero if no hardware ywrap __u32 line_length; //length of a line in bytes unsigned long mmio_start; //Start of Memoryy Mapped I/O //(physical address) __u32 mmio_len; //Length of Memory Mapped I/O __u32 accell //Indicate to driver which //specific chip/card we have __u16 capabilities; //see FB_CAP_ __u16 reserved[2]; //Reserved for future compatibility }<<end fb_fix_screeninfo>>;
可以使用ioctl FBIOGET_FSCREENINFO来读出这些信息,但是比较少用到
映射Framebuffer
要映射一块内存,需要知道它的地址–这由驱动程序来设置,需要知道它的大小–这由应用程序决定。代码如下:
line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.bits_per_pixel / 8;
fb_base = (unsigned char *)mmp(NULL , screen_size, PORT_READ | PORT_WRITE,MAP_SHARED,fb_fb, 0); //screen_size是整个Framebuffer的大小;PORT_READ | PRT_WRITE表示该区域可读、可写;MAP_SHARED表示该区域是共享的,APP写入数据时,会直达驱动程序,这个参数的更深刻理解可以参考后面驱动基础中讲到mmap知识
if (fb_fb_base == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
描点函数
能够在LCD上描绘指定像素后,就可以写字、画图。描点函数是基础,代码如下:
void lcd_put_pixel(int x, int y, unsigned int color) //此处color表示颜色,格式永远是0x00RRGGBB,即RGB888。当LCD是16bpp时,要把color变量中的R、G、B吃藕上来再合并成RGB565格式
{
unsigned char *pen_8 = fb_base + y * line_width + x * pixel_width; //此处计算(x,y)坐标上对应的Framebuffer地址
unsigned short *pen_16;
unsigned int *pen_32;
unsigned int red, green, blue;
pen_16 = (unsigned short *)pen_8;
pen_32 = (unsigned int *)pen_8;
switch (var.bits_per_pixel)
{
case 8:
{
*pen_8 = color; //对于8bpp,color就不再表示RGB三原色,这涉及到调色板的概念,color是调色板的值
break;
}
case 16:
{
//565
red = (color >> 16 ) & 0xff; //先从color变量中把R、G、B抽出来
green = (color >> 8) & 0xff;
blue = (color >> 0) & 0xff;
color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3); //把red、green、blue这三种8位颜色值,根据RGB565的格式,只保留留red中的高5位,green中的高6位,blue中的高5位,组合成一个新的16位颜色值
*pen_16 = color; //把新的16位颜色值写入Framebuffer
break;
}
case 32:
{
*pen_32 = color; //对于32bpp,颜色格式跟color参数一致,可以直接写入Framebuffer
break;
}
default:
{
printf("can't support %d bpp\n",var.bits_per_pixel);
break;
}
}
}
随便画的几个点
本程序的main函数,在最后只是简单画了几个点:
//清屏:全部设为白色
memset(fbmem, 0xff, screen_size);
//随便设置出100个为红色
for (i = 0; i < 100; i++)
lcd_put_pixel (var.xres/2+, var.yres/2, 0xFF0000);
上机实验
在Ubuntu中编译程序,先设置交叉编译工具链,再执行下面的命令
arm-buildroot-linux-gnueabihf-gcc -o show_pixel show_pixel.c
然后在开发板上执行show_pixel程序
注:板子的出厂程序中一般都有GUI,所以可能要把GUI程序禁止掉,具体放啊看文档,之后会补充禁止GUI的方法。可以先不禁止GUI,直接执行show——pixel看看LCD有无现象
文字显示
字符的编码方式
编码与字体
在一个txt文件中保存字符的核心是字符的编码值,字符的显示由字体决定,和编码值是不同的概念
ASCII
American Standard Code for Information Interchange
,美国信息交换标准代码
字符和数值的对应关系可以在相关页面查询
ANSI
ANSI在不同国家,不同地区表示的意思是不一样的。个人认为可以理解成各个国家的本地编码,本地区的软件生成的文件用本地区的ANSI编码,传送到其他地区后因为ANSI编码规则不同会乱码
ANSI编码只存在于Windows中,ANSI中对于非ASCII编码一般用2个字节表示
UNICODE
如上文所言,ANSI编码保存的文件不利于开地区交流
UNICODE编码存在的意义就是解决跨地区交流的问题,对地球上任意的字符都给一个唯一的数值来表示
UNICODE仍然向下兼容ASCII,对于其他的字符有对应的数值
UNICODE数值的范围是0x0000
至0x10FFFF
,可以表现100多万个字符
UNICODE编码的实现
tips:个人决定UNICODE编码的解释在小白变怪兽C语言的书里面介绍的很好,有机会会在这里补充
ASCII字符的点阵表示
在LCD中显示ASCII字符(即英文字母这些字符),首先需要找到字符对应的点阵。在Linux内核源码中有lib\fonts\font_8x16.c
,在里面以数组的形式保存各个字符的点阵
数组里的数字通过以下的方式来表示点阵
一共16行,每行8列,显示的位用1表示,不显示的位用0表示
要显示某个字符,就根据ASCII码在font_8x16
数组中找到它对应的点阵,再取出这16个字节去描画16行像素
比如字符A的ASCII是0x41
,从fontdata_8x16[0x41*16]
开始取它的点阵数据
源码目录
01_all_series_quickstart\04_嵌入式Linux应用开发基础知识\source\08_show_ascii\show_ascii.c
核心函数
/**********************************************************************
* 函数名称: lcd_put_ascii
* 功能描述: 在LCD指定位置上显示一个8*16的字符
* 输入参数: x坐标,y坐标,ascii码
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/05/12 V1.0 zh(angenao) 创建
***********************************************************************/
void lcd_put_ascii(int x, int y, unsigned char c)
{
unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];
int i, b;
unsigned char byte;
for (i = 0; i < 16; i++)
{
byte = dots[i];
for (b = 7; b >= 0; b--)
{
if (byte & (1<<b))
{
/* show */
lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
}
else
{
/* hide */
lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
}
}
}
}
获取点阵
对于字符c,char c,点阵的获取方法如下
4693 unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];
描点
因为有16行,所以要有个循环16次的大循环;每行里面有8位,所以每个大循环里面还需要一个循环8次的小循环
小循环里面判断单行的描点情况,是1就填充白色,是0就填充黑色,这样就可以显示出黑底白色轮廓的英文字母
for (i = 0; i < 16; i++)
{
byte = dots[i];
for (b = 7; b >= 0; b--)
{
if (byte & (1<<b))
{
/* show */
lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
}
else
{
/* hide */
lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
}
}
}
main函数
main函数中首要先打开LCD设备,获取Framebuffer参数,实现lcd_put_pixel函数;再调用lcd_put_ascii即可绘制字符
代码如下:
int main(int argc, char **argv)
{
fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
printf("can't open /dev/fb0\n");
return -1;
}
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
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;
fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fbmem == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
/* 清屏: 全部设为黑色 */
memset(fbmem, 0, screen_size);
lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/
munmap(fbmem , screen_size);
close(fd_fb);
return 0;
}
编译c文件show_ascii.c
编译命令:
arm-buildroot-linux-gnueabihf-gcc -o show_ascii show_ascii.c
上机实验
把编译出来的可执行文件show_ascii
放到开发板上,执行命令./show_ascii
如果实验成功,屏幕中间会出现一个白色的A
修改部分
修改lcd_put_ascii
函数,可以指定字符颜色
实现lcd_put_str
函数,输出字符串,可以换行
在show_ascii.c
的基础上实现汉字的显示:要找到汉字的字库、了解像素排列顺序、得到汉字编码