STM32MP157_第4篇嵌入式Linux应用基础知识_韦东山老师

内容来自韦东山老师《嵌入式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目录

指定路径
  1. #include "目录/xxx.h" //头文件中这样定义
  2. 编译时用-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链接linking4步才能变成可执行文件

GCC的编译过程

预处理

​ 文件格式:.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所在目录
运行:
  1. 先把libusb.so放到PC或板子上的/lib目录,然后就可以运行test程序

  2. 如果不想把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链接linking4步才能变成可执行文件

​ 日常一般用编译来统称这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文件库文件链接起来,最终生成可以在特定平台运行的可执行文件,用到的工具是ldcollect2

例子

​ 编译程序的时候加上-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常用选项

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规则与示例

  1. 为什么需要Makefile

    可以更高效地编译程序

  2. Makefile其实挺简单的

    ​ 一个简单的Makefile文件包含一系列的“规则”,样式如下:

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

    ​ 如果“依赖文件”比“目标文件”更新,就可以执行“命令“以重新生成”目标文件“

    ​ 命令执行的2个条件:1.依赖文件比目标文件新;2.目标文件尚未生成

  3. 介绍Makefile的2个函数

    1. $(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
    2. $(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,效率高,精炼,支持检测头文件(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的使用

    功能如下:

    1. 支持多个目录、多层目录、多个文件
    2. 支持给所有的文件设置编译选项
    3. 支持给某个目录设置编译选项
    4. 支持给某个文件单独设置编译选项
    5. 简单好用

    下载链接:

通用Makefile的解析

  1. 零星知识点:
    1. make命令的使用

      ​ 执行make命令时,它会去当前目录下查找名为Makefile的文件,并根据它的值去执行操作,生成第一个目标

      ​ 我们可以用-f选项指定文件,不再使用名为Makefile的文件,如:

      make -f Makefile.build

      ​ 我们也可以使用-C选项指定目录,切换到其他目录去

      make -C a/ -f Makefile.build

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

      make -C a/ -f Makefile.build other_target
    2. 即时变量、延时变量

      变量的定义语法形式如下:

      A = xxx //延时变量
      B ?= xxx //延时变量,只有第一次定义时赋值才成功;如果之前定义过,则此赋值无效
      C := xxx //立即变量
      D += yyy //如果D在前面是延时变量,那么它现在还是延时变量;如果D在前面是立即变量,那么现在它还是立即变量(感觉绕起来好好笑哈哈哈)

      ​ 在GNU make中对变量的赋值有两种方式:延时变量、立即变量

      ​ 由上可知,变量A是延时变量,它的值在使用时才展开、才确定,比如:

      A = $@
      test:
          @echo $A

      ​ 上述Makefile中,变量A的值在执行时才确定,等于test,是延时变量

      ​ 如果使用A := $@这是立即变量,这时¥@为空,所以A的值就是空

    3. 变量的导出(export)

      在编译程序时,我们不断地使用make -C dir切换到其他目录,执行其他目录里的Makefile,如果想让某个变量的值在所有的目录中都可见,可以把它export出来

      ​ 比如CC [ $(CROSS_COMPILE)gcc],这个CC变量表示编译器,在整个过程中都是一样的。定义它之后,要使用export CC把它导出来

    4. Makefile中可以使用的shell命令

      比如:

      TOPDIR := $(shell pwd)

      ​ 这是一个立即变量,TOPDIR等于shell命令pwd结果

    5. 在Makefile中怎么放置第1个目标

      ​ 执行make命令时如果不指定目标,那么它默认是去生成第1个目标

      ​ 所以第1个目标位置很重要。有时候不太方便把第1个目标完整地放在文件前面,可以在文件的前面直接放置目标,在后面再完善它的依赖和命令

      ​ 比如:

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

      ​ 我们的Makefile中有这样的目标:

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

      ​ 如果当前目录下恰好有名为clean的文件,那么执行make clean时它就不会执行那些删除命令

      ​ 如果我们需要把clean这个目标设置为假想目标,这样可以确保执行make clean删除的命令肯定能够得到执行

      ​ 使用下面的语句把clean设置为假想目标:

      .PHONY : clean
    7. 常用的函数
      1. $(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
      2. $(wildcard pattern)

        pattern所列出的文件是否存在,把存在的文件都列出来

        src_files := $(wildcard *.c) #最终src_files中列出了当前目录下所有的.c文件
      3. $(filter pattern…,text)

        ​ 把text中符合pattern格式的内容,filter(过滤)出来、留下来

        obj-y := a.o b.o c/ d/
        DIR := $(filter %/, $(obj-y)) //结果为:c/ d/
      4. $(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
      5. $(patsubst pattern,replacement,text)

        ​ 寻找text中符合格式pattern的字,用replacement替换它们。patternreplacement中可以使用通配符

        subdir-y := c/ d/
        subdir-y := $(patsubst %/, %, $(subdir-y)) //结果为:c d
  1. 通用Makefile的设计思想
    1. 在Makefile文件中确定要编译的文件、目录,比如:
      obj-y += main.o
      
      obj-y += a/

      Makefile文件总是被Makefile.build包含的

    2. Makefile.build中设置编译规则,有3条编译规则
      1. 怎么编译子目录?进入子目录编译:

        $(subdir-y):
            make -C $@ -f $(TOPDIR)/Makefile.build
      2. 怎么编译当前目录中的文件?

        %.o : %.c
            $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
      3. 当前目录下的.o和子目录下的built-in.o要打包起来:

        built-in.o : $(cur_objs) $(subdir_objs)
            $(LD) -r -o $@ $^
    3. 顶层Makefile中把顶层目录的built-in.o链接成APP:
      $(TARGET) : built-in.o
          $(CC) $(LDFLAGS) -o $(TARGET) built-in.o
  2. 情景演绎
    #顶层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来操作的

文件从哪里来?

  1. 磁盘、Flash、SD卡、U盘

    真是的文件,以某种格式(FAT32,exFAT等)保存在某个存储设备上(/dev/xxx),要先mount

  2. Linux内核提供的虚拟文件系统(也要先mount)

  3. 特殊文件:/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大分类,如下:

  1. Executable programs or shell commands //命令

  2. System calls (functions provided by the kernel) //系统调用,比如man 2 open

  3. Library calls (functions within program libraries) //函数库调用

  4. Special files (ususally found in /dev) //特殊文件,比如man 4 tty

  5. File formats and conventions eg /etc/passwd //文件格式和约定,比如man 5 passwd

  6. Games //游戏

  7. Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7) //杂项

  8. System administration commands (usually only for root) //系统管理命令

  9. 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的操作原理

  1. 驱动程序设置好LCD控制器:

    根据LCD参数设置LCD控制器的时序、信号极性

    根据LCD分辨率BPP分配Framebuffer

  2. APP使用ioctl获得LCD分辨率BPP

  3. APP通过mmap映射Framebuffer,在Framebuffer中写入数据

LCD工作原理

​ 假设需要设置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

RGB位示意图

​ 对于32BPP,一般只设置低24位表示颜色,高8位表示透明度,一般的LCD都不支持

​ 对于24BPP,硬件上为了方便处理,在Framebuffer中也是用32位来表示,效果和32BPP一样

​ 对于16位BPP,常用的是RGB565;很少的场合会用到RGB555,这可以通过ioctl读取驱动程序中的RGB位偏移来确定使用哪一种格式

涉及的API函数

​ 本程序的目的是:打开LCD设备节点,获取分辨率等参数,映射Framebuffer,最后实现描点函数

open函数

​ 在Ubuntu中执行man 2 open,可以看到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);

​ 函数说明:

  1. pathname表示打开文件的路径
  2. flags表示打开文件的方式,常用的有以下6种:
    1. O_RDWR 表示可读可写方式打开
    2. O_RDONLY 表示只读方式打开
    3. O_WRONLY 表示只写方式打开
    4. O_APPEND 表示如果这个文件中本身是有内容的,则新写入的内容会接续到原来的内容后面
    5. O_TRUNC 表示如果这个文件中本来是有内容的,则原来的内容会被丢弃,截断
    6. O_CREAT 表示当前打开文件不存在,我们创建它并打开它,通常与O_EXCL结合使用,当没有文件的时候创建文件,有这个文件的时候会报错提醒我们
  3. Mode表示创建文件的权限,只有在flags中使用了O_CREAT时才有效,否则忽略
  4. 返回值:打开成功返回文件描述符,失败将返回-1

ioctl函数

​ 在Ubuntu中执行man ioctl,可以看到ioctl函数的说明ioctl函数的说明

​ 头文件:

#include <sys/ioctl.h>

​ 函数原型:

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

​ 函数说明:

  1. fd表示文件描述符

  2. request表示与驱动程序交互的命令,用不同的命令控制驱动程序输出我们需要的数据

  3. …表示可变参数arg,根据request命令,设备驱动程序返回输出的数据

  4. 返回值:打开成功返回文件描述符,失败将返回-1

    ioctl的作用非常强大、灵魂。不同的驱动程序内部会实现不同的ioctl,APP可以使用各种ioctl跟驱动程序交互:可以传数据给驱动程序,也可以从驱动程序中读出数据

mmap函数

​ 在Ubuntu中执行man mmap,可以看到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);

​ 函数说明:

  1. addr表示指定映射的内存起始地址,通常设为NULL表示让系统自动选定地址,并在成功映射后返回该地址;
  2. length表示将文件中多大的内容映射到内存中;
  3. prot表示映射区域的保护方式,可以分为以下4种方式的组合
    1. PROT_EXEC映射区域可被执行
    2. PROT_READ映射区域可被读出
    3. PROT_WRITE映射区域可被写入
    4. PROT_NONE映射区域不能被存取
  4. Flags表示影响映射区域的不同特性,常见的有以下两种
    1. MAP_SHARED表示对映射区域写入的数据会复制回文件内,原来的文件会改变
    2. MAP_PRIVATE表示对映射区域的操作会产生一个映射文件的复制,对此区域的任何修改都不会写回原来的文件内容中
  5. 返回值:若成功映射,将返回指向映射的区域的指针,失败将返回-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,美国信息交换标准代码

​ 字符和数值的对应关系可以在相关页面查询

ASCII码 – 百度百科

ASCII码 – 维基百科

ANSI

ANSI介绍 – 博客园

​ ANSI在不同国家,不同地区表示的意思是不一样的。个人认为可以理解成各个国家的本地编码,本地区的软件生成的文件用本地区的ANSI编码,传送到其他地区后因为ANSI编码规则不同会乱码

​ ANSI编码只存在于Windows中,ANSI中对于非ASCII编码一般用2个字节表示

UNICODE

​ 如上文所言,ANSI编码保存的文件不利于开地区交流

​ UNICODE编码存在的意义就是解决跨地区交流的问题,对地球上任意的字符都给一个唯一的数值来表示

​ UNICODE仍然向下兼容ASCII,对于其他的字符有对应的数值

​ UNICODE数值的范围是0x00000x10FFFF,可以表现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的基础上实现汉字的显示:要找到汉字的字库、了解像素排列顺序、得到汉字编码

//未完,最后更新时间:2021年5月12日 19:59


   转载规则


《STM32MP157_第4篇嵌入式Linux应用基础知识_韦东山老师》 InImpasse 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Chromebook折腾小记 Chromebook折腾小记
欢迎关注我的微信公众号【万能的小江江】
2021-04-19
下一篇 
阿里网盘Win Mac电脑客户端下载(含最新扩容福利码) 阿里网盘Win Mac电脑客户端下载(含最新扩容福利码)
欢迎关注我的微信公众号【万能的小江江】 阿里云盘最新可用扩容码2021 年 4 月 13 日更新:春暖花开、鸟语花香、面朝大海、春意盎然 (每个 200 GB) 阿里云盘各版本下载链接电脑版Windows​ 蓝奏云 ​ 百度
  目录